package rag import ( "fmt" "strings" "resume-builder/internal/db" "resume-builder/internal/llm" ) type Service struct { db *db.Database llm *llm.Client } func New(d *db.Database, client *llm.Client) *Service { return &Service{db: d, llm: client} } func (s *Service) buildContext(resumeData *db.ResumeData) string { var sb strings.Builder sb.WriteString("Here is the candidate's professional information:\n\n") if resumeData.Profile.Summary != "" { sb.WriteString(fmt.Sprintf("## Professional Summary\n%s\n\n", resumeData.Profile.Summary)) } // Experiences if len(resumeData.Experiences) > 0 { sb.WriteString("## Work Experience\n\n") for _, e := range resumeData.Experiences { sb.WriteString(fmt.Sprintf("### %s at %s\n", e.Title, e.Company)) if e.StartDate != "" || e.EndDate != "" { end := e.EndDate if end == "" { end = "Present" } sb.WriteString(fmt.Sprintf("Period: %s - %s\n", e.StartDate, end)) } if e.Location != "" { sb.WriteString(fmt.Sprintf("Location: %s\n", e.Location)) } if e.Description != "" { sb.WriteString(fmt.Sprintf("Description:\n%s\n", e.Description)) } sb.WriteString("\n") } } // Education if len(resumeData.Educations) > 0 { sb.WriteString("## Education\n\n") for _, e := range resumeData.Educations { sb.WriteString(fmt.Sprintf("### %s, %s\n", e.Degree, e.FieldOfStudy)) sb.WriteString(fmt.Sprintf("Institution: %s\n", e.Institution)) if e.StartDate != "" || e.EndDate != "" { end := e.EndDate if end == "" { end = "Present" } sb.WriteString(fmt.Sprintf("Period: %s - %s\n", e.StartDate, end)) } if e.GPA != "" { sb.WriteString(fmt.Sprintf("GPA: %s\n", e.GPA)) } if e.Description != "" { sb.WriteString(fmt.Sprintf("Details: %s\n", e.Description)) } sb.WriteString("\n") } } // Skills if len(resumeData.Skills) > 0 { sb.WriteString("## Skills\n\n") currentCategory := "" for _, sk := range resumeData.Skills { if sk.Category != currentCategory { currentCategory = sk.Category if currentCategory != "" { sb.WriteString(fmt.Sprintf("### %s\n", currentCategory)) } } level := "" if sk.Level != "" { level = fmt.Sprintf(" (%s)", sk.Level) } sb.WriteString(fmt.Sprintf("- %s%s", sk.Name, level)) if sk.Description != "" { sb.WriteString(fmt.Sprintf(": %s", sk.Description)) } sb.WriteString("\n") } sb.WriteString("\n") } // Work samples if len(resumeData.WorkSamples) > 0 { sb.WriteString("## Work Samples & Projects\n\n") for _, w := range resumeData.WorkSamples { sb.WriteString(fmt.Sprintf("### %s\n", w.Title)) if w.Description != "" { sb.WriteString(fmt.Sprintf("Description: %s\n", w.Description)) } if w.URL != "" { sb.WriteString(fmt.Sprintf("URL: %s\n", w.URL)) } if w.Content != "" { sb.WriteString(fmt.Sprintf("Content: %s\n", w.Content)) } sb.WriteString("\n") } } return sb.String() } var resumeYAMLPrompt = `You are an expert resume writer. Generate a targeted resume in YAML format based on the candidate's information. %s %s Instructions: 1. Output ONLY valid YAML matching the schema below - no markdown code fences, no explanation 2. Tailor the content to emphasize skills and experiences relevant to the target position 3. Use action verbs and quantifiable achievements in bullet points 4. Keep each bullet to one concise line (max ~20 words) 5. Keep it to one page worth of content 6. Only include: %s 7. DO NOT include name, email, phone, location, or website - those will be filled in separately 8. If "work samples" is requested and relevant samples exist, include 1-3 as work_samples entries YAML Schema (omit the profile fields at the top): summary: "1-2 sentence professional summary" experience: - company: "Company Name" title: "Job Title" start_date: "Mon YYYY" end_date: "Mon YYYY" or "Present" location: "City, State" bullets: - "Achievement bullet point starting with action verb" - "Another achievement" education: - institution: "University Name" degree: "Degree Name" field_of_study: "Field" start_date: "YYYY" end_date: "YYYY" location: "City, State" gpa: "3.8" (optional, omit if not notable) skills: - category: "Category Name" items: - "Skill 1" - "Skill 2" work_samples: - title: "Project Name" description: "Brief description of the project and your role" url: "https://github.com/..." (optional) Output only the YAML, nothing else.` func (s *Service) buildResumeMessages(jobPosting, includeSections string) ([]llm.ChatMessage, error) { resumeData, err := s.db.GetResumeData() if err != nil { return nil, fmt.Errorf("failed to get resume data: %w", err) } context := s.buildContext(resumeData) jobContext := "" if jobPosting != "" { jobContext = fmt.Sprintf("\n## Target Job Posting\n%s\n", jobPosting) } sections := "all sections" if includeSections != "" { sections = includeSections } prompt := fmt.Sprintf(resumeYAMLPrompt, context, jobContext, sections) return []llm.ChatMessage{ {Role: "system", Content: "You are a professional resume writer that produces clean, targeted YAML resume content. Output only valid YAML with no markdown code fences."}, {Role: "user", Content: prompt}, }, nil } func (s *Service) GenerateResume(jobPosting string, includeSections string, cfg *llm.Config) (string, error) { messages, err := s.buildResumeMessages(jobPosting, includeSections) if err != nil { return "", err } resp, err := s.llm.Chat(messages, 4096, 0.7, cfg) if err != nil { return "", err } if len(resp.Choices) == 0 { return "", fmt.Errorf("no response from LLM") } return resp.Choices[0].Message.Content, nil } func (s *Service) StreamResume(jobPosting string, includeSections string, callback func(string), cfg *llm.Config) error { messages, err := s.buildResumeMessages(jobPosting, includeSections) if err != nil { return err } return s.llm.StreamChat(messages, 4096, 0.7, callback, cfg) } var coverLetterPrompt = `You are an expert cover letter writer. Generate a compelling, personalized cover letter based on the candidate's information. %s Target position: %s at %s %s Instructions: 1. Write a genuine, professional cover letter 2. Create a personalized opening that shows knowledge of the company/role 3. Connect the candidate's experience directly to the job requirements 4. Use specific examples and achievements 5. Keep it to one page 6. End with a strong call to action 7. Use [Hiring Manager] as the salutation Output only the plain text cover letter, no HTML tags, no markdown code fences, no explanation.` var answerPrompt = `You are helping a candidate answer a question from a job application. Based on the candidate's background, provide a thoughtful, authentic answer. Candidate background: %s %s Question from application: %s Instructions: 1. Write a genuine, professional answer that draws on the candidate's actual experience 2. Be specific and use concrete examples from the candidate's background 3. Keep the answer concise but thorough (2-4 paragraphs typically) 4. Match the tone to the type of question (technical, behavioral, etc.) 5. For behavioral questions, use the STAR method (Situation, Task, Action, Result) 6. Do not fabricate experience - only use what's in the candidate's profile 7. If the candidate's background doesn't directly address the question, acknowledge it honestly and pivot to relevant experience Output only the answer text, no preamble, no explanation.` func (s *Service) buildCoverLetterMessages(company, position, jobPosting string) ([]llm.ChatMessage, error) { resumeData, err := s.db.GetResumeData() if err != nil { return nil, fmt.Errorf("failed to get resume data: %w", err) } context := s.buildContext(resumeData) prompt := fmt.Sprintf(coverLetterPrompt, context, position, company, jobPosting) return []llm.ChatMessage{ {Role: "system", Content: "You are a professional cover letter writer. Output only plain text."}, {Role: "user", Content: prompt}, }, nil } func (s *Service) GenerateCoverLetter(company, position, jobPosting string, cfg *llm.Config) (string, error) { messages, err := s.buildCoverLetterMessages(company, position, jobPosting) if err != nil { return "", err } resp, err := s.llm.Chat(messages, 4096, 0.7, cfg) if err != nil { return "", err } if len(resp.Choices) == 0 { return "", fmt.Errorf("no response from LLM") } return resp.Choices[0].Message.Content, nil } func (s *Service) StreamCoverLetter(company, position, jobPosting string, callback func(string), cfg *llm.Config) error { messages, err := s.buildCoverLetterMessages(company, position, jobPosting) if err != nil { return err } return s.llm.StreamChat(messages, 4096, 0.7, callback, cfg) } func (s *Service) buildAnswerMessages(question, jobPosting string) ([]llm.ChatMessage, error) { resumeData, err := s.db.GetResumeData() if err != nil { return nil, fmt.Errorf("failed to get resume data: %w", err) } context := s.buildContext(resumeData) prompt := fmt.Sprintf(answerPrompt, context, jobPosting, question) return []llm.ChatMessage{ {Role: "system", Content: "You are a career coach helping candidates write authentic, well-crafted answers to job application questions. Be helpful and professional."}, {Role: "user", Content: prompt}, }, nil } func (s *Service) GenerateAnswer(question string, jobPosting string, cfg *llm.Config) (string, error) { messages, err := s.buildAnswerMessages(question, jobPosting) if err != nil { return "", err } resp, err := s.llm.Chat(messages, 4096, 0.7, cfg) if err != nil { return "", err } if len(resp.Choices) == 0 { return "", fmt.Errorf("no response from LLM") } return resp.Choices[0].Message.Content, nil } func (s *Service) StreamAnswer(question string, jobPosting string, callback func(string), cfg *llm.Config) error { messages, err := s.buildAnswerMessages(question, jobPosting) if err != nil { return err } return s.llm.StreamChat(messages, 4096, 0.7, callback, cfg) }