fix(openai-agents): align AgentSpanData stubs and span processor with real SDK (#4229)

This commit is contained in:
Nagkumar Arkalgud
2026-03-06 12:22:32 -08:00
committed by GitHub
parent 7701174242
commit 03891c0aaa
5 changed files with 43 additions and 72 deletions

View File

@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
- Align AgentSpanData test stubs and span processor with real OpenAI Agents SDK;
remove non-existent `operation`, `description`, `agent_id`, and `model` fields.
([#4229](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4229))
- Document official package metadata and README for the OpenAI Agents instrumentation.
([#3859](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3859))

View File

@@ -1519,17 +1519,8 @@ class GenAISemanticProcessor(TracingProcessor):
return GenAIOperationName.CHAT
return GenAIOperationName.TEXT_COMPLETION
if _is_instance_of(span_data, AgentSpanData):
# Could be create_agent or invoke_agent based on context
operation = getattr(span_data, "operation", None)
normalized = (
operation.strip().lower()
if isinstance(operation, str)
else None
)
if normalized in {"create", "create_agent"}:
return GenAIOperationName.CREATE_AGENT
if normalized in {"invoke", "invoke_agent"}:
return GenAIOperationName.INVOKE_AGENT
# The OpenAI Agents SDK AgentSpanData has no "operation" field;
# agent spans always represent invoke_agent.
return GenAIOperationName.INVOKE_AGENT
if _is_instance_of(span_data, FunctionSpanData):
return GenAIOperationName.EXECUTE_TOOL
@@ -1831,24 +1822,20 @@ class GenAISemanticProcessor(TracingProcessor):
if name:
yield GEN_AI_AGENT_NAME, name
agent_id = (
self.agent_id
or getattr(span_data, "agent_id", None)
or self._agent_id_default
)
# agent_id and description are not available on the OpenAI Agents SDK
# AgentSpanData; only use user-configured overrides.
agent_id = self.agent_id or self._agent_id_default
if agent_id:
yield GEN_AI_AGENT_ID, agent_id
description = (
self.agent_description
or getattr(span_data, "description", None)
or self._agent_description_default
)
description = self.agent_description or self._agent_description_default
if description:
yield GEN_AI_AGENT_DESCRIPTION, description
model = getattr(span_data, "model", None)
if not model and agent_content:
# The OpenAI Agents SDK AgentSpanData has no "model" field; fall back to
# the model aggregated from child generation/response spans.
model = None
if agent_content:
model = agent_content.get("request_model")
if model:
yield GEN_AI_REQUEST_MODEL, model

View File

@@ -35,12 +35,9 @@ __all__ = [
@dataclass
class AgentSpanData:
name: str | None = None
handoffs: list[str] | None = None
tools: list[str] | None = None
output_type: str | None = None
description: str | None = None
agent_id: str | None = None
model: str | None = None
operation: str | None = None
@property
def type(self) -> str:
@@ -200,8 +197,16 @@ def generation_span(**kwargs: Any):
@contextmanager
def agent_span(**kwargs: Any):
data = AgentSpanData(**kwargs)
def agent_span(
name: str,
handoffs: list[str] | None = None,
tools: list[str] | None = None,
output_type: str | None = None,
**kwargs: Any,
):
data = AgentSpanData(
name=name, handoffs=handoffs, tools=tools, output_type=output_type
)
span = _PROVIDER.create_span(data, parent=_CURRENT_TRACE)
span.start()
try:

View File

@@ -171,40 +171,31 @@ def test_function_span_records_tool_attributes():
exporter.clear()
def test_agent_create_span_records_attributes():
def test_agent_invoke_span_records_attributes():
instrumentor, exporter = _instrument_with_provider()
try:
with trace("workflow"):
with agent_span(
operation="create",
name="support_bot",
description="Answers support questions",
agent_id="agt_123",
model="gpt-4o-mini",
handoffs=["escalation_bot"],
tools=["search"],
output_type="str",
):
pass
spans = exporter.get_finished_spans()
create_span = next(
invoke_span = next(
span
for span in spans
if span.attributes[GenAI.GEN_AI_OPERATION_NAME]
== GenAI.GenAiOperationNameValues.CREATE_AGENT.value
== GenAI.GenAiOperationNameValues.INVOKE_AGENT.value
)
assert create_span.kind is SpanKind.CLIENT
assert create_span.name == "create_agent support_bot"
assert create_span.attributes[GEN_AI_PROVIDER_NAME] == "openai"
assert create_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "support_bot"
assert (
create_span.attributes[GenAI.GEN_AI_AGENT_DESCRIPTION]
== "Answers support questions"
)
assert create_span.attributes[GenAI.GEN_AI_AGENT_ID] == "agt_123"
assert (
create_span.attributes[GenAI.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini"
)
assert invoke_span.kind is SpanKind.CLIENT
assert invoke_span.name == "invoke_agent support_bot"
assert invoke_span.attributes[GEN_AI_PROVIDER_NAME] == "openai"
assert invoke_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "support_bot"
finally:
instrumentor.uninstrument()
exporter.clear()
@@ -425,7 +416,7 @@ def test_agent_name_override_applied_to_agent_spans():
try:
with trace("workflow"):
with agent_span(operation="invoke", name="support_bot"):
with agent_span(name="support_bot"):
pass
spans = exporter.get_finished_spans()

View File

@@ -156,19 +156,14 @@ def test_operation_and_span_naming(processor_setup):
== sp.GenAIOperationName.EMBEDDINGS
)
agent_create = AgentSpanData(operation=" CREATE ")
# AgentSpanData always maps to invoke_agent (no operation field in OpenAI Agents SDK)
agent_data = AgentSpanData(name="bot")
assert (
processor._get_operation_name(agent_create)
== sp.GenAIOperationName.CREATE_AGENT
)
agent_invoke = AgentSpanData(operation="invoke_agent")
assert (
processor._get_operation_name(agent_invoke)
processor._get_operation_name(agent_data)
== sp.GenAIOperationName.INVOKE_AGENT
)
agent_default = AgentSpanData(operation=None)
agent_default = AgentSpanData()
assert (
processor._get_operation_name(agent_default)
== sp.GenAIOperationName.INVOKE_AGENT
@@ -315,26 +310,19 @@ def test_attribute_builders(processor_setup):
agent_span = AgentSpanData(
name="helper",
output_type="json",
description="desc",
agent_id="agent-123",
model="model-x",
operation="invoke_agent",
)
agent_attrs = _collect(
processor._get_attributes_from_agent_span_data(agent_span, None)
)
assert agent_attrs[sp.GEN_AI_AGENT_NAME] == "helper"
assert agent_attrs[sp.GEN_AI_AGENT_ID] == "agent-123"
assert agent_attrs[sp.GEN_AI_REQUEST_MODEL] == "model-x"
assert sp.GEN_AI_AGENT_ID not in agent_attrs
assert sp.GEN_AI_REQUEST_MODEL not in agent_attrs
assert agent_attrs[sp.GEN_AI_OUTPUT_TYPE] == sp.GenAIOutputType.TEXT
# Fallback to aggregated model when span data lacks it
agent_span_no_model = AgentSpanData(
name="helper-2",
output_type="json",
description="desc",
agent_id="agent-456",
operation="invoke_agent",
)
agent_content = {
"input_messages": [],
@@ -435,9 +423,7 @@ def test_span_lifecycle_and_shutdown(processor_setup):
parent_span = FakeSpan(
trace_id="trace-1",
span_id="span-1",
span_data=AgentSpanData(
operation="invoke", name="agent", model="gpt-4o"
),
span_data=AgentSpanData(name="agent"),
started_at="2024-01-01T00:00:00Z",
ended_at="2024-01-01T00:00:02Z",
)
@@ -476,7 +462,7 @@ def test_span_lifecycle_and_shutdown(processor_setup):
linger_span = FakeSpan(
trace_id="trace-2",
span_id="span-3",
span_data=AgentSpanData(operation=None),
span_data=AgentSpanData(),
started_at="2024-01-01T00:00:06Z",
)
processor.on_span_start(linger_span)
@@ -518,7 +504,6 @@ def test_chat_span_renamed_with_model(processor_setup):
trace_id=trace.trace_id,
span_id="agent-span",
span_data=AgentSpanData(
operation="invoke_agent",
name="Agent",
),
started_at="2025-01-01T00:00:00Z",