Skip to content

Commit 67406e5

Browse files
authored
fix: OpenAI Chat Completions tool calls (#1153)
* fix tool calls * remove use of tracer * linting
1 parent 0511759 commit 67406e5

2 files changed

Lines changed: 62 additions & 29 deletions

File tree

agentops/instrumentation/providers/openai/stream_wrapper.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from agentops.logging import logger
1616
from agentops.instrumentation.common.wrappers import _with_tracer_wrapper
1717
from agentops.instrumentation.providers.openai.utils import is_metrics_enabled
18-
from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes
18+
from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes, _create_tool_span
1919
from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes
2020

2121

@@ -192,30 +192,11 @@ def _finalize_stream(self) -> None:
192192
if self._finish_reason:
193193
self._span.set_attribute(MessageAttributes.COMPLETION_FINISH_REASON.format(i=0), self._finish_reason)
194194

195-
# Set tool calls
195+
# Create tool spans for each tool call
196196
if len(self._tool_calls) > 0:
197197
for idx, tool_call in self._tool_calls.items():
198-
# Only set attributes if values are not None
199-
if tool_call["id"] is not None:
200-
self._span.set_attribute(
201-
MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=0, j=idx), tool_call["id"]
202-
)
203-
204-
if tool_call["type"] is not None:
205-
self._span.set_attribute(
206-
MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=0, j=idx), tool_call["type"]
207-
)
208-
209-
if tool_call["function"]["name"] is not None:
210-
self._span.set_attribute(
211-
MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=0, j=idx), tool_call["function"]["name"]
212-
)
213-
214-
if tool_call["function"]["arguments"] is not None:
215-
self._span.set_attribute(
216-
MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(i=0, j=idx),
217-
tool_call["function"]["arguments"],
218-
)
198+
# Create a child span for this tool call
199+
_create_tool_span(self._span, tool_call)
219200

220201
# Set usage if available from the API
221202
if self._usage is not None:
@@ -374,7 +355,7 @@ def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs):
374355
return OpenaiStreamWrapper(response, span, kwargs)
375356
else:
376357
# Handle non-streaming response
377-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
358+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span)
378359

379360
for key, value in response_attributes.items():
380361
if key not in request_attributes: # Avoid overwriting request attributes
@@ -439,7 +420,7 @@ async def async_chat_completion_stream_wrapper(tracer, wrapped, instance, args,
439420
return OpenAIAsyncStreamWrapper(response, span, kwargs)
440421
else:
441422
# Handle non-streaming response
442-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
423+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span)
443424

444425
for key, value in response_attributes.items():
445426
if key not in request_attributes: # Avoid overwriting request attributes

agentops/instrumentation/providers/openai/wrappers/chat.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,72 @@
88
import logging
99
from typing import Any, Dict, Optional, Tuple
1010

11+
from opentelemetry.trace import Span
12+
1113
from agentops.instrumentation.providers.openai.utils import is_openai_v1
1214
from agentops.instrumentation.providers.openai.wrappers.shared import (
1315
model_as_dict,
1416
should_send_prompts,
1517
)
1618
from agentops.instrumentation.common.attributes import AttributeMap
1719
from agentops.semconv import SpanAttributes, LLMRequestTypeValues
20+
from agentops.semconv.tool import ToolAttributes
21+
from agentops.semconv.span_kinds import AgentOpsSpanKindValues
22+
23+
from opentelemetry import context as context_api
24+
from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer
1825

1926
logger = logging.getLogger(__name__)
2027

2128
LLM_REQUEST_TYPE = LLMRequestTypeValues.CHAT
2229

2330

31+
def _create_tool_span(parent_span, tool_call_data):
32+
"""
33+
Create a distinct span for each tool call.
34+
35+
Args:
36+
parent_span: The parent LLM span
37+
tool_call_data: The tool call data dictionary
38+
"""
39+
# Get the tracer for this module
40+
tracer = get_tracer(__name__)
41+
42+
# Create a child span for the tool call
43+
with tracer.start_as_current_span(
44+
name=f"tool_call.{tool_call_data['function']['name']}",
45+
kind=SpanKind.INTERNAL,
46+
context=context_api.set_value("current_span", parent_span),
47+
) as tool_span:
48+
# Set the span kind to TOOL
49+
tool_span.set_attribute("agentops.span.kind", AgentOpsSpanKindValues.TOOL)
50+
51+
# Set tool-specific attributes
52+
tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data["function"]["name"])
53+
tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data["function"]["arguments"])
54+
tool_span.set_attribute("tool.call.id", tool_call_data["id"])
55+
tool_span.set_attribute("tool.call.type", tool_call_data["type"])
56+
57+
# Set status to OK for successful tool call creation
58+
tool_span.set_status(Status(StatusCode.OK))
59+
60+
2461
def handle_chat_attributes(
2562
args: Optional[Tuple] = None,
2663
kwargs: Optional[Dict] = None,
2764
return_value: Optional[Any] = None,
65+
span: Optional[Span] = None,
2866
) -> AttributeMap:
2967
"""Extract attributes from chat completion calls.
3068
3169
This function is designed to work with the common wrapper pattern,
3270
extracting attributes from the method arguments and return value.
71+
72+
Args:
73+
args: Method arguments (not used in this implementation)
74+
kwargs: Method keyword arguments
75+
return_value: Method return value
76+
span: The parent span for creating tool spans
3377
"""
3478
attributes = {
3579
SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value,
@@ -191,12 +235,20 @@ def handle_chat_attributes(
191235
# Tool calls
192236
if "tool_calls" in message:
193237
tool_calls = message["tool_calls"]
194-
if tool_calls: # Check if tool_calls is not None
238+
if tool_calls and span is not None:
195239
for i, tool_call in enumerate(tool_calls):
240+
# Convert tool_call to the format expected by _create_tool_span
196241
function = tool_call.get("function", {})
197-
attributes[f"{prefix}.tool_calls.{i}.id"] = tool_call.get("id")
198-
attributes[f"{prefix}.tool_calls.{i}.name"] = function.get("name")
199-
attributes[f"{prefix}.tool_calls.{i}.arguments"] = function.get("arguments")
242+
tool_call_data = {
243+
"id": tool_call.get("id", ""),
244+
"type": tool_call.get("type", "function"),
245+
"function": {
246+
"name": function.get("name", ""),
247+
"arguments": function.get("arguments", ""),
248+
},
249+
}
250+
# Create a child span for this tool call
251+
_create_tool_span(span, tool_call_data)
200252

201253
# Prompt filter results
202254
if "prompt_filter_results" in response_dict:

0 commit comments

Comments
 (0)