cozo-memory 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -361,6 +361,124 @@ class MemoryServer {
361
361
  }
362
362
  return result.rows.map((r) => ({ entity_id: String(r[0]), community_id: String(r[1]) }));
363
363
  }
364
+ async summarizeCommunities(args) {
365
+ await this.initPromise;
366
+ const minSize = Math.max(2, Math.floor(args?.min_community_size ?? 3));
367
+ const model = args?.model ?? "demyagent-4b-i1:Q6_K";
368
+ console.error("[GraphRAG] Recomputing communities before summarization...");
369
+ const commResult = await this.recomputeCommunities();
370
+ if (commResult.length === 0) {
371
+ return { status: "no_data", generated_summaries: 0 };
372
+ }
373
+ // Group entities by community
374
+ const byCommunity = new Map();
375
+ for (const row of commResult) {
376
+ const arr = byCommunity.get(row.community_id) ?? [];
377
+ arr.push(row.entity_id);
378
+ byCommunity.set(row.community_id, arr);
379
+ }
380
+ let generatedSummaries = 0;
381
+ const results = [];
382
+ for (const [communityId, entityIds] of byCommunity.entries()) {
383
+ if (entityIds.length < minSize)
384
+ continue;
385
+ console.error(`[GraphRAG] Summarizing community ${communityId} with ${entityIds.length} members...`);
386
+ // Fetch details for all entities in this community
387
+ const entityInfos = [];
388
+ for (const eId of entityIds) {
389
+ try {
390
+ // get entity
391
+ const entRes = await this.db.run('?[name, type] := *entity{id: $id, name, type, @ "NOW"}', { id: eId });
392
+ if (entRes.rows.length > 0) {
393
+ const name = entRes.rows[0][0];
394
+ const type = entRes.rows[0][1];
395
+ // get top observations for this entity
396
+ const obsRes = await this.db.run('?[text, created_at] := *observation{entity_id: $id, text, created_at, @ "NOW"} :limit 3', { id: eId });
397
+ const obs = obsRes.rows.map((r) => r[0]);
398
+ entityInfos.push(`- ${name} (${type})` + (obs.length > 0 ? `\n Notes: ${obs.join("; ")}` : ""));
399
+ }
400
+ }
401
+ catch (e) {
402
+ console.warn(`[GraphRAG] Error fetching info for ${eId}: ${e.message}`);
403
+ }
404
+ }
405
+ if (entityInfos.length === 0)
406
+ continue;
407
+ const systemPrompt = "You are an expert analyst. Summarize the following community of related entities into a single, cohesive 'Community Report'. Identify the overarching themes, topics, and connections between these entities. Respond ONLY with the finalized summary text.";
408
+ const userPrompt = `Community Size: ${entityIds.length}\n\nMembers & Notes:\n${entityInfos.join("\n")}`;
409
+ let summaryText = "";
410
+ try {
411
+ const ollamaMod = await import("ollama");
412
+ const ollamaClient = ollamaMod?.default ?? ollamaMod;
413
+ const response = await ollamaClient.chat({
414
+ model,
415
+ messages: [
416
+ { role: "system", content: systemPrompt },
417
+ { role: "user", content: userPrompt },
418
+ ],
419
+ });
420
+ summaryText = response?.message?.content?.trim?.() ?? "";
421
+ }
422
+ catch (e) {
423
+ console.warn(`[GraphRAG] Ollama error for community ${communityId}: ${e.message}`);
424
+ }
425
+ if (!summaryText || summaryText === "") {
426
+ console.warn(`[GraphRAG] Could not generate summary for community ${communityId}, skipping.`);
427
+ continue;
428
+ }
429
+ // Create new CommunitySummary entity
430
+ const nowIso = new Date().toISOString();
431
+ const summaryName = `Community Summary ${communityId.slice(0, 8)} (${nowIso.slice(0, 10)})`;
432
+ const sumEntity = await this.createEntity({
433
+ name: summaryName,
434
+ type: "CommunitySummary",
435
+ metadata: {
436
+ graphrag: {
437
+ kind: "community_summary",
438
+ community_id: communityId,
439
+ member_count: entityIds.length,
440
+ member_ids: entityIds,
441
+ model,
442
+ summarized_at: nowIso
443
+ }
444
+ }
445
+ });
446
+ const summaryEntityId = sumEntity?.id;
447
+ if (summaryEntityId) {
448
+ await this.addObservation({
449
+ entity_id: summaryEntityId,
450
+ text: summaryText,
451
+ metadata: {
452
+ graphrag: {
453
+ kind: "community_summary",
454
+ community_id: communityId,
455
+ }
456
+ }
457
+ });
458
+ // Link the summary to all members
459
+ for (const memberId of entityIds) {
460
+ await this.createRelation({
461
+ from_id: summaryEntityId,
462
+ to_id: memberId,
463
+ relation_type: "summary_of",
464
+ strength: 1.0,
465
+ metadata: { community_id: communityId }
466
+ });
467
+ }
468
+ generatedSummaries++;
469
+ results.push({
470
+ community_id: communityId,
471
+ summary_entity_id: summaryEntityId,
472
+ member_count: entityIds.length
473
+ });
474
+ }
475
+ }
476
+ return {
477
+ status: "completed",
478
+ generated_summaries: generatedSummaries,
479
+ results
480
+ };
481
+ }
364
482
  async recomputeBetweennessCentrality() {
365
483
  await this.initPromise;
366
484
  const edgeCheckRes = await this.db.run(`?[from_id] := *relationship{from_id, @ "NOW"} :limit 1`);
@@ -1482,6 +1600,7 @@ ids[id] <- $ids
1482
1600
  async reflectMemory(args) {
1483
1601
  await this.initPromise;
1484
1602
  const model = args.model ?? "demyagent-4b-i1:Q6_K";
1603
+ const mode = args.mode ?? "summary";
1485
1604
  const targetEntityId = args.entity_id;
1486
1605
  let entitiesToReflect = [];
1487
1606
  if (targetEntityId) {
@@ -1504,47 +1623,114 @@ ids[id] <- $ids
1504
1623
  }
1505
1624
  const results = [];
1506
1625
  for (const entity of entitiesToReflect) {
1507
- const obsRes = await this.db.run('?[text, ts] := *observation{entity_id: $id, text, created_at, @ "NOW"}, ts = to_int(created_at) :order ts', {
1508
- id: entity.id,
1509
- });
1510
- if (obsRes.rows.length < 2) {
1511
- results.push({ entity_id: entity.id, status: "skipped", reason: "Too few observations for reflection" });
1512
- continue;
1513
- }
1514
- const observations = obsRes.rows.map((r) => `- [${new Date(Number(r[1]) / 1000).toISOString()}] ${r[0]}`);
1515
- const systemPrompt = `You are an analytical memory module. Analyze the following observations about an entity.
1626
+ if (mode === "summary") {
1627
+ const obsRes = await this.db.run('?[text, ts] := *observation{entity_id: $id, text, created_at, @ "NOW"}, ts = to_int(created_at) :order ts', {
1628
+ id: entity.id,
1629
+ });
1630
+ if (obsRes.rows.length < 2) {
1631
+ results.push({ entity_id: entity.id, status: "skipped", reason: "Too few observations for reflection" });
1632
+ continue;
1633
+ }
1634
+ const observations = obsRes.rows.map((r) => `- [${new Date(Number(r[1]) / 1000).toISOString()}] ${r[0]}`);
1635
+ const systemPrompt = `You are an analytical memory module. Analyze the following observations about an entity.
1516
1636
  Look for contradictions, temporal developments, behavioral patterns, or deeper insights.
1517
1637
  Formulate a concise reflection (max. 3-4 sentences) that helps the user understand the current state or evolution.
1518
1638
  If there are contradictory statements, name them explicitly.
1519
1639
  If no special patterns are recognizable, answer with "No new insights".`;
1520
- const userPrompt = `Entity: ${entity.name} (${entity.type})\n\nObservations:\n${observations.join("\n")}`;
1521
- let reflectionText;
1522
- try {
1523
- const ollamaMod = await import("ollama");
1524
- const ollamaClient = ollamaMod?.default ?? ollamaMod;
1525
- const response = await ollamaClient.chat({
1526
- model,
1527
- messages: [
1528
- { role: "system", content: systemPrompt },
1529
- { role: "user", content: userPrompt },
1530
- ],
1531
- });
1532
- reflectionText = response?.message?.content?.trim?.() ?? "";
1533
- }
1534
- catch (e) {
1535
- console.error(`[Reflect] Ollama error for ${entity.name}:`, e);
1536
- reflectionText = "";
1537
- }
1538
- if (reflectionText && reflectionText !== "No new insights" && !reflectionText.includes("No new insights")) {
1539
- await this.addObservation({
1540
- entity_id: entity.id,
1541
- text: `Reflexive insight: ${reflectionText}`,
1542
- metadata: { kind: "reflection", model, generated_at: Date.now() },
1543
- });
1544
- results.push({ entity_id: entity.id, status: "reflected", insight: reflectionText });
1545
- }
1546
- else {
1547
- results.push({ entity_id: entity.id, status: "no_insight_found" });
1640
+ const userPrompt = `Entity: ${entity.name} (${entity.type})\n\nObservations:\n${observations.join("\n")}`;
1641
+ let reflectionText;
1642
+ try {
1643
+ const ollamaMod = await import("ollama");
1644
+ const ollamaClient = ollamaMod?.default ?? ollamaMod;
1645
+ const response = await ollamaClient.chat({
1646
+ model,
1647
+ messages: [
1648
+ { role: "system", content: systemPrompt },
1649
+ { role: "user", content: userPrompt },
1650
+ ],
1651
+ });
1652
+ reflectionText = response?.message?.content?.trim?.() ?? "";
1653
+ }
1654
+ catch (e) {
1655
+ console.error(`[Reflect] Ollama error for ${entity.name}:`, e);
1656
+ reflectionText = "";
1657
+ }
1658
+ if (reflectionText && reflectionText !== "No new insights" && !reflectionText.includes("No new insights")) {
1659
+ await this.addObservation({
1660
+ entity_id: entity.id,
1661
+ text: `Reflexive insight: ${reflectionText}`,
1662
+ metadata: { kind: "reflection", model, generated_at: Date.now() },
1663
+ });
1664
+ results.push({ entity_id: entity.id, status: "reflected", insight: reflectionText });
1665
+ }
1666
+ else {
1667
+ results.push({ entity_id: entity.id, status: "no_insight_found" });
1668
+ }
1669
+ }
1670
+ else if (mode === "discovery") {
1671
+ // Discovery Mode: Find and validate new relationships
1672
+ const candidates = await this.inferenceEngine.getRefinementCandidates(entity.id);
1673
+ const discoveryResults = [];
1674
+ const suggestions = [];
1675
+ for (const candidate of candidates) {
1676
+ // Check if relationship already exists
1677
+ const existing = await this.db.run(`
1678
+ ?[count(from_id)] := *relationship{from_id, to_id, relation_type, @ "NOW"},
1679
+ from_id = $from, to_id = $to, relation_type = $rel
1680
+ `, { from: candidate.from_id, to: candidate.to_id, rel: candidate.relation_type });
1681
+ if (Number(existing.rows[0][0]) > 0)
1682
+ continue;
1683
+ // Load target entity details for context
1684
+ const targetRes = await this.db.run('?[name, type] := *entity{id, name, type, @ "NOW"}, id = $id', { id: candidate.to_id });
1685
+ const targetName = String(targetRes.rows[0][0]);
1686
+ const targetType = String(targetRes.rows[0][1]);
1687
+ const validatePrompt = `You are a Knowledge Graph specialized AI. Evaluate if the following suggested relationship is logically sound based on the reason provided.
1688
+ Source Entity: ${entity.name} (${entity.type})
1689
+ Target Entity: ${targetName} (${targetType})
1690
+ Suggested Relationship: ${candidate.relation_type}
1691
+ Reason: ${candidate.reason}
1692
+
1693
+ Respond with a JSON object:
1694
+ {
1695
+ "confidence": <0.0 to 1.0>,
1696
+ "reasoning": "<short explanation>",
1697
+ "verdict": "create" | "suggest" | "reject"
1698
+ }
1699
+ Assign "create" if confidence > 0.8, "suggest" if confidence > 0.5, else "reject".`;
1700
+ try {
1701
+ const ollamaMod = await import("ollama");
1702
+ const ollamaClient = ollamaMod?.default ?? ollamaMod;
1703
+ const response = await ollamaClient.chat({
1704
+ model,
1705
+ messages: [{ role: "user", content: validatePrompt }],
1706
+ format: "json"
1707
+ });
1708
+ const validation = JSON.parse(response?.message?.content ?? "{}");
1709
+ if (validation.verdict === "create" && validation.confidence > 0.8) {
1710
+ await this.createRelation({
1711
+ from_id: candidate.from_id,
1712
+ to_id: candidate.to_id,
1713
+ relation_type: candidate.relation_type,
1714
+ strength: validation.confidence,
1715
+ metadata: { source: "reflection", reasoning: validation.reasoning, model }
1716
+ });
1717
+ discoveryResults.push({ target: targetName, type: candidate.relation_type, status: "created" });
1718
+ }
1719
+ else if (validation.verdict === "suggest" || validation.confidence > 0.5) {
1720
+ suggestions.push({
1721
+ from_id: candidate.from_id,
1722
+ to_id: candidate.to_id,
1723
+ relation_type: candidate.relation_type,
1724
+ confidence: validation.confidence,
1725
+ reason: validation.reasoning
1726
+ });
1727
+ }
1728
+ }
1729
+ catch (e) {
1730
+ console.error(`[Discovery] Validation failed for ${targetName}:`, e);
1731
+ }
1732
+ }
1733
+ results.push({ entity_id: entity.id, status: "discovery_completed", created: discoveryResults, suggestions });
1548
1734
  }
1549
1735
  }
1550
1736
  return { status: "completed", results };
@@ -2173,6 +2359,64 @@ ids[id] <- $ids
2173
2359
  defaultEntityType: args.defaultEntityType
2174
2360
  });
2175
2361
  }
2362
+ async editUserProfile(args) {
2363
+ try {
2364
+ const current = await this.db.run('?[name, type, metadata] := *entity{id: $id, name, type, metadata, @ "NOW"}', { id: exports.USER_ENTITY_ID });
2365
+ if (current.rows.length === 0) {
2366
+ return { error: "User profile not found. Initialize it first." };
2367
+ }
2368
+ if (args.name || args.type || args.metadata) {
2369
+ const updateResult = await this.updateEntity({
2370
+ id: exports.USER_ENTITY_ID,
2371
+ name: args.name,
2372
+ type: args.type,
2373
+ metadata: args.metadata
2374
+ });
2375
+ if (updateResult.error) {
2376
+ return updateResult;
2377
+ }
2378
+ }
2379
+ if (args.clear_observations) {
2380
+ const existingObs = await this.db.run('?[id] := *observation{id, entity_id: $eid, @ "NOW"}', { eid: exports.USER_ENTITY_ID });
2381
+ for (const row of existingObs.rows) {
2382
+ await this.db.run('?[id, entity_id, text, embedding, metadata, created_at] := *observation{id, entity_id, text, embedding, metadata, created_at, @ "NOW"}, id = $id :delete observation {id, entity_id, text, embedding, metadata, created_at}', { id: row[0] });
2383
+ }
2384
+ }
2385
+ if (args.observations && args.observations.length > 0) {
2386
+ for (const obs of args.observations) {
2387
+ await this.addObservation({
2388
+ entity_id: exports.USER_ENTITY_ID,
2389
+ text: obs.text,
2390
+ metadata: { ...obs.metadata, kind: "user_preference" },
2391
+ deduplicate: true
2392
+ });
2393
+ }
2394
+ }
2395
+ const updated = await this.db.run('?[name, type, metadata] := *entity{id: $id, name, type, metadata, @ "NOW"}', { id: exports.USER_ENTITY_ID });
2396
+ const observations = await this.db.run('?[id, text, metadata] := *observation{id, entity_id: $eid, text, metadata, @ "NOW"}', { eid: exports.USER_ENTITY_ID });
2397
+ return {
2398
+ status: "User profile updated",
2399
+ profile: {
2400
+ id: exports.USER_ENTITY_ID,
2401
+ name: updated.rows[0][0],
2402
+ type: updated.rows[0][1],
2403
+ metadata: updated.rows[0][2],
2404
+ observations: observations.rows.map((r) => ({
2405
+ id: r[0],
2406
+ text: r[1],
2407
+ metadata: r[2]
2408
+ }))
2409
+ }
2410
+ };
2411
+ }
2412
+ catch (error) {
2413
+ console.error("[UserProfile] Error editing user profile:", error);
2414
+ return {
2415
+ error: "Failed to edit user profile",
2416
+ message: error.message || String(error)
2417
+ };
2418
+ }
2419
+ }
2176
2420
  registerTools() {
2177
2421
  const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
2178
2422
  const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
@@ -2443,12 +2687,17 @@ Validation: Invalid syntax or missing columns in inference rules will result in
2443
2687
  max_depth: zod_1.z.number().min(1).max(5).optional().default(3).describe("Maximum walking depth"),
2444
2688
  limit: zod_1.z.number().optional().default(5).describe("Number of results"),
2445
2689
  }),
2690
+ zod_1.z.object({
2691
+ action: zod_1.z.literal("agentic_search"),
2692
+ query: zod_1.z.string().describe("Context query for agentic routing"),
2693
+ limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
2694
+ }),
2446
2695
  ]);
2447
2696
  const QueryMemoryParameters = zod_1.z.object({
2448
2697
  action: zod_1.z
2449
- .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking"])
2698
+ .enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search"])
2450
2699
  .describe("Action (determines which fields are required)"),
2451
- query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking"),
2700
+ query: zod_1.z.string().optional().describe("Required for search/advancedSearch/context/graph_rag/graph_walking/agentic_search"),
2452
2701
  limit: zod_1.z.number().optional().describe("Only for search/advancedSearch/graph_rag/graph_walking"),
2453
2702
  filters: zod_1.z.any().optional().describe("Only for advancedSearch"),
2454
2703
  graphConstraints: zod_1.z.any().optional().describe("Only for advancedSearch"),
@@ -2476,8 +2725,9 @@ Supported actions:
2476
2725
  - 'history': Retrieve historical evolution of an entity. Params: { entity_id: string }.
2477
2726
  - 'graph_rag': Graph-based reasoning (Hybrid RAG). Finds semantic vector seeds first, then expands via graph traversals. Ideal for multi-hop reasoning. Params: { query: string, max_depth?: number, limit?: number }.
2478
2727
  - 'graph_walking': Recursive semantic graph search. Starts at vector seeds or an entity and follows relationships to other semantically relevant entities. Params: { query: string, start_entity_id?: string, max_depth?: number, limit?: number }.
2728
+ - 'agentic_search': Auto-Routing Search. Uses local LLM to analyze intent and routes the query automatically to the best strategy (Vector, Graph, or Community Summaries). Params: { query: string, limit?: number }.
2479
2729
 
2480
- Notes: 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
2730
+ Notes: 'agentic_search' is the most powerful and adaptable, 'context' is ideal for exploratory questions. 'search' and 'advancedSearch' are better for targeted fact retrieval.`,
2481
2731
  parameters: QueryMemoryParameters,
2482
2732
  execute: async (args) => {
2483
2733
  await this.initPromise;
@@ -2631,6 +2881,16 @@ Notes: 'context' is ideal for exploratory questions. 'search' and 'advancedSearc
2631
2881
  };
2632
2882
  return JSON.stringify(context);
2633
2883
  }
2884
+ if (input.action === "agentic_search") {
2885
+ if (!input.query || input.query.trim().length === 0) {
2886
+ return JSON.stringify({ error: "Search query must not be empty." });
2887
+ }
2888
+ const results = await this.hybridSearch.agenticRetrieve({
2889
+ query: input.query,
2890
+ limit: input.limit,
2891
+ });
2892
+ return JSON.stringify(results);
2893
+ }
2634
2894
  if (input.action === "graph_rag") {
2635
2895
  if (!input.query || input.query.trim().length === 0) {
2636
2896
  return JSON.stringify({ error: "Search query must not be empty." });
@@ -3107,15 +3367,21 @@ Supported actions:
3107
3367
  action: zod_1.z.literal("reflect"),
3108
3368
  entity_id: zod_1.z.string().optional().describe("Optional entity ID for targeted reflection"),
3109
3369
  model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
3370
+ mode: zod_1.z.enum(["summary", "discovery"]).optional().default("summary").describe("Reflection mode: 'summary' for insights, 'discovery' for new links"),
3110
3371
  }),
3111
3372
  zod_1.z.object({
3112
3373
  action: zod_1.z.literal("clear_memory"),
3113
3374
  confirm: zod_1.z.boolean().describe("Must be true to confirm deletion"),
3114
3375
  }),
3376
+ zod_1.z.object({
3377
+ action: zod_1.z.literal("summarize_communities"),
3378
+ model: zod_1.z.string().optional().default("demyagent-4b-i1:Q6_K"),
3379
+ min_community_size: zod_1.z.number().min(2).max(100).optional().default(3),
3380
+ }),
3115
3381
  ]);
