Spaces:
Running on CPU Upgrade
Remove ~900 lines of dead code across agent, backend, and frontend (#318)
Browse files* Remove dead code across agent, backend, and frontend
A vulture/knip audit with manual repo-wide verification found ~900 lines
of dead code:
- Delete agent/tools/private_hf_repo_tools.py (650 lines): disabled when
hf_repo_files/hf_repo_git replaced it, never imported since.
- Drop orphaned functions, constants, and type aliases across agent/ and
backend/ (record_jobs_access_blocked, last_fetch_error,
update_local_save_status, verify_session_access, OperationType aliases,
OpType.INTERRUPT in both enums, etc.).
- Trim unused Sandbox client surface (pause/restart/to_dict/
tool_definitions and unused constants); keep Sandbox.connect, which the
live auth integration test uses.
- Remove production code only its own tests kept alive: _needs_approval
(tests retargeted to _base_needs_approval / production path),
fetch_hf_user_plan, _cleanup_user_orphan_sandboxes (superseded by
scripts/sweep_orphan_sandboxes.py), DEFAULT_MODEL_ID alias.
- Drop unused boto3 dependency (still available transitively via the
eval extra for inspect-ai).
- Frontend: delete unused ThinkingIndicator component, 12 orphaned event
payload interfaces, ToolApproval interface, and the duplicate theme
default export.
No behavior change. ruff, 485 unit tests, tsc build, and eslint all pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop stale INTERRUPT reference from agent README
Addresses PR #318 review: OpType.INTERRUPT no longer exists, so remove
it from the handled-operations list in the architecture notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- agent/README.md +1 -1
- agent/core/agent_loop.py +0 -11
- agent/core/hf_access.py +0 -13
- agent/core/hf_router_catalog.py +0 -14
- agent/core/model_ids.py +0 -2
- agent/core/session.py +0 -21
- agent/core/session_persistence.py +0 -7
- agent/core/telemetry.py +0 -24
- agent/core/tools.py +0 -6
- agent/core/usage_thresholds.py +1 -3
- agent/tools/hf_repo_files_tool.py +1 -3
- agent/tools/hf_repo_git_tool.py +1 -18
- agent/tools/jobs_tool.py +1 -19
- agent/tools/papers_tool.py +0 -13
- agent/tools/private_hf_repo_tools.py +0 -650
- agent/tools/sandbox_client.py +0 -27
- agent/tools/sandbox_tool.py +1 -64
- agent/utils/particle_logo.py +0 -4
- backend/dependencies.py +0 -53
- backend/models.py +0 -1
- backend/session_manager.py +0 -21
- frontend/src/components/Chat/ThinkingIndicator.tsx +0 -48
- frontend/src/theme.ts +0 -3
- frontend/src/types/agent.ts +0 -7
- frontend/src/types/events.ts +0 -65
- pyproject.toml +0 -1
- scripts/sweep_orphan_sandboxes.py +0 -1
- tests/integration/test_live_thinking_models.py +3 -3
- tests/unit/test_auto_approval_policy.py +0 -3
- tests/unit/test_hf_access.py +0 -28
- tests/unit/test_sandbox_auto_start.py +11 -5
- tests/unit/test_sandbox_private_spaces.py +0 -27
- uv.lock +0 -2
|
@@ -7,7 +7,7 @@ Async agent loop with LiteLLM.
|
|
| 7 |
**Queue-based async system:**
|
| 8 |
- Submissions in (user input) → Agent Loop → Events output for possible UI updates
|
| 9 |
- Session maintains state (context + tools) for possible future Context Engineering
|
| 10 |
-
- Handlers operations like (USER_INPUT,
|
| 11 |
|
| 12 |
## Components
|
| 13 |
|
|
|
|
| 7 |
**Queue-based async system:**
|
| 8 |
- Submissions in (user input) → Agent Loop → Events output for possible UI updates
|
| 9 |
- Session maintains state (context + tools) for possible future Context Engineering
|
| 10 |
+
- Handlers operations like (USER_INPUT, COMPACT, UNDO, SHUTDOWN) for possible UI control
|
| 11 |
|
| 12 |
## Components
|
| 13 |
|
|
@@ -324,17 +324,6 @@ def _base_needs_approval(
|
|
| 324 |
return False
|
| 325 |
|
| 326 |
|
| 327 |
-
def _needs_approval(
|
| 328 |
-
tool_name: str, tool_args: dict, config: Config | None = None
|
| 329 |
-
) -> bool:
|
| 330 |
-
"""Legacy sync approval predicate used by tests and CLI display helpers."""
|
| 331 |
-
if _is_scheduled_hf_job_run(tool_name, tool_args):
|
| 332 |
-
return True
|
| 333 |
-
if config and config.yolo_mode:
|
| 334 |
-
return False
|
| 335 |
-
return _base_needs_approval(tool_name, tool_args, config)
|
| 336 |
-
|
| 337 |
-
|
| 338 |
def _session_auto_approval_enabled(session: Session | None) -> bool:
|
| 339 |
return bool(session and getattr(session, "auto_approval_enabled", False))
|
| 340 |
|
|
|
|
| 324 |
return False
|
| 325 |
|
| 326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
def _session_auto_approval_enabled(session: Session | None) -> bool:
|
| 328 |
return bool(session and getattr(session, "auto_approval_enabled", False))
|
| 329 |
|
|
@@ -21,7 +21,6 @@ HF_BILLING_URL = "https://huggingface.co/settings/billing"
|
|
| 21 |
HF_PRO_SUBSCRIBE_URL = "https://huggingface.co/subscribe/pro"
|
| 22 |
|
| 23 |
HfUserPlan = Literal["free", "pro"]
|
| 24 |
-
HfUserPlanStatus = Literal["free", "pro", "unknown"]
|
| 25 |
|
| 26 |
|
| 27 |
@dataclass(frozen=True)
|
|
@@ -32,7 +31,6 @@ class JobsAccess:
|
|
| 32 |
org_names: list[str]
|
| 33 |
eligible_namespaces: list[str]
|
| 34 |
default_namespace: str | None
|
| 35 |
-
access_known: bool = True
|
| 36 |
|
| 37 |
|
| 38 |
class JobsAccessError(Exception):
|
|
@@ -126,17 +124,6 @@ async def fetch_whoami_v2(token: str, timeout: float = 5.0) -> dict[str, Any] |
|
|
| 126 |
return None
|
| 127 |
|
| 128 |
|
| 129 |
-
async def fetch_hf_user_plan(
|
| 130 |
-
token: str | None,
|
| 131 |
-
timeout: float = 5.0,
|
| 132 |
-
) -> HfUserPlanStatus:
|
| 133 |
-
"""Return the token owner's HF plan, or ``unknown`` if lookup fails."""
|
| 134 |
-
if not token:
|
| 135 |
-
return "unknown"
|
| 136 |
-
whoami = await fetch_whoami_v2(token, timeout=timeout)
|
| 137 |
-
return normalize_hf_user_plan(whoami) or "unknown"
|
| 138 |
-
|
| 139 |
-
|
| 140 |
async def get_jobs_access(token: str) -> JobsAccess | None:
|
| 141 |
whoami = await fetch_whoami_v2(token)
|
| 142 |
if whoami is None:
|
|
|
|
| 21 |
HF_PRO_SUBSCRIBE_URL = "https://huggingface.co/subscribe/pro"
|
| 22 |
|
| 23 |
HfUserPlan = Literal["free", "pro"]
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
@dataclass(frozen=True)
|
|
|
|
| 31 |
org_names: list[str]
|
| 32 |
eligible_namespaces: list[str]
|
| 33 |
default_namespace: str | None
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class JobsAccessError(Exception):
|
|
|
|
| 124 |
return None
|
| 125 |
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
async def get_jobs_access(token: str) -> JobsAccess | None:
|
| 128 |
whoami = await fetch_whoami_v2(token)
|
| 129 |
if whoami is None:
|
|
@@ -7,7 +7,6 @@ pricing, context length, and tool-use support. We use it to:
|
|
| 7 |
• Validate ``/model`` switches with live data instead of a hard-coded allowlist.
|
| 8 |
• Show the user which providers serve a model, at what price, and whether they
|
| 9 |
support tool calls.
|
| 10 |
-
• Derive a reasonable context-window limit for any routed model.
|
| 11 |
|
| 12 |
The listing is cached in-memory for a few minutes so repeated lookups during a
|
| 13 |
session are free. On fetch failure we return stale data if we have it, or an
|
|
@@ -42,7 +41,6 @@ class ProviderInfo:
|
|
| 42 |
input_price: Optional[float]
|
| 43 |
output_price: Optional[float]
|
| 44 |
supports_tools: bool
|
| 45 |
-
supports_structured_output: bool
|
| 46 |
|
| 47 |
|
| 48 |
@dataclass
|
|
@@ -54,11 +52,6 @@ class ModelInfo:
|
|
| 54 |
def live_providers(self) -> list[ProviderInfo]:
|
| 55 |
return [p for p in self.providers if p.status == "live"]
|
| 56 |
|
| 57 |
-
@property
|
| 58 |
-
def max_context_length(self) -> Optional[int]:
|
| 59 |
-
lengths = [p.context_length for p in self.live_providers if p.context_length]
|
| 60 |
-
return max(lengths) if lengths else None
|
| 61 |
-
|
| 62 |
@property
|
| 63 |
def any_supports_tools(self) -> bool:
|
| 64 |
return any(p.supports_tools for p in self.live_providers)
|
|
@@ -97,9 +90,6 @@ def _parse_entry(entry: dict) -> ModelInfo:
|
|
| 97 |
input_price=pricing.get("input"),
|
| 98 |
output_price=pricing.get("output"),
|
| 99 |
supports_tools=bool(p.get("supports_tools", False)),
|
| 100 |
-
supports_structured_output=bool(
|
| 101 |
-
p.get("supports_structured_output", False)
|
| 102 |
-
),
|
| 103 |
)
|
| 104 |
)
|
| 105 |
return ModelInfo(id=entry.get("id", ""), providers=providers)
|
|
@@ -119,10 +109,6 @@ def lookup(model_id: str) -> Optional[ModelInfo]:
|
|
| 119 |
return None
|
| 120 |
|
| 121 |
|
| 122 |
-
def last_fetch_error() -> Optional[str]:
|
| 123 |
-
return _last_fetch_error
|
| 124 |
-
|
| 125 |
-
|
| 126 |
def fuzzy_suggest(model_id: str, limit: int = 3) -> list[str]:
|
| 127 |
"""Return the closest model ids from the catalog."""
|
| 128 |
bare = model_id.split(":", 1)[0]
|
|
|
|
| 7 |
• Validate ``/model`` switches with live data instead of a hard-coded allowlist.
|
| 8 |
• Show the user which providers serve a model, at what price, and whether they
|
| 9 |
support tool calls.
|
|
|
|
| 10 |
|
| 11 |
The listing is cached in-memory for a few minutes so repeated lookups during a
|
| 12 |
session are free. On fetch failure we return stale data if we have it, or an
|
|
|
|
| 41 |
input_price: Optional[float]
|
| 42 |
output_price: Optional[float]
|
| 43 |
supports_tools: bool
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
@dataclass
|
|
|
|
| 52 |
def live_providers(self) -> list[ProviderInfo]:
|
| 53 |
return [p for p in self.providers if p.status == "live"]
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
@property
|
| 56 |
def any_supports_tools(self) -> bool:
|
| 57 |
return any(p.supports_tools for p in self.live_providers)
|
|
|
|
| 90 |
input_price=pricing.get("input"),
|
| 91 |
output_price=pricing.get("output"),
|
| 92 |
supports_tools=bool(p.get("supports_tools", False)),
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
| 94 |
)
|
| 95 |
return ModelInfo(id=entry.get("id", ""), providers=providers)
|
|
|
|
| 109 |
return None
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def fuzzy_suggest(model_id: str, limit: int = 3) -> list[str]:
|
| 113 |
"""Return the closest model ids from the catalog."""
|
| 114 |
bare = model_id.split(":", 1)[0]
|
|
@@ -10,8 +10,6 @@ MINIMAX_M27_MODEL_ID = "MiniMaxAI/MiniMax-M2.7:novita"
|
|
| 10 |
GLM_51_MODEL_ID = "zai-org/GLM-5.1:novita"
|
| 11 |
DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"
|
| 12 |
|
| 13 |
-
DEFAULT_MODEL_ID = CLAUDE_OPUS_48_MODEL_ID
|
| 14 |
-
|
| 15 |
HOSTED_MODEL_IDS = {
|
| 16 |
CLAUDE_OPUS_48_MODEL_ID,
|
| 17 |
GPT_55_MODEL_ID,
|
|
|
|
| 10 |
GLM_51_MODEL_ID = "zai-org/GLM-5.1:novita"
|
| 11 |
DEEPSEEK_V4_PRO_MODEL_ID = "deepseek-ai/DeepSeek-V4-Pro:novita"
|
| 12 |
|
|
|
|
|
|
|
| 13 |
HOSTED_MODEL_IDS = {
|
| 14 |
CLAUDE_OPUS_48_MODEL_ID,
|
| 15 |
GPT_55_MODEL_ID,
|
|
@@ -80,7 +80,6 @@ def _get_max_tokens_safe(model_name: str) -> int:
|
|
| 80 |
class OpType(Enum):
|
| 81 |
USER_INPUT = "user_input"
|
| 82 |
EXEC_APPROVAL = "exec_approval"
|
| 83 |
-
INTERRUPT = "interrupt"
|
| 84 |
UNDO = "undo"
|
| 85 |
COMPACT = "compact"
|
| 86 |
NEW = "new"
|
|
@@ -631,26 +630,6 @@ class Session:
|
|
| 631 |
logger.error(f"Failed to save session locally: {e}")
|
| 632 |
return None
|
| 633 |
|
| 634 |
-
def update_local_save_status(
|
| 635 |
-
self, filepath: str, upload_status: str, dataset_url: Optional[str] = None
|
| 636 |
-
) -> bool:
|
| 637 |
-
"""Update the upload status of an existing local save file"""
|
| 638 |
-
try:
|
| 639 |
-
with open(filepath, "r") as f:
|
| 640 |
-
data = json.load(f)
|
| 641 |
-
|
| 642 |
-
data["upload_status"] = upload_status
|
| 643 |
-
data["upload_url"] = dataset_url
|
| 644 |
-
data["last_save_time"] = datetime.now().isoformat()
|
| 645 |
-
|
| 646 |
-
with open(filepath, "w") as f:
|
| 647 |
-
json.dump(data, f, indent=2)
|
| 648 |
-
|
| 649 |
-
return True
|
| 650 |
-
except Exception as e:
|
| 651 |
-
logger.error(f"Failed to update local save status: {e}")
|
| 652 |
-
return False
|
| 653 |
-
|
| 654 |
def _personal_trace_repo_id(self) -> Optional[str]:
|
| 655 |
"""Resolve the per-user trace repo id from config + HF username.
|
| 656 |
|
|
|
|
| 80 |
class OpType(Enum):
|
| 81 |
USER_INPUT = "user_input"
|
| 82 |
EXEC_APPROVAL = "exec_approval"
|
|
|
|
| 83 |
UNDO = "undo"
|
| 84 |
COMPACT = "compact"
|
| 85 |
NEW = "new"
|
|
|
|
| 630 |
logger.error(f"Failed to save session locally: {e}")
|
| 631 |
return None
|
| 632 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
def _personal_trace_repo_id(self) -> Optional[str]:
|
| 634 |
"""Resolve the per-user trace repo id from config + HF username.
|
| 635 |
|
|
@@ -518,10 +518,3 @@ def get_session_store() -> NoopSessionStore | MongoSessionStore:
|
|
| 518 |
db_name = os.environ.get("MONGODB_DB", "ml-intern")
|
| 519 |
_store = MongoSessionStore(uri, db_name) if uri else NoopSessionStore()
|
| 520 |
return _store
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
def _reset_store_for_tests(
|
| 524 |
-
store: NoopSessionStore | MongoSessionStore | None = None,
|
| 525 |
-
) -> None:
|
| 526 |
-
global _store
|
| 527 |
-
_store = store
|
|
|
|
| 518 |
db_name = os.environ.get("MONGODB_DB", "ml-intern")
|
| 519 |
_store = MongoSessionStore(uri, db_name) if uri else NoopSessionStore()
|
| 520 |
return _store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -326,30 +326,6 @@ async def record_feedback(
|
|
| 326 |
logger.debug("record_feedback failed (non-fatal): %s", e)
|
| 327 |
|
| 328 |
|
| 329 |
-
async def record_jobs_access_blocked(
|
| 330 |
-
session: Any,
|
| 331 |
-
*,
|
| 332 |
-
tool_call_ids: list[str],
|
| 333 |
-
plan: str,
|
| 334 |
-
eligible_namespaces: list[str],
|
| 335 |
-
) -> None:
|
| 336 |
-
from agent.core.session import Event
|
| 337 |
-
|
| 338 |
-
try:
|
| 339 |
-
await session.send_event(
|
| 340 |
-
Event(
|
| 341 |
-
event_type="jobs_access_blocked",
|
| 342 |
-
data={
|
| 343 |
-
"tool_call_ids": tool_call_ids,
|
| 344 |
-
"plan": plan,
|
| 345 |
-
"eligible_namespaces": eligible_namespaces,
|
| 346 |
-
},
|
| 347 |
-
)
|
| 348 |
-
)
|
| 349 |
-
except Exception as e:
|
| 350 |
-
logger.debug("record_jobs_access_blocked failed (non-fatal): %s", e)
|
| 351 |
-
|
| 352 |
-
|
| 353 |
async def record_pro_cta_click(
|
| 354 |
session: Any,
|
| 355 |
*,
|
|
|
|
| 326 |
logger.debug("record_feedback failed (non-fatal): %s", e)
|
| 327 |
|
| 328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
async def record_pro_cta_click(
|
| 330 |
session: Any,
|
| 331 |
*,
|
|
@@ -51,12 +51,6 @@ from agent.tools.research_tool import RESEARCH_TOOL_SPEC, research_handler
|
|
| 51 |
from agent.tools.sandbox_tool import get_sandbox_tools
|
| 52 |
from agent.tools.web_search_tool import WEB_SEARCH_TOOL_SPEC, web_search_handler
|
| 53 |
|
| 54 |
-
# NOTE: Private HF repo tool disabled - replaced by hf_repo_files and hf_repo_git
|
| 55 |
-
# from agent.tools.private_hf_repo_tools import (
|
| 56 |
-
# PRIVATE_HF_REPO_TOOL_SPEC,
|
| 57 |
-
# private_hf_repo_handler,
|
| 58 |
-
# )
|
| 59 |
-
|
| 60 |
# Suppress aiohttp deprecation warning
|
| 61 |
warnings.filterwarnings(
|
| 62 |
"ignore", category=DeprecationWarning, module="aiohttp.connector"
|
|
|
|
| 51 |
from agent.tools.sandbox_tool import get_sandbox_tools
|
| 52 |
from agent.tools.web_search_tool import WEB_SEARCH_TOOL_SPEC, web_search_handler
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
# Suppress aiohttp deprecation warning
|
| 55 |
warnings.filterwarnings(
|
| 56 |
"ignore", category=DeprecationWarning, module="aiohttp.connector"
|
|
@@ -1,13 +1,11 @@
|
|
| 1 |
"""Helpers for session usage-threshold approval warnings."""
|
| 2 |
|
| 3 |
-
from typing import Any
|
| 4 |
|
| 5 |
USAGE_THRESHOLD_TOOL_NAME = "usage_threshold"
|
| 6 |
USAGE_WARNING_FIRST_THRESHOLD_USD = 5.0
|
| 7 |
USAGE_WARNING_MULTIPLIER = 2.0
|
| 8 |
|
| 9 |
-
UsageApprovalContinuation = Literal["continue_agent", "complete_turn"]
|
| 10 |
-
|
| 11 |
|
| 12 |
def normalize_usage_threshold(value: Any) -> float:
|
| 13 |
"""Return a usable positive threshold, defaulting to the first warning."""
|
|
|
|
| 1 |
"""Helpers for session usage-threshold approval warnings."""
|
| 2 |
|
| 3 |
+
from typing import Any
|
| 4 |
|
| 5 |
USAGE_THRESHOLD_TOOL_NAME = "usage_threshold"
|
| 6 |
USAGE_WARNING_FIRST_THRESHOLD_USD = 5.0
|
| 7 |
USAGE_WARNING_MULTIPLIER = 2.0
|
| 8 |
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def normalize_usage_threshold(value: Any) -> float:
|
| 11 |
"""Return a usable positive threshold, defaulting to the first warning."""
|
|
@@ -5,7 +5,7 @@ Operations: list, read, upload, delete
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import asyncio
|
| 8 |
-
from typing import Any, Dict,
|
| 9 |
|
| 10 |
from huggingface_hub import HfApi, hf_hub_download
|
| 11 |
from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
|
|
@@ -13,8 +13,6 @@ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
|
|
| 13 |
from agent.core.hub_artifacts import is_known_hub_artifact, register_hub_artifact
|
| 14 |
from agent.tools.types import ToolResult
|
| 15 |
|
| 16 |
-
OperationType = Literal["list", "read", "upload", "delete"]
|
| 17 |
-
|
| 18 |
|
| 19 |
async def _async_call(func, *args, **kwargs):
|
| 20 |
"""Wrap synchronous HfApi calls for async context."""
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import asyncio
|
| 8 |
+
from typing import Any, Dict, Optional
|
| 9 |
|
| 10 |
from huggingface_hub import HfApi, hf_hub_download
|
| 11 |
from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
|
|
|
|
| 13 |
from agent.core.hub_artifacts import is_known_hub_artifact, register_hub_artifact
|
| 14 |
from agent.tools.types import ToolResult
|
| 15 |
|
|
|
|
|
|
|
| 16 |
|
| 17 |
async def _async_call(func, *args, **kwargs):
|
| 18 |
"""Wrap synchronous HfApi calls for async context."""
|
|
@@ -5,7 +5,7 @@ Operations: branches, tags, PRs, repo management
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import asyncio
|
| 8 |
-
from typing import Any, Dict,
|
| 9 |
|
| 10 |
from huggingface_hub import HfApi
|
| 11 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
@@ -13,23 +13,6 @@ from huggingface_hub.utils import RepositoryNotFoundError
|
|
| 13 |
from agent.core.hub_artifacts import register_hub_artifact
|
| 14 |
from agent.tools.types import ToolResult
|
| 15 |
|
| 16 |
-
OperationType = Literal[
|
| 17 |
-
"create_branch",
|
| 18 |
-
"delete_branch",
|
| 19 |
-
"create_tag",
|
| 20 |
-
"delete_tag",
|
| 21 |
-
"list_refs",
|
| 22 |
-
"create_pr",
|
| 23 |
-
"list_prs",
|
| 24 |
-
"get_pr",
|
| 25 |
-
"merge_pr",
|
| 26 |
-
"close_pr",
|
| 27 |
-
"comment_pr",
|
| 28 |
-
"change_pr_status",
|
| 29 |
-
"create_repo",
|
| 30 |
-
"update_repo",
|
| 31 |
-
]
|
| 32 |
-
|
| 33 |
|
| 34 |
async def _async_call(func, *args, **kwargs):
|
| 35 |
"""Wrap synchronous HfApi calls for async context."""
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import asyncio
|
| 8 |
+
from typing import Any, Dict, Optional
|
| 9 |
|
| 10 |
from huggingface_hub import HfApi
|
| 11 |
from huggingface_hub.utils import RepositoryNotFoundError
|
|
|
|
| 13 |
from agent.core.hub_artifacts import register_hub_artifact
|
| 14 |
from agent.tools.types import ToolResult
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
async def _async_call(func, *args, **kwargs):
|
| 18 |
"""Wrap synchronous HfApi calls for async context."""
|
|
@@ -10,7 +10,7 @@ import http.client
|
|
| 10 |
import logging
|
| 11 |
import re
|
| 12 |
import shlex
|
| 13 |
-
from typing import Any, Awaitable, Callable, Dict,
|
| 14 |
|
| 15 |
import httpx
|
| 16 |
from huggingface_hub import HfApi
|
|
@@ -63,24 +63,6 @@ GPU_FLAVORS_DESC = (
|
|
| 63 |
"l4x1(8vCPU/30GB/GPU 24GB), l4x4(48vCPU/186GB/GPU 96GB), "
|
| 64 |
"l40sx1(8vCPU/62GB/GPU 48GB), l40sx4(48vCPU/382GB/GPU 192GB), l40sx8(192vCPU/1534GB/GPU 384GB)"
|
| 65 |
)
|
| 66 |
-
SPECIALIZED_FLAVORS = ["inf2x6"]
|
| 67 |
-
ALL_FLAVORS = CPU_FLAVORS + GPU_FLAVORS + SPECIALIZED_FLAVORS
|
| 68 |
-
|
| 69 |
-
# Operation names
|
| 70 |
-
OperationType = Literal[
|
| 71 |
-
"run",
|
| 72 |
-
"ps",
|
| 73 |
-
"logs",
|
| 74 |
-
"inspect",
|
| 75 |
-
"cancel",
|
| 76 |
-
"scheduled run",
|
| 77 |
-
"scheduled ps",
|
| 78 |
-
"scheduled inspect",
|
| 79 |
-
"scheduled delete",
|
| 80 |
-
"scheduled suspend",
|
| 81 |
-
"scheduled resume",
|
| 82 |
-
]
|
| 83 |
-
|
| 84 |
# Constants
|
| 85 |
UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
|
| 86 |
|
|
|
|
| 10 |
import logging
|
| 11 |
import re
|
| 12 |
import shlex
|
| 13 |
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
| 14 |
|
| 15 |
import httpx
|
| 16 |
from huggingface_hub import HfApi
|
|
|
|
| 63 |
"l4x1(8vCPU/30GB/GPU 24GB), l4x4(48vCPU/186GB/GPU 96GB), "
|
| 64 |
"l40sx1(8vCPU/62GB/GPU 48GB), l40sx4(48vCPU/382GB/GPU 192GB), l40sx8(192vCPU/1534GB/GPU 384GB)"
|
| 65 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
# Constants
|
| 67 |
UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
|
| 68 |
|
|
@@ -120,19 +120,6 @@ async def _s2_get_json(
|
|
| 120 |
return None
|
| 121 |
|
| 122 |
|
| 123 |
-
async def _s2_get_paper(
|
| 124 |
-
client: httpx.AsyncClient,
|
| 125 |
-
arxiv_id: str,
|
| 126 |
-
fields: str,
|
| 127 |
-
) -> dict | None:
|
| 128 |
-
"""Fetch a single paper from S2 by arxiv ID. Returns None on failure."""
|
| 129 |
-
return await _s2_get_json(
|
| 130 |
-
client,
|
| 131 |
-
f"/graph/v1/paper/{_s2_paper_id(arxiv_id)}",
|
| 132 |
-
{"fields": fields},
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
# ---------------------------------------------------------------------------
|
| 137 |
# HTML paper parsing
|
| 138 |
# ---------------------------------------------------------------------------
|
|
|
|
| 120 |
return None
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
# ---------------------------------------------------------------------------
|
| 124 |
# HTML paper parsing
|
| 125 |
# ---------------------------------------------------------------------------
|
|
@@ -1,650 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Private HF Repos Tool - Manage private Hugging Face repositories
|
| 3 |
-
|
| 4 |
-
PRIMARY USE: Store job outputs, training scripts, and logs from HF Jobs.
|
| 5 |
-
Since job results are ephemeral, this tool provides persistent storage in private repos.
|
| 6 |
-
|
| 7 |
-
SECONDARY USE: Read back stored files and list repo contents.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import asyncio
|
| 11 |
-
from typing import Any, Dict, Literal, Optional
|
| 12 |
-
|
| 13 |
-
from huggingface_hub import HfApi, hf_hub_download
|
| 14 |
-
from huggingface_hub.utils import HfHubHTTPError
|
| 15 |
-
|
| 16 |
-
from agent.tools.types import ToolResult
|
| 17 |
-
|
| 18 |
-
# Operation names
|
| 19 |
-
OperationType = Literal[
|
| 20 |
-
"upload_file", "create_repo", "check_repo", "list_files", "read_file"
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
async def _async_call(func, *args, **kwargs):
|
| 25 |
-
"""Wrap synchronous HfApi calls for async context."""
|
| 26 |
-
return await asyncio.to_thread(func, *args, **kwargs)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _build_repo_url(repo_id: str, repo_type: str = "dataset") -> str:
|
| 30 |
-
"""Build the Hub URL for a repository."""
|
| 31 |
-
type_path = "" if repo_type == "model" else f"{repo_type}s"
|
| 32 |
-
return f"https://huggingface.co/{type_path}/{repo_id}".replace("//", "/")
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _content_to_bytes(content: str | bytes) -> bytes:
|
| 36 |
-
"""Convert string or bytes content to bytes."""
|
| 37 |
-
if isinstance(content, str):
|
| 38 |
-
return content.encode("utf-8")
|
| 39 |
-
return content
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
class PrivateHfRepoTool:
|
| 43 |
-
"""Tool for managing private Hugging Face repositories."""
|
| 44 |
-
|
| 45 |
-
def __init__(self, hf_token: Optional[str] = None):
|
| 46 |
-
self.api = HfApi(token=hf_token)
|
| 47 |
-
|
| 48 |
-
async def execute(self, params: Dict[str, Any]) -> ToolResult:
|
| 49 |
-
"""Execute the specified upload operation."""
|
| 50 |
-
operation = params.get("operation")
|
| 51 |
-
args = params.get("args", {})
|
| 52 |
-
|
| 53 |
-
# If no operation provided, return usage instructions
|
| 54 |
-
if not operation:
|
| 55 |
-
return self._show_help()
|
| 56 |
-
|
| 57 |
-
# Normalize operation name
|
| 58 |
-
operation = operation.lower()
|
| 59 |
-
|
| 60 |
-
# Check if help is requested
|
| 61 |
-
if args.get("help"):
|
| 62 |
-
return self._show_operation_help(operation)
|
| 63 |
-
|
| 64 |
-
try:
|
| 65 |
-
# Route to appropriate handler
|
| 66 |
-
if operation == "upload_file":
|
| 67 |
-
return await self._upload_file(args)
|
| 68 |
-
elif operation == "create_repo":
|
| 69 |
-
return await self._create_repo(args)
|
| 70 |
-
elif operation == "check_repo":
|
| 71 |
-
return await self._check_repo(args)
|
| 72 |
-
elif operation == "list_files":
|
| 73 |
-
return await self._list_files(args)
|
| 74 |
-
elif operation == "read_file":
|
| 75 |
-
return await self._read_file(args)
|
| 76 |
-
else:
|
| 77 |
-
return {
|
| 78 |
-
"formatted": f'Unknown operation: "{operation}"\n\n'
|
| 79 |
-
"Available operations: upload_file, create_repo, check_repo, list_files, read_file\n\n"
|
| 80 |
-
"Call this tool with no operation for full usage instructions.",
|
| 81 |
-
"totalResults": 0,
|
| 82 |
-
"resultsShared": 0,
|
| 83 |
-
"isError": True,
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
except HfHubHTTPError as e:
|
| 87 |
-
return {
|
| 88 |
-
"formatted": f"API Error: {str(e)}",
|
| 89 |
-
"totalResults": 0,
|
| 90 |
-
"resultsShared": 0,
|
| 91 |
-
"isError": True,
|
| 92 |
-
}
|
| 93 |
-
except Exception as e:
|
| 94 |
-
return {
|
| 95 |
-
"formatted": f"Error executing {operation}: {str(e)}",
|
| 96 |
-
"totalResults": 0,
|
| 97 |
-
"resultsShared": 0,
|
| 98 |
-
"isError": True,
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
def _show_help(self) -> ToolResult:
|
| 102 |
-
"""Show usage instructions when tool is called with no arguments."""
|
| 103 |
-
usage_text = """# Private HF Repos Tool
|
| 104 |
-
|
| 105 |
-
**PRIMARY USE:** Store job outputs, scripts, and logs from HF Jobs to private repos.
|
| 106 |
-
Since job results are ephemeral, use this tool for persistent storage.
|
| 107 |
-
|
| 108 |
-
**SECONDARY USE:** Read back stored files and list repo contents.
|
| 109 |
-
|
| 110 |
-
## Available Commands
|
| 111 |
-
|
| 112 |
-
### Write Operations
|
| 113 |
-
- **upload_file** - Upload file content to a repository
|
| 114 |
-
- **create_repo** - Create a new private repository
|
| 115 |
-
|
| 116 |
-
### Read Operations
|
| 117 |
-
- **list_files** - List all files in a repository
|
| 118 |
-
- **read_file** - Read content of a specific file from a repository
|
| 119 |
-
- **check_repo** - Check if a repository exists
|
| 120 |
-
|
| 121 |
-
## Examples
|
| 122 |
-
|
| 123 |
-
### Upload a script to a dataset repo
|
| 124 |
-
Call this tool with:
|
| 125 |
-
```json
|
| 126 |
-
{
|
| 127 |
-
"operation": "upload_file",
|
| 128 |
-
"args": {
|
| 129 |
-
"file_content": "import pandas as pd\\nprint('Hello from HF!')",
|
| 130 |
-
"path_in_repo": "scripts/hello.py",
|
| 131 |
-
"repo_id": "my-dataset",
|
| 132 |
-
"repo_type": "dataset",
|
| 133 |
-
"create_if_missing": true,
|
| 134 |
-
"commit_message": "Add hello script"
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
### Upload logs from a job
|
| 140 |
-
Call this tool with:
|
| 141 |
-
```json
|
| 142 |
-
{
|
| 143 |
-
"operation": "upload_file",
|
| 144 |
-
"args": {
|
| 145 |
-
"file_content": "Job started...\\nJob completed successfully!",
|
| 146 |
-
"path_in_repo": "jobs/job-abc123/logs.txt",
|
| 147 |
-
"repo_id": "job-results",
|
| 148 |
-
"create_if_missing": true
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
```
|
| 152 |
-
|
| 153 |
-
### Create a repository
|
| 154 |
-
Call this tool with:
|
| 155 |
-
```json
|
| 156 |
-
{
|
| 157 |
-
"operation": "create_repo",
|
| 158 |
-
"args": {
|
| 159 |
-
"repo_id": "my-results",
|
| 160 |
-
"repo_type": "dataset"
|
| 161 |
-
}
|
| 162 |
-
}
|
| 163 |
-
```
|
| 164 |
-
|
| 165 |
-
### Create a Space
|
| 166 |
-
Call this tool with:
|
| 167 |
-
```json
|
| 168 |
-
{
|
| 169 |
-
"operation": "create_repo",
|
| 170 |
-
"args": {
|
| 171 |
-
"repo_id": "my-gradio-app",
|
| 172 |
-
"repo_type": "space",
|
| 173 |
-
"space_sdk": "gradio"
|
| 174 |
-
}
|
| 175 |
-
}
|
| 176 |
-
```
|
| 177 |
-
Note: Repositories are always created as private. For spaces, `space_sdk` is required (gradio, streamlit, static, or docker).
|
| 178 |
-
|
| 179 |
-
### Check if a repository exists
|
| 180 |
-
Call this tool with:
|
| 181 |
-
```json
|
| 182 |
-
{
|
| 183 |
-
"operation": "check_repo",
|
| 184 |
-
"args": {
|
| 185 |
-
"repo_id": "my-dataset",
|
| 186 |
-
"repo_type": "dataset"
|
| 187 |
-
}
|
| 188 |
-
}
|
| 189 |
-
```
|
| 190 |
-
|
| 191 |
-
### List files in a repository
|
| 192 |
-
Call this tool with:
|
| 193 |
-
```json
|
| 194 |
-
{
|
| 195 |
-
"operation": "list_files",
|
| 196 |
-
"args": {
|
| 197 |
-
"repo_id": "job-results",
|
| 198 |
-
"repo_type": "dataset"
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
```
|
| 202 |
-
|
| 203 |
-
### Read a file from a repository
|
| 204 |
-
Call this tool with:
|
| 205 |
-
```json
|
| 206 |
-
{
|
| 207 |
-
"operation": "read_file",
|
| 208 |
-
"args": {
|
| 209 |
-
"repo_id": "job-results",
|
| 210 |
-
"path_in_repo": "jobs/job-abc123/script.py",
|
| 211 |
-
"repo_type": "dataset"
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
```
|
| 215 |
-
|
| 216 |
-
## Repository Types
|
| 217 |
-
|
| 218 |
-
- **dataset** (default) - For storing data, results, logs, scripts
|
| 219 |
-
- **model** - For ML models and related artifacts
|
| 220 |
-
- **space** - For Spaces and applications
|
| 221 |
-
|
| 222 |
-
## Tips
|
| 223 |
-
|
| 224 |
-
- **Content-based**: Pass file content directly as strings or bytes, not file paths
|
| 225 |
-
- **Repo ID format**: Use just the repo name (e.g., "my-dataset"). Username is automatically inferred from HF_TOKEN
|
| 226 |
-
- **Automatic repo creation**: Set `create_if_missing: true` to auto-create repos (requires user approval)
|
| 227 |
-
- **Organization**: Use path_in_repo to organize files (e.g., "jobs/job-123/script.py")
|
| 228 |
-
- **After jobs**: Upload job scripts and logs after compute jobs complete for reproducibility
|
| 229 |
-
"""
|
| 230 |
-
return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
|
| 231 |
-
|
| 232 |
-
def _show_operation_help(self, operation: str) -> ToolResult:
|
| 233 |
-
"""Show help for a specific operation."""
|
| 234 |
-
help_text = f"Help for operation: {operation}\n\nCall with appropriate arguments. Use the main help for examples."
|
| 235 |
-
return {"formatted": help_text, "totalResults": 1, "resultsShared": 1}
|
| 236 |
-
|
| 237 |
-
async def _upload_file(self, args: Dict[str, Any]) -> ToolResult:
|
| 238 |
-
"""Upload file content to a Hub repository."""
|
| 239 |
-
# Validate required arguments
|
| 240 |
-
file_content = args.get("file_content")
|
| 241 |
-
path_in_repo = args.get("path_in_repo")
|
| 242 |
-
repo_id = args.get("repo_id")
|
| 243 |
-
|
| 244 |
-
if not file_content:
|
| 245 |
-
return {
|
| 246 |
-
"formatted": "file_content is required",
|
| 247 |
-
"totalResults": 0,
|
| 248 |
-
"resultsShared": 0,
|
| 249 |
-
"isError": True,
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
if not path_in_repo:
|
| 253 |
-
return {
|
| 254 |
-
"formatted": "path_in_repo is required",
|
| 255 |
-
"totalResults": 0,
|
| 256 |
-
"resultsShared": 0,
|
| 257 |
-
"isError": True,
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
if not repo_id:
|
| 261 |
-
return {
|
| 262 |
-
"formatted": "repo_id is required",
|
| 263 |
-
"totalResults": 0,
|
| 264 |
-
"resultsShared": 0,
|
| 265 |
-
"isError": True,
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
repo_type = args.get("repo_type", "dataset")
|
| 269 |
-
create_if_missing = args.get("create_if_missing", False)
|
| 270 |
-
|
| 271 |
-
# Check if repo exists
|
| 272 |
-
try:
|
| 273 |
-
repo_exists = await _async_call(
|
| 274 |
-
self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
|
| 275 |
-
)
|
| 276 |
-
|
| 277 |
-
# Create repo if needed
|
| 278 |
-
if not repo_exists and create_if_missing:
|
| 279 |
-
create_args = {
|
| 280 |
-
"repo_id": repo_id,
|
| 281 |
-
"repo_type": repo_type,
|
| 282 |
-
"private": True,
|
| 283 |
-
}
|
| 284 |
-
# Pass through space_sdk if provided (required for spaces)
|
| 285 |
-
if "space_sdk" in args:
|
| 286 |
-
create_args["space_sdk"] = args["space_sdk"]
|
| 287 |
-
await self._create_repo(create_args)
|
| 288 |
-
elif not repo_exists:
|
| 289 |
-
return {
|
| 290 |
-
"formatted": f"Repository {repo_id} does not exist. Set create_if_missing: true to create it.",
|
| 291 |
-
"totalResults": 0,
|
| 292 |
-
"resultsShared": 0,
|
| 293 |
-
"isError": True,
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
except Exception as e:
|
| 297 |
-
return {
|
| 298 |
-
"formatted": f"Failed to check repository: {str(e)}",
|
| 299 |
-
"totalResults": 0,
|
| 300 |
-
"resultsShared": 0,
|
| 301 |
-
"isError": True,
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
# Convert content to bytes
|
| 305 |
-
file_bytes = _content_to_bytes(file_content)
|
| 306 |
-
|
| 307 |
-
# Upload file
|
| 308 |
-
try:
|
| 309 |
-
await _async_call(
|
| 310 |
-
self.api.upload_file,
|
| 311 |
-
path_or_fileobj=file_bytes,
|
| 312 |
-
path_in_repo=path_in_repo,
|
| 313 |
-
repo_id=repo_id,
|
| 314 |
-
repo_type=repo_type,
|
| 315 |
-
commit_message=args.get("commit_message", f"Upload {path_in_repo}"),
|
| 316 |
-
)
|
| 317 |
-
|
| 318 |
-
repo_url = _build_repo_url(repo_id, repo_type)
|
| 319 |
-
file_url = f"{repo_url}/blob/main/{path_in_repo}"
|
| 320 |
-
|
| 321 |
-
response = f"""✓ File uploaded successfully!
|
| 322 |
-
|
| 323 |
-
**Repository:** {repo_id}
|
| 324 |
-
**File:** {path_in_repo}
|
| 325 |
-
**View at:** {file_url}
|
| 326 |
-
**Browse repo:** {repo_url}"""
|
| 327 |
-
|
| 328 |
-
return {"formatted": response, "totalResults": 1, "resultsShared": 1}
|
| 329 |
-
|
| 330 |
-
except Exception as e:
|
| 331 |
-
return {
|
| 332 |
-
"formatted": f"Failed to upload file: {str(e)}",
|
| 333 |
-
"totalResults": 0,
|
| 334 |
-
"resultsShared": 0,
|
| 335 |
-
"isError": True,
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
async def _create_repo(self, args: Dict[str, Any]) -> ToolResult:
|
| 339 |
-
"""Create a new Hub repository."""
|
| 340 |
-
repo_id = args.get("repo_id")
|
| 341 |
-
|
| 342 |
-
if not repo_id:
|
| 343 |
-
return {
|
| 344 |
-
"formatted": "repo_id is required",
|
| 345 |
-
"totalResults": 0,
|
| 346 |
-
"resultsShared": 0,
|
| 347 |
-
"isError": True,
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
repo_type = args.get("repo_type", "dataset")
|
| 351 |
-
private = True # Always create private repos
|
| 352 |
-
space_sdk = args.get("space_sdk") # Required if repo_type is "space"
|
| 353 |
-
|
| 354 |
-
try:
|
| 355 |
-
# Check if repo already exists
|
| 356 |
-
repo_exists = await _async_call(
|
| 357 |
-
self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
|
| 358 |
-
)
|
| 359 |
-
|
| 360 |
-
if repo_exists:
|
| 361 |
-
repo_url = _build_repo_url(repo_id, repo_type)
|
| 362 |
-
return {
|
| 363 |
-
"formatted": f"Repository {repo_id} already exists.\n**View at:** {repo_url}",
|
| 364 |
-
"totalResults": 1,
|
| 365 |
-
"resultsShared": 1,
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
# Validate space_sdk for spaces
|
| 369 |
-
if repo_type == "space" and not space_sdk:
|
| 370 |
-
return {
|
| 371 |
-
"formatted": "space_sdk is required when creating a space. Valid values: gradio, streamlit, static, docker",
|
| 372 |
-
"totalResults": 0,
|
| 373 |
-
"resultsShared": 0,
|
| 374 |
-
"isError": True,
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
# Create repository
|
| 378 |
-
create_kwargs = {
|
| 379 |
-
"repo_id": repo_id,
|
| 380 |
-
"repo_type": repo_type,
|
| 381 |
-
"private": private,
|
| 382 |
-
"exist_ok": True,
|
| 383 |
-
}
|
| 384 |
-
# Add space_sdk only for spaces
|
| 385 |
-
if repo_type == "space" and space_sdk:
|
| 386 |
-
create_kwargs["space_sdk"] = space_sdk
|
| 387 |
-
|
| 388 |
-
repo_url = await _async_call(self.api.create_repo, **create_kwargs)
|
| 389 |
-
|
| 390 |
-
response = f"""✓ Repository created successfully!
|
| 391 |
-
|
| 392 |
-
**Repository:** {repo_id}
|
| 393 |
-
**Type:** {repo_type}
|
| 394 |
-
**Private:** Yes
|
| 395 |
-
**View at:** {repo_url}"""
|
| 396 |
-
|
| 397 |
-
return {"formatted": response, "totalResults": 1, "resultsShared": 1}
|
| 398 |
-
|
| 399 |
-
except Exception as e:
|
| 400 |
-
return {
|
| 401 |
-
"formatted": f"Failed to create repository: {str(e)}",
|
| 402 |
-
"totalResults": 0,
|
| 403 |
-
"resultsShared": 0,
|
| 404 |
-
"isError": True,
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
async def _check_repo(self, args: Dict[str, Any]) -> ToolResult:
|
| 408 |
-
"""Check if a Hub repository exists."""
|
| 409 |
-
repo_id = args.get("repo_id")
|
| 410 |
-
|
| 411 |
-
if not repo_id:
|
| 412 |
-
return {
|
| 413 |
-
"formatted": "repo_id is required",
|
| 414 |
-
"totalResults": 0,
|
| 415 |
-
"resultsShared": 0,
|
| 416 |
-
"isError": True,
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
repo_type = args.get("repo_type", "dataset")
|
| 420 |
-
|
| 421 |
-
try:
|
| 422 |
-
repo_exists = await _async_call(
|
| 423 |
-
self.api.repo_exists, repo_id=repo_id, repo_type=repo_type
|
| 424 |
-
)
|
| 425 |
-
|
| 426 |
-
if repo_exists:
|
| 427 |
-
repo_url = _build_repo_url(repo_id, repo_type)
|
| 428 |
-
response = f"""✓ Repository exists!
|
| 429 |
-
|
| 430 |
-
**Repository:** {repo_id}
|
| 431 |
-
**Type:** {repo_type}
|
| 432 |
-
**View at:** {repo_url}"""
|
| 433 |
-
else:
|
| 434 |
-
response = f"""Repository does not exist: {repo_id}
|
| 435 |
-
|
| 436 |
-
To create it, call this tool with:
|
| 437 |
-
```json
|
| 438 |
-
{{
|
| 439 |
-
"operation": "create_repo",
|
| 440 |
-
"args": {{
|
| 441 |
-
"repo_id": "{repo_id}",
|
| 442 |
-
"repo_type": "{repo_type}"
|
| 443 |
-
}}
|
| 444 |
-
}}
|
| 445 |
-
```"""
|
| 446 |
-
|
| 447 |
-
return {
|
| 448 |
-
"formatted": response,
|
| 449 |
-
"totalResults": 1 if repo_exists else 0,
|
| 450 |
-
"resultsShared": 1 if repo_exists else 0,
|
| 451 |
-
}
|
| 452 |
-
|
| 453 |
-
except Exception as e:
|
| 454 |
-
return {
|
| 455 |
-
"formatted": f"Failed to check repository: {str(e)}",
|
| 456 |
-
"totalResults": 0,
|
| 457 |
-
"resultsShared": 0,
|
| 458 |
-
"isError": True,
|
| 459 |
-
}
|
| 460 |
-
|
| 461 |
-
async def _list_files(self, args: Dict[str, Any]) -> ToolResult:
|
| 462 |
-
"""List all files in a Hub repository."""
|
| 463 |
-
repo_id = args.get("repo_id")
|
| 464 |
-
|
| 465 |
-
if not repo_id:
|
| 466 |
-
return {
|
| 467 |
-
"formatted": "repo_id is required",
|
| 468 |
-
"totalResults": 0,
|
| 469 |
-
"resultsShared": 0,
|
| 470 |
-
"isError": True,
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
repo_type = args.get("repo_type", "dataset")
|
| 474 |
-
|
| 475 |
-
try:
|
| 476 |
-
# List all files in the repository
|
| 477 |
-
files = await _async_call(
|
| 478 |
-
self.api.list_repo_files, repo_id=repo_id, repo_type=repo_type
|
| 479 |
-
)
|
| 480 |
-
|
| 481 |
-
if not files:
|
| 482 |
-
return {
|
| 483 |
-
"formatted": f"No files found in repository: {repo_id}",
|
| 484 |
-
"totalResults": 0,
|
| 485 |
-
"resultsShared": 0,
|
| 486 |
-
}
|
| 487 |
-
|
| 488 |
-
# Format file list
|
| 489 |
-
file_list = "\n".join(f"- {f}" for f in sorted(files))
|
| 490 |
-
repo_url = _build_repo_url(repo_id, repo_type)
|
| 491 |
-
|
| 492 |
-
response = f"""✓ Files in repository: {repo_id}
|
| 493 |
-
|
| 494 |
-
**Total files:** {len(files)}
|
| 495 |
-
**Repository URL:** {repo_url}
|
| 496 |
-
|
| 497 |
-
**Files:**
|
| 498 |
-
{file_list}"""
|
| 499 |
-
|
| 500 |
-
return {
|
| 501 |
-
"formatted": response,
|
| 502 |
-
"totalResults": len(files),
|
| 503 |
-
"resultsShared": len(files),
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
except Exception as e:
|
| 507 |
-
return {
|
| 508 |
-
"formatted": f"Failed to list files: {str(e)}",
|
| 509 |
-
"totalResults": 0,
|
| 510 |
-
"resultsShared": 0,
|
| 511 |
-
"isError": True,
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
async def _read_file(self, args: Dict[str, Any]) -> ToolResult:
|
| 515 |
-
"""Read content of a specific file from a Hub repository."""
|
| 516 |
-
repo_id = args.get("repo_id")
|
| 517 |
-
path_in_repo = args.get("path_in_repo")
|
| 518 |
-
|
| 519 |
-
if not repo_id:
|
| 520 |
-
return {
|
| 521 |
-
"formatted": "repo_id is required",
|
| 522 |
-
"totalResults": 0,
|
| 523 |
-
"resultsShared": 0,
|
| 524 |
-
"isError": True,
|
| 525 |
-
}
|
| 526 |
-
|
| 527 |
-
if not path_in_repo:
|
| 528 |
-
return {
|
| 529 |
-
"formatted": "path_in_repo is required",
|
| 530 |
-
"totalResults": 0,
|
| 531 |
-
"resultsShared": 0,
|
| 532 |
-
"isError": True,
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
repo_type = args.get("repo_type", "dataset")
|
| 536 |
-
|
| 537 |
-
try:
|
| 538 |
-
# Download file to cache and read it
|
| 539 |
-
file_path = await _async_call(
|
| 540 |
-
hf_hub_download,
|
| 541 |
-
repo_id=repo_id,
|
| 542 |
-
filename=path_in_repo,
|
| 543 |
-
repo_type=repo_type,
|
| 544 |
-
token=self.api.token,
|
| 545 |
-
)
|
| 546 |
-
|
| 547 |
-
# Read file content
|
| 548 |
-
with open(file_path, "r", encoding="utf-8") as f:
|
| 549 |
-
content = f.read()
|
| 550 |
-
|
| 551 |
-
repo_url = _build_repo_url(repo_id, repo_type)
|
| 552 |
-
file_url = f"{repo_url}/blob/main/{path_in_repo}"
|
| 553 |
-
|
| 554 |
-
response = f"""✓ File read successfully!
|
| 555 |
-
|
| 556 |
-
**Repository:** {repo_id}
|
| 557 |
-
**File:** {path_in_repo}
|
| 558 |
-
**Size:** {len(content)} characters
|
| 559 |
-
**View at:** {file_url}
|
| 560 |
-
|
| 561 |
-
**Content:**
|
| 562 |
-
```
|
| 563 |
-
{content}
|
| 564 |
-
```"""
|
| 565 |
-
|
| 566 |
-
return {"formatted": response, "totalResults": 1, "resultsShared": 1}
|
| 567 |
-
|
| 568 |
-
except UnicodeDecodeError:
|
| 569 |
-
# If file is binary, return size info instead
|
| 570 |
-
try:
|
| 571 |
-
with open(file_path, "rb") as f:
|
| 572 |
-
binary_content = f.read()
|
| 573 |
-
|
| 574 |
-
return {
|
| 575 |
-
"formatted": f"File is binary ({len(binary_content)} bytes). Cannot display as text.",
|
| 576 |
-
"totalResults": 1,
|
| 577 |
-
"resultsShared": 1,
|
| 578 |
-
}
|
| 579 |
-
except Exception as e:
|
| 580 |
-
return {
|
| 581 |
-
"formatted": f"Failed to read binary file: {str(e)}",
|
| 582 |
-
"totalResults": 0,
|
| 583 |
-
"resultsShared": 0,
|
| 584 |
-
"isError": True,
|
| 585 |
-
}
|
| 586 |
-
except Exception as e:
|
| 587 |
-
return {
|
| 588 |
-
"formatted": f"Failed to read file: {str(e)}",
|
| 589 |
-
"totalResults": 0,
|
| 590 |
-
"resultsShared": 0,
|
| 591 |
-
"isError": True,
|
| 592 |
-
}
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
# Tool specification for agent registration
|
| 596 |
-
PRIVATE_HF_REPO_TOOL_SPEC = {
|
| 597 |
-
"name": "hf_private_repos",
|
| 598 |
-
"description": (
|
| 599 |
-
"Manage private HF repositories - create, upload, read, list files in models/datasets/spaces. "
|
| 600 |
-
"⚠️ PRIMARY USE: Store job outputs persistently (job storage is EPHEMERAL - everything deleted after completion). "
|
| 601 |
-
"**Use when:** (1) Job completes and need to store logs/scripts/results, (2) Creating repos for training outputs, "
|
| 602 |
-
"(3) Reading back stored files, (4) Managing Space files, (5) Organizing job artifacts by path. "
|
| 603 |
-
"**Pattern:** hf_jobs (ephemeral) → hf_private_repos upload_file (persistent) → can read_file later. "
|
| 604 |
-
"ALWAYS pass file_content as string/bytes (✓), never file paths (✗) - this is content-based, no filesystem access. "
|
| 605 |
-
"**Operations:** create_repo (new private repo), upload_file (store content), read_file (retrieve content), list_files (browse), check_repo (verify exists). "
|
| 606 |
-
"**Critical for reliability:** Jobs lose all files after completion - use this tool to preserve important outputs. "
|
| 607 |
-
"Repositories created are ALWAYS private by default (good for sensitive training data/models). "
|
| 608 |
-
"For Spaces: must provide space_sdk ('gradio', 'streamlit', 'static', 'docker') when creating. "
|
| 609 |
-
"**Then:** After uploading, provide user with repository URL for viewing/sharing."
|
| 610 |
-
),
|
| 611 |
-
"parameters": {
|
| 612 |
-
"type": "object",
|
| 613 |
-
"properties": {
|
| 614 |
-
"operation": {
|
| 615 |
-
"type": "string",
|
| 616 |
-
"enum": [
|
| 617 |
-
"upload_file",
|
| 618 |
-
"create_repo",
|
| 619 |
-
"check_repo",
|
| 620 |
-
"list_files",
|
| 621 |
-
"read_file",
|
| 622 |
-
],
|
| 623 |
-
"description": (
|
| 624 |
-
"Operation to execute. Valid values: [upload_file, create_repo, check_repo, list_files, read_file]"
|
| 625 |
-
),
|
| 626 |
-
},
|
| 627 |
-
"args": {
|
| 628 |
-
"type": "object",
|
| 629 |
-
"description": (
|
| 630 |
-
"Operation-specific arguments as a JSON object. "
|
| 631 |
-
"Write ops: file_content (string/bytes), path_in_repo (string), repo_id (string), "
|
| 632 |
-
"repo_type (dataset/model/space), create_if_missing (boolean), commit_message (string), "
|
| 633 |
-
"space_sdk (gradio/streamlit/static/docker - required when repo_type=space). "
|
| 634 |
-
"Read ops: repo_id (string), path_in_repo (for read_file), repo_type (optional)."
|
| 635 |
-
),
|
| 636 |
-
"additionalProperties": True,
|
| 637 |
-
},
|
| 638 |
-
},
|
| 639 |
-
},
|
| 640 |
-
}
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
async def private_hf_repo_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
|
| 644 |
-
"""Handler for agent tool router."""
|
| 645 |
-
try:
|
| 646 |
-
tool = PrivateHfRepoTool()
|
| 647 |
-
result = await tool.execute(arguments)
|
| 648 |
-
return result["formatted"], not result.get("isError", False)
|
| 649 |
-
except Exception as e:
|
| 650 |
-
return f"Error executing Private HF Repo tool: {str(e)}", False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -48,17 +48,6 @@ import httpx
|
|
| 48 |
from huggingface_hub import CommitOperationAdd, HfApi
|
| 49 |
|
| 50 |
TEMPLATE_SPACE = "burtenshaw/sandbox"
|
| 51 |
-
HARDWARE_OPTIONS = [
|
| 52 |
-
"cpu-basic",
|
| 53 |
-
"cpu-upgrade",
|
| 54 |
-
"t4-small",
|
| 55 |
-
"t4-medium",
|
| 56 |
-
"a10g-small",
|
| 57 |
-
"a10g-large",
|
| 58 |
-
"a100-large",
|
| 59 |
-
]
|
| 60 |
-
OUTPUT_LIMIT = 25000
|
| 61 |
-
LINE_LIMIT = 4000
|
| 62 |
DEFAULT_READ_LIMIT = 2000
|
| 63 |
DEFAULT_TIMEOUT = 240
|
| 64 |
MAX_TIMEOUT = 1200
|
|
@@ -497,9 +486,6 @@ class ToolResult:
|
|
| 497 |
return self.output or "(no output)"
|
| 498 |
return f"ERROR: {self.error}"
|
| 499 |
|
| 500 |
-
def to_dict(self) -> dict:
|
| 501 |
-
return {"success": self.success, "output": self.output, "error": self.error}
|
| 502 |
-
|
| 503 |
|
| 504 |
@dataclass
|
| 505 |
class Sandbox:
|
|
@@ -794,15 +780,6 @@ class Sandbox:
|
|
| 794 |
if log:
|
| 795 |
log("Deleted.")
|
| 796 |
|
| 797 |
-
def pause(self):
|
| 798 |
-
"""Pause the Space (stops billing, preserves state)."""
|
| 799 |
-
self._hf_api.pause_space(self.space_id)
|
| 800 |
-
|
| 801 |
-
def restart(self):
|
| 802 |
-
"""Restart the Space."""
|
| 803 |
-
self._hf_api.restart_space(self.space_id)
|
| 804 |
-
self._wait_for_api()
|
| 805 |
-
|
| 806 |
@property
|
| 807 |
def url(self) -> str:
|
| 808 |
"""Public URL of the Space."""
|
|
@@ -1131,10 +1108,6 @@ class Sandbox:
|
|
| 1131 |
},
|
| 1132 |
}
|
| 1133 |
|
| 1134 |
-
@classmethod
|
| 1135 |
-
def tool_definitions(cls) -> list[dict]:
|
| 1136 |
-
return [{"name": name, **spec} for name, spec in cls.TOOLS.items()]
|
| 1137 |
-
|
| 1138 |
def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
|
| 1139 |
dispatch = {
|
| 1140 |
"bash": lambda a: self.bash(
|
|
|
|
| 48 |
from huggingface_hub import CommitOperationAdd, HfApi
|
| 49 |
|
| 50 |
TEMPLATE_SPACE = "burtenshaw/sandbox"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
DEFAULT_READ_LIMIT = 2000
|
| 52 |
DEFAULT_TIMEOUT = 240
|
| 53 |
MAX_TIMEOUT = 1200
|
|
|
|
| 486 |
return self.output or "(no output)"
|
| 487 |
return f"ERROR: {self.error}"
|
| 488 |
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
@dataclass
|
| 491 |
class Sandbox:
|
|
|
|
| 780 |
if log:
|
| 781 |
log("Deleted.")
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
@property
|
| 784 |
def url(self) -> str:
|
| 785 |
"""Public URL of the Space."""
|
|
|
|
| 1108 |
},
|
| 1109 |
}
|
| 1110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
def call_tool(self, name: str, arguments: dict[str, Any]) -> ToolResult:
|
| 1112 |
dispatch = {
|
| 1113 |
"bash": lambda a: self.bash(
|
|
@@ -18,7 +18,7 @@ import threading
|
|
| 18 |
import uuid
|
| 19 |
import weakref
|
| 20 |
from collections.abc import Callable
|
| 21 |
-
from datetime import datetime,
|
| 22 |
from typing import Any
|
| 23 |
|
| 24 |
from huggingface_hub import HfApi, SpaceHardware
|
|
@@ -42,11 +42,6 @@ DEFAULT_CPU_SANDBOX_HARDWARE = "cpu-basic"
|
|
| 42 |
# user-renamed lookalikes).
|
| 43 |
SANDBOX_SPACE_NAME_RE = re.compile(r"^sandbox-[a-f0-9]{8}$")
|
| 44 |
|
| 45 |
-
# How stale a sandbox must be before we treat it as definitely orphan.
|
| 46 |
-
# Anything more recent could be tied to a still-live session in another tab,
|
| 47 |
-
# so we leave it alone.
|
| 48 |
-
_ORPHAN_STALE_AFTER = timedelta(hours=1)
|
| 49 |
-
|
| 50 |
# HF Space duplication/build APIs can behave poorly when multiple private
|
| 51 |
# sandboxes are created concurrently for the same namespace. Keep session
|
| 52 |
# creation non-blocking, but serialize the actual Hub create path per owner.
|
|
@@ -344,64 +339,6 @@ async def _clear_persisted_sandbox(session: Any) -> None:
|
|
| 344 |
# ── Tool name mapping (short agent names → Sandbox client names) ──────
|
| 345 |
|
| 346 |
|
| 347 |
-
def _cleanup_user_orphan_sandboxes(
|
| 348 |
-
api: HfApi,
|
| 349 |
-
owner: str,
|
| 350 |
-
log: Any,
|
| 351 |
-
) -> int:
|
| 352 |
-
"""Delete stale ``sandbox-<8hex>`` Spaces in ``owner``'s account.
|
| 353 |
-
|
| 354 |
-
"Stale" = not modified in the last hour. The naming pattern + staleness
|
| 355 |
-
filter together make this safe:
|
| 356 |
-
|
| 357 |
-
* Naming: only matches ``sandbox-<exactly 8 lowercase hex>``, the
|
| 358 |
-
pattern Sandbox.create produces. Won't touch user-renamed Spaces.
|
| 359 |
-
* Staleness: anything modified in the last hour might still be tied
|
| 360 |
-
to a live session in another tab/replica, so we leave it alone.
|
| 361 |
-
|
| 362 |
-
Runs blocking — call via ``asyncio.to_thread``. Best-effort: failures
|
| 363 |
-
are logged but never raised, so a flaky HF API never blocks creation.
|
| 364 |
-
"""
|
| 365 |
-
cutoff = datetime.now(timezone.utc) - _ORPHAN_STALE_AFTER
|
| 366 |
-
deleted = 0
|
| 367 |
-
try:
|
| 368 |
-
spaces = list(api.list_spaces(author=owner, limit=200, full=True))
|
| 369 |
-
except Exception as e:
|
| 370 |
-
log(f"orphan sweep: list_spaces failed: {e}")
|
| 371 |
-
return 0
|
| 372 |
-
|
| 373 |
-
for space in spaces:
|
| 374 |
-
space_name = space.id.rsplit("/", 1)[-1]
|
| 375 |
-
if not SANDBOX_SPACE_NAME_RE.match(space_name):
|
| 376 |
-
continue
|
| 377 |
-
|
| 378 |
-
last_mod = getattr(space, "lastModified", None) or getattr(
|
| 379 |
-
space, "last_modified", None
|
| 380 |
-
)
|
| 381 |
-
if isinstance(last_mod, str):
|
| 382 |
-
try:
|
| 383 |
-
last_mod = datetime.fromisoformat(last_mod.replace("Z", "+00:00"))
|
| 384 |
-
except ValueError:
|
| 385 |
-
last_mod = None
|
| 386 |
-
if last_mod is None:
|
| 387 |
-
log(f"orphan sweep: skipping {space.id}; missing lastModified")
|
| 388 |
-
continue
|
| 389 |
-
if last_mod and last_mod > cutoff:
|
| 390 |
-
# Recent — could be a concurrent live session. Skip.
|
| 391 |
-
continue
|
| 392 |
-
|
| 393 |
-
try:
|
| 394 |
-
api.delete_repo(repo_id=space.id, repo_type="space")
|
| 395 |
-
deleted += 1
|
| 396 |
-
log(f"orphan sweep: deleted {space.id}")
|
| 397 |
-
except Exception as e:
|
| 398 |
-
log(f"orphan sweep: failed to delete {space.id}: {e}")
|
| 399 |
-
|
| 400 |
-
if deleted:
|
| 401 |
-
log(f"orphan sweep: cleaned up {deleted} stale sandbox(es) before create")
|
| 402 |
-
return deleted
|
| 403 |
-
|
| 404 |
-
|
| 405 |
async def _ensure_sandbox(
|
| 406 |
session: Any,
|
| 407 |
hardware: str = DEFAULT_CPU_SANDBOX_HARDWARE,
|
|
|
|
| 18 |
import uuid
|
| 19 |
import weakref
|
| 20 |
from collections.abc import Callable
|
| 21 |
+
from datetime import datetime, timezone
|
| 22 |
from typing import Any
|
| 23 |
|
| 24 |
from huggingface_hub import HfApi, SpaceHardware
|
|
|
|
| 42 |
# user-renamed lookalikes).
|
| 43 |
SANDBOX_SPACE_NAME_RE = re.compile(r"^sandbox-[a-f0-9]{8}$")
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
# HF Space duplication/build APIs can behave poorly when multiple private
|
| 46 |
# sandboxes are created concurrently for the same namespace. Keep session
|
| 47 |
# creation non-blocking, but serialize the actual Hub create path per owner.
|
|
|
|
| 339 |
# ── Tool name mapping (short agent names → Sandbox client names) ──────
|
| 340 |
|
| 341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
async def _ensure_sandbox(
|
| 343 |
session: Any,
|
| 344 |
hardware: str = DEFAULT_CPU_SANDBOX_HARDWARE,
|
|
@@ -59,10 +59,6 @@ class Particle:
|
|
| 59 |
self.x += self.vx
|
| 60 |
self.y += self.vy
|
| 61 |
|
| 62 |
-
@property
|
| 63 |
-
def at_target(self) -> bool:
|
| 64 |
-
return abs(self.x - self.target_x) < 1.5 and abs(self.y - self.target_y) < 1.5
|
| 65 |
-
|
| 66 |
|
| 67 |
def run_particle_logo(console: Console, hold_seconds: float = 1.5) -> None:
|
| 68 |
"""Run the particle coalesce effect."""
|
|
|
|
| 59 |
self.x += self.vx
|
| 60 |
self.y += self.vy
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def run_particle_logo(console: Console, hold_seconds: float = 1.5) -> None:
|
| 64 |
"""Run the particle coalesce effect."""
|
|
@@ -22,15 +22,11 @@ logger = logging.getLogger(__name__)
|
|
| 22 |
|
| 23 |
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
|
| 24 |
AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
|
| 25 |
-
HF_EMPLOYEE_ORG = os.environ.get("HF_EMPLOYEE_ORG", "huggingface")
|
| 26 |
|
| 27 |
# Simple in-memory token cache: token -> (user_info, expiry_time)
|
| 28 |
_token_cache: dict[str, tuple[dict[str, Any], float]] = {}
|
| 29 |
TOKEN_CACHE_TTL = 300 # 5 minutes
|
| 30 |
|
| 31 |
-
# Org membership cache: key -> expiry_time (only caches positive results)
|
| 32 |
-
_org_member_cache: dict[str, float] = {}
|
| 33 |
-
|
| 34 |
DEV_USER: dict[str, Any] = {
|
| 35 |
"user_id": "dev",
|
| 36 |
"username": "dev",
|
|
@@ -211,31 +207,6 @@ async def _dev_user_from_env() -> dict[str, Any]:
|
|
| 211 |
}
|
| 212 |
|
| 213 |
|
| 214 |
-
async def check_org_membership(token: str, org_name: str) -> bool:
|
| 215 |
-
"""Check if the token owner belongs to an HF org. Only caches positive results."""
|
| 216 |
-
now = time.time()
|
| 217 |
-
key = token + org_name
|
| 218 |
-
cached = _org_member_cache.get(key)
|
| 219 |
-
if cached and cached > now:
|
| 220 |
-
return True
|
| 221 |
-
|
| 222 |
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 223 |
-
try:
|
| 224 |
-
resp = await client.get(
|
| 225 |
-
f"{OPENID_PROVIDER_URL}/api/whoami-v2",
|
| 226 |
-
headers={"Authorization": f"Bearer {token}"},
|
| 227 |
-
)
|
| 228 |
-
if resp.status_code != 200:
|
| 229 |
-
return False
|
| 230 |
-
orgs = {o.get("name") for o in resp.json().get("orgs", [])}
|
| 231 |
-
if org_name in orgs:
|
| 232 |
-
_org_member_cache[key] = now + TOKEN_CACHE_TTL
|
| 233 |
-
return True
|
| 234 |
-
return False
|
| 235 |
-
except httpx.HTTPError:
|
| 236 |
-
return False
|
| 237 |
-
|
| 238 |
-
|
| 239 |
async def get_current_user(request: Request) -> dict[str, Any]:
|
| 240 |
"""FastAPI dependency: extract and validate the current user.
|
| 241 |
|
|
@@ -277,27 +248,3 @@ async def get_current_user(request: Request) -> dict[str, Any]:
|
|
| 277 |
detail="Not authenticated. Please log in via /auth/login.",
|
| 278 |
headers={"WWW-Authenticate": "Bearer"},
|
| 279 |
)
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
def _extract_token(request: Request) -> str | None:
|
| 283 |
-
"""Pull the HF access token from the Authorization header or cookie.
|
| 284 |
-
|
| 285 |
-
Mirrors the lookup order used by ``get_current_user``.
|
| 286 |
-
"""
|
| 287 |
-
token = bearer_token_from_header(request.headers.get("Authorization", ""))
|
| 288 |
-
if token:
|
| 289 |
-
return token
|
| 290 |
-
return request.cookies.get("hf_access_token")
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
async def require_huggingface_org_member(request: Request) -> bool:
|
| 294 |
-
"""Return True if the caller is a member of the ``huggingface`` org.
|
| 295 |
-
|
| 296 |
-
Returns True unconditionally in dev mode so local testing isn't blocked.
|
| 297 |
-
"""
|
| 298 |
-
if not AUTH_ENABLED:
|
| 299 |
-
return True
|
| 300 |
-
token = _extract_token(request)
|
| 301 |
-
if not token:
|
| 302 |
-
return False
|
| 303 |
-
return await check_org_membership(token, HF_EMPLOYEE_ORG)
|
|
|
|
| 22 |
|
| 23 |
OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
|
| 24 |
AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
|
|
|
|
| 25 |
|
| 26 |
# Simple in-memory token cache: token -> (user_info, expiry_time)
|
| 27 |
_token_cache: dict[str, tuple[dict[str, Any], float]] = {}
|
| 28 |
TOKEN_CACHE_TTL = 300 # 5 minutes
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
DEV_USER: dict[str, Any] = {
|
| 31 |
"user_id": "dev",
|
| 32 |
"username": "dev",
|
|
|
|
| 207 |
}
|
| 208 |
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
async def get_current_user(request: Request) -> dict[str, Any]:
|
| 211 |
"""FastAPI dependency: extract and validate the current user.
|
| 212 |
|
|
|
|
| 248 |
detail="Not authenticated. Please log in via /auth/login.",
|
| 249 |
headers={"WWW-Authenticate": "Bearer"},
|
| 250 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -11,7 +11,6 @@ class OpType(str, Enum):
|
|
| 11 |
|
| 12 |
USER_INPUT = "user_input"
|
| 13 |
EXEC_APPROVAL = "exec_approval"
|
| 14 |
-
INTERRUPT = "interrupt"
|
| 15 |
UNDO = "undo"
|
| 16 |
COMPACT = "compact"
|
| 17 |
SHUTDOWN = "shutdown"
|
|
|
|
| 11 |
|
| 12 |
USER_INPUT = "user_input"
|
| 13 |
EXEC_APPROVAL = "exec_approval"
|
|
|
|
| 14 |
UNDO = "undo"
|
| 15 |
COMPACT = "compact"
|
| 16 |
SHUTDOWN = "shutdown"
|
|
@@ -1957,27 +1957,6 @@ class SessionManager:
|
|
| 1957 |
self._touch(agent_session)
|
| 1958 |
return self._auto_approval_summary(session)
|
| 1959 |
|
| 1960 |
-
def get_session_owner(self, session_id: str) -> str | None:
|
| 1961 |
-
"""Get the user_id that owns a session, or None if session doesn't exist."""
|
| 1962 |
-
agent_session = self.sessions.get(session_id)
|
| 1963 |
-
if not agent_session:
|
| 1964 |
-
return None
|
| 1965 |
-
return agent_session.user_id
|
| 1966 |
-
|
| 1967 |
-
def verify_session_access(self, session_id: str, user_id: str) -> bool:
|
| 1968 |
-
"""Check if a user has access to a session.
|
| 1969 |
-
|
| 1970 |
-
Returns True if:
|
| 1971 |
-
- The session exists AND the user owns it
|
| 1972 |
-
- The user_id is "dev" (dev mode bypass)
|
| 1973 |
-
"""
|
| 1974 |
-
owner = self.get_session_owner(session_id)
|
| 1975 |
-
if owner is None:
|
| 1976 |
-
return False
|
| 1977 |
-
if user_id == "dev" or owner == "dev":
|
| 1978 |
-
return True
|
| 1979 |
-
return owner == user_id
|
| 1980 |
-
|
| 1981 |
def get_session_info(self, session_id: str) -> dict[str, Any] | None:
|
| 1982 |
"""Get information about a session."""
|
| 1983 |
agent_session = self.sessions.get(session_id)
|
|
|
|
| 1957 |
self._touch(agent_session)
|
| 1958 |
return self._auto_approval_summary(session)
|
| 1959 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1960 |
def get_session_info(self, session_id: str) -> dict[str, Any] | None:
|
| 1961 |
"""Get information about a session."""
|
| 1962 |
agent_session = self.sessions.get(session_id)
|
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
import { Box, Typography } from '@mui/material';
|
| 2 |
-
|
| 3 |
-
/** Pulsing dots shown while the agent is processing. */
|
| 4 |
-
export default function ThinkingIndicator() {
|
| 5 |
-
return (
|
| 6 |
-
<Box sx={{ pt: 0.75 }}>
|
| 7 |
-
<Typography
|
| 8 |
-
variant="caption"
|
| 9 |
-
sx={{
|
| 10 |
-
fontWeight: 700,
|
| 11 |
-
fontSize: '0.72rem',
|
| 12 |
-
color: 'var(--muted-text)',
|
| 13 |
-
textTransform: 'uppercase',
|
| 14 |
-
letterSpacing: '0.04em',
|
| 15 |
-
display: 'flex',
|
| 16 |
-
alignItems: 'center',
|
| 17 |
-
gap: 0.75,
|
| 18 |
-
}}
|
| 19 |
-
>
|
| 20 |
-
Thinking
|
| 21 |
-
<Box
|
| 22 |
-
component="span"
|
| 23 |
-
sx={{
|
| 24 |
-
display: 'inline-flex',
|
| 25 |
-
gap: '3px',
|
| 26 |
-
'& span': {
|
| 27 |
-
width: 4,
|
| 28 |
-
height: 4,
|
| 29 |
-
borderRadius: '50%',
|
| 30 |
-
bgcolor: 'primary.main',
|
| 31 |
-
animation: 'dotPulse 1.4s ease-in-out infinite',
|
| 32 |
-
},
|
| 33 |
-
'& span:nth-of-type(2)': { animationDelay: '0.2s' },
|
| 34 |
-
'& span:nth-of-type(3)': { animationDelay: '0.4s' },
|
| 35 |
-
'@keyframes dotPulse': {
|
| 36 |
-
'0%, 80%, 100%': { opacity: 0.25, transform: 'scale(0.8)' },
|
| 37 |
-
'40%': { opacity: 1, transform: 'scale(1)' },
|
| 38 |
-
},
|
| 39 |
-
}}
|
| 40 |
-
>
|
| 41 |
-
<span />
|
| 42 |
-
<span />
|
| 43 |
-
<span />
|
| 44 |
-
</Box>
|
| 45 |
-
</Typography>
|
| 46 |
-
</Box>
|
| 47 |
-
);
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -218,6 +218,3 @@ export const lightTheme = createTheme({
|
|
| 218 |
},
|
| 219 |
shape: sharedShape,
|
| 220 |
});
|
| 221 |
-
|
| 222 |
-
// Keep default export for backwards compat
|
| 223 |
-
export default darkTheme;
|
|
|
|
| 218 |
},
|
| 219 |
shape: sharedShape,
|
| 220 |
});
|
|
|
|
|
|
|
|
|
|
@@ -36,13 +36,6 @@ export interface SessionMeta {
|
|
| 36 |
autoApprovalRemainingUsd?: number | null;
|
| 37 |
}
|
| 38 |
|
| 39 |
-
export interface ToolApproval {
|
| 40 |
-
tool_call_id: string;
|
| 41 |
-
approved: boolean;
|
| 42 |
-
feedback?: string | null;
|
| 43 |
-
namespace?: string | null;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
export interface User {
|
| 47 |
authenticated: boolean;
|
| 48 |
username?: string;
|
|
|
|
| 36 |
autoApprovalRemainingUsd?: number | null;
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
export interface User {
|
| 40 |
authenticated: boolean;
|
| 41 |
username?: string;
|
|
@@ -31,68 +31,3 @@ export interface AgentEvent {
|
|
| 31 |
data?: Record<string, unknown>;
|
| 32 |
seq?: number;
|
| 33 |
}
|
| 34 |
-
|
| 35 |
-
export interface ReadyEventData {
|
| 36 |
-
message: string;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
export interface ProcessingEventData {
|
| 40 |
-
message: string;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
export interface AssistantMessageEventData {
|
| 44 |
-
content: string;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
export interface ToolCallEventData {
|
| 48 |
-
tool: string;
|
| 49 |
-
arguments: Record<string, unknown>;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
export interface ToolOutputEventData {
|
| 53 |
-
tool: string;
|
| 54 |
-
output: string;
|
| 55 |
-
success: boolean;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
export interface ToolLogEventData {
|
| 59 |
-
tool: string;
|
| 60 |
-
log: string;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
export interface PlanUpdateEventData {
|
| 64 |
-
plan: Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed' }>;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
export interface ApprovalRequiredEventData {
|
| 68 |
-
tools: ApprovalToolItem[];
|
| 69 |
-
count: number;
|
| 70 |
-
yolo_budget?: boolean;
|
| 71 |
-
auto_approval_blocked?: boolean;
|
| 72 |
-
block_reason?: string | null;
|
| 73 |
-
estimated_cost_usd?: number | null;
|
| 74 |
-
remaining_cap_usd?: number | null;
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
export interface ApprovalToolItem {
|
| 78 |
-
tool: string;
|
| 79 |
-
arguments: Record<string, unknown>;
|
| 80 |
-
tool_call_id: string;
|
| 81 |
-
auto_approval_blocked?: boolean;
|
| 82 |
-
block_reason?: string | null;
|
| 83 |
-
estimated_cost_usd?: number | null;
|
| 84 |
-
remaining_cap_usd?: number | null;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
export interface TurnCompleteEventData {
|
| 88 |
-
history_size: number;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
export interface CompactedEventData {
|
| 92 |
-
old_tokens: number;
|
| 93 |
-
new_tokens: number;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
export interface ErrorEventData {
|
| 97 |
-
error: string;
|
| 98 |
-
}
|
|
|
|
| 31 |
data?: Record<string, unknown>;
|
| 32 |
seq?: number;
|
| 33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -12,7 +12,6 @@ dependencies = [
|
|
| 12 |
# Agent runtime dependencies
|
| 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",
|
|
|
|
| 12 |
# Agent runtime dependencies
|
| 13 |
"requests>=2.33.0",
|
| 14 |
"litellm>=1.83.0",
|
|
|
|
| 15 |
"huggingface-hub>=1.12.0",
|
| 16 |
"fastmcp>=3.2.0",
|
| 17 |
"prompt-toolkit>=3.0.0",
|
|
@@ -69,7 +69,6 @@ from huggingface_hub import HfApi
|
|
| 69 |
from huggingface_hub.utils import HfHubHTTPError
|
| 70 |
|
| 71 |
SANDBOX_NAME_RE = re.compile(r"^[^/]+/sandbox-[a-f0-9]{8}$")
|
| 72 |
-
TEMPLATE_REPO = "burtenshaw/sandbox"
|
| 73 |
|
| 74 |
|
| 75 |
def log(record: dict) -> None:
|
|
|
|
| 69 |
from huggingface_hub.utils import HfHubHTTPError
|
| 70 |
|
| 71 |
SANDBOX_NAME_RE = re.compile(r"^[^/]+/sandbox-[a-f0-9]{8}$")
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
def log(record: dict) -> None:
|
|
@@ -20,7 +20,7 @@ from agent.core.agent_loop import (
|
|
| 20 |
_call_llm_streaming,
|
| 21 |
)
|
| 22 |
from agent.core.llm_params import _resolve_llm_params
|
| 23 |
-
from agent.core.model_ids import
|
| 24 |
|
| 25 |
|
| 26 |
if env_file := os.environ.get("ML_INTERN_LIVE_ENV_FILE"):
|
|
@@ -77,9 +77,9 @@ async def test_live_default_router_model_does_not_replay_reasoning_metadata():
|
|
| 77 |
_skip_without_live_flag()
|
| 78 |
_skip_without_hf_token()
|
| 79 |
|
| 80 |
-
session = _session(
|
| 81 |
llm_params = _resolve_llm_params(
|
| 82 |
-
|
| 83 |
os.environ["HF_TOKEN"],
|
| 84 |
reasoning_effort="low",
|
| 85 |
)
|
|
|
|
| 20 |
_call_llm_streaming,
|
| 21 |
)
|
| 22 |
from agent.core.llm_params import _resolve_llm_params
|
| 23 |
+
from agent.core.model_ids import CLAUDE_OPUS_48_MODEL_ID
|
| 24 |
|
| 25 |
|
| 26 |
if env_file := os.environ.get("ML_INTERN_LIVE_ENV_FILE"):
|
|
|
|
| 77 |
_skip_without_live_flag()
|
| 78 |
_skip_without_hf_token()
|
| 79 |
|
| 80 |
+
session = _session(CLAUDE_OPUS_48_MODEL_ID)
|
| 81 |
llm_params = _resolve_llm_params(
|
| 82 |
+
CLAUDE_OPUS_48_MODEL_ID,
|
| 83 |
os.environ["HF_TOKEN"],
|
| 84 |
reasoning_effort="low",
|
| 85 |
)
|
|
@@ -74,9 +74,6 @@ async def test_scheduled_hf_jobs_always_require_manual_approval(operation):
|
|
| 74 |
assert decision.requires_approval is True
|
| 75 |
assert decision.auto_approval_blocked is True
|
| 76 |
assert "Scheduled HF jobs" in decision.block_reason
|
| 77 |
-
assert agent_loop._needs_approval(
|
| 78 |
-
"hf_jobs", {"operation": operation}, session.config
|
| 79 |
-
)
|
| 80 |
|
| 81 |
|
| 82 |
@pytest.mark.asyncio
|
|
|
|
| 74 |
assert decision.requires_approval is True
|
| 75 |
assert decision.auto_approval_blocked is True
|
| 76 |
assert "Scheduled HF jobs" in decision.block_reason
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
@pytest.mark.asyncio
|
|
@@ -1,7 +1,4 @@
|
|
| 1 |
-
import pytest
|
| 2 |
-
|
| 3 |
from agent.core.hf_access import (
|
| 4 |
-
fetch_hf_user_plan,
|
| 5 |
is_billing_error,
|
| 6 |
is_inference_billing_error,
|
| 7 |
jobs_access_from_whoami,
|
|
@@ -97,28 +94,3 @@ def test_normalize_hf_user_plan_uses_ispro_only():
|
|
| 97 |
assert normalize_hf_user_plan({"isPro": False}) == "free"
|
| 98 |
assert normalize_hf_user_plan({"plan": "HF Pro"}) == "free"
|
| 99 |
assert normalize_hf_user_plan(None) is None
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
@pytest.mark.asyncio
|
| 103 |
-
async def test_fetch_hf_user_plan_returns_unknown_without_token():
|
| 104 |
-
assert await fetch_hf_user_plan(None) == "unknown"
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
@pytest.mark.asyncio
|
| 108 |
-
async def test_fetch_hf_user_plan_returns_unknown_when_whoami_unavailable(monkeypatch):
|
| 109 |
-
async def fake_fetch_whoami_v2(_token, timeout=5.0):
|
| 110 |
-
return None
|
| 111 |
-
|
| 112 |
-
monkeypatch.setattr("agent.core.hf_access.fetch_whoami_v2", fake_fetch_whoami_v2)
|
| 113 |
-
|
| 114 |
-
assert await fetch_hf_user_plan("hf-token") == "unknown"
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
@pytest.mark.asyncio
|
| 118 |
-
async def test_fetch_hf_user_plan_normalizes_whoami(monkeypatch):
|
| 119 |
-
async def fake_fetch_whoami_v2(_token, timeout=5.0):
|
| 120 |
-
return {"isPro": True}
|
| 121 |
-
|
| 122 |
-
monkeypatch.setattr("agent.core.hf_access.fetch_whoami_v2", fake_fetch_whoami_v2)
|
| 123 |
-
|
| 124 |
-
assert await fetch_hf_user_plan("hf-token") == "pro"
|
|
|
|
|
|
|
|
|
|
| 1 |
from agent.core.hf_access import (
|
|
|
|
| 2 |
is_billing_error,
|
| 3 |
is_inference_billing_error,
|
| 4 |
jobs_access_from_whoami,
|
|
|
|
| 94 |
assert normalize_hf_user_plan({"isPro": False}) == "free"
|
| 95 |
assert normalize_hf_user_plan({"plan": "HF Pro"}) == "free"
|
| 96 |
assert normalize_hf_user_plan(None) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -6,7 +6,7 @@ import pytest
|
|
| 6 |
|
| 7 |
from agent.config import Config
|
| 8 |
from agent.core import agent_loop
|
| 9 |
-
from agent.core.agent_loop import
|
| 10 |
from agent.core.session import OpType
|
| 11 |
from agent.core.tools import create_builtin_tools
|
| 12 |
from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC
|
|
@@ -16,17 +16,23 @@ from agent.tools.sandbox_tool import get_sandbox_tools
|
|
| 16 |
def test_default_cpu_sandbox_create_does_not_require_approval():
|
| 17 |
config = SimpleNamespace(yolo_mode=False)
|
| 18 |
|
| 19 |
-
assert
|
| 20 |
-
assert
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
def test_non_default_sandbox_create_still_requires_approval():
|
| 24 |
config = SimpleNamespace(yolo_mode=False)
|
| 25 |
|
| 26 |
assert (
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
)
|
| 29 |
-
assert _needs_approval("sandbox_create", {"hardware": "t4-small"}, config) is True
|
| 30 |
|
| 31 |
|
| 32 |
def test_prompt_and_tool_specs_do_not_require_cpu_sandbox_create():
|
|
|
|
| 6 |
|
| 7 |
from agent.config import Config
|
| 8 |
from agent.core import agent_loop
|
| 9 |
+
from agent.core.agent_loop import _base_needs_approval
|
| 10 |
from agent.core.session import OpType
|
| 11 |
from agent.core.tools import create_builtin_tools
|
| 12 |
from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC
|
|
|
|
| 16 |
def test_default_cpu_sandbox_create_does_not_require_approval():
|
| 17 |
config = SimpleNamespace(yolo_mode=False)
|
| 18 |
|
| 19 |
+
assert _base_needs_approval("sandbox_create", {}, config) is False
|
| 20 |
+
assert (
|
| 21 |
+
_base_needs_approval("sandbox_create", {"hardware": "cpu-basic"}, config)
|
| 22 |
+
is False
|
| 23 |
+
)
|
| 24 |
|
| 25 |
|
| 26 |
def test_non_default_sandbox_create_still_requires_approval():
|
| 27 |
config = SimpleNamespace(yolo_mode=False)
|
| 28 |
|
| 29 |
assert (
|
| 30 |
+
_base_needs_approval("sandbox_create", {"hardware": "cpu-upgrade"}, config)
|
| 31 |
+
is True
|
| 32 |
+
)
|
| 33 |
+
assert (
|
| 34 |
+
_base_needs_approval("sandbox_create", {"hardware": "t4-small"}, config) is True
|
| 35 |
)
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def test_prompt_and_tool_specs_do_not_require_cpu_sandbox_create():
|
|
@@ -332,31 +332,6 @@ def test_sandbox_tool_forces_private_spaces(monkeypatch):
|
|
| 332 |
assert "Visibility: private" in out
|
| 333 |
|
| 334 |
|
| 335 |
-
def test_orphan_sweep_preserves_spaces_without_last_modified():
|
| 336 |
-
deleted: list[str] = []
|
| 337 |
-
logs: list[str] = []
|
| 338 |
-
|
| 339 |
-
class FakeApi:
|
| 340 |
-
def list_spaces(self, **kwargs):
|
| 341 |
-
assert kwargs["full"] is True
|
| 342 |
-
return [SimpleNamespace(id="alice/sandbox-12345678")]
|
| 343 |
-
|
| 344 |
-
def delete_repo(self, repo_id, repo_type):
|
| 345 |
-
deleted.append(repo_id)
|
| 346 |
-
|
| 347 |
-
count = sandbox_tool._cleanup_user_orphan_sandboxes(
|
| 348 |
-
FakeApi(),
|
| 349 |
-
"alice",
|
| 350 |
-
logs.append,
|
| 351 |
-
)
|
| 352 |
-
|
| 353 |
-
assert count == 0
|
| 354 |
-
assert deleted == []
|
| 355 |
-
assert logs == [
|
| 356 |
-
"orphan sweep: skipping alice/sandbox-12345678; missing lastModified"
|
| 357 |
-
]
|
| 358 |
-
|
| 359 |
-
|
| 360 |
def test_ensure_sandbox_overrides_private_argument(monkeypatch):
|
| 361 |
captured_kwargs = {}
|
| 362 |
persisted: list[dict] = []
|
|
@@ -398,7 +373,6 @@ def test_ensure_sandbox_overrides_private_argument(monkeypatch):
|
|
| 398 |
pass
|
| 399 |
|
| 400 |
monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
|
| 401 |
-
monkeypatch.setattr(sandbox_tool, "_cleanup_user_orphan_sandboxes", lambda *args: 0)
|
| 402 |
monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
|
| 403 |
monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
|
| 404 |
monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
|
|
@@ -524,7 +498,6 @@ def test_sandbox_creation_is_serialized_per_owner(monkeypatch):
|
|
| 524 |
pass
|
| 525 |
|
| 526 |
monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
|
| 527 |
-
monkeypatch.setattr(sandbox_tool, "_cleanup_user_orphan_sandboxes", lambda *args: 0)
|
| 528 |
monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
|
| 529 |
monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
|
| 530 |
monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
|
|
|
|
| 332 |
assert "Visibility: private" in out
|
| 333 |
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
def test_ensure_sandbox_overrides_private_argument(monkeypatch):
|
| 336 |
captured_kwargs = {}
|
| 337 |
persisted: list[dict] = []
|
|
|
|
| 373 |
pass
|
| 374 |
|
| 375 |
monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
|
|
|
|
| 376 |
monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
|
| 377 |
monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
|
| 378 |
monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
|
|
|
|
| 498 |
pass
|
| 499 |
|
| 500 |
monkeypatch.setattr(sandbox_tool, "HfApi", FakeApi)
|
|
|
|
| 501 |
monkeypatch.setattr(Sandbox, "create", staticmethod(fake_create))
|
| 502 |
monkeypatch.setattr(telemetry, "record_sandbox_create", fake_record_sandbox_create)
|
| 503 |
monkeypatch.setattr("huggingface_hub.metadata_update", _fail_metadata_update)
|
|
@@ -1775,7 +1775,6 @@ version = "0.1.0"
|
|
| 1775 |
source = { editable = "." }
|
| 1776 |
dependencies = [
|
| 1777 |
{ name = "apscheduler" },
|
| 1778 |
-
{ name = "boto3" },
|
| 1779 |
{ name = "datasets" },
|
| 1780 |
{ name = "fastapi" },
|
| 1781 |
{ name = "fastmcp" },
|
|
@@ -1822,7 +1821,6 @@ eval = [
|
|
| 1822 |
[package.metadata]
|
| 1823 |
requires-dist = [
|
| 1824 |
{ name = "apscheduler", specifier = ">=3.10,<4" },
|
| 1825 |
-
{ name = "boto3", specifier = ">=1.35.0" },
|
| 1826 |
{ name = "datasets", specifier = ">=4.4.1" },
|
| 1827 |
{ name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
|
| 1828 |
{ name = "fastapi", specifier = ">=0.115.0" },
|
|
|
|
| 1775 |
source = { editable = "." }
|
| 1776 |
dependencies = [
|
| 1777 |
{ name = "apscheduler" },
|
|
|
|
| 1778 |
{ name = "datasets" },
|
| 1779 |
{ name = "fastapi" },
|
| 1780 |
{ name = "fastmcp" },
|
|
|
|
| 1821 |
[package.metadata]
|
| 1822 |
requires-dist = [
|
| 1823 |
{ name = "apscheduler", specifier = ">=3.10,<4" },
|
|
|
|
| 1824 |
{ name = "datasets", specifier = ">=4.4.1" },
|
| 1825 |
{ name = "datasets", marker = "extra == 'eval'", specifier = ">=4.3.0" },
|
| 1826 |
{ name = "fastapi", specifier = ">=0.115.0" },
|