Fix plan node detail to generate plan from recipe if not cached
- Add load_plan_for_run_with_fallback() that generates plan from recipe when not found in cache - Share this helper between plan page and node detail endpoint - Removes code duplication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
80
server.py
80
server.py
@@ -1373,6 +1373,46 @@ def load_plan_for_run(run: RunStatus) -> Optional[dict]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def load_plan_for_run_with_fallback(run: RunStatus) -> Optional[dict]:
|
||||||
|
"""Load plan data for a run, with fallback to generate from recipe."""
|
||||||
|
# First try cached plans
|
||||||
|
plan_data = load_plan_for_run(run)
|
||||||
|
if plan_data:
|
||||||
|
return plan_data
|
||||||
|
|
||||||
|
# Fallback: generate from recipe
|
||||||
|
recipe_name = run.recipe.replace("recipe:", "") if run.recipe.startswith("recipe:") else run.recipe
|
||||||
|
recipe_status = None
|
||||||
|
for recipe in list_all_recipes():
|
||||||
|
if recipe.name == recipe_name:
|
||||||
|
recipe_status = recipe
|
||||||
|
break
|
||||||
|
|
||||||
|
if recipe_status:
|
||||||
|
recipe_path = cache_manager.get_by_content_hash(recipe_status.recipe_id)
|
||||||
|
if recipe_path and recipe_path.exists():
|
||||||
|
try:
|
||||||
|
recipe_yaml = recipe_path.read_text()
|
||||||
|
# Build input_hashes mapping from run inputs
|
||||||
|
input_hashes = {}
|
||||||
|
for i, var_input in enumerate(recipe_status.variable_inputs):
|
||||||
|
if i < len(run.inputs):
|
||||||
|
input_hashes[var_input.node_id] = run.inputs[i]
|
||||||
|
|
||||||
|
# Try to generate plan
|
||||||
|
try:
|
||||||
|
from tasks.orchestrate import generate_plan as gen_plan_task
|
||||||
|
plan_result = gen_plan_task(recipe_yaml, input_hashes)
|
||||||
|
if plan_result and plan_result.get("status") == "planned":
|
||||||
|
return plan_result
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to generate plan for run {run.run_id}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/run/{run_id}/plan/node/{step_id}", response_class=HTMLResponse)
|
@app.get("/run/{run_id}/plan/node/{step_id}", response_class=HTMLResponse)
|
||||||
async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
||||||
"""HTMX partial: Get node detail HTML fragment."""
|
"""HTMX partial: Get node detail HTML fragment."""
|
||||||
@@ -1384,8 +1424,8 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
|||||||
if not run:
|
if not run:
|
||||||
return HTMLResponse(f'<p class="text-red-400">Run not found</p>', status_code=404)
|
return HTMLResponse(f'<p class="text-red-400">Run not found</p>', status_code=404)
|
||||||
|
|
||||||
# Load plan data
|
# Load plan data (with fallback to generate from recipe)
|
||||||
plan_data = await asyncio.to_thread(load_plan_for_run, run)
|
plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run)
|
||||||
|
|
||||||
if not plan_data:
|
if not plan_data:
|
||||||
return HTMLResponse('<p class="text-gray-400">Plan not found</p>')
|
return HTMLResponse('<p class="text-gray-400">Plan not found</p>')
|
||||||
@@ -1531,40 +1571,8 @@ async def run_plan_visualization(run_id: str, request: Request, node: Optional[s
|
|||||||
content = '<p class="text-red-400 py-8 text-center">Access denied.</p>'
|
content = '<p class="text-red-400 py-8 text-center">Access denied.</p>'
|
||||||
return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403)
|
return HTMLResponse(render_page("Access Denied", content, ctx.actor_id, active_tab="runs"), status_code=403)
|
||||||
|
|
||||||
# Try to load existing plan from cache
|
# Load plan data (with fallback to generate from recipe)
|
||||||
plan_data = await asyncio.to_thread(load_plan_for_run, run)
|
plan_data = await asyncio.to_thread(load_plan_for_run_with_fallback, run)
|
||||||
|
|
||||||
# If no cached plan, try to generate one from the recipe
|
|
||||||
if not plan_data:
|
|
||||||
recipe_name = run.recipe.replace("recipe:", "") if run.recipe.startswith("recipe:") else run.recipe
|
|
||||||
recipe_status = None
|
|
||||||
for recipe in list_all_recipes():
|
|
||||||
if recipe.name == recipe_name:
|
|
||||||
recipe_status = recipe
|
|
||||||
break
|
|
||||||
|
|
||||||
if recipe_status:
|
|
||||||
recipe_path = cache_manager.get_by_content_hash(recipe_status.recipe_id)
|
|
||||||
if recipe_path and recipe_path.exists():
|
|
||||||
try:
|
|
||||||
recipe_yaml = recipe_path.read_text()
|
|
||||||
# Build input_hashes mapping from run inputs
|
|
||||||
input_hashes = {}
|
|
||||||
for i, var_input in enumerate(recipe_status.variable_inputs):
|
|
||||||
if i < len(run.inputs):
|
|
||||||
input_hashes[var_input.node_id] = run.inputs[i]
|
|
||||||
|
|
||||||
# Try to generate plan using the orchestrate module
|
|
||||||
try:
|
|
||||||
from tasks.orchestrate import generate_plan as gen_plan_task
|
|
||||||
# Call synchronously (it's fast for just planning)
|
|
||||||
plan_result = gen_plan_task(recipe_yaml, input_hashes)
|
|
||||||
if plan_result and plan_result.get("status") == "planned":
|
|
||||||
plan_data = plan_result
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to generate plan for run {run_id}: {e}")
|
|
||||||
|
|
||||||
# Build sub-navigation tabs
|
# Build sub-navigation tabs
|
||||||
tabs_html = render_run_sub_tabs(run_id, active="plan")
|
tabs_html = render_run_sub_tabs(run_id, active="plan")
|
||||||
|
|||||||
Reference in New Issue
Block a user