Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload 6 files
Browse files
app.py
CHANGED
|
@@ -113,6 +113,68 @@ def _api_links(*, run_id: str | None, bucket_source: str | None, target_space: s
|
|
| 113 |
"artifacts_url": _run_artifacts_url(run_id, bucket_source),
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
def register_custom_routes(fastapi_app: FastAPI) -> None:
|
| 117 |
"""Register the root custom UI and OAuth-backed JSON endpoints."""
|
| 118 |
|
|
@@ -151,7 +213,7 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 151 |
return JSONResponse(
|
| 152 |
{
|
| 153 |
"name": "Agentic Space Factory",
|
| 154 |
-
"version": "
|
| 155 |
"bucket_default": settings.bucket_name,
|
| 156 |
"workflows": ["build_from_model_card", "validate_existing_space", "runs_explorer"],
|
| 157 |
"custom_ui_status": "root_custom_ui",
|
|
@@ -365,14 +427,66 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 365 |
if not result.get("ok"):
|
| 366 |
raise HTTPException(status_code=400, detail=redact(str(result.get("error") or "Could not cancel Job.")))
|
| 367 |
paths = RunPaths(run_id, bucket_source=bucket_source)
|
| 368 |
-
|
| 369 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
try:
|
| 371 |
write_json(paths.state, cancelled_state, token=ctx["token"])
|
| 372 |
write_json(f"{paths.root}/summary.json", cancelled_summary, token=ctx["token"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
except Exception:
|
| 374 |
pass
|
| 375 |
-
return JSONResponse({"ok": True, "run_id": run_id, "job_id": str(job_id), "status": "cancelled"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 378 |
async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
|
@@ -382,22 +496,44 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 382 |
bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 383 |
state = bundle.get("state") or {}
|
| 384 |
launch = bundle.get("launch") or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
effective_state = {**launch, **state}
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
progress = progress_from_events(events, state=effective_state)
|
| 388 |
-
|
| 389 |
-
view = build_run_view_model(run_id,
|
|
|
|
| 390 |
progress.update(
|
| 391 |
{
|
| 392 |
"run_id": run_id,
|
| 393 |
"bucket_source": bucket_source,
|
| 394 |
"state": effective_state,
|
| 395 |
"summary": summary,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
"inference_gate": bundle.get("inference_gate") or {},
|
| 397 |
"generation_smoke": bundle.get("generation_smoke") or {},
|
| 398 |
"hardware_strategy": bundle.get("hardware_strategy") or {},
|
| 399 |
"technical_blockers": bundle.get("technical_blockers") or {},
|
| 400 |
-
"pi_live_log":
|
| 401 |
"pi_live_log_source": "logs/pi_live_output.txt" if bundle.get("pi_live_log") else ("logs/pi_output.txt" if bundle.get("pi_output_log") else ""),
|
| 402 |
"events": events[-20:],
|
| 403 |
"view": view,
|
|
|
|
| 113 |
"artifacts_url": _run_artifacts_url(run_id, bucket_source),
|
| 114 |
}
|
| 115 |
|
| 116 |
+
|
| 117 |
+
def _job_id_from_run_bundle(summary: dict[str, Any], launch: dict[str, Any], state: dict[str, Any]) -> str:
|
| 118 |
+
return str(summary.get("job_id") or launch.get("job_id") or state.get("job_id") or "").strip()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _normalize_job_stage(stage: Any) -> str:
|
| 122 |
+
value = str(stage or "").strip().lower()
|
| 123 |
+
if "." in value:
|
| 124 |
+
value = value.rsplit(".", 1)[-1]
|
| 125 |
+
value = value.replace("jobstage.", "").replace("_", "-")
|
| 126 |
+
if value in {"running", "queued", "pending", "scheduled", "starting"}:
|
| 127 |
+
return "running"
|
| 128 |
+
if value in {"success", "succeeded", "complete", "completed", "done"}:
|
| 129 |
+
return "success"
|
| 130 |
+
if value in {"failed", "failure", "error"}:
|
| 131 |
+
return "failed"
|
| 132 |
+
if value in {"cancelled", "canceled", "canceling", "cancelling"}:
|
| 133 |
+
return "cancelled"
|
| 134 |
+
return value
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _event_key(event: dict[str, Any]) -> tuple[str, str, str, str]:
|
| 138 |
+
return (
|
| 139 |
+
str(event.get("ts") or ""),
|
| 140 |
+
str(event.get("step") or ""),
|
| 141 |
+
str(event.get("status") or ""),
|
| 142 |
+
str(event.get("message") or ""),
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _events_from_job_logs(log_text: str) -> list[dict[str, Any]]:
|
| 147 |
+
"""Extract worker append_event JSON lines from HF Job logs."""
|
| 148 |
+
events: list[dict[str, Any]] = []
|
| 149 |
+
for raw in (log_text or "").splitlines():
|
| 150 |
+
line = raw.strip()
|
| 151 |
+
start = line.find("{")
|
| 152 |
+
if start > 0:
|
| 153 |
+
line = line[start:]
|
| 154 |
+
if not line.startswith("{"):
|
| 155 |
+
continue
|
| 156 |
+
try:
|
| 157 |
+
payload = json.loads(line)
|
| 158 |
+
except Exception:
|
| 159 |
+
continue
|
| 160 |
+
if isinstance(payload, dict) and payload.get("step") and payload.get("status"):
|
| 161 |
+
events.append(payload)
|
| 162 |
+
return events
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _merge_events(bucket_events: list[dict[str, Any]], log_events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 166 |
+
seen: set[tuple[str, str, str, str]] = set()
|
| 167 |
+
merged: list[dict[str, Any]] = []
|
| 168 |
+
for event in [*(bucket_events or []), *(log_events or [])]:
|
| 169 |
+
if not isinstance(event, dict):
|
| 170 |
+
continue
|
| 171 |
+
key = _event_key(event)
|
| 172 |
+
if key in seen:
|
| 173 |
+
continue
|
| 174 |
+
seen.add(key)
|
| 175 |
+
merged.append(event)
|
| 176 |
+
return merged
|
| 177 |
+
|
| 178 |
def register_custom_routes(fastapi_app: FastAPI) -> None:
|
| 179 |
"""Register the root custom UI and OAuth-backed JSON endpoints."""
|
| 180 |
|
|
|
|
| 213 |
return JSONResponse(
|
| 214 |
{
|
| 215 |
"name": "Agentic Space Factory",
|
| 216 |
+
"version": "v60-cancel-persist-delete-run",
|
| 217 |
"bucket_default": settings.bucket_name,
|
| 218 |
"workflows": ["build_from_model_card", "validate_existing_space", "runs_explorer"],
|
| 219 |
"custom_ui_status": "root_custom_ui",
|
|
|
|
| 427 |
if not result.get("ok"):
|
| 428 |
raise HTTPException(status_code=400, detail=redact(str(result.get("error") or "Could not cancel Job.")))
|
| 429 |
paths = RunPaths(run_id, bucket_source=bucket_source)
|
| 430 |
+
cancelled_at = utc_now_iso()
|
| 431 |
+
job_url = summary.get("job_url") or launch.get("job_url") or state.get("job_url")
|
| 432 |
+
cancel_meta = {
|
| 433 |
+
"requested_by": ctx["username"],
|
| 434 |
+
"cancelled_at": cancelled_at,
|
| 435 |
+
"job_id": str(job_id),
|
| 436 |
+
"job_url": job_url or "",
|
| 437 |
+
"hf_cancel_result": result,
|
| 438 |
+
}
|
| 439 |
+
cancelled_state = {
|
| 440 |
+
**launch,
|
| 441 |
+
**state,
|
| 442 |
+
"run_id": run_id,
|
| 443 |
+
"status": "cancelled",
|
| 444 |
+
"job_id": str(job_id),
|
| 445 |
+
"job_url": job_url,
|
| 446 |
+
"cancel_requested": True,
|
| 447 |
+
"cancelled_at": cancelled_at,
|
| 448 |
+
"updated_at": cancelled_at,
|
| 449 |
+
"cancel": cancel_meta,
|
| 450 |
+
}
|
| 451 |
+
cancelled_summary = {
|
| 452 |
+
**summary,
|
| 453 |
+
"run_id": run_id,
|
| 454 |
+
"status": "cancelled",
|
| 455 |
+
"job_id": str(job_id),
|
| 456 |
+
"job_url": job_url or summary.get("job_url") or "",
|
| 457 |
+
"cancel_requested": True,
|
| 458 |
+
"cancelled_at": cancelled_at,
|
| 459 |
+
"updated_at": cancelled_at,
|
| 460 |
+
}
|
| 461 |
try:
|
| 462 |
write_json(paths.state, cancelled_state, token=ctx["token"])
|
| 463 |
write_json(f"{paths.root}/summary.json", cancelled_summary, token=ctx["token"])
|
| 464 |
+
write_json(f"{paths.root}/cancel.json", cancel_meta, token=ctx["token"])
|
| 465 |
+
append_run_event(
|
| 466 |
+
run_id,
|
| 467 |
+
bucket_source=bucket_source,
|
| 468 |
+
step="cancel",
|
| 469 |
+
status="cancelled",
|
| 470 |
+
message="Job cancellation requested from Agentic Space Factory UI",
|
| 471 |
+
details=cancel_meta,
|
| 472 |
+
token=ctx["token"],
|
| 473 |
+
)
|
| 474 |
except Exception:
|
| 475 |
pass
|
| 476 |
+
return JSONResponse({"ok": True, "run_id": run_id, "job_id": str(job_id), "status": "cancelled", "cancelled_at": cancelled_at})
|
| 477 |
+
|
| 478 |
+
@fastapi_app.delete("/api/runs/{run_id}")
|
| 479 |
+
async def api_delete_run(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
| 480 |
+
ctx = _oauth_context_from_request(request)
|
| 481 |
+
run_id = validate_run_id(run_id)
|
| 482 |
+
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 483 |
+
try:
|
| 484 |
+
delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 485 |
+
except FileNotFoundError:
|
| 486 |
+
pass
|
| 487 |
+
except Exception as exc: # noqa: BLE001
|
| 488 |
+
raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
|
| 489 |
+
return JSONResponse({"ok": True, "run_id": run_id, "deleted": True})
|
| 490 |
|
| 491 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 492 |
async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
|
|
|
|
| 496 |
bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 497 |
state = bundle.get("state") or {}
|
| 498 |
launch = bundle.get("launch") or {}
|
| 499 |
+
summary = bundle.get("summary") or {}
|
| 500 |
+
job_id = _job_id_from_run_bundle(summary, launch, state)
|
| 501 |
+
job_info: dict[str, Any] = {}
|
| 502 |
+
job_logs = ""
|
| 503 |
+
job_log_events: list[dict[str, Any]] = []
|
| 504 |
+
if job_id:
|
| 505 |
+
job_info = inspect_job_safe(job_id, token=ctx["token"])
|
| 506 |
+
job_logs = fetch_recent_logs_safe(job_id, token=ctx["token"], max_lines=220)
|
| 507 |
+
job_log_events = _events_from_job_logs(job_logs)
|
| 508 |
+
job_stage = _normalize_job_stage(job_info.get("stage"))
|
| 509 |
effective_state = {**launch, **state}
|
| 510 |
+
if job_stage and not job_info.get("error"):
|
| 511 |
+
effective_state["job_stage"] = job_stage
|
| 512 |
+
# Use Job stage as live source of truth while the bucket lags.
|
| 513 |
+
if job_stage in {"running", "failed", "cancelled"}:
|
| 514 |
+
effective_state["status"] = job_stage
|
| 515 |
+
elif job_stage == "success" and not (bundle.get("events") or []):
|
| 516 |
+
effective_state["status"] = "success"
|
| 517 |
+
events = _merge_events(bundle.get("events") or [], job_log_events)
|
| 518 |
progress = progress_from_events(events, state=effective_state)
|
| 519 |
+
view_bundle = {**bundle, "events": events}
|
| 520 |
+
view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
|
| 521 |
+
pi_stdout = bundle.get("pi_live_log") or bundle.get("pi_output_log") or ""
|
| 522 |
progress.update(
|
| 523 |
{
|
| 524 |
"run_id": run_id,
|
| 525 |
"bucket_source": bucket_source,
|
| 526 |
"state": effective_state,
|
| 527 |
"summary": summary,
|
| 528 |
+
"job_id": job_id,
|
| 529 |
+
"job_info": job_info,
|
| 530 |
+
"job_logs": job_logs[-20000:],
|
| 531 |
+
"job_log_events_count": len(job_log_events),
|
| 532 |
"inference_gate": bundle.get("inference_gate") or {},
|
| 533 |
"generation_smoke": bundle.get("generation_smoke") or {},
|
| 534 |
"hardware_strategy": bundle.get("hardware_strategy") or {},
|
| 535 |
"technical_blockers": bundle.get("technical_blockers") or {},
|
| 536 |
+
"pi_live_log": pi_stdout[-12000:],
|
| 537 |
"pi_live_log_source": "logs/pi_live_output.txt" if bundle.get("pi_live_log") else ("logs/pi_output.txt" if bundle.get("pi_output_log") else ""),
|
| 538 |
"events": events[-20:],
|
| 539 |
"view": view,
|