MediBot / lib /feature-flags.ts
github-actions[bot]
Deploy MedOS Global from 956ae78f
e3e3e14
Raw
History Blame Contribute Delete
6.92 kB
/**
* MedOS feature flags.
*
* Single, typed, server-side source of truth for feature flags. Read once
* per process from env vars. Any new flag must:
* 1. Be added here.
* 2. Default to OFF.
* 3. Be documented in the JSDoc above its accessor.
* 4. Be checked by tests before being flipped on in any environment.
*
* Do NOT add NEXT_PUBLIC_* prefixed flags. Feature flags are server-side.
* If a flag must affect client behavior, surface it through a normal API
* response, not a public env var.
*/
const truthy = (v: string | undefined): boolean =>
!!v && /^(1|true|yes|on)$/i.test(v.trim());
/**
* Per-user OllaBridge Cloud pairing.
*
* When ON, the chat route (and any future authenticated server path) will
* try the authenticated user's per-user OllaBridge pairing FIRST in the
* provider chain, before the operator-wide bridge or the Hugging Face
* fallback. When OFF, only operator-wide pairing (the legacy
* OLLABRIDGE_URL env) is honored.
*
* See: 9-HuggingFace-Global/docs/OLLABRIDGE_INTEGRATION.md
*/
export const perUserOllaBridgeEnabled = (): boolean =>
truthy(process.env.MEDOS_PER_USER_OLLABRIDGE);
/**
* Offline-Lite small-model provider.
*
* When ON, the offline-lite provider (Phi-3 mini / Gemma 2B / TinyLlama)
* is registered in the provider fallback chain as the LAST step (after
* cached FAQ). Outputs are pinned to R0/R1 only by the safety engine;
* the post-filter still runs.
*
* The flag must remain OFF until the full golden prompt set passes in
* offline mode with the same R5/R4 sensitivity targets as the online
* path.
*
* See: 9-HuggingFace-Global/docs/OFFLINE_LITE.md
*/
export const offlineLiteEnabled = (): boolean =>
truthy(process.env.MEDOS_OFFLINE_LITE_ENABLED);
/**
* Emergency card emission.
*
* When OFF (the default), the chat route does NOT emit the dedicated
* `[card:emergency]` UI card, and the symptom-flow state machine
* downgrades any safety-check red-flag match from care_level `emergency`
* to `urgent` on the guidance card.
*
* The DETERMINISTIC SAFETY FLOOR is preserved either way:
* - preCheck() still runs and still classifies risk (R0–R5)
* - For R5 (explicit claims like "I am having a heart attack"), the
* emergency banner text is still prepended to the LLM response
* - The local emergency number still appears in `seek_care_if`
* sections of guidance cards
*
* What changes when OFF: users with bare chest-pain / stroke-FAST
* / similar inputs are not greeted with the alarming red emergency
* card. They get the structured safety_check → intake → urgent
* guidance flow instead, which still tells them what to do, just
* less dramatically.
*
* Re-enable by setting MEDOS_EMERGENCY_CARD_ENABLED=true on the
* environment for the deployment that needs it (e.g. an internal
* clinician-supervised pilot where the alarm UI is welcome).
*/
export const emergencyCardEnabled = (): boolean =>
truthy(process.env.MEDOS_EMERGENCY_CARD_ENABLED);
/**
* Deterministic symptom-flow cards.
*
* When ON, recognized symptom mentions (headache, back pain, chest pain,
* …) are answered by the medical-flow state machine with a fixed,
* multi-turn card sequence — a red-flag safety_check checklist → intake
* cards (location / duration / severity) → a guidance card — and the LLM
* is not called for those turns.
*
* Default OFF. Those rigid checklists read as robotic / demo-like for a
* conversational medical assistant, so by default non-emergency symptom
* turns fall through to the LLM, which answers professionally and in
* context (acknowledge → likely causes → one targeted follow-up →
* red-flag note) per the system prompt in lib/medical-knowledge.ts.
*
* The safety floor is INDEPENDENT of this flag: preCheck() runs the
* deterministic emergency / red-flag rules on the raw user text BEFORE any
* card or LLM decision, so true emergencies ("worst headache of my life",
* "chest pain radiating to the arm") still escalate to the emergency floor
* whether this is ON or OFF.
*
* Re-enable the structured flow with MEDOS_SYMPTOM_CARDS_ENABLED=true.
*/
export const symptomCardsEnabled = (): boolean =>
truthy(process.env.MEDOS_SYMPTOM_CARDS_ENABLED);
/**
* Grounded hybrid RAG.
*
* When ON, the chat route retrieves from the versioned, source-attributed
* corpus in `/data/medos.db` (FTS5 keyword + vector cosine, fused with
* RRF) instead of the in-memory keyword KB. The KB remains the automatic
* fallback whenever the corpus is not yet ingested or retrieval fails, so
* flipping this on can never produce an empty pipeline.
*
* Keep OFF until `scripts/ingest-corpus.ts` (or POST /api/admin/ingest)
* has populated the corpus on the deployment's persistent `/data` volume.
*
* See: 9-HuggingFace-Global/docs/RAG-PRODUCTION-DESIGN.md
*/
export const ragHybridEnabled = (): boolean =>
truthy(process.env.RAG_HYBRID);
/**
* Require retrieved evidence for clinical answers.
*
* When ON, a substantive clinical turn whose retrieval came back
* "insufficient" (no corpus hit above the similarity floor) is answered
* with a deterministic "I don't have verified information on this" deferral
* instead of letting the model answer from unguarded general knowledge.
* Emergencies are never deferred — the safety floor always runs first.
*
* Only meaningful with RAG_HYBRID on AND a comprehensive corpus ingested.
* Keep OFF until the corpus covers the expected query distribution.
*/
export const ragRequireEvidence = (): boolean =>
truthy(process.env.RAG_REQUIRE_EVIDENCE);
/**
* Post-generation faithfulness check.
*
* When ON, grounded answers at or above RAG_FAITHFULNESS_MIN_RISK are
* verified by a lightweight LLM-judge against the retrieved sources. A
* low groundedness score appends a "could not fully verify" caveat (it
* does NOT silently drop the answer; the safety floor is unchanged). Adds
* one extra model call per qualifying turn, so it is risk-gated.
*/
export const faithfulnessEnabled = (): boolean =>
truthy(process.env.RAG_FAITHFULNESS);
/**
* Minimum risk class at which the faithfulness check runs (R0–R5).
* Defaults to R2 so chit-chat / low-risk turns skip the extra call.
*/
export const faithfulnessMinRisk = (): string =>
(process.env.RAG_FAITHFULNESS_MIN_RISK || 'R2').toUpperCase();
/**
* Snapshot every flag's current value. Convenience for the /api/health
* endpoint and for logging at process startup.
*/
export function snapshotFlags(): Record<string, boolean> {
return {
MEDOS_PER_USER_OLLABRIDGE: perUserOllaBridgeEnabled(),
MEDOS_OFFLINE_LITE_ENABLED: offlineLiteEnabled(),
MEDOS_EMERGENCY_CARD_ENABLED: emergencyCardEnabled(),
MEDOS_SYMPTOM_CARDS_ENABLED: symptomCardsEnabled(),
RAG_HYBRID: ragHybridEnabled(),
RAG_REQUIRE_EVIDENCE: ragRequireEvidence(),
RAG_FAITHFULNESS: faithfulnessEnabled(),
};
}