From ac20e3081789544a696af05f6039c27f82211e65 Mon Sep 17 00:00:00 2001 From: File Date: Sun, 25 Jan 2026 23:34:34 +0100 Subject: [PATCH] fix(memory-ruvector): register ruvector_learn tool and add link CLI Documentation validation fixes: - Register ruvector_learn tool in local mode (was defined but not registered) - Add 'clawdbot ruvector link' CLI command (was documented but missing) - Remove non-existent graph config section from docs Co-Authored-By: Claude Opus 4.5 --- docs/plugins/memory-ruvector.md | 32 +----- extensions/memory-ruvector/index.ts | 150 ++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 30 deletions(-) diff --git a/docs/plugins/memory-ruvector.md b/docs/plugins/memory-ruvector.md index b722a5fb2..3dafedaf7 100644 --- a/docs/plugins/memory-ruvector.md +++ b/docs/plugins/memory-ruvector.md @@ -269,39 +269,11 @@ clawdbot ruvector sona-stats Query message relationships using Neo4j-compatible Cypher syntax. This enables finding conversation threads, reply chains, and topic relationships. -### Configuration - -```json5 -{ - plugins: { - entries: { - "memory-ruvector": { - enabled: true, - config: { - embedding: { - provider: "openai", - apiKey: "${OPENAI_API_KEY}" - }, - graph: { - enabled: true, // Enable graph features - autoLink: true, // Auto-create edges for replies/threads - maxDepth: 5 // Maximum traversal depth - } - } - } - } - } -} -``` +Graph features are automatically available when the ruvector library is built with graph extension support. No additional configuration is needed. ### Linking messages -**Automatic linking** (when `autoLink: true`): -- Messages in the same conversation are linked with `IN_CONVERSATION` -- Reply messages are linked with `REPLIED_BY` -- Messages from the same user are linked with `FROM_USER` - -**Manual linking** via the `ruvector_graph` tool: +**Manual linking** via the `ruvector_graph` tool or CLI: ```json5 { diff --git a/extensions/memory-ruvector/index.ts b/extensions/memory-ruvector/index.ts index c716808d8..0e61883fd 100644 --- a/extensions/memory-ruvector/index.ts +++ b/extensions/memory-ruvector/index.ts @@ -18,6 +18,7 @@ import { createRuvectorFeedbackTool, createRuvectorGraphTool, createRuvectorRecallTool, + createRuvectorLearnTool, } from "./tool.js"; import { ruvectorConfigSchema, type RuvectorConfig } from "./config.js"; import { createDatabase } from "./db.js"; @@ -682,6 +683,123 @@ function registerLocalMode(api: ClawdbotPluginApi, config: RuvectorConfig): void { name: "ruvector_recall", optional: true }, ); + // ruvector_learn tool (manual knowledge injection) + api.registerTool( + { + name: "ruvector_learn", + label: "Manual Knowledge Learning", + description: + "Explicitly learn and index new knowledge with optional graph relationships. " + + "Use this to inject important facts, decisions, or preferences into the memory system.", + parameters: { + type: "object", + properties: { + content: { type: "string", description: "The content/knowledge to learn" }, + category: { + type: "string", + enum: ["preference", "fact", "decision", "entity", "other"], + description: "Category for the knowledge (default: fact)", + }, + importance: { + type: "number", + description: "Importance score from 0-1 (default: 0.5)", + }, + relationships: { + type: "array", + items: { type: "string" }, + description: "Related document IDs to link to", + }, + }, + required: ["content"], + }, + async execute(_toolCallId, params, ctx) { + const { + content, + category = "fact", + importance = 0.5, + relationships = [], + } = params as { + content: string; + category?: string; + importance?: number; + relationships?: string[]; + }; + + try { + const vector = await embeddings.embed(content); + + // Check for duplicates + const existing = await db.search(vector, { limit: 1, minScore: 0.95 }); + if (existing.length > 0) { + return { + content: [ + { + type: "text", + text: `Similar knowledge already exists: "${existing[0].document.content.slice(0, 100)}..."`, + }, + ], + details: { indexed: false, duplicate: true, existingId: existing[0].document.id }, + }; + } + + const validCategories = ["preference", "fact", "decision", "entity", "other"]; + const validCategory = validCategories.includes(category) ? category : "fact"; + const clampedImportance = Math.max(0, Math.min(1, importance)); + + const id = await db.insert({ + content, + vector, + direction: "outbound", + channel: "ruvector_learn", + sessionKey: ctx?.sessionKey, + agentId: ctx?.agentId, + timestamp: Date.now(), + category: validCategory, + importance: clampedImportance, + }); + + // Create graph edges if relationships provided + let edgesCreated = 0; + const hasGraphSupport = + "linkMessages" in db && + typeof (db as Record).linkMessages === "function"; + + if (hasGraphSupport && relationships.length > 0) { + const graphDb = db as typeof db & { + linkMessages: (source: string, target: string, rel: string) => Promise; + }; + for (const targetId of relationships) { + try { + const created = await graphDb.linkMessages(id, targetId, "RELATED_TO"); + if (created) edgesCreated++; + } catch { + // Skip failed edges + } + } + } + + return { + content: [ + { + type: "text", + text: `Learned: "${content.slice(0, 100)}${content.length > 100 ? "..." : ""}" [${validCategory}] with ${edgesCreated} relationship(s)`, + }, + ], + details: { indexed: true, id, category: validCategory, importance: clampedImportance, edges: edgesCreated }, + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + api.logger.warn(`ruvector_learn: learning failed: ${message}`); + return { + content: [{ type: "text", text: `Learning failed: ${message}` }], + details: { indexed: false, error: message }, + }; + } + }, + }, + { name: "ruvector_learn", optional: true }, + ); + // ========================================================================= // Register CLI Commands // ========================================================================= @@ -824,6 +942,38 @@ function registerLocalMode(api: ClawdbotPluginApi, config: RuvectorConfig): void } }); + // GNN link command + rv.command("link") + .description("Create a relationship between two messages") + .argument("", "Source message ID") + .argument("", "Target message ID") + .option("--relationship ", "Relationship type", "RELATES_TO") + .action(async (sourceId, targetId, opts) => { + const hasGraphSupport = "linkMessages" in db && typeof (db as Record).linkMessages === "function"; + + if (!hasGraphSupport) { + console.log("GNN graph features not available."); + console.log("Requires ruvector with graph extension enabled."); + return; + } + + const relationship = opts.relationship || "RELATES_TO"; + const graphDb = db as typeof db & { linkMessages: (source: string, target: string, rel: string) => Promise }; + + try { + const created = await graphDb.linkMessages(sourceId, targetId, relationship); + if (created) { + console.log(`Created link: ${sourceId} -[${relationship}]-> ${targetId}`); + } else { + console.log(`Link already exists or could not be created.`); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error(`Failed to create link: ${message}`); + process.exitCode = 1; + } + }); + // Pattern export command (P3 Advanced Features) rv.command("export-patterns") .description("Export learned patterns to a JSON file")