Skip to content

Commit 53d9ace

Browse files
tconley1428mfateevjssmithSushisourcedandavison
authored
OpenAI agents support (#898)
* Added openai_agents. * added tools and trace interceptor. * Lint error fixes * Lint error fixes * Missing docstrings added. * Fixed tool serialization. * Initial test implementation * Intercept test calls to LLM * Move to optional dependency * Linting * Fixing build errors * Fixing build errors * Fixing typo * Fake API key for test, skip below 3.11 * Tools test * Move activity to a method to allow model customization without monkey patch * Change overrides to context manager * Research workflow test * Customer service and agents as tools tests. These will currently fail without a change to type deserialization of agent responses * Fix up some imports * Remove unneeded print * Doc string improvement * Execute inside sandbox * 3.9 lint error * Add activity configuration to openai overrides and activity_as_tool * Update docstrings * Updating tests * Add experimental warnings * Remove runtime warnings * Test required import for build error * Fix import * Update open_ai_data_converter.py * Remove required import * Simplify custom data converter * Replace rebuild * Add passthrough type namespaces to data converter * Check activity count in customer_service test * cleanup dataconverter imports + doc build errors * add documentation * Fix model rebuild * update for OpenAI Agents SDK release 0.0.19 * lint fixes * fixed and elaborated on README * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Spencer Judge <sjudge@hey.com> * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Spencer Judge <sjudge@hey.com> * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Dan Davison <dan.davison@temporal.io> * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Dan Davison <dan.davison@temporal.io> * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Dan Davison <dan.davison@temporal.io> * Update temporalio/contrib/openai_agents/README.md Co-authored-by: Dan Davison <dan.davison@temporal.io> * readme updates * Addressing PR feedback, mostly some new test validation * PR cleanup from feedback * Remove change from irrelevant file * PR cleanup * Add custom message for tool output failure * Remove commented code and unneeded sandbox statement --------- Co-authored-by: Maxim Fateev <mfateev@gmail.com> Co-authored-by: Johann Schleier-Smith <johann.schleiersmith@temporal.io> Co-authored-by: Johann Schleier-Smith <jssmith@gmail.com> Co-authored-by: Spencer Judge <sjudge@hey.com> Co-authored-by: Dan Davison <dan.davison@temporal.io>
1 parent 7e4857f commit 53d9ace

19 files changed

+3741
-519
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ keywords = [
1111
"workflow",
1212
]
1313
dependencies = [
14-
"protobuf>=3.20",
14+
"protobuf>=3.20,<6",
1515
"python-dateutil>=2.8.2,<3 ; python_version < '3.11'",
1616
"types-protobuf>=3.20",
1717
"typing-extensions>=4.2.0,<5",
@@ -24,6 +24,7 @@ opentelemetry = [
2424
"opentelemetry-sdk>=1.11.1,<2",
2525
]
2626
pydantic = ["pydantic>=2.0.0,<3"]
27+
openai-agents = ["openai-agents >= 0.0.19"]
2728

2829
[project.urls]
2930
Homepage = "https://github.com/temporalio/sdk-python"
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# OpenAI Agents SDK Support
2+
3+
⚠️ **Experimental** - This module is not yet stable and may change in the future.
4+
5+
This module provides a bridge between Temporal durable execution and the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python).
6+
7+
## Background
8+
9+
If you want to build production-ready AI agents quickly, you can use this module to combine [Temporal durable execution](https://docs.temporal.io/evaluate/understanding-temporal#durable-execution) with OpenAI Agents.
10+
Temporal's durable execution provides a crash-proof system foundation, and OpenAI Agents offers a lightweight and yet powerful framework for defining agent functionality.
11+
12+
13+
## Approach
14+
15+
The standard control flow of a single AI agent involves:
16+
17+
1. Receiving *input* and handing it to an *LLM*.
18+
2. At the direction of the LLM, calling *tools*, and returning that output back to the LLM.
19+
3. Repeating as necessary, until the LLM produces *output*.
20+
21+
The diagram below illustrates an AI agent control flow.
22+
23+
```mermaid
24+
graph TD
25+
A["INPUT"] --> B["LLM"]
26+
B <--> C["TOOLS"]
27+
B --> D["OUTPUT"]
28+
```
29+
30+
To provide durable execution, Temporal needs to be able to recover from failures at any step of this process.
31+
To do this, Temporal requires separating an application's deterministic (repeatable) and non-deterministic parts:
32+
33+
1. Deterministic pieces, termed *workflows*, execute the same way if re-run with the same inputs.
34+
2. Non-deterministic pieces, termed *activies*, have no limitations—they may perform I/O and any other operations.
35+
36+
Temporal maintains a server-side execution history of all state state passing in and out of a workflow, using it to recover when needed.
37+
See the [Temporal documentation](https://docs.temporal.io/evaluate/understanding-temporal#temporal-application-the-building-blocks) for more information.
38+
39+
How do we apply the Temporal execution model to enable durable execution for AI agents?
40+
41+
- The core control flow, which is managed by the OpenAI Agents SDK, goes into a Temporal workflow.
42+
- Calls to the LLM provider, which are inherently non-deterministic, go into activities.
43+
- Calls to tools, which could contain arbitrary code, similarly go into activities.
44+
45+
This module ensures that LLM calls and tool calls originating from the OpenAI Agents SDK run as Temporal activities.
46+
It also ensures that their inputs and outputs are properly serialized.
47+
48+
## Basic Example
49+
50+
Let's start with a simple example.
51+
52+
The first file, `hello_world_workflow.py`, defines an OpenAI agent within a Temporal workflow.
53+
54+
```python
55+
# File: hello_world_workflow.py
56+
from temporalio import workflow
57+
58+
# Trusted imports bypass the Temporal sandbox, which otherwise
59+
# prevents imports which may result in non-deterministic execution.
60+
with workflow.unsafe.imports_passed_through():
61+
from agents import Agent, Runner
62+
63+
@workflow.defn
64+
class HelloWorldAgent:
65+
@workflow.run
66+
async def run(self, prompt: str) -> str:
67+
agent = Agent(
68+
name="Assistant",
69+
instructions="You only respond in haikus.",
70+
)
71+
72+
result = await Runner.run(starting_agent=agent, input=prompt)
73+
return result.final_output
74+
```
75+
76+
If you are familiar with Temporal and with Open AI Agents SDK, this code will look very familiar.
77+
We annotate the `HelloWorldAgent` class with `@workflow.defn` to define a workflow, then use the `@workflow.run` annotation to define the entrypoint.
78+
We use the `Agent` class to define a simple agent, one which always responds with haikus.
79+
Within the workflow, we start agent using the `Runner`, as is typical, passing through `prompt` as an argument.
80+
81+
Perhaps the most interesting thing about this code is the `workflow.unsafe.imports_passed_through()` context manager that precedes the OpenAI Agents SDK imports.
82+
This statement tells Temporal to skip sandboxing for these trusted libraries.
83+
This is important because Python's dynamic nature forces Temporal's Python's sandbox to re-validate imports every time a workflow runs, which comes at a performance cost.
84+
The OpenAI Agents SDK also contains certain code that Temporal is not able to validate automatically for determinism.
85+
86+
The second file, `run_worker.py`, lauches a Temporal worker.
87+
This is a program that connects to the Temporal server and receives work to run, in this case `HelloWorldAgent` invocations.
88+
89+
```python
90+
# File: run_worker.py
91+
92+
import asyncio
93+
from datetime import timedelta
94+
95+
from temporalio.client import Client
96+
from temporalio.contrib.openai_agents.invoke_model_activity import ModelActivity
97+
from temporalio.contrib.openai_agents.open_ai_data_converter import open_ai_data_converter
98+
from temporalio.contrib.openai_agents.temporal_openai_agents import set_open_ai_agent_temporal_overrides
99+
from temporalio.worker import Worker
100+
101+
from hello_world_workflow import HelloWorldAgent
102+
103+
async def worker_main():
104+
# Configure the OpenAI Agents SDK to use Temporal activities for LLM API calls
105+
# and for tool calls.
106+
with set_open_ai_agent_temporal_overrides(
107+
start_to_close_timeout=timedelta(seconds=10)
108+
):
109+
# Create a Temporal client connected to server at the given address
110+
# Use the OpenAI data converter to ensure proper serialization/deserialization
111+
client = await Client.connect(
112+
"localhost:7233",
113+
data_converter=open_ai_data_converter,
114+
)
115+
116+
model_activity = ModelActivity(model_provider=None)
117+
worker = Worker(
118+
client,
119+
task_queue="my-task-queue",
120+
workflows=[HelloWorldAgent],
121+
activities=[model_activity.invoke_model_activity],
122+
)
123+
await worker.run()
124+
125+
if __name__ == "__main__":
126+
asyncio.run(worker_main())
127+
```
128+
129+
We wrap the entire `worker_main` function body in the `set_open_ai_agent_temporal_overrides()` context manager.
130+
This causes a Temporal activity to be invoked whenever the OpenAI Agents SDK invokes an LLM or calls a tool.
131+
We also pass the `open_ai_data_converter` to the Temporal Client, which ensures proper serialization of OpenAI Agents SDK data.
132+
We create a `ModelActivity` which serves as a generic wrapper for LLM calls, and we register this wrapper's invocation point, `model_activity.invoke_model_activity`, with the workflow.
133+
134+
In order to launch the agent, use the standard Temporal workflow invocation:
135+
136+
```python
137+
# File: run_hello_world_workflow.py
138+
139+
import asyncio
140+
141+
from temporalio.client import Client
142+
from temporalio.common import WorkflowIDReusePolicy
143+
from temporalio.contrib.openai_agents.open_ai_data_converter import open_ai_data_converter
144+
145+
from hello_world_workflow import HelloWorldAgent
146+
147+
async def main():
148+
# Create client connected to server at the given address
149+
client = await Client.connect(
150+
"localhost:7233",
151+
data_converter=open_ai_data_converter,
152+
)
153+
154+
# Execute a workflow
155+
result = await client.execute_workflow(
156+
HelloWorldAgent.run,
157+
"Tell me about recursion in programming.",
158+
id="my-workflow-id",
159+
task_queue="my-task-queue",
160+
id_reuse_policy=WorkflowIDReusePolicy.TERMINATE_IF_RUNNING,
161+
)
162+
print(f"Result: {result}")
163+
164+
if __name__ == "__main__":
165+
asyncio.run(main())
166+
```
167+
168+
This launcher script executes the Temporal workflow to start the agent.
169+
170+
Note that this basic example works without providing the `open_ai_data_converter` to the Temporal client that executes the workflow, but we include it because morem complex uses will generally need it.
171+
172+
173+
## Using Temporal Activities as OpenAI Agents Tools
174+
175+
One of the powerful features of this integration is the ability to convert Temporal activities into OpenAI Agents tools using `activity_as_tool`.
176+
This allows your agent to leverage Temporal's durable execution for tool calls.
177+
178+
In the example below, we apply the `@activity.defn` decorator to the `get_weather` function to create a Temporal activity.
179+
We then pass this through the `activity_as_tool` helper function to create an OpenAI Agents tool that is passed to the `Agent`.
180+
181+
```python
182+
from dataclasses import dataclass
183+
from datetime import timedelta
184+
from temporalio import activity, workflow
185+
from temporalio.contrib.openai_agents.temporal_tools import activity_as_tool
186+
187+
with workflow.unsafe.imports_passed_through():
188+
from agents import Agent, Runner
189+
190+
@dataclass
191+
class Weather:
192+
city: str
193+
temperature_range: str
194+
conditions: str
195+
196+
@activity.defn
197+
async def get_weather(city: str) -> Weather:
198+
"""Get the weather for a given city."""
199+
return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.")
200+
201+
@workflow.defn
202+
class WeatherAgent:
203+
@workflow.run
204+
async def run(self, question: str) -> str:
205+
agent = Agent(
206+
name="Weather Assistant",
207+
instructions="You are a helpful weather agent.",
208+
tools=[
209+
activity_as_tool(
210+
get_weather,
211+
start_to_close_timeout=timedelta(seconds=10)
212+
)
213+
],
214+
)
215+
result = await Runner.run(starting_agent=agent, input=question)
216+
return result.final_output
217+
```
218+
219+
220+
### Agent Handoffs
221+
222+
The OpenAI Agents SDK supports agent handoffs, where one agent can transfer control to another agent.
223+
In this example, one Temporal workflow wraps the entire multi-agent system:
224+
225+
```python
226+
@workflow.defn
227+
class CustomerServiceWorkflow:
228+
def __init__(self):
229+
self.current_agent = self.init_agents()
230+
231+
def init_agents(self):
232+
faq_agent = Agent(
233+
name="FAQ Agent",
234+
instructions="Answer frequently asked questions",
235+
)
236+
237+
booking_agent = Agent(
238+
name="Booking Agent",
239+
instructions="Help with booking and seat changes",
240+
)
241+
242+
triage_agent = Agent(
243+
name="Triage Agent",
244+
instructions="Route customers to the right agent",
245+
handoffs=[faq_agent, booking_agent],
246+
)
247+
248+
return triage_agent
249+
250+
@workflow.run
251+
async def run(self, customer_message: str) -> str:
252+
result = await Runner.run(
253+
starting_agent=self.current_agent,
254+
input=customer_message,
255+
context=self.context,
256+
)
257+
return result.final_output
258+
```
259+
260+
261+
## Additional Examples
262+
263+
You can find additional examples in the [Temporal Python Samples Repository](https://github.com/temporalio/samples-python/tree/main/openai_agents).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Support for using the OpenAI Agents SDK as part of Temporal workflows.
2+
3+
This module provides compatibility between the
4+
`OpenAI Agents SDK <https://github.com/openai/openai-agents-python>`_ and Temporal workflows.
5+
6+
.. warning::
7+
This module is experimental and may change in future versions.
8+
Use with caution in production environments.
9+
"""
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import asyncio
2+
from functools import wraps
3+
from typing import Any, Awaitable, Callable, TypeVar, cast
4+
5+
from temporalio import activity
6+
7+
F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
8+
9+
10+
def _auto_heartbeater(fn: F) -> F:
11+
# Propagate type hints from the original callable.
12+
@wraps(fn)
13+
async def wrapper(*args, **kwargs):
14+
heartbeat_timeout = activity.info().heartbeat_timeout
15+
heartbeat_task = None
16+
if heartbeat_timeout:
17+
# Heartbeat twice as often as the timeout
18+
heartbeat_task = asyncio.create_task(
19+
heartbeat_every(heartbeat_timeout.total_seconds() / 2)
20+
)
21+
try:
22+
return await fn(*args, **kwargs)
23+
finally:
24+
if heartbeat_task:
25+
heartbeat_task.cancel()
26+
# Wait for heartbeat cancellation to complete
27+
await heartbeat_task
28+
29+
return cast(F, wrapper)
30+
31+
32+
async def heartbeat_every(delay: float, *details: Any) -> None:
33+
"""Heartbeat every so often while not cancelled"""
34+
while True:
35+
await asyncio.sleep(delay)
36+
activity.heartbeat(*details)

0 commit comments

Comments
 (0)