@peopl-health/nexus 2.2.10 → 2.3.0

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.
@@ -0,0 +1,136 @@
1
+ const moment = require('moment-timezone');
2
+ const { Message } = require('../models/messageModel.js');
3
+
4
+ const addMessageToThread = async (reply, messagesChat, provider, thread) => {
5
+ const threadId = thread.getConversationId();
6
+
7
+ if (reply.origin === 'whatsapp_platform') {
8
+ await provider.addMessage({
9
+ threadId,
10
+ role: 'assistant',
11
+ content: messagesChat
12
+ });
13
+ } else if (reply.origin === 'patient') {
14
+ await provider.addMessage({
15
+ threadId,
16
+ role: 'user',
17
+ content: messagesChat
18
+ });
19
+ }
20
+
21
+ console.log(`[addMessageToThread] Message added - ID: ${reply.message_id}, Thread: ${threadId}, Origin: ${reply.origin}`);
22
+ };
23
+
24
+ const updateMessageRecord = async (reply, thread) => {
25
+ const threadId = thread.getConversationId();
26
+
27
+ await Message.updateOne(
28
+ { message_id: reply.message_id, timestamp: reply.timestamp },
29
+ { $set: {
30
+ assistant_id: thread.getAssistantId(),
31
+ thread_id: threadId,
32
+ processed: true
33
+ } }
34
+ );
35
+
36
+ console.log(`[updateMessageRecord] Record updated - ID: ${reply.message_id}, Thread: ${threadId}, Processed: true`);
37
+ };
38
+
39
+
40
+ async function getLastMessages(code) {
41
+ try {
42
+ let query = { processed: false };
43
+ if (code.endsWith('@g.us')) {
44
+ query = { ...query, numero: code, $or: [{ origin: 'patient' }, { origin: 'whatsapp_platform' }] };
45
+ } else {
46
+ query = { ...query, numero: code, $or: [{ origin: 'patient' }] };
47
+ }
48
+
49
+ const lastMessages = await Message.find(query)
50
+ .sort({ createdAt: 1 });
51
+
52
+ if (!lastMessages || lastMessages.length === 0) {
53
+ console.log(`[getLastMessages] No messages found for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}`);
54
+ return null;
55
+ }
56
+
57
+ const unprocessedMessages = lastMessages.filter(msg => !msg.processed);
58
+ if (unprocessedMessages.length === 0) {
59
+ console.log(`[getLastMessages] No unprocessed messages for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}`);
60
+ return null;
61
+ }
62
+
63
+ console.log(`[getLastMessages] Found ${unprocessedMessages.length} unprocessed messages for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}`);
64
+ return unprocessedMessages;
65
+
66
+ } catch (error) {
67
+ console.error(`[getLastMessages] Error for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}:`, error?.message || String(error));
68
+ return null;
69
+ }
70
+ }
71
+
72
+ async function getLastNMessages(code, n) {
73
+ try {
74
+ const lastMessages = await Message.find({ numero: code })
75
+ .sort({ createdAt: -1 })
76
+ .limit(n);
77
+
78
+ if (!lastMessages || lastMessages.length === 0) {
79
+ console.log(`[getLastNMessages] No messages found for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}, limit: ${n}`);
80
+ return [];
81
+ }
82
+
83
+ console.log(`[getLastNMessages] Found ${lastMessages.length} messages for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}, limit: ${n}`);
84
+ return lastMessages;
85
+
86
+ } catch (error) {
87
+ console.error(`[getLastNMessages] Error for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}, limit: ${n}:`, error?.message || String(error));
88
+ return [];
89
+ }
90
+ }
91
+
92
+ function formatMessage(reply) {
93
+ try {
94
+ if (!reply.timestamp) {
95
+ return null;
96
+ }
97
+
98
+ const timestamp = parseInt(reply.timestamp) * 1000;
99
+ const msgDate = new Date(timestamp);
100
+
101
+ if (isNaN(msgDate.getTime())) {
102
+ console.warn(`[formatMessage] Invalid timestamp for message ID: ${reply.message_id}`);
103
+ return reply.body;
104
+ }
105
+
106
+ const mexicoCityTime = moment(msgDate)
107
+ .tz('America/Mexico_City')
108
+ .locale('es')
109
+ .format('dddd, D [de] MMMM [de] YYYY [a las] h:mm A');
110
+
111
+ return `[${mexicoCityTime}] ${reply.body}`;
112
+ } catch (error) {
113
+ console.error(`[formatMessage] Error for message ID: ${reply.message_id}:`, error?.message || String(error));
114
+ return null;
115
+ }
116
+ }
117
+
118
+ async function isRecentMessage(chatId) {
119
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
120
+
121
+ const recentMessage = await Message.find({
122
+ $or: [{ group_id: chatId }, { numero: chatId }],
123
+ createdAt: { $gte: fiveMinutesAgo }
124
+ }).sort({ createdAt: -1 }).limit(1);
125
+
126
+ return !!recentMessage;
127
+ }
128
+
129
+ module.exports = {
130
+ addMessageToThread,
131
+ updateMessageRecord,
132
+ getLastMessages,
133
+ getLastNMessages,
134
+ formatMessage,
135
+ isRecentMessage
136
+ };
@@ -0,0 +1,238 @@
1
+ const fs = require('fs');
2
+ const { generatePresignedUrl } = require('../config/awsConfig.js');
3
+ const { analyzeImage } = require('./llmsHelper.js');
4
+ const { cleanupFiles, downloadMediaAndCreateFile } = require('./filesHelper.js');
5
+ const { formatMessage, addMessageToThread, updateMessageRecord } = require('./messageHelper.js');
6
+ const { sanitizeLogMetadata } = require('../utils/sanitizer.js');
7
+
8
+ /**
9
+ * Structured logging with PHI protection
10
+ */
11
+ const DEBUG_ENABLED = process.env.DEBUG_MESSAGE_CONTENT === 'true';
12
+ const logger = {
13
+ info: (context, metadata = {}) => {
14
+ const safeMetadata = sanitizeLogMetadata(metadata);
15
+ console.log(`[${context}]`, JSON.stringify(safeMetadata));
16
+ },
17
+ warn: (context, metadata = {}) => {
18
+ const safeMetadata = sanitizeLogMetadata(metadata);
19
+ console.warn(`[${context}]`, JSON.stringify(safeMetadata));
20
+ },
21
+ error: (context, error, metadata = {}) => {
22
+ const safeMetadata = sanitizeLogMetadata(metadata);
23
+ console.error(`[${context}]`, {
24
+ error: error?.message || String(error),
25
+ ...safeMetadata
26
+ });
27
+ },
28
+ debug: (context, sensitiveData = {}) => {
29
+ if (DEBUG_ENABLED) {
30
+ console.log(`[DEBUG:${context}]`, sensitiveData);
31
+ }
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Dedicated message processing utilities
37
+ * Handles text messages, media files, audio transcription, and thread operations
38
+ */
39
+ const processTextMessage = (reply) => {
40
+ const formattedMessage = formatMessage(reply);
41
+ logger.info('processTextMessage', {
42
+ message_id: reply.message_id,
43
+ timestamp: reply.timestamp,
44
+ from_me: reply.from_me,
45
+ body: reply.body,
46
+ hasContent: !!formattedMessage
47
+ });
48
+ logger.debug('processTextMessage_content', { formattedMessage });
49
+
50
+ const messagesChat = [];
51
+ if (formattedMessage) {
52
+ messagesChat.push({ type: 'text', text: formattedMessage });
53
+ }
54
+
55
+ return messagesChat;
56
+ };
57
+
58
+ const processImageFile = async (fileName, reply) => {
59
+ let imageAnalysis = null;
60
+ let url = null;
61
+ const messagesChat = [];
62
+
63
+ try {
64
+ imageAnalysis = await analyzeImage(fileName);
65
+
66
+ logger.info('processImageFile', {
67
+ message_id: reply.message_id,
68
+ bucketName: reply.media?.bucketName,
69
+ key: reply.media?.key,
70
+ medical_relevance: imageAnalysis?.medical_relevance,
71
+ has_table: imageAnalysis?.has_table,
72
+ analysis_type: imageAnalysis?.medical_analysis ? 'medical' : 'general'
73
+ });
74
+
75
+ logger.debug('processImageFile_analysis', { imageAnalysis });
76
+
77
+ const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
78
+
79
+ // Generate presigned URL if medically relevant
80
+ if (imageAnalysis?.medical_relevance) {
81
+ url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
82
+ }
83
+
84
+ // Add appropriate text based on analysis
85
+ if (imageAnalysis?.has_table && imageAnalysis.table_data) {
86
+ messagesChat.push({
87
+ type: 'text',
88
+ text: imageAnalysis.table_data,
89
+ });
90
+ } else if (imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
91
+ messagesChat.push({
92
+ type: 'text',
93
+ text: imageAnalysis.medical_analysis,
94
+ });
95
+ } else {
96
+ messagesChat.push({
97
+ type: 'text',
98
+ text: imageAnalysis?.description || 'Image processed',
99
+ });
100
+ }
101
+ } catch (error) {
102
+ logger.error('processImageFile', error, {
103
+ message_id: reply.message_id,
104
+ fileName: fileName ? fileName.split('/').pop() : 'unknown'
105
+ });
106
+ messagesChat.push({
107
+ type: 'text',
108
+ text: 'Image received but could not be analyzed',
109
+ });
110
+ }
111
+
112
+ return { messagesChat, url };
113
+ };
114
+
115
+ const processAudioFile = async (fileName, provider) => {
116
+ const messagesChat = [];
117
+
118
+ try {
119
+ const audioTranscript = await provider.transcribeAudio({
120
+ file: fs.createReadStream(fileName),
121
+ responseFormat: 'text',
122
+ language: 'es'
123
+ });
124
+
125
+ const transcriptText = audioTranscript?.text || audioTranscript;
126
+ messagesChat.push({
127
+ type: 'text',
128
+ text: transcriptText,
129
+ });
130
+
131
+ logger.info('processAudioFile', {
132
+ fileName: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown',
133
+ transcription_success: true,
134
+ transcript_length: transcriptText?.length || 0
135
+ });
136
+
137
+ logger.debug('processAudioFile_transcript', { transcriptText });
138
+
139
+ } catch (error) {
140
+ logger.error('processAudioFile', error, {
141
+ fileName: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown'
142
+ });
143
+ messagesChat.push({
144
+ type: 'text',
145
+ text: 'Audio received but could not be transcribed',
146
+ });
147
+ }
148
+
149
+ return messagesChat;
150
+ };
151
+
152
+ const processMediaFiles = async (code, reply, provider) => {
153
+ let url = null;
154
+ const messagesChat = [];
155
+ const tempFiles = [];
156
+
157
+ if (!reply.is_media) {
158
+ return { messagesChat, url, tempFiles };
159
+ }
160
+
161
+ logger.info('processMediaFiles', {
162
+ message_id: reply.message_id,
163
+ processing_media: true
164
+ });
165
+
166
+ const fileNames = await downloadMediaAndCreateFile(code, reply);
167
+ tempFiles.push(...fileNames);
168
+
169
+ for (const fileName of fileNames) {
170
+ const safeFileName = fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown';
171
+
172
+ logger.info('processMediaFiles_file', {
173
+ message_id: reply.message_id,
174
+ fileName: safeFileName
175
+ });
176
+
177
+ // Skip unsupported file types
178
+ if (fileName.toLowerCase().includes('.wbmp') || fileName.toLowerCase().includes('sticker')) {
179
+ logger.info('processMediaFiles_skip', {
180
+ message_id: reply.message_id,
181
+ fileName: safeFileName,
182
+ reason: 'unsupported_format'
183
+ });
184
+ continue;
185
+ }
186
+
187
+ if (fileName.includes('image') || fileName.includes('document') || fileName.includes('application')) {
188
+ const { messagesChat: imageMessages, url: imageUrl } = await processImageFile(fileName, reply);
189
+ messagesChat.push(...imageMessages);
190
+ if (imageUrl) url = imageUrl;
191
+ } else if (fileName.includes('audio')) {
192
+ const audioMessages = await processAudioFile(fileName, provider);
193
+ messagesChat.push(...audioMessages);
194
+ }
195
+ }
196
+
197
+ return { messagesChat, url, tempFiles };
198
+ };
199
+
200
+ const processIndividualMessage = async (code, reply, provider, thread) => {
201
+ let tempFiles = [];
202
+ try {
203
+ const isPatient = reply.origin === 'patient';
204
+ const textMessages = processTextMessage(reply);
205
+
206
+ const { messagesChat: mediaMessages, url, tempFiles: mediaFiles } = await processMediaFiles(code, reply, provider);
207
+ tempFiles = mediaFiles;
208
+
209
+ const allMessages = [...textMessages, ...mediaMessages];
210
+
211
+ if (allMessages.length > 0) {
212
+ await addMessageToThread(reply, allMessages, provider, thread);
213
+ await updateMessageRecord(reply, thread);
214
+ }
215
+
216
+ return { isPatient, url };
217
+ } catch (error) {
218
+ logger.error('processIndividualMessage', error, {
219
+ message_id: reply.message_id,
220
+ code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
221
+ origin: reply.origin
222
+ });
223
+ await cleanupFiles(tempFiles);
224
+ return { isPatient: false, url: null };
225
+ } finally {
226
+ if (tempFiles.length > 0) {
227
+ await cleanupFiles(tempFiles);
228
+ }
229
+ }
230
+ };
231
+
232
+ module.exports = {
233
+ processTextMessage,
234
+ processImageFile,
235
+ processAudioFile,
236
+ processMediaFiles,
237
+ processIndividualMessage
238
+ };
@@ -0,0 +1,73 @@
1
+ const { Thread } = require('../models/threadModel.js');
2
+ const { createProvider } = require('../providers/createProvider.js');
3
+ const llmConfig = require('../config/llmConfig.js');
4
+
5
+ const log = (level, context, data, error = null) => {
6
+ const safeData = { ...data };
7
+ if (safeData.code) safeData.code = `${safeData.code.substring(0, 3)}***${safeData.code.slice(-4)}`;
8
+ console[level](`[${context}]`, error ? { error: error?.message || String(error), ...safeData } : safeData);
9
+ };
10
+
11
+ const TERMINAL_STATUSES = ['cancelled', 'expired', 'completed', 'failed', 'incomplete'];
12
+ const MAX_ATTEMPTS = 30;
13
+ const MAX_WAIT_MS = 30000;
14
+
15
+ const getThread = async (code, message = null) => {
16
+ try {
17
+ let thread = await Thread.findOne({ code: code });
18
+ log('log', 'getThread', { code, hasThread: !!thread, hasRunId: !!(thread?.run_id) });
19
+
20
+ if (!thread && message) {
21
+ thread = new Thread({ code, active: true });
22
+ await thread.save();
23
+ }
24
+ if (thread?.run_id) {
25
+ const provider = createProvider({ variant: process.env.VARIANT || 'assistants' }) || llmConfig.requireOpenAIProvider();
26
+ let attempts = 0;
27
+ const start = Date.now();
28
+
29
+ while (thread.run_id && attempts < MAX_ATTEMPTS && (Date.now() - start) < MAX_WAIT_MS) {
30
+ attempts++;
31
+ try {
32
+ const run = await provider.getRun({ threadId: thread.getConversationId(), runId: thread.run_id });
33
+
34
+ if (TERMINAL_STATUSES.includes(run.status)) {
35
+ log('log', 'getThread_terminal', { code, status: run.status, attempts });
36
+ break;
37
+ }
38
+ } catch (error) {
39
+ log('error', 'getThread_getRun_failed', { code, attempt: attempts }, error);
40
+ break;
41
+ }
42
+ await new Promise(r => setTimeout(r, 1000));
43
+ }
44
+
45
+ if (attempts >= MAX_ATTEMPTS || (Date.now() - start) >= MAX_WAIT_MS) {
46
+ log('warn', 'getThread_timeout', { code, attempts, elapsed: Date.now() - start });
47
+ }
48
+
49
+ await Thread.updateOne({ code, active: true }, { $set: { run_id: null } });
50
+ thread = await Thread.findOne({ code });
51
+ }
52
+
53
+ return thread;
54
+ } catch (error) {
55
+ log('error', 'getThread', { code }, error);
56
+ return null;
57
+ }
58
+ };
59
+
60
+ const getThreadInfo = async (code) => {
61
+ try {
62
+ let thread = await Thread.findOne({ code: code });
63
+ return thread;
64
+ } catch (error) {
65
+ log('error', 'getThreadInfo', { code }, error);
66
+ return null;
67
+ }
68
+ };
69
+
70
+ module.exports = {
71
+ getThread,
72
+ getThreadInfo
73
+ };
@@ -1,5 +1,5 @@
1
- const { Message } = require('../models/messageModel');
2
-
1
+ const { Message } = require('../models/messageModel.js');
2
+ const { isRecentMessage } = require('./messageHelper.js');
3
3
  const axios = require('axios');
4
4
  const { v4: uuidv4 } = require('uuid');
5
5
 
@@ -65,18 +65,6 @@ function extractTitle(message, mediaType) {
65
65
  }
66
66
 
67
67
 
68
- async function isRecentMessage(chatId) {
69
- const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
70
-
71
- const recentMessage = await Message.find({
72
- $or: [{ group_id: chatId }, { numero: chatId }],
73
- createdAt: { $gte: fiveMinutesAgo }
74
- }).sort({ createdAt: -1 }).limit(1);
75
-
76
- return !!recentMessage;
77
- }
78
-
79
-
80
68
  async function getLastMessages(chatId, n) {
81
69
  const messages = await Message.find({ numero: chatId })
82
70
  .sort({ createdAt: -1 })
@@ -106,7 +106,6 @@ async function insertMessage(values) {
106
106
  origin: values.origin,
107
107
  raw: values.raw || null
108
108
  };
109
- console.log('[MongoStorage] Inserting message', messageData);
110
109
 
111
110
  await Message.findOneAndUpdate(
112
111
  { message_id: values.message_id, body: values.body },
@@ -0,0 +1,184 @@
1
+ const { initTelemetry, shutdownTelemetry } = require('./telemetry');
2
+ const { trace, metrics } = require('@opentelemetry/api');
3
+
4
+ const tracer = trace.getTracer('nexus-assistant');
5
+ const meter = metrics.getMeter('nexus-assistant');
6
+
7
+ const responseTimeHistogram = meter.createHistogram('nexus_response_time', {
8
+ description: 'Response time in milliseconds',
9
+ unit: 'ms',
10
+ });
11
+
12
+ const operationCounter = meter.createCounter('nexus_operations_total', {
13
+ description: 'Total number of operations',
14
+ });
15
+
16
+ const activeOperationsGauge = meter.createUpDownCounter('nexus_active_operations', {
17
+ description: 'Currently active operations',
18
+ });
19
+
20
+ const assistantReplyDuration = meter.createHistogram('nexus_assistant_reply_duration', {
21
+ description: 'Assistant reply duration in milliseconds',
22
+ unit: 'ms',
23
+ });
24
+
25
+ const assistantInstructionDuration = meter.createHistogram('nexus_assistant_instruction_duration', {
26
+ description: 'Assistant instruction execution duration in milliseconds',
27
+ unit: 'ms',
28
+ });
29
+
30
+ const assistantRetryCounter = meter.createCounter('nexus_assistant_retries_total', {
31
+ description: 'Total number of assistant operation retries',
32
+ });
33
+
34
+ const threadOperationsCounter = meter.createCounter('nexus_thread_operations_total', {
35
+ description: 'Total number of thread operations',
36
+ });
37
+
38
+ const messageProcessingDuration = meter.createHistogram('nexus_message_processing_duration', {
39
+ description: 'Message processing duration in milliseconds',
40
+ unit: 'ms',
41
+ });
42
+
43
+ const fileOperationsCounter = meter.createCounter('nexus_file_operations_total', {
44
+ description: 'Total number of file operations',
45
+ });
46
+
47
+ const s3UploadDuration = meter.createHistogram('nexus_s3_upload_duration', {
48
+ description: 'S3 upload duration in milliseconds',
49
+ unit: 'ms',
50
+ });
51
+
52
+ /**
53
+ * Wrapper function to trace any async operation
54
+ */
55
+ async function traceOperation(name, operation, attributes = {}) {
56
+ const span = tracer.startSpan(name, { attributes });
57
+ const startTime = Date.now();
58
+
59
+ try {
60
+ activeOperationsGauge.add(1, attributes);
61
+
62
+ const result = await operation(span);
63
+
64
+ const duration = Date.now() - startTime;
65
+
66
+ responseTimeHistogram.record(duration, attributes);
67
+ operationCounter.add(1, { ...attributes, success: true });
68
+
69
+ span.setAttributes({
70
+ 'operation.duration_ms': duration,
71
+ 'operation.success': true,
72
+ });
73
+
74
+ span.setStatus({ code: 1 });
75
+ return result;
76
+
77
+ } catch (error) {
78
+ const duration = Date.now() - startTime;
79
+
80
+ operationCounter.add(1, { ...attributes, success: false });
81
+ responseTimeHistogram.record(duration, { ...attributes, error: true });
82
+
83
+ span.recordException(error);
84
+ span.setStatus({ code: 2, message: error.message });
85
+ span.setAttributes({
86
+ 'operation.duration_ms': duration,
87
+ 'operation.success': false,
88
+ 'error.message': error.message,
89
+ });
90
+
91
+ throw error;
92
+ } finally {
93
+ activeOperationsGauge.add(-1, attributes);
94
+ span.end();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Helper to add custom span with automatic cleanup
100
+ */
101
+ function createSpan(name, attributes = {}) {
102
+ return tracer.startSpan(name, { attributes });
103
+ }
104
+
105
+ /**
106
+ * Initialize telemetry with config
107
+ */
108
+ function init(config = {}) {
109
+ initTelemetry(config);
110
+ console.log('🎯 Custom tracing and metrics ready');
111
+ }
112
+
113
+ /**
114
+ * Enhanced tracing functions for specific operations
115
+ */
116
+ function traceAssistantReply(operation, attributes = {}) {
117
+ return traceOperation('assistant_reply', async (span) => {
118
+ const startTime = Date.now();
119
+ try {
120
+ const result = await operation(span);
121
+ const duration = Date.now() - startTime;
122
+ assistantReplyDuration.record(duration, attributes);
123
+ return result;
124
+ } catch (error) {
125
+ const duration = Date.now() - startTime;
126
+ assistantReplyDuration.record(duration, { ...attributes, error: true });
127
+ throw error;
128
+ }
129
+ }, attributes);
130
+ }
131
+
132
+ function traceAssistantInstruction(operation, attributes = {}) {
133
+ return traceOperation('assistant_instruction', async (span) => {
134
+ const startTime = Date.now();
135
+ try {
136
+ const result = await operation(span);
137
+ const duration = Date.now() - startTime;
138
+ assistantInstructionDuration.record(duration, attributes);
139
+ return result;
140
+ } catch (error) {
141
+ const duration = Date.now() - startTime;
142
+ assistantInstructionDuration.record(duration, { ...attributes, error: true });
143
+ throw error;
144
+ }
145
+ }, attributes);
146
+ }
147
+
148
+ function recordAssistantRetry(attributes = {}) {
149
+ assistantRetryCounter.add(1, attributes);
150
+ }
151
+
152
+ function recordThreadOperation(operationType, attributes = {}) {
153
+ threadOperationsCounter.add(1, { ...attributes, operation_type: operationType });
154
+ }
155
+
156
+ function recordFileOperation(operationType, attributes = {}) {
157
+ fileOperationsCounter.add(1, { ...attributes, operation_type: operationType });
158
+ }
159
+
160
+ module.exports = {
161
+ init,
162
+ shutdown: shutdownTelemetry,
163
+ traceOperation,
164
+ createSpan,
165
+ tracer,
166
+ meter,
167
+ // Enhanced tracing functions
168
+ traceAssistantReply,
169
+ traceAssistantInstruction,
170
+ recordAssistantRetry,
171
+ recordThreadOperation,
172
+ recordFileOperation,
173
+ // Metrics
174
+ responseTimeHistogram,
175
+ operationCounter,
176
+ activeOperationsGauge,
177
+ assistantReplyDuration,
178
+ assistantInstructionDuration,
179
+ assistantRetryCounter,
180
+ threadOperationsCounter,
181
+ messageProcessingDuration,
182
+ fileOperationsCounter,
183
+ s3UploadDuration,
184
+ };