@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.
Files changed (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +49 -0
  3. package/dist/api.d.ts +30 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +805 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/chat-assistant.d.ts +68 -0
  8. package/dist/chat-assistant.d.ts.map +1 -0
  9. package/dist/chat-assistant.js +663 -0
  10. package/dist/chat-assistant.js.map +1 -0
  11. package/dist/chat-memory.d.ts +171 -0
  12. package/dist/chat-memory.d.ts.map +1 -0
  13. package/dist/chat-memory.js +985 -0
  14. package/dist/chat-memory.js.map +1 -0
  15. package/dist/chat-persistence-queue.d.ts +67 -0
  16. package/dist/chat-persistence-queue.d.ts.map +1 -0
  17. package/dist/chat-persistence-queue.js +245 -0
  18. package/dist/chat-persistence-queue.js.map +1 -0
  19. package/dist/db.d.ts +402 -0
  20. package/dist/db.d.ts.map +1 -0
  21. package/dist/db.js +887 -0
  22. package/dist/db.js.map +1 -0
  23. package/dist/gelf-push-worker.d.ts +67 -0
  24. package/dist/gelf-push-worker.d.ts.map +1 -0
  25. package/dist/gelf-push-worker.js +147 -0
  26. package/dist/gelf-push-worker.js.map +1 -0
  27. package/dist/index.d.ts +20 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +12 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/llm/capability-resolver.d.ts +3 -0
  32. package/dist/llm/capability-resolver.d.ts.map +1 -0
  33. package/dist/llm/capability-resolver.js +21 -0
  34. package/dist/llm/capability-resolver.js.map +1 -0
  35. package/dist/llm/client.d.ts +23 -0
  36. package/dist/llm/client.d.ts.map +1 -0
  37. package/dist/llm/client.js +91 -0
  38. package/dist/llm/client.js.map +1 -0
  39. package/dist/llm/provider-adapter.d.ts +16 -0
  40. package/dist/llm/provider-adapter.d.ts.map +1 -0
  41. package/dist/llm/provider-adapter.js +199 -0
  42. package/dist/llm/provider-adapter.js.map +1 -0
  43. package/dist/llm/types.d.ts +64 -0
  44. package/dist/llm/types.d.ts.map +1 -0
  45. package/dist/llm/types.js +2 -0
  46. package/dist/llm/types.js.map +1 -0
  47. package/dist/metrics-collector.d.ts +36 -0
  48. package/dist/metrics-collector.d.ts.map +1 -0
  49. package/dist/metrics-collector.js +155 -0
  50. package/dist/metrics-collector.js.map +1 -0
  51. package/dist/operation-tracker.d.ts +43 -0
  52. package/dist/operation-tracker.d.ts.map +1 -0
  53. package/dist/operation-tracker.js +195 -0
  54. package/dist/operation-tracker.js.map +1 -0
  55. package/dist/structured-logger.d.ts +16 -0
  56. package/dist/structured-logger.d.ts.map +1 -0
  57. package/dist/structured-logger.js +41 -0
  58. package/dist/structured-logger.js.map +1 -0
  59. package/dist/telemetry.d.ts +35 -0
  60. package/dist/telemetry.d.ts.map +1 -0
  61. package/dist/telemetry.js +45 -0
  62. package/dist/telemetry.js.map +1 -0
  63. package/dist-ui/assets/3d-force-graph-nMUNmvtB.js +964 -0
  64. package/dist-ui/assets/AgentHub-XKCM9uYQ.js +65 -0
  65. package/dist-ui/assets/AppHost-DoLIi89g.js +1 -0
  66. package/dist-ui/assets/Apps-Cc8HfqfD.js +1 -0
  67. package/dist-ui/assets/Dashboard-D5q6MK78.js +2 -0
  68. package/dist-ui/assets/Explorer-B80RVksc.js +64 -0
  69. package/dist-ui/assets/N3Parser-Q_-1ZY5E.js +7 -0
  70. package/dist-ui/assets/Settings-CG7-7GM-.js +71 -0
  71. package/dist-ui/assets/hooks-BLTFNmyP.js +1 -0
  72. package/dist-ui/assets/index-8_35CUX2.js +192 -0
  73. package/dist-ui/assets/index-CKZq_ZB-.css +1 -0
  74. package/dist-ui/assets/index-DH-l6lM0.js +76 -0
  75. package/dist-ui/assets/jsonld-32FQRO67-DhbO8O6B.js +2 -0
  76. package/dist-ui/assets/jsonld-BFI4wECl.js +62 -0
  77. package/dist-ui/assets/ntriples-ZWBY2WET-nIpilpjf.js +1 -0
  78. package/dist-ui/assets/ordinal-DIohFSkg.js +1 -0
  79. package/dist-ui/assets/renderer-3d-2EVDZII7-DsxBsJvs.js +2 -0
  80. package/dist-ui/assets/three.module-uCjFke6H.js +4019 -0
  81. package/dist-ui/assets/turtle-JJPK7LJ5-zezDJZEp.js +1 -0
  82. package/dist-ui/favicon.png +0 -0
  83. package/dist-ui/index.html +14 -0
  84. package/package.json +58 -0
@@ -0,0 +1,663 @@
1
+ import { LlmClient, LlmRequestError } from './llm/client.js';
2
+ const HELP_REPLY = `I'm your node assistant. You can ask me about this node (uptime, peers, triples, operations, logs) or just chat. When you ask me to save or add information to the DKG, I can write it to the knowledge graph (workspace) and optionally finalize it on-chain. Our conversation is stored privately in your DKG.`;
3
+ const DKG_TOOLS = [
4
+ {
5
+ type: 'function',
6
+ function: {
7
+ name: 'dkg_query',
8
+ description: 'Run a read-only SPARQL query (SELECT, CONSTRUCT, ASK, DESCRIBE) against the node\'s knowledge graph. Use to read data. Add LIMIT for safety.',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ sparql: { type: 'string', description: 'The SPARQL query' },
13
+ paranetId: { type: 'string', description: 'Optional paranet id; omit for "all"' },
14
+ },
15
+ required: ['sparql'],
16
+ },
17
+ },
18
+ },
19
+ {
20
+ type: 'function',
21
+ function: {
22
+ name: 'dkg_write_to_workspace',
23
+ description: 'Add RDF triples to a paranet\'s workspace. Use when the user asks to save, add, or remember something. Use paranetId "agent-memory" for personal notes. For literal values use plain strings (e.g. "Tesla"). For URIs use full URIs (e.g. "http://example.org/Tesla").',
24
+ parameters: {
25
+ type: 'object',
26
+ properties: {
27
+ paranetId: { type: 'string', description: 'Paranet id (e.g. agent-memory)' },
28
+ quads: {
29
+ type: 'array',
30
+ description: 'Array of RDF triples to write',
31
+ items: {
32
+ type: 'object',
33
+ properties: {
34
+ subject: { type: 'string', description: 'Subject URI' },
35
+ predicate: { type: 'string', description: 'Predicate URI' },
36
+ object: { type: 'string', description: 'Object URI or literal string value' },
37
+ graph: { type: 'string', description: 'Named graph (use empty string for default)' },
38
+ },
39
+ required: ['subject', 'predicate', 'object'],
40
+ },
41
+ },
42
+ },
43
+ required: ['paranetId', 'quads'],
44
+ },
45
+ },
46
+ },
47
+ {
48
+ type: 'function',
49
+ function: {
50
+ name: 'dkg_list_paranets',
51
+ description: 'List paranets this node knows about.',
52
+ parameters: { type: 'object', properties: {} },
53
+ },
54
+ },
55
+ {
56
+ type: 'function',
57
+ function: {
58
+ name: 'dkg_create_paranet',
59
+ description: 'Create a new paranet (knowledge graph namespace). Use when the user wants to create a new graph/paranet.',
60
+ parameters: {
61
+ type: 'object',
62
+ properties: {
63
+ id: { type: 'string', description: 'Paranet id (e.g. my-data)' },
64
+ name: { type: 'string', description: 'Human-readable name' },
65
+ description: { type: 'string', description: 'Optional description' },
66
+ },
67
+ required: ['id', 'name'],
68
+ },
69
+ },
70
+ },
71
+ {
72
+ type: 'function',
73
+ function: {
74
+ name: 'dkg_enshrine',
75
+ description: 'Promote workspace content to the chain (finalize). Use when the user asks to finalize, publish to chain, or make data permanent.',
76
+ parameters: {
77
+ type: 'object',
78
+ properties: {
79
+ paranetId: { type: 'string' },
80
+ selection: { type: 'string', description: '"all" or comma-separated root entity URIs' },
81
+ },
82
+ required: ['paranetId', 'selection'],
83
+ },
84
+ },
85
+ },
86
+ ];
87
+ /**
88
+ * Chat assistant: rule-based answers plus optional LLM for natural language,
89
+ * NL→SPARQL, and DKG API tools (query, write to workspace, list/create paranets, enshrine).
90
+ */
91
+ export class ChatAssistant {
92
+ db;
93
+ queryFn;
94
+ agentTools;
95
+ llmConfig;
96
+ llmClient = new LlmClient();
97
+ constructor(db, queryFn, llmConfig, agentTools) {
98
+ this.db = db;
99
+ this.queryFn = queryFn;
100
+ this.agentTools = agentTools;
101
+ this.llmConfig = llmConfig;
102
+ }
103
+ updateLlmConfig(llmConfig) {
104
+ this.llmConfig = llmConfig;
105
+ }
106
+ getLlmConfig() {
107
+ return {
108
+ configured: !!this.llmConfig?.apiKey,
109
+ model: this.llmConfig?.model,
110
+ baseURL: this.llmConfig?.baseURL,
111
+ };
112
+ }
113
+ systemPrompt() {
114
+ let p = this.llmConfig?.systemPrompt ?? `You are a friendly DKG node assistant. Chat naturally and helpfully. The user's conversations with you are stored privately as memories in their DKG.
115
+
116
+ - Prefer normal, conversational replies. Do not suggest SPARQL or ask if they want to run a query unless they explicitly ask to query the graph or run SPARQL.
117
+ - For questions about the node (uptime, peers, triples, CPU, memory, operations, logs), give a short direct answer.
118
+ - Only output a SPARQL query in a markdown code block when the user clearly asks to query the knowledge graph or to "run a query" or "show data". Use this format exactly:
119
+ \`\`\`sparql
120
+ SELECT ...
121
+ \`\`\`
122
+ Otherwise, just answer in plain language. Keep any queries read-only (SELECT, CONSTRUCT, ASK, DESCRIBE) and add LIMIT 50 or similar.`;
123
+ if (this.agentTools) {
124
+ p += `
125
+
126
+ You have DKG tools: dkg_query (read graph), dkg_write_to_workspace (add/save triples), dkg_list_paranets, dkg_create_paranet, dkg_enshrine (finalize workspace to chain). When the user asks to save, add, or remember something in the DKG, use dkg_write_to_workspace with paranetId "agent-memory" for personal knowledge. Use proper RDF URIs (e.g. http://schema.org/name for "name"). For literals use quoted strings like "value".
127
+
128
+ The user may have imported memories from other AI assistants (Claude, ChatGPT, Gemini). These are stored in the "agent-memory" paranet. To query them, ALWAYS use dkg_query with paranetId "agent-memory". The data model:
129
+ - Import batches: type <http://dkg.io/ontology/MemoryImport>, with predicates <http://dkg.io/ontology/importSource> (e.g. "claude"), <http://schema.org/dateCreated>, <http://dkg.io/ontology/itemCount>
130
+ - Individual memories: type <http://dkg.io/ontology/ImportedMemory>, with predicates <http://schema.org/text> (the memory content), <http://dkg.io/ontology/category> (preference/fact/context/instruction/relationship), <http://dkg.io/ontology/importBatch> (links to batch), <http://dkg.io/ontology/importSource>
131
+ - Extracted entities: URIs starting with urn:dkg:entity:, typed with schema.org types
132
+
133
+ Example queries:
134
+ - List all imported memories: SELECT ?text ?category WHERE { ?m a <http://dkg.io/ontology/ImportedMemory> ; <http://schema.org/text> ?text ; <http://dkg.io/ontology/category> ?category } LIMIT 50
135
+ - List import batches: SELECT ?batch ?source ?date ?count WHERE { ?batch a <http://dkg.io/ontology/MemoryImport> ; <http://dkg.io/ontology/importSource> ?source ; <http://schema.org/dateCreated> ?date ; <http://dkg.io/ontology/itemCount> ?count } ORDER BY DESC(?date) LIMIT 10
136
+ - Search memories by text: SELECT ?text WHERE { ?m a <http://dkg.io/ontology/ImportedMemory> ; <http://schema.org/text> ?text FILTER(CONTAINS(LCASE(?text), "dark mode")) } LIMIT 20
137
+
138
+ When the user asks about their memories, imported memories, preferences, or what you know about them, use dkg_query to look up their imported memories from the agent-memory paranet. Do NOT say you can't access memories — always try the query first.`;
139
+ }
140
+ return p;
141
+ }
142
+ async executeTool(name, args) {
143
+ if (!this.agentTools)
144
+ return { result: null, summary: 'Tools not available' };
145
+ try {
146
+ switch (name) {
147
+ case 'dkg_query': {
148
+ const sparql = String(args.sparql ?? '');
149
+ const paranetId = args.paranetId != null ? String(args.paranetId) : undefined;
150
+ const res = await this.agentTools.query(sparql, { paranetId, includeWorkspace: paranetId === 'agent-memory' });
151
+ const bindings = res?.result?.bindings ?? res?.bindings ?? [];
152
+ return { result: bindings, summary: `Query returned ${bindings.length} result(s).` };
153
+ }
154
+ case 'dkg_write_to_workspace': {
155
+ const paranetId = String(args.paranetId ?? '');
156
+ let raw = args.quads;
157
+ if (typeof raw === 'string') {
158
+ raw = parseQuadsJson(raw);
159
+ }
160
+ const quads = Array.isArray(raw) ? raw.map((q) => ({
161
+ subject: String(q.subject ?? ''),
162
+ predicate: String(q.predicate ?? ''),
163
+ object: String(q.object ?? ''),
164
+ graph: typeof q.graph === 'string' ? q.graph : '',
165
+ })).filter(q => q.subject && q.predicate && q.object) : [];
166
+ if (quads.length === 0) {
167
+ return { result: { error: 'No valid quads to write' }, summary: 'No valid quads could be parsed from the input.' };
168
+ }
169
+ const { workspaceOperationId } = await this.agentTools.writeToWorkspace(paranetId, quads);
170
+ return { result: { workspaceOperationId, tripleCount: quads.length }, summary: `Successfully wrote ${quads.length} triple(s) to workspace (paranet: ${paranetId}).` };
171
+ }
172
+ case 'dkg_list_paranets': {
173
+ const list = await this.agentTools.listParanets();
174
+ const arr = Array.isArray(list) ? list : [];
175
+ return { result: arr, summary: `Found ${arr.length} paranet(s).` };
176
+ }
177
+ case 'dkg_create_paranet': {
178
+ await this.agentTools.createParanet({
179
+ id: String(args.id ?? ''),
180
+ name: String(args.name ?? ''),
181
+ description: args.description != null ? String(args.description) : undefined,
182
+ });
183
+ return { result: { id: args.id, name: args.name }, summary: `Created paranet "${args.name}" (${args.id}).` };
184
+ }
185
+ case 'dkg_enshrine': {
186
+ const paranetId = String(args.paranetId ?? '');
187
+ const sel = String(args.selection ?? 'all');
188
+ const selection = sel === 'all' ? 'all' : { rootEntities: sel.split(',').map(s => s.trim()).filter(Boolean) };
189
+ const res = await this.agentTools.enshrineFromWorkspace(paranetId, selection);
190
+ return { result: res, summary: `Enshrined workspace content for paranet ${paranetId}.` };
191
+ }
192
+ default:
193
+ return { result: null, summary: `Unknown tool: ${name}` };
194
+ }
195
+ }
196
+ catch (err) {
197
+ const msg = err?.message ?? String(err);
198
+ return { result: { error: msg }, summary: `Error executing ${name}: ${msg}` };
199
+ }
200
+ }
201
+ async callLlm(userMessage, toolCallsCollector) {
202
+ if (!this.llmConfig)
203
+ throw new Error('LLM not configured');
204
+ const caps = this.llmClient.resolveCapabilities(this.llmConfig);
205
+ const messages = [
206
+ { role: 'system', content: this.systemPrompt() },
207
+ { role: 'user', content: userMessage },
208
+ ];
209
+ const maxToolRounds = 3;
210
+ let round = 0;
211
+ let lastContent = '';
212
+ while (round < maxToolRounds) {
213
+ const completion = await this.llmClient.complete({
214
+ config: this.llmConfig,
215
+ request: {
216
+ messages,
217
+ tools: this.agentTools ? DKG_TOOLS : undefined,
218
+ toolChoice: this.agentTools ? 'auto' : undefined,
219
+ maxTokens: caps.supportsMaxTokens ? 1024 : undefined,
220
+ stream: false,
221
+ },
222
+ });
223
+ const msg = completion.message;
224
+ if (!msg)
225
+ throw new Error('Empty LLM response');
226
+ lastContent = (msg.content ?? '').trim();
227
+ const toolCalls = msg.tool_calls;
228
+ if (!toolCalls || toolCalls.length === 0) {
229
+ if (lastContent)
230
+ return lastContent;
231
+ continue;
232
+ }
233
+ messages.push({
234
+ role: 'assistant',
235
+ content: msg.content ?? undefined,
236
+ tool_calls: toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: tc.function })),
237
+ });
238
+ for (const tc of toolCalls) {
239
+ const name = tc.function?.name ?? '';
240
+ let args = {};
241
+ try {
242
+ args = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : {};
243
+ }
244
+ catch {
245
+ args = {};
246
+ }
247
+ const { result, summary } = await this.executeTool(name, args);
248
+ if (toolCallsCollector)
249
+ toolCallsCollector.push({ name, args, result });
250
+ messages.push({
251
+ role: 'tool',
252
+ tool_call_id: tc.id,
253
+ content: typeof summary === 'string' ? summary : JSON.stringify(result ?? summary),
254
+ });
255
+ }
256
+ round++;
257
+ }
258
+ return lastContent || 'I used the available tools but could not produce a final reply. Please try rephrasing.';
259
+ }
260
+ extractSparql(text) {
261
+ const match = text.match(/```(?:sparql)?\s*([\s\S]*?)```/);
262
+ return match ? match[1].trim() : null;
263
+ }
264
+ looksLikeAction(q) {
265
+ return /\b(create|add|save|store|write|publish|remember|enshrine|finalize|make|build|generate|insert|update|delete|remove)\b/.test(q);
266
+ }
267
+ shouldUseRulePath(q) {
268
+ return (matches(q, ['uptime', 'how long', 'running for', 'up for']) ||
269
+ matches(q, ['how many peers', 'peer count', 'connected peers', 'connections']) ||
270
+ matches(q, ['how many triples', 'triple count', 'total triples', 'graph size']) ||
271
+ matches(q, ['cpu', 'memory', 'ram', 'resource', 'hardware', 'system health']) ||
272
+ matches(q, ['failed', 'errors', 'error rate', 'failures']) ||
273
+ matches(q, ['operations', 'how many operations', 'operation count', 'what did', 'processed']) ||
274
+ matches(q, ['paranet', 'which paranet', 'subscribed']) ||
275
+ matches(q, ['agent', 'who is', 'which agents', 'discovered agents', 'nodes on']) ||
276
+ (q.includes('kc') && (q.includes('testing') || q.includes('paranet'))) ||
277
+ matches(q, ['store', 'disk', 'storage', 'how big']) ||
278
+ matches(q, ['recent log', 'last log', 'show log', 'latest log']) ||
279
+ q.startsWith('select') ||
280
+ q.startsWith('ask') ||
281
+ q.startsWith('construct') ||
282
+ q.startsWith('describe'));
283
+ }
284
+ compatibilityHintFromError(message) {
285
+ const lower = message.toLowerCase();
286
+ if (lower.includes('temperature')) {
287
+ return 'This model rejects "temperature". Leave it unset or pick a model that supports it.';
288
+ }
289
+ if (lower.includes('max_tokens') || lower.includes('max completion')) {
290
+ return 'This model rejects token-limit parameters. Leave token limit unset for this model.';
291
+ }
292
+ if (lower.includes('unsupported') || lower.includes('invalid_request_error') || lower.includes('unknown parameter')) {
293
+ return 'The provider rejected at least one request field. Check Base URL/model compatibility.';
294
+ }
295
+ return undefined;
296
+ }
297
+ toLlmDiagnostics(err) {
298
+ const message = err instanceof Error ? err.message : String(err);
299
+ const diagnostics = { message };
300
+ if (err instanceof LlmRequestError) {
301
+ diagnostics.status = err.status;
302
+ diagnostics.provider = err.provider;
303
+ diagnostics.model = err.model;
304
+ }
305
+ const hint = this.compatibilityHintFromError(message);
306
+ if (hint)
307
+ diagnostics.compatibilityHint = hint;
308
+ return diagnostics;
309
+ }
310
+ async *answerStream(req) {
311
+ const q = req.message.toLowerCase().trim();
312
+ if (!this.llmConfig || (this.agentTools && this.looksLikeAction(q)) || this.shouldUseRulePath(q)) {
313
+ const response = await this.answer(req);
314
+ const responseMode = response.responseMode ?? 'rule-based';
315
+ yield {
316
+ type: 'final',
317
+ response: { ...response, responseMode },
318
+ responseMode,
319
+ };
320
+ return;
321
+ }
322
+ const caps = this.llmClient.resolveCapabilities(this.llmConfig);
323
+ const messages = [
324
+ { role: 'system', content: this.systemPrompt() },
325
+ { role: 'user', content: req.message },
326
+ ];
327
+ try {
328
+ const stream = this.llmClient.stream({
329
+ config: this.llmConfig,
330
+ request: {
331
+ messages,
332
+ tools: this.agentTools ? DKG_TOOLS : undefined,
333
+ toolChoice: this.agentTools ? 'auto' : undefined,
334
+ maxTokens: caps.supportsMaxTokens ? 1024 : undefined,
335
+ stream: true,
336
+ },
337
+ });
338
+ let text = '';
339
+ let responseMode = 'blocking';
340
+ let finalToolCalls;
341
+ for await (const event of stream) {
342
+ if (event.type === 'text_delta' && event.delta) {
343
+ text += event.delta;
344
+ yield {
345
+ type: 'text_delta',
346
+ delta: event.delta,
347
+ };
348
+ continue;
349
+ }
350
+ if (event.type === 'final') {
351
+ responseMode = event.mode;
352
+ finalToolCalls = event.message.tool_calls;
353
+ const finalContent = (event.message.content ?? text).trim();
354
+ if (finalContent)
355
+ text = finalContent;
356
+ }
357
+ }
358
+ if (finalToolCalls && finalToolCalls.length > 0) {
359
+ // Preserve tool semantics from the existing blocking tool loop.
360
+ const response = await this.llmAnswer(req);
361
+ const mode = response.responseMode ?? 'blocking';
362
+ yield {
363
+ type: 'final',
364
+ response: { ...response, responseMode: mode },
365
+ responseMode: mode,
366
+ };
367
+ return;
368
+ }
369
+ let reply = text.trim();
370
+ if (!reply)
371
+ throw new Error('Empty LLM response');
372
+ const sparql = this.extractSparql(reply);
373
+ if (sparql) {
374
+ try {
375
+ const result = await this.queryFn(sparql);
376
+ const count = result.bindings?.length ?? 0;
377
+ reply += `\n\n**Executed query** (${count} result(s)):`; // keep parity with blocking path
378
+ const response = {
379
+ reply,
380
+ data: result.bindings,
381
+ sparql,
382
+ responseMode,
383
+ };
384
+ yield { type: 'final', response, responseMode };
385
+ return;
386
+ }
387
+ catch (err) {
388
+ reply += `\n\nSPARQL execution failed: ${err.message}`;
389
+ const response = { reply, sparql, responseMode };
390
+ yield { type: 'final', response, responseMode };
391
+ return;
392
+ }
393
+ }
394
+ yield {
395
+ type: 'final',
396
+ response: { reply, responseMode },
397
+ responseMode,
398
+ };
399
+ }
400
+ catch (err) {
401
+ const diagnostics = this.toLlmDiagnostics(err);
402
+ const response = {
403
+ reply: `LLM error: ${diagnostics.message}. ${HELP_REPLY}`,
404
+ llmDiagnostics: diagnostics,
405
+ responseMode: 'blocking',
406
+ };
407
+ yield {
408
+ type: 'final',
409
+ response,
410
+ responseMode: 'blocking',
411
+ };
412
+ }
413
+ }
414
+ async answer(req) {
415
+ const q = req.message.toLowerCase().trim();
416
+ // If LLM + tools are available and the message looks like an action,
417
+ // skip rule-based handlers and let the LLM decide which tools to call.
418
+ if (this.llmConfig && this.agentTools && this.looksLikeAction(q)) {
419
+ return this.llmAnswer(req);
420
+ }
421
+ // --- Uptime ---
422
+ if (matches(q, ['uptime', 'how long', 'running for', 'up for'])) {
423
+ const snap = this.db.getLatestSnapshot();
424
+ if (!snap)
425
+ return { reply: 'No metrics collected yet. The node may have just started.' };
426
+ const dur = formatSeconds(snap.uptime_seconds ?? 0);
427
+ return { reply: `The node has been running for **${dur}**.`, data: { uptimeSeconds: snap.uptime_seconds } };
428
+ }
429
+ // --- Peer count ---
430
+ if (matches(q, ['how many peers', 'peer count', 'connected peers', 'connections'])) {
431
+ const snap = this.db.getLatestSnapshot();
432
+ if (!snap)
433
+ return { reply: 'No metrics yet.' };
434
+ return {
435
+ reply: `Currently connected to **${snap.peer_count}** peer(s): ${snap.direct_peers} direct, ${snap.relayed_peers} relayed.`,
436
+ data: { peers: snap.peer_count, direct: snap.direct_peers, relayed: snap.relayed_peers },
437
+ };
438
+ }
439
+ // --- Triple count ---
440
+ if (matches(q, ['how many triples', 'triple count', 'total triples', 'graph size'])) {
441
+ const snap = this.db.getLatestSnapshot();
442
+ if (!snap)
443
+ return { reply: 'No metrics yet.' };
444
+ return {
445
+ reply: `The knowledge graph contains **${snap.total_triples ?? 0}** triples across **${snap.total_kcs ?? 0}** Knowledge Collections and **${snap.total_kas ?? 0}** Knowledge Assets.`,
446
+ data: { triples: snap.total_triples, kcs: snap.total_kcs, kas: snap.total_kas },
447
+ };
448
+ }
449
+ // --- CPU / memory ---
450
+ if (matches(q, ['cpu', 'memory', 'ram', 'resource', 'hardware', 'system health'])) {
451
+ const snap = this.db.getLatestSnapshot();
452
+ if (!snap)
453
+ return { reply: 'No metrics yet.' };
454
+ const memMb = snap.mem_used_bytes ? Math.round(snap.mem_used_bytes / 1048576) : '?';
455
+ const totalMb = snap.mem_total_bytes ? Math.round(snap.mem_total_bytes / 1048576) : '?';
456
+ return {
457
+ reply: `**CPU**: ${snap.cpu_percent ?? 0}% \n**Memory**: ${memMb} MB / ${totalMb} MB \n**Heap**: ${snap.heap_used_bytes ? Math.round(snap.heap_used_bytes / 1048576) : '?'} MB`,
458
+ data: { cpu: snap.cpu_percent, memUsed: snap.mem_used_bytes, memTotal: snap.mem_total_bytes },
459
+ };
460
+ }
461
+ // --- Failed operations ---
462
+ if (matches(q, ['failed', 'errors', 'error rate', 'failures'])) {
463
+ const { operations, total } = this.db.getOperations({ status: 'error', limit: 5 });
464
+ if (total === 0)
465
+ return { reply: 'No failed operations found. Everything is running smoothly.' };
466
+ const lines = operations.map(o => `- **${o.operation_name}** (${timeAgo(o.started_at)}): ${o.error_message}`).join('\n');
467
+ return {
468
+ reply: `Found **${total}** failed operation(s). Most recent:\n\n${lines}`,
469
+ data: { total, recent: operations },
470
+ };
471
+ }
472
+ // --- Operations summary ---
473
+ if (matches(q, ['operations', 'how many operations', 'operation count', 'what did', 'processed'])) {
474
+ const { total: allTotal } = this.db.getOperations({});
475
+ const { total: pubTotal } = this.db.getOperations({ name: 'publish' });
476
+ const { total: queryTotal } = this.db.getOperations({ name: 'query' });
477
+ const { total: syncTotal } = this.db.getOperations({ name: 'sync' });
478
+ return {
479
+ reply: `**${allTotal}** total operations: ${pubTotal} publishes, ${queryTotal} queries, ${syncTotal} syncs.`,
480
+ data: { total: allTotal, publish: pubTotal, query: queryTotal, sync: syncTotal },
481
+ };
482
+ }
483
+ // --- Paranets ---
484
+ if (matches(q, ['paranet', 'which paranet', 'subscribed'])) {
485
+ const sparql = `SELECT DISTINCT ?id ?name WHERE { ?p <https://schema.org/name> ?name . ?p <urn:dkg:paranetId> ?id }`;
486
+ try {
487
+ const result = await this.queryFn(sparql);
488
+ if (result.bindings.length === 0)
489
+ return { reply: 'No paranets found in the store.', sparql };
490
+ const list = result.bindings.map(b => `- **${b.name}** (\`${b.id}\`)`).join('\n');
491
+ return { reply: `Known paranets:\n\n${list}`, data: result.bindings, sparql };
492
+ }
493
+ catch {
494
+ return { reply: 'Could not query paranets from the triple store.' };
495
+ }
496
+ }
497
+ // --- Agents ---
498
+ if (matches(q, ['agent', 'who is', 'which agents', 'discovered agents', 'nodes on'])) {
499
+ const sparql = `SELECT ?name ?peerId WHERE { ?a <urn:dkg:agentName> ?name ; <urn:dkg:peerId> ?peerId }`;
500
+ try {
501
+ const result = await this.queryFn(sparql);
502
+ if (result.bindings.length === 0)
503
+ return { reply: 'No agents discovered yet.', sparql };
504
+ const list = result.bindings.map(b => `- **${b.name}** (\`${b.peerId?.slice(0, 16)}...\`)`).join('\n');
505
+ return { reply: `Discovered **${result.bindings.length}** agent(s):\n\n${list}`, data: result.bindings, sparql };
506
+ }
507
+ catch {
508
+ return { reply: 'Could not query agents from the triple store.' };
509
+ }
510
+ }
511
+ // --- KCs in a paranet ---
512
+ if (q.includes('kc') && (q.includes('testing') || q.includes('paranet'))) {
513
+ const paranet = q.includes('testing') ? 'testing' : 'agents';
514
+ const sparql = `SELECT ?kc ?status WHERE { GRAPH <urn:dkg:paranet:${paranet}:meta> { ?kc a <urn:dkg:KC> . OPTIONAL { ?kc <urn:dkg:status> ?status } } } LIMIT 20`;
515
+ try {
516
+ const result = await this.queryFn(sparql);
517
+ if (result.bindings.length === 0)
518
+ return { reply: `No KCs found in the "${paranet}" paranet.`, sparql };
519
+ return {
520
+ reply: `Found **${result.bindings.length}** KC(s) in the "${paranet}" paranet.`,
521
+ data: result.bindings,
522
+ sparql,
523
+ };
524
+ }
525
+ catch {
526
+ return { reply: `Could not query KCs for paranet "${paranet}".` };
527
+ }
528
+ }
529
+ // --- Store / disk ---
530
+ if (matches(q, ['store', 'disk', 'storage', 'how big'])) {
531
+ const snap = this.db.getLatestSnapshot();
532
+ if (!snap)
533
+ return { reply: 'No metrics yet.' };
534
+ const storeMb = snap.store_bytes ? (snap.store_bytes / 1048576).toFixed(2) : '?';
535
+ const diskUsedGb = snap.disk_used_bytes ? (snap.disk_used_bytes / 1073741824).toFixed(1) : '?';
536
+ const diskTotalGb = snap.disk_total_bytes ? (snap.disk_total_bytes / 1073741824).toFixed(1) : '?';
537
+ return {
538
+ reply: `**Triple store**: ${storeMb} MB \n**Disk**: ${diskUsedGb} GB / ${diskTotalGb} GB used`,
539
+ data: { storeBytes: snap.store_bytes, diskUsed: snap.disk_used_bytes, diskTotal: snap.disk_total_bytes },
540
+ };
541
+ }
542
+ // --- Recent logs ---
543
+ if (matches(q, ['recent log', 'last log', 'show log', 'latest log'])) {
544
+ const { logs } = this.db.searchLogs({ limit: 10 });
545
+ if (logs.length === 0)
546
+ return { reply: 'No logs recorded yet.' };
547
+ const lines = logs.map(l => `\`${new Date(l.ts).toLocaleTimeString()}\` **${l.level}** [${l.module}] ${l.message}`).join('\n');
548
+ return { reply: `Last ${logs.length} log entries:\n\n${lines}`, data: logs };
549
+ }
550
+ // --- Fallback: try running as SPARQL ---
551
+ if (q.startsWith('select') || q.startsWith('ask') || q.startsWith('construct') || q.startsWith('describe')) {
552
+ try {
553
+ const result = await this.queryFn(req.message);
554
+ const count = result.bindings?.length ?? 0;
555
+ return {
556
+ reply: `Query returned **${count}** result(s).`,
557
+ data: result.bindings,
558
+ sparql: req.message,
559
+ };
560
+ }
561
+ catch (err) {
562
+ return { reply: `SPARQL error: ${err.message}`, sparql: req.message };
563
+ }
564
+ }
565
+ // --- Help or LLM ---
566
+ if (this.llmConfig) {
567
+ return this.llmAnswer(req);
568
+ }
569
+ return { reply: HELP_REPLY };
570
+ }
571
+ async llmAnswer(req) {
572
+ try {
573
+ const toolCallsCollector = [];
574
+ let reply = await this.callLlm(req.message, toolCallsCollector);
575
+ const sparql = this.extractSparql(reply);
576
+ if (sparql) {
577
+ try {
578
+ const result = await this.queryFn(sparql);
579
+ const count = result.bindings?.length ?? 0;
580
+ reply += `\n\n**Executed query** (${count} result(s)):`;
581
+ return {
582
+ reply,
583
+ data: result.bindings,
584
+ sparql,
585
+ toolCalls: toolCallsCollector.length ? toolCallsCollector : undefined,
586
+ responseMode: 'blocking',
587
+ };
588
+ }
589
+ catch (err) {
590
+ reply += `\n\nSPARQL execution failed: ${err.message}`;
591
+ return { reply, sparql, responseMode: 'blocking' };
592
+ }
593
+ }
594
+ return {
595
+ reply,
596
+ toolCalls: toolCallsCollector.length ? toolCallsCollector : undefined,
597
+ responseMode: 'blocking',
598
+ };
599
+ }
600
+ catch (err) {
601
+ const diagnostics = this.toLlmDiagnostics(err);
602
+ return {
603
+ reply: `LLM error: ${diagnostics.message}. ${HELP_REPLY}`,
604
+ llmDiagnostics: diagnostics,
605
+ responseMode: 'blocking',
606
+ };
607
+ }
608
+ }
609
+ }
610
+ function matches(input, keywords) {
611
+ return keywords.some(k => input.includes(k));
612
+ }
613
+ /** Robustly parse quads JSON from LLM output, handling common malformations. */
614
+ function parseQuadsJson(raw) {
615
+ // Try direct parse first
616
+ try {
617
+ const r = JSON.parse(raw);
618
+ if (Array.isArray(r))
619
+ return r;
620
+ }
621
+ catch { }
622
+ // LLMs sometimes produce values like ""Tesla"" — fix unescaped inner quotes
623
+ try {
624
+ const fixed = raw.replace(/""{1,2}([^"]+)""{1,2}/g, (match, inner) => {
625
+ if (match.startsWith('""') && match.endsWith('""'))
626
+ return `"${inner}"`;
627
+ return match;
628
+ });
629
+ const r = JSON.parse(fixed);
630
+ if (Array.isArray(r))
631
+ return r;
632
+ }
633
+ catch { }
634
+ // Try stripping markdown fences
635
+ try {
636
+ const stripped = raw.replace(/^```(?:json)?\n?/i, '').replace(/\n?```$/i, '').trim();
637
+ const r = JSON.parse(stripped);
638
+ if (Array.isArray(r))
639
+ return r;
640
+ }
641
+ catch { }
642
+ return [];
643
+ }
644
+ function formatSeconds(s) {
645
+ if (s < 60)
646
+ return `${s}s`;
647
+ if (s < 3600)
648
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
649
+ const h = Math.floor(s / 3600);
650
+ const m = Math.floor((s % 3600) / 60);
651
+ return `${h}h ${m}m`;
652
+ }
653
+ function timeAgo(ts) {
654
+ const diff = Date.now() - ts;
655
+ if (diff < 60_000)
656
+ return 'just now';
657
+ if (diff < 3_600_000)
658
+ return `${Math.floor(diff / 60_000)}m ago`;
659
+ if (diff < 86_400_000)
660
+ return `${Math.floor(diff / 3_600_000)}h ago`;
661
+ return `${Math.floor(diff / 86_400_000)}d ago`;
662
+ }
663
+ //# sourceMappingURL=chat-assistant.js.map