Skip to content
3 min read · 632 words

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:

python
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:

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

Rendering diagram…

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

AgentInputsOutputFailure
Analysisraw email textanalysis dictValueError on bad JSON / missing key
Replyemail + analysisreply textSDK exceptions propagate
Revieweremail + analysis + tasks + replyverdict dictValueError 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_created may be empty (no action items, or create_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.