Integrate LLM client with blog generation (Phase 10)

- Add LLMBaseURL and LLMAPIKey config options for narrative generation
- Wire up LLM client to generateBlog() when LLM is configured
- Fix ParticipantData type usage in test files
- Simplify rivalry arc detection (remove alternation check)
- Fix type conversion in upset detection gap calculation
- Mark narrative engine as complete in PROGRESS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-03-29 06:54:02 -04:00
parent 44544622ae
commit 5356c8ee0a
5 changed files with 19 additions and 15 deletions

View file

@ -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

View file

@ -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"),
}
}

View file

@ -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
}

View file

@ -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)
},

View file

@ -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{