@origintrail-official/dkg-node-ui 0.0.1-dev.1773506972.23bf9c0
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/LICENSE +201 -0
- package/README.md +49 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +805 -0
- package/dist/api.js.map +1 -0
- package/dist/chat-assistant.d.ts +68 -0
- package/dist/chat-assistant.d.ts.map +1 -0
- package/dist/chat-assistant.js +663 -0
- package/dist/chat-assistant.js.map +1 -0
- package/dist/chat-memory.d.ts +171 -0
- package/dist/chat-memory.d.ts.map +1 -0
- package/dist/chat-memory.js +985 -0
- package/dist/chat-memory.js.map +1 -0
- package/dist/chat-persistence-queue.d.ts +67 -0
- package/dist/chat-persistence-queue.d.ts.map +1 -0
- package/dist/chat-persistence-queue.js +245 -0
- package/dist/chat-persistence-queue.js.map +1 -0
- package/dist/db.d.ts +402 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +887 -0
- package/dist/db.js.map +1 -0
- package/dist/gelf-push-worker.d.ts +67 -0
- package/dist/gelf-push-worker.d.ts.map +1 -0
- package/dist/gelf-push-worker.js +147 -0
- package/dist/gelf-push-worker.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/capability-resolver.d.ts +3 -0
- package/dist/llm/capability-resolver.d.ts.map +1 -0
- package/dist/llm/capability-resolver.js +21 -0
- package/dist/llm/capability-resolver.js.map +1 -0
- package/dist/llm/client.d.ts +23 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +91 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/provider-adapter.d.ts +16 -0
- package/dist/llm/provider-adapter.d.ts.map +1 -0
- package/dist/llm/provider-adapter.js +199 -0
- package/dist/llm/provider-adapter.js.map +1 -0
- package/dist/llm/types.d.ts +64 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/llm/types.js +2 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/metrics-collector.d.ts +36 -0
- package/dist/metrics-collector.d.ts.map +1 -0
- package/dist/metrics-collector.js +155 -0
- package/dist/metrics-collector.js.map +1 -0
- package/dist/operation-tracker.d.ts +43 -0
- package/dist/operation-tracker.d.ts.map +1 -0
- package/dist/operation-tracker.js +195 -0
- package/dist/operation-tracker.js.map +1 -0
- package/dist/structured-logger.d.ts +16 -0
- package/dist/structured-logger.d.ts.map +1 -0
- package/dist/structured-logger.js +41 -0
- package/dist/structured-logger.js.map +1 -0
- package/dist/telemetry.d.ts +35 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +45 -0
- package/dist/telemetry.js.map +1 -0
- package/dist-ui/assets/3d-force-graph-nMUNmvtB.js +964 -0
- package/dist-ui/assets/AgentHub-XKCM9uYQ.js +65 -0
- package/dist-ui/assets/AppHost-DoLIi89g.js +1 -0
- package/dist-ui/assets/Apps-Cc8HfqfD.js +1 -0
- package/dist-ui/assets/Dashboard-D5q6MK78.js +2 -0
- package/dist-ui/assets/Explorer-B80RVksc.js +64 -0
- package/dist-ui/assets/N3Parser-Q_-1ZY5E.js +7 -0
- package/dist-ui/assets/Settings-CG7-7GM-.js +71 -0
- package/dist-ui/assets/hooks-BLTFNmyP.js +1 -0
- package/dist-ui/assets/index-8_35CUX2.js +192 -0
- package/dist-ui/assets/index-CKZq_ZB-.css +1 -0
- package/dist-ui/assets/index-DH-l6lM0.js +76 -0
- package/dist-ui/assets/jsonld-32FQRO67-DhbO8O6B.js +2 -0
- package/dist-ui/assets/jsonld-BFI4wECl.js +62 -0
- package/dist-ui/assets/ntriples-ZWBY2WET-nIpilpjf.js +1 -0
- package/dist-ui/assets/ordinal-DIohFSkg.js +1 -0
- package/dist-ui/assets/renderer-3d-2EVDZII7-DsxBsJvs.js +2 -0
- package/dist-ui/assets/three.module-uCjFke6H.js +4019 -0
- package/dist-ui/assets/turtle-JJPK7LJ5-zezDJZEp.js +1 -0
- package/dist-ui/favicon.png +0 -0
- package/dist-ui/index.html +14 -0
- package/package.json +58 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { isSafeIri } from '@origintrail-official/dkg-core';
|
|
2
|
+
import { LlmClient } from './llm/client.js';
|
|
3
|
+
export const IMPORT_SOURCES = ['claude', 'chatgpt', 'gemini', 'other'];
|
|
4
|
+
const MEMORY_PARANET = 'agent-memory';
|
|
5
|
+
const OPENCLAW_LOCAL_SESSION_ID = 'openclaw:dkg-ui';
|
|
6
|
+
const CHAT_NS = 'urn:dkg:chat:';
|
|
7
|
+
const MEMORY_NS = 'urn:dkg:memory:';
|
|
8
|
+
const SCHEMA = 'http://schema.org/';
|
|
9
|
+
const DKG_ONT = 'http://dkg.io/ontology/';
|
|
10
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
11
|
+
const XSD_DATETIME = 'http://www.w3.org/2001/XMLSchema#dateTime';
|
|
12
|
+
const OPENCLAW_LOCAL_SESSION_URI = `${CHAT_NS}session:${OPENCLAW_LOCAL_SESSION_ID}`;
|
|
13
|
+
function stripRdfLiteral(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return '';
|
|
16
|
+
const typed = value.match(/^"([\s\S]*)"(?:\^\^<[^>]+>)?(?:@[a-z-]+)?$/);
|
|
17
|
+
if (typed)
|
|
18
|
+
return typed[1];
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function parseRdfInt(value) {
|
|
22
|
+
if (!value)
|
|
23
|
+
return 0;
|
|
24
|
+
const match = value.match(/^"(\d+)"/);
|
|
25
|
+
if (match)
|
|
26
|
+
return parseInt(match[1], 10);
|
|
27
|
+
const n = parseInt(value, 10);
|
|
28
|
+
return isNaN(n) ? 0 : n;
|
|
29
|
+
}
|
|
30
|
+
function sumBindingValues(bindings, key) {
|
|
31
|
+
if (!bindings?.length)
|
|
32
|
+
return 0;
|
|
33
|
+
return bindings.reduce((sum, b) => sum + parseRdfInt(b[key] ?? '0'), 0);
|
|
34
|
+
}
|
|
35
|
+
function buildSessionRootPattern(sessionUri) {
|
|
36
|
+
const clauses = [
|
|
37
|
+
`{ <${sessionUri}> ?sessionP ?sessionO . BIND(<${sessionUri}> AS ?s) }`,
|
|
38
|
+
`{ ?s <${SCHEMA}isPartOf> <${sessionUri}> }`,
|
|
39
|
+
`{
|
|
40
|
+
?msg <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
41
|
+
?msg <${DKG_ONT}usedTool> ?s .
|
|
42
|
+
}`,
|
|
43
|
+
`{
|
|
44
|
+
?msg <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
45
|
+
?s <${DKG_ONT}mentionedIn> ?msg .
|
|
46
|
+
}`,
|
|
47
|
+
`{ ?s <${DKG_ONT}extractedFrom> <${sessionUri}> }`,
|
|
48
|
+
];
|
|
49
|
+
if (sessionUri === OPENCLAW_LOCAL_SESSION_URI) {
|
|
50
|
+
clauses.push(`{ ?s <${RDF_TYPE}> <${DKG_ONT}ImportedMemory> }`, `{ ?s <${RDF_TYPE}> <${DKG_ONT}MemoryImport> }`, `{
|
|
51
|
+
?s <${DKG_ONT}extractedFrom> ?batch .
|
|
52
|
+
?batch <${RDF_TYPE}> <${DKG_ONT}MemoryImport> .
|
|
53
|
+
}`);
|
|
54
|
+
}
|
|
55
|
+
return `{
|
|
56
|
+
${clauses.join('\n UNION ')}
|
|
57
|
+
}`;
|
|
58
|
+
}
|
|
59
|
+
const MENTION_EXTRACTION_PROMPT = `Extract named entities mentioned in the following text. Output ONLY a JSON array of objects with "name" and "type" fields.
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
- "name" is the canonical label (proper casing, e.g. "Porsche", "Bitcoin", "Berlin")
|
|
63
|
+
- "type" is one of: Person, Organization, Place, Product, Event, Technology, Concept
|
|
64
|
+
- Only extract concrete, specific entities — skip vague words like "car", "company", "city"
|
|
65
|
+
- If no entities found, output: []
|
|
66
|
+
- Do NOT wrap in markdown code fences
|
|
67
|
+
|
|
68
|
+
Example output:
|
|
69
|
+
[{"name":"Porsche","type":"Organization"},{"name":"Berlin","type":"Place"}]
|
|
70
|
+
|
|
71
|
+
Text:`;
|
|
72
|
+
const KG_EXTRACTION_PROMPT = `Extract structured knowledge from the following conversation exchange. Output ONLY valid N-Triples (one per line, no blank lines). Use URIs for subjects/predicates and proper RDF syntax.
|
|
73
|
+
|
|
74
|
+
Rules:
|
|
75
|
+
- Subject URIs: use urn:dkg:entity:{slug} where slug is a lowercase-kebab-case identifier for the entity
|
|
76
|
+
- Use schema.org predicates where possible (e.g. <http://schema.org/name>, <http://schema.org/description>, <http://schema.org/knows>, <http://schema.org/about>)
|
|
77
|
+
- Use <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> for types
|
|
78
|
+
- String literals use "value" syntax; typed literals use "value"^^<datatype>
|
|
79
|
+
- Each triple must end with " ."
|
|
80
|
+
- Only extract factual, meaningful entities and relationships — skip conversational filler
|
|
81
|
+
- If no meaningful knowledge can be extracted, output exactly: NONE
|
|
82
|
+
|
|
83
|
+
Example output:
|
|
84
|
+
<urn:dkg:entity:alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
|
|
85
|
+
<urn:dkg:entity:alice> <http://schema.org/name> "Alice Johnson" .
|
|
86
|
+
<urn:dkg:entity:alice> <http://schema.org/worksFor> <urn:dkg:entity:acme-corp> .
|
|
87
|
+
|
|
88
|
+
Conversation:`;
|
|
89
|
+
const MEMORY_PARSE_PROMPT = `Parse the following exported AI memories into individual structured items. Each memory is a discrete fact, preference, or piece of context the user previously shared with an AI assistant.
|
|
90
|
+
|
|
91
|
+
Output ONLY a valid JSON array. Each item should have:
|
|
92
|
+
- "text": the memory content as a clear sentence
|
|
93
|
+
- "category": one of "preference", "fact", "context", "instruction", "relationship"
|
|
94
|
+
|
|
95
|
+
Rules:
|
|
96
|
+
- Split compound memories into separate items when they contain distinct facts
|
|
97
|
+
- Normalize formatting: remove bullet markers, numbering, markdown artifacts
|
|
98
|
+
- Preserve the original meaning faithfully
|
|
99
|
+
- Skip metadata lines like "Here are your memories:" or "Last updated:"
|
|
100
|
+
- If no valid memories can be extracted, output: []
|
|
101
|
+
- Do NOT wrap in markdown code fences
|
|
102
|
+
|
|
103
|
+
Example input:
|
|
104
|
+
"- Prefers dark mode in all apps
|
|
105
|
+
- Works at Acme Corp as a senior engineer
|
|
106
|
+
- Has a dog named Max"
|
|
107
|
+
|
|
108
|
+
Example output:
|
|
109
|
+
[{"text":"Prefers dark mode in all apps","category":"preference"},{"text":"Works at Acme Corp as a senior engineer","category":"fact"},{"text":"Has a dog named Max","category":"fact"}]
|
|
110
|
+
|
|
111
|
+
Memories to parse:`;
|
|
112
|
+
const MEMORY_KG_PROMPT = `Extract structured knowledge from the following personal memory items. These are facts/preferences a user previously stored with an AI assistant. Output ONLY valid N-Triples (one per line).
|
|
113
|
+
|
|
114
|
+
Rules:
|
|
115
|
+
- Subject URIs: use urn:dkg:entity:{slug} where slug is a lowercase-kebab-case identifier
|
|
116
|
+
- Use schema.org predicates where possible (e.g. <http://schema.org/name>, <http://schema.org/description>, <http://schema.org/knows>)
|
|
117
|
+
- Use <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> for types
|
|
118
|
+
- String literals use "value" syntax
|
|
119
|
+
- Each triple must end with " ."
|
|
120
|
+
- Focus on extracting entities (people, organizations, tools, places) and their relationships
|
|
121
|
+
- If no meaningful knowledge can be extracted, output exactly: NONE
|
|
122
|
+
|
|
123
|
+
Memory items:`;
|
|
124
|
+
const SEMANTIC_RECALL_SYSTEM = `You are a SPARQL query generator. Output ONLY a valid SPARQL SELECT query.
|
|
125
|
+
|
|
126
|
+
IMPORTANT: Always use full URI syntax with angle brackets — NEVER use prefix shortcuts.
|
|
127
|
+
|
|
128
|
+
Available patterns:
|
|
129
|
+
- Sessions: ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Conversation>
|
|
130
|
+
- Messages: ?m <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Message>
|
|
131
|
+
- ?m <http://schema.org/text> ?text
|
|
132
|
+
- ?m <http://schema.org/author> <urn:dkg:chat:actor:user> or <urn:dkg:chat:actor:agent>
|
|
133
|
+
- ?m <http://schema.org/isPartOf> ?session
|
|
134
|
+
- ?m <http://schema.org/dateCreated> ?ts
|
|
135
|
+
- Entities: ?e <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?type . FILTER(STRSTARTS(STR(?e), "urn:dkg:entity:"))
|
|
136
|
+
- ?e <http://schema.org/name> ?name
|
|
137
|
+
- ?e <http://schema.org/worksFor> ?org
|
|
138
|
+
- Memory links: ?m <http://dkg.io/ontology/contains> ?entity
|
|
139
|
+
- Entity mentions: ?entity <http://dkg.io/ontology/mentionedIn> ?message
|
|
140
|
+
|
|
141
|
+
Always include LIMIT (default 50). Use FILTER with regex or CONTAINS for text search.`;
|
|
142
|
+
export class ChatMemoryManager {
|
|
143
|
+
tools;
|
|
144
|
+
llmConfig;
|
|
145
|
+
initialized = false;
|
|
146
|
+
knownSessions = new Set();
|
|
147
|
+
llmClient = new LlmClient();
|
|
148
|
+
constructor(tools, llmConfig) {
|
|
149
|
+
this.tools = tools;
|
|
150
|
+
this.llmConfig = llmConfig;
|
|
151
|
+
}
|
|
152
|
+
get paranetId() {
|
|
153
|
+
return MEMORY_PARANET;
|
|
154
|
+
}
|
|
155
|
+
updateConfig(llmConfig) {
|
|
156
|
+
this.llmConfig = llmConfig;
|
|
157
|
+
}
|
|
158
|
+
async ensureInitialized() {
|
|
159
|
+
if (this.initialized)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
const paranets = await this.tools.listParanets();
|
|
163
|
+
const exists = paranets.some((p) => p.id === MEMORY_PARANET || p.paranetId === MEMORY_PARANET);
|
|
164
|
+
if (!exists) {
|
|
165
|
+
await this.tools.createParanet({
|
|
166
|
+
id: MEMORY_PARANET,
|
|
167
|
+
name: 'Agent Memory',
|
|
168
|
+
description: 'Local private memory for agent chat conversations and extracted knowledge.',
|
|
169
|
+
private: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (!err.message?.includes('already exists'))
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
// Pre-populate known sessions so subsequent writes to existing
|
|
178
|
+
// sessions don't re-declare the session entity (DKG Rule 4).
|
|
179
|
+
try {
|
|
180
|
+
const result = await this.tools.query(`SELECT ?sid WHERE { ?s <${RDF_TYPE}> <${SCHEMA}Conversation> . ?s <${DKG_ONT}sessionId> ?sid }`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
181
|
+
for (const b of result.bindings ?? []) {
|
|
182
|
+
const sid = stripRdfLiteral(b.sid ?? '');
|
|
183
|
+
if (sid)
|
|
184
|
+
this.knownSessions.add(sid);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch { /* best-effort */ }
|
|
188
|
+
this.initialized = true;
|
|
189
|
+
}
|
|
190
|
+
async storeChatExchange(sessionId, userMessage, assistantReply, toolCalls, opts) {
|
|
191
|
+
await this.ensureInitialized();
|
|
192
|
+
const userTs = new Date();
|
|
193
|
+
const agentTs = new Date(userTs.getTime() + 1);
|
|
194
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
195
|
+
const userMsgId = crypto.randomUUID().slice(0, 8);
|
|
196
|
+
const assistantMsgId = crypto.randomUUID().slice(0, 8);
|
|
197
|
+
const userMsgUri = `${CHAT_NS}msg:${userMsgId}`;
|
|
198
|
+
const assistantMsgUri = `${CHAT_NS}msg:${assistantMsgId}`;
|
|
199
|
+
const turnId = opts?.turnId?.trim();
|
|
200
|
+
const persistenceState = opts?.persistenceState ?? 'stored';
|
|
201
|
+
const turnUri = turnId ? `${CHAT_NS}turn:${turnId}` : undefined;
|
|
202
|
+
const isNewSession = !this.knownSessions.has(sessionId);
|
|
203
|
+
const quads = [];
|
|
204
|
+
// Only declare the session entity on the first exchange to avoid
|
|
205
|
+
// DKG Rule 4 (entity exclusivity) rejecting subsequent writes.
|
|
206
|
+
if (isNewSession) {
|
|
207
|
+
quads.push({ subject: sessionUri, predicate: RDF_TYPE, object: `${SCHEMA}Conversation`, graph: '' }, { subject: sessionUri, predicate: `${DKG_ONT}sessionId`, object: `"${sessionId}"`, graph: '' });
|
|
208
|
+
}
|
|
209
|
+
quads.push({ subject: userMsgUri, predicate: RDF_TYPE, object: `${SCHEMA}Message`, graph: '' }, { subject: userMsgUri, predicate: `${SCHEMA}isPartOf`, object: sessionUri, graph: '' }, { subject: userMsgUri, predicate: `${SCHEMA}author`, object: `${CHAT_NS}actor:user`, graph: '' }, { subject: userMsgUri, predicate: `${SCHEMA}dateCreated`, object: `"${userTs.toISOString()}"^^<${XSD_DATETIME}>`, graph: '' }, { subject: userMsgUri, predicate: `${SCHEMA}text`, object: JSON.stringify(userMessage), graph: '' }, { subject: assistantMsgUri, predicate: RDF_TYPE, object: `${SCHEMA}Message`, graph: '' }, { subject: assistantMsgUri, predicate: `${SCHEMA}isPartOf`, object: sessionUri, graph: '' }, { subject: assistantMsgUri, predicate: `${SCHEMA}author`, object: `${CHAT_NS}actor:agent`, graph: '' }, { subject: assistantMsgUri, predicate: `${SCHEMA}dateCreated`, object: `"${agentTs.toISOString()}"^^<${XSD_DATETIME}>`, graph: '' }, { subject: assistantMsgUri, predicate: `${SCHEMA}text`, object: JSON.stringify(assistantReply), graph: '' }, { subject: assistantMsgUri, predicate: `${DKG_ONT}replyTo`, object: userMsgUri, graph: '' });
|
|
210
|
+
if (turnId && turnUri) {
|
|
211
|
+
quads.push({ subject: turnUri, predicate: RDF_TYPE, object: `${DKG_ONT}ChatTurn`, graph: '' }, { subject: turnUri, predicate: `${SCHEMA}isPartOf`, object: sessionUri, graph: '' }, { subject: turnUri, predicate: `${DKG_ONT}turnId`, object: JSON.stringify(turnId), graph: '' }, { subject: turnUri, predicate: `${SCHEMA}dateCreated`, object: `"${userTs.toISOString()}"^^<${XSD_DATETIME}>`, graph: '' }, { subject: turnUri, predicate: `${DKG_ONT}hasUserMessage`, object: userMsgUri, graph: '' }, { subject: turnUri, predicate: `${DKG_ONT}hasAssistantMessage`, object: assistantMsgUri, graph: '' }, { subject: turnUri, predicate: `${DKG_ONT}persistenceState`, object: JSON.stringify(persistenceState), graph: '' }, { subject: userMsgUri, predicate: `${DKG_ONT}turnId`, object: JSON.stringify(turnId), graph: '' }, { subject: assistantMsgUri, predicate: `${DKG_ONT}turnId`, object: JSON.stringify(turnId), graph: '' });
|
|
212
|
+
}
|
|
213
|
+
if (toolCalls?.length) {
|
|
214
|
+
for (const tc of toolCalls) {
|
|
215
|
+
const tcUri = `${CHAT_NS}tool:${crypto.randomUUID().slice(0, 8)}`;
|
|
216
|
+
quads.push({ subject: tcUri, predicate: RDF_TYPE, object: `${DKG_ONT}ToolInvocation`, graph: '' }, { subject: tcUri, predicate: `${DKG_ONT}toolName`, object: `"${tc.name}"`, graph: '' }, { subject: tcUri, predicate: `${DKG_ONT}toolArgs`, object: JSON.stringify(JSON.stringify(tc.args)), graph: '' }, { subject: assistantMsgUri, predicate: `${DKG_ONT}usedTool`, object: tcUri, graph: '' });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
await this.tools.writeToWorkspace(MEMORY_PARANET, quads, { localOnly: true });
|
|
220
|
+
this.knownSessions.add(sessionId);
|
|
221
|
+
// Fire-and-forget: extract entity mentions and write them as separate triples
|
|
222
|
+
if (this.llmConfig?.apiKey) {
|
|
223
|
+
this.extractAndWriteMentions(userMsgUri, userMessage, assistantMsgUri, assistantReply)
|
|
224
|
+
.catch(() => { });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async extractAndWriteMentions(userMsgUri, userMessage, assistantMsgUri, assistantReply) {
|
|
228
|
+
const allText = `User: ${userMessage}\nAssistant: ${assistantReply}`;
|
|
229
|
+
const entities = await this.callMentionExtraction(allText);
|
|
230
|
+
if (entities.length === 0)
|
|
231
|
+
return;
|
|
232
|
+
const quads = [];
|
|
233
|
+
const MENTIONED_IN = `${DKG_ONT}mentionedIn`;
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
for (const ent of entities) {
|
|
236
|
+
const slug = ent.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
237
|
+
if (!slug || seen.has(slug))
|
|
238
|
+
continue;
|
|
239
|
+
seen.add(slug);
|
|
240
|
+
const entityUri = `urn:dkg:entity:${slug}`;
|
|
241
|
+
const schemaType = `${SCHEMA}${ent.type}`;
|
|
242
|
+
quads.push({ subject: entityUri, predicate: RDF_TYPE, object: schemaType, graph: '' }, { subject: entityUri, predicate: `${SCHEMA}name`, object: JSON.stringify(ent.name), graph: '' });
|
|
243
|
+
// Use entity as subject to avoid re-declaring message entities (DKG Rule 4)
|
|
244
|
+
const userLower = userMessage.toLowerCase();
|
|
245
|
+
const assistantLower = assistantReply.toLowerCase();
|
|
246
|
+
const nameLower = ent.name.toLowerCase();
|
|
247
|
+
if (userLower.includes(nameLower)) {
|
|
248
|
+
quads.push({ subject: entityUri, predicate: MENTIONED_IN, object: userMsgUri, graph: '' });
|
|
249
|
+
}
|
|
250
|
+
if (assistantLower.includes(nameLower)) {
|
|
251
|
+
quads.push({ subject: entityUri, predicate: MENTIONED_IN, object: assistantMsgUri, graph: '' });
|
|
252
|
+
}
|
|
253
|
+
if (!userLower.includes(nameLower) && !assistantLower.includes(nameLower)) {
|
|
254
|
+
quads.push({ subject: entityUri, predicate: MENTIONED_IN, object: userMsgUri, graph: '' });
|
|
255
|
+
quads.push({ subject: entityUri, predicate: MENTIONED_IN, object: assistantMsgUri, graph: '' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (quads.length > 0) {
|
|
259
|
+
await this.tools.writeToWorkspace(MEMORY_PARANET, quads, { localOnly: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async callMentionExtraction(text) {
|
|
263
|
+
if (!this.llmConfig.apiKey)
|
|
264
|
+
return [];
|
|
265
|
+
try {
|
|
266
|
+
const completion = await this.llmClient.complete({
|
|
267
|
+
config: this.llmConfig,
|
|
268
|
+
request: {
|
|
269
|
+
messages: [
|
|
270
|
+
{ role: 'system', content: MENTION_EXTRACTION_PROMPT },
|
|
271
|
+
{ role: 'user', content: text },
|
|
272
|
+
],
|
|
273
|
+
temperature: 0,
|
|
274
|
+
maxTokens: 512,
|
|
275
|
+
stream: false,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
let output = completion.message.content?.trim() ?? '';
|
|
279
|
+
output = output.replace(/^```(?:json)?\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
280
|
+
const parsed = JSON.parse(output);
|
|
281
|
+
if (!Array.isArray(parsed))
|
|
282
|
+
return [];
|
|
283
|
+
return parsed.filter((e) => e.name && e.type);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async extractKnowledge(sessionId, userMessage, assistantReply) {
|
|
290
|
+
await this.ensureInitialized();
|
|
291
|
+
const exchange = `User: ${userMessage}\nAssistant: ${assistantReply}`;
|
|
292
|
+
if (!this.llmConfig.apiKey)
|
|
293
|
+
return 0;
|
|
294
|
+
let output = '';
|
|
295
|
+
try {
|
|
296
|
+
const completion = await this.llmClient.complete({
|
|
297
|
+
config: this.llmConfig,
|
|
298
|
+
request: {
|
|
299
|
+
messages: [
|
|
300
|
+
{ role: 'system', content: KG_EXTRACTION_PROMPT },
|
|
301
|
+
{ role: 'user', content: exchange },
|
|
302
|
+
],
|
|
303
|
+
temperature: 0.1,
|
|
304
|
+
maxTokens: 1024,
|
|
305
|
+
stream: false,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
output = completion.message.content?.trim() ?? '';
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
if (!output || output === 'NONE')
|
|
314
|
+
return 0;
|
|
315
|
+
const triples = this.parseNTriples(output);
|
|
316
|
+
if (triples.length === 0)
|
|
317
|
+
return 0;
|
|
318
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
319
|
+
const quads = [];
|
|
320
|
+
for (const t of triples) {
|
|
321
|
+
quads.push({ ...t, graph: '' });
|
|
322
|
+
}
|
|
323
|
+
const rootEntities = new Set(triples.map(t => t.subject));
|
|
324
|
+
for (const entity of rootEntities) {
|
|
325
|
+
const memUri = `${MEMORY_NS}${crypto.randomUUID().slice(0, 8)}`;
|
|
326
|
+
quads.push({ subject: memUri, predicate: RDF_TYPE, object: `${DKG_ONT}Memory`, graph: '' }, { subject: memUri, predicate: `${DKG_ONT}extractedFrom`, object: sessionUri, graph: '' }, { subject: memUri, predicate: `${DKG_ONT}contains`, object: entity, graph: '' }, { subject: memUri, predicate: `${SCHEMA}dateCreated`, object: `"${new Date().toISOString()}"^^<${XSD_DATETIME}>`, graph: '' });
|
|
327
|
+
}
|
|
328
|
+
await this.tools.writeToWorkspace(MEMORY_PARANET, quads, { localOnly: true });
|
|
329
|
+
return triples.length;
|
|
330
|
+
}
|
|
331
|
+
async recall(sparql) {
|
|
332
|
+
await this.ensureInitialized();
|
|
333
|
+
return this.tools.query(sparql, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
334
|
+
}
|
|
335
|
+
async semanticRecall(question) {
|
|
336
|
+
if (!this.llmConfig.apiKey)
|
|
337
|
+
throw new Error('LLM not configured');
|
|
338
|
+
const completion = await this.llmClient.complete({
|
|
339
|
+
config: this.llmConfig,
|
|
340
|
+
request: {
|
|
341
|
+
messages: [
|
|
342
|
+
{ role: 'system', content: SEMANTIC_RECALL_SYSTEM },
|
|
343
|
+
{ role: 'user', content: question },
|
|
344
|
+
],
|
|
345
|
+
temperature: 0.1,
|
|
346
|
+
maxTokens: 512,
|
|
347
|
+
stream: false,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
let sparql = completion.message.content?.trim() ?? '';
|
|
351
|
+
sparql = sparql.replace(/^```(?:sparql)?\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
352
|
+
const result = await this.recall(sparql);
|
|
353
|
+
return { sparql, result };
|
|
354
|
+
}
|
|
355
|
+
async getStats() {
|
|
356
|
+
await this.ensureInitialized();
|
|
357
|
+
const base = {
|
|
358
|
+
paranetId: MEMORY_PARANET,
|
|
359
|
+
initialized: true,
|
|
360
|
+
messageCount: 0,
|
|
361
|
+
knowledgeTriples: 0,
|
|
362
|
+
totalTriples: 0,
|
|
363
|
+
sessionCount: 0,
|
|
364
|
+
entityCount: 0,
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
const total = await this.tools.query(`SELECT (COUNT(*) AS ?c) WHERE { ?s ?p ?o }`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
368
|
+
base.totalTriples = sumBindingValues(total.bindings, 'c');
|
|
369
|
+
const sessions = await this.tools.query(`SELECT (COUNT(DISTINCT ?s) AS ?c) WHERE { ?s <${RDF_TYPE}> <${SCHEMA}Conversation> }`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
370
|
+
base.sessionCount = sumBindingValues(sessions.bindings, 'c');
|
|
371
|
+
const msgs = await this.tools.query(`SELECT (COUNT(*) AS ?c) WHERE { ?s <${RDF_TYPE}> <${SCHEMA}Message> }`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
372
|
+
base.messageCount = sumBindingValues(msgs.bindings, 'c');
|
|
373
|
+
const chatRelatedTriples = await this.tools.query(`SELECT (COUNT(*) AS ?c) WHERE {
|
|
374
|
+
{ ?s <${RDF_TYPE}> <${SCHEMA}Message> . ?s ?p ?o }
|
|
375
|
+
UNION
|
|
376
|
+
{ ?s <${RDF_TYPE}> <${SCHEMA}Conversation> . ?s ?p ?o }
|
|
377
|
+
UNION
|
|
378
|
+
{ ?s <${RDF_TYPE}> <${DKG_ONT}ToolInvocation> . ?s ?p ?o }
|
|
379
|
+
}`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
380
|
+
const chatTripleCount = sumBindingValues(chatRelatedTriples.bindings, 'c');
|
|
381
|
+
const entities = await this.tools.query(`SELECT (COUNT(DISTINCT ?e) AS ?c) WHERE { ?e <${RDF_TYPE}> ?t . FILTER(STRSTARTS(STR(?e), "urn:dkg:entity:")) }`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
382
|
+
base.entityCount = sumBindingValues(entities.bindings, 'c');
|
|
383
|
+
base.knowledgeTriples = Math.max(0, base.totalTriples - chatTripleCount);
|
|
384
|
+
}
|
|
385
|
+
catch { /* stats are best-effort */ }
|
|
386
|
+
return base;
|
|
387
|
+
}
|
|
388
|
+
async getEntities(limit = 50) {
|
|
389
|
+
await this.ensureInitialized();
|
|
390
|
+
try {
|
|
391
|
+
const result = await this.tools.query(`SELECT DISTINCT ?e ?type ?label WHERE {
|
|
392
|
+
?e <${RDF_TYPE}> ?type .
|
|
393
|
+
OPTIONAL { ?e <${SCHEMA}name> ?label }
|
|
394
|
+
FILTER(STRSTARTS(STR(?e), "urn:dkg:entity:"))
|
|
395
|
+
} LIMIT ${limit}`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
396
|
+
const entities = [];
|
|
397
|
+
for (const b of result.bindings ?? []) {
|
|
398
|
+
const uri = b.e;
|
|
399
|
+
const propsResult = await this.tools.query(`SELECT ?p ?o WHERE { <${uri}> ?p ?o } LIMIT 20`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
400
|
+
entities.push({
|
|
401
|
+
uri,
|
|
402
|
+
type: b.type ?? '',
|
|
403
|
+
label: b.label ?? uri.split(':').pop() ?? uri,
|
|
404
|
+
properties: (propsResult.bindings ?? []).map((pb) => ({
|
|
405
|
+
predicate: pb.p,
|
|
406
|
+
object: pb.o,
|
|
407
|
+
})),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return entities;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async getSession(sessionId) {
|
|
417
|
+
await this.ensureInitialized();
|
|
418
|
+
try {
|
|
419
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
420
|
+
const msgsResult = await this.tools.query(`SELECT ?author ?text ?ts ?turnId ?persistenceState WHERE {
|
|
421
|
+
?m <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
422
|
+
?m <${SCHEMA}author> ?author .
|
|
423
|
+
?m <${SCHEMA}text> ?text .
|
|
424
|
+
?m <${SCHEMA}dateCreated> ?ts
|
|
425
|
+
OPTIONAL { ?m <${DKG_ONT}turnId> ?turnId }
|
|
426
|
+
OPTIONAL {
|
|
427
|
+
?turn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
428
|
+
?turn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
429
|
+
?turn <${DKG_ONT}turnId> ?turnId .
|
|
430
|
+
?turn <${DKG_ONT}persistenceState> ?persistenceState .
|
|
431
|
+
}
|
|
432
|
+
} ORDER BY ?ts LIMIT 500`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
433
|
+
const bindings = msgsResult.bindings ?? [];
|
|
434
|
+
if (bindings.length === 0)
|
|
435
|
+
return null;
|
|
436
|
+
return {
|
|
437
|
+
session: sessionId,
|
|
438
|
+
messages: bindings.map((mb) => ({
|
|
439
|
+
author: mb.author?.includes('user') ? 'user' : 'agent',
|
|
440
|
+
text: stripRdfLiteral(mb.text ?? ''),
|
|
441
|
+
ts: stripRdfLiteral(mb.ts ?? ''),
|
|
442
|
+
turnId: stripRdfLiteral(mb.turnId ?? '') || undefined,
|
|
443
|
+
persistStatus: (() => {
|
|
444
|
+
const status = stripRdfLiteral(mb.persistenceState ?? '').trim();
|
|
445
|
+
if (status === 'pending' || status === 'in_progress' || status === 'stored' || status === 'failed' || status === 'skipped') {
|
|
446
|
+
return status;
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
})(),
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async getRecentChats(limit = 20) {
|
|
458
|
+
await this.ensureInitialized();
|
|
459
|
+
try {
|
|
460
|
+
const expandedLimit = Math.max(limit, Math.min(limit * 4, 400));
|
|
461
|
+
const sessionsResult = await this.tools.query(`SELECT ?s ?sid (MAX(?mts) AS ?latest) WHERE {
|
|
462
|
+
?s <${RDF_TYPE}> <${SCHEMA}Conversation> .
|
|
463
|
+
?s <${DKG_ONT}sessionId> ?sid .
|
|
464
|
+
OPTIONAL { ?m <${SCHEMA}isPartOf> ?s . ?m <${SCHEMA}dateCreated> ?mts }
|
|
465
|
+
} GROUP BY ?s ?sid ORDER BY DESC(?latest) LIMIT ${expandedLimit}`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
466
|
+
const sessionBindings = sessionsResult.bindings ?? [];
|
|
467
|
+
if (sessionBindings.length === 0)
|
|
468
|
+
return [];
|
|
469
|
+
const seenSessionIds = new Set();
|
|
470
|
+
const sessionEntries = [];
|
|
471
|
+
for (const sb of sessionBindings) {
|
|
472
|
+
const sessionUri = String(sb.s ?? '').replace(/[<>]/g, '');
|
|
473
|
+
const sid = stripRdfLiteral(sb.sid ?? sb.s);
|
|
474
|
+
const sessionId = sid || sessionUri;
|
|
475
|
+
if (!sessionUri || !sessionId)
|
|
476
|
+
continue;
|
|
477
|
+
if (!isSafeIri(sessionUri))
|
|
478
|
+
continue;
|
|
479
|
+
if (seenSessionIds.has(sessionId))
|
|
480
|
+
continue;
|
|
481
|
+
seenSessionIds.add(sessionId);
|
|
482
|
+
sessionEntries.push({ sessionUri, sessionId });
|
|
483
|
+
if (sessionEntries.length >= limit)
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
if (sessionEntries.length === 0)
|
|
487
|
+
return [];
|
|
488
|
+
const values = sessionEntries
|
|
489
|
+
.map((entry) => `<${entry.sessionUri}>`)
|
|
490
|
+
.join(' ');
|
|
491
|
+
const allMsgs = await this.tools.query(`SELECT ?session ?author ?text ?ts WHERE {
|
|
492
|
+
VALUES ?session { ${values} }
|
|
493
|
+
?m <${SCHEMA}isPartOf> ?session .
|
|
494
|
+
?m <${SCHEMA}author> ?author .
|
|
495
|
+
?m <${SCHEMA}text> ?text .
|
|
496
|
+
?m <${SCHEMA}dateCreated> ?ts
|
|
497
|
+
} ORDER BY ?session ?ts`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
498
|
+
const bySession = new Map();
|
|
499
|
+
for (const row of allMsgs.bindings ?? []) {
|
|
500
|
+
const sessionUri = String(row.session ?? '').replace(/[<>]/g, '');
|
|
501
|
+
if (!sessionUri)
|
|
502
|
+
continue;
|
|
503
|
+
if (!bySession.has(sessionUri))
|
|
504
|
+
bySession.set(sessionUri, []);
|
|
505
|
+
const msgs = bySession.get(sessionUri);
|
|
506
|
+
if (msgs.length >= 100)
|
|
507
|
+
continue;
|
|
508
|
+
msgs.push({
|
|
509
|
+
author: row.author?.includes('user') ? 'user' : 'agent',
|
|
510
|
+
text: stripRdfLiteral(row.text ?? ''),
|
|
511
|
+
ts: stripRdfLiteral(row.ts ?? ''),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return sessionEntries.map((entry) => ({
|
|
515
|
+
session: entry.sessionId,
|
|
516
|
+
messages: bySession.get(entry.sessionUri) ?? [],
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async getSessionGraphDelta(sessionId, turnId, opts) {
|
|
524
|
+
await this.ensureInitialized();
|
|
525
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
526
|
+
const baseTurnId = opts?.baseTurnId?.trim() || null;
|
|
527
|
+
const countResult = await this.tools.query(`SELECT (COUNT(*) AS ?c) WHERE {
|
|
528
|
+
?turn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
529
|
+
?turn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
530
|
+
}`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
531
|
+
const turnCount = sumBindingValues(countResult.bindings, 'c');
|
|
532
|
+
if (turnCount === 0) {
|
|
533
|
+
return {
|
|
534
|
+
mode: 'full_refresh_required',
|
|
535
|
+
reason: 'session_empty',
|
|
536
|
+
sessionId,
|
|
537
|
+
turnId,
|
|
538
|
+
watermark: {
|
|
539
|
+
baseTurnId,
|
|
540
|
+
previousTurnId: null,
|
|
541
|
+
appliedTurnId: null,
|
|
542
|
+
latestTurnId: null,
|
|
543
|
+
turnIndex: 0,
|
|
544
|
+
turnCount: 0,
|
|
545
|
+
},
|
|
546
|
+
triples: [],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const turnUri = `${CHAT_NS}turn:${turnId}`;
|
|
550
|
+
const currentTurnResult = await this.tools.query(`SELECT ?tid ?ts WHERE {
|
|
551
|
+
<${turnUri}> <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
552
|
+
<${turnUri}> <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
553
|
+
<${turnUri}> <${DKG_ONT}turnId> ?tid .
|
|
554
|
+
OPTIONAL { <${turnUri}> <${SCHEMA}dateCreated> ?ts }
|
|
555
|
+
} LIMIT 1`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
556
|
+
const currentTurn = (currentTurnResult.bindings ?? [])[0];
|
|
557
|
+
const currentTurnId = stripRdfLiteral(currentTurn?.tid ?? '').trim();
|
|
558
|
+
const currentTurnTs = stripRdfLiteral(currentTurn?.ts ?? '').trim();
|
|
559
|
+
const latestTurnResult = await this.tools.query(`SELECT ?latestTurnId ?latestTs WHERE {
|
|
560
|
+
?latestTurn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
561
|
+
?latestTurn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
562
|
+
?latestTurn <${DKG_ONT}turnId> ?latestTurnId .
|
|
563
|
+
OPTIONAL { ?latestTurn <${SCHEMA}dateCreated> ?latestTs }
|
|
564
|
+
} ORDER BY DESC(?latestTs) DESC(?latestTurnId) LIMIT 1`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
565
|
+
const latestTurnId = stripRdfLiteral((latestTurnResult.bindings ?? [])[0]?.latestTurnId ?? '').trim() || null;
|
|
566
|
+
if (!currentTurnId || currentTurnId !== turnId) {
|
|
567
|
+
return {
|
|
568
|
+
mode: 'full_refresh_required',
|
|
569
|
+
reason: 'turn_not_found',
|
|
570
|
+
sessionId,
|
|
571
|
+
turnId,
|
|
572
|
+
watermark: {
|
|
573
|
+
baseTurnId,
|
|
574
|
+
previousTurnId: latestTurnId,
|
|
575
|
+
appliedTurnId: null,
|
|
576
|
+
latestTurnId,
|
|
577
|
+
turnIndex: 0,
|
|
578
|
+
turnCount,
|
|
579
|
+
},
|
|
580
|
+
triples: [],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const currentTurnIdLiteral = JSON.stringify(currentTurnId);
|
|
584
|
+
const currentTsLiteral = currentTurnTs
|
|
585
|
+
? `"${currentTurnTs}"^^<${XSD_DATETIME}>`
|
|
586
|
+
: null;
|
|
587
|
+
const previousTurnQuery = currentTsLiteral
|
|
588
|
+
? `SELECT ?previousTurnId WHERE {
|
|
589
|
+
?previousTurn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
590
|
+
?previousTurn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
591
|
+
?previousTurn <${DKG_ONT}turnId> ?previousTurnId .
|
|
592
|
+
?previousTurn <${SCHEMA}dateCreated> ?previousTs .
|
|
593
|
+
FILTER(
|
|
594
|
+
?previousTs < ${currentTsLiteral}
|
|
595
|
+
|| (?previousTs = ${currentTsLiteral} && ?previousTurnId < ${currentTurnIdLiteral})
|
|
596
|
+
)
|
|
597
|
+
} ORDER BY DESC(?previousTs) DESC(?previousTurnId) LIMIT 1`
|
|
598
|
+
: `SELECT ?previousTurnId WHERE {
|
|
599
|
+
?previousTurn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
600
|
+
?previousTurn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
601
|
+
?previousTurn <${DKG_ONT}turnId> ?previousTurnId .
|
|
602
|
+
FILTER(?previousTurnId < ${currentTurnIdLiteral})
|
|
603
|
+
} ORDER BY DESC(?previousTurnId) LIMIT 1`;
|
|
604
|
+
const previousTurnResult = await this.tools.query(previousTurnQuery, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
605
|
+
const previousTurnId = stripRdfLiteral((previousTurnResult.bindings ?? [])[0]?.previousTurnId ?? '').trim() || null;
|
|
606
|
+
const turnIndexResult = currentTsLiteral
|
|
607
|
+
? await this.tools.query(`SELECT (COUNT(*) AS ?c) WHERE {
|
|
608
|
+
?turn <${RDF_TYPE}> <${DKG_ONT}ChatTurn> .
|
|
609
|
+
?turn <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
610
|
+
?turn <${DKG_ONT}turnId> ?tid .
|
|
611
|
+
?turn <${SCHEMA}dateCreated> ?ts .
|
|
612
|
+
FILTER(
|
|
613
|
+
?ts < ${currentTsLiteral}
|
|
614
|
+
|| (?ts = ${currentTsLiteral} && ?tid <= ${currentTurnIdLiteral})
|
|
615
|
+
)
|
|
616
|
+
}`, { paranetId: MEMORY_PARANET, includeWorkspace: true })
|
|
617
|
+
: { bindings: [{ c: String(previousTurnId ? 2 : 1) }] };
|
|
618
|
+
const turnIndex = Math.max(0, sumBindingValues(turnIndexResult.bindings, 'c'));
|
|
619
|
+
if (previousTurnId && !baseTurnId) {
|
|
620
|
+
return {
|
|
621
|
+
mode: 'full_refresh_required',
|
|
622
|
+
reason: 'missing_watermark',
|
|
623
|
+
sessionId,
|
|
624
|
+
turnId,
|
|
625
|
+
watermark: {
|
|
626
|
+
baseTurnId,
|
|
627
|
+
previousTurnId,
|
|
628
|
+
appliedTurnId: null,
|
|
629
|
+
latestTurnId,
|
|
630
|
+
turnIndex,
|
|
631
|
+
turnCount,
|
|
632
|
+
},
|
|
633
|
+
triples: [],
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
if ((previousTurnId && baseTurnId !== previousTurnId)
|
|
637
|
+
|| (!previousTurnId && baseTurnId != null)) {
|
|
638
|
+
return {
|
|
639
|
+
mode: 'full_refresh_required',
|
|
640
|
+
reason: 'watermark_mismatch',
|
|
641
|
+
sessionId,
|
|
642
|
+
turnId,
|
|
643
|
+
watermark: {
|
|
644
|
+
baseTurnId,
|
|
645
|
+
previousTurnId,
|
|
646
|
+
appliedTurnId: null,
|
|
647
|
+
latestTurnId,
|
|
648
|
+
turnIndex,
|
|
649
|
+
turnCount,
|
|
650
|
+
},
|
|
651
|
+
triples: [],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const turnMessagesResult = await this.tools.query(`SELECT ?user ?assistant WHERE {
|
|
655
|
+
<${turnUri}> <${SCHEMA}isPartOf> <${sessionUri}> .
|
|
656
|
+
<${turnUri}> <${DKG_ONT}hasUserMessage> ?user .
|
|
657
|
+
<${turnUri}> <${DKG_ONT}hasAssistantMessage> ?assistant .
|
|
658
|
+
} LIMIT 1`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
659
|
+
const turnMessages = (turnMessagesResult.bindings ?? [])[0];
|
|
660
|
+
const userMsgUri = String(turnMessages?.user ?? '').replace(/[<>]/g, '');
|
|
661
|
+
const assistantMsgUri = String(turnMessages?.assistant ?? '').replace(/[<>]/g, '');
|
|
662
|
+
if (!userMsgUri || !assistantMsgUri || !isSafeIri(userMsgUri) || !isSafeIri(assistantMsgUri)) {
|
|
663
|
+
return {
|
|
664
|
+
mode: 'full_refresh_required',
|
|
665
|
+
reason: 'turn_not_found',
|
|
666
|
+
sessionId,
|
|
667
|
+
turnId,
|
|
668
|
+
watermark: {
|
|
669
|
+
baseTurnId,
|
|
670
|
+
previousTurnId,
|
|
671
|
+
appliedTurnId: null,
|
|
672
|
+
latestTurnId,
|
|
673
|
+
turnIndex,
|
|
674
|
+
turnCount,
|
|
675
|
+
},
|
|
676
|
+
triples: [],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const relatedSubjectsResult = await this.tools.query(`SELECT DISTINCT ?s WHERE {
|
|
680
|
+
VALUES ?msg { <${userMsgUri}> <${assistantMsgUri}> }
|
|
681
|
+
{ BIND(<${sessionUri}> AS ?s) }
|
|
682
|
+
UNION { BIND(<${turnUri}> AS ?s) }
|
|
683
|
+
UNION { BIND(?msg AS ?s) }
|
|
684
|
+
UNION { <${assistantMsgUri}> <${DKG_ONT}usedTool> ?s }
|
|
685
|
+
UNION { ?s <${DKG_ONT}mentionedIn> ?msg }
|
|
686
|
+
UNION {
|
|
687
|
+
?entity <${DKG_ONT}mentionedIn> ?msg .
|
|
688
|
+
?s <${DKG_ONT}contains> ?entity .
|
|
689
|
+
?s <${DKG_ONT}extractedFrom> <${sessionUri}> .
|
|
690
|
+
}
|
|
691
|
+
} LIMIT 5000`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
692
|
+
const subjectSet = new Set([sessionUri, turnUri, userMsgUri, assistantMsgUri]);
|
|
693
|
+
for (const b of relatedSubjectsResult.bindings ?? []) {
|
|
694
|
+
const iri = String(b.s ?? '').replace(/[<>]/g, '');
|
|
695
|
+
if (!iri || !isSafeIri(iri))
|
|
696
|
+
continue;
|
|
697
|
+
subjectSet.add(iri);
|
|
698
|
+
}
|
|
699
|
+
const values = [...subjectSet]
|
|
700
|
+
.map((iri) => `<${iri}>`)
|
|
701
|
+
.join(' ');
|
|
702
|
+
const deltaResult = await this.tools.query(`CONSTRUCT { ?s ?p ?o } WHERE {
|
|
703
|
+
VALUES ?s { ${values} }
|
|
704
|
+
?s ?p ?o .
|
|
705
|
+
}`, { paranetId: MEMORY_PARANET, includeWorkspace: true });
|
|
706
|
+
const quads = Array.isArray(deltaResult?.quads) ? deltaResult.quads : [];
|
|
707
|
+
const triples = quads.map((q) => ({
|
|
708
|
+
subject: String(q.subject ?? ''),
|
|
709
|
+
predicate: String(q.predicate ?? ''),
|
|
710
|
+
object: stripRdfLiteral(String(q.object ?? '')),
|
|
711
|
+
}));
|
|
712
|
+
return {
|
|
713
|
+
mode: 'delta',
|
|
714
|
+
sessionId,
|
|
715
|
+
turnId,
|
|
716
|
+
watermark: {
|
|
717
|
+
baseTurnId,
|
|
718
|
+
previousTurnId,
|
|
719
|
+
appliedTurnId: turnId,
|
|
720
|
+
latestTurnId,
|
|
721
|
+
turnIndex,
|
|
722
|
+
turnCount,
|
|
723
|
+
},
|
|
724
|
+
triples,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async getSessionPublicationStatus(sessionId) {
|
|
728
|
+
await this.ensureInitialized();
|
|
729
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
730
|
+
const rootPattern = buildSessionRootPattern(sessionUri);
|
|
731
|
+
const countQuery = `SELECT (COUNT(*) AS ?c) WHERE {
|
|
732
|
+
{
|
|
733
|
+
SELECT DISTINCT ?s WHERE ${rootPattern} LIMIT 5000
|
|
734
|
+
}
|
|
735
|
+
?s ?p ?o
|
|
736
|
+
}`;
|
|
737
|
+
const workspaceCountResult = await this.tools.query(countQuery, {
|
|
738
|
+
paranetId: MEMORY_PARANET,
|
|
739
|
+
graphSuffix: '_workspace',
|
|
740
|
+
});
|
|
741
|
+
const dataCountResult = await this.tools.query(countQuery, {
|
|
742
|
+
paranetId: MEMORY_PARANET,
|
|
743
|
+
});
|
|
744
|
+
const rootEntityResult = await this.tools.query(`SELECT DISTINCT ?s WHERE ${rootPattern} LIMIT 5000`, { paranetId: MEMORY_PARANET, graphSuffix: '_workspace' });
|
|
745
|
+
const workspaceTripleCount = sumBindingValues(workspaceCountResult.bindings, 'c');
|
|
746
|
+
const dataTripleCount = sumBindingValues(dataCountResult.bindings, 'c');
|
|
747
|
+
const rootEntityCount = (rootEntityResult.bindings ?? []).length;
|
|
748
|
+
const scope = dataTripleCount > 0
|
|
749
|
+
? (workspaceTripleCount > dataTripleCount
|
|
750
|
+
? 'enshrined_with_pending'
|
|
751
|
+
: 'enshrined')
|
|
752
|
+
: workspaceTripleCount > 0
|
|
753
|
+
? 'workspace_only'
|
|
754
|
+
: 'empty';
|
|
755
|
+
return {
|
|
756
|
+
sessionId,
|
|
757
|
+
workspaceTripleCount,
|
|
758
|
+
dataTripleCount,
|
|
759
|
+
scope,
|
|
760
|
+
rootEntityCount,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async getSessionRootEntities(sessionId) {
|
|
764
|
+
await this.ensureInitialized();
|
|
765
|
+
const sessionUri = `${CHAT_NS}session:${sessionId}`;
|
|
766
|
+
const rootPattern = buildSessionRootPattern(sessionUri);
|
|
767
|
+
const result = await this.tools.query(`SELECT DISTINCT ?s WHERE ${rootPattern} LIMIT 5000`, { paranetId: MEMORY_PARANET, graphSuffix: '_workspace' });
|
|
768
|
+
const roots = new Set();
|
|
769
|
+
for (const b of result.bindings ?? []) {
|
|
770
|
+
const iri = String(b.s ?? '').replace(/[<>]/g, '');
|
|
771
|
+
if (!iri || !isSafeIri(iri))
|
|
772
|
+
continue;
|
|
773
|
+
roots.add(iri);
|
|
774
|
+
}
|
|
775
|
+
return [...roots];
|
|
776
|
+
}
|
|
777
|
+
async publishSession(sessionId, opts) {
|
|
778
|
+
await this.ensureInitialized();
|
|
779
|
+
const sessionRoots = await this.getSessionRootEntities(sessionId);
|
|
780
|
+
if (sessionRoots.length === 0) {
|
|
781
|
+
throw new Error(`No workspace entities found for session ${sessionId}`);
|
|
782
|
+
}
|
|
783
|
+
const sessionRootSet = new Set(sessionRoots);
|
|
784
|
+
const requestedRoots = (opts?.rootEntities ?? [])
|
|
785
|
+
.map((r) => String(r).trim())
|
|
786
|
+
.filter((r) => isSafeIri(r));
|
|
787
|
+
const rootEntities = requestedRoots.length > 0
|
|
788
|
+
? [...new Set(requestedRoots.filter((r) => sessionRootSet.has(r)))]
|
|
789
|
+
: sessionRoots;
|
|
790
|
+
if (rootEntities.length === 0) {
|
|
791
|
+
throw new Error(`Selected root entities are not part of session ${sessionId}`);
|
|
792
|
+
}
|
|
793
|
+
const enshrined = await this.enshrine({ rootEntities }, { clearWorkspaceAfter: opts?.clearWorkspaceAfter ?? false });
|
|
794
|
+
const publication = await this.getSessionPublicationStatus(sessionId);
|
|
795
|
+
return {
|
|
796
|
+
...enshrined,
|
|
797
|
+
sessionId,
|
|
798
|
+
rootEntityCount: rootEntities.length,
|
|
799
|
+
publication,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async importMemories(rawText, source = 'other', opts = {}) {
|
|
803
|
+
await this.ensureInitialized();
|
|
804
|
+
const batchId = crypto.randomUUID();
|
|
805
|
+
const batchUri = `${MEMORY_NS}import:${batchId}`;
|
|
806
|
+
const now = new Date().toISOString();
|
|
807
|
+
const llmEnabled = opts.useLlm === true && !!this.llmConfig?.apiKey;
|
|
808
|
+
const warnings = [];
|
|
809
|
+
let memories = llmEnabled
|
|
810
|
+
? await this.parseMemoriesWithLlm(rawText)
|
|
811
|
+
: this.parseMemoriesHeuristic(rawText);
|
|
812
|
+
if (memories.length === 0 && llmEnabled) {
|
|
813
|
+
memories = this.parseMemoriesHeuristic(rawText);
|
|
814
|
+
}
|
|
815
|
+
if (memories.length === 0) {
|
|
816
|
+
return { batchId: null, source, memoryCount: 0, tripleCount: 0, entityCount: 0, quads: [] };
|
|
817
|
+
}
|
|
818
|
+
const MAX_MEMORY_ITEMS = 5000;
|
|
819
|
+
if (memories.length > MAX_MEMORY_ITEMS) {
|
|
820
|
+
warnings.push(`Input contained ${memories.length} items; truncated to ${MAX_MEMORY_ITEMS}`);
|
|
821
|
+
memories = memories.slice(0, MAX_MEMORY_ITEMS);
|
|
822
|
+
}
|
|
823
|
+
const quads = [];
|
|
824
|
+
quads.push({ subject: batchUri, predicate: RDF_TYPE, object: `${DKG_ONT}MemoryImport`, graph: '' }, { subject: batchUri, predicate: `${DKG_ONT}importSource`, object: `"${source}"`, graph: '' }, { subject: batchUri, predicate: `${SCHEMA}dateCreated`, object: `"${now}"^^<${XSD_DATETIME}>`, graph: '' }, { subject: batchUri, predicate: `${DKG_ONT}itemCount`, object: `"${memories.length}"^^<http://www.w3.org/2001/XMLSchema#integer>`, graph: '' });
|
|
825
|
+
for (const mem of memories) {
|
|
826
|
+
const memUri = `${MEMORY_NS}item:${crypto.randomUUID()}`;
|
|
827
|
+
quads.push({ subject: memUri, predicate: RDF_TYPE, object: `${DKG_ONT}ImportedMemory`, graph: '' }, { subject: memUri, predicate: `${SCHEMA}text`, object: JSON.stringify(mem.text), graph: '' }, { subject: memUri, predicate: `${DKG_ONT}category`, object: `"${mem.category}"`, graph: '' }, { subject: memUri, predicate: `${SCHEMA}dateCreated`, object: `"${now}"^^<${XSD_DATETIME}>`, graph: '' }, { subject: memUri, predicate: `${DKG_ONT}importBatch`, object: batchUri, graph: '' }, { subject: memUri, predicate: `${DKG_ONT}importSource`, object: `"${source}"`, graph: '' });
|
|
828
|
+
}
|
|
829
|
+
await this.tools.writeToWorkspace(MEMORY_PARANET, quads, { localOnly: true });
|
|
830
|
+
let entityCount = 0;
|
|
831
|
+
let extractionTripleCount = 0;
|
|
832
|
+
const allQuads = quads.map(q => ({ subject: q.subject, predicate: q.predicate, object: q.object }));
|
|
833
|
+
if (llmEnabled) {
|
|
834
|
+
try {
|
|
835
|
+
const extraction = await this.extractKnowledgeFromImport(batchUri, memories);
|
|
836
|
+
entityCount = extraction.entityCount;
|
|
837
|
+
extractionTripleCount = extraction.tripleCount;
|
|
838
|
+
allQuads.push(...extraction.quads);
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
const msg = err?.message ?? String(err);
|
|
842
|
+
console.warn(`[ChatMemoryManager] Knowledge extraction failed for batch ${batchId}: ${msg}`);
|
|
843
|
+
warnings.push(`Knowledge extraction failed: ${msg}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const QUAD_PREVIEW_LIMIT = 500;
|
|
847
|
+
const result = {
|
|
848
|
+
batchId,
|
|
849
|
+
source,
|
|
850
|
+
memoryCount: memories.length,
|
|
851
|
+
tripleCount: quads.length + extractionTripleCount,
|
|
852
|
+
entityCount,
|
|
853
|
+
quads: allQuads.slice(0, QUAD_PREVIEW_LIMIT),
|
|
854
|
+
quadsTruncated: allQuads.length > QUAD_PREVIEW_LIMIT,
|
|
855
|
+
};
|
|
856
|
+
if (warnings.length > 0)
|
|
857
|
+
result.warnings = warnings;
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
async parseMemoriesWithLlm(rawText) {
|
|
861
|
+
const { apiKey, model = 'gpt-5-mini', baseURL = 'https://api.openai.com/v1' } = this.llmConfig;
|
|
862
|
+
if (!apiKey)
|
|
863
|
+
return this.parseMemoriesHeuristic(rawText);
|
|
864
|
+
const url = `${baseURL.replace(/\/$/, '')}/chat/completions`;
|
|
865
|
+
try {
|
|
866
|
+
const body = {
|
|
867
|
+
model,
|
|
868
|
+
messages: [
|
|
869
|
+
{ role: 'system', content: MEMORY_PARSE_PROMPT },
|
|
870
|
+
{ role: 'user', content: rawText },
|
|
871
|
+
],
|
|
872
|
+
};
|
|
873
|
+
const res = await fetch(url, {
|
|
874
|
+
method: 'POST',
|
|
875
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
876
|
+
body: JSON.stringify(body),
|
|
877
|
+
});
|
|
878
|
+
if (!res.ok)
|
|
879
|
+
return this.parseMemoriesHeuristic(rawText);
|
|
880
|
+
const data = (await res.json());
|
|
881
|
+
let output = data.choices?.[0]?.message?.content?.trim() ?? '';
|
|
882
|
+
output = output.replace(/^```(?:json)?\n?/i, '').replace(/\n?```$/i, '').trim();
|
|
883
|
+
const parsed = JSON.parse(output);
|
|
884
|
+
if (!Array.isArray(parsed))
|
|
885
|
+
return this.parseMemoriesHeuristic(rawText);
|
|
886
|
+
const extractText = (m) => (typeof m.text === 'string' && m.text.trim()) ||
|
|
887
|
+
(typeof m.memory === 'string' && m.memory.trim()) ||
|
|
888
|
+
(typeof m.content === 'string' && m.content.trim()) ||
|
|
889
|
+
'';
|
|
890
|
+
const results = parsed
|
|
891
|
+
.filter((m) => extractText(m).length > 0)
|
|
892
|
+
.map((m) => ({
|
|
893
|
+
text: extractText(m),
|
|
894
|
+
category: ['preference', 'fact', 'context', 'instruction', 'relationship'].includes(m.category)
|
|
895
|
+
? m.category
|
|
896
|
+
: 'fact',
|
|
897
|
+
}));
|
|
898
|
+
if (results.length === 0)
|
|
899
|
+
return this.parseMemoriesHeuristic(rawText);
|
|
900
|
+
return results;
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return this.parseMemoriesHeuristic(rawText);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
parseMemoriesHeuristic(rawText) {
|
|
907
|
+
const lines = rawText
|
|
908
|
+
.split(/\n/)
|
|
909
|
+
.map(l => l.replace(/^\s*(?:[-•*]\s+|\d+[.)]\s)\s*/, '').trim())
|
|
910
|
+
.filter(l => l.length > 3 &&
|
|
911
|
+
!l.match(/^(here are|last updated|memories|---)/i) &&
|
|
912
|
+
!l.match(/^```/));
|
|
913
|
+
return lines.map(text => ({ text, category: 'fact' }));
|
|
914
|
+
}
|
|
915
|
+
async extractKnowledgeFromImport(batchUri, memories) {
|
|
916
|
+
const empty = { entityCount: 0, tripleCount: 0, quads: [] };
|
|
917
|
+
const combined = memories.map((m, i) => `${i + 1}. ${m.text}`).join('\n');
|
|
918
|
+
const { apiKey, model = 'gpt-5-mini', baseURL = 'https://api.openai.com/v1' } = this.llmConfig;
|
|
919
|
+
const url = `${baseURL.replace(/\/$/, '')}/chat/completions`;
|
|
920
|
+
const body = {
|
|
921
|
+
model,
|
|
922
|
+
messages: [
|
|
923
|
+
{ role: 'system', content: MEMORY_KG_PROMPT },
|
|
924
|
+
{ role: 'user', content: combined },
|
|
925
|
+
],
|
|
926
|
+
};
|
|
927
|
+
const res = await fetch(url, {
|
|
928
|
+
method: 'POST',
|
|
929
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
930
|
+
body: JSON.stringify(body),
|
|
931
|
+
});
|
|
932
|
+
if (!res.ok)
|
|
933
|
+
return empty;
|
|
934
|
+
const data = (await res.json());
|
|
935
|
+
const output = data.choices?.[0]?.message?.content?.trim() ?? '';
|
|
936
|
+
if (!output || output === 'NONE')
|
|
937
|
+
return empty;
|
|
938
|
+
const triples = this.parseNTriples(output);
|
|
939
|
+
if (triples.length === 0)
|
|
940
|
+
return empty;
|
|
941
|
+
const quads = [];
|
|
942
|
+
for (const t of triples) {
|
|
943
|
+
quads.push({ ...t, graph: '' });
|
|
944
|
+
}
|
|
945
|
+
const rootEntities = new Set(triples.map(t => t.subject));
|
|
946
|
+
for (const entity of rootEntities) {
|
|
947
|
+
quads.push({ subject: entity, predicate: `${DKG_ONT}extractedFrom`, object: batchUri, graph: '' });
|
|
948
|
+
}
|
|
949
|
+
await this.tools.writeToWorkspace(MEMORY_PARANET, quads, { localOnly: true });
|
|
950
|
+
return {
|
|
951
|
+
entityCount: rootEntities.size,
|
|
952
|
+
tripleCount: quads.length,
|
|
953
|
+
quads: quads.map(q => ({ subject: q.subject, predicate: q.predicate, object: q.object })),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
async enshrine(selection = 'all', opts) {
|
|
957
|
+
await this.ensureInitialized();
|
|
958
|
+
const result = await this.tools.enshrineFromWorkspace(MEMORY_PARANET, selection, {
|
|
959
|
+
clearWorkspaceAfter: opts?.clearWorkspaceAfter ?? false,
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
kcId: result?.kcId,
|
|
963
|
+
ual: result?.ual,
|
|
964
|
+
status: result?.status ?? 'confirmed',
|
|
965
|
+
tripleCount: result?.publicQuads?.length ?? 0,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
parseNTriples(text) {
|
|
969
|
+
const triples = [];
|
|
970
|
+
for (const line of text.split('\n')) {
|
|
971
|
+
const trimmed = line.trim();
|
|
972
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
973
|
+
continue;
|
|
974
|
+
const match = trimmed.match(/^<([^>]+)>\s+<([^>]+)>\s+(?:<([^>]+)>|("(?:[^"\\]|\\.)*"(?:\^\^<[^>]+>)?(?:@[a-z-]+)?))\s*\.?\s*$/);
|
|
975
|
+
if (match) {
|
|
976
|
+
const subject = match[1];
|
|
977
|
+
const predicate = match[2];
|
|
978
|
+
const object = match[3] ? match[3] : match[4];
|
|
979
|
+
triples.push({ subject, predicate, object });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return triples;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
//# sourceMappingURL=chat-memory.js.map
|