akseljoonas HF Staff commited on
Commit
eb92351
·
1 Parent(s): 5e8489d

added a api search tool and refined the hf docs search tools

Browse files
agent/prompts/search_docs_system_prompt.yaml CHANGED
@@ -18,288 +18,6 @@ search_docs_system_prompt: |
18
  - Include domain-specific terminology when applicable
19
  - Try both specific terms and general related terms
20
 
21
- # Hugging Face Docs structure
22
-
23
- - id: hub
24
- url: /docs/hub
25
- category: Hub & Client Libraries
26
- docs on: Hub fundamentals — repos, models/datasets/spaces, auth, versioning, metadata.
27
-
28
- - id: transformers
29
- url: /docs/transformers
30
- category: Core ML Libraries
31
- docs on: Core model library — architectures, configs, tokenizers, training & inference APIs.
32
-
33
- - id: diffusers
34
- url: /docs/diffusers
35
- category: Core ML Libraries
36
- docs on: Diffusion pipelines, schedulers, fine-tuning, training, and deployment patterns.
37
-
38
- - id: datasets
39
- url: /docs/datasets
40
- category: Core ML Libraries
41
- docs on: Dataset loading, streaming, processing, Arrow format, Hub integration.
42
-
43
- - id: gradio
44
- url: https://www.gradio.app/docs/
45
- category: Collaboration & Extras
46
- docs on: UI components and demos for interacting with ML models.
47
-
48
- - id: trackio
49
- url: /docs/trackio
50
- category: Collaboration & Extras
51
- docs on: Experiment tracking, metrics logging, and run comparison.
52
-
53
- - id: smolagents
54
- url: /docs/smolagents
55
- category: Collaboration & Extras
56
- docs on: Lightweight agent abstractions and tool-using patterns.
57
-
58
- - id: huggingface_hub
59
- url: /docs/huggingface_hub
60
- category: Hub & Client Libraries
61
- docs on: Python client for Hub operations (auth, upload/download, repo management).
62
-
63
- - id: huggingface.js
64
- url: /docs/huggingface.js
65
- category: Hub & Client Libraries
66
- docs on: JS/TS client for Hub APIs in browser and Node.
67
-
68
- - id: transformers.js
69
- url: /docs/transformers.js
70
- category: Core ML Libraries
71
- docs on: Run Transformer models in browser/Node via WebGPU/WASM.
72
-
73
- - id: inference-providers
74
- url: /docs/inference-providers
75
- category: Deployment & Inference
76
- docs on: Unified interface for third-party inference backends.
77
-
78
- - id: inference-endpoints
79
- url: /docs/inference-endpoints
80
- category: Deployment & Inference
81
- docs on: Managed, scalable model deployments on HF infrastructure.
82
-
83
- - id: peft
84
- url: /docs/peft
85
- category: Training & Optimization
86
- docs on: Parameter-efficient fine-tuning methods (LoRA, adapters, etc.).
87
-
88
- - id: accelerate
89
- url: /docs/accelerate
90
- category: Training & Optimization
91
- docs on: Hardware-agnostic, distributed and mixed-precision training orchestration.
92
-
93
- - id: optimum
94
- url: /docs/optimum
95
- category: Training & Optimization
96
- docs on: Hardware-aware optimization and model export tooling.
97
-
98
- - id: optimum-habana
99
- url: /docs/optimum-habana
100
- category: —
101
- docs on: Training and inference on Habana Gaudi accelerators.
102
-
103
- - id: optimum-neuron
104
- url: /docs/optimum-neuron
105
- category: Training & Optimization
106
- docs on: Optimization workflows for AWS Inferentia/Trainium.
107
-
108
- - id: optimum-intel
109
- url: /docs/optimum-intel
110
- category: —
111
- docs on: Intel CPU/GPU optimizations (OpenVINO, IPEX).
112
-
113
- - id: optimum-executorch
114
- url: /docs/optimum-executorch
115
- category: Training & Optimization
116
- docs on: Exporting models to ExecuTorch for edge/mobile.
117
-
118
- - id: optimum-tpu
119
- url: /docs/optimum-tpu
120
- category: Training & Optimization
121
- docs on: TPU-specific training and optimization paths.
122
-
123
- - id: tokenizers
124
- url: /docs/tokenizers
125
- category: Core ML Libraries
126
- docs on: Fast tokenizer internals, training, and low-level APIs.
127
-
128
- - id: llm-course
129
- url: /learn/llm-course
130
- category: —
131
- docs on: End-to-end LLM concepts, training, and deployment.
132
-
133
- - id: robotics-course
134
- url: /learn/robotics-course
135
- category: —
136
- docs on: Learning-based robotics foundations.
137
-
138
- - id: mcp-course
139
- url: /learn/mcp-course
140
- category: —
141
- docs on: Model Context Protocol concepts and usage.
142
-
143
- - id: smol-course
144
- url: /learn/smol-course
145
- category: —
146
- docs on: Small-model and efficiency-focused workflows.
147
-
148
- - id: agents-course
149
- url: /learn/agents-course
150
- category: —
151
- docs on: Tool-using, planning, and multi-step agent design.
152
-
153
- - id: deep-rl-course
154
- url: /learn/deep-rl-course
155
- category: —
156
- docs on: Deep reinforcement learning foundations.
157
-
158
- - id: computer-vision-course
159
- url: /learn/computer-vision-course
160
- category: —
161
- docs on: Vision models, datasets, and pipelines.
162
-
163
- - id: evaluate
164
- url: /docs/evaluate
165
- category: Core ML Libraries
166
- docs on: Metrics, evaluation workflows, and training-loop integration.
167
-
168
- - id: tasks
169
- url: /tasks
170
- category: Hub & Client Libraries
171
- docs on: Canonical task definitions and model categorization.
172
-
173
- - id: dataset-viewer
174
- url: /docs/dataset-viewer
175
- category: Hub & Client Libraries
176
- docs on: Dataset preview, streaming views, and viewer internals.
177
-
178
- - id: trl
179
- url: /docs/trl
180
- category: Training & Optimization
181
- docs on: RLHF, DPO, PPO, and SFT utilities for LLMs.
182
-
183
- - id: simulate
184
- url: /docs/simulate
185
- category: —
186
- docs on: Experimental simulation tools and workflows.
187
-
188
- - id: sagemaker
189
- url: /docs/sagemaker
190
- category: Deployment & Inference
191
- docs on: Deploying Hugging Face models on AWS SageMaker.
192
-
193
- - id: timm
194
- url: /docs/timm
195
- category: Core ML Libraries
196
- docs on: Image model zoo and utilities via HF integrations.
197
-
198
- - id: safetensors
199
- url: /docs/safetensors
200
- category: Training & Optimization
201
- docs on: Safe, fast tensor serialization format.
202
-
203
- - id: tgi
204
- url: /docs/text-generation-inference
205
- category: Deployment & Inference
206
- docs on: High-throughput text generation server for LLMs.
207
-
208
- - id: setfit
209
- url: /docs/setfit
210
- category: —
211
- docs on: Few-shot text classification via sentence embeddings.
212
-
213
- - id: audio-course
214
- url: /learn/audio-course
215
- category: —
216
- docs on: Speech and audio models, datasets, and tasks.
217
-
218
- - id: lerobot
219
- url: /docs/lerobot
220
- category: Collaboration & Extras
221
- docs on: Robotics datasets, policies, and learning workflows.
222
-
223
- - id: autotrain
224
- url: /docs/autotrain
225
- category: Collaboration & Extras
226
- docs on: No/low-code model training on Hugging Face.
227
-
228
- - id: tei
229
- url: /docs/text-embeddings-inference
230
- category: Deployment & Inference
231
- docs on: Optimized inference server for embedding workloads.
232
-
233
- - id: bitsandbytes
234
- url: /docs/bitsandbytes
235
- category: Training & Optimization
236
- docs on: Quantization and memory-efficient optimizers.
237
-
238
- - id: cookbook
239
- url: /learn/cookbook
240
- category: —
241
- docs on: Practical, task-oriented recipes across the ecosystem.
242
-
243
- - id: sentence_transformers
244
- url: https://sbert.net/
245
- category: Core ML Libraries
246
- docs on: Embedding models, training recipes, similarity/search workflows.
247
-
248
- - id: ml-games-course
249
- url: /learn/ml-games-course
250
- category: —
251
- docs on: Game-based ML and reinforcement learning experiments.
252
-
253
- - id: diffusion-course
254
- url: /learn/diffusion-course
255
- category: —
256
- docs on: Diffusion model theory and hands-on practice.
257
-
258
- - id: ml-for-3d-course
259
- url: /learn/ml-for-3d-course
260
- category: —
261
- docs on: 3D representations, models, and learning techniques.
262
-
263
- - id: chat-ui
264
- url: /docs/chat-ui
265
- category: Collaboration & Extras
266
- docs on: Reference chat interfaces for LLM deployment.
267
-
268
- - id: leaderboards
269
- url: /docs/leaderboards
270
- category: Collaboration & Extras
271
- docs on: Evaluation leaderboards and submission mechanics.
272
-
273
- - id: lighteval
274
- url: /docs/lighteval
275
- category: Training & Optimization
276
- docs on: Lightweight, reproducible LLM evaluation framework.
277
-
278
- - id: argilla
279
- url: https://argilla-io.github.io/argilla/
280
- category: Collaboration & Extras
281
- docs on: Data annotation, feedback, and human-in-the-loop workflows.
282
-
283
- - id: distilabel
284
- url: https://distilabel.argilla.io/
285
- category: Collaboration & Extras
286
- docs on: Synthetic data generation and distillation pipelines.
287
-
288
- - id: microsoft-azure
289
- url: /docs/microsoft-azure
290
- category: Deployment & Inference
291
- docs on: Azure deployment and integration guides.
292
-
293
- - id: kernels
294
- url: /docs/kernels
295
- category: Core ML Libraries
296
- docs on: Lightweight execution environments and notebook-style workflows.
297
-
298
- - id: google-cloud
299
- url: /docs/google-cloud
300
- category: Deployment & Inference
301
- docs on: GCP deployment and serving workflows.
302
-
303
  # Response Guidelines
