78 lines
3.1 KiB
Go
78 lines
3.1 KiB
Go
package classifier
|
|
|
|
import "regexp"
|
|
|
|
// GatingResult indicates the outcome of keyword-based gating for Tier 2 stories.
|
|
type GatingResult int
|
|
|
|
const (
|
|
GatingPost GatingResult = iota // Stage 1: definite post
|
|
GatingDiscard // Stage 2: definite discard
|
|
GatingAmbiguous // Stage 3: escalate to LLM
|
|
)
|
|
|
|
// Stage 1: Definite post patterns — stories with clear public significance.
|
|
// Patterns use multi-word context to reduce false positives (e.g. "Star Wars" won't match).
|
|
var postPatterns = []*regexp.Regexp{
|
|
// Named heads of state / major international bodies
|
|
regexp.MustCompile(`(?i)\b(president|prime minister|chancellor|pope)\s+\w+`),
|
|
regexp.MustCompile(`(?i)\b(UN|United Nations|WHO|NATO|EU|European Union|IMF|World Bank)\b`),
|
|
|
|
// Casualty language with numbers
|
|
regexp.MustCompile(`(?i)\b\d+\s*(dead|killed|injured|casualties|wounded)\b`),
|
|
regexp.MustCompile(`(?i)\b(dead|killed|injured|casualties|wounded)\s*\d+\b`),
|
|
|
|
// Disaster language — require a location-like context word nearby
|
|
regexp.MustCompile(`(?i)\b(earthquake|hurricane|typhoon|tsunami|wildfire|tornado)\b.*\b(hit|struck|destroyed|devastated|kills|deaths)\b`),
|
|
|
|
// Conflict language — require multi-word phrases to avoid "Star Wars", "culture war"
|
|
regexp.MustCompile(`(?i)\b(military invasion|armed coup|airstrike\s+\w+|assassination\s+\w+|car bombing|suicide bombing)\b`),
|
|
regexp.MustCompile(`(?i)\bwar\s+(in|on|with|against|between)\b`),
|
|
|
|
// Crisis language — require context word nearby
|
|
regexp.MustCompile(`(?i)\b(state of emergency|economic crisis|economic collapse|disease outbreak|pandemic declared|pandemic phase|famine)\b`),
|
|
|
|
// Major financial scale
|
|
regexp.MustCompile(`(?i)\$\d+\s*(bn|tn|billion|trillion)\b`),
|
|
regexp.MustCompile(`(?i)\b(trillion|billion)\s*(dollar|euro|pound)\b`),
|
|
}
|
|
|
|
// Stage 2: Definite discard patterns — stories unlikely to warrant posting.
|
|
var discardPatterns = []*regexp.Regexp{
|
|
// Local/municipal scope
|
|
regexp.MustCompile(`(?i)\b(city council|municipal|town hall|zoning|local police)\b`),
|
|
|
|
// Sports scores and routine results
|
|
regexp.MustCompile(`(?i)\b(final score|match result|league standings|defeated|innings)\b`),
|
|
regexp.MustCompile(`(?i)\b\d+-\d+\s*(win|loss|defeat|victory)\b`),
|
|
|
|
// Entertainment/celebrity lifestyle
|
|
regexp.MustCompile(`(?i)\b(red carpet|award show|celebrity gossip|dating rumor|relationship drama|album release)\b`),
|
|
|
|
// Routine corporate earnings
|
|
regexp.MustCompile(`(?i)\b(quarterly earnings|Q[1-4] results|earnings call|fiscal quarter)\b`),
|
|
}
|
|
|
|
// RunGating evaluates Tier 2 gating stages on headline + lede.
|
|
// Returns the gating result and a reason string for logging.
|
|
func RunGating(headline, lede string) (GatingResult, string) {
|
|
combined := headline + " " + lede
|
|
|
|
// Stage 1: Definite post
|
|
for _, re := range postPatterns {
|
|
if re.MatchString(combined) {
|
|
return GatingPost, "stage1: " + re.String()
|
|
}
|
|
}
|
|
|
|
// Stage 2: Definite discard
|
|
for _, re := range discardPatterns {
|
|
if re.MatchString(combined) {
|
|
return GatingDiscard, "stage2: " + re.String()
|
|
}
|
|
}
|
|
|
|
// Stage 3: Ambiguous — escalate to LLM
|
|
return GatingAmbiguous, "stage3: no keyword match"
|
|
}
|