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.
This commit is contained in:
@@ -256,6 +256,7 @@ This project includes comprehensive documentation organized by purpose:
|
||||
- **[ARCHITECTURE.md](doc/ARCHITECTURE.md)** - System design, patterns, and technical decisions
|
||||
- **[API.md](doc/API.md)** - Complete HTTP API reference and HTMX integration
|
||||
- **[AI-CHAT-AGENT.md](doc/28-AI-CHAT-AGENT.md)** - ADK Go agent architecture, tool design, and integration details
|
||||
- **[AI-CHAT-SHOWCASE.md](doc/29-AI-CHAT-SHOWCASE.md)** - Technical showcase: AI-powered CV navigation with ADK Go, dual-provider architecture, and document GPS
|
||||
|
||||
### 📋 Policies & Standards
|
||||
- **[SECURITY.md](doc/9-SECURITY.md)** - Complete security architecture, implementation, and testing guide
|
||||
|
||||
@@ -36,7 +36,7 @@ This documentation covers the core Go systems that power the CV site, with a foc
|
||||
- Coverage gap explanations
|
||||
- Best practices and CI/CD integration
|
||||
|
||||
5. **[AI Chat Agent — CV Assistant Mascot](28-AI-CHAT-AGENT.md)** (~500 lines)
|
||||
5. **[AI Chat Agent — CV Assistant Mascot](28-AI-CHAT-AGENT.md)** (~544 lines)
|
||||
- Complete mascot feature reference: architecture, components, intelligence
|
||||
- ADK Go 1.0 integration with Gemini 2.5 Flash
|
||||
- Agent definition with query_cv tool (11 section types, cross-section search)
|
||||
@@ -47,6 +47,14 @@ This documentation covers the core Go systems that power the CV site, with a foc
|
||||
- Design system integration (CSS tokens, dark theme, responsive)
|
||||
- Graceful degradation, security, and testing (46 Playwright assertions)
|
||||
|
||||
6. **[AI Chat Showcase — Technical Writeup](29-AI-CHAT-SHOWCASE.md)** (~250 lines)
|
||||
- Public-facing technical showcase of the AI chat feature
|
||||
- Architecture diagram with dual-provider fallback
|
||||
- 9 key technical decisions explained with code examples
|
||||
- CV navigation links (GPS for the CV)
|
||||
- Technology stack and file structure
|
||||
- What this demonstrates for potential employers/clients
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
### By Feature
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user