Add v2 API commands for 3-phase execution
- plan: Generate and preview execution plan - execute-plan: Execute pre-generated plan - run-v2: Full 3-phase pipeline (Analyze → Plan → Execute) - run-status: Check v2 run status Uses new /api/v2/* endpoints for the 3-phase execution model. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
299
artdag.py
299
artdag.py
@@ -1090,5 +1090,304 @@ def delete_recipe(recipe_id, force):
|
||||
click.echo(f"Deleted recipe: {recipe_id[:16]}...")
|
||||
|
||||
|
||||
# ============ v2 API Commands (3-Phase Execution) ============
|
||||
|
||||
@cli.command("plan")
|
||||
@click.argument("recipe_file", type=click.Path(exists=True))
|
||||
@click.option("--input", "-i", "inputs", multiple=True, help="Input as name:content_hash")
|
||||
@click.option("--features", "-f", multiple=True, help="Features to extract (default: beats, energy)")
|
||||
@click.option("--output", "-o", type=click.Path(), help="Save plan JSON to file")
|
||||
def generate_plan(recipe_file, inputs, features, output):
|
||||
"""Generate an execution plan from a recipe YAML. Requires login.
|
||||
|
||||
Preview what will be executed without actually running it.
|
||||
|
||||
RECIPE_FILE: Path to recipe YAML file
|
||||
|
||||
Example: artdag plan recipe.yaml -i source_video:abc123
|
||||
"""
|
||||
token_data = load_token()
|
||||
if not token_data.get("access_token"):
|
||||
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Read recipe YAML
|
||||
with open(recipe_file) as f:
|
||||
recipe_yaml = f.read()
|
||||
|
||||
# Parse inputs
|
||||
input_hashes = {}
|
||||
for inp in inputs:
|
||||
if ":" not in inp:
|
||||
click.echo(f"Invalid input format: {inp} (expected name:content_hash)", err=True)
|
||||
sys.exit(1)
|
||||
name, content_hash = inp.split(":", 1)
|
||||
input_hashes[name] = content_hash
|
||||
|
||||
# Build request
|
||||
request_data = {
|
||||
"recipe_yaml": recipe_yaml,
|
||||
"input_hashes": input_hashes,
|
||||
}
|
||||
if features:
|
||||
request_data["features"] = list(features)
|
||||
|
||||
# Submit to API
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||
resp = requests.post(
|
||||
f"{get_server()}/api/v2/plan",
|
||||
json=request_data,
|
||||
headers=headers
|
||||
)
|
||||
if resp.status_code == 401:
|
||||
click.echo("Authentication failed. Please login again.", err=True)
|
||||
sys.exit(1)
|
||||
if resp.status_code == 400:
|
||||
click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True)
|
||||
sys.exit(1)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Plan generation failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Display results
|
||||
click.echo(f"Recipe: {result['recipe']}")
|
||||
click.echo(f"Plan ID: {result['plan_id'][:16]}...")
|
||||
click.echo(f"Total steps: {result['total_steps']}")
|
||||
click.echo(f"Cached: {result['cached_steps']}")
|
||||
click.echo(f"Pending: {result['pending_steps']}")
|
||||
|
||||
if result.get("steps"):
|
||||
click.echo("\nSteps:")
|
||||
for step in result["steps"]:
|
||||
status = "✓ cached" if step["cached"] else "○ pending"
|
||||
click.echo(f" L{step['level']} {step['step_id']:<20} {step['node_type']:<10} {status}")
|
||||
|
||||
# Save plan JSON if requested
|
||||
if output:
|
||||
with open(output, "w") as f:
|
||||
f.write(result["plan_json"])
|
||||
click.echo(f"\nPlan saved to: {output}")
|
||||
elif result.get("plan_json"):
|
||||
click.echo(f"\nUse --output to save the plan JSON for later execution.")
|
||||
|
||||
|
||||
@cli.command("execute-plan")
|
||||
@click.argument("plan_file", type=click.Path(exists=True))
|
||||
@click.option("--wait", "-w", is_flag=True, help="Wait for completion")
|
||||
def execute_plan(plan_file, wait):
|
||||
"""Execute a pre-generated execution plan. Requires login.
|
||||
|
||||
PLAN_FILE: Path to plan JSON file (from 'artdag plan --output')
|
||||
|
||||
Example: artdag execute-plan plan.json --wait
|
||||
"""
|
||||
token_data = load_token()
|
||||
if not token_data.get("access_token"):
|
||||
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Read plan JSON
|
||||
with open(plan_file) as f:
|
||||
plan_json = f.read()
|
||||
|
||||
# Submit to API
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||
resp = requests.post(
|
||||
f"{get_server()}/api/v2/execute",
|
||||
json={"plan_json": plan_json},
|
||||
headers=headers
|
||||
)
|
||||
if resp.status_code == 401:
|
||||
click.echo("Authentication failed. Please login again.", err=True)
|
||||
sys.exit(1)
|
||||
if resp.status_code == 400:
|
||||
click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True)
|
||||
sys.exit(1)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Execution failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
run_id = result["run_id"]
|
||||
click.echo(f"Run started: {run_id}")
|
||||
click.echo(f"Status: {result['status']}")
|
||||
|
||||
if wait:
|
||||
_wait_for_v2_run(token_data, run_id)
|
||||
|
||||
|
||||
@cli.command("run-v2")
|
||||
@click.argument("recipe_file", type=click.Path(exists=True))
|
||||
@click.option("--input", "-i", "inputs", multiple=True, help="Input as name:content_hash")
|
||||
@click.option("--features", "-f", multiple=True, help="Features to extract (default: beats, energy)")
|
||||
@click.option("--wait", "-w", is_flag=True, help="Wait for completion")
|
||||
def run_recipe_v2(recipe_file, inputs, features, wait):
|
||||
"""Run a recipe through 3-phase execution. Requires login.
|
||||
|
||||
Runs the full pipeline: Analyze → Plan → Execute
|
||||
|
||||
RECIPE_FILE: Path to recipe YAML file
|
||||
|
||||
Example: artdag run-v2 recipe.yaml -i source_video:abc123 --wait
|
||||
"""
|
||||
token_data = load_token()
|
||||
if not token_data.get("access_token"):
|
||||
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Read recipe YAML
|
||||
with open(recipe_file) as f:
|
||||
recipe_yaml = f.read()
|
||||
|
||||
# Parse recipe name for display
|
||||
try:
|
||||
recipe_data = yaml.safe_load(recipe_yaml)
|
||||
recipe_name = recipe_data.get("name", "unknown")
|
||||
except Exception:
|
||||
recipe_name = "unknown"
|
||||
|
||||
# Parse inputs
|
||||
input_hashes = {}
|
||||
for inp in inputs:
|
||||
if ":" not in inp:
|
||||
click.echo(f"Invalid input format: {inp} (expected name:content_hash)", err=True)
|
||||
sys.exit(1)
|
||||
name, content_hash = inp.split(":", 1)
|
||||
input_hashes[name] = content_hash
|
||||
|
||||
# Build request
|
||||
request_data = {
|
||||
"recipe_yaml": recipe_yaml,
|
||||
"input_hashes": input_hashes,
|
||||
}
|
||||
if features:
|
||||
request_data["features"] = list(features)
|
||||
|
||||
# Submit to API
|
||||
click.echo(f"Running recipe: {recipe_name}")
|
||||
click.echo(f"Inputs: {len(input_hashes)}")
|
||||
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||
resp = requests.post(
|
||||
f"{get_server()}/api/v2/run-recipe",
|
||||
json=request_data,
|
||||
headers=headers
|
||||
)
|
||||
if resp.status_code == 401:
|
||||
click.echo("Authentication failed. Please login again.", err=True)
|
||||
sys.exit(1)
|
||||
if resp.status_code == 400:
|
||||
click.echo(f"Error: {resp.json().get('detail', 'Bad request')}", err=True)
|
||||
sys.exit(1)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Run failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
run_id = result["run_id"]
|
||||
click.echo(f"Run ID: {run_id}")
|
||||
click.echo(f"Status: {result['status']}")
|
||||
|
||||
if result.get("output_hash"):
|
||||
click.echo(f"Output: {result['output_hash']}")
|
||||
if result.get("output_ipfs_cid"):
|
||||
click.echo(f"IPFS CID: {result['output_ipfs_cid']}")
|
||||
return
|
||||
|
||||
if wait:
|
||||
_wait_for_v2_run(token_data, run_id)
|
||||
|
||||
|
||||
def _wait_for_v2_run(token_data: dict, run_id: str):
|
||||
"""Poll v2 run status until completion."""
|
||||
click.echo("Waiting for completion...")
|
||||
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||
|
||||
while True:
|
||||
time.sleep(2)
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{get_server()}/api/v2/run/{run_id}",
|
||||
headers=headers
|
||||
)
|
||||
resp.raise_for_status()
|
||||
run = resp.json()
|
||||
except requests.RequestException as e:
|
||||
click.echo(f".", nl=False)
|
||||
continue
|
||||
|
||||
status = run.get("status", "unknown")
|
||||
|
||||
if status == "completed":
|
||||
click.echo(f"\nCompleted!")
|
||||
if run.get("output_hash"):
|
||||
click.echo(f"Output: {run['output_hash']}")
|
||||
if run.get("output_ipfs_cid"):
|
||||
click.echo(f"IPFS CID: {run['output_ipfs_cid']}")
|
||||
if run.get("cached"):
|
||||
click.echo(f"Steps cached: {run['cached']}")
|
||||
if run.get("executed"):
|
||||
click.echo(f"Steps executed: {run['executed']}")
|
||||
break
|
||||
elif status == "failed":
|
||||
click.echo(f"\nFailed: {run.get('error', 'Unknown error')}", err=True)
|
||||
sys.exit(1)
|
||||
else:
|
||||
click.echo(".", nl=False)
|
||||
|
||||
|
||||
@cli.command("run-status")
|
||||
@click.argument("run_id")
|
||||
def run_status_v2(run_id):
|
||||
"""Get status of a v2 run. Requires login.
|
||||
|
||||
RUN_ID: The run ID from run-v2 or execute-plan
|
||||
"""
|
||||
token_data = load_token()
|
||||
if not token_data.get("access_token"):
|
||||
click.echo("Not logged in. Please run: artdag login <username>", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {token_data['access_token']}"}
|
||||
resp = requests.get(
|
||||
f"{get_server()}/api/v2/run/{run_id}",
|
||||
headers=headers
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
click.echo(f"Run not found: {run_id}", err=True)
|
||||
sys.exit(1)
|
||||
resp.raise_for_status()
|
||||
run = resp.json()
|
||||
except requests.RequestException as e:
|
||||
click.echo(f"Failed to get status: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(f"Run ID: {run_id}")
|
||||
click.echo(f"Status: {run['status']}")
|
||||
|
||||
if run.get("recipe"):
|
||||
click.echo(f"Recipe: {run['recipe']}")
|
||||
if run.get("plan_id"):
|
||||
click.echo(f"Plan ID: {run['plan_id'][:16]}...")
|
||||
if run.get("output_hash"):
|
||||
click.echo(f"Output: {run['output_hash']}")
|
||||
if run.get("output_ipfs_cid"):
|
||||
click.echo(f"IPFS CID: {run['output_ipfs_cid']}")
|
||||
if run.get("cached") is not None:
|
||||
click.echo(f"Cached: {run['cached']}")
|
||||
if run.get("executed") is not None:
|
||||
click.echo(f"Executed: {run['executed']}")
|
||||
if run.get("error"):
|
||||
click.echo(f"Error: {run['error']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
|
||||
Reference in New Issue
Block a user