@icex-labs/openclaw-memory-engine 5.1.0 → 5.1.1

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.
Files changed (2) hide show
  1. package/lib/graph.js +48 -41
  2. package/package.json +1 -1
package/lib/graph.js CHANGED
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * Knowledge Graph: triple store (subject, relation, object).
3
- * Enables relational queries like "who is George's doctor" or "what treats 荨麻疹".
4
- *
5
- * Storage: memory/graph.jsonl — one triple per line.
6
- * Auto-extraction: pattern-based extraction from archival insert content.
3
+ * v5.1: strict extraction only extract triples from clear, short, structured statements.
7
4
  */
8
5
 
9
6
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
@@ -12,7 +9,7 @@ import { graphPath } from "./paths.js";
12
9
 
13
10
  // ─── In-memory cache ───
14
11
 
15
- const cache = new Map(); // ws → { triples: [], loaded: false }
12
+ const cache = new Map();
16
13
 
17
14
  export function loadGraph(ws) {
18
15
  if (cache.has(ws) && cache.get(ws).loaded) return cache.get(ws).triples;
@@ -31,7 +28,6 @@ export function loadGraph(ws) {
31
28
  export function addTriple(ws, subject, relation, object, sourceId = null) {
32
29
  const triples = loadGraph(ws);
33
30
 
34
- // Deduplicate: same subject+relation+object
35
31
  const exists = triples.some(
36
32
  (t) => t.s === subject && t.r === relation && t.o === object,
37
33
  );
@@ -61,13 +57,6 @@ export function removeTriple(ws, tripleId) {
61
57
  return true;
62
58
  }
63
59
 
64
- /**
65
- * Query the graph from a starting entity, optionally filtering by relation.
66
- * @param {string} entity - starting node
67
- * @param {string} [relation] - optional relation filter
68
- * @param {number} [depth=2] - traversal depth
69
- * @returns {Array<{ path: string[], triple: object }>}
70
- */
71
60
  export function queryGraph(ws, entity, relation = null, depth = 2) {
72
61
  const triples = loadGraph(ws);
73
62
  const entityLower = entity.toLowerCase();
@@ -82,16 +71,14 @@ export function queryGraph(ws, entity, relation = null, depth = 2) {
82
71
 
83
72
  const currentLower = current.toLowerCase();
84
73
  for (const t of triples) {
85
- // Forward: subject matches
86
74
  if (t.s.toLowerCase() === currentLower) {
87
75
  if (relation && t.r.toLowerCase() !== relation.toLowerCase()) continue;
88
- results.push({ path: [...path, `--${t.r}-->`], node: t.o, triple: t });
76
+ results.push({ path: [...path, `--${t.r}-->`], node: t.o, triple: { id: t.id, s: t.s, r: t.r, o: t.o } });
89
77
  traverse(t.o, currentDepth + 1, [...path, `--${t.r}-->`, t.o]);
90
78
  }
91
- // Reverse: object matches
92
79
  if (t.o.toLowerCase() === currentLower) {
93
80
  if (relation && t.r.toLowerCase() !== relation.toLowerCase()) continue;
94
- results.push({ path: [...path, `<--${t.r}--`], node: t.s, triple: t });
81
+ results.push({ path: [...path, `<--${t.r}--`], node: t.s, triple: { id: t.id, s: t.s, r: t.r, o: t.o } });
95
82
  traverse(t.s, currentDepth + 1, [...path, `<--${t.r}--`, t.s]);
96
83
  }
97
84
  }
@@ -101,45 +88,65 @@ export function queryGraph(ws, entity, relation = null, depth = 2) {
101
88
  return results;
102
89
  }
103
90
 
104
- // ─── Auto-extraction patterns ───
91
+ // ─── Auto-extraction: strict patterns only ───
92
+
93
+ /**
94
+ * Validate that a string looks like a proper entity name (not a sentence fragment).
95
+ * - Must be short (≤25 chars)
96
+ * - Must not contain markdown, code, or sentence-like patterns
97
+ * - Must start with a capital letter or CJK character
98
+ */
99
+ function isValidEntity(s) {
100
+ if (!s || s.length > 25 || s.length < 2) return false;
101
+ // Reject markdown, code, URLs, punctuation-heavy strings
102
+ if (/[`*\[\]{}()|→←⚠#>]/.test(s)) return false;
103
+ // Reject if it looks like a sentence (has verb-like patterns or too many words)
104
+ if (s.split(/\s+/).length > 5) return false;
105
+ // Must start with uppercase, CJK, or known pattern
106
+ if (!/^[A-Z\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(s)) return false;
107
+ return true;
108
+ }
105
109
 
106
110
  const EXTRACTION_PATTERNS = [
107
- // "X's doctor is Y" / "X的医生是Y"
108
- { re: /(.+?)(?:'s|的)\s*(?:doctor|医生|主治医生)\s*(?:is|是|为)\s*(.+)/i, r: "has_doctor" },
109
- // "X lives in Y" / "X住在Y"
110
- { re: /(.+?)\s*(?:lives? in|住在|位于)\s*(.+)/i, r: "lives_in" },
111
- // "X works at Y" / "X在Y工作"
112
- { re: /(.+?)\s*(?:works? at|在(.+?)工作)/i, r: "works_at" },
113
- // "X has condition Y" / disease / 疾病
114
- { re: /(.+?)\s*(?:has|有|患有)\s*(?:chronic |慢性)?\s*(.+?(?:症|病|urticaria|condition|disease))/i, r: "has_condition" },
115
- // "X treated by/takes Y" / 用药
116
- { re: /(.+?)\s*(?:takes?|服用|用药|treated (?:by|with))\s*(.+)/i, r: "treated_by" },
111
+ // "X's doctor is Y"
112
+ { re: /^([A-Z]\w+)(?:'s)\s+doctor\s+is\s+(.+)$/i, r: "has_doctor" },
113
+ // "X's lawyer is Y"
114
+ { re: /^([A-Z]\w+)(?:'s)\s+(?:lawyer|attorney)\s+is\s+(.+)$/i, r: "has_lawyer" },
115
+ // "X lives in Y"
116
+ { re: /^([A-Z]\w+)\s+lives?\s+in\s+([A-Z][\w\s,]+)$/i, r: "lives_in" },
117
+ // "X works at Y"
118
+ { re: /^([A-Z]\w+)\s+works?\s+at\s+([A-Z][\w\s&.]+)$/i, r: "works_at" },
119
+ // "X owns Y" only match short clear statements
120
+ { re: /^([A-Z]\w+)\s+owns?\s+(?:a\s+)?([A-Z][\w\s]+)$/i, r: "owns" },
121
+ // "X drives a Y"
122
+ { re: /^([A-Z]\w+)\s+drives?\s+(?:a\s+)?([A-Z][\w\s]+)$/i, r: "owns" },
123
+ // "X has chronic Y" / "X has Y disease/condition"
124
+ { re: /^([A-Z]\w+)\s+has\s+(?:chronic\s+)?(\w[\w\s]*(?:disease|condition|syndrome|urticaria|diabetes|asthma))$/i, r: "has_condition" },
125
+ // "X takes Y" (medication)
126
+ { re: /^([A-Z]\w+)\s+takes?\s+([A-Z][\w\s]+\d+\s*mg)$/i, r: "treated_by" },
127
+ // "X attends Y"
128
+ { re: /^([A-Z]\w+)\s+attends?\s+([A-Z][\w\s]+)$/i, r: "attends" },
117
129
  // "X's wife/husband is Y"
118
- { re: /(.+?)(?:'s|的)\s*(wife|husband|妻子|丈夫|老婆|老公)\s*(?:is|是|为)\s*(.+)/i, r: "spouse", triple: true },
130
+ { re: /^([A-Z]\w+)(?:'s)\s+(?:wife|husband)\s+is\s+(.+)$/i, r: "spouse" },
119
131
  // "X's son/daughter is Y"
120
- { re: /(.+?)(?:'s|的)\s*(son|daughter|儿子|女儿)\s*(?:is|是|为)\s*(.+)/i, r: "has_child", triple: true },
121
- // "X costs/price Y" / 定价
122
- { re: /(.+?)\s*(?:costs?|定价|售价|price[ds]?\s*(?:at)?)\s*\$?([\d,.]+)/i, r: "price" },
123
- // "X's lawyer is Y"
124
- { re: /(.+?)(?:'s|的)\s*(?:lawyer|律师|attorney)\s*(?:is|是|为)\s*(.+)/i, r: "has_lawyer" },
125
- // "X owns Y" / 拥有
126
- { re: /(.+?)\s*(?:owns?|拥有|有一辆|drives?)\s*(.+)/i, r: "owns" },
127
- // "X studies/attends Y" / 就读
128
- { re: /(.+?)\s*(?:attends?|studies? at|就读于?|在(.+?)(?:上学|读书))/i, r: "attends" },
132
+ { re: /^([A-Z]\w+)(?:'s)\s+(?:son|daughter)\s+is\s+(.+)$/i, r: "has_child" },
129
133
  ];
130
134
 
131
135
  /**
132
136
  * Extract triples from a text string.
133
- * @returns {Array<{ s: string, r: string, o: string }>}
137
+ * Strict: only matches clean, short statements with proper entity names.
134
138
  */
135
139
  export function extractTriples(text) {
136
140
  const results = [];
141
+ // Only try extraction on short, clear text (not paragraphs)
142
+ if (text.length > 150) return results;
143
+
137
144
  for (const pat of EXTRACTION_PATTERNS) {
138
145
  const m = text.match(pat.re);
139
146
  if (m) {
140
147
  const s = (m[1] || "").trim();
141
- const o = (pat.triple ? m[3] : m[2] || "").trim();
142
- if (s && o && s.length < 50 && o.length < 80) {
148
+ const o = (m[2] || "").trim();
149
+ if (isValidEntity(s) && o.length >= 2 && o.length <= 40) {
143
150
  results.push({ s, r: pat.r, o });
144
151
  }
145
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icex-labs/openclaw-memory-engine",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "MemGPT-style hierarchical memory plugin for OpenClaw — core memory block + archival storage with semantic search",
5
5
  "type": "module",
6
6
  "main": "index.js",