Files
cv-site/doc/29-AI-CHAT-SHOWCASE.md
juanatsap afa93be8fe docs: add AI Chat Showcase — public technical writeup for GitHub
doc/29-AI-CHAT-SHOWCASE.md: comprehensive technical showcase covering
the AI-powered CV navigation feature for potential clients/employers.

9 technical decisions explained with code:
1. ADK Go 1.0 as agent framework
2. Single agent, single tool design
3. Cross-section search across all CV data
4. CV navigation links (GPS for the CV)
5. Dual-provider with auto-fallback (Gemini→Ollama)
6. Model warmup on chat open
7. HTMX + plain JS (no SPA framework)
8. Rate limiting (30 req/hour/IP)
9. Graceful degradation

Linked from README.md and doc index.
2026-04-08 17:13:18 +01:00

244 lines
11 KiB
Markdown

# 29. AI-Powered CV Navigation — Technical Showcase
## What This Is
This CV site includes an AI assistant that lets visitors navigate and query the CV through natural language conversation. Instead of scanning a dense document, visitors ask questions like *"What Go projects has he built?"* or *"Has he worked with React?"* and get instant, cross-referenced answers with **clickable links that scroll directly to the relevant section**.
**Live at:** [juan.andres.morenorub.io](https://juan.andres.morenorub.io)
---
## The Problem
A CV is information-dense. Recruiters and hiring managers have specific questions but must scan every section to find answers. Technologies span multiple sections (a language appears in experience, projects, AND skills). Cross-referencing is manual and slow.
## The Solution
An AI agent embedded in the CV page that:
1. **Understands the entire CV** — searches across all sections simultaneously
2. **Answers in natural language** — bilingual (English/Spanish), concise, with bullet points
3. **Navigates the document** — every company, project, and section name in the response is a **clickable link** that closes the chat, scrolls to the target, and highlights it with a green pulse
4. **Degrades gracefully** — no API key? No chat icon. API down? Automatic fallback to local AI.
---
## Technical Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ CV Site (Go) │
│ │
│ Visitor clicks mascot → chat opens → types question │
│ │ │
│ ▼ │
│ HTMX POST /api/chat ──→ Go Handler │
│ │ │
│ ┌─────────┴──────────┐ │
│ ▼ ▼ │
│ Try Gemini Try Ollama │
│ (primary) (auto-fallback) │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ADK Go Agent │
│ "cv_assistant" │
│ │ │
│ ▼ │
│ query_cv tool │
│ (cross-section search) │
│ │ │
│ ▼ │
│ Cached CV JSON │
│ (same data that renders the page) │
│ │ │
│ ▼ │
│ Response with navigation links │
│ [Olympic Broadcasting](#exp-olympic) │
│ │ │
│ ▼ │
│ HTMX swaps into chat panel │
│ Links scroll + highlight on click │
└─────────────────────────────────────────────────────────────┘
```
## Key Technical Decisions
### 1. Google ADK Go 1.0 as the Agent Framework
We chose [ADK Go](https://github.com/google/adk-go) (v1.0, released March 2026) for the agent layer. ADK Go provides:
- **`llmagent.New`** — declarative agent definition with instruction and tools
- **`functiontool.New`** — type-safe Go function → agent tool bridge with auto-generated JSON schema
- **`runner.Runner`** — manages agent execution, sessions, and tool calling loops
- **`session.InMemoryService`** — lightweight session management for conversation context
Why not a simpler approach (raw API calls)? ADK Go handles the tool-calling protocol automatically — the agent decides which tool to call, the framework executes it, feeds results back, and the agent synthesizes. With raw API calls, we'd need to implement this loop ourselves.
### 2. Single Agent, Single Tool
The CV data is bounded and structured. We use one agent (`cv_assistant`) with one tool (`query_cv`). Multi-agent orchestration would be over-engineering here. The intelligence comes from:
- A comprehensive instruction prompt covering 8 question types
- A `search` mode that queries across experience, projects, skills, and courses simultaneously
- Instruction to always include navigation links using CV anchor IDs
### 3. Cross-Section Search
When a visitor asks about a technology (e.g., "Go"), the tool searches **all sections at once**:
```go
case "search":
crossResult := make(map[string]any)
if exp := filterExperience(cv.Experience, q); len(exp) > 0 {
crossResult["experience"] = exp
}
if proj := filterProjects(cv.Projects, q); len(proj) > 0 {
crossResult["projects"] = proj
}
if skills := filterSkills(cv.Skills, q); len(skills) > 0 {
crossResult["skills"] = skills
}
if courses := filterCourses(cv.Courses, q); len(courses) > 0 {
crossResult["courses"] = courses
}
```
This prevents the classic problem of "I searched projects but the answer was in experience."
### 4. CV Navigation Links (GPS for the CV)
The agent includes markdown links in its responses:
```markdown
[Olympic Broadcasting](#exp-olympic-broadcasting) — SAP CDC solutions...
[Immich Photo Manager](#proj-immich-photo-manager) — MCP server for...
See the [Skills section](#skills) for full proficiency details.
```
The `formatResponse` function converts these to clickable HTML links. When clicked, JavaScript:
1. Closes the chat panel
2. Smooth-scrolls to the target element
3. Pulses a green highlight for 2 seconds
This turns the chat into a **navigation tool** — like Google Maps for a document.
### 5. Dual-Provider with Automatic Fallback
```go
// Handler has primary + fallback runners
type Handler struct {
primary *chatRunner // Gemini (fast, cloud)
fallback *chatRunner // Ollama (local, unlimited)
}
// Try primary, fall back on any error
response, sessionID, err := h.runAgent(h.primary, message)
if err != nil && h.fallback != nil {
log.Printf("Primary failed, falling back to %s", h.fallback.label)
response, sessionID, err = h.runAgent(h.fallback, message)
}
```
- **Primary:** Gemini 2.5 Flash — fast (~2s), pay-as-you-go (~$0.0003/question)
- **Fallback:** Ollama with Mistral Small 3.2 on local Mac Mini via Tailscale — free, unlimited
- **Switching:** Automatic and transparent. If Gemini returns 429/503, Ollama handles the request.
- **No manual intervention** — visitors never see the provider switch.
### 6. Model Warmup on Chat Open
Ollama loads models on demand (~10-15s cold start). To hide this latency:
```javascript
function toggleChatPanel() {
// ... open panel ...
if (!chatWarmedUp) {
chatWarmedUp = true;
fetch('/api/chat/warmup', { method: 'POST' }); // background
}
}
```
When the visitor opens the chat, a silent warmup request fires. By the time they type a question, the model is loaded and ready.
### 7. HTMX + Plain JavaScript
The chat widget uses HTMX for server communication and plain JavaScript for interactions:
```html
<!-- Form submits via HTMX -->
<form id="chat-form" hx-post="/api/chat"
hx-target="#chat-messages"
hx-swap="beforeend scroll:#chat-messages:bottom"
hx-indicator="#chat-typing">
<!-- Chips trigger via JavaScript -->
<button onclick="sendChatQuestion('What Go projects has he built?')">
Go projects?
</button>
```
Responses are HTML fragments — the server renders the chat bubbles, HTMX swaps them in. No client-side state management, no JSON parsing, no virtual DOM.
### 8. Rate Limiting
```go
chatRateLimiter := middleware.NewRateLimiter(30, 1*time.Hour)
mux.Handle("/api/chat", chatRateLimiter.Middleware(...))
```
30 requests per hour per IP — generous for genuine visitors, prevents abuse.
### 9. Graceful Degradation
```go
func NewHandler(dataCache *cache.DataCache) *Handler {
// Try Gemini → Try Ollama → Disable chat
// If neither provider works, chat icon doesn't appear at all
}
```
```html
{{if .ChatEnabled}}
<!-- entire chat widget -->
{{end}}
```
No API key? No Ollama? The chat icon simply doesn't render. Zero JavaScript errors, zero broken UI, zero console noise.
---
## Technology Stack
| Component | Technology | Purpose |
|-----------|-----------|---------|
| Agent Framework | [Google ADK Go 1.0](https://github.com/google/adk-go) | Agent definition, tool calling, session management |
| Primary LLM | Gemini 2.5 Flash | Cloud inference, fast responses |
| Fallback LLM | Mistral Small 3.2 via [Ollama](https://ollama.com) | Local inference on Apple Silicon |
| Server Communication | HTMX 2.0 | Form submission, response swapping, indicators |
| Interactions | Plain JavaScript | Panel toggle, chip clicks, navigation scroll |
| Backend | Go 1.25+ stdlib `net/http` | HTTP handler, markdown→HTML, rate limiting |
| Styling | CSS with CV design tokens | Green theme, dark mode, responsive |
## Files
```
internal/chat/
├── agent.go # LLM agent + query_cv tool + cross-section search
├── handler.go # Dual-provider handler, warmup, fallback, response rendering
└── ollama.go # Ollama model.LLM adapter (OpenAI-compatible API)
templates/partials/
├── widgets/chat-widget.html # HTMX chat panel + JS functions
└── modals/chat-help-modal.html # Accordion help modal with clickable questions
static/css/04-interactive/_chat.css # Full styling (tokens, dark theme, responsive, nav links)
tests/mjs/83-chat-mascot.test.mjs # 46 Playwright test assertions
```
## What This Demonstrates
- **AI agent integration in production Go applications** — not a prototype, a deployed feature
- **ADK Go 1.0 in a real-world use case** — function calling, session management, multi-provider
- **Multi-provider LLM architecture** — cloud primary with local fallback, transparent switching
- **Hypermedia-driven AI UI** — HTMX server-rendered responses, no SPA framework needed
- **Document navigation via AI** — chat responses that link to and highlight document sections
- **Graceful engineering** — degrades cleanly, rate-limited, bilingual, theme-aware