Embed trackio dashboard in chat for jobs and sandboxes (#161)
Browse files* Embed trackio dashboard in chat for jobs and sandboxes
Wires `trackio_space_id` / `trackio_project` through hf_jobs and
sandbox_create as optional tool args, then renders the resulting
dashboard inline as an embedded iframe in the chat.
The plumbing turned out to need four pieces, not two:
1. Inject TRACKIO_SPACE_ID / TRACKIO_PROJECT into job + sandbox env.
2. Surface the values in tool_state_change so the frontend can render
the embed when the tool call is in flight.
3. Pre-seed the Space with our own README/requirements/app.py — without
this, trackio's init() against an existing empty Space leaves the
iframe stuck on the default Gradio template. We omit `hf_oauth: true`
from the seeded README so the embed renders without a sign-in click;
per-user privacy comes from HF namespace ownership.
4. Mount the metrics bucket on the dashboard Space at /data and set
TRACKIO_DIR / TRACKIO_BUCKET_ID. trackio.init() in the job creates
the bucket but only attaches it to the job process — without
mounting it on the Space too, the dashboard reads an empty /data
and shows "No projects" even though the bucket has data.
The seed helper (`agent/tools/trackio_seed.py`) is idempotent and runs
synchronously before run_job / sandbox_create whenever a trackio_space_id
is provided. Bumps `huggingface-hub` to >= 1.12.0 for the bucket / volume
APIs (`create_bucket`, `Volume`, `set_space_volumes`, `add_space_variable`).
Validated end-to-end with a real trackio job against
akseljoonas/mlintern-trackio-pragmatic-01 — dashboard renders project,
runs, and loss/accuracy charts with no sign-in prompt.
* Brand seeded trackio dashboards with the ml-intern logo
Sets TRACKIO_LOGO_LIGHT_URL / TRACKIO_LOGO_DARK_URL on every seeded Space
to the smolagents.webp hosted on the smolagents/ml-intern Space, so the
dashboard renders ml-intern branding instead of the trackio wordmark.
Idempotent — only writes the variable when the value would change.
TRACKIO_THEME deliberately not set: trackio dropped Gradio-theme support
in 1.x, so the variable is a no-op there.
* Show loading state in trackio embed until iframe paints
A freshly-seeded HF Space takes 30–60s to build, and even after build the
trackio bundle takes a few seconds to render its first frame — during
that whole window the iframe was a blank white rectangle with no signal
that anything was happening.
Stack a 'Spinning up the dashboard…' overlay (spinner + short hint about
typical build time) on top of the iframe and clear it once the iframe's
load event fires.
* Match trackio embed loading state to chat dark theme
- Force trackio dashboard into dark mode via gradio's __theme=dark query
param so the embedded charts blend with the chat instead of flashing
a white panel after load.
- Switch the iframe container's loading background from white to
var(--code-panel-bg) so any frame between iframe paint and load uses
the same dark surface as the surrounding chat.
- Reword the placeholder copy to talk about the trackio dashboard
rather than HF Spaces — that's the abstraction users care about.
* Sandbox trackio iframe, validate space ID, persist dashboards
Address PR review:
- iframe gets a sandbox attribute so the embedded gradio app can't reach
back into the parent (forms, scripts, same-origin, popups, downloads,
modals are all the runtime needs).
- spaceIdToSubdomain only runs on validated repo IDs; an unexpected value
now suppresses the embed instead of building a malformed URL that
could redirect to an attacker-controlled subdomain.
- trackioDashboards mirrors toolErrors/rejectedTools and persists to
localStorage so the dashboard survives a page refresh during a run.
- agent/tools/jobs_tool.py +86 -20
- agent/tools/sandbox_tool.py +86 -5
- agent/tools/trackio_seed.py +205 -0
- frontend/src/components/Chat/ToolCallGroup.tsx +201 -1
- frontend/src/lib/sse-chat-transport.ts +5 -0
- frontend/src/store/agentStore.ts +49 -0
- pyproject.toml +1 -1
- uv.lock +40 -36
|
@@ -19,6 +19,7 @@ from huggingface_hub.utils import HfHubHTTPError
|
|
| 19 |
|
| 20 |
from agent.core.hf_access import JobsAccessError, resolve_jobs_namespace
|
| 21 |
from agent.core.session import Event
|
|
|
|
| 22 |
from agent.tools.types import ToolResult
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
|
@@ -382,6 +383,31 @@ class HfJobsTool:
|
|
| 382 |
"isError": True,
|
| 383 |
}
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
async def _wait_for_job_completion(
|
| 386 |
self, job_id: str, namespace: Optional[str] = None
|
| 387 |
) -> tuple[str, list[str]]:
|
|
@@ -533,11 +559,24 @@ class HfJobsTool:
|
|
| 533 |
# Run the job
|
| 534 |
flavor = args.get("hardware_flavor", "cpu-basic")
|
| 535 |
timeout_str = args.get("timeout", "30m")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
job = await _async_call(
|
| 537 |
self.api.run_job,
|
| 538 |
image=image,
|
| 539 |
command=command,
|
| 540 |
-
env=
|
| 541 |
secrets=_add_environment_variables(args.get("secrets"), self.hf_token),
|
| 542 |
flavor=flavor,
|
| 543 |
timeout=timeout_str,
|
|
@@ -550,16 +589,18 @@ class HfJobsTool:
|
|
| 550 |
|
| 551 |
# Send job URL immediately after job creation (before waiting for completion)
|
| 552 |
if self.session and self.tool_call_id:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
await self.session.send_event(
|
| 554 |
-
Event(
|
| 555 |
-
event_type="tool_state_change",
|
| 556 |
-
data={
|
| 557 |
-
"tool_call_id": self.tool_call_id,
|
| 558 |
-
"tool": "hf_jobs",
|
| 559 |
-
"state": "running",
|
| 560 |
-
"jobUrl": job.url,
|
| 561 |
-
},
|
| 562 |
-
)
|
| 563 |
)
|
| 564 |
|
| 565 |
# Telemetry: job submission + completion (infra consumption signal).
|
|
@@ -594,16 +635,18 @@ class HfJobsTool:
|
|
| 594 |
|
| 595 |
# Notify frontend of final status
|
| 596 |
if self.session and self.tool_call_id:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
await self.session.send_event(
|
| 598 |
-
Event(
|
| 599 |
-
event_type="tool_state_change",
|
| 600 |
-
data={
|
| 601 |
-
"tool_call_id": self.tool_call_id,
|
| 602 |
-
"tool": "hf_jobs",
|
| 603 |
-
"state": final_status.lower(),
|
| 604 |
-
"jobUrl": job.url,
|
| 605 |
-
},
|
| 606 |
-
)
|
| 607 |
)
|
| 608 |
|
| 609 |
# Filter out UV package installation output
|
|
@@ -977,7 +1020,10 @@ HF_JOBS_TOOL_SPEC = {
|
|
| 977 |
"- You MUST have validated dataset format via hf_inspect_dataset or hub_repo_details.\n"
|
| 978 |
"- Training config MUST include push_to_hub=True and hub_model_id. "
|
| 979 |
"Job storage is EPHEMERAL — all files are deleted when the job ends. Without push_to_hub, trained models are lost permanently.\n"
|
| 980 |
-
"- Include trackio monitoring and provide the dashboard URL to the user.
|
|
|
|
|
|
|
|
|
|
| 981 |
"BATCH/ABLATION JOBS: Submit ONE job first. Check logs to confirm it starts training successfully. "
|
| 982 |
"Only then submit the remaining jobs. Never submit all at once — if there's a bug, all jobs fail.\n\n"
|
| 983 |
"Operations: run, ps, logs, inspect, cancel, scheduled run/ps/inspect/delete/suspend/resume.\n\n"
|
|
@@ -1060,6 +1106,26 @@ HF_JOBS_TOOL_SPEC = {
|
|
| 1060 |
"type": "object",
|
| 1061 |
"description": "Environment variables {'KEY': 'VALUE'}. HF_TOKEN is auto-included.",
|
| 1062 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
"namespace": {
|
| 1064 |
"type": "string",
|
| 1065 |
"description": (
|
|
|
|
| 19 |
|
| 20 |
from agent.core.hf_access import JobsAccessError, resolve_jobs_namespace
|
| 21 |
from agent.core.session import Event
|
| 22 |
+
from agent.tools.trackio_seed import ensure_trackio_dashboard
|
| 23 |
from agent.tools.types import ToolResult
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
|
|
|
| 383 |
"isError": True,
|
| 384 |
}
|
| 385 |
|
| 386 |
+
async def _seed_trackio_dashboard(self, space_id: str) -> None:
|
| 387 |
+
"""Idempotently install trackio dashboard files into *space_id* before
|
| 388 |
+
the job runs. Surfaces seed progress as tool_log events but never
|
| 389 |
+
raises — a seed failure should not block job submission, since trackio
|
| 390 |
+
often still works when the Space already has dashboard code from a
|
| 391 |
+
previous run.
|
| 392 |
+
"""
|
| 393 |
+
loop = asyncio.get_running_loop()
|
| 394 |
+
|
| 395 |
+
def _log(msg: str) -> None:
|
| 396 |
+
if self.session is None:
|
| 397 |
+
return
|
| 398 |
+
loop.call_soon_threadsafe(
|
| 399 |
+
self.session.event_queue.put_nowait,
|
| 400 |
+
Event(event_type="tool_log", data={"tool": "hf_jobs", "log": msg}),
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
try:
|
| 404 |
+
await asyncio.to_thread(
|
| 405 |
+
ensure_trackio_dashboard, space_id, self.hf_token, _log
|
| 406 |
+
)
|
| 407 |
+
except Exception as e:
|
| 408 |
+
logger.warning(f"trackio dashboard seed failed for {space_id}: {e}")
|
| 409 |
+
_log(f"trackio dashboard seed failed: {e}")
|
| 410 |
+
|
| 411 |
async def _wait_for_job_completion(
|
| 412 |
self, job_id: str, namespace: Optional[str] = None
|
| 413 |
) -> tuple[str, list[str]]:
|
|
|
|
| 559 |
# Run the job
|
| 560 |
flavor = args.get("hardware_flavor", "cpu-basic")
|
| 561 |
timeout_str = args.get("timeout", "30m")
|
| 562 |
+
|
| 563 |
+
# Trackio: agent-declared space + project become env vars on the job
|
| 564 |
+
# so trackio.init() picks them up automatically. We also surface them
|
| 565 |
+
# in tool_state_change so the frontend can embed the dashboard.
|
| 566 |
+
env_dict = _add_default_env(args.get("env"))
|
| 567 |
+
trackio_space_id = args.get("trackio_space_id")
|
| 568 |
+
trackio_project = args.get("trackio_project")
|
| 569 |
+
if trackio_space_id:
|
| 570 |
+
env_dict["TRACKIO_SPACE_ID"] = trackio_space_id
|
| 571 |
+
await self._seed_trackio_dashboard(trackio_space_id)
|
| 572 |
+
if trackio_project:
|
| 573 |
+
env_dict["TRACKIO_PROJECT"] = trackio_project
|
| 574 |
+
|
| 575 |
job = await _async_call(
|
| 576 |
self.api.run_job,
|
| 577 |
image=image,
|
| 578 |
command=command,
|
| 579 |
+
env=env_dict,
|
| 580 |
secrets=_add_environment_variables(args.get("secrets"), self.hf_token),
|
| 581 |
flavor=flavor,
|
| 582 |
timeout=timeout_str,
|
|
|
|
| 589 |
|
| 590 |
# Send job URL immediately after job creation (before waiting for completion)
|
| 591 |
if self.session and self.tool_call_id:
|
| 592 |
+
state_data: Dict[str, Any] = {
|
| 593 |
+
"tool_call_id": self.tool_call_id,
|
| 594 |
+
"tool": "hf_jobs",
|
| 595 |
+
"state": "running",
|
| 596 |
+
"jobUrl": job.url,
|
| 597 |
+
}
|
| 598 |
+
if trackio_space_id:
|
| 599 |
+
state_data["trackioSpaceId"] = trackio_space_id
|
| 600 |
+
if trackio_project:
|
| 601 |
+
state_data["trackioProject"] = trackio_project
|
| 602 |
await self.session.send_event(
|
| 603 |
+
Event(event_type="tool_state_change", data=state_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
)
|
| 605 |
|
| 606 |
# Telemetry: job submission + completion (infra consumption signal).
|
|
|
|
| 635 |
|
| 636 |
# Notify frontend of final status
|
| 637 |
if self.session and self.tool_call_id:
|
| 638 |
+
final_data: Dict[str, Any] = {
|
| 639 |
+
"tool_call_id": self.tool_call_id,
|
| 640 |
+
"tool": "hf_jobs",
|
| 641 |
+
"state": final_status.lower(),
|
| 642 |
+
"jobUrl": job.url,
|
| 643 |
+
}
|
| 644 |
+
if trackio_space_id:
|
| 645 |
+
final_data["trackioSpaceId"] = trackio_space_id
|
| 646 |
+
if trackio_project:
|
| 647 |
+
final_data["trackioProject"] = trackio_project
|
| 648 |
await self.session.send_event(
|
| 649 |
+
Event(event_type="tool_state_change", data=final_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
)
|
| 651 |
|
| 652 |
# Filter out UV package installation output
|
|
|
|
| 1020 |
"- You MUST have validated dataset format via hf_inspect_dataset or hub_repo_details.\n"
|
| 1021 |
"- Training config MUST include push_to_hub=True and hub_model_id. "
|
| 1022 |
"Job storage is EPHEMERAL — all files are deleted when the job ends. Without push_to_hub, trained models are lost permanently.\n"
|
| 1023 |
+
"- Include trackio monitoring and provide the dashboard URL to the user. "
|
| 1024 |
+
"When the script uses report_to='trackio', also pass `trackio_space_id` "
|
| 1025 |
+
"(e.g. '<username>/mlintern-<8char>') and `trackio_project` as tool args — "
|
| 1026 |
+
"they are injected as TRACKIO_SPACE_ID/TRACKIO_PROJECT env vars and let the UI embed the live dashboard.\n\n"
|
| 1027 |
"BATCH/ABLATION JOBS: Submit ONE job first. Check logs to confirm it starts training successfully. "
|
| 1028 |
"Only then submit the remaining jobs. Never submit all at once — if there's a bug, all jobs fail.\n\n"
|
| 1029 |
"Operations: run, ps, logs, inspect, cancel, scheduled run/ps/inspect/delete/suspend/resume.\n\n"
|
|
|
|
| 1106 |
"type": "object",
|
| 1107 |
"description": "Environment variables {'KEY': 'VALUE'}. HF_TOKEN is auto-included.",
|
| 1108 |
},
|
| 1109 |
+
"trackio_space_id": {
|
| 1110 |
+
"type": "string",
|
| 1111 |
+
"description": (
|
| 1112 |
+
"Optional. The HF Space hosting the trackio dashboard for this run "
|
| 1113 |
+
"(e.g. '<username>/mlintern-<8char>', under YOUR HF namespace). "
|
| 1114 |
+
"Injected as TRACKIO_SPACE_ID env var and used by the UI to embed "
|
| 1115 |
+
"the live dashboard. Set this whenever the script uses "
|
| 1116 |
+
"report_to='trackio'. The Space is auto-created and seeded with the "
|
| 1117 |
+
"trackio dashboard before the job starts — DO NOT pre-create it via "
|
| 1118 |
+
"hf_repo_git, that produces an empty Space that breaks the embed."
|
| 1119 |
+
),
|
| 1120 |
+
},
|
| 1121 |
+
"trackio_project": {
|
| 1122 |
+
"type": "string",
|
| 1123 |
+
"description": (
|
| 1124 |
+
"Optional. The trackio project name to log this run under. "
|
| 1125 |
+
"Injected as TRACKIO_PROJECT env var and used by the UI to filter "
|
| 1126 |
+
"the embedded dashboard to this project."
|
| 1127 |
+
),
|
| 1128 |
+
},
|
| 1129 |
"namespace": {
|
| 1130 |
"type": "string",
|
| 1131 |
"description": (
|
|
@@ -19,6 +19,7 @@ from huggingface_hub import HfApi, SpaceHardware
|
|
| 19 |
|
| 20 |
from agent.core.session import Event
|
| 21 |
from agent.tools.sandbox_client import Sandbox
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def _looks_like_path(script: str) -> bool:
|
|
@@ -62,11 +63,36 @@ async def resolve_sandbox_script(
|
|
| 62 |
return None, f"Failed to read {script} from sandbox: {e}"
|
| 63 |
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# ── Tool name mapping (short agent names → Sandbox client names) ──────
|
| 66 |
|
| 67 |
|
| 68 |
async def _ensure_sandbox(
|
| 69 |
-
session: Any,
|
|
|
|
|
|
|
|
|
|
| 70 |
) -> tuple[Sandbox | None, str | None]:
|
| 71 |
"""
|
| 72 |
Ensure a sandbox exists on the session. Auto-creates with given hardware if needed.
|
|
@@ -120,11 +146,15 @@ async def _ensure_sandbox(
|
|
| 120 |
|
| 121 |
watcher_task = asyncio.create_task(_watch_cancel())
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
kwargs = {
|
| 124 |
"owner": owner,
|
| 125 |
"hardware": hardware,
|
| 126 |
"token": token,
|
| 127 |
-
"secrets":
|
| 128 |
"log": _log,
|
| 129 |
"cancel_event": cancel_flag,
|
| 130 |
**create_kwargs,
|
|
@@ -188,6 +218,9 @@ SANDBOX_CREATE_TOOL_SPEC = {
|
|
| 188 |
"fp32 ≈ 4 bytes/param, plus ~20% overhead for optimizer states during training.\n"
|
| 189 |
"Common picks: t4-small (16GB VRAM, fits ≤1-3B), a10g-small (24GB, ≤7B), a100-large (80GB, ≤30B). "
|
| 190 |
"If the model won't fit, pick larger hardware upfront — OOM on a sandbox wastes time.\n\n"
|
|
|
|
|
|
|
|
|
|
| 191 |
"Hardware: " + ", ".join([e.value for e in SpaceHardware]) + ".\n"
|
| 192 |
),
|
| 193 |
"parameters": {
|
|
@@ -204,16 +237,49 @@ SANDBOX_CREATE_TOOL_SPEC = {
|
|
| 204 |
"type": "boolean",
|
| 205 |
"description": "If true, create a private Space",
|
| 206 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
},
|
| 208 |
},
|
| 209 |
}
|
| 210 |
|
| 211 |
|
| 212 |
async def sandbox_create_handler(
|
| 213 |
-
args: dict[str, Any], session: Any = None
|
| 214 |
) -> tuple[str, bool]:
|
| 215 |
"""Handle sandbox_create tool calls."""
|
| 216 |
hardware = args.get("hardware", "cpu-basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
# If sandbox already exists, return its info
|
| 219 |
if session and getattr(session, "sandbox", None):
|
|
@@ -226,6 +292,7 @@ async def sandbox_create_handler(
|
|
| 226 |
"Hardware cannot be changed by calling sandbox_create again. "
|
| 227 |
"Delete the existing sandbox first if you need a different tier."
|
| 228 |
)
|
|
|
|
| 229 |
return (
|
| 230 |
f"Sandbox already active: {sb.space_id}\n"
|
| 231 |
f"URL: {sb.url}\n"
|
|
@@ -233,18 +300,32 @@ async def sandbox_create_handler(
|
|
| 233 |
f"Use bash/read/write/edit to interact with it."
|
| 234 |
), True
|
| 235 |
|
| 236 |
-
create_kwargs = {}
|
| 237 |
if "private" in args:
|
| 238 |
create_kwargs["private"] = args["private"]
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
try:
|
| 241 |
-
sb, error = await _ensure_sandbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
except Exception as e:
|
| 243 |
return f"Failed to create sandbox: {e}", False
|
| 244 |
|
| 245 |
if error:
|
| 246 |
return error, False
|
| 247 |
|
|
|
|
|
|
|
| 248 |
return (
|
| 249 |
f"Sandbox created: {sb.space_id}\n"
|
| 250 |
f"URL: {sb.url}\n"
|
|
|
|
| 19 |
|
| 20 |
from agent.core.session import Event
|
| 21 |
from agent.tools.sandbox_client import Sandbox
|
| 22 |
+
from agent.tools.trackio_seed import ensure_trackio_dashboard
|
| 23 |
|
| 24 |
|
| 25 |
def _looks_like_path(script: str) -> bool:
|
|
|
|
| 63 |
return None, f"Failed to read {script} from sandbox: {e}"
|
| 64 |
|
| 65 |
|
| 66 |
+
async def _seed_trackio_dashboard_safe(session: Any, space_id: str) -> None:
|
| 67 |
+
"""Idempotently seed *space_id* with trackio dashboard files using the
|
| 68 |
+
session's HF token. Logs progress, swallows errors — a failed seed should
|
| 69 |
+
not block sandbox creation."""
|
| 70 |
+
if not session or not getattr(session, "hf_token", None):
|
| 71 |
+
return
|
| 72 |
+
loop = asyncio.get_running_loop()
|
| 73 |
+
|
| 74 |
+
def _log(msg: str) -> None:
|
| 75 |
+
loop.call_soon_threadsafe(
|
| 76 |
+
session.event_queue.put_nowait,
|
| 77 |
+
Event(event_type="tool_log", data={"tool": "sandbox_create", "log": msg}),
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
await asyncio.to_thread(
|
| 82 |
+
ensure_trackio_dashboard, space_id, session.hf_token, _log
|
| 83 |
+
)
|
| 84 |
+
except Exception as e:
|
| 85 |
+
_log(f"trackio dashboard seed failed: {e}")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
# ── Tool name mapping (short agent names → Sandbox client names) ──────
|
| 89 |
|
| 90 |
|
| 91 |
async def _ensure_sandbox(
|
| 92 |
+
session: Any,
|
| 93 |
+
hardware: str = "cpu-basic",
|
| 94 |
+
extra_secrets: dict[str, str] | None = None,
|
| 95 |
+
**create_kwargs,
|
| 96 |
) -> tuple[Sandbox | None, str | None]:
|
| 97 |
"""
|
| 98 |
Ensure a sandbox exists on the session. Auto-creates with given hardware if needed.
|
|
|
|
| 146 |
|
| 147 |
watcher_task = asyncio.create_task(_watch_cancel())
|
| 148 |
|
| 149 |
+
secrets: dict[str, str] = {"HF_TOKEN": token}
|
| 150 |
+
if extra_secrets:
|
| 151 |
+
secrets.update({k: v for k, v in extra_secrets.items() if v})
|
| 152 |
+
|
| 153 |
kwargs = {
|
| 154 |
"owner": owner,
|
| 155 |
"hardware": hardware,
|
| 156 |
"token": token,
|
| 157 |
+
"secrets": secrets,
|
| 158 |
"log": _log,
|
| 159 |
"cancel_event": cancel_flag,
|
| 160 |
**create_kwargs,
|
|
|
|
| 218 |
"fp32 ≈ 4 bytes/param, plus ~20% overhead for optimizer states during training.\n"
|
| 219 |
"Common picks: t4-small (16GB VRAM, fits ≤1-3B), a10g-small (24GB, ≤7B), a100-large (80GB, ≤30B). "
|
| 220 |
"If the model won't fit, pick larger hardware upfront — OOM on a sandbox wastes time.\n\n"
|
| 221 |
+
"If you intend to run a training script in this sandbox that uses report_to='trackio', "
|
| 222 |
+
"pass `trackio_space_id` (e.g. '<username>/mlintern-<8char>') and `trackio_project` so they "
|
| 223 |
+
"are set as TRACKIO_SPACE_ID/TRACKIO_PROJECT secrets in the sandbox and the UI can embed the live dashboard.\n\n"
|
| 224 |
"Hardware: " + ", ".join([e.value for e in SpaceHardware]) + ".\n"
|
| 225 |
),
|
| 226 |
"parameters": {
|
|
|
|
| 237 |
"type": "boolean",
|
| 238 |
"description": "If true, create a private Space",
|
| 239 |
},
|
| 240 |
+
"trackio_space_id": {
|
| 241 |
+
"type": "string",
|
| 242 |
+
"description": (
|
| 243 |
+
"Optional. The HF Space hosting the trackio dashboard for runs in this sandbox "
|
| 244 |
+
"(e.g. '<username>/mlintern-<8char>', under YOUR HF namespace). Injected as "
|
| 245 |
+
"TRACKIO_SPACE_ID secret and surfaced to the UI. The Space is auto-created and "
|
| 246 |
+
"seeded with the trackio dashboard — DO NOT pre-create it via hf_repo_git, "
|
| 247 |
+
"that produces an empty Space that breaks the embed."
|
| 248 |
+
),
|
| 249 |
+
},
|
| 250 |
+
"trackio_project": {
|
| 251 |
+
"type": "string",
|
| 252 |
+
"description": (
|
| 253 |
+
"Optional. The trackio project name. Injected as TRACKIO_PROJECT secret and "
|
| 254 |
+
"used by the UI to filter the embedded dashboard to this project."
|
| 255 |
+
),
|
| 256 |
+
},
|
| 257 |
},
|
| 258 |
},
|
| 259 |
}
|
| 260 |
|
| 261 |
|
| 262 |
async def sandbox_create_handler(
|
| 263 |
+
args: dict[str, Any], session: Any = None, tool_call_id: str | None = None
|
| 264 |
) -> tuple[str, bool]:
|
| 265 |
"""Handle sandbox_create tool calls."""
|
| 266 |
hardware = args.get("hardware", "cpu-basic")
|
| 267 |
+
trackio_space_id = args.get("trackio_space_id") or None
|
| 268 |
+
trackio_project = args.get("trackio_project") or None
|
| 269 |
+
|
| 270 |
+
async def _emit_trackio_state(sb: Sandbox) -> None:
|
| 271 |
+
"""Tell the frontend which trackio dashboard to embed for this sandbox."""
|
| 272 |
+
if not (session and tool_call_id and trackio_space_id):
|
| 273 |
+
return
|
| 274 |
+
data: dict[str, Any] = {
|
| 275 |
+
"tool_call_id": tool_call_id,
|
| 276 |
+
"tool": "sandbox_create",
|
| 277 |
+
"state": "running",
|
| 278 |
+
"trackioSpaceId": trackio_space_id,
|
| 279 |
+
}
|
| 280 |
+
if trackio_project:
|
| 281 |
+
data["trackioProject"] = trackio_project
|
| 282 |
+
await session.send_event(Event(event_type="tool_state_change", data=data))
|
| 283 |
|
| 284 |
# If sandbox already exists, return its info
|
| 285 |
if session and getattr(session, "sandbox", None):
|
|
|
|
| 292 |
"Hardware cannot be changed by calling sandbox_create again. "
|
| 293 |
"Delete the existing sandbox first if you need a different tier."
|
| 294 |
)
|
| 295 |
+
await _emit_trackio_state(sb)
|
| 296 |
return (
|
| 297 |
f"Sandbox already active: {sb.space_id}\n"
|
| 298 |
f"URL: {sb.url}\n"
|
|
|
|
| 300 |
f"Use bash/read/write/edit to interact with it."
|
| 301 |
), True
|
| 302 |
|
| 303 |
+
create_kwargs: dict[str, Any] = {}
|
| 304 |
if "private" in args:
|
| 305 |
create_kwargs["private"] = args["private"]
|
| 306 |
|
| 307 |
+
extra_secrets: dict[str, str] = {}
|
| 308 |
+
if trackio_space_id:
|
| 309 |
+
extra_secrets["TRACKIO_SPACE_ID"] = trackio_space_id
|
| 310 |
+
await _seed_trackio_dashboard_safe(session, trackio_space_id)
|
| 311 |
+
if trackio_project:
|
| 312 |
+
extra_secrets["TRACKIO_PROJECT"] = trackio_project
|
| 313 |
+
|
| 314 |
try:
|
| 315 |
+
sb, error = await _ensure_sandbox(
|
| 316 |
+
session,
|
| 317 |
+
hardware=hardware,
|
| 318 |
+
extra_secrets=extra_secrets or None,
|
| 319 |
+
**create_kwargs,
|
| 320 |
+
)
|
| 321 |
except Exception as e:
|
| 322 |
return f"Failed to create sandbox: {e}", False
|
| 323 |
|
| 324 |
if error:
|
| 325 |
return error, False
|
| 326 |
|
| 327 |
+
await _emit_trackio_state(sb)
|
| 328 |
+
|
| 329 |
return (
|
| 330 |
f"Sandbox created: {sb.space_id}\n"
|
| 331 |
f"URL: {sb.url}\n"
|
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Seed an HF Space with the trackio dashboard.
|
| 2 |
+
|
| 3 |
+
Background: when the agent creates a Space via `hf_repo_git create_repo` (or
|
| 4 |
+
the user pre-creates one), it ships with no app.py — so the iframe shows the
|
| 5 |
+
default Gradio "Get started" template instead of charts. Trackio's `init()`
|
| 6 |
+
detects the existing Space but does NOT auto-bootstrap dashboard files into it,
|
| 7 |
+
so the dashboard never materializes.
|
| 8 |
+
|
| 9 |
+
This helper writes the three files trackio's runtime expects (README.md,
|
| 10 |
+
requirements.txt, app.py) into the Space, idempotently, BEFORE the job that
|
| 11 |
+
will call `trackio.init()` runs. We deliberately omit `hf_oauth: true` from
|
| 12 |
+
the README so the embedded iframe in ml-intern renders without a login click —
|
| 13 |
+
per-user privacy is enforced by namespace ownership instead.
|
| 14 |
+
|
| 15 |
+
Beyond the dashboard files, the helper also creates the metrics bucket and
|
| 16 |
+
mounts it on the Space at `/data` (with `TRACKIO_DIR` / `TRACKIO_BUCKET_ID`
|
| 17 |
+
Space variables). Without this, the running job writes metrics into a bucket
|
| 18 |
+
that the dashboard Space can't read, and the iframe shows "No projects".
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import io
|
| 24 |
+
from typing import Callable, Optional
|
| 25 |
+
|
| 26 |
+
from huggingface_hub import (
|
| 27 |
+
HfApi,
|
| 28 |
+
Volume,
|
| 29 |
+
add_space_variable,
|
| 30 |
+
create_bucket,
|
| 31 |
+
create_repo,
|
| 32 |
+
)
|
| 33 |
+
from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
_README = """---
|
| 37 |
+
title: Trackio Dashboard
|
| 38 |
+
emoji: 📊
|
| 39 |
+
colorFrom: pink
|
| 40 |
+
colorTo: gray
|
| 41 |
+
sdk: gradio
|
| 42 |
+
app_file: app.py
|
| 43 |
+
pinned: false
|
| 44 |
+
tags:
|
| 45 |
+
- trackio
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
Embedded trackio dashboard for ml-intern runs.
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
_REQUIREMENTS = "trackio\n"
|
| 52 |
+
_APP_PY = "import trackio\ntrackio.show()\n"
|
| 53 |
+
|
| 54 |
+
# ml-intern brand mark surfaced inside the trackio dashboard. Trackio reads
|
| 55 |
+
# `TRACKIO_LOGO_LIGHT_URL` / `TRACKIO_LOGO_DARK_URL` from Space variables and
|
| 56 |
+
# renders them in place of its own logo. We point at the publicly-resolvable
|
| 57 |
+
# copy on the smolagents/ml-intern Space repo so any seeded dashboard inherits
|
| 58 |
+
# the ml-intern branding without each user having to host the asset.
|
| 59 |
+
_LOGO_URL = (
|
| 60 |
+
"https://huggingface.co/spaces/smolagents/ml-intern/"
|
| 61 |
+
"resolve/main/frontend/public/smolagents.webp"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
_FILES = {
|
| 65 |
+
"README.md": _README,
|
| 66 |
+
"requirements.txt": _REQUIREMENTS,
|
| 67 |
+
"app.py": _APP_PY,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _already_seeded(api: HfApi, space_id: str) -> bool:
|
| 72 |
+
"""Cheap check: does the Space already have a trackio dashboard app.py?
|
| 73 |
+
|
| 74 |
+
Avoids re-uploading the same three files on every job submission. We look
|
| 75 |
+
for the literal `trackio.show` call which is the load-bearing line — any
|
| 76 |
+
other app.py shape (the default gradio shell, a stale custom one) means
|
| 77 |
+
we should re-seed.
|
| 78 |
+
"""
|
| 79 |
+
try:
|
| 80 |
+
path = api.hf_hub_download(
|
| 81 |
+
repo_id=space_id, repo_type="space", filename="app.py"
|
| 82 |
+
)
|
| 83 |
+
except (EntryNotFoundError, RepositoryNotFoundError, OSError):
|
| 84 |
+
return False
|
| 85 |
+
try:
|
| 86 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 87 |
+
return "trackio.show" in f.read()
|
| 88 |
+
except OSError:
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _get_space_volumes(api: HfApi, space_id: str) -> list:
|
| 93 |
+
"""Return mounted volumes for a Space.
|
| 94 |
+
|
| 95 |
+
`get_space_runtime()` doesn't always populate `volumes` even when the
|
| 96 |
+
mount exists; mirror trackio's fallback to `space_info().runtime.volumes`.
|
| 97 |
+
"""
|
| 98 |
+
runtime = api.get_space_runtime(space_id)
|
| 99 |
+
if getattr(runtime, "volumes", None):
|
| 100 |
+
return list(runtime.volumes)
|
| 101 |
+
info = api.space_info(space_id)
|
| 102 |
+
if info.runtime and getattr(info.runtime, "volumes", None):
|
| 103 |
+
return list(info.runtime.volumes)
|
| 104 |
+
return []
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _ensure_bucket_mounted(
|
| 108 |
+
api: HfApi,
|
| 109 |
+
space_id: str,
|
| 110 |
+
bucket_id: str,
|
| 111 |
+
hf_token: str,
|
| 112 |
+
log: Optional[Callable[[str], None]] = None,
|
| 113 |
+
) -> None:
|
| 114 |
+
"""Create the bucket if missing, mount it at `/data` on the Space, and
|
| 115 |
+
set the `TRACKIO_DIR` / `TRACKIO_BUCKET_ID` Space variables. Idempotent —
|
| 116 |
+
skips work that has already been done.
|
| 117 |
+
"""
|
| 118 |
+
create_bucket(bucket_id, private=True, exist_ok=True, token=hf_token)
|
| 119 |
+
|
| 120 |
+
existing = _get_space_volumes(api, space_id)
|
| 121 |
+
already_mounted = any(
|
| 122 |
+
getattr(v, "type", None) == "bucket"
|
| 123 |
+
and getattr(v, "source", None) == bucket_id
|
| 124 |
+
and getattr(v, "mount_path", None) == "/data"
|
| 125 |
+
for v in existing
|
| 126 |
+
)
|
| 127 |
+
if not already_mounted:
|
| 128 |
+
preserved = [
|
| 129 |
+
v
|
| 130 |
+
for v in existing
|
| 131 |
+
if not (
|
| 132 |
+
getattr(v, "type", None) == "bucket"
|
| 133 |
+
and (
|
| 134 |
+
getattr(v, "source", None) == bucket_id
|
| 135 |
+
or getattr(v, "mount_path", None) == "/data"
|
| 136 |
+
)
|
| 137 |
+
)
|
| 138 |
+
]
|
| 139 |
+
api.set_space_volumes(
|
| 140 |
+
space_id,
|
| 141 |
+
preserved + [Volume(type="bucket", source=bucket_id, mount_path="/data")],
|
| 142 |
+
)
|
| 143 |
+
if log:
|
| 144 |
+
log(f"mounted bucket {bucket_id} at /data on {space_id}")
|
| 145 |
+
|
| 146 |
+
variables = api.get_space_variables(space_id)
|
| 147 |
+
desired = {
|
| 148 |
+
"TRACKIO_DIR": "/data/trackio",
|
| 149 |
+
"TRACKIO_BUCKET_ID": bucket_id,
|
| 150 |
+
"TRACKIO_LOGO_LIGHT_URL": _LOGO_URL,
|
| 151 |
+
"TRACKIO_LOGO_DARK_URL": _LOGO_URL,
|
| 152 |
+
}
|
| 153 |
+
for key, value in desired.items():
|
| 154 |
+
if getattr(variables.get(key), "value", None) != value:
|
| 155 |
+
add_space_variable(space_id, key, value, token=hf_token)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def ensure_trackio_dashboard(
|
| 159 |
+
space_id: str,
|
| 160 |
+
hf_token: str,
|
| 161 |
+
log: Optional[Callable[[str], None]] = None,
|
| 162 |
+
) -> bool:
|
| 163 |
+
"""Make sure *space_id* is fully wired for trackio:
|
| 164 |
+
1. Space exists with our dashboard files (README without `hf_oauth`,
|
| 165 |
+
`requirements.txt`, `app.py` calling `trackio.show`).
|
| 166 |
+
2. Bucket `<space_id>-bucket` exists, is mounted at `/data`, and the
|
| 167 |
+
Space has `TRACKIO_DIR` / `TRACKIO_BUCKET_ID` variables set.
|
| 168 |
+
|
| 169 |
+
Idempotent — re-running is cheap. Returns True if any seeding happened
|
| 170 |
+
in step (1), False if the dashboard files were already in place. Bucket
|
| 171 |
+
mount is always re-checked.
|
| 172 |
+
"""
|
| 173 |
+
api = HfApi(token=hf_token)
|
| 174 |
+
|
| 175 |
+
create_repo(
|
| 176 |
+
repo_id=space_id,
|
| 177 |
+
repo_type="space",
|
| 178 |
+
space_sdk="gradio",
|
| 179 |
+
exist_ok=True,
|
| 180 |
+
token=hf_token,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
seeded_files = False
|
| 184 |
+
if _already_seeded(api, space_id):
|
| 185 |
+
if log:
|
| 186 |
+
log(f"trackio dashboard already seeded on {space_id}")
|
| 187 |
+
else:
|
| 188 |
+
if log:
|
| 189 |
+
log(f"seeding trackio dashboard files into {space_id}")
|
| 190 |
+
for path_in_repo, content in _FILES.items():
|
| 191 |
+
api.upload_file(
|
| 192 |
+
path_or_fileobj=io.BytesIO(content.encode("utf-8")),
|
| 193 |
+
path_in_repo=path_in_repo,
|
| 194 |
+
repo_id=space_id,
|
| 195 |
+
repo_type="space",
|
| 196 |
+
commit_message=f"ml-intern: seed trackio dashboard ({path_in_repo})",
|
| 197 |
+
)
|
| 198 |
+
seeded_files = True
|
| 199 |
+
|
| 200 |
+
bucket_id = f"{space_id}-bucket"
|
| 201 |
+
_ensure_bucket_mounted(api, space_id, bucket_id, hf_token, log)
|
| 202 |
+
|
| 203 |
+
if log:
|
| 204 |
+
log(f"trackio dashboard ready: https://huggingface.co/spaces/{space_id}")
|
| 205 |
+
return seeded_files
|
|
@@ -220,6 +220,194 @@ function ResearchSteps({ steps }: { steps: string[] }) {
|
|
| 220 |
);
|
| 221 |
}
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
// ---------------------------------------------------------------------------
|
| 224 |
// Hardware pricing ($/hr) — from HF Spaces & Jobs pricing
|
| 225 |
// ---------------------------------------------------------------------------
|
|
@@ -517,7 +705,7 @@ function InlineApproval({
|
|
| 517 |
const EMPTY_AGENTS: Record<string, ResearchAgentState> = {};
|
| 518 |
|
| 519 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 520 |
-
const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, setToolError, getToolError, setToolRejected, getToolRejected } = useAgentStore();
|
| 521 |
const researchAgents = useAgentStore(s => {
|
| 522 |
const activeId = s.activeSessionId;
|
| 523 |
return (activeId && s.sessionStates[activeId]?.researchAgents) || EMPTY_AGENTS;
|
|
@@ -1063,6 +1251,18 @@ export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProp
|
|
| 1063 |
<ResearchSteps steps={researchAgents[tool.toolCallId].steps} />
|
| 1064 |
)}
|
| 1065 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1066 |
{/* Per-tool approval: undecided */}
|
| 1067 |
{isPending && !localDecision && !isSubmitting && (
|
| 1068 |
<InlineApproval
|
|
|
|
| 220 |
);
|
| 221 |
}
|
| 222 |
|
| 223 |
+
// ---------------------------------------------------------------------------
|
| 224 |
+
// Trackio dashboard embed
|
| 225 |
+
// ---------------------------------------------------------------------------
|
| 226 |
+
|
| 227 |
+
// HF repo IDs are `<owner>/<name>` where each segment is alphanumerics plus
|
| 228 |
+
// `_`, `.`, `-`. Anything else (slashes, spaces, query params, missing owner)
|
| 229 |
+
// would let an attacker-controlled string redirect the embed to a different
|
| 230 |
+
// Space, so we refuse to render rather than build a malformed URL.
|
| 231 |
+
const SPACE_ID_PATTERN = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
| 232 |
+
|
| 233 |
+
function isValidSpaceId(spaceId: string): boolean {
|
| 234 |
+
return SPACE_ID_PATTERN.test(spaceId);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/** HF Space embed subdomain: 'user/space_name' → 'user-space-name'. */
|
| 238 |
+
function spaceIdToSubdomain(spaceId: string): string {
|
| 239 |
+
return spaceId
|
| 240 |
+
.toLowerCase()
|
| 241 |
+
.replace(/[/_.]/g, '-')
|
| 242 |
+
.replace(/-+/g, '-')
|
| 243 |
+
.replace(/^-|-$/g, '');
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
function buildTrackioEmbedUrl(spaceId: string, project?: string): string {
|
| 247 |
+
// __theme=dark is gradio's standard query param to force the embedded
|
| 248 |
+
// dashboard into dark mode so it blends with the surrounding chat instead
|
| 249 |
+
// of flashing a bright white panel inside the dark UI.
|
| 250 |
+
const params = new URLSearchParams({
|
| 251 |
+
sidebar: 'hidden',
|
| 252 |
+
footer: 'false',
|
| 253 |
+
__theme: 'dark',
|
| 254 |
+
});
|
| 255 |
+
if (project) params.set('project', project);
|
| 256 |
+
return `https://${spaceIdToSubdomain(spaceId)}.hf.space/?${params.toString()}`;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
function buildTrackioPageUrl(spaceId: string, project?: string): string {
|
| 260 |
+
const qs = project ? `?${new URLSearchParams({ project }).toString()}` : '';
|
| 261 |
+
return `https://huggingface.co/spaces/${spaceId}${qs}`;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
function TrackioEmbed({ spaceId, project }: { spaceId: string; project?: string }) {
|
| 265 |
+
const [expanded, setExpanded] = useState(true);
|
| 266 |
+
const [iframeLoaded, setIframeLoaded] = useState(false);
|
| 267 |
+
const embedUrl = useMemo(() => buildTrackioEmbedUrl(spaceId, project), [spaceId, project]);
|
| 268 |
+
const pageUrl = useMemo(() => buildTrackioPageUrl(spaceId, project), [spaceId, project]);
|
| 269 |
+
const label = project ? `${spaceId} · ${project}` : spaceId;
|
| 270 |
+
|
| 271 |
+
if (!isValidSpaceId(spaceId)) return null;
|
| 272 |
+
|
| 273 |
+
return (
|
| 274 |
+
<Box sx={{ pl: 4.5, pr: 1.5, pb: 1, pt: 0.25 }}>
|
| 275 |
+
<Box
|
| 276 |
+
sx={{
|
| 277 |
+
border: '1px solid var(--tool-border)',
|
| 278 |
+
borderRadius: '8px',
|
| 279 |
+
overflow: 'hidden',
|
| 280 |
+
bgcolor: 'var(--code-panel-bg)',
|
| 281 |
+
}}
|
| 282 |
+
>
|
| 283 |
+
<Stack
|
| 284 |
+
direction="row"
|
| 285 |
+
alignItems="center"
|
| 286 |
+
spacing={1}
|
| 287 |
+
onClick={(e) => e.stopPropagation()}
|
| 288 |
+
sx={{
|
| 289 |
+
px: 1.25,
|
| 290 |
+
py: 0.5,
|
| 291 |
+
borderBottom: expanded ? '1px solid var(--tool-border)' : 'none',
|
| 292 |
+
}}
|
| 293 |
+
>
|
| 294 |
+
<Typography
|
| 295 |
+
sx={{
|
| 296 |
+
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
|
| 297 |
+
fontSize: '0.65rem',
|
| 298 |
+
fontWeight: 600,
|
| 299 |
+
color: 'var(--accent-yellow)',
|
| 300 |
+
letterSpacing: '0.04em',
|
| 301 |
+
}}
|
| 302 |
+
>
|
| 303 |
+
trackio
|
| 304 |
+
</Typography>
|
| 305 |
+
<Typography
|
| 306 |
+
sx={{
|
| 307 |
+
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
|
| 308 |
+
fontSize: '0.65rem',
|
| 309 |
+
color: 'var(--muted-text)',
|
| 310 |
+
flex: 1,
|
| 311 |
+
minWidth: 0,
|
| 312 |
+
overflow: 'hidden',
|
| 313 |
+
textOverflow: 'ellipsis',
|
| 314 |
+
whiteSpace: 'nowrap',
|
| 315 |
+
}}
|
| 316 |
+
>
|
| 317 |
+
{label}
|
| 318 |
+
</Typography>
|
| 319 |
+
<Link
|
| 320 |
+
href={pageUrl}
|
| 321 |
+
target="_blank"
|
| 322 |
+
rel="noopener noreferrer"
|
| 323 |
+
onClick={(e) => e.stopPropagation()}
|
| 324 |
+
sx={{
|
| 325 |
+
display: 'inline-flex',
|
| 326 |
+
alignItems: 'center',
|
| 327 |
+
gap: 0.4,
|
| 328 |
+
color: 'var(--accent-yellow)',
|
| 329 |
+
fontSize: '0.65rem',
|
| 330 |
+
textDecoration: 'none',
|
| 331 |
+
'&:hover': { textDecoration: 'underline' },
|
| 332 |
+
}}
|
| 333 |
+
>
|
| 334 |
+
<LaunchIcon sx={{ fontSize: 11 }} />
|
| 335 |
+
Open
|
| 336 |
+
</Link>
|
| 337 |
+
<Button
|
| 338 |
+
size="small"
|
| 339 |
+
onClick={(e) => {
|
| 340 |
+
e.stopPropagation();
|
| 341 |
+
setExpanded((v) => !v);
|
| 342 |
+
}}
|
| 343 |
+
sx={{
|
| 344 |
+
textTransform: 'none',
|
| 345 |
+
minWidth: 'auto',
|
| 346 |
+
px: 0.75,
|
| 347 |
+
py: 0,
|
| 348 |
+
fontSize: '0.65rem',
|
| 349 |
+
color: 'var(--muted-text)',
|
| 350 |
+
'&:hover': { color: 'var(--text)', bgcolor: 'transparent' },
|
| 351 |
+
}}
|
| 352 |
+
>
|
| 353 |
+
{expanded ? 'Hide' : 'Show'}
|
| 354 |
+
</Button>
|
| 355 |
+
</Stack>
|
| 356 |
+
{expanded && (
|
| 357 |
+
<Box sx={{ position: 'relative', width: '100%', height: 480, bgcolor: 'var(--code-panel-bg)' }}>
|
| 358 |
+
<iframe
|
| 359 |
+
src={embedUrl}
|
| 360 |
+
title={`Trackio dashboard ${label}`}
|
| 361 |
+
loading="lazy"
|
| 362 |
+
onLoad={() => setIframeLoaded(true)}
|
| 363 |
+
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-modals"
|
| 364 |
+
style={{ border: 0, width: '100%', height: '100%', display: 'block' }}
|
| 365 |
+
/>
|
| 366 |
+
{!iframeLoaded && (
|
| 367 |
+
<Stack
|
| 368 |
+
direction="column"
|
| 369 |
+
alignItems="center"
|
| 370 |
+
justifyContent="center"
|
| 371 |
+
spacing={1.5}
|
| 372 |
+
sx={{
|
| 373 |
+
position: 'absolute',
|
| 374 |
+
inset: 0,
|
| 375 |
+
bgcolor: 'var(--code-panel-bg)',
|
| 376 |
+
color: 'var(--muted-text)',
|
| 377 |
+
pointerEvents: 'none',
|
| 378 |
+
}}
|
| 379 |
+
>
|
| 380 |
+
<CircularProgress size={20} sx={{ color: 'var(--accent-yellow)' }} />
|
| 381 |
+
<Typography
|
| 382 |
+
sx={{
|
| 383 |
+
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
|
| 384 |
+
fontSize: '0.75rem',
|
| 385 |
+
color: 'var(--text)',
|
| 386 |
+
}}
|
| 387 |
+
>
|
| 388 |
+
Spinning up the trackio dashboard…
|
| 389 |
+
</Typography>
|
| 390 |
+
<Typography
|
| 391 |
+
sx={{
|
| 392 |
+
fontFamily: '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
|
| 393 |
+
fontSize: '0.65rem',
|
| 394 |
+
color: 'var(--muted-text)',
|
| 395 |
+
textAlign: 'center',
|
| 396 |
+
maxWidth: 360,
|
| 397 |
+
px: 2,
|
| 398 |
+
}}
|
| 399 |
+
>
|
| 400 |
+
First load takes 30–60 seconds. Charts appear automatically once the run starts logging.
|
| 401 |
+
</Typography>
|
| 402 |
+
</Stack>
|
| 403 |
+
)}
|
| 404 |
+
</Box>
|
| 405 |
+
)}
|
| 406 |
+
</Box>
|
| 407 |
+
</Box>
|
| 408 |
+
);
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
// ---------------------------------------------------------------------------
|
| 412 |
// Hardware pricing ($/hr) — from HF Spaces & Jobs pricing
|
| 413 |
// ---------------------------------------------------------------------------
|
|
|
|
| 705 |
const EMPTY_AGENTS: Record<string, ResearchAgentState> = {};
|
| 706 |
|
| 707 |
export default function ToolCallGroup({ tools, approveTools }: ToolCallGroupProps) {
|
| 708 |
+
const { setPanel, lockPanel, getJobUrl, getEditedScript, setJobStatus, getJobStatus, getTrackioDashboard, setToolError, getToolError, setToolRejected, getToolRejected } = useAgentStore();
|
| 709 |
const researchAgents = useAgentStore(s => {
|
| 710 |
const activeId = s.activeSessionId;
|
| 711 |
return (activeId && s.sessionStates[activeId]?.researchAgents) || EMPTY_AGENTS;
|
|
|
|
| 1251 |
<ResearchSteps steps={researchAgents[tool.toolCallId].steps} />
|
| 1252 |
)}
|
| 1253 |
|
| 1254 |
+
{/* Trackio dashboard embed — shown for hf_jobs / sandbox_create runs that declared a trackio space */}
|
| 1255 |
+
{(tool.toolName === 'hf_jobs' || tool.toolName === 'sandbox_create')
|
| 1256 |
+
&& !isPending
|
| 1257 |
+
&& !isRejected
|
| 1258 |
+
&& !cancelled
|
| 1259 |
+
&& (() => {
|
| 1260 |
+
const trackio = getTrackioDashboard(tool.toolCallId);
|
| 1261 |
+
return trackio
|
| 1262 |
+
? <TrackioEmbed spaceId={trackio.spaceId} project={trackio.project} />
|
| 1263 |
+
: null;
|
| 1264 |
+
})()}
|
| 1265 |
+
|
| 1266 |
{/* Per-tool approval: undecided */}
|
| 1267 |
{isPending && !localDecision && !isSubmitting && (
|
| 1268 |
<InlineApproval
|
|
@@ -226,12 +226,17 @@ function createEventToChunkStream(sideChannel: SideChannelCallbacks): TransformS
|
|
| 226 |
const state = (event.data?.state as string) || '';
|
| 227 |
const toolName = (event.data?.tool as string) || '';
|
| 228 |
const jobUrl = (event.data?.jobUrl as string) || undefined;
|
|
|
|
|
|
|
| 229 |
|
| 230 |
if (tcId.startsWith('plan_tool')) break;
|
| 231 |
|
| 232 |
if (jobUrl && tcId) {
|
| 233 |
useAgentStore.getState().setJobUrl(tcId, jobUrl);
|
| 234 |
}
|
|
|
|
|
|
|
|
|
|
| 235 |
if (state === 'running' && toolName) {
|
| 236 |
sideChannel.onToolRunning(toolName);
|
| 237 |
}
|
|
|
|
| 226 |
const state = (event.data?.state as string) || '';
|
| 227 |
const toolName = (event.data?.tool as string) || '';
|
| 228 |
const jobUrl = (event.data?.jobUrl as string) || undefined;
|
| 229 |
+
const trackioSpaceId = (event.data?.trackioSpaceId as string) || undefined;
|
| 230 |
+
const trackioProject = (event.data?.trackioProject as string) || undefined;
|
| 231 |
|
| 232 |
if (tcId.startsWith('plan_tool')) break;
|
| 233 |
|
| 234 |
if (jobUrl && tcId) {
|
| 235 |
useAgentStore.getState().setJobUrl(tcId, jobUrl);
|
| 236 |
}
|
| 237 |
+
if (trackioSpaceId && tcId) {
|
| 238 |
+
useAgentStore.getState().setTrackioDashboard(tcId, trackioSpaceId, trackioProject);
|
| 239 |
+
}
|
| 240 |
if (state === 'running' && toolName) {
|
| 241 |
sideChannel.onToolRunning(toolName);
|
| 242 |
}
|
|
@@ -147,6 +147,11 @@ interface AgentStore {
|
|
| 147 |
// Job statuses (tool_call_id -> job status) for HF jobs
|
| 148 |
jobStatuses: Record<string, string>;
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
// Tool error states (tool_call_id -> true if errored) - persisted across renders
|
| 151 |
toolErrors: Record<string, boolean>;
|
| 152 |
|
|
@@ -200,6 +205,9 @@ interface AgentStore {
|
|
| 200 |
setJobStatus: (toolCallId: string, status: string) => void;
|
| 201 |
getJobStatus: (toolCallId: string) => string | undefined;
|
| 202 |
|
|
|
|
|
|
|
|
|
|
| 203 |
setToolError: (toolCallId: string, hasError: boolean) => void;
|
| 204 |
getToolError: (toolCallId: string) => boolean | undefined;
|
| 205 |
|
|
@@ -264,6 +272,26 @@ function saveRejectedTools(rejected: Record<string, boolean>): void {
|
|
| 264 |
}
|
| 265 |
}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
export const useAgentStore = create<AgentStore>()((set, get) => ({
|
| 268 |
sessionStates: {},
|
| 269 |
activeSessionId: null,
|
|
@@ -287,6 +315,7 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
|
|
| 287 |
approvalNamespaces: {},
|
| 288 |
jobUrls: {},
|
| 289 |
jobStatuses: {},
|
|
|
|
| 290 |
toolErrors: loadToolErrors(),
|
| 291 |
rejectedTools: loadRejectedTools(),
|
| 292 |
|
|
@@ -485,6 +514,26 @@ export const useAgentStore = create<AgentStore>()((set, get) => ({
|
|
| 485 |
|
| 486 |
getJobStatus: (toolCallId) => get().jobStatuses[toolCallId],
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
// ── Tool Errors ─────────────────────────────────────────────────────
|
| 489 |
|
| 490 |
setToolError: (toolCallId, hasError) => {
|
|
|
|
| 147 |
// Job statuses (tool_call_id -> job status) for HF jobs
|
| 148 |
jobStatuses: Record<string, string>;
|
| 149 |
|
| 150 |
+
// Trackio dashboard config per tool call (tool_call_id -> {spaceId, project?})
|
| 151 |
+
// Set by hf_jobs / sandbox_create tools when the agent declares trackio_space_id;
|
| 152 |
+
// the UI uses it to embed the live dashboard via an iframe.
|
| 153 |
+
trackioDashboards: Record<string, { spaceId: string; project?: string }>;
|
| 154 |
+
|
| 155 |
// Tool error states (tool_call_id -> true if errored) - persisted across renders
|
| 156 |
toolErrors: Record<string, boolean>;
|
| 157 |
|
|
|
|
| 205 |
setJobStatus: (toolCallId: string, status: string) => void;
|
| 206 |
getJobStatus: (toolCallId: string) => string | undefined;
|
| 207 |
|
| 208 |
+
setTrackioDashboard: (toolCallId: string, spaceId: string, project?: string) => void;
|
| 209 |
+
getTrackioDashboard: (toolCallId: string) => { spaceId: string; project?: string } | undefined;
|
| 210 |
+
|
| 211 |
setToolError: (toolCallId: string, hasError: boolean) => void;
|
| 212 |
getToolError: (toolCallId: string) => boolean | undefined;
|
| 213 |
|
|
|
|
| 272 |
}
|
| 273 |
}
|
| 274 |
|
| 275 |
+
// Trackio dashboards survive a page reload — without persistence the iframe
|
| 276 |
+
// disappears whenever the user refreshes mid-job, which is the exact moment
|
| 277 |
+
// they'd want to keep watching it.
|
| 278 |
+
function loadTrackioDashboards(): Record<string, { spaceId: string; project?: string }> {
|
| 279 |
+
try {
|
| 280 |
+
const stored = localStorage.getItem('hf-agent-trackio-dashboards');
|
| 281 |
+
return stored ? JSON.parse(stored) : {};
|
| 282 |
+
} catch {
|
| 283 |
+
return {};
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function saveTrackioDashboards(dashboards: Record<string, { spaceId: string; project?: string }>): void {
|
| 288 |
+
try {
|
| 289 |
+
localStorage.setItem('hf-agent-trackio-dashboards', JSON.stringify(dashboards));
|
| 290 |
+
} catch (e) {
|
| 291 |
+
console.warn('Failed to persist trackio dashboards:', e);
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
export const useAgentStore = create<AgentStore>()((set, get) => ({
|
| 296 |
sessionStates: {},
|
| 297 |
activeSessionId: null,
|
|
|
|
| 315 |
approvalNamespaces: {},
|
| 316 |
jobUrls: {},
|
| 317 |
jobStatuses: {},
|
| 318 |
+
trackioDashboards: loadTrackioDashboards(),
|
| 319 |
toolErrors: loadToolErrors(),
|
| 320 |
rejectedTools: loadRejectedTools(),
|
| 321 |
|
|
|
|
| 514 |
|
| 515 |
getJobStatus: (toolCallId) => get().jobStatuses[toolCallId],
|
| 516 |
|
| 517 |
+
// ── Trackio Dashboards ──────────────────────────────────────────────
|
| 518 |
+
|
| 519 |
+
setTrackioDashboard: (toolCallId, spaceId, project) => {
|
| 520 |
+
set((state) => {
|
| 521 |
+
const existing = state.trackioDashboards[toolCallId];
|
| 522 |
+
// Don't churn the object if nothing changed (avoids extra renders).
|
| 523 |
+
if (existing && existing.spaceId === spaceId && existing.project === project) {
|
| 524 |
+
return {};
|
| 525 |
+
}
|
| 526 |
+
const updated = {
|
| 527 |
+
...state.trackioDashboards,
|
| 528 |
+
[toolCallId]: { spaceId, ...(project ? { project } : {}) },
|
| 529 |
+
};
|
| 530 |
+
saveTrackioDashboards(updated);
|
| 531 |
+
return { trackioDashboards: updated };
|
| 532 |
+
});
|
| 533 |
+
},
|
| 534 |
+
|
| 535 |
+
getTrackioDashboard: (toolCallId) => get().trackioDashboards[toolCallId],
|
| 536 |
+
|
| 537 |
// ── Tool Errors ─────────────────────────────────────────────────────
|
| 538 |
|
| 539 |
setToolError: (toolCallId, hasError) => {
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
| 13 |
"requests>=2.33.0",
|
| 14 |
"litellm>=1.83.0",
|
| 15 |
"boto3>=1.35.0",
|
| 16 |
-
"huggingface-hub>=1.
|
| 17 |
"fastmcp>=3.2.0",
|
| 18 |
"prompt-toolkit>=3.0.0",
|
| 19 |
"thefuzz>=0.22.1",
|
|
|
|
| 13 |
"requests>=2.33.0",
|
| 14 |
"litellm>=1.83.0",
|
| 15 |
"boto3>=1.35.0",
|
| 16 |
+
"huggingface-hub>=1.12.0",
|
| 17 |
"fastmcp>=3.2.0",
|
| 18 |
"prompt-toolkit>=3.0.0",
|
| 19 |
"thefuzz>=0.22.1",
|
|
@@ -1006,31 +1006,34 @@ wheels = [
|
|
| 1006 |
|
| 1007 |
[[package]]
|
| 1008 |
name = "hf-xet"
|
| 1009 |
-
version = "1.
|
| 1010 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1011 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 1012 |
-
wheels = [
|
| 1013 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1014 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1015 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1016 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1017 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1018 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1019 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1020 |
-
{ url = "https://files.pythonhosted.org/packages/e2/
|
| 1021 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1022 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1023 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1024 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1025 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1026 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1027 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1028 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1029 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1030 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1031 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1032 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1033 |
-
{ url = "https://files.pythonhosted.org/packages/
|
|
|
|
|
|
|
|
|
|
| 1034 |
]
|
| 1035 |
|
| 1036 |
[[package]]
|
|
@@ -1108,7 +1111,7 @@ wheels = [
|
|
| 1108 |
|
| 1109 |
[[package]]
|
| 1110 |
name = "huggingface-hub"
|
| 1111 |
-
version = "1.
|
| 1112 |
source = { registry = "https://pypi.org/simple" }
|
| 1113 |
dependencies = [
|
| 1114 |
{ name = "filelock" },
|
|
@@ -1117,14 +1120,13 @@ dependencies = [
|
|
| 1117 |
{ name = "httpx" },
|
| 1118 |
{ name = "packaging" },
|
| 1119 |
{ name = "pyyaml" },
|
| 1120 |
-
{ name = "shellingham" },
|
| 1121 |
{ name = "tqdm" },
|
| 1122 |
-
{ name = "typer
|
| 1123 |
{ name = "typing-extensions" },
|
| 1124 |
]
|
| 1125 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 1126 |
wheels = [
|
| 1127 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1128 |
]
|
| 1129 |
|
| 1130 |
[[package]]
|
|
@@ -1822,7 +1824,7 @@ requires-dist = [
|
|
| 1822 |
{ name = "fastapi", specifier = ">=0.115.0" },
|
| 1823 |
{ name = "fastmcp", specifier = ">=3.2.0" },
|
| 1824 |
{ name = "httpx", specifier = ">=0.27.0" },
|
| 1825 |
-
{ name = "huggingface-hub", specifier = ">=1.
|
| 1826 |
{ name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
|
| 1827 |
{ name = "litellm", specifier = ">=1.83.0" },
|
| 1828 |
{ name = "ml-intern", extras = ["eval", "dev"], marker = "extra == 'all'" },
|
|
@@ -3607,16 +3609,18 @@ wheels = [
|
|
| 3607 |
]
|
| 3608 |
|
| 3609 |
[[package]]
|
| 3610 |
-
name = "typer
|
| 3611 |
-
version = "0.
|
| 3612 |
source = { registry = "https://pypi.org/simple" }
|
| 3613 |
dependencies = [
|
|
|
|
| 3614 |
{ name = "click" },
|
| 3615 |
-
{ name = "
|
|
|
|
| 3616 |
]
|
| 3617 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 3618 |
wheels = [
|
| 3619 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 3620 |
]
|
| 3621 |
|
| 3622 |
[[package]]
|
|
|
|
| 1006 |
|
| 1007 |
[[package]]
|
| 1008 |
name = "hf-xet"
|
| 1009 |
+
version = "1.4.3"
|
| 1010 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1011 |
+
sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" }
|
| 1012 |
+
wheels = [
|
| 1013 |
+
{ url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" },
|
| 1014 |
+
{ url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" },
|
| 1015 |
+
{ url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" },
|
| 1016 |
+
{ url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" },
|
| 1017 |
+
{ url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" },
|
| 1018 |
+
{ url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" },
|
| 1019 |
+
{ url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" },
|
| 1020 |
+
{ url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" },
|
| 1021 |
+
{ url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" },
|
| 1022 |
+
{ url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" },
|
| 1023 |
+
{ url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" },
|
| 1024 |
+
{ url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" },
|
| 1025 |
+
{ url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" },
|
| 1026 |
+
{ url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" },
|
| 1027 |
+
{ url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" },
|
| 1028 |
+
{ url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" },
|
| 1029 |
+
{ url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" },
|
| 1030 |
+
{ url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" },
|
| 1031 |
+
{ url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" },
|
| 1032 |
+
{ url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" },
|
| 1033 |
+
{ url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" },
|
| 1034 |
+
{ url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" },
|
| 1035 |
+
{ url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" },
|
| 1036 |
+
{ url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" },
|
| 1037 |
]
|
| 1038 |
|
| 1039 |
[[package]]
|
|
|
|
| 1111 |
|
| 1112 |
[[package]]
|
| 1113 |
name = "huggingface-hub"
|
| 1114 |
+
version = "1.12.0"
|
| 1115 |
source = { registry = "https://pypi.org/simple" }
|
| 1116 |
dependencies = [
|
| 1117 |
{ name = "filelock" },
|
|
|
|
| 1120 |
{ name = "httpx" },
|
| 1121 |
{ name = "packaging" },
|
| 1122 |
{ name = "pyyaml" },
|
|
|
|
| 1123 |
{ name = "tqdm" },
|
| 1124 |
+
{ name = "typer" },
|
| 1125 |
{ name = "typing-extensions" },
|
| 1126 |
]
|
| 1127 |
+
sdist = { url = "https://files.pythonhosted.org/packages/56/52/1b54cb569509c725a32c1315261ac9fd0e6b91bbbf74d86fca10d3376164/huggingface_hub-1.12.0.tar.gz", hash = "sha256:7c3fe85e24b652334e5d456d7a812cd9a071e75630fac4365d9165ab5e4a34b6", size = 763091, upload-time = "2026-04-24T13:32:08.674Z" }
|
| 1128 |
wheels = [
|
| 1129 |
+
{ url = "https://files.pythonhosted.org/packages/7e/2b/ef03ddb96bd1123503c2bd6932001020292deea649e9bf4caa2cb65a85bf/huggingface_hub-1.12.0-py3-none-any.whl", hash = "sha256:d74939969585ee35748bd66de09baf84099d461bda7287cd9043bfb99b0e424d", size = 646806, upload-time = "2026-04-24T13:32:06.717Z" },
|
| 1130 |
]
|
| 1131 |
|
| 1132 |
[[package]]
|
|
|
|
| 1824 |
{ name = "fastapi", specifier = ">=0.115.0" },
|
| 1825 |
{ name = "fastmcp", specifier = ">=3.2.0" },
|
| 1826 |
{ name = "httpx", specifier = ">=0.27.0" },
|
| 1827 |
+
{ name = "huggingface-hub", specifier = ">=1.12.0" },
|
| 1828 |
{ name = "inspect-ai", marker = "extra == 'eval'", specifier = ">=0.3.149" },
|
| 1829 |
{ name = "litellm", specifier = ">=1.83.0" },
|
| 1830 |
{ name = "ml-intern", extras = ["eval", "dev"], marker = "extra == 'all'" },
|
|
|
|
| 3609 |
]
|
| 3610 |
|
| 3611 |
[[package]]
|
| 3612 |
+
name = "typer"
|
| 3613 |
+
version = "0.25.0"
|
| 3614 |
source = { registry = "https://pypi.org/simple" }
|
| 3615 |
dependencies = [
|
| 3616 |
+
{ name = "annotated-doc" },
|
| 3617 |
{ name = "click" },
|
| 3618 |
+
{ name = "rich" },
|
| 3619 |
+
{ name = "shellingham" },
|
| 3620 |
]
|
| 3621 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7b/27/ede8cec7596e0041ba7e7b80b47d132562f56ff454313a16f6084e555c9f/typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930", size = 120150, upload-time = "2026-04-26T08:46:14.767Z" }
|
| 3622 |
wheels = [
|
| 3623 |
+
{ url = "https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc", size = 55993, upload-time = "2026-04-26T08:46:15.889Z" },
|
| 3624 |
]
|
| 3625 |
|
| 3626 |
[[package]]
|