@peopl-health/nexus 4.5.26 → 4.5.28
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/MessageAdapter.js +0 -6
- package/lib/controllers/conversationController.js +11 -2
- package/lib/controllers/interactionController.js +1 -1
- package/lib/controllers/templateController.js +0 -2
- package/lib/core/MessageParser.js +5 -12
- package/lib/core/NexusMessaging.js +3 -10
- package/lib/core/PhiProcessor.js +9 -7
- package/lib/helpers/messageHelper.js +3 -1
- package/lib/index.d.ts +1 -2
- package/lib/models/messageModel.js +1 -0
- package/lib/models/templateModel.js +0 -1
- package/lib/providers/BaseLLMProvider.js +275 -0
- package/lib/providers/OpenAIResponsesProvider.js +3 -239
- package/lib/services/messageService.js +2 -2
- package/lib/services/templateService.js +0 -1
- package/lib/storage/MongoStorage.js +2 -1
- package/package.json +1 -1
|
@@ -15,12 +15,6 @@ class MessageAdapter {
|
|
|
15
15
|
case 'list':
|
|
16
16
|
return interactive.title || interactive.description || '[List item selected]';
|
|
17
17
|
case 'flow':
|
|
18
|
-
if (interactive.data) {
|
|
19
|
-
const flowData = typeof interactive.data === 'string'
|
|
20
|
-
? interactive.data
|
|
21
|
-
: JSON.stringify(interactive.data, null, 2);
|
|
22
|
-
return `Flow Response:\n${flowData}`;
|
|
23
|
-
}
|
|
24
18
|
return '[Flow response]';
|
|
25
19
|
default:
|
|
26
20
|
return '[Interactive message]';
|
|
@@ -91,6 +91,7 @@ const getConversationMessagesController = async (req, res) => {
|
|
|
91
91
|
|
|
92
92
|
const total = await countMessages(query);
|
|
93
93
|
const messages = await getMessages(query, { sort: { createdAt: -1 }, skip, limit });
|
|
94
|
+
messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
|
|
94
95
|
const totalPages = Math.ceil(total / limit);
|
|
95
96
|
|
|
96
97
|
res.status(200).json({
|
|
@@ -207,7 +208,10 @@ const searchConversationsController = async (req, res) => {
|
|
|
207
208
|
secondary: [
|
|
208
209
|
{ $match: {
|
|
209
210
|
group_id: null,
|
|
210
|
-
|
|
211
|
+
$or: [
|
|
212
|
+
{ plainBody: { $regex: escapedQuery, $options: 'i' } },
|
|
213
|
+
{ body: { $regex: escapedQuery, $options: 'i' } }
|
|
214
|
+
]
|
|
211
215
|
}},
|
|
212
216
|
{ $group: { _id: '$numero' } }
|
|
213
217
|
]
|
|
@@ -400,6 +404,7 @@ const getNewMessagesController = async (req, res) => {
|
|
|
400
404
|
const raw = parseInt(req.query.limit, 10);
|
|
401
405
|
const clampedLimit = Number.isFinite(raw) ? Math.min(Math.max(raw, 1), 100) : 20;
|
|
402
406
|
const messages = await getMessages(query, { sort: { createdAt: 1 }, limit: clampedLimit });
|
|
407
|
+
messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
|
|
403
408
|
|
|
404
409
|
res.status(200).json({
|
|
405
410
|
success: true,
|
|
@@ -554,13 +559,17 @@ const searchMessagesByNumberController = async (req, res) => {
|
|
|
554
559
|
const mongoQuery = {
|
|
555
560
|
group_id: null,
|
|
556
561
|
numero: formattedPhoneNumber,
|
|
557
|
-
|
|
562
|
+
$or: [
|
|
563
|
+
{ plainBody: { $regex: escapedQuery, $options: 'i' } },
|
|
564
|
+
{ body: { $regex: escapedQuery, $options: 'i' } }
|
|
565
|
+
]
|
|
558
566
|
};
|
|
559
567
|
|
|
560
568
|
const mongoSort = { createdAt: -1 };
|
|
561
569
|
|
|
562
570
|
const total = await countMessages(mongoQuery);
|
|
563
571
|
const messages = await getMessages(mongoQuery, { sort: mongoSort, skip, limit });
|
|
572
|
+
messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
|
|
564
573
|
const totalPages = Math.ceil(total / limit);
|
|
565
574
|
|
|
566
575
|
res.status(200).json({
|
|
@@ -15,7 +15,7 @@ async function logInteractionToAirtable(messageIds, whatsapp_id, reporter, quali
|
|
|
15
15
|
const conversation = messageObjects.map(msg => {
|
|
16
16
|
const timestamp = msg.createdAt.toISOString().slice(0, 16).replace('T', ' ');
|
|
17
17
|
const role = msg.from_me ? 'Assistant' : 'Patient';
|
|
18
|
-
return `[${timestamp}] ${role}: ${msg.body || '(media)'}`;
|
|
18
|
+
return `[${timestamp}] ${role}: ${msg.plainBody || msg.body || '(media)'}`;
|
|
19
19
|
}).join('\n');
|
|
20
20
|
|
|
21
21
|
let patientId = null;
|
|
@@ -232,7 +232,6 @@ const submitForApproval = async (req, res) => {
|
|
|
232
232
|
status: 'PENDING',
|
|
233
233
|
approvalRequest: {
|
|
234
234
|
sid: response.sid,
|
|
235
|
-
status: response.status || 'PENDING',
|
|
236
235
|
dateSubmitted: validSubmittedDate,
|
|
237
236
|
dateUpdated: validUpdatedDate,
|
|
238
237
|
rejectionReason: response.rejection_reason || ''
|
|
@@ -282,7 +281,6 @@ const checkApprovalStatus = async (req, res) => {
|
|
|
282
281
|
);
|
|
283
282
|
dbTemplate.approvalRequest = {
|
|
284
283
|
sid: status.approvalRequest.sid,
|
|
285
|
-
status: status.approvalRequest.status,
|
|
286
284
|
dateSubmitted,
|
|
287
285
|
dateUpdated: parseDate(
|
|
288
286
|
status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated,
|
|
@@ -8,17 +8,20 @@ class MessageParser {
|
|
|
8
8
|
this.config = config;
|
|
9
9
|
this.commandPrefixes = config.commandPrefixes || ['/', '!'];
|
|
10
10
|
this.keywords = config.keywords || [];
|
|
11
|
-
this.flowTriggers = config.flowTriggers || [];
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
async parseMessage(normalizedMessage) {
|
|
15
14
|
const {media, interactive, ...base} = normalizedMessage;
|
|
16
15
|
await parsePatientInformation(base);
|
|
17
16
|
|
|
18
|
-
if (interactive) {
|
|
17
|
+
if (interactive && interactive.type !== 'flow') {
|
|
19
18
|
return {...base, type: 'interactive', interactive, isInteractive: true};
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
if (interactive && interactive.type === 'flow') {
|
|
22
|
+
return {...base, type: 'flow', flow: interactive, interactive};
|
|
23
|
+
}
|
|
24
|
+
|
|
22
25
|
if (media) {
|
|
23
26
|
return {...base, type: 'media', media, isMedia: true};
|
|
24
27
|
}
|
|
@@ -38,11 +41,6 @@ class MessageParser {
|
|
|
38
41
|
return {...base, type: 'keyword', keyword};
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
const flow = this.findFlowTrigger(body);
|
|
42
|
-
if (flow) {
|
|
43
|
-
return {...base, type: 'flow', flow};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
44
|
return {...base, type: 'message'};
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -76,15 +74,10 @@ class MessageParser {
|
|
|
76
74
|
return this._findMatch(text, this.keywords);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
findFlowTrigger(text) {
|
|
80
|
-
return this._findMatch(text, this.flowTriggers);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
77
|
updateConfig(newConfig) {
|
|
84
78
|
this.config = { ...this.config, ...newConfig };
|
|
85
79
|
this.commandPrefixes = this.config.commandPrefixes || this.commandPrefixes;
|
|
86
80
|
this.keywords = this.config.keywords || this.keywords;
|
|
87
|
-
this.flowTriggers = this.config.flowTriggers || this.flowTriggers;
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
83
|
|
|
@@ -380,10 +380,10 @@ class NexusMessaging {
|
|
|
380
380
|
if (stop) return;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
if (messageData.flow) return this.handleFlow(messageData);
|
|
383
384
|
if (messageData.interactive) return this.handleInteractive(messageData);
|
|
384
385
|
if (messageData.command) return this.handleCommand(messageData);
|
|
385
386
|
if (messageData.keyword) return this.handleKeyword(messageData);
|
|
386
|
-
if (messageData.flow) return this.handleFlow(messageData);
|
|
387
387
|
|
|
388
388
|
if (this.batchingConfig.enabled && chatId) return this._handleWithCheckAfter(chatId);
|
|
389
389
|
return messageData.media ? this.handleMedia(messageData) : this.handleMessage(messageData);
|
|
@@ -415,6 +415,8 @@ class NexusMessaging {
|
|
|
415
415
|
|
|
416
416
|
async handleFlow(messageData) {
|
|
417
417
|
if (this.handlers.onFlow) return await this.handlers.onFlow(messageData, this);
|
|
418
|
+
// Backward compatibility for consumers who don't have onFlow handler
|
|
419
|
+
if (this.handlers.onInteractive) return await this.handlers.onInteractive(messageData, this);
|
|
418
420
|
}
|
|
419
421
|
|
|
420
422
|
/*
|
|
@@ -518,7 +520,6 @@ class NexusMessaging {
|
|
|
518
520
|
}
|
|
519
521
|
|
|
520
522
|
async processInstruction(code, instruction, role = 'developer', { triggeredBy } = {}) {
|
|
521
|
-
const assistantId = await this._getThreadAssistantId(code);
|
|
522
523
|
const messageData = {
|
|
523
524
|
pushName: 'Instruction',
|
|
524
525
|
code,
|
|
@@ -527,7 +528,6 @@ class NexusMessaging {
|
|
|
527
528
|
fromMe: true,
|
|
528
529
|
processed: true,
|
|
529
530
|
origin: 'instruction',
|
|
530
|
-
assistantId,
|
|
531
531
|
raw: { role },
|
|
532
532
|
triggeredBy: triggeredBy || null,
|
|
533
533
|
silent: true,
|
|
@@ -556,7 +556,6 @@ class NexusMessaging {
|
|
|
556
556
|
const thread = await Thread.findOne({ code }).lean();
|
|
557
557
|
if (!thread) return null;
|
|
558
558
|
|
|
559
|
-
const assistantId = await this._getThreadAssistantId(code);
|
|
560
559
|
const normalizedMessages = Array.isArray(messages) ? messages : [messages];
|
|
561
560
|
|
|
562
561
|
for (let i = 0; i < normalizedMessages.length; i++) {
|
|
@@ -569,7 +568,6 @@ class NexusMessaging {
|
|
|
569
568
|
fromMe: true,
|
|
570
569
|
processed: true,
|
|
571
570
|
origin: 'system',
|
|
572
|
-
assistantId,
|
|
573
571
|
raw: { role },
|
|
574
572
|
triggeredBy: triggeredBy || null,
|
|
575
573
|
silent: true,
|
|
@@ -597,11 +595,6 @@ class NexusMessaging {
|
|
|
597
595
|
return result?.output || null;
|
|
598
596
|
}
|
|
599
597
|
|
|
600
|
-
async _getThreadAssistantId(code) {
|
|
601
|
-
const thread = await Thread.findOne({ code }).select('assistant_id prompt_id').lean();
|
|
602
|
-
return thread?.prompt_id || thread?.assistant_id || null;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
598
|
async _processMessages(chatId, processingFn, shouldFinalize = () => true) {
|
|
606
599
|
const query = { numero: chatId, from_me: false, processed: false };
|
|
607
600
|
const unprocessed = await getMessages(query, { select: '_id' });
|
package/lib/core/PhiProcessor.js
CHANGED
|
@@ -69,16 +69,18 @@ class PhiProcessor {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async inData(messageData, saveFn, handler) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
72
|
+
const decodedBody = messageData.body;
|
|
73
|
+
const encodedBody = await this.encodeBody(decodedBody, messageData.code);
|
|
74
|
+
const saved = await saveFn({ ...messageData, body: encodedBody, plainBody: decodedBody });
|
|
75
|
+
return handler({ body: encodedBody, _id: saved?._id });
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
async outDataFromPlain(messageData, saveFn, handler, { onSaved } = {}) {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
79
|
+
const decodedBody = messageData.body;
|
|
80
|
+
const encodedBody = await this.encodeBody(decodedBody, messageData.code);
|
|
81
|
+
const saved = await saveFn({ ...messageData, body: encodedBody, plainBody: decodedBody });
|
|
80
82
|
await onSaved?.(saved?._id);
|
|
81
|
-
return handler({ body:
|
|
83
|
+
return handler({ body: decodedBody, _id: saved?._id });
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
async decodeBody(body, numero) {
|
|
@@ -100,10 +102,10 @@ class PhiProcessor {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async outDataFromEncoded(messageData, saveFn, handler) {
|
|
103
|
-
const saved = await saveFn(messageData);
|
|
104
105
|
const decodedBody = this._phiEnabled
|
|
105
106
|
? await this.decodeBody(messageData.body, messageData.code)
|
|
106
107
|
: messageData.body;
|
|
108
|
+
const saved = await saveFn({ ...messageData, plainBody: decodedBody });
|
|
107
109
|
return handler({ body: decodedBody, _id: saved?._id });
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -90,7 +90,9 @@ async function getLastNMessages(code, n, anchor = null, opts = {}) {
|
|
|
90
90
|
numero: code,
|
|
91
91
|
...(anchor ? { createdAt: { [beforeOperator]: anchor } } : {}),
|
|
92
92
|
};
|
|
93
|
-
|
|
93
|
+
const messages = await getMessages(query, { sort: { createdAt: -1 }, limit: n });
|
|
94
|
+
messages.forEach(msg => { msg.body = msg.plainBody || msg.body; });
|
|
95
|
+
return messages;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
module.exports = {
|
package/lib/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ declare module '@peopl-health/nexus' {
|
|
|
13
13
|
media?: MediaData;
|
|
14
14
|
command?: CommandData;
|
|
15
15
|
keyword?: string;
|
|
16
|
-
flow?:
|
|
16
|
+
flow?: InteractiveData;
|
|
17
17
|
type?: 'message' | 'interactive' | 'media' | 'command' | 'keyword' | 'flow';
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -93,7 +93,6 @@ declare module '@peopl-health/nexus' {
|
|
|
93
93
|
export interface ParserConfig {
|
|
94
94
|
commandPrefixes?: string[];
|
|
95
95
|
keywords?: (string | { pattern: string; flags?: string })[];
|
|
96
|
-
flowTriggers?: (string | { pattern: string; flags?: string })[];
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
// Handler Types
|
|
@@ -5,6 +5,7 @@ const { DELIVERY_ATTEMPT_STATUSES } = require('./deliveryAttemptModel');
|
|
|
5
5
|
const messageSchema = new mongoose.Schema({
|
|
6
6
|
raw: { type: Object, default: null },
|
|
7
7
|
body: { type: String, default: '' },
|
|
8
|
+
plainBody: { type: String, default: null },
|
|
8
9
|
numero: { type: String, required: true },
|
|
9
10
|
nombre_whatsapp: { type: String, default: null },
|
|
10
11
|
message_id: { type: String, default: null},
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
2
|
+
const { logger } = require('../utils/logger');
|
|
3
|
+
const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
|
|
4
|
+
const { isJsonBlob } = require('../utils/formatUtils');
|
|
5
|
+
|
|
6
|
+
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
7
|
+
|
|
8
|
+
const { getMessages } = require('../services/messageService');
|
|
9
|
+
|
|
10
|
+
const { logBugReportToAirtable } = require('../controllers/bugReportController');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_CONVERSATION_RETRIES = 3;
|
|
13
|
+
|
|
14
|
+
class BaseLLMProvider {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
if (new.target === BaseLLMProvider) {
|
|
17
|
+
throw new Error('BaseLLMProvider is abstract and cannot be instantiated directly');
|
|
18
|
+
}
|
|
19
|
+
const { conversationManager, sessionManager, maxConversationRetries } = options;
|
|
20
|
+
this.client = null;
|
|
21
|
+
this.variant = null;
|
|
22
|
+
this.conversationManager = conversationManager || new DefaultMemoryManager();
|
|
23
|
+
this.sessionManager = sessionManager || null;
|
|
24
|
+
const retries = parseInt(maxConversationRetries, 10);
|
|
25
|
+
this.maxConversationRetries = retries > 0 ? retries : DEFAULT_MAX_CONVERSATION_RETRIES;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getVariant() { return this.variant; }
|
|
29
|
+
getClient() { return this.client; }
|
|
30
|
+
|
|
31
|
+
async _executeConversation() {
|
|
32
|
+
throw new Error(`${this.constructor.name} must implement _executeConversation()`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async createConversation() {
|
|
36
|
+
throw new Error(`createConversation is not supported by ${this.constructor.name}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async addMessage() {
|
|
40
|
+
throw new Error(`addMessage is not supported by ${this.constructor.name}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listMessages() {
|
|
44
|
+
throw new Error(`listMessages is not supported by ${this.constructor.name}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async transcribeAudio() {
|
|
48
|
+
throw new Error(`transcribeAudio is not supported by ${this.constructor.name}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async executeRun({ thread, assistant, message = null, tools = [], config = {} }) {
|
|
52
|
+
const { conversationId, assistantId } = this._normalizeThread(thread);
|
|
53
|
+
const promptVersion = thread?.version || null;
|
|
54
|
+
const presetId = thread?.preset_id || null;
|
|
55
|
+
const presetVersion = thread?.preset_version || null;
|
|
56
|
+
|
|
57
|
+
logger.info('[executeRun] Starting', { conversationId, assistantId, promptVersion });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const context = await this.conversationManager.buildContext({
|
|
61
|
+
thread,
|
|
62
|
+
message,
|
|
63
|
+
config: {
|
|
64
|
+
...config,
|
|
65
|
+
threadId: conversationId,
|
|
66
|
+
assistantId,
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const [lastMessage] = await getMessages({ numero: thread.code }, { sort: { createdAt: -1 }, limit: 1 });
|
|
71
|
+
const phiProcessor = config.phiProcessor;
|
|
72
|
+
const maskedCode = phiProcessor ? phiProcessor.hashCode(thread.code) : thread.code;
|
|
73
|
+
const metadata = {
|
|
74
|
+
numero: maskedCode,
|
|
75
|
+
message_id: message?.message_id || lastMessage?.message_id || null
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
logger.info('[executeRun] Context built', { conversationId, messageCount: context?.length || 0 });
|
|
79
|
+
|
|
80
|
+
const override = config.clinicalData;
|
|
81
|
+
const hasOverride = override && typeof override === 'object' && Object.keys(override).length > 0;
|
|
82
|
+
const clinicalData = hasOverride ? override : await this.conversationManager.getClinicalData(thread.code);
|
|
83
|
+
const promptVariables = {
|
|
84
|
+
clinical_context: clinicalData?.clinicalContext ?? '',
|
|
85
|
+
last_symptoms: clinicalData?.lastSymptoms ?? '',
|
|
86
|
+
current_date: getCurrentMexicoDateTime(),
|
|
87
|
+
patient_memories: clinicalData?.patientMemories ?? '',
|
|
88
|
+
conversation_summaries: clinicalData?.conversationSummaries ?? '',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = await this.runConversation({
|
|
92
|
+
threadId: conversationId,
|
|
93
|
+
assistantId,
|
|
94
|
+
presetId,
|
|
95
|
+
presetVersion,
|
|
96
|
+
tools,
|
|
97
|
+
context,
|
|
98
|
+
promptVariables,
|
|
99
|
+
promptVersion,
|
|
100
|
+
assistant,
|
|
101
|
+
metadata,
|
|
102
|
+
...config
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await this.conversationManager.processResponse(result, thread, config);
|
|
106
|
+
this.sessionManager?.recordActivity(thread.code);
|
|
107
|
+
|
|
108
|
+
const completed = result.status === 'completed';
|
|
109
|
+
const output = result.output_text || this._extractMessageOutput(result);
|
|
110
|
+
const toolsExecuted = result.tools_executed?.length || 0;
|
|
111
|
+
|
|
112
|
+
logger.info('[executeRun] Complete', {
|
|
113
|
+
runId: result.id,
|
|
114
|
+
completed,
|
|
115
|
+
outputLength: output?.length || 0,
|
|
116
|
+
toolsExecuted
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return { run: result, completed, output, tools_executed: result.tools_executed || [], retries: result.retries || 0 };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.error('[executeRun] Failed', { conversationId, assistantId, error: error.message });
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async runConversation(config = {}) {
|
|
127
|
+
const { threadId, assistantId } = config;
|
|
128
|
+
const maxRetries = this.maxConversationRetries;
|
|
129
|
+
|
|
130
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
131
|
+
try {
|
|
132
|
+
logger.info('[runConversation] Attempt', { attempt, maxRetries, threadId, assistantId });
|
|
133
|
+
|
|
134
|
+
const result = await this._executeConversation(config);
|
|
135
|
+
const extractedOutput = this._extractMessageOutput(result);
|
|
136
|
+
|
|
137
|
+
if (extractedOutput?.trim()) {
|
|
138
|
+
result.output_text = extractedOutput;
|
|
139
|
+
logger.info('[runConversation] Success', {
|
|
140
|
+
attempt,
|
|
141
|
+
outputLength: extractedOutput.length,
|
|
142
|
+
toolsExecuted: result.tools_executed?.length || 0
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
logger.warn('[runConversation] Empty output', { attempt });
|
|
148
|
+
if (attempt === maxRetries) {
|
|
149
|
+
throw new Error(`Conversation failed after ${attempt} attempts - no valid output`);
|
|
150
|
+
}
|
|
151
|
+
await new Promise(r => setTimeout(r, 500));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('[runConversation] Attempt failed', { attempt, error: error.message });
|
|
154
|
+
if (attempt === maxRetries) throw error;
|
|
155
|
+
await new Promise(r => setTimeout(r, 500));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_normalizeThread(thread) {
|
|
161
|
+
return {
|
|
162
|
+
conversationId: thread.conversation_id || thread.getConversationId?.(),
|
|
163
|
+
assistantId: thread.prompt_id || thread.getAssistantId?.()
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_convertItemsToApiFormat(items) {
|
|
168
|
+
return items.map(item => {
|
|
169
|
+
const type = item.type || 'message';
|
|
170
|
+
if (type === 'function_call' || type === 'function_call_output') {
|
|
171
|
+
return { ...item, type };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
role: item.role || 'user',
|
|
175
|
+
type,
|
|
176
|
+
content: this._normalizeContent(item.content)
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_normalizeContent(content) {
|
|
182
|
+
if (typeof content === 'string') return content;
|
|
183
|
+
if (Array.isArray(content)) return content;
|
|
184
|
+
if (content?.text) return content.text;
|
|
185
|
+
if (content && typeof content === 'object') return JSON.stringify(content);
|
|
186
|
+
return content || '';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_contentPartToText(part) {
|
|
190
|
+
if (part == null || typeof part !== 'object') return '';
|
|
191
|
+
return typeof part.text === 'string' ? part.text : (typeof part.text?.value === 'string' ? part.text.value : '');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_messageItemToText(item) {
|
|
195
|
+
if (!item) return '';
|
|
196
|
+
if (Array.isArray(item.content)) {
|
|
197
|
+
return item.content.map(c => this._contentPartToText(c)).filter(Boolean).join('').trim();
|
|
198
|
+
}
|
|
199
|
+
if (typeof item.content === 'string') return item.content.trim();
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_extractMessageOutput(result) {
|
|
204
|
+
if (result == null || typeof result !== 'object') return '';
|
|
205
|
+
|
|
206
|
+
if (result.output && Array.isArray(result.output)) {
|
|
207
|
+
const messageItems = result.output.filter(item => item && item.type === 'message');
|
|
208
|
+
if (messageItems.length > 0) {
|
|
209
|
+
const texts = messageItems.map(item => this._messageItemToText(item)).filter(Boolean);
|
|
210
|
+
const proseTexts = texts.filter(text => !isJsonBlob(text));
|
|
211
|
+
const jsonDiscarded = texts.length - proseTexts.length;
|
|
212
|
+
const keptOne = proseTexts.length > 0 ? 1 : 0;
|
|
213
|
+
|
|
214
|
+
if (messageItems.length > 1 || jsonDiscarded > 0) {
|
|
215
|
+
const hasFunctionCalls = result.output.some(item => item?.type === 'function_call');
|
|
216
|
+
logger.warn(`[${this.constructor.name}] Multiple/structured message items in response; keeping the first natural-language reply (likely OpenAI multi-output bug)`, {
|
|
217
|
+
discarded: messageItems.length - keptOne,
|
|
218
|
+
jsonDiscarded,
|
|
219
|
+
responseId: result.id,
|
|
220
|
+
model: result.model,
|
|
221
|
+
hasFunctionCalls
|
|
222
|
+
});
|
|
223
|
+
logBugReportToAirtable({
|
|
224
|
+
reporter: 'system',
|
|
225
|
+
description: [
|
|
226
|
+
`Responses API (${this.constructor.name}) returned multiple message items in a single response.`,
|
|
227
|
+
'Suspected upstream bug (model failed to emit stop-of-message token).',
|
|
228
|
+
`Discarded ${messageItems.length - keptOne} extra message(s); kept the first natural-language reply as the canonical reply.`,
|
|
229
|
+
jsonDiscarded > 0 ? `${jsonDiscarded} discarded item(s) were raw JSON/structured output that must never reach the patient.` : null,
|
|
230
|
+
`Response ID: ${result.id || 'unknown'}`,
|
|
231
|
+
`Model: ${result.model || 'unknown'}`,
|
|
232
|
+
`Function calls present: ${hasFunctionCalls ? 'yes' : 'no'}`
|
|
233
|
+
].filter(Boolean).join('\n'),
|
|
234
|
+
severity: 'medium',
|
|
235
|
+
status: 'Open',
|
|
236
|
+
clasificacion: 'alucinaciones',
|
|
237
|
+
bugType: 'backend',
|
|
238
|
+
owner: ['ariana'],
|
|
239
|
+
request_id: result.id || null,
|
|
240
|
+
server: runtimeConfig.get('SERVICE_NAME') || 'nexus'
|
|
241
|
+
}).catch((err) => logger.warn(`[${this.constructor.name}] Bug report logger failed`, { error: err.message }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (proseTexts.length > 0) return proseTexts[0];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (result.output_text) return this._extractMessageFromConcatenatedOutput(result.output_text);
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_extractMessageFromConcatenatedOutput(outputText) {
|
|
252
|
+
if (!outputText || typeof outputText !== 'string') return '';
|
|
253
|
+
if (!/^\s*assistant\s*\n/i.test(outputText)) return outputText.trim();
|
|
254
|
+
|
|
255
|
+
const segments = outputText
|
|
256
|
+
.split(/\n\s*assistant\s*\n/i)
|
|
257
|
+
.map(s => s.replace(/^assistant\s*\n?/i, '').trim())
|
|
258
|
+
.filter(Boolean);
|
|
259
|
+
|
|
260
|
+
if (segments.length === 0) return '';
|
|
261
|
+
if (segments.length > 1) logger.debug(`[${this.constructor.name}] Concatenated transcript detected, using last segment`, { segmentCount: segments.length });
|
|
262
|
+
return segments[segments.length - 1].trim();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_ensureId(value) {
|
|
266
|
+
if (!value) throw new Error('Identifier value is required');
|
|
267
|
+
if (typeof value === 'string') return value;
|
|
268
|
+
if (value?.id) return value.id;
|
|
269
|
+
throw new Error('Unable to resolve identifier value');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
BaseLLMProvider,
|
|
275
|
+
};
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
const { OpenAI } = require('openai');
|
|
2
2
|
|
|
3
|
-
const runtimeConfig = require('../config/runtimeConfig');
|
|
4
3
|
const { retryWithBackoff } = require('../utils/retryUtils');
|
|
5
4
|
const { logger } = require('../utils/logger');
|
|
6
|
-
const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
|
|
7
|
-
const { isJsonBlob } = require('../utils/formatUtils');
|
|
8
|
-
|
|
9
|
-
const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
|
|
10
5
|
|
|
11
6
|
const { composePrompt, resolveTools } = require('../services/promptComposerService');
|
|
12
7
|
const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
|
|
13
|
-
const { getMessages } = require('../services/messageService');
|
|
14
8
|
|
|
15
|
-
const {
|
|
9
|
+
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
16
10
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
17
11
|
|
|
18
12
|
const CONVERSATION_PREFIX = 'conv_';
|
|
@@ -20,19 +14,17 @@ const RESPONSE_PREFIX = 'resp_';
|
|
|
20
14
|
const MAX_ITEMS_ON_CREATE = 20;
|
|
21
15
|
const MAX_ITEMS_PER_BATCH = 20;
|
|
22
16
|
const DEFAULT_MAX_HISTORICAL_MESSAGES = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
|
|
23
|
-
const MAX_CONVERSATION_RETRIES = parseInt(process.env.MAX_CONVERSATION_RETRIES || '3', 10);
|
|
24
17
|
const MAX_FUNCTION_ROUNDS = parseInt(process.env.MAX_FUNCTION_ROUNDS || '5', 10);
|
|
25
18
|
const PROVIDER_NAME = 'OpenAIResponsesProvider';
|
|
26
19
|
|
|
27
|
-
class OpenAIResponsesProvider {
|
|
20
|
+
class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
28
21
|
constructor(options = {}) {
|
|
22
|
+
super(options);
|
|
29
23
|
const {
|
|
30
24
|
apiKey = process.env.OPENAI_API_KEY,
|
|
31
25
|
organization,
|
|
32
26
|
client,
|
|
33
27
|
defaultModels = {},
|
|
34
|
-
conversationManager,
|
|
35
|
-
sessionManager,
|
|
36
28
|
} = options;
|
|
37
29
|
|
|
38
30
|
if (!client && !apiKey) {
|
|
@@ -51,16 +43,11 @@ class OpenAIResponsesProvider {
|
|
|
51
43
|
};
|
|
52
44
|
|
|
53
45
|
this.variant = 'responses';
|
|
54
|
-
this.conversationManager = conversationManager || new DefaultMemoryManager();
|
|
55
|
-
this.sessionManager = sessionManager || null;
|
|
56
46
|
|
|
57
47
|
this.responses = this.client.responses;
|
|
58
48
|
this.conversations = this.client.conversations;
|
|
59
49
|
}
|
|
60
50
|
|
|
61
|
-
getVariant() { return this.variant; }
|
|
62
|
-
getClient() { return this.client; }
|
|
63
|
-
|
|
64
51
|
async createConversation({ metadata, messages = [], toolResources } = {}) {
|
|
65
52
|
const capped = messages.length > DEFAULT_MAX_HISTORICAL_MESSAGES;
|
|
66
53
|
const messagesToProcess = capped ? messages.slice(-DEFAULT_MAX_HISTORICAL_MESSAGES) : messages;
|
|
@@ -116,28 +103,6 @@ class OpenAIResponsesProvider {
|
|
|
116
103
|
logger.info(`[_addItemsInBatches] Added ${items.length} messages in ${Math.ceil(items.length / batchSize)} batches`);
|
|
117
104
|
}
|
|
118
105
|
|
|
119
|
-
_convertItemsToApiFormat(items) {
|
|
120
|
-
return items.map(item => {
|
|
121
|
-
const type = item.type || 'message';
|
|
122
|
-
if (type === 'function_call' || type === 'function_call_output') {
|
|
123
|
-
return { ...item, type };
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
role: item.role || 'user',
|
|
127
|
-
type,
|
|
128
|
-
content: this._normalizeContent(item.content)
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
_normalizeContent(content) {
|
|
134
|
-
if (typeof content === 'string') return content;
|
|
135
|
-
if (Array.isArray(content)) return content;
|
|
136
|
-
if (content?.text) return content.text;
|
|
137
|
-
if (content && typeof content === 'object') return JSON.stringify(content);
|
|
138
|
-
return content || '';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
106
|
async addMessage({ threadId, messages, role = 'user', content, metadata }) {
|
|
142
107
|
const id = this._ensurethreadId(threadId);
|
|
143
108
|
const messagesToAdd = messages || [{ role, content, metadata }];
|
|
@@ -156,199 +121,6 @@ class OpenAIResponsesProvider {
|
|
|
156
121
|
);
|
|
157
122
|
}
|
|
158
123
|
|
|
159
|
-
_normalizeThread(thread) {
|
|
160
|
-
return {
|
|
161
|
-
conversationId: thread.conversation_id || thread.getConversationId?.(),
|
|
162
|
-
assistantId: thread.prompt_id || thread.getAssistantId?.()
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
_contentPartToText(part) {
|
|
167
|
-
if (part == null || typeof part !== 'object') return '';
|
|
168
|
-
return typeof part.text === 'string' ? part.text : (typeof part.text?.value === 'string' ? part.text.value : '');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
_messageItemToText(item) {
|
|
172
|
-
if (!item) return '';
|
|
173
|
-
if (Array.isArray(item.content)) {
|
|
174
|
-
return item.content.map(c => this._contentPartToText(c)).filter(Boolean).join('').trim();
|
|
175
|
-
}
|
|
176
|
-
if (typeof item.content === 'string') return item.content.trim();
|
|
177
|
-
return '';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
_extractMessageOutput(result) {
|
|
181
|
-
if (result == null || typeof result !== 'object') return '';
|
|
182
|
-
|
|
183
|
-
if (result.output && Array.isArray(result.output)) {
|
|
184
|
-
const messageItems = result.output.filter(item => item && item.type === 'message');
|
|
185
|
-
if (messageItems.length > 0) {
|
|
186
|
-
const texts = messageItems.map(item => this._messageItemToText(item)).filter(Boolean);
|
|
187
|
-
const proseTexts = texts.filter(text => !isJsonBlob(text));
|
|
188
|
-
const jsonDiscarded = texts.length - proseTexts.length;
|
|
189
|
-
const keptOne = proseTexts.length > 0 ? 1 : 0;
|
|
190
|
-
|
|
191
|
-
if (messageItems.length > 1 || jsonDiscarded > 0) {
|
|
192
|
-
const hasFunctionCalls = result.output.some(item => item?.type === 'function_call');
|
|
193
|
-
logger.warn('[OpenAIResponsesProvider] Multiple/structured message items in response; keeping the first natural-language reply (likely OpenAI multi-output bug)', {
|
|
194
|
-
discarded: messageItems.length - keptOne,
|
|
195
|
-
jsonDiscarded,
|
|
196
|
-
responseId: result.id,
|
|
197
|
-
model: result.model,
|
|
198
|
-
hasFunctionCalls
|
|
199
|
-
});
|
|
200
|
-
logBugReportToAirtable({
|
|
201
|
-
reporter: 'system',
|
|
202
|
-
description: [
|
|
203
|
-
'OpenAI Responses API returned multiple message items in a single response.',
|
|
204
|
-
'Suspected upstream bug (model failed to emit stop-of-message token).',
|
|
205
|
-
`Discarded ${messageItems.length - keptOne} extra message(s); kept the first natural-language reply as the canonical reply.`,
|
|
206
|
-
jsonDiscarded > 0 ? `${jsonDiscarded} discarded item(s) were raw JSON/structured output that must never reach the patient.` : null,
|
|
207
|
-
`Response ID: ${result.id || 'unknown'}`,
|
|
208
|
-
`Model: ${result.model || 'unknown'}`,
|
|
209
|
-
`Function calls present: ${hasFunctionCalls ? 'yes' : 'no'}`
|
|
210
|
-
].filter(Boolean).join('\n'),
|
|
211
|
-
severity: 'medium',
|
|
212
|
-
status: 'Open',
|
|
213
|
-
clasificacion: 'alucinaciones',
|
|
214
|
-
bugType: 'backend',
|
|
215
|
-
owner: ['ariana'],
|
|
216
|
-
request_id: result.id || null,
|
|
217
|
-
server: runtimeConfig.get('SERVICE_NAME') || 'nexus'
|
|
218
|
-
}).catch((err) => logger.warn('[OpenAIResponsesProvider] Bug report logger failed', { error: err.message }));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (proseTexts.length > 0) return proseTexts[0];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (result.output_text) return this._extractMessageFromConcatenatedOutput(result.output_text);
|
|
225
|
-
return '';
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
_extractMessageFromConcatenatedOutput(outputText) {
|
|
229
|
-
if (!outputText || typeof outputText !== 'string') return '';
|
|
230
|
-
if (!/^\s*assistant\s*\n/i.test(outputText)) return outputText.trim();
|
|
231
|
-
|
|
232
|
-
const segments = outputText
|
|
233
|
-
.split(/\n\s*assistant\s*\n/i)
|
|
234
|
-
.map(s => s.replace(/^assistant\s*\n?/i, '').trim())
|
|
235
|
-
.filter(Boolean);
|
|
236
|
-
|
|
237
|
-
if (segments.length === 0) return '';
|
|
238
|
-
if (segments.length > 1) logger.debug('[OpenAIResponsesProvider] Concatenated transcript detected, using last segment', { segmentCount: segments.length });
|
|
239
|
-
return segments[segments.length - 1].trim();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async executeRun({ thread, assistant, message = null, tools = [], config = {} }) {
|
|
243
|
-
const { conversationId, assistantId } = this._normalizeThread(thread);
|
|
244
|
-
const promptVersion = thread?.version || null;
|
|
245
|
-
const presetId = thread?.preset_id || null;
|
|
246
|
-
const presetVersion = thread?.preset_version || null;
|
|
247
|
-
|
|
248
|
-
logger.info('[executeRun] Starting', { conversationId, assistantId, promptVersion });
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const context = await this.conversationManager.buildContext({
|
|
252
|
-
thread,
|
|
253
|
-
message,
|
|
254
|
-
config: {
|
|
255
|
-
...config,
|
|
256
|
-
threadId: conversationId,
|
|
257
|
-
assistantId,
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const [lastMessage] = await getMessages({ numero: thread.code }, { sort: { createdAt: -1 }, limit: 1 });
|
|
262
|
-
const phiProcessor = config.phiProcessor;
|
|
263
|
-
const maskedCode = phiProcessor ? phiProcessor.hashCode(thread.code) : thread.code;
|
|
264
|
-
const metadata = {
|
|
265
|
-
numero: maskedCode,
|
|
266
|
-
message_id: message?.message_id || lastMessage?.message_id || null
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
logger.info('[executeRun] Context built', { conversationId, messageCount: context?.length || 0 });
|
|
270
|
-
|
|
271
|
-
// config.clinicalData, when given, fully replaces the fetched bundle (all four fields).
|
|
272
|
-
// Must be a complete, non-empty object; anything else falls through to the normal fetch.
|
|
273
|
-
const override = config.clinicalData;
|
|
274
|
-
const hasOverride = override && typeof override === 'object' && Object.keys(override).length > 0;
|
|
275
|
-
const clinicalData = hasOverride ? override : await this.conversationManager.getClinicalData(thread.code);
|
|
276
|
-
const promptVariables = {
|
|
277
|
-
clinical_context: clinicalData?.clinicalContext ?? '',
|
|
278
|
-
last_symptoms: clinicalData?.lastSymptoms ?? '',
|
|
279
|
-
current_date: getCurrentMexicoDateTime(),
|
|
280
|
-
patient_memories: clinicalData?.patientMemories ?? '',
|
|
281
|
-
conversation_summaries: clinicalData?.conversationSummaries ?? '',
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const result = await this.runConversation({
|
|
285
|
-
threadId: conversationId,
|
|
286
|
-
assistantId,
|
|
287
|
-
presetId,
|
|
288
|
-
presetVersion,
|
|
289
|
-
tools,
|
|
290
|
-
context,
|
|
291
|
-
promptVariables,
|
|
292
|
-
promptVersion,
|
|
293
|
-
assistant,
|
|
294
|
-
metadata,
|
|
295
|
-
...config
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
await this.conversationManager.processResponse(result, thread, config);
|
|
299
|
-
this.sessionManager?.recordActivity(thread.code);
|
|
300
|
-
|
|
301
|
-
const completed = result.status === 'completed';
|
|
302
|
-
const output = result.output_text || this._extractMessageOutput(result);
|
|
303
|
-
const toolsExecuted = result.tools_executed?.length || 0;
|
|
304
|
-
|
|
305
|
-
logger.info('[executeRun] Complete', {
|
|
306
|
-
runId: result.id,
|
|
307
|
-
completed,
|
|
308
|
-
outputLength: output?.length || 0,
|
|
309
|
-
toolsExecuted
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
return { run: result, completed, output, tools_executed: result.tools_executed || [], retries: result.retries || 0 };
|
|
313
|
-
} catch (error) {
|
|
314
|
-
logger.error('[executeRun] Failed', { conversationId, assistantId, error: error.message });
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async runConversation(config = {}) {
|
|
320
|
-
const { threadId, assistantId } = config;
|
|
321
|
-
|
|
322
|
-
for (let attempt = 1; attempt <= MAX_CONVERSATION_RETRIES; attempt++) {
|
|
323
|
-
try {
|
|
324
|
-
logger.info('[runConversation] Attempt', { attempt, maxRetries: MAX_CONVERSATION_RETRIES, threadId, assistantId });
|
|
325
|
-
|
|
326
|
-
const result = await this._executeConversation(config);
|
|
327
|
-
const extractedOutput = this._extractMessageOutput(result);
|
|
328
|
-
|
|
329
|
-
if (extractedOutput?.trim()) {
|
|
330
|
-
result.output_text = extractedOutput;
|
|
331
|
-
logger.info('[runConversation] Success', {
|
|
332
|
-
attempt,
|
|
333
|
-
outputLength: extractedOutput.length,
|
|
334
|
-
toolsExecuted: result.tools_executed?.length || 0
|
|
335
|
-
});
|
|
336
|
-
return result;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
logger.warn('[runConversation] Empty output', { attempt });
|
|
340
|
-
if (attempt === MAX_CONVERSATION_RETRIES) {
|
|
341
|
-
throw new Error(`Conversation failed after ${attempt} attempts - no valid output`);
|
|
342
|
-
}
|
|
343
|
-
await new Promise(r => setTimeout(r, 500));
|
|
344
|
-
} catch (error) {
|
|
345
|
-
logger.error('[runConversation] Attempt failed', { attempt, error: error.message });
|
|
346
|
-
if (attempt === MAX_CONVERSATION_RETRIES) throw error;
|
|
347
|
-
await new Promise(r => setTimeout(r, 500));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
124
|
async _executeConversation(config = {}) {
|
|
353
125
|
const {
|
|
354
126
|
threadId, assistantId, presetId = null, presetVersion = null, additionalMessages = [], context = null,
|
|
@@ -471,7 +243,6 @@ class OpenAIResponsesProvider {
|
|
|
471
243
|
if (assistant && response.output) {
|
|
472
244
|
let currentInput = [...input];
|
|
473
245
|
|
|
474
|
-
// Follow-up calls always use the default tool_choice.
|
|
475
246
|
apiCallConfig.tool_choice = toolChoice;
|
|
476
247
|
|
|
477
248
|
for (let round = 1; round <= MAX_FUNCTION_ROUNDS; round++) {
|
|
@@ -532,13 +303,6 @@ class OpenAIResponsesProvider {
|
|
|
532
303
|
return id;
|
|
533
304
|
}
|
|
534
305
|
|
|
535
|
-
_ensureId(value) {
|
|
536
|
-
if (!value) throw new Error('Identifier value is required');
|
|
537
|
-
if (typeof value === 'string') return value;
|
|
538
|
-
if (value?.id) return value.id;
|
|
539
|
-
throw new Error('Unable to resolve identifier value');
|
|
540
|
-
}
|
|
541
|
-
|
|
542
306
|
async _post(path, body, options = {}) { return this.client.post(path, { ...options, body }); }
|
|
543
307
|
async _get(path, query, options = {}) { return this.client.get(path, { ...options, query }); }
|
|
544
308
|
async _delete(path, options = {}) { return this.client.delete(path, options); }
|
|
@@ -63,7 +63,7 @@ async function insertMessage(values) {
|
|
|
63
63
|
{
|
|
64
64
|
$set: {
|
|
65
65
|
lastMessageAt: new Date(),
|
|
66
|
-
lastMessageBody: values.
|
|
66
|
+
lastMessageBody: values.plainBody || '',
|
|
67
67
|
lastMessageFromMe: !!values.from_me,
|
|
68
68
|
lastMessageMedia: values.media || null
|
|
69
69
|
},
|
|
@@ -77,7 +77,7 @@ async function insertMessage(values) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
updateRecordByFilter(Monitoreo_ID, 'message_monitor', `{whatsapp_id} = "${values.numero}"`, {
|
|
80
|
-
...(values.from_me ? { last_message_bot:
|
|
80
|
+
...(values.from_me ? { last_message_bot: doc.plainBody } : { last_message_patient: doc.plainBody, read: false }),
|
|
81
81
|
...(values.from_me ? { last_message_bot_time: doc.createdAt.toISOString() } : { last_message_patient_time: doc.createdAt.toISOString() })
|
|
82
82
|
}, values.numero).catch(err => logger.error('[MongoStorage] Failed to update message_monitor table', { numero: values.numero, error: err.message }));
|
|
83
83
|
|
|
@@ -16,7 +16,6 @@ const refreshApprovalStatuses = async (templates) => {
|
|
|
16
16
|
const updateFields = {
|
|
17
17
|
approvalRequest: {
|
|
18
18
|
sid: reqData.sid,
|
|
19
|
-
status: reqData.status,
|
|
20
19
|
dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
|
|
21
20
|
dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
|
|
22
21
|
rejectionReason: reqData.rejectionReason || ''
|
|
@@ -89,7 +89,7 @@ class MongoStorage {
|
|
|
89
89
|
code: doc.numero,
|
|
90
90
|
name: doc.nombre_whatsapp,
|
|
91
91
|
origin: doc.origin,
|
|
92
|
-
body: doc.
|
|
92
|
+
body: doc.plainBody,
|
|
93
93
|
media: doc.media,
|
|
94
94
|
type: doc.interactive_type ? 'interactive' : doc.media ? 'media' : 'message',
|
|
95
95
|
triggeredBy: doc.triggeredBy,
|
|
@@ -106,6 +106,7 @@ class MongoStorage {
|
|
|
106
106
|
nombre_whatsapp: messageData.pushName ?? (fromMe ? runtimeConfig.get('USER_DB_MONGO') : null),
|
|
107
107
|
numero: ensureWhatsAppFormat(messageData.code),
|
|
108
108
|
body: messageData.body || '',
|
|
109
|
+
plainBody: messageData.plainBody || '',
|
|
109
110
|
processed: messageData.processed || false,
|
|
110
111
|
message_id: messageId,
|
|
111
112
|
interactive_type: messageData.interactive?.type || null,
|