126 lines
4.1 KiB
Python
126 lines
4.1 KiB
Python
"""Command line entry point: uv run miniflow <command> ..."""
|
|||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from miniflow import memory, tools
|
||
|
|
from miniflow.engine import WorkflowError, load_workflow, run_workflow
|
||
|
|
|
||
|
|
|
||
|
|
def parse_inputs(pairs: list[str]) -> dict[str, str]:
|
||
|
|
inputs = {}
|
||
|
|
for pair in pairs:
|
||
|
|
if "=" not in pair:
|
||
|
|
raise WorkflowError(f"Input must look like name=value, got '{pair}'")
|
||
|
|
name, value = pair.split("=", 1)
|
||
|
|
inputs[name.strip()] = value
|
||
|
|
return inputs
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_run(args: argparse.Namespace) -> int:
|
||
|
|
workflow = load_workflow(args.flow)
|
||
|
|
print(f"Workflow: {workflow.name}")
|
||
|
|
if workflow.description:
|
||
|
|
print(f" {workflow.description}")
|
||
|
|
print("-" * 72)
|
||
|
|
report = run_workflow(workflow, parse_inputs(args.input))
|
||
|
|
print("-" * 72)
|
||
|
|
print(f"Status: {report['status']}")
|
||
|
|
print("Result:")
|
||
|
|
result = report["result"]
|
||
|
|
print(result if isinstance(result, str) else json.dumps(result, ensure_ascii=False, indent=2))
|
||
|
|
|
||
|
|
runs = Path("runs")
|
||
|
|
runs.mkdir(exist_ok=True)
|
||
|
|
log_file = runs / f"{datetime.now():%Y%m%d-%H%M%S}-{Path(args.flow).stem}.json"
|
||
|
|
log_file.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
|
print(f"Trace saved: {log_file}")
|
||
|
|
return 0 if report["status"] == "completed" else 1
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_validate(args: argparse.Namespace) -> int:
|
||
|
|
workflow = load_workflow(args.flow)
|
||
|
|
print(f"OK: {workflow.name} ({len(workflow.nodes)} nodes, {len(workflow.edges)} edges)")
|
||
|
|
print("Execution order: " + " -> ".join(workflow.order()))
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_show(args: argparse.Namespace) -> int:
|
||
|
|
print(load_workflow(args.flow).to_mermaid())
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_tools(_: argparse.Namespace) -> int:
|
||
|
|
for name, doc in tools.describe_tools():
|
||
|
|
print(f"{name:<14} {doc}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_check(_: argparse.Namespace) -> int:
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
|
||
|
|
from miniflow import llm
|
||
|
|
|
||
|
|
provider = os.getenv("MINIFLOW_LLM", "mock").lower()
|
||
|
|
print(f"MINIFLOW_LLM : {provider}")
|
||
|
|
if provider == "ollama":
|
||
|
|
print(f"OLLAMA_HOST : {os.getenv('OLLAMA_HOST', 'http://localhost:11434')}")
|
||
|
|
print(f"OLLAMA_MODEL : {os.getenv('OLLAMA_MODEL', 'gemma3:4b')}")
|
||
|
|
started = time.perf_counter()
|
||
|
|
try:
|
||
|
|
reply, used = llm.complete({"temperature": 0}, "Reply with exactly one word: ready", "", {})
|
||
|
|
except llm.LLMError as error:
|
||
|
|
print(f"FAILED: {error}")
|
||
|
|
return 1
|
||
|
|
print(f"Reply ({used}, {time.perf_counter() - started:.1f}s): {reply[:80]}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def cmd_memory(args: argparse.Namespace) -> int:
|
||
|
|
if args.clear:
|
||
|
|
memory.clear()
|
||
|
|
print("Memory cleared")
|
||
|
|
return 0
|
||
|
|
path = memory.memory_path()
|
||
|
|
print(path.read_text(encoding="utf-8") if path.exists() else "{}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str] | None = None) -> int:
|
||
|
|
parser = argparse.ArgumentParser(prog="miniflow", description="Run JSON-defined AI workflows")
|
||
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
||
|
|
|
||
|
|
run = sub.add_parser("run", help="run a workflow file")
|
||
|
|
run.add_argument("flow")
|
||
|
|
run.add_argument("--input", "-i", action="append", default=[], metavar="NAME=VALUE")
|
||
|
|
run.set_defaults(func=cmd_run)
|
||
|
|
|
||
|
|
validate = sub.add_parser("validate", help="check a workflow file without running it")
|
||
|
|
validate.add_argument("flow")
|
||
|
|
validate.set_defaults(func=cmd_validate)
|
||
|
|
|
||
|
|
show = sub.add_parser("show", help="print the workflow as a Mermaid diagram")
|
||
|
|
show.add_argument("flow")
|
||
|
|
show.set_defaults(func=cmd_show)
|
||
|
|
|
||
|
|
sub.add_parser("tools", help="list available tools").set_defaults(func=cmd_tools)
|
||
|
|
sub.add_parser("check", help="check the configured LLM provider").set_defaults(func=cmd_check)
|
||
|
|
|
||
|
|
mem = sub.add_parser("memory", help="show or clear the memory store")
|
||
|
|
mem.add_argument("--clear", action="store_true")
|
||
|
|
mem.set_defaults(func=cmd_memory)
|
||
|
|
|
||
|
|
args = parser.parse_args(argv)
|
||
|
|
try:
|
||
|
|
return args.func(args)
|
||
|
|
except (WorkflowError, FileNotFoundError, json.JSONDecodeError) as error:
|
||
|
|
print(f"Error: {error}", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|