From 7b8faae4b18fc1d770f7a3d354cef58b5d2a827a Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Sat, 12 Jul 2025 05:26:34 +0530 Subject: [PATCH 1/3] fix tool calls --- .../providers/openai/stream_wrapper.py | 47 +++++---------- .../providers/openai/wrappers/chat.py | 60 +++++++++++++++++-- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/agentops/instrumentation/providers/openai/stream_wrapper.py b/agentops/instrumentation/providers/openai/stream_wrapper.py index 15e541d82..cbcabdec1 100644 --- a/agentops/instrumentation/providers/openai/stream_wrapper.py +++ b/agentops/instrumentation/providers/openai/stream_wrapper.py @@ -9,13 +9,13 @@ from typing import Any, AsyncIterator, Iterator from opentelemetry import context as context_api -from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context +from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context, get_tracer from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY from agentops.logging import logger from agentops.instrumentation.common.wrappers import _with_tracer_wrapper from agentops.instrumentation.providers.openai.utils import is_metrics_enabled -from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes +from agentops.instrumentation.providers.openai.wrappers.chat import handle_chat_attributes, _create_tool_span from agentops.semconv import SpanAttributes, LLMRequestTypeValues, MessageAttributes @@ -30,17 +30,19 @@ class OpenaiStreamWrapper: - Chunk statistics """ - def __init__(self, stream: Any, span: Span, request_kwargs: dict): + def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None): """Initialize the stream wrapper. Args: stream: The original OpenAI stream object span: The OpenTelemetry span for tracking request_kwargs: Original request parameters for context + tracer: The OpenTelemetry tracer for creating child spans """ self._stream = stream self._span = span self._request_kwargs = request_kwargs + self._tracer = tracer self._start_time = time.time() self._first_token_time = None self._chunk_count = 0 @@ -192,30 +194,11 @@ def _finalize_stream(self) -> None: if self._finish_reason: self._span.set_attribute(MessageAttributes.COMPLETION_FINISH_REASON.format(i=0), self._finish_reason) - # Set tool calls - if len(self._tool_calls) > 0: + # Create tool spans for each tool call + if len(self._tool_calls) > 0 and self._tracer is not None: for idx, tool_call in self._tool_calls.items(): - # Only set attributes if values are not None - if tool_call["id"] is not None: - self._span.set_attribute( - MessageAttributes.COMPLETION_TOOL_CALL_ID.format(i=0, j=idx), tool_call["id"] - ) - - if tool_call["type"] is not None: - self._span.set_attribute( - MessageAttributes.COMPLETION_TOOL_CALL_TYPE.format(i=0, j=idx), tool_call["type"] - ) - - if tool_call["function"]["name"] is not None: - self._span.set_attribute( - MessageAttributes.COMPLETION_TOOL_CALL_NAME.format(i=0, j=idx), tool_call["function"]["name"] - ) - - if tool_call["function"]["arguments"] is not None: - self._span.set_attribute( - MessageAttributes.COMPLETION_TOOL_CALL_ARGUMENTS.format(i=0, j=idx), - tool_call["function"]["arguments"], - ) + # Create a child span for this tool call + _create_tool_span(self._span, tool_call, self._tracer) # Set usage if available from the API if self._usage is not None: @@ -254,17 +237,19 @@ def _finalize_stream(self) -> None: class OpenAIAsyncStreamWrapper: """Async wrapper for OpenAI Chat Completions streaming responses.""" - def __init__(self, stream: Any, span: Span, request_kwargs: dict): + def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None): """Initialize the async stream wrapper. Args: stream: The original OpenAI async stream object span: The OpenTelemetry span for tracking request_kwargs: Original request parameters for context + tracer: The OpenTelemetry tracer for creating child spans """ self._stream = stream self._span = span self._request_kwargs = request_kwargs + self._tracer = tracer self._start_time = time.time() self._first_token_time = None self._chunk_count = 0 @@ -371,10 +356,10 @@ def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs): if is_streaming: # Wrap the stream context_api.detach(token) - return OpenaiStreamWrapper(response, span, kwargs) + return OpenaiStreamWrapper(response, span, kwargs, tracer) else: # Handle non-streaming response - response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response) + response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer) for key, value in response_attributes.items(): 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, if is_streaming: # Wrap the stream context_api.detach(token) - return OpenAIAsyncStreamWrapper(response, span, kwargs) + return OpenAIAsyncStreamWrapper(response, span, kwargs, tracer) else: # Handle non-streaming response - response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response) + response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer) for key, value in response_attributes.items(): if key not in request_attributes: # Avoid overwriting request attributes diff --git a/agentops/instrumentation/providers/openai/wrappers/chat.py b/agentops/instrumentation/providers/openai/wrappers/chat.py index 5a40501ce..ad2d17e74 100644 --- a/agentops/instrumentation/providers/openai/wrappers/chat.py +++ b/agentops/instrumentation/providers/openai/wrappers/chat.py @@ -8,6 +8,8 @@ import logging from typing import Any, Dict, Optional, Tuple +from opentelemetry.trace import Span + from agentops.instrumentation.providers.openai.utils import is_openai_v1 from agentops.instrumentation.providers.openai.wrappers.shared import ( model_as_dict, @@ -15,21 +17,63 @@ ) from agentops.instrumentation.common.attributes import AttributeMap from agentops.semconv import SpanAttributes, LLMRequestTypeValues +from agentops.semconv.tool import ToolAttributes +from agentops.semconv.span_kinds import AgentOpsSpanKindValues + +from opentelemetry import context as context_api +from opentelemetry.trace import SpanKind, Status, StatusCode logger = logging.getLogger(__name__) LLM_REQUEST_TYPE = LLMRequestTypeValues.CHAT +def _create_tool_span(parent_span, tool_call_data, tracer): + """ + Create a distinct span for each tool call. + + Args: + parent_span: The parent LLM span + tool_call_data: The tool call data dictionary + tracer: The OpenTelemetry tracer instance + """ + # Create a child span for the tool call + with tracer.start_as_current_span( + name=f"tool_call.{tool_call_data['function']['name']}", + kind=SpanKind.INTERNAL, + context=context_api.set_value("current_span", parent_span) + ) as tool_span: + # Set the span kind to TOOL + tool_span.set_attribute("agentops.span.kind", AgentOpsSpanKindValues.TOOL) + + # Set tool-specific attributes + tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data['function']['name']) + tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data['function']['arguments']) + tool_span.set_attribute("tool.call.id", tool_call_data['id']) + tool_span.set_attribute("tool.call.type", tool_call_data['type']) + + # Set status to OK for successful tool call creation + tool_span.set_status(Status(StatusCode.OK)) + + def handle_chat_attributes( args: Optional[Tuple] = None, kwargs: Optional[Dict] = None, return_value: Optional[Any] = None, + span: Optional[Span] = None, + tracer: Optional[Any] = None, ) -> AttributeMap: """Extract attributes from chat completion calls. This function is designed to work with the common wrapper pattern, extracting attributes from the method arguments and return value. + + Args: + args: Method arguments (not used in this implementation) + kwargs: Method keyword arguments + return_value: Method return value + span: The parent span for creating tool spans + tracer: The OpenTelemetry tracer for creating child spans """ attributes = { SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value, @@ -191,12 +235,20 @@ def handle_chat_attributes( # Tool calls if "tool_calls" in message: tool_calls = message["tool_calls"] - if tool_calls: # Check if tool_calls is not None + if tool_calls and span is not None and tracer is not None: for i, tool_call in enumerate(tool_calls): + # Convert tool_call to the format expected by _create_tool_span function = tool_call.get("function", {}) - attributes[f"{prefix}.tool_calls.{i}.id"] = tool_call.get("id") - attributes[f"{prefix}.tool_calls.{i}.name"] = function.get("name") - attributes[f"{prefix}.tool_calls.{i}.arguments"] = function.get("arguments") + tool_call_data = { + "id": tool_call.get("id", ""), + "type": tool_call.get("type", "function"), + "function": { + "name": function.get("name", ""), + "arguments": function.get("arguments", "") + } + } + # Create a child span for this tool call + _create_tool_span(span, tool_call_data, tracer) # Prompt filter results if "prompt_filter_results" in response_dict: From 631e855e11da2bdaef8d169d647122986036e9e6 Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Sun, 13 Jul 2025 11:26:15 +0530 Subject: [PATCH 2/3] remove use of tracer --- .../providers/openai/stream_wrapper.py | 20 ++++++++----------- .../providers/openai/wrappers/chat.py | 14 ++++++------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/agentops/instrumentation/providers/openai/stream_wrapper.py b/agentops/instrumentation/providers/openai/stream_wrapper.py index cbcabdec1..181a85bbe 100644 --- a/agentops/instrumentation/providers/openai/stream_wrapper.py +++ b/agentops/instrumentation/providers/openai/stream_wrapper.py @@ -30,19 +30,17 @@ class OpenaiStreamWrapper: - Chunk statistics """ - def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None): + def __init__(self, stream: Any, span: Span, request_kwargs: dict): """Initialize the stream wrapper. Args: stream: The original OpenAI stream object span: The OpenTelemetry span for tracking request_kwargs: Original request parameters for context - tracer: The OpenTelemetry tracer for creating child spans """ self._stream = stream self._span = span self._request_kwargs = request_kwargs - self._tracer = tracer self._start_time = time.time() self._first_token_time = None self._chunk_count = 0 @@ -195,10 +193,10 @@ def _finalize_stream(self) -> None: self._span.set_attribute(MessageAttributes.COMPLETION_FINISH_REASON.format(i=0), self._finish_reason) # Create tool spans for each tool call - if len(self._tool_calls) > 0 and self._tracer is not None: + if len(self._tool_calls) > 0: for idx, tool_call in self._tool_calls.items(): # Create a child span for this tool call - _create_tool_span(self._span, tool_call, self._tracer) + _create_tool_span(self._span, tool_call) # Set usage if available from the API if self._usage is not None: @@ -237,19 +235,17 @@ def _finalize_stream(self) -> None: class OpenAIAsyncStreamWrapper: """Async wrapper for OpenAI Chat Completions streaming responses.""" - def __init__(self, stream: Any, span: Span, request_kwargs: dict, tracer=None): + def __init__(self, stream: Any, span: Span, request_kwargs: dict): """Initialize the async stream wrapper. Args: stream: The original OpenAI async stream object span: The OpenTelemetry span for tracking request_kwargs: Original request parameters for context - tracer: The OpenTelemetry tracer for creating child spans """ self._stream = stream self._span = span self._request_kwargs = request_kwargs - self._tracer = tracer self._start_time = time.time() self._first_token_time = None self._chunk_count = 0 @@ -356,10 +352,10 @@ def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs): if is_streaming: # Wrap the stream context_api.detach(token) - return OpenaiStreamWrapper(response, span, kwargs, tracer) + return OpenaiStreamWrapper(response, span, kwargs) else: # Handle non-streaming response - response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer) + response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span) for key, value in response_attributes.items(): if key not in request_attributes: # Avoid overwriting request attributes @@ -421,10 +417,10 @@ async def async_chat_completion_stream_wrapper(tracer, wrapped, instance, args, if is_streaming: # Wrap the stream context_api.detach(token) - return OpenAIAsyncStreamWrapper(response, span, kwargs, tracer) + return OpenAIAsyncStreamWrapper(response, span, kwargs) else: # Handle non-streaming response - response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span, tracer=tracer) + response_attributes = handle_chat_attributes(kwargs=kwargs, return_value=response, span=span) for key, value in response_attributes.items(): if key not in request_attributes: # Avoid overwriting request attributes diff --git a/agentops/instrumentation/providers/openai/wrappers/chat.py b/agentops/instrumentation/providers/openai/wrappers/chat.py index ad2d17e74..126dec49a 100644 --- a/agentops/instrumentation/providers/openai/wrappers/chat.py +++ b/agentops/instrumentation/providers/openai/wrappers/chat.py @@ -21,22 +21,24 @@ from agentops.semconv.span_kinds import AgentOpsSpanKindValues from opentelemetry import context as context_api -from opentelemetry.trace import SpanKind, Status, StatusCode +from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer logger = logging.getLogger(__name__) LLM_REQUEST_TYPE = LLMRequestTypeValues.CHAT -def _create_tool_span(parent_span, tool_call_data, tracer): +def _create_tool_span(parent_span, tool_call_data): """ Create a distinct span for each tool call. Args: parent_span: The parent LLM span tool_call_data: The tool call data dictionary - tracer: The OpenTelemetry tracer instance """ + # Get the tracer for this module + tracer = get_tracer(__name__) + # Create a child span for the tool call with tracer.start_as_current_span( name=f"tool_call.{tool_call_data['function']['name']}", @@ -61,7 +63,6 @@ def handle_chat_attributes( kwargs: Optional[Dict] = None, return_value: Optional[Any] = None, span: Optional[Span] = None, - tracer: Optional[Any] = None, ) -> AttributeMap: """Extract attributes from chat completion calls. @@ -73,7 +74,6 @@ def handle_chat_attributes( kwargs: Method keyword arguments return_value: Method return value span: The parent span for creating tool spans - tracer: The OpenTelemetry tracer for creating child spans """ attributes = { SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value, @@ -235,7 +235,7 @@ def handle_chat_attributes( # Tool calls if "tool_calls" in message: tool_calls = message["tool_calls"] - if tool_calls and span is not None and tracer is not None: + if tool_calls and span is not None: for i, tool_call in enumerate(tool_calls): # Convert tool_call to the format expected by _create_tool_span function = tool_call.get("function", {}) @@ -248,7 +248,7 @@ def handle_chat_attributes( } } # Create a child span for this tool call - _create_tool_span(span, tool_call_data, tracer) + _create_tool_span(span, tool_call_data) # Prompt filter results if "prompt_filter_results" in response_dict: From ca5356b8b9e50886504c5dd71358f0e827fe80ed Mon Sep 17 00:00:00 2001 From: Pratyush Shukla Date: Thu, 17 Jul 2025 05:35:26 +0530 Subject: [PATCH 3/3] linting --- .../providers/openai/stream_wrapper.py | 2 +- .../providers/openai/wrappers/chat.py | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/agentops/instrumentation/providers/openai/stream_wrapper.py b/agentops/instrumentation/providers/openai/stream_wrapper.py index 181a85bbe..1d8c18c12 100644 --- a/agentops/instrumentation/providers/openai/stream_wrapper.py +++ b/agentops/instrumentation/providers/openai/stream_wrapper.py @@ -9,7 +9,7 @@ from typing import Any, AsyncIterator, Iterator from opentelemetry import context as context_api -from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context, get_tracer +from opentelemetry.trace import Span, SpanKind, Status, StatusCode, set_span_in_context from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY from agentops.logging import logger diff --git a/agentops/instrumentation/providers/openai/wrappers/chat.py b/agentops/instrumentation/providers/openai/wrappers/chat.py index 126dec49a..df730ee9e 100644 --- a/agentops/instrumentation/providers/openai/wrappers/chat.py +++ b/agentops/instrumentation/providers/openai/wrappers/chat.py @@ -31,29 +31,29 @@ def _create_tool_span(parent_span, tool_call_data): """ Create a distinct span for each tool call. - + Args: parent_span: The parent LLM span tool_call_data: The tool call data dictionary """ # Get the tracer for this module tracer = get_tracer(__name__) - + # Create a child span for the tool call with tracer.start_as_current_span( name=f"tool_call.{tool_call_data['function']['name']}", kind=SpanKind.INTERNAL, - context=context_api.set_value("current_span", parent_span) + context=context_api.set_value("current_span", parent_span), ) as tool_span: # Set the span kind to TOOL tool_span.set_attribute("agentops.span.kind", AgentOpsSpanKindValues.TOOL) - + # Set tool-specific attributes - tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data['function']['name']) - tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data['function']['arguments']) - tool_span.set_attribute("tool.call.id", tool_call_data['id']) - tool_span.set_attribute("tool.call.type", tool_call_data['type']) - + tool_span.set_attribute(ToolAttributes.TOOL_NAME, tool_call_data["function"]["name"]) + tool_span.set_attribute(ToolAttributes.TOOL_PARAMETERS, tool_call_data["function"]["arguments"]) + tool_span.set_attribute("tool.call.id", tool_call_data["id"]) + tool_span.set_attribute("tool.call.type", tool_call_data["type"]) + # Set status to OK for successful tool call creation tool_span.set_status(Status(StatusCode.OK)) @@ -68,7 +68,7 @@ def handle_chat_attributes( This function is designed to work with the common wrapper pattern, extracting attributes from the method arguments and return value. - + Args: args: Method arguments (not used in this implementation) kwargs: Method keyword arguments @@ -244,8 +244,8 @@ def handle_chat_attributes( "type": tool_call.get("type", "function"), "function": { "name": function.get("name", ""), - "arguments": function.get("arguments", "") - } + "arguments": function.get("arguments", ""), + }, } # Create a child span for this tool call _create_tool_span(span, tool_call_data)