Interview Kickstart · Applied Agentic AI for SWEs · Capstone Project

✉️ GovBridge
AI-Powered Civic
Email Assistant

A multi-agent AI system that helps citizens write clear, formal, well-addressed emails to government officials — anywhere in the world.

LangGraph GPT-4o / Claude Tavily Search LangSmith Streamlit 5 Agents
The Problem

Citizens want to act.
They don't know how to write.

🏛️

Wrong recipient

Emails sent to the wrong department or tier of government are ignored or never reach the decision-maker.

📝

Weak content

Missing key details — exact location, duration, prior reports — means officials can't act on the complaint.

🌍

Global complexity

Every country has different tiers of government. What's a city issue in London is a state issue in Bavaria.

⏱️

Time and confidence

Writing formal government correspondence takes 15–20 minutes and requires a level of confidence many don't have.

Our Solution

CivicMail guides you from idea
to polished draft in minutes.

  • Understands your issue or idea in plain language
  • Detects your location — city, state, country — automatically
  • Researches the issue using live web search before asking questions
  • Asks only the questions that matter (max 3 turns)
  • Identifies the correct government tier: local → state → national → federal
  • Generates a formal, well-addressed draft with CC list
  • Reviews the draft for grammar, tone, and structure before showing you
  • Lets you approve, revise, or open directly in Gmail
📩 User message
🔍 Input Agent — classify + locate
💬 Complaint / Ideas Agent — gather details
✍️ Email Writer Agent — generate draft
🔬 Review Agent — validate quality
✅ User approves → Gmail / Email App
Development Journey

How we actually built it:
Code → AI Studio → Claude Code → Model Upgrade

Phase 1 — Build First
Built the full application in Python · Results were inconsistent
Wrote the LangGraph pipeline — agents, state, routing, Streamlit UI. The system ran but agent responses were unreliable. Prompts were too vague and the LLM wasn't following the intended workflow.
Phase 2 — Prompt Iteration in AI Studio
Took the problem to AI Studio · Discovered what worked
Iterated on agent system prompts interactively in the playground. Found key constraints that worked — one question per turn, READY_TO_DRAFT signal, structured output tags — without touching the codebase.
Phase 3 — Refinement with Claude Code
Brought prompts back into code · Used Claude Code to harden them
Copied the working AI Studio prompts into the agent files and used Claude Code to sharpen them further. Agents were better but still inconsistent across diverse, real-world inputs.
Phase 4 — Model Upgrade
Upgraded to GPT-5.4 · System finally behaved reliably
Switching to a more capable model was the turning point. The same prompts that produced inconsistent results now followed instructions correctly. Model capability matters as much as prompt design.
Agent Architecture

Five agents, one pipeline

AgentModelKey ResponsibilityTools
Input Agent Small Classify intent, extract location, route to specialist
Complaint Agent Large Gather mandatory fields in ≤3 turns; force draft at turn 3 Web Search
Ideas Agent Large Research existing programs; analyse tradeoffs; confirm addressee tier Web Search
Email Writer Large Generate draft → show for approval → handle revisions with tone support Web Search
Review Agent NEW Small Check grammar, tone, structure — every draft, every revision
User
Input Agent
Complaint / Ideas
Email Writer
Review Agent
Approval Loop
Orchestration

LangGraph: human-in-the-loop
at every turn

Key LangGraph Concepts Used

interrupt() / Command(resume=)

Pauses graph execution and waits for user input. On resume, the node re-executes from the beginning — mocks and guards must account for this.

Conditional edges

Routes based on state: category → complaint/ideas; conversation_complete → email_writer; draft_approved → END or loop back.

MemorySaver checkpointer

Persists full state between interrupts so multi-turn conversations survive page refreshes and server restarts.

# Conditional routing def _route_after_input(state): category = state.get("category") if category == "idea": return "ideas_agent" if category == "complaint": return "complaint_agent" return "input_agent" # loop # Approval loop edge def _route_after_email_writer(state): if state.get("draft_approved"): return END return "email_writer" # loop # Human-in-the-loop interrupt user_reply = interrupt(draft_message) if user_reply.lower() in ("approve", "yes"): return {"draft_approved": True, ...} else: # revise and loop ...
Observability

LangSmith: from blind spots
to full visibility

Before LangSmith

  • Silent except blocks swallowed errors
  • No idea which agent was slow
  • Couldn't see what the LLM actually received
  • Tool call results were invisible
  • Token usage unknown

