55
66PATTERN 1: Simple External Tools as Activities (activity_as_tool)
77- Convert individual Temporal activities directly into agent tools
8- - 1:1 mapping between tool calls and activities
8+ - 1:1 mapping between tool calls and activities
99- Best for: single non-deterministic operations (API calls, DB queries)
1010- Example: get_weather activity → weather tool
1111
1919
2020WHY THIS APPROACH IS GAME-CHANGING:
2121===================================
22- There's a crucial meta-point that should be coming through here: **why is this different?**
23- This approach is truly transactional because of how the `await` works in Temporal workflows.
24- Consider a "move money" example - if the operation fails between the withdraw and deposit,
25- Temporal will resume exactly where it left off - the agent gets real-world flexibility even
22+ There's a crucial meta-point that should be coming through here: **why is this different?**
23+ This approach is truly transactional because of how the `await` works in Temporal workflows.
24+ Consider a "move money" example - if the operation fails between the withdraw and deposit,
25+ Temporal will resume exactly where it left off - the agent gets real-world flexibility even
2626if systems die.
2727
28- **Why even use Temporal? Why are we adding complexity?** The gain is enormous when you
28+ **Why even use Temporal? Why are we adding complexity?** The gain is enormous when you
2929consider what happens without it:
3030
31- In a traditional approach without Temporal, if you withdraw money but then the system crashes
32- before depositing, you're stuck in a broken state. The money has been withdrawn, but never
33- deposited. In a banking scenario, you can't just "withdraw again" - the money is already gone
31+ In a traditional approach without Temporal, if you withdraw money but then the system crashes
32+ before depositing, you're stuck in a broken state. The money has been withdrawn, but never
33+ deposited. In a banking scenario, you can't just "withdraw again" - the money is already gone
3434from the source account, and your agent has no way to recover or know what state it was in.
3535
36- This is why you can't build very complicated agents without this confidence in transactional
36+ This is why you can't build very complicated agents without this confidence in transactional
3737behavior. Temporal gives us:
3838
3939- **Guaranteed execution**: If the workflow starts, it will complete, even through failures
4040- **Exact resumption**: Pick up exactly where we left off, not start over
41- - **Transactional integrity**: Either both operations complete, or the workflow can be designed
41+ - **Transactional integrity**: Either both operations complete, or the workflow can be designed
4242 to handle partial completion
4343- **Production reliability**: Build agents that can handle real-world complexity and failures
4444
45- Without this foundation, agents remain fragile toys. With Temporal, they become production-ready
45+ Without this foundation, agents remain fragile toys. With Temporal, they become production-ready
4646systems that can handle the complexities of the real world.
4747"""
4848
7272
7373logger = make_logger (__name__ )
7474
75+
7576@workflow .defn (name = environment_variables .WORKFLOW_NAME )
7677class ExampleTutorialWorkflow (BaseWorkflow ):
7778 """
7879 Minimal async workflow template for AgentEx Temporal agents.
7980 """
81+
8082 def __init__ (self ):
8183 super ().__init__ (display_name = environment_variables .AGENT_NAME )
8284 self ._complete_task = False
@@ -85,35 +87,35 @@ def __init__(self):
8587 @workflow .signal (name = SignalName .RECEIVE_EVENT )
8688 async def on_task_event_send (self , params : SendEventParams ) -> None :
8789 logger .info (f"Received task message instruction: { params } " )
88-
89- # Echo back the client's message to show it in the UI. This is not done by default
90+
91+ # Echo back the client's message to show it in the UI. This is not done by default
9092 # so the agent developer has full control over what is shown to the user.
9193 await adk .messages .create (task_id = params .task .id , content = params .event .content )
9294
9395 # ============================================================================
9496 # OpenAI Agents SDK + Temporal Integration: Two Patterns for Tool Creation
9597 # ============================================================================
96-
98+
9799 # #### When to Use Activities for Tools
98100 #
99101 # You'll want to use the activity pattern for tools in the following scenarios:
100102 #
101- # - **API calls within the tool**: Whenever your tool makes an API call (external
102- # service, database, etc.), you must wrap it as an activity since these are
103+ # - **API calls within the tool**: Whenever your tool makes an API call (external
104+ # service, database, etc.), you must wrap it as an activity since these are
103105 # non-deterministic operations that could fail or return different results
104- # - **Idempotent single operations**: When the tool performs an already idempotent
105- # single call that you want to ensure gets executed reliably with Temporal's retry
106+ # - **Idempotent single operations**: When the tool performs an already idempotent
107+ # single call that you want to ensure gets executed reliably with Temporal's retry
106108 # guarantees
107109 #
108- # Let's start with the case where it is non-deterministic. If this is the case, we
109- # want this tool to be an activity to guarantee that it will be executed. The way to
110- # do this is to add some syntax to make the tool call an activity. Let's create a tool
111- # that gives us the weather and create a weather agent. For this example, we will just
112- # return a hard-coded string but we can easily imagine this being an API call to a
113- # weather service which would make it non-deterministic. First we will create a new
114- # file called `activities.py`. Here we will create a function to get the weather and
110+ # Let's start with the case where it is non-deterministic. If this is the case, we
111+ # want this tool to be an activity to guarantee that it will be executed. The way to
112+ # do this is to add some syntax to make the tool call an activity. Let's create a tool
113+ # that gives us the weather and create a weather agent. For this example, we will just
114+ # return a hard-coded string but we can easily imagine this being an API call to a
115+ # weather service which would make it non-deterministic. First we will create a new
116+ # file called `activities.py`. Here we will create a function to get the weather and
115117 # simply add an activity annotation on top.
116-
118+
117119 # There are TWO key patterns for integrating tools with the OpenAI Agents SDK in Temporal:
118120 #
119121 # PATTERN 1: Simple External Tools as Activities
@@ -147,7 +149,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
147149 # The get_weather activity will be executed with durability guarantees
148150 activity_as_tool (
149151 get_weather , # This is defined in activities.py as @activity.defn
150- start_to_close_timeout = timedelta (seconds = 10 )
152+ start_to_close_timeout = timedelta (seconds = 10 ),
151153 ),
152154 ],
153155 )
@@ -156,7 +158,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
156158 result = await Runner .run (weather_agent , params .event .content .content )
157159
158160 # ============================================================================
159- # PATTERN 2: Multiple Activities Within Tools
161+ # PATTERN 2: Multiple Activities Within Tools
160162 # ============================================================================
161163 # Use this pattern when:
162164 # - You need multiple sequential non-deterministic operations within one tool
@@ -171,7 +173,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
171173 #
172174 # BENEFITS:
173175 # - Guaranteed execution order (withdraw THEN deposit)
174- # - Each step is durable and retryable individually
176+ # - Each step is durable and retryable individually
175177 # - Atomic operations from the agent's perspective
176178 # - Better than having LLM make multiple separate tool calls
177179
@@ -186,7 +188,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
186188 # move_money,
187189 # ],
188190 # )
189-
191+
190192 # # Run the agent - when it calls move_money tool, it will create TWO activities:
191193 # # 1. withdraw_money activity
192194 # # 2. deposit_money activity (only after withdraw succeeds)
@@ -195,17 +197,17 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
195197 # ============================================================================
196198 # PATTERN COMPARISON SUMMARY:
197199 # ============================================================================
198- #
200+ #
199201 # Pattern 1 (activity_as_tool): | Pattern 2 (function_tool with activities):
200202 # - Single activity per tool call | - Multiple activities per tool call
201- # - 1:1 tool to activity mapping | - 1:many tool to activity mapping
203+ # - 1:1 tool to activity mapping | - 1:many tool to activity mapping
202204 # - Simple non-deterministic ops | - Complex multi-step operations
203205 # - Let LLM sequence multiple tools | - Code controls activity sequencing
204206 # - Example: get_weather, db_lookup | - Example: money_transfer, multi_step_workflow
205207 #
206208 # BOTH patterns provide:
207209 # - Automatic retries and failure recovery
208- # - Full observability in Temporal UI
210+ # - Full observability in Temporal UI
209211 # - Durable execution guarantees
210212 # - Seamless integration with OpenAI Agents SDK
211213 # ============================================================================
@@ -234,11 +236,12 @@ async def on_task_create(self, params: CreateTaskParams) -> str:
234236
235237 await workflow .wait_condition (
236238 lambda : self ._complete_task ,
237- timeout = None , # Set a timeout if you want to prevent the task from running indefinitely. Generally this is not needed. Temporal can run hundreds of millions of workflows in parallel and more. Only do this if you have a specific reason to do so.
239+ timeout = None , # Set a timeout if you want to prevent the task from running indefinitely. Generally this is not needed. Temporal can run hundreds of millions of workflows in parallel and more. Only do this if you have a specific reason to do so.
238240 )
239241 return "Task completed"
240242
241243 @workflow .signal
242244 async def fulfill_order_signal (self , success : bool ) -> None :
243245 if success == True :
244- await self ._pending_confirmation .put (True )
246+ await self ._pending_confirmation .put (True )
247+
0 commit comments