@peopl-health/nexus 5.67.0-dev.11930 → 5.67.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.
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
const { Config_ID } = require('../../config/airtableConfig');
|
|
2
|
+
const { fieldEquals } = require('../../helpers/airtableFormulaHelper');
|
|
3
|
+
const { getRecordByFilter } = require('../../services/airtableService');
|
|
1
4
|
const { safeParse, isPlainObject } = require('../../utils/jsonUtils');
|
|
2
5
|
const { logger } = require('../../utils/logger');
|
|
3
6
|
const { sanitizeInlineText } = require('../../utils/sanitizerUtils');
|
|
4
7
|
const { withTimeout } = require('../../utils/timeoutUtils');
|
|
5
|
-
const { composePrompt } = require('./promptComposerService');
|
|
6
8
|
|
|
7
9
|
const NUTRITION_GUIDE_PROMPT_ID = 'nutrition_plan_guide';
|
|
8
10
|
const PLAN_MAX_CHARS = 1500;
|
|
9
11
|
const GUIDE_MAX_CHARS = 2000;
|
|
10
12
|
const ISO_DATE_RX = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
13
|
const GUIDE_TIMEOUT_MS = 3000;
|
|
14
|
+
const GUIDE_REFRESH_MS = 5 * 60 * 1000;
|
|
15
|
+
const GUIDE_RETRY_MS = 30 * 1000;
|
|
12
16
|
|
|
13
17
|
// Each line of a plan or guide is one kind of indication, so the breaks survive and only each line is flattened.
|
|
14
18
|
function keepLines(text, maxChars) {
|
|
@@ -32,43 +36,42 @@ function readPlanSection(section) {
|
|
|
32
36
|
return { text: typeof text === 'string' ? text : '', reviewBy };
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
// The guide is served from memory and refreshed in the background: only the very first read can hold a
|
|
40
|
+
// turn (up to GUIDE_TIMEOUT_MS), and there is never more than one Airtable read in flight.
|
|
41
|
+
let guide = { text: '', freshUntil: 0 };
|
|
36
42
|
let guideRead = null;
|
|
37
|
-
let readSeq = 0;
|
|
38
|
-
let appliedSeq = 0;
|
|
39
43
|
|
|
40
|
-
function
|
|
44
|
+
function refreshGuide() {
|
|
41
45
|
if (guideRead) return guideRead;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.then((
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return lastGuide;
|
|
46
|
+
// Read straight from Airtable: the in-memory copy is this guide's only cache.
|
|
47
|
+
guideRead = getRecordByFilter(Config_ID, 'responses', fieldEquals('prompt_id', NUTRITION_GUIDE_PROMPT_ID))
|
|
48
|
+
.then((records) => {
|
|
49
|
+
const text = keepLines(records?.[0]?.content || '', GUIDE_MAX_CHARS);
|
|
50
|
+
// A missing row reads as empty: keep the last guide and look again soon rather than in five minutes.
|
|
51
|
+
guide = text
|
|
52
|
+
? { text, freshUntil: Date.now() + GUIDE_REFRESH_MS }
|
|
53
|
+
: { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
51
54
|
})
|
|
52
55
|
.catch((error) => {
|
|
53
|
-
logger.warn('[nutritionPlanService] nutrition guide unreadable;
|
|
54
|
-
|
|
56
|
+
logger.warn('[nutritionPlanService] nutrition guide unreadable; keeping the last one read', { error: error.message });
|
|
57
|
+
guide = { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
55
58
|
})
|
|
56
|
-
.finally(() => {
|
|
57
|
-
guideRead
|
|
58
|
-
return read;
|
|
59
|
+
.finally(() => { guideRead = null; });
|
|
60
|
+
return guideRead;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
// The guide only adds to the plan, so a slow read must not hold the patient's turn,
|
|
62
|
-
// and a read that never settles must not hold every later turn either.
|
|
63
63
|
async function loadNutritionGuide() {
|
|
64
|
-
|
|
64
|
+
if (guide.freshUntil) {
|
|
65
|
+
if (Date.now() >= guide.freshUntil) refreshGuide();
|
|
66
|
+
return guide.text;
|
|
67
|
+
}
|
|
65
68
|
try {
|
|
66
|
-
|
|
69
|
+
await withTimeout(refreshGuide(), GUIDE_TIMEOUT_MS, 'nutrition guide read');
|
|
67
70
|
} catch {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return lastGuide;
|
|
71
|
+
logger.warn('[nutritionPlanService] first nutrition guide read timed out; plan sent without it for now', { timeoutMs: GUIDE_TIMEOUT_MS });
|
|
72
|
+
if (!guide.freshUntil) guide = { ...guide, freshUntil: Date.now() + GUIDE_RETRY_MS };
|
|
71
73
|
}
|
|
74
|
+
return guide.text;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
@@ -87,6 +90,8 @@ async function resolveNutritionPlan(section, { now = new Date() } = {}) {
|
|
|
87
90
|
module.exports = {
|
|
88
91
|
NUTRITION_GUIDE_PROMPT_ID,
|
|
89
92
|
GUIDE_TIMEOUT_MS,
|
|
93
|
+
GUIDE_REFRESH_MS,
|
|
94
|
+
GUIDE_RETRY_MS,
|
|
90
95
|
resolveNutritionPlan,
|
|
91
|
-
_test: { reset: () => {
|
|
96
|
+
_test: { reset: () => { guide = { text: '', freshUntil: 0 }; guideRead = null; } },
|
|
92
97
|
};
|
|
@@ -14,8 +14,6 @@ const promptCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
|
14
14
|
const snippetCache = new MapCache({ maxSize: 200, ttl: CACHE_TTL });
|
|
15
15
|
const toolCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
16
16
|
const presetCache = new MapCache({ maxSize: 50, ttl: CACHE_TTL });
|
|
17
|
-
let promptReadClock = 0;
|
|
18
|
-
let promptCacheEpoch = 0;
|
|
19
17
|
|
|
20
18
|
async function fetchConfigRecords(table, filter) {
|
|
21
19
|
const records = await getRecordByFilter(Config_ID, table, filter);
|
|
@@ -49,17 +47,11 @@ async function fetchPreset(presetId, version = null) {
|
|
|
49
47
|
async function fetchBasePrompt(promptId) {
|
|
50
48
|
const cacheKey = `prompt:${promptId}`;
|
|
51
49
|
const cached = promptCache.get(cacheKey);
|
|
52
|
-
if (cached) return cached
|
|
50
|
+
if (cached) return cached;
|
|
53
51
|
|
|
54
|
-
// A result may fill the cache unless a read started later already did, or the cache was cleared meanwhile.
|
|
55
|
-
const startedAt = ++promptReadClock;
|
|
56
|
-
const epoch = promptCacheEpoch;
|
|
57
52
|
const records = await getRecordByFilter(Config_ID, 'responses', `{prompt_id} = "${promptId}"`);
|
|
58
53
|
const record = records?.[0] || null;
|
|
59
|
-
|
|
60
|
-
if (record && epoch === promptCacheEpoch && !(current && current.startedAt > startedAt)) {
|
|
61
|
-
promptCache.set(cacheKey, { record, startedAt });
|
|
62
|
-
}
|
|
54
|
+
if (record) promptCache.set(cacheKey, record);
|
|
63
55
|
return record;
|
|
64
56
|
}
|
|
65
57
|
|
|
@@ -265,7 +257,6 @@ async function resolveTools({ promptId, assistant, presetToolIds = null, status
|
|
|
265
257
|
|
|
266
258
|
function clearCache() {
|
|
267
259
|
promptCache.clear();
|
|
268
|
-
promptCacheEpoch += 1;
|
|
269
260
|
snippetCache.clear();
|
|
270
261
|
toolCache.clear();
|
|
271
262
|
presetCache.clear();
|