-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Concepts docs #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Concepts docs #51
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,17 @@ | ||
# `pydantic_ai.messages` | ||
|
||
::: pydantic_ai.messages | ||
options: | ||
members: | ||
- Message | ||
- SystemPrompt | ||
- UserPrompt | ||
- ToolReturn | ||
- RetryPrompt | ||
- ModelAnyResponse | ||
- ModelTextResponse | ||
- ModelStructuredResponse | ||
- ToolCall | ||
- ArgsJson | ||
- ArgsObject | ||
- MessagesTypeAdapter |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,339 @@ | ||
# Dependencies | ||
|
||
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). | ||
|
||
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. | ||
|
||
## Defining Dependencies | ||
|
||
Dependencies can be any python type. While in simple cases you might be able to pass a single object | ||
as a dependency (e.g. an HTTP connection), [dataclasses][] are generally a convenient container when your dependencies included multiple objects. | ||
|
||
Here's an example of defining an agent that requires dependencies. | ||
|
||
(**Note:** dependencies aren't actually used in this example, see [Accessing Dependencies](#accessing-dependencies) below) | ||
|
||
```python title="unused_dependencies.py" | ||
from dataclasses import dataclass | ||
|
||
import httpx | ||
|
||
from pydantic_ai import Agent | ||
|
||
|
||
@dataclass | ||
class MyDeps: # (1)! | ||
api_key: str | ||
http_client: httpx.AsyncClient | ||
|
||
|
||
agent = Agent( | ||
'openai:gpt-4o', | ||
deps_type=MyDeps, # (2)! | ||
) | ||
|
||
|
||
async def main(): | ||
async with httpx.AsyncClient() as client: | ||
deps = MyDeps('foobar', client) | ||
result = await agent.run( | ||
'Tell me a joke.', | ||
deps=deps, # (3)! | ||
) | ||
print(result.data) | ||
``` | ||
|
||
1. Define a dataclass to hold dependencies. | ||
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. | ||
3. When running the agent, pass an instance of the dataclass to the `deps` parameter. | ||
|
||
_(This example is complete, it can be run "as is" inside an async context)_ | ||
|
||
## Accessing Dependencies | ||
|
||
Dependencies are accessed through the [`CallContext`][pydantic_ai.dependencies.CallContext] type, this should be the first parameter of system prompt functions etc. | ||
|
||
|
||
```python title="system_prompt_dependencies.py" hl_lines="20-27" | ||
from dataclasses import dataclass | ||
|
||
import httpx | ||
|
||
from pydantic_ai import Agent, CallContext | ||
|
||
|
||
@dataclass | ||
class MyDeps: | ||
api_key: str | ||
http_client: httpx.AsyncClient | ||
|
||
|
||
agent = Agent( | ||
'openai:gpt-4o', | ||
deps_type=MyDeps, | ||
) | ||
|
||
|
||
@agent.system_prompt # (1)! | ||
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str: # (2)! | ||
response = await ctx.deps.http_client.get( # (3)! | ||
'https://example.com', | ||
headers={'Authorization': f'Bearer {ctx.deps.api_key}'} # (4)! | ||
) | ||
response.raise_for_status() | ||
return f'Prompt: {response.text}' | ||
|
||
|
||
async def main(): | ||
async with httpx.AsyncClient() as client: | ||
deps = MyDeps('foobar', client) | ||
result = await agent.run('Tell me a joke.', deps=deps) | ||
print(result.data) | ||
``` | ||
|
||
1. [`CallContext`][pydantic_ai.dependencies.CallContext] may optionally be passed to a [`system_prompt`][pydantic_ai.Agent.system_prompt] function as the only argument. | ||
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. | ||
3. Access dependencies through the [`.deps`][pydantic_ai.dependencies.CallContext.deps] attribute. | ||
4. Access dependencies through the [`.deps`][pydantic_ai.dependencies.CallContext.deps] attribute. | ||
|
||
_(This example is complete, it can be run "as is" inside an async context)_ | ||
|
||
### Asynchronous vs. Synchronous dependencies | ||
|
||
System prompt functions, retriever functions and result validator are all run in the async context of an agent run. | ||
|
||
If these functions are not coroutines (e.g. `async def`) they are called with | ||
[`run_in_executor`][asyncio.loop.run_in_executor] in a thread pool, it's therefore marginally preferable | ||
to use `async` methods where dependencies perform IO, although synchronous dependencies should work fine too. | ||
|
||
!!! note "`run` vs. `run_sync` and Asynchronous vs. Synchronous dependencies" | ||
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. | ||
|
||
Here's the same example as above, but with a synchronous dependency: | ||
|
||
```python title="sync_dependencies.py" | ||
from dataclasses import dataclass | ||
|
||
import httpx | ||
|
||
from pydantic_ai import Agent, CallContext | ||
|
||
|
||
@dataclass | ||
class MyDeps: | ||
api_key: str | ||
http_client: httpx.Client # (1)! | ||
|
||
|
||
agent = Agent( | ||
'openai:gpt-4o', | ||
deps_type=MyDeps, | ||
) | ||
|
||
|
||
@agent.system_prompt | ||
def get_system_prompt(ctx: CallContext[MyDeps]) -> str: # (2)! | ||
response = ctx.deps.http_client.get( | ||
'https://example.com', | ||
headers={'Authorization': f'Bearer {ctx.deps.api_key}'} | ||
) | ||
response.raise_for_status() | ||
return f'Prompt: {response.text}' | ||
|
||
|
||
async def main(): | ||
deps = MyDeps('foobar', httpx.Client()) | ||
result = await agent.run( | ||
'Tell me a joke.', | ||
deps=deps, | ||
) | ||
print(result.data) | ||
``` | ||
|
||
1. Here we use a synchronous `httpx.Client` instead of an asynchronous `httpx.AsyncClient`. | ||
2. To match the synchronous dependency, the system prompt function is now a plain function, not a coroutine. | ||
|
||
_(This example is complete, it can be run "as is")_ | ||
|
||
## Full Example | ||
|
||
As well as system prompts, dependencies can be used in [retrievers](retrievers.md) and [result validators](result-validation.md#TODO). | ||
|
||
```python title="full_example.py" hl_lines="27-35 38-48" | ||
from dataclasses import dataclass | ||
|
||
import httpx | ||
|
||
from pydantic_ai import Agent, CallContext, ModelRetry | ||
|
||
|
||
@dataclass | ||
class MyDeps: | ||
api_key: str | ||
http_client: httpx.AsyncClient | ||
|
||
|
||
agent = Agent( | ||
'openai:gpt-4o', | ||
deps_type=MyDeps, | ||
) | ||
|
||
|
||
@agent.system_prompt | ||
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str: | ||
response = await ctx.deps.http_client.get('https://example.com') | ||
response.raise_for_status() | ||
return f'Prompt: {response.text}' | ||
|
||
|
||
@agent.retriever_context # (1)! | ||
async def get_joke_material(ctx: CallContext[MyDeps], subject: str) -> str: | ||
response = await ctx.deps.http_client.get( | ||
'https://example.com#jokes', | ||
params={'subject': subject}, | ||
headers={'Authorization': f'Bearer {ctx.deps.api_key}'}, | ||
) | ||
response.raise_for_status() | ||
return response.text | ||
|
||
|
||
@agent.result_validator # (2)! | ||
async def validate_result(ctx: CallContext[MyDeps], final_response: str) -> str: | ||
response = await ctx.deps.http_client.post( | ||
'https://example.com#validate', | ||
headers={'Authorization': f'Bearer {ctx.deps.api_key}'}, | ||
params={'query': final_response}, | ||
) | ||
if response.status_code == 400: | ||
raise ModelRetry(f'invalid response: {response.text}') | ||
response.raise_for_status() | ||
return final_response | ||
|
||
|
||
async def main(): | ||
async with httpx.AsyncClient() as client: | ||
deps = MyDeps('foobar', client) | ||
result = await agent.run('Tell me a joke.', deps=deps) | ||
print(result.data) | ||
``` | ||
|
||
1. To pass `CallContext` and to a retriever, us the [`retriever_context`][pydantic_ai.Agent.retriever_context] decorator. | ||
2. `CallContext` may optionally be passed to a [`result_validator`][pydantic_ai.Agent.result_validator] function as the first argument. | ||
|
||
## Overriding Dependencies | ||
|
||
When testing agents, it's useful to be able to customise dependencies. | ||
|
||
While this can sometimes be done by calling the agent directly within unit tests, we can also override dependencies | ||
while calling application code which in turn calls the agent. | ||
|
||
This is done via the [`override_deps`][pydantic_ai.Agent.override_deps] method on the agent. | ||
|
||
```py title="joke_app.py" | ||
from dataclasses import dataclass | ||
|
||
import httpx | ||
|
||
from pydantic_ai import Agent, CallContext | ||
|
||
|
||
@dataclass | ||
class MyDeps: | ||
api_key: str | ||
http_client: httpx.AsyncClient | ||
|
||
async def system_prompt_factory(self) -> str: # (1)! | ||
response = await self.http_client.get('https://example.com') | ||
response.raise_for_status() | ||
return f'Prompt: {response.text}' | ||
|
||
|
||
joke_agent = Agent('openai:gpt-4o', deps_type=MyDeps) | ||
|
||
|
||
@joke_agent.system_prompt | ||
async def get_system_prompt(ctx: CallContext[MyDeps]) -> str: | ||
return await ctx.deps.system_prompt_factory() # (2)! | ||
|
||
|
||
async def application_code(prompt: str) -> str: # (3)! | ||
... | ||
... | ||
# now deep within application code we call our agent | ||
async with httpx.AsyncClient() as client: | ||
app_deps = MyDeps('foobar', client) | ||
result = await joke_agent.run(prompt, deps=app_deps) # (4)! | ||
return result.data | ||
|
||
``` | ||
|
||
1. Define a method on the dependency to make the system prompt easier to customise. | ||
2. Call the system prompt factory from within the system prompt function. | ||
3. Application code that calls the agent, in a real application this might be an API endpoint. | ||
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. | ||
|
||
```py title="test_joke_app.py" hl_lines="10-12" | ||
from joke_app import application_code, joke_agent, MyDeps | ||
|
||
|
||
class TestMyDeps(MyDeps): # (1)! | ||
async def system_prompt_factory(self) -> str: | ||
return 'test prompt' | ||
|
||
|
||
async def test_application_code(): | ||
test_deps = TestMyDeps('test_key', None) # (2)! | ||
with joke_agent.override_deps(test_deps): # (3)! | ||
joke = application_code('Tell me a joke.') # (4)! | ||
assert joke == 'funny' | ||
``` | ||
|
||
1. Define a subclass of `MyDeps` in tests to customise the system prompt factory. | ||
2. Create an instance of the test dependency, we don't need to pass an `http_client` here as it's not used. | ||
3. Override the dependencies of the agent for the duration of the `with` block, `test_deps` will be used when the agent is run. | ||
4. Now we can safely call our application code, the agent will use the overridden dependencies. | ||
|
||
## Agents as dependencies of other Agents | ||
|
||
Since dependencies can be any python type, and agents are just python objects, agents can be dependencies of other agents. | ||
|
||
```py title="agents_as_dependencies.py" | ||
from dataclasses import dataclass | ||
|
||
from pydantic_ai import Agent, CallContext | ||
|
||
|
||
@dataclass | ||
class MyDeps: | ||
factory_agent: Agent[None, list[str]] | ||
|
||
|
||
joke_agent = Agent( | ||
'openai:gpt-4o', | ||
deps_type=MyDeps, | ||
system_prompt=( | ||
'Use the "joke_factory" to generate some jokes, then choose the best. ' | ||
'You must return just a single joke.' | ||
) | ||
) | ||
|
||
factory_agent = Agent('gemini-1.5-pro', result_type=list[str]) | ||
|
||
|
||
@joke_agent.retriever_context | ||
async def joke_factory(ctx: CallContext[MyDeps], count: int) -> str: | ||
r = await ctx.deps.factory_agent.run(f'Please generate {count} jokes.') | ||
return '\n'.join(r.data) | ||
|
||
|
||
result = joke_agent.run_sync('Tell me a joke.', deps=MyDeps(factory_agent)) | ||
print(result.data) | ||
``` | ||
|
||
## Examples | ||
|
||
The following examples demonstrate how to use dependencies in PydanticAI: | ||
|
||
- [Weather Agent](../examples/weather-agent.md) | ||
- [SQL Generation](../examples/sql-gen.md) | ||
- [RAG](../examples/rag.md) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.