"""Authoring pipeline: Langflow export -> canonical file -> version -> LFX deploy.""" from __future__ import annotations from pathlib import Path from sqlmodel import Session from app.models import FlowDefinition, FlowVersion from app.settings import settings from . import export, sync from .langflow_client import LangflowClient, LangflowError from .lfx_client import LfxClient def normalize_exported_flow(exported: dict, langflow_flow_id: str, name: str | None = None) -> dict: """Reduce a Langflow FlowRead to the canonical fields we persist.""" flow_name = name or exported.get("name") or f"langflow-{langflow_flow_id}" return { "id": exported.get("id", langflow_flow_id), "name": flow_name, "description": exported.get("description") or "", "endpoint_name": exported.get("endpoint_name"), "data": exported.get("data", {}), "tags": exported.get("tags", []), "webhook": exported.get("webhook", False), "is_component": exported.get("is_component", False), } async def pull_all_from_langflow( langflow: LangflowClient, flows_dir: str | Path ) -> dict[str, list[str] | dict[str, str]]: """Export every Langflow flow to its canonical file. Returns {"pulled": [...stems], "errors": {langflow_id: reason}}. Per-flow failures are collected, never raised; listing failures raise (caller maps them to 503/502). """ flows_dir = Path(flows_dir) headers = await langflow.list_flows() pulled: list[str] = [] errors: dict[str, str] = {} for header in headers: flow_id = str(header.get("id", "")) if not flow_id: continue try: exported = await langflow.export_flow(flow_id) doc = normalize_exported_flow(exported, flow_id) reuse, identical = export.find_reuse_path(flows_dir, doc) if identical and reuse is not None: pulled.append(reuse.stem) continue target = reuse or export.flow_path(flows_dir, export.slugify(doc["name"])) path = export.write_flow_bytes(target, doc) pulled.append(path.stem) except (LangflowError, OSError, ValueError) as exc: errors[flow_id] = str(exc) return {"pulled": sorted(pulled), "errors": errors} async def import_from_langflow( session: Session, langflow: LangflowClient, lfx: LfxClient, langflow_flow_id: str, *, name: str | None = None, author: str = "", deploy: bool = True, ) -> tuple[FlowDefinition, FlowVersion]: """Export a flow from Langflow, persist it to backend/flows, then sync. The canonical file is written first so the directory stays the source of truth; sync_document() derives the definition + version from it. """ exported = await langflow.export_flow(langflow_flow_id) langflow_json = normalize_exported_flow(exported, langflow_flow_id, name=name) path = export.write_flow_file(settings.flows_dir, langflow_json, name=langflow_json["name"]) definition, version, _action = await sync.sync_document( session, lfx, langflow_json, source_filename=path.name, author=author, deploy=deploy, ) assert version is not None # sync_document always versions on import return definition, version