Ctrl K
ComunidadGuías…

config.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package graymatter

import (
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/angelnicolasc/graymatter/pkg/memory"
)

// EmbeddingMode controls how GrayMatter generates vector embeddings.
type EmbeddingMode int

const (
	// EmbeddingAuto detects the best available provider at runtime.
	// Detection order: Ollama → OpenAI → Voyage → keyword-only.
	EmbeddingAuto EmbeddingMode = iota
	// EmbeddingOllama forces Ollama (requires a running Ollama instance).
	EmbeddingOllama
	// EmbeddingAnthropic resolves to the Voyage-backed embeddings slot.
	// Deprecated: Anthropic has no embeddings API; kept for numeric stability.
	// Use EmbeddingVoyage with VOYAGE_API_KEY instead.
	EmbeddingAnthropic
	// EmbeddingKeyword disables vector search; uses keyword+recency scoring only.
	EmbeddingKeyword
	// EmbeddingOpenAI forces OpenAI Embeddings API (requires OPENAI_API_KEY).
	EmbeddingOpenAI
	// EmbeddingVoyage forces the Voyage AI Embeddings API (requires VOYAGE_API_KEY).
	EmbeddingVoyage
)

// Config holds all GrayMatter configuration. All fields have sane defaults
// via DefaultConfig(). Zero-value Config is not valid — always call DefaultConfig().
type Config struct {
	// ConfidenceWeight is the product preference default in [0, 0.5]. The
	// compatible opt-in delivery keeps it zero pending a published rollout.
	ConfidenceWeight float64
	// DataDir is the directory where gray.db and vector files are stored.
	// Default: ".graymatter"
	DataDir string

	// TopK is the maximum number of facts returned by Recall.
	// Default: 8
	TopK int

	// EmbeddingMode controls which embedding backend is used.
	// Default: EmbeddingAuto (Ollama → OpenAI → Anthropic → keyword)
	EmbeddingMode EmbeddingMode

	// OllamaURL is the base URL of the Ollama API.
	// Default: value of GRAYMATTER_OLLAMA_URL env var, or "http://localhost:11434"
	OllamaURL string

	// OllamaModel is the embedding model used with Ollama.
	// Default: value of GRAYMATTER_OLLAMA_MODEL env var, or "nomic-embed-text"
	OllamaModel string

	// OllamaConsolidateModel is the local model used for consolidation
	// summarisation when ConsolidateLLM is "ollama".
	// Default: value of GRAYMATTER_OLLAMA_CONSOLIDATE_MODEL env var, or "llama3.2"
	OllamaConsolidateModel string

	// AnthropicAPIKey for the Anthropic consolidation endpoints.
	// Default: value of ANTHROPIC_API_KEY env var.
	AnthropicAPIKey string

	// VoyageAPIKey for the Voyage AI embeddings endpoint (api.voyageai.com),
	// which is what the third slot of the embedding chain actually dials.
	// Before v0.15 that slot dialled an Anthropic embeddings endpoint that
	// does not exist, silently degrading every store to keyword-only.
	// Default: value of VOYAGE_API_KEY env var.
	VoyageAPIKey string

	// OpenAIAPIKey for the OpenAI Embeddings API (text-embedding-3-small).
	// Default: value of OPENAI_API_KEY env var.
	OpenAIAPIKey string

	// OpenAIModel overrides the OpenAI embedding model.
	// Default: value of GRAYMATTER_OPENAI_MODEL env var, or "text-embedding-3-small"
	OpenAIModel string

	// ConsolidateLLM specifies which LLM provider drives memory consolidation.
	// Values: "anthropic", "ollama", "" (disable consolidation).
	// Default: "anthropic" if ANTHROPIC_API_KEY is set, else "" (disabled).
	// To use Ollama as the consolidation LLM, set this field explicitly to "ollama".
	ConsolidateLLM string

	// ConsolidateModel is the model used for consolidation summarisation.
	// Default: "claude-haiku-4-5-20251001"
	ConsolidateModel string

	// ConsolidateThreshold is the minimum fact count that triggers consolidation.
	// Default: 20
	ConsolidateThreshold int

	// DecayHalfLife is the half-life for the exponential weight decay curve.
	// Facts not accessed within this window lose half their retrieval weight.
	// Default: 720h (30 days)
	DecayHalfLife time.Duration

	// AsyncConsolidate runs consolidation in a background goroutine after Remember.
	// Default: true
	AsyncConsolidate bool

	// MaxAsyncConsolidations bounds how many consolidation goroutines may run
	// concurrently. Additional triggers while at capacity are silently dropped.
	// Default: 2
	MaxAsyncConsolidations int

	// OnConsolidateError is called when an async consolidation goroutine returns
	// an error. If nil, errors are discarded. The callback must be safe for
	// concurrent use.
	OnConsolidateError func(agentID string, err error)

	// OnVectorIndexError is called when the vector store fails to index a fact
	// after the bbolt write has already succeeded. The fact is durably marked
	// as pending and will be retried on the next reconcile tick or process
	// restart. If nil, errors are silently retried. Must be safe for concurrent
	// use.
	OnVectorIndexError func(agentID, factID string, err error)

	// VectorReconcileInterval controls how often the background reconciler
	// drains the pending-vector queue. Vectors that failed to index inline
	// (e.g. transient embedder error) are retried on this cadence.
	// Default: 30s. Set to 0 to disable the background loop (reconciliation
	// will then only run at Open()).
	VectorReconcileInterval time.Duration

	// ReadOnly opens the store in read-only mode, skipping all mutating
	// operations. When false (default), the TUI and CLI automatically fall
	// back to read-only if the write lock is held by another process (e.g.
	// opencode running in the same directory).
	ReadOnly bool

	// StrictWrite disables the automatic read-only fallback: if the write
	// lock cannot be acquired, Open fails immediately instead of degrading.
	// The store daemon sets this — a store owner that silently came up
	// read-only would break every connected client. StrictWrite wins over
	// ReadOnly when both are set.
	StrictWrite bool

	// SignalWeights sets how much vector similarity, keyword relevance and
	// recency each contribute to the fused ranking.
	// Default: nil, meaning memory.DefaultSignalWeights() — vector 1.0,
	// keyword 1.0, recency 0.5, the values hardcoded before v0.10.0.
	SignalWeights *memory.SignalWeights

	// MinRelevance drops recalled facts scoring below this fraction of the
	// best score in the same result set.
	// Default: 0 — no cut, Recall returns exactly TopK as it always has.
	MinRelevance float64

	// StemKeywords folds English morphology into the keyword signal so a
	// question about "backups" reaches a fact about "backup retention". Pure
	// Go, no model, no download, no state.
	//
	// ON by default since the measurement that earned it: on a corpus this
	// change was not designed against — the scale corpus, 35 probes, 5k/10k/30k
	// facts — it lifts one-query retrieval from 25/35 to 29/35 at every size,
	// and the four it fixes are morphology and nothing else (backups/backup,
	// rotations/rotation, releases/release, deployment/deploy). The queries it
	// fails with stemming are a STRICT SUBSET of the ones it fails without: it
	// wins four and loses none. That subset property is the revert criterion,
	// and TestStemmingNeverLosesAQueryItWinsWithout pins it.
	//
	// The cost is latency on the scan path, and it is not small. Two machines
	// measured it at +61%/+65% and at +90%/+90% (10k and 30k facts), so the
	// durable statement is the ratio and not either point: the scan pays
	// roughly TWICE, and the candidate-set path pays NOTHING — 5 ms either way
	// at 10k, 11 ms at 30k, on both machines.
	//
	// That gap is structural, not luck. On the scan path keywordScoreDetailed
	// re-tokenises every stored fact on every query, so each token of each fact
	// walks the suffix rules once per query. The index stems the corpus once at
	// write time and stems only the query's own terms at read time: O(N) versus
	// O(query). The cost falls away entirely on the candidate-set path, which
	// is default ON now — its gate was the counterpart of this default, not a
	// competing optimisation. Below a few thousand facts it is single-digit
	// milliseconds either way.
	//
	// Env: GRAYMATTER_STEM_KEYWORDS=0 opts out.
	StemKeywords bool

	// UsageAliasLearning lets the store promote its own vocabulary from
	// observed reformulations: a weak match followed by a strong one from the
	// same agent is evidence that the agent's word and the store's word mean
	// the same thing, and the second independent observation of the pair
	// promotes an alias. No agent action, no server-side semantics, no model.
	// Off by default; see pkg/memory/usagealias.go for the guardrails.
	//
	// Env: GRAYMATTER_USAGE_ALIAS=1
	UsageAliasLearning bool

	// UsageAliasAffinityMin is how much lexical affinity an unknown word needs
	// with the working word before the store promotes a usage alias: the
	// minimum shared leading prefix, in characters.
	//
	// 0 (the zero value) means the conservative default of 3 — the
	// morphology class, where co-occurrence evidence is decisive. -1 removes
	// the gate and lets the store learn the synonym class as well.
	//
	// Measured on a blind evaluation corpus with real agent reformulations, the
	// open gate is worth +2 families out of 40 and promotes about one junk
	// pair in three. That is why it is reachable and not a default.
	//
	// Env: GRAYMATTER_USAGE_ALIAS_AFFINITY (e.g. -1)
	UsageAliasAffinityMin int

	// CandidateRetrieval answers a recall from an inverted index and a
	// recency spine instead of loading and re-tokenising every stored fact.
	// The ranking is identical — pinned by a test that runs both paths over
	// one store and compares fused scores, per-signal ranks and lineage — so
	// this changes what gets read, never what gets returned.
	//
	// ON by default since both of its gates went green on two machines: the
	// 30k latency gate measured p99 20.4 ms and 21.2 ms against the 40 ms bar,
	// with a harness that verifies every measured recall returned facts, and
	// the write-cost bar (<= 3 ms) was ratified by the owner — a write-once,
	// read-many store pays ~1.5 ms per Put to keep every read sub-linear.
	// A store written before this default keeps answering: the first recall
	// rebuilds the index and the ranking does not change.
	//
	// Env: GRAYMATTER_CANDIDATE_RETRIEVAL=0 opts out.
	CandidateRetrieval bool
}

