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
+33
View File
@@ -30,6 +30,39 @@ func TestHardKeywordFilter(t *testing.T) {
}
}
func TestScoringFilterWordBoundaries(t *testing.T) {
ctx := context.Background()
sf := NewScoringFilter([]KeywordWeight{
{Keyword: "go", Weight: 20.0},
{Keyword: "ai", Weight: 15.0},
{Keyword: "c++", Weight: 10.0},
})
// "algorithm" and "email" should NOT trigger "go" or "ai"
res1, err := sf.Process(ctx, &db.RawJob{
Title: "Algorithm Developer",
RawContent: "Send email to apply for algorithm development.",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res1.Score != 0.0 {
t.Errorf("expected score 0 for false-positive substrings, got %f", res1.Score)
}
// Real "go", "ai", and "c++"
res2, err := sf.Process(ctx, &db.RawJob{
Title: "AI Runtime Engineer",
RawContent: "Must know Go and C++ programming.",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res2.Score != 45.0 {
t.Errorf("expected score 45.0 (20+15+10), got %f", res2.Score)
}
}
func TestFilterPipeline(t *testing.T) {
ctx := context.Background()
hardFilter := NewHardKeywordFilter([]string{"Unpaid", "No Sponsorship"})
+29 -12
View File
@@ -3,25 +3,43 @@ package filter
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/hireme/hireme/internal/db"
)
type compiledBlacklist struct {
keyword string
regex *regexp.Regexp
}
type HardKeywordFilter struct {
Blacklist []string
compiled []compiledBlacklist
}
func NewHardKeywordFilter(blacklist []string) *HardKeywordFilter {
if len(blacklist) == 0 {
blacklist = []string{
"no sponsorship",
"us citizen only",
"unpaid",
"volunteer",
compiled := make([]compiledBlacklist, 0, len(blacklist))
for _, kw := range blacklist {
term := strings.TrimSpace(kw)
if term != "" {
quoted := regexp.QuoteMeta(term)
quoted = regexp.MustCompile(`\\\s+`).ReplaceAllString(quoted, `\s+`)
pattern := `(?i)(?:^|[^a-zA-Z0-9_])` + quoted + `(?:[^a-zA-Z0-9_]|$)`
if re, err := regexp.Compile(pattern); err == nil {
compiled = append(compiled, compiledBlacklist{
keyword: kw,
regex: re,
})
}
}
}
return &HardKeywordFilter{Blacklist: blacklist}
return &HardKeywordFilter{
Blacklist: blacklist,
compiled: compiled,
}
}
func (f *HardKeywordFilter) Name() string {
@@ -29,15 +47,14 @@ func (f *HardKeywordFilter) Name() string {
}
func (f *HardKeywordFilter) Process(ctx context.Context, job *db.RawJob) (FilterResult, error) {
combined := strings.ToLower(job.Title + " " + job.RawContent)
combined := job.Title + " " + job.RawContent
for _, kw := range f.Blacklist {
term := strings.ToLower(strings.TrimSpace(kw))
if term != "" && strings.Contains(combined, term) {
for _, cb := range f.compiled {
if cb.regex.MatchString(combined) {
return FilterResult{
Keep: false,
Score: 0,
Reason: fmt.Sprintf("Contains blacklisted keyword: %q", kw),
Reason: fmt.Sprintf("Contains blacklisted keyword: %q", cb.keyword),
}, nil
}
}
+54 -23
View File
@@ -3,37 +3,62 @@ package filter
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/hireme/hireme/internal/db"
)
type KeywordWeight struct {
Keyword string
Weight float64
Keyword string `yaml:"keyword"`
Weight float64 `yaml:"weight"`
}
type compiledWeight struct {
keyword string
weight float64
regex *regexp.Regexp
}
type ScoringFilter struct {
Weights []KeywordWeight
Weights []KeywordWeight
compiled []compiledWeight
}
func compilePattern(term string) *regexp.Regexp {
clean := strings.TrimSpace(term)
if clean == "" {
return nil
}
// Quote meta characters, replace multiple whitespace with \s+
quoted := regexp.QuoteMeta(clean)
quoted = regexp.MustCompile(`\\\s+`).ReplaceAllString(quoted, `\s+`)
// Boundary regex that works for both alphanumeric and punctuation (e.g. c++)
pattern := `(?i)(?:^|[^a-zA-Z0-9_])` + quoted + `(?:[^a-zA-Z0-9_]|$)`
re, err := regexp.Compile(pattern)
if err != nil {
return nil
}
return re
}
func NewScoringFilter(weights []KeywordWeight) *ScoringFilter {
if len(weights) == 0 {
weights = []KeywordWeight{
{Keyword: "go", Weight: 25.0},
{Keyword: "golang", Weight: 25.0},
{Keyword: "backend", Weight: 15.0},
{Keyword: "distributed systems", Weight: 20.0},
{Keyword: "microservices", Weight: 15.0},
{Keyword: "sqlite", Weight: 10.0},
{Keyword: "postgres", Weight: 10.0},
{Keyword: "api", Weight: 10.0},
{Keyword: "full stack", Weight: 10.0},
{Keyword: "rest", Weight: 5.0},
{Keyword: "grpc", Weight: 15.0},
compiled := make([]compiledWeight, 0, len(weights))
for _, kw := range weights {
re := compilePattern(kw.Keyword)
if re != nil {
compiled = append(compiled, compiledWeight{
keyword: kw.Keyword,
weight: kw.Weight,
regex: re,
})
}
}
return &ScoringFilter{Weights: weights}
return &ScoringFilter{
Weights: weights,
compiled: compiled,
}
}
func (s *ScoringFilter) Name() string {
@@ -41,18 +66,24 @@ func (s *ScoringFilter) Name() string {
}
func (f *ScoringFilter) Process(ctx context.Context, job *db.RawJob) (FilterResult, error) {
combinedLower := strings.ToLower(job.Title + " " + job.RawContent)
combined := job.Title + " " + job.RawContent
var totalScore float64
var matched []string
for _, kw := range f.Weights {
term := strings.ToLower(strings.TrimSpace(kw.Keyword))
if term != "" && strings.Contains(combinedLower, term) {
totalScore += kw.Weight
matched = append(matched, kw.Keyword)
for _, cw := range f.compiled {
if cw.regex.MatchString(combined) {
totalScore += cw.weight
matched = append(matched, cw.keyword)
}
}
if totalScore < 0 {
totalScore = 0
}
if totalScore > 100 {
totalScore = 100
}
return FilterResult{
Keep: true,
Score: totalScore,