| from __future__ import annotations |
|
|
| import json |
| from typing import Any |
|
|
| import gradio as gr |
|
|
| from src.bucket import read_run_bundle |
| from src.config import settings |
| from src.jobs import ( |
| fetch_recent_logs_safe, |
| inspect_job_safe, |
| launch_create_private_space_job, |
| launch_hello_job, |
| launch_pi_gist_recipe_job, |
| launch_pi_model_card_job, |
| launch_pi_space_smoke_job, |
| ) |
| from src.runs import make_run_id, validate_run_id |
| from src.security import redact |
|
|
|
|
| APP_DESCRIPTION = f""" |
| # Agentic Space Factory — V5 Model Card |
| |
| This version validates the two critical foundations: |
| |
| ```text |
| Phase 1: HF OAuth → HF Job → mounted private Bucket → run state/events/report → UI readback |
| Phase 2: HF OAuth → HF Job → private target Space → file upload → live Gradio API validation → Bucket report |
| Phase 3: HF OAuth → HF Job → Pi modifies app.py → private target Space → live API validation → Pi traces |
| Phase 4: HF OAuth → HF Job → Pi reads gist → uses hf CLI → private Space → wrapper live API validation |
| Phase 5: HF OAuth → HF Job → model-card analysis → Pi adapts template → private model Space → live API validation |
| ``` |
| |
| Configured bucket: `{settings.bucket_uri}` |
| """ |
|
|
|
|
| def _profile_username(profile: Any) -> str | None: |
| if profile is None: |
| return None |
| if isinstance(profile, dict): |
| return profile.get("preferred_username") or profile.get("username") or profile.get("name") |
| return getattr(profile, "preferred_username", None) or getattr(profile, "username", None) or getattr(profile, "name", None) |
|
|
|
|
| def _token_value(oauth_token: Any) -> str | None: |
| if oauth_token is None: |
| return None |
| if isinstance(oauth_token, str): |
| return oauth_token |
| return getattr(oauth_token, "token", None) or getattr(oauth_token, "access_token", None) |
|
|
|
|
| def get_login_status(profile: gr.OAuthProfile | None) -> str: |
| username = _profile_username(profile) |
| if not username: |
| return "Not signed in. Use the Hugging Face login button before launching a Job." |
| return f"Signed in as **{username}**. Phase 2 will only create target Spaces under `{username}/...`." |
|
|
|
|
| def propose_hello_run_id() -> str: |
| return make_run_id("hello") |
|
|
|
|
| def propose_space_run_id() -> str: |
| return make_run_id("space") |
|
|
|
|
|
|
| def propose_pi_run_id() -> str: |
| return make_run_id("pi") |
|
|
|
|
| def propose_gist_run_id() -> str: |
| return make_run_id("gist") |
|
|
|
|
| def propose_model_run_id() -> str: |
| return make_run_id("model") |
|
|
|
|
|
|
| def launch_pi_model_card_job_ui( |
| requested_run_id: str, |
| model_id: str, |
| target_space_name: str, |
| pi_model: str, |
| profile: gr.OAuthProfile | None, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str, str, str]: |
| username = _profile_username(profile) |
| token = _token_value(oauth_token) |
| if not username or not token: |
| raise gr.Error("Please sign in with Hugging Face first. OAuth profile/token is missing.") |
|
|
| run_id = validate_run_id(requested_run_id or propose_model_run_id()) |
| result = launch_pi_model_card_job( |
| token=token, |
| username=username, |
| target_slug=target_space_name, |
| model_id=model_id, |
| pi_model=pi_model, |
| run_id=run_id, |
| ) |
|
|
| job_url = result.get("job_url") or "" |
| target_space = result.get("target_space") or "" |
| target_url = result.get("target_space_url") or "" |
| summary = json.dumps(result, indent=2) |
| return run_id, result["job_id"], job_url, target_space, target_url, summary |
|
|
|
|
| def launch_pi_gist_job_ui( |
| requested_run_id: str, |
| target_space_name: str, |
| pi_model: str, |
| profile: gr.OAuthProfile | None, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str, str, str]: |
| username = _profile_username(profile) |
| token = _token_value(oauth_token) |
| if not username or not token: |
| raise gr.Error("Please sign in with Hugging Face first. OAuth profile/token is missing.") |
|
|
| run_id = validate_run_id(requested_run_id or propose_gist_run_id()) |
| result = launch_pi_gist_recipe_job( |
| token=token, |
| username=username, |
| target_slug=target_space_name, |
| pi_model=pi_model, |
| run_id=run_id, |
| ) |
|
|
| job_url = result.get("job_url") or "" |
| target_space = result.get("target_space") or "" |
| target_url = result.get("target_space_url") or "" |
| summary = json.dumps(result, indent=2) |
| return run_id, result["job_id"], job_url, target_space, target_url, summary |
|
|
|
|
| def launch_pi_space_job_ui( |
| requested_run_id: str, |
| target_space_name: str, |
| pi_model: str, |
| profile: gr.OAuthProfile | None, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str, str, str]: |
| username = _profile_username(profile) |
| token = _token_value(oauth_token) |
| if not username or not token: |
| raise gr.Error("Please sign in with Hugging Face first. OAuth profile/token is missing.") |
|
|
| run_id = validate_run_id(requested_run_id or propose_pi_run_id()) |
| result = launch_pi_space_smoke_job( |
| token=token, |
| username=username, |
| target_slug=target_space_name, |
| pi_model=pi_model, |
| run_id=run_id, |
| ) |
|
|
| job_url = result.get("job_url") or "" |
| target_space = result.get("target_space") or "" |
| target_url = result.get("target_space_url") or "" |
| summary = json.dumps(result, indent=2) |
| return run_id, result["job_id"], job_url, target_space, target_url, summary |
|
|
| def launch_hello_job_ui( |
| requested_run_id: str, |
| profile: gr.OAuthProfile | None, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str, str]: |
| username = _profile_username(profile) |
| token = _token_value(oauth_token) |
| if not username or not token: |
| raise gr.Error("Please sign in with Hugging Face first. OAuth profile/token is missing.") |
|
|
| run_id = validate_run_id(requested_run_id or propose_hello_run_id()) |
| result = launch_hello_job(token=token, username=username, run_id=run_id) |
|
|
| job_url = result.get("job_url") or "" |
| summary = json.dumps(result, indent=2) |
| return run_id, result["job_id"], job_url, summary, f"Launched hello run `{run_id}`." |
|
|
|
|
| def launch_private_space_job_ui( |
| requested_run_id: str, |
| target_space_name: str, |
| profile: gr.OAuthProfile | None, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str, str, str]: |
| username = _profile_username(profile) |
| token = _token_value(oauth_token) |
| if not username or not token: |
| raise gr.Error("Please sign in with Hugging Face first. OAuth profile/token is missing.") |
|
|
| run_id = validate_run_id(requested_run_id or propose_space_run_id()) |
| result = launch_create_private_space_job( |
| token=token, |
| username=username, |
| target_slug=target_space_name, |
| run_id=run_id, |
| ) |
|
|
| job_url = result.get("job_url") or "" |
| target_space = result.get("target_space") or "" |
| target_url = result.get("target_space_url") or "" |
| summary = json.dumps(result, indent=2) |
| return run_id, result["job_id"], job_url, target_space, target_url, summary |
|
|
|
|
| def refresh_run_ui( |
| run_id: str, |
| job_id: str, |
| oauth_token: gr.OAuthToken | None, |
| ) -> tuple[str, str, str, str]: |
| token = _token_value(oauth_token) |
| if not token: |
| raise gr.Error("Please sign in with Hugging Face first.") |
| run_id = validate_run_id(run_id) |
|
|
| bundle = read_run_bundle(run_id, token=token) |
| job_info = inspect_job_safe(job_id, token=token) if job_id else {} |
| logs = redact(fetch_recent_logs_safe(job_id, token=token)) if job_id else "" |
|
|
| state_text = json.dumps(bundle.get("state") or {"status": "not_available_yet"}, indent=2, ensure_ascii=False) |
| events = bundle.get("events") or [] |
| events_text = "\n".join(json.dumps(event, ensure_ascii=False) for event in events) or "No events found yet. The Job may still be scheduling." |
| report_text = bundle.get("report") or "No report found yet. Refresh after the Job has started writing to the bucket." |
| job_text = json.dumps(job_info, indent=2, ensure_ascii=False) |
| if logs: |
| job_text += "\n\nRecent job logs:\n" + logs |
|
|
| return state_text, events_text, report_text, job_text |
|
|
|
|
| def build_demo() -> gr.Blocks: |
| with gr.Blocks(title="Agentic Space Factory") as demo: |
| gr.Markdown(APP_DESCRIPTION) |
| gr.LoginButton() |
|
|
| login_status = gr.Markdown() |
| demo.load(fn=get_login_status, inputs=None, outputs=login_status) |
|
|
|
|
| with gr.Tab("Phase 5 — Model card → private Space"): |
| gr.Markdown( |
| """ |
| This phase starts from a real Hugging Face `model_id`. The Job fetches model metadata/model card, gates to a small set of simple Transformers text-pipeline tasks, prepares a Gradio template, asks Pi to adapt the app, then creates a **private** target Space and validates the live API. |
| |
| Recommended first test model: `sshleifer/tiny-gpt2`. |
| |
| This phase is intentionally limited: simple text models only, private Space only, no ZeroGPU yet, no gated-model secret injection yet. |
| """ |
| ) |
| with gr.Row(): |
| model_run_id_box = gr.Textbox(label="Run ID", value=propose_model_run_id, interactive=True) |
| new_model_run_btn = gr.Button("Generate new run id") |
| new_model_run_btn.click(fn=propose_model_run_id, inputs=None, outputs=model_run_id_box) |
|
|
| model_id_box = gr.Textbox( |
| label="Model ID", |
| value="sshleifer/tiny-gpt2", |
| info="Use a small public model for the first test. Phase 5 supports simple Transformers text pipeline tasks only.", |
| ) |
| model_target_space_name = gr.Textbox( |
| label="Target Space name", |
| placeholder="e.g. space-factory-model-tiny-gpt2-v1", |
| info="Use a new name. The Space is created under your own username and is private.", |
| ) |
| model_pi_model_box = gr.Textbox( |
| label="Pi model", |
| value="moonshotai/Kimi-K2.5", |
| info="Model used by Pi through Hugging Face Inference Providers.", |
| ) |
| launch_model_btn = gr.Button("Generate model Space", variant="primary") |
|
|
| phase5_job_id_box = gr.Textbox(label="Job ID", interactive=True) |
| phase5_job_url_box = gr.Textbox(label="Job URL", interactive=False) |
| phase5_target_space_box = gr.Textbox(label="Target Space", interactive=False) |
| phase5_target_url_box = gr.Textbox(label="Target Space URL", interactive=False) |
| phase5_launch_result = gr.Code(label="Launch result", language="json") |
|
|
| launch_model_btn.click( |
| fn=launch_pi_model_card_job_ui, |
| inputs=[model_run_id_box, model_id_box, model_target_space_name, model_pi_model_box], |
| outputs=[ |
| model_run_id_box, |
| phase5_job_id_box, |
| phase5_job_url_box, |
| phase5_target_space_box, |
| phase5_target_url_box, |
| phase5_launch_result, |
| ], |
| ) |
|
|
| phase5_refresh_btn = gr.Button("Refresh Phase 5 run status") |
| with gr.Tab("Phase 5 state"): |
| phase5_state_code = gr.Code(label="state.json", language="json") |
| with gr.Tab("Phase 5 events"): |
| phase5_events_code = gr.Code(label="events.jsonl", language="json") |
| with gr.Tab("Phase 5 report"): |
| phase5_report_md = gr.Markdown() |
| with gr.Tab("Phase 5 job"): |
| phase5_job_code = gr.Code(label="Job status/logs", language="json") |
|
|
| phase5_refresh_btn.click( |
| fn=refresh_run_ui, |
| inputs=[model_run_id_box, phase5_job_id_box], |
| outputs=[phase5_state_code, phase5_events_code, phase5_report_md, phase5_job_code], |
| ) |
|
|
| with gr.Tab("Phase 4 — Pi gist recipe"): |
| gr.Markdown( |
| """ |
| This phase is the first closer reproduction of the article workflow. The HF Job installs Pi, gives it the HF Spaces Agent Quickstart gist, and asks Pi to use the `hf` CLI to create/upload a **private** target Space. |
| |
| The wrapper still stays in control of final success: it independently checks that the target Space exists and validates the live Gradio API. |
| |
| Use a fresh Space name. This phase intentionally fails if the target Space already exists. |
| """ |
| ) |
| with gr.Row(): |
| gist_run_id_box = gr.Textbox(label="Run ID", value=propose_gist_run_id, interactive=True) |
| new_gist_run_btn = gr.Button("Generate new run id") |
| new_gist_run_btn.click(fn=propose_gist_run_id, inputs=None, outputs=gist_run_id_box) |
|
|
| gist_target_space_name = gr.Textbox( |
| label="Target Space name", |
| placeholder="e.g. space-factory-pi-gist-v1", |
| info="Use a new name. Pi is asked to create this private Space under your username using hf CLI.", |
| ) |
| gist_model_box = gr.Textbox( |
| label="Pi model", |
| value="moonshotai/Kimi-K2.5", |
| info="Model used by Pi through Hugging Face Inference Providers.", |
| ) |
| launch_gist_btn = gr.Button("Run Pi gist recipe", variant="primary") |
|
|
| phase4_job_id_box = gr.Textbox(label="Job ID", interactive=True) |
| phase4_job_url_box = gr.Textbox(label="Job URL", interactive=False) |
| phase4_target_space_box = gr.Textbox(label="Target Space", interactive=False) |
| phase4_target_url_box = gr.Textbox(label="Target Space URL", interactive=False) |
| phase4_launch_result = gr.Code(label="Launch result", language="json") |
|
|
| launch_gist_btn.click( |
| fn=launch_pi_gist_job_ui, |
| inputs=[gist_run_id_box, gist_target_space_name, gist_model_box], |
| outputs=[ |
| gist_run_id_box, |
| phase4_job_id_box, |
| phase4_job_url_box, |
| phase4_target_space_box, |
| phase4_target_url_box, |
| phase4_launch_result, |
| ], |
| ) |
|
|
| phase4_refresh_btn = gr.Button("Refresh Phase 4 run status") |
| with gr.Tab("Phase 4 state"): |
| phase4_state_code = gr.Code(label="state.json", language="json") |
| with gr.Tab("Phase 4 events"): |
| phase4_events_code = gr.Code(label="events.jsonl", language="json") |
| with gr.Tab("Phase 4 report"): |
| phase4_report_md = gr.Markdown() |
| with gr.Tab("Phase 4 job"): |
| phase4_job_code = gr.Code(label="Job status/logs", language="json") |
|
|
| phase4_refresh_btn.click( |
| fn=refresh_run_ui, |
| inputs=[gist_run_id_box, phase4_job_id_box], |
| outputs=[phase4_state_code, phase4_events_code, phase4_report_md, phase4_job_code], |
| ) |
|
|
|
|
| with gr.Tab("Phase 3 — Pi smoke test"): |
| gr.Markdown( |
| """ |
| This is the first Pi integration. It launches an HF Job that installs Pi, configures it with Hugging Face Inference Providers, asks Pi to make a small safe edit to `app.py`, then creates a **private** target Space and validates the live API. |
| |
| This phase is intentionally narrow: Pi must only add the marker phrase `Pi modified this app` to the hello app. If Pi fails or does not make the expected edit, the Job fails safely before reporting success. |
| """ |
| ) |
| with gr.Row(): |
| pi_run_id_box = gr.Textbox(label="Run ID", value=propose_pi_run_id, interactive=True) |
| new_pi_run_btn = gr.Button("Generate new run id") |
| new_pi_run_btn.click(fn=propose_pi_run_id, inputs=None, outputs=pi_run_id_box) |
|
|
| pi_target_space_name = gr.Textbox( |
| label="Target Space name", |
| placeholder="e.g. space-factory-pi-smoke-v1", |
| info="Use a new name. The Space is created under your own username and is private.", |
| ) |
| pi_model_box = gr.Textbox( |
| label="Pi model", |
| value="moonshotai/Kimi-K2.5", |
| info="Model used by Pi through Hugging Face Inference Providers. You can replace it with another provider-backed coding model.", |
| ) |
| launch_pi_btn = gr.Button("Run Pi smoke test", variant="primary") |
|
|
| phase3_job_id_box = gr.Textbox(label="Job ID", interactive=True) |
| phase3_job_url_box = gr.Textbox(label="Job URL", interactive=False) |
| phase3_target_space_box = gr.Textbox(label="Target Space", interactive=False) |
| phase3_target_url_box = gr.Textbox(label="Target Space URL", interactive=False) |
| phase3_launch_result = gr.Code(label="Launch result", language="json") |
|
|
| launch_pi_btn.click( |
| fn=launch_pi_space_job_ui, |
| inputs=[pi_run_id_box, pi_target_space_name, pi_model_box], |
| outputs=[ |
| pi_run_id_box, |
| phase3_job_id_box, |
| phase3_job_url_box, |
| phase3_target_space_box, |
| phase3_target_url_box, |
| phase3_launch_result, |
| ], |
| ) |
|
|
| phase3_refresh_btn = gr.Button("Refresh Phase 3 run status") |
| with gr.Tab("Phase 3 state"): |
| phase3_state_code = gr.Code(label="state.json", language="json") |
| with gr.Tab("Phase 3 events"): |
| phase3_events_code = gr.Code(label="events.jsonl", language="json") |
| with gr.Tab("Phase 3 report"): |
| phase3_report_md = gr.Markdown() |
| with gr.Tab("Phase 3 job"): |
| phase3_job_code = gr.Code(label="Job status/logs", language="json") |
|
|
| phase3_refresh_btn.click( |
| fn=refresh_run_ui, |
| inputs=[pi_run_id_box, phase3_job_id_box], |
| outputs=[phase3_state_code, phase3_events_code, phase3_report_md, phase3_job_code], |
| ) |
|
|
| with gr.Tab("Phase 2 — Create private Space"): |
| gr.Markdown( |
| """ |
| This is the next safe increment. It creates a **private** hello-world Gradio Space under your own namespace, uploads files, waits for the live API, then writes the result to the bucket. |
| |
| It will fail safely if the target Space already exists, to avoid overwriting user resources unexpectedly. |
| """ |
| ) |
| with gr.Row(): |
| space_run_id_box = gr.Textbox(label="Run ID", value=propose_space_run_id, interactive=True) |
| new_space_run_btn = gr.Button("Generate new run id") |
| new_space_run_btn.click(fn=propose_space_run_id, inputs=None, outputs=space_run_id_box) |
|
|
| target_space_name = gr.Textbox( |
| label="Target Space name", |
| placeholder="e.g. space-factory-hello-test", |
| info="Use a new name. For Phase 2, the Space is always created under your own username and is private.", |
| ) |
| launch_space_btn = gr.Button("Create private hello Space", variant="primary") |
|
|
| phase2_job_id_box = gr.Textbox(label="Job ID", interactive=True) |
| phase2_job_url_box = gr.Textbox(label="Job URL", interactive=False) |
| phase2_target_space_box = gr.Textbox(label="Target Space", interactive=False) |
| phase2_target_url_box = gr.Textbox(label="Target Space URL", interactive=False) |
| phase2_launch_result = gr.Code(label="Launch result", language="json") |
|
|
| launch_space_btn.click( |
| fn=launch_private_space_job_ui, |
| inputs=[space_run_id_box, target_space_name], |
| outputs=[ |
| space_run_id_box, |
| phase2_job_id_box, |
| phase2_job_url_box, |
| phase2_target_space_box, |
| phase2_target_url_box, |
| phase2_launch_result, |
| ], |
| ) |
|
|
| phase2_refresh_btn = gr.Button("Refresh Phase 2 run status") |
| with gr.Tab("Phase 2 state"): |
| phase2_state_code = gr.Code(label="state.json", language="json") |
| with gr.Tab("Phase 2 events"): |
| phase2_events_code = gr.Code(label="events.jsonl", language="json") |
| with gr.Tab("Phase 2 report"): |
| phase2_report_md = gr.Markdown() |
| with gr.Tab("Phase 2 job"): |
| phase2_job_code = gr.Code(label="Job status/logs", language="json") |
|
|
| phase2_refresh_btn.click( |
| fn=refresh_run_ui, |
| inputs=[space_run_id_box, phase2_job_id_box], |
| outputs=[phase2_state_code, phase2_events_code, phase2_report_md, phase2_job_code], |
| ) |
|
|
| with gr.Tab("Phase 1 — Hello Job"): |
| gr.Markdown("Keep this tab as a regression test for OAuth → Job → Bucket only.") |
| with gr.Row(): |
| run_id_box = gr.Textbox(label="Run ID", value=propose_hello_run_id, interactive=True) |
| new_run_btn = gr.Button("Generate new run id") |
| new_run_btn.click(fn=propose_hello_run_id, inputs=None, outputs=run_id_box) |
|
|
| launch_btn = gr.Button("Launch hello HF Job", variant="secondary") |
| job_id_box = gr.Textbox(label="Job ID", interactive=True) |
| job_url_box = gr.Textbox(label="Job URL", interactive=False) |
| launch_result = gr.Code(label="Launch result", language="json") |
| status_md = gr.Markdown() |
|
|
| launch_btn.click( |
| fn=launch_hello_job_ui, |
| inputs=[run_id_box], |
| outputs=[run_id_box, job_id_box, job_url_box, launch_result, status_md], |
| ) |
|
|
| refresh_btn = gr.Button("Refresh hello run status") |
| with gr.Tab("Hello state"): |
| state_code = gr.Code(label="state.json", language="json") |
| with gr.Tab("Hello events"): |
| events_code = gr.Code(label="events.jsonl", language="json") |
| with gr.Tab("Hello report"): |
| report_md = gr.Markdown() |
| with gr.Tab("Hello job"): |
| job_code = gr.Code(label="Job status/logs", language="json") |
|
|
| refresh_btn.click( |
| fn=refresh_run_ui, |
| inputs=[run_id_box, job_id_box], |
| outputs=[state_code, events_code, report_md, job_code], |
| ) |
|
|
| gr.Markdown( |
| """ |
| ## Next increments |
| |
| After Phase 5 passes, the next step is Phase 6: |
| |
| 1. add a ZeroGPU Diffusers template, |
| 2. improve model compatibility gating, |
| 3. support richer validation by output type, |
| 4. keep the wrapper-owned live API validation gate. |
| """ |
| ) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| build_demo().launch() |
|
|