# -*- coding: utf-8 -*- """ API-Client für Desktop-Anwendungen. Stellt alle Backend-Aufrufe als einfache Funktionen bereit. Hybrid-Modus: versucht Backend, fällt auf lokalen OpenAI-Call zurück. from workforce_planner.api_client import WorkforceClient client = WorkforceClient("http://localhost:8000") client.login("admin@praxis.ch", "admin123") result = client.transcribe_audio("recording.wav") """ import os import threading from typing import Optional, Callable import requests from requests.exceptions import ConnectionError, Timeout, RequestException _BACKEND_TIMEOUT_TRANSCRIBE = 20 _BACKEND_TIMEOUT_DEFAULT = 12 class WorkforceClient: """HTTP-Client für das workforce_planner Backend.""" def __init__(self, base_url: str = "http://localhost:8000"): self.base_url = base_url.rstrip("/") self.token: Optional[str] = None self.employee: Optional[dict] = None self._session = requests.Session() @property def _headers(self) -> dict: h = {} if self.token: h["Authorization"] = f"Bearer {self.token}" return h @property def is_authenticated(self) -> bool: return self.token is not None def is_backend_available(self) -> bool: try: r = self._session.get( f"{self.base_url}/api/v1/health", timeout=3 ) return r.status_code == 200 except RequestException: return False # ─── Auth ─────────────────────────────────── def login(self, email: str, password: str) -> dict: r = self._session.post( f"{self.base_url}/api/v1/auth/login", json={"email": email, "password": password}, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() data = r.json() self.token = data["access_token"] self.employee = data["employee"] return data def logout(self): self.token = None self.employee = None # ─── Employees ────────────────────────────── def list_employees(self, active_only: bool = True) -> list[dict]: r = self._session.get( f"{self.base_url}/api/v1/employees/", headers=self._headers, params={"active_only": active_only}, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() return r.json() def create_employee(self, data: dict) -> dict: r = self._session.post( f"{self.base_url}/api/v1/employees/", headers=self._headers, json=data, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() return r.json() def update_employee(self, employee_id: str, data: dict) -> dict: r = self._session.patch( f"{self.base_url}/api/v1/employees/{employee_id}", headers=self._headers, json=data, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() return r.json() def delete_employee(self, employee_id: str): r = self._session.delete( f"{self.base_url}/api/v1/employees/{employee_id}", headers=self._headers, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() # ─── Absences ─────────────────────────────── def list_absences(self, year: Optional[int] = None, employee_id: Optional[str] = None) -> list[dict]: params = {} if year: params["year"] = year if employee_id: params["employee_id"] = employee_id r = self._session.get( f"{self.base_url}/api/v1/absences/", headers=self._headers, params=params, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() return r.json() def create_absence(self, data: dict) -> dict: r = self._session.post( f"{self.base_url}/api/v1/absences/", headers=self._headers, json=data, timeout=_BACKEND_TIMEOUT_DEFAULT, ) if r.status_code == 422: detail = r.json().get("detail", {}) raise ValueError(detail.get("message", str(detail))) r.raise_for_status() return r.json() def delete_absence(self, absence_id: str): r = self._session.delete( f"{self.base_url}/api/v1/absences/{absence_id}", headers=self._headers, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() def get_balance(self, employee_id: str, year: int) -> dict: r = self._session.get( f"{self.base_url}/api/v1/balance/{employee_id}/{year}", headers=self._headers, timeout=_BACKEND_TIMEOUT_DEFAULT, ) r.raise_for_status() return r.json() # ─── KI-Service (Hybrid) ──────────────────── def transcribe_audio(self, wav_path: str, language: str = "de") -> dict: """ Sendet WAV an Backend. Bei Fehler: wirft Exception (Fallback wird vom Caller gehandhabt). """ with open(wav_path, "rb") as f: r = self._session.post( f"{self.base_url}/api/v1/ai/transcribe", headers=self._headers, files={"file": (os.path.basename(wav_path), f, "audio/wav")}, data={"language": language}, timeout=_BACKEND_TIMEOUT_TRANSCRIBE, ) r.raise_for_status() return r.json() def summarize(self, transcript: str, system_prompt: str = "", model: str = "gpt-4o") -> dict: r = self._session.post( f"{self.base_url}/api/v1/ai/summarize", headers=self._headers, json={"transcript": transcript, "system_prompt": system_prompt, "model": model}, timeout=30, ) r.raise_for_status() return r.json() def merge_kg(self, existing_kg: str, full_transcript: str, system_prompt: str = "", model: str = "gpt-4o") -> dict: r = self._session.post( f"{self.base_url}/api/v1/ai/merge", headers=self._headers, json={ "existing_kg": existing_kg, "full_transcript": full_transcript, "system_prompt": system_prompt, "model": model, }, timeout=30, ) r.raise_for_status() return r.json() def chat(self, messages: list[dict], model: str = "gpt-4o") -> dict: r = self._session.post( f"{self.base_url}/api/v1/ai/chat", headers=self._headers, json={"messages": messages, "model": model}, timeout=30, ) r.raise_for_status() return r.json() # ═══════════════════════════════════════════════════════════ # HYBRID-TRANSKRIPTION: Backend mit lokalem Fallback # ═══════════════════════════════════════════════════════════ def hybrid_transcribe( wav_path: str, api_client: Optional[WorkforceClient], local_transcribe_fn: Callable[[str], str], on_success: Callable[[str, str], None], on_error: Optional[Callable[[str], None]] = None, ): """ Hybrid-Transkription: versucht Backend, fällt auf lokal zurück. Läuft in eigenem Thread → UI blockiert nie. Args: wav_path: Pfad zur WAV-Datei api_client: WorkforceClient (oder None = immer lokal) local_transcribe_fn: Bestehende lokale Transkription (self.transcribe_wav) on_success: Callback(text, source) – source = "backend" oder "local" on_error: Callback(error_message) bei totalem Fehler """ def _worker(): source = "local" text = "" # ── Versuch 1: Backend ── if api_client and api_client.is_authenticated: try: result = api_client.transcribe_audio(wav_path) text = result.get("text", "") source = "backend" except (ConnectionError, Timeout): pass except RequestException: pass # ── Versuch 2: Lokal (Fallback) ── if not text and source == "local": try: text = local_transcribe_fn(wav_path) source = "local" except Exception as exc: if on_error: on_error(f"Transkription fehlgeschlagen: {exc}") return on_success(text, source) t = threading.Thread(target=_worker, daemon=True) t.start() return t