@peopl-health/nexus 3.1.6 → 3.2.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.
@@ -695,7 +695,9 @@ class NexusMessaging {
695
695
  async _processWithLock(chatId, existingTypingInterval = null) {
696
696
  this.processingLocks.set(chatId, true);
697
697
  let typingInterval = existingTypingInterval;
698
- let runId = null;
698
+
699
+ const runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
700
+ this.activeRequests.set(chatId, runId);
699
701
 
700
702
  try {
701
703
  if (!typingInterval) {
@@ -707,6 +709,12 @@ class NexusMessaging {
707
709
  let lastCount = messageCount;
708
710
 
709
711
  while (Date.now() - startTime < this.batchingConfig.batchWindowMs) {
712
+ if (this.abandonedRuns.has(runId)) {
713
+ logger.info(`[Batching] Run ${runId} abandoned during batching for ${chatId}`);
714
+ this.abandonedRuns.delete(runId);
715
+ return;
716
+ }
717
+
710
718
  await new Promise(resolve => setTimeout(resolve, 500));
711
719
  const newCount = await this._getUnprocessedMessageCount(chatId);
712
720
 
@@ -721,11 +729,14 @@ class NexusMessaging {
721
729
  }
722
730
  }
723
731
 
732
+ if (this.abandonedRuns.has(runId)) {
733
+ logger.info(`[CheckAfter] Run ${runId} abandoned before AI call for ${chatId}`);
734
+ this.abandonedRuns.delete(runId);
735
+ return;
736
+ }
737
+
724
738
  logger.info(`[CheckAfter] Processing ${lastCount} messages for ${chatId} after batching`);
725
739
 
726
- runId = `run_${Date.now()}_${Math.random().toString(36).substring(7)}`;
727
- this.activeRequests.set(chatId, runId);
728
-
729
740
  const result = await this._processMessages(chatId, () => replyAssistant(chatId, null, null, { runId }));
730
741
 
731
742
  if (this.abandonedRuns.has(runId)) {
@@ -16,6 +16,7 @@ function getCurRow(baseID, code) {
16
16
  const runAssistantAndWait = async ({
17
17
  thread,
18
18
  assistant,
19
+ message = null,
19
20
  runConfig = {}
20
21
  }) => {
21
22
  if (!thread || !thread.getConversationId()) {
@@ -34,6 +35,7 @@ const runAssistantAndWait = async ({
34
35
  async (currentThread = thread) => {
35
36
  return await provider.executeRun({
36
37
  thread: currentThread,
38
+ message,
37
39
  assistant,
38
40
  tools,
39
41
  config,
@@ -58,7 +60,7 @@ const runAssistantWithRetries = async (thread, assistant, runConfig, patientRepl
58
60
  'thread.id': thread.getConversationId(),
59
61
  'assistant.id': thread.getAssistantId()
60
62
  })
61
- )({ thread, assistant, runConfig });
63
+ )({ thread, assistant, runConfig, message: patientReply });
62
64
 
63
65
  const predictionTimeMs = Date.now() - startTime;
64
66
 
@@ -59,11 +59,17 @@ async function getLastMessages(code) {
59
59
  }
60
60
  }
61
61
 
62
- async function getLastNMessages(code, n) {
62
+ // Create a variable to use as a chackpoint
63
+ async function getLastNMessages(code, n, before=null) {
63
64
  try {
64
- const lastMessages = await Message.find({ numero: code })
65
+ const query = { numero: code };
66
+
67
+ if (before) query.createdAt = { $lte: before };
68
+
69
+ const lastMessages = await Message.find(query)
65
70
  .sort({ createdAt: -1 })
66
- .limit(n);
71
+ .limit(n)
72
+ .lean();
67
73
 
68
74
  if (!lastMessages || lastMessages.length === 0) {
69
75
  logger.info(`[getLastNMessages] No messages found for code: ${code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown'}, limit: ${n}`);
@@ -11,26 +11,26 @@ class DefaultMemoryManager extends MemoryManager {
11
11
  this.maxHistoricalMessages = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
12
12
  }
13
13
 
14
- async buildContext({ thread, config = {} }) {
14
+ async buildContext({ thread, message = null, config = {} }) {
15
15
  this._logActivity('Building context', { threadCode: thread.code });
16
16
 
17
17
  try {
18
- const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages);
19
- const additionalMessages = config.additionalMessages || [];
18
+ const beforeCheckpoint = message ? message.createdAt : null;
19
+ const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages, beforeCheckpoint);
20
20
 
21
21
  if (!allMessages?.length) {
22
- return additionalMessages;
22
+ return [];
23
23
  }
24
24
 
25
25
  const messageContext = allMessages.reverse().flatMap(msg => {
26
26
  const formattedContents = formatMessage(msg);
27
27
  return formattedContents.map(content => ({
28
- role: msg.origin === 'patient' ? 'user' : 'assistant',
28
+ role: msg.origin === 'instruction' ? 'developer' : msg.origin === 'patient' ? 'user' : 'assistant',
29
29
  content: content || msg.body || msg.content || ''
30
30
  }));
31
31
  }).filter(msg => msg.content);
32
32
 
33
- return [...additionalMessages, ...messageContext];
33
+ return messageContext;
34
34
  } catch (error) {
35
35
  logger.error('[DefaultMemoryManager] Context building failed', {
36
36
  threadCode: thread.code,
@@ -173,7 +173,7 @@ class OpenAIResponsesProvider {
173
173
  /**
174
174
  * Main entry point for running assistant
175
175
  */
176
- async executeRun({ thread, assistant, tools = [], config = {} }) {
176
+ async executeRun({ thread, assistant, message = null, tools = [], config = {} }) {
177
177
  const { conversationId, assistantId } = this._normalizeThread(thread);
178
178
  const promptVersion = thread?.version || null;
179
179
 
@@ -187,6 +187,7 @@ class OpenAIResponsesProvider {
187
187
  // Delegate context building to conversation manager
188
188
  const context = await this.conversationManager.buildContext({
189
189
  thread,
190
+ message,
190
191
  config: {
191
192
  ...config,
192
193
  threadId: conversationId,
@@ -201,7 +202,7 @@ class OpenAIResponsesProvider {
201
202
  logger.info('[OpenAIResponsesProvider] Context built', {
202
203
  conversationId,
203
204
  assistantId,
204
- lastContext: context[-1] || null
205
+ context
205
206
  });
206
207
 
207
208
  const filter = thread.code ? { code: thread.code, active: true } : null;
@@ -292,7 +293,7 @@ class OpenAIResponsesProvider {
292
293
 
293
294
  const promptConfig = { id: assistantId };
294
295
  if (promptVariables) promptConfig.variables = promptVariables;
295
- if (promptVersion) promptConfig.version = promptVersion;
296
+ if (promptVersion) promptConfig.version = String(promptVersion);
296
297
  logger.info('[OpenAIResponsesProvider] Prompt config', { promptConfig });
297
298
 
298
299
  const makeAPICall = (inputData) => retryWithBackoff(() =>
@@ -112,14 +112,8 @@ const addInstructionCore = async (code, instruction, role = 'system') => {
112
112
 
113
113
  try {
114
114
  const assistant = getAssistantById(thread.getAssistantId(), thread);
115
- const runResult = await runAssistantWithRetries(thread, assistant, {
116
- additionalInstructions: instruction,
117
- additionalMessages: [
118
- { role: role, content: instruction }
119
- ]
120
- });
121
115
 
122
- // Save instruction message to database for frontend visibility
116
+ // Save instruction message to database
123
117
  try {
124
118
  const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
125
119
  await insertMessage({
@@ -137,6 +131,13 @@ const addInstructionCore = async (code, instruction, role = 'system') => {
137
131
  logger.error('[addInstructionCore] Error saving instruction message', { err });
138
132
  }
139
133
 
134
+ const runResult = await runAssistantWithRetries(thread, assistant, {
135
+ additionalInstructions: instruction,
136
+ additionalMessages: [
137
+ { role: role, content: instruction }
138
+ ]
139
+ });
140
+
140
141
  logger.info('[addInstructionCore] Run response', { output: runResult?.output });
141
142
  return runResult?.output || null;
142
143
  } catch (error) {
@@ -158,7 +159,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
158
159
  const finalThread = thread;
159
160
 
160
161
  const messagesStart = Date.now();
161
- const lastMessage = await getLastNMessages(code, 1);
162
+ const beforeCheckpoint = message_?.createdAt ?
163
+ (message_.createdAt.$date ? new Date(message_.createdAt.$date) : message_.createdAt) : null;
164
+ const lastMessage = await getLastNMessages(code, 1, beforeCheckpoint);
162
165
  timings.get_messages_ms = Date.now() - messagesStart;
163
166
 
164
167
  if (!lastMessage || lastMessage.length === 0 || lastMessage[0].from_me) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.1.6",
3
+ "version": "3.2.1",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -1,80 +0,0 @@
1
- # Migration Guide
2
-
3
- This guide summarizes changes introduced in the current Nexus library refresh to help you migrate quickly with minimal breakage.
4
-
5
- ## TL;DR
6
- - Twilio remains first‑class; Baileys is supported for messaging but not for templates/flows (returns clear errors).
7
- - Message sends accept both `to` (preferred) and legacy `code` (normalized internally).
8
- - Templates and flows are supported through Twilio Content API; provider is auto‑injected into template controllers on `Nexus.initialize()` when Twilio is active.
9
- - New event bus + middleware model; existing handlers continue to work.
10
- - Storage adapter registry; use built‑in `mongo`/`noop` or plug your own adapter or instance.
11
- - Interactive/flows now have a provider‑agnostic API with Twilio mapping and event‑driven routing.
12
- - Assistant registry with optional override for `getAssistantById`.
13
-
14
- ## Messaging API
15
- - BREAKING (soft): Prefer `to` over `code` in `sendMessage`. Legacy `code` is still accepted and normalized internally.
16
-
17
- Before:
18
- ```js
19
- await nexus.sendMessage({ code: 'whatsapp:+521555...', message: 'Hi' });
20
- ```
21
- After (preferred):
22
- ```js
23
- await nexus.sendMessage({ code: '+521555...', message: 'Hi' });
24
- ```
25
-
26
- ## Templates & Flows (Twilio)
27
- - Twilio provider now implements content operations: `listTemplates`, `getTemplate`, `createTemplate`, `deleteTemplate`, `submitForApproval`, `checkApprovalStatus`.
28
- - On `Nexus.initialize()` with Twilio, the provider is auto‑injected into template controllers — default routes under `/api/template` work immediately.
29
- - Baileys: content/template operations are not supported; a clear error is thrown if called.
30
-
31
- ## Interactive & Flows (Provider‑agnostic)
32
- - New helper APIs:
33
- - `registerFlow(id, spec)`, `sendInteractive(nexus, { to, id | spec, variables })`.
34
- - Twilio: spec is converted to Content API payload and sent; if `spec.contentSid` is provided, it is used directly.
35
- - Baileys: currently unsupported.
36
- - Event‑driven routing:
37
- - `registerInteractiveHandler(match, handler)` and `attachInteractiveRouter(nexus)`; handlers are called when interactive messages are received.
38
-
39
- ## Middleware & Events
40
- - New event bus on `NexusMessaging`: subscribe to `*:received` and `*:handled` (message, media, interactive, command, keyword, flow).
41
- - New middleware: `nexus.getMessaging().use(type?, async (msg, nexus, next) => { ... })`.
42
- - Existing `setHandlers` and `onMessage`/`onInteractive`/etc. continue to work.
43
-
44
- ## Storage
45
- - Storage is now pluggable via a registry:
46
- - Built‑ins: `mongo` (default), `noop`.
47
- - Register your adapter: `registerStorage('src', MyStorageClass)` then `storage: 'src'`.
48
- - Or pass an instance: `storage: new MyStorageClass()`.
49
- - If the adapter has `connect()`, Nexus calls it automatically.
50
- - Controllers that rely on your Mongo models can continue to do so — no change required.
51
-
52
- ## Assistants
53
- - Register assistant classes at init:
54
- ```js
55
- await nexus.initialize({
56
- provider: 'twilio',
57
- llm: 'openai', llmConfig: { apiKey: process.env.OPENAI_API_KEY },
58
- assistants: {
59
- registry: { SUPPORT: SupportAssistantClass, SALES: SalesAssistantClass },
60
- getAssistantById: (id, thread) => null // optional override
61
- }
62
- });
63
- ```
64
- - An internal override for `getAssistantById` is supported; if provided, it is tried first, then fallback to registry.
65
-
66
- ## Utilities & Fixes
67
- - Utils index corrected to export existing files: `{ DefaultLLMProvider, MessageParser, logger }`.
68
- - Default OpenAI import fixed for CommonJS: `const OpenAI = require('openai');`.
69
- - Message model helpers unified; `LegacyMessage` references replaced with `Message` and a new exported `insertMessage`.
70
- - Types `declare module` now matches package name: `@peopl-health/nexus`.
71
-
72
- ## Routes
73
- - Built‑in route bundles remain available and importable via `setupDefaultRoutes(app)` or per‑group via `routes` + `createRouter()`.
74
-
75
- ## Testing
76
- - If your environment restricts forking, run Jest in‑band: `jest --runInBand`.
77
-
78
- ## Notes
79
- - Twilio approvals data shape varies by account/region. The provider looks up `content.links` and falls back to the documented REST path; data is normalized where possible.
80
- - Baileys: templates/flows remain unsupported; messaging and media send/receive are supported.