Simple middleware library for Pydantic-AI - before/after hooks without imposed guardrails structure
Project description
pydantic-ai-middleware
Looking for a complete agent framework? Check out pydantic-deep - a full-featured deep agent framework with planning, subagents, and skills system built on pydantic-ai.
Need task planning tools? Check out pydantic-ai-todo - todo/task planning toolset that works with any pydantic-ai agent.
Need file storage or Docker sandbox? Check out pydantic-ai-backend - file storage and sandbox backends that work with any pydantic-ai agent.
Simple middleware library for Pydantic-AI - clean before/after hooks without imposed guardrails structure.
Features
- Clean Middleware API - Simple before/after hooks at every lifecycle stage
- No Imposed Structure - You decide what to do (logging, guardrails, metrics, transformations)
- Full Control - Modify prompts, outputs, tool calls, and handle errors
- Decorator Support - Simple decorators for quick middleware creation
- Type Safe - Full typing support with generics for dependencies
Installation
pip install pydantic-ai-middleware
Or with uv:
uv add pydantic-ai-middleware
Quick Start
from pydantic_ai import Agent
from pydantic_ai_middleware import MiddlewareAgent, AgentMiddleware, InputBlocked
class SecurityMiddleware(AgentMiddleware[None]):
"""Block dangerous inputs."""
async def before_run(self, prompt, deps):
if "dangerous" in prompt.lower():
raise InputBlocked("Dangerous content detected")
return prompt
class LoggingMiddleware(AgentMiddleware[None]):
"""Log agent activity."""
async def before_run(self, prompt, deps):
print(f"Starting: {prompt[:50]}...")
return prompt
async def after_run(self, prompt, output, deps):
print(f"Finished: {output}")
return output
# Create base agent
base_agent = Agent('openai:gpt-4o')
# Wrap with middleware
agent = MiddlewareAgent(
agent=base_agent,
middleware=[
LoggingMiddleware(),
SecurityMiddleware(),
],
)
# Use normally
result = await agent.run("Hello, how are you?")
Middleware Hooks
| Hook | When Called | Can Modify |
|---|---|---|
before_run |
Before agent starts | Prompt |
after_run |
After agent finishes | Output |
before_model_request |
Before each model call | Messages |
before_tool_call |
Before tool execution | Tool arguments |
after_tool_call |
After tool execution | Tool result |
on_error |
When error occurs | Exception |
Decorator Syntax
For simple cases, use decorators:
from pydantic_ai_middleware import before_run, after_run, before_tool_call, ToolBlocked
@before_run
async def log_input(prompt, deps):
print(f"Input: {prompt}")
return prompt
@after_run
async def log_output(prompt, output, deps):
print(f"Output: {output}")
return output
@before_tool_call
async def validate_tools(tool_name, tool_args, deps):
if tool_name == "dangerous_tool":
raise ToolBlocked(tool_name, "Not allowed")
return tool_args
Middleware Execution Order
Middleware executes in order for before_* hooks and reverse order for after_* hooks:
agent = MiddlewareAgent(
agent=base_agent,
middleware=[
RateLimitMiddleware(), # 1st before, last after
LoggingMiddleware(), # 2nd before, 2nd-to-last after
SecurityMiddleware(), # 3rd before, 3rd-to-last after
],
)
# before_run order: RateLimit -> Logging -> Security -> [Agent]
# after_run order: [Agent] -> Security -> Logging -> RateLimit
Example Middleware
Rate Limiting
import time
from pydantic_ai_middleware import AgentMiddleware
class RateLimitMiddleware(AgentMiddleware[None]):
def __init__(self, max_calls: int = 10, window: int = 60):
self.max_calls = max_calls
self.window = window
self._calls: list[float] = []
async def before_run(self, prompt, deps):
now = time.time()
self._calls = [t for t in self._calls if now - t < self.window]
if len(self._calls) >= self.max_calls:
raise Exception("Rate limit exceeded")
self._calls.append(now)
return prompt
Tool Authorization
from pydantic_ai_middleware import AgentMiddleware, ToolBlocked
class ToolAuthMiddleware(AgentMiddleware[MyDeps]):
dangerous_tools = {"delete_file", "execute_code", "send_email"}
async def before_tool_call(self, tool_name, tool_args, deps):
if tool_name in self.dangerous_tools:
if not deps.user.is_admin:
raise ToolBlocked(tool_name, "Requires admin privileges")
return tool_args
Error Handling
from pydantic_ai_middleware import AgentMiddleware
class ErrorHandlerMiddleware(AgentMiddleware[MyDeps]):
async def on_error(self, error, deps):
# Log error
await error_tracker.report(error, user_id=deps.user_id)
# Convert to user-friendly message
if isinstance(error, RateLimitError):
return UserFacingError("Service busy, try later")
return None # Re-raise original
Building Guardrails
This library provides flexible building blocks for implementing guardrails without imposing a rigid structure. You decide what guardrails you need and how they behave.
Input Validation Guardrail
from pydantic_ai_middleware import AgentMiddleware, InputBlocked
class InputValidationGuardrail(AgentMiddleware[MyDeps]):
"""Validate and sanitize user input before processing."""
async def before_run(self, prompt, deps):
# Check for profanity
if has_profanity(prompt):
raise InputBlocked("Inappropriate content detected")
# Redact PII (emails, phone numbers, SSNs)
prompt = redact_pii(prompt)
# Length validation
if len(prompt) > 10000:
raise InputBlocked("Input too long")
return prompt
Content Moderation Guardrail
Use a separate AI model to moderate content before processing:
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai_middleware import AgentMiddleware, InputBlocked
class ModerationResult(BaseModel):
is_safe: bool
reason: str | None = None
class ContentModerationGuardrail(AgentMiddleware[None]):
"""Use AI to moderate content before processing."""
def __init__(self):
self.moderator = Agent(
'openai:gpt-4o-mini',
output_type=ModerationResult,
system_prompt="Analyze if the content is safe. Return is_safe=False for harmful content.",
)
async def before_run(self, prompt, deps):
result = await self.moderator.run(str(prompt))
if not result.output.is_safe:
raise InputBlocked(result.output.reason or "Content not allowed")
return prompt
PII Redaction Guardrail
import re
from pydantic_ai_middleware import AgentMiddleware
class PIIRedactionGuardrail(AgentMiddleware[None]):
"""Redact personally identifiable information from prompts and outputs."""
patterns = {
'email': r'\b[\w.-]+@[\w.-]+\.\w+\b',
'phone': r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b',
'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
}
def redact(self, text: str) -> str:
for name, pattern in self.patterns.items():
text = re.sub(pattern, f'[{name.upper()}_REDACTED]', text)
return text
async def before_run(self, prompt, deps):
if isinstance(prompt, str):
return self.redact(prompt)
return prompt
async def after_run(self, prompt, output, deps):
if isinstance(output, str):
return self.redact(output)
return output
Audit Logging Guardrail
from datetime import datetime
from pydantic_ai_middleware import AgentMiddleware
class AuditGuardrail(AgentMiddleware[MyDeps]):
"""Log all agent activity for compliance and debugging."""
async def before_run(self, prompt, deps):
await audit_log.record(
user_id=deps.user_id,
action="agent:start",
input_summary=str(prompt)[:100],
timestamp=datetime.now(),
)
return prompt
async def before_tool_call(self, tool_name, tool_args, deps):
await audit_log.record(
user_id=deps.user_id,
action=f"tool:{tool_name}",
params=tool_args,
timestamp=datetime.now(),
)
return tool_args
async def after_run(self, prompt, output, deps):
await audit_log.record(
user_id=deps.user_id,
action="agent:complete",
output_summary=str(output)[:100],
timestamp=datetime.now(),
)
return output
Middleware vs Traditional Guardrails
| Aspect | Middleware (this library) | Traditional Guardrails |
|---|---|---|
| Complexity | Low | High |
| Structure | No imposed structure | Fixed result types, actions |
| Flexibility | Maximum | Constrained by design |
| Learning curve | Flat | Steeper |
| Built-in guardrails | None (you build what you need) | Pre-built (PII, moderation) |
| Parallel execution | Manual (use asyncio) | Often built-in |
| Type safety | Full generics support | Varies |
When to Use This Library
- You want simple hooks without complex abstractions
- You need full control over behavior
- You prefer building custom guardrails over using pre-built ones
- Your use case is logging, metrics, rate limiting, or basic validation
- You want minimal dependencies and learning curve
When to Consider Full Guardrails Libraries
- You need pre-built guardrails (PII detection, content moderation)
- You want parallel execution of multiple guardrails
- You need human-in-the-loop approval workflows
- You prefer a standardized API with built-in retry logic
Development
# Install dependencies
make install
# Run tests
make test
# Run all checks
make all
Related Projects
- pydantic-ai - The foundation: Agent framework by Pydantic
- pydantic-deep - Full agent framework with planning, subagents, and skills
- pydantic-ai-todo - Todo/task planning toolset for agents
- pydantic-ai-backend - File storage and sandbox backends
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_ai_middleware-0.1.0.tar.gz.
File metadata
- Download URL: pydantic_ai_middleware-0.1.0.tar.gz
- Upload date:
- Size: 251.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52815b6c0c443241600cc8e8c77dfc368a7ca624090549414e0494e46a1758d6
|
|
| MD5 |
a55f19e5d736b50745b7118b9954397e
|
|
| BLAKE2b-256 |
54e75af8b6adc6b8f8ff5ee586c1ee344f51d1e5daf737b9f6626b81ba3b52c4
|
Provenance
The following attestation bundles were made for pydantic_ai_middleware-0.1.0.tar.gz:
Publisher:
publish.yml on vstorm-co/pydantic-ai-middleware
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_ai_middleware-0.1.0.tar.gz -
Subject digest:
52815b6c0c443241600cc8e8c77dfc368a7ca624090549414e0494e46a1758d6 - Sigstore transparency entry: 780920535
- Sigstore integration time:
-
Permalink:
vstorm-co/pydantic-ai-middleware@644b2849b54580271493bf9db6f5b48b0791b8cf -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/vstorm-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@644b2849b54580271493bf9db6f5b48b0791b8cf -
Trigger Event:
release
-
Statement type:
File details
Details for the file pydantic_ai_middleware-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pydantic_ai_middleware-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
928b1f02f63d6bc38363700a8716fb22dc55bf895bfa9cade16b218477781c5a
|
|
| MD5 |
b4b987b3260bd7b1df0aa805e1c6d2cf
|
|
| BLAKE2b-256 |
66ae64bd0d4df3c75b8293dc98dcd2a811d9254e8d78d85987dbb7c2549989c7
|
Provenance
The following attestation bundles were made for pydantic_ai_middleware-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on vstorm-co/pydantic-ai-middleware
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_ai_middleware-0.1.0-py3-none-any.whl -
Subject digest:
928b1f02f63d6bc38363700a8716fb22dc55bf895bfa9cade16b218477781c5a - Sigstore transparency entry: 780920536
- Sigstore integration time:
-
Permalink:
vstorm-co/pydantic-ai-middleware@644b2849b54580271493bf9db6f5b48b0791b8cf -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/vstorm-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@644b2849b54580271493bf9db6f5b48b0791b8cf -
Trigger Event:
release
-
Statement type: