feat(ai): pluggable provider interface, Ollama + Mistral impls, migrate Pass2 sites

Replaces the Mistral-only ai.Client with an ai.Provider interface backed by
Ollama and Mistral implementations. Migrates enrichment + similarity callers
to ai.Provider.Chat. Research endpoint returns 501 until commit 2 reinstates
it on the new orchestrator.
This commit is contained in:
2026-04-24 16:35:18 +02:00
parent 2adb4882c7
commit 24e072b63d
26 changed files with 982 additions and 1078 deletions

View File

@@ -29,6 +29,7 @@ import (
"os"
"time"
"marktvogt.de/backend/internal/config"
"marktvogt.de/backend/internal/domain/discovery/enrich"
"marktvogt.de/backend/internal/pkg/ai"
"marktvogt.de/backend/internal/pkg/scrape"
@@ -65,8 +66,14 @@ func realMain() int {
)
flag.Parse()
apiKey := os.Getenv("AI_API_KEY")
model := os.Getenv("AI_MODEL_COMPLEX")
apiKey := os.Getenv("AI_MISTRAL_API_KEY")
if apiKey == "" {
apiKey = os.Getenv("AI_API_KEY") // legacy fallback
}
model := os.Getenv("AI_MISTRAL_MODEL")
if model == "" {
model = os.Getenv("AI_MODEL_COMPLEX") // legacy fallback
}
if model == "" {
model = "mistral-large-latest"
}
@@ -74,9 +81,14 @@ func realMain() int {
if userAgent == "" {
userAgent = "marktvogt-eval/1.0 (+https://marktvogt.de)"
}
client := ai.New(apiKey, "", model, 1.0)
if !client.Enabled() {
slog.Error("AI client not configured (set AI_API_KEY)")
client, err := ai.NewFromConfig(config.AIConfig{
Provider: "mistral",
MistralAPIKey: apiKey,
MistralModel: model,
RateLimitRPS: 1.0,
})
if err != nil {
slog.Error("AI client not configured", "error", err)
return 2
}
@@ -95,7 +107,7 @@ func realMain() int {
return runSimilarityMode(ctx, client, cfg)
case modeCategory:
scraper := scrape.New(userAgent)
enricher := enrich.NewMistralLLMEnricher(client, scraper)
enricher := enrich.NewLLMEnricher(client, scraper)
cfg := evalConfig{
model: model,
fixturePath: pathWithDefault(*fixturePath, "backend/cmd/discovery-eval/fixtures/category.json"),
@@ -117,7 +129,7 @@ func pathWithDefault(p, dflt string) string {
return p
}
func runSimilarityMode(ctx context.Context, client *ai.Client, cfg evalConfig) int {
func runSimilarityMode(ctx context.Context, client ai.Provider, cfg evalConfig) int {
fixture, err := loadFixture(cfg.fixturePath)
if err != nil {
slog.Error("load fixture", "path", cfg.fixturePath, "error", err)
@@ -125,7 +137,7 @@ func runSimilarityMode(ctx context.Context, client *ai.Client, cfg evalConfig) i
}
slog.Info("loaded fixture", "mode", modeSimilarity, "pairs", len(fixture.Pairs), "path", cfg.fixturePath)
classifier := enrich.NewMistralSimilarityClassifier(client)
classifier := enrich.NewSimilarityClassifier(client)
cache, err := loadCache(cfg.cachePath)
if err != nil {