import asyncio import logging from pathlib import Path import httpx from .config import settings log = logging.getLogger("voice-agent.mac_client") # Transient-Fehler, bei denen ein einmaliger Retry sinnvoll ist # (typisch bei Ollama-Reload oder kurzem Mac-Worker-Restart). _RETRY_EXCEPTIONS = ( httpx.RemoteProtocolError, httpx.ConnectError, httpx.ReadError, ) class MacClient: def __init__(self, base_url: str | None = None, timeout: int | None = None): self.base_url = (base_url or settings.mac_api_url).rstrip("/") self.timeout = timeout or settings.mac_api_timeout @staticmethod def _hdrs(job_id: int | None) -> dict[str, str] | None: if job_id is None: return None return {"X-Job-Id": str(job_id)} async def _post_json_retry(self, url: str, payload: dict, job_id: int | None) -> dict: """JSON-POST mit einmaligem Retry bei transienten Verbindungsabbrüchen.""" for attempt in (0, 1): try: async with httpx.AsyncClient(timeout=self.timeout) as c: r = await c.post(url, json=payload, headers=self._hdrs(job_id)) r.raise_for_status() return r.json() except _RETRY_EXCEPTIONS as e: if attempt == 0: log.warning("[job %s] %s bei %s — Retry in 3s", job_id, type(e).__name__, url) await asyncio.sleep(3.0) continue raise async def _post_bytes_retry(self, url: str, payload: dict, job_id: int | None) -> bytes: for attempt in (0, 1): try: async with httpx.AsyncClient(timeout=self.timeout) as c: r = await c.post(url, json=payload, headers=self._hdrs(job_id)) r.raise_for_status() return r.content except _RETRY_EXCEPTIONS as e: if attempt == 0: log.warning("[job %s] %s bei %s — Retry in 3s", job_id, type(e).__name__, url) await asyncio.sleep(3.0) continue raise async def health(self) -> dict: async with httpx.AsyncClient(timeout=10) as c: r = await c.get(f"{self.base_url}/health") r.raise_for_status() return r.json() async def list_profiles(self) -> list[dict]: async with httpx.AsyncClient(timeout=10) as c: r = await c.get(f"{self.base_url}/api/profiles") r.raise_for_status() return r.json().get("profiles", []) async def fetch_log(self, job_id: int, last: int = 200) -> list[str]: """Holt die letzten Mac-Worker-Logzeilen für einen Job (für die Diagnose-Anzeige).""" try: async with httpx.AsyncClient(timeout=8) as c: r = await c.get(f"{self.base_url}/api/jobs/{job_id}/log", params={"last": last}) r.raise_for_status() return r.json().get("lines", []) except Exception as e: # noqa: BLE001 log.warning("fetch_log job=%s failed: %s", job_id, e) return [] async def preload(self, profile: str | None = None, job_id: int | None = None) -> dict: """Vor-Heizen: Ollama soll das Modell laden, bevor der echte Call läuft.""" return await self._post_json_retry( f"{self.base_url}/api/preload", {"profile": profile}, job_id, ) async def transcribe(self, audio_path: Path, language: str = "de", job_id: int | None = None) -> dict: # Kein Retry: Datei-Upload ist nicht idempotent, und die Transkription # ist der teuerste Schritt — Fehler hier sollen sichtbar bleiben. async with httpx.AsyncClient(timeout=self.timeout) as c: with audio_path.open("rb") as f: files = {"audio": (audio_path.name, f, "application/octet-stream")} data = {"language": language} r = await c.post( f"{self.base_url}/api/transcribe", files=files, data=data, headers=self._hdrs(job_id), ) r.raise_for_status() return r.json() async def summarize(self, transcript: str, title: str = "", profile: str | None = None, job_id: int | None = None) -> dict: return await self._post_json_retry( f"{self.base_url}/api/summarize", {"transcript": transcript, "title": title, "profile": profile}, job_id, ) async def protocol(self, transcript: str, summary: dict, title: str = "", profile: str | None = None, job_id: int | None = None) -> dict: return await self._post_json_retry( f"{self.base_url}/api/protocol", {"transcript": transcript, "summary": summary, "title": title, "profile": profile}, job_id, ) async def export_docx(self, data: dict, profile: str | None = None, job_id: int | None = None) -> bytes: return await self._post_bytes_retry( f"{self.base_url}/api/export/docx", {"data": data, "profile": profile}, job_id, ) async def export_pdf(self, data: dict, profile: str | None = None, job_id: int | None = None) -> bytes: return await self._post_bytes_retry( f"{self.base_url}/api/export/pdf", {"data": data, "profile": profile}, job_id, )