neoagent 2.3.0 → 2.3.1-beta.10
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 +13 -0
- package/README.md +3 -1
- package/docs/automation.md +1 -1
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +14 -1
- package/docs/integrations.md +6 -1
- package/lib/manager.js +127 -1
- package/package.json +2 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +61049 -60444
- package/server/routes/integrations.js +97 -8
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -0
- package/server/services/ai/engine.js +9 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openaiCodex.js +26 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +150 -11
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +126 -0
- package/server/services/integrations/env.js +19 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/home_assistant/provider.js +630 -0
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +10 -0
- package/server/services/integrations/spotify/provider.js +487 -0
- package/server/services/integrations/weather/provider.js +559 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/messaging/manager.js +29 -7
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +2 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/adapters/schedule.js +33 -5
- package/server/services/tasks/adapters/weather_event.js +84 -0
- package/server/services/tasks/integration_runtime.js +85 -0
- package/server/services/tasks/runtime.js +2 -2
- package/server/services/voice/agentBridge.js +20 -4
- package/server/services/voice/message.js +3 -0
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/voice/runtimeManager.js +136 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
summarizeAccessPolicy,
|
|
34
34
|
classifyRecentTarget,
|
|
35
35
|
} = require('./access_policy');
|
|
36
|
+
const { decryptValue, encryptValue } = require('../integrations/secrets');
|
|
36
37
|
|
|
37
38
|
const LEGACY_WHATSAPP_AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
|
|
38
39
|
|
|
@@ -268,6 +269,31 @@ class MessagingManager extends EventEmitter {
|
|
|
268
269
|
return undefined;
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
_encodeStoredConfig(config) {
|
|
273
|
+
const serialized = JSON.stringify(this._persistableConfig(config) || {});
|
|
274
|
+
if (!serialized) return '{}';
|
|
275
|
+
try {
|
|
276
|
+
return encryptValue(serialized);
|
|
277
|
+
} catch {
|
|
278
|
+
return serialized;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_decodeStoredConfig(value) {
|
|
283
|
+
const raw = String(value || '').trim();
|
|
284
|
+
if (!raw) return {};
|
|
285
|
+
try {
|
|
286
|
+
const decoded = decryptValue(raw);
|
|
287
|
+
return decoded ? JSON.parse(decoded) : {};
|
|
288
|
+
} catch {
|
|
289
|
+
try {
|
|
290
|
+
return JSON.parse(raw);
|
|
291
|
+
} catch {
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
async connectPlatform(userId, platformName, config = {}, options = {}) {
|
|
272
298
|
const agentId = this._agentId(userId, options);
|
|
273
299
|
config = { ...(config || {}) };
|
|
@@ -322,7 +348,7 @@ class MessagingManager extends EventEmitter {
|
|
|
322
348
|
config.voiceRuntimeManager = this.voiceRuntimeManager || null;
|
|
323
349
|
}
|
|
324
350
|
|
|
325
|
-
const storedConfig =
|
|
351
|
+
const storedConfig = this._encodeStoredConfig(config);
|
|
326
352
|
|
|
327
353
|
const key = this._key(userId, agentId, platformName);
|
|
328
354
|
let platform = this.platforms.get(key);
|
|
@@ -569,11 +595,7 @@ class MessagingManager extends EventEmitter {
|
|
|
569
595
|
if (platformName === 'whatsapp') {
|
|
570
596
|
reconnectConfig = platform?.config || {};
|
|
571
597
|
if ((!reconnectConfig || Object.keys(reconnectConfig).length === 0) && row?.config) {
|
|
572
|
-
|
|
573
|
-
reconnectConfig = JSON.parse(row.config);
|
|
574
|
-
} catch {
|
|
575
|
-
reconnectConfig = {};
|
|
576
|
-
}
|
|
598
|
+
reconnectConfig = this._decodeStoredConfig(row.config);
|
|
577
599
|
}
|
|
578
600
|
}
|
|
579
601
|
if (platform && platform.logout) {
|
|
@@ -594,7 +616,7 @@ class MessagingManager extends EventEmitter {
|
|
|
594
616
|
).all();
|
|
595
617
|
for (const row of rows) {
|
|
596
618
|
try {
|
|
597
|
-
const config =
|
|
619
|
+
const config = this._decodeStoredConfig(row.config);
|
|
598
620
|
console.log(`[Messaging] Restoring ${row.platform} for user ${row.user_id} agent ${row.agent_id || 'main'}`);
|
|
599
621
|
await this.connectPlatform(row.user_id, row.platform, config, { agentId: row.agent_id });
|
|
600
622
|
} catch (err) {
|
|
@@ -1161,6 +1161,39 @@ Auto-orients and arranges the STL, applies settings from JSON files, slices all
|
|
|
1161
1161
|
3. Build the command from the flags above
|
|
1162
1162
|
4. Run it and check for errors in the output (debug level 2 is a good default)
|
|
1163
1163
|
5. Report the output file location and any warnings`
|
|
1164
|
+
},
|
|
1165
|
+
|
|
1166
|
+
{
|
|
1167
|
+
id: 'package-tracker',
|
|
1168
|
+
name: 'Package Tracker',
|
|
1169
|
+
description: 'Track packages by automatically detecting the carrier and fetching the tracking link.',
|
|
1170
|
+
category: 'info',
|
|
1171
|
+
icon: '📦',
|
|
1172
|
+
content: `---
|
|
1173
|
+
name: package-tracker
|
|
1174
|
+
description: Track packages by automatically detecting the carrier and fetching the tracking link.
|
|
1175
|
+
category: info
|
|
1176
|
+
icon: 📦
|
|
1177
|
+
enabled: true
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
You are a package tracking assistant. When the user gives you a tracking number, you should:
|
|
1181
|
+
|
|
1182
|
+
1. Identify the carrier using common regex patterns:
|
|
1183
|
+
- UPS: \`\\b(1Z[0-9A-Z]{16})\\b\`
|
|
1184
|
+
- FedEx: \`\\b([0-9]{12,15})\\b\` (usually 12 or 15 digits)
|
|
1185
|
+
- USPS: \`\\b(94[0-9]{20})\\b\` or \`\\b([A-Z]{2}[0-9]{9}US)\\b\`
|
|
1186
|
+
- DHL: \`\\b([0-9]{10})\\b\` or \`\\b([0-9]{20})\\b\`
|
|
1187
|
+
|
|
1188
|
+
2. Generate the direct tracking link for the user:
|
|
1189
|
+
- UPS: \`https://www.ups.com/track?tracknum={number}\`
|
|
1190
|
+
- FedEx: \`https://www.fedex.com/fedextrack/?trknbr={number}\`
|
|
1191
|
+
- USPS: \`https://tools.usps.com/go/TrackConfirmAction?tLabels={number}\`
|
|
1192
|
+
- DHL: \`https://www.dhl.com/global-en/home/tracking/tracking-express.html?submit=1&tracking-id={number}\`
|
|
1193
|
+
|
|
1194
|
+
3. If the carrier is not obvious, use \`web_search\` with the query "track package {number}" to identify the carrier.
|
|
1195
|
+
4. Present the carrier and the direct tracking link to the user clearly.
|
|
1196
|
+
5. Optionally use \`browser_navigate\` or \`http_request\` to fetch the current status from the tracking link, but note that many carriers block automated scraping. The direct link is the most important output.`
|
|
1164
1197
|
}
|
|
1165
1198
|
];
|
|
1166
1199
|
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = [
|
|
4
4
|
require('./schedule'),
|
|
5
|
+
require('./manual'),
|
|
5
6
|
require('./gmail_message_received'),
|
|
6
7
|
require('./outlook_email_received'),
|
|
7
8
|
require('./slack_message_received'),
|
|
8
9
|
require('./teams_message_received'),
|
|
10
|
+
require('./weather_event'),
|
|
9
11
|
require('./whatsapp_personal_message_received'),
|
|
10
12
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const cron = require('node-cron');
|
|
4
|
-
const { findNextRun } = require('../schedule_utils');
|
|
4
|
+
const { findNextRun, parseCronExpression } = require('../schedule_utils');
|
|
5
5
|
|
|
6
6
|
function normalizeRunAt(value) {
|
|
7
7
|
if (!value) return null;
|
|
@@ -12,6 +12,37 @@ function normalizeRunAt(value) {
|
|
|
12
12
|
return date.toISOString();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function normalizeCronExpression(value) {
|
|
16
|
+
const raw = String(value || '').trim();
|
|
17
|
+
if (!raw) {
|
|
18
|
+
throw new Error('A valid cron expression is required.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fields = raw.split(/\s+/);
|
|
22
|
+
if (fields.length === 6) {
|
|
23
|
+
const seconds = String(fields[0] || '').trim();
|
|
24
|
+
if (seconds !== '0' && seconds !== '*') {
|
|
25
|
+
throw new Error('Cron expressions with seconds are not supported. Use a 5-field expression.');
|
|
26
|
+
}
|
|
27
|
+
fields.shift();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (fields.length !== 5) {
|
|
31
|
+
throw new Error('A valid cron expression is required.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Quartz-style "?" means "no specific value"; convert to standard wildcard.
|
|
35
|
+
if (fields[2] === '?') fields[2] = '*';
|
|
36
|
+
if (fields[4] === '?') fields[4] = '*';
|
|
37
|
+
|
|
38
|
+
const normalized = fields.join(' ');
|
|
39
|
+
parseCronExpression(normalized);
|
|
40
|
+
if (!cron.validate(normalized)) {
|
|
41
|
+
throw new Error('A valid cron expression is required.');
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
15
46
|
module.exports = {
|
|
16
47
|
type: 'schedule',
|
|
17
48
|
label: 'Schedule',
|
|
@@ -31,10 +62,7 @@ module.exports = {
|
|
|
31
62
|
};
|
|
32
63
|
}
|
|
33
64
|
|
|
34
|
-
const cronExpression =
|
|
35
|
-
if (!cronExpression || !cron.validate(cronExpression)) {
|
|
36
|
-
throw new Error('A valid cron expression is required.');
|
|
37
|
-
}
|
|
65
|
+
const cronExpression = normalizeCronExpression(config.cronExpression || config.cron_expression);
|
|
38
66
|
return {
|
|
39
67
|
mode,
|
|
40
68
|
cronExpression,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ensureOwnedIntegrationConnection,
|
|
5
|
+
normalizeTrimmedText,
|
|
6
|
+
} = require('../security');
|
|
7
|
+
|
|
8
|
+
const WEATHER_EVENT_TYPES = new Set([
|
|
9
|
+
'rain_start',
|
|
10
|
+
'snow_start',
|
|
11
|
+
'wind_alert',
|
|
12
|
+
'temperature_above',
|
|
13
|
+
'temperature_below',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function normalizeNumber(value, fallback) {
|
|
17
|
+
const numeric = Number(value);
|
|
18
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeEventTypes(value) {
|
|
22
|
+
const raw = Array.isArray(value)
|
|
23
|
+
? value
|
|
24
|
+
: String(value || '')
|
|
25
|
+
.split(',')
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const normalized = raw
|
|
29
|
+
.map((entry) => String(entry || '').trim().toLowerCase())
|
|
30
|
+
.filter((entry) => WEATHER_EVENT_TYPES.has(entry));
|
|
31
|
+
return Array.from(new Set(normalized));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
type: 'weather_event',
|
|
36
|
+
label: 'Weather Event',
|
|
37
|
+
providerKey: 'weather',
|
|
38
|
+
appKey: 'forecast',
|
|
39
|
+
async validateConfig(config = {}, context = {}) {
|
|
40
|
+
const connection = ensureOwnedIntegrationConnection(context.integrationManager, {
|
|
41
|
+
userId: context.userId,
|
|
42
|
+
agentId: context.agentId,
|
|
43
|
+
connectionId: config.connectionId || config.connection_id,
|
|
44
|
+
providerKey: 'weather',
|
|
45
|
+
appKey: 'forecast',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const eventTypes = normalizeEventTypes(config.eventTypes || config.event_types);
|
|
49
|
+
if (eventTypes.length === 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'At least one weather event type is required: rain_start, snow_start, wind_alert, temperature_above, temperature_below.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const location = normalizeTrimmedText(
|
|
56
|
+
config.location || config.locationQuery || config.query,
|
|
57
|
+
180,
|
|
58
|
+
);
|
|
59
|
+
if (!location) {
|
|
60
|
+
throw new Error('Weather event location is required (for example: Berlin, DE).');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
connectionId: connection.id,
|
|
65
|
+
accountEmail: connection.account_email || null,
|
|
66
|
+
location,
|
|
67
|
+
eventTypes,
|
|
68
|
+
minPrecipitationMm: normalizeNumber(config.minPrecipitationMm ?? config.min_precipitation_mm, 0.4),
|
|
69
|
+
minSnowfallCm: normalizeNumber(config.minSnowfallCm ?? config.min_snowfall_cm, 0.2),
|
|
70
|
+
windAlertKph: normalizeNumber(config.windAlertKph ?? config.wind_alert_kph, 40),
|
|
71
|
+
temperatureAboveC: normalizeNumber(config.temperatureAboveC ?? config.temperature_above_c, 32),
|
|
72
|
+
temperatureBelowC: normalizeNumber(config.temperatureBelowC ?? config.temperature_below_c, 0),
|
|
73
|
+
horizonHours: Math.max(1, Math.min(Number(config.horizonHours || config.horizon_hours) || 12, 48)),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
summarize(config = {}) {
|
|
77
|
+
const parts = ['Weather'];
|
|
78
|
+
if (config.location) parts.push(config.location);
|
|
79
|
+
if (Array.isArray(config.eventTypes) && config.eventTypes.length > 0) {
|
|
80
|
+
parts.push(config.eventTypes.join(', '));
|
|
81
|
+
}
|
|
82
|
+
return parts.join(' · ');
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -8,6 +8,7 @@ const POLLED_TRIGGER_TYPES = Object.freeze([
|
|
|
8
8
|
'outlook_email_received',
|
|
9
9
|
'slack_message_received',
|
|
10
10
|
'teams_message_received',
|
|
11
|
+
'weather_event',
|
|
11
12
|
'whatsapp_personal_message_received',
|
|
12
13
|
]);
|
|
13
14
|
|
|
@@ -166,6 +167,90 @@ async function fetchTriggerRows({ integrationManager, userId, agentId, triggerTy
|
|
|
166
167
|
.sort(sortByTimestamp);
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
if (triggerType === 'weather_event') {
|
|
171
|
+
const forecast = await integrationManager.executeTool(userId, 'weather_get_forecast', {
|
|
172
|
+
...connectionArg,
|
|
173
|
+
...(config.location ? { location: config.location } : {}),
|
|
174
|
+
forecast_hours: Math.max(1, Math.min(Number(config.horizonHours) || 12, 48)),
|
|
175
|
+
}, scopedAgentId);
|
|
176
|
+
const hourly = Array.isArray(forecast?.hourly) ? forecast.hourly : [];
|
|
177
|
+
const eventTypes = Array.isArray(config.eventTypes) ? config.eventTypes : [];
|
|
178
|
+
const rows = [];
|
|
179
|
+
|
|
180
|
+
for (let index = 0; index < hourly.length; index += 1) {
|
|
181
|
+
const row = hourly[index] || {};
|
|
182
|
+
const previous = index > 0 ? (hourly[index - 1] || {}) : null;
|
|
183
|
+
const time = String(row.time || '').trim();
|
|
184
|
+
if (!time) continue;
|
|
185
|
+
|
|
186
|
+
const rain = Number(row.rain || row.precipitation || 0);
|
|
187
|
+
const prevRain = Number(previous?.rain || previous?.precipitation || 0);
|
|
188
|
+
const snowfall = Number(row.snowfall || 0);
|
|
189
|
+
const prevSnow = Number(previous?.snowfall || 0);
|
|
190
|
+
const windSpeed = Number(row.windSpeed || 0);
|
|
191
|
+
const temperature = Number(row.temperature);
|
|
192
|
+
|
|
193
|
+
const candidates = [
|
|
194
|
+
{
|
|
195
|
+
type: 'rain_start',
|
|
196
|
+
active:
|
|
197
|
+
eventTypes.includes('rain_start')
|
|
198
|
+
&& rain >= Number(config.minPrecipitationMm || 0.4)
|
|
199
|
+
&& prevRain < Number(config.minPrecipitationMm || 0.4),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'snow_start',
|
|
203
|
+
active:
|
|
204
|
+
eventTypes.includes('snow_start')
|
|
205
|
+
&& snowfall >= Number(config.minSnowfallCm || 0.2)
|
|
206
|
+
&& prevSnow < Number(config.minSnowfallCm || 0.2),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
type: 'wind_alert',
|
|
210
|
+
active:
|
|
211
|
+
eventTypes.includes('wind_alert')
|
|
212
|
+
&& windSpeed >= Number(config.windAlertKph || 40),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: 'temperature_above',
|
|
216
|
+
active:
|
|
217
|
+
eventTypes.includes('temperature_above')
|
|
218
|
+
&& Number.isFinite(temperature)
|
|
219
|
+
&& temperature >= Number(config.temperatureAboveC || 32),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'temperature_below',
|
|
223
|
+
active:
|
|
224
|
+
eventTypes.includes('temperature_below')
|
|
225
|
+
&& Number.isFinite(temperature)
|
|
226
|
+
&& temperature <= Number(config.temperatureBelowC || 0),
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
if (!candidate.active) continue;
|
|
232
|
+
rows.push({
|
|
233
|
+
fingerprint: `weather:${config.connectionId}:${candidate.type}:${time}`,
|
|
234
|
+
timestamp: time,
|
|
235
|
+
context: {
|
|
236
|
+
triggerEvent: {
|
|
237
|
+
provider: 'weather',
|
|
238
|
+
eventType: candidate.type,
|
|
239
|
+
location: forecast?.location?.label || config.location || null,
|
|
240
|
+
time,
|
|
241
|
+
rain,
|
|
242
|
+
snowfall,
|
|
243
|
+
windSpeed,
|
|
244
|
+
temperature,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return rows.sort(sortByTimestamp);
|
|
252
|
+
}
|
|
253
|
+
|
|
169
254
|
return [];
|
|
170
255
|
}
|
|
171
256
|
|
|
@@ -66,7 +66,7 @@ class TaskRuntime {
|
|
|
66
66
|
label: adapter.label,
|
|
67
67
|
providerKey: adapter.providerKey || null,
|
|
68
68
|
appKey: adapter.appKey || null,
|
|
69
|
-
available: adapter.type === 'schedule'
|
|
69
|
+
available: adapter.type === 'schedule' || adapter.type === 'manual'
|
|
70
70
|
? true
|
|
71
71
|
: this._hasConnectedApp(userId, agentId, adapter.providerKey, adapter.appKey),
|
|
72
72
|
}));
|
|
@@ -222,8 +222,8 @@ class TaskRuntime {
|
|
|
222
222
|
const existing = this.scheduleJobs.get(taskId);
|
|
223
223
|
if (existing) {
|
|
224
224
|
existing.task.stop();
|
|
225
|
-
this.scheduleJobs.delete(taskId);
|
|
226
225
|
}
|
|
226
|
+
this.scheduleJobs.delete(taskId);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
async _executeTask(taskId, userId, executionMeta = {}) {
|
|
@@ -23,6 +23,7 @@ class VoiceAgentBridge {
|
|
|
23
23
|
session.currentRunId = runId;
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
|
+
const deferredFollowUp = await this.runtimeManager.prepareDeferredVoiceFollowUp(session);
|
|
26
27
|
const result = await runVoiceTranscriptTurn({
|
|
27
28
|
userId: session.userId,
|
|
28
29
|
agentId: session.agentId,
|
|
@@ -41,10 +42,25 @@ class VoiceAgentBridge {
|
|
|
41
42
|
runId,
|
|
42
43
|
});
|
|
43
44
|
session.currentRunId = result.runId || runId;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const replyText = String(result.replyText || '').trim();
|
|
46
|
+
if (replyText) {
|
|
47
|
+
if (deferredFollowUp) {
|
|
48
|
+
const followUp = await this.runtimeManager.deliverDeferredVoiceFollowUp(
|
|
49
|
+
session,
|
|
50
|
+
deferredFollowUp,
|
|
51
|
+
replyText,
|
|
52
|
+
result.runId || runId,
|
|
53
|
+
);
|
|
54
|
+
if (!followUp?.sent) {
|
|
55
|
+
await this.runtimeManager.deliverAssistantMessage(session, replyText, {
|
|
56
|
+
kind: 'final',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
await this.runtimeManager.deliverAssistantMessage(session, replyText, {
|
|
61
|
+
kind: 'final',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
48
64
|
}
|
|
49
65
|
await session.setState('idle');
|
|
50
66
|
session.currentRunId = null;
|
|
@@ -61,6 +61,9 @@ function buildDirectVoiceContext({
|
|
|
61
61
|
allowInterimUpdates
|
|
62
62
|
? 'Do not use send_message. Use send_interim_update only for short spoken progress updates when silence would otherwise be noticeable.'
|
|
63
63
|
: 'Do not use send_message or send_interim_update.',
|
|
64
|
+
allowInterimUpdates
|
|
65
|
+
? 'You decide when a task is taking long. If so, send a natural deferral via send_interim_update and set defer_follow_up=true.'
|
|
66
|
+
: 'Keep the spoken response direct and concise.',
|
|
64
67
|
'Return only the assistant reply.',
|
|
65
68
|
];
|
|
66
69
|
|
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { OpenAI } = require('openai');
|
|
6
6
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
7
|
+
const { decryptLocalValue } = require('../../utils/local_secrets');
|
|
7
8
|
|
|
8
9
|
let cachedClient = null;
|
|
9
10
|
|
|
@@ -15,7 +16,9 @@ function resolveOpenAiApiKey() {
|
|
|
15
16
|
try {
|
|
16
17
|
const keysPath = path.join(AGENT_DATA_DIR, 'API_KEYS.json');
|
|
17
18
|
const keys = JSON.parse(fs.readFileSync(keysPath, 'utf8'));
|
|
18
|
-
const candidate = keys.OPENAI_API_KEY
|
|
19
|
+
const candidate = decryptLocalValue(keys.OPENAI_API_KEY)
|
|
20
|
+
|| decryptLocalValue(keys.openai_api_key)
|
|
21
|
+
|| decryptLocalValue(keys.openai);
|
|
19
22
|
return typeof candidate === 'string' && candidate.trim() ? candidate.trim() : '';
|
|
20
23
|
} catch {
|
|
21
24
|
return '';
|
|
@@ -6,6 +6,7 @@ const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
|
6
6
|
const { getOpenAiClient } = require('./openaiClient');
|
|
7
7
|
const { synthesizeSpeechBuffer } = require('./openaiSpeech');
|
|
8
8
|
const { transcribeChunkWithDeepgram } = require('../recordings/deepgram');
|
|
9
|
+
const { decryptLocalValue } = require('../../utils/local_secrets');
|
|
9
10
|
|
|
10
11
|
const DEFAULT_STT_PROVIDER = 'openai';
|
|
11
12
|
const DEFAULT_TTS_PROVIDER = 'openai';
|
|
@@ -94,7 +95,7 @@ function resolveApiKey(candidates = []) {
|
|
|
94
95
|
const snake = lower.replace(/[^a-z0-9]+/g, '_');
|
|
95
96
|
const variants = [key, lower, snake];
|
|
96
97
|
for (const variant of variants) {
|
|
97
|
-
const value = keys[variant];
|
|
98
|
+
const value = decryptLocalValue(keys[variant]);
|
|
98
99
|
if (typeof value === 'string' && value.trim()) {
|
|
99
100
|
return value.trim();
|
|
100
101
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require('crypto');
|
|
4
|
+
const db = require('../../db/database');
|
|
4
5
|
|
|
5
6
|
const { getProviderRuntimeConfig } = require('../ai/models');
|
|
7
|
+
const { isMainAgent, resolveAgentId } = require('../agents/manager');
|
|
6
8
|
const { getVoiceRuntimeSettings } = require('./liveSettings');
|
|
7
9
|
const { VoiceLiveSession } = require('./liveSession');
|
|
8
10
|
const { OpenAiLiveRelayAdapter } = require('./openaiLiveRelayAdapter');
|
|
@@ -180,11 +182,14 @@ class VoiceRuntimeManager {
|
|
|
180
182
|
await session.setState('idle');
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
async publishInterimUpdate({ sessionId, content, kind = 'progress' } = {}) {
|
|
185
|
+
async publishInterimUpdate({ sessionId, content, kind = 'progress', deferFollowUp = false } = {}) {
|
|
184
186
|
const session = this.getSession(sessionId);
|
|
185
187
|
if (!session || session.closed) {
|
|
186
188
|
return { sent: false, skipped: true, reason: 'Voice session is not active.' };
|
|
187
189
|
}
|
|
190
|
+
if (deferFollowUp === true) {
|
|
191
|
+
session.deferFollowUpRequested = true;
|
|
192
|
+
}
|
|
188
193
|
await this.deliverAssistantMessage(session, content, { kind });
|
|
189
194
|
return { sent: true };
|
|
190
195
|
}
|
|
@@ -196,6 +201,88 @@ class VoiceRuntimeManager {
|
|
|
196
201
|
await session.publishAssistantOutput(normalized, options);
|
|
197
202
|
}
|
|
198
203
|
|
|
204
|
+
async prepareDeferredVoiceFollowUp(session) {
|
|
205
|
+
if (!session || session.closed) return null;
|
|
206
|
+
if (String(session.platform || '').trim() !== 'voice_live') return null;
|
|
207
|
+
|
|
208
|
+
const target = this.#resolvePreferredMessagingTarget(session.userId, session.agentId);
|
|
209
|
+
if (!target) return null;
|
|
210
|
+
return {
|
|
211
|
+
target,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deliverDeferredVoiceFollowUp(session, followUpPlan, replyText, runId = null) {
|
|
216
|
+
if (!session || session.closed || !followUpPlan || session.deferFollowUpRequested !== true) {
|
|
217
|
+
return { sent: false, skipped: true };
|
|
218
|
+
}
|
|
219
|
+
const manager = this.#messagingManager();
|
|
220
|
+
if (!manager || typeof manager.sendMessage !== 'function') {
|
|
221
|
+
return { sent: false, skipped: true, reason: 'Messaging manager unavailable.' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const target = followUpPlan.target;
|
|
225
|
+
if (!target?.platform || !target?.to) {
|
|
226
|
+
return { sent: false, skipped: true, reason: 'No deferred follow-up target.' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const status = typeof manager.getPlatformStatus === 'function'
|
|
230
|
+
? manager.getPlatformStatus(session.userId, target.platform, { agentId: session.agentId })
|
|
231
|
+
: null;
|
|
232
|
+
if (status && status.status !== 'connected') {
|
|
233
|
+
return { sent: false, skipped: true, reason: `Platform ${target.platform} is not connected.` };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const body = String(replyText || '').trim();
|
|
237
|
+
if (!body) {
|
|
238
|
+
return { sent: false, skipped: true, reason: 'Reply text is empty.' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const followUpContent = [
|
|
242
|
+
'Update from your voice request:',
|
|
243
|
+
'',
|
|
244
|
+
body,
|
|
245
|
+
].join('\n');
|
|
246
|
+
|
|
247
|
+
let sendResult;
|
|
248
|
+
try {
|
|
249
|
+
sendResult = await manager.sendMessage(
|
|
250
|
+
session.userId,
|
|
251
|
+
target.platform,
|
|
252
|
+
target.to,
|
|
253
|
+
followUpContent,
|
|
254
|
+
{
|
|
255
|
+
agentId: session.agentId,
|
|
256
|
+
runId,
|
|
257
|
+
persistConversation: true,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('Failed to send deferred voice follow-up:', err);
|
|
262
|
+
return {
|
|
263
|
+
sent: false,
|
|
264
|
+
skipped: false,
|
|
265
|
+
success: false,
|
|
266
|
+
error: err instanceof Error ? err.message : String(err),
|
|
267
|
+
runId,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
session.deferFollowUpRequested = false;
|
|
272
|
+
const label = this.#platformLabel(target.platform);
|
|
273
|
+
await this.deliverAssistantMessage(
|
|
274
|
+
session,
|
|
275
|
+
`I sent the full result to your ${label} chat.`,
|
|
276
|
+
{ kind: 'final' },
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
sent: true,
|
|
281
|
+
target,
|
|
282
|
+
result: sendResult,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
199
286
|
async startTelnyxTurn({
|
|
200
287
|
userId,
|
|
201
288
|
agentId = null,
|
|
@@ -284,6 +371,54 @@ class VoiceRuntimeManager {
|
|
|
284
371
|
}
|
|
285
372
|
}
|
|
286
373
|
|
|
374
|
+
#messagingManager() {
|
|
375
|
+
return this.agentEngine?.messagingManager || this.agentEngine?.app?.locals?.messagingManager || null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#readScopedSetting(userId, agentId, key) {
|
|
379
|
+
const row = db.prepare(
|
|
380
|
+
'SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?'
|
|
381
|
+
).get(userId, agentId, key);
|
|
382
|
+
if (row) {
|
|
383
|
+
try {
|
|
384
|
+
return JSON.parse(row.value);
|
|
385
|
+
} catch {
|
|
386
|
+
return row.value;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!isMainAgent(userId, agentId)) return null;
|
|
390
|
+
const userRow = db.prepare(
|
|
391
|
+
'SELECT value FROM user_settings WHERE user_id = ? AND key = ?'
|
|
392
|
+
).get(userId, key);
|
|
393
|
+
if (!userRow) return null;
|
|
394
|
+
try {
|
|
395
|
+
return JSON.parse(userRow.value);
|
|
396
|
+
} catch {
|
|
397
|
+
return userRow.value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#resolvePreferredMessagingTarget(userId, agentId = null) {
|
|
402
|
+
const manager = this.#messagingManager();
|
|
403
|
+
if (!manager) return null;
|
|
404
|
+
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
405
|
+
const platform = String(this.#readScopedSetting(userId, scopedAgentId, 'last_platform') || '').trim();
|
|
406
|
+
const to = String(this.#readScopedSetting(userId, scopedAgentId, 'last_chat_id') || '').trim();
|
|
407
|
+
if (!platform || !to) return null;
|
|
408
|
+
|
|
409
|
+
const status = typeof manager.getPlatformStatus === 'function'
|
|
410
|
+
? manager.getPlatformStatus(userId, platform, { agentId: scopedAgentId })
|
|
411
|
+
: null;
|
|
412
|
+
if (!status || status.status !== 'connected') return null;
|
|
413
|
+
return { platform, to };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#platformLabel(platformName) {
|
|
417
|
+
const raw = String(platformName || '').trim();
|
|
418
|
+
if (!raw) return 'message';
|
|
419
|
+
return raw.replace(/[_-]+/g, ' ');
|
|
420
|
+
}
|
|
421
|
+
|
|
287
422
|
async #deliverFlutterAssistantOutput(socket, sessionId, session, content, options = {}) {
|
|
288
423
|
const kind = String(options.kind || 'final').trim() || 'final';
|
|
289
424
|
socket.emit('voice:assistant_text', {
|