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:
Jonas Miederer 2026-09-06 23:01:47 +00:00
parent 96c0da33c7
commit 1034206870
8 changed files with 451 additions and 0 deletions

View file

@ -15,6 +15,13 @@ from app.db import get_engine, get_session
from app.settings import settings
from . import runner, store
from .deploy import import_from_langflow
from .langflow_client import (
LangflowClient,
LangflowError,
LangflowNotFound,
LangflowUnavailable,
)
from .lfx_client import (
LfxClient,
LfxConflict,
@ -29,6 +36,8 @@ from .schemas import (
ApprovalRead,
FlowDefinitionCreate,
FlowDefinitionRead,
FlowImportRead,
FlowImportRequest,
FlowVersionCreate,
FlowVersionRead,
RunRead,
@ -47,6 +56,12 @@ def get_lfx() -> LfxClient:
return LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
def get_langflow() -> LangflowClient:
return LangflowClient(
settings.langflow_url, api_key=settings.langflow_api_key or ""
)
def _read_definition(d) -> FlowDefinitionRead:
return FlowDefinitionRead(
id=d.id,
@ -282,6 +297,58 @@ def list_runs(flow_id: UUID, session: Session = Depends(get_db)):
raise HTTPException(status_code=404, detail=str(exc)) from exc
@flows_router.get("/langflow/available")
async def list_langflow_flows(langflow: LangflowClient = Depends(get_langflow)):
"""Browse flows on the Langflow authoring server (admin import source)."""
try:
return await langflow.list_flows()
except LangflowUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except LangflowError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
@flows_router.post("/import", response_model=FlowImportRead, status_code=201)
async def import_flow(
data: FlowImportRequest,
session: Session = Depends(get_db),
langflow: LangflowClient = Depends(get_langflow),
lfx: LfxClient = Depends(get_lfx),
):
"""Import a flow from Langflow: export -> version -> (deploy to LFX)."""
try:
definition, version = await import_from_langflow(
session,
langflow,
lfx,
data.langflow_flow_id,
name=data.name,
author=data.author,
deploy=data.deploy,
)
except LangflowNotFound as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except LangflowUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except LangflowError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
except store.ConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except store.InvalidError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except LfxInvalidFlow as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except LfxConflict as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except LfxUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except LfxError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return FlowImportRead(
definition=_read_definition(definition), version=_read_version(version)
)
@flows_router.get("/runs/{run_id}", response_model=RunRead)
def get_run(run_id: UUID, session: Session = Depends(get_db)):
try: