Henri Bonamy commited on
Commit
160da13
·
1 Parent(s): a82c70a

pass ToolErrors to agent, correct HF token handling, simplify uv job calls

Browse files
agent/core/tools.py CHANGED
@@ -8,6 +8,7 @@ from dataclasses import dataclass
8
  from typing import Any, Awaitable, Callable, Optional
9
 
10
  from fastmcp import Client
 
11
  from lmnr import observe
12
  from mcp.types import EmbeddedResource, ImageContent, TextContent
13
 
@@ -166,10 +167,14 @@ class ToolRouter:
166
 
167
  # Otherwise, use MCP client
168
  if self._mcp_initialized:
169
- result = await self.mcp_client.call_tool(tool_name, arguments)
170
- # Convert MCP content blocks to string
171
- output = convert_mcp_content_to_string(result.content)
172
- return output, not result.is_error
 
 
 
 
173
 
174
  return "MCP client not initialized", False
175
 
 
8
  from typing import Any, Awaitable, Callable, Optional
9
 
10
  from fastmcp import Client
11
+ from fastmcp.exceptions import ToolError
12
  from lmnr import observe
13
  from mcp.types import EmbeddedResource, ImageContent, TextContent
14
 
 
167
 
168
  # Otherwise, use MCP client
169
  if self._mcp_initialized:
170
+ try:
171
+ result = await self.mcp_client.call_tool(tool_name, arguments)
172
+ output = convert_mcp_content_to_string(result.content)
173
+ return output, not result.is_error
174
+ except ToolError as e:
175
+ # Catch MCP tool errors and return them to the agent
176
+ error_msg = f"Tool error: {str(e)}"
177
+ return error_msg, False
178
 
179
  return "MCP client not initialized", False
180
 
agent/prompts/system_prompt.yaml CHANGED
@@ -85,6 +85,7 @@ system_prompt: |
85
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
86
  - When referencing models, datasets, or papers, include direct links from search results
87
  - Never assume a library is available - check documentation first
 
88
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
89
  - For training tasks, consider compute requirements and suggest appropriate hardware
90
  - Never expose or log API keys, tokens, or secrets
 
85
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
86
  - When referencing models, datasets, or papers, include direct links from search results
87
  - Never assume a library is available - check documentation first
88
+ - Before processing any dataset: inspect its actual structure first using the mcp__hf-mcp-server__hub_repo_details tool. Never assume column names: verify them beforehand.
89
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
90
  - For training tasks, consider compute requirements and suggest appropriate hardware
91
  - Never expose or log API keys, tokens, or secrets
agent/tools/jobs_tool.py CHANGED
@@ -6,6 +6,7 @@ Refactored to use official huggingface-hub library instead of custom HTTP client
6
 
7
  import asyncio
8
  import base64
 
9
  from typing import Any, Dict, Literal, Optional
10
 
11
  from huggingface_hub import HfApi
