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..51922b54e2 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 @@ -1064,6 +1064,13 @@ def _build_content_payload(self, span: Span[Any]) -> ContentPayload: sys_instr = self._collect_system_instructions(span_input) if sys_instr: payload.system_instructions = sys_instr + if capture_system and not payload.system_instructions: + response = getattr(span_data, "response", None) + instructions = getattr(response, "instructions", None) + if isinstance(instructions, str) and instructions: + payload.system_instructions = [ + {"type": "text", "content": instructions} + ] if capture_messages: normalized_out = self._normalize_output_messages_to_role_parts( span_data diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing/__init__.py index 4ed06c8977..74bdde1bc2 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing/__init__.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/stubs/agents/tracing/__init__.py @@ -29,6 +29,7 @@ "GenerationSpanData", "FunctionSpanData", "ResponseSpanData", + "MCPListToolsSpanData", ] @@ -74,12 +75,26 @@ def type(self) -> str: @dataclass class ResponseSpanData: response: Any = None + input: Sequence[Mapping[str, Any]] | None = None @property def type(self) -> str: return SPAN_TYPE_RESPONSE +SPAN_TYPE_MCP_LIST_TOOLS = "mcp_tools" + + +@dataclass +class MCPListToolsSpanData: + server: str | None = None + result: list[str] | None = None + + @property + def type(self) -> str: + return "mcp_tools" + + class _ProcessorFanout(TracingProcessor): def __init__(self) -> None: self._processors: list[TracingProcessor] = [] diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_z_span_processor_unit.py b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_z_span_processor_unit.py index b2c8c7c8f3..f041691e9f 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_z_span_processor_unit.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/test_z_span_processor_unit.py @@ -508,6 +508,57 @@ def test_span_lifecycle_and_shutdown(processor_setup): ) +def test_response_span_instructions_fallback(processor_setup): + """Instructions on Response object populate gen_ai.system_instructions.""" + processor, exporter = processor_setup + + class _Usage: + def __init__(self): + self.input_tokens = None + self.prompt_tokens = 5 + self.output_tokens = None + self.completion_tokens = 3 + self.total_tokens = 8 + + class _Response: + def __init__(self): + self.id = "resp-instr" + self.model = "gpt-4o" + self.usage = _Usage() + self.output = [{"finish_reason": "stop"}] + self.instructions = "You are a helpful assistant." + + trace = FakeTrace( + name="instr-trace", + trace_id="trace-instr", + started_at="2024-01-01T00:00:00Z", + ended_at="2024-01-01T00:00:02Z", + ) + processor.on_trace_start(trace) + + response_data = ResponseSpanData(response=_Response()) + # No system messages in input – instructions only on response object + response_data.input = [{"role": "user", "content": "Hello"}] + span = FakeSpan( + trace_id="trace-instr", + span_id="span-instr", + span_data=response_data, + started_at="2024-01-01T00:00:00Z", + ended_at="2024-01-01T00:00:01Z", + ) + processor.on_span_start(span) + processor.on_span_end(span) + processor.on_trace_end(trace) + + finished = exporter.get_finished_spans() + response_span = next(s for s in finished if s.name.startswith("chat")) + sys_instr = json.loads( + response_span.attributes[sp.GEN_AI_SYSTEM_INSTRUCTIONS] + ) + assert len(sys_instr) == 1 + assert sys_instr[0]["content"] == "You are a helpful assistant." + + def test_chat_span_renamed_with_model(processor_setup): processor, exporter = processor_setup