After LangSmith

  • Every LLM call traced with exact prompt + response
  • Latency per node visible — know which agent is slow
  • Tool call inputs and outputs captured
  • Token usage per session tracked
  • Zero code change — just two env vars

Zero-code integration

# .env — that's it LANGCHAIN_TRACING_V2=true LANGCHAIN_API_KEY=ls__... LANGCHAIN_PROJECT=civicmail

LangChain's core classes auto-instrument. Every ChatOpenAI, ChatAnthropic, and LangGraph node sends traces automatically.

What a trace shows

ideas_agent → web_search (3×) → LLM call → interrupt
email_writer → web_search → LLM call → review_agent → interrupt
Total: 8 LLM calls, 4 search calls, 3.2s, 4,200 tokens

Key Features

What makes CivicMail different

🌍

Truly global

Works for London, Bavaria, NYC, Bangkok, Algiers — detects the right government tier automatically.

🔍

Research-first

Every agent searches the web before responding. Opens with real findings, not generic questions.

🎯

Correct addressee

Identifies local vs city vs state vs national official. Finds contact details via live search.

🔬

Auto-reviewed

Every draft passes through the Review Agent before the user sees it. Grammar, tone, structure — checked silently.

🎨

Tone selector

Formal, Assertive, or Diplomatic — choose the right register for your situation.

📤

One-click send

Open directly in Gmail, default email app, or download as .txt. No copy-paste needed.

Testing Strategy

Three layers of confidence

Layer 1

Unit Tests

Isolated tests for each agent function. No LLM calls. Tests routing logic, state parsing, prompt extraction. Fast — runs in <1s.

38 tests
Layer 2

Integration Tests

Full LangGraph runs with mocked LLMs. 5 global locations × complaint + idea + routing + write-now intent. Covers all agent transitions.

67 tests
Layer 3

LLM Evaluation

12 real-world cases through live LLMs. LLM-as-judge scores 6 criteria: addressee tier, contact hints, required terms, no wrong locations, tone, specificity.

12 golden cases
Correct addressee tier
9.2/10
Addressee hint matched
8.8/10
Required terms present
9.5/10
No wrong locations
10/10
Limitations

What the system can't do —
and why that matters

🔍

No RAG = No reliable officials data

The system finds officeholder names by searching the web at runtime. This works most of the time, but search results lag reality — a newly elected mayor like Mamdani in NYC may not surface confidently from a live query. Without a curated, up-to-date knowledge base (RAG) of current officeholders, the agent has to hedge: "I cannot confirm the current Mayor from a reliable source."

With RAG: a structured database of officials, updated on each election, would give every query an authoritative ground truth to look up instead of guessing from snippets.

🌐

Search quality is inconsistent

Official contact pages, email addresses, and office structures vary wildly across governments. Some are easy to find; others are buried or missing entirely. The agent can only be as good as the search results it receives.

🏙️

A universal system is not realistic

Every city and state has its own government structure, tier logic, contact conventions, and language. A single generic system tuned for "anywhere in the world" will always produce mediocre results for specific localities.

The right architecture is one deployment per jurisdiction — a city of New York instance, a Bavaria instance — each backed by:

  • A RAG index of current officials, departments, and contacts
  • Pre-configured tiers and routing rules for that jurisdiction
  • Local language and tone conventions
🖥️

The ideal end state: city-specific websites

Rather than one global app, the production model is a white-labeled civic portal deployed per city or state — nyc.civicmail.gov, bavaria.civicmail.de — each maintained by the local authority with their own verified data. The AI layer remains shared; the knowledge layer is local.

Conclusion

What we learned

🧪

Debug prompts in the playground, not in code

When agent responses are inconsistent, AI Studio is faster for diagnosis than a Python REPL. Fix the prompt there, then bring it back to code.

👁️

Observability is not optional

LangSmith wasn't an add-on — it caught bugs that were invisible otherwise. In multi-agent systems, silent failures are the norm without tracing.

🏛️

Domain specificity beats breadth

Focusing on civic emails — not "all emails" — let us build smarter agents. The Complaint Agent knows to ask about prior reports. The Ideas Agent knows to check government tiers.

📊

Bring your own data — or the model does it for you

Applications need to be grounded in proprietary or real-world data to justify the complexity. For generic tasks with no unique context, newer Claude models can handle them directly — no pipeline needed.

105 tests passing  ·  5 agents  ·  6 global regions  ·  Full LangSmith tracing

LangGraph GPT-4o / Claude Tavily Search LangSmith Streamlit python-dotenv pytest · 105 tests
Live Demo

✉️ CivicMail

Let's see it in action — then open the floor for questions.

🙋

Questions?