Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| import json | |
| import os | |
| import sys | |
| import tempfile | |
| import unittest | |
| from pathlib import Path | |
| from unittest.mock import patch | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| from carepath.config import Settings | |
| from carepath.services.llm import LLMError, build_llm | |
| from carepath.services.retrieval import RetrievedTerm | |
| from carepath.services.scribe_local import LocalScribeLLM | |
| def _bundle( | |
| root: Path, *, scope: str = "research_only", correction_mode: str = "adapter" | |
| ) -> Path: | |
| adapter_names = ("gec", "soap") if correction_mode == "adapter" else ("soap",) | |
| for name in adapter_names: | |
| (root / "adapters" / name).mkdir(parents=True) | |
| manifest = { | |
| "schema": "carepath.scribe.bundle/1", | |
| "usage_scope": scope, | |
| "promotion_status": "blocked_research_only", | |
| "base_model": "Qwen/Qwen3-4B-Instruct-2507", | |
| "adapters": {name: f"adapters/{name}" for name in adapter_names}, | |
| "correction_mode": correction_mode, | |
| } | |
| (root / "scribe_manifest.json").write_text(json.dumps(manifest), encoding="utf-8") | |
| return root | |
| class LocalScribeTests(unittest.TestCase): | |
| def test_dual_adapter_flow_is_grounded_and_identifiable(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| calls: list[str] = [] | |
| def generate(adapter: str, prompt: str) -> str: | |
| calls.append(adapter) | |
| task = json.loads(prompt)["task"] | |
| if task == "correct_asr_transcript": | |
| return "Bệnh nhân dùng metformin 500 mg" | |
| if task == "extract_grounded_clinical_facts": | |
| transcript = "Bệnh nhân dùng metformin 500 mg" | |
| span = "dùng metformin 500 mg" | |
| start = transcript.index(span) | |
| return json.dumps( | |
| { | |
| "facts": [ | |
| { | |
| "type": "medication", | |
| "value": "metformin 500 mg", | |
| "negated": False, | |
| "uncertain": False, | |
| "source_span": { | |
| "start": start, | |
| "end": start + len(span), | |
| "text": span, | |
| }, | |
| } | |
| ] | |
| } | |
| ) | |
| return json.dumps( | |
| { | |
| "subjective": "metformin 500 mg", | |
| "objective": "Chưa có thông tin khách quan.", | |
| "assessment": "Chưa có đánh giá trong bản ghi.", | |
| "plan": "metformin 500 mg", | |
| "missing_information": ["Đánh giá"], | |
| "review_required": False, | |
| } | |
| ) | |
| llm = LocalScribeLLM(_bundle(Path(temp)), generate_fn=generate) | |
| terms = [RetrievedTerm("metformin", 1.0, "drug", "test")] | |
| correction = llm.correct_transcript("Benh nhan dung metformin 500 mg", terms) | |
| result = llm.generate_soap(correction.corrected_text, terms) | |
| self.assertEqual(correction.provider, "scribe_local") | |
| self.assertEqual(result.provider, "scribe_local") | |
| self.assertTrue(result.soap.review_required) | |
| self.assertEqual(calls, ["gec", "soap", "soap"]) | |
| ready, details = llm.readiness() | |
| self.assertTrue(ready) | |
| self.assertEqual(details["promotion_status"], "blocked_research_only") | |
| self.assertEqual(details["fallback"], "disabled") | |
| def test_unsupported_fact_and_number_fail_closed(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| outputs = iter( | |
| [ | |
| json.dumps( | |
| { | |
| "facts": [ | |
| { | |
| "type": "medication", | |
| "value": "warfarin 5 mg", | |
| "source_span": { | |
| "start": 0, | |
| "end": 13, | |
| "text": "warfarin 5 mg", | |
| }, | |
| } | |
| ] | |
| } | |
| ) | |
| ] | |
| ) | |
| llm = LocalScribeLLM( | |
| _bundle(Path(temp)), generate_fn=lambda adapter, prompt: next(outputs) | |
| ) | |
| with self.assertRaises(LLMError): | |
| llm.generate_soap("Bệnh nhân dùng metformin 500 mg", []) | |
| def test_writer_cannot_append_text_outside_grounded_fact_values(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| transcript = "Bác sĩ đánh giá viêm họng" | |
| span = "viêm họng" | |
| start = transcript.index(span) | |
| outputs = iter( | |
| [ | |
| json.dumps( | |
| { | |
| "facts": [ | |
| { | |
| "type": "assessment", | |
| "value": span, | |
| "negated": False, | |
| "uncertain": False, | |
| "source_span": { | |
| "start": start, | |
| "end": start + len(span), | |
| "text": span, | |
| }, | |
| } | |
| ] | |
| } | |
| ), | |
| json.dumps( | |
| { | |
| "subjective": "Chưa có thông tin chủ quan.", | |
| "objective": "Chưa có thông tin khách quan.", | |
| "assessment": "viêm họng; ung thư", | |
| "plan": "Chưa có kế hoạch trong bản ghi.", | |
| "missing_information": [], | |
| "review_required": True, | |
| } | |
| ), | |
| ] | |
| ) | |
| llm = LocalScribeLLM( | |
| _bundle(Path(temp), correction_mode="identity"), | |
| generate_fn=lambda adapter, prompt: next(outputs), | |
| ) | |
| with self.assertRaisesRegex(LLMError, "outside grounded fact values"): | |
| llm.generate_soap(transcript, []) | |
| def test_manifest_cannot_claim_promotable_scope(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| with self.assertRaisesRegex(ValueError, "research_only"): | |
| LocalScribeLLM(_bundle(Path(temp), scope="production")) | |
| def test_soap_only_bundle_declares_identity_correction(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| llm = LocalScribeLLM(_bundle(Path(temp), correction_mode="identity")) | |
| correction = llm.correct_transcript("Giữ nguyên bản ghi", []) | |
| ready, details = llm.readiness() | |
| self.assertTrue(ready) | |
| self.assertEqual(correction.corrected_text, "Giữ nguyên bản ghi") | |
| self.assertEqual(correction.provider, "scribe_local_identity") | |
| self.assertEqual(details["adapters"], ["soap"]) | |
| def test_build_llm_requires_explicit_staging_bundle_without_fallback(self) -> None: | |
| with tempfile.TemporaryDirectory() as temp: | |
| _bundle(Path(temp)) | |
| with patch.dict( | |
| os.environ, | |
| { | |
| "LLM_PROVIDER": "scribe_local", | |
| "SCRIBE_BUNDLE_PATH": temp, | |
| "LLM_FALLBACK_OFFLINE": "false", | |
| }, | |
| clear=True, | |
| ): | |
| llm = build_llm(Settings.from_env()) | |
| self.assertIsInstance(llm, LocalScribeLLM) | |
| if __name__ == "__main__": | |
| unittest.main() | |