package gemini import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/jobs-scraper/internal/pkg/domain" "google.golang.org/genai" ) type GeminiService struct { client *genai.Client model string } func NewGeminiService(ctx context.Context, key, model string) (*GeminiService, error) { config := genai.ClientConfig{ APIKey: key, } client, err := genai.NewClient(ctx, &config) if err != nil { return nil, err } return &GeminiService{client: client, model: model}, nil } func (gs *GeminiService) ParseJobData(ctx context.Context, htmlContent string) (*domain.JobDescription, error) { prompt := fmt.Sprintf(`Extract job posting information from the following HTML content. CRITICAL RULES: 1. Return ONLY valid JSON - no markdown, no code blocks, no backticks, no explanations 2. Use SINGLE-LINE JSON (no pretty printing, no newlines inside the JSON) 3. All criteria values MUST be strings (never use booleans, numbers, or arrays) 4. Properly escape all quotes inside strings using \" 5. Use only standard ASCII quotes ("), never smart quotes (" " ' ') Required fields: - description: Full job description as a single string - criteria: Object with keys like title, company, location, salary, skills, experience, job_type, remote Example (SINGLE LINE): {"description":"Full job description text here...","criteria":{"title":"Software Engineer","location":"City, Country","salary":"$100k-$150k","skills":"Go, Python, Docker","experience":"3-5 years","job_type":"Full-time","remote":"Remote"}} HTML Content: %s`, htmlContent) response, err := gs.client.Models.GenerateContent( ctx, gs.model, genai.Text(prompt), nil, ) if err != nil { return &domain.JobDescription{}, err } jsonContent := response.Text() log.Printf("Raw API response for job description: %s", jsonContent) jsonContent = cleanMarkdownCodeBlocks(jsonContent) log.Printf("Cleaned JSON (first 500 chars): %s", jsonContent[:min(500, len(jsonContent))]) var result struct { Description string `json:"description"` Criteria map[string]string `json:"criteria"` } if err := json.Unmarshal([]byte(jsonContent), &result); err != nil { if syntaxErr, ok := err.(*json.SyntaxError); ok { start := int(syntaxErr.Offset) - 50 if start < 0 { start = 0 } end := int(syntaxErr.Offset) + 50 if end > len(jsonContent) { end = len(jsonContent) } log.Printf("JSON parse error at position %d, context: ...%s...", syntaxErr.Offset, jsonContent[start:end]) } return nil, fmt.Errorf("failed to parse JSON response: %v. Cleaned JSON length: %d", err, len(jsonContent)) } jobDescription := &domain.JobDescription{ Description: result.Description, Criteria: result.Criteria, } return jobDescription, nil } // cleanMarkdownCodeBlocks removes markdown code block formatting from JSON response func cleanMarkdownCodeBlocks(content string) string { // Remove ```json prefix and ``` suffix if present content = strings.TrimSpace(content) // Remove opening code block markers if strings.HasPrefix(content, "```json") { content = strings.TrimPrefix(content, "```json") } else if strings.HasPrefix(content, "```") { content = strings.TrimPrefix(content, "```") } // Remove closing code block markers content = strings.TrimSuffix(content, "```") return strings.TrimSpace(content) }