neoagent 1.4.0 → 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/.env.example +5 -0
- package/com.neoagent.plist +8 -6
- package/docs/configuration.md +9 -1
- package/docs/skills.md +6 -2
- package/lib/manager.js +37 -10
- package/package.json +4 -1
- package/runtime/paths.js +80 -0
- package/server/db/database.js +78 -4
- package/server/index.js +5 -5
- 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 +42 -6
- package/server/routes/skills.js +124 -84
- package/server/routes/store.js +102 -1
- package/server/services/ai/compaction.js +15 -31
- package/server/services/ai/engine.js +224 -202
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/providers/google.js +8 -1
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -98
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +26 -7
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +158 -5
- package/server/services/browser/controller.js +124 -48
- package/server/services/manager.js +26 -3
- package/server/services/mcp/client.js +1 -1
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +211 -17
- package/server/services/messaging/telnyx.js +3 -2
- package/server/services/messaging/whatsapp.js +3 -2
- package/server/services/scheduler/cron.js +6 -1
- package/server/services/websocket.js +19 -6
|
@@ -9,8 +9,41 @@ const {
|
|
|
9
9
|
deserializeEmbedding,
|
|
10
10
|
keywordSimilarity
|
|
11
11
|
} = require('./embeddings');
|
|
12
|
+
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
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
|
+
}
|
|
12
33
|
|
|
13
|
-
const
|
|
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
|
+
|
|
46
|
+
const DATA_DIR = AGENT_DATA_DIR;
|
|
14
47
|
const SOUL_FILE = path.join(DATA_DIR, 'SOUL.md');
|
|
15
48
|
const API_KEYS_FILE = path.join(DATA_DIR, 'API_KEYS.json');
|
|
16
49
|
const DAILY_DIR = path.join(DATA_DIR, 'daily');
|
|
@@ -32,6 +65,33 @@ const CATEGORIES = ['user_fact', 'preference', 'personality', 'episodic'];
|
|
|
32
65
|
// Core memory keys (always injected into every prompt)
|
|
33
66
|
const CORE_KEYS = ['user_profile', 'preferences', 'ai_personality', 'active_context'];
|
|
34
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
|
+
|
|
35
95
|
class MemoryManager {
|
|
36
96
|
constructor() {
|
|
37
97
|
this._ensureDirs();
|
|
@@ -41,7 +101,7 @@ class MemoryManager {
|
|
|
41
101
|
for (const dir of [DATA_DIR, DAILY_DIR, MEMORY_DIR, SKILLS_DIR]) {
|
|
42
102
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
43
103
|
}
|
|
44
|
-
if (!fs.existsSync(SOUL_FILE))
|
|
104
|
+
if (!fs.existsSync(SOUL_FILE)) fs.writeFileSync(SOUL_FILE, DEFAULT_SOUL, 'utf-8');
|
|
45
105
|
if (!fs.existsSync(API_KEYS_FILE)) fs.writeFileSync(API_KEYS_FILE, '{}', 'utf-8');
|
|
46
106
|
}
|
|
47
107
|
|
|
@@ -58,7 +118,7 @@ class MemoryManager {
|
|
|
58
118
|
category = CATEGORIES.includes(category) ? category : 'episodic';
|
|
59
119
|
importance = Math.max(1, Math.min(10, Number(importance) || 5));
|
|
60
120
|
|
|
61
|
-
const embedding = await getEmbedding(content);
|
|
121
|
+
const embedding = await getEmbedding(content, getActiveProvider(userId));
|
|
62
122
|
|
|
63
123
|
// Dedup check: compare against existing non-archived memories for this user
|
|
64
124
|
const existing = db.prepare(
|
|
@@ -111,7 +171,7 @@ class MemoryManager {
|
|
|
111
171
|
|
|
112
172
|
if (!all.length) return [];
|
|
113
173
|
|
|
114
|
-
const queryVec = await getEmbedding(query);
|
|
174
|
+
const queryVec = await getEmbedding(query, getActiveProvider(userId));
|
|
115
175
|
|
|
116
176
|
const scored = all.map(mem => {
|
|
117
177
|
let score = 0;
|
|
@@ -169,13 +229,13 @@ class MemoryManager {
|
|
|
169
229
|
const mem = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id);
|
|
170
230
|
if (!mem) return null;
|
|
171
231
|
|
|
172
|
-
const newContent
|
|
232
|
+
const newContent = content ?? mem.content;
|
|
173
233
|
const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
|
|
174
|
-
const newCategory
|
|
234
|
+
const newCategory = (category && CATEGORIES.includes(category)) ? category : mem.category;
|
|
175
235
|
|
|
176
236
|
let newEmbed = mem.embedding;
|
|
177
237
|
if (content && content !== mem.content) {
|
|
178
|
-
const vec = await getEmbedding(newContent);
|
|
238
|
+
const vec = await getEmbedding(newContent, getActiveProvider(null));
|
|
179
239
|
newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
|
|
180
240
|
}
|
|
181
241
|
|
|
@@ -321,19 +381,153 @@ class MemoryManager {
|
|
|
321
381
|
}
|
|
322
382
|
|
|
323
383
|
getRecentConversations(userId, limit = 20) {
|
|
324
|
-
|
|
325
|
-
SELECT
|
|
326
|
-
|
|
327
|
-
|
|
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 ?
|
|
328
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
|
+
}));
|
|
329
412
|
}
|
|
330
413
|
|
|
331
|
-
searchConversations(userId, query) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}));
|
|
337
531
|
}
|
|
338
532
|
|
|
339
533
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -5,8 +5,9 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const { OpenAI } = require('openai');
|
|
8
|
+
const { DATA_DIR, AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
8
9
|
|
|
9
|
-
const AUDIO_DIR = path.join(
|
|
10
|
+
const AUDIO_DIR = path.join(DATA_DIR, 'telnyx-audio');
|
|
10
11
|
const RECORDING_TURN_LIMIT_MS = 4000; // auto-stop recording after 4 s of silence
|
|
11
12
|
|
|
12
13
|
class TelnyxVoicePlatform extends BasePlatform {
|
|
@@ -57,7 +58,7 @@ class TelnyxVoicePlatform extends BasePlatform {
|
|
|
57
58
|
let openAiKey = process.env.OPENAI_API_KEY;
|
|
58
59
|
if (!openAiKey) {
|
|
59
60
|
try {
|
|
60
|
-
const keysPath = path.join(
|
|
61
|
+
const keysPath = path.join(AGENT_DATA_DIR, 'API_KEYS.json');
|
|
61
62
|
const keys = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
|
62
63
|
openAiKey = keys.OPENAI_API_KEY || keys.openai_api_key || keys.openai || null;
|
|
63
64
|
} catch { /* file missing or unreadable — fine */ }
|
|
@@ -2,8 +2,9 @@ const { BasePlatform } = require('./base');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { toWhatsAppJid } = require('../../utils/whatsapp');
|
|
5
|
+
const { DATA_DIR } = require('../../../runtime/paths');
|
|
5
6
|
|
|
6
|
-
const AUTH_DIR = path.join(
|
|
7
|
+
const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
|
|
7
8
|
|
|
8
9
|
class WhatsAppPlatform extends BasePlatform {
|
|
9
10
|
constructor(config = {}) {
|
|
@@ -137,7 +138,7 @@ class WhatsAppPlatform extends BasePlatform {
|
|
|
137
138
|
if (mediaType && mediaType !== 'sticker') {
|
|
138
139
|
try {
|
|
139
140
|
const { downloadMediaMessage } = require('baileys');
|
|
140
|
-
const MEDIA_DIR = path.join(
|
|
141
|
+
const MEDIA_DIR = path.join(DATA_DIR, 'media');
|
|
141
142
|
if (!fs.existsSync(MEDIA_DIR)) fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
|
142
143
|
const buffer = await downloadMediaMessage(msg, 'buffer', {}, {
|
|
143
144
|
logger: this._logger,
|
|
@@ -3,9 +3,10 @@ const crypto = require('crypto');
|
|
|
3
3
|
const db = require('../../db/database');
|
|
4
4
|
|
|
5
5
|
class Scheduler {
|
|
6
|
-
constructor(io, agentEngine) {
|
|
6
|
+
constructor(io, agentEngine, app = null) {
|
|
7
7
|
this.io = io;
|
|
8
8
|
this.agentEngine = agentEngine;
|
|
9
|
+
this.app = app;
|
|
9
10
|
this.jobs = new Map();
|
|
10
11
|
this.heartbeatJob = null;
|
|
11
12
|
}
|
|
@@ -95,7 +96,9 @@ class Scheduler {
|
|
|
95
96
|
const convId = this._getMessagingConversation(user.id);
|
|
96
97
|
|
|
97
98
|
await this.agentEngine.run(user.id, (prompt?.value || defaultPrompt) + platformHint, {
|
|
99
|
+
triggerType: 'heartbeat',
|
|
98
100
|
triggerSource: 'heartbeat',
|
|
101
|
+
app: this.app,
|
|
99
102
|
...(convId ? { conversationId: convId } : {}),
|
|
100
103
|
});
|
|
101
104
|
}
|
|
@@ -246,7 +249,9 @@ class Scheduler {
|
|
|
246
249
|
const convId = this._getMessagingConversation(userId);
|
|
247
250
|
|
|
248
251
|
const result = await this.agentEngine.run(userId, config.prompt + notifyHint, {
|
|
252
|
+
triggerType: 'scheduler',
|
|
249
253
|
triggerSource: 'scheduler',
|
|
254
|
+
app: this.app,
|
|
250
255
|
...(convId ? { conversationId: convId } : {}),
|
|
251
256
|
taskId,
|
|
252
257
|
});
|
|
@@ -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) {
|