Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions samples/pydantic-ai-extended/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
.env
.venv/
98 changes: 98 additions & 0 deletions samples/pydantic-ai-extended/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Pydantic AI Extended

[![1-click-deploy](https://raw.githubusercontent.com/DefangLabs/defang-assets/main/Logos/Buttons/SVG/deploy-with-defang.svg)](https://portal.defang.dev/redirect?url=https%3A%2F%2Fgithub.com%2Fnew%3Ftemplate_name%3Dsample-pydantic-ai-extended-template%26template_owner%3DDefangSamples)

This sample shows a multi-service AI app built with [Pydantic AI](https://ai.pydantic.dev/), FastAPI, PostgreSQL (with pgvector), and Redis, deployed with Defang from a single Docker Compose file.

A background worker generates sample support tickets and system alerts with an LLM, pushes them to a queue, then they get classified and stored with vector embeddings in PostgreSQL for semantic search. A Pydantic AI copilot agent uses tools to inspect the current state before answering questions.

## Prerequisites

1. Download [Defang CLI](https://github.com/DefangLabs/defang)
2. (Optional) If you are using [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) authenticate with your cloud provider account
3. (Optional for local development) [Docker CLI](https://docs.docker.com/engine/install/)

## Development

To run the application locally for development, use the development compose file:

```bash
docker compose -f compose.dev.yaml up --build
```

This will:

- Start the FastAPI app on `http://localhost:8000`
- Start PostgreSQL on port 5432
- Start Redis on port 6379
- Start a background worker for item classification
- Start Docker model-provider services for chat + embeddings

Local development uses:

- `ai/qwen2.5:3B-Q4_K_M` for chat/tool-calling
- `mxbai-embed-large` for embeddings

This relies on Docker Model Runner / model-provider support being available in your local Docker installation. The first run will download both models, so startup can take a few minutes.
If `docker compose -f compose.dev.yaml up` fails with `exec: "model": executable file not found in $PATH`, your local Docker installation does not have Docker Model Runner enabled yet.
To keep iteration practical on CPU-only setups, `compose.dev.yaml` enables `LOCAL_FAST_DATA=true`, which uses deterministic sample generation and classification locally while still exercising the real chat and embedding services.

In deployed environments, the app uses dedicated `chat` and `embedding` model services defined in `compose.yaml`. Defang injects OpenAI-compatible `CHAT_URL` / `CHAT_MODEL` and `EMBEDDING_URL` / `EMBEDDING_MODEL` environment variables automatically, so the application code stays platform-independent.

## Configuration

For this sample, you will need to provide the following [configuration](https://docs.defang.io/docs/concepts/configuration). Note that if you are using the 1-click deploy option, you can set these values as secrets in your GitHub repository and the action will automatically deploy them for you.

### `POSTGRES_PASSWORD`

The password for your PostgreSQL database. You need to set this before deploying for the first time.

*You can easily set this to a random string using `defang config set POSTGRES_PASSWORD --random`*

## Usage

1. Open the app.
2. Click **Generate sample items**.
3. Watch the worker create 10 tickets and 10 alerts, then fan out per-item classify/embed jobs (progress updates in real time via polling).
4. Ask questions like:
- `What should I look at first?`
- `Summarize the current tickets and alerts.`
- `Which items seem related?`
- `Find alerts similar to the payment outage.`

## Deployment

> [!NOTE]
> Download [Defang CLI](https://github.com/DefangLabs/defang)

### Defang Playground

Deploy your application to the Defang Playground by opening up your terminal and typing:

```bash
defang compose up
```

### BYOC (Deploy to your own AWS or GCP cloud account)

If you want to deploy to your own cloud account, you can [use Defang BYOC](https://docs.defang.io/docs/tutorials/deploy-to-your-cloud).

The default sample uses Defang's managed model provider services:

- `chat` uses `chat-default`
- `embedding` uses `embedding-default`

If you want to pin different models, edit the `provider.options.model` values in [compose.yaml](compose.yaml).

> [!WARNING]
> **Extended deployment time:** This sample creates a managed PostgreSQL database which may take upwards of 20 minutes to provision on first deployment. Subsequent deployments are much faster (2-5 minutes).

---

Title: Pydantic AI Extended

Short Description: A Defang sample where background jobs classify and embed support tickets and system alerts, and a Pydantic AI copilot answers questions with tools.

Tags: Pydantic AI, FastAPI, PostgreSQL, Redis, AI, Agents

Languages: Python, Docker
3 changes: 3 additions & 0 deletions samples/pydantic-ai-extended/app/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
*.pyc
.venv
18 changes: 18 additions & 0 deletions samples/pydantic-ai-extended/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:3.12-slim-bookworm

RUN apt-get update -qq \
&& apt-get install -y curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt ./

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file.
163 changes: 163 additions & 0 deletions samples/pydantic-ai-extended/app/agents/copilot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
Pydantic AI copilot agent with tools for inspecting tickets, alerts, tags,
and semantic matches before answering questions.
"""

from __future__ import annotations

from dataclasses import dataclass

from pydantic_ai import Agent, RunContext

from lib.ai import embed_text_for_search
from lib.items import (
ItemFilters,
get_available_tags,
get_items_by_type,
search_items_by_embedding,
)
from lib.model import get_chat_model


@dataclass
class CopilotDeps:
"""No external deps needed; tools query the database directly."""
pass


copilot_agent = Agent(
get_chat_model,
deps_type=CopilotDeps,
instructions="""
You are the copilot for a demo app that tracks support tickets and system alerts.
Tickets are customer issues from tools like Zendesk, Intercom, Jira, Linear, and GitHub Issues.
Alerts are system notifications from tools like Datadog, PagerDuty, Sentry, Vercel, and Stripe.

Use tools before answering:
- Use get_tickets for questions about customer issues, bugs, blockers, status, owners, priorities, categories, or tags.
- Use get_alerts for questions about system alerts, incidents, deploys, monitoring, sources, categories, or tags.
- Use get_tags to discover the classification vocabulary before filtering by tags or explaining patterns.
- Use search_items when the user asks about similar issues, related items, or semantic matches.

Investigation pattern:
1. Start with the smallest useful tool call.
2. If the first result is broad or ambiguous, refine with status, source, priority, category, tag, assignee, or query filters.
3. For pattern questions, inspect tags first, then query tickets/alerts or search similar items.
4. For cross-cutting questions, use at least two tools when it materially improves the answer.
5. Stop once the evidence is sufficient; do not call tools just to use every tool.

Final answer rules:
- Base every final answer on tool output only.
- Mention exact ticket/alert titles, owners or sources, priorities, categories, and tags when relevant.
- If the user asks which items match a status or priority, name the exact matching items.
- Keep the final answer concise and practical.
If the system has no items yet, tell the user to generate sample items first.
""",
)


@copilot_agent.tool
async def get_tickets(
ctx: RunContext[CopilotDeps],
status: str | None = None,
assignee: str | None = None,
source: str | None = None,
category: str | None = None,
priority: str | None = None,
tag: str | None = None,
query: str | None = None,
limit: int = 10,
) -> list[dict]:
"""Fetch support tickets with optional filters."""
filters = ItemFilters(
status=status,
assignee=assignee,
source=source,
category=category,
priority=priority,
tag=tag,
query=query,
)
items = await get_items_by_type("ticket", limit=limit, filters=filters)
return [
{
"id": i.id,
"source": i.source,
"title": i.title,
"body": i.body,
"status": i.status,
"assignee": i.assignee,
"category": i.category,
"priority": i.priority,
"tags": i.tags,
}
for i in items
]


@copilot_agent.tool
async def get_alerts(
ctx: RunContext[CopilotDeps],
source: str | None = None,
category: str | None = None,
priority: str | None = None,
tag: str | None = None,
query: str | None = None,
limit: int = 10,
) -> list[dict]:
"""Fetch system alerts with optional filters."""
filters = ItemFilters(
source=source,
category=category,
priority=priority,
tag=tag,
query=query,
)
items = await get_items_by_type("alert", limit=limit, filters=filters)
return [
{
"id": i.id,
"source": i.source,
"title": i.title,
"body": i.body,
"category": i.category,
"priority": i.priority,
"tags": i.tags,
}
for i in items
]


@copilot_agent.tool
async def get_tags(
ctx: RunContext[CopilotDeps],
item_type: str | None = None,
) -> list[dict]:
"""Get all classification tags with their counts. Optionally filter by item_type ('ticket' or 'alert')."""
t = item_type if item_type in ("ticket", "alert") else None
return await get_available_tags(t)


@copilot_agent.tool
async def search_items(
ctx: RunContext[CopilotDeps],
search_query: str,
item_type: str | None = None,
limit: int = 5,
) -> list[dict]:
"""Semantic search across tickets and alerts. Returns the most similar items."""
embedding = await embed_text_for_search(search_query)
t = item_type if item_type in ("ticket", "alert") else None
results = await search_items_by_embedding(embedding, item_type=t, limit=limit)
return [
{
"title": r["item"].title,
"source": r["item"].source,
"body": r["item"].body,
"category": r["item"].category,
"priority": r["item"].priority,
"tags": r["item"].tags,
"score": r["score"],
}
for r in results
]
Empty file.
Loading
Loading