Spaces:
Running
Running
| import logging | |
| import re | |
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel | |
| router = APIRouter() | |
| class Workflow(BaseModel): | |
| name: str | |
| steps: list | |
| async def get_automation_list(): | |
| """User automations plus the live scheduler jobs. | |
| This used to be a hardcoded `return []`, so the endpoint reported success | |
| while claiming the system had no automations — even though | |
| automation_service.load_automations() reads real rows from | |
| `custom_automations`, and the scheduler genuinely runs persisted jobs | |
| (mcu_suit_vitals, mcu_battlefield_intel, hf_keepalive_ping) out of the | |
| `apscheduler_jobs` table. Anything driven off this route saw an empty system. | |
| """ | |
| from backend.services.automation_service import load_automations, scheduler | |
| try: | |
| automations = load_automations() | |
| except Exception as exc: # a missing table must not 500 the whole panel | |
| automations = [] | |
| logging.warning("load_automations failed: %s", exc) | |
| jobs = [] | |
| try: | |
| for job in scheduler.get_jobs(): | |
| jobs.append({ | |
| "id": job.id, | |
| "name": getattr(job, "name", job.id), | |
| "trigger": str(job.trigger), | |
| "next_run_time": job.next_run_time.isoformat() if job.next_run_time else None, | |
| }) | |
| except Exception as exc: | |
| logging.warning("scheduler.get_jobs failed: %s", exc) | |
| return {"automations": automations, "scheduled_jobs": jobs, | |
| "counts": {"automations": len(automations), "scheduled_jobs": len(jobs)}} | |
| async def trigger_automation_route(id: str): | |
| from backend.services.automation_service import trigger_automation | |
| await trigger_automation(id) | |
| return {"status": "triggered"} | |
| async def pause_all(): | |
| from backend.services.automation_service import pause_all_automations | |
| await pause_all_automations() | |
| return {"status": "paused"} | |
| async def resume_all(): | |
| from backend.services.automation_service import resume_all_automations | |
| await resume_all_automations() | |
| return {"status": "resumed"} | |
| async def pause_single_job(job_id: str): | |
| from backend.services.automation_service import pause_job | |
| # APScheduler raises JobLookupError for an unknown id. Unhandled, that | |
| # surfaced as a 500 "Internal Server Error" for what is really a client | |
| # asking about a job that does not exist — a 404. | |
| try: | |
| pause_job(job_id) | |
| except Exception as exc: | |
| if "No job by the id" in str(exc): | |
| raise HTTPException(status_code=404, detail=f"No job with id {job_id}") | |
| raise | |
| return {"status": f"job {job_id} paused"} | |
| async def resume_single_job(job_id: str): | |
| from backend.services.automation_service import resume_job | |
| try: | |
| resume_job(job_id) | |
| except Exception as exc: | |
| if "No job by the id" in str(exc): | |
| raise HTTPException(status_code=404, detail=f"No job with id {job_id}") | |
| raise | |
| return {"status": f"job {job_id} resumed"} | |
| async def delete_single_job(job_id: str): | |
| from backend.services.automation_service import delete_job | |
| delete_job(job_id) | |
| return {"status": f"job {job_id} deleted"} | |
| async def save_workflow(w: Workflow): | |
| """Persist a workflow. | |
| This previously returned {"status": "saved"} without writing anything — the | |
| Save action reported success and the workflow was gone on the next read. | |
| automation_service.save_automation() is the real write path into the | |
| `custom_automations` table that /list reads back. | |
| """ | |
| from backend.services.automation_service import save_automation | |
| if not (w.name or "").strip(): | |
| raise HTTPException(status_code=400, detail="name is required") | |
| auto_id = re.sub(r"[^a-z0-9]+", "-", w.name.strip().lower()).strip("-") or "workflow" | |
| try: | |
| save_automation( | |
| auto_id=auto_id, | |
| trigger_type="manual", | |
| trigger_data={}, | |
| action_type="workflow", | |
| action_data={"name": w.name, "steps": w.steps}, | |
| ) | |
| except Exception as exc: | |
| logging.exception("save_automation failed") | |
| raise HTTPException(status_code=500, detail=str(exc)) | |
| return {"status": "saved", "id": auto_id, "steps": len(w.steps)} | |
| async def get_history(limit: int = 100): | |
| # Part 18: real backing store — automation_service.execute_action records | |
| # every run (ok/error) into the automation_history table in the same | |
| # env-driven SQLite file as custom_automations. | |
| from backend.services.automation_service import load_history | |
| try: | |
| return load_history(limit) | |
| except Exception as exc: | |
| logging.exception("automation history read failed") | |
| raise HTTPException(status_code=500, detail=str(exc)) | |
| async def dry_run_workflow(w: Workflow): | |
| """ | |
| Simulates workflow execution without side effects. | |
| Returns a preview of the steps and their expected mock results. | |
| """ | |
| import asyncio | |
| preview_steps = [] | |
| for i, step in enumerate(w.steps): | |
| # Determine a mock result based on step type if present | |
| step_type = step.get('type', 'unknown') if isinstance(step, dict) else 'unknown' | |
| mock_result = f"Mock result for {step_type}" | |
| if step_type == 'api_call': mock_result = "HTTP 200 OK (Mock)" | |
| elif step_type == 'script': mock_result = "Script execution simulated" | |
| elif step_type == 'agent': mock_result = "Agent reasoning simulated" | |
| preview_steps.append({ | |
| "step_index": i, | |
| "step_config": step, | |
| "expected_status": "success", | |
| "mock_result": mock_result | |
| }) | |
| # Simulate minor delay | |
| await asyncio.sleep(0.1) | |
| return { | |
| "status": "dry-run-complete", | |
| "total_steps": len(w.steps), | |
| "execution_preview": preview_steps | |
| } | |