From a5f4eeb2ba0a76572a93854c0d9c5354b1ac6111 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 14 Nov 2024 21:37:47 +0000 Subject: [PATCH 1/3] writing concepts docs --- docs/api/messages.md | 14 +++ docs/concepts/dependencies.md | 174 +++++++++++++++++++++++++++++++ docs/concepts/message-history.md | 101 ++++++++++++++++++ docs/examples/chat-app.md | 6 +- docs/examples/pydantic-model.md | 4 - docs/examples/rag.md | 4 - docs/examples/sql-gen.md | 4 - docs/examples/stream-markdown.md | 4 - docs/examples/stream-whales.md | 4 - docs/examples/weather-agent.md | 4 - docs/img/pydantic-ai-dark.svg | 2 +- docs/img/pydantic-ai-light.svg | 2 +- docs/index.md | 2 +- docs/install.md | 2 +- pydantic_ai/agent.py | 2 + pydantic_ai/messages.py | 35 +++++-- pydantic_ai/models/__init__.py | 2 +- 17 files changed, 326 insertions(+), 40 deletions(-) diff --git a/docs/api/messages.md b/docs/api/messages.md index 3cc144e3d0..9986b8504e 100644 --- a/docs/api/messages.md +++ b/docs/api/messages.md @@ -1,3 +1,17 @@ # `pydantic_ai.messages` ::: pydantic_ai.messages + options: + members: + - Message + - SystemPrompt + - UserPrompt + - ToolReturn + - RetryPrompt + - ModelAnyResponse + - ModelTextResponse + - ModelStructuredResponse + - ToolCall + - ArgsJson + - ArgsObject + - MessagesTypeAdapter diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index e69de29bb2..1874cac060 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -0,0 +1,174 @@ +from write_docs import result + +# 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). + +Dependencies provide a scalable, flexible, type safe and testable way to build agents. + +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 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. + +!!! note "Asynchronous vs Synchronous dependencies" + System prompt functions, retriever functions and result validator functions which are not coroutines (e.g. `async def`) + 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. + +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 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 with dependencies" 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 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)_ + +## 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-34 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 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 + +## Agents as dependencies of other Agents diff --git a/docs/concepts/message-history.md b/docs/concepts/message-history.md index e69de29bb2..1a56dc3446 100644 --- a/docs/concepts/message-history.md +++ b/docs/concepts/message-history.md @@ -0,0 +1,101 @@ +# Messages and chat history + +PydanticAI provides access to messages exchanged during an agent run. These messages can be used both to continue a coherent conversation, and to understand how an agent performed. + +## Messages types + +[API documentation for `messages`][pydantic_ai.messages] contains details of the message types and their meaning. + +### Accessing Messages from Results + +After running an agent, you can access the messages exchanged during that run from the `result` object. + +Both [`RunResult`][pydantic_ai.result.RunResult] +(returned by [`Agent.run`][pydantic_ai.Agent.run], [`Agent.run_sync`][pydantic_ai.Agent.run_sync]) +and [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] (returned by [`Agent.run_stream`][pydantic_ai.Agent.run_stream]) have the following methods: + +* [`all_messages()`][pydantic_ai.result.RunResult.all_messages]: returns all messages, including messages from prior runs and system prompts. There's also a variant that returns JSON bytes, [`all_messages_json()`][pydantic_ai.result.RunResult.all_messages_json]. +* [`new_messages()`][pydantic_ai.result.RunResult.new_messages]: returns only the messages from the current run, excluding system prompts, this is generally the data you want when you want to use the messages in further runs to continue the conversation. There's also a variant that returns JSON bytes, [`new_messages_json()`][pydantic_ai.result.RunResult.new_messages_json]. + +!!! info "StreamedRunResult and complete messages" + On [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], the messages returned from these methods will only include the final response message once the stream has finished. + + E.g. you've awaited one of the following coroutines: + + * [`StreamedRunResult.stream()`][pydantic_ai.result.StreamedRunResult.stream] + * [`StreamedRunResult.stream_text()`][pydantic_ai.result.StreamedRunResult.stream_text] + * [`StreamedRunResult.stream_structured()`][pydantic_ai.result.StreamedRunResult.stream_structured] + * [`StreamedRunResult.get_data()`][pydantic_ai.result.StreamedRunResult.get_data] + +Example of accessing methods on a [`RunResult`][pydantic_ai.result.RunResult] : + +```python title="Accessing messages from a RunResult" hl_lines="9 12" +from pydantic_ai import Agent + +agent = Agent('openai:gpt-4o', system_prompt='Be a helpful assistant.') + +result = agent.run_sync('Tell me a joke.') +print(result.data) + +# all messages from the run +print(result.all_messages()) + +# messages excluding system prompts +print(result.new_messages()) +``` +_(This example is complete, it can be run "as is")_ + +Example of accessing methods on a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] : + +```python title="Accessing messages from a StreamedRunResult" hl_lines="7 13" +from pydantic_ai import Agent + +agent = Agent('openai:gpt-4o', system_prompt='Be a helpful assistant.') + +async with agent.run_stream('Tell me a joke.') as result: + # incomplete messages before the stream finishes + print(result.all_messages()) + + async for text in result.stream(): + print(text) + + # complete messages once the stream finishes + print(result.all_messages()) +``` +_(This example is complete, it can be run "as is" inside an async context)_ + +### Using Messages as Input for Further Agent Runs + +The primary use of message histories in PydanticAI is to maintain context across multiple agent runs. + +To use existing messages in a run, pass them to the `message_history` parameter of +[`Agent.run`][pydantic_ai.Agent.run], [`Agent.run_sync`][pydantic_ai.Agent.run_sync] or +[`Agent.run_stream`][pydantic_ai.Agent.run_stream]. + +!!! info "`all_messages()` vs. `new_messages()`" + PydanticAI will inspect any messages it receives for system prompts. + + If any system prompts are found in `message_history`, new system prompts are not generated, + otherwise new system prompts are generated and inserted before `message_history` in the list of messages + used in the run. + + Thus you can decide whether you want to use system prompts from a previous run or generate them again by using + [`all_messages()`][pydantic_ai.result.RunResult.all_messages] or [`new_messages()`][pydantic_ai.result.RunResult.new_messages]. + + +```py title="Reusing messages in a conversation" hl_lines="8 11" +from pydantic_ai import Agent + +agent = Agent('openai:gpt-4o', system_prompt='Be a helpful assistant.') + +result1 = agent.run_sync('Tell me a joke.') +print(result1.data) + +result2 = agent.run_sync('Explain?', message_history=result1.new_messages()) +print(result2.data) + +print(result2.all_messages()) +``` +_(This example is complete, it can be run "as is")_ + +For a more complete example of using messages in conversations, see the [chat app](../examples/chat-app.md) example. diff --git a/docs/examples/chat-app.md b/docs/examples/chat-app.md index 3af5719ac3..a64859f8a3 100644 --- a/docs/examples/chat-app.md +++ b/docs/examples/chat-app.md @@ -1,12 +1,8 @@ ---- -hide: [toc] ---- - Simple chat app example build with FastAPI. Demonstrates: -* reusing chat history +* [reusing chat history](../concepts/message-history.md) * serializing messages * streaming responses diff --git a/docs/examples/pydantic-model.md b/docs/examples/pydantic-model.md index b2f2cc14c7..4460e5e80e 100644 --- a/docs/examples/pydantic-model.md +++ b/docs/examples/pydantic-model.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - Simple example of using Pydantic AI to construct a Pydantic model from a text input. Demonstrates: diff --git a/docs/examples/rag.md b/docs/examples/rag.md index 20e77b526a..0fccbcff06 100644 --- a/docs/examples/rag.md +++ b/docs/examples/rag.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - # RAG RAG search example. This demo allows you to ask question of the [logfire](https://pydantic.dev/logfire) documentation. diff --git a/docs/examples/sql-gen.md b/docs/examples/sql-gen.md index badf8778ab..b4b2193773 100644 --- a/docs/examples/sql-gen.md +++ b/docs/examples/sql-gen.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - # SQL Generation Example demonstrating how to use Pydantic AI to generate SQL queries based on user input. diff --git a/docs/examples/stream-markdown.md b/docs/examples/stream-markdown.md index 2b81c44b94..20b1899490 100644 --- a/docs/examples/stream-markdown.md +++ b/docs/examples/stream-markdown.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - This example shows how to stream markdown from an agent, using the [`rich`](https://github.com/Textualize/rich) library to highlight the output in the terminal. It'll run the example with both OpenAI and Google Gemini models if the required environment variables are set. diff --git a/docs/examples/stream-whales.md b/docs/examples/stream-whales.md index d9a435f947..e537b4f9a3 100644 --- a/docs/examples/stream-whales.md +++ b/docs/examples/stream-whales.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - Information about whales — an example of streamed structured response validation. Demonstrates: diff --git a/docs/examples/weather-agent.md b/docs/examples/weather-agent.md index 4c96527435..8d7e267271 100644 --- a/docs/examples/weather-agent.md +++ b/docs/examples/weather-agent.md @@ -1,7 +1,3 @@ ---- -hide: [toc] ---- - Example of Pydantic AI with multiple tools which the LLM needs to call in turn to answer a question. Demonstrates: diff --git a/docs/img/pydantic-ai-dark.svg b/docs/img/pydantic-ai-dark.svg index de3a32b951..f24d822d94 100644 --- a/docs/img/pydantic-ai-dark.svg +++ b/docs/img/pydantic-ai-dark.svg @@ -1,4 +1,4 @@ - + diff --git a/docs/img/pydantic-ai-light.svg b/docs/img/pydantic-ai-light.svg index 86e06d3298..e94b9f5068 100644 --- a/docs/img/pydantic-ai-light.svg +++ b/docs/img/pydantic-ai-light.svg @@ -1,4 +1,4 @@ - + diff --git a/docs/index.md b/docs/index.md index 76d9a30a66..5b93a77ce1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -89,7 +89,7 @@ async def main(): 7. Multiple retrievers can be registered with the same agent, the LLM can choose which (if any) retrievers to call in order to respond to a user. 8. Run the agent asynchronously, conducting a conversation with the LLM until a final response is reached. You can also run agents synchronously with `run_sync`. Internally agents are all async, so `run_sync` is a helper using `asyncio.run` to call `run()`. 9. The response from the LLM, in this case a `str`, Agents are generic in both the type of `deps` and `result_type`, so calls are typed end-to-end. -10. `result.all_messages()` includes details of messages exchanged, this is useful both to understand the conversation that took place and useful if you want to continue the conversation later — messages can be passed back to later `run/sync_run` calls. +10. `result.all_messages()` includes details of messages exchanged, this is useful both to understand the conversation that took place and useful if you want to continue the conversation later — messages can be passed back to later `run/run_sync` calls. !!! tip "Complete `weather_agent.py` example" This example is incomplete for the sake of brevity; you can find a complete `weather_agent.py` example [here](examples/weather-agent.md). diff --git a/docs/install.md b/docs/install.md index 9fd8cf2ece..b4bb26d552 100644 --- a/docs/install.md +++ b/docs/install.md @@ -20,7 +20,7 @@ It requires Python 3.9+. PydanticAI has an excellent (but completely optional) integration with [Pydantic Logfire](https://pydantic.dev/logfire) to help you view and understand agent runs. -To use Logfire, install PydanticAI with the `logfire` optional group: +To use Logfire with PydanticAI, install PydanticAI with the `logfire` optional group: === "pip" diff --git a/pydantic_ai/agent.py b/pydantic_ai/agent.py index b5792ba258..fb4211b06b 100644 --- a/pydantic_ai/agent.py +++ b/pydantic_ai/agent.py @@ -29,6 +29,8 @@ 'openai:gpt-4o-mini', 'openai:gpt-4-turbo', 'openai:gpt-4', + 'openai:o1-preview', + 'openai:o1-mini', 'openai:gpt-3.5-turbo', 'gemini-1.5-flash', 'gemini-1.5-pro', diff --git a/pydantic_ai/messages.py b/pydantic_ai/messages.py index 0db923c386..9074366439 100644 --- a/pydantic_ai/messages.py +++ b/pydantic_ai/messages.py @@ -14,7 +14,10 @@ @dataclass class SystemPrompt: - """A system prompt, generally written by the application developer.""" + """A system prompt, generally written by the application developer. + + This give the model context and guidance on how to respond. + """ content: str """The content of the prompt.""" @@ -24,7 +27,11 @@ class SystemPrompt: @dataclass class UserPrompt: - """A user prompt, generally written by the end user.""" + """A user prompt, generally written by the end user. + + Content comes from the `user_prompt` parameter of [`Agent.run`][pydantic_ai.Agent.run], + [`Agent.run_sync`][pydantic_ai.Agent.run_sync], and [`Agent.run_stream`][pydantic_ai.Agent.run_stream]. + """ content: str """The content of the prompt.""" @@ -68,7 +75,19 @@ def model_response_object(self) -> dict[str, Any]: @dataclass class RetryPrompt: - """A message sent when running a retriever failed, result validation failed, or no tool could be found to call.""" + """A message back to a model asking it to try again. + + This can be sent for a number of reasons: + + * Pydantic validation of retriever arguments failed, here content is derived from a Pydantic + [`ValidationError`][pydantic_core.ValidationError] + * a retriever raised a [ModelRetry][pydantic_ai.exceptions.ModelRetry] exception + * no retriever was found for the tool name + * the model returned plain text when a structured response was expected + * Pydantic validation of a structured response failed, here content is derived from a Pydantic + [`ValidationError`][pydantic_core.ValidationError] + * a result validator raised a [ModelRetry][pydantic_ai.exceptions.ModelRetry] exception + """ content: list[pydantic_core.ErrorDetails] | str """Details of why and how the model should retry. @@ -122,7 +141,7 @@ class ArgsObject: @dataclass class ToolCall: - """Either a retriever/tool call from the agent.""" + """Either a tool call from the agent.""" tool_name: str """The name of the tool to call.""" @@ -151,7 +170,10 @@ def has_content(self) -> bool: @dataclass class ModelStructuredResponse: - """A structured response from a model.""" + """A structured response from a model. + + This is used either to call a retriever or to return a structured response from an agent run. + """ calls: list[ToolCall] """The tool calls being made.""" @@ -166,7 +188,8 @@ class ModelStructuredResponse: ModelAnyResponse = Union[ModelTextResponse, ModelStructuredResponse] """Any response from a model.""" -Message = Union[SystemPrompt, UserPrompt, ToolReturn, RetryPrompt, ModelAnyResponse] +Message = Union[SystemPrompt, UserPrompt, ToolReturn, RetryPrompt, ModelTextResponse, ModelStructuredResponse] """Any message send to or returned by a model.""" MessagesTypeAdapter = _pydantic.LazyTypeAdapter(list[Annotated[Message, pydantic.Field(discriminator='role')]]) +"""Pydantic [`TypeAdapter`][pydantic.type_adapter.TypeAdapter] for (de)serializing messages.""" diff --git a/pydantic_ai/models/__init__.py b/pydantic_ai/models/__init__.py index 32de16a344..0378ab4c16 100644 --- a/pydantic_ai/models/__init__.py +++ b/pydantic_ai/models/__init__.py @@ -151,7 +151,7 @@ def timestamp(self) -> datetime: EitherStreamedResponse = Union[StreamTextResponse, StreamStructuredResponse] -ALLOW_MODEL_REQUESTS = False +ALLOW_MODEL_REQUESTS = True """Whether to allow requests to models. This global setting allows you to disable request to most models, e.g. to make sure you don't accidentally From f1c7d70c4c8a1cdead6782e8fbb8f5d6109b71a8 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 14 Nov 2024 22:53:31 +0000 Subject: [PATCH 2/3] documenting dependencies --- docs/concepts/dependencies.md | 221 +++++++++++++++++++++++++++---- docs/concepts/message-history.md | 28 ++++ docs/examples/rag.md | 2 +- docs/examples/sql-gen.md | 2 +- docs/examples/weather-agent.md | 2 +- 5 files changed, 224 insertions(+), 31 deletions(-) diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index 1874cac060..843ddcfeca 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -1,23 +1,14 @@ -from write_docs import result - # 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). -Dependencies provide a scalable, flexible, type safe and testable way to build agents. - -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 and ultimately easier to deploy in production. +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 +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. -!!! note "Asynchronous vs Synchronous dependencies" - System prompt functions, retriever functions and result validator functions which are not coroutines (e.g. `async def`) - 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. - 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) @@ -42,13 +33,14 @@ agent = Agent( ) -async with httpx.AsyncClient() as client: - deps = MyDeps('foobar', client) - result = await agent.run( - 'Tell me a joke.', - deps=deps, # (3)! - ) - print(result.data) +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. @@ -62,7 +54,7 @@ _(This example is complete, it can be run "as is" inside an async context)_ 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 with dependencies" hl_lines="20-27" +```python title="system_prompt_dependencies.py" hl_lines="20-27" from dataclasses import dataclass import httpx @@ -92,10 +84,11 @@ async def get_system_prompt(ctx: CallContext[MyDeps]) -> str: # (2)! return f'Prompt: {response.text}' -async with httpx.AsyncClient() as client: - deps = MyDeps('foobar', client) - result = await agent.run('Tell me a joke.', deps=deps) - print(result.data) +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. @@ -105,11 +98,68 @@ async with httpx.AsyncClient() as client: _(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-34 38-48" +```python title="full_example.py" hl_lines="27-35 38-48" from dataclasses import dataclass import httpx @@ -160,10 +210,11 @@ async def validate_result(ctx: CallContext[MyDeps], final_response: str) -> str: return final_response -async with httpx.AsyncClient() as client: - deps = MyDeps('foobar', client) - result = await agent.run('Tell me a joke.', deps=deps) - print(result.data) +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. @@ -171,4 +222,118 @@ async with httpx.AsyncClient() as client: ## 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) diff --git a/docs/concepts/message-history.md b/docs/concepts/message-history.md index 1a56dc3446..8fff79a8c9 100644 --- a/docs/concepts/message-history.md +++ b/docs/concepts/message-history.md @@ -1,3 +1,5 @@ +from pydantic_ai_examples.pydantic_model import model + # Messages and chat history PydanticAI provides access to messages exchanged during an agent run. These messages can be used both to continue a coherent conversation, and to understand how an agent performed. @@ -98,4 +100,30 @@ print(result2.all_messages()) ``` _(This example is complete, it can be run "as is")_ +## Other ways of using messages + +Since messages are defined by simple dataclasses, you can manually create and manipulate, e.g. for testing. + +The message format is independent of the model used, so you can use messages in different agents, or the same agent with different models. + +```python +from pydantic_ai import Agent + +agent = Agent('openai:gpt-4o', system_prompt='Be a helpful assistant.') + +result1 = agent.run_sync('Tell me a joke.') +print(result1.data) + +result2 = agent.run_sync('Explain?', model='gemini-1.5-pro', message_history=result1.new_messages()) +print(result2.data) + +print(result2.all_messages()) +``` + +## Last Run Messages + +TODO: document [`last_run_messages`][pydantic_ai.Agent.last_run_messages]. + +## Examples + For a more complete example of using messages in conversations, see the [chat app](../examples/chat-app.md) example. diff --git a/docs/examples/rag.md b/docs/examples/rag.md index 0fccbcff06..3db3f891a8 100644 --- a/docs/examples/rag.md +++ b/docs/examples/rag.md @@ -5,7 +5,7 @@ RAG search example. This demo allows you to ask question of the [logfire](https: Demonstrates: * retrievers -* agent dependencies +* [agent dependencies](../concepts/dependencies.md) * RAG search This is done by creating a database containing each section of the markdown documentation, then registering diff --git a/docs/examples/sql-gen.md b/docs/examples/sql-gen.md index b4b2193773..1b43abedf1 100644 --- a/docs/examples/sql-gen.md +++ b/docs/examples/sql-gen.md @@ -7,7 +7,7 @@ Demonstrates: * custom `result_type` * dynamic system prompt * result validation -* agent dependencies +* [agent dependencies](../concepts/dependencies.md) ## Running the Example diff --git a/docs/examples/weather-agent.md b/docs/examples/weather-agent.md index 8d7e267271..fb2f774d1d 100644 --- a/docs/examples/weather-agent.md +++ b/docs/examples/weather-agent.md @@ -4,7 +4,7 @@ Demonstrates: * retrievers * multiple retrievers -* agent dependencies +* [agent dependencies](../concepts/dependencies.md) In this case the idea is a "weather" agent — the user can ask for the weather in multiple locations, the agent will use the `get_lat_lng` tool to get the latitude and longitude of the locations, then use From 9937e2024ab52003f5e7fbbb0d621679c08c7b26 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 15 Nov 2024 00:18:55 +0000 Subject: [PATCH 3/3] tweak messages --- pydantic_ai/messages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydantic_ai/messages.py b/pydantic_ai/messages.py index 9074366439..4da93aa197 100644 --- a/pydantic_ai/messages.py +++ b/pydantic_ai/messages.py @@ -16,7 +16,7 @@ class SystemPrompt: """A system prompt, generally written by the application developer. - This give the model context and guidance on how to respond. + This gives the model context and guidance on how to respond. """ content: str @@ -81,18 +81,18 @@ class RetryPrompt: * Pydantic validation of retriever arguments failed, here content is derived from a Pydantic [`ValidationError`][pydantic_core.ValidationError] - * a retriever raised a [ModelRetry][pydantic_ai.exceptions.ModelRetry] exception + * a retriever raised a [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] exception * no retriever was found for the tool name * the model returned plain text when a structured response was expected * Pydantic validation of a structured response failed, here content is derived from a Pydantic [`ValidationError`][pydantic_core.ValidationError] - * a result validator raised a [ModelRetry][pydantic_ai.exceptions.ModelRetry] exception + * a result validator raised a [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] exception """ content: list[pydantic_core.ErrorDetails] | str """Details of why and how the model should retry. - If the retry was triggered by a [ValidationError][pydantic_core.ValidationError], this will be a list of + If the retry was triggered by a [`ValidationError`][pydantic_core.ValidationError], this will be a list of error details. """ tool_name: str | None = None