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>
This commit is contained in:
Luan VuandLuvu182 authored and GitHub committed 2026-03-11 14:31:00 +07:00
1 parent e9e0a4e813
commit 1b99406012
5 files changed
+53 -11

No files matched your search

+11 -2
View File
@@ -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())
+32 -5
View File
@@ -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
}
+1 -1
View File
@@ -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")
+2 -2
View File
@@ -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
+7 -1
View File
@@ -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