# 28. AI Chat Agent — CV Assistant Mascot ## 1. Overview The CV site includes an AI-powered conversational assistant (the "mascot") that lets visitors ask natural language questions about the CV content. Built with [Google ADK Go 1.0](https://github.com/google/adk-go) (Agent Development Kit) and Gemini AI, it provides instant, accurate answers by querying the same cached JSON data that renders the site. The mascot appears as a floating robot icon in the bottom-right corner of the page. Clicking it opens a chat panel where visitors can type questions or click suggested question chips. All answers are sourced from real CV data — no hallucination, no stale data. **Why it exists:** A CV is a dense document. Visitors (recruiters, hiring managers) often have specific questions: "Does he know React?", "How many years of experience?", "What certifications?". Instead of making them scan every section, the mascot lets them ask directly and get precise, cross-referenced answers. **Live example:** A visitor asks *"What is Juan's experience with Go?"* and gets a response listing Go projects (Immich Photo Manager, Cmux Resurrect), skill categories where Go appears, and experience entries involving Go — all pulled from the actual CV data in real time. ## 2. Architecture ``` ┌──────────────────────────────────────────────────────────────────┐ │ CV Site Server │ │ │ │ ┌──────────────┐ ┌──────────────────────────────────────┐ │ │ │ Data Cache │─────▶│ ADK Go Agent │ │ │ │ (cv-en.json) │ │ ┌────────────────────────────────┐ │ │ │ │ (cv-es.json) │ │ │ cv_assistant (LLM Agent) │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ │ │ Tools: │ │ │ │ │ │ │ └─ query_cv(section, query) │ │ │ │ │ │ │ ├─ search (cross-section) │ │ │ │ │ │ │ ├─ experience │ │ │ │ │ │ │ ├─ projects │ │ │ │ │ │ │ ├─ skills │ │ │ │ │ │ │ ├─ education │ │ │ │ │ │ │ ├─ languages │ │ │ │ │ │ │ ├─ certifications │ │ │ │ │ │ │ ├─ courses │ │ │ │ │ │ │ ├─ awards │ │ │ │ │ │ │ ├─ summary │ │ │ │ │ │ │ └─ all │ │ │ │ │ │ └────────────────────────────────┘ │ │ │ │ └────────────────┬───────────────────┘ │ │ │ │ │ │ ┌──────▼────────────────────────────────▼────────────────────┐ │ │ │ POST /api/chat │ │ │ │ (chat.Handler) │ │ │ │ ├─ Session management (in-memory) │ │ │ │ ├─ ADK Runner execution │ │ │ │ ├─ Markdown-to-HTML conversion │ │ │ │ └─ HTML fragment response (HTMX swap) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ▲ │ │ │ hx-post="/api/chat" │ │ ┌───────────────────────────┴────────────────────────────────┐ │ │ │ Chat Widget (HTMX + Hyperscript) │ │ │ │ ├─ Floating mascot button (robot icon) │ │ │ │ ├─ Expandable chat panel │ │ │ │ ├─ Suggested question chips (5 per language) │ │ │ │ ├─ Message history with auto-scroll │ │ │ │ ├─ Typing indicator (animated dots) │ │ │ │ └─ Session ID persistence (OOB swap) │ │ │ └────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────┐ │ Gemini 2.5 Flash │ │ (Google AI) │ └──────────────────┘ ``` ### End-to-End Flow 1. **User clicks a chip or types a question** in the chat panel. 2. **Hyperscript** sets the input value (for chips) and triggers `submit` on `#chat-form`. 3. **HTMX** intercepts the form submit and sends `POST /api/chat` with `message`, `session_id`, and `lang` fields. 4. **Go handler** (`chat.HandleChat`) receives the request, ensures a session exists, and creates an ADK `runner.Run()` call. 5. **ADK Runner** sends the message to Gemini along with the agent instruction and available tools. 6. **Gemini calls `query_cv`** with appropriate `section` and `query` parameters (the agent decides which sections to query based on its instruction strategy). 7. **`query_cv` tool** searches the cached CV JSON data (`cache.DataCache`) — the same data that renders the HTML pages. For technology queries, it performs cross-section search across experience, projects, skills, and courses simultaneously. 8. **Gemini synthesizes** the tool results into a natural language response. 9. **Handler renders** the response as an HTML fragment: user message bubble + agent message bubble + session ID hidden input. 10. **HTMX swaps** the fragment into `#chat-messages` with `beforeend` swap and auto-scrolls to the bottom. 11. **OOB swap** updates the `#chat-session-id` hidden input so subsequent messages maintain conversation context. ## 3. Components ### File Structure ``` internal/chat/ ├── agent.go # Agent definition, query_cv tool, filter functions └── handler.go # HTTP handler, session mgmt, Gemini init, response rendering templates/partials/ ├── widgets/chat-widget.html # HTMX chat panel with Hyperscript └── modals/chat-help-modal.html # Help modal with example questions by category static/css/04-interactive/ └── _chat.css # Styling (CV design tokens, dark theme, responsive) tests/mjs/ └── 83-chat-mascot.test.mjs # 46 Playwright test assertions ``` ### `internal/chat/agent.go` Defines the single LLM agent (`cv_assistant`) with one tool (`query_cv`). Contains: - **`NewAgent()`** — Creates the agent with a comprehensive instruction prompt covering 8 question types and query strategies. - **`QueryCVArgs` / `QueryCVResult`** — Input/output structs for the tool with JSON schema annotations used by ADK for function calling. - **`newQueryCVTool()`** — Wraps the query function as an agent-callable tool via `functiontool.New`. Supports 11 section values: `search`, `experience`, `projects`, `skills`, `education`, `languages`, `certifications`, `courses`, `awards`, `summary`, `all`. - **Filter helpers** — `filterExperience()`, `filterProjects()`, `filterSkills()`, `filterCourses()` perform case-insensitive keyword matching across all relevant fields (title, company, technologies, descriptions, responsibilities). - **`matchesAny()` / `matchesSlice()`** — Low-level string matching used by all filters. - **`calculateYears()`** — Computes years of experience from career start date (April 2005). **Why a single agent?** The CV data is structured and bounded. There is no need for multi-agent orchestration. One agent with one tool is the right abstraction: simple, fast, predictable. ### `internal/chat/handler.go` Handles the HTTP lifecycle: - **`NewHandler()`** — Initializes Gemini model, creates the agent, sets up in-memory session service and ADK runner. Returns a disabled handler if `GOOGLE_API_KEY` is not set. - **`Enabled()`** — Boolean check used by templates to conditionally render the widget. - **`HandleChat()`** — Processes `POST /api/chat`. Validates input, ensures session exists, runs the agent with a 30-second timeout (using a dedicated context, not the HTTP request context), renders the HTML fragment response. - **`formatResponse()`** — Converts basic markdown to HTML: escapes HTML entities first, then applies `**bold**` to ``, converts `- ` bullet lines to `