Service — multi_agent_email_service.py
Three functions, three specialist "agents": one extracts structure from an email, one drafts the reply, one judges the whole package and scores it 0–1.
Purpose
backend/app/services/multi_agent_email_service.py provides the LLM stages of the multi-agent email workflow. Each function is a stateless prompt-plus-call; the pipeline — ordering, task creation, persistence, tracing — lives in the router. This separation keeps the agents individually testable and reusable.
The three agents
run_analysis_agent(email_text) -> dict
A thin delegation:
def run_analysis_agent(email_text: str) -> dict:
return analyze_email(email_text)It reuses analyze_email so the single-agent endpoint and the workflow share one extraction brain — same prompt, same JSON contract (summary, intent, priority, deadline, action_items, suggested_reply), temperature 0.2.
run_reply_agent(email_text, analysis) -> str
Drafts the outbound reply with both the original email and the structured analysis in context, temperature 0.3:
You are a professional email reply agent.
Your job:
- Write a clear professional reply.
- Use the analysis provided.
- Do not promise anything unrealistic.
- If a deadline is mentioned, acknowledge it carefully.
- Keep the reply concise and business-friendly.
Return only the email reply text.Returns plain text (no JSON parsing). The workflow only invokes this agent when the agent has the reply_generator tool; otherwise the analysis's own suggested_reply is used as a fallback (Tools).
run_reviewer_agent(email_text, analysis, suggested_reply, tasks_created) -> dict
The quality gate, temperature 0. Receives the entire package — original email, analysis JSON, created tasks, and the drafted reply — and returns strict JSON:
{
"approved": true,
"quality_score": 0.0,
"issues": ["issue 1", "issue 2"],
"recommendation": "short recommendation"
}Parsed with json.loads (raising ValueError("Reviewer Agent did not return valid JSON: ...") on garbage) and normalized defensively — bool(...), float(result.get("quality_score", 0)), defaults for issues/recommendation. The quality_score ends up on WorkflowRun.quality_score and feeds the dashboard's average (Evaluation).
Data flow
Note what is absent: no task-creation function. The "Task Agent" stage of the workflow is deterministic Python in the router (a loop over action_items), not an LLM call — see Email Workflow.
Module setup
The module creates its own OpenAI client and reads OPENAI_MODEL (default gpt-4o-mini) independently of openai_service — duplicated setup that would consolidate naturally into a shared client factory.
Inputs / outputs
| Agent | Inputs | Output | Failure |
|---|---|---|---|
| Analysis | raw email text | analysis dict | ValueError on bad JSON / missing key |
| Reply | email + analysis | reply text | SDK exceptions propagate |
| Reviewer | email + analysis + tasks + reply | verdict dict | ValueError on bad JSON |
All failures propagate to the workflow handler, which marks the WorkflowRun failed, logs a workflow_error step and a failed ToolCall, and returns 500 (Error Handling).
Edge cases
- The reviewer runs after tasks are committed — a rejected review (
approved: false) does not roll tasks back. The verdict is advisory; consumers decide what to do with a low score. tasks_createdmay be empty (no action items, orcreate_tasks=false) — the reviewer prompt handles an empty list fine.- A reviewer that returns valid JSON with missing keys is silently defaulted (score 0, empty issues) rather than failed.
Future improvements
- Share one OpenAI client across services.
- Retry-once-on-parse-failure for the two JSON agents.
- Optionally gate the reply on
approved(auto-regenerate on rejection) — the natural next iteration of the pipeline.
Related files
- Email Workflow — where these agents are orchestrated
- Prompt Catalog — full prompt texts
- Service: OpenAI — the shared analysis function