Files
goclaw/internal/store/pg/memory_embedding_cache.go
T
Kai (Tam Nhu) Tran 30708ae79d feat(providers): support Codex OAuth pools with inherited routing defaults
* feat(auth): support named chatgpt oauth providers

- add provider-scoped ChatGPT OAuth routes and CLI support

- persist refresh tokens per provider and reject provider-type collisions

- wire provider OAuth setup flows in the dashboard and setup UI

Refs #448

* feat(agent): add chatgpt oauth account routing

- add agent other_config routing for manual and round-robin selection

- reuse routed provider resolution across resolver and pending loaders

- add router, parser, and agent advanced dialog coverage for multi-account use

Refs #448

* docs(api): describe chatgpt oauth routing

- document named-provider ChatGPT OAuth auth routes

- describe agent-side account routing and round-robin behavior

- update OpenAPI agent config schema and provider type enum

Refs #448

* fix(store): add missing agent key context helpers

* feat(ui): clarify chatgpt oauth account setup and routing

* docs(providers): align chatgpt oauth alias examples

* feat(agent): add codex pool activity dashboard

* fix(providers): harden codex oauth alias setup

* feat(codex-pool): improve routing dashboard UX

- redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence

- add clearer selection, attention, and recent-proof states for pool members

- make the lower panels fill the remaining desktop viewport while staying responsive

* fix(store): resolve context helper merge duplication

* feat(oauth): add codex pool quota and observation APIs

- add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers

- teach codex routing to surface pool activity, observation metadata, and quota-aware readiness

- extend tests and HTTP docs/OpenAPI for the new pool monitoring flows

* feat(web): add codex pool quota monitor and controls

- add provider quota fetching, readiness badges, and live routing evidence on the account pool page

- redesign pool setup and activity panels for multi-account management with localized copy updates

- keep the live monitor internally scrollable and compact the account cards for better viewport fit

* fix(web): clarify pool routing labels

- rename the recent request badge from Direct to Selected

- restore compact quota bars in the live pool cards

* feat(codex-pool): add runtime health dashboard

- derive per-provider success and failure health from routed Codex traces

- surface routing, quota, and recent request evidence in the pool UI

- align provider alias guidance and owner access with the dashboard role model

* docs(auth): document tenant scoping and key roles

* fix(auth): harden tenant and codex pool access control

* fix(providers): align codex pool runtime defaults

* feat(ui): tighten codex pool responsive layout

* feat(chatgpt-oauth): refine codex pool management UX

* feat(chatgpt-oauth): surface quota bars on provider pages

- add compact quota bars to Codex provider rows and provider detail

- fetch quota only for ready visible provider rows and ready detail aliases

- fix managed-member detail visibility and tighten provider locale copy
2026-03-27 09:35:57 +07:00

133 lines
4.0 KiB
Go

package pg
import (
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
)
// embeddingCacheEntry holds data for a single cache row.
type embeddingCacheEntry struct {
Hash string
Embedding []float32
}
// lookupEmbeddingCache fetches cached embeddings for the given content hashes.
// Returns a map from hash -> embedding vector. Missing hashes are simply absent.
func (s *PGMemoryStore) lookupEmbeddingCache(ctx context.Context, hashes []string, provider, model string) (map[string][]float32, error) {
if len(hashes) == 0 {
return nil, nil
}
// Build positional params: $1..$N for hashes, $N+1 for provider, $N+2 for model
placeholders := make([]string, len(hashes))
args := make([]any, 0, len(hashes)+2)
for i, h := range hashes {
placeholders[i] = fmt.Sprintf("$%d", i+1)
args = append(args, h)
}
args = append(args, provider, model)
query := fmt.Sprintf(
"SELECT hash, embedding FROM embedding_cache WHERE hash IN (%s) AND provider = $%d AND model = $%d",
strings.Join(placeholders, ","), len(hashes)+1, len(hashes)+2,
)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("lookup embedding cache: %w", err)
}
defer rows.Close()
result := make(map[string][]float32, len(hashes))
for rows.Next() {
var hash, vecStr string
if err := rows.Scan(&hash, &vecStr); err != nil {
slog.Warn("embedding cache scan error", "error", err)
continue
}
vec, err := parseVector(vecStr)
if err != nil {
slog.Warn("embedding cache parse error", "hash", hash, "error", err)
continue
}
result[hash] = vec
}
return result, rows.Err()
}
// writeEmbeddingCache batch-upserts embedding cache entries.
// Gracefully skips on dimension mismatch (schema uses vector(1536)).
func (s *PGMemoryStore) writeEmbeddingCache(ctx context.Context, entries []embeddingCacheEntry, provider, model string) error {
if len(entries) == 0 {
return nil
}
now := time.Now()
tenantID := tenantIDForInsert(ctx)
// Process in batches of 100 to avoid exceeding max query params
const batchSize = 100
for start := 0; start < len(entries); start += batchSize {
end := min(start+batchSize, len(entries))
batch := entries[start:end]
var sb strings.Builder
sb.WriteString(`INSERT INTO embedding_cache (hash, provider, model, embedding, dims, created_at, updated_at, tenant_id) VALUES `)
args := make([]any, 0, len(batch)*7)
for i, e := range batch {
if i > 0 {
sb.WriteByte(',')
}
base := i * 7
fmt.Fprintf(&sb, "($%d,$%d,$%d,$%d::vector,$%d,$%d,$%d,$%d)",
base+1, base+2, base+3, base+4, base+5, base+6, base+6, base+7)
args = append(args, e.Hash, provider, model, vectorToString(e.Embedding), len(e.Embedding), now, tenantID)
}
sb.WriteString(` ON CONFLICT (hash, provider, model) DO UPDATE SET embedding = EXCLUDED.embedding, dims = EXCLUDED.dims, updated_at = EXCLUDED.updated_at`)
_, err := s.db.ExecContext(ctx, sb.String(), args...)
if err != nil {
// pgvector dimension mismatch — skip cache gracefully
if strings.Contains(err.Error(), "dimensions") {
slog.Warn("embedding cache skipped: vector dimension mismatch",
"provider", provider, "model", model,
"actual_dims", len(batch[0].Embedding), "error", err)
return nil
}
return fmt.Errorf("batch write embedding cache: %w", err)
}
}
return nil
}
// parseVector converts a pgvector string like "[0.1,0.2,0.3]" into []float32.
func parseVector(s string) ([]float32, error) {
s = strings.TrimSpace(s)
if len(s) < 2 {
return nil, fmt.Errorf("vector string too short: %q", s)
}
// Strip surrounding brackets ([] from pgvector, () as fallback)
s = strings.TrimPrefix(s, "[")
s = strings.TrimSuffix(s, "]")
s = strings.TrimPrefix(s, "(")
s = strings.TrimSuffix(s, ")")
if s == "" {
return nil, nil
}
parts := strings.Split(s, ",")
vec := make([]float32, 0, len(parts))
for _, p := range parts {
f, err := strconv.ParseFloat(strings.TrimSpace(p), 32)
if err != nil {
return nil, fmt.Errorf("parse vector element %q: %w", p, err)
}
vec = append(vec, float32(f))
}
return vec, nil
}