Spaces:
Runtime error
Runtime error
| <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 <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 Β· 5 agents Β· 6 global regions Β· 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 (β)">←</button> | |
| <span id="slide-counter">1 / 12</span> | |
| <button id="btn-next" title="Next (β)">→</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> | |