ai-code-battle/cmd/acb-enrichment/main.go
jedarden e88c108010 feat(acb-enrichment): implement AI replay enrichment service
Implements the acb-enrichment service (plan §13.3) that generates AI
commentary for featured matches.

Key features:
- LLM client (OpenAI/Anthropic API compatible)
- Replay fetch from B2/R2 storage
- Structured commentary output (key_moments array with turn,
  description, significance, tags)
- Rate limiting to control LLM costs
- Match selection based on:
  - Minimum turn count
  - Win probability crossings
  - Upset threshold
  - Close finishes

Components:
- cmd/acb-enrichment/main.go - service entry point
- cmd/acb-enrichment/config.go - configuration from env vars
- cmd/acb-enrichment/service.go - orchestration logic
- internal/db/store.go - database access for match selection
- internal/llm/client.go - OpenAI-compatible LLM client
- internal/selector/selector.go - match selection with priority
- internal/generator/generator.go - commentary generation
- internal/storage/client.go - S3-compatible storage client
- Dockerfile - container image
- manifests/acb-enrichment-deployment.yml - K8s deployment
- metrics/metrics.go - Prometheus metrics for enrichment

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 02:22:28 -04:00

127 lines
3.2 KiB
Go

// Package main implements acb-enrichment, a service that generates AI commentary
// for featured AI Code Battle matches. It polls for matches without commentary,
// downloads replays from B2/R2, generates turn-by-turn narrative highlights via
// LLM, and stores results back to R2.
package main
import (
"context"
"database/sql"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/aicodebattle/acb/metrics"
_ "github.com/lib/pq"
)
func main() {
// Setup structured logging
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
cfg := LoadConfig()
// Connect to PostgreSQL
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
slog.Error("Failed to connect to PostgreSQL", "error", err)
os.Exit(1)
}
defer db.Close()
// Verify connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := db.PingContext(ctx); err != nil {
cancel()
slog.Error("Failed to ping PostgreSQL", "error", err)
os.Exit(1)
}
cancel()
slog.Info("Connected to PostgreSQL", "database", cfg.DatabaseName)
// Create enrichment service
svc := NewEnrichmentService(db, cfg)
// Verify storage configuration
if err := svc.CheckStorage(ctx); err != nil {
slog.Error("Storage check failed", "error", err)
os.Exit(1)
}
// Verify LLM configuration
if err := svc.CheckLLM(ctx); err != nil {
slog.Error("LLM check failed", "error", err)
os.Exit(1)
}
// Start internal metrics server
metricsSrv := metrics.StartServer()
defer metricsSrv.Close()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
startTime := time.Now()
cycleCount := 0
// Main enrichment loop
for {
// Check lifetime
if time.Since(startTime) > cfg.MaxLifetime {
slog.Info("Max lifetime reached, exiting", "lifetime", cfg.MaxLifetime)
os.Exit(0)
}
// Check for shutdown signal
select {
case sig := <-sigChan:
slog.Info("Received signal, shutting down", "signal", sig)
os.Exit(0)
default:
}
// Run enrichment cycle
cycleCount++
slog.Info("Starting enrichment cycle", "count", cycleCount)
cycleStart := time.Now()
cycleCtx, cycleCancel := context.WithTimeout(context.Background(), cfg.CycleTimeout)
results, err := svc.RunCycle(cycleCtx)
cycleCancel()
if err != nil {
slog.Error("Enrichment cycle failed", "error", err)
metrics.EnrichmentCycles.WithLabelValues("error").Inc()
} else {
slog.Info("Enrichment cycle completed",
"processed", results.Processed,
"enriched", results.Enriched,
"skipped", results.Skipped,
"failed", results.Failed,
)
metrics.EnrichmentCycles.WithLabelValues("success").Inc()
metrics.EnrichmentProcessed.Add(float64(results.Processed))
metrics.EnrichmentGenerated.Add(float64(results.Enriched))
}
metrics.EnrichmentCycleDuration.Observe(time.Since(cycleStart).Seconds())
// Sleep until next cycle
slog.Info("Sleeping until next enrichment cycle", "duration", cfg.CycleInterval)
time.Sleep(cfg.CycleInterval)
}
}
// CycleResults holds statistics from a single enrichment cycle.
type CycleResults struct {
Processed int
Enriched int
Skipped int
Failed int
}