neoagent 2.4.1-beta.34 → 2.4.1-beta.35
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 +10 -3
- package/flutter_app/lib/main_admin.dart +13 -0
- package/flutter_app/lib/main_controller.dart +24 -6
- package/flutter_app/lib/main_models.dart +14 -0
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +13767 -13696
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +1 -0
- package/server/services/ai/engine.js +15 -6
- package/server/services/ai/systemPrompt.js +28 -22
- package/server/services/ai/tools.js +12 -3
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/tasks/runtime.js +133 -21
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveAgentId } = require('../agents/manager');
|
|
4
|
+
const {
|
|
5
|
+
buildCoverageForConnection,
|
|
6
|
+
sourceTypesForConnection,
|
|
7
|
+
} = require('./ingestion_support');
|
|
8
|
+
|
|
9
|
+
function listConnectionStatuses(service, userId, { agentId = null } = {}) {
|
|
10
|
+
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
11
|
+
const connections = service.db.prepare(
|
|
12
|
+
`SELECT *
|
|
13
|
+
FROM integration_connections
|
|
14
|
+
WHERE user_id = ? AND agent_id = ?
|
|
15
|
+
ORDER BY updated_at DESC, id DESC`,
|
|
16
|
+
).all(userId, scopedAgentId);
|
|
17
|
+
const jobs = service.memoryManager.listIngestionJobs(
|
|
18
|
+
userId,
|
|
19
|
+
{ agentId: scopedAgentId, limit: 100 },
|
|
20
|
+
);
|
|
21
|
+
return connections.map((connection) => {
|
|
22
|
+
const latestJob = jobs.find(
|
|
23
|
+
(job) => Number(job.connectionId || 0) === Number(connection.id),
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
connectionId: connection.id,
|
|
27
|
+
providerKey: connection.provider_key,
|
|
28
|
+
appKey: connection.app_key,
|
|
29
|
+
accountEmail: connection.account_email || null,
|
|
30
|
+
...buildCoverageForConnection(connection, latestJob),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decorateProviderSnapshot(service, snapshot, userId, agentId = null) {
|
|
36
|
+
if (!snapshot || typeof snapshot !== 'object') return snapshot;
|
|
37
|
+
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
38
|
+
const connections = service.db.prepare(
|
|
39
|
+
`SELECT *
|
|
40
|
+
FROM integration_connections
|
|
41
|
+
WHERE user_id = ? AND agent_id = ? AND provider_key = ?`,
|
|
42
|
+
).all(userId, scopedAgentId, snapshot.provider);
|
|
43
|
+
const jobs = service.memoryManager.listIngestionJobs(userId, {
|
|
44
|
+
agentId: scopedAgentId,
|
|
45
|
+
providerKey: snapshot.provider,
|
|
46
|
+
limit: 100,
|
|
47
|
+
});
|
|
48
|
+
const latestJobForConnection = (connectionId) =>
|
|
49
|
+
jobs.find((job) => Number(job.connectionId || 0) === Number(connectionId));
|
|
50
|
+
|
|
51
|
+
const connectionCoverage = connections.map((connection) => ({
|
|
52
|
+
connectionId: connection.id,
|
|
53
|
+
appKey: connection.app_key,
|
|
54
|
+
accountEmail: connection.account_email || null,
|
|
55
|
+
...buildCoverageForConnection(connection, latestJobForConnection(connection.id)),
|
|
56
|
+
}));
|
|
57
|
+
const providerDomains = Array.from(
|
|
58
|
+
new Set(connectionCoverage.flatMap((item) => item.dataDomains || [])),
|
|
59
|
+
);
|
|
60
|
+
const providerJob = jobs[0] || null;
|
|
61
|
+
const decorated = {
|
|
62
|
+
...snapshot,
|
|
63
|
+
memoryCoverage: {
|
|
64
|
+
supported: providerDomains.length > 0,
|
|
65
|
+
contributesToMemory: providerDomains.length > 0,
|
|
66
|
+
contributesToTaskExecution: Boolean(snapshot.connection?.connected),
|
|
67
|
+
status: providerJob?.status || (providerDomains.length > 0 ? 'ready' : 'not_supported'),
|
|
68
|
+
dataDomains: providerDomains,
|
|
69
|
+
documentCount: connectionCoverage.reduce(
|
|
70
|
+
(sum, item) => sum + Number(item.documentCount || 0),
|
|
71
|
+
0,
|
|
72
|
+
),
|
|
73
|
+
lastRefreshAt: providerJob?.completedAt || providerJob?.updatedAt || null,
|
|
74
|
+
nextRefreshAt: providerJob?.nextSyncAt || null,
|
|
75
|
+
error: providerJob?.error || null,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
decorated.apps = (snapshot.apps || []).map((app) => {
|
|
80
|
+
const appConnections = connectionCoverage.filter((item) => item.appKey === app.id);
|
|
81
|
+
const dataDomains = Array.from(new Set([
|
|
82
|
+
...sourceTypesForConnection(snapshot.provider, app.id),
|
|
83
|
+
...appConnections.flatMap((item) => item.dataDomains || []),
|
|
84
|
+
]));
|
|
85
|
+
const latest = appConnections[0] || null;
|
|
86
|
+
return {
|
|
87
|
+
...app,
|
|
88
|
+
memoryCoverage: {
|
|
89
|
+
supported: dataDomains.length > 0,
|
|
90
|
+
contributesToMemory: dataDomains.length > 0 && app.connection?.connected === true,
|
|
91
|
+
contributesToTaskExecution: app.connection?.connected === true,
|
|
92
|
+
status: latest?.status || (dataDomains.length > 0 ? 'ready' : 'not_supported'),
|
|
93
|
+
dataDomains,
|
|
94
|
+
documentCount: appConnections.reduce(
|
|
95
|
+
(sum, item) => sum + Number(item.documentCount || 0),
|
|
96
|
+
0,
|
|
97
|
+
),
|
|
98
|
+
lastRefreshAt: latest?.lastRefreshAt || null,
|
|
99
|
+
nextRefreshAt: latest?.nextRefreshAt || null,
|
|
100
|
+
error: latest?.error || null,
|
|
101
|
+
},
|
|
102
|
+
accounts: (app.accounts || []).map((account) => {
|
|
103
|
+
const accountCoverage = appConnections.find(
|
|
104
|
+
(item) => Number(item.connectionId) === Number(account.connectionId),
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
...account,
|
|
108
|
+
memoryCoverage: accountCoverage || {
|
|
109
|
+
supported: dataDomains.length > 0,
|
|
110
|
+
contributesToMemory: false,
|
|
111
|
+
contributesToTaskExecution: app.connection?.connected === true,
|
|
112
|
+
status: dataDomains.length > 0 ? 'ready' : 'not_supported',
|
|
113
|
+
dataDomains,
|
|
114
|
+
documentCount: 0,
|
|
115
|
+
lastRefreshAt: null,
|
|
116
|
+
nextRefreshAt: null,
|
|
117
|
+
error: null,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
return decorated;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
decorateProviderSnapshot,
|
|
128
|
+
listConnectionStatuses,
|
|
129
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
const { resolveAgentId } = require('../agents/manager');
|
|
5
|
+
const {
|
|
6
|
+
SOURCE_MEMORY_CATEGORIES,
|
|
7
|
+
getFreshnessPolicy,
|
|
8
|
+
nextSyncFromPolicy,
|
|
9
|
+
normalizeDocument,
|
|
10
|
+
normalizeSourceType,
|
|
11
|
+
parseJsonObject,
|
|
12
|
+
safeTrim,
|
|
13
|
+
} = require('./ingestion_support');
|
|
14
|
+
|
|
15
|
+
async function ingestDocuments(service, userId, documents = [], options = {}) {
|
|
16
|
+
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
17
|
+
const normalizedDocs = (Array.isArray(documents) ? documents : [])
|
|
18
|
+
.map((document) => normalizeDocument(document, {
|
|
19
|
+
sourceType: options.sourceType,
|
|
20
|
+
providerKey: options.providerKey,
|
|
21
|
+
connectionId: options.connectionId,
|
|
22
|
+
sourceAccount: options.sourceAccount,
|
|
23
|
+
}));
|
|
24
|
+
const sourceType = normalizeSourceType(options.sourceType || normalizedDocs[0]?.sourceType);
|
|
25
|
+
const policy = getFreshnessPolicy(sourceType);
|
|
26
|
+
const jobId = uuidv4();
|
|
27
|
+
const connectionId = Number.isInteger(Number(options.connectionId))
|
|
28
|
+
? Number(options.connectionId)
|
|
29
|
+
: 0;
|
|
30
|
+
|
|
31
|
+
service.memoryManager.recordIngestionJob(userId, {
|
|
32
|
+
id: jobId,
|
|
33
|
+
sourceType,
|
|
34
|
+
providerKey: safeTrim(options.providerKey, 80),
|
|
35
|
+
connectionId,
|
|
36
|
+
status: 'running',
|
|
37
|
+
freshnessPolicy: policy,
|
|
38
|
+
metadata: parseJsonObject(options.metadata, {}),
|
|
39
|
+
documentCount: 0,
|
|
40
|
+
nextSyncAt: nextSyncFromPolicy(policy),
|
|
41
|
+
}, { agentId });
|
|
42
|
+
|
|
43
|
+
const documentIds = [];
|
|
44
|
+
const memoryIds = [];
|
|
45
|
+
try {
|
|
46
|
+
for (const document of normalizedDocs) {
|
|
47
|
+
const documentId = service.memoryManager.upsertIngestionDocument(
|
|
48
|
+
userId,
|
|
49
|
+
document,
|
|
50
|
+
{ agentId },
|
|
51
|
+
);
|
|
52
|
+
documentIds.push(documentId);
|
|
53
|
+
const memoryId = await service.memoryManager.saveMemory(
|
|
54
|
+
userId,
|
|
55
|
+
`${document.title}: ${document.summary || document.content}`,
|
|
56
|
+
SOURCE_MEMORY_CATEGORIES[document.normalizedType]
|
|
57
|
+
|| SOURCE_MEMORY_CATEGORIES[document.sourceType]
|
|
58
|
+
|| 'episodic',
|
|
59
|
+
document.salience,
|
|
60
|
+
{
|
|
61
|
+
agentId,
|
|
62
|
+
staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
|
|
63
|
+
sourceRef: {
|
|
64
|
+
sourceType: 'memory_ingestion',
|
|
65
|
+
sourceId: document.externalObjectId,
|
|
66
|
+
sourceLabel: document.title,
|
|
67
|
+
},
|
|
68
|
+
scope: {
|
|
69
|
+
scopeType: 'agent',
|
|
70
|
+
scopeId: agentId,
|
|
71
|
+
},
|
|
72
|
+
metadata: {
|
|
73
|
+
ingestionJobId: jobId,
|
|
74
|
+
ingestionDocumentId: documentId,
|
|
75
|
+
providerKey: document.providerKey || null,
|
|
76
|
+
connectionId: document.connectionId || null,
|
|
77
|
+
sourceType: document.sourceType,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
if (memoryId) memoryIds.push(memoryId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
service.memoryManager.recordIngestionJob(userId, {
|
|
85
|
+
id: jobId,
|
|
86
|
+
sourceType,
|
|
87
|
+
providerKey: safeTrim(options.providerKey, 80),
|
|
88
|
+
connectionId,
|
|
89
|
+
status: 'completed',
|
|
90
|
+
freshnessPolicy: policy,
|
|
91
|
+
summary: { documentIds, memoryIds },
|
|
92
|
+
metadata: parseJsonObject(options.metadata, {}),
|
|
93
|
+
documentCount: documentIds.length,
|
|
94
|
+
completedAt: new Date().toISOString(),
|
|
95
|
+
nextSyncAt: nextSyncFromPolicy(policy),
|
|
96
|
+
}, { agentId });
|
|
97
|
+
|
|
98
|
+
const knowledgeViews = service.memoryManager.materializeKnowledgeViews(userId, { agentId });
|
|
99
|
+
return {
|
|
100
|
+
jobId,
|
|
101
|
+
status: 'completed',
|
|
102
|
+
documentIds,
|
|
103
|
+
memoryIds,
|
|
104
|
+
knowledgeViews,
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
service.memoryManager.recordIngestionJob(userId, {
|
|
108
|
+
id: jobId,
|
|
109
|
+
sourceType,
|
|
110
|
+
providerKey: safeTrim(options.providerKey, 80),
|
|
111
|
+
connectionId,
|
|
112
|
+
status: 'failed',
|
|
113
|
+
freshnessPolicy: policy,
|
|
114
|
+
documentCount: documentIds.length,
|
|
115
|
+
error: err.message,
|
|
116
|
+
completedAt: new Date().toISOString(),
|
|
117
|
+
nextSyncAt: nextSyncFromPolicy(policy),
|
|
118
|
+
}, { agentId });
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { ingestDocuments };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { compactTextPayload } = require('../ai/preModelCompaction');
|
|
4
|
+
|
|
5
|
+
const SOURCE_TYPES = Object.freeze([
|
|
6
|
+
'email',
|
|
7
|
+
'calendar',
|
|
8
|
+
'chat',
|
|
9
|
+
'docs',
|
|
10
|
+
'tickets',
|
|
11
|
+
'repos',
|
|
12
|
+
'files',
|
|
13
|
+
'crm',
|
|
14
|
+
'payments',
|
|
15
|
+
'notes',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const FRESHNESS_POLICIES = Object.freeze({
|
|
19
|
+
email: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
|
|
20
|
+
calendar: Object.freeze({ intervalMinutes: 180, staleAfterDays: 30 }),
|
|
21
|
+
chat: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
|
|
22
|
+
docs: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
|
|
23
|
+
tickets: Object.freeze({ intervalMinutes: 120, staleAfterDays: 30 }),
|
|
24
|
+
repos: Object.freeze({ intervalMinutes: 180, staleAfterDays: 45 }),
|
|
25
|
+
files: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
|
|
26
|
+
crm: Object.freeze({ intervalMinutes: 240, staleAfterDays: 45 }),
|
|
27
|
+
payments: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
|
|
28
|
+
notes: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const SOURCE_MEMORY_CATEGORIES = Object.freeze({
|
|
32
|
+
email: 'episodic',
|
|
33
|
+
calendar: 'events',
|
|
34
|
+
chat: 'episodic',
|
|
35
|
+
docs: 'projects',
|
|
36
|
+
tickets: 'tasks',
|
|
37
|
+
repos: 'projects',
|
|
38
|
+
files: 'episodic',
|
|
39
|
+
crm: 'contacts',
|
|
40
|
+
payments: 'events',
|
|
41
|
+
notes: 'episodic',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const INTEGRATION_SOURCE_TYPES = Object.freeze({
|
|
45
|
+
google_workspace: Object.freeze({
|
|
46
|
+
gmail: Object.freeze(['email']),
|
|
47
|
+
calendar: Object.freeze(['calendar']),
|
|
48
|
+
drive: Object.freeze(['files']),
|
|
49
|
+
docs: Object.freeze(['docs']),
|
|
50
|
+
sheets: Object.freeze(['docs', 'files']),
|
|
51
|
+
}),
|
|
52
|
+
microsoft_365: Object.freeze({
|
|
53
|
+
outlook: Object.freeze(['email']),
|
|
54
|
+
calendar: Object.freeze(['calendar']),
|
|
55
|
+
onedrive: Object.freeze(['files']),
|
|
56
|
+
teams: Object.freeze(['chat']),
|
|
57
|
+
}),
|
|
58
|
+
github: Object.freeze({
|
|
59
|
+
repos: Object.freeze(['repos', 'tickets']),
|
|
60
|
+
}),
|
|
61
|
+
slack: Object.freeze({
|
|
62
|
+
slack: Object.freeze(['chat']),
|
|
63
|
+
}),
|
|
64
|
+
whatsapp: Object.freeze({
|
|
65
|
+
personal: Object.freeze(['chat']),
|
|
66
|
+
}),
|
|
67
|
+
notion: Object.freeze({
|
|
68
|
+
notion: Object.freeze(['notes', 'docs']),
|
|
69
|
+
}),
|
|
70
|
+
trello: Object.freeze({
|
|
71
|
+
trello: Object.freeze(['tickets']),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function safeTrim(value, maxLength = 240) {
|
|
76
|
+
return String(value || '').trim().slice(0, maxLength);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseJsonObject(value, fallback = {}) {
|
|
80
|
+
if (!value) return { ...fallback };
|
|
81
|
+
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(String(value));
|
|
84
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
85
|
+
? parsed
|
|
86
|
+
: { ...fallback };
|
|
87
|
+
} catch {
|
|
88
|
+
return { ...fallback };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeSourceType(value) {
|
|
93
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
94
|
+
return SOURCE_TYPES.includes(normalized) ? normalized : 'notes';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getFreshnessPolicy(sourceType) {
|
|
98
|
+
const normalized = normalizeSourceType(sourceType);
|
|
99
|
+
return { ...FRESHNESS_POLICIES[normalized] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function nextSyncFromPolicy(policy, now = Date.now()) {
|
|
103
|
+
const intervalMinutes = Math.max(15, Number(policy?.intervalMinutes) || 360);
|
|
104
|
+
return new Date(now + intervalMinutes * 60 * 1000).toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeDocument(raw = {}, defaults = {}) {
|
|
108
|
+
const sourceType = normalizeSourceType(raw.sourceType || defaults.sourceType);
|
|
109
|
+
const externalObjectId = safeTrim(raw.externalObjectId || raw.id || raw.sourceId, 180);
|
|
110
|
+
const contentSource = raw.content ?? raw.text ?? raw.body ?? '';
|
|
111
|
+
const content = safeTrim(contentSource, 12000);
|
|
112
|
+
if (!externalObjectId || !content) {
|
|
113
|
+
throw new Error('Normalized memory documents require externalObjectId and content.');
|
|
114
|
+
}
|
|
115
|
+
const compacted = compactTextPayload(content, { maxChars: 2400, maxLines: 50 });
|
|
116
|
+
return {
|
|
117
|
+
sourceType,
|
|
118
|
+
normalizedType: normalizeSourceType(raw.normalizedType || raw.memoryType || sourceType),
|
|
119
|
+
providerKey: safeTrim(raw.providerKey || defaults.providerKey, 80),
|
|
120
|
+
connectionId: Number.isInteger(Number(raw.connectionId ?? defaults.connectionId))
|
|
121
|
+
? Number(raw.connectionId ?? defaults.connectionId)
|
|
122
|
+
: 0,
|
|
123
|
+
externalObjectId,
|
|
124
|
+
sourceAccount: safeTrim(raw.sourceAccount || defaults.sourceAccount, 180),
|
|
125
|
+
title: safeTrim(raw.title || raw.subject || raw.name || externalObjectId, 220),
|
|
126
|
+
content,
|
|
127
|
+
summary: safeTrim(raw.summary || compacted.text, 1200),
|
|
128
|
+
salience: Math.max(1, Math.min(10, Number(raw.salience) || 5)),
|
|
129
|
+
sourceTimestamp: safeTrim(raw.sourceTimestamp || raw.updatedAt || raw.createdAt, 80) || null,
|
|
130
|
+
metadata: {
|
|
131
|
+
...parseJsonObject(raw.metadata, {}),
|
|
132
|
+
compaction: compacted.metrics,
|
|
133
|
+
},
|
|
134
|
+
payload: parseJsonObject(raw.payload, {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sourceTypesForConnection(providerKey, appKey) {
|
|
139
|
+
const providerMap = INTEGRATION_SOURCE_TYPES[String(providerKey || '').trim()] || {};
|
|
140
|
+
return Array.from(providerMap[String(appKey || '').trim()] || []);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildCoverageForConnection(connection, latestJob = null) {
|
|
144
|
+
const dataDomains = sourceTypesForConnection(connection.provider_key, connection.app_key);
|
|
145
|
+
const supported = dataDomains.length > 0;
|
|
146
|
+
return {
|
|
147
|
+
supported,
|
|
148
|
+
contributesToMemory: supported,
|
|
149
|
+
contributesToTaskExecution: String(connection.status || '') === 'connected',
|
|
150
|
+
status: latestJob?.status || (supported ? 'ready' : 'not_supported'),
|
|
151
|
+
dataDomains,
|
|
152
|
+
lastRefreshAt: latestJob?.completedAt || latestJob?.updatedAt || null,
|
|
153
|
+
nextRefreshAt: latestJob?.nextSyncAt || null,
|
|
154
|
+
documentCount: Number(latestJob?.documentCount || 0),
|
|
155
|
+
error: latestJob?.error || null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
FRESHNESS_POLICIES,
|
|
161
|
+
SOURCE_MEMORY_CATEGORIES,
|
|
162
|
+
SOURCE_TYPES,
|
|
163
|
+
buildCoverageForConnection,
|
|
164
|
+
getFreshnessPolicy,
|
|
165
|
+
nextSyncFromPolicy,
|
|
166
|
+
normalizeDocument,
|
|
167
|
+
normalizeSourceType,
|
|
168
|
+
parseJsonObject,
|
|
169
|
+
safeTrim,
|
|
170
|
+
sourceTypesForConnection,
|
|
171
|
+
};
|
|
@@ -18,9 +18,12 @@ const {
|
|
|
18
18
|
buildVoiceMessagingRunOptions,
|
|
19
19
|
isVoiceLikeMessage,
|
|
20
20
|
} = require('../voice/runtime');
|
|
21
|
+
const { getErrorMessage } = require('../bootstrap_helpers');
|
|
22
|
+
const { processInboundQueue } = require('./inbound_queue');
|
|
23
|
+
const { startTypingKeepalive } = require('./typing_keepalive');
|
|
21
24
|
|
|
22
25
|
function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
|
|
23
|
-
const userQueues =
|
|
26
|
+
const userQueues = Object.create(null);
|
|
24
27
|
app.locals.userQueues = userQueues;
|
|
25
28
|
|
|
26
29
|
messagingManager.registerHandler(async (userId, msg) => {
|
|
@@ -100,62 +103,85 @@ function registerMessagingAutomation({ app, io, messagingManager, agentEngine })
|
|
|
100
103
|
messagingManager,
|
|
101
104
|
agentEngine,
|
|
102
105
|
userId,
|
|
103
|
-
msg
|
|
106
|
+
msg,
|
|
107
|
+
onProcessingError: ({ error, runId, failedMessage }) => {
|
|
108
|
+
const errorMessage = getErrorMessage(error);
|
|
109
|
+
console.error(
|
|
110
|
+
`[MessagingAutomation] Agent run failed platform=${failedMessage.platform} user=${userId}:`,
|
|
111
|
+
errorMessage
|
|
112
|
+
);
|
|
113
|
+
io.to(`user:${userId}`).emit('messaging:error', {
|
|
114
|
+
error: `NeoAgent could not finish the ${failedMessage.platform} request. Check the run history for details.`,
|
|
115
|
+
platform: failedMessage.platform,
|
|
116
|
+
chatId: failedMessage.chatId,
|
|
117
|
+
runId
|
|
118
|
+
});
|
|
119
|
+
}
|
|
104
120
|
});
|
|
105
121
|
});
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
async function processQueuedMessage({
|
|
109
125
|
userQueues,
|
|
126
|
+
messagingManager,
|
|
127
|
+
agentEngine,
|
|
128
|
+
userId,
|
|
129
|
+
msg,
|
|
130
|
+
onProcessingError = null
|
|
131
|
+
}) {
|
|
132
|
+
return processInboundQueue({
|
|
133
|
+
userQueues,
|
|
134
|
+
userId,
|
|
135
|
+
msg,
|
|
136
|
+
executeMessage: (queuedMessage) =>
|
|
137
|
+
executeQueuedMessage({
|
|
138
|
+
messagingManager,
|
|
139
|
+
agentEngine,
|
|
140
|
+
userId,
|
|
141
|
+
msg: queuedMessage
|
|
142
|
+
}),
|
|
143
|
+
onProcessingError
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeQueuedMessage({
|
|
110
148
|
messagingManager,
|
|
111
149
|
agentEngine,
|
|
112
150
|
userId,
|
|
113
151
|
msg
|
|
114
152
|
}) {
|
|
115
153
|
const agentId = msg.agentId || null;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
const runId = randomUUID();
|
|
155
|
+
const reportSideEffectError = (operation, error) => {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[MessagingAutomation] ${operation} failed platform=${msg.platform} user=${userId}:`,
|
|
158
|
+
getErrorMessage(error)
|
|
159
|
+
);
|
|
160
|
+
};
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
try {
|
|
163
|
+
await messagingManager.markRead(
|
|
164
|
+
userId,
|
|
165
|
+
msg.platform,
|
|
166
|
+
msg.chatId,
|
|
167
|
+
msg.messageId,
|
|
168
|
+
{ agentId }
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
reportSideEffectError('mark read', error);
|
|
125
172
|
}
|
|
126
173
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
last.messageId = msg.messageId;
|
|
137
|
-
} else {
|
|
138
|
-
queue.pending.push({ ...msg });
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
174
|
+
const stopTypingKeepalive = startTypingKeepalive({
|
|
175
|
+
messagingManager,
|
|
176
|
+
userId,
|
|
177
|
+
agentId,
|
|
178
|
+
runId,
|
|
179
|
+
platform: msg.platform,
|
|
180
|
+
chatId: msg.chatId,
|
|
181
|
+
onError: reportSideEffectError
|
|
182
|
+
});
|
|
142
183
|
|
|
143
|
-
queue.running = true;
|
|
144
|
-
let stopTypingKeepalive = async () => {};
|
|
145
184
|
try {
|
|
146
|
-
const runId = randomUUID();
|
|
147
|
-
await messagingManager
|
|
148
|
-
.markRead(userId, msg.platform, msg.chatId, msg.messageId, { agentId })
|
|
149
|
-
.catch(() => {});
|
|
150
|
-
stopTypingKeepalive = startTypingKeepalive({
|
|
151
|
-
messagingManager,
|
|
152
|
-
userId,
|
|
153
|
-
agentId,
|
|
154
|
-
runId,
|
|
155
|
-
platform: msg.platform,
|
|
156
|
-
chatId: msg.chatId
|
|
157
|
-
});
|
|
158
|
-
|
|
159
185
|
const prompt = buildIncomingPrompt(msg);
|
|
160
186
|
const conversationId = ensureConversation(userId, msg);
|
|
161
187
|
const runOptions = isVoiceLikeMessage(msg)
|
|
@@ -182,105 +208,15 @@ async function processQueuedMessage({
|
|
|
182
208
|
];
|
|
183
209
|
}
|
|
184
210
|
|
|
185
|
-
await agentEngine.run(userId, prompt, runOptions);
|
|
211
|
+
const result = await agentEngine.run(userId, prompt, runOptions);
|
|
212
|
+
return { runId, result, error: null };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { runId, result: null, error };
|
|
186
215
|
} finally {
|
|
187
216
|
await stopTypingKeepalive();
|
|
188
|
-
if (queue.cancelRequested) {
|
|
189
|
-
queue.pending = [];
|
|
190
|
-
queue.running = false;
|
|
191
|
-
queue.cancelRequested = false;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
queue.running = false;
|
|
195
|
-
if (queue.pending.length > 0) {
|
|
196
|
-
const next = queue.pending.shift();
|
|
197
|
-
await processQueuedMessage({
|
|
198
|
-
userQueues,
|
|
199
|
-
messagingManager,
|
|
200
|
-
agentEngine,
|
|
201
|
-
userId,
|
|
202
|
-
msg: next
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
|
|
208
|
-
function startTypingKeepalive({
|
|
209
|
-
messagingManager,
|
|
210
|
-
userId,
|
|
211
|
-
agentId,
|
|
212
|
-
runId,
|
|
213
|
-
platform,
|
|
214
|
-
chatId,
|
|
215
|
-
intervalMs = 4000
|
|
216
|
-
}) {
|
|
217
|
-
let stopped = false;
|
|
218
|
-
let timer = null;
|
|
219
|
-
let releaseWait = null;
|
|
220
|
-
let stopPromise = null;
|
|
221
|
-
|
|
222
|
-
const matchesRunDelivery = (event) => (
|
|
223
|
-
event?.runId
|
|
224
|
-
&& runId
|
|
225
|
-
&& event.runId === runId
|
|
226
|
-
&& event.userId === userId
|
|
227
|
-
&& event.platform === platform
|
|
228
|
-
&& event.to === chatId
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const onMessageSent = (event) => {
|
|
232
|
-
if (matchesRunDelivery(event) && event.deliveryKind !== 'interim') {
|
|
233
|
-
stop().catch(() => {});
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
if (typeof messagingManager?.on === 'function' && typeof messagingManager?.off === 'function') {
|
|
238
|
-
messagingManager.on('message_sent', onMessageSent);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const wait = () =>
|
|
242
|
-
new Promise((resolve) => {
|
|
243
|
-
releaseWait = resolve;
|
|
244
|
-
timer = setTimeout(resolve, intervalMs);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const loop = (async () => {
|
|
248
|
-
while (!stopped) {
|
|
249
|
-
await messagingManager
|
|
250
|
-
.sendTyping(userId, platform, chatId, true, { agentId })
|
|
251
|
-
.catch(() => {});
|
|
252
|
-
|
|
253
|
-
if (stopped) break;
|
|
254
|
-
await wait();
|
|
255
|
-
}
|
|
256
|
-
})();
|
|
257
|
-
|
|
258
|
-
const stop = async () => {
|
|
259
|
-
if (stopPromise) return stopPromise;
|
|
260
|
-
stopPromise = (async () => {
|
|
261
|
-
if (typeof messagingManager?.off === 'function') {
|
|
262
|
-
messagingManager.off('message_sent', onMessageSent);
|
|
263
|
-
}
|
|
264
|
-
stopped = true;
|
|
265
|
-
if (timer) {
|
|
266
|
-
clearTimeout(timer);
|
|
267
|
-
timer = null;
|
|
268
|
-
}
|
|
269
|
-
if (releaseWait) {
|
|
270
|
-
releaseWait();
|
|
271
|
-
releaseWait = null;
|
|
272
|
-
}
|
|
273
|
-
await loop.catch(() => {});
|
|
274
|
-
await messagingManager
|
|
275
|
-
.sendTyping(userId, platform, chatId, false, { agentId })
|
|
276
|
-
.catch(() => {});
|
|
277
|
-
})();
|
|
278
|
-
return stopPromise;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
return stop;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
220
|
function ensureConversation(userId, msg) {
|
|
285
221
|
const agentId = msg.agentId || null;
|
|
286
222
|
let conversation = db
|
|
@@ -419,6 +355,7 @@ function emitBlockedSenderSuggestion({ io, userId, msg }) {
|
|
|
419
355
|
module.exports = {
|
|
420
356
|
buildIncomingPrompt,
|
|
421
357
|
buildSenderIdentityBlock,
|
|
358
|
+
executeQueuedMessage,
|
|
422
359
|
isAllowedMessagingSender,
|
|
423
360
|
processQueuedMessage,
|
|
424
361
|
registerMessagingAutomation,
|