From 7bf158ff648cae3e7f4c0f9d7bf35df9ca0c9fd4 Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:24:58 +0200 Subject: [PATCH 1/6] Add scientific interpretation knowledge base --- google_health_viewer/scientific_context.py | 435 +++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 google_health_viewer/scientific_context.py diff --git a/google_health_viewer/scientific_context.py b/google_health_viewer/scientific_context.py new file mode 100644 index 0000000..bd5079d --- /dev/null +++ b/google_health_viewer/scientific_context.py @@ -0,0 +1,435 @@ +"""Curated scientific interpretation context for VitalChronicle health metrics. + +This module is deliberately separate from deterministic measurements. The snapshot says +what was observed in the user's data; this catalogue says what a metric can plausibly mean, +which confounders matter, and which conclusions are not justified from a wearable metric alone. +The local language model may supplement this background with its own general knowledge, but +must never treat the background as evidence that a listed cause applies to the user. +""" + +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +KNOWLEDGE_BASE_VERSION = "scientific-context-v1" + +SOURCES: dict[str, dict[str, str]] = { + "JACC_WEARABLES_2023": { + "title": "Consumer Wearable Health and Fitness Technology in Cardiovascular Medicine", + "year": "2023", + "kind": "state-of-the-art review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10662962/", + }, + "HRV_WEARABLE_2023": { + "title": "Heart Rate Variability Measurement through a Smart Wearable Device", + "year": "2023", + "kind": "review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10742885/", + }, + "HRV_TRAINING_2024": { + "title": "Heart Rate Variability Applications in Strength and Conditioning", + "year": "2024", + "kind": "narrative review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC11204851/", + }, + "RESPIRATORY_RATE_2020": { + "title": "The Importance of Respiratory Rate Monitoring: From Healthcare to Sport and Exercise", + "year": "2020", + "kind": "review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC7665156/", + }, + "SLEEP_STAGING_2021": { + "title": "A Systematic Review of Sensing Technologies for Wearable Sleep Staging", + "year": "2021", + "kind": "systematic review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC7956647/", + }, + "SLEEP_RELIABILITY_2024": { + "title": "Evaluating reliability in wearable devices for sleep staging", + "year": "2024", + "kind": "scoping review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10948771/", + }, + "PPG_ROADMAP_2023": { + "title": "The 2023 wearable photoplethysmography roadmap", + "year": "2023", + "kind": "roadmap/review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10686289/", + }, + "VO2_INTERLIVE_2022": { + "title": "Validity of Estimating the Maximal Oxygen Consumption by Consumer Wearables", + "year": "2022", + "kind": "systematic review and meta-analysis", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC9213394/", + }, + "WEARABLE_ACTIVITY_2022": { + "title": "Wearable activity trackers—advanced technology or advanced marketing?", + "year": "2022", + "kind": "review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC9022022/", + }, + "WEARABLE_INFECTION_2022": { + "title": "The performance of wearable sensors in the detection of SARS-CoV-2 infection", + "year": "2022", + "kind": "systematic review", + "url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC9020803/", + }, + "WHO_ACTIVITY_2020": { + "title": "WHO guidelines on physical activity and sedentary behaviour", + "year": "2020", + "kind": "guideline", + "url": "https://www.who.int/publications/i/item/9789240015128", + }, + "CDC_BMI_2024": { + "title": "CDC Body Mass Index guidance", + "year": "2024", + "kind": "public-health guidance", + "url": "https://www.cdc.gov/bmi/faq/index.html", + }, +} + + +def _topic( + meaning: str, + *, + baseline_rule: str, + higher: list[str] | None = None, + lower: list[str] | None = None, + confounders: list[str] | None = None, + relationships: list[str] | None = None, + limitations: list[str] | None = None, + sources: tuple[str, ...] = (), +) -> dict[str, Any]: + return { + "meaning": meaning, + "baseline_rule": baseline_rule, + "higher": higher or [], + "lower": lower or [], + "confounders": confounders or [], + "relationships": relationships or [], + "limitations": limitations or [], + "source_ids": list(sources), + } + + +TOPICS: dict[str, dict[str, Any]] = { + "activity_volume": _topic( + "Movement-volume metrics such as steps, distance, active minutes and floors estimate how much physical activity occurred. They describe behaviour and workload, not fitness by themselves.", + baseline_rule="Interpret trends against the person's usual activity, day type and measurement completeness; a partial day must not be compared directly with completed days.", + higher=[ + "Often reflects more locomotion or longer/more intense activity, which is generally favourable when sustainable.", + "A sudden increase can also represent an unusually heavy workload and may help explain higher heart rate, lower short-term HRV or greater fatigue afterwards.", + ], + lower=[ + "May reflect rest/recovery, illness, travel, sedentary behaviour, schedule changes or missing wear time.", + "Persistent reductions matter more when confirmed by other activity metrics and complete device coverage.", + ], + confounders=["device non-wear", "activity type poorly captured by wrist motion", "stride/algorithm error", "terrain", "assistive transport"], + relationships=["heart rate", "active-zone minutes", "exercise sessions", "energy expenditure", "sleep/recovery"], + limitations=["Consumer wearables vary in validity by metric and activity type; step count is usually more robust than energy expenditure."], + sources=("WEARABLE_ACTIVITY_2022", "WHO_ACTIVITY_2020"), + ), + "energy": _topic( + "Active or total energy expenditure is an algorithmic estimate of metabolic energy use, commonly derived from motion, heart rate and personal characteristics.", + baseline_rule="Use within-device longitudinal changes more confidently than exact calorie values; absolute wearable calorie estimates can have substantial error.", + higher=["Can accompany greater activity duration/intensity, larger body size, heat stress or elevated heart rate."], + lower=["Can accompany lower activity, rest or incomplete wear time."], + confounders=["device algorithm", "body-profile settings", "exercise modality", "heart-rate sensor error", "non-wear"], + relationships=["steps", "exercise", "heart rate", "active minutes"], + limitations=["Do not infer energy balance, weight change or dietary adequacy from wearable expenditure alone."], + sources=("WEARABLE_ACTIVITY_2022",), + ), + "heart_rate": _topic( + "Heart rate is the number of cardiac beats per minute. It responds rapidly to exercise and also to autonomic tone, temperature, posture, hydration, stress, stimulants, illness and many medications.", + baseline_rule="Interpret heart rate in context and relative to the individual's own time-of-day and activity-matched baseline. Resting and exercising heart rate are physiologically different states.", + higher=[ + "During exercise, a rise is expected and should be interpreted with exercise intensity and duration.", + "Outside exercise, a sustained rise from personal baseline can accompany heat, dehydration, stress, poor recovery, infection/inflammation, pain, stimulants or medications, among many other causes.", + ], + lower=[ + "A lower resting value can accompany endurance training or recovery, but can also reflect medication effects, conduction abnormalities or sensor error.", + "An unexpectedly low value is not automatically beneficial; context and symptoms matter.", + ], + confounders=["movement artefact", "poor skin contact", "skin perfusion", "exercise modality", "temperature", "medications", "caffeine/nicotine"], + relationships=["exercise/activity level", "HRV", "temperature", "respiratory rate", "sleep", "oxygen saturation"], + limitations=["Wrist PPG is less accurate during some forms of exercise than ECG/chest-strap measurement; isolated outliers should be treated cautiously."], + sources=("JACC_WEARABLES_2023", "PPG_ROADMAP_2023", "WEARABLE_INFECTION_2022"), + ), + "resting_heart_rate": _topic( + "Resting heart rate is a low-activity estimate intended to reflect basal cardiac rate with minimal acute exertional influence.", + baseline_rule="Personal longitudinal baseline is more informative than population comparison; verify that changes are not explained by exercise, sleep, temperature or measurement conditions.", + higher=["Can occur with reduced recovery, heat, dehydration, acute illness/inflammation, psychological stress, pain, stimulants or deconditioning."], + lower=["Can accompany endurance adaptation, improved recovery or medication effects; marked unexplained decreases are not inherently favourable."], + confounders=["measurement timing", "sleep/wake state", "recent exercise", "medications", "illness", "sensor quality"], + relationships=["HRV", "sleep", "temperature", "respiratory rate", "recent exercise"], + limitations=["A deviation is a nonspecific physiological signal, not a diagnosis."], + sources=("JACC_WEARABLES_2023", "WEARABLE_INFECTION_2022"), + ), + "hrv": _topic( + "Heart-rate variability (HRV) quantifies beat-to-beat timing variation and is strongly influenced by autonomic regulation. Time-domain indices such as RMSSD are commonly used by wearables as recovery/stress markers.", + baseline_rule="Prefer consistent measurement conditions and the person's own rolling baseline. Absolute HRV values are difficult to compare across people, devices, algorithms and times of day.", + higher=["Relative increases can accompany stronger vagal modulation, recovery or training adaptation, but a single high value is not automatically better."], + lower=["Relative decreases can accompany recent intense exercise, poor sleep, alcohol, dehydration, psychological stress, acute illness, pain, travel or medications."], + confounders=["measurement posture/time", "breathing pattern", "ectopic beats/artefact filtering", "recent exercise", "alcohol/nicotine", "hydration", "sleep", "medications"], + relationships=["resting heart rate", "exercise load", "sleep", "temperature", "respiratory rate"], + limitations=["HRV should not be simplified to a direct measure of sympathetic activity or 'sympathovagal balance'; interpretation depends on the HRV metric used."], + sources=("HRV_WEARABLE_2023", "HRV_TRAINING_2024", "JACC_WEARABLES_2023"), + ), + "oxygen_saturation": _topic( + "Peripheral oxygen saturation (SpO2) estimates the fraction of haemoglobin carrying oxygen using optical pulse oximetry/PPG.", + baseline_rule="Look for repeated, technically plausible deviations from the individual's baseline and measurement context rather than relying on a single consumer-wearable value.", + higher=["Within the normal physiological range, small upward fluctuations usually have little interpretive value."], + lower=["True reductions may occur with respiratory or cardiopulmonary problems, sleep-disordered breathing or altitude, but motion/perfusion/sensor artefact can also lower readings."], + confounders=["motion", "low peripheral perfusion", "sensor fit", "skin characteristics", "altitude", "device algorithm"], + relationships=["respiratory rate", "sleep", "heart rate", "altitude"], + limitations=["Consumer SpO2 is not equivalent to arterial blood-gas measurement and accuracy worsens in some conditions, particularly at lower saturation."], + sources=("PPG_ROADMAP_2023", "JACC_WEARABLES_2023"), + ), + "respiratory_rate": _topic( + "Respiratory rate is breaths per minute. It is sensitive to metabolic demand and to multiple physiological stressors, including exercise, heat/cold, emotional stress and illness.", + baseline_rule="A sustained change from a stable personal resting/sleep baseline is generally more informative than an isolated value.", + higher=["Can accompany exercise, heat stress, anxiety, pain, fever/infection, respiratory or cardiovascular stress and other causes."], + lower=["Can occur during deeper sleep/relaxation, with some medications or with altered respiratory control; isolated low wearable estimates may be artefactual."], + confounders=["sleep stage", "exercise", "talking", "posture", "temperature", "device inference method"], + relationships=["heart rate", "temperature", "SpO2", "sleep", "exercise"], + limitations=["It is sensitive but nonspecific: the same directional change can have many causes."], + sources=("RESPIRATORY_RATE_2020", "WEARABLE_INFECTION_2022"), + ), + "temperature": _topic( + "Wearable temperature metrics may represent skin/peripheral temperature, a nocturnal deviation from personal baseline, or an algorithmic estimate. They are not automatically equivalent to core body temperature.", + baseline_rule="Interpret direction and persistence relative to the same device's personal baseline and similar measurement conditions; first establish what anatomical/derived temperature the field represents.", + higher=[ + "Can reflect a warmer environment, bedding/clothing, altered skin perfusion, circadian effects or exercise/heat exposure.", + "A sustained rise together with higher resting heart rate and/or respiratory rate, lower HRV and altered sleep can be compatible with systemic physiological stress such as infection/inflammation, but remains nonspecific.", + ], + lower=["Can reflect a cooler environment, altered peripheral perfusion, circadian timing, device contact or other behavioural/physiological changes."], + confounders=["ambient temperature", "bedding/clothing", "sensor location/contact", "circadian phase", "exercise", "peripheral perfusion"], + relationships=["resting heart rate", "HRV", "respiratory rate", "sleep", "activity"], + limitations=["Do not label a wearable skin-temperature change as fever unless the measurement is explicitly a validated core-temperature estimate; inflammation/infection is only one possible explanation."], + sources=("WEARABLE_INFECTION_2022", "JACC_WEARABLES_2023"), + ), + "vo2max": _topic( + "VO2 max estimates maximal aerobic oxygen uptake and is a marker of cardiorespiratory fitness. Consumer wearables usually estimate it indirectly from heart rate, speed/workload and personal characteristics rather than measuring respiratory gases.", + baseline_rule="Use longitudinal trends under comparable exercise conditions more confidently than small single-session changes.", + higher=["A sustained increase can reflect improved aerobic fitness or favourable algorithm inputs/training performance."], + lower=["A sustained decrease can reflect detraining, fatigue/illness, changed exercise conditions, heat, altitude or estimation error."], + confounders=["exercise protocol", "heart-rate accuracy", "terrain", "temperature", "altitude", "device algorithm"], + relationships=["exercise volume/intensity", "heart rate", "pace/distance", "recovery"], + limitations=["Individual wearable VO2-max estimation error can be substantial; exercise-based estimates tend to perform better than resting estimates but are not laboratory gas-exchange tests."], + sources=("VO2_INTERLIVE_2022", "JACC_WEARABLES_2023"), + ), + "sleep": _topic( + "Sleep metrics estimate sleep duration, timing, awakenings and architecture. Consumer wearables infer sleep and stages from movement and physiological signals; polysomnography remains the reference method for clinical sleep staging.", + baseline_rule="Emphasize repeated patterns in duration, continuity, timing and awakenings. Treat individual stage percentages as estimates rather than exact neurophysiological measurements.", + higher=["More total sleep may reflect recovery or increased sleep opportunity, but unusually long sleep can also occur after sleep debt, illness or schedule change."], + lower=["Reduced duration or fragmented sleep can follow schedule constraints, stress, environment, exercise timing, alcohol, illness and many other causes."], + confounders=["quiet wake misclassified as sleep", "device algorithm", "sensor contact", "sleep environment", "irregular schedules", "naps"], + relationships=["resting heart rate", "HRV", "temperature", "respiratory rate", "activity/exercise"], + limitations=["Wearable sleep-stage classification is less reliable than polysomnography; do not diagnose sleep disorders from stage estimates or awakening counts alone."], + sources=("SLEEP_STAGING_2021", "SLEEP_RELIABILITY_2024"), + ), + "glucose": _topic( + "Blood-glucose measurements reflect circulating glucose concentration and are strongly influenced by meals, fasting state, exercise, hormones, stress, illness and glucose-regulating medications.", + baseline_rule="Interpret only with measurement method and timing (fasting, post-meal, random) known. Clinical thresholds depend on validated measurement protocols and should not be inferred from an unspecified wearable field.", + higher=["Can occur after carbohydrate intake and with stress hormones, illness or impaired glucose regulation, among other causes."], + lower=["Can occur with fasting, prolonged exercise, some medications or inadequate intake; sensor/measurement artefact is also possible."], + confounders=["meal timing", "exercise", "measurement method", "medications", "illness/stress"], + relationships=["nutrition", "exercise", "sleep", "weight/body composition"], + limitations=["VitalChronicle should not diagnose diabetes or hypoglycaemia from consumer data alone; clinical interpretation requires validated methods and context."], + sources=(), + ), + "weight": _topic( + "Body weight is total body mass. Short-term changes often reflect fluid balance and gastrointestinal contents; longer-term trends can reflect changes in fat mass, lean mass and/or fluid status.", + baseline_rule="Use a trend across repeated measurements under similar conditions; day-to-day changes should not automatically be interpreted as fat gain or loss.", + higher=["Can reflect increased tissue mass, fluid retention, food/gut contents or measurement conditions."], + lower=["Can reflect tissue loss, fluid loss/dehydration, glycogen-associated water shifts or measurement conditions."], + confounders=["time of day", "hydration", "meals", "clothing", "scale placement/calibration"], + relationships=["body fat estimate", "activity", "nutrition", "hydration"], + limitations=["Weight and BMI are screening/context measures, not complete measures of health or body composition."], + sources=("CDC_BMI_2024",), + ), + "body_fat": _topic( + "Body-fat percentage estimates the fraction of body mass attributed to adipose tissue. Consumer scales commonly use bioelectrical impedance and are sensitive to hydration and algorithm assumptions.", + baseline_rule="Interpret longer-term within-device trends under standardized conditions; small daily changes are often measurement/fluid noise.", + higher=["A sustained increase may reflect increased fat mass, but short-term increases can be driven by hydration/impedance variation."], + lower=["A sustained decrease may reflect fat loss, but short-term decreases can be driven by hydration/impedance variation."], + confounders=["hydration", "recent exercise", "skin temperature", "meals", "device algorithm"], + relationships=["weight", "activity", "nutrition"], + limitations=["Consumer bioimpedance is not equivalent to reference body-composition methods."], + sources=("WEARABLE_ACTIVITY_2022",), + ), + "hydration": _topic( + "Hydration logs record reported fluid intake, not whole-body hydration status. Hydration status also depends on losses through urine, sweat and respiration and on dietary water/electrolytes.", + baseline_rule="Interpret intake in context of body size, climate, exercise/sweat losses and other fluids/foods; a logged volume is incomplete if logging is incomplete.", + higher=["Can reflect deliberate rehydration, hot weather, exercise, thirst or increased logging."], + lower=["Can reflect lower intake or simply incomplete logging."], + confounders=["manual logging completeness", "food water", "sweat loss", "climate", "exercise"], + relationships=["exercise", "temperature", "heart rate", "weight"], + limitations=["Do not infer dehydration solely from low logged intake."], + sources=(), + ), + "nutrition": _topic( + "Nutrition logs describe reported food/energy/nutrient intake. They can provide behavioural context for weight, glucose and activity but are usually incomplete and subject to portion-size and database error.", + baseline_rule="Treat logged intake as an observed subset unless completeness is known; compare like periods and avoid assuming unlogged food equals zero intake.", + higher=["Higher logged energy or nutrient intake may represent true intake changes or better logging completeness."], + lower=["Lower logged intake may represent true intake changes or missing entries."], + confounders=["under-reporting", "portion estimates", "food database", "missing meals"], + relationships=["weight", "glucose", "activity", "hydration"], + limitations=["Do not diagnose nutrient deficiency or excess from sparse food logs alone."], + sources=(), + ), + "altitude": _topic( + "Altitude is environmental elevation and is primarily contextual rather than a physiological outcome. Higher altitude lowers ambient oxygen pressure and can influence oxygen saturation, heart rate, breathing and exercise performance.", + baseline_rule="Use altitude to explain concurrent physiological measurements rather than treating altitude itself as a health improvement or deterioration.", + higher=["May plausibly contribute to lower SpO2, higher respiratory/heart rate and altered exercise capacity depending on elevation and acclimatization."], + lower=["Returning to lower altitude can reverse altitude-related physiological stress."], + confounders=["GPS/barometer error", "indoor location", "device calibration"], + relationships=["SpO2", "respiratory rate", "heart rate", "VO2 max/exercise"], + limitations=["Effects depend strongly on absolute altitude, ascent rate, acclimatization and individual susceptibility."], + sources=("JACC_WEARABLES_2023",), + ), + "zones": _topic( + "Heart-rate zones classify exercise intensity using heart-rate thresholds. Time or calories in zones are derived workload descriptors, not independent physiological measurements.", + baseline_rule="Interpret with the zone-definition method and the person's exercise context; thresholds may be estimated and can change with settings or fitness.", + higher=["More time in higher zones generally indicates greater cardiovascular intensity if the heart-rate data and thresholds are valid."], + lower=["More time in lower zones can reflect easier/recovery activity or different exercise modality."], + confounders=["zone threshold settings", "heart-rate sensor error", "medications affecting heart rate", "temperature", "fitness changes"], + relationships=["heart rate", "exercise", "active minutes", "energy expenditure"], + limitations=["Do not equate a zone label with a direct measurement of lactate threshold or metabolic substrate use unless specifically validated."], + sources=("JACC_WEARABLES_2023",), + ), + "exercise": _topic( + "Exercise-session records identify structured bouts of physical activity, their timing, duration and type. They provide crucial context for interpreting simultaneous heart-rate and short-term recovery changes.", + baseline_rule="Interpret physiological changes during and after exercise relative to exercise type, duration and intensity and to comparable previous sessions.", + higher=["Greater duration/intensity can explain higher heart rate and may transiently lower HRV or increase temperature/respiratory rate during recovery."], + lower=["Reduced training load can reflect planned recovery, schedule change, illness or reduced activity."], + confounders=["auto-detection errors", "misclassified exercise type", "unrecorded workouts"], + relationships=["heart rate", "HRV", "temperature", "respiratory rate", "activity volume", "VO2 max"], + limitations=["Exercise timing overlap supports contextual attribution but does not prove that every concurrent physiological change was caused by exercise."], + sources=("JACC_WEARABLES_2023", "WHO_ACTIVITY_2020"), + ), + "ecg": _topic( + "A wearable ECG records cardiac electrical activity, usually from a limited-lead configuration. It can characterize rhythm during the recording but does not replace a clinical 12-lead ECG for many diagnostic questions.", + baseline_rule="Interpret the actual tracing, recording quality, symptoms and rhythm classification together; a device label alone is insufficient for diagnosis.", + higher=[], + lower=[], + confounders=["motion", "poor electrode contact", "short recording duration", "algorithm classification limits"], + relationships=["heart rate", "irregular-rhythm notifications", "symptom timing"], + limitations=["Automated wearable ECG/rhythm labels can be false positive or false negative; clinically important findings require professional review."], + sources=("JACC_WEARABLES_2023",), + ), + "irregular_rhythm": _topic( + "An irregular-rhythm notification is an algorithmic event indicating that pulse/rhythm patterns met the device's detection criteria. It is a screening signal rather than a confirmed arrhythmia diagnosis.", + baseline_rule="Treat occurrence, recurrence, recording quality and confirmatory ECG information as more informative than a notification alone.", + higher=[], + lower=[], + confounders=["motion/artefact", "ectopic beats", "algorithm/device characteristics"], + relationships=["ECG", "heart rate", "symptom timing"], + limitations=["A notification cannot determine all arrhythmia types and absence of notifications does not exclude arrhythmia."], + sources=("JACC_WEARABLES_2023",), + ), + "height": _topic( + "Height is a relatively stable anthropometric characteristic in adults and is mainly useful for normalization or derived measures such as BMI and some fitness estimates.", + baseline_rule="Adult short-term variation is usually measurement/posture error rather than biological change.", + higher=[], + lower=[], + confounders=["measurement posture", "time of day", "manual entry error"], + relationships=["weight", "BMI/body composition", "VO2 normalization"], + limitations=["Do not interpret small adult height changes from consumer records as a physiological trend."], + sources=("CDC_BMI_2024",), + ), + "catalogue": _topic( + "Catalogue/reference records describe available foods or measurement units and are metadata rather than physiological measurements.", + baseline_rule="Do not interpret catalogue records as evidence of consumption or health change.", + limitations=["Reference metadata should not enter physiological trend or association calculations."], + sources=(), + ), +} + + +DATA_TYPE_TO_TOPIC: dict[str, str] = { + "active-energy-burned": "energy", + "active-minutes": "activity_volume", + "active-zone-minutes": "zones", + "activity-level": "activity_volume", + "altitude": "altitude", + "calories-in-heart-rate-zone": "zones", + "daily-vo2-max": "vo2max", + "distance": "activity_volume", + "exercise": "exercise", + "floors": "activity_volume", + "run-vo2-max": "vo2max", + "sedentary-period": "activity_volume", + "steps": "activity_volume", + "swim-lengths-data": "activity_volume", + "time-in-heart-rate-zone": "zones", + "total-calories": "energy", + "vo2-max": "vo2max", + "blood-glucose": "glucose", + "body-fat": "body_fat", + "core-body-temperature": "temperature", + "daily-heart-rate-variability": "hrv", + "daily-heart-rate-zones": "zones", + "daily-oxygen-saturation": "oxygen_saturation", + "daily-respiratory-rate": "respiratory_rate", + "daily-resting-heart-rate": "resting_heart_rate", + "daily-sleep-temperature-derivations": "temperature", + "heart-rate": "heart_rate", + "heart-rate-variability": "hrv", + "height": "height", + "oxygen-saturation": "oxygen_saturation", + "respiratory-rate-sleep-summary": "respiratory_rate", + "weight": "weight", + "sleep": "sleep", + "food": "catalogue", + "food-measurement-unit": "catalogue", + "hydration-log": "hydration", + "nutrition-log": "nutrition", + "electrocardiogram": "ecg", + "irregular-rhythm-notification": "irregular_rhythm", +} + + +def scientific_context_for(data_type: str, *, detailed: bool = False) -> dict[str, Any] | None: + topic_name = DATA_TYPE_TO_TOPIC.get(str(data_type)) + if topic_name is None: + return None + source = TOPICS[topic_name] + if detailed: + result = deepcopy(source) + result["topic"] = topic_name + result["sources"] = [ + {"source_id": source_id, **SOURCES[source_id]} + for source_id in source["source_ids"] + if source_id in SOURCES + ] + return result + return { + "topic": topic_name, + "meaning": source["meaning"], + "baseline_rule": source["baseline_rule"], + "higher": source["higher"][:1], + "lower": source["lower"][:1], + "limitations": source["limitations"][:1], + "source_ids": list(source["source_ids"]), + } + + +def scientific_context_for_types( + data_types: list[str] | tuple[str, ...] | set[str], + *, + detailed: bool = False, + maximum: int | None = None, +) -> dict[str, dict[str, Any]]: + result: dict[str, dict[str, Any]] = {} + for data_type in data_types: + key = str(data_type) + if key in result: + continue + context = scientific_context_for(key, detailed=detailed) + if context is not None: + result[key] = context + if maximum is not None and len(result) >= maximum: + break + return result From 4cff6f732537011f2c5cc34e9150ba30d2c635ee Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:26:30 +0200 Subject: [PATCH 2/6] Inject scientific context into adaptive AI retrieval --- .../scientific_context_core.py | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 google_health_viewer/scientific_context_core.py diff --git a/google_health_viewer/scientific_context_core.py b/google_health_viewer/scientific_context_core.py new file mode 100644 index 0000000..a0b27b1 --- /dev/null +++ b/google_health_viewer/scientific_context_core.py @@ -0,0 +1,266 @@ +"""Runtime integration of curated physiology with the adaptive AI evidence pipeline.""" + +from __future__ import annotations + +from typing import Any + +from . import ai_adaptive_retrieval, ai_engine +from .scientific_context import ( + DATA_TYPE_TO_TOPIC, + KNOWLEDGE_BASE_VERSION, + scientific_context_for_types, +) + +_INSTALLED = False +_ORIGINAL_SELECT = None + +_SCIENCE_HINTS = ( + "what is", + "what does", + "meaning", + "means", + "explain", + "why", + "cause", + "causes", + "interpret", + "physiology", + "scientific", + "cosa e", + "cos e", + "cosa significa", + "significa", + "spiega", + "spiegami", + "perche", + "perché", + "causa", + "cause", + "interpretare", + "fisiologia", + "scientifico", +) + +_SCIENCE_ALIASES: dict[str, tuple[str, ...]] = { + "heart-rate": ("heart rate", "frequenza cardiaca", "battito", "battiti"), + "daily-resting-heart-rate": ("resting heart rate", "rhr", "frequenza cardiaca a riposo"), + "daily-heart-rate-variability": ("hrv", "heart rate variability", "variabilita cardiaca", "variabilità cardiaca"), + "daily-oxygen-saturation": ("spo2", "oxygen saturation", "saturazione", "ossigenazione"), + "daily-respiratory-rate": ("respiratory rate", "breathing rate", "frequenza respiratoria", "respirazione"), + "daily-sleep-temperature-derivations": ("skin temperature", "sleep temperature", "temperatura cutanea", "temperatura nel sonno", "temperatura"), + "sleep": ("sleep", "sonno", "awakening", "awakenings", "risveglio", "risvegli", "rem", "deep sleep", "sonno profondo"), + "daily-vo2-max": ("vo2 max", "vo2max", "capacita aerobica", "capacità aerobica"), + "exercise": ("exercise", "workout", "training", "allenamento", "esercizio"), + "steps": ("steps", "passi"), + "active-minutes": ("active minutes", "minuti attivi", "attivita fisica", "attività fisica"), + "weight": ("weight", "peso"), + "body-fat": ("body fat", "grasso corporeo"), + "blood-glucose": ("blood glucose", "glucose", "glicemia", "glucosio"), + "hydration-log": ("hydration", "idratazione", "acqua"), + "nutrition-log": ("nutrition", "diet", "food", "nutrizione", "dieta", "alimentazione"), + "altitude": ("altitude", "elevation", "altitudine", "quota"), + "electrocardiogram": ("ecg", "electrocardiogram", "elettrocardiogramma"), + "irregular-rhythm-notification": ("irregular rhythm", "arrhythmia", "aritmia", "ritmo irregolare"), +} + + +def _normalize(text: str) -> str: + return ai_adaptive_retrieval._normalize(text) + + +def _science_question(question: str) -> bool: + query = _normalize(question) + return any(_normalize(hint) in query for hint in _SCIENCE_HINTS) + + +def _question_science_types(question: str) -> list[str]: + query = _normalize(question) + matched: list[str] = [] + for data_type, aliases in _SCIENCE_ALIASES.items(): + if any(_normalize(alias) in query for alias in aliases): + matched.append(data_type) + return matched + + +def _present_data_types(packet: dict[str, Any]) -> list[str]: + result: list[str] = [] + for metrics in (packet.get("domains") or {}).values(): + if not isinstance(metrics, list): + continue + for metric in metrics: + if isinstance(metric, dict) and metric.get("data_type"): + value = str(metric["data_type"]) + if value not in result: + result.append(value) + return result + + +def _evidence_data_types(packet: dict[str, Any]) -> list[str]: + result: list[str] = [] + for insight in packet.get("strongest_evidence") or []: + if not isinstance(insight, dict): + continue + for value in insight.get("data_types") or []: + data_type = str(value) + if data_type in DATA_TYPE_TO_TOPIC and data_type not in result: + result.append(data_type) + return result + + +def _scientific_types_for_request( + result: dict[str, Any], question: str, analysis_mode: str +) -> tuple[list[str], bool]: + metadata = result.get("packet") or {} + retrieval_mode = str(metadata.get("retrieval_mode") or "") + question_types = _question_science_types(question) + present = _present_data_types(result) + + if question_types: + # Scientific explanations remain available even if that metric has no personal data. + return question_types, _science_question(question) + if retrieval_mode == "specific_metrics": + selected = [str(value) for value in metadata.get("retrieval_selected_data_types") or []] + return selected or present, _science_question(question) + if retrieval_mode == "domain": + return present[:6], False + if analysis_mode == "deep" or retrieval_mode == "global": + evidence_types = _evidence_data_types(result) + return (evidence_types or present)[:8], False + return _evidence_data_types(result)[:4], False + + +def _attach_scientific_context( + result: dict[str, Any], + question: str, + *, + performance_profile: str, + analysis_mode: str, +) -> dict[str, Any]: + data_types, wants_detail = _scientific_types_for_request(result, question, analysis_mode) + if not data_types: + return result + + # Full explanatory cards are reserved for focused scientific/"why" questions. + detailed = wants_detail and len(data_types) <= 2 and performance_profile != "fast" + maximum = 2 if detailed else (4 if performance_profile == "fast" else 6 if performance_profile == "standard" else 8) + contexts = scientific_context_for_types(data_types, detailed=detailed, maximum=maximum) + if not contexts: + return result + + result["scientific_context"] = { + "knowledge_base_version": KNOWLEDGE_BASE_VERSION, + "role": ( + "Curated general scientific background, not evidence that any listed mechanism " + "or cause applies to this user. Match explanations to the measured evidence." + ), + "model_knowledge_rule": ( + "The model may add established general scientific knowledge beyond this catalogue, " + "but must label unsupported user-specific causes as possibilities and must not invent " + "measurements, symptoms, diagnoses or clinical thresholds." + ), + "metrics": contexts, + } + + target = ai_adaptive_retrieval.PROFILE_EVIDENCE_TARGETS.get( + performance_profile, + ai_adaptive_retrieval.PROFILE_EVIDENCE_TARGETS["standard"], + ) + # Give the existing evidence trimmer first chance to remove redundant optional details. + ai_adaptive_retrieval._trim_to_target(result, target) + metrics = result.get("scientific_context", {}).get("metrics") + while ( + isinstance(metrics, dict) + and len(metrics) > 1 + and ai_adaptive_retrieval.estimate_json_tokens(result) > target + ): + metrics.pop(next(reversed(metrics))) + if ai_adaptive_retrieval.estimate_json_tokens(result) > target and not detailed: + result.pop("scientific_context", None) + return result + + +def _select_with_science( + packet: dict[str, Any], + question: str, + *, + performance_profile: str = "standard", + analysis_mode: str = "question", +) -> dict[str, Any]: + if _ORIGINAL_SELECT is None: + raise RuntimeError("Scientific context integration was not initialized") + result = _ORIGINAL_SELECT( + packet, + question, + performance_profile=performance_profile, + analysis_mode=analysis_mode, + ) + result = _attach_scientific_context( + result, + question, + performance_profile=performance_profile, + analysis_mode=analysis_mode, + ) + metadata = result.get("packet") + if isinstance(metadata, dict): + metadata["scientific_context_version"] = KNOWLEDGE_BASE_VERSION + metadata["estimated_tokens"] = ai_adaptive_retrieval.estimate_json_tokens(result) + metadata["json_bytes"] = ai_adaptive_retrieval.json_size_bytes(result) + return result + + +_SCIENTIFIC_SYSTEM_PROMPT = """You are VitalChronicle's local health-data synthesis module. +The supplied JSON can contain two different epistemic layers: +1) measured/derived personal evidence (coverage, domains, strongest_evidence, associations), and +2) scientific_context, which is curated general background about what metrics mean and which +mechanisms or confounders can plausibly affect them. + +Personal evidence tells you what happened to this user. scientific_context does NOT prove that any +listed cause applies to this user. You may also use your own established general scientific knowledge +to explain physiology or add plausible mechanisms, especially when the user asks a scientific +question. Clearly distinguish general knowledge and plausible explanations from user-specific facts. +Never invent measurements, symptoms or events. If your internal knowledge conflicts with supplied +curated scientific_context, prefer the supplied context and express uncertainty. + +Read coverage first, then domains, strongest_evidence, associations and relevant scientific_context. +Lead with the strongest useful finding and quantify supported changes. Cite evidence_id values in +square brackets for important user-specific claims. A higher or lower value is not automatically +better or worse. Missing data are not zero. If observation says the current day is incomplete, do not +compare partial totals with complete days or extrapolate them linearly. Limit personal conclusions to +dates and metrics actually covered by the packet. + +When explaining a variation, rank explanations by how well they fit concurrent measured data. For +example, an increase in heart rate overlapping an exercise session has direct contextual support; +infection, dehydration, heat or stress remain possibilities unless corroborated by other supplied +signals. Multi-metric patterns can strengthen plausibility but remain nonspecific. + +Associations are exploratory and do not prove causation, prediction or diagnosis. Wearable data can +contain measurement error and weak coverage lowers confidence. Do not diagnose, prescribe, change +treatment, or invent clinical thresholds. If a potentially relevant pattern deserves follow-up, say +which measurements could be reviewed with a qualified professional. + +For complete-history analysis, synthesize across all available domains without creating sections for +absent domains. Prefer sustained matched-period changes, personal baselines, robust anomalies and +cross-domain patterns over isolated values. End with important uncertainty and a short list of useful +things to monitor. +""" + + +def _scientific_system_prompt() -> str: + language = ai_engine.RESPONSE_LANGUAGE_NAMES.get(ai_engine.current_language(), "English") + return ( + _SCIENTIFIC_SYSTEM_PROMPT + + "\nRespond to the user in " + + language + + ". Keep JSON field names, evidence_id values and scientific source identifiers unchanged." + ) + + +def install_scientific_context_core() -> None: + """Install selective scientific retrieval and a non-restrictive evidence hierarchy.""" + global _INSTALLED, _ORIGINAL_SELECT + if _INSTALLED: + return + _ORIGINAL_SELECT = ai_adaptive_retrieval.select_evidence_for_request + ai_adaptive_retrieval.select_evidence_for_request = _select_with_science + ai_engine.compact_system_prompt = _scientific_system_prompt + _INSTALLED = True From 2163f97c5f4a9ea6b2d88eeed599c1b2074e3493 Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:26:43 +0200 Subject: [PATCH 3/6] Install scientific interpretation layer --- google_health_viewer/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/google_health_viewer/__init__.py b/google_health_viewer/__init__.py index b53ac8f..a23af55 100644 --- a/google_health_viewer/__init__.py +++ b/google_health_viewer/__init__.py @@ -8,6 +8,9 @@ install_deterministic_detail_core as _install_deterministic_detail_core, ) from .heart_rate_core import install_shared_heart_rate_core as _install_shared_heart_rate_core +from .scientific_context_core import ( + install_scientific_context_core as _install_scientific_context_core, +) __version__ = "1.2.1" @@ -24,3 +27,7 @@ # Correct Google Health sleep-stage/short-awakening parsing and add temporal # heart-rate context for detected workouts/activity levels. _install_deterministic_context_patch(_analysis) + +# Selectively add curated scientific interpretation to relevant AI requests while +# still allowing the language model to supplement it with established general knowledge. +_install_scientific_context_core() From 2b9cd900ba07f572288a5b9eaa72655f254f474c Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:27:07 +0200 Subject: [PATCH 4/6] Test scientific interpretation retrieval and coverage --- tests/test_scientific_context.py | 104 +++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_scientific_context.py diff --git a/tests/test_scientific_context.py b/tests/test_scientific_context.py new file mode 100644 index 0000000..132076b --- /dev/null +++ b/tests/test_scientific_context.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from google_health_viewer import ai_adaptive_retrieval, ai_engine +from google_health_viewer.constants import DATA_TYPES +from google_health_viewer.scientific_context import ( + DATA_TYPE_TO_TOPIC, + scientific_context_for, +) + + +def _packet(data_type: str | None = None) -> dict: + domains = {} + if data_type is not None: + domains = { + "vitals": [ + { + "data_type": data_type, + "label": "Sleep temperature variation", + "metric": "Temperature variation", + "unit": "°C", + "summary": { + "count": 12, + "latest": 0.4, + "mean": 0.1, + "trend_percent": 20.0, + }, + "domain": "vitals", + } + ] + } + return { + "packet": {"health_evidence_present": True}, + "period": {"start": "2026-08-01", "end": "2026-09-01"}, + "coverage": {}, + "domains": domains, + "strongest_evidence": [], + "associations": [], + "archive_quality": {}, + } + + +def test_every_supported_google_health_type_has_scientific_context(): + keys = {spec.key for spec in DATA_TYPES} + assert keys == set(DATA_TYPE_TO_TOPIC) + assert all(scientific_context_for(key) is not None for key in keys) + + +def test_focused_temperature_question_receives_detailed_scientific_context(): + result = ai_adaptive_retrieval.select_evidence_for_request( + _packet("daily-sleep-temperature-derivations"), + "Perché la mia temperatura nel sonno è aumentata?", + performance_profile="standard", + ) + + science = result["scientific_context"] + context = science["metrics"]["daily-sleep-temperature-derivations"] + assert context["topic"] == "temperature" + assert context["confounders"] + assert context["relationships"] + assert context["sources"] + assert "not evidence" in science["role"] + assert ai_adaptive_retrieval.estimate_json_tokens(result) <= 2500 + + +def test_scientific_definition_is_available_without_personal_measurements(): + result = ai_adaptive_retrieval.select_evidence_for_request( + _packet(), + "Cosa significa HRV?", + performance_profile="standard", + ) + + science = result["scientific_context"]["metrics"] + assert "daily-heart-rate-variability" in science + assert "source_ids" in science["daily-heart-rate-variability"] + + +def test_general_analysis_uses_compact_science_only_for_relevant_evidence(): + packet = _packet("daily-sleep-temperature-derivations") + packet["strongest_evidence"] = [ + { + "kind": "trend", + "data_types": ["daily-sleep-temperature-derivations"], + "headline": "Temperature increased", + "relevance_score": 0.8, + } + ] + result = ai_adaptive_retrieval.select_evidence_for_request( + packet, + "Analizza tutto", + performance_profile="standard", + analysis_mode="deep", + ) + + context = result["scientific_context"]["metrics"]["daily-sleep-temperature-derivations"] + assert "sources" not in context + assert "confounders" not in context + assert context["baseline_rule"] + + +def test_system_prompt_allows_general_knowledge_but_preserves_evidence_hierarchy(): + prompt = ai_engine.compact_system_prompt() + assert "own established general scientific knowledge" in prompt + assert "does NOT prove" in prompt + assert "Never invent measurements" in prompt From 56e22db3c8346dcaecaba4b9b6daddec691184a3 Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:29:10 +0200 Subject: [PATCH 5/6] Preserve full evidence in maximum deep analysis --- .../scientific_context_preserve_core.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 google_health_viewer/scientific_context_preserve_core.py diff --git a/google_health_viewer/scientific_context_preserve_core.py b/google_health_viewer/scientific_context_preserve_core.py new file mode 100644 index 0000000..a3a924e --- /dev/null +++ b/google_health_viewer/scientific_context_preserve_core.py @@ -0,0 +1,64 @@ +"""Preserve Maximum/deep evidence breadth while adding compact scientific context.""" + +from __future__ import annotations + +from typing import Any + +from . import scientific_context_core +from .scientific_context import KNOWLEDGE_BASE_VERSION, scientific_context_for_types + +_INSTALLED = False +_ORIGINAL_ATTACH = None + + +def _attach_preserving_maximum( + result: dict[str, Any], + question: str, + *, + performance_profile: str, + analysis_mode: str, +) -> dict[str, Any]: + if _ORIGINAL_ATTACH is None: + raise RuntimeError("Scientific context preserve hook was not initialized") + if performance_profile != "max" or analysis_mode != "deep": + return _ORIGINAL_ATTACH( + result, + question, + performance_profile=performance_profile, + analysis_mode=analysis_mode, + ) + + data_types, _wants_detail = scientific_context_core._scientific_types_for_request( + result, question, analysis_mode + ) + if not data_types: + return result + contexts = scientific_context_for_types(data_types, detailed=False, maximum=8) + if not contexts: + return result + + result["scientific_context"] = { + "knowledge_base_version": KNOWLEDGE_BASE_VERSION, + "role": ( + "Curated general scientific background, not evidence that any listed mechanism " + "or cause applies to this user. Match explanations to the measured evidence." + ), + "model_knowledge_rule": ( + "The model may add established general scientific knowledge beyond this catalogue, " + "but must label unsupported user-specific causes as possibilities and must not invent " + "measurements, symptoms, diagnoses or clinical thresholds." + ), + "metrics": contexts, + } + # Maximum/deep has a pre-existing guarantee that every compact personal metric is preserved. + # Scientific background is additive and must never evict deterministic evidence. + return result + + +def install_scientific_context_preserve_core() -> None: + global _INSTALLED, _ORIGINAL_ATTACH + if _INSTALLED: + return + _ORIGINAL_ATTACH = scientific_context_core._attach_scientific_context + scientific_context_core._attach_scientific_context = _attach_preserving_maximum + _INSTALLED = True From 90a26f2d63f94821ab65d4d64777213ff4e09c28 Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Fri, 4 Sep 2026 15:29:22 +0200 Subject: [PATCH 6/6] Keep maximum deep evidence complete --- google_health_viewer/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/google_health_viewer/__init__.py b/google_health_viewer/__init__.py index a23af55..963b3f7 100644 --- a/google_health_viewer/__init__.py +++ b/google_health_viewer/__init__.py @@ -11,6 +11,9 @@ from .scientific_context_core import ( install_scientific_context_core as _install_scientific_context_core, ) +from .scientific_context_preserve_core import ( + install_scientific_context_preserve_core as _install_scientific_context_preserve_core, +) __version__ = "1.2.1" @@ -31,3 +34,7 @@ # Selectively add curated scientific interpretation to relevant AI requests while # still allowing the language model to supplement it with established general knowledge. _install_scientific_context_core() + +# Preserve the established Maximum/deep guarantee: scientific context is additive +# there and must never evict compact deterministic measurements. +_install_scientific_context_preserve_core()