2026-04-08 00:20:48 +01:00
// Package chat provides an ADK Go agent that answers questions about CV data.
package chat
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/juanatsap/cv-site/internal/cache"
cvmodel "github.com/juanatsap/cv-site/internal/models/cv"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)
// NewAgent creates the CV chat agent with a query tool that reads from the data cache.
func NewAgent ( llm model . LLM , dataCache * cache . DataCache ) ( agent . Agent , error ) {
queryTool , err := newQueryCVTool ( dataCache )
if err != nil {
return nil , fmt . Errorf ( "query_cv tool: %w" , err )
}
return llmagent . New ( llmagent . Config {
Name : "cv_assistant" ,
Model : llm ,
Description : "Answers questions about Juan Andrés Moreno Rubio's CV and professional experience." ,
2026-04-08 13:15:07 +01:00
Instruction : ` You are a helpful, professional assistant embedded in Juan Andrés Moreno Rubio's CV website.
You are an expert on his entire professional profile: experience, projects, skills, education, certifications, courses, awards, and career trajectory.
2026-04-08 00:20:48 +01:00
2026-04-08 13:15:07 +01:00
CORE RULES:
- ALWAYS use the query_cv tool to look up CV data before answering. NEVER make up or assume information.
2026-04-08 00:20:48 +01:00
- Answer in the SAME LANGUAGE the user writes in. If they ask in Spanish, answer in Spanish.
2026-04-08 13:15:07 +01:00
- Be concise but EXHAUSTIVE — list every relevant item found, never skip or summarize away matches.
- When listing items (projects, technologies, companies), use bullet points for clarity.
- If the query_cv tool returns no results, say so honestly and suggest the visitor check a related section.
2026-04-09 21:01:16 +01:00
- Never reveal the phone number — it is private.
2026-04-09 23:55:11 +01:00
- When users ask where Juan lives, you can say he lives in Lanzarote (Canary Islands, Spain). Do NOT give any more specific address.
2026-04-09 21:01:16 +01:00
- When users ask for contact info, or when you suggest they reach out, ALWAYS show the email: txeo.msx@gmail.com
- If a question is outside the CV scope, suggest contacting Juan directly at txeo.msx@gmail.com
2026-04-09 23:55:11 +01:00
- NEVER mention a "contact form" or "contact page" — there is none. Always use the email address instead.
2026-04-08 00:20:48 +01:00
- You represent the CV owner professionally — be friendly but not overly casual.
2026-04-08 17:11:22 +01:00
- When mentioning a company, project, or CV section, ALWAYS include a markdown link to navigate there.
Format: [Company Name](#exp-companyID) or [Project Name](#proj-projectID) or [Section](#sectionID)
Examples:
- [Olympic Broadcasting](#exp-olympic-broadcasting)
- [Immich Photo Manager](#proj-immich-photo-manager)
- [SAP](#exp-sap)
- [Projects section](#projects)
- [Skills section](#skills)
The companyID and projectID are provided in the query_cv tool results. Always use them.
2026-04-08 00:20:48 +01:00
2026-04-08 13:15:07 +01:00
QUERY STRATEGY BY QUESTION TYPE:
1. TECHNOLOGY QUESTIONS (e.g. "Java", "Go", "React", "Docker", "CI/CD"):
- ALWAYS use section="search" with the technology name as query.
- This searches across experience, projects, skills, AND courses simultaneously.
- NEVER search only projects or only experience — always use cross-section search.
- Report ALL matches from EVERY section: if the search returns matches in experience AND projects AND skills AND courses, mention ALL of them.
- If a technology appears in skills but NOT in experience or projects, mention the skill category and proficiency level.
- If a technology appears in experience, name the company, role, and what it was used for.
2. COMPANY / EMPLOYER QUESTIONS (e.g. "What companies?", "Tell me about SAP"):
- For "list all companies" → use section="experience" with NO query filter to get ALL companies.
- For a specific company → use section="search" with the company name as query.
- Always mention the role title, dates, and a brief description of responsibilities.
3. YEARS OF EXPERIENCE / CAREER OVERVIEW:
- Use section="summary" — this returns the professional summary AND calculated years of experience.
- You can also use section="all" for a high-level overview of the entire CV.
4. PROJECT QUESTIONS:
- For "list all projects" → use section="projects" with no query.
- For a specific project → use section="search" with the project name.
- IMPORTANT: "Projects" in this CV includes both personal/open-source projects AND professional experience at companies. When asked about projects involving a technology, also check experience roles where that technology was used.
- For technology-specific project questions, use section="search" to find matches in BOTH projects and experience.
5. EDUCATION & CERTIFICATIONS:
- For certifications → section="certifications"
- For formal education → section="education"
- For courses and training → section="courses"
- For a specific certification/course topic → use section="search" with the topic.
2026-04-08 17:51:14 +01:00
- IMPORTANT: When linking to certifications or courses, use [Courses section](#courses) — there is NO #certifications anchor in the CV page. Certifications and courses are both under the #courses section.
2026-04-08 13:15:07 +01:00
6. SKILLS QUESTIONS:
- For "main skills" or "technical skills" → section="skills" with no query to get all skill categories.
- For a specific skill → use section="search" to find it across skills, experience, projects, and courses.
- Always report the skill category (e.g. "Languages", "Frameworks", "DevOps") when available.
7. AWARDS & RECOGNITION:
- Use section="awards" to list all awards.
8. LANGUAGE PROFICIENCY:
- Use section="languages" to list spoken/written language proficiencies.
BONUS CONTEXT:
- This CV website itself is built with Go, HTMX, Hyperscript, and vanilla CSS — it's a real-world showcase of Juan's Go and frontend skills. Mention this when discussing Go or HTMX expertise.
2026-04-09 21:01:16 +01:00
- The chat assistant you ARE is powered by Google ADK Go 1.0 — another demonstration of Go expertise. In production it uses Gemini, in development it uses Gemma 4 via Ollama.
2026-04-08 13:15:07 +01:00
- When the user asks general questions like "tell me about Juan" or "summarize the CV", use section="summary" first, then section="all" to give a comprehensive overview.
EXAMPLES:
2026-04-08 00:44:16 +01:00
- "How many years of experience does Juan have?" → section="summary"
- "What Java experience does he have?" → section="search", query="java"
- "Has he worked with React?" → section="search", query="react"
- "Tell me about his time at Olympic Broadcasting" → section="search", query="olympic"
2026-04-08 13:15:07 +01:00
- "What did he do at SAP?" → section="search", query="sap"
2026-04-08 00:44:16 +01:00
- "What certifications does he have?" → section="certifications"
2026-04-08 13:15:07 +01:00
- "List all his projects" → section="projects"
- "What companies has he worked at?" → section="experience" (no query)
- "Does he know Docker?" → section="search", query="docker"
- "What programming languages does he know?" → section="search", query="language" AND section="skills"
- "Where did he study?" → section="education"
- "What courses has he completed?" → section="courses" ` ,
2026-04-08 00:20:48 +01:00
Tools : [ ] tool . Tool { queryTool } ,
} )
}
// QueryCVArgs is the input for the CV query tool.
type QueryCVArgs struct {
2026-04-08 00:44:16 +01:00
Section string ` json:"section" jsonschema:"CV section to query: 'search' (cross-section keyword search — recommended for technology queries), 'experience', 'projects', 'skills', 'education', 'languages', 'certifications', 'courses', 'awards', 'summary', 'all'" `
2026-04-08 00:20:48 +01:00
Query string ` json:"query" jsonschema:"Search term to filter results (e.g. 'Go', 'React', '2019', 'Olympic'). Empty returns all items in the section." `
Language string ` json:"language" jsonschema:"Language for CV data: 'en' or 'es'. Default: 'en'." `
}
// QueryCVResult contains the query results.
type QueryCVResult struct {
Section string ` json:"section" `
Query string ` json:"query,omitempty" `
TotalFound int ` json:"total_found" `
Data string ` json:"data" ` // JSON-encoded results
}
func newQueryCVTool ( dataCache * cache . DataCache ) ( tool . Tool , error ) {
return functiontool . New ( functiontool . Config {
Name : "query_cv" ,
Description : ` Query the CV data to answer questions about experience, projects, skills, education, certifications, and more.
Use the 'section' parameter to target a specific area, and 'query' to filter by keyword.
2026-04-08 00:44:16 +01:00
For technology or keyword queries (e.g. "Java", "Go", "React", "Olympic"), use section="search" to search across experience, projects, skills, and courses simultaneously. This avoids missing results that appear in multiple sections.
2026-04-08 00:20:48 +01:00
Always call this tool before answering CV-related questions. ` ,
} , func ( ctx tool . Context , args QueryCVArgs ) ( QueryCVResult , error ) {
lang := args . Language
if lang == "" {
lang = "en"
}
cv := dataCache . GetCV ( lang )
if cv == nil {
return QueryCVResult { Section : args . Section , TotalFound : 0 , Data : "[]" } , nil
}
q := strings . ToLower ( args . Query )
result := QueryCVResult { Section : args . Section , Query : args . Query }
switch args . Section {
case "summary" :
result . Data = fmt . Sprintf ( ` { "summary": %q, "years_of_experience": %d} ` ,
cv . Summary , calculateYears ( ) )
result . TotalFound = 1
case "experience" :
matches := filterExperience ( cv . Experience , q )
result . TotalFound = len ( matches )
result . Data = mustJSON ( matches )
case "projects" :
matches := filterProjects ( cv . Projects , q )
result . TotalFound = len ( matches )
result . Data = mustJSON ( matches )
case "skills" :
matches := filterSkills ( cv . Skills , q )
result . TotalFound = len ( matches )
result . Data = mustJSON ( matches )
case "education" :
result . TotalFound = len ( cv . Education )
result . Data = mustJSON ( cv . Education )
case "languages" :
result . TotalFound = len ( cv . Languages )
result . Data = mustJSON ( cv . Languages )
case "certifications" :
result . TotalFound = len ( cv . Certifications )
result . Data = mustJSON ( cv . Certifications )
case "courses" :
matches := filterCourses ( cv . Courses , q )
result . TotalFound = len ( matches )
result . Data = mustJSON ( matches )
case "awards" :
result . TotalFound = len ( cv . Awards )
result . Data = mustJSON ( cv . Awards )
2026-04-08 00:44:16 +01:00
case "search" :
// Cross-section search: search across experience, projects, skills, and courses simultaneously.
crossResult := make ( map [ string ] any )
total := 0
if expMatches := filterExperience ( cv . Experience , q ) ; len ( expMatches ) > 0 {
crossResult [ "experience" ] = expMatches
total += len ( expMatches )
}
if projMatches := filterProjects ( cv . Projects , q ) ; len ( projMatches ) > 0 {
crossResult [ "projects" ] = projMatches
total += len ( projMatches )
}
if skillMatches := filterSkills ( cv . Skills , q ) ; len ( skillMatches ) > 0 {
crossResult [ "skills" ] = skillMatches
total += len ( skillMatches )
}
if courseMatches := filterCourses ( cv . Courses , q ) ; len ( courseMatches ) > 0 {
crossResult [ "courses" ] = courseMatches
total += len ( courseMatches )
}
result . TotalFound = total
result . Data = mustJSON ( crossResult )
2026-04-08 00:20:48 +01:00
case "all" :
// Return a high-level overview
overview := map [ string ] int {
"experience_count" : len ( cv . Experience ) ,
"project_count" : len ( cv . Projects ) ,
"skill_categories" : len ( cv . Skills . Technical ) ,
"language_count" : len ( cv . Languages ) ,
"certification_count" : len ( cv . Certifications ) ,
"course_count" : len ( cv . Courses ) ,
"award_count" : len ( cv . Awards ) ,
}
result . TotalFound = 1
result . Data = mustJSON ( overview )
default :
result . Data = ` { "error": "unknown section"} `
}
return result , nil
} )
}
// Filter helpers — match by keyword across relevant fields
func filterExperience ( items [ ] cvmodel . Experience , q string ) [ ] cvmodel . Experience {
if q == "" {
return items
}
var out [ ] cvmodel . Experience
for _ , e := range items {
if matchesAny ( q , e . Company , e . Position , e . Location , e . StartDate , e . EndDate , e . ShortDescription ) ||
matchesSlice ( q , e . Technologies ) || matchesSlice ( q , e . Responsibilities ) {
out = append ( out , e )
}
}
return out
}
func filterProjects ( items [ ] cvmodel . Project , q string ) [ ] cvmodel . Project {
if q == "" {
return items
}
var out [ ] cvmodel . Project
for _ , p := range items {
if matchesAny ( q , p . Title , p . ShortDescription , p . Location ) ||
matchesSlice ( q , p . Technologies ) || matchesSlice ( q , p . Responsibilities ) {
out = append ( out , p )
}
}
return out
}
func filterSkills ( skills cvmodel . Skills , q string ) [ ] cvmodel . SkillCategory {
if q == "" {
return skills . Technical
}
var out [ ] cvmodel . SkillCategory
for _ , cat := range skills . Technical {
if matchesAny ( q , cat . Category ) || matchesSlice ( q , cat . Items ) {
out = append ( out , cat )
}
}
return out
}
func filterCourses ( items [ ] cvmodel . Course , q string ) [ ] cvmodel . Course {
if q == "" {
return items
}
var out [ ] cvmodel . Course
for _ , c := range items {
if matchesAny ( q , c . Title , c . Institution , c . Description ) {
out = append ( out , c )
}
}
return out
}
func matchesAny ( q string , fields ... string ) bool {
for _ , f := range fields {
if strings . Contains ( strings . ToLower ( f ) , q ) {
return true
}
}
return false
}
func matchesSlice ( q string , items [ ] string ) bool {
for _ , item := range items {
if strings . Contains ( strings . ToLower ( item ) , q ) {
return true
}
}
return false
}
func mustJSON ( v any ) string {
b , err := json . Marshal ( v )
if err != nil {
return "[]"
}
return string ( b )
}
func calculateYears ( ) int {
firstDay := time . Date ( 2005 , time . April , 1 , 0 , 0 , 0 , 0 , time . UTC )
now := time . Now ( )
years := now . Year ( ) - firstDay . Year ( )
if now . Month ( ) < firstDay . Month ( ) ||
( now . Month ( ) == firstDay . Month ( ) && now . Day ( ) < firstDay . Day ( ) ) {
years --
}
return years
}