// DefaultConfig returns a Config with all defaults applied from environment
// variables and runtime probes. Safe to call multiple times.
func DefaultConfig() Config {
	return Config{
		ConfidenceWeight:        memory.DefaultConfidenceWeight,
		DataDir:                 ".graymatter",
		TopK:                    8,
		EmbeddingMode:           EmbeddingAuto,
		OllamaURL:               envOrDefault("GRAYMATTER_OLLAMA_URL", "http://localhost:11434"),
		OllamaModel:             envOrDefault("GRAYMATTER_OLLAMA_MODEL", "nomic-embed-text"),
		OllamaConsolidateModel:  envOrDefault("GRAYMATTER_OLLAMA_CONSOLIDATE_MODEL", "llama3.2"),
		AnthropicAPIKey:         os.Getenv("ANTHROPIC_API_KEY"),
		VoyageAPIKey:            os.Getenv("VOYAGE_API_KEY"),
		OpenAIAPIKey:            os.Getenv("OPENAI_API_KEY"),
		OpenAIModel:             envOrDefault("GRAYMATTER_OPENAI_MODEL", "text-embedding-3-small"),
		ConsolidateLLM:          resolveConsolidateLLM(),
		ConsolidateModel:        "claude-haiku-4-5-20251001",
		ConsolidateThreshold:    20,
		VectorReconcileInterval: 30 * time.Second,
		DecayHalfLife:           720 * time.Hour,
		AsyncConsolidate:        true,
		MaxAsyncConsolidations:  2,
		StemKeywords:            envBoolDefault("GRAYMATTER_STEM_KEYWORDS", true),
		UsageAliasLearning:      envBool("GRAYMATTER_USAGE_ALIAS"),
		UsageAliasAffinityMin:   envInt("GRAYMATTER_USAGE_ALIAS_AFFINITY"),
		CandidateRetrieval:      envBoolDefault("GRAYMATTER_CANDIDATE_RETRIEVAL", true),
	}
}

