@peopl-health/nexus 5.10.0-dev.1114 → 5.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/clinical/providers/BaseLLMProvider.js +4 -7
- package/lib/core/NexusMessaging.js +1 -1
- package/lib/helpers/messageStatusHelper.js +10 -2
- package/lib/helpers/templateRecoveryHelper.js +36 -2
- package/lib/jobs/TemplateApprovalJob.js +2 -6
- package/lib/templates/templateStructure.js +15 -2
- package/package.json +1 -1
|
@@ -62,8 +62,7 @@ class BaseLLMProvider {
|
|
|
62
62
|
|
|
63
63
|
const resolvedSchemas = this._resolveToolSchemas({ assistant, toolIds, filtered, toolDescriptions });
|
|
64
64
|
const { toolChoice: flooredToolChoice, toolSchemas, narrowed } = this._applyDeliveryFloor(
|
|
65
|
-
toolChoice, resolvedSchemas,
|
|
66
|
-
Boolean(additionalInstructions && assistant?.toolRuntimeContext));
|
|
65
|
+
toolChoice, resolvedSchemas, Boolean(assistant?.toolRuntimeContext));
|
|
67
66
|
const effectiveToolIds = narrowed ? toolSchemas.map(s => s.function?.name).filter(Boolean) : toolIds;
|
|
68
67
|
const devContent = this._buildInstructions({ resolvedPrompt, toolSchemas, prePromptResult });
|
|
69
68
|
const auditedPrompt = additionalInstructions
|
|
@@ -94,7 +93,7 @@ class BaseLLMProvider {
|
|
|
94
93
|
instructions: instructions || devContent || '',
|
|
95
94
|
input,
|
|
96
95
|
toolSchemas,
|
|
97
|
-
toolChoice: prePromptResult?.toolChoice || flooredToolChoice,
|
|
96
|
+
toolChoice: narrowed ? flooredToolChoice : prePromptResult?.toolChoice || flooredToolChoice,
|
|
98
97
|
followUpToolChoice: flooredToolChoice,
|
|
99
98
|
modelConfig,
|
|
100
99
|
metadata,
|
|
@@ -372,11 +371,9 @@ class BaseLLMProvider {
|
|
|
372
371
|
return toolNames.length > 0 && toolNames.every(name => TERMINAL_DELIVERY_TOOLS.has(name));
|
|
373
372
|
}
|
|
374
373
|
|
|
375
|
-
_applyDeliveryFloor(toolChoice, toolSchemas,
|
|
376
|
-
if (prePromptToolChoice) return { toolChoice, toolSchemas, narrowed: false };
|
|
374
|
+
_applyDeliveryFloor(toolChoice, toolSchemas, hasToolRuntimeContext = false) {
|
|
377
375
|
if (toolChoice !== 'none') return { toolChoice, toolSchemas, narrowed: false };
|
|
378
|
-
|
|
379
|
-
if (isClinicalDirective) return { toolChoice: 'auto', toolSchemas, narrowed: false };
|
|
376
|
+
if (!hasToolRuntimeContext) return { toolChoice, toolSchemas, narrowed: false };
|
|
380
377
|
const deliveryOnly = toolSchemas.filter(s => TERMINAL_DELIVERY_TOOLS.has(s.function?.name));
|
|
381
378
|
if (!deliveryOnly.length) return { toolChoice, toolSchemas, narrowed: false };
|
|
382
379
|
return { toolChoice: 'auto', toolSchemas: deliveryOnly, narrowed: true };
|
|
@@ -255,7 +255,7 @@ class NexusMessaging {
|
|
|
255
255
|
},
|
|
256
256
|
saveFn,
|
|
257
257
|
async ({ body, _id }) => {
|
|
258
|
-
await triggerTemplateRecovery({ _id, body, numero: messageData.code }, { source: 'preemptive' });
|
|
258
|
+
await triggerTemplateRecovery({ _id, body, numero: messageData.code, media: messageData.media }, { source: 'preemptive' });
|
|
259
259
|
return { success: true, messageId: String(_id), status: 'queued', preemptive: true, delivered: false, deferred: true };
|
|
260
260
|
},
|
|
261
261
|
{ onSaved: messageData.onParentMessageId },
|
|
@@ -6,7 +6,7 @@ const { createEvent, safeEmit } = require('../utils/eventUtils');
|
|
|
6
6
|
const { Message } = require('../models/messageModel');
|
|
7
7
|
const { DeliveryAttempt, DELIVERY_ATTEMPT_TERMINAL_STATUSES } = require('../models/deliveryAttemptModel');
|
|
8
8
|
|
|
9
|
-
const { handle24HourWindowError } = require('../helpers/templateRecoveryHelper');
|
|
9
|
+
const { handle24HourWindowError, cleanupRecoveryTemplate } = require('../helpers/templateRecoveryHelper');
|
|
10
10
|
const { updateDeliveryAttemptByTwilioSid } = require('../helpers/deliveryAttemptHelper');
|
|
11
11
|
|
|
12
12
|
const { addLinkedRecord, updateRecordByFilter } = require('../services/airtableService');
|
|
@@ -105,11 +105,19 @@ async function handleStatusCallback(twilioStatusData, { eventBus } = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if ((ErrorCode === 63016 || ErrorCode === '63016') && updated) {
|
|
108
|
-
handle24HourWindowError(updated, MessageSid).catch(err =>
|
|
108
|
+
handle24HourWindowError(updated, MessageSid).catch(err =>
|
|
109
109
|
logger.error('[MessageStatus] Recovery error', { messageSid: MessageSid, error: err.message })
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
const deliveryStatus = MessageStatus.toLowerCase();
|
|
114
|
+
if (updated?.statusInfo?.recoverySentAt && updated?.statusInfo?.recoveryTemplateSid &&
|
|
115
|
+
(deliveryStatus === 'sent' || deliveryStatus === 'delivered' || deliveryStatus === 'read')) {
|
|
116
|
+
cleanupRecoveryTemplate(updated).catch(err =>
|
|
117
|
+
logger.warn('[MessageStatus] Recovery template cleanup failed', { messageSid: MessageSid, error: err.message })
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
return updated;
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
1
2
|
const { logger } = require('../utils/logger');
|
|
2
3
|
const { Message } = require('../models/messageModel');
|
|
3
4
|
const TemplateModel = require('../models/templateModel');
|
|
@@ -7,6 +8,17 @@ const { recordDeliveryAttempt } = require('./deliveryAttemptHelper');
|
|
|
7
8
|
const getMessaging = () => require('../core/NexusMessaging');
|
|
8
9
|
|
|
9
10
|
const TEMPLATE_BODY_LIMIT = 1024;
|
|
11
|
+
const RECOVERY_MEDIA_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
12
|
+
|
|
13
|
+
async function resolveRecoveryMediaUrl(media) {
|
|
14
|
+
if (!media?.bucketName || !media?.key) return null;
|
|
15
|
+
try {
|
|
16
|
+
return await generatePresignedUrl(media.bucketName, media.key, RECOVERY_MEDIA_URL_TTL_SECONDS);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logger.warn('[TemplateRecovery] Could not presign media; sending text-only', { error: error.message });
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messageSid = null } = {}) {
|
|
12
24
|
try {
|
|
@@ -45,6 +57,9 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
|
|
|
45
57
|
const template = new Template(templateName, 'UTILITY', 'es');
|
|
46
58
|
template.setBody(messageDoc.body, []);
|
|
47
59
|
|
|
60
|
+
const mediaUrl = await resolveRecoveryMediaUrl(messageDoc.media);
|
|
61
|
+
if (mediaUrl) template.setMedia(mediaUrl);
|
|
62
|
+
|
|
48
63
|
let twilioContent;
|
|
49
64
|
let setupErr = null;
|
|
50
65
|
if (messageDoc.body.length > TEMPLATE_BODY_LIMIT) {
|
|
@@ -96,7 +111,7 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
|
|
|
96
111
|
{ $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
|
|
97
112
|
);
|
|
98
113
|
|
|
99
|
-
logger.info('[TemplateRecovery] Template created', { ...logCtx, templateSid: twilioContent.sid });
|
|
114
|
+
logger.info('[TemplateRecovery] Template created', { ...logCtx, templateSid: twilioContent.sid, hasMedia: !!mediaUrl });
|
|
100
115
|
|
|
101
116
|
await scheduleTemplateApproval({
|
|
102
117
|
templateSid: twilioContent.sid,
|
|
@@ -114,4 +129,23 @@ async function handle24HourWindowError(message, messageSid) {
|
|
|
114
129
|
return triggerTemplateRecovery(message, { source: 'reactive', messageSid });
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
|
|
132
|
+
async function cleanupRecoveryTemplate(messageDoc) {
|
|
133
|
+
const templateSid = messageDoc?.statusInfo?.recoveryTemplateSid;
|
|
134
|
+
if (!templateSid || !messageDoc?._id) return;
|
|
135
|
+
|
|
136
|
+
const claim = await Message.updateOne(
|
|
137
|
+
{ _id: messageDoc._id, 'statusInfo.recoveryTemplateSid': templateSid },
|
|
138
|
+
{ $unset: { 'statusInfo.recoveryTemplateSid': '' } }
|
|
139
|
+
);
|
|
140
|
+
if (!claim.modifiedCount && !claim.nModified) return;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const provider = getMessaging().requireProvider();
|
|
144
|
+
await provider.deleteTemplate(templateSid);
|
|
145
|
+
logger.info('[TemplateRecovery] Template deleted after delivery', { messageDocId: String(messageDoc._id), templateSid });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.warn('[TemplateRecovery] Failed to delete template after delivery', { templateSid, error: error.message });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { handle24HourWindowError, triggerTemplateRecovery, cleanupRecoveryTemplate };
|
|
@@ -84,7 +84,7 @@ class TemplateApprovalJob extends BaseJob {
|
|
|
84
84
|
logger.info('[TemplateApprovalJob] Approval status', { templateSid, approvalStatus, attempt, messageId, originalMessageSid });
|
|
85
85
|
|
|
86
86
|
if (approvalStatus === 'APPROVED') {
|
|
87
|
-
return await this._handleApproved({
|
|
87
|
+
return await this._handleApproved({ message, templateSid, originalMessageSid });
|
|
88
88
|
}
|
|
89
89
|
if (approvalStatus === 'REJECTED') {
|
|
90
90
|
return await this._handleRejected({ provider, message, templateSid });
|
|
@@ -109,7 +109,7 @@ class TemplateApprovalJob extends BaseJob {
|
|
|
109
109
|
return { success: false, reason, nextAttempt };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
async _handleApproved({
|
|
112
|
+
async _handleApproved({ message, templateSid, originalMessageSid }) {
|
|
113
113
|
const claim = await Message.updateOne(
|
|
114
114
|
{ _id: message._id, 'statusInfo.recoverySentAt': null },
|
|
115
115
|
{ $set: { 'statusInfo.recoverySentAt': new Date() } }
|
|
@@ -148,10 +148,6 @@ class TemplateApprovalJob extends BaseJob {
|
|
|
148
148
|
{ $set: { status: 'sent', completedAt: new Date() } }
|
|
149
149
|
);
|
|
150
150
|
|
|
151
|
-
provider.deleteTemplate(templateSid).catch((deleteErr) =>
|
|
152
|
-
logger.warn('[TemplateApprovalJob] Failed to delete template after send', { templateSid, error: deleteErr.message })
|
|
153
|
-
);
|
|
154
|
-
|
|
155
151
|
return { success: true, recoveryMessageId: sendResult.messageId };
|
|
156
152
|
}
|
|
157
153
|
|
|
@@ -14,7 +14,8 @@ class Template {
|
|
|
14
14
|
buttons: []
|
|
15
15
|
};
|
|
16
16
|
this.variables = [];
|
|
17
|
-
|
|
17
|
+
this.media = null;
|
|
18
|
+
|
|
18
19
|
const uniqueId = `${Date.now()}_${uuidv4().substring(0, 6)}`;
|
|
19
20
|
this.friendlyName = `${name}_${uniqueId}`;
|
|
20
21
|
this.templateName = this.friendlyName;
|
|
@@ -45,6 +46,11 @@ class Template {
|
|
|
45
46
|
return this;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
setMedia(url) {
|
|
50
|
+
this.media = url;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
addQuickReply(text) {
|
|
49
55
|
if (this.components.buttons.length >= 3) {
|
|
50
56
|
throw new Error('Maximum of 3 buttons allowed');
|
|
@@ -116,7 +122,14 @@ class Template {
|
|
|
116
122
|
if (this.category) {
|
|
117
123
|
template.categories = [this.category];
|
|
118
124
|
}
|
|
119
|
-
|
|
125
|
+
|
|
126
|
+
if (this.media) {
|
|
127
|
+
template.types['twilio/media'] = {
|
|
128
|
+
body: this.components.body[0].text,
|
|
129
|
+
media: [this.media]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
if (this.components.buttons && this.components.buttons.length > 0) {
|
|
121
134
|
const actions = this.components.buttons.map((button, index) => {
|
|
122
135
|
return {
|