3116
3382
  const ManageSystemParameters = zod_1.z.object({
3117
3383
  action: zod_1.z
3118
- .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
3384
+ .enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory", "summarize_communities"])
3119
3385
  .describe("Action (determines which fields are required)"),
3120
3386
  format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
3121
3387
  includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
@@ -3134,8 +3400,10 @@ Supported actions:
3134
3400
  older_than_days: zod_1.z.number().optional().describe("Optional for cleanup"),
3135
3401
  max_observations: zod_1.z.number().optional().describe("Optional for cleanup"),
3136
3402
  min_entity_degree: zod_1.z.number().optional().describe("Optional for cleanup"),
3137
- model: zod_1.z.string().optional().describe("Optional for cleanup/reflect"),
3403
+ model: zod_1.z.string().optional().describe("Optional for cleanup/reflect/summarize_communities"),
3138
3404
  entity_id: zod_1.z.string().optional().describe("Optional for reflect"),
3405
+ min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
3406
+ mode: zod_1.z.enum(["summary", "discovery"]).optional().describe("Optional for reflect"),
3139
3407
  });
3140
3408
  this.mcp.addTool({
3141
3409
  name: "manage_system",
@@ -3159,7 +3427,8 @@ Supported actions:
3159
3427
  * With confirm=false: Dry-Run (shows candidates).
3160
3428
  * With confirm=true: Merges old/isolated fragments using LLM (Executive Summary) and removes noise.
3161
3429
  - 'reflect': Reflection service. Analyzes memory for contradictions and insights. Params: { entity_id?: string, model?: string }.
3162
- - 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.`,
3430
+ - 'clear_memory': Resets the entire database. Params: { confirm: boolean (must be true) }.
3431
+ - 'summarize_communities': Hierarchical GraphRAG. Generates summaries for entity clusters. Params: { model?: string, min_community_size?: number }.`,
3163
3432
  parameters: ManageSystemParameters,
3164
3433
  execute: async (args) => {
3165
3434
  await this.initPromise;
@@ -3352,6 +3621,7 @@ Supported actions:
3352
3621
  const result = await this.reflectMemory({
3353
3622
  entity_id: input.entity_id,
3354
3623
  model: input.model,
3624
+ mode: input.mode,
3355
3625
  });
3356
3626
  return JSON.stringify(result);
3357
3627
  }
@@ -3359,6 +3629,18 @@ Supported actions:
3359
3629
  return JSON.stringify({ error: error.message || "Error during reflection" });
3360
3630
  }
3361
3631
  }
