Skip to content

Commit ff51013

Browse files
authored
Concepts docs (#51)
1 parent d5e3bda commit ff51013

17 files changed

+523
-44
lines changed

docs/api/messages.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
# `pydantic_ai.messages`
22

33
::: pydantic_ai.messages
4+
options:
5+
members:
6+
- Message
7+
- SystemPrompt
8+
- UserPrompt
9+
- ToolReturn
10+
- RetryPrompt
11+
- ModelAnyResponse
12+
- ModelTextResponse
13+
- ModelStructuredResponse
14+
- ToolCall
15+
- ArgsJson
16+
- ArgsObject
17+
- MessagesTypeAdapter

docs/concepts/dependencies.md

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# Dependencies
2+
3+
PydanticAI uses a dependency injection system to provide data and services to your agent's [system prompts](system-prompt.md), [retrievers](retrievers.md) and [result validators](result-validation.md#TODO).
4+
5+
Matching PydanticAI's design philosophy, our dependency system tries to use existing best practice in Python development rather than inventing esoteric "magic", this should make dependencies type-safe, understandable easier to test and ultimately easier to deploy in production.
6+
7+
## Defining Dependencies
8+
9+
Dependencies can be any python type. While in simple cases you might be able to pass a single object
10+
as a dependency (e.g. an HTTP connection), [dataclasses][] are generally a convenient container when your dependencies included multiple objects.
11+
12+
Here's an example of defining an agent that requires dependencies.
13+
14+
(**Note:** dependencies aren't actually used in this example, see [Accessing Dependencies](#accessing-dependencies) below)
15+
16+
```python title="unused_dependencies.py"
17+
from dataclasses import dataclass
18+
19+
import httpx
20+
21+
from pydantic_ai import Agent
22+
23+
24+
@dataclass
25+
class MyDeps: # (1)!
26+
api_key: str
27+
http_client: httpx.AsyncClient
28+
29+
30+
agent = Agent(
31+
'openai:gpt-4o',
32+
deps_type=MyDeps, # (2)!
33+
)
34+
35+
36+
async def main():
37+
async with httpx.AsyncClient() as client:
38+
deps = MyDeps('foobar', client)
39+
result = await agent.run(
40+
'Tell me a joke.',
41+
deps=deps, # (3)!
42+
)
43+
print(result.data)
44+
```
45+
46+
1. Define a dataclass to hold dependencies.
47+
2. Pass the dataclass type to the `deps_type` argument of the [`Agent` constructor][pydantic_ai.Agent.__init__]. **Note**: we're passing the type here, NOT an instance, this parameter is not actually used at runtime, it's here so we can get full type checking of the agent.
48+
3. When running the agent, pass an instance of the dataclass to the `deps` parameter.
49+
50+
_(This example is complete, it can be run "as is" inside an async context)_
51+
52+
## Accessing Dependencies
53+
54+
Dependencies are accessed through the [`CallContext`][pydantic_ai.dependencies.CallContext] type, this should be the first parameter of system prompt functions etc.
55+
56+
57+
```python title="system_prompt_dependencies.py" hl_lines="20-27"
58+
from dataclasses import dataclass
59+
60+
import httpx
61+
62+
from pydantic_ai import Agent, CallContext
63+
64+
65+
@dataclass
66+
class MyDeps:
67+
api_key: str
68+
http_client: httpx.AsyncClient
69+
70+
71+
agent = Agent(
72+
'openai:gpt-4o',
73+
deps_type=MyDeps,
74+
)
75+
76+
77+
@agent.system_prompt # (1)!
78+
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str: # (2)!
79+
response = await ctx.deps.http_client.get( # (3)!
80+
'https://example.com',
81+
headers={'Authorization': f'Bearer {ctx.deps.api_key}'} # (4)!
82+
)
83+
response.raise_for_status()
84+
return f'Prompt: {response.text}'
85+
86+
87+
async def main():
88+
async with httpx.AsyncClient() as client:
89+
deps = MyDeps('foobar', client)
90+
result = await agent.run('Tell me a joke.', deps=deps)
91+
print(result.data)
92+
```
93+
94+
1. [`CallContext`][pydantic_ai.dependencies.CallContext] may optionally be passed to a [`system_prompt`][pydantic_ai.Agent.system_prompt] function as the only argument.
95+
2. [`CallContext`][pydantic_ai.dependencies.CallContext] is parameterized with the type of the dependencies, if this type is incorrect, static type checkers will raise an error.
96+
3. Access dependencies through the [`.deps`][pydantic_ai.dependencies.CallContext.deps] attribute.
97+
4. Access dependencies through the [`.deps`][pydantic_ai.dependencies.CallContext.deps] attribute.
98+
99+
_(This example is complete, it can be run "as is" inside an async context)_
100+
101+
### Asynchronous vs. Synchronous dependencies
102+
103+
System prompt functions, retriever functions and result validator are all run in the async context of an agent run.
104+
105+
If these functions are not coroutines (e.g. `async def`) they are called with
106+
[`run_in_executor`][asyncio.loop.run_in_executor] in a thread pool, it's therefore marginally preferable
107+
to use `async` methods where dependencies perform IO, although synchronous dependencies should work fine too.
108+
109+
!!! note "`run` vs. `run_sync` and Asynchronous vs. Synchronous dependencies"
110+
Whether you use synchronous or asynchronous dependencies, is completely independent of whether you use `run` or `run_sync``run_sync` is just a wrapper around `run` and agents are always run in an async context.
111+
112+
Here's the same example as above, but with a synchronous dependency:
113+
114+
```python title="sync_dependencies.py"
115+
from dataclasses import dataclass
116+
117+
import httpx
118+
119+
from pydantic_ai import Agent, CallContext
120+
121+
122+
@dataclass
123+
class MyDeps:
124+
api_key: str
125+
http_client: httpx.Client # (1)!
126+
127+
128+
agent = Agent(
129+
'openai:gpt-4o',
130+
deps_type=MyDeps,
131+
)
132+
133+
134+
@agent.system_prompt
135+
def get_system_prompt(ctx: CallContext[MyDeps]) -> str: # (2)!
136+
response = ctx.deps.http_client.get(
137+
'https://example.com',
138+
headers={'Authorization': f'Bearer {ctx.deps.api_key}'}
139+
)
140+
response.raise_for_status()
141+
return f'Prompt: {response.text}'
142+
143+
144+
async def main():
145+
deps = MyDeps('foobar', httpx.Client())
146+
result = await agent.run(
147+
'Tell me a joke.',
148+
deps=deps,
149+
)
150+
print(result.data)
151+
```
152+
153+
1. Here we use a synchronous `httpx.Client` instead of an asynchronous `httpx.AsyncClient`.
154+
2. To match the synchronous dependency, the system prompt function is now a plain function, not a coroutine.
155+
156+
_(This example is complete, it can be run "as is")_
157+
158+
## Full Example
159+
160+
As well as system prompts, dependencies can be used in [retrievers](retrievers.md) and [result validators](result-validation.md#TODO).
161+
162+
```python title="full_example.py" hl_lines="27-35 38-48"
163+
from dataclasses import dataclass
164+
165+
import httpx
166+
167+
from pydantic_ai import Agent, CallContext, ModelRetry
168+
169+
170+
@dataclass
171+
class MyDeps:
172+
api_key: str
173+
http_client: httpx.AsyncClient
174+
175+
176+
agent = Agent(
177+
'openai:gpt-4o',
178+
deps_type=MyDeps,
179+
)
180+
181+
182+
@agent.system_prompt
183+
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str:
184+
response = await ctx.deps.http_client.get('https://example.com')
185+
response.raise_for_status()
186+
return f'Prompt: {response.text}'
187+
188+
189+
@agent.retriever_context # (1)!
190+
async def get_joke_material(ctx: CallContext[MyDeps], subject: str) -> str:
191+
response = await ctx.deps.http_client.get(
192+
'https://example.com#jokes',
193+
params={'subject': subject},
194+
headers={'Authorization': f'Bearer {ctx.deps.api_key}'},
195+
)
196+
response.raise_for_status()
197+
return response.text
198+
199+
200+
@agent.result_validator # (2)!
201+
async def validate_result(ctx: CallContext[MyDeps], final_response: str) -> str:
202+
response = await ctx.deps.http_client.post(
203+
'https://example.com#validate',
204+
headers={'Authorization': f'Bearer {ctx.deps.api_key}'},
205+
params={'query': final_response},
206+
)
207+
if response.status_code == 400:
208+
raise ModelRetry(f'invalid response: {response.text}')
209+
response.raise_for_status()
210+
return final_response
211+
212+
213+
async def main():
214+
async with httpx.AsyncClient() as client:
215+
deps = MyDeps('foobar', client)
216+
result = await agent.run('Tell me a joke.', deps=deps)
217+
print(result.data)
218+
```
219+
220+
1. To pass `CallContext` and to a retriever, us the [`retriever_context`][pydantic_ai.Agent.retriever_context] decorator.
221+
2. `CallContext` may optionally be passed to a [`result_validator`][pydantic_ai.Agent.result_validator] function as the first argument.
222+
223+
## Overriding Dependencies
224+
225+
When testing agents, it's useful to be able to customise dependencies.
226+
227+
While this can sometimes be done by calling the agent directly within unit tests, we can also override dependencies
228+
while calling application code which in turn calls the agent.
229+
230+
This is done via the [`override_deps`][pydantic_ai.Agent.override_deps] method on the agent.
231+
232+
```py title="joke_app.py"
233+
from dataclasses import dataclass
234+
235+
import httpx
236+
237+
from pydantic_ai import Agent, CallContext
238+
239+
240+
@dataclass
241+
class MyDeps:
242+
api_key: str
243+
http_client: httpx.AsyncClient
244+
245+
async def system_prompt_factory(self) -> str: # (1)!
246+
response = await self.http_client.get('https://example.com')
247+
response.raise_for_status()
248+
return f'Prompt: {response.text}'
249+
250+
251+
joke_agent = Agent('openai:gpt-4o', deps_type=MyDeps)
252+
253+
254+
@joke_agent.system_prompt
255+
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str:
256+
return await ctx.deps.system_prompt_factory() # (2)!
257+
258+
259+
async def application_code(prompt: str) -> str: # (3)!
260+
...
261+
...
262+
# now deep within application code we call our agent
263+
async with httpx.AsyncClient() as client:
264+
app_deps = MyDeps('foobar', client)
265+
result = await joke_agent.run(prompt, deps=app_deps) # (4)!
266+
return result.data
267+
268+
```
269+
270+
1. Define a method on the dependency to make the system prompt easier to customise.
271+
2. Call the system prompt factory from within the system prompt function.
272+
3. Application code that calls the agent, in a real application this might be an API endpoint.
273+
4. Call the agent from within the application code, in a real application this call might be deep within a call stack. Note `app_deps` here will NOT be used when deps are overridden.
274+
275+
```py title="test_joke_app.py" hl_lines="10-12"
276+
from joke_app import application_code, joke_agent, MyDeps
277+
278+
279+
class TestMyDeps(MyDeps): # (1)!
280+
async def system_prompt_factory(self) -> str:
281+
return 'test prompt'
282+
283+
284+
async def test_application_code():
285+
test_deps = TestMyDeps('test_key', None) # (2)!
286+
with joke_agent.override_deps(test_deps): # (3)!
287+
joke = application_code('Tell me a joke.') # (4)!
288+
assert joke == 'funny'
289+
```
290+
291+
1. Define a subclass of `MyDeps` in tests to customise the system prompt factory.
292+
2. Create an instance of the test dependency, we don't need to pass an `http_client` here as it's not used.
293+
3. Override the dependencies of the agent for the duration of the `with` block, `test_deps` will be used when the agent is run.
294+
4. Now we can safely call our application code, the agent will use the overridden dependencies.
295+
296+
## Agents as dependencies of other Agents
297+
298+
Since dependencies can be any python type, and agents are just python objects, agents can be dependencies of other agents.
299+
300+
```py title="agents_as_dependencies.py"
301+
from dataclasses import dataclass
302+
303+
from pydantic_ai import Agent, CallContext
304+
305+
306+
@dataclass
307+
class MyDeps:
308+
factory_agent: Agent[None, list[str]]
309+
310+
311+
joke_agent = Agent(
312+
'openai:gpt-4o',
313+
deps_type=MyDeps,
314+
system_prompt=(
315+
'Use the "joke_factory" to generate some jokes, then choose the best. '
316+
'You must return just a single joke.'
317+
)
318+
)
319+
320+
factory_agent = Agent('gemini-1.5-pro', result_type=list[str])
321+
322+
323+
@joke_agent.retriever_context
324+
async def joke_factory(ctx: CallContext[MyDeps], count: int) -> str:
325+
r = await ctx.deps.factory_agent.run(f'Please generate {count} jokes.')
326+
return '\n'.join(r.data)
327+
328+
329+
result = joke_agent.run_sync('Tell me a joke.', deps=MyDeps(factory_agent))
330+
print(result.data)
331+
```
332+
333+
## Examples
334+
335+
The following examples demonstrate how to use dependencies in PydanticAI:
336+
337+
- [Weather Agent](../examples/weather-agent.md)
338+
- [SQL Generation](../examples/sql-gen.md)
339+
- [RAG](../examples/rag.md)

0 commit comments

Comments
 (0)