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
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user