Use orchestrated run_recipe for all recipe runs, remove v2 prefix
- UI recipe run now uses tasks.orchestrate.run_recipe (3-phase) - Deterministic run_id via compute_run_id for cache deduplication - Check for already-completed runs before starting - Rename /api/v2/* endpoints to /api/* (plan, execute, run-recipe, run) - All recipe runs now go through: analyze → plan → execute 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
63
server.py
63
server.py
@@ -2593,7 +2593,7 @@ async def recipe_dag_visualization(recipe_id: str, request: Request):
|
|||||||
|
|
||||||
@app.post("/ui/recipes/{recipe_id}/run", response_class=HTMLResponse)
|
@app.post("/ui/recipes/{recipe_id}/run", response_class=HTMLResponse)
|
||||||
async def ui_run_recipe(recipe_id: str, request: Request):
|
async def ui_run_recipe(recipe_id: str, request: Request):
|
||||||
"""HTMX handler: run a recipe with form inputs."""
|
"""HTMX handler: run a recipe with form inputs using 3-phase orchestration."""
|
||||||
ctx = await get_user_context_from_cookie(request)
|
ctx = await get_user_context_from_cookie(request)
|
||||||
if not ctx:
|
if not ctx:
|
||||||
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Login required</div>'
|
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Login required</div>'
|
||||||
@@ -2604,13 +2604,13 @@ async def ui_run_recipe(recipe_id: str, request: Request):
|
|||||||
|
|
||||||
# Parse form data
|
# Parse form data
|
||||||
form_data = await request.form()
|
form_data = await request.form()
|
||||||
inputs = {}
|
input_hashes = {}
|
||||||
for var_input in recipe.variable_inputs:
|
for var_input in recipe.variable_inputs:
|
||||||
value = form_data.get(var_input.node_id, "").strip()
|
value = form_data.get(var_input.node_id, "").strip()
|
||||||
if var_input.required and not value:
|
if var_input.required and not value:
|
||||||
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Missing required input: {var_input.name}</div>'
|
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Missing required input: {var_input.name}</div>'
|
||||||
if value:
|
if value:
|
||||||
inputs[var_input.node_id] = value
|
input_hashes[var_input.node_id] = value
|
||||||
|
|
||||||
# Load recipe YAML
|
# Load recipe YAML
|
||||||
recipe_path = cache_manager.get_by_content_hash(recipe_id)
|
recipe_path = cache_manager.get_by_content_hash(recipe_id)
|
||||||
@@ -2618,22 +2618,33 @@ async def ui_run_recipe(recipe_id: str, request: Request):
|
|||||||
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Recipe YAML not found in cache</div>'
|
return '<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Recipe YAML not found in cache</div>'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(recipe_path) as f:
|
recipe_yaml = recipe_path.read_text()
|
||||||
yaml_config = yaml.safe_load(f)
|
|
||||||
|
|
||||||
# Build DAG from recipe
|
# Compute deterministic run_id
|
||||||
dag = build_dag_from_recipe(yaml_config, inputs, recipe)
|
run_id = compute_run_id(
|
||||||
|
list(input_hashes.values()),
|
||||||
|
recipe.name,
|
||||||
|
recipe_id # recipe_id is already the content hash
|
||||||
|
)
|
||||||
|
|
||||||
# Create run
|
# Check if already completed
|
||||||
run_id = str(uuid.uuid4())
|
cached = await database.get_run_cache(run_id)
|
||||||
actor_id = ctx.actor_id
|
if cached:
|
||||||
|
output_hash = cached.get("output_hash")
|
||||||
|
if cache_manager.has_content(output_hash):
|
||||||
|
return f'''
|
||||||
|
<div class="bg-blue-900/50 border border-blue-700 text-blue-300 px-4 py-3 rounded-lg mb-4">
|
||||||
|
Already completed! <a href="/run/{run_id}" class="underline">View run</a>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
# Collect all input hashes
|
# Collect all input hashes for RunStatus
|
||||||
all_inputs = list(inputs.values())
|
all_inputs = list(input_hashes.values())
|
||||||
for fixed in recipe.fixed_inputs:
|
for fixed in recipe.fixed_inputs:
|
||||||
if fixed.content_hash:
|
if fixed.content_hash:
|
||||||
all_inputs.append(fixed.content_hash)
|
all_inputs.append(fixed.content_hash)
|
||||||
|
|
||||||
|
# Create run status
|
||||||
run = RunStatus(
|
run = RunStatus(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
status="pending",
|
status="pending",
|
||||||
@@ -2641,12 +2652,17 @@ async def ui_run_recipe(recipe_id: str, request: Request):
|
|||||||
inputs=all_inputs,
|
inputs=all_inputs,
|
||||||
output_name=f"{recipe.name}-{run_id[:8]}",
|
output_name=f"{recipe.name}-{run_id[:8]}",
|
||||||
created_at=datetime.now(timezone.utc).isoformat(),
|
created_at=datetime.now(timezone.utc).isoformat(),
|
||||||
username=actor_id
|
username=ctx.actor_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Submit to Celery
|
# Submit to orchestrated run_recipe task (3-phase: analyze, plan, execute)
|
||||||
dag_json = dag.to_json()
|
from tasks.orchestrate import run_recipe
|
||||||
task = execute_dag.delay(dag_json, run.run_id)
|
task = run_recipe.delay(
|
||||||
|
recipe_yaml=recipe_yaml,
|
||||||
|
input_hashes=input_hashes,
|
||||||
|
features=["beats", "energy"],
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
run.celery_task_id = task.id
|
run.celery_task_id = task.id
|
||||||
run.status = "running"
|
run.status = "running"
|
||||||
|
|
||||||
@@ -2658,6 +2674,7 @@ async def ui_run_recipe(recipe_id: str, request: Request):
|
|||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Recipe run failed: {e}")
|
||||||
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Error: {str(e)}</div>'
|
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Error: {str(e)}</div>'
|
||||||
|
|
||||||
|
|
||||||
@@ -6012,7 +6029,7 @@ class ExecutePlanRequest(BaseModel):
|
|||||||
plan_json: str # JSON-serialized ExecutionPlan
|
plan_json: str # JSON-serialized ExecutionPlan
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v2/plan")
|
@app.post("/api/plan")
|
||||||
async def generate_plan_endpoint(
|
async def generate_plan_endpoint(
|
||||||
request: PlanRequest,
|
request: PlanRequest,
|
||||||
ctx: UserContext = Depends(get_required_user_context)
|
ctx: UserContext = Depends(get_required_user_context)
|
||||||
@@ -6051,7 +6068,7 @@ async def generate_plan_endpoint(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v2/execute")
|
@app.post("/api/execute")
|
||||||
async def execute_plan_endpoint(
|
async def execute_plan_endpoint(
|
||||||
request: ExecutePlanRequest,
|
request: ExecutePlanRequest,
|
||||||
ctx: UserContext = Depends(get_required_user_context)
|
ctx: UserContext = Depends(get_required_user_context)
|
||||||
@@ -6084,7 +6101,7 @@ async def execute_plan_endpoint(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v2/run-recipe")
|
@app.post("/api/run-recipe")
|
||||||
async def run_recipe_endpoint(
|
async def run_recipe_endpoint(
|
||||||
request: RecipeRunRequest,
|
request: RecipeRunRequest,
|
||||||
ctx: UserContext = Depends(get_required_user_context)
|
ctx: UserContext = Depends(get_required_user_context)
|
||||||
@@ -6096,7 +6113,7 @@ async def run_recipe_endpoint(
|
|||||||
2. Plan: Generate execution plan with cache IDs
|
2. Plan: Generate execution plan with cache IDs
|
||||||
3. Execute: Run steps with parallel execution
|
3. Execute: Run steps with parallel execution
|
||||||
|
|
||||||
Returns immediately with run_id. Poll /api/v2/run/{run_id} for status.
|
Returns immediately with run_id. Poll /api/run/{run_id} for status.
|
||||||
"""
|
"""
|
||||||
from tasks.orchestrate import run_recipe
|
from tasks.orchestrate import run_recipe
|
||||||
|
|
||||||
@@ -6162,10 +6179,10 @@ async def run_recipe_endpoint(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v2/run/{run_id}")
|
@app.get("/api/run/{run_id}")
|
||||||
async def get_run_v2(run_id: str, ctx: UserContext = Depends(get_required_user_context)):
|
async def get_run_api(run_id: str, ctx: UserContext = Depends(get_required_user_context)):
|
||||||
"""
|
"""
|
||||||
Get status of a 3-phase execution run.
|
Get status of a recipe execution run.
|
||||||
"""
|
"""
|
||||||
# Check Redis for run status
|
# Check Redis for run status
|
||||||
run_data = redis_client.get(f"{RUNS_KEY_PREFIX}{run_id}")
|
run_data = redis_client.get(f"{RUNS_KEY_PREFIX}{run_id}")
|
||||||
|
|||||||
Reference in New Issue
Block a user