Email Agent Architecture with Phi-4 Local Model
November 26, 2025 ยท View on GitHub
System Overview
graph TB
User[User Question] --> Supervisor[Supervisor Agent]
Supervisor --> SearchEmail[search_email_history]
Supervisor --> ManageEmail[manage_email]
Supervisor --> ScheduleEvent[schedule_event]
Supervisor --> Question[Question Tool]
Supervisor --> Done[Done Tool]
ManageEmail --> EmailAgent[Email Sub-Agent]
ScheduleEvent --> CalendarAgent[Calendar Sub-Agent]
EmailAgent --> MCPEmail[MCP Email Tools]
CalendarAgent --> MCPCalendar[MCP Calendar Tools]
SearchEmail --> VectorDB[PostgreSQL + pgvector]
MCPEmail --> M365[Microsoft 365 API]
MCPCalendar --> M365
style Supervisor fill:#e1f5ff
style EmailAgent fill:#fff4e1
style CalendarAgent fill:#fff4e1
style VectorDB fill:#e8f5e9
Phi-4 Integration with Custom Middleware
flowchart LR
Input[User Input] --> Agent[LangChain Agent]
Agent --> Phi[Phi-4-generic-gpu]
Phi --> JSON[JSON Output]
JSON --> MW1[PhiJSONToToolCallMiddleware]
MW1 --> ToolCall[tool_calls Format]
ToolCall --> Execute[Execute Tool]
Execute --> ToolResult[Tool Results]
ToolResult --> MW2[PhiToolResultMiddleware]
MW2 --> Trim[Trimmed + Reminder]
Trim --> Phi
Phi --> Answer[Final Answer]
style Phi fill:#d4edda
style MW1 fill:#fff3cd
style MW2 fill:#fff3cd
style JSON fill:#f8d7da
style ToolCall fill:#d1ecf1
Middleware Details
PhiJSONToToolCallMiddleware (after_model)
flowchart TD
Start[Model Response] --> Check{Has tool_calls?}
Check -->|Yes| Skip[Skip Processing]
Check -->|No| Extract[Extract JSON Pattern]
Extract --> Match{JSON Match?}
Match -->|No| Return[Return None]
Match -->|Yes| Parse[Parse Tool Name and Args]
Parse --> Validate{Tool Valid?}
Validate -->|No| Return
Validate -->|Yes| Convert[Convert to AIMessage]
Convert --> Replace[Replace Last Message]
Replace --> Log[Log Success]
style Extract fill:#e1f5ff
style Convert fill:#d4edda
style Replace fill:#d4edda
Purpose: Converts Phi's natural JSON output into LangChain's tool_calls format
How it works:
- Detects JSON: Regex pattern
{"name": "tool_name", "args": {...}} - Validates: Checks if tool exists in schema
- Converts: Creates AIMessage with proper tool_calls structure
- Replaces: Swaps text response with structured tool call
PhiToolResultMiddleware (before_model)
flowchart TD
Start[Before Model Call] --> Check{Last msg is ToolMessage?}
Check -->|No| Skip[Skip Processing]
Check -->|Yes| Length{Content over 1000 chars?}
Length -->|No| Skip
Length -->|Yes| Trim[Trim to 1000 chars]
Trim --> Create[Create New ToolMessage]
Create --> Reminder[Add Reminder Message]
Reminder --> Replace[Replace Messages]
Replace --> Log[Log Trimming]
style Trim fill:#fff3cd
style Reminder fill:#d1ecf1
style Replace fill:#d4edda
Purpose: Prevents context overflow and prompts Phi to synthesize answers
How it works:
- Detects long results: Checks if tool result > 1000 characters
- Trims intelligently: Keeps first 1000 chars, notes trimmed amount
- Adds reminder: Appends "Based on the tool results above, please answer the original question."
- Prevents empty responses: Phi was returning empty content without this
Component Breakdown
Supervisor Agent
- Model: Phi-4-generic-gpu (Temperature: 0.0)
- System Prompt:
agent_system_prompt_local_modelwith tool calling instructions - Tools: High-level orchestration tools
search_email_history: Vector search in PostgreSQLmanage_email: Delegates to Email Sub-Agentschedule_event: Delegates to Calendar Sub-AgentQuestion: Human-in-the-loop for clarificationDone: Signals completion
- Middleware: Both custom middleware components
Sub-Agents
Both use simpler prompts with JSON output instructions:
Email Sub-Agent:
- Tools:
list-mail-messages,create-draft-email,get-mail-message,send-mail - Middleware:
PhiJSONToToolCallMiddleware
Calendar Sub-Agent:
- Tools:
list-calendar-events,create-calendar-event, etc. - Middleware:
PhiJSONToToolCallMiddleware - Special constraint: Always use
{"top": 10}parameter
Data Flow Example
sequenceDiagram
participant User
participant Supervisor
participant MW1 as JSONToToolCall
participant MW2 as ToolResult
participant Tool as search_email_history
participant VectorDB
User->>Supervisor: What time is my meeting on Nov 4?
Supervisor->>Supervisor: Generate response
Note over Supervisor: Phi outputs JSON with tool name and args
Supervisor->>MW1: after_model hook
MW1->>MW1: Parse JSON
MW1->>MW1: Convert to tool_calls
MW1-->>Supervisor: AIMessage with tool_calls
Supervisor->>Tool: Execute search_email_history
Tool->>VectorDB: Vector search
VectorDB-->>Tool: 5 email results
Tool-->>Supervisor: ToolMessage with results
Supervisor->>MW2: before_model hook
MW2->>MW2: Trim to 1000 chars
MW2->>MW2: Add reminder message
MW2-->>Supervisor: Modified messages
Supervisor->>Supervisor: Generate answer
Note over Supervisor: Your meeting is at 14:00 UTC
Supervisor-->>User: Final answer
Key Innovations
-
JSON-First Approach: Leverage Phi's strength at generating JSON instead of fighting its tool calling limitations
-
Middleware Pattern: Use LangChain's middleware hooks to transform Phi's output at the right moments
after_model: Convert text to structured formatbefore_model: Trim context and add guidance
-
Context Management: Trim tool results to prevent overwhelming the 14B parameter model
-
Hierarchical Architecture: Supervisor delegates to specialized sub-agents, reducing complexity per agent
Performance Characteristics
- Model: Phi-4-generic-gpu (8.37 GB)
- Temperature: 0.0 (maximum accuracy)
- Endpoint: Foundry Local on port 57389
- Context Window: Managed via trimming at 1000 characters
- Tool Call Success Rate: ~100% with middleware (was ~0% without)
Storage Architecture
flowchart LR
Email[Email Content] --> Files[Blob Storage]
Email --> Embed[Generate Embedding]
Embed --> Azure[Azure OpenAI Embeddings]
Azure --> PG[PostgreSQL + pgvector]
Query[Search Query] --> Embed2[Generate Embedding]
Embed2 --> Azure
Azure --> PG
PG --> Results[Search Results]
style PG fill:#e8f5e9
style Azure fill:#d1ecf1
MCP Integration
flowchart TB
Agent[Email Agent] --> MCP[MCP Adapters]
MCP -->|stdio| Server[ms-365-mcp-server]
Server --> Tools[99 MCP Tools]
Tools --> Outlook[Outlook Mail]
Tools --> Calendar[Outlook Calendar]
style Server fill:#e1f5ff
style Tools fill:#fff4e1
Communication: stdio (standard input/output)
Authentication: Delegated to MCP server
Tools Loaded: 99 (Email: 4, Calendar: 12, Auth: 3)