Upload 14 files
Browse files- src/bucket.py +16 -2
- src/timeline_model.py +91 -12
- src/version.py +2 -2
- src/worker_payload.py +107 -8
src/bucket.py
CHANGED
|
@@ -678,6 +678,7 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
|
|
| 678 |
eval_publish_payload = bundle.get("eval_publish") if isinstance(bundle.get("eval_publish"), dict) else bundle.get("eval_publish_status") if isinstance(bundle.get("eval_publish_status"), dict) else {}
|
| 679 |
eval_publish_present = bool(eval_publish_payload) or manifest_has("eval_publish_status.json") or "eval_publish_status.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/eval_publish_status.json", token=token))
|
| 680 |
space_logs_index_present = manifest_has("logs/space_logs_index.json") or "logs/space_logs_index.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/logs/space_logs_index.json", token=token))
|
|
|
|
| 681 |
repair_decision_present = manifest_has("repair/REPAIR_DECISION.json") or "repair/REPAIR_DECISION.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/repair/REPAIR_DECISION.json", token=token))
|
| 682 |
blockage_present = manifest_has("repair/BLOCKAGE.json") or "repair/BLOCKAGE.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/repair/BLOCKAGE.json", token=token))
|
| 683 |
repair_present = manifest_has("repair") or any(path.startswith("repair/") for path in file_paths) or (bool(token) and _path_exists(f"{paths.root}/repair/REPAIR_SUMMARY.md", token=token))
|
|
@@ -726,7 +727,7 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
|
|
| 726 |
"icon": "◎",
|
| 727 |
"present": eval_publish_present,
|
| 728 |
"url": _bucket_file_url(bucket_source, run_id, "eval_publish_status.json"),
|
| 729 |
-
"tone": "
|
| 730 |
},
|
| 731 |
{
|
| 732 |
"id": "space_logs",
|
|
@@ -735,7 +736,16 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
|
|
| 735 |
"icon": "▣",
|
| 736 |
"present": space_logs_index_present,
|
| 737 |
"url": _bucket_file_url(bucket_source, run_id, "logs/space_logs_index.json"),
|
| 738 |
-
"tone": "neutral",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
},
|
| 740 |
]
|
| 741 |
if repair_decision_present:
|
|
@@ -786,6 +796,9 @@ def _run_document_links(run_id: str, *, bucket_source: str, bundle: dict[str, An
|
|
| 786 |
"tone": "warn",
|
| 787 |
}
|
| 788 |
)
|
|
|
|
|
|
|
|
|
|
| 789 |
return docs
|
| 790 |
|
| 791 |
|
|
@@ -938,6 +951,7 @@ def read_run_bundle(run_id: str, *, bucket_source: str, token: str | None = None
|
|
| 938 |
"space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
|
| 939 |
"api_schema": _safe_read_json(f"{paths.root}/tests/api_schema.json", token=token) or _safe_read_json(f"{paths.root}/tests/generation_api_schema.json", token=token),
|
| 940 |
"eval_publish_status": _safe_read_json(f"{paths.root}/eval_publish_status.json", token=token),
|
|
|
|
| 941 |
"repair_decision": _safe_read_json(f"{paths.root}/repair/REPAIR_DECISION.json", token=token),
|
| 942 |
"blockage": _safe_read_json(f"{paths.root}/repair/BLOCKAGE.json", token=token),
|
| 943 |
"artifact_manifest": _safe_read_json(f"{paths.root}/artifact_manifest.json", token=token),
|
|
|
|
| 678 |
eval_publish_payload = bundle.get("eval_publish") if isinstance(bundle.get("eval_publish"), dict) else bundle.get("eval_publish_status") if isinstance(bundle.get("eval_publish_status"), dict) else {}
|
| 679 |
eval_publish_present = bool(eval_publish_payload) or manifest_has("eval_publish_status.json") or "eval_publish_status.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/eval_publish_status.json", token=token))
|
| 680 |
space_logs_index_present = manifest_has("logs/space_logs_index.json") or "logs/space_logs_index.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/logs/space_logs_index.json", token=token))
|
| 681 |
+
build_error_present = manifest_has("build_error_observation.json") or "build_error_observation.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/build_error_observation.json", token=token))
|
| 682 |
repair_decision_present = manifest_has("repair/REPAIR_DECISION.json") or "repair/REPAIR_DECISION.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/repair/REPAIR_DECISION.json", token=token))
|
| 683 |
blockage_present = manifest_has("repair/BLOCKAGE.json") or "repair/BLOCKAGE.json" in file_paths or (bool(token) and _path_exists(f"{paths.root}/repair/BLOCKAGE.json", token=token))
|
| 684 |
repair_present = manifest_has("repair") or any(path.startswith("repair/") for path in file_paths) or (bool(token) and _path_exists(f"{paths.root}/repair/REPAIR_SUMMARY.md", token=token))
|
|
|
|
| 727 |
"icon": "◎",
|
| 728 |
"present": eval_publish_present,
|
| 729 |
"url": _bucket_file_url(bucket_source, run_id, "eval_publish_status.json"),
|
| 730 |
+
"tone": "success" if eval_publish_payload.get("published") else "pending" if eval_publish_present and str(eval_publish_payload.get("reason") or "") == "record_not_ready" else "warn" if eval_publish_present else "neutral",
|
| 731 |
},
|
| 732 |
{
|
| 733 |
"id": "space_logs",
|
|
|
|
| 736 |
"icon": "▣",
|
| 737 |
"present": space_logs_index_present,
|
| 738 |
"url": _bucket_file_url(bucket_source, run_id, "logs/space_logs_index.json"),
|
| 739 |
+
"tone": "warn" if "failed" in status or "error" in status else "neutral",
|
| 740 |
+
},
|
| 741 |
+
{
|
| 742 |
+
"id": "build_error",
|
| 743 |
+
"label": "Build error",
|
| 744 |
+
"subtitle": "Observed build failure",
|
| 745 |
+
"icon": "⛔",
|
| 746 |
+
"present": build_error_present,
|
| 747 |
+
"url": _bucket_file_url(bucket_source, run_id, "build_error_observation.json"),
|
| 748 |
+
"tone": "warn",
|
| 749 |
},
|
| 750 |
]
|
| 751 |
if repair_decision_present:
|
|
|
|
| 796 |
"tone": "warn",
|
| 797 |
}
|
| 798 |
)
|
| 799 |
+
if "failed" in status or "error" in status:
|
| 800 |
+
priority = {"report": 0, "blockage": 1, "build_error": 2, "space_logs": 3, "repair_decision": 4, "repair": 5, "blockers": 6, "pi_raw_trace": 7, "pi_redacted_trace": 8, "smoke": 9, "eval_publish": 10}
|
| 801 |
+
docs.sort(key=lambda item: priority.get(str(item.get("id") or ""), 50))
|
| 802 |
return docs
|
| 803 |
|
| 804 |
|
|
|
|
| 951 |
"space_runtime": _safe_read_json(f"{paths.root}/space_runtime.json", token=token),
|
| 952 |
"api_schema": _safe_read_json(f"{paths.root}/tests/api_schema.json", token=token) or _safe_read_json(f"{paths.root}/tests/generation_api_schema.json", token=token),
|
| 953 |
"eval_publish_status": _safe_read_json(f"{paths.root}/eval_publish_status.json", token=token),
|
| 954 |
+
"build_error_observation": _safe_read_json(f"{paths.root}/build_error_observation.json", token=token),
|
| 955 |
"repair_decision": _safe_read_json(f"{paths.root}/repair/REPAIR_DECISION.json", token=token),
|
| 956 |
"blockage": _safe_read_json(f"{paths.root}/repair/BLOCKAGE.json", token=token),
|
| 957 |
"artifact_manifest": _safe_read_json(f"{paths.root}/artifact_manifest.json", token=token),
|
src/timeline_model.py
CHANGED
|
@@ -144,6 +144,44 @@ def _eval_publish(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
| 144 |
return status if isinstance(status, dict) else {}
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def _phase_status_from_events(bundle: dict[str, Any], phase: str) -> str:
|
| 148 |
latest_by_step = _latest_events_by_step(bundle, phase)
|
| 149 |
if not latest_by_step:
|
|
@@ -250,15 +288,19 @@ def _logs_warning(bundle: dict[str, Any]) -> dict[str, Any] | None:
|
|
| 250 |
return None
|
| 251 |
|
| 252 |
|
| 253 |
-
def _archive_warning(bundle: dict[str, Any]) -> dict[str, Any] | None:
|
| 254 |
status = _eval_publish(bundle)
|
| 255 |
-
if not status:
|
| 256 |
return None
|
| 257 |
-
|
|
|
|
|
|
|
| 258 |
return None
|
| 259 |
if status.get("attempted"):
|
| 260 |
return {"code": "eval_archive_not_published", "label": "Eval archive not published", "detail": str(status.get("reason") or "Backend archive copy did not complete.")}
|
| 261 |
-
|
|
|
|
|
|
|
| 262 |
|
| 263 |
|
| 264 |
def _build_phase_details(bundle: dict[str, Any], phase: str) -> list[dict[str, Any]]:
|
|
@@ -277,6 +319,9 @@ def _build_phase_details(bundle: dict[str, Any], phase: str) -> list[dict[str, A
|
|
| 277 |
elif smoke:
|
| 278 |
reason = smoke.get("failure_type") or smoke.get("error") or "Generation was not verified"
|
| 279 |
details.append({"label": f"Generation not verified: {reason}", "status": "warning"})
|
|
|
|
|
|
|
|
|
|
| 280 |
recommendation = smoke.get("recommended_zero_gpu_duration_seconds") or (_gate(bundle).get("zero_gpu_duration_recommendation") or {}).get("recommended_zero_gpu_duration_seconds")
|
| 281 |
if recommendation:
|
| 282 |
details.append({"label": f"ZeroGPU duration recommendation: {recommendation}s", "status": "complete"})
|
|
@@ -288,7 +333,10 @@ def _build_phase_details(bundle: dict[str, Any], phase: str) -> list[dict[str, A
|
|
| 288 |
if eval_status.get("archive_relative_path"):
|
| 289 |
details.append({"label": str(eval_status.get("archive_relative_path")), "status": "info"})
|
| 290 |
elif eval_status.get("attempted"):
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
| 292 |
else:
|
| 293 |
details.append({"label": "Eval archive pending", "status": "pending"})
|
| 294 |
elif phase == "recovery":
|
|
@@ -296,14 +344,22 @@ def _build_phase_details(bundle: dict[str, Any], phase: str) -> list[dict[str, A
|
|
| 296 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 297 |
details.append({"label": "Recovery not needed", "status": "skipped"})
|
| 298 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
for label, candidates in [
|
| 300 |
("Diagnose", {"failure_diagnosis", "pi_diagnosis"}),
|
| 301 |
("Decide", {"repair_decision"}),
|
| 302 |
-
("
|
| 303 |
("Revalidate", {"repair_validation"}),
|
| 304 |
]:
|
| 305 |
-
status = "complete" if steps.intersection(candidates) else "pending"
|
| 306 |
details.append({"label": label, "status": status})
|
|
|
|
|
|
|
|
|
|
| 307 |
else:
|
| 308 |
latest = _latest_event_for_phase(bundle, phase)
|
| 309 |
if latest:
|
|
@@ -321,6 +377,9 @@ def _phase_summary(bundle: dict[str, Any], phase: str, status: str) -> str:
|
|
| 321 |
if warning:
|
| 322 |
return warning["label"]
|
| 323 |
if phase == "live_validation":
|
|
|
|
|
|
|
|
|
|
| 324 |
signals = _signals(bundle)
|
| 325 |
smoke = _smoke(bundle)
|
| 326 |
if signals.get("generation_smoke_passed") is True or _lower(smoke.get("status")) == "success":
|
|
@@ -333,16 +392,24 @@ def _phase_summary(bundle: dict[str, Any], phase: str, status: str) -> str:
|
|
| 333 |
steps = _event_steps(bundle)
|
| 334 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 335 |
return "Not needed"
|
|
|
|
|
|
|
| 336 |
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in SUCCESS_STATUSES for e in _events(bundle)):
|
| 337 |
return "Repair revalidated"
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
if phase == "archive":
|
| 340 |
eval_status = _eval_publish(bundle)
|
| 341 |
if eval_status.get("published") is True:
|
| 342 |
return "Eval archive published"
|
| 343 |
-
if eval_status.get("attempted"):
|
| 344 |
return "Eval archive attempted"
|
| 345 |
-
return "
|
| 346 |
if phase == "done":
|
| 347 |
label, _ = _status_label(_verdict(bundle))
|
| 348 |
return label
|
|
@@ -372,10 +439,20 @@ def _phase_status(bundle: dict[str, Any], phase: str, verdict: str) -> str:
|
|
| 372 |
if phase == "recovery":
|
| 373 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 374 |
return "skipped" if terminal else "pending"
|
|
|
|
|
|
|
| 375 |
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in SUCCESS_STATUSES for e in _events(bundle)):
|
| 376 |
return "complete"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
if phase == "live_validation":
|
|
|
|
|
|
|
|
|
|
| 379 |
signals = _signals(bundle)
|
| 380 |
smoke = _smoke(bundle)
|
| 381 |
if signals.get("generation_smoke_passed") is True or _lower(smoke.get("status")) == "success":
|
|
@@ -389,10 +466,12 @@ def _phase_status(bundle: dict[str, Any], phase: str, verdict: str) -> str:
|
|
| 389 |
if eval_status.get("published") is True:
|
| 390 |
return "complete"
|
| 391 |
if eval_status.get("attempted"):
|
|
|
|
|
|
|
| 392 |
return "warning"
|
| 393 |
if terminal:
|
| 394 |
return "warning" if bundle.get("eval_record") else "pending"
|
| 395 |
-
return
|
| 396 |
|
| 397 |
if phase == "agent" and _pi_model_warning(bundle):
|
| 398 |
return "warning"
|
|
@@ -429,7 +508,7 @@ def build_run_timeline_model(bundle: dict[str, Any]) -> dict[str, Any]:
|
|
| 429 |
terminal = visual_status in {"success", "warn", "error", "stopped"}
|
| 430 |
percent = 100 if terminal else int(progress.get("progress") or 0)
|
| 431 |
|
| 432 |
-
warnings = [w for w in (_pi_model_warning(bundle), _hardware_warning(bundle), _logs_warning(bundle), _archive_warning(bundle)) if w]
|
| 433 |
phases: list[dict[str, Any]] = []
|
| 434 |
for phase in PHASE_ORDER:
|
| 435 |
status = _phase_status(bundle, phase, verdict)
|
|
|
|
| 144 |
return status if isinstance(status, dict) else {}
|
| 145 |
|
| 146 |
|
| 147 |
+
def _build_error_observation(bundle: dict[str, Any]) -> dict[str, Any]:
|
| 148 |
+
observation = bundle.get("build_error_observation") or {}
|
| 149 |
+
return observation if isinstance(observation, dict) else {}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _final_blocker_label(bundle: dict[str, Any]) -> str:
|
| 153 |
+
observation = _build_error_observation(bundle)
|
| 154 |
+
reason = _lower(observation.get("reason"))
|
| 155 |
+
tail = str(observation.get("tail") or observation.get("first_error") or "")
|
| 156 |
+
if reason or "pyenv install 3.1" in tail or "BUILD FAILED" in tail:
|
| 157 |
+
if "pyenv install 3.1" in tail or "python3.1" in tail.replace(" ", ""):
|
| 158 |
+
return "Build failed: invalid Python version requested pyenv install 3.1"
|
| 159 |
+
return "Build failed after repair"
|
| 160 |
+
blockage = bundle.get("blockage") or {}
|
| 161 |
+
if isinstance(blockage, dict):
|
| 162 |
+
status = str(blockage.get("status") or "").replace("_", " ").strip()
|
| 163 |
+
decision = blockage.get("decision") if isinstance(blockage.get("decision"), dict) else {}
|
| 164 |
+
reason_text = str(decision.get("reason") or blockage.get("reason") or "").strip()
|
| 165 |
+
if reason_text:
|
| 166 |
+
return reason_text[:220]
|
| 167 |
+
if status:
|
| 168 |
+
return status.title()
|
| 169 |
+
return ""
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _is_record_not_ready(status: dict[str, Any]) -> bool:
|
| 173 |
+
return _lower(status.get("reason")) == "record_not_ready"
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _has_terminal_event(bundle: dict[str, Any]) -> bool:
|
| 177 |
+
return any(_lower(event.get("step")) in {"done", "failure", "technical_blocker", "manual_hardware_required"} for event in _events(bundle))
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _latest_recovery_event(bundle: dict[str, Any]) -> dict[str, Any] | None:
|
| 181 |
+
events = _events_for_phase(bundle, "recovery")
|
| 182 |
+
return events[-1] if events else None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
def _phase_status_from_events(bundle: dict[str, Any], phase: str) -> str:
|
| 186 |
latest_by_step = _latest_events_by_step(bundle, phase)
|
| 187 |
if not latest_by_step:
|
|
|
|
| 288 |
return None
|
| 289 |
|
| 290 |
|
| 291 |
+
def _archive_warning(bundle: dict[str, Any], *, terminal: bool) -> dict[str, Any] | None:
|
| 292 |
status = _eval_publish(bundle)
|
| 293 |
+
if not status or status.get("published") is True:
|
| 294 |
return None
|
| 295 |
+
# record_not_ready is the expected state while a run is still executing.
|
| 296 |
+
# It should not become a global warning until the run has actually ended.
|
| 297 |
+
if not terminal and _is_record_not_ready(status):
|
| 298 |
return None
|
| 299 |
if status.get("attempted"):
|
| 300 |
return {"code": "eval_archive_not_published", "label": "Eval archive not published", "detail": str(status.get("reason") or "Backend archive copy did not complete.")}
|
| 301 |
+
if terminal:
|
| 302 |
+
return {"code": "eval_archive_pending", "label": "Eval archive pending", "detail": "Backend archive copy has not been confirmed yet."}
|
| 303 |
+
return None
|
| 304 |
|
| 305 |
|
| 306 |
def _build_phase_details(bundle: dict[str, Any], phase: str) -> list[dict[str, Any]]:
|
|
|
|
| 319 |
elif smoke:
|
| 320 |
reason = smoke.get("failure_type") or smoke.get("error") or "Generation was not verified"
|
| 321 |
details.append({"label": f"Generation not verified: {reason}", "status": "warning"})
|
| 322 |
+
blocker = _final_blocker_label(bundle)
|
| 323 |
+
if blocker:
|
| 324 |
+
details.append({"label": blocker, "status": "failed"})
|
| 325 |
recommendation = smoke.get("recommended_zero_gpu_duration_seconds") or (_gate(bundle).get("zero_gpu_duration_recommendation") or {}).get("recommended_zero_gpu_duration_seconds")
|
| 326 |
if recommendation:
|
| 327 |
details.append({"label": f"ZeroGPU duration recommendation: {recommendation}s", "status": "complete"})
|
|
|
|
| 333 |
if eval_status.get("archive_relative_path"):
|
| 334 |
details.append({"label": str(eval_status.get("archive_relative_path")), "status": "info"})
|
| 335 |
elif eval_status.get("attempted"):
|
| 336 |
+
if _is_record_not_ready(eval_status):
|
| 337 |
+
details.append({"label": "Eval archive pending until the run completes", "status": "pending"})
|
| 338 |
+
else:
|
| 339 |
+
details.append({"label": f"Eval archive not published: {eval_status.get('reason') or 'unknown'}", "status": "warning"})
|
| 340 |
else:
|
| 341 |
details.append({"label": "Eval archive pending", "status": "pending"})
|
| 342 |
elif phase == "recovery":
|
|
|
|
| 344 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 345 |
details.append({"label": "Recovery not needed", "status": "skipped"})
|
| 346 |
else:
|
| 347 |
+
failed_steps = {
|
| 348 |
+
_lower(e.get("step"))
|
| 349 |
+
for e in _events(bundle)
|
| 350 |
+
if _lower(e.get("step")) in PHASE_STEPS["recovery"] and _lower(e.get("status")) in FAILED_STATUSES
|
| 351 |
+
}
|
| 352 |
for label, candidates in [
|
| 353 |
("Diagnose", {"failure_diagnosis", "pi_diagnosis"}),
|
| 354 |
("Decide", {"repair_decision"}),
|
| 355 |
+
("Patch", {"repair", "repair_patch", "repair_upload"}),
|
| 356 |
("Revalidate", {"repair_validation"}),
|
| 357 |
]:
|
| 358 |
+
status = "failed" if failed_steps.intersection(candidates) else "complete" if steps.intersection(candidates) else "pending"
|
| 359 |
details.append({"label": label, "status": status})
|
| 360 |
+
blocker = _final_blocker_label(bundle)
|
| 361 |
+
if blocker:
|
| 362 |
+
details.append({"label": blocker, "status": "failed"})
|
| 363 |
else:
|
| 364 |
latest = _latest_event_for_phase(bundle, phase)
|
| 365 |
if latest:
|
|
|
|
| 377 |
if warning:
|
| 378 |
return warning["label"]
|
| 379 |
if phase == "live_validation":
|
| 380 |
+
blocker = _final_blocker_label(bundle)
|
| 381 |
+
if blocker and _final_visual_status(_verdict(bundle)) == "error":
|
| 382 |
+
return "Live validation incomplete"
|
| 383 |
signals = _signals(bundle)
|
| 384 |
smoke = _smoke(bundle)
|
| 385 |
if signals.get("generation_smoke_passed") is True or _lower(smoke.get("status")) == "success":
|
|
|
|
| 392 |
steps = _event_steps(bundle)
|
| 393 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 394 |
return "Not needed"
|
| 395 |
+
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in FAILED_STATUSES for e in _events(bundle)):
|
| 396 |
+
return "Patch attempted; final blocker remains"
|
| 397 |
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in SUCCESS_STATUSES for e in _events(bundle)):
|
| 398 |
return "Repair revalidated"
|
| 399 |
+
latest = _latest_recovery_event(bundle) or {}
|
| 400 |
+
latest_step = _lower(latest.get("step"))
|
| 401 |
+
if latest_step in {"repair_validation"}:
|
| 402 |
+
return "Revalidating repair"
|
| 403 |
+
if latest_step in {"repair", "repair_plan", "repair_patch", "repair_upload"}:
|
| 404 |
+
return "Repairing"
|
| 405 |
+
return "Diagnosing repair"
|
| 406 |
if phase == "archive":
|
| 407 |
eval_status = _eval_publish(bundle)
|
| 408 |
if eval_status.get("published") is True:
|
| 409 |
return "Eval archive published"
|
| 410 |
+
if eval_status.get("attempted") and not _is_record_not_ready(eval_status):
|
| 411 |
return "Eval archive attempted"
|
| 412 |
+
return "Archive pending" if status in {"pending", "running"} else "Final artifacts written"
|
| 413 |
if phase == "done":
|
| 414 |
label, _ = _status_label(_verdict(bundle))
|
| 415 |
return label
|
|
|
|
| 439 |
if phase == "recovery":
|
| 440 |
if not steps.intersection(PHASE_STEPS["recovery"]):
|
| 441 |
return "skipped" if terminal else "pending"
|
| 442 |
+
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in FAILED_STATUSES for e in _events(bundle)):
|
| 443 |
+
return "failed"
|
| 444 |
if any(_lower(e.get("step")) == "repair_validation" and _lower(e.get("status")) in SUCCESS_STATUSES for e in _events(bundle)):
|
| 445 |
return "complete"
|
| 446 |
+
latest_recovery = _latest_recovery_event(bundle) or {}
|
| 447 |
+
latest_status = _lower(latest_recovery.get("status"))
|
| 448 |
+
if latest_status in FAILED_STATUSES:
|
| 449 |
+
return "failed"
|
| 450 |
+
return "warning" if terminal else "running"
|
| 451 |
|
| 452 |
if phase == "live_validation":
|
| 453 |
+
blocker = _final_blocker_label(bundle)
|
| 454 |
+
if blocker and _final_visual_status(_verdict(bundle)) == "error":
|
| 455 |
+
return "Live validation incomplete"
|
| 456 |
signals = _signals(bundle)
|
| 457 |
smoke = _smoke(bundle)
|
| 458 |
if signals.get("generation_smoke_passed") is True or _lower(smoke.get("status")) == "success":
|
|
|
|
| 466 |
if eval_status.get("published") is True:
|
| 467 |
return "complete"
|
| 468 |
if eval_status.get("attempted"):
|
| 469 |
+
if not terminal and _is_record_not_ready(eval_status):
|
| 470 |
+
return "pending"
|
| 471 |
return "warning"
|
| 472 |
if terminal:
|
| 473 |
return "warning" if bundle.get("eval_record") else "pending"
|
| 474 |
+
return "pending"
|
| 475 |
|
| 476 |
if phase == "agent" and _pi_model_warning(bundle):
|
| 477 |
return "warning"
|
|
|
|
| 508 |
terminal = visual_status in {"success", "warn", "error", "stopped"}
|
| 509 |
percent = 100 if terminal else int(progress.get("progress") or 0)
|
| 510 |
|
| 511 |
+
warnings = [w for w in (_pi_model_warning(bundle), _hardware_warning(bundle), _logs_warning(bundle), _archive_warning(bundle, terminal=terminal)) if w]
|
| 512 |
phases: list[dict[str, Any]] = []
|
| 513 |
for phase in PHASE_ORDER:
|
| 514 |
status = _phase_status(bundle, phase, verdict)
|
src/version.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
ASF_APP_VERSION = "v189.
|
| 4 |
-
ASF_RELEASE_NAME = "Agentic Space Factory v189.
|
| 5 |
|
| 6 |
|
| 7 |
def resolve_app_version(value: str | None = None) -> str:
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
ASF_APP_VERSION = "v189.7"
|
| 4 |
+
ASF_RELEASE_NAME = "Agentic Space Factory v189.7"
|
| 5 |
|
| 6 |
|
| 7 |
def resolve_app_version(value: str | None = None) -> str:
|
src/worker_payload.py
CHANGED
|
@@ -52,14 +52,14 @@ INTERNAL_WORKSPACE_ARTIFACT_NAMES = {
|
|
| 52 |
|
| 53 |
|
| 54 |
def internal_workspace_upload_ignore_patterns() -> list[str]:
|
| 55 |
-
patterns = [".git/*", "node_modules/*", "__pycache__/*", "*.pyc"]
|
| 56 |
for name in sorted(INTERNAL_WORKSPACE_ARTIFACT_NAMES):
|
| 57 |
patterns.extend([name, f"**/{name}"])
|
| 58 |
return patterns
|
| 59 |
|
| 60 |
|
| 61 |
def internal_workspace_copy_ignore(_dir: str, names: list[str]) -> set[str]:
|
| 62 |
-
ignored = {".git", "node_modules", "__pycache__"}
|
| 63 |
ignored.update(name for name in names if name in INTERNAL_WORKSPACE_ARTIFACT_NAMES or name.endswith(".pyc"))
|
| 64 |
return ignored
|
| 65 |
|
|
@@ -67,7 +67,7 @@ def internal_workspace_copy_ignore(_dir: str, names: list[str]) -> set[str]:
|
|
| 67 |
def is_publishable_workspace_file(path: Path) -> bool:
|
| 68 |
if path.name in INTERNAL_WORKSPACE_ARTIFACT_NAMES:
|
| 69 |
return False
|
| 70 |
-
if any(part in {".git", "node_modules", "__pycache__"} for part in path.parts):
|
| 71 |
return False
|
| 72 |
if path.suffix == ".pyc":
|
| 73 |
return False
|
|
@@ -92,6 +92,43 @@ def append_event(path: Path, step: str, status: str, message: str, data: dict |
|
|
| 92 |
print(line, flush=True)
|
| 93 |
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
def _artifact_entry(run_dir: Path, rel_path: str, *, kind: str = "file") -> dict:
|
| 96 |
path = run_dir / rel_path
|
| 97 |
if kind == "folder":
|
|
@@ -2332,12 +2369,16 @@ def sanitize_readme_metadata(workspace: Path, events_path: Path):
|
|
| 2332 |
else:
|
| 2333 |
changed = True
|
| 2334 |
|
|
|
|
|
|
|
| 2335 |
required = {
|
| 2336 |
"title": metadata.get("title") or "Generated Model Space",
|
| 2337 |
"sdk": "gradio",
|
| 2338 |
"app_file": "app.py",
|
| 2339 |
-
"python_version":
|
| 2340 |
}
|
|
|
|
|
|
|
| 2341 |
suggested = metadata.get("suggested_hardware")
|
| 2342 |
if suggested:
|
| 2343 |
required["suggested_hardware"] = suggested
|
|
@@ -2351,14 +2392,26 @@ def sanitize_readme_metadata(workspace: Path, events_path: Path):
|
|
| 2351 |
for key in ordered_keys:
|
| 2352 |
if key in required and required[key]:
|
| 2353 |
value = str(required[key]).strip()
|
| 2354 |
-
if key in {"title", "short_description"} and not (value.startswith('"') or value.startswith("'")):
|
| 2355 |
value = json.dumps(value, ensure_ascii=False)
|
| 2356 |
lines.append(f"{key}: {value}")
|
| 2357 |
normalized_body = body.lstrip("\n") or "# Generated Model Space\n"
|
| 2358 |
new_text = "---\n" + "\n".join(lines) + "\n---\n\n" + normalized_body
|
| 2359 |
if new_text != text:
|
| 2360 |
readme_path.write_text(new_text, encoding="utf-8")
|
| 2361 |
-
append_event(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2362 |
|
| 2363 |
def normalize_requirements_for_modern_hub(workspace: Path, events_path: Path):
|
| 2364 |
"""Normalize only broad known-dangerous base dependencies before upload.
|
|
@@ -2426,6 +2479,12 @@ def normalize_requirements_for_modern_hub(workspace: Path, events_path: Path):
|
|
| 2426 |
stable_policy_lines.append(policy[canonical])
|
| 2427 |
changed = True
|
| 2428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2429 |
new_lines = prefix_lines + stable_policy_lines + filtered
|
| 2430 |
new = "\n".join(line for line in new_lines if line.strip()) + "\n"
|
| 2431 |
if new != raw:
|
|
@@ -2440,7 +2499,10 @@ def normalize_requirements_for_modern_hub(workspace: Path, events_path: Path):
|
|
| 2440 |
{
|
| 2441 |
"huggingface_hub": policy["huggingface-hub"],
|
| 2442 |
"transformers": policy["transformers"],
|
| 2443 |
-
"reason": "Avoid uncontrolled Transformers 5.x while preserving model-specific dependency choices for Pi to repair from build logs.",
|
|
|
|
|
|
|
|
|
|
| 2444 |
},
|
| 2445 |
)
|
| 2446 |
|
|
@@ -2538,7 +2600,7 @@ def workspace_file_inventory(workspace: Path, max_files: int = 80) -> list[str]:
|
|
| 2538 |
for path in sorted(workspace.rglob("*")):
|
| 2539 |
if not path.is_file():
|
| 2540 |
continue
|
| 2541 |
-
if any(part in {".git", "node_modules", "__pycache__"} for part in path.parts):
|
| 2542 |
continue
|
| 2543 |
try:
|
| 2544 |
files.append(str(path.relative_to(workspace)))
|
|
@@ -3740,6 +3802,43 @@ def append_event(path: Path, step: str, status: str, message: str, data: dict |
|
|
| 3740 |
print(line, flush=True)
|
| 3741 |
|
| 3742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3743 |
def redact_text(text: str | None) -> str:
|
| 3744 |
if not text:
|
| 3745 |
return ""
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
def internal_workspace_upload_ignore_patterns() -> list[str]:
|
| 55 |
+
patterns = [".git/*", ".cache/*", "**/.cache/*", "node_modules/*", "__pycache__/*", "*.pyc"]
|
| 56 |
for name in sorted(INTERNAL_WORKSPACE_ARTIFACT_NAMES):
|
| 57 |
patterns.extend([name, f"**/{name}"])
|
| 58 |
return patterns
|
| 59 |
|
| 60 |
|
| 61 |
def internal_workspace_copy_ignore(_dir: str, names: list[str]) -> set[str]:
|
| 62 |
+
ignored = {".git", ".cache", "node_modules", "__pycache__"}
|
| 63 |
ignored.update(name for name in names if name in INTERNAL_WORKSPACE_ARTIFACT_NAMES or name.endswith(".pyc"))
|
| 64 |
return ignored
|
| 65 |
|
|
|
|
| 67 |
def is_publishable_workspace_file(path: Path) -> bool:
|
| 68 |
if path.name in INTERNAL_WORKSPACE_ARTIFACT_NAMES:
|
| 69 |
return False
|
| 70 |
+
if any(part in {".git", ".cache", "node_modules", "__pycache__"} for part in path.parts):
|
| 71 |
return False
|
| 72 |
if path.suffix == ".pyc":
|
| 73 |
return False
|
|
|
|
| 92 |
print(line, flush=True)
|
| 93 |
|
| 94 |
|
| 95 |
+
SAFE_SPACE_PYTHON_VERSIONS = {"3.10", "3.11", "3.12"}
|
| 96 |
+
DEFAULT_SPACE_PYTHON_VERSION = "3.10"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def normalize_space_python_version(value) -> tuple[str, bool, str]:
|
| 100 |
+
raw = str(value or "").strip().strip("\"'")
|
| 101 |
+
lowered = raw.lower().replace("python", "").strip()
|
| 102 |
+
match = re.search(r"(3)\.(\d+)(?:\.\d+)?", lowered)
|
| 103 |
+
if match:
|
| 104 |
+
normalized = f"{match.group(1)}.{match.group(2)}"
|
| 105 |
+
else:
|
| 106 |
+
normalized = lowered
|
| 107 |
+
if normalized in SAFE_SPACE_PYTHON_VERSIONS:
|
| 108 |
+
return normalized, normalized != raw, "allowed"
|
| 109 |
+
return DEFAULT_SPACE_PYTHON_VERSION, True, f"unsupported_or_ambiguous:{raw or 'missing'}"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def requirements_has_package(lines: list[str], package: str) -> bool:
|
| 113 |
+
wanted = package.lower().replace("_", "-")
|
| 114 |
+
for line in lines:
|
| 115 |
+
stripped = line.strip()
|
| 116 |
+
if not stripped or stripped.startswith("#") or stripped.startswith("-") or "://" in stripped:
|
| 117 |
+
continue
|
| 118 |
+
name = re.split(r"[<>=!~;\[]", stripped, 1)[0].strip().lower().replace("_", "-")
|
| 119 |
+
if name == wanted:
|
| 120 |
+
return True
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def workspace_app_imports_torch(workspace: Path) -> bool:
|
| 125 |
+
app_path = workspace / "app.py"
|
| 126 |
+
if not app_path.exists():
|
| 127 |
+
return False
|
| 128 |
+
text = app_path.read_text(encoding="utf-8", errors="ignore")
|
| 129 |
+
return bool(re.search(r"(?m)^\s*(import\s+torch\b|from\s+torch\b)", text))
|
| 130 |
+
|
| 131 |
+
|
| 132 |
def _artifact_entry(run_dir: Path, rel_path: str, *, kind: str = "file") -> dict:
|
| 133 |
path = run_dir / rel_path
|
| 134 |
if kind == "folder":
|
|
|
|
| 2369 |
else:
|
| 2370 |
changed = True
|
| 2371 |
|
| 2372 |
+
raw_python_version = metadata.get("python_version") or DEFAULT_SPACE_PYTHON_VERSION
|
| 2373 |
+
python_version, python_version_changed, python_version_reason = normalize_space_python_version(raw_python_version)
|
| 2374 |
required = {
|
| 2375 |
"title": metadata.get("title") or "Generated Model Space",
|
| 2376 |
"sdk": "gradio",
|
| 2377 |
"app_file": "app.py",
|
| 2378 |
+
"python_version": python_version,
|
| 2379 |
}
|
| 2380 |
+
if python_version_changed:
|
| 2381 |
+
changed = True
|
| 2382 |
suggested = metadata.get("suggested_hardware")
|
| 2383 |
if suggested:
|
| 2384 |
required["suggested_hardware"] = suggested
|
|
|
|
| 2392 |
for key in ordered_keys:
|
| 2393 |
if key in required and required[key]:
|
| 2394 |
value = str(required[key]).strip()
|
| 2395 |
+
if key in {"title", "short_description", "python_version"} and not (value.startswith('"') or value.startswith("'")):
|
| 2396 |
value = json.dumps(value, ensure_ascii=False)
|
| 2397 |
lines.append(f"{key}: {value}")
|
| 2398 |
normalized_body = body.lstrip("\n") or "# Generated Model Space\n"
|
| 2399 |
new_text = "---\n" + "\n".join(lines) + "\n---\n\n" + normalized_body
|
| 2400 |
if new_text != text:
|
| 2401 |
readme_path.write_text(new_text, encoding="utf-8")
|
| 2402 |
+
append_event(
|
| 2403 |
+
events_path,
|
| 2404 |
+
"metadata_sanitize",
|
| 2405 |
+
"success",
|
| 2406 |
+
"Ensured README Space metadata",
|
| 2407 |
+
{
|
| 2408 |
+
"metadata_keys": [k for k in ordered_keys if k in required],
|
| 2409 |
+
"python_version_before": str(raw_python_version).strip().strip("\"'"),
|
| 2410 |
+
"python_version_after": python_version,
|
| 2411 |
+
"python_version_normalized": bool(python_version_changed),
|
| 2412 |
+
"python_version_reason": python_version_reason,
|
| 2413 |
+
},
|
| 2414 |
+
)
|
| 2415 |
|
| 2416 |
def normalize_requirements_for_modern_hub(workspace: Path, events_path: Path):
|
| 2417 |
"""Normalize only broad known-dangerous base dependencies before upload.
|
|
|
|
| 2479 |
stable_policy_lines.append(policy[canonical])
|
| 2480 |
changed = True
|
| 2481 |
|
| 2482 |
+
torch_added = False
|
| 2483 |
+
if workspace_app_imports_torch(workspace) and not requirements_has_package(filtered + stable_policy_lines, "torch"):
|
| 2484 |
+
stable_policy_lines.append("torch>=2.0.0")
|
| 2485 |
+
torch_added = True
|
| 2486 |
+
changed = True
|
| 2487 |
+
|
| 2488 |
new_lines = prefix_lines + stable_policy_lines + filtered
|
| 2489 |
new = "\n".join(line for line in new_lines if line.strip()) + "\n"
|
| 2490 |
if new != raw:
|
|
|
|
| 2499 |
{
|
| 2500 |
"huggingface_hub": policy["huggingface-hub"],
|
| 2501 |
"transformers": policy["transformers"],
|
| 2502 |
+
"reason": "Avoid uncontrolled Transformers 5.x while preserving model-specific dependency choices for Pi to repair from build logs; require torch when app.py imports torch.",
|
| 2503 |
+
"torch_added": torch_added,
|
| 2504 |
+
"torch_policy": "torch>=2.0.0",
|
| 2505 |
+
"torch_reason": "app_imports_torch" if torch_added else "not_needed_or_already_present",
|
| 2506 |
},
|
| 2507 |
)
|
| 2508 |
|
|
|
|
| 2600 |
for path in sorted(workspace.rglob("*")):
|
| 2601 |
if not path.is_file():
|
| 2602 |
continue
|
| 2603 |
+
if any(part in {".git", ".cache", "node_modules", "__pycache__"} for part in path.parts):
|
| 2604 |
continue
|
| 2605 |
try:
|
| 2606 |
files.append(str(path.relative_to(workspace)))
|
|
|
|
| 3802 |
print(line, flush=True)
|
| 3803 |
|
| 3804 |
|
| 3805 |
+
SAFE_SPACE_PYTHON_VERSIONS = {"3.10", "3.11", "3.12"}
|
| 3806 |
+
DEFAULT_SPACE_PYTHON_VERSION = "3.10"
|
| 3807 |
+
|
| 3808 |
+
|
| 3809 |
+
def normalize_space_python_version(value) -> tuple[str, bool, str]:
|
| 3810 |
+
raw = str(value or "").strip().strip("\"'")
|
| 3811 |
+
lowered = raw.lower().replace("python", "").strip()
|
| 3812 |
+
match = re.search(r"(3)\.(\d+)(?:\.\d+)?", lowered)
|
| 3813 |
+
if match:
|
| 3814 |
+
normalized = f"{match.group(1)}.{match.group(2)}"
|
| 3815 |
+
else:
|
| 3816 |
+
normalized = lowered
|
| 3817 |
+
if normalized in SAFE_SPACE_PYTHON_VERSIONS:
|
| 3818 |
+
return normalized, normalized != raw, "allowed"
|
| 3819 |
+
return DEFAULT_SPACE_PYTHON_VERSION, True, f"unsupported_or_ambiguous:{raw or 'missing'}"
|
| 3820 |
+
|
| 3821 |
+
|
| 3822 |
+
def requirements_has_package(lines: list[str], package: str) -> bool:
|
| 3823 |
+
wanted = package.lower().replace("_", "-")
|
| 3824 |
+
for line in lines:
|
| 3825 |
+
stripped = line.strip()
|
| 3826 |
+
if not stripped or stripped.startswith("#") or stripped.startswith("-") or "://" in stripped:
|
| 3827 |
+
continue
|
| 3828 |
+
name = re.split(r"[<>=!~;\[]", stripped, 1)[0].strip().lower().replace("_", "-")
|
| 3829 |
+
if name == wanted:
|
| 3830 |
+
return True
|
| 3831 |
+
return False
|
| 3832 |
+
|
| 3833 |
+
|
| 3834 |
+
def workspace_app_imports_torch(workspace: Path) -> bool:
|
| 3835 |
+
app_path = workspace / "app.py"
|
| 3836 |
+
if not app_path.exists():
|
| 3837 |
+
return False
|
| 3838 |
+
text = app_path.read_text(encoding="utf-8", errors="ignore")
|
| 3839 |
+
return bool(re.search(r"(?m)^\s*(import\s+torch\b|from\s+torch\b)", text))
|
| 3840 |
+
|
| 3841 |
+
|
| 3842 |
def redact_text(text: str | None) -> str:
|
| 3843 |
if not text:
|
| 3844 |
return ""
|