package analyzer import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) type GeminiAnalyzer struct { apiKey string client *http.Client } func NewGeminiAnalyzer(apiKey string) *GeminiAnalyzer { return &GeminiAnalyzer{ apiKey: apiKey, client: &http.Client{Timeout: 30 * time.Second}, } } func (g *GeminiAnalyzer) AnalyzeJD(ctx context.Context, jobTitle, rawContent string) (*AnalysisOutput, error) { prompt := fmt.Sprintf(`You are an expert technical resume screener. Analyze the following job description for the title "%s". Provide a structured JSON output with 3 fields: - "matched_highlights": list of 3-5 core technical skills/requirements matched (e.g. ["Go (Golang)", "Microservices", "REST APIs"]). - "missing_highlights": list of 1-3 missing skills, experience gaps, or extra requirements (e.g. ["10+ yrs experience", "Kubernetes administration"]). - "tailoring_suggestions": list of 2-3 specific action points for tailoring a resume to this JD. Job Title: %s Job Content: %s Respond STRICTLY with valid JSON matching the format: {"matched_highlights": [...], "missing_highlights": [...], "tailoring_suggestions": [...]}`, jobTitle, jobTitle, rawContent) reqBody := map[string]interface{}{ "contents": []map[string]interface{}{ { "parts": []map[string]string{ {"text": prompt}, }, }, }, "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) } return &output, nil }