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>
117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Config holds all configuration for the enrichment service.
|
|
type Config struct {
|
|
// Database
|
|
DatabaseURL string
|
|
DatabaseName string
|
|
|
|
// LLM
|
|
LLMBaseURL string
|
|
LLMAPIKey string
|
|
LLMModel string // Model to use for commentary (e.g., "gpt-4o-mini", "claude-3-haiku")
|
|
LLMMaxTokens int
|
|
LLMTemperature float64
|
|
|
|
// Rate limiting
|
|
MaxEnrichmentsPerHour int // Maximum number of enrichments per hour
|
|
MaxConcurrentRequests int // Maximum parallel LLM requests
|
|
|
|
// Storage (B2/R2)
|
|
B2BucketName string
|
|
B2AccessKeyID string
|
|
B2SecretAccessKey string
|
|
B2Endpoint string // S3-compatible endpoint URL
|
|
|
|
R2BucketName string
|
|
R2AccessKeyID string
|
|
R2SecretAccessKey string
|
|
R2Endpoint string
|
|
|
|
// Enrichment criteria
|
|
MinTurnCount int // Minimum turn count to consider enrichment
|
|
MinWinProbCrossings int // Minimum win probability crossings
|
|
UpsetThreshold float64 // Minimum rating difference for upset consideration
|
|
|
|
// Timing
|
|
CycleInterval time.Duration // How often to run enrichment cycles
|
|
CycleTimeout time.Duration // Max duration per cycle
|
|
MaxLifetime time.Duration // Process lifetime before restart
|
|
}
|
|
|
|
// LoadConfig reads configuration from environment variables.
|
|
func LoadConfig() Config {
|
|
return Config{
|
|
DatabaseURL: envOr("ACB_DATABASE_URL", "postgres://localhost:5432/acb?sslmode=disable"),
|
|
DatabaseName: envOr("ACB_DATABASE_NAME", "acb"),
|
|
|
|
LLMBaseURL: envOr("ACB_LLM_BASE_URL", "https://api.openai.com/v1"),
|
|
LLMAPIKey: os.Getenv("ACB_LLM_API_KEY"),
|
|
LLMModel: envOr("ACB_LLM_MODEL", "gpt-4o-mini"),
|
|
LLMMaxTokens: envInt("ACB_LLM_MAX_TOKENS", 2000),
|
|
LLMTemperature: envFloat("ACB_LLM_TEMPERATURE", 0.7),
|
|
|
|
MaxEnrichmentsPerHour: envInt("ACB_ENRICHMENT_MAX_PER_HOUR", 20),
|
|
MaxConcurrentRequests: envInt("ACB_ENRICHMENT_MAX_CONCURRENT", 3),
|
|
|
|
B2BucketName: envOr("ACB_B2_BUCKET", "ai-code-battle"),
|
|
B2AccessKeyID: os.Getenv("ACB_B2_ACCESS_KEY_ID"),
|
|
B2SecretAccessKey: os.Getenv("ACB_B2_SECRET_ACCESS_KEY"),
|
|
B2Endpoint: envOr("ACB_B2_ENDPOINT", "https://s3.us-west-004.backblazeb2.com"),
|
|
|
|
R2BucketName: envOr("ACB_R2_BUCKET", "ai-code-battle"),
|
|
R2AccessKeyID: os.Getenv("ACB_R2_ACCESS_KEY_ID"),
|
|
R2SecretAccessKey: os.Getenv("ACB_R2_SECRET_ACCESS_KEY"),
|
|
R2Endpoint: envOr("ACB_R2_ENDPOINT", "https://r2.cloudflarestorage.com"),
|
|
|
|
MinTurnCount: envInt("ACB_ENRICHMENT_MIN_TURNS", 100),
|
|
MinWinProbCrossings: envInt("ACB_ENRICHMENT_MIN_CROSSINGS", 3),
|
|
UpsetThreshold: envFloat("ACB_ENRICHMENT_UPSET_THRESHOLD", 150),
|
|
|
|
CycleInterval: envDuration("ACB_ENRICHMENT_INTERVAL", 30*time.Minute),
|
|
CycleTimeout: envDuration("ACB_ENRICHMENT_TIMEOUT", 25*time.Minute),
|
|
MaxLifetime: envDuration("ACB_ENRICHMENT_MAX_LIFETIME", 4*time.Hour),
|
|
}
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envInt(key string, fallback int) int {
|
|
if v := os.Getenv(key); v != "" {
|
|
var i int
|
|
if _, err := fmt.Sscanf(v, "%d", &i); err == nil && i > 0 {
|
|
return i
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envFloat(key string, fallback float64) float64 {
|
|
if v := os.Getenv(key); v != "" {
|
|
var f float64
|
|
if _, err := fmt.Sscanf(v, "%f", &f); err == nil {
|
|
return f
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envDuration(key string, fallback time.Duration) time.Duration {
|
|
if v := os.Getenv(key); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
return fallback
|
|
}
|