3632
+ if (input.action === "summarize_communities") {
3633
+ try {
3634
+ const result = await this.summarizeCommunities({
3635
+ model: input.model,
3636
+ min_community_size: input.min_community_size,
3637
+ });
3638
+ return JSON.stringify(result);
3639
+ }
3640
+ catch (error) {
3641
+ return JSON.stringify({ error: error.message || "Error during summarize_communities" });
3642
+ }
3643
+ }
3362
3644
  if (input.action === "clear_memory") {
3363
3645
  if (!input.confirm) {
3364
3646
  return JSON.stringify({ error: "Deletion not confirmed. Set 'confirm' to true." });
@@ -3378,6 +3660,42 @@ Supported actions:
3378
3660
  return JSON.stringify({ error: "Unknown action" });
3379
3661
  },
3380
3662
  });
3663
+ // User Profile Management Tool
3664
+ this.mcp.addTool({
3665
+ name: "edit_user_profile",
3666
+ description: `Direct management of the global user profile ('global_user_profile').
3667
+ This tool allows manual editing of user preferences, work style, and profile metadata.
3668
+
3669
+ The user profile is automatically boosted in all searches (50% score boost) and used for personalization.
3670
+
3671
+ Parameters:
3672
+ - name?: string - Update the profile name (default: "The User")
3673
+ - type?: string - Update the profile type (default: "User")
3674
+ - metadata?: object - Update or merge profile metadata
3675
+ - observations?: Array<{ text: string, metadata?: object }> - Add new preference observations
3676
+ - clear_observations?: boolean - Remove all existing observations before adding new ones
3677
+
3678
+ Examples:
3679
+ - Add preferences: { observations: [{ text: "Prefers TypeScript over JavaScript" }] }
3680
+ - Update metadata: { metadata: { timezone: "Europe/Berlin", language: "de" } }
3681
+ - Reset and set new preferences: { clear_observations: true, observations: [{ text: "New preference" }] }
3682
+
3683
+ Note: Use 'mutate_memory' with action='add_observation' and entity_id='global_user_profile' for implicit preference updates.`,
3684
+ parameters: zod_1.z.object({
3685
+ name: zod_1.z.string().optional().describe("New name for the user profile"),
3686
+ type: zod_1.z.string().optional().describe("New type for the user profile"),
3687
+ metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional().describe("Metadata to merge with existing metadata"),
3688
+ observations: zod_1.z.array(zod_1.z.object({
3689
+ text: zod_1.z.string().describe("Preference or work style description"),
3690
+ metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional().describe("Optional metadata for this observation")
3691
+ })).optional().describe("New observations to add to the profile"),
3692
+ clear_observations: zod_1.z.boolean().optional().default(false).describe("Clear all existing observations before adding new ones")
3693
+ }),
3694
+ execute: async (args) => {
3695
+ await this.initPromise;
3696
+ return JSON.stringify(await this.editUserProfile(args));
3697
+ }
3698
+ });
3381
3699
  }
