96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""Reconcile backend/flows/*.json into the DB + LFX.
|
|
|
|
Usage (from backend/, venv active):
|
|
uv run python scripts/sync_flows.py --sync [--dir backend/flows]
|
|
uv run python scripts/sync_flows.py --pull [--sync] # export all Langflow flows to files first
|
|
|
|
Exit codes: 0 ok, 1 sync errors, 2 flows dir missing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import typer
|
|
from sqlmodel import Session
|
|
|
|
from app.db import get_engine, reset_engine
|
|
from app.flows import sync
|
|
from app.flows.deploy import pull_all_from_langflow
|
|
from app.flows.langflow_client import LangflowClient
|
|
from app.flows.lfx_client import LfxClient
|
|
from app.flows.schemas import SyncReport
|
|
from app.settings import settings
|
|
|
|
|
|
async def pull_from_langflow(flows_dir: Path) -> dict:
|
|
"""Export every Langflow flow to its own canonical file. Returns a report."""
|
|
client = LangflowClient(settings.langflow_url, api_key=settings.langflow_api_key)
|
|
return await pull_all_from_langflow(client, flows_dir)
|
|
|
|
|
|
async def run_sync(flows_dir: Path) -> SyncReport:
|
|
reset_engine()
|
|
session = Session(get_engine())
|
|
try:
|
|
lfx = LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
|
return await sync.sync_from_directory(session, lfx, flows_dir)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
async def run_full_pass(flows_dir: Path) -> SyncReport:
|
|
"""Single orchestration: Langflow pull + file sync (shared with API)."""
|
|
reset_engine()
|
|
session = Session(get_engine())
|
|
try:
|
|
lfx = LfxClient(settings.lfx_url, api_key=settings.lfx_api_key or None)
|
|
langflow = LangflowClient(settings.langflow_url, api_key=settings.langflow_api_key)
|
|
return await sync.sync_full_pass(session, lfx, langflow, flows_dir)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
async def main_async(args) -> int:
|
|
flows_dir = Path(args.dir or settings.flows_dir)
|
|
if args.pull and args.sync:
|
|
try:
|
|
report = await run_full_pass(flows_dir)
|
|
except FileNotFoundError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
print(report.model_dump_json(indent=2))
|
|
return 1 if report.errors else 0
|
|
if args.pull:
|
|
pulled = await pull_from_langflow(flows_dir)
|
|
print(json.dumps(pulled, indent=2, ensure_ascii=False))
|
|
if pulled["errors"]:
|
|
return 1
|
|
if args.sync or not args.pull:
|
|
try:
|
|
report = await run_sync(flows_dir)
|
|
except FileNotFoundError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
print(report.model_dump_json(indent=2))
|
|
return 1 if report.errors else 0
|
|
return 0
|
|
|
|
|
|
def main(
|
|
pull: bool = typer.Option(False, "--pull", help="export all Langflow flows first"),
|
|
sync: bool = typer.Option(False, "--sync", help="reconcile backend/flows into DB + LFX"),
|
|
dir: str | None = typer.Option(None, "--dir", help="flows directory"),
|
|
) -> int:
|
|
"""Reconcile flows (typer CLI; run_sync/run_full_pass stay importable)."""
|
|
if not pull and not sync:
|
|
sync = True
|
|
args = type("Args", (), {"pull": pull, "sync": sync, "dir": dir})()
|
|
return asyncio.run(main_async(args))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(typer.run(main))
|