fix: route medical content strictly local

This commit is contained in:
2026-07-30 19:29:55 +02:00
parent 77b510bd22
commit 9148193806
4 changed files with 50 additions and 6 deletions
+6 -4
View File
@@ -102,6 +102,8 @@ Rootserver:
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und - Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext. 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 - `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
durch Modus `local` und technischen Provider-Fallback. durch Modus `local` und technischen Provider-Fallback.
@@ -176,7 +178,7 @@ Letzter bestätigter Projektstand:
- uv-Lock und `uv sync --dev`: bestanden - uv-Lock und `uv sync --dev`: bestanden
- Python in `.venv`: 3.12.13 - Python in `.venv`: 3.12.13
- Ruff in `.venv`: 0.16.0 - 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 - PowerShell-Syntax des Startskripts: erfolgreich geparst
- Ruff Lint: bestanden - Ruff Lint: bestanden
- Ruff Formatprüfung: bestanden - Ruff Formatprüfung: bestanden
@@ -287,9 +289,9 @@ Abnahmestatus:
## Nächster sinnvoller Auftrag ## Nächster sinnvoller Auftrag
Ein separates, kleines Paket für Chatkomfort und Robustheit planen, Auf `feat/chat-comfort` als nächstes Streaming mit sicherem Abbruch umsetzen.
beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine Obsidian- Medizinische Antwortqualität bleibt ein späteres Sicherheits-/Systemprompt-Thema.
Integration, Tools, Sprache oder Serversteuerung beginnen. Noch keine Obsidian-Integration oder Tools beginnen.
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen. Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
+7 -2
View File
@@ -42,8 +42,13 @@ _NEVER_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
), ),
( (
re.compile( re.compile(
r"(?i)\b(diagnose|gesundheit|krankheit|medikament|patient|arztbericht|" r"(?i)\b(symptom(?:e|en)?|blut(?:e|est|et|en|ung|ungen|wert|werte)?|"
r"psychotherapie|blutwert)\b" 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", "Gesundheitsinformation",
), ),
+20
View File
@@ -63,6 +63,26 @@ class PrivacyRouterTests(unittest.TestCase):
CloudPolicy.NEVER, 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: def test_unknown_content_is_conservatively_ask(self) -> None:
self.assertEqual( self.assertEqual(
self.router.classify("Erkläre das bitte genauer.").policy, self.router.classify("Erkläre das bitte genauer.").policy,
+17
View File
@@ -131,6 +131,23 @@ class HybridProviderTests(unittest.TestCase):
self.assertTrue(router.last_route.fallback) self.assertTrue(router.last_route.fallback)
self.assertIn("lokale Antwort", self.notices[-1]) 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: def test_ask_defaults_to_local_when_approval_is_denied(self) -> None:
router = self._router() router = self._router()