Skip to content

Commit 48569ad

Browse files
committed
Merge remote-tracking branch 'origin/main' into openai/retry_takeover
2 parents 351e93d + 9e7dc7a commit 48569ad

23 files changed

+1052
-704
lines changed

pyproject.toml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ opentelemetry = [
2626
]
2727
pydantic = ["pydantic>=2.0.0,<3"]
2828
openai-agents = [
29-
"openai-agents >= 0.1,<0.2",
29+
"openai-agents >= 0.2.3,<0.3",
3030
"eval-type-backport>=0.2.2; python_version < '3.10'"
3131
]
3232

@@ -165,6 +165,7 @@ reportAny = "none"
165165
reportCallInDefaultInitializer = "none"
166166
reportExplicitAny = "none"
167167
reportIgnoreCommentWithoutRule = "none"
168+
reportImplicitAbstractClass = "none"
168169
reportImplicitOverride = "none"
169170
reportImplicitStringConcatenation = "none"
170171
reportImportCycles = "none"
@@ -184,11 +185,6 @@ exclude = [
184185
"temporalio/bridge/proto",
185186
"tests/worker/workflow_sandbox/testmodules/proto",
186187
"temporalio/bridge/worker.py",
187-
"temporalio/contrib/opentelemetry.py",
188-
"temporalio/contrib/pydantic.py",
189-
"temporalio/converter.py",
190-
"temporalio/testing/_workflow.py",
191-
"temporalio/worker/_activity.py",
192188
"temporalio/worker/_replayer.py",
193189
"temporalio/worker/_worker.py",
194190
"temporalio/worker/workflow_sandbox/_importer.py",
@@ -203,9 +199,7 @@ exclude = [
203199
"tests/contrib/pydantic/workflows.py",
204200
"tests/test_converter.py",
205201
"tests/test_service.py",
206-
"tests/test_workflow.py",
207202
"tests/worker/test_activity.py",
208-
"tests/worker/test_workflow.py",
209203
"tests/worker/workflow_sandbox/test_importer.py",
210204
"tests/worker/workflow_sandbox/test_restrictions.py",
211205
# TODO: these pass locally but fail in CI with

temporalio/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2889,7 +2889,7 @@ def _from_raw_info(
28892889
cls,
28902890
info: temporalio.api.workflow.v1.WorkflowExecutionInfo,
28912891
converter: temporalio.converter.DataConverter,
2892-
**additional_fields,
2892+
**additional_fields: Any,
28932893
) -> WorkflowExecution:
28942894
return cls(
28952895
close_time=info.close_time.ToDatetime().replace(tzinfo=timezone.utc)

temporalio/contrib/openai_agents/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
Use with caution in production environments.
99
"""
1010

11-
from temporalio.contrib.openai_agents._invoke_model_activity import ModelActivity
1211
from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters
1312
from temporalio.contrib.openai_agents._temporal_openai_agents import (
13+
OpenAIAgentsPlugin,
1414
TestModel,
1515
TestModelProvider,
16-
set_open_ai_agent_temporal_overrides,
1716
)
1817
from temporalio.contrib.openai_agents._trace_interceptor import (
1918
OpenAIAgentsTracingInterceptor,
@@ -22,10 +21,9 @@
2221
from . import workflow
2322

2423
__all__ = [
25-
"ModelActivity",
24+
"OpenAIAgentsPlugin",
2625
"ModelActivityParameters",
27-
"set_open_ai_agent_temporal_overrides",
28-
"OpenAIAgentsTracingInterceptor",
26+
"workflow",
2927
"TestModel",
3028
"TestModelProvider",
3129
]

temporalio/contrib/openai_agents/_heartbeat_decorator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
def _auto_heartbeater(fn: F) -> F:
1111
# Propagate type hints from the original callable.
1212
@wraps(fn)
13-
async def wrapper(*args, **kwargs):
13+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
1414
heartbeat_timeout = activity.info().heartbeat_timeout
1515
heartbeat_task = None
1616
if heartbeat_timeout:

temporalio/contrib/openai_agents/_invoke_model_activity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def make_tool(tool: ToolInput) -> Tool:
173173
raise UserError(f"Unknown tool type: {tool.name}")
174174

175175
tools = [make_tool(x) for x in input.get("tools", [])]
176-
handoffs = [
176+
handoffs: list[Handoff[Any, Any]] = [
177177
Handoff(
178178
tool_name=x.tool_name,
179179
tool_description=x.tool_description,

temporalio/contrib/openai_agents/_openai_runner.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from dataclasses import replace
2-
from datetime import timedelta
3-
from typing import Optional, Union
2+
from typing import Any, Union
43

54
from agents import (
65
Agent,
76
RunConfig,
8-
RunHooks,
97
RunResult,
108
RunResultStreaming,
119
TContext,
@@ -14,10 +12,8 @@
1412
from agents.run import DEFAULT_AGENT_RUNNER, DEFAULT_MAX_TURNS, AgentRunner
1513

1614
from temporalio import workflow
17-
from temporalio.common import Priority, RetryPolicy
1815
from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters
1916
from temporalio.contrib.openai_agents._temporal_model_stub import _TemporalModelStub
20-
from temporalio.workflow import ActivityCancellationType, VersioningIntent
2117

2218

2319
class TemporalOpenAIRunner(AgentRunner):
@@ -36,7 +32,7 @@ async def run(
3632
self,
3733
starting_agent: Agent[TContext],
3834
input: Union[str, list[TResponseInputItem]],
39-
**kwargs,
35+
**kwargs: Any,
4036
) -> RunResult:
4137
"""Run the agent in a Temporal workflow."""
4238
if not workflow.in_workflow():
@@ -82,7 +78,7 @@ def run_sync(
8278
self,
8379
starting_agent: Agent[TContext],
8480
input: Union[str, list[TResponseInputItem]],
85-
**kwargs,
81+
**kwargs: Any,
8682
) -> RunResult:
8783
"""Run the agent synchronously (not supported in Temporal workflows)."""
8884
if not workflow.in_workflow():
@@ -97,7 +93,7 @@ def run_streamed(
9793
self,
9894
starting_agent: Agent[TContext],
9995
input: Union[str, list[TResponseInputItem]],
100-
**kwargs,
96+
**kwargs: Any,
10197
) -> RunResultStreaming:
10298
"""Run the agent with streaming responses (not supported in Temporal workflows)."""
10399
if not workflow.in_workflow():

temporalio/contrib/openai_agents/_temporal_openai_agents.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@
2121
from agents.tracing.provider import DefaultTraceProvider
2222
from openai.types.responses import ResponsePromptParam
2323

24+
import temporalio.client
25+
import temporalio.worker
26+
from temporalio.client import ClientConfig
27+
from temporalio.contrib.openai_agents._invoke_model_activity import ModelActivity
2428
from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters
2529
from temporalio.contrib.openai_agents._openai_runner import TemporalOpenAIRunner
2630
from temporalio.contrib.openai_agents._temporal_trace_provider import (
2731
TemporalTraceProvider,
2832
)
33+
from temporalio.contrib.openai_agents._trace_interceptor import (
34+
OpenAIAgentsTracingInterceptor,
35+
)
36+
from temporalio.contrib.pydantic import pydantic_data_converter
37+
from temporalio.worker import Worker, WorkerConfig
2938

3039

3140
@contextmanager
@@ -133,3 +142,121 @@ def stream_response(
133142
) -> AsyncIterator[TResponseStreamEvent]:
134143
"""Get a streamed response from the model. Unimplemented."""
135144
raise NotImplementedError()
145+
146+
147+
class OpenAIAgentsPlugin(temporalio.client.Plugin, temporalio.worker.Plugin):
148+
"""Temporal plugin for integrating OpenAI agents with Temporal workflows.
149+
150+
.. warning::
151+
This class is experimental and may change in future versions.
152+
Use with caution in production environments.
153+
154+
This plugin provides seamless integration between the OpenAI Agents SDK and
155+
Temporal workflows. It automatically configures the necessary interceptors,
156+
activities, and data converters to enable OpenAI agents to run within
157+
Temporal workflows with proper tracing and model execution.
158+
159+
The plugin:
160+
1. Configures the Pydantic data converter for type-safe serialization
161+
2. Sets up tracing interceptors for OpenAI agent interactions
162+
3. Registers model execution activities
163+
4. Manages the OpenAI agent runtime overrides during worker execution
164+
165+
Args:
166+
model_params: Configuration parameters for Temporal activity execution
167+
of model calls. If None, default parameters will be used.
168+
model_provider: Optional model provider for custom model implementations.
169+
Useful for testing or custom model integrations.
170+
171+
Example:
172+
>>> from temporalio.client import Client
173+
>>> from temporalio.worker import Worker
174+
>>> from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters
175+
>>> from datetime import timedelta
176+
>>>
177+
>>> # Configure model parameters
178+
>>> model_params = ModelActivityParameters(
179+
... start_to_close_timeout=timedelta(seconds=30),
180+
... retry_policy=RetryPolicy(maximum_attempts=3)
181+
... )
182+
>>>
183+
>>> # Create plugin
184+
>>> plugin = OpenAIAgentsPlugin(model_params=model_params)
185+
>>>
186+
>>> # Use with client and worker
187+
>>> client = await Client.connect(
188+
... "localhost:7233",
189+
... plugins=[plugin]
190+
... )
191+
>>> worker = Worker(
192+
... client,
193+
... task_queue="my-task-queue",
194+
... workflows=[MyWorkflow],
195+
... )
196+
"""
197+
198+
def __init__(
199+
self,
200+
model_params: Optional[ModelActivityParameters] = None,
201+
model_provider: Optional[ModelProvider] = None,
202+
) -> None:
203+
"""Initialize the OpenAI agents plugin.
204+
205+
Args:
206+
model_params: Configuration parameters for Temporal activity execution
207+
of model calls. If None, default parameters will be used.
208+
model_provider: Optional model provider for custom model implementations.
209+
Useful for testing or custom model integrations.
210+
"""
211+
self._model_params = model_params
212+
self._model_provider = model_provider
213+
214+
def configure_client(self, config: ClientConfig) -> ClientConfig:
215+
"""Configure the Temporal client for OpenAI agents integration.
216+
217+
This method sets up the Pydantic data converter to enable proper
218+
serialization of OpenAI agent objects and responses.
219+
220+
Args:
221+
config: The client configuration to modify.
222+
223+
Returns:
224+
The modified client configuration.
225+
"""
226+
config["data_converter"] = pydantic_data_converter
227+
return super().configure_client(config)
228+
229+
def configure_worker(self, config: WorkerConfig) -> WorkerConfig:
230+
"""Configure the Temporal worker for OpenAI agents integration.
231+
232+
This method adds the necessary interceptors and activities for OpenAI
233+
agent execution:
234+
- Adds tracing interceptors for OpenAI agent interactions
235+
- Registers model execution activities
236+
237+
Args:
238+
config: The worker configuration to modify.
239+
240+
Returns:
241+
The modified worker configuration.
242+
"""
243+
config["interceptors"] = list(config.get("interceptors") or []) + [
244+
OpenAIAgentsTracingInterceptor()
245+
]
246+
config["activities"] = list(config.get("activities") or []) + [
247+
ModelActivity(self._model_provider).invoke_model_activity
248+
]
249+
return super().configure_worker(config)
250+
251+
async def run_worker(self, worker: Worker) -> None:
252+
"""Run the worker with OpenAI agents temporal overrides.
253+
254+
This method sets up the necessary runtime overrides for OpenAI agents
255+
to work within the Temporal worker context, including custom runners
256+
and trace providers.
257+
258+
Args:
259+
worker: The worker instance to run.
260+
"""
261+
with set_open_ai_agent_temporal_overrides(self._model_params):
262+
await super().run_worker(worker)

temporalio/contrib/openai_agents/_temporal_trace_provider.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Provides support for integration with OpenAI Agents SDK tracing across workflows"""
22

33
import uuid
4-
from typing import Any, Optional, Union, cast
4+
from types import TracebackType
5+
from typing import Any, Optional, cast
56

67
from agents import SpanData, Trace, TracingProcessor
78
from agents.tracing import (
@@ -184,6 +185,11 @@ def __enter__(self):
184185
"""Enter the context of the Temporal trace provider."""
185186
return self
186187

187-
def __exit__(self, exc_type, exc_val, exc_tb):
188+
def __exit__(
189+
self,
190+
exc_type: type[BaseException],
191+
exc_val: BaseException,
192+
exc_tb: TracebackType,
193+
):
188194
"""Exit the context of the Temporal trace provider."""
189195
self._multi_processor.shutdown()

temporalio/contrib/openai_agents/workflow.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,26 @@ async def run_operation(ctx: RunContextWrapper[Any], input: str) -> Any:
240240

241241

242242
class ToolSerializationError(TemporalError):
243-
"""Error that occurs when a tool output could not be serialized."""
243+
"""Error that occurs when a tool output could not be serialized.
244+
245+
.. warning::
246+
This exception is experimental and may change in future versions.
247+
Use with caution in production environments.
248+
249+
This exception is raised when a tool (created from an activity or Nexus operation)
250+
returns a value that cannot be properly serialized for use by the OpenAI agent.
251+
All tool outputs must be convertible to strings for the agent to process them.
252+
253+
The error typically occurs when:
254+
- A tool returns a complex object that doesn't have a meaningful string representation
255+
- The returned object cannot be converted using str()
256+
- Custom serialization is needed but not implemented
257+
258+
Example:
259+
>>> @activity.defn
260+
>>> def problematic_tool() -> ComplexObject:
261+
... return ComplexObject() # This might cause ToolSerializationError
262+
263+
To fix this error, ensure your tool returns string-convertible values or
264+
modify the tool to return a string representation of the result.
265+
"""

temporalio/contrib/opentelemetry.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class should return the workflow interceptor subclass from
7171
custom attributes desired.
7272
"""
7373

74-
def __init__(
74+
def __init__( # type: ignore[reportMissingSuperCall]
7575
self,
7676
tracer: Optional[opentelemetry.trace.Tracer] = None,
7777
*,
@@ -125,11 +125,10 @@ def workflow_interceptor_class(
125125
:py:meth:`temporalio.worker.Interceptor.workflow_interceptor_class`.
126126
"""
127127
# Set the externs needed
128-
# TODO(cretz): MyPy works w/ spread kwargs instead of direct passing
129128
input.unsafe_extern_functions.update(
130-
**_WorkflowExternFunctions(
131-
__temporal_opentelemetry_completed_span=self._completed_workflow_span,
132-
)
129+
{
130+
"__temporal_opentelemetry_completed_span": self._completed_workflow_span,
131+
}
133132
)
134133
return TracingWorkflowInboundInterceptor
135134

0 commit comments

Comments
 (0)