jobs-monorepo/internal/pkg/gemini/gemini.go
2025-12-16 17:19:44 +05:00

114 lines
3.3 KiB
Go

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)
}