package analyzer import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type GeminiAnalyzer struct { apiKey string systemPrompt string userPromptTemplate string client *http.Client } func NewGeminiAnalyzer(apiKey, systemPrompt, userPromptTemplate string) *GeminiAnalyzer { return &GeminiAnalyzer{ apiKey: apiKey, systemPrompt: systemPrompt, userPromptTemplate: userPromptTemplate, client: &http.Client{Timeout: 30 * time.Second}, } } func (g *GeminiAnalyzer) buildUserPrompt(jobTitle, rawContent string) string { if g.userPromptTemplate == "" { return fmt.Sprintf("Analyze the following job description for candidate Pengzhan Hao.\n\nJob Title: %s\n\nJob Content:\n%s\n\nRespond with structured JSON.", jobTitle, rawContent) } res := strings.ReplaceAll(g.userPromptTemplate, "{{.JobTitle}}", jobTitle) res = strings.ReplaceAll(res, "{{.JobContent}}", rawContent) return res } func (g *GeminiAnalyzer) AnalyzeJD(ctx context.Context, jobTitle, rawContent string) (*AnalysisOutput, error) { sysPrompt := g.systemPrompt if sysPrompt == "" { sysPrompt = "You are an expert technical resume screener. Respond STRICTLY with valid JSON matching {\"ai_score\": 85.0, \"is_qualified\": true, \"summary\": \"...\", \"matched_highlights\": [...], \"missing_highlights\": [...], \"tailoring_suggestions\": [...]}." } userPrompt := g.buildUserPrompt(jobTitle, rawContent) fullPrompt := fmt.Sprintf("%s\n\n%s", sysPrompt, userPrompt) reqBody := map[string]interface{}{ "contents": []map[string]interface{}{ { "parts": []map[string]string{ {"text": fullPrompt}, }, }, }, "generationConfig": map[string]interface{}{ "response_mime_type": "application/json", }, } jsonBytes, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal Gemini payload: %w", err) } url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=%s", g.apiKey) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBytes)) if err != nil { return nil, fmt.Errorf("failed to create Gemini HTTP request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := g.client.Do(req) if err != nil { return nil, fmt.Errorf("gemini api call failed: %w", err) } defer func() { _ = resp.Body.Close() }() respBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read Gemini response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gemini api error (status %d): %s", resp.StatusCode, string(respBytes)) } var geminiResp struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` } `json:"parts"` } `json:"content"` } `json:"candidates"` } if err := json.Unmarshal(respBytes, &geminiResp); err != nil { return nil, fmt.Errorf("failed to decode Gemini envelope: %w", err) } if len(geminiResp.Candidates) == 0 || len(geminiResp.Candidates[0].Content.Parts) == 0 { return nil, fmt.Errorf("empty candidates in Gemini response") } rawJSON := geminiResp.Candidates[0].Content.Parts[0].Text rawJSON = cleanJSONResponse(rawJSON) var output AnalysisOutput if err := json.Unmarshal([]byte(rawJSON), &output); err != nil { return nil, fmt.Errorf("failed to parse Gemini JSON output: %w", err) } if output.AIScore < 0 { output.AIScore = 0 } else if output.AIScore > 100 { output.AIScore = 100 } if output.AIScore >= 60.0 { output.IsQualified = true } return &output, nil }