@tpsdev-ai/openclaw-flair 0.4.1 → 0.5.6
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/README.md +1 -1
- package/index.ts +182 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
package/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ interface FlairMemoryConfig {
|
|
|
34
34
|
maxBootstrapTokens?: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const DEFAULT_URL = "http://127.0.0.1:
|
|
37
|
+
const DEFAULT_URL = "http://127.0.0.1:19926";
|
|
38
38
|
const DEFAULT_MAX_RECALL = 5;
|
|
39
39
|
const DEFAULT_MAX_BOOTSTRAP_TOKENS = 4000;
|
|
40
40
|
|
|
@@ -107,6 +107,130 @@ function excerptForCapture(text: string, maxChars = 500): string {
|
|
|
107
107
|
return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// ─── Entity detection ────────────────────────────────────────────────────────
|
|
111
|
+
// Passive entity extraction from conversation content. No setup wizards,
|
|
112
|
+
// no "tell me about yourself" — Flair learns from natural conversation.
|
|
113
|
+
|
|
114
|
+
interface DetectedEntity {
|
|
115
|
+
name: string;
|
|
116
|
+
kind: "person" | "project" | "service" | "org" | "concept";
|
|
117
|
+
confidence: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface DetectedRelationship {
|
|
121
|
+
subject: string;
|
|
122
|
+
predicate: string;
|
|
123
|
+
object: string;
|
|
124
|
+
confidence: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Person detection: "Nathan said", "ask @Kern", "my name is X", "X is the founder"
|
|
128
|
+
const PERSON_PATTERNS = [
|
|
129
|
+
/\b([A-Z][a-z]{2,})\s+(?:said|asked|mentioned|decided|approved|rejected|thinks|wants|needs|prefers)\b/g,
|
|
130
|
+
/\b(?:ask|ping|tell|check with|talk to)\s+(?:@)?([A-Z][a-z]{2,})\b/g,
|
|
131
|
+
/\b(?:my name is|i'm|call me)\s+([A-Z][a-z]{2,})\b/ig,
|
|
132
|
+
/\b([A-Z][a-z]{2,})\s+(?:is the|is our|is a|was the|was our)\s+(\w+(?:\s+\w+)?)\b/g,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Project/service detection: repo references, "the X project", service names
|
|
136
|
+
const PROJECT_PATTERNS = [
|
|
137
|
+
/\b(?:tpsdev-ai|github\.com)\/([a-z0-9-]+)\b/g,
|
|
138
|
+
/\b(?:the|our)\s+([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\s+(?:project|repo|service|system|app|tool|plugin)\b/g,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// Relationship detection: "X manages Y", "X owns Y", "X depends on Y"
|
|
142
|
+
const RELATIONSHIP_PATTERNS = [
|
|
143
|
+
{ re: /\b([A-Z][a-z]{2,})\s+(?:manages|leads|runs|owns)\s+(?:the\s+)?([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b/g, predicate: "manages" },
|
|
144
|
+
{ re: /\b([A-Z][a-z]{2,})\s+(?:works on|is working on|maintains)\s+(?:the\s+)?([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)\b/g, predicate: "works_on" },
|
|
145
|
+
{ re: /\b([A-Z][a-z]{2,})\s+(?:reviews|reviewed)\s+(?:the\s+)?([A-Z][a-z]+(?:'s)?(?:\s\w+)?)\b/g, predicate: "reviews" },
|
|
146
|
+
{ re: /\b([A-Z][a-z]+)\s+(?:depends on|requires|needs)\s+([A-Z][a-z]+)\b/g, predicate: "depends_on" },
|
|
147
|
+
{ re: /\b([A-Z][a-z]+)\s+(?:replaces|supersedes)\s+([A-Z][a-z]+)\b/g, predicate: "replaces" },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Common words that look like names but aren't
|
|
151
|
+
const ENTITY_STOPWORDS = new Set([
|
|
152
|
+
"the", "this", "that", "with", "from", "into", "also", "just", "here",
|
|
153
|
+
"there", "what", "when", "where", "which", "while", "should", "would",
|
|
154
|
+
"could", "will", "does", "have", "been", "being", "make", "made",
|
|
155
|
+
"take", "taken", "like", "look", "good", "well", "much", "many",
|
|
156
|
+
"some", "each", "every", "both", "other", "such", "only", "same",
|
|
157
|
+
"than", "then", "now", "how", "all", "any", "few", "most", "very",
|
|
158
|
+
"after", "before", "between", "under", "over", "through", "during",
|
|
159
|
+
"about", "against", "above", "below", "off", "down", "out",
|
|
160
|
+
"let", "set", "get", "put", "run", "use", "try", "see", "new",
|
|
161
|
+
"old", "big", "end", "way", "day", "man", "did", "got", "had",
|
|
162
|
+
"yes", "not", "but", "for", "are", "was", "can", "may", "one",
|
|
163
|
+
"two", "its", "his", "her", "our", "has", "him", "her", "per",
|
|
164
|
+
"via", "bug", "fix", "add", "api", "url", "cli", "tcp", "ssh",
|
|
165
|
+
"keep", "next", "last", "best", "sure", "okay", "done", "want",
|
|
166
|
+
"need", "know", "think", "start", "stop", "check", "update",
|
|
167
|
+
"instead", "currently", "actually", "already", "however", "because",
|
|
168
|
+
"since", "until", "still", "right", "first", "great", "sounds",
|
|
169
|
+
"interesting", "important", "note", "issue", "pull", "push",
|
|
170
|
+
"merge", "branch", "commit", "deploy", "build", "test", "spec",
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
function isValidEntity(name: string): boolean {
|
|
174
|
+
if (name.length < 3 || name.length > 30) return false;
|
|
175
|
+
if (ENTITY_STOPWORDS.has(name.toLowerCase())) return false;
|
|
176
|
+
if (/^\d+$/.test(name)) return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function detectEntities(text: string): DetectedEntity[] {
|
|
181
|
+
const entities = new Map<string, DetectedEntity>();
|
|
182
|
+
|
|
183
|
+
for (const pattern of PERSON_PATTERNS) {
|
|
184
|
+
pattern.lastIndex = 0;
|
|
185
|
+
let match: RegExpExecArray | null;
|
|
186
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
187
|
+
const name = match[1];
|
|
188
|
+
if (!isValidEntity(name)) continue;
|
|
189
|
+
const key = name.toLowerCase();
|
|
190
|
+
if (!entities.has(key) || entities.get(key)!.confidence < 0.7) {
|
|
191
|
+
entities.set(key, { name, kind: "person", confidence: 0.7 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const pattern of PROJECT_PATTERNS) {
|
|
197
|
+
pattern.lastIndex = 0;
|
|
198
|
+
let match: RegExpExecArray | null;
|
|
199
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
200
|
+
const name = match[1];
|
|
201
|
+
if (!isValidEntity(name)) continue;
|
|
202
|
+
const key = name.toLowerCase();
|
|
203
|
+
if (!entities.has(key)) {
|
|
204
|
+
entities.set(key, { name, kind: "project", confidence: 0.8 });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [...entities.values()];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function detectRelationships(text: string): DetectedRelationship[] {
|
|
213
|
+
const relationships: DetectedRelationship[] = [];
|
|
214
|
+
|
|
215
|
+
for (const { re, predicate } of RELATIONSHIP_PATTERNS) {
|
|
216
|
+
re.lastIndex = 0;
|
|
217
|
+
let match: RegExpExecArray | null;
|
|
218
|
+
while ((match = re.exec(text)) !== null) {
|
|
219
|
+
const subject = match[1];
|
|
220
|
+
const object = match[2];
|
|
221
|
+
if (!isValidEntity(subject) || !isValidEntity(object)) continue;
|
|
222
|
+
relationships.push({
|
|
223
|
+
subject: subject.toLowerCase(),
|
|
224
|
+
predicate,
|
|
225
|
+
object: object.toLowerCase(),
|
|
226
|
+
confidence: 0.6,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return relationships;
|
|
232
|
+
}
|
|
233
|
+
|
|
110
234
|
// ─── Plugin export ────────────────────────────────────────────────────────────
|
|
111
235
|
|
|
112
236
|
export default {
|
|
@@ -358,16 +482,67 @@ export default {
|
|
|
358
482
|
const client = getCurrentClient();
|
|
359
483
|
const messages = (event.messages ?? []) as Array<{ role: string; content?: string }>;
|
|
360
484
|
let stored = 0;
|
|
485
|
+
const allEntities = new Map<string, DetectedEntity>();
|
|
486
|
+
const allRelationships: DetectedRelationship[] = [];
|
|
487
|
+
|
|
361
488
|
for (const msg of messages) {
|
|
362
489
|
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
363
490
|
const text = typeof msg.content === "string" ? msg.content : "";
|
|
364
|
-
if (!text ||
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
stored
|
|
368
|
-
|
|
491
|
+
if (!text || text.length < MIN_CAPTURE_LENGTH) continue;
|
|
492
|
+
|
|
493
|
+
// Traditional trigger-based capture
|
|
494
|
+
if (shouldCapture(text) && stored < 3) {
|
|
495
|
+
const excerpt = excerptForCapture(text);
|
|
496
|
+
// Tag with detected subject if available
|
|
497
|
+
const entities = detectEntities(text);
|
|
498
|
+
const subject = entities.length > 0 ? entities[0].name.toLowerCase() : undefined;
|
|
499
|
+
await client.memory.write(excerpt, {
|
|
500
|
+
type: "session",
|
|
501
|
+
tags: ["auto-captured"],
|
|
502
|
+
subject,
|
|
503
|
+
});
|
|
504
|
+
stored++;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Entity detection — accumulate across all messages
|
|
508
|
+
for (const entity of detectEntities(text)) {
|
|
509
|
+
const key = entity.name.toLowerCase();
|
|
510
|
+
const existing = allEntities.get(key);
|
|
511
|
+
if (!existing || existing.confidence < entity.confidence) {
|
|
512
|
+
allEntities.set(key, entity);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Relationship detection
|
|
517
|
+
for (const rel of detectRelationships(text)) {
|
|
518
|
+
allRelationships.push(rel);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Store detected relationships via Flair's Relationship API
|
|
523
|
+
let relStored = 0;
|
|
524
|
+
for (const rel of allRelationships) {
|
|
525
|
+
if (relStored >= 5) break; // cap per session
|
|
526
|
+
try {
|
|
527
|
+
await client.request("PUT", `/Relationship/${Date.now()}-${relStored}`, {
|
|
528
|
+
subject: rel.subject,
|
|
529
|
+
predicate: rel.predicate,
|
|
530
|
+
object: rel.object,
|
|
531
|
+
confidence: rel.confidence,
|
|
532
|
+
source: "auto-detected",
|
|
533
|
+
});
|
|
534
|
+
relStored++;
|
|
535
|
+
} catch {
|
|
536
|
+
// best effort — don't fail the session over relationship storage
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const total = stored + relStored;
|
|
541
|
+
if (total > 0) {
|
|
542
|
+
api.logger.info(
|
|
543
|
+
`openclaw-flair: auto-captured ${stored} memories, ${relStored} relationships, ${allEntities.size} entities detected`
|
|
544
|
+
);
|
|
369
545
|
}
|
|
370
|
-
if (stored > 0) api.logger.info(`openclaw-flair: auto-captured ${stored} memories`);
|
|
371
546
|
} catch (err: any) {
|
|
372
547
|
api.logger.warn(`openclaw-flair: auto-capture failed: ${err.message}`);
|
|
373
548
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/openclaw-flair",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenClaw memory plugin for Flair
|
|
3
|
+
"version": "0.5.6",
|
|
4
|
+
"description": "OpenClaw memory plugin for Flair — agent identity and semantic memory",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"exports": {
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"./index.ts"
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
37
40
|
"peerDependencies": {
|
|
38
41
|
"openclaw": ">=2026.3.7"
|
|
39
42
|
},
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
"node": ">=22"
|
|
42
45
|
},
|
|
43
46
|
"dependencies": {
|
|
44
|
-
"@sinclair/typebox": "
|
|
45
|
-
"@tpsdev-ai/flair-client": "
|
|
47
|
+
"@sinclair/typebox": "0.34.48",
|
|
48
|
+
"@tpsdev-ai/flair-client": "0.5.0"
|
|
46
49
|
}
|
|
47
|
-
}
|
|
50
|
+
}
|