neoagent 2.0.6 → 2.0.8
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/README.md +1 -21
- package/package.json +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +31782 -31656
- package/server/routes/recordings.js +20 -0
- package/server/routes/scheduler.js +2 -2
- package/server/routes/settings.js +32 -0
- package/server/services/ai/tools.js +6 -0
- package/server/services/recordings/manager.js +66 -3
- package/server/services/scheduler/cron.js +33 -17
|
@@ -110,4 +110,24 @@ router.post('/:sessionId/retry', async (req, res) => {
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
router.delete('/:sessionId/segments/:segmentId', (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const manager = req.app.locals.recordingManager;
|
|
116
|
+
const session = manager.deleteTranscriptSegment(
|
|
117
|
+
req.session.userId,
|
|
118
|
+
req.params.sessionId,
|
|
119
|
+
req.params.segmentId,
|
|
120
|
+
);
|
|
121
|
+
res.json({ session });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = sanitizeError(err);
|
|
124
|
+
const status = /not found/i.test(message)
|
|
125
|
+
? 404
|
|
126
|
+
: /positive integer/i.test(message)
|
|
127
|
+
? 400
|
|
128
|
+
: 500;
|
|
129
|
+
res.status(status).json({ error: message });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
113
133
|
module.exports = router;
|
|
@@ -14,13 +14,13 @@ router.get('/', (req, res) => {
|
|
|
14
14
|
// Create a new scheduled task
|
|
15
15
|
router.post('/', (req, res) => {
|
|
16
16
|
try {
|
|
17
|
-
const { name, cronExpression, prompt, enabled } = req.body;
|
|
17
|
+
const { name, cronExpression, prompt, enabled, model } = req.body;
|
|
18
18
|
if (!name || !cronExpression || !prompt) {
|
|
19
19
|
return res.status(400).json({ error: 'name, cronExpression, and prompt required' });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const scheduler = req.app.locals.scheduler;
|
|
23
|
-
const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled });
|
|
23
|
+
const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled, model });
|
|
24
24
|
res.status(201).json(task);
|
|
25
25
|
} catch (err) {
|
|
26
26
|
res.status(400).json({ error: sanitizeError(err) });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
3
4
|
const router = express.Router();
|
|
4
5
|
const db = require('../db/database');
|
|
5
6
|
const { requireAuth } = require('../middleware/auth');
|
|
@@ -28,6 +29,17 @@ function readUpdateStatus() {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function writeUpdateStatus(patch) {
|
|
33
|
+
const next = {
|
|
34
|
+
...readUpdateStatus(),
|
|
35
|
+
...patch,
|
|
36
|
+
updatedAt: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
fs.mkdirSync(path.dirname(UPDATE_STATUS_FILE), { recursive: true });
|
|
39
|
+
fs.writeFileSync(UPDATE_STATUS_FILE, JSON.stringify(next, null, 2));
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
// Get supported models metadata
|
|
32
44
|
router.get('/meta/models', async (req, res) => {
|
|
33
45
|
const { getSupportedModels } = require('../services/ai/models');
|
|
@@ -221,6 +233,16 @@ router.post('/update', (req, res) => {
|
|
|
221
233
|
return res.status(409).json({ success: false, error: 'An update is already running' });
|
|
222
234
|
}
|
|
223
235
|
console.log('[Settings] Triggering update-runner...');
|
|
236
|
+
writeUpdateStatus({
|
|
237
|
+
state: 'running',
|
|
238
|
+
progress: 1,
|
|
239
|
+
phase: 'starting',
|
|
240
|
+
message: 'Launching update job',
|
|
241
|
+
startedAt: new Date().toISOString(),
|
|
242
|
+
completedAt: null,
|
|
243
|
+
changelog: [],
|
|
244
|
+
logs: []
|
|
245
|
+
});
|
|
224
246
|
|
|
225
247
|
// Spawn detached runner so status survives server restarts.
|
|
226
248
|
const child = spawn(process.execPath, ['scripts/update-runner.js'], {
|
|
@@ -229,6 +251,16 @@ router.post('/update', (req, res) => {
|
|
|
229
251
|
cwd: APP_DIR
|
|
230
252
|
});
|
|
231
253
|
|
|
254
|
+
child.once('error', (error) => {
|
|
255
|
+
writeUpdateStatus({
|
|
256
|
+
state: 'failed',
|
|
257
|
+
progress: 100,
|
|
258
|
+
phase: 'failed',
|
|
259
|
+
message: `Failed to launch update job: ${error.message}`,
|
|
260
|
+
completedAt: new Date().toISOString()
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
232
264
|
child.unref();
|
|
233
265
|
res.json({ success: true, message: 'Update triggered', pid: child.pid });
|
|
234
266
|
});
|
|
@@ -437,6 +437,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
437
437
|
cron_expression: { type: 'string', description: 'Cron expression for the schedule, e.g. "0 9 * * 1-5" for weekdays at 9am, "*/30 * * * *" for every 30 minutes. Use standard 5-field cron syntax.' },
|
|
438
438
|
prompt: { type: 'string', description: 'The prompt/instructions the agent will run when triggered. Be specific about what to do and who to notify.' },
|
|
439
439
|
enabled: { type: 'boolean', description: 'Whether to activate immediately (default true)' },
|
|
440
|
+
model: { type: 'string', description: 'Optional specific AI model ID to force for this task. Omit to use the normal automatic/default model selection.' },
|
|
440
441
|
call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires, e.g. "+12125550100".' },
|
|
441
442
|
call_greeting: { type: 'string', description: 'Opening sentence spoken to the user when the call is answered. Required if call_to is set.' }
|
|
442
443
|
},
|
|
@@ -452,6 +453,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
452
453
|
name: { type: 'string', description: 'Short descriptive name, e.g. "Remind about meeting"' },
|
|
453
454
|
run_at: { type: 'string', description: 'ISO 8601 datetime when the run should fire, e.g. "2026-03-09T22:00:00"' },
|
|
454
455
|
prompt: { type: 'string', description: 'The prompt/instructions the agent will execute at that time. Be specific.' },
|
|
456
|
+
model: { type: 'string', description: 'Optional specific AI model ID to force for this run. Omit to use the normal automatic/default model selection.' },
|
|
455
457
|
call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this fires.' },
|
|
456
458
|
call_greeting: { type: 'string', description: 'Opening sentence spoken when the Telnyx call is answered.' }
|
|
457
459
|
},
|
|
@@ -485,6 +487,7 @@ function getAvailableTools(app, options = {}) {
|
|
|
485
487
|
cron_expression: { type: 'string', description: 'New cron expression, e.g. "0 8 * * *" for daily at 8am' },
|
|
486
488
|
prompt: { type: 'string', description: 'New prompt/instructions for the task' },
|
|
487
489
|
enabled: { type: 'boolean', description: 'Enable or disable the task' },
|
|
490
|
+
model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override and go back to automatic/default selection.' },
|
|
488
491
|
call_to: { type: 'string', description: 'E.164 phone number to call via Telnyx when this task fires. Set to empty string to remove.' },
|
|
489
492
|
call_greeting: { type: 'string', description: 'New opening sentence spoken when the Telnyx call is answered.' }
|
|
490
493
|
},
|
|
@@ -997,6 +1000,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
997
1000
|
cronExpression: args.cron_expression,
|
|
998
1001
|
prompt: args.prompt,
|
|
999
1002
|
enabled: args.enabled !== false,
|
|
1003
|
+
model: args.model || null,
|
|
1000
1004
|
callTo: args.call_to || null,
|
|
1001
1005
|
callGreeting: args.call_greeting || null
|
|
1002
1006
|
});
|
|
@@ -1016,6 +1020,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1016
1020
|
prompt: args.prompt,
|
|
1017
1021
|
runAt: args.run_at,
|
|
1018
1022
|
oneTime: true,
|
|
1023
|
+
model: args.model || null,
|
|
1019
1024
|
callTo: args.call_to || null,
|
|
1020
1025
|
callGreeting: args.call_greeting || null
|
|
1021
1026
|
});
|
|
@@ -1052,6 +1057,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1052
1057
|
if (args.cron_expression !== undefined) updates.cronExpression = args.cron_expression;
|
|
1053
1058
|
if (args.prompt !== undefined) updates.prompt = args.prompt;
|
|
1054
1059
|
if (args.enabled !== undefined) updates.enabled = args.enabled;
|
|
1060
|
+
if (args.model !== undefined) updates.model = args.model || null;
|
|
1055
1061
|
if (args.call_to !== undefined) updates.callTo = args.call_to || null;
|
|
1056
1062
|
if (args.call_greeting !== undefined) updates.callGreeting = args.call_greeting || null;
|
|
1057
1063
|
const updated = s.updateTask(args.task_id, userId, updates);
|
|
@@ -441,9 +441,7 @@ class RecordingManager {
|
|
|
441
441
|
}
|
|
442
442
|
})();
|
|
443
443
|
|
|
444
|
-
const transcriptText = ordered
|
|
445
|
-
.map((segment) => `[${this.#formatTimestamp(segment.startMs)}] ${segment.text}`)
|
|
446
|
-
.join('\n');
|
|
444
|
+
const transcriptText = this.#composeTranscriptText(ordered);
|
|
447
445
|
db.prepare(`
|
|
448
446
|
UPDATE recording_sessions
|
|
449
447
|
SET
|
|
@@ -490,6 +488,57 @@ class RecordingManager {
|
|
|
490
488
|
return this.getSession(userId, sessionId);
|
|
491
489
|
}
|
|
492
490
|
|
|
491
|
+
deleteTranscriptSegment(userId, sessionId, segmentId) {
|
|
492
|
+
const session = db.prepare(`
|
|
493
|
+
SELECT id
|
|
494
|
+
FROM recording_sessions
|
|
495
|
+
WHERE id = ? AND user_id = ?
|
|
496
|
+
`).get(sessionId, userId);
|
|
497
|
+
if (!session) {
|
|
498
|
+
throw new Error('Recording session not found.');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const normalizedSegmentId = Number(segmentId);
|
|
502
|
+
if (!Number.isInteger(normalizedSegmentId) || normalizedSegmentId <= 0) {
|
|
503
|
+
throw new Error('segmentId must be a positive integer.');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const segment = db.prepare(`
|
|
507
|
+
SELECT id
|
|
508
|
+
FROM recording_transcript_segments
|
|
509
|
+
WHERE session_id = ? AND id = ?
|
|
510
|
+
`).get(sessionId, normalizedSegmentId);
|
|
511
|
+
if (!segment) {
|
|
512
|
+
throw new Error('Transcript segment not found.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const now = new Date().toISOString();
|
|
516
|
+
let transcriptText = '';
|
|
517
|
+
db.transaction(() => {
|
|
518
|
+
db.prepare(`
|
|
519
|
+
DELETE FROM recording_transcript_segments
|
|
520
|
+
WHERE session_id = ? AND id = ?
|
|
521
|
+
`).run(sessionId, normalizedSegmentId);
|
|
522
|
+
|
|
523
|
+
const remainingSegments = db.prepare(`
|
|
524
|
+
SELECT start_ms, text
|
|
525
|
+
FROM recording_transcript_segments
|
|
526
|
+
WHERE session_id = ?
|
|
527
|
+
ORDER BY start_ms ASC, id ASC
|
|
528
|
+
`).all(sessionId);
|
|
529
|
+
transcriptText = this.#composeTranscriptText(remainingSegments);
|
|
530
|
+
|
|
531
|
+
db.prepare(`
|
|
532
|
+
UPDATE recording_sessions
|
|
533
|
+
SET transcript_text = ?, updated_at = ?
|
|
534
|
+
WHERE id = ?
|
|
535
|
+
`).run(transcriptText, now, sessionId);
|
|
536
|
+
})();
|
|
537
|
+
|
|
538
|
+
this.#emitUpdate(userId, sessionId);
|
|
539
|
+
return this.getSession(userId, sessionId);
|
|
540
|
+
}
|
|
541
|
+
|
|
493
542
|
async #transcribeSourceChunks(source, chunks) {
|
|
494
543
|
const segments = [];
|
|
495
544
|
|
|
@@ -677,6 +726,20 @@ class RecordingManager {
|
|
|
677
726
|
};
|
|
678
727
|
}
|
|
679
728
|
|
|
729
|
+
#composeTranscriptText(segments) {
|
|
730
|
+
return (Array.isArray(segments) ? segments : [])
|
|
731
|
+
.map((segment) => {
|
|
732
|
+
const startMs = Number(segment.startMs ?? segment.start_ms) || 0;
|
|
733
|
+
const text = `${segment.text || ''}`.trim();
|
|
734
|
+
if (!text) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
return `[${this.#formatTimestamp(startMs)}] ${text}`;
|
|
738
|
+
})
|
|
739
|
+
.filter((line) => line != null)
|
|
740
|
+
.join('\n');
|
|
741
|
+
}
|
|
742
|
+
|
|
680
743
|
#emitUpdate(userId, sessionId) {
|
|
681
744
|
this.io?.to?.(`user:${userId}`)?.emit('recordings:updated', { sessionId });
|
|
682
745
|
}
|
|
@@ -108,7 +108,7 @@ class Scheduler {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
createTask(userId, { name, cronExpression, prompt, enabled = true, callTo = null, callGreeting = null, runAt = null, oneTime = false }) {
|
|
111
|
+
createTask(userId, { name, cronExpression, prompt, enabled = true, callTo = null, callGreeting = null, model = null, runAt = null, oneTime = false }) {
|
|
112
112
|
if (oneTime) {
|
|
113
113
|
if (!runAt) throw new Error('runAt is required for one-time tasks');
|
|
114
114
|
const runAtDate = new Date(runAt);
|
|
@@ -116,12 +116,13 @@ class Scheduler {
|
|
|
116
116
|
|
|
117
117
|
const config = { prompt };
|
|
118
118
|
if (callTo) { config.callTo = callTo; config.callGreeting = callGreeting || ''; }
|
|
119
|
+
if (typeof model === 'string' && model.trim()) config.model = model.trim();
|
|
119
120
|
|
|
120
121
|
const result = db.prepare(
|
|
121
122
|
'INSERT INTO scheduled_tasks (user_id, name, cron_expression, run_at, one_time, task_type, task_config, enabled) VALUES (?, ?, NULL, ?, 1, ?, ?, ?)'
|
|
122
123
|
).run(userId, name, runAtDate.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''), 'agent_prompt', JSON.stringify(config), enabled ? 1 : 0);
|
|
123
124
|
|
|
124
|
-
return { id: result.lastInsertRowid, name, runAt: runAtDate.toISOString(), oneTime: true, enabled };
|
|
125
|
+
return { id: result.lastInsertRowid, name, runAt: runAtDate.toISOString(), oneTime: true, enabled, model: config.model || null };
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
if (!cronExpression || !cron.validate(cronExpression)) {
|
|
@@ -130,6 +131,7 @@ class Scheduler {
|
|
|
130
131
|
|
|
131
132
|
const config = { prompt };
|
|
132
133
|
if (callTo) { config.callTo = callTo; config.callGreeting = callGreeting || ''; }
|
|
134
|
+
if (typeof model === 'string' && model.trim()) config.model = model.trim();
|
|
133
135
|
|
|
134
136
|
const result = db.prepare(
|
|
135
137
|
'INSERT INTO scheduled_tasks (user_id, name, cron_expression, task_type, task_config, enabled) VALUES (?, ?, ?, ?, ?, ?)'
|
|
@@ -141,7 +143,7 @@ class Scheduler {
|
|
|
141
143
|
this._scheduleTask(taskId, userId, cronExpression, config);
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null };
|
|
146
|
+
return { id: taskId, name, cronExpression, enabled, callTo: config.callTo || null, model: config.model || null };
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
updateTask(taskId, userId, updates) {
|
|
@@ -157,6 +159,13 @@ class Scheduler {
|
|
|
157
159
|
if (updates.prompt !== undefined) config.prompt = updates.prompt;
|
|
158
160
|
if (updates.callTo !== undefined) config.callTo = updates.callTo || null;
|
|
159
161
|
if (updates.callGreeting !== undefined) config.callGreeting = updates.callGreeting || null;
|
|
162
|
+
if (updates.model !== undefined) {
|
|
163
|
+
if (typeof updates.model === 'string' && updates.model.trim()) {
|
|
164
|
+
config.model = updates.model.trim();
|
|
165
|
+
} else {
|
|
166
|
+
delete config.model;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
160
169
|
// Clean up nulls
|
|
161
170
|
if (!config.callTo) { delete config.callTo; delete config.callGreeting; }
|
|
162
171
|
|
|
@@ -178,7 +187,7 @@ class Scheduler {
|
|
|
178
187
|
this._scheduleTask(taskId, userId, cronExpr, config);
|
|
179
188
|
}
|
|
180
189
|
|
|
181
|
-
return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null };
|
|
190
|
+
return { id: taskId, name, cronExpression: cronExpr, enabled, callTo: config.callTo || null, model: config.model || null };
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
deleteTask(taskId, userId) {
|
|
@@ -197,17 +206,21 @@ class Scheduler {
|
|
|
197
206
|
|
|
198
207
|
listTasks(userId) {
|
|
199
208
|
const tasks = db.prepare('SELECT * FROM scheduled_tasks WHERE user_id = ? ORDER BY created_at DESC').all(userId);
|
|
200
|
-
return tasks.map(t =>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
return tasks.map(t => {
|
|
210
|
+
const config = JSON.parse(t.task_config || '{}');
|
|
211
|
+
return {
|
|
212
|
+
id: t.id,
|
|
213
|
+
name: t.name,
|
|
214
|
+
cronExpression: t.cron_expression,
|
|
215
|
+
runAt: t.run_at || null,
|
|
216
|
+
oneTime: !!t.one_time,
|
|
217
|
+
enabled: !!t.enabled,
|
|
218
|
+
lastRun: t.last_run,
|
|
219
|
+
nextRun: t.one_time ? t.run_at : this._getNextRun(t.cron_expression),
|
|
220
|
+
config,
|
|
221
|
+
model: config.model || null
|
|
222
|
+
};
|
|
223
|
+
});
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
runTaskNow(taskId, userId) {
|
|
@@ -248,13 +261,16 @@ class Scheduler {
|
|
|
248
261
|
|
|
249
262
|
const convId = this._getMessagingConversation(userId);
|
|
250
263
|
|
|
251
|
-
const
|
|
264
|
+
const runOptions = {
|
|
252
265
|
triggerType: 'scheduler',
|
|
253
266
|
triggerSource: 'scheduler',
|
|
254
267
|
app: this.app,
|
|
255
268
|
...(convId ? { conversationId: convId } : {}),
|
|
256
269
|
taskId,
|
|
257
|
-
}
|
|
270
|
+
};
|
|
271
|
+
const result = typeof this.agentEngine.runWithModel === 'function'
|
|
272
|
+
? await this.agentEngine.runWithModel(userId, config.prompt + notifyHint, runOptions, config.model || null)
|
|
273
|
+
: await this.agentEngine.run(userId, config.prompt + notifyHint, runOptions);
|
|
258
274
|
this.io.to(`user:${userId}`).emit('scheduler:task_complete', { taskId, result });
|
|
259
275
|
}
|
|
260
276
|
} catch (err) {
|