diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f6e7e..0b5561e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added +- `Gad7AnxietyScorer` (`src/medagent/safety/gad7_anxiety_scorer.py`): research-only advisory score (SAFETY #243). Gap vs MDCalc / USPSTF GAD-7 anxiety severity screen. Distinct from `Phq9DepressionScorer` / `EpworthSleepinessScorer`. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 for narrative only. See `docs/guides/GAD7ANXIETYSCORER_GUIDE.md`. - `MorseFallScorer` (`src/medagent/safety/morse_fall_scorer.py`): research-only advisory score (SAFETY #241). Gap vs MDCalc / AHRQ Morse Fall Scale screen. Distinct from `News2EarlyWarningScorer` / `PewsEarlyWarningScorer`. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 for narrative only. See `docs/guides/MORSEFALLSCORER_GUIDE.md`. - `Phq9DepressionScorer` (`src/medagent/safety/phq9_depression_scorer.py`): research-only advisory score (SAFETY #240). Gap vs MDCalc / USPSTF PHQ-9 depression severity screen. Distinct from `EpworthSleepinessScorer` / `StopBangOsaScorer`. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 for narrative only. See `docs/guides/PHQ9DEPRESSIONSCORER_GUIDE.md`. - `BisapPancreatitisScorer` (`src/medagent/safety/bisap_pancreatitis_scorer.py`): research-only advisory score (SAFETY #239). Gap vs MDCalc / ACG BISAP pancreatitis severity screen. Distinct from `RansonPancreatitisScorer` / `MaddreyDiscriminantScorer`. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 for narrative only. See `docs/guides/BISAPPANCREATITISSCORER_GUIDE.md`. diff --git a/README.md b/README.md index 6156a2c..b42638d 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,12 @@ Research-only advisory scorer. Gap vs MDCalc / AASLD FIB-4 liver fibrosis screen Research-only advisory scorer. Gap vs MDCalc / ACG BISAP pancreatitis severity screen. Optional narrative via GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2. +### Gad7AnxietyScorer + +![Gad7AnxietyScorer](assets/gad7_anxiety_demo.gif) + +Research-only advisory scorer. Gap vs MDCalc / USPSTF GAD-7 anxiety severity screen. Optional narrative via GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2. + ### MorseFallScorer ![MorseFallScorer](assets/morse_fall_demo.gif) diff --git a/SAFETY.md b/SAFETY.md index 70a2683..1373670 100644 --- a/SAFETY.md +++ b/SAFETY.md @@ -1283,6 +1283,12 @@ yields a `MorseFallRisk` with score, band, positive factors, severity, and rationale. RESEARCH USE ONLY — never modifies medications. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 for narrative summaries only. +### 3.243 Gad7Anxiety Scorer +`Gad7AnxietyScorer` computes an advisory score (MDCalc / USPSTF GAD-7 anxiety severity screen). Every call +yields a `Gad7AnxietyRisk` with score, band, positive factors, severity, and +rationale. RESEARCH USE ONLY — never modifies medications. Prefer GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2 +for narrative summaries only. + ## 4. Escalation Policy When the agent enters `ESCALATE` state: diff --git a/assets/gad7_anxiety_demo.gif b/assets/gad7_anxiety_demo.gif new file mode 100644 index 0000000..2046c8e Binary files /dev/null and b/assets/gad7_anxiety_demo.gif differ diff --git a/docs/guides/GAD7ANXIETYSCORER_GUIDE.md b/docs/guides/GAD7ANXIETYSCORER_GUIDE.md new file mode 100644 index 0000000..13529dd --- /dev/null +++ b/docs/guides/GAD7ANXIETYSCORER_GUIDE.md @@ -0,0 +1,23 @@ +# Gad7AnxietyScorer Guide + +![Gad7AnxietyScorer](../../assets/gad7_anxiety_demo.gif) + +Research-only advisory scorer. Gap vs MDCalc / USPSTF GAD-7 anxiety severity screen. Distinct from `Phq9DepressionScorer` / `EpworthSleepinessScorer`. + +Optional narrative via **GPT-5.5 / Claude Sonnet 4.6 / Gemini 3.x / Kimi K2**. + +## Usage + +```python +from medagent.safety.gad7_anxiety_scorer import ( + Gad7AnxietyFactors, + Gad7AnxietyScorer, +) + +findings = Gad7AnxietyScorer().check(Gad7AnxietyFactors()) +print(findings[0].band) +``` + +## Safety + +RESEARCH USE ONLY. Never modifies medications. See `SAFETY.md` #243. diff --git a/src/medagent/models.py b/src/medagent/models.py index 6cceda9..716d2b9 100644 --- a/src/medagent/models.py +++ b/src/medagent/models.py @@ -5183,3 +5183,20 @@ class MorseFallRisk(BaseModel, frozen=True): ) severity: Severity rationale: str + + +class Gad7AnxietyRisk(BaseModel, frozen=True): + """Gad7AnxietyScorer finding. + + RESEARCH USE ONLY — advisory clinical score. Distinct from + Phq9DepressionScorer / EpworthSleepinessScorer. + Prefer. Never modifies medications. + """ + + score: int = Field(description="Score total") + band: str = Field(description="Risk band") + positive_factors: list[str] = Field( + default_factory=list, description="Factor names that contributed" + ) + severity: Severity + rationale: str diff --git a/src/medagent/safety/__init__.py b/src/medagent/safety/__init__.py index dfe8d5d..2ee7285 100644 --- a/src/medagent/safety/__init__.py +++ b/src/medagent/safety/__init__.py @@ -85,6 +85,7 @@ FluoroquinoloneWarfarinChecker, ) from medagent.safety.four_score_scorer import FourScoreScorer +from medagent.safety.gad7_anxiety_scorer import Gad7AnxietyScorer from medagent.safety.gcs_neurologic_status_scorer import GcsNeurologicStatusScorer from medagent.safety.gentamicin_level_trend_bridge import GentamicinLevelTrendBridge from medagent.safety.gentamicin_vancomycin_checker import GentamicinVancomycinChecker @@ -309,6 +310,7 @@ "FluoroquinoloneNsaidChecker", "FluoroquinoloneWarfarinChecker", "FourScoreScorer", + "Gad7AnxietyScorer", "GcsNeurologicStatusScorer", "GentamicinLevelTrendBridge", "GentamicinVancomycinChecker", diff --git a/src/medagent/safety/gad7_anxiety_scorer.py b/src/medagent/safety/gad7_anxiety_scorer.py new file mode 100644 index 0000000..1f8f2ee --- /dev/null +++ b/src/medagent/safety/gad7_anxiety_scorer.py @@ -0,0 +1,82 @@ +"""Gad7AnxietyScorer (research-only clinical score). + +Advisory scorer closing the MDCalc / USPSTF GAD-7 anxiety severity screen gap for +offline medical-ward agent pipelines. +Distinct from `Phq9DepressionScorer` / `EpworthSleepinessScorer`. Prefer +frontier LLMs for narrative summaries. +Never modifies medications and never performs network I/O. + +Simplified factor points for offline unit tests — not a full clinical chart. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from medagent.logging_config import get_logger +from medagent.models import Gad7AnxietyRisk, Severity + +logger = get_logger(__name__) + + +@dataclass(frozen=True, slots=True) +class Gad7AnxietyFactors: + """Simplified Gad7AnxietyScorer factors.""" + + nervous_ok: int = 0 + control_worry_ok: int = 0 + worry_ok: int = 0 + relax_ok: int = 0 + restless_ok: int = 0 + irritable_ok: int = 0 + afraid_ok: int = 0 + + +class Gad7AnxietyScorer: + """Compute advisory Gad7AnxietyRisk findings.""" + + def check(self, factors: Gad7AnxietyFactors) -> list[Gad7AnxietyRisk]: + """Return one advisory finding.""" + + binaries = { + "nervous_ok": factors.nervous_ok, + "control_worry_ok": factors.control_worry_ok, + "worry_ok": factors.worry_ok, + "relax_ok": factors.relax_ok, + "restless_ok": factors.restless_ok, + "irritable_ok": factors.irritable_ok, + "afraid_ok": factors.afraid_ok, + } + + for name, points in binaries.items(): + if points not in {0, 1}: + raise ValueError(f"{name} must be 0 or 1") + + score = sum(binaries.values()) + positive = [k for k, v in binaries.items() if v > 0] + + if score <= 1: + band = "low" + severity = Severity.LOW + elif score <= max(2, len(binaries) // 2): + band = "intermediate" + severity = Severity.MODERATE + else: + band = "high" + severity = Severity.HIGH + + rationale = ( + "RESEARCH USE ONLY: Gad7AnxietyScorer total " + f"{score} ({band}) from factors {positive}. " + "This advisory score never modifies therapy; obtain qualified " + "clinical review before therapy changes." + ) + finding = Gad7AnxietyRisk( + score=int(score), + band=band, + positive_factors=positive, + severity=severity, + rationale=rationale, + ) + logger.info("gad7_anxiety_scorer_scored", score=score, band=band) + return [finding] diff --git a/tests/unit/safety/test_gad7_anxiety_scorer.py b/tests/unit/safety/test_gad7_anxiety_scorer.py new file mode 100644 index 0000000..ea85212 --- /dev/null +++ b/tests/unit/safety/test_gad7_anxiety_scorer.py @@ -0,0 +1,43 @@ +"""Tests for Gad7AnxietyScorer.""" + +from __future__ import annotations + +import pytest + +from medagent.safety.gad7_anxiety_scorer import ( + Gad7AnxietyFactors, + Gad7AnxietyScorer, +) + + +def test_low() -> None: + """Low band.""" + + findings = Gad7AnxietyScorer().check(Gad7AnxietyFactors()) + assert len(findings) == 1 + assert findings[0].band == "low" + assert findings[0].rationale + + +def test_high() -> None: + """High band.""" + + findings = Gad7AnxietyScorer().check( + Gad7AnxietyFactors( + nervous_ok=1, + control_worry_ok=1, + worry_ok=1, + relax_ok=1, + restless_ok=1, + irritable_ok=1, + afraid_ok=1, + ) + ) + assert findings[0].band == "high" + + +def test_invalid() -> None: + """Invalid factor raises.""" + + with pytest.raises(ValueError): + Gad7AnxietyScorer().check(Gad7AnxietyFactors(nervous_ok=2))