304
 
305
  After gathering results, synthesize them following these principles:
 
18
  - Include domain-specific terminology when applicable
19
  - Try both specific terms and general related terms
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Response Guidelines
22
 
23
  After gathering results, synthesize them following these principles:
agent/tools/_search_agent_tools.py CHANGED
@@ -3,12 +3,156 @@ Tools available to the search sub-agent
3
  These tools are used by the search sub-agent spawned by search_docs_tool
4
  """
5
 
 
6
  import os
 
7
  from typing import Any
8
 
9
  import httpx
10
  from bs4 import BeautifulSoup
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  async def explore_docs_structure_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
14
  """
@@ -31,109 +175,316 @@ async def explore_docs_structure_handler(arguments: dict[str, Any]) -> tuple[str
31
  if not hf_token:
32
  return "Error: HF_TOKEN environment variable not set", False
33
 
34
- # Build the URL for the main page (without .md to get HTML with navigation)
35
- base_url = "https://huggingface.co/docs"
36
  endpoint = endpoint.lstrip("/")
37
- url = f"{base_url}/{endpoint}"
38
 
39
  try:
40
- headers = {"Authorization": f"Bearer {hf_token}"}
41
-
42
- # Fetch the main HTML page
43
- async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
44
- response = await client.get(url, headers=headers)
45
- response.raise_for_status()
46
-
47
- html_content = response.text
48
-
49
- # Parse the sidebar navigation with BeautifulSoup
50
- soup = BeautifulSoup(html_content, "html.parser")
51
-
52
- # Find the sidebar nav (contains flex-auto class)
53
- sidebar = soup.find("nav", class_=lambda x: x and "flex-auto" in x)
54
 
55
- if not sidebar:
56
- return (
57
- f"Error: Could not find navigation sidebar on {url}. "
58
- "The page structure might be different.",
59
- False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- # Extract all links from the sidebar
63
- links = sidebar.find_all("a", href=True)
64
- nav_data = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- for link in links:
67
- title = link.get_text(strip=True)
68
- href = link["href"]
69
 
70
- # Make URL absolute
71
- if href.startswith("/"):
72
- page_url = f"https://huggingface.co{href}"
73
- else:
74
- page_url = href
 
75
 
76
- nav_data.append({"title": title, "url": page_url})
 
77
 
78
- if not nav_data:
79
- return f"No navigation links found in sidebar at {url}", False
 
80
 
81
- # Now fetch glimpses (first 200 chars) for each page
82
- result_items = []
83
 
84
- async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
85
- for item in nav_data[:20]: # Limit to first 20 to avoid too many requests
86
- md_url = f"{item['url']}.md"
87
-
88
- try:
89
- md_response = await client.get(md_url, headers=headers)
90
- md_response.raise_for_status()
91
-
92
- content = md_response.text
93
- # Get first 200 characters as glimpse
94
- glimpse = content[:200].strip()
95
- if len(content) > 200:
96
- glimpse += "..."
97
-
98
- result_items.append(
99
- {
100
- "title": item["title"],
101
- "url": item["url"],
102
- "md_url": md_url,
103
- "glimpse": glimpse,
104
- }
105
- )
106
- except Exception as e:
107
- # If fetching glimpse fails, include without glimpse
108
- result_items.append(
109
- {
110
- "title": item["title"],
111
- "url": item["url"],
112
- "md_url": f"{item['url']}.md",
113
- "glimpse": f"[Could not fetch glimpse: {str(e)[:50]}]",
114
- }
115
- )
116
-
117
- # Format the results nicely
118
- result = f"Documentation structure for: {url}\n\n"
119
- result += f"Found {len(result_items)} pages:\n\n"
120
-
121
- for i, item in enumerate(result_items, 1):
122
- result += f"{i}. **{item['title']}**\n"
123
- result += f" URL: {item['url']}\n"
124
- result += f" Glimpse: {item['glimpse']}\n\n"
125
 
126
- return result, True
 
 
 
127
 
128
  except httpx.HTTPStatusError as e:
129
- return (
130
- f"HTTP error fetching {url}: {e.response.status_code} - {e.response.text[:200]}",
131
- False,
132
- )
133
  except httpx.RequestError as e:
134
- return f"Request error fetching {url}: {str(e)}", False
135
  except Exception as e:
136
- return f"Error exploring docs structure: {str(e)}", False
137
 
138
 
139
  async def hf_docs_fetch_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
@@ -146,7 +497,9 @@ async def hf_docs_fetch_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
146
  Returns:
147
  Tuple of (full_markdown_content, success)
148
  """
 
149
  url = arguments.get("url", "")
 
150
 
151
  if not url:
152
  return "Error: No URL provided", False
@@ -168,14 +521,25 @@ async def hf_docs_fetch_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
168
  # Make request with auth
169
  headers = {"Authorization": f"Bearer {hf_token}"}
170
 
 
171
  async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
172
  response = await client.get(url, headers=headers)
173
  response.raise_for_status()
174
 
 
175
  content = response.text
 
 
 
 
 
176
 
177
  # Return the markdown content directly
178
  result = f"Documentation from: {url}\n\n{content}"
 
 
 
 
179
  return result, True
180
 
181
  except httpx.HTTPStatusError as e:
@@ -195,8 +559,8 @@ EXPLORE_DOCS_STRUCTURE_TOOL_SPEC = {
195
  "name": "explore_docs_structure",
196
  "description": (
197
  "Explore the structure of HF documentation by parsing the sidebar navigation. "
198
- "Provide an endpoint (e.g., 'trl', 'transformers', 'datasets') and get a list of all "
199
- "documentation pages with their titles, URLs, and a 200-character glimpse of each page. "
200
  "Use this to discover what documentation is available before fetching specific pages."
201
  ),
202
  "parameters": {
@@ -204,9 +568,122 @@ EXPLORE_DOCS_STRUCTURE_TOOL_SPEC = {
204
  "properties": {
205
  "endpoint": {
206
  "type": "string",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  "description": (
208
- "The documentation endpoint to explore (e.g., 'trl', 'transformers', 'hub'). "
209
- "Do not include '/docs/' or leading slashes."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  ),
211
  },
212
  },
@@ -237,3 +714,34 @@ HF_DOCS_FETCH_TOOL_SPEC = {
237
  "required": ["url"],
238
  },
239
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  These tools are used by the search sub-agent spawned by search_docs_tool
4
  """
5
 
6
+ import asyncio
7
  import os
8
+ import time
9
  from typing import Any
10
 
11
  import httpx
12
  from bs4 import BeautifulSoup
13
 
14
+ # Cache for OpenAPI spec to avoid repeated fetches
15
+ _openapi_spec_cache: dict[str, Any] | None = None
16
+
17
+
18
+ async def _fetch_html_page(hf_token: str, endpoint: str) -> str:
19
+ """Fetch the HTML page for a given endpoint"""
20
+ base_url = "https://huggingface.co/docs"
21
+ url = f"{base_url}/{endpoint}"
22
+ headers = {"Authorization": f"Bearer {hf_token}"}
23
+
24
+ fetch_start = time.perf_counter()
25
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
26
+ response = await client.get(url, headers=headers)
27
+ response.raise_for_status()
28
+
29
+ fetch_time = time.perf_counter() - fetch_start
30
+ print(f"[DEBUG] _fetch_html_page: Fetched in {fetch_time:.2f}s")
31
+
32
+ return response.text
33
+
34
+
35
+ def _parse_sidebar_navigation(html_content: str) -> list[dict[str, str]]:
36
+ """Parse the sidebar navigation and extract all links"""
37
+ parse_start = time.perf_counter()
38
+
39
+ soup = BeautifulSoup(html_content, "html.parser")
40
+ sidebar = soup.find("nav", class_=lambda x: x and "flex-auto" in x)
41
+
42
+ if not sidebar:
43
+ raise ValueError("Could not find navigation sidebar")
44
+
45
+ links = sidebar.find_all("a", href=True)
46
+ nav_data = []
47
+
48
+ for link in links:
49
+ title = link.get_text(strip=True)
50
+ href = link["href"]
51
+
52
+ # Make URL absolute
53
+ page_url = f"https://huggingface.co{href}" if href.startswith("/") else href
54
+ nav_data.append({"title": title, "url": page_url})
55
+
56
+ parse_time = time.perf_counter() - parse_start
57
+ print(
58
+ f"[DEBUG] _parse_sidebar_navigation: Parsed in {parse_time:.2f}s, found {len(nav_data)} links"
59
+ )
60
+
61
+ return nav_data
62
+
63
+
64
+ async def _fetch_single_glimpse(
65
+ client: httpx.AsyncClient, hf_token: str, item: dict[str, str]
66
+ ) -> dict[str, str]:
67
+ """Fetch a glimpse (first 300 chars) for a single page"""
68
+ md_url = f"{item['url']}.md"
69
+ headers = {"Authorization": f"Bearer {hf_token}"}
70
+
71
+ try:
72
+ response = await client.get(md_url, headers=headers)
73
+ response.raise_for_status()
74
+
75
+ content = response.text
76
+ glimpse = content[:300].strip()
77
+ if len(content) > 300:
78
+ glimpse += "..."
79
+
80
+ return {
81
+ "title": item["title"],
82
+ "url": item["url"],
83
+ "md_url": md_url,
84
+ "glimpse": glimpse,
85
+ }
86
+ except Exception as e:
87
+ return {
88
+ "title": item["title"],
89
+ "url": item["url"],
90
+ "md_url": md_url,
91
+ "glimpse": f"[Could not fetch glimpse: {str(e)[:50]}]",
92
+ }
93
+
94
+
95
+ async def _fetch_all_glimpses(
96
+ hf_token: str, nav_data: list[dict[str, str]]
97
+ ) -> list[dict[str, str]]:
98
+ """Fetch glimpses for all pages in parallel"""
99
+ glimpse_start = time.perf_counter()
100
+
101
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
102
+ result_items = await asyncio.gather(
103
+ *[_fetch_single_glimpse(client, hf_token, item) for item in nav_data]
104
+ )
105
+
106
+ glimpse_time = time.perf_counter() - glimpse_start
107
+ print(
108
+ f"[DEBUG] _fetch_all_glimpses: Fetched {len(result_items)} glimpses in {glimpse_time:.2f}s"
109
+ )
110
+
111
+ return list(result_items)
112
+
113
+
114
+ def _format_exploration_results(
115
+ endpoint: str, result_items: list[dict[str, str]]
116
+ ) -> str:
117
+ """Format the exploration results as a readable string"""
118
+ base_url = "https://huggingface.co/docs"
119
+ url = f"{base_url}/{endpoint}"
120
+ result = f"Documentation structure for: {url}\n\n"
121
+ result += f"Found {len(result_items)} pages:\n\n"
122
+
123
+ for i, item in enumerate(result_items, 1):
124
+ result += f"{i}. **{item['title']}**\n"
125
+ result += f" URL: {item['url']}\n"
126
+ result += f" Glimpse: {item['glimpse']}\n\n"
127
+
128
+ return result
129
+
130
+
131
+ async def _explore_docs_structure(hf_token: str, endpoint: str) -> str:
132
+ """Main function to explore documentation structure"""
133
+ start_time = time.perf_counter()
134
+ print(f"[DEBUG] _explore_docs_structure: Starting for endpoint '{endpoint}'")
135
+
136
+ # Fetch HTML page
137
+ html_content = await _fetch_html_page(hf_token, endpoint)
138
+
139
+ # Parse navigation
140
+ nav_data = _parse_sidebar_navigation(html_content)
141
+
142
+ if not nav_data:
143
+ raise ValueError(f"No navigation links found for endpoint '{endpoint}'")
144
+
145
+ # Fetch all glimpses in parallel
146
+ result_items = await _fetch_all_glimpses(hf_token, nav_data)
147
+
148
+ # Format results
149
+ result = _format_exploration_results(endpoint, result_items)
150
+
151
+ total_time = time.perf_counter() - start_time
152
+ print(f"[DEBUG] _explore_docs_structure: Total time {total_time:.2f}s")
153
+
154
+ return result
155
+
156
 
157
  async def explore_docs_structure_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
158
  """
 
175
  if not hf_token:
176
  return "Error: HF_TOKEN environment variable not set", False
177
 
 
 
178
  endpoint = endpoint.lstrip("/")
 
179
 
180
  try:
181
+ result = await _explore_docs_structure(hf_token, endpoint)
182
+ return result, True
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
+ except httpx.HTTPStatusError as e:
185
+ return (
186
+ f"HTTP error: {e.response.status_code} - {e.response.text[:200]}",
187
+ False,
188
+ )
189
+ except httpx.RequestError as e:
190
+ return f"Request error: {str(e)}", False
191
+ except ValueError as e:
192
+ return f"Error: {str(e)}", False
193
+ except Exception as e:
194
+ return f"Unexpected error: {str(e)}", False
195
+
196
+
197
+ async def _fetch_openapi_spec() -> dict[str, Any]:
198
+ """Fetch and cache the HuggingFace OpenAPI specification"""
199
+ global _openapi_spec_cache
200
+
201
+ if _openapi_spec_cache is not None:
202
+ print("[DEBUG] _fetch_openapi_spec: Using cached spec")
203
+ return _openapi_spec_cache
204
+
205
+ start_time = time.perf_counter()
206
+ print("[DEBUG] _fetch_openapi_spec: Fetching from API")
207
+
208
+ url = "https://huggingface.co/.well-known/openapi.json"
209
+
210
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
211
+ response = await client.get(url)
212
+ response.raise_for_status()
213
+
214
+ spec = response.json()
215
+ _openapi_spec_cache = spec
216
+
217
+ fetch_time = time.perf_counter() - start_time
218
+ print(f"[DEBUG] _fetch_openapi_spec: Fetched and cached in {fetch_time:.2f}s")
219
+
220
+ return spec
221
+
222
+
223
+ def _extract_all_tags(spec: dict[str, Any]) -> list[str]:
224
+ """Extract all unique tags from the OpenAPI spec"""
225
+ tags = set()
226
+
227
+ # Get tags from the tags section
228
+ for tag_obj in spec.get("tags", []):
229
+ if "name" in tag_obj:
230
+ tags.add(tag_obj["name"])
231
+
232
+ # Also get tags from paths (in case some aren't in the tags section)
233
+ for path, path_item in spec.get("paths", {}).items():
234
+ for method, operation in path_item.items():
235
+ if method in ["get", "post", "put", "delete", "patch", "head", "options"]:
236
+ for tag in operation.get("tags", []):
237
+ tags.add(tag)
238
+
239
+ return sorted(list(tags))
240
+
241
+
242
+ def _search_openapi_by_tag(spec: dict[str, Any], tag: str) -> list[dict[str, Any]]:
243
+ """Search for API endpoints with a specific tag"""
244
+ results = []
245
+ paths = spec.get("paths", {})
246
+ servers = spec.get("servers", [])
247
+ base_url = (
248
+ servers[0].get("url", "https://huggingface.co")
249
+ if servers
250
+ else "https://huggingface.co"
251
+ )
252
+
253
+ for path, path_item in paths.items():
254
+ for method, operation in path_item.items():
255
+ if method not in [
256
+ "get",
257
+ "post",
258
+ "put",
259
+ "delete",
260
+ "patch",
261
+ "head",
262
+ "options",
263
+ ]:
264
+ continue
265
+
266
+ operation_tags = operation.get("tags", [])
267
+ if tag in operation_tags:
268
+ # Extract parameters
269
+ parameters = operation.get("parameters", [])
270
+ request_body = operation.get("requestBody", {})
271
+ responses = operation.get("responses", {})
272
+
273
+ results.append(
274
+ {
275
+ "path": path,
276
+ "method": method.upper(),
277
+ "operationId": operation.get("operationId", ""),
278
+ "summary": operation.get("summary", ""),
279
+ "description": operation.get("description", ""),
280
+ "parameters": parameters,
281
+ "request_body": request_body,
282
+ "responses": responses,
283
+ "base_url": base_url,
284
+ }
285
+ )
286
+
287
+ return results
288
+
289
+
290
+ def _generate_curl_example(endpoint: dict[str, Any]) -> str:
291
+ """Generate a curl command example for an endpoint"""
292
+ method = endpoint["method"]
293
+ path = endpoint["path"]
294
+ base_url = endpoint["base_url"]
295
+
296
+ # Build the full URL with example path parameters
297
+ full_path = path
298
+ for param in endpoint.get("parameters", []):
299
+ if param.get("in") == "path" and param.get("required"):
300
+ param_name = param["name"]
301
+ example = param.get(
302
+ "example", param.get("schema", {}).get("example", f"<{param_name}>")
303
  )
304
+ full_path = full_path.replace(f"{{{param_name}}}", str(example))
305
+
306
+ curl = f"curl -X {method} \\\n '{base_url}{full_path}'"
307
+
308
+ # Add query parameters if any
309
+ query_params = [p for p in endpoint.get("parameters", []) if p.get("in") == "query"]
310
+ if query_params and query_params[0].get("required"):
311
+ param = query_params[0]
312
+ example = param.get("example", param.get("schema", {}).get("example", "value"))
313
+ curl += f"?{param['name']}={example}"
314
+
315
+ # Add headers
316
+ curl += " \\\n -H 'Authorization: Bearer $HF_TOKEN'"
317
+
318
+ # Add request body if applicable
319
+ if method in ["POST", "PUT", "PATCH"] and endpoint.get("request_body"):
320
+ content = endpoint["request_body"].get("content", {})
321
+ if "application/json" in content:
322
+ curl += " \\\n -H 'Content-Type: application/json'"
323
+ schema = content["application/json"].get("schema", {})
324
+ example = schema.get("example", "{}")
325
+ if isinstance(example, dict):
326
+ import json
327
+
328
+ example = json.dumps(example, indent=2)
329
+ curl += f" \\\n -d '{example}'"
330
+
331
+ return curl
332
+
333
+
334
+ def _format_parameters(parameters: list[dict[str, Any]]) -> str:
335
+ """Format parameter information from OpenAPI spec"""
336
+ if not parameters:
337
+ return ""
338
+
339
+ # Group parameters by type
340
+ path_params = [p for p in parameters if p.get("in") == "path"]
341
+ query_params = [p for p in parameters if p.get("in") == "query"]
342
+ header_params = [p for p in parameters if p.get("in") == "header"]
343
+
344
+ output = []
345
+
346
+ if path_params:
347
+ output.append("**Path Parameters:**")
348
+ for param in path_params:
349
+ name = param.get("name", "")
350
+ required = " (required)" if param.get("required") else " (optional)"
351
+ description = param.get("description", "")
352
+ param_type = param.get("schema", {}).get("type", "string")
353
+ example = param.get("example") or param.get("schema", {}).get("example", "")
354
+
355
+ output.append(f"- `{name}` ({param_type}){required}: {description}")
356
+ if example:
357
+ output.append(f" Example: `{example}`")
358
+
359
+ if query_params:
360
+ if output:
361
+ output.append("")
362
+ output.append("**Query Parameters:**")
363
+ for param in query_params:
364
+ name = param.get("name", "")
365
+ required = " (required)" if param.get("required") else " (optional)"
366
+ description = param.get("description", "")
367
+ param_type = param.get("schema", {}).get("type", "string")
368
+ example = param.get("example") or param.get("schema", {}).get("example", "")
369
+
370
+ output.append(f"- `{name}` ({param_type}){required}: {description}")
371
+ if example:
372
+ output.append(f" Example: `{example}`")
373
+
374
+ if header_params:
375
+ if output:
376
+ output.append("")
377
+ output.append("**Header Parameters:**")
378
+ for param in header_params:
379
+ name = param.get("name", "")
380
+ required = " (required)" if param.get("required") else " (optional)"
381
+ description = param.get("description", "")
382
+
383
+ output.append(f"- `{name}`{required}: {description}")
384
+
385
+ return "\n".join(output)
386
+
387
+
388
+ def _format_response_info(responses: dict[str, Any]) -> str:
389
+ """Format response information from OpenAPI spec"""
390
+ if not responses:
391
+ return "No response information available"
392
+
393
+ output = []
394
+ for status_code, response_obj in list(responses.items())[
395
+ :3
396
+ ]: # Show first 3 status codes
397
+ desc = response_obj.get("description", "")
398
+ output.append(f"- **{status_code}**: {desc}")
399
+
400
+ content = response_obj.get("content", {})
401
+ if "application/json" in content:
402
+ schema = content["application/json"].get("schema", {})
403
+ if "type" in schema:
404
+ output.append(f" Returns: {schema.get('type', 'object')}")
405
+
406
+ return "\n".join(output)
407
+
408
+
409
+ def _format_openapi_results(results: list[dict[str, Any]], tag: str) -> str:
410
+ """Format OpenAPI search results as markdown with curl examples"""
411
+ if not results:
412
+ return f"No API endpoints found with tag '{tag}'"
413
+
414
+ output = f"# API Endpoints for tag: `{tag}`\n\n"
415
+ output += f"Found {len(results)} endpoint(s)\n\n"
416
+ output += "---\n\n"
417
+
418
+ for i, endpoint in enumerate(results, 1):
419
+ output += f"## {i}. {endpoint['method']} {endpoint['path']}\n\n"
420
+
421
+ if endpoint["summary"]:
422
+ output += f"**Summary:** {endpoint['summary']}\n\n"
423
+
424
+ if endpoint["description"]:
425
+ desc = endpoint["description"][:300]
426
+ if len(endpoint["description"]) > 300:
427
+ desc += "..."
428
+ output += f"**Description:** {desc}\n\n"
429
 
430
+ # Parameters
431
+ params_info = _format_parameters(endpoint.get("parameters", []))
432
+ if params_info:
433
+ output += params_info + "\n\n"
434
+
435
+ # Curl example
436
+ output += "**Usage:**\n```bash\n"
437
+ output += _generate_curl_example(endpoint)
438
+ output += "\n```\n\n"
439
+
440
+ # Response info
441
+ output += "**Returns:**\n"
442
+ output += _format_response_info(endpoint["responses"])
443
+ output += "\n\n"
444
+
445
+ output += "---\n\n"
446
+
447
+ return output
448
+
449
+
450
+ async def search_openapi_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
451
+ """
452
+ Search the HuggingFace OpenAPI specification by tag
453
 
454
+ Args:
455
+ arguments: Dictionary with 'tag' parameter
 
456
 
457
+ Returns:
458
+ Tuple of (search_results, success)
459
+ """
460
+ start_time = time.perf_counter()
461
+ tag = arguments.get("tag", "")
462
+ print(f"[DEBUG] search_openapi: Starting for tag '{tag}'")
463
 
464
+ if not tag:
465
+ return "Error: No tag provided", False
466
 
467
+ try:
468
+ # Fetch OpenAPI spec (cached after first fetch)
469
+ spec = await _fetch_openapi_spec()
470
 
471
+ # Search for endpoints with this tag
472
+ results = _search_openapi_by_tag(spec, tag)
473
 
474
+ # Format results
475
+ formatted = _format_openapi_results(results, tag)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ total_time = time.perf_counter() - start_time
478
+ print(f"[DEBUG] search_openapi: Total time {total_time:.2f}s")
479
+
480
+ return formatted, True
481
 
482
  except httpx.HTTPStatusError as e:
483
+ return f"HTTP error fetching OpenAPI spec: {e.response.status_code}", False
 
 
 
484
  except httpx.RequestError as e:
485
+ return f"Request error: {str(e)}", False
486
  except Exception as e:
487
+ return f"Error searching OpenAPI spec: {str(e)}", False
488
 
489
 
490
  async def hf_docs_fetch_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
 
497
  Returns:
498
  Tuple of (full_markdown_content, success)
499
  """
500
+ start_time = time.perf_counter()
501
  url = arguments.get("url", "")
502
+ print(f"[DEBUG] fetch_hf_docs: Starting for URL '{url}'")
503
 
504
  if not url:
505
  return "Error: No URL provided", False
 
521
  # Make request with auth
522
  headers = {"Authorization": f"Bearer {hf_token}"}
523
 
524
+ fetch_start = time.perf_counter()
525
  async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
526
  response = await client.get(url, headers=headers)
527
  response.raise_for_status()
528
 
529
+ fetch_time = time.perf_counter() - fetch_start
530
  content = response.text
531
+ content_size_kb = len(content) / 1024
532
+
533
+ print(
534
+ f"[DEBUG] fetch_hf_docs: Fetched {content_size_kb:.1f}KB in {fetch_time:.2f}s"
535
+ )
536
 
537
  # Return the markdown content directly
538
  result = f"Documentation from: {url}\n\n{content}"
539
+
540
+ total_time = time.perf_counter() - start_time
541
+ print(f"[DEBUG] fetch_hf_docs: Total time {total_time:.2f}s")
542
+
543
  return result, True
544
 
545
  except httpx.HTTPStatusError as e:
 
559
  "name": "explore_docs_structure",
560
  "description": (
561
  "Explore the structure of HF documentation by parsing the sidebar navigation. "
562
+ "Select an endpoint from the available options and get a list of all documentation pages "
563
+ "with their titles, URLs, and a 300-character glimpse of each page. "
564
  "Use this to discover what documentation is available before fetching specific pages."
565
  ),
566
  "parameters": {
 
568
  "properties": {
569
  "endpoint": {
570
  "type": "string",
571
+ "enum": [
572
+ "hub",
573
+ "transformers",
574
+ "diffusers",
575
+ "datasets",
576
+ "gradio",
577
+ "trackio",
578
+ "smolagents",
579
+ "huggingface_hub",
580
+ "huggingface.js",
581
+ "transformers.js",
582
+ "inference-providers",
583
+ "inference-endpoints",
584
+ "peft",
585
+ "accelerate",
586
+ "optimum",
587
+ "optimum-habana",
588
+ "optimum-neuron",
589
+ "optimum-intel",
590
+ "optimum-executorch",
591
+ "optimum-tpu",
592
+ "tokenizers",
593
+ "llm-course",
594
+ "robotics-course",
595
+ "mcp-course",
596
+ "smol-course",
597
+ "agents-course",
598
+ "deep-rl-course",
599
+ "computer-vision-course",
600
+ "evaluate",
601
+ "tasks",
602
+ "dataset-viewer",
603
+ "trl",
604
+ "simulate",
605
+ "sagemaker",
606
+ "timm",
607
+ "safetensors",
608
+ "tgi",
609
+ "setfit",
610
+ "audio-course",
611
+ "lerobot",
612
+ "autotrain",
613
+ "tei",
614
+ "bitsandbytes",
615
+ "cookbook",
616
+ "sentence_transformers",
617
+ "ml-games-course",
618
+ "diffusion-course",
619
+ "ml-for-3d-course",
620
+ "chat-ui",
621
+ "leaderboards",
622
+ "lighteval",
623
+ "argilla",
624
+ "distilabel",
625
+ "microsoft-azure",
626
+ "kernels",
627
+ "google-cloud",
628
+ ],
629
  "description": (
630
+ "The documentation endpoint to explore. Each endpoint corresponds to a major section of the Hugging Face documentation:\n\n"
631
+ " hub Find answers to questions about models/datasets/spaces, auth, versioning, metadata.\n"
632
+ "• transformers — Core model library: architectures, configs, tokenizers, training & inference APIs.\n"
633
+ "• diffusers — Diffusion pipelines, schedulers, fine-tuning, training, and deployment patterns.\n"
634
+ "• datasets — Dataset loading, streaming, processing, Arrow format, Hub integration.\n"
635
+ "• gradio — UI components and demos for interacting with ML models.\n"
636
+ "• trackio — Experiment tracking, metrics logging, and run comparison.\n"
637
+ "• smolagents — Lightweight agent abstractions and tool-using patterns.\n"
638
+ "• huggingface_hub — Python client for Hub operations (auth, upload/download, repo management).\n"
639
+ "• huggingface.js — JS/TS client for Hub APIs in browser and Node.\n"
640
+ "• transformers.js — Run Transformer models in browser/Node via WebGPU/WASM.\n"
641
+ "• inference-providers — Unified interface for third-party inference backends.\n"
642
+ "• inference-endpoints — Managed, scalable model deployments on HF infrastructure.\n"
643
+ "• peft — Parameter-efficient fine-tuning methods (LoRA, adapters, etc.).\n"
644
+ "• accelerate — Hardware-agnostic, distributed and mixed-precision training orchestration.\n"
645
+ "• optimum — Hardware-aware optimization and model export tooling.\n"
646
+ "• optimum-habana — Training and inference on Habana Gaudi accelerators.\n"
647
+ "• optimum-neuron — Optimization workflows for AWS Inferentia/Trainium.\n"
648
+ "• optimum-intel — Intel CPU/GPU optimizations (OpenVINO, IPEX).\n"
649
+ "• optimum-executorch — Exporting models to ExecuTorch for edge/mobile.\n"
650
+ "• optimum-tpu — TPU-specific training and optimization paths.\n"
651
+ "• tokenizers — Fast tokenizer internals, training, and low-level APIs.\n"
652
+ "• llm-course — End-to-end LLM concepts, training, and deployment.\n"
653
+ "• robotics-course — Learning-based robotics foundations.\n"
654
+ "• mcp-course — Model Context Protocol concepts and usage.\n"
655
+ "• smol-course — Small-model and efficiency-focused workflows.\n"
656
+ "• agents-course — Tool-using, planning, and multi-step agent design.\n"
657
+ "• deep-rl-course — Deep reinforcement learning foundations.\n"
658
+ "• computer-vision-course — Vision models, datasets, and pipelines.\n"
659
+ "• evaluate — Metrics, evaluation workflows, and training-loop integration.\n"
660
+ "• tasks — Canonical task definitions and model categorization.\n"
661
+ "• dataset-viewer — Dataset preview, streaming views, and viewer internals.\n"
662
+ "• trl — RLHF, DPO, PPO, and SFT utilities for LLMs.\n"
663
+ "• simulate — Experimental simulation tools and workflows.\n"
664
+ "• sagemaker — Deploying Hugging Face models on AWS SageMaker.\n"
665
+ "• timm — Image model zoo and utilities via HF integrations.\n"
666
+ "• safetensors — Safe, fast tensor serialization format.\n"
667
+ "• tgi — High-throughput text generation server for LLMs.\n"
668
+ "• setfit — Few-shot text classification via sentence embeddings.\n"
669
+ "• audio-course — Speech and audio models, datasets, and tasks.\n"
670
+ "• lerobot — Robotics datasets, policies, and learning workflows.\n"
671
+ "• autotrain — No/low-code model training on Hugging Face.\n"
672
+ "• tei — Optimized inference server for embedding workloads.\n"
673
+ "• bitsandbytes — Quantization and memory-efficient optimizers.\n"
674
+ "• cookbook — Practical, task-oriented recipes across the ecosystem.\n"
675
+ "• sentence_transformers — Embedding models, training recipes, similarity/search workflows.\n"
676
+ "• ml-games-course — Game-based ML and reinforcement learning experiments.\n"
677
+ "• diffusion-course — Diffusion model theory and hands-on practice.\n"
678
+ "• ml-for-3d-course — 3D representations, models, and learning techniques.\n"
679
+ "• chat-ui — Reference chat interfaces for LLM deployment.\n"
680
+ "• leaderboards — Evaluation leaderboards and submission mechanics.\n"
681
+ "• lighteval — Lightweight, reproducible LLM evaluation framework.\n"
682
+ "• argilla — Data annotation, feedback, and human-in-the-loop workflows.\n"
683
+ "• distilabel — Synthetic data generation and distillation pipelines.\n"
684
+ "• microsoft-azure — Azure deployment and integration guides.\n"
685
+ "• kernels — Lightweight execution environments and notebook-style workflows.\n"
686
+ "• google-cloud — GCP deployment and serving workflows.\n"
687
  ),
688
  },
689
  },
 
714
  "required": ["url"],
715
  },
716
  }
717
+
718
+
719
+ async def _get_api_search_tool_spec() -> dict[str, Any]:
720
+ """
721
+ Dynamically generate the OpenAPI tool spec with tag enum populated at runtime
722
+ This must be called async to fetch the OpenAPI spec and extract tags
723
+ """
724
+ spec = await _fetch_openapi_spec()
725
+ tags = _extract_all_tags(spec)
726
+
727
+ return {
728
+ "name": "search_hf_api_endpoints",
729
+ "description": (
730
+ "Search the HuggingFace OpenAPI specification by tag to find related API endpoints. "
731
+ "Returns all endpoints with the specified tag including curl examples showing how to use them. "
732
+ "Each result includes the endpoint path, summary, usage example with curl, and response information."
733
+ ),
734
+ "parameters": {
735
+ "type": "object",
736
+ "properties": {
737
+ "tag": {
738
+ "type": "string",
739
+ "enum": tags,
740
+ "description": (
741
+ "The API tag to search for. Each tag groups related API endpoints. "
742
+ ),
743
+ },
744
+ },
745
+ "required": ["tag"],
746
+ },
747
+ }
agent/tools/search_docs_tool.py CHANGED
@@ -12,8 +12,11 @@ from agent.config import Config
12
  from agent.core.session import Session
13
 
14
 
15
- def create_search_tool_router():
16
- """Create a ToolRouter instance for the search sub-agent"""
 
 
 
17
  # Import at runtime to avoid circular dependency
18
  from agent.core.tools import ToolRouter
19
 
@@ -26,10 +29,15 @@ def create_search_tool_router():
26
  self._mcp_initialized = False
27
  self.mcp_client = None
28
 
29
- for tool in make_search_agent_tools():
 
 
 
30
  self.register_tool(tool)
31
 
32
- return SearchDocsToolRouter()
 
 
33
 
34
 
35
  async def search_docs_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
@@ -56,7 +64,7 @@ async def search_docs_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
56
  sub_event_queue = asyncio.Queue()
57
 
58
  # Create specialized tool router for search
59
- search_tool_router = create_search_tool_router()
60
 
61
  # Create config for sub-agent (using same model as main agent)
62
  sub_config = Config(
@@ -131,19 +139,25 @@ SEARCH_DOCS_TOOL_SPEC = {
131
 
132
 
133
 
134
- def make_search_agent_tools():
135
  """
136
  Create a list of tools for the search agent
 
137
  """
138
  # Import at runtime to avoid circular dependency
139
  from agent.core.tools import ToolSpec
140
  from agent.tools._search_agent_tools import (
141
  EXPLORE_DOCS_STRUCTURE_TOOL_SPEC,
142
  HF_DOCS_FETCH_TOOL_SPEC,
 
143
  explore_docs_structure_handler,
144
  hf_docs_fetch_handler,
 
145
  )
146
 
 
 
 
147
  return [
148
  ToolSpec(
149
  name=EXPLORE_DOCS_STRUCTURE_TOOL_SPEC["name"],
@@ -157,4 +171,10 @@ def make_search_agent_tools():
157
  parameters=HF_DOCS_FETCH_TOOL_SPEC["parameters"],
158
  handler=hf_docs_fetch_handler,
159
  ),
 
 
 
 
 
 
160
  ]
 
12
  from agent.core.session import Session
13
 
14
 
15
+ async def create_search_tool_router():
16
+ """
17
+ Create a ToolRouter instance for the search sub-agent
18
+ Async because OpenAPI tool needs to fetch and parse spec at initialization
19
+ """
20
  # Import at runtime to avoid circular dependency
21
  from agent.core.tools import ToolRouter
22
 
 
29
  self._mcp_initialized = False
30
  self.mcp_client = None
31
 
32
+ async def initialize_tools(self):
33
+ """Initialize tools asynchronously"""
34
+ tools = await make_search_agent_tools()
35
+ for tool in tools:
36
  self.register_tool(tool)
37
 
38
+ router = SearchDocsToolRouter()
39
+ await router.initialize_tools()
40
+ return router
41
 
42
 
43
  async def search_docs_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
 
64
  sub_event_queue = asyncio.Queue()
65
 
66
  # Create specialized tool router for search
67
+ search_tool_router = await create_search_tool_router()
68
 
69
  # Create config for sub-agent (using same model as main agent)
70
  sub_config = Config(
 
139
 
140
 
141
 
142
+ async def make_search_agent_tools():
143
  """
144
  Create a list of tools for the search agent
145
+ Async because OpenAPI tool spec needs to be populated at runtime
146
  """
147
  # Import at runtime to avoid circular dependency
148
  from agent.core.tools import ToolSpec
149
  from agent.tools._search_agent_tools import (
150
  EXPLORE_DOCS_STRUCTURE_TOOL_SPEC,
151
  HF_DOCS_FETCH_TOOL_SPEC,
152
+ _get_api_search_tool_spec,
153
  explore_docs_structure_handler,
154
  hf_docs_fetch_handler,
155
+ search_openapi_handler,
156
  )
157
 
158
+ # Get the OpenAPI tool spec with dynamically populated tags
159
+ openapi_spec = await _get_api_search_tool_spec()
160
+
161
  return [
162
  ToolSpec(
163
  name=EXPLORE_DOCS_STRUCTURE_TOOL_SPEC["name"],
 
171
  parameters=HF_DOCS_FETCH_TOOL_SPEC["parameters"],
172
  handler=hf_docs_fetch_handler,
173
  ),
174
+ ToolSpec(
175
+ name=openapi_spec["name"],
176
+ description=openapi_spec["description"],
177
+ parameters=openapi_spec["parameters"],
178
+ handler=search_openapi_handler,
179
+ ),
180
  ]
run_search_agent.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Standalone test script for the search sub-agent
3
+ Run with: uv run python test_search_agent.py
4
+ """
5
+
6
+ import asyncio
7
+
8
+ from litellm.utils import get_max_tokens
9
+
10
+ from agent.config import Config
11
+ from agent.context_manager.manager import ContextManager
12
+ from agent.core.agent_loop import Handlers
13
+ from agent.core.session import Session
14
+ from agent.tools.search_docs_tool import create_search_tool_router
15
+
16
+
17
+ async def test_search_agent(query: str):
18
+ """Test the search sub-agent with a query"""
19
+ print(f"Testing search agent with query: {query}\n")
20
+ print("=" * 60)
21
+
22
+ # Create event queue for the sub-agent
23
+ sub_event_queue = asyncio.Queue()
24
+
25
+ # Create search tool router
26
+ search_tool_router = await create_search_tool_router()
27
+
28
+ # Create config
29
+ sub_config = Config(
30
+ model_name="anthropic/claude-haiku-4-5",
31
+ )
32
+
33
+ # Create session with custom system prompt
34
+ sub_session = Session(
35
+ event_queue=sub_event_queue,
36
+ config=sub_config,
37
+ tool_router=search_tool_router,
38
+ context_manager=ContextManager(
39
+ tool_specs=search_tool_router.get_tool_specs_for_llm(),
40
+ max_context=get_max_tokens(sub_config.model_name),
41
+ compact_size=0.1,
42
+ untouched_messages=5,
43
+ prompt_file_suffix="search_docs_system_prompt.yaml",
44
+ ),
45
+ )
46
+
47
+ # Event listener to show what the sub-agent is doing
48
+ async def event_monitor():
49
+ while True:
50
+ try:
51
+ event = await asyncio.wait_for(sub_event_queue.get(), timeout=1.0)
52
+
53
+ if event.event_type == "assistant_message":
54
+ content = event.data.get("content", "") if event.data else ""
55
+ if content:
56
+ print(f"\n🤖 Sub-agent: {content}\n")
57
+
58
+ elif event.event_type == "tool_call":
59
+ tool_name = event.data.get("tool", "") if event.data else ""
60
+ arguments = event.data.get("arguments", {}) if event.data else {}
61
+ print(f"🔧 Tool call: {tool_name}")
62
+ print(f" Args: {arguments}")
63
+
64
+ elif event.event_type == "tool_output":
65
+ output = event.data.get("output", "") if event.data else ""
66
+ success = event.data.get("success", False) if event.data else False
67
+ status = "✅" if success else "❌"
68
+
69
+ print(f"{status} Tool output: {output}\n")
70
+
71
+ elif event.event_type == "turn_complete":
72
+ print("✅ Sub-agent turn complete")
73
+ break
74
+
75
+ except asyncio.TimeoutError:
76
+ # Check if agent is still running
77
+ continue
78
+ except Exception as e:
79
+ print(f"⚠️ Event error: {e}")
80
+ break
81
+
82
+ # Run the sub-agent and event monitor concurrently
83
+ async with search_tool_router:
84
+ monitor_task = asyncio.create_task(event_monitor())
85
+
86
+ result = await Handlers.run_agent(
87
+ session=sub_session, text=query, max_iterations=30
88
+ )
89
+
90
+ # Wait for event monitor to finish
91
+ await asyncio.wait_for(monitor_task, timeout=5.0)
92
+
93
+ print("\n" + "=" * 60)
94
+ print("FINAL RESULT:")
95
+ print("=" * 60)
96
+ if result:
97
+ print(result)
98
+ else:
99
+ print("No result returned")
100
+ print("=" * 60)
101
+
102
+
103
+ async def main():
104
+ """Main test function"""
105
+ print("🧪 Search Sub-Agent Test\n")
106
+
107
+ # Example queries to test
108
+ test_queries = [
109
+ # "Explore the TRL documentation structure and find information about DPO trainer",
110
+ # "is there a way to get the logs from a served huggingface space",
111
+ # "How do I train GLM4.7 with a GRPO training loop with trl with llm judge as a reward model for training on hle?"
112
+ "can i stream logs through the api for a served huggingface space",
113
+ ]
114
+
115
+ for i, query in enumerate(test_queries, 1):
116
+ print(f"\n{'=' * 60}")
117
+ print(f"TEST {i}/{len(test_queries)}")
118
+ print(f"{'=' * 60}\n")
119
+
120
+ try:
121
+ await test_search_agent(query)
122
+ except Exception as e:
123
+ print(f"\n❌ Test failed: {e}")
124
+ import traceback
125
+
126
+ traceback.print_exc()
127
+
128
+ if i < len(test_queries):
129
+ print("\n\nPress Enter to continue to next test...")
130
+ input()
131
+
132
+
133
+ if __name__ == "__main__":
134
+ try:
135
+ asyncio.run(main())
136
+ except KeyboardInterrupt:
137
+ print("\n\n⚠️ Test interrupted")
138
+ except Exception as e:
139
+ print(f"\n❌ Error: {e}")
140
+ import traceback
141
+
142
+ traceback.print_exc()