From 48be7f44d0c725af20fe951f4580bcb2be3dbbef Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 3 Nov 2024 14:37:48 +0000 Subject: [PATCH] add pydantic_ai_examples to sdist and wheel, and examples CLI --- .github/workflows/ci.yml | 2 +- pydantic_ai/__init__.py | 5 +- {examples => pydantic_ai_examples}/README.md | 38 +++++++--- pydantic_ai_examples/__main__.py | 69 +++++++++++++++++++ .../pydantic_model.py | 2 +- {examples => pydantic_ai_examples}/rag.py | 8 +-- {examples => pydantic_ai_examples}/sql_gen.py | 2 +- {examples => pydantic_ai_examples}/weather.py | 2 +- pyproject.toml | 12 ++-- uv.lock | 14 ++-- 10 files changed, 121 insertions(+), 33 deletions(-) rename {examples => pydantic_ai_examples}/README.md (69%) create mode 100644 pydantic_ai_examples/__main__.py rename {examples => pydantic_ai_examples}/pydantic_model.py (93%) rename {examples => pydantic_ai_examples}/rag.py (95%) rename {examples => pydantic_ai_examples}/sql_gen.py (97%) rename {examples => pydantic_ai_examples}/weather.py (98%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f780c1ca..1f818e338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --python 3.12 --frozen --group examples + run: uv sync --python 3.12 --frozen --all-extras - uses: pre-commit/action@v3.0.0 with: diff --git a/pydantic_ai/__init__.py b/pydantic_ai/__init__.py index fef68bd6f..5d0832598 100644 --- a/pydantic_ai/__init__.py +++ b/pydantic_ai/__init__.py @@ -1,4 +1,7 @@ +from importlib.metadata import version + from .agent import Agent from .shared import AgentError, CallContext, ModelRetry, UnexpectedModelBehaviour, UserError -__all__ = 'Agent', 'AgentError', 'CallContext', 'ModelRetry', 'UnexpectedModelBehaviour', 'UserError' +__all__ = 'Agent', 'AgentError', 'CallContext', 'ModelRetry', 'UnexpectedModelBehaviour', 'UserError', '__version__' +__version__ = version('pydantic_ai') diff --git a/examples/README.md b/pydantic_ai_examples/README.md similarity index 69% rename from examples/README.md rename to pydantic_ai_examples/README.md index 4d62d31f7..24ef90ab2 100644 --- a/examples/README.md +++ b/pydantic_ai_examples/README.md @@ -4,10 +4,28 @@ Examples of how to use Pydantic AI and what it can do. ## Usage +These examples are distributed with `pydantic-ai` so you can run them either by cloning the [pydantic-ai repo](https://github.com/pydantic/pydantic-ai) or by simply installing `pydantic-ai` from PyPI with `pip` or `uv`. + +Either way you'll need to install extra dependencies to run some examples, you just need to install the `examples` optional dependency group. + +If you've cloned the repo, add the extra dependencies with: + +```bash +uv sync --extra examples +``` + +If you've installed `pydantic-ai` via pip/uv, you can install the extra dependencies with: + +```bash +pip install 'pydantic-ai[examples]' +# of if you're using uv +uv add 'pydantic-ai[examples]' +``` + To run the examples, run: ```bash -uv run -m examples. +uv run -m pydantic_ai_examples. ``` ## Examples @@ -19,17 +37,17 @@ uv run -m examples. Simple example of using Pydantic AI to construct a Pydantic model from a text input. ```bash -uv run --group examples -m examples.pydantic_model +uv run --extra examples -m pydantic_ai_examples.pydantic_model ``` -This examples uses `openai:gpt-4o` by default but it works well with other models, e.g. you can run it +This examples uses `openai:gpt-4o` by default, but it works well with other models, e.g. you can run it with Gemini using: ```bash -PYDANTIC_AI_MODEL=gemini-1.5-pro uv run --group examples -m examples.pydantic_model +PYDANTIC_AI_MODEL=gemini-1.5-pro uv run --extra examples -m pydantic_ai_examples.pydantic_model ``` -(or `PYDANTIC_AI_MODEL=gemini-1.5-flash...`) +(or `PYDANTIC_AI_MODEL=gemini-1.5-flash ...`) ### `sql_gen.py` @@ -38,13 +56,13 @@ PYDANTIC_AI_MODEL=gemini-1.5-pro uv run --group examples -m examples.pydantic_mo Example demonstrating how to use Pydantic AI to generate SQL queries based on user input. ```bash -uv run --group examples -m examples.sql_gen +uv run --extra examples -m pydantic_ai_examples.sql_gen ``` or to use a custom prompt: ```bash -uv run --group examples -m examples.sql_gen "find me whatever" +uv run --extra examples -m pydantic_ai_examples.sql_gen "find me whatever" ``` This model uses `gemini-1.5-flash` by default since Gemini is good at single shot queries. @@ -66,7 +84,7 @@ To run this example properly, you'll need two extra API keys: **(Note if either key is missing, the code will fall back to dummy data.)** ```bash -uv run --group examples -m examples.weather +uv run --extra examples -m pydantic_ai_examples.weather ``` This example uses `openai:gpt-4o` by default. Gemini seems to be unable to handle the multiple tool @@ -97,7 +115,7 @@ We also mount the postgresql `data` directory locally to persist the data if you With that running, we can build the search database with (**WARNING**: this requires the `OPENAI_API_KEY` env variable and will calling the OpenAI embedding API around 300 times to generate embeddings for each section of the documentation): ```bash -uv run --group examples -m examples.rag build +uv run --extra examples -m pydantic_ai_examples.rag build ``` (Note building the database doesn't use Pydantic AI right now, instead it uses the OpenAI SDK directly.) @@ -105,5 +123,5 @@ uv run --group examples -m examples.rag build You can then ask the agent a question with: ```bash -uv run --group examples -m examples.rag search "How do I configure logfire to work with FastAPI?" +uv run --extra examples -m pydantic_ai_examples.rag search "How do I configure logfire to work with FastAPI?" ``` diff --git a/pydantic_ai_examples/__main__.py b/pydantic_ai_examples/__main__.py new file mode 100644 index 000000000..1eb847fa3 --- /dev/null +++ b/pydantic_ai_examples/__main__.py @@ -0,0 +1,69 @@ +"""Very simply CLI to aid in running the examples, and for copying examples code to a new directory.""" +import argparse +import os +import re +import sys +from pathlib import Path + + +def cli(): + this_dir = Path(__file__).parent + + parser = argparse.ArgumentParser(prog='pydantic_ai_examples', description=get_description(this_dir), formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('-v', '--version', action='store_true', help='show the version and exit') + parser.add_argument('--copy-to', dest='DEST', help='Copy all examples to a new directory') + + args = parser.parse_args() + if args.version: + from pydantic_ai import __version__ + + print(f'pydantic_ai v{__version__}') + elif args.DEST: + copy_to(this_dir, Path(args.DEST)) + else: + parser.print_help() + + +def get_description(this_dir: Path) -> str: + description = f"""\ +{__doc__} + +The following examples are available: +(you might need to prefix the command you run with `uv run` or similar depending on your environment) + +""" + for file in this_dir.glob('*.py'): + if file.name == '__main__.py': + continue + file_descr = re.match(r'"""(.+)', file.read_text()) + if file_descr: + description += f""" +## {file.name} + +{file_descr.group(1).strip()} + + python -m {this_dir.name}.{file.stem} +""" + + return description + + +def copy_to(this_dir: Path, dst: Path): + if dst.exists(): + print(f'Error: destination path "{dst}" already exists', file=sys.stderr) + sys.exit(1) + + dst.mkdir(parents=True) + + count = 0 + for file in this_dir.glob('*.*'): + with open(file, 'rb') as src: + with open(dst / file.name, 'wb') as dst: + dst.write(src.read()) + count += 1 + + print(f'Copied {count} example files to "{dst}"') + + +if __name__ == '__main__': + cli() diff --git a/examples/pydantic_model.py b/pydantic_ai_examples/pydantic_model.py similarity index 93% rename from examples/pydantic_model.py rename to pydantic_ai_examples/pydantic_model.py index 109e0c3c1..10040997c 100644 --- a/examples/pydantic_model.py +++ b/pydantic_ai_examples/pydantic_model.py @@ -2,7 +2,7 @@ Run with: - uv run --group examples -m examples.pydantic_model + uv run -m pydantic_ai_examples.pydantic_model """ import os diff --git a/examples/rag.py b/pydantic_ai_examples/rag.py similarity index 95% rename from examples/rag.py rename to pydantic_ai_examples/rag.py index 541bbffe2..97c0f1d27 100644 --- a/examples/rag.py +++ b/pydantic_ai_examples/rag.py @@ -1,4 +1,4 @@ -"""RAG example with pydantic-ai. +"""RAG example with pydantic-ai — using vector search to augment a chat agent. Run pgvector with: @@ -10,11 +10,11 @@ Build the search DB with: - uv run --group examples -m examples.rag build + uv run -m pydantic_ai_examples.rag build Ask the agent a question with: - uv run --group examples -m examples.rag search "How do I configure logfire to work with FastAPI?" + uv run -m pydantic_ai_examples.rag search "How do I configure logfire to work with FastAPI?" """ from __future__ import annotations as _annotations @@ -226,5 +226,5 @@ def slugify(value: str, separator: str, unicode: bool = False) -> str: q = 'How do I configure logfire to work with FastAPI?' asyncio.run(run_agent(q)) else: - print('uv run --group examples -m examples.rag build|search', file=sys.stderr) + print('uv run --extra examples -m pydantic_ai_examples.rag build|search', file=sys.stderr) sys.exit(1) diff --git a/examples/sql_gen.py b/pydantic_ai_examples/sql_gen.py similarity index 97% rename from examples/sql_gen.py rename to pydantic_ai_examples/sql_gen.py index de7c06525..624f67bbe 100644 --- a/examples/sql_gen.py +++ b/pydantic_ai_examples/sql_gen.py @@ -2,7 +2,7 @@ Run with: - uv run --group examples -m examples.sql_gen "show me logs from yesterday, with level 'error'" + uv run -m pydantic_ai_examples.sql_gen "show me logs from yesterday, with level 'error'" """ import asyncio diff --git a/examples/weather.py b/pydantic_ai_examples/weather.py similarity index 98% rename from examples/weather.py rename to pydantic_ai_examples/weather.py index e73780257..657e9af54 100644 --- a/examples/weather.py +++ b/pydantic_ai_examples/weather.py @@ -6,7 +6,7 @@ Run with: - uv run --group examples -m examples.weather + uv run -m pydantic_ai_examples.weather """ from __future__ import annotations as _annotations diff --git a/pyproject.toml b/pyproject.toml index 008d397e2..6961dbab6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,10 @@ dependencies = [ logfire = [ "logfire>=1.2.0", ] +examples = [ + "asyncpg>=0.30.0", + "logfire[asyncpg]>=1.2.0", +] [dependency-groups] dev = [ @@ -60,16 +64,12 @@ dev = [ "devtools>=0.12.2", "anyio>=4.5.0", ] -examples = [ - "asyncpg>=0.30.0", - "logfire[asyncpg]>=1.2.0", -] [tool.hatch.build.targets.wheel] -packages = ["pydantic_ai"] +packages = ["pydantic_ai", "pydantic_ai_examples"] [tool.hatch.build.targets.sdist] -include = ["/README.md", "/Makefile", "/pydantic_ai", "/tests"] +include = ["/README.md", "/Makefile", "/pydantic_ai", "/pydantic_ai_examples", "/tests"] [tool.ruff] line-length = 120 diff --git a/uv.lock b/uv.lock index 1f4043ecd..d73c5301f 100644 --- a/uv.lock +++ b/uv.lock @@ -897,6 +897,10 @@ dependencies = [ ] [package.optional-dependencies] +examples = [ + { name = "asyncpg" }, + { name = "logfire", extra = ["asyncpg"] }, +] logfire = [ { name = "logfire" }, ] @@ -914,17 +918,15 @@ dev = [ { name = "pytest-pretty" }, { name = "ruff" }, ] -examples = [ - { name = "asyncpg" }, - { name = "logfire", extra = ["asyncpg"] }, -] [package.metadata] requires-dist = [ + { name = "asyncpg", marker = "extra == 'examples'", specifier = ">=0.30.0" }, { name = "eval-type-backport", specifier = ">=0.2.0" }, { name = "griffe", specifier = ">=1.3.2" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "logfire", marker = "extra == 'logfire'", specifier = ">=1.2.0" }, + { name = "logfire", extras = ["asyncpg"], marker = "extra == 'examples'", specifier = ">=1.2.0" }, { name = "logfire-api", specifier = ">=1.2.0" }, { name = "openai", specifier = ">=1.51.2" }, { name = "pydantic", specifier = ">=2.9.2" }, @@ -943,10 +945,6 @@ dev = [ { name = "pytest-pretty", specifier = ">=1.2.0" }, { name = "ruff", specifier = ">=0.6.9" }, ] -examples = [ - { name = "asyncpg", specifier = ">=0.30.0" }, - { name = "logfire", extras = ["asyncpg"], specifier = ">=1.2.0" }, -] [[package]] name = "pydantic-core"