@peopl-health/nexus 3.7.2 → 3.8.0

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.
@@ -8,7 +8,6 @@ const predictionMetricsSchema = new mongoose.Schema({
8
8
  message_id: { type: String, required: true, index: true },
9
9
  numero: { type: String, required: true, index: true },
10
10
  assistant_id: { type: String, required: true, index: true },
11
- thread_id: { type: String },
12
11
  prediction_time_ms: { type: Number },
13
12
  retry_count: { type: Number, default: 1 },
14
13
  completed: { type: Boolean, default: true },
@@ -20,6 +19,12 @@ const predictionMetricsSchema = new mongoose.Schema({
20
19
  total_tokens: { type: Number, default: 0 },
21
20
  model: { type: String },
22
21
  },
22
+ prompt_config: { type: Object, default: null },
23
+ response_id: { type: String, default: null, index: true },
24
+ context_message_count: { type: Number, default: null },
25
+ resolved_prompt: { type: String, default: null },
26
+ snippet_ids: { type: [String], default: [] },
27
+ tool_ids: { type: [String], default: [] },
23
28
  source: { type: String, default: () => process.env.USER_DB_MONGO }
24
29
  }, { timestamps: true });
25
30
 
@@ -291,6 +291,9 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
291
291
  completed: completed,
292
292
  timing_breakdown: timings,
293
293
  token_usage: tokenUsage,
294
+ prompt_config: prompt || null,
295
+ response_id: response_id || null,
296
+ context_message_count: lastMessage?.length || null,
294
297
  }).catch(err => logger.error('[replyAssistant] Failed to store metrics', { error: err.message }));
295
298
 
296
299
  const alertThreshold = parseInt(process.env.TOKEN_ALERT_THRESHOLD, 10);
@@ -0,0 +1,94 @@
1
+ const { Config_ID } = require('../config/airtableConfig');
2
+
3
+ const { logger } = require('../utils/logger');
4
+ const MapCache = require('../utils/MapCache');
5
+
6
+ const { getRecordByFilter } = require('./airtableService');
7
+
8
+ const SNIPPET_TYPE_ORDER = ['safety', 'persona', 'context', 'clinical', 'instructions'];
9
+ const CACHE_TTL = 5 * 60 * 1000;
10
+
11
+ const promptCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
12
+ const snippetCache = new MapCache({ maxSize: 200, ttl: CACHE_TTL });
13
+
14
+ async function fetchBasePrompt(promptId) {
15
+ const cacheKey = `prompt:${promptId}`;
16
+ const cached = promptCache.get(cacheKey);
17
+ if (cached) return cached;
18
+
19
+ const records = await getRecordByFilter(Config_ID, 'responses', `{prompt_id} = "${promptId}"`);
20
+ const record = records?.[0] || null;
21
+ if (record) promptCache.set(cacheKey, record);
22
+ return record;
23
+ }
24
+
25
+ async function fetchSnippets(promptId) {
26
+ const cacheKey = `snippets:${promptId}`;
27
+ const cached = snippetCache.get(cacheKey);
28
+ if (cached) return cached;
29
+
30
+ try {
31
+ const records = await getRecordByFilter(
32
+ Config_ID,
33
+ 'context_snippets',
34
+ `FIND("${promptId}", ARRAYJOIN({prompts}))`,
35
+ );
36
+ const snippets = records || [];
37
+ snippetCache.set(cacheKey, snippets);
38
+ return snippets;
39
+ } catch (error) {
40
+ logger.warn('[promptComposer] Failed to fetch snippets, continuing without them', {
41
+ promptId, error: error.message,
42
+ });
43
+ return [];
44
+ }
45
+ }
46
+
47
+ function sortSnippets(snippets) {
48
+ return [...snippets].sort((a, b) => {
49
+ const aIndex = SNIPPET_TYPE_ORDER.indexOf(a.type || '');
50
+ const bIndex = SNIPPET_TYPE_ORDER.indexOf(b.type || '');
51
+ return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex);
52
+ });
53
+ }
54
+
55
+ function applyVariables(text, variables) {
56
+ if (!variables || !text) return text || '';
57
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? '');
58
+ }
59
+
60
+ async function composePrompt({ promptId, variables = null, status = null }) {
61
+ const baseRecord = await fetchBasePrompt(promptId);
62
+ let baseContent = baseRecord?.content || '';
63
+
64
+ const snippets = await fetchSnippets(promptId);
65
+ const activeSnippets = status
66
+ ? snippets.filter(s => !s.status || s.status === status)
67
+ : snippets;
68
+
69
+ const sorted = sortSnippets(activeSnippets);
70
+ const snippetIds = sorted.map(s => s.snippet_id || s.name || 'unknown');
71
+
72
+ if (sorted.length > 0) {
73
+ const snippetTexts = sorted.map(s => s.content).filter(Boolean);
74
+ baseContent = [baseContent, ...snippetTexts].join('\n\n');
75
+ }
76
+
77
+ const resolvedPrompt = applyVariables(baseContent, variables);
78
+
79
+ logger.debug('[promptComposer] Prompt composed', {
80
+ promptId,
81
+ snippetCount: sorted.length,
82
+ snippetIds,
83
+ resolvedLength: resolvedPrompt.length,
84
+ });
85
+
86
+ return { resolvedPrompt, snippetIds };
87
+ }
88
+
89
+ function clearCache() {
90
+ promptCache.clear();
91
+ snippetCache.clear();
92
+ }
93
+
94
+ module.exports = { composePrompt, clearCache, _test: { applyVariables, sortSnippets } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.7.2",
3
+ "version": "3.8.0",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",