/** * 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 { 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(), }; }