@peopl-health/nexus 2.2.10 → 2.3.1
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/lib/adapters/TwilioProvider.js +0 -6
- package/lib/helpers/assistantHelper.js +92 -269
- package/lib/helpers/baileysHelper.js +1 -11
- package/lib/helpers/filesHelper.js +75 -32
- package/lib/helpers/mediaHelper.js +4 -10
- package/lib/helpers/messageHelper.js +136 -0
- package/lib/helpers/processHelper.js +238 -0
- package/lib/helpers/threadHelper.js +73 -0
- package/lib/helpers/twilioHelper.js +2 -14
- package/lib/models/messageModel.js +0 -1
- package/lib/observability/index.js +184 -0
- package/lib/observability/telemetry.js +118 -0
- package/lib/providers/OpenAIResponsesProvider.js +0 -3
- package/lib/services/assistantService.js +107 -203
- package/lib/storage/MongoStorage.js +0 -2
- package/lib/utils/logger.js +91 -4
- package/lib/utils/sanitizer.js +62 -0
- package/lib/utils/tracingDecorator.js +48 -0
- package/package.json +13 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
2
|
+
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
3
|
+
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
|
|
4
|
+
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
|
|
5
|
+
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
|
|
6
|
+
|
|
7
|
+
class TelemetryManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.sdk = null;
|
|
10
|
+
this.isInitialized = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init(config = {}) {
|
|
14
|
+
if (this.isInitialized) {
|
|
15
|
+
console.log('✅ OpenTelemetry already initialized');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
serviceName = 'nexus-assistant',
|
|
21
|
+
serviceVersion = '1.0.0',
|
|
22
|
+
jaegerEndpoint = 'http://localhost:14268/api/traces',
|
|
23
|
+
prometheusEndpoint = '/metrics',
|
|
24
|
+
prometheusPort = 9090
|
|
25
|
+
} = config;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
process.env.OTEL_SERVICE_NAME = serviceName;
|
|
29
|
+
process.env.OTEL_SERVICE_VERSION = serviceVersion;
|
|
30
|
+
|
|
31
|
+
const prometheusExporter = new PrometheusExporter({
|
|
32
|
+
endpoint: prometheusEndpoint,
|
|
33
|
+
port: prometheusPort,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const jaegerExporter = new JaegerExporter({
|
|
37
|
+
endpoint: jaegerEndpoint,
|
|
38
|
+
tags: [],
|
|
39
|
+
timeout: 10000,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const batchSpanProcessor = new BatchSpanProcessor(jaegerExporter, {
|
|
43
|
+
maxExportBatchSize: process.env.NODE_ENV === 'production' ? 50 : 10,
|
|
44
|
+
maxQueueSize: process.env.NODE_ENV === 'production' ? 2048 : 100,
|
|
45
|
+
exportTimeoutMillis: process.env.NODE_ENV === 'production' ? 5000 : 2000,
|
|
46
|
+
scheduledDelayMillis: process.env.NODE_ENV === 'production' ? 2000 : 500,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.sdk = new NodeSDK({
|
|
50
|
+
instrumentations: [getNodeAutoInstrumentations({
|
|
51
|
+
'@opentelemetry/instrumentation-fs': {
|
|
52
|
+
enabled: false,
|
|
53
|
+
},
|
|
54
|
+
'@opentelemetry/instrumentation-dns': {
|
|
55
|
+
enabled: process.env.NODE_ENV !== 'production',
|
|
56
|
+
},
|
|
57
|
+
'@opentelemetry/instrumentation-net': {
|
|
58
|
+
enabled: process.env.NODE_ENV !== 'production',
|
|
59
|
+
},
|
|
60
|
+
'@opentelemetry/instrumentation-http': {
|
|
61
|
+
enabled: true,
|
|
62
|
+
requestHook: (span, request) => {
|
|
63
|
+
span.setAttributes({
|
|
64
|
+
'http.request.content_length': request.headers['content-length'] || 0,
|
|
65
|
+
'http.route': request.url,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
'@opentelemetry/instrumentation-express': {
|
|
70
|
+
enabled: true,
|
|
71
|
+
},
|
|
72
|
+
'@opentelemetry/instrumentation-mongodb': {
|
|
73
|
+
enabled: true,
|
|
74
|
+
},
|
|
75
|
+
'@opentelemetry/instrumentation-mongoose': {
|
|
76
|
+
enabled: true,
|
|
77
|
+
}
|
|
78
|
+
})],
|
|
79
|
+
spanProcessor: batchSpanProcessor,
|
|
80
|
+
metricReader: prometheusExporter,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await this.sdk.start();
|
|
84
|
+
this.isInitialized = true;
|
|
85
|
+
|
|
86
|
+
console.log(`🚀 OpenTelemetry initialized for "${serviceName}"`);
|
|
87
|
+
console.log(`📊 Metrics available at: http://localhost:${prometheusPort}${prometheusEndpoint}`);
|
|
88
|
+
console.log(`🔍 Traces sent to: ${jaegerEndpoint}`);
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('❌ Failed to initialize OpenTelemetry:', error.message);
|
|
92
|
+
this.isInitialized = false; // Ensure state is consistent on failure
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async shutdown() {
|
|
98
|
+
if (this.sdk && this.isInitialized) {
|
|
99
|
+
try {
|
|
100
|
+
await this.sdk.shutdown();
|
|
101
|
+
this.isInitialized = false;
|
|
102
|
+
console.log('🛑 OpenTelemetry shutdown completed');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('❌ Error during OpenTelemetry shutdown:', error.message);
|
|
105
|
+
this.isInitialized = false; // Reset state even on error
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const telemetryManager = new TelemetryManager();
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
telemetryManager,
|
|
116
|
+
initTelemetry: async (config) => await telemetryManager.init(config),
|
|
117
|
+
shutdownTelemetry: async () => await telemetryManager.shutdown()
|
|
118
|
+
};
|
|
@@ -205,7 +205,6 @@ class OpenAIResponsesProvider {
|
|
|
205
205
|
content: this._normalizeContent(role, content),
|
|
206
206
|
type: 'message',
|
|
207
207
|
});
|
|
208
|
-
console.log('payload', payload);
|
|
209
208
|
|
|
210
209
|
if (payload.content) {
|
|
211
210
|
return this._retryWithRateLimit(async () => {
|
|
@@ -307,11 +306,9 @@ class OpenAIResponsesProvider {
|
|
|
307
306
|
tools: transformedTools,
|
|
308
307
|
});
|
|
309
308
|
|
|
310
|
-
console.log('payload', payload);
|
|
311
309
|
const response = await this._retryWithRateLimit(() =>
|
|
312
310
|
this.client.responses.create(payload)
|
|
313
311
|
);
|
|
314
|
-
console.log('response', response);
|
|
315
312
|
|
|
316
313
|
if (response?.status !== 'completed') {
|
|
317
314
|
await this.cleanupOrphanedFunctionCalls(id, true);
|
|
@@ -5,13 +5,15 @@ const runtimeConfig = require('../config/runtimeConfig');
|
|
|
5
5
|
const { BaseAssistant } = require('../assistants/BaseAssistant');
|
|
6
6
|
const { createProvider } = require('../providers/createProvider');
|
|
7
7
|
|
|
8
|
-
const { Message, formatTimestamp } = require('../models/messageModel.js');
|
|
9
8
|
const { Thread } = require('../models/threadModel.js');
|
|
10
9
|
|
|
11
10
|
const { getCurRow } = require('../helpers/assistantHelper.js');
|
|
12
|
-
const {
|
|
11
|
+
const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
|
|
12
|
+
const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
|
|
13
|
+
const { withTracing } = require('../utils/tracingDecorator.js');
|
|
14
|
+
const { processIndividualMessage } = require('../helpers/processHelper.js');
|
|
15
|
+
const { getLastMessages } = require('../helpers/messageHelper.js');
|
|
13
16
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
14
|
-
const { delay } = require('../helpers/whatsappHelper.js');
|
|
15
17
|
|
|
16
18
|
const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
|
|
17
19
|
|
|
@@ -27,64 +29,6 @@ const configureAssistants = (config) => {
|
|
|
27
29
|
assistantConfig = config;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
const runAssistantAndWait = async ({
|
|
31
|
-
thread,
|
|
32
|
-
assistant,
|
|
33
|
-
runConfig = {}
|
|
34
|
-
}) => {
|
|
35
|
-
if (!thread || !thread.getConversationId()) {
|
|
36
|
-
throw new Error('runAssistantAndWait requires a thread with a valid thread_id or conversation_id');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!assistant) {
|
|
40
|
-
throw new Error('runAssistantAndWait requires an assistant instance');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
44
|
-
const { polling, tools: configTools, ...conversationConfig } = runConfig || {};
|
|
45
|
-
const variant = provider.getVariant ? provider.getVariant() : (process.env.VARIANT || 'assistants');
|
|
46
|
-
|
|
47
|
-
// Get tool schemas from assistant if available, fallback to config tools
|
|
48
|
-
const tools = assistant.getToolSchemas ? assistant.getToolSchemas() : (configTools || []);
|
|
49
|
-
|
|
50
|
-
// Only pass assistant for Responses API (needed to handle pending function calls)
|
|
51
|
-
const runConfigWithAssistant = variant === 'responses'
|
|
52
|
-
? { ...conversationConfig, assistant }
|
|
53
|
-
: conversationConfig;
|
|
54
|
-
|
|
55
|
-
let run = await provider.runConversation({
|
|
56
|
-
threadId: thread.getConversationId(),
|
|
57
|
-
assistantId: thread.getAssistantId(),
|
|
58
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
59
|
-
...runConfigWithAssistant,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const filter = thread.code ? { code: thread.code, active: true } : null;
|
|
63
|
-
if (filter) {
|
|
64
|
-
await Thread.updateOne(filter, { $set: { run_id: run.id } });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const maxRetries = polling?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
68
|
-
let completed = false;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
console.log('RUN ID', run.id, 'THREAD ID', thread.getConversationId(), 'ASSISTANT ID', thread.getAssistantId());
|
|
72
|
-
({run, completed} = await provider.checkRunStatus(assistant, thread.getConversationId(), run.id, 0, maxRetries));
|
|
73
|
-
} finally {
|
|
74
|
-
if (filter) {
|
|
75
|
-
await Thread.updateOne(filter, { $set: { run_id: null } });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (!completed) {
|
|
80
|
-
return { run: run, completed: false, output: '' };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const output = await provider.getRunText({ threadId: thread.getConversationId(), runId: run.id, fallback: '' });
|
|
84
|
-
|
|
85
|
-
return { completed: true, output };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
32
|
const registerAssistant = (assistantId, definition) => {
|
|
89
33
|
if (!assistantId || typeof assistantId !== 'string') {
|
|
90
34
|
throw new Error('registerAssistant requires a string assistantId');
|
|
@@ -189,7 +133,6 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
189
133
|
|
|
190
134
|
|
|
191
135
|
const createAssistant = async (code, assistant_id, messages=[], force=false) => {
|
|
192
|
-
// If thread already exists, update it
|
|
193
136
|
const findThread = await Thread.findOne({ code: code });
|
|
194
137
|
console.log('[createAssistant] findThread', findThread);
|
|
195
138
|
if (findThread && findThread.getConversationId()) {
|
|
@@ -279,162 +222,123 @@ const addMsgAssistant = async (code, inMessages, role = 'user', reply = false) =
|
|
|
279
222
|
}
|
|
280
223
|
};
|
|
281
224
|
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
225
|
+
const addInstructionCore = async (code, instruction, role = 'user') => {
|
|
226
|
+
const thread = await withTracing(getThread, 'get_thread_operation',
|
|
227
|
+
(threadCode) => ({
|
|
228
|
+
'thread.code': threadCode,
|
|
229
|
+
'operation.type': 'thread_retrieval'
|
|
230
|
+
})
|
|
231
|
+
)(code);
|
|
232
|
+
if (thread === null) return null;
|
|
233
|
+
|
|
234
|
+
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
235
|
+
const { output, completed, retries } = await withTracing(
|
|
236
|
+
runAssistantWithRetries,
|
|
237
|
+
'run_assistant_with_retries',
|
|
238
|
+
(thread, assistant, runConfig, patientReply) => ({
|
|
239
|
+
'assistant.id': thread.getAssistantId(),
|
|
240
|
+
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
241
|
+
'assistant.has_patient_reply': !!patientReply
|
|
242
|
+
})
|
|
243
|
+
)(
|
|
244
|
+
thread,
|
|
245
|
+
assistant,
|
|
246
|
+
{
|
|
247
|
+
additionalInstructions: instruction,
|
|
248
|
+
additionalMessages: [
|
|
249
|
+
{ role: role, content: instruction }
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
null // no patientReply for instructions
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
console.log('RUN RESPONSE', output);
|
|
256
|
+
return output;
|
|
316
257
|
};
|
|
317
258
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
await delay(5000);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return thread;
|
|
346
|
-
} catch (error) {
|
|
347
|
-
console.error('Error in getThread:', error.message || error);
|
|
259
|
+
const addInsAssistant = withTracing(
|
|
260
|
+
addInstructionCore,
|
|
261
|
+
'add_instruction_assistant',
|
|
262
|
+
(code, instruction, role) => ({
|
|
263
|
+
'instruction.thread_code': code,
|
|
264
|
+
'instruction.content_length': instruction?.length || 0,
|
|
265
|
+
'instruction.role': role,
|
|
266
|
+
'operation.type': 'add_instruction'
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
|
|
271
|
+
const thread = thread_ || await withTracing(getThread, 'get_thread_operation',
|
|
272
|
+
(threadCode) => ({
|
|
273
|
+
'thread.code': threadCode,
|
|
274
|
+
'operation.type': 'thread_retrieval',
|
|
275
|
+
'thread.provided': !!thread_
|
|
276
|
+
})
|
|
277
|
+
)(code);
|
|
278
|
+
if (!thread) return null;
|
|
279
|
+
|
|
280
|
+
const patientReply = await getLastMessages(code);
|
|
281
|
+
if (!patientReply) {
|
|
282
|
+
console.log('[replyAssistantCore] No relevant data found for this assistant.');
|
|
348
283
|
return null;
|
|
349
284
|
}
|
|
350
|
-
};
|
|
351
285
|
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
286
|
+
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
287
|
+
|
|
288
|
+
let patientMsg = false;
|
|
289
|
+
let urls = [];
|
|
290
|
+
for (let i = 0; i < patientReply.length; i++) {
|
|
291
|
+
const reply = patientReply[i];
|
|
292
|
+
const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
|
|
293
|
+
console.log(`[replyAssistantCore] Processing message ${i + 1}/${patientReply.length}: isPatient=${isPatient}, hasUrl=${!!url}`);
|
|
294
|
+
patientMsg = patientMsg || isPatient;
|
|
295
|
+
if (url) urls.push({ 'url': url });
|
|
359
296
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
const patientReply = await getLastMessages(code);
|
|
369
|
-
if (!patientReply) {
|
|
370
|
-
console.log('No relevant data found for this assistant.');
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
375
|
-
let activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
|
|
376
|
-
let activeRunsCount = activeRuns?.data?.length || 0;
|
|
377
|
-
console.log('ACTIVE RUNS:', activeRunsCount, activeRuns?.data?.map(run => ({ id: run.id, status: run.status })));
|
|
378
|
-
while (activeRunsCount > 0) {
|
|
379
|
-
console.log(`WAITING FOR ${activeRunsCount} ACTIVE RUNS TO COMPLETE - ${thread.getConversationId()}`);
|
|
380
|
-
activeRuns = await provider.listRuns({ threadId: thread.getConversationId(), activeOnly: true });
|
|
381
|
-
activeRunsCount = activeRuns?.data?.length || 0;
|
|
382
|
-
await delay(5000);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
let patientMsg = false;
|
|
386
|
-
let urls = [];
|
|
387
|
-
for (const reply of patientReply) {
|
|
388
|
-
const { isPatient, url } = await processIndividualMessage(code, reply, provider, thread);
|
|
389
|
-
console.log(`isPatient ${isPatient} ${url}`);
|
|
390
|
-
patientMsg = patientMsg || isPatient;
|
|
391
|
-
if (url) urls.push({ 'url': url });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (urls.length > 0) {
|
|
395
|
-
console.log('urls', urls);
|
|
396
|
-
const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
|
|
397
|
-
console.log('AFTER COMBINED IN BUFFER', processedFiles);
|
|
297
|
+
|
|
298
|
+
if (urls.length > 0) {
|
|
299
|
+
console.log(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
|
|
300
|
+
const { pdfBuffer, processedFiles } = await combineImagesToPDF({ code });
|
|
301
|
+
console.log(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
|
|
302
|
+
|
|
303
|
+
if (pdfBuffer) {
|
|
398
304
|
const key = `${code}-${Date.now()}-combined.pdf`;
|
|
399
305
|
const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
400
|
-
if (bucket
|
|
306
|
+
if (bucket) {
|
|
401
307
|
await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
|
|
402
308
|
}
|
|
403
|
-
if (processedFiles && processedFiles.length) {
|
|
404
|
-
cleanupFiles(processedFiles);
|
|
405
|
-
}
|
|
406
309
|
}
|
|
310
|
+
|
|
311
|
+
if (processedFiles && processedFiles.length) {
|
|
312
|
+
cleanupFiles(processedFiles);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
407
315
|
|
|
408
|
-
|
|
316
|
+
if (!patientMsg || thread.stopped) return null;
|
|
409
317
|
|
|
410
|
-
|
|
411
|
-
|
|
318
|
+
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
319
|
+
const { run, output, completed, retries } = await withTracing(
|
|
320
|
+
runAssistantWithRetries,
|
|
321
|
+
'run_assistant_with_retries',
|
|
322
|
+
(thread, assistant, runConfig, patientReply) => ({
|
|
323
|
+
'assistant.id': thread.getAssistantId(),
|
|
324
|
+
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
325
|
+
'assistant.has_patient_reply': !!patientReply
|
|
326
|
+
})
|
|
327
|
+
)(thread, assistant, runOptions, patientReply);
|
|
412
328
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const maxRetries = DEFAULT_MAX_RETRIES;
|
|
416
|
-
do {
|
|
417
|
-
({ run, output, completed } = await runAssistantAndWait({
|
|
418
|
-
thread,
|
|
419
|
-
assistant,
|
|
420
|
-
runConfig: runOptions
|
|
421
|
-
}));
|
|
422
|
-
console.log(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
|
|
423
|
-
|
|
424
|
-
if (completed && output) break;
|
|
425
|
-
if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
|
|
426
|
-
retries++;
|
|
427
|
-
} while (retries <= maxRetries && (!completed || !output));
|
|
428
|
-
|
|
429
|
-
console.log('RUN LAST ERROR:', run?.last_error);
|
|
430
|
-
console.log('RUN STATUS', completed);
|
|
431
|
-
console.log(output);
|
|
329
|
+
return output;
|
|
330
|
+
};
|
|
432
331
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
332
|
+
const replyAssistant = withTracing(
|
|
333
|
+
replyAssistantCore,
|
|
334
|
+
'assistant_reply',
|
|
335
|
+
(code, message_, thread_, runOptions) => ({
|
|
336
|
+
'assistant.thread_code': code,
|
|
337
|
+
'assistant.has_message': !!message_,
|
|
338
|
+
'assistant.has_custom_thread': !!thread_,
|
|
339
|
+
'assistant.run_options': JSON.stringify(runOptions)
|
|
340
|
+
})
|
|
341
|
+
);
|
|
438
342
|
|
|
439
343
|
const switchAssistant = async (code, assistant_id) => {
|
|
440
344
|
try {
|
|
@@ -43,9 +43,7 @@ class MongoStorage {
|
|
|
43
43
|
async saveMessage(messageData) {
|
|
44
44
|
try {
|
|
45
45
|
const enrichedMessage = await this._enrichTwilioMedia(messageData);
|
|
46
|
-
console.log('[MongoStorage] Enriched message', enrichedMessage);
|
|
47
46
|
const values = this.buildMessageValues(enrichedMessage);
|
|
48
|
-
console.log('[MongoStorage] Message values', values);
|
|
49
47
|
const { insertMessage } = require('../models/messageModel');
|
|
50
48
|
await insertMessage(values);
|
|
51
49
|
console.log('[MongoStorage] Message stored');
|
package/lib/utils/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const pino = require('pino');
|
|
2
|
+
const { trace, context } = require('@opentelemetry/api');
|
|
2
3
|
|
|
3
4
|
const createLogger = (config = {}) => {
|
|
4
5
|
const {
|
|
@@ -10,13 +11,99 @@ const createLogger = (config = {}) => {
|
|
|
10
11
|
transport: process.env.NODE_ENV !== 'production' ? {
|
|
11
12
|
target: 'pino-pretty',
|
|
12
13
|
options: {
|
|
13
|
-
colorize: true
|
|
14
|
+
colorize: true,
|
|
15
|
+
ignore: 'trace_id,span_id,trace_flags'
|
|
14
16
|
}
|
|
15
|
-
} : undefined
|
|
17
|
+
} : undefined,
|
|
18
|
+
// Add OpenTelemetry trace context automatically
|
|
19
|
+
formatters: {
|
|
20
|
+
log: (object) => {
|
|
21
|
+
const span = trace.getActiveSpan();
|
|
22
|
+
if (span) {
|
|
23
|
+
const spanContext = span.spanContext();
|
|
24
|
+
return {
|
|
25
|
+
...object,
|
|
26
|
+
trace_id: spanContext.traceId,
|
|
27
|
+
span_id: spanContext.spanId,
|
|
28
|
+
trace_flags: spanContext.traceFlags.toString(16).padStart(2, '0')
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return object;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
});
|
|
17
35
|
};
|
|
18
36
|
|
|
19
|
-
//
|
|
37
|
+
// Enhanced logger with observability methods
|
|
38
|
+
const createObservabilityLogger = (serviceName = 'nexus-assistant', config = {}) => {
|
|
39
|
+
const baseLogger = createLogger(config);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...baseLogger,
|
|
43
|
+
|
|
44
|
+
// Trace-aware logging methods
|
|
45
|
+
traceInfo: (message, extra = {}) => {
|
|
46
|
+
const span = trace.getActiveSpan();
|
|
47
|
+
baseLogger.info({
|
|
48
|
+
...extra,
|
|
49
|
+
operation: span?.name || 'unknown',
|
|
50
|
+
service: serviceName
|
|
51
|
+
}, message);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
traceError: (error, message, extra = {}) => {
|
|
55
|
+
const span = trace.getActiveSpan();
|
|
56
|
+
if (span) {
|
|
57
|
+
span.recordException(error);
|
|
58
|
+
span.setStatus({ code: 2, message: error.message });
|
|
59
|
+
}
|
|
60
|
+
baseLogger.error({
|
|
61
|
+
...extra,
|
|
62
|
+
err: error,
|
|
63
|
+
operation: span?.name || 'unknown',
|
|
64
|
+
service: serviceName
|
|
65
|
+
}, message);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
traceWarn: (message, extra = {}) => {
|
|
69
|
+
const span = trace.getActiveSpan();
|
|
70
|
+
baseLogger.warn({
|
|
71
|
+
...extra,
|
|
72
|
+
operation: span?.name || 'unknown',
|
|
73
|
+
service: serviceName
|
|
74
|
+
}, message);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Performance logging
|
|
78
|
+
performance: (operation, duration, extra = {}) => {
|
|
79
|
+
baseLogger.info({
|
|
80
|
+
...extra,
|
|
81
|
+
operation,
|
|
82
|
+
duration_ms: duration,
|
|
83
|
+
service: serviceName,
|
|
84
|
+
performance: true
|
|
85
|
+
}, `Operation ${operation} completed in ${duration}ms`);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Assistant-specific logging
|
|
89
|
+
assistantOperation: (operation, threadCode, extra = {}) => {
|
|
90
|
+
baseLogger.info({
|
|
91
|
+
...extra,
|
|
92
|
+
assistant_operation: operation,
|
|
93
|
+
thread_code: threadCode,
|
|
94
|
+
service: serviceName
|
|
95
|
+
}, `Assistant operation: ${operation} for thread ${threadCode}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Default logger instances
|
|
20
101
|
const logger = createLogger();
|
|
102
|
+
const observabilityLogger = createObservabilityLogger();
|
|
21
103
|
|
|
22
|
-
module.exports = {
|
|
104
|
+
module.exports = {
|
|
105
|
+
logger,
|
|
106
|
+
createLogger,
|
|
107
|
+
observabilityLogger,
|
|
108
|
+
createObservabilityLogger
|
|
109
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize filename to prevent path traversal and command injection
|
|
3
|
+
* @param {string} filename - The filename to sanitize
|
|
4
|
+
* @param {number} maxLength - Maximum length (default: 50)
|
|
5
|
+
* @returns {string} Sanitized filename
|
|
6
|
+
*/
|
|
7
|
+
function sanitizeFilename(filename, maxLength = 50) {
|
|
8
|
+
if (typeof filename !== 'string') return 'unknown';
|
|
9
|
+
|
|
10
|
+
return filename
|
|
11
|
+
.replace(/[^\w.-]/g, '_')
|
|
12
|
+
.replace(/^\.+/, '')
|
|
13
|
+
.replace(/\.+$/, '')
|
|
14
|
+
.slice(0, maxLength);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize filename for media uploads (more restrictive)
|
|
19
|
+
* @param {string} filename - The filename to sanitize
|
|
20
|
+
* @param {number} maxLength - Maximum length (default: 50)
|
|
21
|
+
* @returns {string} Sanitized filename for media
|
|
22
|
+
*/
|
|
23
|
+
function sanitizeMediaFilename(filename, maxLength = 50) {
|
|
24
|
+
if (typeof filename !== 'string') return '';
|
|
25
|
+
|
|
26
|
+
return filename
|
|
27
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
28
|
+
.slice(0, maxLength);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize log metadata to remove sensitive information
|
|
33
|
+
* @param {Object} metadata - Metadata object to sanitize
|
|
34
|
+
* @returns {Object} Sanitized metadata
|
|
35
|
+
*/
|
|
36
|
+
function sanitizeLogMetadata(metadata) {
|
|
37
|
+
const safe = { ...metadata };
|
|
38
|
+
|
|
39
|
+
delete safe.text;
|
|
40
|
+
delete safe.content;
|
|
41
|
+
delete safe.body;
|
|
42
|
+
delete safe.phone;
|
|
43
|
+
delete safe.numero;
|
|
44
|
+
|
|
45
|
+
if (safe.message_id) {
|
|
46
|
+
safe.message_id = safe.message_id.length > 8
|
|
47
|
+
? `${safe.message_id.slice(0, 4)}***${safe.message_id.slice(-4)}`
|
|
48
|
+
: '***';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (safe.code && safe.code.length > 8) {
|
|
52
|
+
safe.code = `${safe.code.substring(0, 3)}***${safe.code.slice(-4)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return safe;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
sanitizeFilename,
|
|
60
|
+
sanitizeMediaFilename,
|
|
61
|
+
sanitizeLogMetadata
|
|
62
|
+
};
|