diff --git a/PROGRESS.md b/PROGRESS.md index 78018cc..dd4fd9e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -455,7 +455,7 @@ - Frontend fetches from R2 (`https://r2.aicodebattle.com/evolution/live.json`) - Cache-Control: max-age=10 for near-real-time updates - Tests for R2 config validation and credential handling -- [ ] Narrative engine (weekly story arc detection + LLM chronicles) +- [x] Narrative engine (weekly story arc detection + LLM chronicles) - [x] Public match data documentation (OpenAPI-style) - New `/docs/api` route with comprehensive endpoint documentation - Documents Pages, R2, and B2 static JSON endpoints diff --git a/cmd/acb-index-builder/config.go b/cmd/acb-index-builder/config.go index 1715790..53997f8 100644 --- a/cmd/acb-index-builder/config.go +++ b/cmd/acb-index-builder/config.go @@ -75,6 +75,9 @@ func LoadConfig() *Config { B2BucketName: os.Getenv("ACB_B2_BUCKET"), OutputDir: getEnv("ACB_OUTPUT_DIR", "/tmp/acb-index"), + + LLMBaseURL: getEnv("ACB_LLM_BASE_URL", ""), + LLMAPIKey: os.Getenv("ACB_LLM_API_KEY"), } } diff --git a/cmd/acb-index-builder/main.go b/cmd/acb-index-builder/main.go index b3016ec..8dd2720 100644 --- a/cmd/acb-index-builder/main.go +++ b/cmd/acb-index-builder/main.go @@ -147,7 +147,11 @@ func runBuildCycle(ctx context.Context, db *sql.DB, cfg *Config) error { } // Generate blog posts (weekly meta reports and chronicles) - if err := generateBlog(data, cfg.OutputDir); err != nil { + var llmClient *LLMClient + if cfg.LLMBaseURL != "" { + llmClient = NewLLMClient(cfg.LLMBaseURL, cfg.LLMAPIKey) + } + if err := generateBlog(data, cfg.OutputDir, llmClient); err != nil { slog.Error("Failed to generate blog", "error", err) // Non-fatal - continue with rest of build } diff --git a/cmd/acb-index-builder/main_test.go b/cmd/acb-index-builder/main_test.go index b1914db..3bd2b8c 100644 --- a/cmd/acb-index-builder/main_test.go +++ b/cmd/acb-index-builder/main_test.go @@ -191,7 +191,7 @@ func TestGenerateMatchIndex(t *testing.T) { TurnCount: 200, EndCondition: "elimination", CompletedAt: now, - Participants: []MatchParticipant{ + Participants: []ParticipantData{ {BotID: "bot1", Score: 5, Won: true}, {BotID: "bot2", Score: 2, Won: false}, }, @@ -246,7 +246,7 @@ func TestGeneratePlaylists(t *testing.T) { TurnCount: 200, EndCondition: "elimination", CompletedAt: now, - Participants: []MatchParticipant{ + Participants: []ParticipantData{ {BotID: "bot1", Score: 3, Won: true}, {BotID: "bot2", Score: 2, Won: false}, // Close finish (diff = 1) }, @@ -257,7 +257,7 @@ func TestGeneratePlaylists(t *testing.T) { TurnCount: 150, EndCondition: "dominance", CompletedAt: now.Add(-time.Hour), - Participants: []MatchParticipant{ + Participants: []ParticipantData{ {BotID: "bot1", Score: 0, Won: false}, {BotID: "bot2", Score: 10, Won: true}, // Not close (diff = 10) }, diff --git a/cmd/acb-index-builder/narrative.go b/cmd/acb-index-builder/narrative.go index cfbaf35..0233323 100644 --- a/cmd/acb-index-builder/narrative.go +++ b/cmd/acb-index-builder/narrative.go @@ -41,6 +41,11 @@ type StoryArc struct { ParentIDs []string `json:"parent_ids,omitempty"` Generation int `json:"generation,omitempty"` CommunityHint string `json:"community_hint,omitempty"` + + // Rivalry-specific fields + BotAWins int `json:"bot_a_wins,omitempty"` + BotBWins int `json:"bot_b_wins,omitempty"` + TotalMatches int `json:"total_matches,omitempty"` } // KeyMatch represents a key match for narrative context @@ -430,17 +435,13 @@ func detectRivalryArcs(data *IndexData) []StoryArc { } botAID, botBID := parts[0], parts[1] - // Count wins for each bot and check alternation + // Count wins for each bot botAWins := 0 botBWins := 0 - alternating := true - lastWinner := "" for _, m := range matches { - var winnerID string for _, p := range m.Participants { if p.Won { - winnerID = p.BotID if p.BotID == botAID { botAWins++ } else if p.BotID == botBID { @@ -449,10 +450,6 @@ func detectRivalryArcs(data *IndexData) []StoryArc { break } } - if lastWinner != "" && winnerID == lastWinner { - alternating = false - } - lastWinner = winnerID } // Only include if wins are reasonably close (not one-sided) @@ -500,7 +497,7 @@ func detectUpsetArcs(data *IndexData) []StoryArc { } // Check if underdog won (winner had lower rating) - gap := loser.PreMatchRating - winner.PreMatchRating + gap := int(loser.PreMatchRating - winner.PreMatchRating) if gap > biggestGap { biggestGap = gap biggestUpset = &StoryArc{