Skip to content

Commit 7b8faae

Browse files
committed
fix tool calls
1 parent 4e2d116 commit 7b8faae

File tree

2 files changed

+72
-35
lines changed

2 files changed

+72
-35
lines changed

agentops/instrumentation/providers/openai/stream_wrapper.py

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from typing import Any, AsyncIterator, Iterator
1010

1111
from opentelemetry import context as context_api
12-
from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context
12+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context, get_tracer
1313
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
1414

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

@@ -30,17 +30,19 @@ class OpenaiStreamWrapper:
3030
- Chunk statistics
3131
"""
3232

33-
def __init__(self, stream: Any, span: Span, request_kwargs: dict):
33+
def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None):
3434
"""Initialize the stream wrapper.
3535
3636
Args:
3737
stream: The original OpenAI stream object
3838
span: The OpenTelemetry span for tracking
3939
request_kwargs: Original request parameters for context
40+
tracer: The OpenTelemetry tracer for creating child spans
4041
"""
4142
self._stream = stream
4243
self._span = span
4344
self._request_kwargs = request_kwargs
45+
self._tracer = tracer
4446
self._start_time = time.time()
4547
self._first_token_time = None
4648
self._chunk_count = 0
@@ -192,30 +194,11 @@ def _finalize_stream(self) -> None:
192194
if self._finish_reason:
193195
self._span.set_attribute(MessageAttributes.COMPLETION_FINISH_REASON.format(i=0), self._finish_reason)
194196

195-
# Set tool calls
196-
if len(self._tool_calls) > 0:
197+
# Create tool spans for each tool call
198+
if len(self._tool_calls) > 0 and self._tracer is not None:
197199
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-
)
200+
# Create a child span for this tool call
201+
_create_tool_span(self._span, tool_call, self._tracer)
219202

220203
# Set usage if available from the API
221204
if self._usage is not None:
@@ -254,17 +237,19 @@ def _finalize_stream(self) -> None:
254237
class OpenAIAsyncStreamWrapper:
255238
"""Async wrapper for OpenAI Chat Completions streaming responses."""
256239

257-
def __init__(self, stream: Any, span: Span, request_kwargs: dict):
240+
def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None):
258241
"""Initialize the async stream wrapper.
259242
260243
Args:
261244
stream: The original OpenAI async stream object
262245
span: The OpenTelemetry span for tracking
263246
request_kwargs: Original request parameters for context
247+
tracer: The OpenTelemetry tracer for creating child spans
264248
"""
265249
self._stream = stream
266250
self._span = span
267251
self._request_kwargs = request_kwargs
252+
self._tracer = tracer
268253
self._start_time = time.time()
269254
self._first_token_time = None
270255
self._chunk_count = 0
@@ -371,10 +356,10 @@ def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs):
371356
if is_streaming:
372357
# Wrap the stream
373358
context_api.detach(token)
374-
return OpenaiStreamWrapper(response, span, kwargs)
359+
return OpenaiStreamWrapper(response, span, kwargs, tracer)
375360
else:
376361
# Handle non-streaming response
377-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
362+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer)
378363

379364
for key, value in response_attributes.items():
380365
if key not in request_attributes: # Avoid overwriting request attributes
@@ -436,10 +421,10 @@ async def async_chat_completion_stream_wrapper(tracer, wrapped, instance, args,
436421
if is_streaming:
437422
# Wrap the stream
438423
context_api.detach(token)
439-
return OpenAIAsyncStreamWrapper(response, span, kwargs)
424+
return OpenAIAsyncStreamWrapper(response, span, kwargs, tracer)
440425
else:
441426
# Handle non-streaming response
442-
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response)
427+
response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer)
443428

444429
for key, value in response_attributes.items():
445430
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
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, tracer):
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+
tracer: The OpenTelemetry tracer instance
39+
"""
40+
# Create a child span for the tool call
41+
with tracer.start_as_current_span(
42+
name=f"tool_call.{tool_call_data['function']['name']}",
43+
kind=SpanKind.INTERNAL,
44+
context=context_api.set_value("current_span", parent_span)
45+
) as tool_span:
46+
# Set the span kind to TOOL
47+
tool_span.set_attribute("agentops.span.kind", AgentOpsSpanKindValues.TOOL)
48+
49+
# Set tool-specific attributes
50+
tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data['function']['name'])
51+
tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data['function']['arguments'])
52+
tool_span.set_attribute("tool.call.id", tool_call_data['id'])
53+
tool_span.set_attribute("tool.call.type", tool_call_data['type'])
54+
55+
# Set status to OK for successful tool call creation
56+
tool_span.set_status(Status(StatusCode.OK))
57+
58+
2459
def handle_chat_attributes(
2560
args: Optional[Tuple] = None,
2661
kwargs: Optional[Dict] = None,
2762
return_value: Optional[Any] = None,
63+
span: Optional[Span] = None,
64+
tracer: Optional[Any] = None,
2865
) -> AttributeMap:
2966
"""Extract attributes from chat completion calls.
3067
3168
This function is designed to work with the common wrapper pattern,
3269
extracting attributes from the method arguments and return value.
70+
71+
Args:
72+
args: Method arguments (not used in this implementation)
73+
kwargs: Method keyword arguments
74+
return_value: Method return value
75+
span: The parent span for creating tool spans
76+
tracer: The OpenTelemetry tracer for creating child 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 and tracer 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, tracer)
200252

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

0 commit comments

Comments
 (0)