@peopl-health/nexus 5.68.3 → 5.68.5
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.
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
const { getCurrentMexicoDateTime, getMexicoDayLabels } = require('../../utils/dateUtils');
|
|
1
|
+
const { getCurrentMexicoDateTime, getMexicoDayLabels, getMexicoUpcomingDayLabels } = require('../../utils/dateUtils');
|
|
2
2
|
|
|
3
3
|
const ANCHOR_SPAN_DAYS = 7;
|
|
4
|
+
const UPCOMING_SPAN_DAYS = 14;
|
|
4
5
|
|
|
5
6
|
function buildDateAnchor() {
|
|
6
7
|
const at = Date.now();
|
|
7
8
|
const labels = getMexicoDayLabels({ days: ANCHOR_SPAN_DAYS, at });
|
|
8
9
|
const week = labels.map((label, i) => (i === labels.length - 1 ? `${label} (hoy)` : label));
|
|
9
|
-
|
|
10
|
+
const upcoming = getMexicoUpcomingDayLabels({ days: UPCOMING_SPAN_DAYS, at });
|
|
11
|
+
return [
|
|
12
|
+
getCurrentMexicoDateTime(at),
|
|
13
|
+
`Los últimos siete días: ${week.join('; ')}.`,
|
|
14
|
+
`Los próximos días: ${upcoming.join('; ')}.`,
|
|
15
|
+
].join('\n');
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
module.exports = { buildDateAnchor };
|
|
@@ -19,6 +19,7 @@ const { logger } = require('../../utils/logger');
|
|
|
19
19
|
const CONFIG_TABLE = 'registry';
|
|
20
20
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
21
21
|
const CONFIG_CACHE_KEY = 'clinicalExtractorConfig';
|
|
22
|
+
const ENDING_STATUSES = new Set(['paused', 'completed']);
|
|
22
23
|
|
|
23
24
|
const extractorConfigCache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
|
|
24
25
|
|
|
@@ -37,6 +38,12 @@ function namesNothingSpecific(canonicalTerm, vagueWords) {
|
|
|
37
38
|
return tokens.length > 0 && tokens.every((token) => vagueWords.has(token));
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function endsNamedLine(raw, fact) {
|
|
42
|
+
return fact.category === 'treatment'
|
|
43
|
+
&& Boolean(raw.treatment_line)
|
|
44
|
+
&& ENDING_STATUSES.has(fact.observedFields?.status);
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
async function loadPatientContext(code) {
|
|
41
48
|
try {
|
|
42
49
|
const { clinicalContext, contextAsk } = await getClinicalContext(code);
|
|
@@ -115,7 +122,8 @@ async function extractClinicalMentions({ text, turnId, code, provider, detectedT
|
|
|
115
122
|
unanchored += 1;
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
|
-
if (!mention.provenance.needsConfirmation &&
|
|
125
|
+
if (!mention.provenance.needsConfirmation && !endsNamedLine(raw, mention.fact)
|
|
126
|
+
&& namesNothingSpecific(mention.fact.canonicalTerm, vagueWords)) {
|
|
119
127
|
forcedPending += 1;
|
|
120
128
|
mention = toClinicalMention(
|
|
121
129
|
{ ...raw, needs_confirmation: true, missing: raw.missing?.length ? raw.missing : ['name'] },
|
|
@@ -5,6 +5,7 @@ const { Symptoms_ID } = require('../../config/airtableConfig');
|
|
|
5
5
|
const { getMessages } = require('../../services/messageService');
|
|
6
6
|
const { DEFAULT_TIMEZONE, isWithinHourWindow } = require('../../utils/scheduleUtils');
|
|
7
7
|
const { logger } = require('../../utils/logger');
|
|
8
|
+
const { botIsPaused } = require('../../helpers/pausedThreadHelper');
|
|
8
9
|
const { PATIENT_CONTACT_WINDOW } = require('./contingencyDispatchService');
|
|
9
10
|
const { hasPendingReminderForPatient } = require('./clinicalAirtableService');
|
|
10
11
|
|
|
@@ -48,6 +49,16 @@ const AUDIT = {
|
|
|
48
49
|
turnId: 'seguimiento_turn_id',
|
|
49
50
|
};
|
|
50
51
|
|
|
52
|
+
const UNDELIVERED_STATUSES = ['failed', 'undelivered'];
|
|
53
|
+
const CONTACT_SENT = {
|
|
54
|
+
from_me: true,
|
|
55
|
+
origin: { $nin: ['instruction', 'system'] },
|
|
56
|
+
$or: [
|
|
57
|
+
{ 'statusInfo.status': { $nin: UNDELIVERED_STATUSES } },
|
|
58
|
+
{ 'statusInfo.recoveryTemplateSid': { $ne: null }, 'statusInfo.recoverySentAt': null },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
51
62
|
const RECENT_OUTBOUND_HOURS = 24;
|
|
52
63
|
const COLLIDING_REMINDER_MINUTES = 24 * 60;
|
|
53
64
|
const INACTIVITY_DAYS = 7;
|
|
@@ -226,6 +237,7 @@ async function rejectionReason(candidate, {
|
|
|
226
237
|
} = {}) {
|
|
227
238
|
const opsReason = opsOptedOut(candidate, { now });
|
|
228
239
|
if (opsReason) return opsReason;
|
|
240
|
+
if (await botIsPaused(candidate.code, { label: '[outreachSweep]' })) return 'bot_paused';
|
|
229
241
|
if (!includePreviouslySwept) {
|
|
230
242
|
const sweptReason = priorSweepReason(candidate);
|
|
231
243
|
if (sweptReason) return sweptReason;
|
|
@@ -236,7 +248,7 @@ async function rejectionReason(candidate, {
|
|
|
236
248
|
const recentOutbound = await getMessages(
|
|
237
249
|
{
|
|
238
250
|
numero: candidate.code,
|
|
239
|
-
|
|
251
|
+
...CONTACT_SENT,
|
|
240
252
|
createdAt: { $gt: since },
|
|
241
253
|
...(mode === 'monday_triage_recovery' && candidate.triageContentSid
|
|
242
254
|
? { content_sid: { $ne: candidate.triageContentSid } }
|
|
@@ -271,7 +283,7 @@ async function rejectionReason(candidate, {
|
|
|
271
283
|
const recentOutreach = await getMessages(
|
|
272
284
|
{
|
|
273
285
|
numero: candidate.code,
|
|
274
|
-
|
|
286
|
+
...CONTACT_SENT,
|
|
275
287
|
triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
|
|
276
288
|
createdAt: { $gte: inactiveSince },
|
|
277
289
|
},
|
|
@@ -283,7 +295,7 @@ async function rejectionReason(candidate, {
|
|
|
283
295
|
const lastTwoOutreaches = await getMessages(
|
|
284
296
|
{
|
|
285
297
|
numero: candidate.code,
|
|
286
|
-
|
|
298
|
+
...CONTACT_SENT,
|
|
287
299
|
triggeredBy: { $regex: '^(?:seguimiento:|followup_)' },
|
|
288
300
|
createdAt: { $gte: unansweredSince },
|
|
289
301
|
},
|
|
@@ -320,7 +332,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
|
|
|
320
332
|
{ sort: { createdAt: -1 }, limit: 1 },
|
|
321
333
|
),
|
|
322
334
|
getMessages(
|
|
323
|
-
{ numero: code,
|
|
335
|
+
{ numero: code, ...CONTACT_SENT, createdAt: { $gte: today } },
|
|
324
336
|
{ sort: { createdAt: -1 }, limit: 1 },
|
|
325
337
|
),
|
|
326
338
|
]);
|
|
@@ -331,7 +343,7 @@ async function routineOutreachCollisionReason(code, { now = new Date() } = {}) {
|
|
|
331
343
|
|
|
332
344
|
async function enrichBrief(candidate) {
|
|
333
345
|
const [lastInbound] = await getMessages({ numero: candidate.code, from_me: false }, { sort: { createdAt: -1 }, limit: 1 });
|
|
334
|
-
const [lastOutbound] = await getMessages({ numero: candidate.code,
|
|
346
|
+
const [lastOutbound] = await getMessages({ numero: candidate.code, ...CONTACT_SENT }, { sort: { createdAt: -1 }, limit: 1 });
|
|
335
347
|
const fmt = (doc) => (doc?.createdAt ? moment.tz(doc.createdAt, DEFAULT_TIMEZONE).format('YYYY-MM-DD HH:mm') : null);
|
|
336
348
|
return {
|
|
337
349
|
...candidate.brief,
|
|
@@ -20,6 +20,7 @@ const { isWithin24HourWindow } = require('../helpers/conversationWindowHelper');
|
|
|
20
20
|
const { maskCaregiverLabel, remapCaregiverSender, replyContactFor } = require('../helpers/caregiverHelper');
|
|
21
21
|
const { triggerTemplateRecovery } = require('../helpers/templateRecoveryHelper');
|
|
22
22
|
const { prepareMessageMedia } = require('../helpers/mediaHelper');
|
|
23
|
+
const { botIsPaused } = require('../helpers/pausedThreadHelper');
|
|
23
24
|
const { acquireSendClaim, completeSendClaim, releaseSendClaim } = require('../helpers/sendClaimHelper');
|
|
24
25
|
|
|
25
26
|
const { createMessagingProvider } = require('../adapters/registry');
|
|
@@ -490,6 +491,10 @@ class NexusMessaging {
|
|
|
490
491
|
logger.info('[NexusMessaging] proactive send cancelled — patient became active', { code: messageData.code });
|
|
491
492
|
return { success: false, messageId: null, status: 'patient_active', delivered: false, deferred: false };
|
|
492
493
|
}
|
|
494
|
+
if (await botIsPaused(messageData.code, { label: '[NexusMessaging]' })) {
|
|
495
|
+
logger.info('[NexusMessaging] proactive send cancelled — bot paused by the team', { code: messageData.code });
|
|
496
|
+
return { success: false, messageId: null, status: 'bot_paused', delivered: false, deferred: false };
|
|
497
|
+
}
|
|
493
498
|
}
|
|
494
499
|
if (!this.provider) throw new Error('No provider initialized');
|
|
495
500
|
|
|
@@ -1027,6 +1032,9 @@ class NexusMessaging {
|
|
|
1027
1032
|
if (delivery?.status === 'patient_active') {
|
|
1028
1033
|
return { decision: 'skipped', rationale: 'la paciente escribió durante la decisión', holdReason: 'patient_active', nextCheckAt: null, draft: null, accepted: false, delivered: false, deferred: false, deliveryStatus: 'patient_active', turnId: runResult.turnId || null, traceId: runResult.traceId || null };
|
|
1029
1034
|
}
|
|
1035
|
+
if (delivery?.status === 'bot_paused') {
|
|
1036
|
+
return { decision: 'skipped', rationale: 'el equipo pausó el bot durante la decisión', holdReason: 'bot_paused', nextCheckAt: null, draft: null, accepted: false, delivered: false, deferred: false, deliveryStatus: 'bot_paused', turnId: runResult.turnId || null, traceId: runResult.traceId || null };
|
|
1037
|
+
}
|
|
1030
1038
|
const accepted = Boolean(runResult.output) && delivery?.success === true && delivery?.deferred !== true;
|
|
1031
1039
|
const delivered = ['delivered', 'read'].includes(String(delivery?.status || '').toLowerCase());
|
|
1032
1040
|
if (!dryRun && (decision.decision === 'write') !== accepted) {
|
package/lib/utils/dateUtils.js
CHANGED
|
@@ -6,13 +6,20 @@ const nowInMexico = (at) => moment(at).tz(MEXICO_TZ).locale('es');
|
|
|
6
6
|
|
|
7
7
|
const getCurrentMexicoDateTime = (at) => nowInMexico(at).format(MEXICO_STAMP);
|
|
8
8
|
|
|
9
|
+
const dayLabel = (day, now) => (day.year() === now.year() ? day.format('dddd D [de] MMMM') : day.format('dddd D [de] MMMM [de] YYYY'));
|
|
10
|
+
|
|
9
11
|
const getMexicoDayLabels = ({ days, at }) => {
|
|
10
12
|
const now = nowInMexico(at);
|
|
11
13
|
const labels = [];
|
|
12
|
-
for (let back = days - 1; back >= 0; back -= 1)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
for (let back = days - 1; back >= 0; back -= 1) labels.push(dayLabel(now.clone().subtract(back, 'days'), now));
|
|
15
|
+
return labels;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// The days after today, nearest first; today itself is not included.
|
|
19
|
+
const getMexicoUpcomingDayLabels = ({ days, at }) => {
|
|
20
|
+
const now = nowInMexico(at);
|
|
21
|
+
const labels = [];
|
|
22
|
+
for (let ahead = 1; ahead <= days; ahead += 1) labels.push(dayLabel(now.clone().add(ahead, 'days'), now));
|
|
16
23
|
return labels;
|
|
17
24
|
};
|
|
18
25
|
|
|
@@ -83,6 +90,7 @@ module.exports = {
|
|
|
83
90
|
addWorkingMinutes,
|
|
84
91
|
getCurrentMexicoDateTime,
|
|
85
92
|
getMexicoDayLabels,
|
|
93
|
+
getMexicoUpcomingDayLabels,
|
|
86
94
|
parseDate,
|
|
87
95
|
toIsoInstant,
|
|
88
96
|
ageInYears,
|