jobs-monorepo/apps/cron-analyzer/internal/analyze-jobs.go
Elshimy Ziad Magdy Taha 2ab810be72
All checks were successful
Deploy scraper-google / build (push) Successful in 21m58s
Deploy scraper-google / deploy (push) Has been skipped
Move shared Go packages from libs/ to internal/
internal/ and libs/ were both shared-code roots with no rule for which
one a package belonged in, so the split carried no information. Adopt a
rule the language answers by itself:

  shared Go          -> internal/
  shared TypeScript  -> libs/   (npm workspace packages)

libs/{ports,repo,server} move to internal/, leaving libs/ holding only
the two TypeScript packages (@jobs-scraper/rabbitmq-ts and
@jobs-scraper/browser-automation), which matches npm workspace
convention. internal/ is also Go's marker for code not importable from
outside the repo, which is accurate here since none of it is published.

Import paths are rewritten mechanically (jobs-scraper/libs/ ->
jobs-scraper/internal/) across 15 lines in 10 files. The 6 moved files
are pure renames with no content change. Doing this after the module
collapse in the previous commit meant no go.mod or replace-directive
edits were needed.

gofmt is applied to services/api/pkg/http/job.go, whose import group the
rewrite left out of order. Three files were already unformatted before
this refactor (internal/openai/interface.go, internal/ports/job-queries.go,
internal/repo/job-analysis-result.go) and are deliberately left alone to
keep this diff limited to the move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:30:04 +05:00

157 lines
4.4 KiB
Go

package internal
import (
"database/sql"
"encoding/json"
"log"
"os"
// "path/filepath"
"github.com/jobs-scraper/internal/domain"
"github.com/jobs-scraper/internal/infrastructure/rabbitmq"
"github.com/jobs-scraper/internal/openai"
"github.com/jobs-scraper/internal/openrouter"
"github.com/jobs-scraper/internal/repo"
)
type JobAnalyzer struct {
db *sql.DB
jobRepo *repo.JobRepository
jobDescriptionRepo *repo.JobDescriptionRepository
jobAnalysisResultRepo *repo.JobAnalysisResultRepository
openRouterService openrouter.OpenRouterService
rmq *rabbitmq.RabbitMQClient
openAiService *openai.OpenAIService
}
func NewJobAnalyzer(db *sql.DB) *JobAnalyzer {
apiKey := os.Getenv("OPENROUTER_API_KEY")
model := os.Getenv("CV_AI_MODEL")
openAiapiKey := os.Getenv("OPENAI_API_KEY")
if openAiapiKey == "" {
log.Fatal("OPENAI_API_KEY environment variable is required")
}
// Create OpenAI service
openAiModel := os.Getenv("OPENAI_MODEL")
if openAiModel == "" {
openAiModel = "gpt-4o-mini" // Default to cost-effective model
}
jobRepo := repo.NewJobRepository(db)
jobDescriptionRepo := repo.NewJobDescriptionRepository(db)
jobAnalysisResultRepo := repo.NewJobAnalysisResultRepository(db)
openRouterService := openrouter.NewOpenRouterService(model, apiKey)
openAiService := openai.NewOpenAIService(openAiapiKey, openAiModel)
rmq, err := rabbitmq.NewRabbitMQClient()
if err != nil {
log.Printf("Warning: Failed to create RabbitMQ client: %v", err)
}
return &JobAnalyzer{
db: db,
jobRepo: jobRepo,
jobDescriptionRepo: jobDescriptionRepo,
jobAnalysisResultRepo: jobAnalysisResultRepo,
openRouterService: openRouterService,
rmq: rmq,
openAiService: openAiService,
}
}
func (ja *JobAnalyzer) AnalyzeJobs() error {
// Get jobs that need analysis (status = created)
jobs, err := ja.jobRepo.GetJobsByStatus(domain.JobStatusCreated)
if err != nil {
return err
}
log.Printf("Found %d jobs to analyze", len(jobs))
cv, err := os.ReadFile("cv.txt")
if err != nil {
return err
}
for _, job := range jobs {
if err := ja.analyzeJob(job, string(cv)); err != nil {
log.Printf("Error analyzing job %d: %v", job.ID, err)
// Update job status to error
if updateErr := ja.jobRepo.UpdateJobStatus(job.ID, domain.JobStatusError); updateErr != nil {
log.Printf("Error updating job status to error: %v", updateErr)
}
continue
}
// Update job status to analyzed
if err := ja.jobRepo.UpdateJobStatus(job.ID, domain.JobStatusAnalyzed); err != nil {
log.Printf("Error updating job status to analyzed: %v", err)
}
log.Printf("Successfully analyzed job %d", job.ID)
}
return nil
}
func (ja *JobAnalyzer) analyzeJob(job domain.Job, cv string) error {
// Get job description
jobDescription, jobCriteria, err := ja.jobDescriptionRepo.GetJobDescriptionByJobID(job.ID)
if err != nil {
return err
}
// Analyze job with CV
result, err := ja.openAiService.AnalyzeJobDescription(cv, domain.JobDescription{
JobID: job.ID,
Description: jobDescription,
Criteria: jobCriteria,
})
if err != nil {
return err
}
log.Printf("Analysis result for job %d: Recommendation=%s, Score=%d", job.ID, result.Recommendation, result.ConfidenceScore)
// Save analysis result to database
analysisResult := &domain.JobAnalysisResult{
JobID: job.ID,
AnalysisResult: result.Summary,
MatchScore: &result.ConfidenceScore,
KeySkills: result.MatchingSkills,
MissingSkills: result.MissingSkills,
Recommendations: &result.Recommendation,
}
if err := ja.jobAnalysisResultRepo.SaveAnalysisResult(analysisResult); err != nil {
log.Printf("Warning: Failed to save analysis result to database: %v", err)
} else {
log.Printf("Successfully saved analysis result for job %d to database", job.ID)
}
// Publish CV analysis message to RabbitMQ if available
if ja.rmq != nil {
message := rabbitmq.CvAnalyzeMessage{
JobID: job.ID,
}
if err := ja.publishCvAnalysisMessage(message); err != nil {
log.Printf("Warning: Failed to publish CV analysis message: %v", err)
}
}
return nil
}
func (ja *JobAnalyzer) publishCvAnalysisMessage(message rabbitmq.CvAnalyzeMessage) error {
data, err := json.Marshal(message)
if err != nil {
return err
}
return ja.rmq.Publish(rabbitmq.CvAnalyzeQueue, rabbitmq.CvAnalyzeExchange, data)
}