update
This commit is contained in:
266
AzA march 2026/workforce_planner/api_client.py
Normal file
266
AzA march 2026/workforce_planner/api_client.py
Normal file
@@ -0,0 +1,266 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user