Skip to content

Commit af3cf48

Browse files
feat: add cookbook (#3870)
* feat: add cookbook * chore: adjust typescript config * add Github source button functionality * style the tiles * adjust spacing * add existing recipies * add source * fix broken link issue * fix broken links issue * add source for retry example * adjust box shadow * chore: add e2e tests * add date and change tab behavior * add orderng mechanism * rename to ai cookbook * add cookbook recipes * broken link fix * adjust tile order * docs: add source for recipes * Update AI Cookbook heading and capitalize Workflow * change title * changed heading levels, added you in the description, changed date stamp contrast ratio, moved breadcrumb above h1 --------- Co-authored-by: angelazhou32 <[email protected]>
1 parent 493f15e commit af3cf48

26 files changed

+3459
-190
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ package-lock.json
3434

3535
# Env
3636
.env
37-
/assembly/.env
37+
/assembly/.env
38+
39+
# Tests
40+
test-results/*

cookbook/basic-python.mdx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)