Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Commit Β·
5af3ab5
1
Parent(s): ca13fee
sync: org onboarding, model updates, sandbox cleanup, prompt fixes
Browse files- agent/core/session.py +2 -0
- agent/core/tools.py +1 -7
- agent/prompts/system_prompt_v3.yaml +2 -3
- agent/tools/dataset_tools.py +3 -5
- backend/dependencies.py +28 -0
- backend/routes/agent.py +6 -6
- backend/routes/auth.py +1 -1
- backend/session_manager.py +16 -0
- configs/main_agent_config.json +2 -3
- frontend/src/components/Chat/ActivityStatusBar.tsx +1 -0
- frontend/src/components/Chat/ChatInput.tsx +10 -10
- frontend/src/components/SessionChat.tsx +12 -3
- frontend/src/components/WelcomeScreen/WelcomeScreen.tsx +191 -118
agent/core/session.py
CHANGED
|
@@ -19,12 +19,14 @@ logger = logging.getLogger(__name__)
|
|
| 19 |
# on network calls for certain providers (known litellm issue).
|
| 20 |
_MAX_TOKENS_MAP: dict[str, int] = {
|
| 21 |
# Anthropic
|
|
|
|
| 22 |
"anthropic/claude-opus-4-5-20251101": 200_000,
|
| 23 |
"anthropic/claude-sonnet-4-5-20250929": 200_000,
|
| 24 |
"anthropic/claude-sonnet-4-20250514": 200_000,
|
| 25 |
"anthropic/claude-haiku-3-5-20241022": 200_000,
|
| 26 |
"anthropic/claude-3-5-sonnet-20241022": 200_000,
|
| 27 |
"anthropic/claude-3-opus-20240229": 200_000,
|
|
|
|
| 28 |
"huggingface/novita/minimax/minimax-m2.1": 196_608,
|
| 29 |
"huggingface/novita/moonshotai/kimi-k2.5": 262_144,
|
| 30 |
"huggingface/novita/zai-org/glm-5": 200_000,
|
|
|
|
| 19 |
# on network calls for certain providers (known litellm issue).
|
| 20 |
_MAX_TOKENS_MAP: dict[str, int] = {
|
| 21 |
# Anthropic
|
| 22 |
+
"anthropic/claude-opus-4-6": 200_000,
|
| 23 |
"anthropic/claude-opus-4-5-20251101": 200_000,
|
| 24 |
"anthropic/claude-sonnet-4-5-20250929": 200_000,
|
| 25 |
"anthropic/claude-sonnet-4-20250514": 200_000,
|
| 26 |
"anthropic/claude-haiku-3-5-20241022": 200_000,
|
| 27 |
"anthropic/claude-3-5-sonnet-20241022": 200_000,
|
| 28 |
"anthropic/claude-3-opus-20240229": 200_000,
|
| 29 |
+
"huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5": 200_000,
|
| 30 |
"huggingface/novita/minimax/minimax-m2.1": 196_608,
|
| 31 |
"huggingface/novita/moonshotai/kimi-k2.5": 262_144,
|
| 32 |
"huggingface/novita/zai-org/glm-5": 200_000,
|
agent/core/tools.py
CHANGED
|
@@ -62,13 +62,7 @@ warnings.filterwarnings(
|
|
| 62 |
"ignore", category=DeprecationWarning, module="aiohttp.connector"
|
| 63 |
)
|
| 64 |
|
| 65 |
-
NOT_ALLOWED_TOOL_NAMES = [
|
| 66 |
-
"hf_jobs",
|
| 67 |
-
"hf_doc_search",
|
| 68 |
-
"hf_doc_fetch",
|
| 69 |
-
"hf_whoami",
|
| 70 |
-
"paper_search",
|
| 71 |
-
]
|
| 72 |
|
| 73 |
|
| 74 |
def convert_mcp_content_to_string(content: list) -> str:
|
|
|
|
| 62 |
"ignore", category=DeprecationWarning, module="aiohttp.connector"
|
| 63 |
)
|
| 64 |
|
| 65 |
+
NOT_ALLOWED_TOOL_NAMES = ["hf_jobs", "hf_doc_search", "hf_doc_fetch", "hf_whoami"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
def convert_mcp_content_to_string(content: list) -> str:
|
agent/prompts/system_prompt_v3.yaml
CHANGED
|
@@ -16,8 +16,8 @@ system_prompt: |
|
|
| 16 |
|
| 17 |
Skip research only for trivial non-code operations.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
|
| 22 |
# Mistakes you WILL make without research
|
| 23 |
|
|
@@ -52,7 +52,6 @@ system_prompt: |
|
|
| 52 |
SFT: "messages", "text", or "prompt"/"completion"
|
| 53 |
DPO: "prompt", "chosen", "rejected"
|
| 54 |
GRPO: "prompt"
|
| 55 |
-
All datasets for training should be in conversational ChatML format for HF library compatibility.
|
| 56 |
|
| 57 |
# When submitting a training job
|
| 58 |
|
|
|
|
| 16 |
|
| 17 |
Skip research only for trivial non-code operations.
|
| 18 |
|
| 19 |
+
For open-ended research tasks (improving model performance, finding the best approach for a task, exploring a field, implementing a paper's method):
|
| 20 |
+
hf_papers(trending/search) β hf_papers(read_paper) β hf_papers(find_all_resources) β hf_inspect_dataset
|
| 21 |
|
| 22 |
# Mistakes you WILL make without research
|
| 23 |
|
|
|
|
| 52 |
SFT: "messages", "text", or "prompt"/"completion"
|
| 53 |
DPO: "prompt", "chosen", "rejected"
|
| 54 |
GRPO: "prompt"
|
|
|
|
| 55 |
|
| 56 |
# When submitting a training job
|
| 57 |
|
agent/tools/dataset_tools.py
CHANGED
|
@@ -10,7 +10,6 @@ from typing import Any, TypedDict
|
|
| 10 |
|
| 11 |
import httpx
|
| 12 |
|
| 13 |
-
from agent.core.session import Session
|
| 14 |
from agent.tools.types import ToolResult
|
| 15 |
|
| 16 |
BASE_URL = "https://datasets-server.huggingface.co"
|
|
@@ -424,17 +423,16 @@ HF_INSPECT_DATASET_TOOL_SPEC = {
|
|
| 424 |
}
|
| 425 |
|
| 426 |
|
| 427 |
-
async def hf_inspect_dataset_handler(
|
| 428 |
-
arguments: dict[str, Any], session: Session = None
|
| 429 |
-
) -> tuple[str, bool]:
|
| 430 |
"""Handler for agent tool router"""
|
| 431 |
try:
|
|
|
|
| 432 |
result = await inspect_dataset(
|
| 433 |
dataset=arguments["dataset"],
|
| 434 |
config=arguments.get("config"),
|
| 435 |
split=arguments.get("split"),
|
| 436 |
sample_rows=min(arguments.get("sample_rows", 3), 10),
|
| 437 |
-
hf_token=
|
| 438 |
)
|
| 439 |
return result["formatted"], not result.get("isError", False)
|
| 440 |
except Exception as e:
|
|
|
|
| 10 |
|
| 11 |
import httpx
|
| 12 |
|
|
|
|
| 13 |
from agent.tools.types import ToolResult
|
| 14 |
|
| 15 |
BASE_URL = "https://datasets-server.huggingface.co"
|
|
|
|
| 423 |
}
|
| 424 |
|
| 425 |
|
| 426 |
+
async def hf_inspect_dataset_handler(arguments: dict[str, Any], session=None) -> tuple[str, bool]:
|
|
|
|
|
|
|
| 427 |
"""Handler for agent tool router"""
|
| 428 |
try:
|
| 429 |
+
hf_token = session.hf_token if session else None
|
| 430 |
result = await inspect_dataset(
|
| 431 |
dataset=arguments["dataset"],
|
| 432 |
config=arguments.get("config"),
|
| 433 |
split=arguments.get("split"),
|
| 434 |
sample_rows=min(arguments.get("sample_rows", 3), 10),
|
| 435 |
+
hf_token=hf_token,
|
| 436 |
)
|
| 437 |
return result["formatted"], not result.get("isError", False)
|
| 438 |
except Exception as e:
|
backend/dependencies.py
CHANGED
|
@@ -22,6 +22,9 @@ AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", ""))
|
|
| 22 |
_token_cache: dict[str, tuple[dict[str, Any], float]] = {}
|
| 23 |
TOKEN_CACHE_TTL = 300 # 5 minutes
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
DEV_USER: dict[str, Any] = {
|
| 26 |
"user_id": "dev",
|
| 27 |
"username": "dev",
|
|
@@ -80,6 +83,31 @@ async def _extract_user_from_token(token: str) -> dict[str, Any] | None:
|
|
| 80 |
return None
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
async def get_current_user(request: Request) -> dict[str, Any]:
|
| 84 |
"""FastAPI dependency: extract and validate the current user.
|
| 85 |
|
|
|
|
| 22 |
_token_cache: dict[str, tuple[dict[str, Any], float]] = {}
|
| 23 |
TOKEN_CACHE_TTL = 300 # 5 minutes
|
| 24 |
|
| 25 |
+
# Org membership cache: key -> expiry_time (only caches positive results)
|
| 26 |
+
_org_member_cache: dict[str, float] = {}
|
| 27 |
+
|
| 28 |
DEV_USER: dict[str, Any] = {
|
| 29 |
"user_id": "dev",
|
| 30 |
"username": "dev",
|
|
|
|
| 83 |
return None
|
| 84 |
|
| 85 |
|
| 86 |
+
async def check_org_membership(token: str, org_name: str) -> bool:
|
| 87 |
+
"""Check if the token owner belongs to an HF org. Only caches positive results."""
|
| 88 |
+
now = time.time()
|
| 89 |
+
key = token + org_name
|
| 90 |
+
cached = _org_member_cache.get(key)
|
| 91 |
+
if cached and cached > now:
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 95 |
+
try:
|
| 96 |
+
resp = await client.get(
|
| 97 |
+
f"{OPENID_PROVIDER_URL}/api/whoami-v2",
|
| 98 |
+
headers={"Authorization": f"Bearer {token}"},
|
| 99 |
+
)
|
| 100 |
+
if resp.status_code != 200:
|
| 101 |
+
return False
|
| 102 |
+
orgs = {o.get("name") for o in resp.json().get("orgs", [])}
|
| 103 |
+
if org_name in orgs:
|
| 104 |
+
_org_member_cache[key] = now + TOKEN_CACHE_TTL
|
| 105 |
+
return True
|
| 106 |
+
return False
|
| 107 |
+
except httpx.HTTPError:
|
| 108 |
+
return False
|
| 109 |
+
|
| 110 |
+
|
| 111 |
async def get_current_user(request: Request) -> dict[str, Any]:
|
| 112 |
"""FastAPI dependency: extract and validate the current user.
|
| 113 |
|
backend/routes/agent.py
CHANGED
|
@@ -37,15 +37,15 @@ router = APIRouter(prefix="/api", tags=["agent"])
|
|
| 37 |
|
| 38 |
AVAILABLE_MODELS = [
|
| 39 |
{
|
| 40 |
-
"id": "
|
| 41 |
-
"label": "
|
| 42 |
-
"provider": "
|
| 43 |
"recommended": True,
|
| 44 |
},
|
| 45 |
{
|
| 46 |
-
"id": "
|
| 47 |
-
"label": "
|
| 48 |
-
"provider": "
|
| 49 |
"recommended": True,
|
| 50 |
},
|
| 51 |
{
|
|
|
|
| 37 |
|
| 38 |
AVAILABLE_MODELS = [
|
| 39 |
{
|
| 40 |
+
"id": "anthropic/claude-opus-4-6",
|
| 41 |
+
"label": "Claude Opus 4.6",
|
| 42 |
+
"provider": "anthropic",
|
| 43 |
"recommended": True,
|
| 44 |
},
|
| 45 |
{
|
| 46 |
+
"id": "huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5",
|
| 47 |
+
"label": "MiniMax M2.5",
|
| 48 |
+
"provider": "huggingface",
|
| 49 |
"recommended": True,
|
| 50 |
},
|
| 51 |
{
|
backend/routes/auth.py
CHANGED
|
@@ -142,7 +142,7 @@ async def oauth_callback(
|
|
| 142 |
httponly=True,
|
| 143 |
secure=is_production, # Secure flag only in production (HTTPS)
|
| 144 |
samesite="lax",
|
| 145 |
-
max_age=3600 * 24, #
|
| 146 |
path="/",
|
| 147 |
)
|
| 148 |
return response
|
|
|
|
| 142 |
httponly=True,
|
| 143 |
secure=is_production, # Secure flag only in production (HTTPS)
|
| 144 |
samesite="lax",
|
| 145 |
+
max_age=3600 * 24 * 7, # 7 days
|
| 146 |
path="/",
|
| 147 |
)
|
| 148 |
return response
|
backend/session_manager.py
CHANGED
|
@@ -164,6 +164,17 @@ class SessionManager:
|
|
| 164 |
logger.info(f"Created session {session_id} for user {user_id}")
|
| 165 |
return session_id
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
async def _run_session(
|
| 168 |
self,
|
| 169 |
session_id: str,
|
|
@@ -218,6 +229,8 @@ class SessionManager:
|
|
| 218 |
except asyncio.CancelledError:
|
| 219 |
pass
|
| 220 |
|
|
|
|
|
|
|
| 221 |
async with self._lock:
|
| 222 |
if session_id in self.sessions:
|
| 223 |
self.sessions[session_id].is_active = False
|
|
@@ -309,6 +322,9 @@ class SessionManager:
|
|
| 309 |
|
| 310 |
ws_manager.clear_buffer(session_id)
|
| 311 |
|
|
|
|
|
|
|
|
|
|
| 312 |
# Cancel the task if running
|
| 313 |
if agent_session.task and not agent_session.task.done():
|
| 314 |
agent_session.task.cancel()
|
|
|
|
| 164 |
logger.info(f"Created session {session_id} for user {user_id}")
|
| 165 |
return session_id
|
| 166 |
|
| 167 |
+
@staticmethod
|
| 168 |
+
async def _cleanup_sandbox(session: Session) -> None:
|
| 169 |
+
"""Delete the sandbox Space if one was created for this session."""
|
| 170 |
+
sandbox = getattr(session, "sandbox", None)
|
| 171 |
+
if sandbox and getattr(sandbox, "_owns_space", False):
|
| 172 |
+
try:
|
| 173 |
+
logger.info(f"Deleting sandbox {sandbox.space_id}...")
|
| 174 |
+
await asyncio.to_thread(sandbox.delete)
|
| 175 |
+
except Exception as e:
|
| 176 |
+
logger.warning(f"Failed to delete sandbox {sandbox.space_id}: {e}")
|
| 177 |
+
|
| 178 |
async def _run_session(
|
| 179 |
self,
|
| 180 |
session_id: str,
|
|
|
|
| 229 |
except asyncio.CancelledError:
|
| 230 |
pass
|
| 231 |
|
| 232 |
+
await self._cleanup_sandbox(session)
|
| 233 |
+
|
| 234 |
async with self._lock:
|
| 235 |
if session_id in self.sessions:
|
| 236 |
self.sessions[session_id].is_active = False
|
|
|
|
| 322 |
|
| 323 |
ws_manager.clear_buffer(session_id)
|
| 324 |
|
| 325 |
+
# Clean up sandbox Space before cancelling the task
|
| 326 |
+
await self._cleanup_sandbox(agent_session.session)
|
| 327 |
+
|
| 328 |
# Cancel the task if running
|
| 329 |
if agent_session.task and not agent_session.task.done():
|
| 330 |
agent_session.task.cancel()
|
configs/main_agent_config.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
{
|
| 2 |
-
"model_name": "
|
| 3 |
"save_sessions": true,
|
| 4 |
"session_dataset_repo": "akseljoonas/hf-agent-sessions",
|
| 5 |
"yolo_mode": false,
|
| 6 |
"confirm_cpu_jobs": true,
|
| 7 |
"auto_file_upload": true,
|
| 8 |
-
"mcpServers": {
|
| 9 |
-
"_mcpServers_disabled": {
|
| 10 |
"hf-mcp-server": {
|
| 11 |
"transport": "http",
|
| 12 |
"url": "https://huggingface.co/mcp?login"
|
|
|
|
| 1 |
{
|
| 2 |
+
"model_name": "anthropic/claude-opus-4-6",
|
| 3 |
"save_sessions": true,
|
| 4 |
"session_dataset_repo": "akseljoonas/hf-agent-sessions",
|
| 5 |
"yolo_mode": false,
|
| 6 |
"confirm_cpu_jobs": true,
|
| 7 |
"auto_file_upload": true,
|
| 8 |
+
"mcpServers": {
|
|
|
|
| 9 |
"hf-mcp-server": {
|
| 10 |
"transport": "http",
|
| 11 |
"url": "https://huggingface.co/mcp?login"
|
frontend/src/components/Chat/ActivityStatusBar.tsx
CHANGED
|
@@ -9,6 +9,7 @@ const shimmer = keyframes`
|
|
| 9 |
`;
|
| 10 |
|
| 11 |
const TOOL_LABELS: Record<string, string> = {
|
|
|
|
| 12 |
hf_jobs: 'Running job',
|
| 13 |
hf_repo_files: 'Uploading file',
|
| 14 |
hf_repo_git: 'Git operation',
|
|
|
|
| 9 |
`;
|
| 10 |
|
| 11 |
const TOOL_LABELS: Record<string, string> = {
|
| 12 |
+
sandbox_create: 'Creating sandbox, this might take 1-2 minutes',
|
| 13 |
hf_jobs: 'Running job',
|
| 14 |
hf_repo_files: 'Uploading file',
|
| 15 |
hf_repo_git: 'Git operation',
|
frontend/src/components/Chat/ChatInput.tsx
CHANGED
|
@@ -21,22 +21,22 @@ const getHfAvatarUrl = (modelId: string) => {
|
|
| 21 |
};
|
| 22 |
|
| 23 |
const MODEL_OPTIONS: ModelOption[] = [
|
| 24 |
-
{
|
| 25 |
-
id: 'minimax-m2.1',
|
| 26 |
-
name: 'MiniMax M2.1',
|
| 27 |
-
description: 'Via Novita',
|
| 28 |
-
modelPath: 'huggingface/novita/minimax/minimax-m2.1',
|
| 29 |
-
avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.1'),
|
| 30 |
-
recommended: true,
|
| 31 |
-
},
|
| 32 |
{
|
| 33 |
id: 'claude-opus',
|
| 34 |
-
name: 'Claude Opus 4.
|
| 35 |
description: 'Anthropic',
|
| 36 |
-
modelPath: 'anthropic/claude-opus-4-
|
| 37 |
avatarUrl: 'https://huggingface.co/api/avatars/Anthropic',
|
| 38 |
recommended: true,
|
| 39 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
{
|
| 41 |
id: 'kimi-k2.5',
|
| 42 |
name: 'Kimi K2.5',
|
|
|
|
| 21 |
};
|
| 22 |
|
| 23 |
const MODEL_OPTIONS: ModelOption[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
{
|
| 25 |
id: 'claude-opus',
|
| 26 |
+
name: 'Claude Opus 4.6',
|
| 27 |
description: 'Anthropic',
|
| 28 |
+
modelPath: 'anthropic/claude-opus-4-6',
|
| 29 |
avatarUrl: 'https://huggingface.co/api/avatars/Anthropic',
|
| 30 |
recommended: true,
|
| 31 |
},
|
| 32 |
+
{
|
| 33 |
+
id: 'minimax-m2.5',
|
| 34 |
+
name: 'MiniMax M2.5',
|
| 35 |
+
description: 'Via Fireworks',
|
| 36 |
+
modelPath: 'huggingface/fireworks-ai/MiniMaxAI/MiniMax-M2.5',
|
| 37 |
+
avatarUrl: getHfAvatarUrl('MiniMaxAI/MiniMax-M2.5'),
|
| 38 |
+
recommended: true,
|
| 39 |
+
},
|
| 40 |
{
|
| 41 |
id: 'kimi-k2.5',
|
| 42 |
name: 'Kimi K2.5',
|
frontend/src/components/SessionChat.tsx
CHANGED
|
@@ -94,9 +94,18 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
|
|
| 94 |
store.setActivityStatus({ type: 'tool', toolName: 'running' });
|
| 95 |
store.setProcessing(true);
|
| 96 |
} else {
|
| 97 |
-
//
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
}
|
| 102 |
prevActiveRef.current = isActive;
|
|
|
|
| 94 |
store.setActivityStatus({ type: 'tool', toolName: 'running' });
|
| 95 |
store.setProcessing(true);
|
| 96 |
} else {
|
| 97 |
+
// Check if any tools are still running (non-approval tools like bash, read, etc.)
|
| 98 |
+
const runningTool = lastAssistant?.parts.find(
|
| 99 |
+
(p) => p.type === 'dynamic-tool' && (p.state === 'input-available' || p.state === 'input-streaming')
|
| 100 |
+
);
|
| 101 |
+
if (runningTool && runningTool.type === 'dynamic-tool') {
|
| 102 |
+
const desc = (runningTool.input as Record<string, unknown>)?.description as string | undefined;
|
| 103 |
+
store.setActivityStatus({ type: 'tool', toolName: runningTool.toolName, description: desc });
|
| 104 |
+
store.setProcessing(true);
|
| 105 |
+
} else {
|
| 106 |
+
store.setActivityStatus({ type: 'idle' });
|
| 107 |
+
store.setProcessing(false);
|
| 108 |
+
}
|
| 109 |
}
|
| 110 |
}
|
| 111 |
prevActiveRef.current = isActive;
|
frontend/src/components/WelcomeScreen/WelcomeScreen.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useState, useCallback } from 'react';
|
| 2 |
import {
|
| 3 |
Box,
|
| 4 |
Typography,
|
|
@@ -7,6 +7,7 @@ import {
|
|
| 7 |
Alert,
|
| 8 |
} from '@mui/material';
|
| 9 |
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
|
|
| 10 |
import { useSessionStore } from '@/store/sessionStore';
|
| 11 |
import { useAgentStore } from '@/store/agentStore';
|
| 12 |
import { apiFetch } from '@/utils/api';
|
|
@@ -15,29 +16,43 @@ import { isInIframe, triggerLogin } from '@/hooks/useAuth';
|
|
| 15 |
/** HF brand orange */
|
| 16 |
const HF_ORANGE = '#FF9D00';
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export default function WelcomeScreen() {
|
| 19 |
const { createSession } = useSessionStore();
|
| 20 |
const { setPlan, clearPanel, user } = useAgentStore();
|
| 21 |
const [isCreating, setIsCreating] = useState(false);
|
| 22 |
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
| 23 |
|
| 24 |
const inIframe = isInIframe();
|
| 25 |
const isAuthenticated = user?.authenticated;
|
| 26 |
const isDevUser = user?.username === 'dev';
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
// This shouldn't happen because we show a different button in iframe
|
| 35 |
-
// But just in case:
|
| 36 |
-
if (inIframe) return;
|
| 37 |
-
triggerLogin();
|
| 38 |
-
return;
|
| 39 |
-
}
|
| 40 |
|
|
|
|
| 41 |
setIsCreating(true);
|
| 42 |
setError(null);
|
| 43 |
|
|
@@ -65,7 +80,19 @@ export default function WelcomeScreen() {
|
|
| 65 |
} finally {
|
| 66 |
setIsCreating(false);
|
| 67 |
}
|
| 68 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
// Build the direct Space URL for the "open in new tab" link
|
| 71 |
const spaceHost = typeof window !== 'undefined'
|
|
@@ -74,6 +101,52 @@ export default function WelcomeScreen() {
|
|
| 74 |
: `https://smolagents-ml-agent.hf.space`
|
| 75 |
: '';
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return (
|
| 78 |
<Box
|
| 79 |
sx={{
|
|
@@ -109,112 +182,112 @@ export default function WelcomeScreen() {
|
|
| 109 |
HF Agent
|
| 110 |
</Typography>
|
| 111 |
|
| 112 |
-
{/*
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
</Typography>
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
<
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
| 218 |
)}
|
| 219 |
|
| 220 |
{/* Error */}
|
|
|
|
| 1 |
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
| 2 |
import {
|
| 3 |
Box,
|
| 4 |
Typography,
|
|
|
|
| 7 |
Alert,
|
| 8 |
} from '@mui/material';
|
| 9 |
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
| 10 |
+
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
| 11 |
import { useSessionStore } from '@/store/sessionStore';
|
| 12 |
import { useAgentStore } from '@/store/agentStore';
|
| 13 |
import { apiFetch } from '@/utils/api';
|
|
|
|
| 16 |
/** HF brand orange */
|
| 17 |
const HF_ORANGE = '#FF9D00';
|
| 18 |
|
| 19 |
+
const ORG_JOIN_URL = 'https://huggingface.co/organizations/ml-agent-explorers/share/GzPMJUivoFPlfkvFtIqEouZKSytatKQSZT';
|
| 20 |
+
const ORG_JOINED_KEY = 'hf-agent-org-joined';
|
| 21 |
+
|
| 22 |
+
function hasJoinedOrg(): boolean {
|
| 23 |
+
try { return localStorage.getItem(ORG_JOINED_KEY) === '1'; } catch { return false; }
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function markOrgJoined(): void {
|
| 27 |
+
try { localStorage.setItem(ORG_JOINED_KEY, '1'); } catch { /* ignore */ }
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
export default function WelcomeScreen() {
|
| 31 |
const { createSession } = useSessionStore();
|
| 32 |
const { setPlan, clearPanel, user } = useAgentStore();
|
| 33 |
const [isCreating, setIsCreating] = useState(false);
|
| 34 |
const [error, setError] = useState<string | null>(null);
|
| 35 |
+
const [orgJoined, setOrgJoined] = useState(hasJoinedOrg);
|
| 36 |
+
const joinLinkOpened = useRef(false);
|
| 37 |
|
| 38 |
const inIframe = isInIframe();
|
| 39 |
const isAuthenticated = user?.authenticated;
|
| 40 |
const isDevUser = user?.username === 'dev';
|
| 41 |
|
| 42 |
+
// Auto-advance when user returns from the join link
|
| 43 |
+
useEffect(() => {
|
| 44 |
+
const handleVisibility = () => {
|
| 45 |
+
if (document.visibilityState !== 'visible' || !joinLinkOpened.current) return;
|
| 46 |
+
joinLinkOpened.current = false;
|
| 47 |
+
markOrgJoined();
|
| 48 |
+
setOrgJoined(true);
|
| 49 |
+
};
|
| 50 |
|
| 51 |
+
document.addEventListener('visibilitychange', handleVisibility);
|
| 52 |
+
return () => document.removeEventListener('visibilitychange', handleVisibility);
|
| 53 |
+
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
const tryCreateSession = useCallback(async () => {
|
| 56 |
setIsCreating(true);
|
| 57 |
setError(null);
|
| 58 |
|
|
|
|
| 80 |
} finally {
|
| 81 |
setIsCreating(false);
|
| 82 |
}
|
| 83 |
+
}, [createSession, setPlan, clearPanel]);
|
| 84 |
+
|
| 85 |
+
const handleStart = useCallback(async () => {
|
| 86 |
+
if (isCreating) return;
|
| 87 |
+
|
| 88 |
+
if (!isAuthenticated && !isDevUser) {
|
| 89 |
+
if (inIframe) return;
|
| 90 |
+
triggerLogin();
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
await tryCreateSession();
|
| 95 |
+
}, [isCreating, isAuthenticated, isDevUser, inIframe, tryCreateSession]);
|
| 96 |
|
| 97 |
// Build the direct Space URL for the "open in new tab" link
|
| 98 |
const spaceHost = typeof window !== 'undefined'
|
|
|
|
| 101 |
: `https://smolagents-ml-agent.hf.space`
|
| 102 |
: '';
|
| 103 |
|
| 104 |
+
// Shared button style
|
| 105 |
+
const primaryBtnSx = {
|
| 106 |
+
px: 5,
|
| 107 |
+
py: 1.5,
|
| 108 |
+
fontSize: '1rem',
|
| 109 |
+
fontWeight: 700,
|
| 110 |
+
textTransform: 'none' as const,
|
| 111 |
+
borderRadius: '12px',
|
| 112 |
+
bgcolor: HF_ORANGE,
|
| 113 |
+
color: '#000',
|
| 114 |
+
boxShadow: '0 4px 24px rgba(255, 157, 0, 0.3)',
|
| 115 |
+
textDecoration: 'none',
|
| 116 |
+
'&:hover': {
|
| 117 |
+
bgcolor: '#FFB340',
|
| 118 |
+
boxShadow: '0 6px 32px rgba(255, 157, 0, 0.45)',
|
| 119 |
+
},
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
// Description block (reused across screens)
|
| 123 |
+
const description = (
|
| 124 |
+
<Typography
|
| 125 |
+
variant="body1"
|
| 126 |
+
sx={{
|
| 127 |
+
color: 'var(--muted-text)',
|
| 128 |
+
maxWidth: 520,
|
| 129 |
+
mb: 5,
|
| 130 |
+
lineHeight: 1.8,
|
| 131 |
+
fontSize: '0.95rem',
|
| 132 |
+
textAlign: 'center',
|
| 133 |
+
px: 2,
|
| 134 |
+
'& strong': { color: 'var(--text)', fontWeight: 600 },
|
| 135 |
+
}}
|
| 136 |
+
>
|
| 137 |
+
A general-purpose AI agent for <strong>machine learning engineering</strong>.
|
| 138 |
+
It browses <strong>Hugging Face documentation</strong>, manages{' '}
|
| 139 |
+
<strong>repositories</strong>, launches <strong>training jobs</strong>,
|
| 140 |
+
and explores <strong>datasets</strong> β all through natural conversation.
|
| 141 |
+
</Typography>
|
| 142 |
+
);
|
| 143 |
+
|
| 144 |
+
// Which screen to show
|
| 145 |
+
const needsJoin = inIframe && !orgJoined;
|
| 146 |
+
const showOpenAgent = inIframe && orgJoined;
|
| 147 |
+
const showSignin = !inIframe && !isAuthenticated && !isDevUser;
|
| 148 |
+
const showReady = !inIframe && (isAuthenticated || isDevUser);
|
| 149 |
+
|
| 150 |
return (
|
| 151 |
<Box
|
| 152 |
sx={{
|
|
|
|
| 182 |
HF Agent
|
| 183 |
</Typography>
|
| 184 |
|
| 185 |
+
{/* ββ Iframe: join org (first visit only) ββββββββββββββββββββ */}
|
| 186 |
+
{needsJoin && (
|
| 187 |
+
<>
|
| 188 |
+
<Typography
|
| 189 |
+
variant="body1"
|
| 190 |
+
sx={{
|
| 191 |
+
color: 'var(--muted-text)',
|
| 192 |
+
maxWidth: 480,
|
| 193 |
+
mb: 4,
|
| 194 |
+
lineHeight: 1.8,
|
| 195 |
+
fontSize: '0.95rem',
|
| 196 |
+
textAlign: 'center',
|
| 197 |
+
px: 2,
|
| 198 |
+
'& strong': { color: 'var(--text)', fontWeight: 600 },
|
| 199 |
+
}}
|
| 200 |
+
>
|
| 201 |
+
Under the hood, this agent uses GPUs, inference APIs, and other paid Hub goodies β but we made them all free for you. Just join <strong>ML Agent Explorers</strong> to get started!
|
| 202 |
+
</Typography>
|
|
|
|
| 203 |
|
| 204 |
+
<Button
|
| 205 |
+
variant="contained"
|
| 206 |
+
size="large"
|
| 207 |
+
component="a"
|
| 208 |
+
href={ORG_JOIN_URL}
|
| 209 |
+
target="_blank"
|
| 210 |
+
rel="noopener noreferrer"
|
| 211 |
+
onClick={() => { joinLinkOpened.current = true; }}
|
| 212 |
+
startIcon={<GroupAddIcon />}
|
| 213 |
+
sx={primaryBtnSx}
|
| 214 |
+
>
|
| 215 |
+
Join ML Agent Explorers
|
| 216 |
+
</Button>
|
| 217 |
+
</>
|
| 218 |
+
)}
|
| 219 |
+
|
| 220 |
+
{/* ββ Iframe: already joined β open Space ββββββββββββββββββββ */}
|
| 221 |
+
{showOpenAgent && (
|
| 222 |
+
<>
|
| 223 |
+
{description}
|
| 224 |
+
<Button
|
| 225 |
+
variant="contained"
|
| 226 |
+
size="large"
|
| 227 |
+
component="a"
|
| 228 |
+
href={spaceHost}
|
| 229 |
+
target="_blank"
|
| 230 |
+
rel="noopener noreferrer"
|
| 231 |
+
endIcon={<OpenInNewIcon />}
|
| 232 |
+
sx={primaryBtnSx}
|
| 233 |
+
>
|
| 234 |
+
Open HF Agent
|
| 235 |
+
</Button>
|
| 236 |
+
</>
|
| 237 |
+
)}
|
| 238 |
+
|
| 239 |
+
{/* ββ Direct: not logged in β sign in βββββββββββββββοΏ½οΏ½οΏ½ββββββββ */}
|
| 240 |
+
{showSignin && (
|
| 241 |
+
<>
|
| 242 |
+
{description}
|
| 243 |
+
<Button
|
| 244 |
+
variant="contained"
|
| 245 |
+
size="large"
|
| 246 |
+
onClick={() => triggerLogin()}
|
| 247 |
+
sx={primaryBtnSx}
|
| 248 |
+
>
|
| 249 |
+
Sign in with Hugging Face
|
| 250 |
+
</Button>
|
| 251 |
+
|
| 252 |
+
<Typography
|
| 253 |
+
variant="caption"
|
| 254 |
+
sx={{
|
| 255 |
+
mt: 2.5,
|
| 256 |
+
color: 'var(--muted-text)',
|
| 257 |
+
fontSize: '0.78rem',
|
| 258 |
+
textAlign: 'center',
|
| 259 |
+
maxWidth: 360,
|
| 260 |
+
lineHeight: 1.6,
|
| 261 |
+
}}
|
| 262 |
+
>
|
| 263 |
+
Make sure to enable access to the <strong>ml-agent-explorers</strong> org when prompted.
|
| 264 |
+
</Typography>
|
| 265 |
+
</>
|
| 266 |
+
)}
|
| 267 |
+
|
| 268 |
+
{/* ββ Direct: authenticated β start session ββββββββββββββββββ */}
|
| 269 |
+
{showReady && (
|
| 270 |
+
<>
|
| 271 |
+
{description}
|
| 272 |
+
<Button
|
| 273 |
+
variant="contained"
|
| 274 |
+
size="large"
|
| 275 |
+
onClick={handleStart}
|
| 276 |
+
disabled={isCreating}
|
| 277 |
+
startIcon={
|
| 278 |
+
isCreating ? <CircularProgress size={20} color="inherit" /> : null
|
| 279 |
+
}
|
| 280 |
+
sx={{
|
| 281 |
+
...primaryBtnSx,
|
| 282 |
+
'&.Mui-disabled': {
|
| 283 |
+
bgcolor: 'rgba(255, 157, 0, 0.35)',
|
| 284 |
+
color: 'rgba(0,0,0,0.45)',
|
| 285 |
+
},
|
| 286 |
+
}}
|
| 287 |
+
>
|
| 288 |
+
{isCreating ? 'Initializing...' : 'Start Session'}
|
| 289 |
+
</Button>
|
| 290 |
+
</>
|
| 291 |
)}
|
| 292 |
|
| 293 |
{/* Error */}
|