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 <noreply@anthropic.com>
This commit is contained in:
File 2026-01-25 23:34:34 +01:00 committed by krejcif
parent a801c7e721
commit ac20e30817
2 changed files with 152 additions and 30 deletions

View File

@ -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
{

View File

@ -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<string, unknown>).linkMessages === "function";
if (hasGraphSupport && relationships.length > 0) {
const graphDb = db as typeof db & {
linkMessages: (source: string, target: string, rel: string) => Promise<boolean>;
};
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("<sourceId>", "Source message ID")
.argument("<targetId>", "Target message ID")
.option("--relationship <rel>", "Relationship type", "RELATES_TO")
.action(async (sourceId, targetId, opts) => {
const hasGraphSupport = "linkMessages" in db && typeof (db as Record<string, unknown>).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<boolean> };
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")