Initial commit
This commit is contained in:
123
internal/classifier/parse.go
Normal file
123
internal/classifier/parse.go
Normal 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]) + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user