feat: manual full-pass sync Langflow -> files -> DB+LFX with admin guide
This commit is contained in:
parent
acea6ae624
commit
1326cece42
11 changed files with 284 additions and 41 deletions
|
|
@ -29,7 +29,7 @@ from app.settings import settings
|
|||
|
||||
from . import input_schema as form_schema
|
||||
from . import runner, store
|
||||
from .deploy import import_from_langflow
|
||||
from .deploy import import_from_langflow, pull_all_from_langflow
|
||||
from .langflow_client import (
|
||||
LangflowClient,
|
||||
LangflowError,
|
||||
|
|
@ -160,13 +160,25 @@ def list_flows(session: Annotated[Session, Depends(get_db)]):
|
|||
async def sync_flows(
|
||||
session: Annotated[Session, Depends(get_db)],
|
||||
lfx: Annotated[LfxClient, Depends(get_lfx)],
|
||||
langflow: Annotated[LangflowClient, Depends(get_langflow)],
|
||||
_admin: Annotated[User, Depends(require_admin)],
|
||||
):
|
||||
"""Reconcile backend/flows/*.json into the DB + LFX (add/update/delete)."""
|
||||
"""Manual full pass: Langflow -> backend/flows/*.json -> DB + LFX."""
|
||||
try:
|
||||
return await sync_from_directory(session, lfx, Path(settings.flows_dir))
|
||||
pulled_result = await pull_all_from_langflow(langflow, Path(settings.flows_dir))
|
||||
except LangflowUnavailable as exc:
|
||||
logger.warning("langflow unavailable: %s", exc)
|
||||
raise HTTPException(status_code=503, detail="authoring server unavailable") from exc
|
||||
except LangflowError as exc:
|
||||
logger.warning("langflow error: %s", exc)
|
||||
raise HTTPException(status_code=502, detail="authoring server error") from exc
|
||||
try:
|
||||
report = await sync_from_directory(session, lfx, Path(settings.flows_dir))
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
report.pulled = pulled_result["pulled"]
|
||||
report.errors = {**pulled_result["errors"], **report.errors}
|
||||
return report
|
||||
|
||||
|
||||
@flows_router.get("/{flow_id}", response_model=FlowDefinitionRead)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,60 @@
|
|||
|
||||
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
|
||||
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)
|
||||
path = export.write_flow_file(flows_dir, doc, name=doc["name"])
|
||||
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,
|
||||
|
|
@ -28,18 +72,8 @@ async def import_from_langflow(
|
|||
truth; sync_document() derives the definition + version from it.
|
||||
"""
|
||||
exported = await langflow.export_flow(langflow_flow_id)
|
||||
flow_name = name or exported.get("name") or f"langflow-{langflow_flow_id}"
|
||||
langflow_json = {
|
||||
"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),
|
||||
}
|
||||
path = export.write_flow_file(settings.flows_dir, langflow_json, name=flow_name)
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -127,13 +127,15 @@ class DecisionRead(BaseModel):
|
|||
|
||||
|
||||
class SyncReport(BaseModel):
|
||||
"""Result of reconciling backend/flows/*.json into DB + LFX.
|
||||
"""Result of the manual full pass: Langflow -> files -> DB + LFX.
|
||||
|
||||
Lists hold filename stems; errors maps filename to reason.
|
||||
Lists hold filename stems; errors maps filename (or Langflow id on
|
||||
pull failures) to reason.
|
||||
"""
|
||||
|
||||
added: list[str] = Field(default_factory=list)
|
||||
updated: list[str] = Field(default_factory=list)
|
||||
deleted: list[str] = Field(default_factory=list)
|
||||
skipped: list[str] = Field(default_factory=list)
|
||||
pulled: list[str] = Field(default_factory=list)
|
||||
errors: dict[str, str] = Field(default_factory=dict)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue