From afee854043d26f48ab35bee50a95bfb10b4ea3ff Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 12 Nov 2024 06:01:45 +0000 Subject: [PATCH] further docs improvements --- Makefile | 3 ++ docs/index.md | 8 ++++-- docs/install.md | 38 ++++++++++++++++++++++++- preview.README.md | 66 -------------------------------------------- pydantic_ai/agent.py | 14 ++++++++-- tests/typed_agent.py | 4 +++ 6 files changed, 62 insertions(+), 71 deletions(-) delete mode 100644 preview.README.md diff --git a/Makefile b/Makefile index 39f273fa57..d10bd36c88 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,9 @@ typecheck-mypy: .PHONY: typecheck # Run static type checking typecheck: typecheck-pyright +.PHONY: typecheck-both # Run static type checking with both Pyright and Mypy +typecheck-both: typecheck-pyright typecheck-mypy + .PHONY: test # Run tests and collect coverage data test: uv run coverage run -m pytest diff --git a/docs/index.md b/docs/index.md index 2a39d7e2cf..361417f802 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,11 @@ You can think of PydanticAI as an Agent Framework or a shim to use Pydantic with LLMs — they're the same thing. -PydanticAI ties to make working with LLMs feel similar to building a web application. +PydanticAI tries to make working with LLMs feel similar to building a web application. + +!!! example "In Beta" + PydanticAI is in early beta, the API is subject to change and there's a lot more to do. + [Feedback](https://github.com/pydantic/pydantic-ai/issues) is very welcome! ## Example — Retrievers @@ -42,7 +46,7 @@ print(result.all_messages()) # (10)! 2. Here we configure the agent to use OpenAI's GPT-4o model, you can also customise the model when running the agent. 3. We specify a dependency for the agent, in this case an HTTP client, which retrievers will use to make requests to external services. PydanticAI's system of dependency injection provides a powerful, type safe way to customise the behaviour of your agents, including for unit tests and evals. 4. Static system prompts can be registered as key word arguments to the agent, dynamic system prompts can be registered with the `@agent.system_prompot` decorator and benefit from dependency injection. -5. Retrievers let you register "tools" which the LLM can call while trying to respond to a user. You inject dependencies into the retriever with `CallContext`, any other arguments become the tool schema passed to the LLM, Pydantic is used to validate these arguments, errors are passed back to the LLM so it can retry. +5. Retrievers let you register "tools" which the LLM may call while to respond to a user. You inject dependencies into the retriever with `CallContext`, any other arguments become the tool schema passed to the LLM, Pydantic is used to validate these arguments, errors are passed back to the LLM so it can retry. 6. This docstring is also passed to the LLM as a description of the tool. 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 synchronously, conducting a conversation with the LLM until a final response is reached. (internally agents are all async, `run_sync` is a helper using `asyncio.run` to call `run()`) diff --git a/docs/install.md b/docs/install.md index 7cd42228f3..9fd8cf2ece 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,3 +1,39 @@ # Installation -TODO +PydanticAI is available [on PyPI](https://pypi.org/project/pydantic-ai/) so installation is as simple as: + +=== "pip" + + ```bash + pip install pydantic-ai + ``` + +=== "uv" + + ```bash + uv add pydantic-ai + ``` + +It requires Python 3.9+. + +## Use with Pydantic Logfire + +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: + +=== "pip" + + ```bash + pip install 'pydantic-ai[logfire]' + ``` + +=== "uv" + + ```bash + uv add 'pydantic-ai[logfire]' + ``` + +From there, follow the [Logfire documentation](https://logfire.pydantic.dev/docs/) to configure Logfire. + +TODO screenshot of Logfire with PydanticAI in action. diff --git a/preview.README.md b/preview.README.md deleted file mode 100644 index daeaf16397..0000000000 --- a/preview.README.md +++ /dev/null @@ -1,66 +0,0 @@ -# PydanticAI - -Shim to use Pydantic with LLMs. - -## Example of usage - -```py -from pydantic_ai import Agent - -# An agent that can tell users about the weather in a particular location. -# Agents combine a system prompt, a response type (here `str`) and one or more -# "retrievers" (aka tools). They can be independent of the LLM used. -weather_agent = Agent( - 'openai:gpt-4o', - system_prompt='Be concise, reply with one sentence.', -) - - -# retrievers let you register "tools" which the LLM can call while trying to respond to a user. -@weather_agent.retriever_plain(retries=2) -async def get_location(location_description: str) -> str: - """ - Get the latitude and longitude of a location by its description. - - In a real-world application, this function would use a geocoding API, - hence it's async, you could pass an HTTP client via `CallInfo.context`. - - Arguments are validated via Pydantic, and validation errors are returned to the LLM - `retries` times. - - This docstring is also passed to the LLM as a description for This. - """ - import json - - if 'london' in location_description.lower(): - lat_lng = {'lat': 51.1, 'lng': -0.1} - elif 'wiltshire' in location_description.lower(): - lat_lng = {'lat': 51.1, 'lng': -2.11} - else: - lat_lng = {'lat': 0, 'lng': 0} - return json.dumps(lat_lng) - - -@weather_agent.retriever_plain -async def get_weather(lat: float, lng: float): - """ - Get the weather at a location by its latitude and longitude. - """ - # In a real-world application, this function would use a weather API. - if abs(lat - 51.1) < 0.1 and abs(lng + 0.1) < 0.1: - # it always rains in London - return 'Raining' - else: - return 'Sunny' - - -# Run the agent synchronously, conducting a conversation with the LLM until a final response is reached. -# (internally agents are all async, `run_sync` is a helper using `asyncio.run`) -result = weather_agent.run_sync('What is the weather like in West London and in Wiltshire?') -print(result.data) -# > 'The weather in West London is raining, while in Wiltshire it is sunny.' - -# `result.all_messages` includes details of messages exchanged, useful if you want to continue -# the conversation later, via the `message_history` argument of `run_sync`. -print(result.all_messages()) -``` diff --git a/pydantic_ai/agent.py b/pydantic_ai/agent.py index 0c97abe413..133141979f 100644 --- a/pydantic_ai/agent.py +++ b/pydantic_ai/agent.py @@ -32,6 +32,15 @@ _logfire = logfire_api.Logfire(otel_scope='pydantic-ai') +class Unset: + """A sentinel value to indicate that a parameter was not set.""" + + pass + + +UNSET = Unset() + + @final @dataclass(init=False) class Agent(Generic[AgentDeps, ResultData]): @@ -62,8 +71,7 @@ def __init__( result_type: type[ResultData] = str, *, system_prompt: str | Sequence[str] = (), - # type here looks odd, but it's required os you can avoid "partially unknown" type errors with `deps=None` - deps: AgentDeps | tuple[()] = (), + deps: AgentDeps | Unset = UNSET, retries: int = 1, result_tool_name: str = 'final_result', result_tool_description: str | None = None, @@ -79,6 +87,8 @@ def __init__( prompts via a function with [`system_prompt`][pydantic_ai.Agent.system_prompt]. deps: The type used for dependency injection, this parameter exists solely to allow you to fully parameterize the agent, and therefore get the best out of static type checking. + If you're not using deps, but want type checking to pass, you can set `deps=None` to satisfy Pyright + or add a type hint `: Agent[None, ]`. retries: The default number of retries to allow before raising an error. result_tool_name: The name of the tool to use for the final result. result_tool_description: The description of the final result tool. diff --git a/tests/typed_agent.py b/tests/typed_agent.py index 908e0543ad..f7fb951be9 100644 --- a/tests/typed_agent.py +++ b/tests/typed_agent.py @@ -107,3 +107,7 @@ def foo_result(response: Union[Foo, Bar]) -> str: def run_sync3() -> None: result: RunResult[Union[Foo, Bar]] = union_agent.run_sync('testing') foo_result(result.data) + + +agent_unknown_deps = Agent() # type: ignore[var-annotated] +agent_known_deps: Agent[None, str] = Agent()