openclaw/extensions/memory-ruvector/config.ts
File 4670817426 feat(memory): add ruvector vector database plugin
Add new memory-ruvector extension providing high-performance vector
storage and semantic search capabilities using the ruvector database.

Features:
- Dual-mode operation (remote server or local database)
- Automatic message indexing via hooks
- Semantic search tool for agents
- Multiple embedding providers (OpenAI, Voyage AI, local)
- SONA self-learning for improved search accuracy
- GNN and Cypher graph queries for relationship traversal
- Graceful in-memory fallback
- CLI commands for management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 08:14:01 +01:00

339 lines
11 KiB
TypeScript

/**
* Configuration schema for ruvector Memory Plugin
*/
import { join } from "node:path";
import { homedir } from "node:os";
import type { HooksConfig } from "./hooks.js";
import type { DistanceMetric, SONAConfig } from "./types.js";
// ============================================================================
// Types
// ============================================================================
export type RuvectorConfig = {
/** Path to ruvector database directory */
dbPath: string;
/** Vector dimension (must match embedding model) */
dimension: number;
/** Distance metric for similarity search */
metric: DistanceMetric;
/** Embedding provider configuration */
embedding: {
provider: "openai" | "voyage" | "local";
apiKey?: string;
model?: string;
baseUrl?: string;
};
/** Hook configuration for automatic indexing */
hooks: HooksConfig;
/** SONA self-learning configuration */
sona?: SONAConfig;
};
// ============================================================================
// Defaults
// ============================================================================
const DEFAULT_DB_PATH = join(homedir(), ".clawdbot", "memory", "ruvector");
const DEFAULT_DIMENSION = 1536;
const DEFAULT_METRIC = "cosine";
const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
// ============================================================================
// Dimension mappings for known models
// ============================================================================
const EMBEDDING_DIMENSIONS: Record<string, number> = {
// OpenAI
"text-embedding-3-small": 1536,
"text-embedding-3-large": 3072,
"text-embedding-ada-002": 1536,
// Voyage AI
"voyage-3": 1024,
"voyage-3-large": 1024,
"voyage-3.5-lite": 512,
"voyage-code-3": 1024,
// Local (common models)
"nomic-embed-text": 768,
"all-minilm-l6-v2": 384,
};
export function dimensionForModel(model: string): number {
const dims = EMBEDDING_DIMENSIONS[model];
if (dims) return dims;
// Default fallback for unknown models
return DEFAULT_DIMENSION;
}
// ============================================================================
// Validation helpers
// ============================================================================
function assertAllowedKeys(
value: Record<string, unknown>,
allowed: string[],
label: string,
): void {
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length === 0) return;
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
}
function resolveEnvVars(value: string): string {
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
const envValue = process.env[envVar];
if (!envValue) {
throw new Error(`Environment variable ${envVar} is not set`);
}
return envValue;
});
}
// ============================================================================
// Config Schema
// ============================================================================
export const ruvectorConfigSchema = {
parse(value: unknown): RuvectorConfig {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("ruvector config required");
}
const cfg = value as Record<string, unknown>;
assertAllowedKeys(
cfg,
["dbPath", "dimension", "metric", "embedding", "hooks", "sona"],
"ruvector config",
);
// Parse embedding config
const embedding = cfg.embedding as Record<string, unknown> | undefined;
if (!embedding) {
throw new Error("embedding config is required");
}
assertAllowedKeys(
embedding,
["provider", "apiKey", "model", "baseUrl"],
"embedding config",
);
const embeddingProvider = (embedding.provider as string) ?? "openai";
if (!["openai", "voyage", "local"].includes(embeddingProvider)) {
throw new Error(
`Invalid embedding provider: ${embeddingProvider}. Must be openai, voyage, or local`,
);
}
// API key required for non-local providers (empty string treated as missing)
const rawApiKey = embedding.apiKey as string | undefined;
if (embeddingProvider !== "local" && (!rawApiKey || rawApiKey.trim() === "")) {
throw new Error(`embedding.apiKey is required for provider: ${embeddingProvider}`);
}
const embeddingModel =
typeof embedding.model === "string"
? embedding.model
: DEFAULT_EMBEDDING_MODEL;
const resolvedDimension =
typeof cfg.dimension === "number"
? cfg.dimension
: dimensionForModel(embeddingModel);
// Validate dimension is a positive integer
if (!Number.isInteger(resolvedDimension) || resolvedDimension <= 0) {
throw new Error(`Invalid dimension: ${resolvedDimension}. Must be a positive integer`);
}
// Parse hooks config
const hooksRaw = cfg.hooks as Record<string, unknown> | undefined;
if (hooksRaw) {
assertAllowedKeys(
hooksRaw,
["enabled", "indexInbound", "indexOutbound", "indexAgentResponses", "batchSize", "debounceMs"],
"hooks config",
);
}
const batchSize = typeof hooksRaw?.batchSize === "number" ? hooksRaw.batchSize : 10;
const debounceMs = typeof hooksRaw?.debounceMs === "number" ? hooksRaw.debounceMs : 500;
// Validate hooks numeric values
if (!Number.isInteger(batchSize) || batchSize <= 0) {
throw new Error(`Invalid hooks.batchSize: ${batchSize}. Must be a positive integer`);
}
if (!Number.isInteger(debounceMs) || debounceMs < 0) {
throw new Error(`Invalid hooks.debounceMs: ${debounceMs}. Must be a non-negative integer`);
}
const hooks: HooksConfig = {
enabled: hooksRaw?.enabled !== false,
indexInbound: hooksRaw?.indexInbound !== false,
indexOutbound: hooksRaw?.indexOutbound !== false,
indexAgentResponses: hooksRaw?.indexAgentResponses !== false,
batchSize,
debounceMs,
};
// Validate metric with proper type narrowing
const validMetrics = ["cosine", "euclidean", "dot"] as const;
const metricRaw = (cfg.metric as string | undefined) ?? DEFAULT_METRIC;
if (!validMetrics.includes(metricRaw as DistanceMetric)) {
throw new Error(`Invalid metric: ${metricRaw}. Must be cosine, euclidean, or dot`);
}
const metric = metricRaw as DistanceMetric;
// Parse SONA config
const sonaRaw = cfg.sona as Record<string, unknown> | undefined;
let sona: SONAConfig | undefined;
if (sonaRaw) {
assertAllowedKeys(
sonaRaw,
["enabled", "hiddenDim", "learningRate", "qualityThreshold", "backgroundIntervalMs"],
"sona config",
);
const hiddenDim = typeof sonaRaw.hiddenDim === "number" ? sonaRaw.hiddenDim : 256;
const learningRate = typeof sonaRaw.learningRate === "number" ? sonaRaw.learningRate : undefined;
const qualityThreshold = typeof sonaRaw.qualityThreshold === "number" ? sonaRaw.qualityThreshold : undefined;
const backgroundIntervalMs = typeof sonaRaw.backgroundIntervalMs === "number" ? sonaRaw.backgroundIntervalMs : undefined;
// Validate SONA numeric values
if (!Number.isInteger(hiddenDim) || hiddenDim <= 0) {
throw new Error(`Invalid sona.hiddenDim: ${hiddenDim}. Must be a positive integer`);
}
if (learningRate !== undefined && (learningRate < 0 || learningRate > 1)) {
throw new Error(`Invalid sona.learningRate: ${learningRate}. Must be between 0 and 1`);
}
if (qualityThreshold !== undefined && (qualityThreshold < 0 || qualityThreshold > 1)) {
throw new Error(`Invalid sona.qualityThreshold: ${qualityThreshold}. Must be between 0 and 1`);
}
if (backgroundIntervalMs !== undefined && (!Number.isInteger(backgroundIntervalMs) || backgroundIntervalMs <= 0)) {
throw new Error(`Invalid sona.backgroundIntervalMs: ${backgroundIntervalMs}. Must be a positive integer`);
}
sona = {
enabled: sonaRaw.enabled === true,
hiddenDim,
learningRate,
qualityThreshold,
backgroundIntervalMs,
};
}
return {
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
dimension: resolvedDimension,
metric,
embedding: {
provider: embeddingProvider as "openai" | "voyage" | "local",
apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : undefined,
model: embeddingModel,
baseUrl: embedding.baseUrl
? resolveEnvVars(embedding.baseUrl as string)
: undefined,
},
hooks,
sona,
};
},
uiHints: {
dbPath: {
label: "Database Path",
placeholder: "~/.clawdbot/memory/ruvector",
advanced: true,
help: "Directory for ruvector database storage",
},
dimension: {
label: "Vector Dimension",
placeholder: "1536",
advanced: true,
help: "Must match your embedding model output dimension",
},
metric: {
label: "Distance Metric",
placeholder: "cosine",
advanced: true,
help: "Similarity metric: cosine (default), euclidean, or dot",
},
"embedding.provider": {
label: "Embedding Provider",
placeholder: "openai",
help: "openai, voyage, or local",
},
"embedding.apiKey": {
label: "Embedding API Key",
sensitive: true,
placeholder: "sk-...",
help: "API key for embedding provider (or use ${ENV_VAR})",
},
"embedding.model": {
label: "Embedding Model",
placeholder: "text-embedding-3-small",
help: "Model to use for generating embeddings",
},
"embedding.baseUrl": {
label: "Base URL",
placeholder: "https://api.openai.com/v1",
advanced: true,
help: "Custom API base URL (for local/self-hosted)",
},
"hooks.enabled": {
label: "Enable Auto-Indexing",
help: "Automatically index messages via hooks",
},
"hooks.indexInbound": {
label: "Index Inbound Messages",
help: "Index incoming user messages",
},
"hooks.indexOutbound": {
label: "Index Outbound Messages",
help: "Index outgoing bot messages",
},
"hooks.indexAgentResponses": {
label: "Index Agent Responses",
help: "Index full agent conversation turns",
},
"hooks.batchSize": {
label: "Batch Size",
placeholder: "10",
advanced: true,
help: "Number of messages to batch before indexing",
},
"hooks.debounceMs": {
label: "Debounce (ms)",
placeholder: "500",
advanced: true,
help: "Delay before flushing partial batch",
},
"sona.enabled": {
label: "Enable SONA Self-Learning",
help: "Enable Self-Organizing Neural Architecture for adaptive learning",
},
"sona.hiddenDim": {
label: "Hidden Dimension",
placeholder: "256",
advanced: true,
help: "Hidden dimension for SONA neural architecture",
},
"sona.learningRate": {
label: "Learning Rate",
placeholder: "0.01",
advanced: true,
help: "Learning rate for SONA adaptation (0-1)",
},
"sona.qualityThreshold": {
label: "Quality Threshold",
placeholder: "0.5",
advanced: true,
help: "Minimum quality score for learning (0-1)",
},
"sona.backgroundIntervalMs": {
label: "Background Interval (ms)",
placeholder: "30000",
advanced: true,
help: "Interval for background learning cycles",
},
},
};