neoagent 1.4.1 → 1.4.3
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/docs/skills.md +4 -0
- package/package.json +3 -1
- package/server/db/database.js +76 -0
- package/server/public/app.html +124 -49
- package/server/public/assets/world-office-dark.png +0 -0
- package/server/public/assets/world-office-light.png +0 -0
- package/server/public/css/app.css +575 -242
- package/server/public/css/styles.css +445 -121
- package/server/public/js/app.js +1041 -423
- package/server/routes/memory.js +3 -1
- package/server/routes/settings.js +40 -2
- package/server/routes/skills.js +124 -85
- package/server/routes/store.js +100 -0
- package/server/services/ai/compaction.js +14 -30
- package/server/services/ai/engine.js +222 -200
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -119
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +24 -6
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +71 -2
- package/server/services/manager.js +25 -2
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +209 -16
- package/server/services/websocket.js +19 -6
|
@@ -11,6 +11,38 @@ const {
|
|
|
11
11
|
} = require('./embeddings');
|
|
12
12
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Derive the active AI provider name from user settings so the right
|
|
16
|
+
* embedding model is selected automatically (e.g. Gemini when using Google).
|
|
17
|
+
*/
|
|
18
|
+
function getActiveProvider(userId) {
|
|
19
|
+
try {
|
|
20
|
+
const { SUPPORTED_MODELS } = require('../ai/models');
|
|
21
|
+
const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?)')
|
|
22
|
+
.all(userId || 1, 'default_chat_model', 'enabled_models');
|
|
23
|
+
|
|
24
|
+
let defaultChatModel = null;
|
|
25
|
+
let enabledIds = null;
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
try {
|
|
28
|
+
const v = JSON.parse(row.value);
|
|
29
|
+
if (row.key === 'default_chat_model') defaultChatModel = v;
|
|
30
|
+
if (row.key === 'enabled_models') enabledIds = v;
|
|
31
|
+
} catch { }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const modelId = defaultChatModel && defaultChatModel !== 'auto'
|
|
35
|
+
? defaultChatModel
|
|
36
|
+
: (Array.isArray(enabledIds) && enabledIds.length > 0 ? enabledIds[0] : null);
|
|
37
|
+
|
|
38
|
+
if (modelId) {
|
|
39
|
+
const def = SUPPORTED_MODELS.find(m => m.id === modelId);
|
|
40
|
+
if (def) return def.provider;
|
|
41
|
+
}
|
|
42
|
+
} catch { }
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
14
46
|
const DATA_DIR = AGENT_DATA_DIR;
|
|
15
47
|
const SOUL_FILE = path.join(DATA_DIR, 'SOUL.md');
|
|
16
48
|
const API_KEYS_FILE = path.join(DATA_DIR, 'API_KEYS.json');
|
|
@@ -33,6 +65,33 @@ const CATEGORIES = ['user_fact', 'preference', 'personality', 'episodic'];
|
|
|
33
65
|
// Core memory keys (always injected into every prompt)
|
|
34
66
|
const CORE_KEYS = ['user_profile', 'preferences', 'ai_personality', 'active_context'];
|
|
35
67
|
|
|
68
|
+
function buildFtsQuery(query) {
|
|
69
|
+
const tokens = String(query || '')
|
|
70
|
+
.match(/[\p{L}\p{N}_-]{2,}/gu) || [];
|
|
71
|
+
if (!tokens.length) return null;
|
|
72
|
+
return tokens.map((token) => `${token.replace(/"/g, '')}*`).join(' AND ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stripHighlight(text) {
|
|
76
|
+
return String(text || '').replace(/<\/?mark>/g, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildExcerpt(text, query) {
|
|
80
|
+
const raw = stripHighlight(text);
|
|
81
|
+
const needle = String(query || '').trim().toLowerCase();
|
|
82
|
+
if (!raw) return '';
|
|
83
|
+
if (!needle) return raw.slice(0, 220);
|
|
84
|
+
|
|
85
|
+
const pos = raw.toLowerCase().indexOf(needle);
|
|
86
|
+
if (pos === -1) return raw.slice(0, 220);
|
|
87
|
+
|
|
88
|
+
const start = Math.max(0, pos - 80);
|
|
89
|
+
const end = Math.min(raw.length, pos + needle.length + 140);
|
|
90
|
+
const prefix = start > 0 ? '...' : '';
|
|
91
|
+
const suffix = end < raw.length ? '...' : '';
|
|
92
|
+
return `${prefix}${raw.slice(start, end)}${suffix}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
36
95
|
class MemoryManager {
|
|
37
96
|
constructor() {
|
|
38
97
|
this._ensureDirs();
|
|
@@ -42,7 +101,7 @@ class MemoryManager {
|
|
|
42
101
|
for (const dir of [DATA_DIR, DAILY_DIR, MEMORY_DIR, SKILLS_DIR]) {
|
|
43
102
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
44
103
|
}
|
|
45
|
-
if (!fs.existsSync(SOUL_FILE))
|
|
104
|
+
if (!fs.existsSync(SOUL_FILE)) fs.writeFileSync(SOUL_FILE, DEFAULT_SOUL, 'utf-8');
|
|
46
105
|
if (!fs.existsSync(API_KEYS_FILE)) fs.writeFileSync(API_KEYS_FILE, '{}', 'utf-8');
|
|
47
106
|
}
|
|
48
107
|
|
|
@@ -59,7 +118,7 @@ class MemoryManager {
|
|
|
59
118
|
category = CATEGORIES.includes(category) ? category : 'episodic';
|
|
60
119
|
importance = Math.max(1, Math.min(10, Number(importance) || 5));
|
|
61
120
|
|
|
62
|
-
const embedding = await getEmbedding(content);
|
|
121
|
+
const embedding = await getEmbedding(content, getActiveProvider(userId));
|
|
63
122
|
|
|
64
123
|
// Dedup check: compare against existing non-archived memories for this user
|
|
65
124
|
const existing = db.prepare(
|
|
@@ -112,7 +171,7 @@ class MemoryManager {
|
|
|
112
171
|
|
|
113
172
|
if (!all.length) return [];
|
|
114
173
|
|
|
115
|
-
const queryVec = await getEmbedding(query);
|
|
174
|
+
const queryVec = await getEmbedding(query, getActiveProvider(userId));
|
|
116
175
|
|
|
117
176
|
const scored = all.map(mem => {
|
|
118
177
|
let score = 0;
|
|
@@ -170,13 +229,13 @@ class MemoryManager {
|
|
|
170
229
|
const mem = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id);
|
|
171
230
|
if (!mem) return null;
|
|
172
231
|
|
|
173
|
-
const newContent
|
|
232
|
+
const newContent = content ?? mem.content;
|
|
174
233
|
const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
|
|
175
|
-
const newCategory
|
|
234
|
+
const newCategory = (category && CATEGORIES.includes(category)) ? category : mem.category;
|
|
176
235
|
|
|
177
236
|
let newEmbed = mem.embedding;
|
|
178
237
|
if (content && content !== mem.content) {
|
|
179
|
-
const vec = await getEmbedding(newContent);
|
|
238
|
+
const vec = await getEmbedding(newContent, getActiveProvider(null));
|
|
180
239
|
newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
|
|
181
240
|
}
|
|
182
241
|
|
|
@@ -322,19 +381,153 @@ class MemoryManager {
|
|
|
322
381
|
}
|
|
323
382
|
|
|
324
383
|
getRecentConversations(userId, limit = 20) {
|
|
325
|
-
|
|
326
|
-
SELECT
|
|
327
|
-
|
|
328
|
-
|
|
384
|
+
const rows = db.prepare(`
|
|
385
|
+
SELECT
|
|
386
|
+
ar.id AS run_id,
|
|
387
|
+
ar.title,
|
|
388
|
+
ar.created_at,
|
|
389
|
+
ar.completed_at,
|
|
390
|
+
ar.status,
|
|
391
|
+
(
|
|
392
|
+
SELECT content
|
|
393
|
+
FROM conversation_history ch
|
|
394
|
+
WHERE ch.agent_run_id = ar.id
|
|
395
|
+
ORDER BY ch.created_at DESC
|
|
396
|
+
LIMIT 1
|
|
397
|
+
) AS latest_content
|
|
398
|
+
FROM agent_runs ar
|
|
399
|
+
WHERE ar.user_id = ?
|
|
400
|
+
ORDER BY COALESCE(ar.completed_at, ar.created_at) DESC
|
|
401
|
+
LIMIT ?
|
|
329
402
|
`).all(userId, limit);
|
|
403
|
+
|
|
404
|
+
return rows.map((row) => ({
|
|
405
|
+
runId: row.run_id,
|
|
406
|
+
title: row.title || 'Untitled run',
|
|
407
|
+
createdAt: row.created_at,
|
|
408
|
+
completedAt: row.completed_at,
|
|
409
|
+
status: row.status,
|
|
410
|
+
excerpt: buildExcerpt(row.latest_content, '')
|
|
411
|
+
}));
|
|
330
412
|
}
|
|
331
413
|
|
|
332
|
-
searchConversations(userId, query) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
414
|
+
searchConversations(userId, query, options = {}) {
|
|
415
|
+
const ftsQuery = buildFtsQuery(query);
|
|
416
|
+
const maxHits = Math.max(6, Math.min(Number(options.limit) || 24, 60));
|
|
417
|
+
if (!ftsQuery) return [];
|
|
418
|
+
|
|
419
|
+
let webHits = [];
|
|
420
|
+
let messageHits = [];
|
|
421
|
+
try {
|
|
422
|
+
webHits = db.prepare(`
|
|
423
|
+
SELECT
|
|
424
|
+
'web' AS source,
|
|
425
|
+
ch.id AS message_id,
|
|
426
|
+
COALESCE(ch.agent_run_id, 'web:' || ch.id) AS session_id,
|
|
427
|
+
COALESCE(ar.title, 'Web chat') AS title,
|
|
428
|
+
ch.role,
|
|
429
|
+
ch.created_at,
|
|
430
|
+
snippet(conversation_history_fts, 0, '<mark>', '</mark>', ' ... ', 16) AS snippet,
|
|
431
|
+
bm25(conversation_history_fts) AS score
|
|
432
|
+
FROM conversation_history_fts
|
|
433
|
+
JOIN conversation_history ch ON ch.id = conversation_history_fts.rowid
|
|
434
|
+
LEFT JOIN agent_runs ar ON ar.id = ch.agent_run_id
|
|
435
|
+
WHERE conversation_history_fts MATCH ? AND ch.user_id = ?
|
|
436
|
+
ORDER BY score
|
|
437
|
+
LIMIT ?
|
|
438
|
+
`).all(ftsQuery, userId, maxHits);
|
|
439
|
+
|
|
440
|
+
messageHits = db.prepare(`
|
|
441
|
+
SELECT
|
|
442
|
+
'message' AS source,
|
|
443
|
+
m.id AS message_id,
|
|
444
|
+
COALESCE(m.run_id, m.platform || ':' || COALESCE(m.platform_chat_id, m.id)) AS session_id,
|
|
445
|
+
COALESCE(ar.title, json_extract(m.metadata, '$.senderName'), m.platform_chat_id, m.platform, 'Message thread') AS title,
|
|
446
|
+
m.role,
|
|
447
|
+
m.created_at,
|
|
448
|
+
m.platform,
|
|
449
|
+
snippet(messages_fts, 0, '<mark>', '</mark>', ' ... ', 16) AS snippet,
|
|
450
|
+
bm25(messages_fts) AS score
|
|
451
|
+
FROM messages_fts
|
|
452
|
+
JOIN messages m ON m.id = messages_fts.rowid
|
|
453
|
+
LEFT JOIN agent_runs ar ON ar.id = m.run_id
|
|
454
|
+
WHERE messages_fts MATCH ? AND m.user_id = ?
|
|
455
|
+
ORDER BY score
|
|
456
|
+
LIMIT ?
|
|
457
|
+
`).all(ftsQuery, userId, maxHits);
|
|
458
|
+
} catch {
|
|
459
|
+
const likeQuery = `%${String(query || '').trim()}%`;
|
|
460
|
+
webHits = db.prepare(`
|
|
461
|
+
SELECT
|
|
462
|
+
'web' AS source,
|
|
463
|
+
ch.id AS message_id,
|
|
464
|
+
COALESCE(ch.agent_run_id, 'web:' || ch.id) AS session_id,
|
|
465
|
+
COALESCE(ar.title, 'Web chat') AS title,
|
|
466
|
+
ch.role,
|
|
467
|
+
ch.created_at,
|
|
468
|
+
ch.content AS snippet,
|
|
469
|
+
0 AS score
|
|
470
|
+
FROM conversation_history ch
|
|
471
|
+
LEFT JOIN agent_runs ar ON ar.id = ch.agent_run_id
|
|
472
|
+
WHERE ch.user_id = ? AND ch.content LIKE ?
|
|
473
|
+
ORDER BY ch.created_at DESC
|
|
474
|
+
LIMIT ?
|
|
475
|
+
`).all(userId, likeQuery, maxHits);
|
|
476
|
+
|
|
477
|
+
messageHits = db.prepare(`
|
|
478
|
+
SELECT
|
|
479
|
+
'message' AS source,
|
|
480
|
+
m.id AS message_id,
|
|
481
|
+
COALESCE(m.run_id, m.platform || ':' || COALESCE(m.platform_chat_id, m.id)) AS session_id,
|
|
482
|
+
COALESCE(ar.title, json_extract(m.metadata, '$.senderName'), m.platform_chat_id, m.platform, 'Message thread') AS title,
|
|
483
|
+
m.role,
|
|
484
|
+
m.created_at,
|
|
485
|
+
m.platform,
|
|
486
|
+
m.content AS snippet,
|
|
487
|
+
0 AS score
|
|
488
|
+
FROM messages m
|
|
489
|
+
LEFT JOIN agent_runs ar ON ar.id = m.run_id
|
|
490
|
+
WHERE m.user_id = ? AND m.content LIKE ?
|
|
491
|
+
ORDER BY m.created_at DESC
|
|
492
|
+
LIMIT ?
|
|
493
|
+
`).all(userId, likeQuery, maxHits);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const grouped = new Map();
|
|
497
|
+
for (const hit of [...webHits, ...messageHits]) {
|
|
498
|
+
const key = `${hit.source}:${hit.session_id}`;
|
|
499
|
+
if (!grouped.has(key)) {
|
|
500
|
+
grouped.set(key, {
|
|
501
|
+
sessionId: hit.session_id,
|
|
502
|
+
source: hit.source,
|
|
503
|
+
title: hit.title || 'Untitled session',
|
|
504
|
+
platform: hit.platform || 'web',
|
|
505
|
+
createdAt: hit.created_at,
|
|
506
|
+
score: Number(hit.score || 0),
|
|
507
|
+
matches: []
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const group = grouped.get(key);
|
|
512
|
+
group.score = Math.min(group.score, Number(hit.score || 0));
|
|
513
|
+
group.createdAt = hit.created_at > group.createdAt ? hit.created_at : group.createdAt;
|
|
514
|
+
if (group.matches.length < 3) {
|
|
515
|
+
group.matches.push({
|
|
516
|
+
role: hit.role,
|
|
517
|
+
createdAt: hit.created_at,
|
|
518
|
+
excerpt: buildExcerpt(hit.snippet, query)
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return Array.from(grouped.values())
|
|
524
|
+
.sort((a, b) => a.score - b.score || String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
525
|
+
.slice(0, Math.max(1, Math.min(Number(options.sessions) || 8, 12)))
|
|
526
|
+
.map((session) => ({
|
|
527
|
+
...session,
|
|
528
|
+
matchCount: session.matches.length,
|
|
529
|
+
summary: session.matches.map((match) => `${match.role}: ${match.excerpt}`).join('\n')
|
|
530
|
+
}));
|
|
338
531
|
}
|
|
339
532
|
|
|
340
533
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const db = require('../db/database');
|
|
2
2
|
const { sanitizeError } = require('../utils/security');
|
|
3
|
+
const { getProviderForUser } = require('./ai/engine');
|
|
4
|
+
const { ensureDefaultAiSettings, getAiSettings } = require('./ai/settings');
|
|
5
|
+
const { getWebChatContext, refreshWebChatSummary, clearWebChatSummary } = require('./ai/history');
|
|
3
6
|
|
|
4
7
|
function setupWebSocket(io, services) {
|
|
5
8
|
const { agentEngine, messagingManager, mcpClient, scheduler, memoryManager } = services;
|
|
@@ -31,6 +34,7 @@ function setupWebSocket(io, services) {
|
|
|
31
34
|
case 'new':
|
|
32
35
|
case 'clear':
|
|
33
36
|
db.prepare('DELETE FROM conversation_history WHERE user_id = ?').run(userId);
|
|
37
|
+
clearWebChatSummary(userId);
|
|
34
38
|
socket.emit('chat:cleared');
|
|
35
39
|
{
|
|
36
40
|
const resetResult = await agentEngine.run(userId, 'context was just cleared. say something very brief (1-2 sentences max) acknowledging the fresh start, in your own style. no tools needed.', {});
|
|
@@ -61,17 +65,26 @@ function setupWebSocket(io, services) {
|
|
|
61
65
|
db.prepare('INSERT INTO conversation_history (user_id, role, content, metadata) VALUES (?, ?, ?, ?)')
|
|
62
66
|
.run(userId, 'user', task, JSON.stringify({ platform: 'web' }));
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const prior =
|
|
68
|
+
ensureDefaultAiSettings(userId);
|
|
69
|
+
const aiSettings = getAiSettings(userId);
|
|
70
|
+
const webContext = getWebChatContext(userId, aiSettings.chat_history_window);
|
|
71
|
+
const prior = webContext.recentMessages.filter((m) => !(m.role === 'user' && m.content === task)).slice(-aiSettings.chat_history_window);
|
|
68
72
|
|
|
69
|
-
const result = await agentEngine.run(userId, task, {
|
|
73
|
+
const result = await agentEngine.run(userId, task, {
|
|
74
|
+
...options,
|
|
75
|
+
priorMessages: prior,
|
|
76
|
+
priorSummary: webContext.summary
|
|
77
|
+
});
|
|
70
78
|
|
|
71
79
|
if (result?.content) {
|
|
72
80
|
db.prepare('INSERT INTO conversation_history (user_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)')
|
|
73
81
|
.run(userId, result.runId, 'assistant', result.content, JSON.stringify({ tokens: result.totalTokens }));
|
|
74
82
|
}
|
|
83
|
+
|
|
84
|
+
const { provider, model } = getProviderForUser(userId, task, false, options?.model || null);
|
|
85
|
+
refreshWebChatSummary(userId, provider, model, aiSettings.chat_history_window).catch((summaryErr) => {
|
|
86
|
+
console.error('[WS] Web summary refresh failed:', summaryErr.message);
|
|
87
|
+
});
|
|
75
88
|
} catch (err) {
|
|
76
89
|
socket.emit('run:error', { error: sanitizeError(err) });
|
|
77
90
|
}
|
|
@@ -102,7 +115,7 @@ function setupWebSocket(io, services) {
|
|
|
102
115
|
socket.on('agent:run_detail', (data) => {
|
|
103
116
|
try {
|
|
104
117
|
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(data.runId, userId);
|
|
105
|
-
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY
|
|
118
|
+
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(data.runId);
|
|
106
119
|
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(data.runId);
|
|
107
120
|
socket.emit('agent:run_detail', { run, steps, history });
|
|
108
121
|
} catch (err) {
|