A production-ready template for building applications with Pydantic AI, FastAPI, and modern Python tooling.
The application follows a layered architecture where user requests flow through multiple services before reaching the LLM provider. The flow is broken down into clear, manageable sections below.
graph LR
User[π€ User] -->|1. Request| FastAPI[π FastAPI]
FastAPI -->|2. Process| Agent[π€ Agent]
Agent -->|3. Route| LiteLLM[π LiteLLM]
LiteLLM -->|4. Call| Provider[π€ LLM Provider]
Provider -->|5. Response| User
style User fill:#e1f5ff
style FastAPI fill:#4CAF50
style Agent fill:#FF9800
style LiteLLM fill:#2196F3
style Provider fill:#9C27B0
π₯ Part 1: Request Entry & Security (Click to expand)
graph TB
User[π€ User Request] -->|HTTP Request| FastAPI[π FastAPI Application]
FastAPI --> Security[π‘οΈ Security Middleware]
Security -->|Rate Limiting| Redis[πΎ Redis Cache]
Security --> Session[π Session Middleware]
Session --> Auth[π Authentication]
Auth -->|Valid| Continue[β
Continue to Agent]
Auth -->|Invalid| Error[β 401 Unauthorized]
style User fill:#e1f5ff
style FastAPI fill:#4CAF50
style Security fill:#FF5722
style Redis fill:#F44336
style Auth fill:#FF9800
style Error fill:#F44336
style Continue fill:#4CAF50
What happens here:
- FastAPI receives the HTTP request
- Security middleware checks rate limits (stored in Redis)
- Session middleware manages user sessions
- JWT token is validated for authentication
- Invalid requests are rejected immediately
π Part 2: Prompt Retrieval & Caching (Click to expand)
graph TB
Agent[π€ Pydantic AI Agent] --> PromptService[π Prompt Service]
PromptService -->|1. Check Cache| Redis[πΎ Redis Cache]
Redis -->|Cache Hit| Cached[β
Return Cached Prompt]
Redis -->|Cache Miss| DB[(ποΈ PostgreSQL)]
DB -->|2. Load Prompt| PromptDB[π Prompt Content]
PromptDB -->|3. Store in Cache| Redis
PromptDB -->|Return| Agent
Cached --> Agent
style Agent fill:#FF9800
style PromptService fill:#2196F3
style Redis fill:#F44336
style DB fill:#607D8B
style Cached fill:#4CAF50
What happens here:
- Agent requests a prompt by slug/identifier
- Prompt Service first checks Redis cache (fast)
- If not cached, loads from PostgreSQL database
- Caches the prompt in Redis for future requests
- Uses active version from prompt versioning system
π Part 3: LLM Routing via LiteLLM (Click to expand)
graph TB
Agent[π€ Pydantic AI Agent] -->|Request with Prompt| LiteLLM[π LiteLLM Proxy]
LiteLLM -->|Route| Router{Select Provider}
Router -->|OpenAI| OpenAI[OpenAI API]
Router -->|Anthropic| Anthropic[Anthropic API]
Router -->|Google| Google[Google API]
Router -->|Other| Other[Other Providers]
OpenAI -->|Response| LiteLLM
Anthropic -->|Response| LiteLLM
Google -->|Response| LiteLLM
Other -->|Response| LiteLLM
LiteLLM -->|Usage Tracking| DB[(ποΈ PostgreSQL)]
LiteLLM -->|Response| Agent
style Agent fill:#FF9800
style LiteLLM fill:#2196F3
style Router fill:#9C27B0
style OpenAI fill:#10A37F
style Anthropic fill:#D4A574
style Google fill:#4285F4
style DB fill:#607D8B
What happens here:
- Agent sends the request to LiteLLM Proxy
- LiteLLM routes to the configured provider
- Handles load balancing and failover
- Tracks usage and costs in PostgreSQL
- Returns the LLM response to the agent
π€ Part 4: Response Flow & Observability (Click to expand)
graph TB
Agent[π€ Pydantic AI Agent] -->|Process Response| Logfire[π Logfire]
Logfire -->|Logs & Metrics| Monitoring[π Monitoring Dashboard]
Logfire -->|Traces| Tracing[π Request Tracing]
Agent -->|Final Response| FastAPI[π FastAPI]
FastAPI -->|JSON Response| User[π€ User]
style Agent fill:#FF9800
style Logfire fill:#FFC107
style Monitoring fill:#4CAF50
style Tracing fill:#2196F3
style FastAPI fill:#4CAF50
style User fill:#e1f5ff
What happens here:
- Agent processes and structures the LLM response
- Logfire captures logs, metrics, and traces
- Response flows back through FastAPI
- User receives the final JSON response
- All metrics available in monitoring dashboard
- FastAPI: Modern, fast web framework for building APIs
- Pydantic AI Agent: Intelligent agent framework for LLM interactions
- LiteLLM Proxy: Unified interface for managing multiple LLM providers
- PostgreSQL: Stores prompts, versions, user data, and usage metrics
- Redis: Provides caching for prompts and session management
- Logfire: Observability platform for monitoring and debugging
- Python >= 3.13
- uv package manager
Install project dependencies:
make installFor development, install with dev dependencies:
make install-devThe application uses environment-specific configuration files. Create .env.development for development and .env.production for production.
The application automatically loads configuration from:
.env.developmentwhenENVIRONMENT=development(default).env.productionwhenENVIRONMENT=production
Create your environment file (.env.development or .env.production) with the following variables:
Configure Logfire for observability and monitoring:
- Sign in to https://logfire.pydantic.dev
- Go to Projects
- Click New project
- Add project name and select visibility to Private
- After this you will be redirected to Settings page of the project you have created
- Go to Write tokens and press on "New write token" to create a new token
- Copy the token and add it to your
.env.development/productionfile as:
LOGFIRE_TOKEN=your_token_hereJWT settings (defaults provided, but recommended to set in production):
JWT_SECRET_KEY=your_secret_key_here # Auto-generated if not provided
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30- Start PostgreSQL (using Docker for development):
make docker-dev-up- Run migrations:
make migration-upgrade- Create a superuser:
make createsuperuser- Never commit
.env.developmentor.env.productionfiles to version control - Use strong, unique passwords in production
- Generate secure
JWT_SECRET_KEYfor production (usesecrets.token_hex(32)or similar) - Restrict
ALLOWED_ORIGINSin production to your actual domains - Keep
DEBUG=falsein production
Pre-commit won't run automatically until you actually install the hooks into .git/hooks. Run the installer once (it's not in git history) so Git knows to invoke them:
uv run pre-commit install --hook-type pre-commit --hook-type commit-msgAfter that, every git commit will trigger the lint/format checks plus the Commitizen commit-msg hook from your .pre-commit-config.yaml. If you ever need to lint everything manually, use:
uv run pre-commit run --all-filesAlternatively, you can use the Makefile targets:
make pre-commit-install # Install pre-commit hooks
make pre-commit-run # Run pre-commit hooks on all filesThis project uses Commitizen (GitHub) to ensure consistent and semantic commit messages following the Conventional Commits standard.
Instead of using git commit, use Commitizen's interactive CLI:
uv run cz commitThis will guide you through creating a properly formatted commit message with the following prompts:
- Type: Select the type of change you are committing (e.g.,
fix: A bug fix. Correlates with PATCH in SemVer) - Scope (optional): What is the scope of this change? (class or file name) - Press Enter to skip
- Subject: Write a short and imperative summary of the code changes (lower case and no period)
- Body (optional): Provide additional contextual information about the code changes - Press Enter to skip
- Breaking Change: Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer (Yes/No)
- Footer (optional): Information about Breaking Changes and reference issues that this commit closes - Press Enter to skip
# Stage your changes
git add .
# Use Commitizen to create a commit
uv run cz commit
# Example interactive prompts:
# ? Select the type of change you are committing: feat
# feat: A new feature. Correlates with MINOR in SemVer
# ? What is the scope of this change? (class or file name): (press [enter] to skip)
# api
# ? Write a short and imperative summary of the code changes: (lower case and no period)
# add user authentication endpoint
# ? Provide additional contextual information about the code changes: (press [enter] to skip)
# [Enter]
# ? Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer
# No
# ? Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)
# [Enter]
# Result: feat(api): add user authentication endpointThe standard format is:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat: A new featurefix: A bug fixdocs: Documentation changesstyle: Code style changes (formatting, missing semicolons, etc.)refactor: Code refactoring without changing functionalityperf: Performance improvementstest: Adding or updating testschore: Maintenance tasks (build, CI/CD, dependencies)ci: CI/CD configuration changesbuild: Build system changes
Examples:
feat(auth): add JWT authentication
fix(database): resolve connection pool timeout
docs(readme): update installation instructions
test(api): add integration tests for health endpoint- β Automatically validates commit messages
- β Enables semantic versioning and automatic changelog generation
- β Improves collaboration through clear commit history
- β Enforced by pre-commit hooks (commit-msg stage)
For more information about Commitizen, visit:
- Documentation: https://commitizen-tools.github.io/commitizen/
- GitHub Repository: https://github.com/commitizen-tools/commitizen
- Conventional Commits Specification: https://www.conventionalcommits.org/
Run make help to see all available commands, or use:
make install- Install project dependenciesmake install-dev- Install project dependencies including dev dependencies
make run- Run the application in production modemake run-dev- Run the application in development mode with auto-reload
make format- Format code using Ruffmake test- Run tests using pytestmake test-cov- Run tests with coverage reportmake pre-commit-install- Install pre-commit hooksmake pre-commit-run- Run pre-commit hooks on all files
make migration-create MESSAGE="message"- Create a new migrationmake migration-upgrade- Upgrade database to the latest migrationmake migration-downgrade- Downgrade database by one revisionmake migration-current- Show current database revisionmake migration-history- Show migration historymake createsuperuser- Create a superuser account (interactive)
make docker-dev-up- Start development Docker servicesmake docker-dev-down- Stop development Docker servicesmake docker-dev-logs- View development Docker services logsmake docker-dev-restart- Restart development Docker servicesmake docker-up- Start production Docker servicesmake docker-down- Stop production Docker servicesmake docker-logs- View production Docker services logsmake docker-restart- Restart production Docker services
The application includes a built-in admin panel for managing prompts, users, and environment variables.
-
Start the application:
make run-dev
-
Navigate to
http://localhost:8000/adminin your browser. -
Log in using your superuser credentials (create one using
make createsuperuserif you haven't already).
Secure authentication to access the admin panel.
Manage system users, roles, and permissions.
Securely manage environment variables and configuration settings directly from the admin interface.
Create, edit, and version control your AI prompts with commit messages and easy rollback capabilities.
Agent instructions are stored as prompts so you can edit them from the admin panel without changing code. The default main_agent uses the slug main_agent_instructions:
@main_agent.instructions
async def main_agent_instructions(
ctx: RunContext[MainAgentDependencies],
) -> str:
"""Main agent instructions."""
content = await PromptService.get_cached_content(
session=ctx.deps.session,
redis=ctx.deps.redis,
slug="main_agent_instructions",
)
return content or ""To update these instructions:
- Open the Prompt Versioning Control page in the admin panel.
- Locate or create a prompt with the slug
main_agent_instructions. - Edit the content and add a commit message.
- Save (and optionally activate) the new version. The agent will now use the updated instructions. Cached content is refreshed automatically when versions change.
The application includes Grafana for monitoring and observability with pre-configured dashboards for container metrics.
-
Start the Docker services (includes Grafana and Prometheus):
make docker-dev-up
-
Navigate to
http://localhost:3000in your browser. -
Log in using the default credentials (or your configured credentials from
.env.development):- Username:
admin(orGF_ADMIN_USERfrom your env file) - Password:
admin(orGF_ADMIN_PASSWORDfrom your env file)
- Username:
Grafana is pre-configured with:
- Prometheus Data Source: Automatically configured to connect to the Prometheus service at
http://prometheus:9090 - Container Monitoring Dashboard: Pre-provisioned dashboard for Docker container metrics
Configure Grafana in your .env.development or .env.production file:
# Grafana Configuration
GF_ADMIN_USER=admin # Grafana admin username
GF_ADMIN_PASSWORD=admin # Grafana admin password
GF_USERS_ALLOW_SIGN_UP=false # Disable user signup (recommended for production)Grafana runs on port 3000 by default. You can change this by setting the GRAFANA_PORT environment variable in your docker-compose configuration.
The pre-configured dashboard provides insights into:
- Container CPU usage
- Memory consumption
- Network traffic
- Disk I/O metrics
- Log into Grafana at
http://localhost:3000 - Navigate to Dashboards in the left sidebar
- You can edit existing dashboards or create new ones
- Custom dashboards are stored in
./grafana/provisioning/dashboards/json/for persistence
The application includes LiteLLM Proxy, a unified interface to manage multiple LLM providers, enabling easy model switching, cost tracking, and usage monitoring.
-
Start the Docker services (includes LiteLLM Proxy):
make docker-dev-up
-
Navigate to
http://localhost:4000in your browser. -
Log in using your configured credentials from
.env.development:- Username:
admin(orLITELLM_UI_USERNAMEfrom your env file) - Password:
password(orLITELLM_UI_PASSWORDfrom your env file)
- Username:
LiteLLM is pre-configured with:
- PostgreSQL Database: Stores model configurations and usage data
- Redis Cache: Enables response caching for improved performance
- Cost Tracking: Automatically tracks costs per deployment/model
Configure LiteLLM in your .env.development or .env.production file:
# LiteLLM Configuration
LITELLM_PROXY_ADMIN_ID=admin # Admin user ID
LITELLM_MASTER_KEY=sk-password # Master key (must start with "sk-")
LITELLM_UI_USERNAME=admin # UI login username
LITELLM_UI_PASSWORD=password # UI login password
LITELLM_API_KEY= # Virtual API key (optional, can be set via admin panel)Note: You can also set LITELLM_API_KEY (and other environment variables) directly from the admin panel. See the Setting LITELLM_API_KEY from Admin Panel section below for detailed instructions.
The LiteLLM configuration file is located at ./litellm/litellm.yaml. You can edit this file to add or modify model configurations.
You can add models to LiteLLM in two ways:
Option A: Via LiteLLM UI (Recommended)
- Navigate to
http://localhost:4000and log in - Go to Models section
- Click Add Model or Add Endpoint
- Configure your model:
- Select the provider (OpenAI, Anthropic, Google, etc.)
- Enter your API key
- Set the model name (e.g.,
gpt-4,claude-3-opus,gemini-pro) - Configure any additional settings
- Save the configuration
Option B: Via Configuration File
Edit ./litellm/litellm.yaml and add your model configuration:
model_list:
- model_name: gpt-4
litellm_params:
model: gpt-4
api_key: your-api-key-here
- model_name: claude-3-opus
litellm_params:
model: claude-3-opus
api_key: your-anthropic-key-hereAfter adding models via the config file (ONLY), restart the LiteLLM service:
make docker-dev-restartOnce you've added your model in LiteLLM, you can use it in your agents by updating the model name in the agent definition.
For example, in src/core/agentic_system/agents/main_agent.py:
from attr import dataclass
from pydantic_ai import Agent, ModelSettings
from src.core.agentic_system.utils import get_chat_model
@dataclass
class MainAgentDependencies:
"""Main agent dependencies."""
user_name: str
main_agent = Agent[MainAgentDependencies, str](
name="main_agent",
model=get_chat_model("your-model-name-in-litellm", ModelSettings(temperature=0.3)),
deps_type=MainAgentDependencies,
)Important Notes:
- Replace
"your-model-name-in-litellm"with the exact model name you configured in LiteLLM - The model name must match exactly what you set in LiteLLM (case-sensitive)
- You can adjust
ModelSettingsparameters liketemperature,max_tokens, etc. - The
get_chat_model()function automatically connects to your LiteLLM proxy at the configured base URL
Example:
If you added a model named gpt-4 in LiteLLM, your code would look like:
main_agent = Agent[MainAgentDependencies, str](
name="main_agent",
model=get_chat_model("gpt-4", ModelSettings(temperature=0.3)),
deps_type=MainAgentDependencies,
)Configure and manage LLM models from various providers (OpenAI, Anthropic, Google, etc.) and set up custom endpoints.
Create and manage virtual API keys for different teams or projects, enabling usage tracking and access control.
After creating a virtual API key from the LiteLLM dashboard, you can set it as an environment variable using the admin panel:
-
Create a Virtual API Key in LiteLLM Dashboard:
- Navigate to
http://localhost:4000and log in - Go to API Keys section
- Click Create API Key or Add Key
- Configure the key settings (team, budget, etc.)
- Copy the generated API key
- Navigate to
-
Set the API Key via Admin Panel:
- Navigate to
http://localhost:8000/adminand log in with your superuser credentials (create one usingmake createsuperuserif you haven't already) - Go to Env Settings in the admin panel
- Add or update the
LITELLM_API_KEYenvironment variable:- Key:
LITELLM_API_KEY - Value: Paste the virtual API key you created from the LiteLLM dashboard
- Key:
- Click Update to save
- Navigate to
-
Restart the Application: After setting the environment variable, restart your application for the changes to take effect:
make docker-dev-restart
The application will now use this virtual API key when making requests to LiteLLM, enabling usage tracking and access control for your specific team or project.
Monitor usage statistics for each team or API key, including token consumption, costs, and request metrics.
LiteLLM Proxy runs on port 4000 by default. You can change this by setting the LITELLM_PORT environment variable in your docker-compose configuration.
[Add your license here]







