"""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"), ) -> None: """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})() raise typer.Exit(asyncio.run(main_async(args))) if __name__ == "__main__": typer.run(main)