Files
cv-site/doc/28-AI-CHAT-AGENT.md
T
juanatsap f67126e8c3 docs: add AI Chat Agent documentation and update README
- doc/28-AI-CHAT-AGENT.md: comprehensive technical documentation covering
  architecture, agent design, query_cv tool, HTMX integration, graceful
  degradation, security, and example conversations
- README.md: add AI Chat Agent section with examples, ADK Go badge,
  updated tech stack and documentation index
- doc/00-GO-DOCUMENTATION-INDEX.md: add chat agent to doc index
2026-04-08 00:30:01 +01:00

10 KiB

28. AI Chat Agent — ADK Go Integration

Overview

The CV site includes an AI-powered conversational assistant that lets visitors ask natural language questions about the CV content. Built with Google ADK Go 1.0 (Agent Development Kit), it provides instant answers by querying the same cached JSON data that renders the site.

Live example: A visitor can ask "How many Go projects has Juan built?" and get an accurate answer drawn directly from the CV data — no hallucination, no stale data.

Architecture

┌─────────────────────────────────────────────────┐
│                  CV Site Server                  │
│                                                  │
│  ┌─────────────┐     ┌────────────────────────┐ │
│  │ Data Cache   │────▶│  ADK Go Agent          │ │
│  │ (cv-en.json) │     │  ┌──────────────────┐  │ │
│  │ (cv-es.json) │     │  │ cv_assistant      │  │ │
│  └─────────────┘     │  │ (LLM Agent)       │  │ │
│         │             │  │                    │  │ │
│         │             │  │ Tools:             │  │ │
│         │             │  │ ├─ query_cv        │  │ │
│         │             │  │ │  (section+query) │  │ │
│         │             │  └──────────────────┘  │ │
│         │             └───────────┬────────────┘ │
│         │                         │               │
│  ┌──────▼─────────────────────────▼──────────┐   │
│  │         POST /api/chat                     │   │
│  │         (chat.Handler)                     │   │
│  │         ├─ Session management              │   │
│  │         ├─ ADK Runner execution            │   │
│  │         └─ HTML fragment response (HTMX)   │   │
│  └────────────────────────────────────────────┘   │
│                         ▲                         │
│                         │ hx-post                 │
│  ┌──────────────────────┴─────────────────────┐   │
│  │         Chat Widget (HTMX + Hyperscript)   │   │
│  │         ├─ Floating chat icon               │   │
│  │         ├─ Expandable panel                 │   │
│  │         ├─ Message history                  │   │
│  │         └─ Session persistence              │   │
│  └────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘
                         │
                         ▼
              ┌──────────────────┐
              │ Gemini 2.5 Flash │
              │ (Google AI)      │
              └──────────────────┘

How It Works

1. Agent Definition (internal/chat/agent.go)

A single LLM agent (cv_assistant) with one tool (query_cv):

llmagent.New(llmagent.Config{
    Name:  "cv_assistant",
    Model: llm,
    Instruction: `You answer questions about the CV owner's experience,
    projects, skills, education, and career.
    Use the query_cv tool to look up CV data before answering.
    Answer in the SAME LANGUAGE the user writes in.`,
    Tools: []tool.Tool{queryTool},
})

Why a single agent? The CV data is structured and bounded — there's no need for multi-agent orchestration. One agent with one tool is the right abstraction: simple, fast, predictable.

2. The query_cv Tool

The tool accepts two parameters:

  • section — which CV section to search: experience, projects, skills, education, languages, certifications, courses, awards, summary, or all
  • query — keyword filter (e.g., "Go", "React", "2019", "Olympic")

The tool reads from the same cache.DataCache that powers the website rendering — zero additional I/O, zero data duplication.

Filtering logic: Case-insensitive keyword matching across all relevant fields in each section (title, company, technologies, descriptions, responsibilities).

3. HTTP Handler (internal/chat/handler.go)

POST /api/chat
Content-Type: application/x-www-form-urlencoded

message=How many Go projects has Juan built?
session_id=<optional, auto-assigned>

Response: HTML fragment for HTMX swap:

<div class="chat-message chat-user">How many Go projects has Juan built?</div>
<div class="chat-message chat-agent">
  <p>Juan has built 2 projects that use Go:</p>
  <ul>
    <li>Immich Photo Manager - AI-Powered Photo Library MCP Server</li>
    <li>Cmux Resurrect - Terminal Session Persistence Tool</li>
  </ul>
