Logpy-AgentOne/pipe/stapel.py
Jeuner 1a1280e79d Praxis-Telefon-Agent: Anrufannahme, Transkription, Triage — vollständig on-prem
Nimmt Anrufe einer Hausarztpraxis entgegen, transkribiert sie lokal,
kategorisiert das Anliegen und legt es strukturiert ab. Keine Patientendaten
verlassen den Rechner: Spracherkennung, Sprachmodell und Sprachsynthese laufen
lokal, im Datenpfad steht kein Cloud-Dienst.

Aufbau:
- FreeSWITCH nimmt über einen SIP-Trunk an, spielt die Ansage und nimmt auf.
- pipe.watch erkennt fertige Aufnahmen und schiebt sie durch die Pipe.
- whisper.cpp transkribiert, ein lokales Sprachmodell (Ollama) ordnet in
  Kategorie und Dringlichkeit ein — mit erzwungenem JSON-Schema.
- Ablage lokal, optional Nextcloud (WebDAV) und ein Deck-Board.
- Leitstand (pipe.server): Board mit Eingang/In Bearbeitung/Rückfragen/Erledigt,
  Aufnahmen zum Anhören, Verlauf und Ampeln für alle beteiligten Dienste.

Zugangsdaten und Anrufdaten liegen außerhalb des Repositorys (.env, ablage/,
telefon/). Konfiguration siehe .env.example, Einrichtung siehe README.
2026-08-22 22:37:17 +02:00

58 lines
1.8 KiB
Python

"""Bearbeitungsstand eines Anrufs — wo er auf dem Board liegt.
Der Stand gehört nicht in ``meta.json``: dort steht, was der Anruf war, und das
ändert sich nie. Wo er in der Bearbeitung steht, ändert sich dauernd. Deshalb
eine eigene kleine Datei im Anrufordner — so bleibt beides unabhängig, und ein
verlorener Stand kostet nie die Anrufdaten.
"""
from __future__ import annotations
import json
from datetime import datetime
from . import config
DATEI = "stapel.json"
def erlaubt() -> list[str]:
return config.DECK_STAPEL
def lies(ordner) -> str:
"""Stapel eines Anrufs. Unbekanntes oder Fehlendes gilt als Eingang."""
standard = erlaubt()[0]
pfad = ordner / DATEI
if not pfad.is_file():
return standard
try:
wert = json.loads(pfad.read_text(encoding="utf-8")).get("stapel")
except Exception:
return standard
return wert if wert in erlaubt() else standard
def setze(ordner, stapel: str) -> bool:
"""Schreibt den Stapel. False, wenn der Name nicht vorgesehen ist."""
if stapel not in erlaubt():
return False
try:
(ordner / DATEI).write_text(json.dumps(
{"stapel": stapel, "geaendert": datetime.now().isoformat(timespec="seconds")},
ensure_ascii=False, indent=2), encoding="utf-8")
return True
except Exception:
return False
def ordner_zu(kennung: str):
"""Wandelt eine Anruf-Kennung (JJJJ-MM-TT/HH-MM-SS_nummer) in einen Pfad.
Prüft, dass der Pfad wirklich in der Ablage liegt — die Kennung kommt aus
dem Browser und ist damit nichts, worauf man sich verlässt.
"""
ziel = (config.ABLAGE_LOKAL / kennung).resolve()
wurzel = config.ABLAGE_LOKAL.resolve()
if not str(ziel).startswith(str(wurzel) + "/") or not ziel.is_dir():
return None
return ziel