Initial commit

This commit is contained in:
prosolis
2026-05-22 17:25:27 -07:00
commit 652d6dfa38
40 changed files with 5855 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
package classifier
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
// Tier1Result is the LLM response for Tier 1 (routing only) stories.
type Tier1Result struct {
Channel string `json:"channel"`
Platforms []string `json:"platforms"`
DuplicateOf *string `json:"duplicate_of"`
Rationale string `json:"rationale"`
}
// Tier2Result is the LLM response for Tier 2 (gating + routing) stories.
type Tier2Result struct {
Post bool `json:"post"`
Channel string `json:"channel"`
Platforms []string `json:"platforms"`
DuplicateOf *string `json:"duplicate_of"`
Rationale string `json:"rationale"`
}
var validChannels = map[string]bool{
"tech": true,
"politics": true,
"gaming": true,
}
var validPlatforms = map[string]bool{
"nintendo-switch": true,
"playstation": true,
"xbox": true,
"pc": true,
"retro": true,
"mobile": true,
"multi": true,
}
var thinkRe = regexp.MustCompile(`(?s)<think>.*?</think>`)
var fenceRe = regexp.MustCompile("(?s)```(?:json)?\\s*(.+?)\\s*```")
var trailingCommaRe = regexp.MustCompile(`,\s*([}\]])`)
// ParseTier1Response parses an LLM response into a Tier1Result.
func ParseTier1Response(raw string) (*Tier1Result, error) {
cleaned := repairJSON(raw)
var result Tier1Result
if err := json.Unmarshal([]byte(cleaned), &result); err != nil {
return nil, fmt.Errorf("parse tier1 response: %w (raw: %s)", err, truncate(raw, 200))
}
if !validChannels[result.Channel] {
return nil, fmt.Errorf("invalid channel: %q", result.Channel)
}
result.Platforms = filterPlatforms(result.Platforms)
return &result, nil
}
// ParseTier2Response parses an LLM response into a Tier2Result.
func ParseTier2Response(raw string) (*Tier2Result, error) {
cleaned := repairJSON(raw)
var result Tier2Result
if err := json.Unmarshal([]byte(cleaned), &result); err != nil {
return nil, fmt.Errorf("parse tier2 response: %w (raw: %s)", err, truncate(raw, 200))
}
// Always validate channel — gating may override post=false to true later
if !validChannels[result.Channel] {
if result.Post {
return nil, fmt.Errorf("invalid channel: %q", result.Channel)
}
// Default to "politics" for discarded stories with garbage channels
result.Channel = "politics"
}
result.Platforms = filterPlatforms(result.Platforms)
return &result, nil
}
// repairJSON attempts to fix common LLM output issues.
func repairJSON(raw string) string {
s := strings.TrimSpace(raw)
// Strip <think>...</think> blocks (some models include reasoning)
s = thinkRe.ReplaceAllString(s, "")
// Strip markdown code fences
if matches := fenceRe.FindStringSubmatch(s); len(matches) > 1 {
s = matches[1]
}
s = strings.TrimSpace(s)
// Remove trailing commas before closing braces/brackets (outside quoted strings)
s = trailingCommaRe.ReplaceAllString(s, "$1")
return s
}
func filterPlatforms(platforms []string) []string {
var valid []string
for _, p := range platforms {
p = strings.TrimSpace(strings.ToLower(p))
if validPlatforms[p] {
valid = append(valid, p)
}
}
return valid
}
func truncate(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {
return s
}
return string(runes[:n]) + "..."
}