neoagent 2.3.1-beta.98 → 2.4.0
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/.env.example +6 -3
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_integrations.dart +21 -2
- package/flutter_app/lib/main_models.dart +60 -0
- package/flutter_app/lib/main_theme.dart +31 -2
- package/flutter_app/macos/Runner/AppDelegate.swift +11 -1
- package/flutter_app/macos/Runner/DebugProfile.entitlements +4 -0
- package/flutter_app/macos/Runner/Release.entitlements +4 -0
- package/flutter_app/pubspec.lock +5 -5
- package/lib/manager.js +164 -2
- package/package.json +1 -1
- package/server/db/database.js +85 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +971 -1066
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/shaders/ink_sparkle.frag +1 -1
- package/server/public/assets/shaders/stretch_effect.frag +1 -1
- package/server/public/canvaskit/canvaskit.js +2 -2
- package/server/public/canvaskit/canvaskit.js.symbols +11796 -11733
- package/server/public/canvaskit/canvaskit.wasm +0 -0
- package/server/public/canvaskit/chromium/canvaskit.js +2 -2
- package/server/public/canvaskit/chromium/canvaskit.js.symbols +10706 -10643
- package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js +171 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.js.symbols +9134 -0
- package/server/public/canvaskit/experimental_webparagraph/canvaskit.wasm +0 -0
- package/server/public/canvaskit/skwasm.js +14 -14
- package/server/public/canvaskit/skwasm.js.symbols +12787 -12676
- package/server/public/canvaskit/skwasm.wasm +0 -0
- package/server/public/canvaskit/skwasm_heavy.js +14 -14
- package/server/public/canvaskit/skwasm_heavy.js.symbols +14400 -14286
- package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
- package/server/public/canvaskit/wimp.js +94 -95
- package/server/public/canvaskit/wimp.js.symbols +11325 -11177
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +83866 -82074
- package/server/routes/integrations.js +2 -2
- package/server/routes/memory.js +73 -0
- package/server/services/ai/engine.js +65 -26
- package/server/services/ai/models.js +21 -0
- package/server/services/ai/preModelCompaction.js +191 -0
- package/server/services/ai/providers/claudeCode.js +273 -0
- package/server/services/ai/providers/openaiCodex.js +226 -41
- package/server/services/ai/settings.js +11 -1
- package/server/services/integrations/google/provider.js +78 -0
- package/server/services/integrations/manager.js +29 -13
- package/server/services/manager.js +25 -0
- package/server/services/memory/ingestion.js +486 -0
- package/server/services/memory/manager.js +422 -0
- package/server/services/memory/openhuman_uplift.test.js +98 -0
- package/server/services/widgets/focus_widget.js +45 -4
|
@@ -117,6 +117,16 @@ function parseJsonObject(value, fallback = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function parseJsonArray(value, fallback = []) {
|
|
121
|
+
if (Array.isArray(value)) return [...value];
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(String(value || '[]'));
|
|
124
|
+
return Array.isArray(parsed) ? parsed : [...fallback];
|
|
125
|
+
} catch {
|
|
126
|
+
return [...fallback];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
function computeFreshnessMultiplier(row) {
|
|
121
131
|
const staleAfterDays = Number(row?.stale_after_days);
|
|
122
132
|
if (!Number.isFinite(staleAfterDays) || staleAfterDays <= 0) return 1;
|
|
@@ -194,6 +204,14 @@ function tokenizeRecallQuery(query) {
|
|
|
194
204
|
.slice(0, 12);
|
|
195
205
|
}
|
|
196
206
|
|
|
207
|
+
function normalizeStringArray(value, maxItems = 24, maxLength = 160) {
|
|
208
|
+
return [...new Set(
|
|
209
|
+
(Array.isArray(value) ? value : [])
|
|
210
|
+
.map((item) => String(item || '').trim().slice(0, maxLength))
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
)].slice(0, maxItems);
|
|
213
|
+
}
|
|
214
|
+
|
|
197
215
|
function scoreSchedulerRunMatch(queryTokens, title, finalResponse) {
|
|
198
216
|
if (!queryTokens.length) return 0;
|
|
199
217
|
const haystack = `${String(title || '')} ${String(finalResponse || '')}`.toLowerCase();
|
|
@@ -313,6 +331,410 @@ class MemoryManager {
|
|
|
313
331
|
return this.getAssistantSelfState(userId, { agentId });
|
|
314
332
|
}
|
|
315
333
|
|
|
334
|
+
listIngestionJobs(userId, { agentId = null, sourceType = null, providerKey = null, limit = 25 } = {}) {
|
|
335
|
+
const scopedAgentId = this._agentId(userId, { agentId });
|
|
336
|
+
let sql = `SELECT *
|
|
337
|
+
FROM memory_ingestion_jobs
|
|
338
|
+
WHERE user_id = ? AND agent_id = ?`;
|
|
339
|
+
const params = [userId, scopedAgentId];
|
|
340
|
+
if (sourceType) {
|
|
341
|
+
sql += ' AND source_type = ?';
|
|
342
|
+
params.push(String(sourceType).trim());
|
|
343
|
+
}
|
|
344
|
+
if (providerKey) {
|
|
345
|
+
sql += ' AND provider_key = ?';
|
|
346
|
+
params.push(String(providerKey).trim());
|
|
347
|
+
}
|
|
348
|
+
sql += ' ORDER BY updated_at DESC LIMIT ?';
|
|
349
|
+
params.push(Math.max(1, Math.min(Number(limit) || 25, 100)));
|
|
350
|
+
return db.prepare(sql).all(...params).map((row) => ({
|
|
351
|
+
id: row.id,
|
|
352
|
+
sourceType: row.source_type,
|
|
353
|
+
providerKey: row.provider_key || null,
|
|
354
|
+
connectionId: row.connection_id == null ? null : Number(row.connection_id),
|
|
355
|
+
status: row.status || 'pending',
|
|
356
|
+
freshnessPolicy: parseJsonObject(row.freshness_policy_json, {}),
|
|
357
|
+
cursor: parseJsonObject(row.cursor_json, {}),
|
|
358
|
+
summary: parseJsonObject(row.summary_json, {}),
|
|
359
|
+
metadata: parseJsonObject(row.metadata_json, {}),
|
|
360
|
+
documentCount: Number(row.document_count || 0),
|
|
361
|
+
error: row.error_text || null,
|
|
362
|
+
startedAt: row.started_at || null,
|
|
363
|
+
completedAt: row.completed_at || null,
|
|
364
|
+
nextSyncAt: row.next_sync_at || null,
|
|
365
|
+
updatedAt: row.updated_at || null,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
recordIngestionJob(userId, job = {}, options = {}) {
|
|
370
|
+
const scopedAgentId = this._agentId(userId, options);
|
|
371
|
+
const jobId = String(job.id || uuidv4()).trim();
|
|
372
|
+
db.prepare(
|
|
373
|
+
`INSERT INTO memory_ingestion_jobs (
|
|
374
|
+
id, user_id, agent_id, source_type, provider_key, connection_id, status,
|
|
375
|
+
freshness_policy_json, cursor_json, summary_json, metadata_json, document_count,
|
|
376
|
+
error_text, started_at, completed_at, next_sync_at, created_at, updated_at
|
|
377
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), ?, ?, datetime('now'), datetime('now'))
|
|
378
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
379
|
+
status = excluded.status,
|
|
380
|
+
freshness_policy_json = excluded.freshness_policy_json,
|
|
381
|
+
cursor_json = excluded.cursor_json,
|
|
382
|
+
summary_json = excluded.summary_json,
|
|
383
|
+
metadata_json = excluded.metadata_json,
|
|
384
|
+
document_count = excluded.document_count,
|
|
385
|
+
error_text = excluded.error_text,
|
|
386
|
+
completed_at = excluded.completed_at,
|
|
387
|
+
next_sync_at = excluded.next_sync_at,
|
|
388
|
+
updated_at = excluded.updated_at`
|
|
389
|
+
).run(
|
|
390
|
+
jobId,
|
|
391
|
+
userId,
|
|
392
|
+
scopedAgentId,
|
|
393
|
+
String(job.sourceType || '').trim() || 'unknown',
|
|
394
|
+
String(job.providerKey || '').trim(),
|
|
395
|
+
Number.isInteger(Number(job.connectionId)) && Number(job.connectionId) > 0 ? Number(job.connectionId) : null,
|
|
396
|
+
String(job.status || 'completed').trim() || 'completed',
|
|
397
|
+
JSON.stringify(parseJsonObject(job.freshnessPolicy, {})),
|
|
398
|
+
JSON.stringify(parseJsonObject(job.cursor, {})),
|
|
399
|
+
JSON.stringify(parseJsonObject(job.summary, {})),
|
|
400
|
+
JSON.stringify(parseJsonObject(job.metadata, {})),
|
|
401
|
+
Number(job.documentCount) || 0,
|
|
402
|
+
String(job.error || '').trim() || null,
|
|
403
|
+
job.startedAt || null,
|
|
404
|
+
job.completedAt || null,
|
|
405
|
+
job.nextSyncAt || null,
|
|
406
|
+
);
|
|
407
|
+
return jobId;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
upsertIngestionDocument(userId, document = {}, options = {}) {
|
|
411
|
+
const scopedAgentId = this._agentId(userId, options);
|
|
412
|
+
const providerKey = String(document.providerKey || '').trim();
|
|
413
|
+
const connectionId = Number.isInteger(Number(document.connectionId)) && Number(document.connectionId) > 0
|
|
414
|
+
? Number(document.connectionId)
|
|
415
|
+
: 0;
|
|
416
|
+
const sourceType = String(document.sourceType || '').trim() || 'unknown';
|
|
417
|
+
const externalObjectId = String(document.externalObjectId || '').trim();
|
|
418
|
+
const content = String(document.content || '').trim();
|
|
419
|
+
if (!externalObjectId || !content) {
|
|
420
|
+
throw new Error('Ingestion documents require externalObjectId and content.');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const existing = db.prepare(
|
|
424
|
+
`SELECT id, metadata_json
|
|
425
|
+
FROM memory_ingestion_documents
|
|
426
|
+
WHERE user_id = ? AND agent_id = ? AND source_type = ? AND provider_key = ? AND connection_id = ? AND external_object_id = ?`
|
|
427
|
+
).get(userId, scopedAgentId, sourceType, providerKey, connectionId, externalObjectId);
|
|
428
|
+
|
|
429
|
+
const docId = existing?.id || uuidv4();
|
|
430
|
+
const nextMetadata = {
|
|
431
|
+
...parseJsonObject(existing?.metadata_json, {}),
|
|
432
|
+
...parseJsonObject(document.metadata, {}),
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
db.prepare(
|
|
436
|
+
`INSERT INTO memory_ingestion_documents (
|
|
437
|
+
id, user_id, agent_id, source_type, normalized_type, provider_key, connection_id,
|
|
438
|
+
external_object_id, source_account, title, content, summary, salience, source_timestamp,
|
|
439
|
+
metadata_json, payload_json, created_at, updated_at
|
|
440
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
441
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
442
|
+
normalized_type = excluded.normalized_type,
|
|
443
|
+
source_account = excluded.source_account,
|
|
444
|
+
title = excluded.title,
|
|
445
|
+
content = excluded.content,
|
|
446
|
+
summary = excluded.summary,
|
|
447
|
+
salience = excluded.salience,
|
|
448
|
+
source_timestamp = excluded.source_timestamp,
|
|
449
|
+
metadata_json = excluded.metadata_json,
|
|
450
|
+
payload_json = excluded.payload_json,
|
|
451
|
+
updated_at = excluded.updated_at`
|
|
452
|
+
).run(
|
|
453
|
+
docId,
|
|
454
|
+
userId,
|
|
455
|
+
scopedAgentId,
|
|
456
|
+
sourceType,
|
|
457
|
+
String(document.normalizedType || sourceType).trim() || sourceType,
|
|
458
|
+
providerKey,
|
|
459
|
+
connectionId,
|
|
460
|
+
externalObjectId,
|
|
461
|
+
String(document.sourceAccount || '').trim() || null,
|
|
462
|
+
String(document.title || '').trim() || null,
|
|
463
|
+
content,
|
|
464
|
+
String(document.summary || '').trim() || null,
|
|
465
|
+
Math.max(1, Math.min(10, Number(document.salience) || 5)),
|
|
466
|
+
document.sourceTimestamp || null,
|
|
467
|
+
JSON.stringify(nextMetadata),
|
|
468
|
+
JSON.stringify(parseJsonObject(document.payload, {})),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return docId;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
listIngestionDocuments(userId, { agentId = null, sourceType = null, providerKey = null, limit = 40 } = {}) {
|
|
475
|
+
const scopedAgentId = this._agentId(userId, { agentId });
|
|
476
|
+
let sql = `SELECT *
|
|
477
|
+
FROM memory_ingestion_documents
|
|
478
|
+
WHERE user_id = ? AND agent_id = ?`;
|
|
479
|
+
const params = [userId, scopedAgentId];
|
|
480
|
+
if (sourceType) {
|
|
481
|
+
sql += ' AND source_type = ?';
|
|
482
|
+
params.push(String(sourceType).trim());
|
|
483
|
+
}
|
|
484
|
+
if (providerKey) {
|
|
485
|
+
sql += ' AND provider_key = ?';
|
|
486
|
+
params.push(String(providerKey).trim());
|
|
487
|
+
}
|
|
488
|
+
sql += ' ORDER BY updated_at DESC LIMIT ?';
|
|
489
|
+
params.push(Math.max(1, Math.min(Number(limit) || 40, 200)));
|
|
490
|
+
return db.prepare(sql).all(...params).map((row) => ({
|
|
491
|
+
id: row.id,
|
|
492
|
+
sourceType: row.source_type,
|
|
493
|
+
normalizedType: row.normalized_type,
|
|
494
|
+
providerKey: row.provider_key || null,
|
|
495
|
+
connectionId: row.connection_id ? Number(row.connection_id) : null,
|
|
496
|
+
externalObjectId: row.external_object_id,
|
|
497
|
+
sourceAccount: row.source_account || null,
|
|
498
|
+
title: row.title || null,
|
|
499
|
+
content: row.content,
|
|
500
|
+
summary: row.summary || null,
|
|
501
|
+
salience: Number(row.salience || 0),
|
|
502
|
+
sourceTimestamp: row.source_timestamp || null,
|
|
503
|
+
metadata: parseJsonObject(row.metadata_json, {}),
|
|
504
|
+
payload: parseJsonObject(row.payload_json, {}),
|
|
505
|
+
createdAt: row.created_at,
|
|
506
|
+
updatedAt: row.updated_at,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
getIngestionOverview(userId, { agentId = null, limit = 12 } = {}) {
|
|
511
|
+
const jobs = this.listIngestionJobs(userId, { agentId, limit });
|
|
512
|
+
const byProvider = new Map();
|
|
513
|
+
for (const job of jobs) {
|
|
514
|
+
const providerKey = job.providerKey || `local:${job.sourceType}`;
|
|
515
|
+
if (!byProvider.has(providerKey)) {
|
|
516
|
+
byProvider.set(providerKey, {
|
|
517
|
+
providerKey,
|
|
518
|
+
sourceTypes: new Set(),
|
|
519
|
+
status: job.status,
|
|
520
|
+
lastRefreshAt: job.completedAt || job.updatedAt || null,
|
|
521
|
+
nextRefreshAt: job.nextSyncAt || null,
|
|
522
|
+
documentCount: 0,
|
|
523
|
+
error: job.error || null,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const current = byProvider.get(providerKey);
|
|
527
|
+
current.sourceTypes.add(job.sourceType);
|
|
528
|
+
current.documentCount += job.documentCount;
|
|
529
|
+
if (current.status !== 'failed' && job.status === 'failed') current.status = 'failed';
|
|
530
|
+
if (!current.error && job.error) current.error = job.error;
|
|
531
|
+
}
|
|
532
|
+
return Array.from(byProvider.values()).map((item) => ({
|
|
533
|
+
providerKey: item.providerKey,
|
|
534
|
+
sourceTypes: Array.from(item.sourceTypes),
|
|
535
|
+
status: item.status,
|
|
536
|
+
lastRefreshAt: item.lastRefreshAt,
|
|
537
|
+
nextRefreshAt: item.nextRefreshAt,
|
|
538
|
+
documentCount: item.documentCount,
|
|
539
|
+
error: item.error,
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
materializeKnowledgeViews(userId, { agentId = null } = {}) {
|
|
544
|
+
const scopedAgentId = this._agentId(userId, { agentId });
|
|
545
|
+
const memories = this.listMemories(userId, { limit: 200, agentId: scopedAgentId });
|
|
546
|
+
const documents = this.listIngestionDocuments(userId, { limit: 200, agentId: scopedAgentId });
|
|
547
|
+
const views = [];
|
|
548
|
+
|
|
549
|
+
const topicGroups = new Map();
|
|
550
|
+
for (const memory of memories) {
|
|
551
|
+
const key = memory.category || 'episodic';
|
|
552
|
+
if (!topicGroups.has(key)) topicGroups.set(key, []);
|
|
553
|
+
topicGroups.get(key).push(memory);
|
|
554
|
+
}
|
|
555
|
+
for (const [topic, items] of topicGroups.entries()) {
|
|
556
|
+
const summary = items.slice(0, 4).map((item) => `- ${item.content}`).join('\n');
|
|
557
|
+
views.push({
|
|
558
|
+
viewType: 'topic',
|
|
559
|
+
subjectKey: topic,
|
|
560
|
+
title: topic.replace(/_/g, ' '),
|
|
561
|
+
summary: summary.slice(0, 320),
|
|
562
|
+
markdownText: `# ${topic}\n\n${summary}`,
|
|
563
|
+
sourceMemoryIds: items.map((item) => item.id),
|
|
564
|
+
sourceDocumentIds: [],
|
|
565
|
+
metadata: {
|
|
566
|
+
itemCount: items.length,
|
|
567
|
+
category: topic,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const accountGroups = new Map();
|
|
573
|
+
for (const doc of documents) {
|
|
574
|
+
const accountKey = `${doc.providerKey || 'local'}:${doc.sourceAccount || 'default'}`;
|
|
575
|
+
if (!accountGroups.has(accountKey)) accountGroups.set(accountKey, []);
|
|
576
|
+
accountGroups.get(accountKey).push(doc);
|
|
577
|
+
}
|
|
578
|
+
for (const [accountKey, items] of accountGroups.entries()) {
|
|
579
|
+
const lead = items[0];
|
|
580
|
+
const lines = items.slice(0, 4).map((item) => `- ${item.title || item.normalizedType}: ${item.summary || item.content}`);
|
|
581
|
+
views.push({
|
|
582
|
+
viewType: 'account',
|
|
583
|
+
subjectKey: accountKey,
|
|
584
|
+
title: `${lead.providerKey || 'local'} ${lead.sourceAccount || 'account'}`,
|
|
585
|
+
summary: lines.join('\n').slice(0, 320),
|
|
586
|
+
markdownText: `# ${lead.providerKey || 'local'} / ${lead.sourceAccount || 'account'}\n\n${lines.join('\n')}`,
|
|
587
|
+
sourceMemoryIds: [],
|
|
588
|
+
sourceDocumentIds: items.map((item) => item.id),
|
|
589
|
+
metadata: {
|
|
590
|
+
providerKey: lead.providerKey || null,
|
|
591
|
+
sourceAccount: lead.sourceAccount || null,
|
|
592
|
+
itemCount: items.length,
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const recentTimeline = [...documents]
|
|
598
|
+
.sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || '')))
|
|
599
|
+
.slice(0, 8);
|
|
600
|
+
if (recentTimeline.length > 0) {
|
|
601
|
+
views.push({
|
|
602
|
+
viewType: 'timeline',
|
|
603
|
+
subjectKey: 'recent',
|
|
604
|
+
title: 'Recent knowledge changes',
|
|
605
|
+
summary: recentTimeline.map((item) => `${item.title || item.normalizedType}: ${item.summary || item.content}`).join(' | ').slice(0, 320),
|
|
606
|
+
markdownText: `# Recent knowledge changes\n\n${recentTimeline.map((item) => `- ${item.title || item.normalizedType}: ${item.summary || item.content}`).join('\n')}`,
|
|
607
|
+
sourceMemoryIds: [],
|
|
608
|
+
sourceDocumentIds: recentTimeline.map((item) => item.id),
|
|
609
|
+
metadata: {
|
|
610
|
+
itemCount: recentTimeline.length,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const projectMemories = memories.filter((memory) => memory.category === 'projects');
|
|
616
|
+
for (const memory of projectMemories.slice(0, 12)) {
|
|
617
|
+
views.push({
|
|
618
|
+
viewType: 'project',
|
|
619
|
+
subjectKey: memory.id,
|
|
620
|
+
title: memory.content.split(/[.!?\n]/)[0].slice(0, 120) || 'Project',
|
|
621
|
+
summary: memory.content.slice(0, 320),
|
|
622
|
+
markdownText: `# ${memory.content.split(/[.!?\n]/)[0].slice(0, 120) || 'Project'}\n\n${memory.content}`,
|
|
623
|
+
sourceMemoryIds: [memory.id],
|
|
624
|
+
sourceDocumentIds: [],
|
|
625
|
+
metadata: {
|
|
626
|
+
importance: memory.importance,
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const personMemories = memories.filter((memory) => ['contacts', 'identity'].includes(memory.category));
|
|
632
|
+
for (const memory of personMemories.slice(0, 12)) {
|
|
633
|
+
views.push({
|
|
634
|
+
viewType: 'person',
|
|
635
|
+
subjectKey: memory.id,
|
|
636
|
+
title: memory.content.split(/[.!?\n]/)[0].slice(0, 120) || 'Person',
|
|
637
|
+
summary: memory.content.slice(0, 320),
|
|
638
|
+
markdownText: `# ${memory.content.split(/[.!?\n]/)[0].slice(0, 120) || 'Person'}\n\n${memory.content}`,
|
|
639
|
+
sourceMemoryIds: [memory.id],
|
|
640
|
+
sourceDocumentIds: [],
|
|
641
|
+
metadata: {
|
|
642
|
+
category: memory.category,
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const view of views) {
|
|
648
|
+
const existing = db.prepare(
|
|
649
|
+
`SELECT id FROM materialized_knowledge_views
|
|
650
|
+
WHERE user_id = ? AND agent_id = ? AND view_type = ? AND subject_key = ?`
|
|
651
|
+
).get(userId, scopedAgentId, view.viewType, view.subjectKey);
|
|
652
|
+
const viewId = existing?.id || uuidv4();
|
|
653
|
+
db.prepare(
|
|
654
|
+
`INSERT INTO materialized_knowledge_views (
|
|
655
|
+
id, user_id, agent_id, view_type, subject_key, title, summary, markdown_text,
|
|
656
|
+
source_memory_ids_json, source_document_ids_json, metadata_json, created_at, updated_at
|
|
657
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
658
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
659
|
+
title = excluded.title,
|
|
660
|
+
summary = excluded.summary,
|
|
661
|
+
markdown_text = excluded.markdown_text,
|
|
662
|
+
source_memory_ids_json = excluded.source_memory_ids_json,
|
|
663
|
+
source_document_ids_json = excluded.source_document_ids_json,
|
|
664
|
+
metadata_json = excluded.metadata_json,
|
|
665
|
+
updated_at = excluded.updated_at`
|
|
666
|
+
).run(
|
|
667
|
+
viewId,
|
|
668
|
+
userId,
|
|
669
|
+
scopedAgentId,
|
|
670
|
+
view.viewType,
|
|
671
|
+
view.subjectKey,
|
|
672
|
+
view.title,
|
|
673
|
+
view.summary,
|
|
674
|
+
view.markdownText,
|
|
675
|
+
JSON.stringify(normalizeStringArray(view.sourceMemoryIds, 48, 64)),
|
|
676
|
+
JSON.stringify(normalizeStringArray(view.sourceDocumentIds, 48, 64)),
|
|
677
|
+
JSON.stringify(parseJsonObject(view.metadata, {})),
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return this.listKnowledgeViews(userId, { agentId: scopedAgentId, limit: 64 });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
listKnowledgeViews(userId, { agentId = null, viewType = null, limit = 40 } = {}) {
|
|
685
|
+
const scopedAgentId = this._agentId(userId, { agentId });
|
|
686
|
+
let sql = `SELECT *
|
|
687
|
+
FROM materialized_knowledge_views
|
|
688
|
+
WHERE user_id = ? AND agent_id = ?`;
|
|
689
|
+
const params = [userId, scopedAgentId];
|
|
690
|
+
if (viewType) {
|
|
691
|
+
sql += ' AND view_type = ?';
|
|
692
|
+
params.push(String(viewType).trim());
|
|
693
|
+
}
|
|
694
|
+
sql += ' ORDER BY updated_at DESC LIMIT ?';
|
|
695
|
+
params.push(Math.max(1, Math.min(Number(limit) || 40, 100)));
|
|
696
|
+
return db.prepare(sql).all(...params).map((row) => ({
|
|
697
|
+
id: row.id,
|
|
698
|
+
viewType: row.view_type,
|
|
699
|
+
subjectKey: row.subject_key,
|
|
700
|
+
title: row.title,
|
|
701
|
+
summary: row.summary || '',
|
|
702
|
+
markdownText: row.markdown_text || '',
|
|
703
|
+
sourceMemoryIds: normalizeStringArray(parseJsonArray(row.source_memory_ids_json)),
|
|
704
|
+
sourceDocumentIds: normalizeStringArray(parseJsonArray(row.source_document_ids_json)),
|
|
705
|
+
metadata: parseJsonObject(row.metadata_json, {}),
|
|
706
|
+
updatedAt: row.updated_at || null,
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
listRecentKnowledgeChanges(userId, { agentId = null, limit = 8 } = {}) {
|
|
711
|
+
const docs = this.listIngestionDocuments(userId, { agentId, limit });
|
|
712
|
+
const views = this.listKnowledgeViews(userId, { agentId, limit });
|
|
713
|
+
const changes = [
|
|
714
|
+
...docs.map((doc) => ({
|
|
715
|
+
kind: 'document',
|
|
716
|
+
id: doc.id,
|
|
717
|
+
title: doc.title || doc.normalizedType,
|
|
718
|
+
summary: doc.summary || doc.content,
|
|
719
|
+
sourceType: doc.sourceType,
|
|
720
|
+
providerKey: doc.providerKey,
|
|
721
|
+
updatedAt: doc.updatedAt,
|
|
722
|
+
})),
|
|
723
|
+
...views.map((view) => ({
|
|
724
|
+
kind: 'view',
|
|
725
|
+
id: view.id,
|
|
726
|
+
title: view.title,
|
|
727
|
+
summary: view.summary,
|
|
728
|
+
sourceType: view.viewType,
|
|
729
|
+
providerKey: view.metadata?.providerKey || null,
|
|
730
|
+
updatedAt: view.updatedAt,
|
|
731
|
+
})),
|
|
732
|
+
];
|
|
733
|
+
return changes
|
|
734
|
+
.sort((left, right) => String(right.updatedAt || '').localeCompare(String(left.updatedAt || '')))
|
|
735
|
+
.slice(0, Math.max(1, Math.min(Number(limit) || 8, 24)));
|
|
736
|
+
}
|
|
737
|
+
|
|
316
738
|
// ─────────────────────────────────────────────────────────────────────────
|
|
317
739
|
// Semantic Memories (SQLite + embeddings)
|
|
318
740
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const { randomUUID } = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
process.env.OPENAI_API_KEY = '';
|
|
8
|
+
process.env.GOOGLE_AI_KEY = '';
|
|
9
|
+
|
|
10
|
+
const db = require('../../db/database');
|
|
11
|
+
const { resolveAgentId } = require('../agents/manager');
|
|
12
|
+
const { compactTextPayload } = require('../ai/preModelCompaction');
|
|
13
|
+
const { MemoryManager } = require('./manager');
|
|
14
|
+
const { MemoryIngestionService, sourceTypesForConnection } = require('./ingestion');
|
|
15
|
+
const { buildAssistantFocusSnapshot } = require('../widgets/focus_widget');
|
|
16
|
+
|
|
17
|
+
function createTestUser() {
|
|
18
|
+
const username = `openhuman-${randomUUID()}`;
|
|
19
|
+
const result = db.prepare(
|
|
20
|
+
'INSERT INTO users (username, password) VALUES (?, ?)',
|
|
21
|
+
).run(username, 'test');
|
|
22
|
+
const userId = Number(result.lastInsertRowid);
|
|
23
|
+
return {
|
|
24
|
+
userId,
|
|
25
|
+
agentId: resolveAgentId(userId, null),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanupUser(userId) {
|
|
30
|
+
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('pre-model compaction strips noisy HTML and records metrics', () => {
|
|
34
|
+
const input = '<html><style>.x{}</style><body>Read https://example.com/a/really/long/path today.<br>Read https://example.com/a/really/long/path today.</body></html>';
|
|
35
|
+
const result = compactTextPayload(input, { maxChars: 120, maxLines: 5 });
|
|
36
|
+
|
|
37
|
+
assert.equal(result.metrics.applied, true);
|
|
38
|
+
assert.ok(result.metrics.strategies.includes('html_to_text'));
|
|
39
|
+
assert.ok(result.metrics.strategies.includes('url_shortening'));
|
|
40
|
+
assert.match(result.text, /example\.com/);
|
|
41
|
+
assert.doesNotMatch(result.text, /<body>/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('memory ingestion writes typed documents, memory, and materialized views', async () => {
|
|
45
|
+
const { userId, agentId } = createTestUser();
|
|
46
|
+
const memoryManager = new MemoryManager();
|
|
47
|
+
const service = new MemoryIngestionService({ memoryManager, intervalMs: 60_000 });
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await service.ingestDocuments(userId, [
|
|
51
|
+
{
|
|
52
|
+
externalObjectId: 'thread-123',
|
|
53
|
+
sourceType: 'email',
|
|
54
|
+
normalizedType: 'email',
|
|
55
|
+
title: 'Launch review',
|
|
56
|
+
content: 'The launch review moved to Friday. Alice owns the deck and Bob owns QA.',
|
|
57
|
+
sourceAccount: 'team@example.com',
|
|
58
|
+
salience: 8,
|
|
59
|
+
},
|
|
60
|
+
], {
|
|
61
|
+
agentId,
|
|
62
|
+
sourceType: 'email',
|
|
63
|
+
providerKey: 'google_workspace',
|
|
64
|
+
sourceAccount: 'team@example.com',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(result.status, 'completed');
|
|
68
|
+
assert.equal(result.documentIds.length, 1);
|
|
69
|
+
assert.equal(result.memoryIds.length, 1);
|
|
70
|
+
|
|
71
|
+
const docs = memoryManager.listIngestionDocuments(userId, {
|
|
72
|
+
agentId,
|
|
73
|
+
providerKey: 'google_workspace',
|
|
74
|
+
});
|
|
75
|
+
assert.equal(docs.length, 1);
|
|
76
|
+
assert.equal(docs[0].sourceType, 'email');
|
|
77
|
+
assert.equal(docs[0].sourceAccount, 'team@example.com');
|
|
78
|
+
|
|
79
|
+
const changes = memoryManager.listRecentKnowledgeChanges(userId, { agentId });
|
|
80
|
+
assert.ok(changes.some((change) => change.kind === 'document'));
|
|
81
|
+
|
|
82
|
+
const views = memoryManager.listKnowledgeViews(userId, { agentId });
|
|
83
|
+
assert.ok(views.some((view) => view.viewType === 'timeline'));
|
|
84
|
+
assert.ok(views.some((view) => view.viewType === 'account'));
|
|
85
|
+
|
|
86
|
+
const focus = buildAssistantFocusSnapshot(memoryManager, userId, agentId);
|
|
87
|
+
assert.equal(focus.backgroundAwareness.changedCount > 0, true);
|
|
88
|
+
assert.ok(focus.recentKnowledgeChanges.some((change) => change.title === 'Launch review'));
|
|
89
|
+
} finally {
|
|
90
|
+
cleanupUser(userId);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('integration coverage maps connected apps to durable memory domains', () => {
|
|
95
|
+
assert.deepEqual(sourceTypesForConnection('google_workspace', 'gmail'), ['email']);
|
|
96
|
+
assert.deepEqual(sourceTypesForConnection('github', 'repos'), ['repos', 'tickets']);
|
|
97
|
+
assert.deepEqual(sourceTypesForConnection('spotify', 'spotify'), []);
|
|
98
|
+
});
|
|
@@ -52,6 +52,18 @@ function buildAssistantFocusSnapshot(memoryManager, userId, agentId) {
|
|
|
52
52
|
).all(userId, scopedAgentId);
|
|
53
53
|
const conversations = memoryManager?.getRecentConversations?.(userId, 4, { agentId: scopedAgentId }) || [];
|
|
54
54
|
const memories = memoryManager?.listMemories?.(userId, { limit: 6, agentId: scopedAgentId }) || [];
|
|
55
|
+
const recentKnowledgeChanges = memoryManager?.listRecentKnowledgeChanges?.(userId, {
|
|
56
|
+
agentId: scopedAgentId,
|
|
57
|
+
limit: 5,
|
|
58
|
+
}) || [];
|
|
59
|
+
const knowledgeViews = memoryManager?.listKnowledgeViews?.(userId, {
|
|
60
|
+
agentId: scopedAgentId,
|
|
61
|
+
limit: 4,
|
|
62
|
+
}) || [];
|
|
63
|
+
const ingestionOverview = memoryManager?.getIngestionOverview?.(userId, {
|
|
64
|
+
agentId: scopedAgentId,
|
|
65
|
+
limit: 8,
|
|
66
|
+
}) || [];
|
|
55
67
|
|
|
56
68
|
const activeThreads = conversations.slice(0, 3).map((conversation) => ({
|
|
57
69
|
title: safeTrim(conversation.title, 80),
|
|
@@ -75,6 +87,24 @@ function buildAssistantFocusSnapshot(memoryManager, userId, agentId) {
|
|
|
75
87
|
value: safeTrim(run.status || 'unknown', 32),
|
|
76
88
|
}));
|
|
77
89
|
const rememberedContext = memories.slice(0, 2).map((memory) => safeTrim(memory.content, 140));
|
|
90
|
+
const backgroundAwareness = {
|
|
91
|
+
summary: safeTrim(
|
|
92
|
+
recentKnowledgeChanges[0]?.summary
|
|
93
|
+
|| recentKnowledgeChanges[0]?.title
|
|
94
|
+
|| knowledgeViews[0]?.summary
|
|
95
|
+
|| '',
|
|
96
|
+
180,
|
|
97
|
+
),
|
|
98
|
+
changedCount: recentKnowledgeChanges.length,
|
|
99
|
+
lastChangedAt: recentKnowledgeChanges[0]?.updatedAt || null,
|
|
100
|
+
};
|
|
101
|
+
const syncHealth = ingestionOverview.slice(0, 3).map((source) => ({
|
|
102
|
+
label: safeTrim(source.providerKey || source.sourceTypes?.[0] || 'Background context', 80),
|
|
103
|
+
value: safeTrim(source.status || 'ready', 32),
|
|
104
|
+
lastRefreshAt: source.lastRefreshAt || null,
|
|
105
|
+
nextRefreshAt: source.nextRefreshAt || null,
|
|
106
|
+
documentCount: Number(source.documentCount || 0),
|
|
107
|
+
}));
|
|
78
108
|
|
|
79
109
|
const currentFocus = safeTrim(
|
|
80
110
|
selfState.focus?.currentFocus
|
|
@@ -90,6 +120,10 @@ function buildAssistantFocusSnapshot(memoryManager, userId, agentId) {
|
|
|
90
120
|
nearTermPriorities,
|
|
91
121
|
recentSignals,
|
|
92
122
|
rememberedContext,
|
|
123
|
+
recentKnowledgeChanges,
|
|
124
|
+
knowledgeViews,
|
|
125
|
+
backgroundAwareness,
|
|
126
|
+
syncHealth,
|
|
93
127
|
assistantIdentity: {
|
|
94
128
|
name: safeTrim(selfState.identity?.displayName, 80),
|
|
95
129
|
style: safeTrim(selfState.identity?.style, 120),
|
|
@@ -99,11 +133,15 @@ function buildAssistantFocusSnapshot(memoryManager, userId, agentId) {
|
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
function buildAssistantFocusWidgetPayload(snapshot) {
|
|
136
|
+
const knowledgeChangeCount = Number(snapshot.backgroundAwareness?.changedCount || 0);
|
|
137
|
+
const primaryMetric = knowledgeChangeCount > 0 ? knowledgeChangeCount : snapshot.activeThreads.length;
|
|
102
138
|
const rows = [
|
|
103
139
|
...snapshot.nearTermPriorities,
|
|
104
140
|
...snapshot.recentSignals,
|
|
141
|
+
...snapshot.syncHealth,
|
|
105
142
|
].slice(0, 3);
|
|
106
143
|
const chips = [
|
|
144
|
+
...snapshot.recentKnowledgeChanges.map((item) => item.title),
|
|
107
145
|
...snapshot.activeThreads.map((item) => item.title),
|
|
108
146
|
...snapshot.rememberedContext,
|
|
109
147
|
].filter(Boolean).slice(0, 3);
|
|
@@ -113,14 +151,17 @@ function buildAssistantFocusWidgetPayload(snapshot) {
|
|
|
113
151
|
kicker: 'Assistant state',
|
|
114
152
|
subtitle: snapshot.currentFocus,
|
|
115
153
|
body: snapshot.activeThreads[0]?.preview
|
|
154
|
+
|| snapshot.backgroundAwareness?.summary
|
|
116
155
|
|| snapshot.rememberedContext[0]
|
|
117
156
|
|| 'Watching recent conversations, tasks, and runs.',
|
|
118
|
-
metric: String(
|
|
119
|
-
metricLabel:
|
|
157
|
+
metric: String(primaryMetric),
|
|
158
|
+
metricLabel: knowledgeChangeCount > 0
|
|
159
|
+
? (knowledgeChangeCount === 1 ? 'knowledge change' : 'knowledge changes')
|
|
160
|
+
: (snapshot.activeThreads.length === 1 ? 'active thread' : 'active threads'),
|
|
120
161
|
secondaryMetric: String(snapshot.nearTermPriorities.length),
|
|
121
162
|
secondaryLabel: 'priorities',
|
|
122
|
-
tertiaryMetric: formatRelativeTimestamp(snapshot.generatedAt),
|
|
123
|
-
tertiaryLabel: 'updated',
|
|
163
|
+
tertiaryMetric: formatRelativeTimestamp(snapshot.backgroundAwareness?.lastChangedAt || snapshot.generatedAt),
|
|
164
|
+
tertiaryLabel: snapshot.backgroundAwareness?.lastChangedAt ? 'last change' : 'updated',
|
|
124
165
|
rows,
|
|
125
166
|
chips,
|
|
126
167
|
iconToken: 'focus',
|