From 1b994060124206594ac908817a4beea845c5ce83 Mon Sep 17 00:00:00 2001 From: Luan Vu Date: Wed, 11 Mar 2026 14:31:00 +0700 Subject: [PATCH] fix: resolve embedding provider from DB registry + per-agent config (#134) The embedding provider resolution only matched 3 hardcoded names (openai, openrouter, gemini), silently failing for DB-stored providers like "openai-embedding". This caused memory chunks to be stored without vectors even when a valid embedding provider was configured. Changes: - resolveEmbeddingProvider: fallback to provider registry for DB-stored provider names when hardcoded match fails - gateway startup: read per-agent memory config from DB (priority over config file defaults) for embedding provider resolution - memory IndexDocument: log embedding errors instead of swallowing them - memory admin ListChunks: return full chunk text instead of truncating to 200 chars, avoiding confusing partial content in the UI Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com> --- cmd/gateway.go | 13 +++++++++-- cmd/gateway_agents.go | 37 ++++++++++++++++++++++++++----- cmd/gateway_managed.go | 2 +- internal/store/pg/memory_admin.go | 4 ++-- internal/store/pg/memory_docs.go | 8 ++++++- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index c4ed8d32..6dd69276 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -338,9 +338,18 @@ func runGateway() { } // Wire embedding provider to PGMemoryStore so IndexDocument generates vectors. + // Per-agent DB config takes priority over config file defaults. if pgStores.Memory != nil { memCfg := cfg.Agents.Defaults.Memory - if embProvider := resolveEmbeddingProvider(cfg, memCfg); embProvider != nil { + if pgStores.Agents != nil { + if defaultAgent, agErr := pgStores.Agents.GetByKey(context.Background(), "default"); agErr == nil { + if agentMemCfg := defaultAgent.ParseMemoryConfig(); agentMemCfg != nil { + memCfg = agentMemCfg + slog.Debug("using per-agent memory config from DB", "agent", defaultAgent.AgentKey) + } + } + } + if embProvider := resolveEmbeddingProvider(cfg, memCfg, providerRegistry); embProvider != nil { pgStores.Memory.SetEmbeddingProvider(embProvider) slog.Info("memory embeddings enabled", "provider", embProvider.Name(), "model", embProvider.Model()) @@ -495,7 +504,7 @@ func runGateway() { } if pgSkills, ok := pgStores.Skills.(*pg.PGSkillStore); ok { memCfg := cfg.Agents.Defaults.Memory - if embProvider := resolveEmbeddingProvider(cfg, memCfg); embProvider != nil { + if embProvider := resolveEmbeddingProvider(cfg, memCfg, providerRegistry); embProvider != nil { pgSkills.SetEmbeddingProvider(embProvider) skillSearchTool.SetEmbeddingSearcher(pgSkills, embProvider) slog.Info("skill embeddings enabled", "provider", embProvider.Name()) diff --git a/cmd/gateway_agents.go b/cmd/gateway_agents.go index d6879c49..b6269609 100644 --- a/cmd/gateway_agents.go +++ b/cmd/gateway_agents.go @@ -1,6 +1,7 @@ package cmd import ( + "log/slog" "strings" "github.com/nextlevelbuilder/goclaw/internal/bus" @@ -13,23 +14,33 @@ import ( ) // resolveEmbeddingProvider auto-selects an embedding provider based on config and available API keys. -// Matching TS embedding provider auto-selection order. -func resolveEmbeddingProvider(cfg *config.Config, memCfg *config.MemoryConfig) memory.EmbeddingProvider { +// Resolution order: +// 1. Explicit provider name from memCfg → hardcoded match → registry lookup (DB providers) +// 2. Auto-detect: openai → openrouter → gemini (config-file keys) +// +// The optional providerReg allows resolving DB-stored provider names (e.g. "openai-embedding") +// that don't match the hardcoded provider names. +func resolveEmbeddingProvider(cfg *config.Config, memCfg *config.MemoryConfig, providerReg *providers.Registry) memory.EmbeddingProvider { // Explicit provider in config if memCfg != nil && memCfg.EmbeddingProvider != "" { - return createEmbeddingProvider(memCfg.EmbeddingProvider, cfg, memCfg) + if p := createEmbeddingProvider(memCfg.EmbeddingProvider, cfg, memCfg, providerReg); p != nil { + return p + } + // Explicit name set but no match — don't auto-detect, log and return nil + slog.Warn("embedding provider not found", "name", memCfg.EmbeddingProvider) + return nil } // Auto-select: openai → openrouter → gemini for _, name := range []string{"openai", "openrouter", "gemini"} { - if p := createEmbeddingProvider(name, cfg, memCfg); p != nil { + if p := createEmbeddingProvider(name, cfg, memCfg, nil); p != nil { return p } } return nil } -func createEmbeddingProvider(name string, cfg *config.Config, memCfg *config.MemoryConfig) memory.EmbeddingProvider { +func createEmbeddingProvider(name string, cfg *config.Config, memCfg *config.MemoryConfig, providerReg *providers.Registry) memory.EmbeddingProvider { model := "text-embedding-3-small" apiBase := "" if memCfg != nil { @@ -71,6 +82,22 @@ func createEmbeddingProvider(name string, cfg *config.Config, memCfg *config.Mem return memory.NewOpenAIEmbeddingProvider("gemini", cfg.Providers.Gemini.APIKey, "https://generativelanguage.googleapis.com/v1beta/openai", geminiModel). WithDimensions(1536) } + + // Fallback: resolve from provider registry (DB-stored providers like "openai-embedding"). + // Any OpenAI-compatible provider in the registry can serve embeddings. + if providerReg != nil { + if regProv, err := providerReg.Get(name); err == nil { + if op, ok := regProv.(*providers.OpenAIProvider); ok { + embBase := op.APIBase() + if apiBase != "" { + embBase = apiBase // memCfg override takes precedence + } + slog.Info("embedding provider resolved from registry", "name", name, "base", embBase, "model", model) + return memory.NewOpenAIEmbeddingProvider(name, op.APIKey(), embBase, model) + } + slog.Warn("embedding provider in registry is not OpenAI-compatible", "name", name) + } + } return nil } diff --git a/cmd/gateway_managed.go b/cmd/gateway_managed.go index 4d321814..3525583a 100644 --- a/cmd/gateway_managed.go +++ b/cmd/gateway_managed.go @@ -458,7 +458,7 @@ func wireExtras( var delegateEmbProvider store.EmbeddingProvider if agentStore, ok := stores.Agents.(*pg.PGAgentStore); ok { memCfg := appCfg.Agents.Defaults.Memory - if embProvider := resolveEmbeddingProvider(appCfg, memCfg); embProvider != nil { + if embProvider := resolveEmbeddingProvider(appCfg, memCfg, providerReg); embProvider != nil { agentStore.SetEmbeddingProvider(embProvider) delegateEmbProvider = embProvider slog.Info("agent embeddings enabled") diff --git a/internal/store/pg/memory_admin.go b/internal/store/pg/memory_admin.go index 09de58c0..2adf19db 100644 --- a/internal/store/pg/memory_admin.go +++ b/internal/store/pg/memory_admin.go @@ -127,7 +127,7 @@ func (s *PGMemoryStore) ListChunks(ctx context.Context, agentID, userID, path st var args []any if userID == "" { q = `SELECT c.id, c.start_line, c.end_line, - LEFT(c.text, 200) AS text_preview, + c.text AS text_preview, (c.embedding IS NOT NULL) AS has_embedding FROM memory_chunks c JOIN memory_documents d ON c.document_id = d.id @@ -136,7 +136,7 @@ func (s *PGMemoryStore) ListChunks(ctx context.Context, agentID, userID, path st args = []any{aid, path} } else { q = `SELECT c.id, c.start_line, c.end_line, - LEFT(c.text, 200) AS text_preview, + c.text AS text_preview, (c.embedding IS NOT NULL) AS has_embedding FROM memory_chunks c JOIN memory_documents d ON c.document_id = d.id diff --git a/internal/store/pg/memory_docs.go b/internal/store/pg/memory_docs.go index 6243cb8c..e9af2215 100644 --- a/internal/store/pg/memory_docs.go +++ b/internal/store/pg/memory_docs.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "log/slog" "time" "github.com/google/uuid" @@ -175,7 +176,12 @@ func (s *PGMemoryStore) IndexDocument(ctx context.Context, agentID, userID, path for i, c := range chunks { texts[i] = c.Text } - embeddings, _ = s.provider.Embed(ctx, texts) + var embErr error + embeddings, embErr = s.provider.Embed(ctx, texts) + if embErr != nil { + slog.Warn("memory embedding failed, storing chunks without vectors", + "path", path, "chunks", len(chunks), "error", embErr) + } } // Insert chunks