feat: Langflow authoring pipeline with tests
- LangflowClient (list/export, x-api-key auth, error mapping) - import_from_langflow: export -> versioned definition -> LFX deploy - POST /api/flows/import, GET /api/flows/langflow/available - scripts/deploy_flow.py CLI - 25 tests green (Langflow faked via dependency override + respx client tests; respx passthrough proven unreliable for LFX traffic)
This commit is contained in:
parent
96c0da33c7
commit
1034206870
8 changed files with 451 additions and 0 deletions
66
backend/app/flows/langflow_client.py
Normal file
66
backend/app/flows/langflow_client.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""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
|
||||
|
||||
|
||||
class LangflowError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LangflowNotFound(LangflowError):
|
||||
pass
|
||||
|
||||
|
||||
class LangflowUnavailable(LangflowError):
|
||||
pass
|
||||
|
||||
|
||||
class LangflowClient:
|
||||
"""Thin async wrapper around the Langflow `/api/v1/flows` endpoints."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str = "", timeout: float = 60.0) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
return {"x-api-key": self.api_key} if self.api_key else {}
|
||||
|
||||
async def list_flows(self) -> list[dict]:
|
||||
"""List flows (header mode keeps payloads small)."""
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url, timeout=self.timeout, headers=self._headers()
|
||||
) 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
|
||||
|
||||
async def export_flow(self, langflow_flow_id: str) -> dict:
|
||||
"""Export one flow as Langflow JSON (FlowRead: id/name/description/data)."""
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url, timeout=self.timeout, headers=self._headers()
|
||||
) 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue