@peopl-health/nexus 3.8.4 → 3.8.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.
@@ -2,7 +2,9 @@ const { Monitoreo_ID, Symptoms_ID } = require('../config/airtableConfig');
2
2
 
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const { getRecordByFilter } = require('../services/airtableService');
5
+ const { Thread } = require('../models/threadModel');
6
+
7
+ const { getRecordByFilter, updateRecordByFilter } = require('../services/airtableService');
6
8
 
7
9
  const getPatientInfoController = async (req, res) => {
8
10
  try {
@@ -87,4 +89,42 @@ const getPatientTriageInfoController = async (req, res) => {
87
89
  }
88
90
  };
89
91
 
90
- module.exports = { getPatientInfoController, getPatientTriageInfoController };
92
+ const updatePatientController = async (req, res) => {
93
+ try {
94
+ const { id } = req.params;
95
+ if (!id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
96
+ if (!req.body || Object.keys(req.body).length === 0)
97
+ return res.status(400).json({ success: false, error: 'No fields provided for update' });
98
+
99
+ const filter = `{whatsapp_id}='${id}'`;
100
+
101
+ const existing = await getRecordByFilter(Monitoreo_ID, 'patient_information', filter);
102
+ if (!existing || existing.length === 0)
103
+ return res.status(404).json({ success: false, error: 'Patient not found' });
104
+
105
+ const validFieldNames = Object.keys(existing[0]);
106
+ const fields = {};
107
+ for (const key of Object.keys(req.body)) {
108
+ if (validFieldNames.includes(key)) {
109
+ fields[key] = req.body[key];
110
+ }
111
+ }
112
+
113
+ if (Object.keys(fields).length === 0)
114
+ return res.status(400).json({ success: false, error: 'No valid fields provided for update' });
115
+
116
+ await updateRecordByFilter(Monitoreo_ID, 'patient_information', filter, fields);
117
+
118
+ if (fields.name) {
119
+ await Thread.updateOne({ code: id }, { nombre: fields.name });
120
+ }
121
+
122
+ const updated = await getRecordByFilter(Monitoreo_ID, 'patient_information', filter);
123
+ res.status(200).json({ success: true, whatsappId: id, patientInfo: updated[0] });
124
+ } catch (error) {
125
+ logger.error('Error updating patient info:', { error: error.message, id: req.params?.id });
126
+ res.status(500).json({ success: false, error: error.message });
127
+ }
128
+ };
129
+
130
+ module.exports = { getPatientInfoController, getPatientTriageInfoController, updatePatientController };
@@ -8,6 +8,7 @@ const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
8
8
  const { OpenAIResponsesProvider } = require('../providers/OpenAIResponsesProvider');
9
9
  const { handleFunctionCalls } = require('../providers/OpenAIResponsesProviderTools');
10
10
  const { composePrompt, resolveTools } = require('../services/promptComposerService');
11
+ const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
11
12
  const { getAssistantById } = require('../services/assistantResolver');
12
13
 
13
14
  const MAX_FUNCTION_ROUNDS = parseInt(process.env.MAX_FUNCTION_ROUNDS || '5', 10);
@@ -122,18 +123,22 @@ class EvalProvider {
122
123
  if (this.mode !== 'context-only' && assistantId) {
123
124
  try {
124
125
  assistant = getAssistantById(assistantId, thread);
125
- toolSchemas = assistant.getToolSchemas?.() || [];
126
- if (assistant.tools?.size) {
126
+ const assistantSchemas = assistant.getToolSchemas?.() || [];
127
+ const registrySchemas = getRegistrySchemas();
128
+ const schemasByName = new Map();
129
+ for (const s of assistantSchemas) schemasByName.set(s.function?.name, s);
130
+ for (const s of registrySchemas) schemasByName.set(s.function?.name, s);
131
+ toolSchemas = Array.from(schemasByName.values());
132
+
133
+ if (toolSchemas.length > 0) {
127
134
  const { toolIds, filtered } = await resolveTools({ promptId: assistantId, assistant });
128
- const activeToolNames = filtered && toolIds.length > 0
129
- ? toolIds
130
- : Array.from(assistant.tools.keys());
131
- const toolNames = activeToolNames.join(', ');
132
- devContent += `\n\nYou only have access to these tools: ${toolNames}. Do not call or reference any tools not listed here.`;
135
+ const activeToolSchemas = filtered && toolIds.length > 0
136
+ ? toolSchemas.filter(s => toolIds.includes(s.function?.name))
137
+ : toolSchemas;
133
138
 
134
- if (filtered && toolIds.length > 0) {
135
- toolSchemas = toolSchemas.filter(s => toolIds.includes(s.function?.name));
136
- }
139
+ toolSchemas = activeToolSchemas;
140
+ const toolNames = activeToolSchemas.map(s => s.function?.name).join(', ');
141
+ devContent += `\n\nYou only have access to these tools: ${toolNames}. Do not call or reference any tools not listed here.`;
137
142
  }
138
143
  } catch {
139
144
  logger.warn('[NexusEvalProvider] Failed to resolve assistant', { assistantId });
package/lib/index.js CHANGED
@@ -261,4 +261,5 @@ module.exports = {
261
261
  mongoGetConnection,
262
262
  EvalProvider,
263
263
  airtableService,
264
+
264
265
  };
@@ -9,6 +9,7 @@ const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
9
9
  const { getLastNMessages } = require('../helpers/messageHelper');
10
10
 
11
11
  const { composePrompt, resolveTools } = require('../services/promptComposerService');
12
+ const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
12
13
  const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
13
14
 
14
15
  const CONVERSATION_PREFIX = 'conv_';
@@ -318,12 +319,21 @@ class OpenAIResponsesProvider {
318
319
  });
319
320
 
320
321
  const { toolIds, filtered } = await resolveTools({ promptId: assistantId, assistant });
322
+
323
+ const assistantSchemas = assistant?.getToolSchemas?.() || [];
324
+ const registrySchemas = getRegistrySchemas();
325
+ const schemasByName = new Map();
326
+ for (const s of assistantSchemas) schemasByName.set(s.function?.name, s);
327
+ for (const s of registrySchemas) schemasByName.set(s.function?.name, s);
328
+ const allSchemas = Array.from(schemasByName.values());
329
+
330
+ const activeToolSchemas = filtered && toolIds.length > 0
331
+ ? allSchemas.filter(s => toolIds.includes(s.function?.name))
332
+ : allSchemas;
333
+
321
334
  let devContent = resolvedPrompt;
322
- if (assistant?.tools?.size) {
323
- const activeToolNames = filtered && toolIds.length > 0
324
- ? toolIds
325
- : Array.from(assistant.tools.keys());
326
- const toolNames = activeToolNames.join(', ');
335
+ if (activeToolSchemas.length > 0) {
336
+ const toolNames = activeToolSchemas.map(s => s.function?.name).join(', ');
327
337
  devContent += `\n\nYou only have access to these tools: ${toolNames}. Do not call or reference any tools not listed here.`;
328
338
  }
329
339
 
@@ -335,11 +345,6 @@ class OpenAIResponsesProvider {
335
345
  if (promptVariables) promptConfig.variables = promptVariables;
336
346
  if (promptVersion) promptConfig.version = String(promptVersion);
337
347
 
338
- const toolSchemas = assistant?.getToolSchemas?.() || [];
339
- const activeToolSchemas = filtered && toolIds.length > 0
340
- ? toolSchemas.filter(s => toolIds.includes(s.function?.name))
341
- : toolSchemas;
342
-
343
348
  const apiCallConfig = {
344
349
  prompt: promptConfig,
345
350
  instructions: instructions || additionalInstructions || '',
@@ -61,7 +61,8 @@ const interactionRouteDefinitions = {
61
61
 
62
62
  const patientRouteDefinitions = {
63
63
  'GET /:id': 'getPatientInfoController',
64
- 'GET /triage/:whatsapp_id': 'getPatientTriageInfoController'
64
+ 'GET /triage/:whatsapp_id': 'getPatientTriageInfoController',
65
+ 'PATCH /:id': 'updatePatientController'
65
66
  };
66
67
 
67
68
  const templateRouteDefinitions = {
@@ -170,6 +171,7 @@ const builtInControllers = {
170
171
  // Patient controllers
171
172
  getPatientInfoController: patientController.getPatientInfoController,
172
173
  getPatientTriageInfoController: patientController.getPatientTriageInfoController,
174
+ updatePatientController: patientController.updatePatientController,
173
175
 
174
176
  // Template controllers
175
177
  createTemplate: templateController.createTemplate,
@@ -4,6 +4,7 @@ const { logger } = require('../utils/logger');
4
4
  const MapCache = require('../utils/MapCache');
5
5
 
6
6
  const { getRecordByFilter } = require('./airtableService');
7
+ const { hasTool, getRegisteredToolNames } = require('./toolRegistryService');
7
8
 
8
9
  const SNIPPET_TYPE_ORDER = ['safety', 'persona', 'context', 'clinical', 'instructions'];
9
10
  const CACHE_TTL = 5 * 60 * 1000;
@@ -115,7 +116,10 @@ async function fetchToolMapping(promptId) {
115
116
  }
116
117
 
117
118
  async function resolveTools({ promptId, assistant, status = null }) {
118
- if (!assistant?.tools?.size) return { toolIds: [], filtered: false };
119
+ const hasRegistryTools = getRegisteredToolNames().length > 0;
120
+ const hasAssistantTools = assistant?.tools?.size > 0;
121
+
122
+ if (!hasRegistryTools && !hasAssistantTools) return { toolIds: [], filtered: false };
119
123
 
120
124
  const mappedTools = await fetchToolMapping(promptId);
121
125
  if (!mappedTools.length) return { toolIds: [], filtered: false };
@@ -124,12 +128,11 @@ async function resolveTools({ promptId, assistant, status = null }) {
124
128
  ? mappedTools.filter(t => !t.status || t.status === status)
125
129
  : mappedTools;
126
130
 
127
- const registeredNames = new Set(assistant.tools.keys());
128
131
  const validToolIds = [];
129
132
 
130
133
  for (const tool of activeTools) {
131
134
  const id = tool.tool_id;
132
- if (registeredNames.has(id)) {
135
+ if (hasTool(id) || assistant?.tools?.has(id)) {
133
136
  validToolIds.push(id);
134
137
  } else {
135
138
  logger.warn('[promptComposer] Tool mapped in Airtable but not registered in code', {
@@ -0,0 +1,77 @@
1
+ const { logger } = require('../utils/logger');
2
+
3
+ const registry = new Map();
4
+
5
+ function registerTool(config) {
6
+ const { name, handler, description, parameters, strict } = config;
7
+
8
+ if (!name || typeof name !== 'string') {
9
+ throw new Error('registerTool requires a string name');
10
+ }
11
+ if (typeof handler !== 'function') {
12
+ throw new Error(`registerTool '${name}' requires a handler function`);
13
+ }
14
+
15
+ if (registry.has(name)) {
16
+ logger.warn('[toolRegistry] Overwriting existing tool', { name });
17
+ }
18
+
19
+ const toolParams = parameters
20
+ ? { ...parameters }
21
+ : { type: 'object', properties: {} };
22
+
23
+ if (toolParams.additionalProperties === undefined) {
24
+ toolParams.additionalProperties = false;
25
+ }
26
+
27
+ registry.set(name, {
28
+ schema: {
29
+ type: 'function',
30
+ function: {
31
+ name,
32
+ description: description || 'Custom assistant tool',
33
+ parameters: toolParams,
34
+ ...(strict !== undefined && { strict })
35
+ }
36
+ },
37
+ execute: handler,
38
+ });
39
+ }
40
+
41
+ function getToolSchemas(names) {
42
+ if (names && names.length > 0) {
43
+ return names
44
+ .filter(n => registry.has(n))
45
+ .map(n => registry.get(n).schema);
46
+ }
47
+ return Array.from(registry.values()).map(t => t.schema);
48
+ }
49
+
50
+ async function executeTool(name, args) {
51
+ const tool = registry.get(name);
52
+ if (!tool) {
53
+ throw new Error(`Tool '${name}' is not registered`);
54
+ }
55
+ return await tool.execute(args);
56
+ }
57
+
58
+ function hasTool(name) {
59
+ return registry.has(name);
60
+ }
61
+
62
+ function getRegisteredToolNames() {
63
+ return Array.from(registry.keys());
64
+ }
65
+
66
+ function clearRegistry() {
67
+ registry.clear();
68
+ }
69
+
70
+ module.exports = {
71
+ registerTool,
72
+ getToolSchemas,
73
+ executeTool,
74
+ hasTool,
75
+ getRegisteredToolNames,
76
+ clearRegistry,
77
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.8.4",
3
+ "version": "3.8.6",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",