kirche-einfach/backend/app/flows/deploy.py

98 lines
3.5 KiB
Python

"""Authoring pipeline: Langflow export -> canonical file -> version -> LFX deploy."""
from __future__ import annotations
import logging
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
logger = logging.getLogger(__name__)
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()
ignored = export.ignored_stems(flows_dir)
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)
if export.filename_stem(doc["name"]) in ignored:
logger.warning("pull skips ignored flow %r", doc["name"])
continue
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.filename_stem(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