fffiloni commited on
Commit
c3d309d
·
verified ·
1 Parent(s): db12bdc

Upload 6 files

Browse files
Files changed (1) hide show
  1. app.py +144 -8
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": "v58-stable-refresh-pi-stdout-bucket-links",
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
- cancelled_state = {**launch, **state, "run_id": run_id, "status": "cancelled", "job_id": str(job_id), "job_url": summary.get("job_url") or launch.get("job_url") or state.get("job_url"), "updated_at": result.get("updated_at") or ""}
369
- cancelled_summary = {**summary, "run_id": run_id, "status": "cancelled", "job_id": str(job_id), "job_url": cancelled_state.get("job_url") or summary.get("job_url") or "", "updated_at": cancelled_state.get("updated_at") or summary.get("updated_at") or ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- events = bundle.get("events") or []
 
 
 
 
 
 
 
387
  progress = progress_from_events(events, state=effective_state)
388
- summary = bundle.get("summary") or {}
389
- view = build_run_view_model(run_id, bundle, bucket_source=bucket_source)
 
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": (bundle.get("pi_live_log") or bundle.get("pi_output_log") or "")[-12000:],
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,