@@ -60,6 +61,29 @@ OperationType = Literal[
60
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def _build_uv_command(
64
  script: str,
65
  with_deps: list[str] | None = None,
@@ -99,6 +123,20 @@ def _wrap_inline_script(
99
  return f'echo "{encoded}" | base64 -d | {uv_command_str}'
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  def _resolve_uv_command(
103
  script: str,
104
  with_deps: list[str] | None = None,
@@ -332,7 +370,6 @@ Call this tool with:
332
  **String format (simple cases only):**
333
  - Still accepted for backwards compatibility, parsed with POSIX shell semantics
334
  - Rejects shell operators and can mis-handle characters such as `&`; switch to arrays when things turn complex
335
- - `$HF_TOKEN` stays literal—forward it via `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}`
336
 
337
  ### Show command-specific help
338
  Call this tool with:
@@ -344,7 +381,7 @@ Call this tool with:
344
 
345
  - Jobs default to non-detached mode (stream logs until completion). Set `detach: true` to return immediately.
346
  - Prefer array commands to avoid shell parsing surprises
347
- - To access private Hub assets, include `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}` to inject your auth token.
348
  - Before calling a job, think about dependencies (they must be specified), which hardware flavor to run on (choose simplest for task), and whether to include secrets.
349
  """
350
  return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
@@ -388,8 +425,8 @@ Call this tool with:
388
  self.api.run_job,
389
  image=args.get("image", "python:3.12"),
390
  command=args.get("command"),
391
- env=args.get("env"),
392
- secrets=args.get("secrets"),
393
  flavor=args.get("flavor", "cpu-basic"),
394
  timeout=args.get("timeout", "30m"),
395
  namespace=args.get("namespace") or self.namespace,
@@ -441,12 +478,18 @@ To inspect, call this tool with `{{"operation": "inspect", "args": {{"job_id": "
441
  if not script:
442
  raise ValueError("script is required")
443
 
 
 
 
 
 
 
 
 
444
  # Resolve the command based on script type (URL, inline, or file)
445
  command = _resolve_uv_command(
446
  script=script,
447
- with_deps=args.get("with_deps")
448
- or args.get("dependencies")
449
- or args.get("packages"),
450
  python=args.get("python"),
451
  script_args=args.get("script_args"),
452
  )
@@ -456,8 +499,8 @@ To inspect, call this tool with `{{"operation": "inspect", "args": {{"job_id": "
456
  self.api.run_job,
457
  image=UV_DEFAULT_IMAGE,
458
  command=command,
459
- env=args.get("env"),
460
- secrets=args.get("secrets"),
461
  flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
462
  timeout=args.get("timeout", "30m"),
463
  namespace=args.get("namespace") or self.namespace,
@@ -645,8 +688,8 @@ To verify, call this tool with `{{"operation": "inspect", "args": {{"job_id": "{
645
  image=args.get("image", "python:3.12"),
646
  command=args.get("command"),
647
  schedule=args.get("schedule"),
648
- env=args.get("env"),
649
- secrets=args.get("secrets"),
650
  flavor=args.get("flavor", "cpu-basic"),
651
  timeout=args.get("timeout", "30m"),
652
  namespace=args.get("namespace") or self.namespace,
@@ -680,12 +723,18 @@ To list all, call this tool with `{{"operation": "scheduled ps"}}`"""
680
  if not schedule:
681
  raise ValueError("schedule is required")
682
 
 
 
 
 
 
 
 
 
683
  # Resolve the command based on script type
684
  command = _resolve_uv_command(
685
  script=script,
686
- with_deps=args.get("with_deps")
687
- or args.get("dependencies")
688
- or args.get("packages"),
689
  python=args.get("python"),
690
  script_args=args.get("script_args"),
691
  )
@@ -696,8 +745,8 @@ To list all, call this tool with `{{"operation": "scheduled ps"}}`"""
696
  image=UV_DEFAULT_IMAGE,
697
  command=command,
698
  schedule=schedule,
699
- env=args.get("env"),
700
- secrets=args.get("secrets"),
701
  flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
702
  timeout=args.get("timeout", "30m"),
703
  namespace=args.get("namespace") or self.namespace,
 
6
 
7
  import asyncio
8
  import base64
9
+ import os
10
  from typing import Any, Dict, Literal, Optional
11
 
12
  from huggingface_hub import HfApi
 
61
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
62
 
63
 
64
+ def _substitute_hf_token(params: Dict[str, Any] | None) -> Dict[str, Any] | None:
65
+ """
66
+ Substitute $HF_TOKEN with actual token value from environment.
67
+
68
+ Args:
69
+ params: Dictionary that may contain "$HF_TOKEN" in values
70
+
71
+ Returns:
72
+ Dictionary with $HF_TOKEN substituted
73
+ """
74
+ if params is None:
75
+ return None
76
+
77
+ result = {}
78
+ for key, value in params.items():
79
+ if value == "$HF_TOKEN":
80
+ result[key] = os.environ.get("HF_TOKEN", "")
81
+ else:
82
+ result[key] = value
83
+
84
+ return result
85
+
86
+
87
  def _build_uv_command(
88
  script: str,
89
  with_deps: list[str] | None = None,
 
123
  return f'echo "{encoded}" | base64 -d | {uv_command_str}'
124
 
125
 
126
+ def _ensure_hf_transfer_dependency(deps: list[str] | None) -> list[str]:
127
+ """Ensure hf-transfer is included in the dependencies list"""
128
+ if deps is None:
129
+ return ["hf-transfer"]
130
+
131
+ if isinstance(deps, list):
132
+ deps_copy = deps.copy() # Don't modify the original
133
+ if "hf-transfer" not in deps_copy:
134
+ deps_copy.append("hf-transfer")
135
+ return deps_copy
136
+
137
+ return ["hf-transfer"]
138
+
139
+
140
  def _resolve_uv_command(
141
  script: str,
142
  with_deps: list[str] | None = None,
 
370
  **String format (simple cases only):**
371
  - Still accepted for backwards compatibility, parsed with POSIX shell semantics
372
  - Rejects shell operators and can mis-handle characters such as `&`; switch to arrays when things turn complex
 
373
 
374
  ### Show command-specific help
375
  Call this tool with:
 
381
 
382
  - Jobs default to non-detached mode (stream logs until completion). Set `detach: true` to return immediately.
383
  - Prefer array commands to avoid shell parsing surprises
384
+ - To access private Hub assets (spaces, private models, datasets, collections), pass `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}`
385
  - Before calling a job, think about dependencies (they must be specified), which hardware flavor to run on (choose simplest for task), and whether to include secrets.
386
  """
387
  return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
 
425
  self.api.run_job,
426
  image=args.get("image", "python:3.12"),
427
  command=args.get("command"),
428
+ env=_substitute_hf_token(args.get("env")),
429
+ secrets=_substitute_hf_token(args.get("secrets")),
430
  flavor=args.get("flavor", "cpu-basic"),
431
  timeout=args.get("timeout", "30m"),
432
  namespace=args.get("namespace") or self.namespace,
 
478
  if not script:
479
  raise ValueError("script is required")
480
 
481
+ # Get dependencies and ensure hf-transfer is included
482
+ deps = (
483
+ args.get("with_deps")
484
+ or args.get("dependencies")
485
+ or args.get("packages")
486
+ )
487
+ deps = _ensure_hf_transfer_dependency(deps)
488
+
489
  # Resolve the command based on script type (URL, inline, or file)
490
  command = _resolve_uv_command(
491
  script=script,
492
+ with_deps=deps,
 
 
493
  python=args.get("python"),
494
  script_args=args.get("script_args"),
495
  )
 
499
  self.api.run_job,
500
  image=UV_DEFAULT_IMAGE,
501
  command=command,
502
+ env=_substitute_hf_token(args.get("env")),
503
+ secrets=_substitute_hf_token(args.get("secrets")),
504
  flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
505
  timeout=args.get("timeout", "30m"),
506
  namespace=args.get("namespace") or self.namespace,
 
688
  image=args.get("image", "python:3.12"),
689
  command=args.get("command"),
690
  schedule=args.get("schedule"),
691
+ env=_substitute_hf_token(args.get("env")),
692
+ secrets=_substitute_hf_token(args.get("secrets")),
693
  flavor=args.get("flavor", "cpu-basic"),
694
  timeout=args.get("timeout", "30m"),
695
  namespace=args.get("namespace") or self.namespace,
 
723
  if not schedule:
724
  raise ValueError("schedule is required")
725
 
726
+ # Get dependencies and ensure hf-transfer is included
727
+ deps = (
728
+ args.get("with_deps")
729
+ or args.get("dependencies")
730
+ or args.get("packages")
731
+ )
732
+ deps = _ensure_hf_transfer_dependency(deps)
733
+
734
  # Resolve the command based on script type
735
  command = _resolve_uv_command(
736
  script=script,
737
+ with_deps=deps,
 
 
738
  python=args.get("python"),
739
  script_args=args.get("script_args"),
740
  )
 
745
  image=UV_DEFAULT_IMAGE,
746
  command=command,
747
  schedule=schedule,
748
+ env=_substitute_hf_token(args.get("env")),
749
+ secrets=_substitute_hf_token(args.get("secrets")),
750
  flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
751
  timeout=args.get("timeout", "30m"),
752
  namespace=args.get("namespace") or self.namespace,