Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload 6 files
Browse files- CHANGELOG.md +42 -3
- README.md +28 -3
- app.py +184 -8
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
- Treat linked validation runs as children of Build Runs in Runs Explorer by default.
|
| 5 |
- Rework Run Stats around Build Runs, linked tests, automatic success, and effective success.
|
|
@@ -15,9 +54,9 @@
|
|
| 15 |
- Active Run snapshots and run summaries can now surface manual validation status, effective verdict, latency, and ZeroGPU duration measurements sourced from linked Space Test.
|
| 16 |
- Runs Explorer labels linked validation runs, shows manual validation passed on Build cards, and opens the parent Build Run when a linked validation is selected.
|
| 17 |
|
| 18 |
-
# Agentic Space Factory v190.
|
| 19 |
|
| 20 |
-
## v190.
|
| 21 |
|
| 22 |
- Generation smoke payloads now preserve Gradio schema choice value types before calling predict.
|
| 23 |
- If Gradio rejects a smoke payload with a choice type mismatch, the worker records the first error, coerces args to schema choices, and retries once.
|
|
|
|
| 1 |
+
# v190.19 — Eval archive effective outcomes + owner flush
|
| 2 |
|
| 3 |
+
- Eval records now preserve automatic outcome and add effective outcome metadata for linked Space Test validations.
|
| 4 |
+
- Backend archive publishing enriches records with linked validation summaries, endpoint discovery metrics, smoke retry metrics and stricter privacy flags without storing prompts, args, target Space IDs or generated code.
|
| 5 |
+
- Eval publish statuses now use schema v1.3 and republish when a parent Build Run gains new effective validation state.
|
| 6 |
+
- Instance owners / configured eval admins can flush archived eval records from the mounted eval bucket while preserving archive configuration.
|
| 7 |
+
|
| 8 |
+
# v190.19 — Build completion matrix + endpoint discovery-first Space Test
|
| 9 |
+
|
| 10 |
+
- Added canonical `space_test_policy` for replay/complete/recover/blocked/unavailable Build Run states.
|
| 11 |
+
- Linked Space Test can now discover Gradio endpoints before validation when a Build Run has no known endpoint schema.
|
| 12 |
+
- Space Test UI consumes policy labels/messages and shows discovery-first validation state.
|
| 13 |
+
- Runs Explorer/Stats can surface recoverable and needs-validation states without treating Space Test as standalone.
|
| 14 |
+
|
| 15 |
+
## v190.19 — Linked validation hardening and effective status UX
|
| 16 |
+
|
| 17 |
+
- Fixed linked Space Test parent update after smoke success by sourcing `api_name` and `expected_output_type` from the smoke payload/environment instead of out-of-scope variables.
|
| 18 |
+
- Prevented failed linked validation attempts from downgrading a parent Build Run that already has a successful linked validation.
|
| 19 |
+
- Clarified effective status filters/stats, Eval Archive effective hints, Space Test context, and latency/ZeroGPU recommendation provenance.
|
| 20 |
+
|
| 21 |
+
## v190.16 — Cascade delete linked Space Tests
|
| 22 |
+
|
| 23 |
+
- Deleting a Build Run now cascades to its linked Space Test validation runs so no hidden validation artifacts are orphaned.
|
| 24 |
+
- Deleting a linked validation run updates the parent Build Run linked validation metadata and manual validation status.
|
| 25 |
+
- The delete modal now reports linked validation counts and the frontend removes parent/child cache entries together.
|
| 26 |
+
|
| 27 |
+
## v190.16 — Build-first explorer compatibility cleanup
|
| 28 |
+
|
| 29 |
+
- Keeps Runs Explorer simple after the linked-only Space Test change: Build Runs remain the primary list items, linked validations are summarized on their parent, and no standalone/legacy validation section is introduced.
|
| 30 |
+
- Hardens the run cache, stats, pagination, and selection paths against null, partial, or legacy Build Run records that do not contain manual validation metadata.
|
| 31 |
+
- Fixes linked validation parent selection without duplicating variables in the snapshot path.
|
| 32 |
+
- Keeps the v190.12 generic Gradio schema-choice smoke retry intact.
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
## v190.16 hotfix — Runs Explorer null-safe linked validation cache
|
| 36 |
+
|
| 37 |
+
- Hardened Runs Explorer against null or malformed run summaries returned by stale bucket/list responses.
|
| 38 |
+
- Preserved build-first linked validation grouping while filtering invalid cache entries safely.
|
| 39 |
+
- Added regression coverage for null-safe linked validation parent lookup.
|
| 40 |
+
|
| 41 |
+
## v190.16 — Build-first explorer and linked validation stats polish
|
| 42 |
|
| 43 |
- Treat linked validation runs as children of Build Runs in Runs Explorer by default.
|
| 44 |
- Rework Run Stats around Build Runs, linked tests, automatic success, and effective success.
|
|
|
|
| 54 |
- Active Run snapshots and run summaries can now surface manual validation status, effective verdict, latency, and ZeroGPU duration measurements sourced from linked Space Test.
|
| 55 |
- Runs Explorer labels linked validation runs, shows manual validation passed on Build cards, and opens the parent Build Run when a linked validation is selected.
|
| 56 |
|
| 57 |
+
# Agentic Space Factory v190.16
|
| 58 |
|
| 59 |
+
## v190.16 smoke retry and final summary hardening
|
| 60 |
|
| 61 |
- Generation smoke payloads now preserve Gradio schema choice value types before calling predict.
|
| 62 |
- If Gradio rejects a smoke payload with a choice type mismatch, the worker records the first error, coerces args to schema choices, and retries once.
|
README.md
CHANGED
|
@@ -12,10 +12,29 @@ hf_oauth_scopes:
|
|
| 12 |
- jobs
|
| 13 |
- read-billing
|
| 14 |
---
|
| 15 |
-
## v190.14 smoke retry and live validation telemetry
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- The canonical run snapshot now includes a `live_validation` telemetry model so the Live test phase can show Space build/runtime, health, Gradio endpoint discovery and generation smoke state instead of a generic running message.
|
| 20 |
- Active polling runs at a faster visible cadence for smoother event updates.
|
| 21 |
|
|
@@ -395,3 +414,9 @@ The Active Run progress header now separates pipeline completion from product ve
|
|
| 395 |
## v190.9 — Space Test canonical validation persistence
|
| 396 |
|
| 397 |
- v190.9 makes terminal Space Test validation status survive page reloads. Failed, success, partial, and manual validation outcomes override stale `running` launch metadata in the Runs Explorer, delete modal, and progress polling.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
- jobs
|
| 13 |
- read-billing
|
| 14 |
---
|
|
|
|
| 15 |
|
| 16 |
+
## v190.19 Linked validation hardening
|
| 17 |
+
|
| 18 |
+
- v190.19 hardens linked Space Test success/failure paths: parent Build Run updates read API/output metadata from the smoke result, failed manual attempts cannot downgrade an already validated parent, and latency/ZeroGPU recommendations expose their linked-test hardware provenance.
|
| 19 |
+
- Runs Explorer and Run Stats now treat automatic and effective statuses explicitly so manually validated partial builds remain discoverable in both Partial and Validated views.
|
| 20 |
+
- Space Test is contextualized from the selected Build Run even before pressing Prefill, while remaining linked-only for launches.
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
## v190.16 Cascade delete linked Space Tests
|
| 24 |
+
|
| 25 |
+
- Build Run deletion now removes linked Space Test validation runs from the run bucket as part of the same cleanup.
|
| 26 |
+
- Linked validation deletion keeps the parent Build Run but refreshes its manual validation metadata.
|
| 27 |
+
- Runs Explorer remains Build-first and clears parent/child rows from cache immediately after deletion.
|
| 28 |
+
|
| 29 |
+
## v190.16 Build-first explorer compatibility
|
| 30 |
+
|
| 31 |
+
- v190.16 keeps Runs Explorer simple: Build Runs are the primary objects, linked Space Tests are summarized on their parent, and legacy/incomplete Build Runs remain visible without requiring validation metadata.
|
| 32 |
+
- It hardens the run cache against null or partial records and keeps validation runs out of the primary explorer by default.
|
| 33 |
+
|
| 34 |
+
## v190.16 smoke retry and live validation telemetry
|
| 35 |
+
|
| 36 |
+
- v190.16 keeps Space Test prefill disabled until a build has reached a final Space state that can be tested manually.
|
| 37 |
+
- v190.16 retries generation smoke once when Gradio reports schema-choice type mismatches, and writes terminal summary.json plus live_status.json for cleaner audits.
|
| 38 |
- The canonical run snapshot now includes a `live_validation` telemetry model so the Live test phase can show Space build/runtime, health, Gradio endpoint discovery and generation smoke state instead of a generic running message.
|
| 39 |
- Active polling runs at a faster visible cadence for smoother event updates.
|
| 40 |
|
|
|
|
| 414 |
## v190.9 — Space Test canonical validation persistence
|
| 415 |
|
| 416 |
- v190.9 makes terminal Space Test validation status survive page reloads. Failed, success, partial, and manual validation outcomes override stale `running` launch metadata in the Runs Explorer, delete modal, and progress polling.
|
| 417 |
+
|
| 418 |
+
## v190.19 Eval archive effective outcomes
|
| 419 |
+
|
| 420 |
+
- Eval archive records now distinguish automatic verdicts from effective verdicts after linked Space Test validation.
|
| 421 |
+
- Archived records include anonymized linked-validation, endpoint-discovery and smoke-retry metrics while preserving privacy constraints.
|
| 422 |
+
- Instance owners can flush all archived eval records from the mounted eval bucket without deleting the eval archive configuration.
|
app.py
CHANGED
|
@@ -12,7 +12,7 @@ from huggingface_hub import HfApi, attach_huggingface_oauth
|
|
| 12 |
|
| 13 |
from src.bucket import RunPaths, check_user_bucket, create_user_bucket, delete_run_folder, read_run_bundle, list_recent_runs, write_json, write_launch_metadata
|
| 14 |
from src.config import settings, user_bucket_source
|
| 15 |
-
from src.eval_config import activate_eval_archive_config, disable_eval_archive_config, public_eval_config
|
| 16 |
from src.eval_archive import maybe_publish_eval_record
|
| 17 |
from src.auth import extract_oauth_context, public_oauth_context, oauth_warning_messages, verify_token_identity
|
| 18 |
from src.jobs import (
|
|
@@ -420,6 +420,19 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 420 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 421 |
return JSONResponse(cfg)
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
@fastapi_app.get("/api/oauth/diagnostics")
|
| 424 |
async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
|
| 425 |
ctx = extract_oauth_context(request)
|
|
@@ -544,7 +557,7 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 544 |
if not isinstance(test_kwargs, dict):
|
| 545 |
raise ValueError("test_kwargs_json must be a JSON object; it is passed as keyword args to gradio_client.predict().")
|
| 546 |
parent_build_run_id = validate_run_id(str(payload.get("parent_build_run_id") or ""))
|
| 547 |
-
parent_bundle = read_run_bundle(parent_build_run_id, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name), token=ctx["token"], include_heavy=
|
| 548 |
parent_summary = parent_bundle.get("summary") or {}
|
| 549 |
parent_target = parent_summary.get("target_space") or (parent_bundle.get("state") or {}).get("target_space") or (parent_bundle.get("launch") or {}).get("target_space") or ""
|
| 550 |
requested_target = str(payload.get("target_space_id") or "").strip()
|
|
@@ -552,16 +565,25 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 552 |
raise ValueError("Linked Space Test requires a parent Build Run with a generated target Space.")
|
| 553 |
if requested_target != parent_target:
|
| 554 |
raise ValueError("Space Test must remain linked to the selected Build Run target Space. Standalone validation is not supported in this app.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
result = launch_validate_existing_space_job(
|
| 556 |
token=ctx["token"],
|
| 557 |
username=ctx["username"],
|
| 558 |
target_space_id=requested_target,
|
| 559 |
parent_build_run_id=parent_build_run_id,
|
| 560 |
-
api_name=
|
| 561 |
test_args_json=json.dumps(test_args, ensure_ascii=False),
|
| 562 |
test_kwargs_json=json.dumps(test_kwargs, ensure_ascii=False),
|
| 563 |
expected_output_type=payload.get("expected_output_type"),
|
| 564 |
live_timeout_seconds=payload.get("live_timeout_seconds") or 1800,
|
|
|
|
|
|
|
| 565 |
run_id=payload.get("run_id"),
|
| 566 |
bucket_name=bucket_name,
|
| 567 |
)
|
|
@@ -723,26 +745,73 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 723 |
body = {}
|
| 724 |
except Exception:
|
| 725 |
body = {}
|
|
|
|
| 726 |
delete_space = bool(body.get("delete_space"))
|
| 727 |
bundle: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
space_report: dict[str, Any] = {"space_delete_requested": delete_space, "space_deleted": False}
|
| 729 |
if delete_space:
|
| 730 |
try:
|
| 731 |
-
|
| 732 |
-
associated_space = _associated_space_from_bundle(bundle)
|
| 733 |
-
kind = str((bundle.get("summary") or {}).get("kind") or (bundle.get("state") or {}).get("kind") or (bundle.get("launch") or {}).get("kind") or "").lower()
|
| 734 |
-
if "validation" in kind or "space_test" in kind:
|
| 735 |
raise PermissionError("Associated Space deletion is not available for validation runs.")
|
|
|
|
| 736 |
space_report = _delete_associated_space(associated_space, username=ctx["username"], token=ctx["token"])
|
| 737 |
except Exception as exc: # noqa: BLE001
|
| 738 |
space_report = {"space_delete_requested": True, "space_deleted": False, "space_delete_error": redact(str(exc))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
try:
|
| 740 |
delete_report = delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 741 |
except FileNotFoundError:
|
| 742 |
delete_report = {"matched_count": 0, "deleted_count": 0, "remaining_count": 0}
|
| 743 |
except Exception as exc: # noqa: BLE001
|
| 744 |
raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
|
| 745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
|
| 747 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 748 |
async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_job_logs: bool = False): # type: ignore[no-untyped-def]
|
|
@@ -1298,6 +1367,113 @@ def _delete_associated_space(space_id: str, *, username: str, token: str) -> dic
|
|
| 1298 |
return {"space_delete_requested": True, "space_deleted": False, "space_already_missing": True, "space_id": cleaned, "space_delete_error": message}
|
| 1299 |
return {"space_delete_requested": True, "space_deleted": False, "space_id": cleaned, "space_delete_error": message}
|
| 1300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
def create_app() -> FastAPI:
|
| 1302 |
"""Create the product FastAPI app.
|
| 1303 |
|
|
|
|
| 12 |
|
| 13 |
from src.bucket import RunPaths, check_user_bucket, create_user_bucket, delete_run_folder, read_run_bundle, list_recent_runs, write_json, write_launch_metadata
|
| 14 |
from src.config import settings, user_bucket_source
|
| 15 |
+
from src.eval_config import activate_eval_archive_config, disable_eval_archive_config, flush_eval_archive_records, public_eval_config
|
| 16 |
from src.eval_archive import maybe_publish_eval_record
|
| 17 |
from src.auth import extract_oauth_context, public_oauth_context, oauth_warning_messages, verify_token_identity
|
| 18 |
from src.jobs import (
|
|
|
|
| 420 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 421 |
return JSONResponse(cfg)
|
| 422 |
|
| 423 |
+
@fastapi_app.post("/api/eval-archive/flush")
|
| 424 |
+
async def api_eval_archive_flush(request: Request): # type: ignore[no-untyped-def]
|
| 425 |
+
ctx = _oauth_context_from_request(request)
|
| 426 |
+
try:
|
| 427 |
+
cfg = flush_eval_archive_records(username=ctx["username"])
|
| 428 |
+
except PermissionError as exc:
|
| 429 |
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
| 430 |
+
except FileNotFoundError as exc:
|
| 431 |
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
| 432 |
+
except Exception as exc: # noqa: BLE001
|
| 433 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 434 |
+
return JSONResponse(cfg)
|
| 435 |
+
|
| 436 |
@fastapi_app.get("/api/oauth/diagnostics")
|
| 437 |
async def api_oauth_diagnostics(request: Request): # type: ignore[no-untyped-def]
|
| 438 |
ctx = extract_oauth_context(request)
|
|
|
|
| 557 |
if not isinstance(test_kwargs, dict):
|
| 558 |
raise ValueError("test_kwargs_json must be a JSON object; it is passed as keyword args to gradio_client.predict().")
|
| 559 |
parent_build_run_id = validate_run_id(str(payload.get("parent_build_run_id") or ""))
|
| 560 |
+
parent_bundle = read_run_bundle(parent_build_run_id, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name), token=ctx["token"], include_heavy=True)
|
| 561 |
parent_summary = parent_bundle.get("summary") or {}
|
| 562 |
parent_target = parent_summary.get("target_space") or (parent_bundle.get("state") or {}).get("target_space") or (parent_bundle.get("launch") or {}).get("target_space") or ""
|
| 563 |
requested_target = str(payload.get("target_space_id") or "").strip()
|
|
|
|
| 565 |
raise ValueError("Linked Space Test requires a parent Build Run with a generated target Space.")
|
| 566 |
if requested_target != parent_target:
|
| 567 |
raise ValueError("Space Test must remain linked to the selected Build Run target Space. Standalone validation is not supported in this app.")
|
| 568 |
+
parent_view = build_run_view_model(parent_build_run_id, parent_bundle, bucket_source=bucket_status.get("bucket_source") or user_bucket_source(username=ctx["username"], bucket_name=bucket_name))
|
| 569 |
+
space_test_policy = parent_view.get("space_test_policy") or parent_view.get("space_test", {}).get("policy") or {}
|
| 570 |
+
if not space_test_policy.get("enabled"):
|
| 571 |
+
raise ValueError(str(space_test_policy.get("message") or "This Build Run is not eligible for linked Space Test yet."))
|
| 572 |
+
api_name_for_validation = payload.get("api_name")
|
| 573 |
+
if space_test_policy.get("requires_endpoint_discovery") and str(api_name_for_validation or "").strip() in {"", "/generate"}:
|
| 574 |
+
api_name_for_validation = ""
|
| 575 |
result = launch_validate_existing_space_job(
|
| 576 |
token=ctx["token"],
|
| 577 |
username=ctx["username"],
|
| 578 |
target_space_id=requested_target,
|
| 579 |
parent_build_run_id=parent_build_run_id,
|
| 580 |
+
api_name=api_name_for_validation,
|
| 581 |
test_args_json=json.dumps(test_args, ensure_ascii=False),
|
| 582 |
test_kwargs_json=json.dumps(test_kwargs, ensure_ascii=False),
|
| 583 |
expected_output_type=payload.get("expected_output_type"),
|
| 584 |
live_timeout_seconds=payload.get("live_timeout_seconds") or 1800,
|
| 585 |
+
validation_mode=space_test_policy.get("mode"),
|
| 586 |
+
effective_status_on_success=space_test_policy.get("effective_status_on_success") or "validated_after_manual_space_test",
|
| 587 |
run_id=payload.get("run_id"),
|
| 588 |
bucket_name=bucket_name,
|
| 589 |
)
|
|
|
|
| 745 |
body = {}
|
| 746 |
except Exception:
|
| 747 |
body = {}
|
| 748 |
+
|
| 749 |
delete_space = bool(body.get("delete_space"))
|
| 750 |
bundle: dict[str, Any] = {}
|
| 751 |
+
try:
|
| 752 |
+
bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
|
| 753 |
+
except Exception:
|
| 754 |
+
bundle = {}
|
| 755 |
+
|
| 756 |
+
is_validation = _run_is_validation_like({**bundle, "run_id": run_id})
|
| 757 |
+
linked_children: list[dict[str, Any]] = []
|
| 758 |
+
if not is_validation:
|
| 759 |
+
linked_children = _linked_validation_summaries(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 760 |
+
|
| 761 |
space_report: dict[str, Any] = {"space_delete_requested": delete_space, "space_deleted": False}
|
| 762 |
if delete_space:
|
| 763 |
try:
|
| 764 |
+
if is_validation:
|
|
|
|
|
|
|
|
|
|
| 765 |
raise PermissionError("Associated Space deletion is not available for validation runs.")
|
| 766 |
+
associated_space = _associated_space_from_bundle(bundle)
|
| 767 |
space_report = _delete_associated_space(associated_space, username=ctx["username"], token=ctx["token"])
|
| 768 |
except Exception as exc: # noqa: BLE001
|
| 769 |
space_report = {"space_delete_requested": True, "space_deleted": False, "space_delete_error": redact(str(exc))}
|
| 770 |
+
|
| 771 |
+
deleted_linked_validations: list[str] = []
|
| 772 |
+
linked_delete_errors: list[str] = []
|
| 773 |
+
for child in linked_children:
|
| 774 |
+
child_id = str(child.get("run_id") or "").strip()
|
| 775 |
+
if not child_id or child_id == run_id:
|
| 776 |
+
continue
|
| 777 |
+
try:
|
| 778 |
+
delete_run_folder(validate_run_id(child_id), bucket_source=bucket_source, token=ctx["token"])
|
| 779 |
+
deleted_linked_validations.append(child_id)
|
| 780 |
+
except FileNotFoundError:
|
| 781 |
+
deleted_linked_validations.append(child_id)
|
| 782 |
+
except Exception as exc: # noqa: BLE001
|
| 783 |
+
linked_delete_errors.append(f"{child_id}: {redact(str(exc))}")
|
| 784 |
+
|
| 785 |
+
parent_update: dict[str, Any] = {}
|
| 786 |
+
parent_build_run_id = _linked_validation_parent_id({**bundle, "run_id": run_id}) if is_validation else ""
|
| 787 |
try:
|
| 788 |
delete_report = delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 789 |
except FileNotFoundError:
|
| 790 |
delete_report = {"matched_count": 0, "deleted_count": 0, "remaining_count": 0}
|
| 791 |
except Exception as exc: # noqa: BLE001
|
| 792 |
raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
|
| 793 |
+
|
| 794 |
+
if is_validation and parent_build_run_id:
|
| 795 |
+
remaining = [row for row in _linked_validation_summaries(parent_build_run_id, bucket_source=bucket_source, token=ctx["token"]) if str(row.get("run_id") or "") != run_id]
|
| 796 |
+
try:
|
| 797 |
+
parent_update = _write_parent_linked_validation_state(parent_build_run_id, remaining, bucket_source=bucket_source, token=ctx["token"])
|
| 798 |
+
except Exception as exc: # noqa: BLE001
|
| 799 |
+
parent_update = {"parent_update_error": redact(str(exc))}
|
| 800 |
+
|
| 801 |
+
response = {
|
| 802 |
+
"ok": True,
|
| 803 |
+
"run_id": run_id,
|
| 804 |
+
"deleted": True,
|
| 805 |
+
**delete_report,
|
| 806 |
+
**space_report,
|
| 807 |
+
"deleted_linked_validations": deleted_linked_validations,
|
| 808 |
+
"linked_delete_errors": linked_delete_errors,
|
| 809 |
+
"parent_build_run_id": parent_build_run_id,
|
| 810 |
+
"parent_update": parent_update,
|
| 811 |
+
}
|
| 812 |
+
if linked_delete_errors:
|
| 813 |
+
response["partial_delete"] = True
|
| 814 |
+
return JSONResponse(response)
|
| 815 |
|
| 816 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 817 |
async def api_run_progress(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_job_logs: bool = False): # type: ignore[no-untyped-def]
|
|
|
|
| 1367 |
return {"space_delete_requested": True, "space_deleted": False, "space_already_missing": True, "space_id": cleaned, "space_delete_error": message}
|
| 1368 |
return {"space_delete_requested": True, "space_deleted": False, "space_id": cleaned, "space_delete_error": message}
|
| 1369 |
|
| 1370 |
+
|
| 1371 |
+
def _run_kind_from_bundle(bundle: dict[str, Any]) -> str:
|
| 1372 |
+
summary = bundle.get("summary") or bundle.get("summary_file") or {}
|
| 1373 |
+
state = bundle.get("state") or {}
|
| 1374 |
+
launch = bundle.get("launch") or {}
|
| 1375 |
+
return str(summary.get("kind") or state.get("kind") or launch.get("kind") or bundle.get("kind") or "").lower()
|
| 1376 |
+
|
| 1377 |
+
|
| 1378 |
+
def _run_is_validation_like(bundle_or_summary: dict[str, Any]) -> bool:
|
| 1379 |
+
kind = str(
|
| 1380 |
+
bundle_or_summary.get("kind")
|
| 1381 |
+
or bundle_or_summary.get("run_type")
|
| 1382 |
+
or (bundle_or_summary.get("summary") or {}).get("kind")
|
| 1383 |
+
or (bundle_or_summary.get("state") or {}).get("kind")
|
| 1384 |
+
or (bundle_or_summary.get("launch") or {}).get("kind")
|
| 1385 |
+
or ""
|
| 1386 |
+
).lower()
|
| 1387 |
+
run_id = str(bundle_or_summary.get("run_id") or (bundle_or_summary.get("summary") or {}).get("run_id") or "").lower()
|
| 1388 |
+
return "validation" in kind or "space_test" in kind or "validate" in kind or run_id.startswith("validate-")
|
| 1389 |
+
|
| 1390 |
+
|
| 1391 |
+
def _linked_validation_parent_id(run: dict[str, Any]) -> str:
|
| 1392 |
+
summary = run.get("summary") or {}
|
| 1393 |
+
state = run.get("state") or {}
|
| 1394 |
+
launch = run.get("launch") or {}
|
| 1395 |
+
return str(
|
| 1396 |
+
run.get("parent_build_run_id")
|
| 1397 |
+
or run.get("parentBuildRunId")
|
| 1398 |
+
or summary.get("parent_build_run_id")
|
| 1399 |
+
or state.get("parent_build_run_id")
|
| 1400 |
+
or launch.get("parent_build_run_id")
|
| 1401 |
+
or ""
|
| 1402 |
+
).strip()
|
| 1403 |
+
|
| 1404 |
+
|
| 1405 |
+
def _linked_validation_summaries(parent_run_id: str, *, bucket_source: str, token: str | None = None) -> list[dict[str, Any]]:
|
| 1406 |
+
if not parent_run_id:
|
| 1407 |
+
return []
|
| 1408 |
+
try:
|
| 1409 |
+
rows = list_recent_runs(bucket_source=bucket_source, token=token, limit=200)
|
| 1410 |
+
except Exception:
|
| 1411 |
+
return []
|
| 1412 |
+
linked: list[dict[str, Any]] = []
|
| 1413 |
+
for row in rows:
|
| 1414 |
+
if not isinstance(row, dict):
|
| 1415 |
+
continue
|
| 1416 |
+
if not _run_is_validation_like(row):
|
| 1417 |
+
continue
|
| 1418 |
+
if _linked_validation_parent_id(row) == parent_run_id:
|
| 1419 |
+
linked.append(row)
|
| 1420 |
+
linked.sort(key=lambda r: str(r.get("updated_at") or r.get("created_at") or r.get("run_id") or ""), reverse=True)
|
| 1421 |
+
return linked
|
| 1422 |
+
|
| 1423 |
+
|
| 1424 |
+
def _manual_validation_status_from_linked(parent_run_id: str, linked: list[dict[str, Any]]) -> dict[str, Any]:
|
| 1425 |
+
successes = [row for row in linked if str(row.get("status") or row.get("effective_status") or "").lower() in {"full_inference_success", "success", "succeeded", "passed", "validated_after_manual_space_test"}]
|
| 1426 |
+
failures = [row for row in linked if "failed" in str(row.get("status") or "").lower() or "error" in str(row.get("status") or "").lower()]
|
| 1427 |
+
source = successes[0] if successes else (failures[0] if failures else {})
|
| 1428 |
+
if not source:
|
| 1429 |
+
return {"status": "none", "parent_build_run_id": parent_run_id, "validation_run_id": "", "updated_at": utc_now_iso()}
|
| 1430 |
+
status = "success" if successes else "failed"
|
| 1431 |
+
payload: dict[str, Any] = {
|
| 1432 |
+
"status": status,
|
| 1433 |
+
"parent_build_run_id": parent_run_id,
|
| 1434 |
+
"validation_run_id": source.get("run_id") or "",
|
| 1435 |
+
"target_space": source.get("target_space") or source.get("target_space_id") or "",
|
| 1436 |
+
"api_name": source.get("api_name") or "",
|
| 1437 |
+
"updated_at": utc_now_iso(),
|
| 1438 |
+
}
|
| 1439 |
+
if status == "success":
|
| 1440 |
+
payload["effective_status"] = "validated_after_manual_space_test"
|
| 1441 |
+
for key in ["latency_seconds", "observed_latency_seconds", "recommended_zero_gpu_duration_seconds", "recommended_zerogpu_duration_seconds", "recommendation_source", "recommendation_hardware", "hardware_used_for_validation"]:
|
| 1442 |
+
if source.get(key) is not None:
|
| 1443 |
+
payload[key] = source.get(key)
|
| 1444 |
+
payload.setdefault("recommendation_source", "linked_space_test")
|
| 1445 |
+
return payload
|
| 1446 |
+
|
| 1447 |
+
|
| 1448 |
+
def _write_parent_linked_validation_state(parent_run_id: str, linked: list[dict[str, Any]], *, bucket_source: str, token: str | None = None) -> dict[str, Any]:
|
| 1449 |
+
paths = RunPaths(parent_run_id, bucket_source=bucket_source)
|
| 1450 |
+
compact = []
|
| 1451 |
+
for row in linked:
|
| 1452 |
+
compact.append({
|
| 1453 |
+
"validation_run_id": row.get("run_id") or "",
|
| 1454 |
+
"status": row.get("status") or "unknown",
|
| 1455 |
+
"target_space": row.get("target_space") or row.get("target_space_id") or "",
|
| 1456 |
+
"api_name": row.get("api_name") or "",
|
| 1457 |
+
"latency_seconds": row.get("latency_seconds") or row.get("observed_latency_seconds"),
|
| 1458 |
+
"observed_latency_seconds": row.get("observed_latency_seconds") or row.get("latency_seconds"),
|
| 1459 |
+
"recommended_zero_gpu_duration_seconds": row.get("recommended_zero_gpu_duration_seconds") or row.get("recommended_zerogpu_duration_seconds"),
|
| 1460 |
+
"recommendation_source": row.get("recommendation_source") or "",
|
| 1461 |
+
"recommendation_hardware": row.get("recommendation_hardware") or row.get("hardware_used_for_validation") or "",
|
| 1462 |
+
"hardware_used_for_validation": row.get("hardware_used_for_validation") or row.get("recommendation_hardware") or "",
|
| 1463 |
+
"updated_at": row.get("updated_at") or row.get("created_at") or "",
|
| 1464 |
+
})
|
| 1465 |
+
linked_payload = {
|
| 1466 |
+
"parent_build_run_id": parent_run_id,
|
| 1467 |
+
"validations": compact,
|
| 1468 |
+
"updated_at": utc_now_iso(),
|
| 1469 |
+
}
|
| 1470 |
+
manual_status = _manual_validation_status_from_linked(parent_run_id, linked)
|
| 1471 |
+
if manual_status.get("status") == "success":
|
| 1472 |
+
linked_payload["effective_status"] = "validated_after_manual_space_test"
|
| 1473 |
+
write_json(f"{paths.root}/linked_validations.json", linked_payload, token=token)
|
| 1474 |
+
write_json(f"{paths.root}/manual_validation_status.json", manual_status, token=token)
|
| 1475 |
+
return {"linked_validations": linked_payload, "manual_validation_status": manual_status}
|
| 1476 |
+
|
| 1477 |
def create_app() -> FastAPI:
|
| 1478 |
"""Create the product FastAPI app.
|
| 1479 |
|