@peopl-health/nexus 3.3.16 → 3.3.18-fix-model
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 +1 -1
- package/lib/assistants/BaseAssistant.js +11 -2
- package/lib/config/lifecycle.js +19 -0
- package/lib/config/llmConfig.js +9 -1
- package/lib/config/metaConfig.js +6 -1
- package/lib/config/runtimeConfig.js +3 -1
- package/lib/core/NexusMessaging.js +10 -18
- package/lib/helpers/llmsHelper.js +1 -1
- package/lib/helpers/twilioMediaHelper.js +37 -1
- package/lib/index.d.ts +42 -61
- package/lib/index.js +4 -1
- package/lib/models/predictionMetricsModel.js +25 -1
- package/lib/models/threadModel.js +7 -8
- package/lib/observability/index.js +2 -1
- package/lib/observability/telemetry.js +12 -3
- package/lib/providers/OpenAIResponsesProvider.js +15 -0
- package/lib/providers/OpenAIResponsesProviderTools.js +1 -1
- package/lib/routes/index.js +6 -0
- package/lib/services/assistantResolver.js +8 -1
- package/lib/services/assistantService.js +31 -4
- package/lib/storage/MongoStorage.js +11 -44
- package/package.json +1 -1
|
@@ -145,7 +145,7 @@ class TwilioProvider extends MessageProvider {
|
|
|
145
145
|
}
|
|
146
146
|
} else {
|
|
147
147
|
result = await this.twilioClient.messages.create(messageParams);
|
|
148
|
-
await saveMessage(messageParams.body, result);
|
|
148
|
+
await saveMessage(messageParams.body || messageData.body, result);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
return {
|
|
@@ -50,13 +50,21 @@ class BaseAssistant {
|
|
|
50
50
|
registerTool(config) {
|
|
51
51
|
const { name, handler, description, parameters, strict } = config;
|
|
52
52
|
|
|
53
|
+
const toolParams = parameters
|
|
54
|
+
? { ...parameters }
|
|
55
|
+
: { type: 'object', properties: {} };
|
|
56
|
+
|
|
57
|
+
if (toolParams.additionalProperties === undefined) {
|
|
58
|
+
toolParams.additionalProperties = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
this.tools.set(name, {
|
|
54
62
|
schema: {
|
|
55
63
|
type: 'function',
|
|
56
64
|
function: {
|
|
57
65
|
name,
|
|
58
66
|
description: description || 'Custom assistant tool',
|
|
59
|
-
parameters:
|
|
67
|
+
parameters: toolParams,
|
|
60
68
|
...(strict !== undefined && { strict })
|
|
61
69
|
}
|
|
62
70
|
},
|
|
@@ -71,7 +79,8 @@ class BaseAssistant {
|
|
|
71
79
|
async executeTool(name, args) {
|
|
72
80
|
const tool = this.tools.get(name);
|
|
73
81
|
if (!tool) {
|
|
74
|
-
|
|
82
|
+
logger.warn('[executeTool] Rejected unregistered tool', { name, assistantId: this.assistantId });
|
|
83
|
+
throw new Error(`Tool '${name}' is not available`);
|
|
75
84
|
}
|
|
76
85
|
return await tool.execute(args);
|
|
77
86
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { _resetState: _resetLLMState } = require('../config/llmConfig');
|
|
2
|
+
const { _resetOverrides } = require('../config/runtimeConfig');
|
|
3
|
+
const { _resetMetaConfig } = require('../config/metaConfig');
|
|
4
|
+
const { _clearClinicalContextCache } = require('../models/messageModel');
|
|
5
|
+
const { _resetRegistry } = require('../services/assistantResolver');
|
|
6
|
+
const { setPreprocessingHandler } = require('../services/preprocessingService');
|
|
7
|
+
const { _resetDefaultInstance } = require('../core/NexusMessaging');
|
|
8
|
+
|
|
9
|
+
function resetAll() {
|
|
10
|
+
_resetDefaultInstance();
|
|
11
|
+
_resetRegistry();
|
|
12
|
+
_resetLLMState();
|
|
13
|
+
setPreprocessingHandler(null);
|
|
14
|
+
_resetOverrides();
|
|
15
|
+
_resetMetaConfig();
|
|
16
|
+
_clearClinicalContextCache();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { resetAll };
|
package/lib/config/llmConfig.js
CHANGED
|
@@ -51,6 +51,13 @@ const getAnthropicClient = () => {
|
|
|
51
51
|
return state.anthropicClient;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
const _resetState = () => {
|
|
55
|
+
state.anthropicClient = null;
|
|
56
|
+
state.openaiClient = null;
|
|
57
|
+
state.provider = null;
|
|
58
|
+
state.variant = process.env.VARIANT || 'assistants';
|
|
59
|
+
};
|
|
60
|
+
|
|
54
61
|
module.exports = {
|
|
55
62
|
get openaiClient() { return state.openaiClient; },
|
|
56
63
|
get providerInstance() { return state.provider; },
|
|
@@ -59,5 +66,6 @@ module.exports = {
|
|
|
59
66
|
setOpenAIProvider,
|
|
60
67
|
getOpenAIProvider,
|
|
61
68
|
requireOpenAIProvider,
|
|
62
|
-
configureOpenAIProvider
|
|
69
|
+
configureOpenAIProvider,
|
|
70
|
+
_resetState
|
|
63
71
|
};
|
package/lib/config/metaConfig.js
CHANGED
|
@@ -25,7 +25,12 @@ const setMetaConfig = ({ accessToken, wabaId, apiVersion }) => {
|
|
|
25
25
|
|
|
26
26
|
const getMetaConfig = () => metaConfig;
|
|
27
27
|
|
|
28
|
+
const _resetMetaConfig = () => {
|
|
29
|
+
metaConfig = { accessToken: null, wabaId: null, apiVersion: 'v21.0' };
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
module.exports = {
|
|
29
33
|
setMetaConfig,
|
|
30
|
-
getMetaConfig
|
|
34
|
+
getMetaConfig,
|
|
35
|
+
_resetMetaConfig
|
|
31
36
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
2
|
|
|
3
3
|
const { airtable } = require('../config/airtableConfig');
|
|
4
|
-
const runtimeConfig = require('../config/runtimeConfig');
|
|
5
4
|
const llmConfigModule = require('../config/llmConfig');
|
|
6
5
|
const { connect } = require('../config/mongoConfig');
|
|
7
6
|
|
|
@@ -11,7 +10,7 @@ const { Message, getMessageValues, insertMessage } = require('../models/messageM
|
|
|
11
10
|
const { Thread } = require('../models/threadModel');
|
|
12
11
|
|
|
13
12
|
const { ensureThreadExists } = require('../helpers/threadHelper');
|
|
14
|
-
const {
|
|
13
|
+
const { enrichMessageWithTwilioMedia } = require('../helpers/twilioMediaHelper');
|
|
15
14
|
const { convertTwilioToInternalFormat } = require('../helpers/twilioHelper');
|
|
16
15
|
|
|
17
16
|
const { createMessagingProvider } = require('../adapters/registry');
|
|
@@ -360,25 +359,15 @@ class NexusMessaging {
|
|
|
360
359
|
async _ensureMediaPersistence(messageData) {
|
|
361
360
|
try {
|
|
362
361
|
const raw = messageData?.raw;
|
|
363
|
-
|
|
362
|
+
const result = await enrichMessageWithTwilioMedia(raw);
|
|
363
|
+
if (!result) return;
|
|
364
364
|
|
|
365
|
-
const
|
|
366
|
-
if (numMedia <= 0 || !raw.MediaUrl0) return;
|
|
367
|
-
|
|
368
|
-
const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
369
|
-
if (!bucketName) return;
|
|
370
|
-
|
|
371
|
-
const mediaItems = await processTwilioMediaMessage(raw, bucketName);
|
|
372
|
-
if (!mediaItems?.length) return;
|
|
373
|
-
|
|
374
|
-
raw.__nexusMediaProcessed = true;
|
|
375
|
-
const [primary, ...rest] = mediaItems;
|
|
376
|
-
const mediaPayload = rest.length ? { ...primary, metadata: { ...primary.metadata, attachments: rest } } : primary;
|
|
365
|
+
const { mediaPayload, caption } = result;
|
|
377
366
|
|
|
378
367
|
if (messageData.media == null) messageData.media = mediaPayload;
|
|
379
368
|
if (messageData.fileUrl == null) messageData.fileUrl = mediaPayload.metadata?.presignedUrl;
|
|
380
369
|
if (messageData.fileType == null) messageData.fileType = mediaPayload.mediaType;
|
|
381
|
-
if (messageData.caption == null) messageData.caption =
|
|
370
|
+
if (messageData.caption == null) messageData.caption = caption;
|
|
382
371
|
if (messageData.body == null) messageData.body = messageData.caption;
|
|
383
372
|
messageData.isMedia = true;
|
|
384
373
|
|
|
@@ -483,12 +472,15 @@ const sendScheduledMessage = async (scheduledMessage) => {
|
|
|
483
472
|
return await requireDefaultInstance().sendScheduledMessage(scheduledMessage);
|
|
484
473
|
};
|
|
485
474
|
|
|
486
|
-
|
|
475
|
+
const _resetDefaultInstance = () => { defaultInstance = null; };
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
487
478
|
NexusMessaging,
|
|
488
479
|
sendMessage,
|
|
489
480
|
sendScheduledMessage,
|
|
490
481
|
setDefaultInstance,
|
|
491
482
|
getDefaultInstance,
|
|
492
483
|
getProvider,
|
|
493
|
-
requireProvider
|
|
484
|
+
requireProvider,
|
|
485
|
+
_resetDefaultInstance
|
|
494
486
|
};
|
|
@@ -37,7 +37,7 @@ async function analyzeImage(imagePath, isSticker = false, contentType = null) {
|
|
|
37
37
|
const imageBuffer = await fs.promises.readFile(imagePath);
|
|
38
38
|
const base64Image = imageBuffer.toString('base64');
|
|
39
39
|
|
|
40
|
-
const createImageMessage = (prompt, model = 'claude-
|
|
40
|
+
const createImageMessage = (prompt, model = 'claude-sonnet-4-5-20250929') => anthropicClient.messages.create({
|
|
41
41
|
model, max_tokens: 1024,
|
|
42
42
|
messages: [{ role: 'user', content: [
|
|
43
43
|
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64Image } },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
2
2
|
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
3
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
3
4
|
|
|
4
5
|
const { validateMedia } = require('../utils/mediaValidator');
|
|
5
6
|
const { logger } = require('../utils/logger');
|
|
@@ -120,6 +121,41 @@ async function processTwilioMediaMessage(twilioMessage, bucketName) {
|
|
|
120
121
|
return mediaItems;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
async function enrichMessageWithTwilioMedia(rawMessage) {
|
|
125
|
+
if (!rawMessage || rawMessage.__nexusMediaProcessed) return null;
|
|
126
|
+
|
|
127
|
+
const numMedia = parseInt(rawMessage.NumMedia || '0', 10);
|
|
128
|
+
if (numMedia <= 0 || !rawMessage.MediaUrl0) return null;
|
|
129
|
+
|
|
130
|
+
const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
131
|
+
if (!bucketName) {
|
|
132
|
+
logger.warn('[mediaEnrichment] AWS_S3_BUCKET_NAME not configured. Skipping media upload.');
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
|
|
137
|
+
if (!mediaItems || mediaItems.length === 0) {
|
|
138
|
+
logger.warn('[mediaEnrichment] Media processing returned no items');
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const [primary, ...rest] = mediaItems;
|
|
143
|
+
const mediaPayload = rest.length > 0
|
|
144
|
+
? { ...primary, metadata: { ...(primary.metadata || {}), attachments: rest } }
|
|
145
|
+
: primary;
|
|
146
|
+
|
|
147
|
+
rawMessage.__nexusMediaProcessed = true;
|
|
148
|
+
|
|
149
|
+
logger.info('[mediaEnrichment] Media processed successfully', {
|
|
150
|
+
primaryType: mediaPayload.mediaType,
|
|
151
|
+
mediaCount: mediaItems.length,
|
|
152
|
+
s3Key: mediaPayload.key
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { mediaPayload, caption: primary.caption };
|
|
156
|
+
}
|
|
157
|
+
|
|
123
158
|
module.exports = {
|
|
124
|
-
processTwilioMediaMessage
|
|
159
|
+
processTwilioMediaMessage,
|
|
160
|
+
enrichMessageWithTwilioMedia
|
|
125
161
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
declare module '@peopl-health/nexus' {
|
|
2
|
-
import mongoose from 'mongoose';
|
|
3
|
-
|
|
4
2
|
// Core Types
|
|
5
3
|
export interface MessageData {
|
|
6
4
|
id?: string;
|
|
@@ -178,7 +176,6 @@ declare module '@peopl-health/nexus' {
|
|
|
178
176
|
definition: typeof BaseAssistant | ((thread?: any) => any) | AssistantConfigDefinition
|
|
179
177
|
): any;
|
|
180
178
|
|
|
181
|
-
export function configureAssistantsLLM(client: any): void;
|
|
182
179
|
export function overrideGetAssistantById(resolver: (assistantId: string, thread?: any) => any): void;
|
|
183
180
|
export function configureAssistants(config: any): void;
|
|
184
181
|
export function setPreprocessingHandler(
|
|
@@ -231,23 +228,42 @@ declare module '@peopl-health/nexus' {
|
|
|
231
228
|
disconnect(): Promise<void>;
|
|
232
229
|
}
|
|
233
230
|
|
|
234
|
-
export class AssistantManager {
|
|
235
|
-
constructor(config?: AssistantConfig);
|
|
236
|
-
setLLMClient(llmClient: any): void;
|
|
237
|
-
registerAssistants(assistantConfigs: Record<string, string>): void;
|
|
238
|
-
setHandlers(handlers: AssistantConfig['handlers']): void;
|
|
239
|
-
createThread(code: string, assistantId: string, initialMessages?: string[]): Promise<ThreadData>;
|
|
240
|
-
sendMessage(threadData: ThreadData, message: string, runOptions?: any): Promise<string | null>;
|
|
241
|
-
submitToolOutputs(threadId: string, runId: string, toolOutputs: any[]): Promise<any>;
|
|
242
|
-
addInstruction(threadData: ThreadData, instruction: string): Promise<string | null>;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
231
|
export class MessageParser {
|
|
246
232
|
constructor(config?: ParserConfig);
|
|
247
233
|
parseMessage(rawMessage: any): MessageData;
|
|
248
234
|
updateConfig(newConfig: ParserConfig): void;
|
|
249
235
|
}
|
|
250
236
|
|
|
237
|
+
// LLM Providers
|
|
238
|
+
export class OpenAIAssistantsProvider {
|
|
239
|
+
constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string> });
|
|
240
|
+
getVariant(): string;
|
|
241
|
+
getClient(): any;
|
|
242
|
+
createConversation(options?: any): Promise<any>;
|
|
243
|
+
addMessage(options: { threadId: string; messages?: any[]; role?: string; content: string; metadata?: any }): Promise<any>;
|
|
244
|
+
listMessages(options?: any): Promise<any>;
|
|
245
|
+
getRunText(options?: any): Promise<string>;
|
|
246
|
+
executeRun(options: { thread: any; assistant: any; tools?: any[]; config?: any }): Promise<any>;
|
|
247
|
+
runConversation(options?: any): Promise<any>;
|
|
248
|
+
getRun(options: { threadId: string; runId: string }): Promise<any>;
|
|
249
|
+
submitToolOutputs(options: { threadId: string; runId: string; toolOutputs: any[] }): Promise<any>;
|
|
250
|
+
transcribeAudio(options?: any): Promise<any>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export class OpenAIResponsesProvider {
|
|
254
|
+
constructor(options?: { apiKey?: string; organization?: string; client?: any; defaultModels?: Record<string, string>; conversationManager?: any });
|
|
255
|
+
getVariant(): string;
|
|
256
|
+
getClient(): any;
|
|
257
|
+
createConversation(options?: any): Promise<any>;
|
|
258
|
+
addMessage(options: { threadId: string; messages?: any[]; role?: string; content: string; metadata?: any }): Promise<any>;
|
|
259
|
+
listMessages(options?: any): Promise<any>;
|
|
260
|
+
executeRun(options: { thread: any; assistant: any; message?: string; tools?: any[]; config?: any }): Promise<any>;
|
|
261
|
+
runConversation(config?: any): Promise<any>;
|
|
262
|
+
transcribeAudio(options?: any): Promise<any>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function createLLMProvider(config?: { variant?: string; [key: string]: any }): OpenAIAssistantsProvider | OpenAIResponsesProvider;
|
|
266
|
+
|
|
251
267
|
// Main Nexus Class
|
|
252
268
|
export interface NexusConfig {
|
|
253
269
|
messaging?: any;
|
|
@@ -270,18 +286,15 @@ declare module '@peopl-health/nexus' {
|
|
|
270
286
|
sendMessage(messageData: MessageData): Promise<any>;
|
|
271
287
|
sendScheduledMessage(scheduledMessage: ScheduledMessage): Promise<void>;
|
|
272
288
|
processMessage(rawMessage: any): Promise<any>;
|
|
273
|
-
createAssistantThread(code: string, assistantId: string, initialMessages?: string[]): Promise<ThreadData>;
|
|
274
|
-
sendToAssistant(code: string, message: string, runOptions?: any): Promise<string | null>;
|
|
275
289
|
isConnected(): boolean;
|
|
276
290
|
disconnect(): Promise<void>;
|
|
277
291
|
getMessaging(): NexusMessaging;
|
|
278
292
|
getStorage(): MongoStorage | null;
|
|
279
|
-
getAssistantManager(): AssistantManager | null;
|
|
280
293
|
getMessageParser(): MessageParser | null;
|
|
294
|
+
getLLMProvider(): OpenAIAssistantsProvider | OpenAIResponsesProvider | null;
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
// Utility Functions
|
|
284
|
-
export function createLogger(config?: any): any;
|
|
285
298
|
export function formatCode(codeBase: string): string;
|
|
286
299
|
export function calculateDelay(sendTime: string | Date, timeZone?: string): number;
|
|
287
300
|
export function ensureWhatsAppFormat(phoneNumber: any): string | null;
|
|
@@ -291,49 +304,17 @@ declare module '@peopl-health/nexus' {
|
|
|
291
304
|
export function extractTitle(message: any, mediaType: string): string | null;
|
|
292
305
|
export function useMongoDBAuthState(uri: string, dbName: string, sessionId: string): Promise<any>;
|
|
293
306
|
|
|
294
|
-
|
|
295
|
-
export
|
|
296
|
-
export
|
|
297
|
-
|
|
298
|
-
export function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
export const core: {
|
|
307
|
-
NexusMessaging: typeof NexusMessaging;
|
|
308
|
-
MessageProvider: typeof MessageProvider;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
export const storage: {
|
|
312
|
-
MongoStorage: typeof MongoStorage;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
export const utils: {
|
|
316
|
-
AssistantManager: typeof AssistantManager;
|
|
317
|
-
MessageParser: typeof MessageParser;
|
|
318
|
-
logger: any;
|
|
319
|
-
createLogger: typeof createLogger;
|
|
320
|
-
delay: typeof delay;
|
|
321
|
-
formatCode: typeof formatCode;
|
|
322
|
-
calculateDelay: typeof calculateDelay;
|
|
323
|
-
useMongoDBAuthState: typeof useMongoDBAuthState;
|
|
324
|
-
convertTwilioToInternalFormat: typeof convertTwilioToInternalFormat;
|
|
325
|
-
downloadMediaFromTwilio: typeof downloadMediaFromTwilio;
|
|
326
|
-
getMediaTypeFromContentType: typeof getMediaTypeFromContentType;
|
|
327
|
-
extractTitle: typeof extractTitle;
|
|
328
|
-
ensureWhatsAppFormat: typeof ensureWhatsAppFormat;
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
export const models: {
|
|
332
|
-
Message: typeof Message;
|
|
333
|
-
Thread: typeof Thread;
|
|
334
|
-
getMessageValues: typeof getMessageValues;
|
|
335
|
-
formatTimestamp: typeof formatTimestamp;
|
|
336
|
-
};
|
|
307
|
+
export const logger: any;
|
|
308
|
+
export function requestIdMiddleware(req: any, res: any, next: () => void): void;
|
|
309
|
+
export function getRequestId(): string | null;
|
|
310
|
+
|
|
311
|
+
export function setModelDatabases(mapping: Record<string, string>): void;
|
|
312
|
+
export function setModelDatabase(modelName: string, dbName: string): void;
|
|
313
|
+
export function getModelDatabase(modelName: string): string | null;
|
|
314
|
+
|
|
315
|
+
export const routes: any;
|
|
316
|
+
export function setupDefaultRoutes(app: any): void;
|
|
317
|
+
export function createRouter(routeDefinitions: Record<string, string>, controllers: Record<string, Function>): any;
|
|
337
318
|
|
|
338
319
|
// Queue Adapters
|
|
339
320
|
export interface QueueJobOptions {
|
package/lib/index.js
CHANGED
|
@@ -218,6 +218,7 @@ class Nexus {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
const routes = require('./routes');
|
|
221
|
+
const { resetAll } = require('./config/lifecycle');
|
|
221
222
|
|
|
222
223
|
module.exports = {
|
|
223
224
|
Nexus,
|
|
@@ -251,5 +252,7 @@ module.exports = {
|
|
|
251
252
|
LocalQueueAdapter,
|
|
252
253
|
RedisQueueAdapter,
|
|
253
254
|
createQueueAdapter,
|
|
254
|
-
registerQueueAdapter
|
|
255
|
+
registerQueueAdapter,
|
|
256
|
+
|
|
257
|
+
resetAll
|
|
255
258
|
};
|
|
@@ -14,6 +14,12 @@ const predictionMetricsSchema = new mongoose.Schema({
|
|
|
14
14
|
completed: { type: Boolean, default: true },
|
|
15
15
|
error: { type: String, default: null },
|
|
16
16
|
timing_breakdown: { type: Object, default: {} },
|
|
17
|
+
token_usage: {
|
|
18
|
+
input_tokens: { type: Number, default: 0 },
|
|
19
|
+
output_tokens: { type: Number, default: 0 },
|
|
20
|
+
total_tokens: { type: Number, default: 0 },
|
|
21
|
+
model: { type: String },
|
|
22
|
+
},
|
|
17
23
|
source: { type: String, default: () => process.env.USER_DB_MONGO }
|
|
18
24
|
}, { timestamps: true });
|
|
19
25
|
|
|
@@ -28,4 +34,22 @@ const getPredictionMetrics = () => {
|
|
|
28
34
|
return db.models.PredictionMetrics || db.model('PredictionMetrics', predictionMetricsSchema);
|
|
29
35
|
};
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
const getConversationTokenUsage = async (numero, since = null) => {
|
|
38
|
+
const Model = getPredictionMetrics();
|
|
39
|
+
const match = { numero, 'token_usage.total_tokens': { $gt: 0 } };
|
|
40
|
+
if (since) match.createdAt = { $gte: since };
|
|
41
|
+
|
|
42
|
+
const [result] = await Model.aggregate([
|
|
43
|
+
{ $match: match },
|
|
44
|
+
{ $group: {
|
|
45
|
+
_id: '$numero',
|
|
46
|
+
total_input_tokens: { $sum: '$token_usage.input_tokens' },
|
|
47
|
+
total_output_tokens: { $sum: '$token_usage.output_tokens' },
|
|
48
|
+
total_tokens: { $sum: '$token_usage.total_tokens' },
|
|
49
|
+
request_count: { $sum: 1 },
|
|
50
|
+
}}
|
|
51
|
+
]);
|
|
52
|
+
return result || { _id: numero, total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, request_count: 0 };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
module.exports = { getPredictionMetrics, getConversationTokenUsage, predictionMetricsSchema };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
3
|
+
|
|
4
|
+
const VARIANT = runtimeConfig.get('VARIANT', 'assistants');
|
|
2
5
|
|
|
3
6
|
const threadSchema = new mongoose.Schema({
|
|
4
7
|
code: { type: String, required: true },
|
|
@@ -21,18 +24,15 @@ threadSchema.index({ thread_id: 1 });
|
|
|
21
24
|
threadSchema.index({ conversation_id: 1 });
|
|
22
25
|
|
|
23
26
|
threadSchema.methods.getConversationId = function() {
|
|
24
|
-
|
|
25
|
-
return variant === 'assistants' ? this.thread_id : this.conversation_id;
|
|
27
|
+
return VARIANT === 'assistants' ? this.thread_id : this.conversation_id;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
threadSchema.methods.getAssistantId = function() {
|
|
29
|
-
|
|
30
|
-
return variant === 'assistants' ? this.assistant_id : this.prompt_id;
|
|
31
|
+
return VARIANT === 'assistants' ? this.assistant_id : this.prompt_id;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
threadSchema.statics.setAssistantId = function(threadObj, assistantId) {
|
|
34
|
-
|
|
35
|
-
if (variant === 'assistants') {
|
|
35
|
+
if (VARIANT === 'assistants') {
|
|
36
36
|
threadObj.assistant_id = assistantId;
|
|
37
37
|
} else {
|
|
38
38
|
threadObj.prompt_id = assistantId;
|
|
@@ -41,8 +41,7 @@ threadSchema.statics.setAssistantId = function(threadObj, assistantId) {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
threadSchema.statics.setConversationId = function(threadObj, conversationId) {
|
|
44
|
-
|
|
45
|
-
if (variant === 'assistants') {
|
|
44
|
+
if (VARIANT === 'assistants') {
|
|
46
45
|
threadObj.thread_id = conversationId;
|
|
47
46
|
} else {
|
|
48
47
|
threadObj.conversation_id = conversationId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { trace, metrics } = require('@opentelemetry/api');
|
|
2
2
|
|
|
3
|
-
const { initTelemetry, shutdownTelemetry } = require('../observability/telemetry');
|
|
3
|
+
const { initTelemetry, shutdownTelemetry, getMetricsRequestHandler } = require('../observability/telemetry');
|
|
4
4
|
|
|
5
5
|
const tracer = trace.getTracer('nexus-assistant');
|
|
6
6
|
const meter = metrics.getMeter('nexus-assistant');
|
|
@@ -149,6 +149,7 @@ function recordFileOperation(operationType, attributes = {}) {
|
|
|
149
149
|
module.exports = {
|
|
150
150
|
init,
|
|
151
151
|
shutdown: shutdownTelemetry,
|
|
152
|
+
getMetricsRequestHandler,
|
|
152
153
|
traceOperation,
|
|
153
154
|
createSpan,
|
|
154
155
|
tracer,
|
|
@@ -17,6 +17,7 @@ class TelemetryManager {
|
|
|
17
17
|
constructor() {
|
|
18
18
|
this.sdk = null;
|
|
19
19
|
this.isInitialized = false;
|
|
20
|
+
this.prometheusExporter = null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async init(config = {}) {
|
|
@@ -74,14 +75,18 @@ class TelemetryManager {
|
|
|
74
75
|
exportTimeoutMillis: isProd ? 5000 : 2000,
|
|
75
76
|
scheduledDelayMillis: isProd ? 2000 : 500,
|
|
76
77
|
}),
|
|
77
|
-
metricReader: new PrometheusExporter({
|
|
78
|
+
metricReader: this.prometheusExporter = new PrometheusExporter({
|
|
79
|
+
endpoint: prometheusEndpoint,
|
|
80
|
+
port: prometheusPort,
|
|
81
|
+
preventServerStart: true,
|
|
82
|
+
}),
|
|
78
83
|
});
|
|
79
84
|
|
|
80
85
|
await this.sdk.start();
|
|
81
86
|
this.isInitialized = true;
|
|
82
87
|
|
|
83
88
|
console.log(`🚀 OpenTelemetry initialized for "${serviceName}"`);
|
|
84
|
-
console.log(`📊 Metrics:
|
|
89
|
+
console.log(`📊 Metrics: available at ${prometheusEndpoint} (mounted on Express app)`);
|
|
85
90
|
console.log(`🔍 Traces: ${otlpEndpoint || jaegerEndpoint}`);
|
|
86
91
|
} catch (error) {
|
|
87
92
|
console.error('❌ Failed to initialize OpenTelemetry:', error.message);
|
|
@@ -109,5 +114,9 @@ const telemetryManager = new TelemetryManager();
|
|
|
109
114
|
module.exports = {
|
|
110
115
|
telemetryManager,
|
|
111
116
|
initTelemetry: (config) => telemetryManager.init(config),
|
|
112
|
-
shutdownTelemetry: () => telemetryManager.shutdown()
|
|
117
|
+
shutdownTelemetry: () => telemetryManager.shutdown(),
|
|
118
|
+
getMetricsRequestHandler: () => {
|
|
119
|
+
if (!telemetryManager.prometheusExporter) return null;
|
|
120
|
+
return (req, res) => telemetryManager.prometheusExporter.getMetricsRequestHandler(req, res);
|
|
121
|
+
}
|
|
113
122
|
};
|
|
@@ -305,11 +305,23 @@ class OpenAIResponsesProvider {
|
|
|
305
305
|
|
|
306
306
|
let totalRetries = 0;
|
|
307
307
|
let allToolsExecuted = [];
|
|
308
|
+
const accumulatedUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
309
|
+
const addUsage = (usage) => {
|
|
310
|
+
if (!usage) return;
|
|
311
|
+
accumulatedUsage.input_tokens += usage.input_tokens || 0;
|
|
312
|
+
accumulatedUsage.output_tokens += usage.output_tokens || 0;
|
|
313
|
+
accumulatedUsage.total_tokens += usage.total_tokens || 0;
|
|
314
|
+
};
|
|
308
315
|
|
|
309
316
|
const devRecord = await getRecordByFilter(Config_ID, 'responses', `{prompt_id} = "${assistantId}"`);
|
|
310
317
|
let devContent = devRecord?.[0]?.content || '';
|
|
311
318
|
if (promptVariables) devContent = devContent.replace(/\{\{(\w+)\}\}/g, (_, key) => promptVariables[key] ?? '');
|
|
312
319
|
|
|
320
|
+
if (assistant?.tools?.size) {
|
|
321
|
+
const toolNames = Array.from(assistant.tools.keys()).join(', ');
|
|
322
|
+
devContent += `\n\nYou only have access to these tools: ${toolNames}. Do not call or reference any tools not listed here.`;
|
|
323
|
+
}
|
|
324
|
+
|
|
313
325
|
const messages = (context || this._convertItemsToApiFormat(additionalMessages))
|
|
314
326
|
.filter(item => item.type !== 'function_call' && item.type !== 'function_call_output');
|
|
315
327
|
const input = [{ role: 'developer', content: devContent }, ...messages];
|
|
@@ -329,6 +341,7 @@ class OpenAIResponsesProvider {
|
|
|
329
341
|
|
|
330
342
|
const { result: response, retries } = await makeAPICall(input);
|
|
331
343
|
totalRetries += retries;
|
|
344
|
+
addUsage(response.usage);
|
|
332
345
|
let finalResponse = response;
|
|
333
346
|
|
|
334
347
|
if (assistant && response.output) {
|
|
@@ -349,6 +362,7 @@ class OpenAIResponsesProvider {
|
|
|
349
362
|
|
|
350
363
|
const { result: followUp, retries: followUpRetries } = await makeAPICall(currentInput);
|
|
351
364
|
totalRetries += followUpRetries;
|
|
365
|
+
addUsage(followUp.usage);
|
|
352
366
|
finalResponse = followUp;
|
|
353
367
|
}
|
|
354
368
|
}
|
|
@@ -360,6 +374,7 @@ class OpenAIResponsesProvider {
|
|
|
360
374
|
object: finalResponse.object || 'response',
|
|
361
375
|
tools_executed: allToolsExecuted,
|
|
362
376
|
retries: totalRetries,
|
|
377
|
+
usage: accumulatedUsage,
|
|
363
378
|
};
|
|
364
379
|
}
|
|
365
380
|
|
|
@@ -4,7 +4,7 @@ async function executeFunctionCall(assistant, call) {
|
|
|
4
4
|
const startTime = Date.now();
|
|
5
5
|
const name = call.name;
|
|
6
6
|
const args = call.arguments ? JSON.parse(call.arguments) : {};
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
logger.info('[executeFunctionCall] Calling', { name, call_id: call.call_id });
|
|
9
9
|
|
|
10
10
|
let result, success = true;
|
package/lib/routes/index.js
CHANGED
|
@@ -176,6 +176,12 @@ const builtInControllers = {
|
|
|
176
176
|
};
|
|
177
177
|
|
|
178
178
|
const setupDefaultRoutes = (app) => {
|
|
179
|
+
const { getMetricsRequestHandler } = require('../observability');
|
|
180
|
+
const metricsHandler = getMetricsRequestHandler();
|
|
181
|
+
if (metricsHandler) {
|
|
182
|
+
app.get('/metrics', metricsHandler);
|
|
183
|
+
}
|
|
184
|
+
|
|
179
185
|
app.use('/api/assistant', createRouter(assistantRouteDefinitions, builtInControllers));
|
|
180
186
|
app.use('/api/conversation', createRouter(conversationRouteDefinitions, builtInControllers));
|
|
181
187
|
app.use('/api/interaction', createRouter(interactionRouteDefinitions, builtInControllers));
|
|
@@ -98,9 +98,16 @@ const getAssistantById = (assistant_id, thread) => {
|
|
|
98
98
|
throw new Error(`Assistant with ID "${assistant_id}" not found`);
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
+
const _resetRegistry = () => {
|
|
102
|
+
assistantConfig = null;
|
|
103
|
+
assistantRegistry = {};
|
|
104
|
+
customGetAssistantById = null;
|
|
105
|
+
};
|
|
106
|
+
|
|
101
107
|
module.exports = {
|
|
102
108
|
getAssistantById,
|
|
103
109
|
configureAssistants,
|
|
104
110
|
registerAssistant,
|
|
105
|
-
overrideGetAssistantById
|
|
111
|
+
overrideGetAssistantById,
|
|
112
|
+
_resetRegistry
|
|
106
113
|
};
|
|
@@ -242,6 +242,8 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
242
242
|
const { output: rawOutput, completed, retries, predictionTimeMs, tools_executed } = runResult;
|
|
243
243
|
const prompt = runResult.run?.prompt || null;
|
|
244
244
|
const response_id = runResult.run?.id || null;
|
|
245
|
+
const usage = runResult.run?.usage || null;
|
|
246
|
+
const model = runResult.run?.model || null;
|
|
245
247
|
|
|
246
248
|
const output = sanitizeOutput(rawOutput);
|
|
247
249
|
if (rawOutput !== output) {
|
|
@@ -259,15 +261,28 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
259
261
|
hasMedia: urls.length > 0,
|
|
260
262
|
retries,
|
|
261
263
|
totalMs: timings.total_ms,
|
|
262
|
-
toolsExecuted: tools_executed?.length || 0
|
|
264
|
+
toolsExecuted: tools_executed?.length || 0,
|
|
265
|
+
token_usage: usage ? {
|
|
266
|
+
input_tokens: usage.input_tokens,
|
|
267
|
+
output_tokens: usage.output_tokens,
|
|
268
|
+
total_tokens: usage.total_tokens,
|
|
269
|
+
model,
|
|
270
|
+
} : undefined,
|
|
263
271
|
});
|
|
264
272
|
|
|
265
273
|
if (output && predictionTimeMs) {
|
|
266
|
-
logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
|
|
274
|
+
logger.debug('[replyAssistant] Storing metrics with timing_breakdown', {
|
|
267
275
|
timing_breakdown: timings,
|
|
268
276
|
has_breakdown: !!timings.process_messages_breakdown
|
|
269
277
|
});
|
|
270
|
-
|
|
278
|
+
|
|
279
|
+
const tokenUsage = usage ? {
|
|
280
|
+
input_tokens: usage.input_tokens || 0,
|
|
281
|
+
output_tokens: usage.output_tokens || 0,
|
|
282
|
+
total_tokens: usage.total_tokens || 0,
|
|
283
|
+
model: model || undefined,
|
|
284
|
+
} : undefined;
|
|
285
|
+
|
|
271
286
|
await getPredictionMetrics().create({
|
|
272
287
|
message_id: `${code}-${Date.now()}`,
|
|
273
288
|
numero: code,
|
|
@@ -275,8 +290,20 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
275
290
|
prediction_time_ms: predictionTimeMs,
|
|
276
291
|
retry_count: retries,
|
|
277
292
|
completed: completed,
|
|
278
|
-
timing_breakdown: timings
|
|
293
|
+
timing_breakdown: timings,
|
|
294
|
+
token_usage: tokenUsage,
|
|
279
295
|
}).catch(err => logger.error('[replyAssistant] Failed to store metrics', { error: err.message }));
|
|
296
|
+
|
|
297
|
+
const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
|
|
298
|
+
if (alertThreshold && usage?.total_tokens > alertThreshold) {
|
|
299
|
+
logger.warn('[replyAssistant] Token usage spike detected', {
|
|
300
|
+
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
301
|
+
total_tokens: usage.total_tokens,
|
|
302
|
+
threshold: alertThreshold,
|
|
303
|
+
model,
|
|
304
|
+
assistant_id: thread.getAssistantId(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
280
307
|
}
|
|
281
308
|
|
|
282
309
|
return { output, tools_executed, prompt, response_id };
|
|
@@ -7,7 +7,7 @@ const { Message, insertMessage } = require('../models/messageModel');
|
|
|
7
7
|
const { Thread } = require('../models/threadModel');
|
|
8
8
|
|
|
9
9
|
const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
|
|
10
|
-
const {
|
|
10
|
+
const { enrichMessageWithTwilioMedia } = require('../helpers/twilioMediaHelper');
|
|
11
11
|
|
|
12
12
|
class MongoStorage {
|
|
13
13
|
constructor(config) {
|
|
@@ -46,53 +46,22 @@ class MongoStorage {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async _enrichTwilioMedia(messageData = {}) {
|
|
49
|
-
logger.debug('[_enrichTwilioMedia] Input', { messageData });
|
|
50
49
|
try {
|
|
51
50
|
const rawMessage = messageData?.raw;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const bucketName = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
61
|
-
if (!bucketName) {
|
|
62
|
-
logger.warn('[MongoStorage] AWS_S3_BUCKET_NAME not configured. Skipping media upload.');
|
|
63
|
-
return messageData;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const mediaItems = await processTwilioMediaMessage(rawMessage, bucketName);
|
|
67
|
-
if (!mediaItems || mediaItems.length === 0) {
|
|
68
|
-
logger.warn('[MongoStorage] Media processing returned no items');
|
|
69
|
-
return messageData;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const [primary, ...rest] = mediaItems;
|
|
73
|
-
const mediaPayload = rest.length > 0
|
|
74
|
-
? { ...primary, metadata: { ...(primary.metadata || {}), attachments: rest } }
|
|
75
|
-
: primary;
|
|
76
|
-
|
|
77
|
-
rawMessage.__nexusMediaProcessed = true;
|
|
78
|
-
|
|
79
|
-
logger.info('[MongoStorage] Media processed successfully', {
|
|
80
|
-
primaryType: mediaPayload.mediaType,
|
|
81
|
-
mediaCount: mediaItems.length,
|
|
82
|
-
s3Key: mediaPayload.key
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const result = {
|
|
51
|
+
if (!rawMessage?.From) return messageData;
|
|
52
|
+
|
|
53
|
+
const result = await enrichMessageWithTwilioMedia(rawMessage);
|
|
54
|
+
if (!result) return messageData;
|
|
55
|
+
|
|
56
|
+
return {
|
|
86
57
|
...messageData,
|
|
87
|
-
media: mediaPayload,
|
|
58
|
+
media: result.mediaPayload,
|
|
88
59
|
fileUrl: undefined,
|
|
89
|
-
fileType: mediaPayload.mediaType,
|
|
60
|
+
fileType: result.mediaPayload.mediaType,
|
|
90
61
|
isMedia: true,
|
|
91
|
-
body: messageData.body ||
|
|
92
|
-
caption:
|
|
62
|
+
body: messageData.body || result.caption,
|
|
63
|
+
caption: result.caption
|
|
93
64
|
};
|
|
94
|
-
logger.debug('[_enrichTwilioMedia] Output', { result });
|
|
95
|
-
return result;
|
|
96
65
|
} catch (error) {
|
|
97
66
|
logger.error('[MongoStorage] Failed to enrich Twilio media message', { error });
|
|
98
67
|
return messageData;
|
|
@@ -100,7 +69,6 @@ class MongoStorage {
|
|
|
100
69
|
}
|
|
101
70
|
|
|
102
71
|
buildMessageValues(messageData = {}) {
|
|
103
|
-
logger.debug('[buildMessageValues] Input', { messageData });
|
|
104
72
|
const now = new Date();
|
|
105
73
|
const numero = ensureWhatsAppFormat(messageData.code || messageData.from || '');
|
|
106
74
|
const isMedia = messageData.isMedia === true || (messageData.fileType && messageData.fileType !== 'text');
|
|
@@ -128,7 +96,6 @@ class MongoStorage {
|
|
|
128
96
|
response_id: messageData.response_id || null,
|
|
129
97
|
statusInfo: messageData.statusInfo || null
|
|
130
98
|
};
|
|
131
|
-
logger.debug('[buildMessageValues] Output', { values });
|
|
132
99
|
return values;
|
|
133
100
|
}
|
|
134
101
|
|