diff --git a/docs/develop/python/integrations/strands-agents.mdx b/docs/develop/python/integrations/strands-agents.mdx index f4734daced..545875356b 100644 --- a/docs/develop/python/integrations/strands-agents.mdx +++ b/docs/develop/python/integrations/strands-agents.mdx @@ -73,9 +73,7 @@ Create a Workflow that holds a `TemporalAgent` and invokes it with a prompt. The maximum time each model call Activity can run: - [strands_plugin/hello_world/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/workflow.py) - ```py from datetime import timedelta @@ -92,8 +90,9 @@ class HelloWorldWorkflow: async def run(self, prompt: str) -> str: result = await self.agent.invoke_async(prompt) return str(result) -``` + +``` :::caution @@ -109,9 +108,7 @@ Create a Worker that registers the Workflow and the `StrandsPlugin`. The plugin that handle model calls: - [strands_plugin/hello_world/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_worker.py) - ```py import asyncio import os @@ -142,7 +139,6 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) ``` - **3. Run the Workflow** @@ -151,9 +147,7 @@ Start the Workflow from a separate client script. This example sends the prompt and prints the agent's response: - [strands_plugin/hello_world/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_workflow.py) - ```py import asyncio import os @@ -179,7 +173,6 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) ``` - ## Build the agent @@ -233,9 +226,7 @@ on the Worker, and pass them to the agent using `activity_as_tool`. Define an Activity for the tool: - [strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py) - ```py @activity.defn async def fetch_weather(city: str) -> dict: @@ -245,16 +236,15 @@ async def fetch_weather(city: str) -> dict: "temperature_f": 72, "conditions": "sunny", } -``` + +``` Pass the Activity to the agent in the Workflow using `activity_as_tool`: - [strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py) - ```py @workflow.defn class ToolsWorkflow: @@ -278,16 +268,15 @@ class ToolsWorkflow: async def run(self, prompt: str) -> str: result = await self.agent.invoke_async(prompt) return str(result) -``` + +``` Register the Activity functions on the Worker: - [strands_plugin/tools/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/run_worker.py) - ```py import asyncio import os @@ -323,7 +312,6 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) ``` - If you are using built-in `strands_tools`, wrap them in a thin async function decorated with `@activity.defn` so they @@ -343,22 +331,19 @@ Temporal Activity. The following example shows both patterns in one `HookProvide Workflow context (deterministic), while `persist_tool_call` runs as an Activity (I/O-safe): - [strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py) - ```py @activity.defn async def persist_tool_call(tool_name: str) -> None: # In production, write to a database / S3 / your audit pipeline. activity.logger.info(f"audit: tool {tool_name} completed") -``` + +``` - [strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py) - ```py class AuditHook(HookProvider): def __init__(self) -> None: @@ -377,8 +362,9 @@ class AuditHook(HookProvider): def _record(self, event: AfterToolCallEvent) -> None: self.fired.append(event.tool_use["name"]) -``` + +``` :::caution @@ -405,9 +391,7 @@ plugin registers a per-server Activity and connects at Worker startup to enumera Define the Workflow with a `TemporalMCPClient`: - [strands_plugin/mcp/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/workflow.py) - ```py from datetime import timedelta @@ -431,16 +415,15 @@ class MCPWorkflow: async def run(self, prompt: str) -> str: result = await self.agent.invoke_async(prompt) return str(result) -``` + +``` Register the MCP client factory on the Worker: - [strands_plugin/mcp/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/run_worker.py) - ```py # ... from mcp import StdioServerParameters, stdio_client @@ -474,7 +457,6 @@ async def main() -> None: print("Worker started. Ctrl+C to exit.") await worker.run() ``` - Each factory returns a fully configured `MCPClient`, so you can pass options like `tool_filters`, `prefix`, @@ -509,9 +491,7 @@ A hook on an interruptible event such as `BeforeToolCallEvent` can pause the age Define the approval hook: - [strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py) - ```py class ApprovalHook(HookProvider): def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None: @@ -526,16 +506,15 @@ class ApprovalHook(HookProvider): ) if approval != "approve": event.cancel_tool = "denied" -``` + +``` The Workflow waits for a Signal carrying the approval response, then resumes the agent: - [strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py) - ```py @workflow.defn class HumanInTheLoopWorkflow: @@ -572,8 +551,9 @@ class HumanInTheLoopWorkflow: ] result = await self.agent.invoke_async(responses) return str(result) -``` + +``` #### Interrupt from a tool @@ -599,9 +579,7 @@ The same approach works from an `activity_as_tool`-wrapped Activity. The plugin' Define the Activity that raises the interrupt: - [strands_plugin/activity_interrupt/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/workflow.py) - ```py @activity.defn async def delete_thing(name: str) -> str: @@ -615,8 +593,9 @@ async def delete_thing(name: str) -> str: ) ) return f"deleted {name}" -``` + +``` :::caution @@ -629,9 +608,7 @@ Attach `StrandsPlugin` to the **client** (not just the Worker) for Activity-tool Workers built from that client pick up the plugin automatically: - [strands_plugin/activity_interrupt/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/run_worker.py) - ```py import asyncio import os @@ -667,7 +644,6 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) ``` - ### Return structured data from an agent @@ -677,9 +653,7 @@ The plugin defaults to the [`pydantic_data_converter`](/develop/python/data-hand serialize cleanly across the Activity and Workflow boundary: - [strands_plugin/structured_output/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/structured_output/workflow.py) - ```py from datetime import timedelta @@ -707,8 +681,9 @@ class StructuredOutputWorkflow: result = await self.agent.invoke_async(prompt) assert isinstance(result.structured_output, PersonInfo) return result.structured_output -``` + +``` ### Stream agent output to clients @@ -723,9 +698,7 @@ published from inside the model Activity. Subscribers read events through `Workf Define the Workflow with a `WorkflowStream` and a streaming topic: - [strands_plugin/streaming/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/workflow.py) - ```py from datetime import timedelta @@ -747,16 +720,15 @@ class StreamingWorkflow: async def run(self, prompt: str) -> str: result = await self.agent.invoke_async(prompt) return str(result) -``` + +``` Subscribe to the stream from a client: - [strands_plugin/streaming/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/run_workflow.py) - ```py import asyncio import os @@ -806,7 +778,6 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) ``` - ## Run in production @@ -845,9 +816,7 @@ the chat ends or Temporal suggests continue-as-new. When it does, the Workflow d fresh execution with the agent's accumulated messages: - [strands_plugin/continue_as_new/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/continue_as_new/workflow.py) - ```py import asyncio from dataclasses import dataclass, field @@ -901,8 +870,9 @@ class ChatWorkflow: if not self._done: workflow.continue_as_new(ChatInput(messages=self._agent.messages)) -``` + +``` ### Add tracing with OpenTelemetry