14 Middleware
Hotel Booking with Priority Member Middleware
This notebook demonstrates function-based middleware using the Microsoft Agent Framework. We build upon the conditional workflow example by adding a middleware layer that gives priority members special privileges.
What You'll Learn:
- Function-Based Middleware: Intercept and modify function results
- Context Access: Read and modify
context.resultafter execution - Business Logic Implementation: Priority member benefits
- Result Override: Change function outcomes based on user status
- Same Workflow, Different Outcomes: Middleware-driven behavior changes
Workflow Architecture with Middleware:
User Input: "I want to book a hotel in Paris"
โ
[availability_agent]
- Calls hotel_booking tool
- ๐ priority_check middleware intercepts
- Checks user membership status
- IF priority + no rooms โ Override to available!
- Returns BookingCheckResult
โ
Conditional Routing
/ \
[has_availability] [no_availability]
โ โ
[booking_agent] [alternative_agent]
(Priority override!) (Regular users)
โ โ
[display_result executor]
Key Difference from Conditional Workflow:
Without Middleware (14-conditional-workflow.ipynb):
- Paris has no rooms โ Route to alternative_agent
With Middleware (this notebook):
- Regular user + Paris โ No rooms โ Route to alternative_agent
- Priority user + Paris โ ๐ Middleware overrides! โ Available โ Route to booking_agent
Prerequisites:
- Microsoft Agent Framework installed
- Understanding of conditional workflows (see 14-conditional-workflow.ipynb)
- GitHub token or OpenAI API key
- Basic understanding of middleware patterns
โ All imports successful!
Step 1: Define Pydantic Models for Structured Outputs
These models define the schema that agents will return. We've added a priority_override field to track when middleware modifies the availability result.
โ Pydantic models defined: - BookingCheckResult (availability check with priority_override) - AlternativeResult (alternative suggestion) - BookingConfirmation (booking confirmation)
Step 2: Define Priority Members Database
For this demo, we'll simulate a priority membership database. In production, this would query a real database or API.
Priority Members:
alice@example.com- VIP memberbob@example.com- Premium memberpriority_user- Test account
โ Priority members database created Priority members: 3 users
Step 3: Create the Hotel Booking Tool
Same as the conditional workflow, but now it will be intercepted by middleware!
โ hotel_booking tool created with @ai_function decorator
Step 4: ๐ Create Priority Check Middleware (THE KEY FEATURE!)
This is the core functionality of this notebook. The middleware:
- Intercepts the hotel_booking function call
- Executes the function normally by calling
next(context) - Inspects the result in
context.result - Overrides the result if user is priority and no rooms available
- Returns the modified result back to the agent
Key Pattern:
async def my_middleware(context, next):
await next(context) # Execute function
# Now context.result contains the function's output
if some_condition:
context.result = new_value # Override!
โ priority_check_middleware created - Intercepts hotel_booking function - Overrides availability for priority members
Step 5: Define Condition Functions for Routing
Same condition functions as the conditional workflow - they inspect the structured output to determine routing.
โ Condition functions defined
Step 6: Create Custom Display Executor
Same executor as before - displays the final workflow output.
โ display_result executor created
Step 7: Load Environment Variables
Configure the LLM client (GitHub Models or OpenAI).
Step 8: Create AI Agents with Middleware
KEY DIFFERENCE: When creating the availability_agent, we pass the middleware parameter!
This is how we inject the priority_check_middleware into the agent's function invocation pipeline.
Step 9: Build the Workflow
Same workflow structure as before - conditional routing based on availability.
Step 10: Test Case 1 - Regular User in Paris (No Override)
A regular user tries to book Paris โ No rooms โ Routes to alternative_agent
Step 11: Test Case 2 - ๐ Priority User in Paris (WITH Override!)
A priority member tries to book Paris โ No rooms initially โ ๐ Middleware overrides! โ Routes to booking_agent
This is the key demonstration of middleware power!
Step 12: Test Case 3 - Priority User in Stockholm (Already Available)
Priority user tries Stockholm โ Rooms available โ No override needed โ Routes to booking_agent
This shows that middleware only acts when needed!
Key Takeaways and Middleware Concepts
โ What You've Learned:
1. Function-Based Middleware Pattern
Middleware intercepts function calls using a simple async function:
async def my_middleware(
context: FunctionInvocationContext,
next: Callable,
) -> None:
# Before function execution
print("Intercepting...")
# Execute the function
await next(context)
# After function execution - inspect result
if context.result:
# Modify result if needed
context.result = modified_value
2. Context Access and Result Override
context.function- Access the function being calledcontext.arguments- Read function argumentscontext.kwargs- Access additional parametersawait next(context)- Execute the functioncontext.result- Read/modify the function's output
3. Business Logic Implementation
Our middleware implements priority member benefits:
- Regular users: No modifications, standard workflow
- Priority users: Override "no availability" โ "available"
- Conditional logic: Only overrides when needed
4. Same Workflow, Different Outcomes
The power of middleware:
- โ No changes to the workflow structure
- โ No changes to the tool function
- โ No changes to conditional routing logic
- โ Just middleware โ Different behavior!
๐ Real-World Applications:
-
VIP/Premium Features
- Override rate limits for premium users
- Provide priority access to resources
- Unlock premium features dynamically
-
A/B Testing
- Route users to different implementations
- Test new features with specific users
- Gradual feature rollouts
-
Security & Compliance
- Audit function calls
- Block sensitive operations
- Enforce business rules
-
Performance Optimization
- Cache results for specific users
- Skip expensive operations when possible
- Dynamic resource allocation
-
Error Handling & Retry
- Catch and handle errors gracefully
- Implement retry logic
- Fallback to alternative implementations
-
Logging & Monitoring
- Track function execution times
- Log parameters and results
- Monitor usage patterns
๐ Key Differences from Decorators:
| Feature | Decorator | Middleware |
|---|---|---|
| Scope | Single function | All functions in agent |
| Flexibility | Fixed at definition | Dynamic at runtime |
| Context | Limited | Full agent context |
| Composition | Multiple decorators | Middleware pipeline |
| Agent-Aware | No | Yes (access to agent state) |
๐ When to Use Middleware:
โ Use middleware when:
- You need to modify behavior based on user/session state
- You want to apply logic to multiple functions
- You need access to agent-level context
- You're implementing cross-cutting concerns (logging, auth, etc.)
โ Don't use middleware when:
- Simple input validation (use Pydantic)
- Function-specific logic (keep in function)
- One-time modifications (just change the function)
๐ Advanced Patterns:
# Multiple middleware (execution order matters!)
middleware=[
logging_middleware, # Logs first
auth_middleware, # Then checks auth
cache_middleware, # Then checks cache
rate_limit_middleware, # Then rate limits
priority_check_middleware # Finally priority check
]
# Conditional middleware execution
async def conditional_middleware(context, next):
if should_execute(context):
await next(context)
# Modify result
else:
# Skip execution entirely
context.result = cached_value
๐ Related Concepts:
- Agent Middleware: Intercepts agent.run() calls
- Function Middleware: Intercepts tool function calls (what we used!)
- Middleware Pipeline: Chain of middleware executing in order
- Context Propagation: Pass state through middleware chain