fix: route medical content strictly local
This commit is contained in:
@@ -102,6 +102,8 @@ Rootserver:
|
||||
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
|
||||
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
|
||||
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext.
|
||||
- Gesundheits-, Symptom-, Blutungs-, Verletzungs-, Medikamenten-, Diagnose-,
|
||||
Behandlungs- und medizinische Notfallfragen sind zwingend lokal `never`.
|
||||
- `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
|
||||
durch Modus `local` und technischen Provider-Fallback.
|
||||
|
||||
@@ -176,7 +178,7 @@ Letzter bestätigter Projektstand:
|
||||
- uv-Lock und `uv sync --dev`: bestanden
|
||||
- Python in `.venv`: 3.12.13
|
||||
- Ruff in `.venv`: 0.16.0
|
||||
- Unit-Tests: 66 bestanden; neue Technikklassifikation und Privacy-Ausgabe abgedeckt
|
||||
- Unit-Tests: 69 bestanden; medizinisches `never` und Cloudclient-Sperre abgedeckt
|
||||
- PowerShell-Syntax des Startskripts: erfolgreich geparst
|
||||
- Ruff Lint: bestanden
|
||||
- Ruff Formatprüfung: bestanden
|
||||
@@ -287,9 +289,9 @@ Abnahmestatus:
|
||||
|
||||
## Nächster sinnvoller Auftrag
|
||||
|
||||
Ein separates, kleines Paket für Chatkomfort und Robustheit planen,
|
||||
beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine Obsidian-
|
||||
Integration, Tools, Sprache oder Serversteuerung beginnen.
|
||||
Auf `feat/chat-comfort` als nächstes Streaming mit sicherem Abbruch umsetzen.
|
||||
Medizinische Antwortqualität bleibt ein späteres Sicherheits-/Systemprompt-Thema.
|
||||
Noch keine Obsidian-Integration oder Tools beginnen.
|
||||
|
||||
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
|
||||
|
||||
|
||||
@@ -42,8 +42,13 @@ _NEVER_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
r"(?i)\b(diagnose|gesundheit|krankheit|medikament|patient|arztbericht|"
|
||||
r"psychotherapie|blutwert)\b"
|
||||
r"(?i)\b(symptom(?:e|en)?|blut(?:e|est|et|en|ung|ungen|wert|werte)?|"
|
||||
r"verletz(?:ung|ungen|t)|wunde[n]?|medikament(?:e|en)?|diagnos(?:e|en|tiziert)|"
|
||||
r"behandl(?:ung|ungen|e|en)|medizinisch(?:e[rsn]?|er)?\s+notfall|"
|
||||
r"notaufnahme|rettungsdienst|gesundheit|krankheit(?:en)?|patient(?:en)?|"
|
||||
r"arzt|ärzt(?:in|lich(?:e[rsn]?|er)?)|arztbericht|krankenhaus|"
|
||||
r"psychotherapie|schmerz(?:en)?|fieber|atemnot|bewusstlos|ohnmacht|"
|
||||
r"vergiftung|allergisch(?:e[rsn]?|er)?\s+reaktion)\b"
|
||||
),
|
||||
"Gesundheitsinformation",
|
||||
),
|
||||
|
||||
@@ -63,6 +63,26 @@ class PrivacyRouterTests(unittest.TestCase):
|
||||
CloudPolicy.NEVER,
|
||||
)
|
||||
|
||||
def test_medical_questions_are_always_never(self) -> None:
|
||||
for question in (
|
||||
"Wenn ich blute, sollte ich dann zum Arzt?",
|
||||
"Welche Symptome hat eine Grippe?",
|
||||
"Wie nehme ich dieses Medikament ein?",
|
||||
"Was bedeutet diese Diagnose?",
|
||||
"Wie sollte eine Verletzung behandelt werden?",
|
||||
):
|
||||
with self.subTest(question=question):
|
||||
self.assertEqual(
|
||||
self.router.classify(question).policy,
|
||||
CloudPolicy.NEVER,
|
||||
)
|
||||
|
||||
def test_technical_terms_cannot_override_medical_never(self) -> None:
|
||||
decision = self.router.classify(
|
||||
"Wie speichere ich meine Symptome und Diagnose mit Python in SQLite?"
|
||||
)
|
||||
self.assertEqual(decision.policy, CloudPolicy.NEVER)
|
||||
|
||||
def test_unknown_content_is_conservatively_ask(self) -> None:
|
||||
self.assertEqual(
|
||||
self.router.classify("Erkläre das bitte genauer.").policy,
|
||||
|
||||
@@ -131,6 +131,23 @@ class HybridProviderTests(unittest.TestCase):
|
||||
self.assertTrue(router.last_route.fallback)
|
||||
self.assertIn("lokale Antwort", self.notices[-1])
|
||||
|
||||
def test_medical_never_routes_locally_before_cloud_construction(self) -> None:
|
||||
constructed = False
|
||||
|
||||
def cloud_factory() -> _RecordingProvider:
|
||||
nonlocal constructed
|
||||
constructed = True
|
||||
return self.cloud
|
||||
|
||||
router = self._router(cloud_provider_factory=cloud_factory, mode="gemini")
|
||||
|
||||
answer = router.chat([ChatMessage("user", "Wenn ich blute, sollte ich dann zum Arzt?")])
|
||||
|
||||
self.assertEqual(answer, "Lokal")
|
||||
self.assertFalse(constructed)
|
||||
self.assertFalse(self.approvals)
|
||||
self.assertEqual(router.last_route.privacy_policy, CloudPolicy.NEVER)
|
||||
|
||||
def test_ask_defaults_to_local_when_approval_is_denied(self) -> None:
|
||||
router = self._router()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user