114 lines
3.3 KiB
Go
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)
|
|
}
|