3382
3700
  async start() {
3383
3701
  await this.mcp.start({ transportType: "stdio" });
@@ -28,6 +28,13 @@ class InferenceEngine {
28
28
  results.push(...custom);
29
29
  return results;
30
30
  }
31
+ /**
32
+ * Aggregates potential links from all strategies for the "discovery" mode of reflection.
33
+ */
34
+ async getRefinementCandidates(entityId) {
35
+ // This is essentially the same as inferRelations but explicitly for refinement
36
+ return this.inferRelations(entityId);
37
+ }
31
38
  async inferImplicitRelations(entityId) {
32
39
  const expertise = await this.findTransitiveExpertise(entityId);
33
40
  const custom = await this.applyCustomRules(entityId);
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ const uuid_1 = require("uuid");
5
+ async function run() {
6
+ const server = new index_1.MemoryServer();
7
+ await server.initPromise;
8
+ console.log("Directly inserting old entity and observations via CozoDB...");
9
+ const fortyDaysAgo = Math.floor(Date.now() - 40 * 24 * 60 * 60 * 1000) * 1000;
10
+ const entityId = (0, uuid_1.v4)();
11
+ const name = "Very Old Project";
12
+ const type = "Project";
13
+ const metadata = { purpose: "testing janitor" };
14
+ const zeroVec = new Array(1024).fill(0);
15
+ try {
16
+ await server.db.run(`
17
+ ?[id, name, type, embedding, name_embedding, metadata, created_at] <- [[$id, $name, $type, $embedding, $name_embedding, $metadata, [$fortyDaysAgo, true]]]
18
+ :insert entity {id, name, type, embedding, name_embedding, metadata, created_at}
19
+ `, {
20
+ id: entityId,
21
+ name,
22
+ type,
23
+ embedding: zeroVec,
24
+ name_embedding: zeroVec,
25
+ metadata,
26
+ fortyDaysAgo
27
+ });
28
+ console.log("Old entity inserted: " + entityId);
29
+ const obsTexts = [
30
+ "This is a really old architecture note.",
31
+ "We decided to use subversion for version control.",
32
+ "The server is a physical machine in the basement.",
33
+ "We wrote our own ORM from scratch.",
34
+ "Deployment takes 3 days and a lot of manual steps."
35
+ ];
36
+ for (const text of obsTexts) {
37
+ const obsId = (0, uuid_1.v4)();
38
+ await server.db.run(`
39
+ ?[id, entity_id, text, embedding, metadata, created_at] <- [[$id, $entity_id, $text, $embedding, $metadata, [$fortyDaysAgo, true]]]
40
+ :insert observation {id, entity_id, text, embedding, metadata, created_at}
41
+ `, {
42
+ id: obsId,
43
+ entity_id: entityId,
44
+ text,
45
+ embedding: zeroVec,
46
+ metadata: {},
47
+ fortyDaysAgo
48
+ });
49
+ console.log("Old observation inserted: " + obsId);
50
+ }
51
+ console.log("Successfully inserted old data!");
52
+ }
53
+ catch (e) {
54
+ console.error("DB error:", e.message);
55
+ }
56
+ process.exit(0);
57
+ }
58
+ run().catch(console.error);
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ async function run() {
5
+ const server = new index_1.MemoryServer();
6
+ await server.initPromise;
7
+ console.log("Testing Agentic Retrieval Routing Logic...");
8
+ // Expose the protected hybridSearch for testing (TypeScript hack)
9
+ const hybridSearch = server.hybridSearch;
10
+ const queries = [
11
+ { text: "Welches Datenbank-System nutzt das Backend Project B?", expected: ["vector_search", "hybrid"] },
12
+ { text: "Wer arbeitet alles mit ReactJS oder was nutzt ReactJS?", expected: ["graph_walk", "hybrid"] },
13
+ { text: "Wie ist der generelle Status aller Frontend-Projekte?", expected: ["community_summary"] }
14
+ ];
15
+ for (const q of queries) {
16
+ console.log(`\n\n--- Query: "${q.text}" ---`);
17
+ console.log(`Expected Route: ${q.expected.join(" or ")}`);
18
+ const results = await hybridSearch.agenticRetrieve({ query: q.text, limit: 1 });
19
+ if (results.length > 0) {
20
+ console.log(`-> LLM Routed to: ${results[0].metadata?.agentic_routing}`);
21
+ }
22
+ else {
23
+ console.log(`\nNo results found. LLM might have routed to an empty strategy.`);
24
+ }
25
+ }
26
+ process.exit(0);
27
+ }
28
+ run().catch(console.error);