Skip to content
10 changes: 10 additions & 0 deletions temporalio/contrib/openai_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,31 @@
StatelessMCPServerProvider,
)
from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters
from temporalio.contrib.openai_agents._otel_tracing import (
setup_tracing,
workflow_span,
)
from temporalio.contrib.openai_agents._temporal_openai_agents import (
OpenAIAgentsPlugin,
OpenAIPayloadConverter,
)
from temporalio.contrib.openai_agents.workflow import AgentsWorkflowError

# Re-export OtelTracingPlugin from its new location for backward compatibility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment makes any sense.

from temporalio.contrib.opentelemetry import OtelTracingPlugin

from . import testing, workflow

__all__ = [
"AgentsWorkflowError",
"ModelActivityParameters",
"OpenAIAgentsPlugin",
"OpenAIPayloadConverter",
"OtelTracingPlugin",
"setup_tracing",
"StatelessMCPServerProvider",
"StatefulMCPServerProvider",
"testing",
"workflow",
"workflow_span",
]
73 changes: 73 additions & 0 deletions temporalio/contrib/openai_agents/_otel_tracing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""OpenTelemetry tracing support for OpenAI Agents in Temporal workflows."""

from __future__ import annotations

from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator

from opentelemetry import trace as otel_trace
from opentelemetry.trace import Span

from temporalio import workflow

if TYPE_CHECKING:
from opentelemetry.sdk.trace import TracerProvider


def setup_tracing(tracer_provider: TracerProvider) -> None:
"""Set up OpenAI Agents OTEL tracing with OpenInference instrumentation.

This instruments the OpenAI Agents SDK with OpenInference, which converts
agent spans to OTEL spans. Combined with opentelemetry passthrough in the
sandbox, this enables proper span parenting inside Temporal's workflows.

Args:
tracer_provider: The TracerProvider to use for creating spans.
"""
from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor

otel_trace.set_tracer_provider(tracer_provider)
OpenAIAgentsInstrumentor().instrument(tracer_provider=tracer_provider)


@contextmanager
def workflow_span(name: str, **attributes: str) -> Iterator[Span | None]:
"""Create an OTEL span in workflow code that is replay-safe.

This context manager creates a span only on the first execution of the
workflow task, not during replay. This prevents span duplication when
workflow code is re-executed during replay (e.g., when max_cached_workflows=0).

.. warning::
This API is experimental and may change in future versions.
Consider using ReplayFilteringSpanProcessor instead for automatic
filtering of all spans during replay.

Args:
name: The name of the span.
**attributes: Optional attributes to set on the span.

Yields:
The span on first execution, None during replay.

Example:
>>> @workflow.defn
... class MyWorkflow:
... @workflow.run
... async def run(self) -> str:
... with workflow_span("my_operation", query="test") as span:
... result = await workflow.execute_activity(...)
... return result

Note:
Spans created in activities do not need this wrapper since activities
are not replayed - they only execute once and their results are cached.
"""
if workflow.unsafe.is_replaying():
yield None
else:
tracer = otel_trace.get_tracer(__name__)
with tracer.start_as_current_span(name) as span:
for key, value in attributes.items():
span.set_attribute(key, value)
yield span
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def __init__(
"StatelessMCPServerProvider | StatefulMCPServerProvider"
] = (),
register_activities: bool = True,
create_spans: bool = True,
) -> None:
"""Initialize the OpenAI agents plugin.
Expand All @@ -196,7 +197,11 @@ def __init__(
register_activities: Whether to register activities during the worker execution.
This can be disabled on some workers to allow a separation of workflows and activities
but should not be disabled on all workers, or agents will not be able to progress.
create_spans: Whether to create ``temporal:*`` spans for workflow and activity
operations. If False, trace context is still propagated but no spans are
created. Defaults to True.
"""
self._create_spans = create_spans
if model_params is None:
model_params = ModelActivityParameters()

Expand Down Expand Up @@ -252,7 +257,7 @@ async def run_context() -> AsyncIterator[None]:
super().__init__(
name="OpenAIAgentsPlugin",
data_converter=_data_converter,
worker_interceptors=[OpenAIAgentsTracingInterceptor()],
worker_interceptors=[OpenAIAgentsTracingInterceptor(create_spans=self._create_spans)],
activities=add_activities,
workflow_runner=workflow_runner,
workflow_failure_exception_types=[AgentsWorkflowError],
Expand Down
Loading
Loading