|
| 1 | +--- |
| 2 | +title: Hello World |
| 3 | +description: Simple example demonstrating how to call an LLM from Temporal using the OpenAI Python API library. |
| 4 | +tags: [foundations, openai, python] |
| 5 | +source: https://github.com/temporalio/ai-cookbook/tree/main/foundations/hello_world_openai_responses_python |
| 6 | +priority: 999 |
| 7 | +--- |
| 8 | + |
| 9 | +This is a simple example showing how to call an LLM from Temporal using the [OpenAI Python API library](https://github.com/openai/openai-python). |
| 10 | + |
| 11 | +Being an external API call, the LLM invocation happens in a Temporal Activity. |
| 12 | + |
| 13 | +This recipe highlights two key design decisions: |
| 14 | + |
| 15 | +- A generic activity for invoking an LLM API. This activity can be re-used with different arguments throughout your codebase. |
| 16 | +- Configuring the Temporal client with a `dataconverter` to allow serialization of Pydantic types. |
| 17 | +- Retries are handled by Temporal and not by the underlying libraries such as the OpenAI client. This is important because if you leave the client retires on they can interfere with correct and durable error handling and recovery. |
| 18 | + |
| 19 | + |
| 20 | +## Create the Activity |
| 21 | + |
| 22 | +We create wrapper for the `create` method of the `AsyncOpenAI` client object. |
| 23 | +This is a generic activity that invokes the OpenAI LLM. |
| 24 | + |
| 25 | +We set `max_retries=0` on when creating the `AsyncOpenAI` client. |
| 26 | +This moves the responsibility for retries from the OpenAI client to Temporal. |
| 27 | + |
| 28 | +In this implementation, we include only the `instructions` and `input` argument, but it could be extended to others. |
| 29 | + |
| 30 | +*File: activities/openai_responses.py* |
| 31 | +```python |
| 32 | + |
| 33 | +from temporalio import activity |
| 34 | +from openai import AsyncOpenAI |
| 35 | +from openai.types.responses import Response |
| 36 | +from dataclasses import dataclass |
| 37 | + |
| 38 | +# Temporal best practice: Create a data structure to hold the request parameters. |
| 39 | +@dataclass |
| 40 | +class OpenAIResponsesRequest: |
| 41 | + model: str |
| 42 | + instructions: str |
| 43 | + input: str |
| 44 | + |
| 45 | +@activity.defn |
| 46 | +async def create(request: OpenAIResponsesRequest) -> Response: |
| 47 | + # Temporal best practice: Disable retry logic in OpenAI API client library. |
| 48 | + client = AsyncOpenAI(max_retries=0) |
| 49 | + |
| 50 | + resp = await client.responses.create( |
| 51 | + model=request.model, |
| 52 | + instructions=request.instructions, |
| 53 | + input=request.input, |
| 54 | + timeout=15, |
| 55 | + ) |
| 56 | + |
| 57 | + return resp |
| 58 | +``` |
| 59 | + |
| 60 | +## Create the Workflow |
| 61 | + |
| 62 | +In this example, we take the user input and generate a response in haiku format, using the OpenAI Responses activity. The |
| 63 | +Workflow returns `result.output_text` from the OpenAI `Response`. |
| 64 | + |
| 65 | +As per usual, the activity retry configuration is set here in the Workflow. In this case, a retry policy is not specified |
| 66 | +so the default retry policy is used (exponential backoff with 1s initial interval, 2.0 backoff coefficient, max interval |
| 67 | +100× initial, unlimited attempts, no non-retryable errors). |
| 68 | + |
| 69 | +*File: workflows/hello_world_workflow.py* |
| 70 | +```python |
| 71 | +from temporalio import workflow |
| 72 | +from datetime import timedelta |
| 73 | + |
| 74 | +from activities import openai_responses |
| 75 | + |
| 76 | + |
| 77 | +@workflow.defn |
| 78 | +class HelloWorld: |
| 79 | + @workflow.run |
| 80 | + async def run(self, input: str) -> str: |
| 81 | + system_instructions = "You only respond in haikus." |
| 82 | + result = await workflow.execute_activity( |
| 83 | + openai_responses.create, |
| 84 | + openai_responses.OpenAIResponsesRequest( |
| 85 | + model="gpt-4o-mini", |
| 86 | + instructions=system_instructions, |
| 87 | + input=input, |
| 88 | + ), |
| 89 | + start_to_close_timeout=timedelta(seconds=30), |
| 90 | + ) |
| 91 | + return result.output_text |
| 92 | +``` |
| 93 | + |
| 94 | +## Create the Worker |
| 95 | + |
| 96 | +Create the process for executing Activities and Workflows. |
| 97 | +We configure the Temporal client with `pydantic_data_converter` so Temporal can serialize/deserialize output of the OpenAI SDK. |
| 98 | + |
| 99 | +*File: worker.py* |
| 100 | +```python |
| 101 | +import asyncio |
| 102 | + |
| 103 | +from temporalio.client import Client |
| 104 | +from temporalio.worker import Worker |
| 105 | + |
| 106 | +from workflows.hello_world_workflow import HelloWorld |
| 107 | +from activities import openai_responses |
| 108 | +from temporalio.contrib.pydantic import pydantic_data_converter |
| 109 | + |
| 110 | + |
| 111 | +async def main(): |
| 112 | + client = await Client.connect( |
| 113 | + "localhost:7233", |
| 114 | + data_converter=pydantic_data_converter, |
| 115 | + ) |
| 116 | + |
| 117 | + worker = Worker( |
| 118 | + client, |
| 119 | + task_queue="hello-world-python-task-queue", |
| 120 | + workflows=[ |
| 121 | + HelloWorld, |
| 122 | + ], |
| 123 | + activities=[ |
| 124 | + openai_responses.create, |
| 125 | + ], |
| 126 | + ) |
| 127 | + await worker.run() |
| 128 | + |
| 129 | + |
| 130 | +if __name__ == "__main__": |
| 131 | + asyncio.run(main()) |
| 132 | +``` |
| 133 | + |
| 134 | +## Create the Workflow Starter |
| 135 | + |
| 136 | +The starter script submits the workflow to Temporal for execution, then waits for the result and prints it out. |
| 137 | +It uses the `pydantic_data_converter` to match the Worker configuration. |
| 138 | + |
| 139 | +*File: start_workflow.py* |
| 140 | +```python |
| 141 | +import asyncio |
| 142 | + |
| 143 | +from temporalio.client import Client |
| 144 | + |
| 145 | +from workflows.hello_world_workflow import HelloWorld |
| 146 | +from temporalio.contrib.pydantic import pydantic_data_converter |
| 147 | + |
| 148 | + |
| 149 | +async def main(): |
| 150 | + client = await Client.connect( |
| 151 | + "localhost:7233", |
| 152 | + data_converter=pydantic_data_converter, |
| 153 | + ) |
| 154 | + |
| 155 | + # Submit the Hello World workflow for execution |
| 156 | + result = await client.execute_workflow( |
| 157 | + HelloWorld.run, |
| 158 | + "Tell me about recursion in programming.", |
| 159 | + id="my-workflow-id", |
| 160 | + task_queue="hello-world-python-task-queue", |
| 161 | + ) |
| 162 | + print(f"Result: {result}") |
| 163 | + |
| 164 | + |
| 165 | +if __name__ == "__main__": |
| 166 | + asyncio.run(main()) |
| 167 | + |
| 168 | +``` |
| 169 | + |
| 170 | +## Running |
| 171 | + |
| 172 | +Start the Temporal Dev Server: |
| 173 | + |
| 174 | +```bash |
| 175 | +temporal server start-dev |
| 176 | +``` |
| 177 | + |
| 178 | +Run the worker: |
| 179 | + |
| 180 | +```bash |
| 181 | +uv run python -m worker |
| 182 | +``` |
| 183 | + |
| 184 | +Start execution: |
| 185 | + |
| 186 | +```bash |
| 187 | +uv run python -m start_workflow |
| 188 | +``` |
0 commit comments