@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.
- package/lib/core/NexusMessaging.js +15 -4
- package/lib/helpers/assistantHelper.js +3 -1
- package/lib/helpers/messageHelper.js +9 -3
- package/lib/memory/DefaultMemoryManager.js +6 -6
- package/lib/providers/OpenAIResponsesProvider.js +4 -3
- package/lib/services/assistantServiceCore.js +11 -8
- package/package.json +1 -1
- package/MIGRATION_GUIDE.md +0 -80
|
@@ -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
|
-
|
|
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
|
-
|
|
62
|
+
// Create a variable to use as a chackpoint
|
|
63
|
+
async function getLastNMessages(code, n, before=null) {
|
|
63
64
|
try {
|
|
64
|
-
const
|
|
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
|
|
19
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/MIGRATION_GUIDE.md
DELETED
|
@@ -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.
|