govbridge / presentation.html
Vishnu Rama
Improve official name handling and add current-officeholder search
6ffbb70
Raw
History Blame Contribute Delete
37.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GovBridge β€” Capstone Presentation</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--navy: #0a0e1a;
--navy2: #111827;
--card: #1a2235;
--border: #2a3a55;
--blue: #3b82f6;
--indigo: #6366f1;
--teal: #14b8a6;
--green: #22c55e;
--amber: #f59e0b;
--red: #ef4444;
--purple: #a855f7;
--text: #f1f5f9;
--muted: #94a3b8;
--accent: #e2e8f0;
}
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--navy); font-family: 'Inter', sans-serif; color: var(--text); }
/* ── Slide container ── */
#deck { width: 100vw; height: 100vh; position: relative; }
.slide {
position: absolute; inset: 0;
display: flex; flex-direction: column; justify-content: center; align-items: center;
padding: 60px 80px;
opacity: 0; pointer-events: none;
transition: opacity .45s ease, transform .45s ease;
transform: translateY(24px);
}
.slide.active { opacity: 1; pointer-events: auto; transform: translateY(0); }
/* ── Progress bar ── */
#progress-bar {
position: fixed; top: 0; left: 0; height: 3px;
background: linear-gradient(90deg, var(--blue), var(--indigo));
transition: width .4s ease; z-index: 100;
}
/* ── Nav ── */
#nav {
position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px; align-items: center; z-index: 100;
background: rgba(26,34,53,.85); backdrop-filter: blur(12px);
border: 1px solid var(--border); border-radius: 999px; padding: 8px 20px;
}
#nav button {
background: none; border: none; color: var(--muted); cursor: pointer;
font-size: 18px; padding: 4px 8px; border-radius: 6px;
transition: color .2s, background .2s;
}
#nav button:hover { color: var(--text); background: var(--border); }
#slide-counter { font-size: 13px; color: var(--muted); min-width: 56px; text-align: center; }
/* ── Dots ── */
#dots { position: fixed; right: 28px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 100; }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border); cursor: pointer; transition: background .2s, transform .2s; }
.dot.active { background: var(--blue); transform: scale(1.4); }
/* ── Slide tag ── */
.tag {
font-size: 11px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
color: var(--blue); background: rgba(59,130,246,.12); border: 1px solid rgba(59,130,246,.3);
border-radius: 999px; padding: 4px 14px; margin-bottom: 20px; align-self: flex-start;
}
/* ── Typography ── */
h1 { font-size: 52px; font-weight: 800; line-height: 1.1; }
h2 { font-size: 38px; font-weight: 700; line-height: 1.2; margin-bottom: 32px; }
h3 { font-size: 20px; font-weight: 600; margin-bottom: 10px; }
p { font-size: 17px; line-height: 1.7; color: var(--muted); }
.lead { font-size: 22px; line-height: 1.6; color: var(--accent); }
.grad-text {
background: linear-gradient(135deg, var(--blue) 0%, var(--indigo) 50%, var(--teal) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
/* ── Card ── */
.card {
background: var(--card); border: 1px solid var(--border);
border-radius: 16px; padding: 24px 28px;
}
.card-grid { display: grid; gap: 16px; width: 100%; }
.card-grid.cols-2 { grid-template-columns: 1fr 1fr; }
.card-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
/* ── Icon badge ── */
.icon { font-size: 28px; margin-bottom: 10px; }
/* ── Pill ── */
.pill {
display: inline-block; font-size: 12px; font-weight: 600;
padding: 3px 12px; border-radius: 999px; margin: 3px;
}
.pill-blue { background: rgba(59,130,246,.15); color: #93c5fd; border: 1px solid rgba(59,130,246,.3); }
.pill-indigo { background: rgba(99,102,241,.15); color: #a5b4fc; border: 1px solid rgba(99,102,241,.3); }
.pill-teal { background: rgba(20,184,166,.15); color: #5eead4; border: 1px solid rgba(20,184,166,.3); }
.pill-green { background: rgba(34,197,94,.15); color: #86efac; border: 1px solid rgba(34,197,94,.3); }
.pill-amber { background: rgba(245,158,11,.15); color: #fcd34d; border: 1px solid rgba(245,158,11,.3); }
.pill-purple { background: rgba(168,85,247,.15); color: #d8b4fe; border: 1px solid rgba(168,85,247,.3); }
.pill-red { background: rgba(239,68,68,.15); color: #fca5a5; border: 1px solid rgba(239,68,68,.3); }
/* ── Code block ── */
.code {
font-family: 'Fira Code', monospace; font-size: 13px; line-height: 1.65;
background: #0d1117; border: 1px solid var(--border); border-radius: 10px;
padding: 18px 22px; color: #e2e8f0; overflow-x: auto; white-space: pre;
}
.kw { color: #ff79c6; }
.str { color: #f1fa8c; }
.cm { color: #6272a4; }
.fn { color: #50fa7b; }
.val { color: #bd93f9; }
/* ── Flow diagram ── */
.flow { display: flex; align-items: center; gap: 0; flex-wrap: wrap; justify-content: center; }
.flow-box {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 12px 18px; font-size: 14px; font-weight: 600;
text-align: center; min-width: 120px;
}
.flow-arrow { font-size: 20px; color: var(--blue); padding: 0 6px; }
.flow-box.highlight { border-color: var(--blue); background: rgba(59,130,246,.1); }
.flow-box.new { border-color: var(--teal); background: rgba(20,184,166,.1); }
/* ── Check list ── */
.checklist { list-style: none; display: flex; flex-direction: column; gap: 10px; }
.checklist li { display: flex; align-items: flex-start; gap: 12px; font-size: 16px; color: var(--accent); }
.checklist li::before { content: 'βœ“'; color: var(--green); font-weight: 700; margin-top: 1px; flex-shrink: 0; }
/* ── Two-col layout ── */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; width: 100%; align-items: start; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 24px; width: 100%; }
/* ── Table ── */
table { width: 100%; border-collapse: collapse; font-size: 15px; }
th { text-align: left; padding: 10px 14px; color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; border-bottom: 1px solid var(--border); }
td { padding: 12px 14px; border-bottom: 1px solid rgba(42,58,85,.5); color: var(--accent); }
tr:last-child td { border-bottom: none; }
/* ── Timeline ── */
.timeline { display: flex; flex-direction: column; gap: 0; width: 100%; }
.tl-item { display: flex; gap: 20px; align-items: stretch; }
.tl-left { display: flex; flex-direction: column; align-items: center; width: 40px; flex-shrink: 0; }
.tl-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--blue); border: 2px solid var(--navy); flex-shrink: 0; margin-top: 4px; }
.tl-dot.teal { background: var(--teal); }
.tl-dot.indigo { background: var(--indigo); }
.tl-dot.green { background: var(--green); }
.tl-line { width: 2px; background: var(--border); flex: 1; margin: 4px 0; }
.tl-body { padding-bottom: 24px; flex: 1; }
.tl-label { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--blue); margin-bottom: 4px; }
.tl-label.teal { color: var(--teal); }
.tl-label.indigo { color: var(--indigo); }
.tl-label.green { color: var(--green); }
.tl-title { font-size: 17px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.tl-desc { font-size: 14px; color: var(--muted); line-height: 1.6; }
/* ── Score bar ── */
.score-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.score-label { font-size: 13px; color: var(--muted); width: 200px; flex-shrink: 0; }
.score-track { flex: 1; height: 8px; background: var(--border); border-radius: 999px; overflow: hidden; }
.score-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--blue), var(--teal)); }
.score-val { font-size: 13px; font-weight: 600; color: var(--text); width: 36px; text-align: right; }
/* ── Slide 1 β€” Title ── */
#s1 { background: radial-gradient(ellipse at 30% 40%, rgba(59,130,246,.15) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(99,102,241,.12) 0%, transparent 60%), var(--navy); align-items: flex-start; }
#s1 .kicker { font-size: 13px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--teal); margin-bottom: 18px; }
#s1 h1 { font-size: 60px; max-width: 750px; }
#s1 .sub { font-size: 20px; color: var(--muted); margin-top: 18px; max-width: 560px; line-height: 1.6; }
#s1 .badges { margin-top: 36px; display: flex; gap: 10px; flex-wrap: wrap; }
/* ── Slide backgrounds ── */
.slide { background: var(--navy); }
</style>
</head>
<body>
<div id="progress-bar"></div>
<div id="dots"></div>
<div id="deck">
<!-- ─────────── SLIDE 1: Title ─────────── -->
<div class="slide active" id="s1">
<div class="kicker">Interview Kickstart Β· Applied Agentic AI for SWEs Β· Capstone Project</div>
<h1>βœ‰οΈ GovBridge<br><span class="grad-text">AI-Powered Civic<br>Email Assistant</span></h1>
<p class="sub">A multi-agent AI system that helps citizens write clear, formal, well-addressed emails to government officials β€” anywhere in the world.</p>
<div class="badges">
<span class="pill pill-blue">LangGraph</span>
<span class="pill pill-indigo">GPT-4o / Claude</span>
<span class="pill pill-teal">Tavily Search</span>
<span class="pill pill-green">LangSmith</span>
<span class="pill pill-purple">Streamlit</span>
<span class="pill pill-amber">5 Agents</span>
</div>
</div>
<!-- ─────────── SLIDE 2: Problem ─────────── -->
<div class="slide" id="s2">
<div class="tag">The Problem</div>
<h2>Citizens want to act.<br>They don't know how to write.</h2>
<div class="card-grid cols-2" style="margin-top: 8px;">
<div class="card">
<div class="icon">πŸ›οΈ</div>
<h3>Wrong recipient</h3>
<p>Emails sent to the wrong department or tier of government are ignored or never reach the decision-maker.</p>
</div>
<div class="card">
<div class="icon">πŸ“</div>
<h3>Weak content</h3>
<p>Missing key details β€” exact location, duration, prior reports β€” means officials can't act on the complaint.</p>
</div>
<div class="card">
<div class="icon">🌍</div>
<h3>Global complexity</h3>
<p>Every country has different tiers of government. What's a city issue in London is a state issue in Bavaria.</p>
</div>
<div class="card">
<div class="icon">⏱️</div>
<h3>Time and confidence</h3>
<p>Writing formal government correspondence takes 15–20 minutes and requires a level of confidence many don't have.</p>
</div>
</div>
</div>
<!-- ─────────── SLIDE 3: Solution ─────────── -->
<div class="slide" id="s3">
<div class="tag">Our Solution</div>
<h2>CivicMail guides you from idea<br>to polished draft in minutes.</h2>
<div class="two-col" style="margin-top: 8px;">
<ul class="checklist">
<li>Understands your issue or idea in plain language</li>
<li>Detects your location β€” city, state, country β€” automatically</li>
<li>Researches the issue using live web search before asking questions</li>
<li>Asks only the questions that matter (max 3 turns)</li>
<li>Identifies the correct government tier: local β†’ state β†’ national β†’ federal</li>
<li>Generates a formal, well-addressed draft with CC list</li>
<li>Reviews the draft for grammar, tone, and structure before showing you</li>
<li>Lets you approve, revise, or open directly in Gmail</li>
</ul>
<div>
<div class="flow" style="flex-direction: column; align-items: flex-start; gap: 0;">
<div class="flow-box highlight" style="width: 100%; margin-bottom: 0;">πŸ“© User message</div>
<div style="width: 2px; height: 14px; background: var(--border); margin-left: 59px;"></div>
<div class="flow-box" style="width: 100%;">πŸ” Input Agent β€” classify + locate</div>
<div style="width: 2px; height: 14px; background: var(--border); margin-left: 59px;"></div>
<div class="flow-box" style="width: 100%;">πŸ’¬ Complaint / Ideas Agent β€” gather details</div>
<div style="width: 2px; height: 14px; background: var(--border); margin-left: 59px;"></div>
<div class="flow-box" style="width: 100%;">✍️ Email Writer Agent β€” generate draft</div>
<div style="width: 2px; height: 14px; background: var(--border); margin-left: 59px;"></div>
<div class="flow-box new" style="width: 100%;">πŸ”¬ Review Agent β€” validate quality</div>
<div style="width: 2px; height: 14px; background: var(--border); margin-left: 59px;"></div>
<div class="flow-box highlight" style="width: 100%;">βœ… User approves β†’ Gmail / Email App</div>
</div>
</div>
</div>
</div>
<!-- ─────────── SLIDE 4: Development Journey ─────────── -->
<div class="slide" id="s4">
<div class="tag">Development Journey</div>
<h2>How we actually built it:<br>Code β†’ AI Studio β†’ Claude Code β†’ Model Upgrade</h2>
<div class="timeline" style="margin-top: 8px;">
<div class="tl-item">
<div class="tl-left"><div class="tl-dot"></div><div class="tl-line"></div></div>
<div class="tl-body">
<div class="tl-label">Phase 1 β€” Build First</div>
<div class="tl-title">Built the full application in Python Β· Results were inconsistent</div>
<div class="tl-desc">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.</div>
</div>
</div>
<div class="tl-item">
<div class="tl-left"><div class="tl-dot teal"></div><div class="tl-line"></div></div>
<div class="tl-body">
<div class="tl-label teal">Phase 2 β€” Prompt Iteration in AI Studio</div>
<div class="tl-title">Took the problem to AI Studio Β· Discovered what worked</div>
<div class="tl-desc">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.</div>
</div>
</div>
<div class="tl-item">
<div class="tl-left"><div class="tl-dot indigo"></div><div class="tl-line"></div></div>
<div class="tl-body">
<div class="tl-label indigo">Phase 3 β€” Refinement with Claude Code</div>
<div class="tl-title">Brought prompts back into code Β· Used Claude Code to harden them</div>
<div class="tl-desc">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.</div>
</div>
</div>
<div class="tl-item">
<div class="tl-left"><div class="tl-dot green"></div></div>
<div class="tl-body">
<div class="tl-label green">Phase 4 β€” Model Upgrade</div>
<div class="tl-title">Upgraded to GPT-5.4 Β· System finally behaved reliably</div>
<div class="tl-desc">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.</div>
</div>
</div>
</div>
</div>
<!-- ─────────── SLIDE 5: Agent Architecture ─────────── -->
<div class="slide" id="s6">
<div class="tag">Agent Architecture</div>
<h2>Five agents, one pipeline</h2>
<table style="margin-top: 8px;">
<thead>
<tr><th>Agent</th><th>Model</th><th>Key Responsibility</th><th>Tools</th></tr>
</thead>
<tbody>
<tr>
<td><strong>Input Agent</strong></td>
<td><span class="pill pill-amber">Small</span></td>
<td>Classify intent, extract location, route to specialist</td>
<td>β€”</td>
</tr>
<tr>
<td><strong>Complaint Agent</strong></td>
<td><span class="pill pill-blue">Large</span></td>
<td>Gather mandatory fields in ≀3 turns; force draft at turn 3</td>
<td>Web Search</td>
</tr>
<tr>
<td><strong>Ideas Agent</strong></td>
<td><span class="pill pill-blue">Large</span></td>
<td>Research existing programs; analyse tradeoffs; confirm addressee tier</td>
<td>Web Search</td>
</tr>
<tr>
<td><strong>Email Writer</strong></td>
<td><span class="pill pill-blue">Large</span></td>
<td>Generate draft β†’ show for approval β†’ handle revisions with tone support</td>
<td>Web Search</td>
</tr>
<tr>
<td><strong>Review Agent</strong> <span class="pill pill-green" style="font-size:10px;">NEW</span></td>
<td><span class="pill pill-amber">Small</span></td>
<td>Check grammar, tone, structure β€” every draft, every revision</td>
<td>β€”</td>
</tr>
</tbody>
</table>
<div style="margin-top: 24px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: center;">
<div class="flow-box">User</div>
<div class="flow-arrow">β†’</div>
<div class="flow-box highlight">Input Agent</div>
<div class="flow-arrow">β†’</div>
<div class="flow-box">Complaint / Ideas</div>
<div class="flow-arrow">β†’</div>
<div class="flow-box">Email Writer</div>
<div class="flow-arrow">β†’</div>
<div class="flow-box new">Review Agent</div>
<div class="flow-arrow">β†’</div>
<div class="flow-box highlight">Approval Loop</div>
</div>
</div>
<!-- ─────────── SLIDE 7: LangGraph ─────────── -->
<div class="slide" id="s7">
<div class="tag">Orchestration</div>
<h2>LangGraph: human-in-the-loop<br>at every turn</h2>
<div class="two-col" style="margin-top: 8px;">
<div>
<h3 style="color: var(--muted); font-size: 14px; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 14px;">Key LangGraph Concepts Used</h3>
<div class="card" style="margin-bottom: 12px;">
<h3>interrupt() / Command(resume=)</h3>
<p style="font-size:14px; margin-top: 6px;">Pauses graph execution and waits for user input. On resume, the node re-executes from the beginning β€” mocks and guards must account for this.</p>
</div>
<div class="card" style="margin-bottom: 12px;">
<h3>Conditional edges</h3>
<p style="font-size:14px; margin-top: 6px;">Routes based on state: category β†’ complaint/ideas; conversation_complete β†’ email_writer; draft_approved β†’ END or loop back.</p>
</div>
<div class="card">
<h3>MemorySaver checkpointer</h3>
<p style="font-size:14px; margin-top: 6px;">Persists full state between interrupts so multi-turn conversations survive page refreshes and server restarts.</p>
</div>
</div>
<div class="code" style="font-size: 12.5px;"><span class="cm"># Conditional routing</span>
<span class="kw">def</span> <span class="fn">_route_after_input</span>(state):
category = state.get(<span class="str">"category"</span>)
<span class="kw">if</span> category == <span class="str">"idea"</span>:
<span class="kw">return</span> <span class="str">"ideas_agent"</span>
<span class="kw">if</span> category == <span class="str">"complaint"</span>:
<span class="kw">return</span> <span class="str">"complaint_agent"</span>
<span class="kw">return</span> <span class="str">"input_agent"</span> <span class="cm"># loop</span>
<span class="cm"># Approval loop edge</span>
<span class="kw">def</span> <span class="fn">_route_after_email_writer</span>(state):
<span class="kw">if</span> state.get(<span class="str">"draft_approved"</span>):
<span class="kw">return</span> END
<span class="kw">return</span> <span class="str">"email_writer"</span> <span class="cm"># loop</span>
<span class="cm"># Human-in-the-loop interrupt</span>
user_reply = <span class="fn">interrupt</span>(draft_message)
<span class="kw">if</span> user_reply.lower() <span class="kw">in</span> (<span class="str">"approve"</span>, <span class="str">"yes"</span>):
<span class="kw">return</span> {<span class="str">"draft_approved"</span>: <span class="val">True</span>, ...}
<span class="kw">else</span>:
<span class="cm"># revise and loop</span>
...</div>
</div>
</div>
<!-- ─────────── SLIDE 8: LangSmith ─────────── -->
<div class="slide" id="s8">
<div class="tag">Observability</div>
<h2>LangSmith: from blind spots<br>to full visibility</h2>
<div class="two-col" style="margin-top: 12px;">
<div>
<h3 style="color: var(--muted); font-size: 14px; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 16px;">Before LangSmith</h3>
<ul class="checklist" style="gap: 8px;">
<li style="color: var(--muted);">Silent <code style="font-family:'Fira Code';color:var(--red)">except</code> blocks swallowed errors</li>
<li style="color: var(--muted);">No idea which agent was slow</li>
<li style="color: var(--muted);">Couldn't see what the LLM actually received</li>
<li style="color: var(--muted);">Tool call results were invisible</li>
<li style="color: var(--muted);">Token usage unknown</li>
</ul>
<div style="margin-top: 24px;">
<h3 style="color: var(--muted); font-size: 14px; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 16px;">After LangSmith</h3>
<ul class="checklist">
<li>Every LLM call traced with exact prompt + response</li>
<li>Latency per node visible β€” know which agent is slow</li>
<li>Tool call inputs and outputs captured</li>
<li>Token usage per session tracked</li>
<li>Zero code change β€” just two env vars</li>
</ul>
</div>
</div>
<div>
<div class="card" style="margin-bottom: 14px; border-color: rgba(99,102,241,.4);">
<h3 style="color: var(--indigo);">Zero-code integration</h3>
<div class="code" style="margin-top: 10px; font-size: 12.5px;"><span class="cm"># .env β€” that's it</span>
<span class="kw">LANGCHAIN_TRACING_V2</span>=<span class="val">true</span>
<span class="kw">LANGCHAIN_API_KEY</span>=<span class="str">ls__...</span>
<span class="kw">LANGCHAIN_PROJECT</span>=<span class="str">civicmail</span></div>
<p style="font-size: 13px; margin-top: 10px; color: var(--muted);">LangChain's core classes auto-instrument. Every <code style="font-family:'Fira Code'">ChatOpenAI</code>, <code style="font-family:'Fira Code'">ChatAnthropic</code>, and LangGraph node sends traces automatically.</p>
</div>
<div class="card" style="border-color: rgba(20,184,166,.4);">
<h3 style="color: var(--teal);">What a trace shows</h3>
<p style="font-size: 13px; margin-top: 6px; line-height: 1.7; color: var(--muted);">
ideas_agent β†’ web_search (3Γ—) β†’ LLM call β†’ interrupt<br>
email_writer β†’ web_search β†’ LLM call β†’ review_agent β†’ interrupt<br>
<strong style="color: var(--text);">Total: 8 LLM calls, 4 search calls, 3.2s, 4,200 tokens</strong>
</p>
</div>
</div>
</div>
</div>
<!-- ─────────── SLIDE 9: Key Features ─────────── -->
<div class="slide" id="s9">
<div class="tag">Key Features</div>
<h2>What makes CivicMail different</h2>
<div class="card-grid cols-3" style="margin-top: 12px;">
<div class="card">
<div class="icon">🌍</div>
<h3>Truly global</h3>
<p>Works for London, Bavaria, NYC, Bangkok, Algiers β€” detects the right government tier automatically.</p>
</div>
<div class="card">
<div class="icon">πŸ”</div>
<h3>Research-first</h3>
<p>Every agent searches the web before responding. Opens with real findings, not generic questions.</p>
</div>
<div class="card">
<div class="icon">🎯</div>
<h3>Correct addressee</h3>
<p>Identifies local vs city vs state vs national official. Finds contact details via live search.</p>
</div>
<div class="card">
<div class="icon">πŸ”¬</div>
<h3>Auto-reviewed</h3>
<p>Every draft passes through the Review Agent before the user sees it. Grammar, tone, structure β€” checked silently.</p>
</div>
<div class="card">
<div class="icon">🎨</div>
<h3>Tone selector</h3>
<p>Formal, Assertive, or Diplomatic β€” choose the right register for your situation.</p>
</div>
<div class="card">
<div class="icon">πŸ“€</div>
<h3>One-click send</h3>
<p>Open directly in Gmail, default email app, or download as .txt. No copy-paste needed.</p>
</div>
</div>
</div>
<!-- ─────────── SLIDE 10: Testing ─────────── -->
<div class="slide" id="s10">
<div class="tag">Testing Strategy</div>
<h2>Three layers of confidence</h2>
<div class="three-col" style="margin-top: 12px;">
<div class="card" style="border-color: rgba(59,130,246,.4);">
<div class="pill pill-blue" style="margin-bottom: 12px;">Layer 1</div>
<h3>Unit Tests</h3>
<p style="font-size: 14px; margin-top: 8px;">Isolated tests for each agent function. No LLM calls. Tests routing logic, state parsing, prompt extraction. Fast β€” runs in &lt;1s.</p>
<div style="margin-top: 14px; font-size: 24px; font-weight: 700; color: var(--blue);">38 tests</div>
</div>
<div class="card" style="border-color: rgba(99,102,241,.4);">
<div class="pill pill-indigo" style="margin-bottom: 12px;">Layer 2</div>
<h3>Integration Tests</h3>
<p style="font-size: 14px; margin-top: 8px;">Full LangGraph runs with mocked LLMs. 5 global locations Γ— complaint + idea + routing + write-now intent. Covers all agent transitions.</p>
<div style="margin-top: 14px; font-size: 24px; font-weight: 700; color: var(--indigo);">67 tests</div>
</div>
<div class="card" style="border-color: rgba(20,184,166,.4);">
<div class="pill pill-teal" style="margin-bottom: 12px;">Layer 3</div>
<h3>LLM Evaluation</h3>
<p style="font-size: 14px; margin-top: 8px;">12 real-world cases through live LLMs. LLM-as-judge scores 6 criteria: addressee tier, contact hints, required terms, no wrong locations, tone, specificity.</p>
<div style="margin-top: 14px; font-size: 24px; font-weight: 700; color: var(--teal);">12 golden cases</div>
</div>
</div>
<div style="margin-top: 20px; width: 100%;">
<div class="score-row"><div class="score-label">Correct addressee tier</div><div class="score-track"><div class="score-fill" style="width:92%"></div></div><div class="score-val">9.2/10</div></div>
<div class="score-row"><div class="score-label">Addressee hint matched</div><div class="score-track"><div class="score-fill" style="width:88%"></div></div><div class="score-val">8.8/10</div></div>
<div class="score-row"><div class="score-label">Required terms present</div><div class="score-track"><div class="score-fill" style="width:95%"></div></div><div class="score-val">9.5/10</div></div>
<div class="score-row"><div class="score-label">No wrong locations</div><div class="score-track"><div class="score-fill" style="width:100%"></div></div><div class="score-val">10/10</div></div>
</div>
</div>
<!-- ─────────── SLIDE 11: Limitations ─────────── -->
<div class="slide" id="s11">
<div class="tag">Limitations</div>
<h2>What the system can't do β€”<br>and why that matters</h2>
<div class="two-col" style="margin-top: 12px; align-items: start;">
<div>
<div class="card" style="border-color: rgba(239,68,68,.4); margin-bottom: 16px;">
<div class="icon">πŸ”</div>
<h3 style="color: var(--red);">No RAG = No reliable officials data</h3>
<p style="font-size: 14px; margin-top: 8px;">The system finds officeholder names by searching the web at runtime. This works <em>most</em> 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."</p>
<p style="font-size: 14px; margin-top: 10px; color: var(--amber);">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.</p>
</div>
<div class="card" style="border-color: rgba(245,158,11,.4);">
<div class="icon">🌐</div>
<h3 style="color: var(--amber);">Search quality is inconsistent</h3>
<p style="font-size: 14px; margin-top: 8px;">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.</p>
</div>
</div>
<div>
<div class="card" style="border-color: rgba(168,85,247,.4); margin-bottom: 16px;">
<div class="icon">πŸ™οΈ</div>
<h3 style="color: var(--purple);">A universal system is not realistic</h3>
<p style="font-size: 14px; margin-top: 8px;">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.</p>
<p style="font-size: 14px; margin-top: 10px;">The right architecture is <strong style="color: var(--text);">one deployment per jurisdiction</strong> β€” a city of New York instance, a Bavaria instance β€” each backed by:</p>
<ul style="font-size: 14px; color: var(--muted); margin-top: 8px; padding-left: 18px; line-height: 1.8; list-style: disc;">
<li>A RAG index of current officials, departments, and contacts</li>
<li>Pre-configured tiers and routing rules for that jurisdiction</li>
<li>Local language and tone conventions</li>
</ul>
</div>
<div class="card" style="border-color: rgba(20,184,166,.4);">
<div class="icon">πŸ–₯️</div>
<h3 style="color: var(--teal);">The ideal end state: city-specific websites</h3>
<p style="font-size: 14px; margin-top: 8px;">Rather than one global app, the production model is a white-labeled civic portal deployed per city or state β€” <strong style="color: var(--text);">nyc.civicmail.gov</strong>, <strong style="color: var(--text);">bavaria.civicmail.de</strong> β€” each maintained by the local authority with their own verified data. The AI layer remains shared; the knowledge layer is local.</p>
</div>
</div>
</div>
</div>
<!-- ─────────── SLIDE 10: Conclusion ─────────── -->
<div class="slide" id="s12">
<div class="tag">Conclusion</div>
<h2>What we learned</h2>
<div class="card-grid cols-2" style="margin-top: 8px;">
<div class="card">
<div class="icon">πŸ§ͺ</div>
<h3>Debug prompts in the playground, not in code</h3>
<p>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.</p>
</div>
<div class="card">
<div class="icon">πŸ‘οΈ</div>
<h3>Observability is not optional</h3>
<p>LangSmith wasn't an add-on β€” it caught bugs that were invisible otherwise. In multi-agent systems, silent failures are the norm without tracing.</p>
</div>
<div class="card">
<div class="icon">πŸ›οΈ</div>
<h3>Domain specificity beats breadth</h3>
<p>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.</p>
</div>
<div class="card">
<div class="icon">πŸ“Š</div>
<h3>Bring your own data β€” or the model does it for you</h3>
<p>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.</p>
</div>
</div>
<div style="margin-top: 28px; text-align: center;">
<p style="font-size: 20px; color: var(--text);">105 tests passing &nbsp;Β·&nbsp; 5 agents &nbsp;Β·&nbsp; 6 global regions &nbsp;Β·&nbsp; Full LangSmith tracing</p>
<div style="margin-top: 12px; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
<span class="pill pill-blue">LangGraph</span>
<span class="pill pill-indigo">GPT-4o / Claude</span>
<span class="pill pill-teal">Tavily Search</span>
<span class="pill pill-green">LangSmith</span>
<span class="pill pill-purple">Streamlit</span>
<span class="pill pill-amber">python-dotenv</span>
<span class="pill pill-red">pytest Β· 105 tests</span>
</div>
</div>
</div>
<!-- ─────────── SLIDE: Demo / Questions ─────────── -->
<div class="slide" id="s-demo" style="background: radial-gradient(ellipse at 30% 40%, rgba(59,130,246,.15) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(99,102,241,.12) 0%, transparent 60%), var(--navy); justify-content: center; align-items: center; text-align: center;">
<div class="kicker" style="font-size: 13px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: var(--teal); margin-bottom: 24px; align-self: center;">Live Demo</div>
<h1 style="font-size: 64px; margin-bottom: 24px;">βœ‰οΈ <span class="grad-text">CivicMail</span></h1>
<p class="lead" style="max-width: 560px; margin-bottom: 48px;">Let's see it in action β€” then open the floor for questions.</p>
<div style="font-size: 52px; margin-bottom: 32px;">πŸ™‹</div>
<h2 style="font-size: 36px; color: var(--muted);">Questions?</h2>
</div>
</div><!-- /deck -->
<!-- Nav -->
<div id="nav">
<button id="btn-prev" title="Previous (←)">&#8592;</button>
<span id="slide-counter">1 / 12</span>
<button id="btn-next" title="Next (β†’)">&#8594;</button>
</div>
<script>
const slides = Array.from(document.querySelectorAll('.slide'));
const total = slides.length;
let current = 0;
const bar = document.getElementById('progress-bar');
const counter = document.getElementById('slide-counter');
const dotsEl = document.getElementById('dots');
// Build dots
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot' + (i === 0 ? ' active' : '');
d.addEventListener('click', () => goTo(i));
dotsEl.appendChild(d);
});
const dots = Array.from(dotsEl.querySelectorAll('.dot'));
function goTo(n) {
slides[current].classList.remove('active');
dots[current].classList.remove('active');
current = Math.max(0, Math.min(total - 1, n));
slides[current].classList.add('active');
dots[current].classList.add('active');
bar.style.width = ((current + 1) / total * 100) + '%';
counter.textContent = (current + 1) + ' / ' + total;
}
document.getElementById('btn-prev').addEventListener('click', () => goTo(current - 1));
document.getElementById('btn-next').addEventListener('click', () => goTo(current + 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') goTo(current + 1);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') goTo(current - 1);
if (e.key === 'Home') goTo(0);
if (e.key === 'End') goTo(total - 1);
});
// Touch swipe
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx;
if (Math.abs(dx) > 50) goTo(current + (dx < 0 ? 1 : -1));
});
// Init progress bar
bar.style.width = (1 / total * 100) + '%';
</script>
</body>
</html>