@peopl-health/nexus 5.12.4 → 5.12.6

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.
@@ -208,7 +208,8 @@ class AnthropicProvider extends BaseLLMProvider {
208
208
  const turnContent = Array.isArray(lastUserTurn.content)
209
209
  ? lastUserTurn.content
210
210
  : [{ type: 'text', text: String(lastUserTurn.content ?? '') }];
211
- lastUserTurn.content = [...turnContent, { type: 'text', text: DELIVERY_NUDGE }];
211
+ const alreadyNudged = turnContent.some(part => part?.type === 'text' && part.text === DELIVERY_NUDGE);
212
+ if (!alreadyNudged) lastUserTurn.content = [...turnContent, { type: 'text', text: DELIVERY_NUDGE }];
212
213
  // The nudge is a model call like any other: gate thinking on it so the per-iteration counter stays honest.
213
214
  prepThinking();
214
215
  const { result: { result: nudged, retries: nudgeRetries }, durationMs: nudgeDurationMs } = await withTiming(() => makeAPICall(currentMessages));
@@ -16,7 +16,12 @@ const CONVERSATION_CONTINUATION = '(continuar)';
16
16
  const NEGATIVE_GUIDANCE_MARKER = '**When NOT to call';
17
17
  const TERMINAL_DELIVERY_TOOLS = new Set(['DeliverPatientMessage']);
18
18
  const DELIVERY_NUDGE = 'sistema: el turno no puede cerrar sin un mensaje para el paciente. Llama ahora a DeliverPatientMessage con message_text en español. No repitas herramientas clínicas ya ejecutadas en este turno.';
19
- const MAX_DELIVERY_NUDGES = 1;
19
+ const MAX_DELIVERY_NUDGES = 2;
20
+ const DELIVERY_FALLBACK_MESSAGES = [
21
+ 'Gracias por tu mensaje. Si necesitas apoyo, escríbeme cuando quieras.',
22
+ 'Gracias por escribirme. Aquí estoy cuando necesites algo.',
23
+ 'Recibí tu mensaje. Cuando quieras contarme más o pedir apoyo, escríbeme.',
24
+ ];
20
25
 
21
26
  function isSuperseded(shouldContinue) {
22
27
  return typeof shouldContinue === 'function' && !shouldContinue();
@@ -324,7 +329,13 @@ class BaseLLMProvider {
324
329
  const result = await this._executeConversation(config);
325
330
  // DeliverPatientMessage is the curated patient reply; prefer it over any model narration in the final response.
326
331
  const delivered = this._extractDeliveredMessage(config, result);
327
- const extractedOutput = delivered?.trim() ? delivered : this._extractMessageOutput(result);
332
+ let extractedOutput = delivered?.trim() ? delivered : this._extractMessageOutput(result);
333
+
334
+ if (!delivered?.trim() && extractedOutput?.trim() && this._deliveryExpected(result)) {
335
+ logger.warn('[runConversation] Delivery tool offered but never committed — substituting safe fallback for raw narration', { attempt, suppressedLength: extractedOutput.length });
336
+ config?.assistant?.toolRuntimeContext?.trace?.setSignals?.({ deliveryFallbackUsed: true });
337
+ extractedOutput = this._deliveryFallbackMessage(config);
338
+ }
328
339
 
329
340
  if (extractedOutput?.trim()) {
330
341
  result.output_text = extractedOutput;
@@ -371,6 +382,19 @@ class BaseLLMProvider {
371
382
  return (toolsExecuted || []).some(t => TERMINAL_DELIVERY_TOOLS.has(t?.tool_name) && this._toolSucceeded(t));
372
383
  }
373
384
 
385
+ _deliveryExpected(result) {
386
+ return (result?.tool_ids || []).some(id => TERMINAL_DELIVERY_TOOLS.has(id));
387
+ }
388
+
389
+ _deliveryFallbackMessage(config) {
390
+ const key = String(config?.assistant?.toolRuntimeContext?.turnId
391
+ ?? config?.assistant?.toolRuntimeContext?.patientCode
392
+ ?? config?.threadId ?? '');
393
+ let hash = 0;
394
+ for (let i = 0; i < key.length; i++) hash = (hash * 31 + key.charCodeAt(i)) | 0;
395
+ return DELIVERY_FALLBACK_MESSAGES[Math.abs(hash) % DELIVERY_FALLBACK_MESSAGES.length];
396
+ }
397
+
374
398
  _isTerminalOnly(toolNames) {
375
399
  return toolNames.length > 0 && toolNames.every(name => TERMINAL_DELIVERY_TOOLS.has(name));
376
400
  }
@@ -531,6 +555,7 @@ module.exports = {
531
555
  CONVERSATION_CONTINUATION,
532
556
  DELIVERY_NUDGE,
533
557
  MAX_DELIVERY_NUDGES,
558
+ DELIVERY_FALLBACK_MESSAGES,
534
559
  TERMINAL_DELIVERY_TOOLS,
535
560
  isSuperseded,
536
561
  };
@@ -2,7 +2,7 @@ const { requireGatewayProvider } = require('../config/llmConfig');
2
2
  const { renderMessage } = require('../services/composerService');
3
3
  const { isComposerEnabled, isAgentSelfRenderEnabled } = require('../flags/composerFlags');
4
4
 
5
- const EMPTY_OUTBOUND_FALLBACK = 'Estoy revisando tu mensaje con detalle, te respondo en un momento.';
5
+ const EMPTY_OUTBOUND_FALLBACK = 'Recibí tu mensaje. Si quieres contarme más o necesitas apoyo, escríbeme.';
6
6
 
7
7
  const definition = {
8
8
  name: 'DeliverPatientMessage',
@@ -216,7 +216,6 @@ const submitForApproval = async (req, res) => {
216
216
  $set: {
217
217
  status: 'PENDING',
218
218
  approvalRequest: {
219
- sid: response.sid,
220
219
  dateSubmitted: validSubmittedDate,
221
220
  dateUpdated: validUpdatedDate,
222
221
  rejectionReason: response.rejection_reason || ''
@@ -270,7 +269,6 @@ const checkApprovalStatus = async (req, res) => {
270
269
  status.approvalRequest.date_created || status.approvalRequest.dateCreated || status.content.dateCreated
271
270
  );
272
271
  dbTemplate.approvalRequest = {
273
- sid: status.approvalRequest.sid,
274
272
  dateSubmitted,
275
273
  dateUpdated: parseDate(
276
274
  status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated,
@@ -288,7 +286,7 @@ const checkApprovalStatus = async (req, res) => {
288
286
  contentSid,
289
287
  content: status.content,
290
288
  approvalRequest: status.approvalRequest,
291
- template: dbTemplate
289
+ template: dbTemplate ? _formatTemplate(dbTemplate) : null
292
290
  });
293
291
  } catch (err) {
294
292
  // If Twilio check fails but we have DB record, return what we know
@@ -301,7 +299,7 @@ const checkApprovalStatus = async (req, res) => {
301
299
  status: dbTemplate.status
302
300
  },
303
301
  approvalRequest: dbTemplate.approvalRequest,
304
- template: dbTemplate,
302
+ template: _formatTemplate(dbTemplate),
305
303
  warning: 'Could not fetch latest status from Twilio'
306
304
  });
307
305
  }
@@ -412,7 +410,7 @@ const getCompleteTemplate = async (req, res) => {
412
410
 
413
411
  return res.status(200).json({
414
412
  success: true,
415
- template
413
+ template: _formatTemplate(template)
416
414
  });
417
415
  } catch (error) {
418
416
  return handleApiError(res, error, 'Failed to get complete template');
package/lib/index.d.ts CHANGED
@@ -116,13 +116,13 @@ declare module '@peopl-health/nexus' {
116
116
  name: string;
117
117
  description?: string;
118
118
  parameters?: any;
119
- handler: (args: any) => any;
119
+ handler: (args: any) => string | Promise<string>;
120
120
  }
121
121
 
122
122
  export interface AssistantConfigDefinition {
123
123
  extends?: typeof BaseAssistant;
124
124
  create?: (this: BaseAssistant, code: string, context?: any) => Promise<any>;
125
- tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
125
+ tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => string | Promise<string> }>;
126
126
  setup?: (this: BaseAssistant, context: { assistantId: string; thread?: any; options?: any }) => void;
127
127
  }
128
128
 
@@ -138,7 +138,7 @@ declare module '@peopl-health/nexus' {
138
138
  assistantId?: string;
139
139
  thread?: any;
140
140
  client?: any;
141
- tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
141
+ tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => string | Promise<string> }>;
142
142
  setup?: (context: { assistantId: string; thread?: any; options?: any }) => void;
143
143
  status?: string;
144
144
  } | any);
@@ -146,9 +146,9 @@ declare module '@peopl-health/nexus' {
146
146
  thread: any;
147
147
  status: string;
148
148
  createdAt: Date;
149
- registerTool(definition: AssistantToolDefinition | string, schema?: any, handler?: (args: any) => any): void;
149
+ registerTool(definition: AssistantToolDefinition | string, schema?: any, handler?: (args: any) => string | Promise<string>): void;
150
150
  getToolSchemas(): any[];
151
- executeTool(name: string, args: any): Promise<any>;
151
+ executeTool(name: string, args: any): Promise<string>;
152
152
  getPreviousMessages(thread?: any): Promise<Array<{ role: string; content: any }>>;
153
153
  createThread(code: string, context?: any): Promise<any>;
154
154
  sendMessage(userId: string, message: string, options?: any): Promise<any>;
@@ -36,7 +36,6 @@ const TemplateSchema = new mongoose.Schema({
36
36
  }
37
37
  ],
38
38
  approvalRequest: {
39
- sid: String,
40
39
  dateSubmitted: Date,
41
40
  dateUpdated: Date,
42
41
  rejectionReason: String
@@ -15,7 +15,6 @@ const refreshApprovalStatuses = async (templates) => {
15
15
  const reqData = approvalInfo.approvalRequest;
16
16
  const updateFields = {
17
17
  approvalRequest: {
18
- sid: reqData.sid,
19
18
  dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
20
19
  dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
21
20
  rejectionReason: reqData.rejectionReason || ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.12.4",
3
+ "version": "5.12.6",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",