feat: implement YAML-based configuration, .env support, and customizable prompt templates for AI analysis

This commit is contained in:
2026-08-22 10:17:38 -07:00
parent 7ec91f384f
commit 0de312edbd
30 changed files with 895 additions and 211 deletions
+59 -35
View File
@@ -10,11 +10,11 @@ import (
)
// Target Queries
func (db *DB) CreateTarget(name, targetURL, platform string) (*SearchTarget, error) {
query := `INSERT INTO search_targets (name, target_url, platform, is_active) VALUES (?, ?, ?, 1) RETURNING id, name, target_url, platform, is_active, last_crawled_at`
func (db *DB) CreateTarget(name, targetURL, platform string, isActive bool) (*SearchTarget, error) {
query := `INSERT INTO search_targets (name, target_url, platform, is_active) VALUES (?, ?, ?, ?) RETURNING id, name, target_url, platform, is_active, last_crawled_at`
var target SearchTarget
var lastCrawled sql.NullTime
err := db.QueryRow(query, name, targetURL, platform).Scan(
err := db.QueryRow(query, name, targetURL, platform, isActive).Scan(
&target.ID, &target.Name, &target.TargetURL, &target.Platform, &target.IsActive, &lastCrawled,
)
if err != nil {
@@ -78,6 +78,12 @@ func (db *DB) ToggleTargetStatus(id int64) error {
return err
}
func (db *DB) SetTargetActive(id int64, isActive bool) error {
query := `UPDATE search_targets SET is_active = ? WHERE id = ?`
_, err := db.Exec(query, isActive, id)
return err
}
func (db *DB) UpdateTargetCrawledAt(id int64) error {
query := `UPDATE search_targets SET last_crawled_at = CURRENT_TIMESTAMP WHERE id = ?`
_, err := db.Exec(query, id)
@@ -408,49 +414,59 @@ func tokenizeToVector(text string) map[string]int {
}
// Job Analyses Queries
func (db *DB) CreateJobAnalysis(rawJobID int64, score float64, isQualified bool, status string) error {
func (db *DB) CreateJobAnalysis(rawJobID int64, filterScore, score float64, isQualified bool, status string) error {
query := `
INSERT INTO job_analyses (raw_job_id, score, is_qualified, status)
VALUES (?, ?, ?, ?)
ON CONFLICT(raw_job_id) DO UPDATE SET score=excluded.score, is_qualified=excluded.is_qualified, status=excluded.status
INSERT INTO job_analyses (raw_job_id, filter_score, score, is_qualified, status)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(raw_job_id) DO UPDATE SET filter_score=excluded.filter_score, score=excluded.score, is_qualified=excluded.is_qualified, status=excluded.status
`
_, err := db.Exec(query, rawJobID, score, isQualified, status)
_, err := db.Exec(query, rawJobID, filterScore, score, isQualified, status)
return err
}
func (db *DB) CreateJobAnalysisFull(rawJobID int64, score float64, isQualified bool, matched, missing, tailoring, status string) error {
func (db *DB) CreateJobAnalysisFull(rawJobID int64, filterScore, score float64, isQualified bool, summary, matched, missing, tailoring, status string) error {
query := `
INSERT INTO job_analyses (raw_job_id, score, is_qualified, matched_highlights, missing_highlights, tailoring_suggestions, status, analyzed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
INSERT INTO job_analyses (raw_job_id, filter_score, score, is_qualified, summary, matched_highlights, missing_highlights, tailoring_suggestions, status, analyzed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(raw_job_id) DO UPDATE SET
filter_score=excluded.filter_score,
score=excluded.score,
is_qualified=excluded.is_qualified,
summary=excluded.summary,
matched_highlights=excluded.matched_highlights,
missing_highlights=excluded.missing_highlights,
tailoring_suggestions=excluded.tailoring_suggestions,
status=excluded.status,
analyzed_at=CURRENT_TIMESTAMP
`
_, err := db.Exec(query, rawJobID, score, isQualified, matched, missing, tailoring, status)
_, err := db.Exec(query, rawJobID, filterScore, score, isQualified, summary, matched, missing, tailoring, status)
return err
}
func (db *DB) UpdateJobAnalysisFull(rawJobID int64, score float64, isQualified bool, matchedJSON, missingJSON, suggestionsJSON, status string) error {
func (db *DB) UpdateJobAnalysisFull(rawJobID int64, filterScore, score float64, isQualified bool, summary, matchedJSON, missingJSON, suggestionsJSON, status string) error {
query := `
UPDATE job_analyses
SET score = ?, is_qualified = ?, matched_highlights = ?, missing_highlights = ?, tailoring_suggestions = ?, status = ?, analyzed_at = CURRENT_TIMESTAMP
SET filter_score = ?, score = ?, is_qualified = ?, summary = ?, matched_highlights = ?, missing_highlights = ?, tailoring_suggestions = ?, status = ?, analyzed_at = CURRENT_TIMESTAMP
WHERE raw_job_id = ?
`
_, err := db.Exec(query, score, isQualified, matchedJSON, missingJSON, suggestionsJSON, status, rawJobID)
_, err := db.Exec(query, filterScore, score, isQualified, summary, matchedJSON, missingJSON, suggestionsJSON, status, rawJobID)
return err
}
func (db *DB) UpdateJobAnalysisLLM(rawJobID int64, matchedJSON, missingJSON, suggestionsJSON string) error {
func (db *DB) UpdateJobAnalysisLLM(rawJobID int64, aiScore float64, isQualified bool, summary, matchedJSON, missingJSON, suggestionsJSON string) error {
query := `
UPDATE job_analyses
SET matched_highlights = ?, missing_highlights = ?, tailoring_suggestions = ?, status = 'PARSED', analyzed_at = CURRENT_TIMESTAMP
SET score = ?, is_qualified = ?, summary = ?, matched_highlights = ?, missing_highlights = ?, tailoring_suggestions = ?, status = 'PARSED', analyzed_at = CURRENT_TIMESTAMP
WHERE raw_job_id = ?
`
_, err := db.Exec(query, matchedJSON, missingJSON, suggestionsJSON, rawJobID)
_, err := db.Exec(query, aiScore, isQualified, summary, matchedJSON, missingJSON, suggestionsJSON, rawJobID)
return err
}
func (db *DB) GetPendingQualifiedAnalyses() ([]JobWithAnalysis, error) {
query := `
SELECT r.id, r.canonical_id, r.target_id, r.title, r.company, r.location, r.apply_url, r.raw_content, r.crawled_at, r.last_seen_at,
a.id, a.score, a.is_qualified, COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), a.status, a.analyzed_at
a.id, COALESCE(a.filter_score, 0), COALESCE(a.score, 0), a.is_qualified, COALESCE(a.summary, ''), COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), a.status, a.analyzed_at
FROM raw_jobs r
JOIN job_analyses a ON r.id = a.raw_job_id
WHERE a.is_qualified = 1 AND a.status = 'PENDING'
@@ -466,14 +482,14 @@ func (db *DB) GetPendingQualifiedAnalyses() ([]JobWithAnalysis, error) {
var item JobWithAnalysis
var tID sql.NullInt64
var aID int64
var score float64
var filterScore, score float64
var isQualified bool
var matched, missing, suggestions, aStatus string
var summary, matched, missing, suggestions, aStatus string
var analyzedAt sql.NullTime
err := rows.Scan(
&item.ID, &item.CanonicalID, &tID, &item.Title, &item.Company, &item.Location, &item.ApplyURL, &item.RawContent, &item.CrawledAt, &item.LastSeenAt,
&aID, &score, &isQualified, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&aID, &filterScore, &score, &isQualified, &summary, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
)
if err != nil {
return nil, err
@@ -484,8 +500,10 @@ func (db *DB) GetPendingQualifiedAnalyses() ([]JobWithAnalysis, error) {
analysis := &JobAnalysis{
ID: aID,
RawJobID: item.ID,
FilterScore: filterScore,
Score: score,
IsQualified: isQualified,
Summary: summary,
MatchedHighlights: matched,
MissingHighlights: missing,
TailoringSuggestions: suggestions,
@@ -503,12 +521,12 @@ func (db *DB) GetPendingQualifiedAnalyses() ([]JobWithAnalysis, error) {
func (db *DB) GetJobsWithAnalysis() ([]JobWithAnalysis, error) {
query := `
SELECT r.id, r.canonical_id, r.target_id, r.title, r.company, r.location, r.apply_url, r.raw_content, r.crawled_at, r.last_seen_at,
a.id, a.score, a.is_qualified, COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
a.id, COALESCE(a.filter_score, 0), COALESCE(a.score, 0), a.is_qualified, COALESCE(a.summary, ''), COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
app.id, app.status, COALESCE(app.notes, ''), app.applied_at, app.updated_at
FROM raw_jobs r
LEFT JOIN job_analyses a ON r.id = a.raw_job_id
LEFT JOIN job_applications app ON r.id = app.raw_job_id
ORDER BY COALESCE(a.score, 0) DESC, r.crawled_at DESC
ORDER BY COALESCE(a.score, a.filter_score, 0) DESC, r.crawled_at DESC
`
rows, err := db.Query(query)
if err != nil {
@@ -521,9 +539,9 @@ func (db *DB) GetJobsWithAnalysis() ([]JobWithAnalysis, error) {
var item JobWithAnalysis
var tID sql.NullInt64
var aID, appID sql.NullInt64
var score sql.NullFloat64
var filterScore, score sql.NullFloat64
var isQualified sql.NullBool
var matched, missing, suggestions, aStatus sql.NullString
var summary, matched, missing, suggestions, aStatus sql.NullString
var analyzedAt sql.NullTime
var appStatus, notes sql.NullString
var appliedAt sql.NullTime
@@ -531,7 +549,7 @@ func (db *DB) GetJobsWithAnalysis() ([]JobWithAnalysis, error) {
err := rows.Scan(
&item.ID, &item.CanonicalID, &tID, &item.Title, &item.Company, &item.Location, &item.ApplyURL, &item.RawContent, &item.CrawledAt, &item.LastSeenAt,
&aID, &score, &isQualified, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&aID, &filterScore, &score, &isQualified, &summary, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&appID, &appStatus, &notes, &appliedAt, &updatedAt,
)
if err != nil {
@@ -544,8 +562,10 @@ func (db *DB) GetJobsWithAnalysis() ([]JobWithAnalysis, error) {
analysis := &JobAnalysis{
ID: aID.Int64,
RawJobID: item.ID,
FilterScore: filterScore.Float64,
Score: score.Float64,
IsQualified: isQualified.Bool,
Summary: summary.String,
MatchedHighlights: matched.String,
MissingHighlights: missing.String,
TailoringSuggestions: suggestions.String,
@@ -577,7 +597,7 @@ func (db *DB) GetJobsWithAnalysis() ([]JobWithAnalysis, error) {
func (db *DB) GetJobByID(rawJobID int64) (*JobWithAnalysis, error) {
query := `
SELECT r.id, r.canonical_id, r.target_id, r.title, r.company, r.location, r.apply_url, r.raw_content, r.crawled_at, r.last_seen_at,
a.id, a.score, a.is_qualified, COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
a.id, COALESCE(a.filter_score, 0), COALESCE(a.score, 0), a.is_qualified, COALESCE(a.summary, ''), COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
app.id, app.status, COALESCE(app.notes, ''), app.applied_at, app.updated_at
FROM raw_jobs r
LEFT JOIN job_analyses a ON r.id = a.raw_job_id
@@ -588,9 +608,9 @@ func (db *DB) GetJobByID(rawJobID int64) (*JobWithAnalysis, error) {
var item JobWithAnalysis
var tID sql.NullInt64
var aID, appID sql.NullInt64
var score sql.NullFloat64
var filterScore, score sql.NullFloat64
var isQualified sql.NullBool
var matched, missing, suggestions, aStatus sql.NullString
var summary, matched, missing, suggestions, aStatus sql.NullString
var analyzedAt sql.NullTime
var appStatus, notes sql.NullString
var appliedAt sql.NullTime
@@ -598,7 +618,7 @@ func (db *DB) GetJobByID(rawJobID int64) (*JobWithAnalysis, error) {
err := row.Scan(
&item.ID, &item.CanonicalID, &tID, &item.Title, &item.Company, &item.Location, &item.ApplyURL, &item.RawContent, &item.CrawledAt, &item.LastSeenAt,
&aID, &score, &isQualified, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&aID, &filterScore, &score, &isQualified, &summary, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&appID, &appStatus, &notes, &appliedAt, &updatedAt,
)
if err != nil {
@@ -611,8 +631,10 @@ func (db *DB) GetJobByID(rawJobID int64) (*JobWithAnalysis, error) {
analysis := &JobAnalysis{
ID: aID.Int64,
RawJobID: item.ID,
FilterScore: filterScore.Float64,
Score: score.Float64,
IsQualified: isQualified.Bool,
Summary: summary.String,
MatchedHighlights: matched.String,
MissingHighlights: missing.String,
TailoringSuggestions: suggestions.String,
@@ -674,7 +696,7 @@ func (db *DB) CreateOrUpdateApplication(rawJobID int64, status, notes string) (*
func (db *DB) GetApplications() ([]JobWithAnalysis, error) {
query := `
SELECT r.id, r.canonical_id, r.target_id, r.title, r.company, r.location, r.apply_url, r.raw_content, r.crawled_at, r.last_seen_at,
a.id, a.score, a.is_qualified, COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
a.id, COALESCE(a.filter_score, 0), COALESCE(a.score, 0), a.is_qualified, COALESCE(a.summary, ''), COALESCE(a.matched_highlights, '[]'), COALESCE(a.missing_highlights, '[]'), COALESCE(a.tailoring_suggestions, '[]'), COALESCE(a.status, 'PENDING'), a.analyzed_at,
app.id, app.status, COALESCE(app.notes, ''), app.applied_at, app.updated_at
FROM job_applications app
JOIN raw_jobs r ON app.raw_job_id = r.id
@@ -692,9 +714,9 @@ func (db *DB) GetApplications() ([]JobWithAnalysis, error) {
var item JobWithAnalysis
var tID sql.NullInt64
var aID, appID sql.NullInt64
var score sql.NullFloat64
var filterScore, score sql.NullFloat64
var isQualified sql.NullBool
var matched, missing, suggestions, aStatus sql.NullString
var summary, matched, missing, suggestions, aStatus sql.NullString
var analyzedAt sql.NullTime
var appStatus, notes sql.NullString
var appliedAt sql.NullTime
@@ -702,7 +724,7 @@ func (db *DB) GetApplications() ([]JobWithAnalysis, error) {
err := rows.Scan(
&item.ID, &item.CanonicalID, &tID, &item.Title, &item.Company, &item.Location, &item.ApplyURL, &item.RawContent, &item.CrawledAt, &item.LastSeenAt,
&aID, &score, &isQualified, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&aID, &filterScore, &score, &isQualified, &summary, &matched, &missing, &suggestions, &aStatus, &analyzedAt,
&appID, &appStatus, &notes, &appliedAt, &updatedAt,
)
if err != nil {
@@ -715,8 +737,10 @@ func (db *DB) GetApplications() ([]JobWithAnalysis, error) {
analysis := &JobAnalysis{
ID: aID.Int64,
RawJobID: item.ID,
FilterScore: filterScore.Float64,
Score: score.Float64,
IsQualified: isQualified.Bool,
Summary: summary.String,
MatchedHighlights: matched.String,
MissingHighlights: missing.String,
TailoringSuggestions: suggestions.String,