"""Async client for the Langflow authoring server (visual designer). Only used for the authoring pipeline (list/export flows). Execution always goes through LFX (see lfx_client.py). """ from __future__ import annotations import httpx from .base_client import BaseApiClient, transport_retry class LangflowError(Exception): pass class LangflowNotFound(LangflowError): pass class LangflowUnavailable(LangflowError): pass class LangflowClient(BaseApiClient): """Thin async wrapper around the Langflow `/api/v1/flows` endpoints.""" def __init__(self, base_url: str, api_key: str = "", timeout: float = 60.0) -> None: super().__init__(base_url, timeout=timeout, api_key=api_key) @transport_retry async def list_flows(self) -> list[dict]: """List flows (header mode keeps payloads small).""" try: async with self._client() as client: response = await client.get("/api/v1/flows/", params={"header_flows": "true"}) if response.status_code == 404: raise LangflowNotFound("flow list not found") response.raise_for_status() data = response.json() return data if isinstance(data, list) else data.get("items", []) except httpx.HTTPStatusError as exc: raise LangflowError(f"Langflow error {exc.response.status_code}") from exc except httpx.HTTPError as exc: raise LangflowUnavailable(f"Langflow unreachable: {exc}") from exc @transport_retry async def export_flow(self, langflow_flow_id: str) -> dict: """Export one flow as Langflow JSON (FlowRead: id/name/description/data).""" try: async with self._client() as client: response = await client.get(f"/api/v1/flows/{langflow_flow_id}") if response.status_code == 404: raise LangflowNotFound(f"flow {langflow_flow_id} not found") response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: raise LangflowError(f"Langflow error {exc.response.status_code}") from exc except httpx.HTTPError as exc: raise LangflowUnavailable(f"Langflow unreachable: {exc}") from exc @transport_retry async def upsert_flow(self, doc: dict) -> dict: """Create or update a flow in Langflow by name (authoring UI refresh). Used after flow/component tightening so the visual editor shows the latest canonical JSON without a container restart. """ try: async with self._client() as client: existing = await self.list_flows() match = next((f for f in existing if f.get("name") == doc.get("name")), None) if match and match.get("id"): response = await client.patch(f"/api/v1/flows/{match['id']}", json=doc) else: response = await client.post("/api/v1/flows/", json=doc) response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: raise LangflowError(f"Langflow error {exc.response.status_code}") from exc except httpx.HTTPError as exc: raise LangflowUnavailable(f"Langflow unreachable: {exc}") from exc