@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 } };
|