Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Upload 6 files
Browse files- CHANGELOG.md +8 -16
- README.md +15 -14
- app.py +85 -7
CHANGELOG.md
CHANGED
|
@@ -1,18 +1,10 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
-
|
| 4 |
-
-
|
| 5 |
-
- Kept
|
| 6 |
-
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
Validation:
|
| 9 |
-
|
| 10 |
-
# v163 — Section Harmony Audit
|
| 11 |
-
|
| 12 |
-
- Reviewed the one-page UI section by section for state ownership, event-handler conflicts and cross-panel interference.
|
| 13 |
-
- Fixed duplicate DOM IDs in the support/compatibility area so hidden legacy elements no longer shadow visible Active Run hardware fields.
|
| 14 |
-
- Separated Space Test's visible “Started at” field from the Active Run polling compatibility field. Active Run progress no longer writes into the Space Test timestamp.
|
| 15 |
-
- Hardened Run traces document state so backend-provided `present: true` document links are not downgraded when a light payload lacks a `files` listing.
|
| 16 |
-
- Added regression tests covering unique DOM IDs, Active Run vs Space Test timestamp separation, hidden-field compatibility boundaries, and stable Run traces document presence.
|
| 17 |
-
|
| 18 |
-
Validation: 448 tests pass.
|
|
|
|
| 1 |
+
# v171 — delete Space option, ghost-run cleanup, storage polish
|
| 2 |
|
| 3 |
+
- Added an opt-in checkbox in the premium delete modal to also delete the associated Hugging Face Space for build runs.
|
| 4 |
+
- Extended the DELETE run API to accept `{ "delete_space": true }`, delete the Space repo in the authenticated user's namespace, and return an explicit Space deletion report.
|
| 5 |
+
- Kept Space deletion disabled for validation runs to avoid deleting existing Spaces that were only tested.
|
| 6 |
+
- Made incomplete/unknown ghost runs deletable from Run Explorer so stale bucket prefixes can be cleaned up.
|
| 7 |
+
- Polished the Run Storage card error state with compact, user-readable messages instead of dumping long bucket HTTP errors into the card.
|
| 8 |
+
- Preserved the v170 real recursive bucket deletion behavior.
|
| 9 |
|
| 10 |
+
Validation: `478 passed, 2 warnings`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,21 +1,14 @@
|
|
| 1 |
-
|
| 2 |
-
title: Agentic Space Factory
|
| 3 |
-
sdk: docker
|
| 4 |
-
hf_oauth: true
|
| 5 |
-
hf_oauth_expiration_minutes: 480
|
| 6 |
-
hf_oauth_scopes:
|
| 7 |
-
- read-repos
|
| 8 |
-
- write-repos
|
| 9 |
-
- manage-repos
|
| 10 |
-
- gated-repos
|
| 11 |
-
- inference-api
|
| 12 |
-
- jobs
|
| 13 |
-
- read-billing
|
| 14 |
-
---
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# Agentic Space Factory
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
- **v161** fixes duplicate delete modal triggers and stuck `Deleting…` states with single-flight delete guards.
|
| 20 |
|
| 21 |
---
|
|
@@ -261,3 +254,11 @@ Run traces now renders as a horizontal document dock: compact source-file icons
|
|
| 261 |
## v164 timeline
|
| 262 |
|
| 263 |
The Active Run timeline uses compact product milestones backed by exact worker steps. Grouped milestones expose clockwise partial progress rings and substep counts, while recovery details remain in Agent recovery and source files remain in Run traces.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## v170 — Real bucket recursive run deletion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
- DELETE `/api/runs/{run_id}` now removes concrete bucket objects under `runs/<run_id>/` using recursive discovery instead of relying only on virtual-folder deletion.
|
| 4 |
+
- The API returns a cleanup report with deleted and remaining object counts.
|
| 5 |
+
- Frontend cache invalidation remains as a protection layer, but deletion is now a real backend bucket cleanup.
|
| 6 |
|
| 7 |
# Agentic Space Factory
|
| 8 |
|
| 9 |
+
- **v170** fixes run deletion so the backend recursively removes all concrete Bucket objects for the run prefix.
|
| 10 |
+
- **v168** refreshes Pi assistant model choices and surfaces requested/effective provider model routing in Active Run.
|
| 11 |
+
- **v165** fixes fresh-install recovery failures, README metadata preservation, timeline failure anchoring, premium pre-scan/progress polish, and broader Pi assistant model choices.
|
| 12 |
- **v161** fixes duplicate delete modal triggers and stuck `Deleting…` states with single-flight delete guards.
|
| 13 |
|
| 14 |
---
|
|
|
|
| 254 |
## v164 timeline
|
| 255 |
|
| 256 |
The Active Run timeline uses compact product milestones backed by exact worker steps. Grouped milestones expose clockwise partial progress rings and substep counts, while recovery details remain in Agent recovery and source files remain in Run traces.
|
| 257 |
+
|
| 258 |
+
### v167 note: run artifact manifest
|
| 259 |
+
|
| 260 |
+
The worker writes `runs/<run_id>/artifact_manifest.json` as the source of truth for run artifacts. This lets the API expose Run traces from what the worker actually wrote, including failed runs where the Space was created but later entered build/runtime error.
|
| 261 |
+
|
| 262 |
+
## v171 notes
|
| 263 |
+
|
| 264 |
+
Run deletion now supports an explicit opt-in option to delete the associated generated Space for build runs. Validation run deletion remains run-artifact-only by default. The Run Storage card also summarizes bucket setup errors so fresh installs remain visually clean.
|
app.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Any
|
|
| 8 |
import gradio as gr
|
| 9 |
from fastapi import FastAPI, HTTPException, Request
|
| 10 |
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
| 11 |
-
from huggingface_hub import 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
|
|
@@ -562,13 +562,33 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
|
|
| 562 |
ctx = _oauth_context_from_request(request)
|
| 563 |
run_id = validate_run_id(run_id)
|
| 564 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
|
|
|
| 565 |
try:
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
except FileNotFoundError:
|
| 568 |
-
|
| 569 |
except Exception as exc: # noqa: BLE001
|
| 570 |
raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
|
| 571 |
-
return JSONResponse({"ok": True, "run_id": run_id, "deleted": True})
|
| 572 |
|
| 573 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 574 |
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]
|
|
@@ -905,7 +925,7 @@ def build_demo() -> gr.Blocks:
|
|
| 905 |
with gr.Tab("Build from model card"):
|
| 906 |
gr.Markdown(
|
| 907 |
"""
|
| 908 |
-
Paste a Hugging Face model ID or model-card URL. The worker creates a **private** Space, asks Pi +
|
| 909 |
"""
|
| 910 |
)
|
| 911 |
with gr.Row():
|
|
@@ -921,10 +941,19 @@ Paste a Hugging Face model ID or model-card URL. The worker creates a **private*
|
|
| 921 |
placeholder="e.g. space-factory-z-image-v1",
|
| 922 |
info="Use a fresh name. The Space is created under your username and remains private.",
|
| 923 |
)
|
| 924 |
-
pi_model = gr.
|
| 925 |
label="Pi model",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
value="Qwen/Qwen3-Coder-Next",
|
| 927 |
-
|
|
|
|
| 928 |
)
|
| 929 |
implementation_mode = gr.Dropdown(
|
| 930 |
label="Build goal",
|
|
@@ -1074,6 +1103,55 @@ It cannot guarantee that every model card becomes a working Space. It cannot byp
|
|
| 1074 |
return demo
|
| 1075 |
|
| 1076 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
def create_app() -> FastAPI:
|
| 1078 |
"""Create the product FastAPI app.
|
| 1079 |
|
|
|
|
| 8 |
import gradio as gr
|
| 9 |
from fastapi import FastAPI, HTTPException, Request
|
| 10 |
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
| 11 |
+
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
|
|
|
|
| 562 |
ctx = _oauth_context_from_request(request)
|
| 563 |
run_id = validate_run_id(run_id)
|
| 564 |
bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
|
| 565 |
+
body: dict[str, Any] = {}
|
| 566 |
try:
|
| 567 |
+
body = await request.json()
|
| 568 |
+
if not isinstance(body, dict):
|
| 569 |
+
body = {}
|
| 570 |
+
except Exception:
|
| 571 |
+
body = {}
|
| 572 |
+
delete_space = bool(body.get("delete_space"))
|
| 573 |
+
bundle: dict[str, Any] = {}
|
| 574 |
+
space_report: dict[str, Any] = {"space_delete_requested": delete_space, "space_deleted": False}
|
| 575 |
+
if delete_space:
|
| 576 |
+
try:
|
| 577 |
+
bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
|
| 578 |
+
associated_space = _associated_space_from_bundle(bundle)
|
| 579 |
+
kind = str((bundle.get("summary") or {}).get("kind") or (bundle.get("state") or {}).get("kind") or (bundle.get("launch") or {}).get("kind") or "").lower()
|
| 580 |
+
if "validation" in kind or "space_test" in kind:
|
| 581 |
+
raise PermissionError("Associated Space deletion is not available for validation runs.")
|
| 582 |
+
space_report = _delete_associated_space(associated_space, username=ctx["username"], token=ctx["token"])
|
| 583 |
+
except Exception as exc: # noqa: BLE001
|
| 584 |
+
space_report = {"space_delete_requested": True, "space_deleted": False, "space_delete_error": redact(str(exc))}
|
| 585 |
+
try:
|
| 586 |
+
delete_report = delete_run_folder(run_id, bucket_source=bucket_source, token=ctx["token"])
|
| 587 |
except FileNotFoundError:
|
| 588 |
+
delete_report = {"matched_count": 0, "deleted_count": 0, "remaining_count": 0}
|
| 589 |
except Exception as exc: # noqa: BLE001
|
| 590 |
raise HTTPException(status_code=400, detail=redact(str(exc))) from exc
|
| 591 |
+
return JSONResponse({"ok": True, "run_id": run_id, "deleted": True, **delete_report, **space_report})
|
| 592 |
|
| 593 |
@fastapi_app.get("/api/runs/{run_id}/progress")
|
| 594 |
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]
|
|
|
|
| 925 |
with gr.Tab("Build from model card"):
|
| 926 |
gr.Markdown(
|
| 927 |
"""
|
| 928 |
+
Paste a Hugging Face model ID or model-card URL. The worker creates a **private** Space, asks Pi + the selected coding assistant to build the best Gradio app it can, attempts ZeroGPU first, then a fixed-GPU fallback if enabled. If automatic hardware assignment fails, set the hardware manually in the generated Space settings and run the validation tab.
|
| 929 |
"""
|
| 930 |
)
|
| 931 |
with gr.Row():
|
|
|
|
| 941 |
placeholder="e.g. space-factory-z-image-v1",
|
| 942 |
info="Use a fresh name. The Space is created under your username and remains private.",
|
| 943 |
)
|
| 944 |
+
pi_model = gr.Dropdown(
|
| 945 |
label="Pi model",
|
| 946 |
+
choices=[
|
| 947 |
+
"Qwen/Qwen3-Coder-Next",
|
| 948 |
+
"moonshotai/Kimi-K2-Instruct-0905",
|
| 949 |
+
"zai-org/GLM-4.7",
|
| 950 |
+
"zai-org/GLM-4.5-Air",
|
| 951 |
+
"deepseek-ai/DeepSeek-V3.2",
|
| 952 |
+
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
| 953 |
+
],
|
| 954 |
value="Qwen/Qwen3-Coder-Next",
|
| 955 |
+
allow_custom_value=True,
|
| 956 |
+
info="Assistant model used by Pi through Hugging Face Inference Providers.",
|
| 957 |
)
|
| 958 |
implementation_mode = gr.Dropdown(
|
| 959 |
label="Build goal",
|
|
|
|
| 1103 |
return demo
|
| 1104 |
|
| 1105 |
|
| 1106 |
+
|
| 1107 |
+
def _first_non_empty(*values: Any) -> str:
|
| 1108 |
+
for value in values:
|
| 1109 |
+
text = str(value or "").strip()
|
| 1110 |
+
if text:
|
| 1111 |
+
return text
|
| 1112 |
+
return ""
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def _associated_space_from_bundle(bundle: dict[str, Any]) -> str:
|
| 1116 |
+
summary = bundle.get("summary") or {}
|
| 1117 |
+
summary_file = bundle.get("summary_file") or {}
|
| 1118 |
+
launch = bundle.get("launch") or {}
|
| 1119 |
+
state = bundle.get("state") or {}
|
| 1120 |
+
links = bundle.get("links") or {}
|
| 1121 |
+
target = _first_non_empty(
|
| 1122 |
+
summary.get("target_space"),
|
| 1123 |
+
summary_file.get("target_space"),
|
| 1124 |
+
launch.get("target_space"),
|
| 1125 |
+
state.get("target_space"),
|
| 1126 |
+
links.get("target_space"),
|
| 1127 |
+
)
|
| 1128 |
+
if target:
|
| 1129 |
+
return target.replace("https://huggingface.co/spaces/", "").strip("/")
|
| 1130 |
+
url = _first_non_empty(summary.get("target_space_url"), summary_file.get("target_space_url"), launch.get("target_space_url"), state.get("target_space_url"), links.get("target_space_url"))
|
| 1131 |
+
marker = "huggingface.co/spaces/"
|
| 1132 |
+
if marker in url:
|
| 1133 |
+
return url.split(marker, 1)[1].split("?", 1)[0].split("#", 1)[0].strip("/")
|
| 1134 |
+
return ""
|
| 1135 |
+
|
| 1136 |
+
|
| 1137 |
+
def _delete_associated_space(space_id: str, *, username: str, token: str) -> dict[str, Any]:
|
| 1138 |
+
cleaned = str(space_id or "").strip().strip("/")
|
| 1139 |
+
if not cleaned or "/" not in cleaned:
|
| 1140 |
+
raise ValueError("No associated Space id is available for this run.")
|
| 1141 |
+
owner = cleaned.split("/", 1)[0]
|
| 1142 |
+
if owner != username:
|
| 1143 |
+
raise PermissionError(f"Refusing to delete Space {cleaned}: it is not in your namespace.")
|
| 1144 |
+
api = HfApi(token=token)
|
| 1145 |
+
try:
|
| 1146 |
+
api.delete_repo(repo_id=cleaned, repo_type="space")
|
| 1147 |
+
return {"space_delete_requested": True, "space_deleted": True, "space_id": cleaned}
|
| 1148 |
+
except Exception as exc: # noqa: BLE001
|
| 1149 |
+
message = redact(str(exc))
|
| 1150 |
+
missing_markers = ("404", "not found", "Repository Not Found", "does not exist")
|
| 1151 |
+
if any(marker.lower() in message.lower() for marker in missing_markers):
|
| 1152 |
+
return {"space_delete_requested": True, "space_deleted": False, "space_already_missing": True, "space_id": cleaned, "space_delete_error": message}
|
| 1153 |
+
return {"space_delete_requested": True, "space_deleted": False, "space_id": cleaned, "space_delete_error": message}
|
| 1154 |
+
|
| 1155 |
def create_app() -> FastAPI:
|
| 1156 |
"""Create the product FastAPI app.
|
| 1157 |
|