- Reorganized project structure into services/ and apps/ directories for better separation of concerns - Added comprehensive CI/CD pipeline with GitHub Actions for testing and Docker builds - Created .dockerignore file to optimize container builds - Updated Makefile with new targets for each service and application - Added detailed README with architecture overview, setup instructions and development guidelines - Moved cron-analyzer to dedicate
153 lines
5.1 KiB
Go
153 lines
5.1 KiB
Go
package openrouter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/eduardolat/openroutergo"
|
|
"github.com/jobs-scraper/internal/pkg/domain"
|
|
)
|
|
|
|
// JobAnalysisResult represents the structured response from job analysis
|
|
type JobAnalysisResult struct {
|
|
Recommendation string `json:"recommendation"`
|
|
ConfidenceScore int `json:"confidence_score"`
|
|
MatchingSkills []string `json:"matching_skills"`
|
|
MissingSkills []string `json:"missing_skills"`
|
|
ExperienceMatch string `json:"experience_match"`
|
|
Summary string `json:"summary"`
|
|
ImprovementSuggestions []string `json:"improvement_suggestions"`
|
|
}
|
|
|
|
// ShouldApply returns true if the recommendation is to apply for the job
|
|
func (r *JobAnalysisResult) ShouldApply() bool {
|
|
return r.Recommendation == "apply"
|
|
}
|
|
|
|
// IsHighConfidence returns true if the confidence score is 70 or above
|
|
func (r *JobAnalysisResult) IsHighConfidence() bool {
|
|
return r.ConfidenceScore >= 70
|
|
}
|
|
|
|
type OpenRouterService struct {
|
|
model string
|
|
apiKey string
|
|
}
|
|
|
|
func NewOpenRouterService(model string, apiKey string) OpenRouterService {
|
|
return OpenRouterService{
|
|
model: model,
|
|
apiKey: apiKey,
|
|
}
|
|
}
|
|
|
|
func (s *OpenRouterService) AnalyzeJobDescription(cv string, jobDesc domain.JobDescription) (*JobAnalysisResult, error) {
|
|
client, err := openroutergo.
|
|
NewClient().
|
|
WithAPIKey(s.apiKey).
|
|
Create()
|
|
if err != nil {
|
|
log.Fatalf("Failed to create client: %v", err)
|
|
}
|
|
|
|
// Build the user message with all the provided data
|
|
userMessage := fmt.Sprintf(`Analyze the following CV against the job description and criteria, then provide a recommendation following the schema below.
|
|
1) If there are missing skills, try to guess if they still match based on similar skills or experience in the cv.
|
|
for example: Javascript is mentioned in the cv, but the job requires Vanilla js, since they are the same thing, it should be included in the matching skills.
|
|
|
|
2) The job shouldn't require any language skills, preferbly only english.
|
|
|
|
3) The job should be remote, or provide relocation to the country.
|
|
CV:
|
|
%s
|
|
|
|
Job Description:
|
|
%s
|
|
|
|
Job Criteria (key-value):
|
|
%v
|
|
|
|
CRITICAL OUTPUT REQUIREMENTS:
|
|
- Return ONLY raw JSON - NO markdown formatting whatsoever
|
|
- NO backticks, NO code blocks, NO json prefix
|
|
- NO additional text before or after the JSON
|
|
- Start your response directly with { and end with }
|
|
- Use this exact schema and key names:
|
|
{
|
|
"recommendation": "apply" | "do_not_apply",
|
|
"confidence_score": number, // integer 0-100
|
|
"matching_skills": [string],
|
|
"missing_skills": [string],
|
|
"experience_match": "excellent" | "good" | "fair" | "poor",
|
|
"summary": string,
|
|
"improvement_suggestions": [string]
|
|
}`, cv, jobDesc.Description, jobDesc.Criteria)
|
|
|
|
// Build and execute your request with a fluent API
|
|
_, resp, err := client.
|
|
NewChatCompletion().
|
|
WithModel(s.model).
|
|
WithSystemMessage("You are an expert HR assistant specializing in job application analysis. You help candidates determine if they should apply for specific positions based on their CV and the job requirements. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }.").
|
|
WithUserMessage(userMessage).
|
|
Execute()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute completion: %v", err)
|
|
}
|
|
|
|
if len(resp.Choices) == 0 {
|
|
return nil, fmt.Errorf("no response choices received from API")
|
|
}
|
|
|
|
// Extract JSON from the response (handle markdown code blocks)
|
|
jsonContent := resp.Choices[0].Message.Content
|
|
|
|
// Log the raw response for debugging
|
|
log.Printf("Raw API response: %s", jsonContent)
|
|
|
|
// Check if response looks like HTML/XML (common error response format)
|
|
if len(jsonContent) > 0 && jsonContent[0] == '<' {
|
|
return nil, fmt.Errorf("API returned HTML/XML instead of JSON. This usually indicates an API error, authentication issue, or rate limiting. Response: %s", jsonContent[:min(200, len(jsonContent))])
|
|
}
|
|
|
|
// Clean up markdown code blocks if present
|
|
jsonContent = cleanMarkdownCodeBlocks(jsonContent)
|
|
|
|
// Parse the JSON response into our struct
|
|
var result JobAnalysisResult
|
|
if err := json.Unmarshal([]byte(jsonContent), &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse JSON response: %v. Raw response: %s", err, jsonContent[:min(200, len(jsonContent))])
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// min returns the smaller of two integers
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// func (s *OpenRouterService) CreateCV(cv string, jobDesc domain.JobDescription) (*JobAnalysisResult, error) {
|
|
// }
|