// resolveConsolidateLLM returns the best available consolidation LLM based on
// environment variables. It does NOT probe network endpoints at startup.
//
// Detection order:
//  1. "anthropic" — if ANTHROPIC_API_KEY is set
//  2. ""          — disabled (set ConsolidateLLM="ollama" explicitly for Ollama)
//
// Ollama is excluded from auto-detection because probing the Ollama endpoint on
// every process startup would add 500 ms+ of latency. Configure it explicitly:
//
//	cfg := graymatter.DefaultConfig()
//	cfg.ConsolidateLLM = "ollama"
func resolveConsolidateLLM() string {
	if os.Getenv("ANTHROPIC_API_KEY") != "" {
		return "anthropic"
	}
	return ""
}

func envOrDefault(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

// envInt reads a signed integer knob. Unset or unparseable is 0, which every
// consumer reads as "use the built-in default" — so a typo degrades to the
// documented behaviour instead of to a number nobody chose.
func envInt(key string) int {
	v, err := strconv.Atoi(strings.TrimSpace(os.Getenv(key)))
	if err != nil {
		return 0
	}
	return v
}

// envBool reads an opt-in switch: unset means off, and only an explicit yes
// turns it on.
//
// DefaultConfig is the single place the daemon, the CLI's --no-daemon path,
// the MCP server, the REST server and the harness all build their store from,
// so wiring a switch there reaches every surface at once — and keeps the
// README's promise intact, since an environment variable is not a config file.
func envBool(key string) bool {
	return envBoolDefault(key, false)
}

// envBoolDefault reads a switch that has a default in either direction.
//
// envBool alone cannot express a default-on flag: it maps everything that is
// not an explicit yes to false, so "unset" and "0" are the same answer and a
// flag flipped to default-on becomes impossible to turn off. That is the same
// shape as the affinity knob nobody could reach and the memory_alias that
// resolved nowhere — a setting that exists in the documentation and not in the
// product. Three states, so an opt-out is sayable.
func envBoolDefault(key string, def bool) bool {
	switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
	case "1", "true", "yes", "on":
		return true
	case "0", "false", "no", "off":
		return false
	}
	return def
}

// Config implements memory.ConsolidateConfig so it can be passed directly
// to Store.Consolidate / Store.MaybeConsolidate without an adapter.

func (c Config) GetAnthropicAPIKey() string        { return c.AnthropicAPIKey }
func (c Config) GetConsolidateLLM() string         { return c.ConsolidateLLM }
func (c Config) GetConsolidateModel() string       { return c.ConsolidateModel }
func (c Config) GetOllamaURL() string              { return c.OllamaURL }
func (c Config) GetOllamaConsolidateModel() string { return c.OllamaConsolidateModel }
func (c Config) GetConsolidateThreshold() int      { return c.ConsolidateThreshold }
func (c Config) GetDecayHalfLife() time.Duration   { return c.DecayHalfLife }