From aee1ce50e70b61c4f1cec1895977b6119ce54013 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Fri, 13 Feb 2026 11:31:44 +1100 Subject: [PATCH 1/3] openai-agents-v2: Populate instructions and tool definitions from Response obj --- .../openai_agents/span_processor.py | 36 +++++++++++++++++ .../tests/test_tracer.py | 39 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index d1dce8ec5e..c99c780155 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -1056,10 +1056,20 @@ def _build_content_payload(self, span: Span[Any]) -> ContentPayload: elif _is_instance_of(span_data, ResponseSpanData): span_input = getattr(span_data, "input", None) + response_obj = getattr(span_data, "response", None) if capture_messages and span_input: payload.input_messages = ( self._normalize_messages_to_role_parts(span_input) ) + + if ( + capture_system + and response_obj + and hasattr(response_obj, "instructions") + ): + payload.system_instructions = self._normalize_to_text_parts( + response_obj.instructions + ) if capture_system and span_input: sys_instr = self._collect_system_instructions(span_input) if sys_instr: @@ -2029,6 +2039,32 @@ def _get_attributes_from_response_span_data( if output_tokens is not None: yield GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens + # Tool definitions from response + if self._capture_tool_definitions and hasattr( + span_data.response, "tools" + ): + + def _serialize_tool_value(value: Any) -> Optional[str]: + if value is None: + return None + return { + "name": getattr(value, "name", None), + "type": getattr(value, "type", None), + "description": getattr(value, "description", None), + "parameters": getattr(value, "parameters", None), + } + + yield ( + GEN_AI_TOOL_DEFINITIONS, + safe_json_dumps( + list( + map( + _serialize_tool_value, span_data.response.tools + ) + ) + ), + ) + # Input/output messages if ( self.include_sensitive_data diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index 1f21ab25c0..cef35f0591 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -484,11 +484,26 @@ def __init__(self, input_tokens: int, output_tokens: int) -> None: self.input_tokens = input_tokens self.output_tokens = output_tokens + class _FunctionTool: + def __init__(self) -> None: + self.name = "get_current_weather" + self.type = "function" + self.description = "Get the current weather in a given location" + self.parameters = { + "type": "object", + "properties": { + "location": {"title": "Location", "type": "string"}, + }, + "required": ["location"], + } + class _Response: def __init__(self) -> None: self.id = "resp-123" + self.instructions = "You are a helpful assistant." self.model = "gpt-4o-mini" self.usage = _Usage(42, 9) + self.tools = [_FunctionTool()] self.output = [{"finish_reason": "stop"}] try: @@ -516,6 +531,30 @@ def __init__(self) -> None: assert response.attributes[GenAI.GEN_AI_RESPONSE_FINISH_REASONS] == ( "stop", ) + + system_instructions = json.loads( + response.attributes[GenAI.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert system_instructions == [ + {"type": "text", "content": "You are a helpful assistant."} + ] + tool_definitions = json.loads( + response.attributes[GenAI.GEN_AI_TOOL_DEFINITIONS] + ) + assert tool_definitions == [ + { + "type": "function", + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": {"title": "Location", "type": "string"}, + }, + "required": ["location"], + }, + } + ] finally: instrumentor.uninstrument() exporter.clear() From c0d890927b91b520155d9cc4578832ead8c0f427 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Wed, 18 Feb 2026 13:29:34 +1100 Subject: [PATCH 2/3] fix failing test on py*-test-instrumentation-openai_agents-v2-oldest --- .../openai_agents/span_processor.py | 14 +------ .../tests/test_tracer.py | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py index c99c780155..3a72ba5b07 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/src/opentelemetry/instrumentation/openai_agents/span_processor.py @@ -2043,23 +2043,13 @@ def _get_attributes_from_response_span_data( if self._capture_tool_definitions and hasattr( span_data.response, "tools" ): - - def _serialize_tool_value(value: Any) -> Optional[str]: - if value is None: - return None - return { - "name": getattr(value, "name", None), - "type": getattr(value, "type", None), - "description": getattr(value, "description", None), - "parameters": getattr(value, "parameters", None), - } - yield ( GEN_AI_TOOL_DEFINITIONS, safe_json_dumps( list( map( - _serialize_tool_value, span_data.response.tools + lambda tool: tool.to_dict(), + span_data.response.tools, ) ) ), diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py index cef35f0591..ed92b83d10 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_tracer.py @@ -25,6 +25,7 @@ set_trace_processors, trace, ) +from openai.types.responses import FunctionTool # noqa: E402 from opentelemetry.instrumentation.openai_agents import ( # noqa: E402 OpenAIAgentsInstrumentor, @@ -62,6 +63,9 @@ GEN_AI_OUTPUT_MESSAGES = getattr( GenAI, "GEN_AI_OUTPUT_MESSAGES", "gen_ai.output.messages" ) +GEN_AI_TOOL_DEFINITIONS = getattr( + GenAI, "GEN_AI_TOOL_DEFINITIONS", "gen_ai.tool.definitions" +) def _instrument_with_provider(**instrument_kwargs): @@ -484,26 +488,29 @@ def __init__(self, input_tokens: int, output_tokens: int) -> None: self.input_tokens = input_tokens self.output_tokens = output_tokens - class _FunctionTool: - def __init__(self) -> None: - self.name = "get_current_weather" - self.type = "function" - self.description = "Get the current weather in a given location" - self.parameters = { - "type": "object", - "properties": { - "location": {"title": "Location", "type": "string"}, - }, - "required": ["location"], - } - class _Response: def __init__(self) -> None: self.id = "resp-123" self.instructions = "You are a helpful assistant." self.model = "gpt-4o-mini" self.usage = _Usage(42, 9) - self.tools = [_FunctionTool()] + self.tools = [ + FunctionTool( + name="get_current_weather", + type="function", + description="Get the current weather in a given location", + parameters={ + "type": "object", + "properties": { + "location": { + "title": "Location", + "type": "string", + }, + }, + "required": ["location"], + }, + ) + ] self.output = [{"finish_reason": "stop"}] try: @@ -539,7 +546,7 @@ def __init__(self) -> None: {"type": "text", "content": "You are a helpful assistant."} ] tool_definitions = json.loads( - response.attributes[GenAI.GEN_AI_TOOL_DEFINITIONS] + response.attributes[GEN_AI_TOOL_DEFINITIONS] ) assert tool_definitions == [ { From 71ee6ec3b9b7dae9af686b8c34245fee72b4cd96 Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Thu, 19 Feb 2026 10:00:02 +1100 Subject: [PATCH 3/3] update changelog --- .../opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md index 95f69d6ded..4434fe558a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Document official package metadata and README for the OpenAI Agents instrumentation. ([#3859](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3859)) +- Populate instructions and tool definitions from Response obj. + ([#4196](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4196)) ## Version 0.1.0 (2025-10-15)