</div>
<input type="hidden" name="session_id" value="c06faf66-..." form="chat-form"/>

Session management: ADK Go's in-memory session service maintains conversation context. The session ID is preserved via a hidden form input, enabling follow-up questions.

4. Chat Widget (HTMX + Hyperscript)

The UI is a floating chat panel that follows the site's existing widget pattern:

<!-- Toggle button -->
<button class="fixed-btn chat-toggle-btn"
    _="on click toggle .chat-open on #chat-panel">
    <iconify-icon icon="mdi:chat-outline"></iconify-icon>
</button>

<!-- Chat panel -->
<form hx-post="/api/chat"
      hx-target="#chat-messages"
      hx-swap="beforeend scroll:#chat-messages:bottom"
      hx-indicator="#chat-spinner">
    <input type="text" name="message" />
</form>

Key HTMX attributes:

  • hx-post="/api/chat" — sends message to the agent
  • hx-target="#chat-messages" — appends response to chat history
  • hx-swap="beforeend scroll:bottom" — auto-scrolls to latest message
  • hx-indicator="#chat-spinner" — shows loading spinner during request

Graceful Degradation

The chat feature is entirely optional. When GOOGLE_API_KEY is not set:

  1. chat.NewHandler() returns a disabled handler
  2. CVHandler receives chatEnabled: false
  3. Template data includes ChatEnabled: false
  4. The chat widget template renders nothing ({{if .ChatEnabled}}...{{end}})
  5. No JavaScript errors, no broken UI, no hidden network requests

Zero impact on the site when disabled.

Configuration

Required

# .env
GOOGLE_API_KEY=your-gemini-api-key    # From https://aistudio.google.com/apikey

Optional

MODEL_NAME=gemini-2.5-flash           # Default model (free tier)

Cost

Gemini 2.5 Flash free tier: 15 requests/minute, no credit card needed. Each chat message = 1 request. For a personal CV site, this is more than sufficient.

Example Conversations

English

Question Answer
"How many years of experience?" "Juan has 21 years of professional experience, starting in April 2005."
"What Go projects has he built?" Lists Immich Photo Manager and Cmux Resurrect with descriptions
"Has he worked with React?" Lists companies where React was used (Olympic Broadcasting, LIV Golf, etc.)
"Tell me about his time at SAP" Pulls SAP experience entry with responsibilities and technologies
"What certifications does he have?" Lists SAP CDC Full Training, SAP Cloud Platform, GDPR Compliance

Spanish

Pregunta Respuesta
"¿En cuántas empresas ha trabajado?" Lista las 11 empresas con nombres
"¿Qué tecnologías domina?" Categorías de skills con proficiency levels
"¿Tiene experiencia con autenticación?" Detalla SAP CDC, Gigya, sistemas de auth

File Structure

internal/chat/
├── agent.go          # LLM agent + query_cv tool + filter helpers
└── handler.go        # HTTP handler + session management + response rendering

templates/partials/widgets/
└── chat-widget.html  # HTMX chat panel template

static/css/04-interactive/
└── _chat.css         # Chat UI styles (responsive, dark theme)

Dependencies Added

Package Purpose Size Impact
google.golang.org/adk Agent framework (runner, session, tools) ~2 MB binary increase
google.golang.org/genai Gemini API client Included with ADK

Security Considerations

  • No personal data exposure: The agent instruction explicitly prohibits revealing email, phone, or other contact details — it directs visitors to the contact form instead
  • Input sanitization: User messages are HTML-escaped before rendering
  • Response sanitization: Agent responses go through formatResponse() which escapes HTML then applies safe markdown-to-HTML conversion
  • Rate limiting: The /api/chat endpoint inherits the site's middleware chain (recovery, logging, security headers)
  • Session isolation: Each visitor gets an independent in-memory session; sessions are ephemeral and not persisted to disk

ADK Go Concepts Used

Concept Usage
llmagent.New Creates the CV assistant agent with instruction and tools
functiontool.New Wraps the query_cv Go function as an agent-callable tool
runner.Runner Executes the agent within the HTTP handler
session.InMemoryService Maintains conversation context per visitor
genai.NewContentFromText Converts user message to ADK content format
event.IsFinalResponse() Extracts the agent's final answer from the event stream
agent.RunConfig{} Default run configuration (non-streaming)

Relation to Other Documentation