@peopl-health/nexus 5.1.0-dev.469 → 6.0.0-dev.466

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.
@@ -44,17 +44,6 @@ turnTraceSchema.methods.addStep = function addStep(step) { this.steps.push({ ...
44
44
  turnTraceSchema.methods.setStatus = function setStatus(status, abort = null) { this.status = status; this.abort = abort; };
45
45
  turnTraceSchema.methods.setTimings = function setTimings(timings) { this.timings = { ...this.timings, ...timings }; };
46
46
 
47
- turnTraceSchema.methods.toDecision = function toDecision() {
48
- const decision = this.safetyDecision || {};
49
- const signals = this.signals || {};
50
- return {
51
- escalation_kind: decision.escalationKind || null,
52
- outbound_purpose: signals.outboundPurpose || null,
53
- branch_taken: signals.branchTaken || null,
54
- red_flags_matched: decision.redFlagsMatched || [],
55
- };
56
- };
57
-
58
47
  turnTraceSchema.methods.setPrompt = function setPrompt({ resolved, resolvedPresetId, resolvedPresetVersion }) {
59
48
  this.prompt = {
60
49
  resolved: resolved || null,
@@ -1,22 +1,24 @@
1
1
  const { load } = require('../config/fhirConfig');
2
- const { createProjectorRegistry } = require('../helpers/projectorHelper');
3
2
 
4
- const registry = createProjectorRegistry({
5
- label: 'FHIR projector',
6
- assemble: (items) => {
7
- const entry = items
8
- .flatMap(({ content }) => (content || []).flat())
9
- .map((resource) => ({ resource }));
10
- return { resourceType: 'Bundle', type: 'collection', entry };
11
- },
12
- });
3
+ const projectors = new Map();
4
+
5
+ function registerProjector(name, fn) {
6
+ projectors.set(name, fn);
7
+ }
13
8
 
14
9
  async function project(items) {
15
10
  await load();
16
- return registry.project(items);
11
+ const entry = items
12
+ .flatMap(({ name, aggregate }) => {
13
+ const fn = projectors.get(name);
14
+ if (!fn) throw new Error(`no FHIR projector registered for '${name}'`);
15
+ return (fn(aggregate) || []).flat();
16
+ })
17
+ .map((resource) => ({ resource }));
18
+ return { resourceType: 'Bundle', type: 'collection', entry };
17
19
  }
18
20
 
19
21
  module.exports = {
20
- registerProjector: registry.register,
22
+ registerProjector,
21
23
  project,
22
24
  };
@@ -9,12 +9,6 @@ function jsonStringifyWithUnicodeEscapes(obj) {
9
9
  });
10
10
  }
11
11
 
12
- function isPlainObject(value) {
13
- if (value === null || typeof value !== 'object') return false;
14
- const proto = Object.getPrototypeOf(value);
15
- return proto === Object.prototype || proto === null;
16
- }
17
-
18
12
  function safeParse(value, fallback = {}) {
19
13
  try {
20
14
  return JSON.parse(value || JSON.stringify(fallback));
@@ -23,31 +17,4 @@ function safeParse(value, fallback = {}) {
23
17
  }
24
18
  }
25
19
 
26
- function safeParseArray(value, fallback = null) {
27
- const parsed = safeParse(value, fallback);
28
- return Array.isArray(parsed) ? parsed : fallback;
29
- }
30
-
31
- function cleanForJson(value) {
32
- if (value === null || value === undefined) return value;
33
- if (value instanceof Date) return value.toISOString();
34
- if (typeof value === 'object' && typeof value.toHexString === 'function') return value.toHexString();
35
- if (Array.isArray(value)) return value.map(cleanForJson);
36
- if (typeof value === 'object') {
37
- const out = {};
38
- for (const [key, val] of Object.entries(value)) {
39
- if (key === '_id' || key === '__v') continue;
40
- out[key] = cleanForJson(val);
41
- }
42
- return out;
43
- }
44
- return value;
45
- }
46
-
47
- module.exports = {
48
- jsonStringifyWithUnicodeEscapes,
49
- safeParse,
50
- safeParseArray,
51
- cleanForJson,
52
- isPlainObject,
53
- };
20
+ module.exports = { jsonStringifyWithUnicodeEscapes, safeParse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.1.0-dev.469",
3
+ "version": "6.0.0-dev.466",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -1,58 +0,0 @@
1
- const { Config_ID } = require('../../config/airtableConfig');
2
-
3
- const { getRecordByFilter } = require('../../services/airtableService');
4
-
5
- const MapCache = require('../../utils/MapCache');
6
- const { safeParse, safeParseArray, isPlainObject } = require('../../utils/jsonUtils');
7
-
8
- const DIVERGENCE_CONFIG_TABLE = 'divergence';
9
- const CACHE_TTL = 5 * 60 * 1000;
10
- const CACHE_KEY = 'divergenceConfig';
11
- const COMPARATOR_TYPES = ['scalar', 'set'];
12
- const ORACLE_CONNECTION_DEFAULTS = { enabled: false, baseUrl: '', timeoutMs: null };
13
-
14
- const cache = new MapCache({ maxSize: 1, ttl: CACHE_TTL });
15
-
16
- async function load() {
17
- const cached = cache.get(CACHE_KEY);
18
- if (cached) return cached;
19
-
20
- const records = await getRecordByFilter(Config_ID, DIVERGENCE_CONFIG_TABLE, 'TRUE()');
21
- if (!records) throw new Error('divergence config could not be read from the Config base');
22
- const byKey = Object.fromEntries(records.map((r) => [r.config, r.value]));
23
-
24
- if (!byKey.FIELDS) throw new Error('divergence config missing from Config base: FIELDS');
25
- const fields = safeParseArray(byKey.FIELDS);
26
- if (!fields) throw new Error('divergence config FIELDS is not a JSON array');
27
- if (!fields.length) throw new Error('divergence config FIELDS is empty; nothing would be compared');
28
-
29
- const unknown = fields.filter(({ type }) => !COMPARATOR_TYPES.includes(type));
30
- if (unknown.length) throw new Error(`divergence config FIELDS has unknown types: ${unknown.map(({ field, type }) => `${field}:${type}`).join(', ')}`);
31
-
32
- const misplaced = fields.filter(({ type, emergencyKinds }) => emergencyKinds && type !== 'scalar');
33
- if (misplaced.length) throw new Error(`divergence config emergencyKinds only applies to scalar fields: ${misplaced.map(({ field }) => field).join(', ')}`);
34
-
35
- const parsedOracle = safeParse(byKey.ORACLE_CONNECTION, null);
36
- if (byKey.ORACLE_CONNECTION && !isPlainObject(parsedOracle)) throw new Error('divergence config ORACLE_CONNECTION is not a JSON object');
37
- const oracleConnection = { ...ORACLE_CONNECTION_DEFAULTS, ...(parsedOracle || {}) };
38
- if (oracleConnection.enabled) {
39
- oracleConnection.timeoutMs = Number(oracleConnection.timeoutMs);
40
- if (!Number.isFinite(oracleConnection.timeoutMs) || oracleConnection.timeoutMs <= 0) throw new Error('divergence config ORACLE_CONNECTION.timeoutMs is not a positive number');
41
- }
42
-
43
- const config = { FIELDS: fields, ORACLE_CONNECTION: oracleConnection };
44
- cache.set(CACHE_KEY, config);
45
- return config;
46
- }
47
-
48
- function current() {
49
- const cached = cache.get(CACHE_KEY);
50
- if (!cached) throw new Error('divergence config not loaded; call load() first');
51
- return cached;
52
- }
53
-
54
- module.exports = {
55
- load,
56
- getFields: () => current().FIELDS,
57
- getOracleConnection: () => current().ORACLE_CONNECTION,
58
- };
@@ -1,52 +0,0 @@
1
- const { isDeepStrictEqual } = require('node:util');
2
-
3
- const divergenceConfig = require('../config/divergenceConfig');
4
-
5
- const COMPARATORS = {
6
- scalar: (expected, actual) => expected === actual,
7
- set: (expected, actual) => isDeepStrictEqual(new Set(expected || []), new Set(actual || [])),
8
- };
9
-
10
- const OUTCOME_RULES = [
11
- [({ deterministic }) => deterministic.divergences.some((d) => d.isEmergencyFlip), 'review'],
12
- [({ stochastic }) => stochastic.verdict === 'fail', 'fail'],
13
- [({ deterministic }) => !deterministic.hasPassed, 'diverged'],
14
- [({ stochastic }) => stochastic.verdict === 'review', 'review'],
15
- ];
16
-
17
- function deterministicDivergence(expected = {}, actual = {}, fields) {
18
- const divergences = fields
19
- .filter(({ field, type }) => {
20
- const compare = COMPARATORS[type];
21
- if (!compare) throw new Error(`unknown divergence field type '${type}'`);
22
- return !compare(expected?.[field], actual?.[field]);
23
- })
24
- .map(({ field, emergencyKinds = [] }) => ({
25
- field,
26
- expected: expected?.[field],
27
- actual: actual?.[field],
28
- isEmergencyFlip: emergencyKinds.includes(expected?.[field]) || emergencyKinds.includes(actual?.[field]),
29
- }));
30
- return { divergences, hasPassed: divergences.length === 0 };
31
- }
32
-
33
- function stochasticDivergence() {
34
- return { verdict: 'review', rationale: 'stochastic message grading not yet enabled', graded: false };
35
- }
36
-
37
- async function classify({ expected, actual, expectedMessage, actualMessage }) {
38
- const deterministic = deterministicDivergence(expected, actual, divergenceConfig.getFields());
39
- const stochastic = await stochasticDivergence(expectedMessage, actualMessage);
40
- const result = OUTCOME_RULES.find(([applies]) => applies({ deterministic, stochastic }))?.[1] || 'pass';
41
- return {
42
- deterministic,
43
- stochastic,
44
- result,
45
- isEmergencyReview: deterministic.divergences.some((d) => d.isEmergencyFlip),
46
- };
47
- }
48
-
49
- module.exports = {
50
- deterministicDivergence,
51
- classify,
52
- };
@@ -1,18 +0,0 @@
1
- function createProjectorRegistry({ assemble, label = 'projector' }) {
2
- const projectors = new Map();
3
- return {
4
- register: (name, fn) => projectors.set(name, fn),
5
- project: (items) =>
6
- assemble(
7
- items.map(({ name, aggregate }) => {
8
- const fn = projectors.get(name);
9
- if (!fn) throw new Error(`no ${label} registered for '${name}'`);
10
- return { name, content: fn(aggregate) };
11
- }),
12
- ),
13
- };
14
- }
15
-
16
- module.exports = {
17
- createProjectorRegistry,
18
- };
@@ -1,62 +0,0 @@
1
- const { cleanForJson, isPlainObject } = require('../../utils/jsonUtils');
2
-
3
- const SEED_FORMATS = {
4
- '.json': {
5
- isValid: (c) => isPlainObject(c) && Object.values(c).every(isPlainObject),
6
- hint: 'an object keyed by id whose values are plain objects',
7
- },
8
- '.jsonl': {
9
- isValid: (c) => Array.isArray(c) && c.every(isPlainObject),
10
- hint: 'a list of plain objects',
11
- },
12
- };
13
-
14
- // clinical-agent file-store shapes:
15
- // keyedFile → KeyedJSONFile (`{pk: doc}` written as `.json`)
16
- // jsonlFile → JSONLFileStore (list-of-dicts written as `.jsonl`)
17
- function keyedFile(records, primaryKey) {
18
- const out = {};
19
- for (const record of records) {
20
- const doc = cleanForJson(record);
21
- const key = doc[primaryKey];
22
- if (key === undefined || key === null || key === '') {
23
- throw new Error(`keyed store record is missing primary key '${primaryKey}'`);
24
- }
25
- const skey = String(key);
26
- if (Object.prototype.hasOwnProperty.call(out, skey)) {
27
- throw new Error(`keyed store has duplicate primary key '${primaryKey}' value '${skey}'`);
28
- }
29
- out[skey] = doc;
30
- }
31
- return out;
32
- }
33
-
34
- function jsonlFile(records) {
35
- return records.map(cleanForJson);
36
- }
37
-
38
- function validateSeed(seed) {
39
- const basenames = new Set();
40
- for (const [name, content] of Object.entries(seed)) {
41
- const basename = name.split('/').pop();
42
- if (!basename || basename === '.' || basename === '..') {
43
- throw new Error(`invalid seed file name: '${name}'`);
44
- }
45
- if (basenames.has(basename)) {
46
- throw new Error(`duplicate seed file basename: '${basename}'`);
47
- }
48
- basenames.add(basename);
49
-
50
- const ext = Object.keys(SEED_FORMATS).find((extension) => basename.endsWith(extension));
51
- const format = ext ? SEED_FORMATS[ext] : null;
52
- if (!format) throw new Error(`seed file must end in .json or .jsonl: '${name}'`);
53
- if (!format.isValid(content)) throw new Error(`'${name}' must be ${format.hint}`);
54
- }
55
- return seed;
56
- }
57
-
58
- module.exports = {
59
- keyedFile,
60
- jsonlFile,
61
- validateSeed,
62
- };
@@ -1,18 +0,0 @@
1
- const axios = require('axios');
2
-
3
- async function runClinicalAgent(request, { baseUrl, apiKey, timeoutMs } = {}) {
4
- if (!baseUrl) throw new Error('runClinicalAgent requires a baseUrl');
5
- const res = await axios({
6
- url: `${baseUrl}/api/oracle/turn`,
7
- method: 'POST',
8
- data: request,
9
- timeout: timeoutMs,
10
- headers: { 'content-type': 'application/json', 'x-api-key': apiKey },
11
- validateStatus: () => true,
12
- });
13
- return { status: res.status, body: res.data ?? null };
14
- }
15
-
16
- module.exports = {
17
- runClinicalAgent
18
- };
@@ -1,58 +0,0 @@
1
- const runtimeConfig = require('../../config/runtimeConfig');
2
- const divergenceConfig = require('../config/divergenceConfig');
3
-
4
- const { runClinicalAgent } = require('./clinicalAgentService');
5
-
6
- const { classify } = require('../helpers/divergenceHelper');
7
-
8
- async function runDivergence({ request, connection, trace, outbound }) {
9
- let post;
10
- try {
11
- post = await runClinicalAgent(request, connection);
12
- if (post.status !== 200) {
13
- return { ok: false, status: post.status, response: post.body };
14
- }
15
- } catch (error) {
16
- return { ok: false, status: 503, response: { error: error.message } };
17
- }
18
- const expected = post.body?.run?.sessions?.[0]?.turns?.[0] || {};
19
-
20
- const actual = trace.toDecision();
21
- const outcome = await classify({
22
- expected,
23
- actual,
24
- expectedMessage: post.body?.response,
25
- actualMessage: outbound,
26
- });
27
-
28
- return { ok: true, ...outcome, expected, actual, response: post.body };
29
- }
30
-
31
- async function diverge({ shadow, body, runId, inputs }) {
32
- if (!inputs) return null;
33
-
34
- await divergenceConfig.load();
35
- const { enabled, baseUrl, timeoutMs } = divergenceConfig.getOracleConnection();
36
- if (!enabled || !baseUrl) return null;
37
-
38
- const request = {
39
- runId,
40
- body,
41
- clinicalData: inputs.clinicalData,
42
- input: inputs.input || [],
43
- model: inputs.model,
44
- enabledTools: inputs.toolIds || [],
45
- };
46
-
47
- const connection = {
48
- baseUrl,
49
- apiKey: runtimeConfig.get('CLINICAL_AGENT_BENCH_KEY', ''),
50
- timeoutMs,
51
- };
52
-
53
- return runDivergence({ request, connection, trace: shadow?.context?.trace, outbound: shadow?.result?.output || '' });
54
- }
55
-
56
- module.exports = {
57
- diverge,
58
- };
@@ -1,21 +0,0 @@
1
- const { createProjectorRegistry } = require('../helpers/projectorHelper');
2
- const { validateSeed } = require('../helpers/seedHelper');
3
-
4
- const registry = createProjectorRegistry({
5
- label: 'seed projector',
6
- assemble: (items) => {
7
- const seed = {};
8
- const seen = new Set();
9
- for (const { name, content } of items) {
10
- if (seen.has(name)) throw new Error(`duplicate seed file name: '${name}'`);
11
- seen.add(name);
12
- if (content !== undefined) seed[name] = content;
13
- }
14
- return validateSeed(seed);
15
- },
16
- });
17
-
18
- module.exports = {
19
- registerSeedProjector: registry.register,
20
- projectSeed: registry.project,
21
- };