akseljoonas HF Staff commited on
Commit
b70fed7
·
2 Parent(s): 8362e4d71477da

Merge branch 'main' into private-repo-tool

Browse files

Resolved merge conflicts between PR #7 (streamlined architecture) and PR #8 (private repo tools):

**Conflicts resolved:**
- agent/core/tools.py: Merged all tools (docs, plan, jobs, private_repos, utils)
- agent/main.py: Integrated private repo approvals into batch approval system
- agent/tools/jobs_tool.py: Kept PR #7 improvements + added hf_private_repos note

**Key integration points:**
- Private repo operations (create_repo, upload_file) now use same batch approval flow as jobs
- Tool ordering: explore_hf_docs → fetch_hf_docs → plan → hf_jobs → hf_private_repos → utils
- Jobs tool description includes note about using hf_private_repos for persistent storage

Both branches' functionality preserved and integrated.

agent/core/agent_loop.py CHANGED
@@ -131,32 +131,23 @@ class Handlers:
131
  Event(event_type="assistant_message", data={"content": content})
132
  )
133
 
134
- # Execute tools
 
 
 
135
  for tc in tool_calls:
136
  tool_name = tc.function.name
137
  tool_args = json.loads(tc.function.arguments)
138
 
139
- # Check if this tool requires user approval
140
  if _needs_approval(tool_name, tool_args):
141
- await session.send_event(
142
- Event(
143
- event_type="approval_required",
144
- data={
145
- "tool": tool_name,
146
- "arguments": tool_args,
147
- "tool_call_id": tc.id,
148
- },
149
- )
150
- )
151
-
152
- # Store pending approval and return early
153
- session.pending_approval = {
154
- "tool_call": tc,
155
- "arguments": tool_args,
156
- }
157
 
158
- # Return early - wait for EXEC_APPROVAL operation
159
- return None
 
 
160
 
161
  # Validate tool arguments before calling
162
  args_valid, error_msg = _validate_tool_args(tool_args)
@@ -196,6 +187,39 @@ class Handlers:
196
  )
197
  )
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  iteration += 1
200
 
201
  except Exception as e:
@@ -260,10 +284,8 @@ class Handlers:
260
  await session.send_event(Event(event_type="undo_complete"))
261
 
262
  @staticmethod
263
- async def exec_approval(
264
- session: Session, approved: bool, feedback: str | None = None
265
- ) -> None:
266
- """Handle job execution approval"""
267
  if not session.pending_approval:
268
  await session.send_event(
269
  Event(
@@ -273,12 +295,36 @@ class Handlers:
273
  )
274
  return
275
 
276
- tc = session.pending_approval["tool_call"]
277
- tool_args = session.pending_approval["arguments"]
278
- tool_name = tc.function.name
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- if approved:
281
- # Execute the pending tool
 
 
 
 
 
 
 
 
 
 
 
282
  await session.send_event(
283
  Event(
284
  event_type="tool_call",
@@ -288,34 +334,57 @@ class Handlers:
288
 
289
  output, success = await session.tool_router.call_tool(tool_name, tool_args)
290
 
291
- # Add tool result to context
292
- tool_msg = Message(
293
- role="tool",
294
- content=output,
295
- tool_call_id=tc.id,
296
- name=tool_name,
 
 
 
 
297
  )
298
- session.context_manager.add_message(tool_msg)
299
 
300
- await session.send_event(
301
- Event(
302
- event_type="tool_output",
303
- data={
304
- "tool": tool_name,
305
- "output": output,
306
- "success": success,
307
- },
 
 
 
 
 
 
 
308
  )
309
- )
310
- else:
311
- # User rejected - add cancellation message to context
312
- cancellation_msg = "Job execution cancelled by user"
313
- if feedback:
314
- cancellation_msg += f". User feedback: {feedback}"
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  tool_msg = Message(
317
  role="tool",
318
- content=cancellation_msg,
319
  tool_call_id=tc.id,
320
  name=tool_name,
321
  )
@@ -326,7 +395,7 @@ class Handlers:
326
  event_type="tool_output",
327
  data={
328
  "tool": tool_name,
329
- "output": cancellation_msg,
330
  "success": False,
331
  },
332
  )
@@ -335,7 +404,7 @@ class Handlers:
335
  # Clear pending approval
336
  session.pending_approval = None
337
 
338
- # Continue agent loop with empty input to process the tool result
339
  await Handlers.run_agent(session, "")
340
 
341
  @staticmethod
@@ -374,9 +443,8 @@ async def process_submission(session: Session, submission) -> bool:
374
  return True
375
 
376
  if op.op_type == OpType.EXEC_APPROVAL:
377
- approved = op.data.get("approved", False) if op.data else False
378
- feedback = op.data.get("feedback") if op.data else None
379
- await Handlers.exec_approval(session, approved, feedback)
380
  return True
381
 
382
  if op.op_type == OpType.SHUTDOWN:
 
131
  Event(event_type="assistant_message", data={"content": content})
132
  )
133
 
134
+ # Separate tools into those requiring approval and those that don't
135
+ approval_required_tools = []
136
+ non_approval_tools = []
137
+
138
  for tc in tool_calls:
139
  tool_name = tc.function.name
140
  tool_args = json.loads(tc.function.arguments)
141
 
 
142
  if _needs_approval(tool_name, tool_args):
143
+ approval_required_tools.append(tc)
144
+ else:
145
+ non_approval_tools.append(tc)
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
+ # Execute non-approval tools first
148
+ for tc in non_approval_tools:
149
+ tool_name = tc.function.name
150
+ tool_args = json.loads(tc.function.arguments)
151
 
152
  # Validate tool arguments before calling
153
  args_valid, error_msg = _validate_tool_args(tool_args)
 
187
  )
188
  )
189
 
190
+ # If there are tools requiring approval, ask for batch approval
191
+ if approval_required_tools:
192
+ # Prepare batch approval data
193
+ tools_data = []
194
+ for tc in approval_required_tools:
195
+ tool_name = tc.function.name
196
+ tool_args = json.loads(tc.function.arguments)
197
+ tools_data.append(
198
+ {
199
+ "tool": tool_name,
200
+ "arguments": tool_args,
201
+ "tool_call_id": tc.id,
202
+ }
203
+ )
204
+
205
+ await session.send_event(
206
+ Event(
207
+ event_type="approval_required",
208
+ data={
209
+ "tools": tools_data, # Batch of tools
210
+ "count": len(tools_data),
211
+ },
212
+ )
213
+ )
214
+
215
+ # Store all approval-requiring tools
216
+ session.pending_approval = {
217
+ "tool_calls": approval_required_tools,
218
+ }
219
+
220
+ # Return early - wait for EXEC_APPROVAL operation
221
+ return None
222
+
223
  iteration += 1
224
 
225
  except Exception as e:
 
284
  await session.send_event(Event(event_type="undo_complete"))
285
 
286
  @staticmethod
287
+ async def exec_approval(session: Session, approvals: list[dict]) -> None:
288
+ """Handle batch job execution approval"""
 
 
289
  if not session.pending_approval:
290
  await session.send_event(
291
  Event(
 
295
  )
296
  return
297
 
298
+ tool_calls = session.pending_approval.get("tool_calls", [])
299
+ if not tool_calls:
300
+ await session.send_event(
301
+ Event(
302
+ event_type="error",
303
+ data={"error": "No pending tool calls found"},
304
+ )
305
+ )
306
+ return
307
+
308
+ # Create a map of tool_call_id -> approval decision
309
+ approval_map = {a["tool_call_id"]: a for a in approvals}
310
+
311
+ # Separate approved and rejected tool calls
312
+ approved_tasks = []
313
+ rejected_tasks = []
314
 
315
+ for tc in tool_calls:
316
+ tool_name = tc.function.name
317
+ tool_args = json.loads(tc.function.arguments)
318
+ approval_decision = approval_map.get(tc.id, {"approved": False})
319
+
320
+ if approval_decision.get("approved", False):
321
+ approved_tasks.append((tc, tool_name, tool_args))
322
+ else:
323
+ rejected_tasks.append((tc, tool_name, approval_decision))
324
+
325
+ # Execute all approved tools concurrently
326
+ async def execute_tool(tc, tool_name, tool_args):
327
+ """Execute a single tool and return its result"""
328
  await session.send_event(
329
  Event(
330
  event_type="tool_call",
 
334
 
335
  output, success = await session.tool_router.call_tool(tool_name, tool_args)
336
 
337
+ return (tc, tool_name, output, success)
338
+
339
+ # Execute all approved tools concurrently and wait for ALL to complete
340
+ if approved_tasks:
341
+ results = await asyncio.gather(
342
+ *[
343
+ execute_tool(tc, tool_name, tool_args)
344
+ for tc, tool_name, tool_args in approved_tasks
345
+ ],
346
+ return_exceptions=True,
347
  )
 
348
 
349
+ # Process results and add to context
350
+ for result in results:
351
+ if isinstance(result, Exception):
352
+ # Handle execution error
353
+ print(f"Tool execution error: {result}")
354
+ continue
355
+
356
+ tc, tool_name, output, success = result
357
+
358
+ # Add tool result to context
359
+ tool_msg = Message(
360
+ role="tool",
361
+ content=output,
362
+ tool_call_id=tc.id,
363
+ name=tool_name,
364
  )
365
+ session.context_manager.add_message(tool_msg)
366
+
367
+ await session.send_event(
368
+ Event(
369
+ event_type="tool_output",
370
+ data={
371
+ "tool": tool_name,
372
+ "output": output,
373
+ "success": success,
374
+ },
375
+ )
376
+ )
377
+
378
+ # Process rejected tools
379
+ for tc, tool_name, approval_decision in rejected_tasks:
380
+ rejection_msg = "Job execution cancelled by user"
381
+ user_feedback = approval_decision.get("feedback")
382
+ if user_feedback:
383
+ rejection_msg += f". User feedback: {user_feedback}"
384
 
385
  tool_msg = Message(
386
  role="tool",
387
+ content=rejection_msg,
388
  tool_call_id=tc.id,
389
  name=tool_name,
390
  )
 
395
  event_type="tool_output",
396
  data={
397
  "tool": tool_name,
398
+ "output": rejection_msg,
399
  "success": False,
400
  },
401
  )
 
404
  # Clear pending approval
405
  session.pending_approval = None
406
 
407
+ # Continue agent loop with empty input to process the tool results
408
  await Handlers.run_agent(session, "")
409
 
410
  @staticmethod
 
443
  return True
444
 
445
  if op.op_type == OpType.EXEC_APPROVAL:
446
+ approvals = op.data.get("approvals", []) if op.data else []
447
+ await Handlers.exec_approval(session, approvals)
 
448
  return True
449
 
450
  if op.op_type == OpType.SHUTDOWN:
agent/core/tools.py CHANGED
@@ -13,13 +13,18 @@ from lmnr import observe
13
  from mcp.types import EmbeddedResource, ImageContent, TextContent
14
 
15
  from agent.config import MCPServerConfig
 
 
 
 
 
 
 
 
16
  from agent.tools.private_hf_repo_tools import (
17
  PRIVATE_HF_REPO_TOOL_SPEC,
18
  private_hf_repo_handler,
19
  )
20
- from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, hf_jobs_handler
21
- from agent.tools.plan_tool import PLAN_TOOL_SPEC, plan_tool_handler
22
- from agent.tools.search_docs_tool import SEARCH_DOCS_TOOL_SPEC, search_docs_handler
23
  from agent.tools.utils_tools import UTILS_TOOL_SPEC, utils_handler
24
 
25
  # Suppress aiohttp deprecation warning
@@ -127,6 +132,27 @@ class ToolRouter:
127
  )
128
  )
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  def get_tool_specs_for_llm(self) -> list[dict[str, Any]]:
131
  """Get tool specifications in OpenAI format"""
132
  specs = []
@@ -150,6 +176,10 @@ class ToolRouter:
150
  await self.register_mcp_tools()
151
  self._mcp_initialized = True
152
  print(f"MCP initialized: {self._mcp_initialized}")
 
 
 
 
153
  return self
154
 
155
  async def __aexit__(self, exc_type, exc, tb) -> None:
@@ -194,9 +224,30 @@ class ToolRouter:
194
  def create_builtin_tools() -> list[ToolSpec]:
195
  """Create built-in tool specifications"""
196
  print(
197
- f"Creating built-in tools: {HF_JOBS_TOOL_SPEC['name']}, {PRIVATE_HF_REPO_TOOL_SPEC['name']}, {SEARCH_DOCS_TOOL_SPEC['name']}, {PLAN_TOOL_SPEC['name']}, {UTILS_TOOL_SPEC['name']}"
198
  )
 
199
  return [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  ToolSpec(
201
  name=HF_JOBS_TOOL_SPEC["name"],
202
  description=HF_JOBS_TOOL_SPEC["description"],
@@ -209,18 +260,6 @@ def create_builtin_tools() -> list[ToolSpec]:
209
  parameters=PRIVATE_HF_REPO_TOOL_SPEC["parameters"],
210
  handler=private_hf_repo_handler,
211
  ),
212
- ToolSpec(
213
- name=SEARCH_DOCS_TOOL_SPEC["name"],
214
- description=SEARCH_DOCS_TOOL_SPEC["description"],
215
- parameters=SEARCH_DOCS_TOOL_SPEC["parameters"],
216
- handler=search_docs_handler,
217
- ),
218
- ToolSpec(
219
- name=PLAN_TOOL_SPEC["name"],
220
- description=PLAN_TOOL_SPEC["description"],
221
- parameters=PLAN_TOOL_SPEC["parameters"],
222
- handler=plan_tool_handler,
223
- ),
224
  ToolSpec(
225
  name=UTILS_TOOL_SPEC["name"],
226
  description=UTILS_TOOL_SPEC["description"],
 
13
  from mcp.types import EmbeddedResource, ImageContent, TextContent
14
 
15
  from agent.config import MCPServerConfig
16
+ from agent.tools.docs_tools import (
17
+ EXPLORE_HF_DOCS_TOOL_SPEC,
18
+ HF_DOCS_FETCH_TOOL_SPEC,
19
+ explore_hf_docs_handler,
20
+ hf_docs_fetch_handler,
21
+ )
22
+ from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, hf_jobs_handler
23
+ from agent.tools.plan_tool import PLAN_TOOL_SPEC, plan_tool_handler
24
  from agent.tools.private_hf_repo_tools import (
25
  PRIVATE_HF_REPO_TOOL_SPEC,
26
  private_hf_repo_handler,
27
  )
 
 
 
28
  from agent.tools.utils_tools import UTILS_TOOL_SPEC, utils_handler
29
 
30
  # Suppress aiohttp deprecation warning
 
132
  )
133
  )
134
 
135
+ async def register_openapi_tool(self) -> None:
136
+ """Register the OpenAPI search tool (requires async initialization)"""
137
+ from agent.tools.docs_tools import (
138
+ _get_api_search_tool_spec,
139
+ search_openapi_handler,
140
+ )
141
+
142
+ print("Registering OpenAPI search tool...")
143
+
144
+ # Register search_hf_api_endpoints with dynamic spec
145
+ openapi_spec = await _get_api_search_tool_spec()
146
+ self.register_tool(
147
+ ToolSpec(
148
+ name=openapi_spec["name"],
149
+ description=openapi_spec["description"],
150
+ parameters=openapi_spec["parameters"],
151
+ handler=search_openapi_handler,
152
+ )
153
+ )
154
+ print(f"Registered: {openapi_spec['name']}")
155
+
156
  def get_tool_specs_for_llm(self) -> list[dict[str, Any]]:
157
  """Get tool specifications in OpenAI format"""
158
  specs = []
 
176
  await self.register_mcp_tools()
177
  self._mcp_initialized = True
178
  print(f"MCP initialized: {self._mcp_initialized}")
179
+
180
+ # Register OpenAPI tool (requires async initialization)
181
+ await self.register_openapi_tool()
182
+
183
  return self
184
 
185
  async def __aexit__(self, exc_type, exc, tb) -> None:
 
224
  def create_builtin_tools() -> list[ToolSpec]:
225
  """Create built-in tool specifications"""
226
  print(
227
+ f"Creating built-in tools: {EXPLORE_HF_DOCS_TOOL_SPEC['name']}, {HF_DOCS_FETCH_TOOL_SPEC['name']}, {PLAN_TOOL_SPEC['name']}, {HF_JOBS_TOOL_SPEC['name']}, {PRIVATE_HF_REPO_TOOL_SPEC['name']}, {UTILS_TOOL_SPEC['name']}"
228
  )
229
+ # in order of importance
230
  return [
231
+ # Documentation search tools
232
+ ToolSpec(
233
+ name=EXPLORE_HF_DOCS_TOOL_SPEC["name"],
234
+ description=EXPLORE_HF_DOCS_TOOL_SPEC["description"],
235
+ parameters=EXPLORE_HF_DOCS_TOOL_SPEC["parameters"],
236
+ handler=explore_hf_docs_handler,
237
+ ),
238
+ ToolSpec(
239
+ name=HF_DOCS_FETCH_TOOL_SPEC["name"],
240
+ description=HF_DOCS_FETCH_TOOL_SPEC["description"],
241
+ parameters=HF_DOCS_FETCH_TOOL_SPEC["parameters"],
242
+ handler=hf_docs_fetch_handler,
243
+ ),
244
+ # Planning and job management tools
245
+ ToolSpec(
246
+ name=PLAN_TOOL_SPEC["name"],
247
+ description=PLAN_TOOL_SPEC["description"],
248
+ parameters=PLAN_TOOL_SPEC["parameters"],
249
+ handler=plan_tool_handler,
250
+ ),
251
  ToolSpec(
252
  name=HF_JOBS_TOOL_SPEC["name"],
253
  description=HF_JOBS_TOOL_SPEC["description"],
 
260
  parameters=PRIVATE_HF_REPO_TOOL_SPEC["parameters"],
261
  handler=private_hf_repo_handler,
262
  ),
 
 
 
 
 
 
 
 
 
 
 
 
263
  ToolSpec(
264
  name=UTILS_TOOL_SPEC["name"],
265
  description=UTILS_TOOL_SPEC["description"],
agent/main.py CHANGED
@@ -11,6 +11,7 @@ from typing import Any, Optional
11
 
12
  import litellm
13
  from lmnr import Laminar, LaminarLiteLLMCallback
 
14
 
15
  from agent.config import load_config
16
  from agent.core.agent_loop import submission_loop
@@ -70,6 +71,7 @@ async def event_listener(
70
  submission_queue: asyncio.Queue,
71
  turn_complete_event: asyncio.Event,
72
  ready_event: asyncio.Event,
 
73
  ) -> None:
74
  """Background task that listens for events and displays them"""
75
  submission_id = [1000] # Use list to make it mutable in closure
@@ -126,106 +128,162 @@ async def event_listener(
126
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
127
  print(f"Compacted context: {old_tokens} → {new_tokens} tokens")
128
  elif event.event_type == "approval_required":
129
- # Display job details and prompt for approval
130
- tool_name = event.data.get("tool", "") if event.data else ""
131
- arguments = event.data.get("arguments", {}) if event.data else {}
132
 
133
- operation = arguments.get("operation", "")
134
- args = _safe_get_args(arguments)
135
-
136
- print(f"\nOperation: {operation}")
137
-
138
- if operation == "uv":
139
- script = args.get("script", "")
140
- dependencies = args.get("dependencies", [])
141
- print(f"Script to run:\n{script}")
142
- if dependencies:
143
- print(f"Dependencies: {', '.join(dependencies)}")
144
- elif operation == "run":
145
- image = args.get("image", "")
146
- command = args.get("command", "")
147
- print(f"Docker image: {image}")
148
- print(f"Command: {command}")
149
-
150
- # Common parameters
151
- flavor = args.get("flavor", "cpu-basic")
152
- detached = args.get("detached", False)
153
- print(f"Hardware: {flavor}")
154
- print(f"Detached mode: {detached}")
155
-
156
- secrets = args.get("secrets", [])
157
- if secrets:
158
- print(f"Secrets: {', '.join(secrets)}")
159
- elif operation in ["create_repo", "upload_file"]:
160
- repo_id = args.get("repo_id", "")
161
- repo_type = args.get("repo_type", "dataset")
162
-
163
- # Build repo URL
164
- type_path = "" if repo_type == "model" else f"{repo_type}s"
165
- repo_url = f"https://huggingface.co/{type_path}/{repo_id}".replace("//", "/")
166
-
167
- print(f"Repository: {repo_id}")
168
- print(f"Type: {repo_type}")
169
- print(f"Private: Yes")
170
- print(f"URL: {repo_url}")
171
-
172
- # Show file preview for upload_file operation
173
- if operation == "upload_file":
174
- path_in_repo = args.get("path_in_repo", "")
175
- file_content = args.get("file_content", "")
176
- print(f"File: {path_in_repo}")
177
-
178
- if isinstance(file_content, str):
179
- # Calculate metrics
180
- all_lines = file_content.split('\n')
181
- line_count = len(all_lines)
182
- size_bytes = len(file_content.encode('utf-8'))
183
- size_kb = size_bytes / 1024
184
- size_mb = size_kb / 1024
185
-
186
- print(f"Line count: {line_count}")
187
- if size_kb < 1024:
188
- print(f"Size: {size_kb:.2f} KB")
189
- else:
190
- print(f"Size: {size_mb:.2f} MB")
191
-
192
- # Show preview
193
- preview_lines = all_lines[:5]
194
- preview = '\n'.join(preview_lines)
195
- print(f"Content preview (first 5 lines):\n{preview}")
196
- if len(all_lines) > 5:
197
- print("...")
198
-
199
- # Get user decision
200
  print("\n" + format_separator())
201
- if tool_name == "hf_jobs":
202
- header_text = "JOB EXECUTION APPROVAL REQUIRED"
203
- elif operation == "upload_file":
204
- header_text = "FILE UPLOAD APPROVAL REQUIRED"
205
- else:
206
- header_text = "REPO CREATION APPROVAL REQUIRED"
207
- print(format_header(header_text))
208
- print(format_separator())
209
- loop = asyncio.get_event_loop()
210
- response = await loop.run_in_executor(
211
- None,
212
- input,
213
- "Approve? (y=yes, n=no, or provide feedback to reject): ",
214
- )
215
-
216
- response = response.strip()
217
- approved = response.lower() in ["y", "yes"]
218
- feedback = (
219
- None if approved or response.lower() in ["n", "no"] else response
220
  )
 
221
 
222
- # Submit approval
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  submission_id[0] += 1
224
  approval_submission = Submission(
225
  id=f"approval_{submission_id[0]}",
226
  operation=Operation(
227
  op_type=OpType.EXEC_APPROVAL,
228
- data={"approved": approved, "feedback": feedback},
229
  ),
230
  )
231
  await submission_queue.put(approval_submission)
@@ -238,10 +296,9 @@ async def event_listener(
238
  print(f"Event listener error: {e}")
239
 
240
 
241
- async def get_user_input() -> str:
242
  """Get user input asynchronously"""
243
- loop = asyncio.get_event_loop()
244
- return await loop.run_in_executor(None, input, "You: ")
245
 
246
 
247
  async def main():
@@ -282,6 +339,9 @@ async def main():
282
  print(f"Config: {config.mcpServers}")
283
  tool_router = ToolRouter(config.mcpServers)
284
 
 
 
 
285
  agent_task = asyncio.create_task(
286
  submission_loop(
287
  submission_queue,
@@ -293,7 +353,13 @@ async def main():
293
 
294
  # Start event listener in background
295
  listener_task = asyncio.create_task(
296
- event_listener(event_queue, submission_queue, turn_complete_event, ready_event)
 
 
 
 
 
 
297
  )
298
 
299
  # Wait for agent to initialize
@@ -310,7 +376,7 @@ async def main():
310
 
311
  # Get user input
312
  try:
313
- user_input = await get_user_input()
314
  except EOFError:
315
  break
316
 
 
11
 
12
  import litellm
13
  from lmnr import Laminar, LaminarLiteLLMCallback
14
+ from prompt_toolkit import PromptSession
15
 
16
  from agent.config import load_config
17
  from agent.core.agent_loop import submission_loop
 
71
  submission_queue: asyncio.Queue,
72
  turn_complete_event: asyncio.Event,
73
  ready_event: asyncio.Event,
74
+ prompt_session: PromptSession,
75
  ) -> None:
76
  """Background task that listens for events and displays them"""
77
  submission_id = [1000] # Use list to make it mutable in closure
 
128
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
129
  print(f"Compacted context: {old_tokens} → {new_tokens} tokens")
130
  elif event.event_type == "approval_required":
131
+ # Handle batch approval format
132
+ tools_data = event.data.get("tools", []) if event.data else []
133
+ count = event.data.get("count", 0) if event.data else 0
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  print("\n" + format_separator())
136
+ print(
137
+ format_header(
138
+ f"APPROVAL REQUIRED ({count} item{'s' if count != 1 else ''})"
139
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  )
141
+ print(format_separator())
142
 
143
+ approvals = []
144
+
145
+ # Ask for approval for each tool
146
+ for i, tool_info in enumerate(tools_data, 1):
147
+ tool_name = tool_info.get("tool", "")
148
+ arguments = tool_info.get("arguments", {})
149
+ tool_call_id = tool_info.get("tool_call_id", "")
150
+
151
+ # Handle case where arguments might be a JSON string
152
+ if isinstance(arguments, str):
153
+ try:
154
+ arguments = json.loads(arguments)
155
+ except json.JSONDecodeError:
156
+ print(f"Warning: Failed to parse arguments for {tool_name}")
157
+ arguments = {}
158
+
159
+ operation = arguments.get("operation", "")
160
+
161
+ print(f"\n[Item {i}/{count}]")
162
+ print(f"Tool: {tool_name}")
163
+ print(f"Operation: {operation}")
164
+
165
+ # Handle different tool types
166
+ if tool_name == "hf_jobs":
167
+ # Check if this is Python mode (script) or Docker mode (command)
168
+ script = arguments.get("script")
169
+ command = arguments.get("command")
170
+
171
+ if script:
172
+ # Python mode
173
+ dependencies = arguments.get("dependencies", [])
174
+ python_version = arguments.get("python")
175
+ script_args = arguments.get("script_args", [])
176
+
177
+ # Show script (truncate if too long)
178
+ script_display = (
179
+ script if len(script) < 200 else script[:200] + "..."
180
+ )
181
+ print(f"Script: {script_display}")
182
+ if dependencies:
183
+ print(f"Dependencies: {', '.join(dependencies)}")
184
+ if python_version:
185
+ print(f"Python version: {python_version}")
186
+ if script_args:
187
+ print(f"Script args: {' '.join(script_args)}")
188
+ elif command:
189
+ # Docker mode
190
+ image = arguments.get("image", "python:3.12")
191
+ command_str = (
192
+ " ".join(command)
193
+ if isinstance(command, list)
194
+ else str(command)
195
+ )
196
+ print(f"Docker image: {image}")
197
+ print(f"Command: {command_str}")
198
+
199
+ # Common parameters for jobs
200
+ hardware_flavor = arguments.get("hardware_flavor", "cpu-basic")
201
+ timeout = arguments.get("timeout", "30m")
202
+ env = arguments.get("env", {})
203
+ schedule = arguments.get("schedule")
204
+
205
+ print(f"Hardware: {hardware_flavor}")
206
+ print(f"Timeout: {timeout}")
207
+
208
+ if env:
209
+ env_keys = ", ".join(env.keys())
210
+ print(f"Environment variables: {env_keys}")
211
+
212
+ if schedule:
213
+ print(f"Schedule: {schedule}")
214
+
215
+ elif tool_name == "hf_private_repos":
216
+ # Handle private repo operations
217
+ args = _safe_get_args(arguments)
218
+
219
+ if operation in ["create_repo", "upload_file"]:
220
+ repo_id = args.get("repo_id", "")
221
+ repo_type = args.get("repo_type", "dataset")
222
+
223
+ # Build repo URL
224
+ type_path = "" if repo_type == "model" else f"{repo_type}s"
225
+ repo_url = f"https://huggingface.co/{type_path}/{repo_id}".replace("//", "/")
226
+
227
+ print(f"Repository: {repo_id}")
228
+ print(f"Type: {repo_type}")
229
+ print(f"Private: Yes")
230
+ print(f"URL: {repo_url}")
231
+
232
+ # Show file preview for upload_file operation
233
+ if operation == "upload_file":
234
+ path_in_repo = args.get("path_in_repo", "")
235
+ file_content = args.get("file_content", "")
236
+ print(f"File: {path_in_repo}")
237
+
238
+ if isinstance(file_content, str):
239
+ # Calculate metrics
240
+ all_lines = file_content.split('\n')
241
+ line_count = len(all_lines)
242
+ size_bytes = len(file_content.encode('utf-8'))
243
+ size_kb = size_bytes / 1024
244
+ size_mb = size_kb / 1024
245
+
246
+ print(f"Line count: {line_count}")
247
+ if size_kb < 1024:
248
+ print(f"Size: {size_kb:.2f} KB")
249
+ else:
250
+ print(f"Size: {size_mb:.2f} MB")
251
+
252
+ # Show preview
253
+ preview_lines = all_lines[:5]
254
+ preview = '\n'.join(preview_lines)
255
+ print(f"Content preview (first 5 lines):\n{preview}")
256
+ if len(all_lines) > 5:
257
+ print("...")
258
+
259
+ # Get user decision for this item
260
+ response = await prompt_session.prompt_async(
261
+ f"Approve item {i}? (y=yes, n=no, or provide feedback to reject): "
262
+ )
263
+
264
+ response = response.strip()
265
+ approved = response.lower() in ["y", "yes"]
266
+ feedback = (
267
+ None
268
+ if approved or response.lower() in ["n", "no"]
269
+ else response
270
+ )
271
+
272
+ approvals.append(
273
+ {
274
+ "tool_call_id": tool_call_id,
275
+ "approved": approved,
276
+ "feedback": feedback,
277
+ }
278
+ )
279
+
280
+ # Submit batch approval
281
  submission_id[0] += 1
282
  approval_submission = Submission(
283
  id=f"approval_{submission_id[0]}",
284
  operation=Operation(
285
  op_type=OpType.EXEC_APPROVAL,
286
+ data={"approvals": approvals},
287
  ),
288
  )
289
  await submission_queue.put(approval_submission)
 
296
  print(f"Event listener error: {e}")
297
 
298
 
299
+ async def get_user_input(prompt_session: PromptSession) -> str:
300
  """Get user input asynchronously"""
301
+ return await prompt_session.prompt_async("You: ")
 
302
 
303
 
304
  async def main():
 
339
  print(f"Config: {config.mcpServers}")
340
  tool_router = ToolRouter(config.mcpServers)
341
 
342
+ # Create prompt session for input
343
+ prompt_session = PromptSession()
344
+
345
  agent_task = asyncio.create_task(
346
  submission_loop(
347
  submission_queue,
 
353
 
354
  # Start event listener in background
355
  listener_task = asyncio.create_task(
356
+ event_listener(
357
+ event_queue,
358
+ submission_queue,
359
+ turn_complete_event,
360
+ ready_event,
361
+ prompt_session,
362
+ )
363
  )
364
 
365
  # Wait for agent to initialize
 
376
 
377
  # Get user input
378
  try:
379
+ user_input = await get_user_input(prompt_session)
380
  except EOFError:
381
  break
382
 
agent/prompts/search_docs_system_prompt.yaml DELETED
@@ -1,38 +0,0 @@
1
- search_docs_system_prompt: |
2
- You are a specialized documentation search agent. Your task is to comprehensively search and synthesize information from Hugging Face documentation.
3
-
4
- # Search Strategy
5
-
6
- You must search thoroughly before synthesizing results. Follow this approach:
7
-
8
- 1. **Query Analysis**: Identify the core concepts and intent of the query
9
- 2. **Initial Search**: Start with a broad search capturing the main topic
10
- 3. **Iterative Refinement**: Run multiple searches to go deeper into topics. You will see parsed HTML pages, also look into links on the html pages for best information - first-pass results often miss key details
11
- 4. **You must get to the end truth**: You must get to the bottom of the truth for this search query. You CAN NOT say that somebody should look up documentation. You must look it up yourself and give the best answer you can.
12
-
13
- ## Query Formulation Best Practices
14
-
15
- - Add relevant synonyms and related technical terms
16
- - Remove filler words, focus on searchable concepts
17
- - Break complex questions into focused sub-queries
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:
24
-
25
- 1. **Analyze Relevance**: Evaluate which results directly answer the query
26
- 2. **Synthesize**: Combine information from multiple sources when applicable
27
- 3. **Prioritize**: Present information in order of relevance
28
- 4. **Cite Sources**: Reference which documents you're drawing from especially include relevant code samples and links to the code samples.
29
- 5. **Acknowledge Gaps**: If documents don't fully answer the query, explicitly state this
30
- 6. **Handle Conflicts**: If sources contradict, note this and explain your reasoning
31
- 7. **Be Concise**: Provide a clear, direct answer without unnecessary elaboration
32
-
33
- # Constraints
34
-
35
- - Only provide information found in the documentation
36
- - Do not make assumptions beyond what the sources state
37
- - If information is not found, say so clearly rather than guessing
38
- - Focus on answering the query directly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent/prompts/system_prompt.yaml CHANGED
@@ -3,10 +3,24 @@ system_prompt: |
3
 
4
  # Task Approach
5
 
6
- 1. Always formulate a plan. Pass the todos to the PlanTool. Update the plan as progress is made.
7
- 2. Search for relevant models, datasets, and documentation on Hugging Face Hub.
8
- 3. Use all available tools to complete the task. Leverage existing resources before creating new ones.
9
- 4. Invoke multiple independent tools simultaneously for efficiency
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  # Autonomy / Subordinate trade-off.
12
 
@@ -28,68 +42,18 @@ system_prompt: |
28
  - Image Generation: Generate and transform images
29
  - Planning : a planning/to-do tool.
30
 
31
- # Examples
32
-
33
- <example>
34
- <user>Find the best text generation models</user>
35
- <response>[uses mcp__hf-mcp-server__model_search with task="text-generation" and sort="trendingScore"]
36
-
37
- Top trending text generation models:
38
- - meta-llama/Llama-3.1-405B-Instruct
39
- - mistralai/Mistral-Large-2
40
- </response>
41
- </example>
42
-
43
- <example>
44
- <user>Search for papers about reinforcement learning from human feedback</user>
45
- <response>[uses mcp__hf-mcp-server__paper_search with query="reinforcement learning from human feedback"]
46
-
47
- Found 5 relevant papers on RLHF including "Training language models to follow instructions with human feedback" (Ouyang et al.)
48
- </response>
49
- </example>
50
-
51
- <example>
52
- <user>Find datasets for sentiment analysis</user>
53
- <response>[uses mcp__hf-mcp-server__dataset_search with query="sentiment analysis" and tags for task_categories]
54
-
55
- Top sentiment analysis datasets:
56
- - stanfordnlp/imdb (25k reviews)
57
- - tweet_eval (sentiment task)
58
- </response>
59
- </example>
60
-
61
- <example>
62
- <user>How do I use the transformers library for text generation?</user>
63
- <response>[uses mcp__hf-mcp-server__hf_doc_search with query="text generation transformers"]
64
-
65
- [provides concise answer based on documentation]
66
- </response>
67
- </example>
68
-
69
- <example>
70
- <user>Generate an image of a sunset over mountains</user>
71
- <response>[uses mcp__hf-mcp-server__gr1_flux1_schnell_infer with prompt="sunset over mountains"]
72
-
73
- [returns generated image]
74
- </response>
75
- </example>
76
-
77
- <example>
78
- <user>Get details about the bert-base-uncased model</user>
79
- <response>[uses mcp__hf-mcp-server__hub_repo_details with repo_ids=["google-bert/bert-base-uncased"]]
80
-
81
- BERT base uncased: 110M parameters, trained on English Wikipedia and BookCorpus, commonly used for text classification and NER.
82
- </response>
83
- </example>
84
-
85
  # Conventions
86
 
 
 
 
 
87
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
88
  - Keep in mind that a space is a repo, so you can create a space directly by uploading files that way. Repos should also be used to store files permanently : post-execution, files from jobs are not available.
89
  - To run jobs, you must always pass the whole content of the file to execute. No files are available on server. Your local files and distant files are entirely seperate scopes.
90
- - To access, create, or modify private Hub assets (spaces, private models, datasets, collections), pass `secrets: {% raw %}{{ "HF_TOKEN": "$HF_TOKEN" }}{% endraw %}` along with the jobs parameters. This is important. Without it, you will encounter authentification issues. Do not assume the user is connected on the jobs' server.
 
91
  - When referencing models, datasets, or papers, include direct links from search results
92
- - Never assume a library is available - check documentation first
93
  - 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.
94
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
95
  - Unless absolutely necessary, don't ask user for action. This does not apply to follow-up questions you have.
@@ -107,13 +71,3 @@ system_prompt: |
107
  - Explain what you're doing for non-trivial operations
108
 
109
  Answer the user's question directly without elaboration unless they ask for detail. One word answers are best when appropriate.
110
-
111
- <example>
112
- <user>What's the state-of-the-art model for image classification?</user>
113
- <response>EVA-CLIP-18B or ConvNeXt-XXLarge depending on your constraints</response>
114
- </example>
115
-
116
- <example>
117
- <user>How many parameters does GPT-3 have?</user>
118
- <response>175 billion</response>
119
- </example>
 
3
 
4
  # Task Approach
5
 
6
+ **CRITICAL: Research First, Then Implement**
7
+
8
+ For ANY implementation task (training, fine-tuning, inference, data processing, etc.):
9
+ 1. **FIRST**: Search HF documentation to find the recommended approach
10
+ - This is MANDATORY before writing any code or making implementation decisions
11
+ - Use `explore_hf_docs` to discover documentation structure for relevant libraries (e.g., "trl", "transformers", "diffusers")
12
+ - Use `fetch_hf_docs` to retrieve full content from specific documentation pages
13
+ - Use `search_hf_api_endpoints` to find API endpoints with usage examples
14
+ - Research what libraries to use, find code examples, understand best practices
15
+ - Skip ONLY for simple factual questions (e.g., "What is LoRA?")
16
+
17
+ 2. **THEN**: Formulate a plan based on research findings. Pass todos to the PlanTool. Update as progress is made.
18
+
19
+ 3. **FINALLY**: Implement using researched approaches
20
+ - Search for relevant models/datasets on HF Hub
21
+ - Use all available tools to complete the task
22
+ - Leverage existing resources before creating new ones
23
+ - Invoke multiple independent tools simultaneously for efficiency
24
 
25
  # Autonomy / Subordinate trade-off.
26
 
 
42
  - Image Generation: Generate and transform images
43
  - Planning : a planning/to-do tool.
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  # Conventions
46
 
47
+ - **ALWAYS search documentation BEFORE implementing** any ML workflow (training, inference, data processing, etc.) - This is non-negotiable
48
+ - Use `explore_hf_docs`, `fetch_hf_docs`, and `search_hf_api_endpoints` to research the correct approach
49
+ - Never assume you know the correct library, method, or approach - you must verify with documentation first
50
+ - Base your implementation on researched best practices, not general knowledge or assumptions
51
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
52
  - Keep in mind that a space is a repo, so you can create a space directly by uploading files that way. Repos should also be used to store files permanently : post-execution, files from jobs are not available.
53
  - To run jobs, you must always pass the whole content of the file to execute. No files are available on server. Your local files and distant files are entirely seperate scopes.
54
+ - The HF_TOKEN is automatically loaded from the environment variables.
55
+ -
56
  - When referencing models, datasets, or papers, include direct links from search results
 
57
  - 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.
58
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
59
  - Unless absolutely necessary, don't ask user for action. This does not apply to follow-up questions you have.
 
71
  - Explain what you're doing for non-trivial operations
72
 
73
  Answer the user's question directly without elaboration unless they ask for detail. One word answers are best when appropriate.
 
 
 
 
 
 
 
 
 
 
agent/tools/__init__.py CHANGED
@@ -3,7 +3,6 @@ Hugging Face tools for the agent
3
  """
4
 
5
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, HfJobsTool, hf_jobs_handler
6
- from agent.tools.search_docs_tool import SEARCH_DOCS_TOOL_SPEC, search_docs_handler
7
  from agent.tools.types import ToolResult
8
 
9
  __all__ = [
@@ -11,6 +10,4 @@ __all__ = [
11
  "HF_JOBS_TOOL_SPEC",
12
  "hf_jobs_handler",
13
  "HfJobsTool",
14
- "SEARCH_DOCS_TOOL_SPEC",
15
- "search_docs_handler",
16
  ]
 
3
  """
4
 
5
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, HfJobsTool, hf_jobs_handler
 
6
  from agent.tools.types import ToolResult
7
 
8
  __all__ = [
 
10
  "HF_JOBS_TOOL_SPEC",
11
  "hf_jobs_handler",
12
  "HfJobsTool",
 
 
13
  ]
agent/tools/{_search_agent_tools.py → docs_tools.py} RENAMED
@@ -1,6 +1,6 @@
1
  """
2
- 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 asyncio
@@ -553,7 +553,7 @@ async def hf_docs_fetch_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
553
  return f"Error fetching documentation: {str(e)}", False
554
 
555
 
556
- # Tool specifications for the search sub-agent
557
 
558
  EXPLORE_HF_DOCS_TOOL_SPEC = {
559
  "name": "explore_hf_docs",
 
1
  """
2
+ Documentation search tools for the HF Agent
3
+ Tools for exploring and fetching HuggingFace documentation and API specifications
4
  """
5
 
6
  import asyncio
 
553
  return f"Error fetching documentation: {str(e)}", False
554
 
555
 
556
+ # Tool specifications for documentation search
557
 
558
  EXPLORE_HF_DOCS_TOOL_SPEC = {
559
  "name": "explore_hf_docs",
agent/tools/jobs_tool.py CHANGED
@@ -46,13 +46,11 @@ ALL_FLAVORS = CPU_FLAVORS + GPU_FLAVORS + SPECIALIZED_FLAVORS
46
  # Operation names
47
  OperationType = Literal[
48
  "run",
49
- "uv",
50
  "ps",
51
  "logs",
52
  "inspect",
53
  "cancel",
54
  "scheduled run",
55
- "scheduled uv",
56
  "scheduled ps",
57
  "scheduled inspect",
58
  "scheduled delete",
@@ -64,25 +62,20 @@ OperationType = Literal[
64
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
65
 
66
 
67
- def _substitute_hf_token(params: Dict[str, Any] | None) -> Dict[str, Any] | None:
68
- """
69
- Substitute HF_TOKEN key with actual token value from environment.
70
 
71
- Args:
72
- params: Dictionary that may contain "HF_TOKEN" as a key
73
 
74
- Returns:
75
- Dictionary with HF_TOKEN value substituted from environment
76
- """
77
- if params is None:
78
- return None
79
 
80
- result = {}
81
- for key, value in params.items():
82
- if key == "HF_TOKEN":
83
- result[key] = os.environ.get("HF_TOKEN", "")
84
- else:
85
- result[key] = value
86
 
87
  return result
88
 
@@ -108,6 +101,8 @@ def _build_uv_command(
108
  if script_args:
109
  parts.extend(script_args)
110
 
 
 
111
  return parts
112
 
113
 
@@ -128,8 +123,6 @@ def _wrap_inline_script(
128
 
129
  def _ensure_hf_transfer_dependency(deps: list[str] | None) -> list[str]:
130
  """Ensure hf-transfer is included in the dependencies list"""
131
- if deps is None:
132
- return ["hf-transfer"]
133
 
134
  if isinstance(deps, list):
135
  deps_copy = deps.copy() # Don't modify the original
@@ -174,7 +167,7 @@ def _job_info_to_dict(job_info) -> Dict[str, Any]:
174
  "createdAt": job_info.created_at.isoformat(),
175
  "dockerImage": job_info.docker_image,
176
  "spaceId": job_info.space_id,
177
- "flavor": job_info.flavor,
178
  "owner": {"name": job_info.owner.name},
179
  }
180
 
@@ -213,7 +206,7 @@ def _scheduled_job_info_to_dict(scheduled_job_info) -> Dict[str, Any]:
213
  "dockerImage": job_spec.docker_image,
214
  "spaceId": job_spec.space_id,
215
  "command": job_spec.command or [],
216
- "flavor": job_spec.flavor or "cpu-basic",
217
  },
218
  }
219
 
@@ -228,25 +221,25 @@ class HfJobsTool:
228
  async def execute(self, params: Dict[str, Any]) -> ToolResult:
229
  """Execute the specified operation"""
230
  operation = params.get("operation")
231
- args = params.get("args", {})
232
 
233
- # If no operation provided, return usage instructions
 
 
234
  if not operation:
235
- return self._show_help()
 
 
 
 
 
236
 
237
  # Normalize operation name
238
  operation = operation.lower()
239
 
240
- # Check if help is requested
241
- if args.get("help"):
242
- return self._show_operation_help(operation)
243
-
244
  try:
245
  # Route to appropriate handler
246
  if operation == "run":
247
  return await self._run_job(args)
248
- elif operation == "uv":
249
- return await self._run_uv_job(args)
250
  elif operation == "ps":
251
  return await self._list_jobs(args)
252
  elif operation == "logs":
@@ -257,8 +250,6 @@ class HfJobsTool:
257
  return await self._cancel_job(args)
258
  elif operation == "scheduled run":
259
  return await self._scheduled_run(args)
260
- elif operation == "scheduled uv":
261
- return await self._scheduled_uv(args)
262
  elif operation == "scheduled ps":
263
  return await self._list_scheduled_jobs(args)
264
  elif operation == "scheduled inspect":
@@ -273,8 +264,8 @@ class HfJobsTool:
273
  return {
274
  "formatted": f'Unknown operation: "{operation}"\n\n'
275
  "Available operations:\n"
276
- "- run, uv, ps, logs, inspect, cancel\n"
277
- "- scheduled run, scheduled uv, scheduled ps, scheduled inspect, "
278
  "scheduled delete, scheduled suspend, scheduled resume\n\n"
279
  "Call this tool with no operation for full usage instructions.",
280
  "totalResults": 0,
@@ -297,104 +288,6 @@ class HfJobsTool:
297
  "isError": True,
298
  }
299
 
300
- def _show_help(self) -> ToolResult:
301
- """Show usage instructions when tool is called with no arguments"""
302
- cpu_flavors_list = ", ".join(CPU_FLAVORS)
303
- gpu_flavors_list = ", ".join(GPU_FLAVORS)
304
- specialized_flavors_list = ", ".join(SPECIALIZED_FLAVORS)
305
-
306
- hardware_section = f"**CPU:** {cpu_flavors_list}\n"
307
- if GPU_FLAVORS:
308
- hardware_section += f"**GPU:** {gpu_flavors_list}\n"
309
- if SPECIALIZED_FLAVORS:
310
- hardware_section += f"**Specialized:** {specialized_flavors_list}"
311
-
312
- usage_text = f"""# HuggingFace Jobs API
313
-
314
- Manage compute jobs on Hugging Face infrastructure.
315
-
316
- ## Available Commands
317
-
318
- ### Job Management
319
- - **run** - Run a job with a Docker image
320
- - **uv** - Run a Python script with UV (inline dependencies)
321
- - **ps** - List jobs
322
- - **logs** - Fetch job logs
323
- - **inspect** - Get detailed job information
324
- - **cancel** - Cancel a running job
325
-
326
- ### Scheduled Jobs
327
- - **scheduled run** - Create a scheduled job
328
- - **scheduled uv** - Create a scheduled UV job
329
- - **scheduled ps** - List scheduled jobs
330
- - **scheduled inspect** - Get scheduled job details
331
- - **scheduled delete** - Delete a scheduled job
332
- - **scheduled suspend** - Pause a scheduled job
333
- - **scheduled resume** - Resume a suspended job
334
-
335
- ## Examples
336
-
337
- ### Run a simple job
338
- Call this tool with:
339
- ```json
340
- {{
341
- "operation": "run",
342
- "args": {{
343
- "image": "python:3.12",
344
- "command": ["python", "-c", "print('Hello from HF Jobs!')"],
345
- "flavor": "cpu-basic"
346
- }}
347
- }}
348
- ```
349
-
350
- ### Run a Python script with UV
351
- Call this tool with:
352
- ```json
353
- {{
354
- "operation": "uv",
355
- "args": {{
356
- "script": "import random\\nprint(42 + random.randint(1, 5))",
357
- "dependencies": ["torch", "huggingface_hub"],
358
- "secrets": {{"HF_TOKEN": "$HF_TOKEN"}}
359
- }}
360
- }}
361
- ```
362
-
363
- ## Hardware Flavors
364
-
365
- {hardware_section}
366
-
367
- ## Command Format Guidelines
368
-
369
- **Array format (default):**
370
- - Recommended for every command—JSON keeps arguments intact (URLs with `&`, spaces, etc.)
371
- - Use `["/bin/sh", "-lc", "..."]` when you need shell operators like `&&`, `|`, or redirections
372
- - Works with any language: Python, bash, node, npm, uv, etc.
373
-
374
- **String format (simple cases only):**
375
- - Still accepted for backwards compatibility, parsed with POSIX shell semantics
376
- - Rejects shell operators and can mis-handle characters such as `&`; switch to arrays when things turn complex
377
-
378
- ### Show command-specific help
379
- Call this tool with:
380
- ```json
381
- {{"operation": "<operation>", "args": {{"help": true}}}}
382
- ```
383
-
384
- ## Tips
385
-
386
- - Jobs default to non-detached mode (stream logs until completion). Set `detach: true` to return immediately.
387
- - Prefer array commands to avoid shell parsing surprises
388
- - To access, create, or modify private Hub assets (spaces, private models, datasets, collections), pass `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}`. This is important. Without it, you will encounter authentification issues. Do not assume the user is connected on the jobs' server.
389
- - 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.
390
- """
391
- return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
392
-
393
- def _show_operation_help(self, operation: str) -> ToolResult:
394
- """Show help for a specific operation"""
395
- help_text = f"Help for operation: {operation}\n\nCall with appropriate arguments. Use the main help for examples."
396
- return {"formatted": help_text, "totalResults": 1, "resultsShared": 1}
397
-
398
  async def _wait_for_job_completion(
399
  self, job_id: str, namespace: Optional[str] = None
400
  ) -> tuple[str, list[str]]:
@@ -423,117 +316,69 @@ Call this tool with:
423
  return final_status, all_logs
424
 
425
  async def _run_job(self, args: Dict[str, Any]) -> ToolResult:
426
- """Run a job using HfApi.run_job()"""
427
  try:
428
- job = await _async_call(
429
- self.api.run_job,
430
- image=args.get("image", "python:3.12"),
431
- command=args.get("command"),
432
- env=_substitute_hf_token(args.get("env")),
433
- secrets=_substitute_hf_token(args.get("secrets")),
434
- flavor=args.get("flavor", "cpu-basic"),
435
- timeout=args.get("timeout", "30m"),
436
- namespace=args.get("namespace") or self.namespace,
437
- )
438
-
439
- # If detached, return immediately
440
- if args.get("detach", False):
441
- response = f"""Job started successfully!
442
-
443
- **Job ID:** {job.id}
444
- **Status:** {job.status.stage}
445
- **View at:** {job.url}
446
-
447
- To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "{job.id}"}}}}`
448
- To inspect, call this tool with `{{"operation": "inspect", "args": {{"job_id": "{job.id}"}}}}`"""
449
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
450
-
451
- # Not detached - wait for completion and stream logs
452
- print(f"Job started: {job.url}")
453
- print("Streaming logs...\n---\n")
454
-
455
- final_status, all_logs = await self._wait_for_job_completion(
456
- job_id=job.id,
457
- namespace=args.get("namespace") or self.namespace,
458
- )
459
-
460
- # Format all logs for the agent
461
- log_text = "\n".join(all_logs) if all_logs else "(no logs)"
462
 
463
- response = f"""Job completed!
 
 
 
 
464
 
465
- **Job ID:** {job.id}
466
- **Final Status:** {final_status}
467
- **View at:** {job.url}
 
468
 
469
- **Logs:**
470
- ```
471
- {log_text}
472
- ```"""
473
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
 
 
 
 
 
 
 
474
 
475
- except Exception as e:
476
- raise Exception(f"Failed to run job: {str(e)}")
 
477
 
478
- async def _run_uv_job(self, args: Dict[str, Any]) -> ToolResult:
479
- """Run UV job with inline script support (no local files needed)"""
480
- try:
481
- script = args.get("script")
482
- if not script:
483
- raise ValueError("script is required")
484
-
485
- # Get dependencies and ensure hf-transfer is included
486
- deps = (
487
- args.get("with_deps")
488
- or args.get("dependencies")
489
- or args.get("packages")
490
- )
491
- deps = _ensure_hf_transfer_dependency(deps)
492
-
493
- # Resolve the command based on script type (URL, inline, or file)
494
- command = _resolve_uv_command(
495
- script=script,
496
- with_deps=deps,
497
- python=args.get("python"),
498
- script_args=args.get("script_args"),
499
- )
500
 
501
- # Use run_job with UV image instead of run_uv_job
502
  job = await _async_call(
503
  self.api.run_job,
504
- image=UV_DEFAULT_IMAGE,
505
  command=command,
506
- env=_substitute_hf_token(args.get("env")),
507
- secrets=_substitute_hf_token(args.get("secrets")),
508
- flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
509
  timeout=args.get("timeout", "30m"),
510
- namespace=args.get("namespace") or self.namespace,
511
  )
512
 
513
- # If detached, return immediately
514
- if args.get("detach", False):
515
- response = f"""UV Job started successfully!
516
-
517
- **Job ID:** {job.id}
518
- **Status:** {job.status.stage}
519
- **View at:** {job.url}
520
-
521
- To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "{job.id}"}}}}`"""
522
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
523
-
524
- # Not detached - wait for completion and stream logs
525
- print(f"UV Job started: {job.url}")
526
  print("Streaming logs...\n---\n")
527
 
528
  final_status, all_logs = await self._wait_for_job_completion(
529
  job_id=job.id,
530
- namespace=args.get("namespace") or self.namespace,
531
  )
532
 
533
  # Format all logs for the agent
534
  log_text = "\n".join(all_logs) if all_logs else "(no logs)"
535
 
536
- response = f"""UV Job completed!
537
 
538
  **Job ID:** {job.id}
539
  **Final Status:** {final_status}
@@ -546,13 +391,11 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
546
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
547
 
548
  except Exception as e:
549
- raise Exception(f"Failed to run UV job: {str(e)}")
550
 
551
  async def _list_jobs(self, args: Dict[str, Any]) -> ToolResult:
552
  """List jobs using HfApi.list_jobs()"""
553
- jobs_list = await _async_call(
554
- self.api.list_jobs, namespace=args.get("namespace") or self.namespace
555
- )
556
 
557
  # Filter jobs
558
  if not args.get("all", False):
@@ -575,7 +418,7 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
575
  "resultsShared": 0,
576
  }
577
  return {
578
- "formatted": 'No running jobs found. Use `{"args": {"all": true}}` to show all jobs.',
579
  "totalResults": 0,
580
  "resultsShared": 0,
581
  }
@@ -600,9 +443,7 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
600
 
601
  try:
602
  # Fetch logs (returns generator, convert to list)
603
- logs_gen = self.api.fetch_job_logs(
604
- job_id=job_id, namespace=args.get("namespace") or self.namespace
605
- )
606
  logs = await _async_call(list, logs_gen)
607
 
608
  if not logs:
@@ -646,7 +487,7 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
646
  job = await _async_call(
647
  self.api.inspect_job,
648
  job_id=jid,
649
- namespace=args.get("namespace") or self.namespace,
650
  )
651
  jobs.append(_job_info_to_dict(job))
652
  except Exception as e:
@@ -675,108 +516,93 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
675
  await _async_call(
676
  self.api.cancel_job,
677
  job_id=job_id,
678
- namespace=args.get("namespace") or self.namespace,
679
  )
680
 
681
  response = f"""✓ Job {job_id} has been cancelled.
682
 
683
- To verify, call this tool with `{{"operation": "inspect", "args": {{"job_id": "{job_id}"}}}}`"""
684
 
685
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
686
 
687
  async def _scheduled_run(self, args: Dict[str, Any]) -> ToolResult:
688
- """Create scheduled job using HfApi.create_scheduled_job()"""
689
  try:
690
- scheduled_job = await _async_call(
691
- self.api.create_scheduled_job,
692
- image=args.get("image", "python:3.12"),
693
- command=args.get("command"),
694
- schedule=args.get("schedule"),
695
- env=_substitute_hf_token(args.get("env")),
696
- secrets=_substitute_hf_token(args.get("secrets")),
697
- flavor=args.get("flavor", "cpu-basic"),
698
- timeout=args.get("timeout", "30m"),
699
- namespace=args.get("namespace") or self.namespace,
700
- )
701
-
702
- scheduled_dict = _scheduled_job_info_to_dict(scheduled_job)
703
-
704
- response = f"""✓ Scheduled job created successfully!
705
-
706
- **Scheduled Job ID:** {scheduled_dict["id"]}
707
- **Schedule:** {scheduled_dict["schedule"]}
708
- **Suspended:** {"Yes" if scheduled_dict.get("suspend") else "No"}
709
- **Next Run:** {scheduled_dict.get("nextRun", "N/A")}
710
 
711
- To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"scheduled_job_id": "{scheduled_dict["id"]}"}}}}`
712
- To list all, call this tool with `{{"operation": "scheduled ps"}}`"""
713
 
714
- return {"formatted": response, "totalResults": 1, "resultsShared": 1}
 
 
 
 
715
 
716
- except Exception as e:
717
- raise Exception(f"Failed to create scheduled job: {str(e)}")
 
 
718
 
719
- async def _scheduled_uv(self, args: Dict[str, Any]) -> ToolResult:
720
- """Create scheduled UV job with inline script support"""
721
- try:
722
- script = args.get("script")
723
- if not script:
724
- raise ValueError("script is required")
 
 
 
 
 
 
725
 
726
- schedule = args.get("schedule")
727
- if not schedule:
728
- raise ValueError("schedule is required")
729
 
730
- # Get dependencies and ensure hf-transfer is included
731
- deps = (
732
- args.get("with_deps")
733
- or args.get("dependencies")
734
- or args.get("packages")
735
- )
736
- deps = _ensure_hf_transfer_dependency(deps)
737
-
738
- # Resolve the command based on script type
739
- command = _resolve_uv_command(
740
- script=script,
741
- with_deps=deps,
742
- python=args.get("python"),
743
- script_args=args.get("script_args"),
744
- )
745
 
746
- # Use create_scheduled_job with UV image
747
  scheduled_job = await _async_call(
748
  self.api.create_scheduled_job,
749
- image=UV_DEFAULT_IMAGE,
750
  command=command,
751
  schedule=schedule,
752
- env=_substitute_hf_token(args.get("env")),
753
- secrets=_substitute_hf_token(args.get("secrets")),
754
- flavor=args.get("flavor") or args.get("hardware") or "cpu-basic",
755
  timeout=args.get("timeout", "30m"),
756
- namespace=args.get("namespace") or self.namespace,
757
  )
758
 
759
  scheduled_dict = _scheduled_job_info_to_dict(scheduled_job)
760
 
761
- response = f"""✓ Scheduled UV job created successfully!
762
 
763
  **Scheduled Job ID:** {scheduled_dict["id"]}
764
  **Schedule:** {scheduled_dict["schedule"]}
765
  **Suspended:** {"Yes" if scheduled_dict.get("suspend") else "No"}
766
  **Next Run:** {scheduled_dict.get("nextRun", "N/A")}
767
 
768
- To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"scheduled_job_id": "{scheduled_dict["id"]}"}}}}`"""
 
769
 
770
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
771
 
772
  except Exception as e:
773
- raise Exception(f"Failed to create scheduled UV job: {str(e)}")
774
 
775
  async def _list_scheduled_jobs(self, args: Dict[str, Any]) -> ToolResult:
776
  """List scheduled jobs using HfApi.list_scheduled_jobs()"""
777
  scheduled_jobs_list = await _async_call(
778
  self.api.list_scheduled_jobs,
779
- namespace=args.get("namespace") or self.namespace,
780
  )
781
 
782
  # Filter jobs - default: hide suspended jobs unless --all is specified
@@ -796,7 +622,7 @@ To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"
796
  "resultsShared": 0,
797
  }
798
  return {
799
- "formatted": 'No active scheduled jobs found. Use `{"args": {"all": true}}` to show suspended jobs.',
800
  "totalResults": 0,
801
  "resultsShared": 0,
802
  }
@@ -822,7 +648,7 @@ To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"
822
  scheduled_job = await _async_call(
823
  self.api.inspect_scheduled_job,
824
  scheduled_job_id=scheduled_job_id,
825
- namespace=args.get("namespace") or self.namespace,
826
  )
827
 
828
  scheduled_dict = _scheduled_job_info_to_dict(scheduled_job)
@@ -848,7 +674,7 @@ To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"
848
  await _async_call(
849
  self.api.delete_scheduled_job,
850
  scheduled_job_id=scheduled_job_id,
851
- namespace=args.get("namespace") or self.namespace,
852
  )
853
 
854
  return {
@@ -871,12 +697,12 @@ To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"
871
  await _async_call(
872
  self.api.suspend_scheduled_job,
873
  scheduled_job_id=scheduled_job_id,
874
- namespace=args.get("namespace") or self.namespace,
875
  )
876
 
877
  response = f"""✓ Scheduled job {scheduled_job_id} has been suspended.
878
 
879
- To resume, call this tool with `{{"operation": "scheduled resume", "args": {{"scheduled_job_id": "{scheduled_job_id}"}}}}`"""
880
 
881
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
882
 
@@ -894,12 +720,12 @@ To resume, call this tool with `{{"operation": "scheduled resume", "args": {{"sc
894
  await _async_call(
895
  self.api.resume_scheduled_job,
896
  scheduled_job_id=scheduled_job_id,
897
- namespace=args.get("namespace") or self.namespace,
898
  )
899
 
900
  response = f"""✓ Scheduled job {scheduled_job_id} has been resumed.
901
 
902
- To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"scheduled_job_id": "{scheduled_job_id}"}}}}`"""
903
 
904
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
905
 
@@ -908,11 +734,31 @@ To inspect, call this tool with `{{"operation": "scheduled inspect", "args": {{"
908
  HF_JOBS_TOOL_SPEC = {
909
  "name": "hf_jobs",
910
  "description": (
911
- "Manage Hugging Face CPU/GPU compute jobs. Run commands in Docker containers, "
912
- "execute Python scripts with UV. List, schedule and monitor jobs/logs. "
913
- "Example hardware/flavor: cpu-basic, cpu-performance, t4-medium. "
914
- "After job completion, if needed or asked by the user, use hf_private_repos tool to store scripts/logs/results to Hub."
915
- "Call this tool with no operation for full usage instructions and examples."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
916
  ),
917
  "parameters": {
918
  "type": "object",
@@ -921,13 +767,11 @@ HF_JOBS_TOOL_SPEC = {
921
  "type": "string",
922
  "enum": [
923
  "run",
924
- "uv",
925
  "ps",
926
  "logs",
927
  "inspect",
928
  "cancel",
929
  "scheduled run",
930
- "scheduled uv",
931
  "scheduled ps",
932
  "scheduled inspect",
933
  "scheduled delete",
@@ -935,22 +779,60 @@ HF_JOBS_TOOL_SPEC = {
935
  "scheduled resume",
936
  ],
937
  "description": (
938
- "Operation to execute. Valid values: [run, uv, ps, logs, inspect, cancel, "
939
- "scheduled run, scheduled uv, scheduled ps, scheduled inspect, scheduled delete, "
940
  "scheduled suspend, scheduled resume]"
941
  ),
942
  },
943
- "args": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  "type": "object",
945
- "description": (
946
- "Operation-specific arguments as a JSON object. "
947
- "Common args: script (for uv), packages/dependencies (array), "
948
- "flavor/hardware (e.g., a10g-large, cpu-basic), command (array), "
949
- "image (string), env (object), secrets (object)."
950
- ),
951
- "additionalProperties": True,
 
 
 
 
 
 
 
 
952
  },
953
  },
 
954
  },
955
  }
956
 
@@ -958,7 +840,7 @@ HF_JOBS_TOOL_SPEC = {
958
  async def hf_jobs_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
959
  """Handler for agent tool router"""
960
  try:
961
- tool = HfJobsTool()
962
  result = await tool.execute(arguments)
963
  return result["formatted"], not result.get("isError", False)
964
  except Exception as e:
 
46
  # Operation names
47
  OperationType = Literal[
48
  "run",
 
49
  "ps",
50
  "logs",
51
  "inspect",
52
  "cancel",
53
  "scheduled run",
 
54
  "scheduled ps",
55
  "scheduled inspect",
56
  "scheduled delete",
 
62
  UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
63
 
64
 
65
+ def _add_environment_variables(params: Dict[str, Any] | None) -> Dict[str, Any]:
66
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN") or ""
 
67
 
68
+ # Start with user-provided env vars, then force-set token last
69
+ result = dict(params or {})
70
 
71
+ # If the caller passed HF_TOKEN="$HF_TOKEN", ignore it.
72
+ if result.get("HF_TOKEN", "").strip().startswith("$"):
73
+ result.pop("HF_TOKEN", None)
 
 
74
 
75
+ # Set both names to be safe (different libs check different vars)
76
+ if token:
77
+ result["HF_TOKEN"] = token
78
+ result["HUGGINGFACE_HUB_TOKEN"] = token
 
 
79
 
80
  return result
81
 
 
101
  if script_args:
102
  parts.extend(script_args)
103
 
104
+ # add defaults
105
+ # parts.extend(["--push_to_hub"])
106
  return parts
107
 
108
 
 
123
 
124
  def _ensure_hf_transfer_dependency(deps: list[str] | None) -> list[str]:
125
  """Ensure hf-transfer is included in the dependencies list"""
 
 
126
 
127
  if isinstance(deps, list):
128
  deps_copy = deps.copy() # Don't modify the original
 
167
  "createdAt": job_info.created_at.isoformat(),
168
  "dockerImage": job_info.docker_image,
169
  "spaceId": job_info.space_id,
170
+ "hardware_flavor": job_info.flavor,
171
  "owner": {"name": job_info.owner.name},
172
  }
173
 
 
206
  "dockerImage": job_spec.docker_image,
207
  "spaceId": job_spec.space_id,
208
  "command": job_spec.command or [],
209
+ "hardware_flavor": job_spec.flavor or "cpu-basic",
210
  },
211
  }
212
 
 
221
  async def execute(self, params: Dict[str, Any]) -> ToolResult:
222
  """Execute the specified operation"""
223
  operation = params.get("operation")
 
224
 
225
+ args = params
226
+
227
+ # If no operation provided, return error
228
  if not operation:
229
+ return {
230
+ "formatted": "Error: 'operation' parameter is required. See tool description for available operations and usage examples.",
231
+ "totalResults": 0,
232
+ "resultsShared": 0,
233
+ "isError": True,
234
+ }
235
 
236
  # Normalize operation name
237
  operation = operation.lower()
238
 
 
 
 
 
239
  try:
240
  # Route to appropriate handler
241
  if operation == "run":
242
  return await self._run_job(args)
 
 
243
  elif operation == "ps":
244
  return await self._list_jobs(args)
245
  elif operation == "logs":
 
250
  return await self._cancel_job(args)
251
  elif operation == "scheduled run":
252
  return await self._scheduled_run(args)
 
 
253
  elif operation == "scheduled ps":
254
  return await self._list_scheduled_jobs(args)
255
  elif operation == "scheduled inspect":
 
264
  return {
265
  "formatted": f'Unknown operation: "{operation}"\n\n'
266
  "Available operations:\n"
267
+ "- run, ps, logs, inspect, cancel\n"
268
+ "- scheduled run, scheduled ps, scheduled inspect, "
269
  "scheduled delete, scheduled suspend, scheduled resume\n\n"
270
  "Call this tool with no operation for full usage instructions.",
271
  "totalResults": 0,
 
288
  "isError": True,
289
  }
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  async def _wait_for_job_completion(
292
  self, job_id: str, namespace: Optional[str] = None
293
  ) -> tuple[str, list[str]]:
 
316
  return final_status, all_logs
317
 
318
  async def _run_job(self, args: Dict[str, Any]) -> ToolResult:
319
+ """Run a job using HfApi.run_job() - smart detection of Python vs Docker mode"""
320
  try:
321
+ script = args.get("script")
322
+ command = args.get("command")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ # Validate mutually exclusive parameters
325
+ if script and command:
326
+ raise ValueError(
327
+ "'script' and 'command' are mutually exclusive. Provide one or the other, not both."
328
+ )
329
 
330
+ if not script and not command:
331
+ raise ValueError(
332
+ "Either 'script' (for Python) or 'command' (for Docker) must be provided."
333
+ )
334
 
335
+ # Python mode: script provided
336
+ if script:
337
+ # Get dependencies and ensure hf-transfer is included
338
+ deps = _ensure_hf_transfer_dependency(args.get("dependencies"))
339
+
340
+ # Resolve the command based on script type (URL, inline, or file)
341
+ command = _resolve_uv_command(
342
+ script=script,
343
+ with_deps=deps,
344
+ python=args.get("python"),
345
+ script_args=args.get("script_args"),
346
+ )
347
 
348
+ # Use UV image unless overridden
349
+ image = args.get("image", UV_DEFAULT_IMAGE)
350
+ job_type = "Python"
351
 
352
+ # Docker mode: command provided
353
+ else:
354
+ image = args.get("image", "python:3.12")
355
+ job_type = "Docker"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
+ # Run the job
358
  job = await _async_call(
359
  self.api.run_job,
360
+ image=image,
361
  command=command,
362
+ env=args.get("env"),
363
+ secrets=_add_environment_variables(args.get("secrets")),
364
+ flavor=args.get("hardware_flavor", "cpu-basic"),
365
  timeout=args.get("timeout", "30m"),
366
+ namespace=self.namespace,
367
  )
368
 
369
+ # Wait for completion and stream logs
370
+ print(f"{job_type} job started: {job.url}")
 
 
 
 
 
 
 
 
 
 
 
371
  print("Streaming logs...\n---\n")
372
 
373
  final_status, all_logs = await self._wait_for_job_completion(
374
  job_id=job.id,
375
+ namespace=self.namespace,
376
  )
377
 
378
  # Format all logs for the agent
379
  log_text = "\n".join(all_logs) if all_logs else "(no logs)"
380
 
381
+ response = f"""{job_type} job completed!
382
 
383
  **Job ID:** {job.id}
384
  **Final Status:** {final_status}
 
391
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
392
 
393
  except Exception as e:
394
+ raise Exception(f"Failed to run job: {str(e)}")
395
 
396
  async def _list_jobs(self, args: Dict[str, Any]) -> ToolResult:
397
  """List jobs using HfApi.list_jobs()"""
398
+ jobs_list = await _async_call(self.api.list_jobs, namespace=self.namespace)
 
 
399
 
400
  # Filter jobs
401
  if not args.get("all", False):
 
418
  "resultsShared": 0,
419
  }
420
  return {
421
+ "formatted": 'No running jobs found. Use `{"operation": "ps", "all": true}` to show all jobs.',
422
  "totalResults": 0,
423
  "resultsShared": 0,
424
  }
 
443
 
444
  try:
445
  # Fetch logs (returns generator, convert to list)
446
+ logs_gen = self.api.fetch_job_logs(job_id=job_id, namespace=self.namespace)
 
 
447
  logs = await _async_call(list, logs_gen)
448
 
449
  if not logs:
 
487
  job = await _async_call(
488
  self.api.inspect_job,
489
  job_id=jid,
490
+ namespace=self.namespace,
491
  )
492
  jobs.append(_job_info_to_dict(job))
493
  except Exception as e:
 
516
  await _async_call(
517
  self.api.cancel_job,
518
  job_id=job_id,
519
+ namespace=self.namespace,
520
  )
521
 
522
  response = f"""✓ Job {job_id} has been cancelled.
523
 
524
+ To verify, call this tool with `{{"operation": "inspect", "job_id": "{job_id}"}}`"""
525
 
526
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
527
 
528
  async def _scheduled_run(self, args: Dict[str, Any]) -> ToolResult:
529
+ """Create scheduled job using HfApi.create_scheduled_job() - smart detection of Python vs Docker mode"""
530
  try:
531
+ script = args.get("script")
532
+ command = args.get("command")
533
+ schedule = args.get("schedule")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ if not schedule:
536
+ raise ValueError("schedule is required for scheduled jobs")
537
 
538
+ # Validate mutually exclusive parameters
539
+ if script and command:
540
+ raise ValueError(
541
+ "'script' and 'command' are mutually exclusive. Provide one or the other, not both."
542
+ )
543
 
544
+ if not script and not command:
545
+ raise ValueError(
546
+ "Either 'script' (for Python) or 'command' (for Docker) must be provided."
547
+ )
548
 
549
+ # Python mode: script provided
550
+ if script:
551
+ # Get dependencies and ensure hf-transfer is included
552
+ deps = _ensure_hf_transfer_dependency(args.get("dependencies"))
553
+
554
+ # Resolve the command based on script type
555
+ command = _resolve_uv_command(
556
+ script=script,
557
+ with_deps=deps,
558
+ python=args.get("python"),
559
+ script_args=args.get("script_args"),
560
+ )
561
 
562
+ # Use UV image unless overridden
563
+ image = args.get("image", UV_DEFAULT_IMAGE)
564
+ job_type = "Python"
565
 
566
+ # Docker mode: command provided
567
+ else:
568
+ image = args.get("image", "python:3.12")
569
+ job_type = "Docker"
 
 
 
 
 
 
 
 
 
 
 
570
 
571
+ # Create scheduled job
572
  scheduled_job = await _async_call(
573
  self.api.create_scheduled_job,
574
+ image=image,
575
  command=command,
576
  schedule=schedule,
577
+ env=args.get("env"),
578
+ secrets=_add_environment_variables(args.get("secrets")),
579
+ flavor=args.get("hardware_flavor", "cpu-basic"),
580
  timeout=args.get("timeout", "30m"),
581
+ namespace=self.namespace,
582
  )
583
 
584
  scheduled_dict = _scheduled_job_info_to_dict(scheduled_job)
585
 
586
+ response = f"""✓ Scheduled {job_type} job created successfully!
587
 
588
  **Scheduled Job ID:** {scheduled_dict["id"]}
589
  **Schedule:** {scheduled_dict["schedule"]}
590
  **Suspended:** {"Yes" if scheduled_dict.get("suspend") else "No"}
591
  **Next Run:** {scheduled_dict.get("nextRun", "N/A")}
592
 
593
+ To inspect, call this tool with `{{"operation": "scheduled inspect", "scheduled_job_id": "{scheduled_dict["id"]}"}}`
594
+ To list all, call this tool with `{{"operation": "scheduled ps"}}`"""
595
 
596
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
597
 
598
  except Exception as e:
599
+ raise Exception(f"Failed to create scheduled job: {str(e)}")
600
 
601
  async def _list_scheduled_jobs(self, args: Dict[str, Any]) -> ToolResult:
602
  """List scheduled jobs using HfApi.list_scheduled_jobs()"""
603
  scheduled_jobs_list = await _async_call(
604
  self.api.list_scheduled_jobs,
605
+ namespace=self.namespace,
606
  )
607
 
608
  # Filter jobs - default: hide suspended jobs unless --all is specified
 
622
  "resultsShared": 0,
623
  }
624
  return {
625
+ "formatted": 'No active scheduled jobs found. Use `{"operation": "scheduled ps", "all": true}` to show suspended jobs.',
626
  "totalResults": 0,
627
  "resultsShared": 0,
628
  }
 
648
  scheduled_job = await _async_call(
649
  self.api.inspect_scheduled_job,
650
  scheduled_job_id=scheduled_job_id,
651
+ namespace=self.namespace,
652
  )
653
 
654
  scheduled_dict = _scheduled_job_info_to_dict(scheduled_job)
 
674
  await _async_call(
675
  self.api.delete_scheduled_job,
676
  scheduled_job_id=scheduled_job_id,
677
+ namespace=self.namespace,
678
  )
679
 
680
  return {
 
697
  await _async_call(
698
  self.api.suspend_scheduled_job,
699
  scheduled_job_id=scheduled_job_id,
700
+ namespace=self.namespace,
701
  )
702
 
703
  response = f"""✓ Scheduled job {scheduled_job_id} has been suspended.
704
 
705
+ To resume, call this tool with `{{"operation": "scheduled resume", "scheduled_job_id": "{scheduled_job_id}"}}`"""
706
 
707
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
708
 
 
720
  await _async_call(
721
  self.api.resume_scheduled_job,
722
  scheduled_job_id=scheduled_job_id,
723
+ namespace=self.namespace,
724
  )
725
 
726
  response = f"""✓ Scheduled job {scheduled_job_id} has been resumed.
727
 
728
+ To inspect, call this tool with `{{"operation": "scheduled inspect", "scheduled_job_id": "{scheduled_job_id}"}}`"""
729
 
730
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
731
 
 
734
  HF_JOBS_TOOL_SPEC = {
735
  "name": "hf_jobs",
736
  "description": (
737
+ "Run Python scripts or Docker containers on HF cloud GPUs/CPUs.\n\n"
738
+ "## Operations:\n"
739
+ "run, ps, logs, inspect, cancel, scheduled run, scheduled ps, scheduled inspect, scheduled delete, scheduled suspend, scheduled resume\n\n"
740
+ "## Two modes:\n"
741
+ "1. **Python mode:** Provide 'script' + 'dependencies' auto-handles pip install\n"
742
+ "2. **Docker mode:** Provide 'image' + 'command' → full control\n"
743
+ "(script and command are mutually exclusive)\n\n"
744
+ "## Hardware:\n"
745
+ "CPU: cpu-basic (default), cpu-upgrade, cpu-performance, cpu-xl\n"
746
+ "GPU: t4-small, t4-medium, l4x1, a10g-small, a10g-large, a100-large, h100\n\n"
747
+ "## Examples:\n\n"
748
+ "**Fine-tune LLM and push to Hub:**\n"
749
+ "{'operation': 'run', 'script': 'from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer\\nmodel = AutoModelForCausalLM.from_pretrained(\"gpt2\")\\n# ... training code ...\\nmodel.push_to_hub(\"user-name/my-finetuned-model\")', 'dependencies': ['transformers', 'torch', 'datasets'], 'hardware_flavor': 'a10g-large', 'timeout': '4h', 'env': {'CUSTOM_VAR': 'value'}}\n\n"
750
+ "**Generate dataset daily and upload:**\n"
751
+ "{'operation': 'scheduled run', 'script': 'from datasets import Dataset\\nimport pandas as pd\\n# scrape/generate data\\ndf = pd.DataFrame(data)\\nds = Dataset.from_pandas(df)\\nds.push_to_hub(\"user-name/daily-dataset\")', 'dependencies': ['datasets', 'pandas'], 'schedule': '@daily'}\n\n"
752
+ "**Run custom training with Docker:**\n"
753
+ "{'operation': 'run', 'image': 'pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime', 'command': ['python', 'train.py', '--epochs', '10'], 'hardware_flavor': 'a100-large'}\n\n"
754
+ "**Monitor jobs:**\n"
755
+ "{'operation': 'ps'} - list running\n"
756
+ "{'operation': 'logs', 'job_id': 'xxx'} - stream logs\n"
757
+ "{'operation': 'cancel', 'job_id': 'xxx'} - stop job\n\n"
758
+ "## CRITICAL: Files are ephemeral!\n"
759
+ "Everything created during execution is DELETED when job finishes. Always .push_to_hub() your outputs (models, datasets, artifacts) in the script.\n\n"
760
+ "## After job completion:\n"
761
+ "If needed or asked by the user, use hf_private_repos tool to store scripts/logs/results to Hub for persistent storage."
762
  ),
763
  "parameters": {
764
  "type": "object",
 
767
  "type": "string",
768
  "enum": [
769
  "run",
 
770
  "ps",
771
  "logs",
772
  "inspect",
773
  "cancel",
774
  "scheduled run",
 
775
  "scheduled ps",
776
  "scheduled inspect",
777
  "scheduled delete",
 
779
  "scheduled resume",
780
  ],
781
  "description": (
782
+ "Operation to execute. Valid values: [run, ps, logs, inspect, cancel, "
783
+ "scheduled run, scheduled ps, scheduled inspect, scheduled delete, "
784
  "scheduled suspend, scheduled resume]"
785
  ),
786
  },
787
+ # Python/UV specific parameters
788
+ "script": {
789
+ "type": "string",
790
+ "description": "Python code to execute. Triggers Python mode (auto pip install). Use with 'run'/'scheduled run'. Mutually exclusive with 'command'.",
791
+ },
792
+ "dependencies": {
793
+ "type": "array",
794
+ "items": {"type": "string"},
795
+ "description": "Pip packages to install. Example: ['trl', 'torch', 'datasets', 'transformers']. Only used with 'script'.",
796
+ },
797
+ # Docker specific parameters
798
+ "image": {
799
+ "type": "string",
800
+ "description": "Docker image. Example: 'pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime'. Use with 'run'/'scheduled run'. Optional (auto-selected if not provided).",
801
+ },
802
+ "command": {
803
+ "type": "array",
804
+ "items": {"type": "string"},
805
+ "description": "Command to execute as list. Example: ['python', 'train.py', '--epochs', '10']. Triggers Docker mode. Use with 'run'/'scheduled run'. Mutually exclusive with 'script'.",
806
+ },
807
+ # Hardware and environment
808
+ "hardware_flavor": {
809
+ "type": "string",
810
+ "description": "Hardware type. CPU: cpu-basic (default), cpu-upgrade, cpu-performance, cpu-xl. GPU: t4-small, t4-medium, l4x1, a10g-small, a10g-large, a100-large, h100. Use with 'run'/'scheduled run'.",
811
+ },
812
+ "timeout": {
813
+ "type": "string",
814
+ "description": "Max runtime. Examples: '30m', '2h', '4h'. Default: '30m'. Important for long training jobs. Use with 'run'/'scheduled run'.",
815
+ },
816
+ "env": {
817
  "type": "object",
818
+ "description": "Environment variables. Format: {'KEY': 'VALUE'}. HF_TOKEN is automatically included from your auth. Use with 'run'/'scheduled run'.",
819
+ },
820
+ # Job management parameters
821
+ "job_id": {
822
+ "type": "string",
823
+ "description": "Job ID to operate on. Required for: 'logs', 'inspect', 'cancel'.",
824
+ },
825
+ # Scheduled job parameters
826
+ "scheduled_job_id": {
827
+ "type": "string",
828
+ "description": "Scheduled job ID. Required for: 'scheduled inspect', 'scheduled delete', 'scheduled suspend', 'scheduled resume'.",
829
+ },
830
+ "schedule": {
831
+ "type": "string",
832
+ "description": "Schedule for recurring job. Presets: '@hourly', '@daily', '@weekly', '@monthly'. Cron: '0 9 * * 1' (Mon 9am). Required for: 'scheduled run'.",
833
  },
834
  },
835
+ "required": ["operation"],
836
  },
837
  }
838
 
 
840
  async def hf_jobs_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
841
  """Handler for agent tool router"""
842
  try:
843
+ tool = HfJobsTool(namespace=os.environ.get("HF_NAMESPACE", ""))
844
  result = await tool.execute(arguments)
845
  return result["formatted"], not result.get("isError", False)
846
  except Exception as e:
agent/tools/search_docs_tool.py DELETED
@@ -1,239 +0,0 @@
1
- """
2
- Search documentation tool that spawns a sub-agent
3
- The sub-agent has its own agent loop and set of specialized search tools
4
- """
5
-
6
- import asyncio
7
- from typing import Any
8
-
9
- from litellm.utils import get_max_tokens
10
-
11
- from agent.core.session import Session
12
-
13
-
14
- async def create_search_tool_router(github_mcp_config: dict[str, Any] | None = None):
15
- """
16
- Create a ToolRouter instance for the search sub-agent
17
- Async because OpenAPI tool needs to fetch and parse spec at initialization
18
-
19
- Args:
20
- github_mcp_config: Optional GitHub MCP server configuration
21
- """
22
- # Import at runtime to avoid circular dependency
23
- from fastmcp import Client
24
-
25
- from agent.core.tools import ToolRouter
26
-
27
- # List of allowed GitHub MCP tools
28
- ALLOWED_GITHUB_TOOLS = {
29
- "list_pull_requests",
30
- "list_issues",
31
- "search_code",
32
- "search_issues",
33
- "search_repositories",
34
- "search_users",
35
- "get_pull_request_status",
36
- "get_pull_request_reviews",
37
- "get_pull_request",
38
- "get_issue",
39
- "get_file_contents",
40
- }
41
-
42
- class SearchDocsToolRouter(ToolRouter):
43
- """Specialized ToolRouter for the search sub-agent"""
44
-
45
- def __init__(self, github_mcp_config: dict[str, Any] | None = None):
46
- self.tools: dict[str, Any] = {}
47
- self.mcp_servers: dict[str, dict[str, Any]] = {}
48
- self._mcp_initialized = False
49
-
50
- # Initialize MCP client with GitHub server if provided
51
- if github_mcp_config:
52
- self.mcp_client = Client({"mcpServers": github_mcp_config})
53
- else:
54
- self.mcp_client = None
55
-
56
- async def initialize_tools(self):
57
- """Initialize tools asynchronously"""
58
- tools = await make_search_agent_tools()
59
- for tool in tools:
60
- self.register_tool(tool)
61
-
62
- async def register_mcp_tools(self) -> None:
63
- """Register only allowed GitHub MCP tools"""
64
- if self.mcp_client is None:
65
- return
66
-
67
- tools = await self.mcp_client.list_tools()
68
- for tool in tools:
69
- # Only register allowed GitHub tools
70
- if tool.name in ALLOWED_GITHUB_TOOLS:
71
- print(f"Registering GitHub MCP Tool: {tool.name}")
72
- from agent.core.tools import ToolSpec
73
-
74
- self.register_tool(
75
- ToolSpec(
76
- name=tool.name,
77
- description=tool.description,
78
- parameters=tool.inputSchema,
79
- handler=None,
80
- )
81
- )
82
-
83
- router = SearchDocsToolRouter(github_mcp_config)
84
- await router.initialize_tools()
85
- return router
86
-
87
-
88
- async def search_docs_handler(arguments: dict[str, Any]) -> tuple[str, bool]:
89
- """
90
- Handler that spawns a sub-agent to perform comprehensive doc search
91
-
92
- Args:
93
- arguments: dictionary with 'query' parameter
94
-
95
- Returns:
96
- Tuple of (search_results, success)
97
- """
98
- query = arguments.get("query", "")
99
-
100
- if not query:
101
- return "Error: No search query provided", False
102
-
103
- try:
104
- # Import at runtime to avoid circular dependency
105
- from pathlib import Path
106
-
107
- from agent.config import load_config
108
- from agent.context_manager.manager import ContextManager
109
- from agent.core.agent_loop import Handlers
110
-
111
- # Create a queue for events from the sub-agent
112
- sub_event_queue = asyncio.Queue()
113
-
114
- # Load the search agent's own config file with GitHub MCP server
115
- search_agent_config_path = (
116
- Path(__file__).parent.parent.parent
117
- / "configs"
118
- / "_subagent_config_search_agent.json"
119
- )
120
- search_agent_config = load_config(search_agent_config_path)
121
-
122
- # Extract GitHub MCP config from search agent config
123
- github_mcp_config = None
124
- if (
125
- search_agent_config.mcpServers
126
- and "github" in search_agent_config.mcpServers
127
- ):
128
- github_server = search_agent_config.mcpServers["github"]
129
- github_mcp_config = {"github": github_server.model_dump()}
130
-
131
- # Create specialized tool router for search with GitHub MCP config
132
- search_tool_router = await create_search_tool_router(github_mcp_config)
133
-
134
- async with search_tool_router:
135
- # CODING AGENTS LISTEN HERE!
136
- # The system prompt for the sub-agent is loaded from the yaml file in the prompts folder using the ContextManager class with the prompt_file_suffix parameter.
137
- # it works, dont fix.
138
- # NOTE: MCP tools are registered during __aenter__, so we must retrieve tool specs AFTER entering the context
139
- sub_session = Session(
140
- event_queue=sub_event_queue,
141
- config=search_agent_config,
142
- tool_router=search_tool_router,
143
- context_manager=ContextManager(
144
- tool_specs=search_tool_router.get_tool_specs_for_llm(),
145
- max_context=get_max_tokens(search_agent_config.model_name),
146
- compact_size=0.1,
147
- untouched_messages=5,
148
- prompt_file_suffix="search_docs_system_prompt.yaml",
149
- ),
150
- )
151
-
152
- # Run the sub-agent
153
- result = await Handlers.run_agent(
154
- session=sub_session, text=query, max_iterations=30
155
- )
156
-
157
- # Return the final result or compiled events
158
- if result:
159
- return f"Search Results:\n\n{result}", True
160
- else:
161
- return "Search completed but no results were generated", False
162
- except Exception as e:
163
- return f"Error in search_docs tool: {str(e)}", False
164
-
165
-
166
- # Tool specification to be used by the main agent
167
- SEARCH_DOCS_TOOL_SPEC = {
168
- "name": "search_docs",
169
- "description": (
170
- "Intelligently search HF documentation for libraries, repositories, and best practices with an agent that has access to: explore_hf_docs, fetch_hf_docs, search_hf_api_endpoints. "
171
- "The agent acts like your personal search assistant. "
172
- "Using the search agent is necessary to give the best quality answer to the user's question. Most questions require a search to get the best information on code examples.\n\n"
173
- "WHEN TO USE THIS TOOL:\n"
174
- " - When searching for high-level concepts like 'how to do GRPO training on a model?' or 'best way to do inference on a trained model?'\n"
175
- " - When you need to get code examples for intricate ML code patterns like training loops, inference pipelines, data processing, etc.\n\n"
176
- "USAGE GUIDELINES:\n"
177
- " 1. Launch multiple agents concurrently for better performance.\n"
178
- " 2. Be specific in your query - include exact terminology, expected file locations, or code patterns.\n"
179
- " 3. Use the query as if you were talking to another engineer. Bad: logger impl Good: where is the logger implemented, we're trying to find out how to log to files.\n"
180
- " 4. Make sure to formulate the query in such a way that the agent knows when it's done or has found the result."
181
- ),
182
- "parameters": {
183
- "type": "object",
184
- "properties": {
185
- "query": {
186
- "type": "string",
187
- "description": (
188
- "The search query describing to the agent what it should do. Be "
189
- "specific and include technical terms, file types, or expected "
190
- "code patterns to help the agent find relevant code. Formulate "
191
- "the query in a way that makes it clear to the agent when it "
192
- "has found the right thing."
193
- ),
194
- },
195
- },
196
- "required": ["query"],
197
- },
198
- }
199
-
200
-
201
- async def make_search_agent_tools():
202
- """
203
- Create a list of tools for the search agent
204
- Async because OpenAPI tool spec needs to be populated at runtime
205
- """
206
- # Import at runtime to avoid circular dependency
207
- from agent.core.tools import ToolSpec
208
- from agent.tools._search_agent_tools import (
209
- EXPLORE_HF_DOCS_TOOL_SPEC,
210
- HF_DOCS_FETCH_TOOL_SPEC,
211
- _get_api_search_tool_spec,
212
- explore_hf_docs_handler,
213
- hf_docs_fetch_handler,
214
- search_openapi_handler,
215
- )
216
-
217
- # Get the OpenAPI tool spec with dynamically populated tags
218
- openapi_spec = await _get_api_search_tool_spec()
219
-
220
- return [
221
- ToolSpec(
222
- name=EXPLORE_HF_DOCS_TOOL_SPEC["name"],
223
- description=EXPLORE_HF_DOCS_TOOL_SPEC["description"],
224
- parameters=EXPLORE_HF_DOCS_TOOL_SPEC["parameters"],
225
- handler=explore_hf_docs_handler,
226
- ),
227
- ToolSpec(
228
- name=HF_DOCS_FETCH_TOOL_SPEC["name"],
229
- description=HF_DOCS_FETCH_TOOL_SPEC["description"],
230
- parameters=HF_DOCS_FETCH_TOOL_SPEC["parameters"],
231
- handler=hf_docs_fetch_handler,
232
- ),
233
- ToolSpec(
234
- name=openapi_spec["name"],
235
- description=openapi_spec["description"],
236
- parameters=openapi_spec["parameters"],
237
- handler=search_openapi_handler,
238
- ),
239
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/_subagent_config_search_agent.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "model_name": "anthropic/claude-haiku-4-5",
3
- "mcpServers": {
4
- "github": {
5
- "transport": "http",
6
- "url": "https://api.githubcopilot.com/mcp/",
7
- "headers": {
8
- "Authorization": "Bearer ${GITHUB_TOKEN}"
9
- }
10
- }
11
- }
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -20,5 +20,7 @@ dependencies = [
20
  "transformers>=2.3.0",
21
  "torch>=2.9.1",
22
  "pytest>=9.0.2",
23
- "trafilatura>=2.0.0",
 
 
24
  ]
 
20
  "transformers>=2.3.0",
21
  "torch>=2.9.1",
22
  "pytest>=9.0.2",
23
+ "prompt-toolkit>=3.0.0",
24
+ "ipykernel>=7.1.0",
25
+ "ipywidgets>=8.1.8",
26
  ]
run_search_agent.py DELETED
@@ -1,142 +0,0 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
uv.lock CHANGED
@@ -209,6 +209,24 @@ wheels = [
209
  { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
210
  ]
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  [[package]]
213
  name = "attrs"
214
  version = "25.4.0"
@@ -230,15 +248,6 @@ wheels = [
230
  { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
231
  ]
232
 
233
- [[package]]
234
- name = "babel"
235
- version = "2.17.0"
236
- source = { registry = "https://pypi.org/simple" }
237
- sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
238
- wheels = [
239
- { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
240
- ]
241
-
242
  [[package]]
243
  name = "beartype"
244
  version = "0.22.6"
@@ -443,17 +452,12 @@ wheels = [
443
  ]
444
 
445
  [[package]]
446
- name = "courlan"
447
- version = "1.3.2"
448
  source = { registry = "https://pypi.org/simple" }
449
- dependencies = [
450
- { name = "babel" },
451
- { name = "tld" },
452
- { name = "urllib3" },
453
- ]
454
- sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" }
455
  wheels = [
456
- { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" },
457
  ]
458
 
459
  [[package]]
@@ -552,21 +556,6 @@ wheels = [
552
  { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" },
553
  ]
554
 
555
- [[package]]
556
- name = "dateparser"
557
- version = "1.2.2"
558
- source = { registry = "https://pypi.org/simple" }
559
- dependencies = [
560
- { name = "python-dateutil" },
561
- { name = "pytz" },
562
- { name = "regex" },
563
- { name = "tzlocal" },
564
- ]
565
- sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840, upload-time = "2025-06-26T09:29:23.211Z" }
566
- wheels = [
567
- { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" },
568
- ]
569
-
570
  [[package]]
571
  name = "debugpy"
572
  version = "1.8.17"
@@ -588,6 +577,15 @@ wheels = [
588
  { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" },
589
  ]
590
 
 
 
 
 
 
 
 
 
 
591
  [[package]]
592
  name = "dill"
593
  version = "0.4.0"
@@ -667,6 +665,15 @@ wheels = [
667
  { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
668
  ]
669
 
 
 
 
 
 
 
 
 
 
670
  [[package]]
671
  name = "fastmcp"
672
  version = "2.13.1"
@@ -926,17 +933,19 @@ dependencies = [
926
  { name = "fastmcp" },
927
  { name = "huggingface-hub" },
928
  { name = "inspect-ai" },
 
 
929
  { name = "litellm" },
930
  { name = "lmnr", extra = ["all"] },
931
  { name = "numpy" },
932
  { name = "pandas" },
 
933
  { name = "pydantic" },
934
  { name = "pytest" },
935
  { name = "python-dotenv" },
936
  { name = "requests" },
937
  { name = "tenacity" },
938
  { name = "torch" },
939
- { name = "trafilatura" },
940
  { name = "transformers" },
941
  ]
942
 
@@ -946,17 +955,19 @@ requires-dist = [
946
  { name = "fastmcp", specifier = ">=2.4.0" },
947
  { name = "huggingface-hub", specifier = ">=1.0.1" },
948
  { name = "inspect-ai", specifier = ">=0.3.149" },
 
 
949
  { name = "litellm", specifier = ">=1.0.0" },
950
  { name = "lmnr", extras = ["all"], specifier = ">=0.7.23" },
951
  { name = "numpy", specifier = ">=1.24.0" },
952
  { name = "pandas", specifier = ">=2.3.3" },
 
953
  { name = "pydantic", specifier = ">=2.12.3" },
954
  { name = "pytest", specifier = ">=9.0.2" },
955
  { name = "python-dotenv", specifier = ">=1.2.1" },
956
  { name = "requests", specifier = ">=2.32.5" },
957
  { name = "tenacity", specifier = ">=8.0.0" },
958
  { name = "torch", specifier = ">=2.9.1" },
959
- { name = "trafilatura", specifier = ">=2.0.0" },
960
  { name = "transformers", specifier = ">=2.3.0" },
961
  ]
962
 
@@ -989,22 +1000,6 @@ wheels = [
989
  { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
990
  ]
991
 
992
- [[package]]
993
- name = "htmldate"
994
- version = "1.9.4"
995
- source = { registry = "https://pypi.org/simple" }
996
- dependencies = [
997
- { name = "charset-normalizer" },
998
- { name = "dateparser" },
999
- { name = "lxml" },
1000
- { name = "python-dateutil" },
1001
- { name = "urllib3" },
1002
- ]
1003
- sdist = { url = "https://files.pythonhosted.org/packages/9d/10/ead9dabc999f353c3aa5d0dc0835b1e355215a5ecb489a7f4ef2ddad5e33/htmldate-1.9.4.tar.gz", hash = "sha256:1129063e02dd0354b74264de71e950c0c3fcee191178321418ccad2074cc8ed0", size = 44690, upload-time = "2025-11-04T17:46:44.983Z" }
1004
- wheels = [
1005
- { url = "https://files.pythonhosted.org/packages/a1/bd/adfcdaaad5805c0c5156aeefd64c1e868c05e9c1cd6fd21751f168cd88c7/htmldate-1.9.4-py3-none-any.whl", hash = "sha256:1b94bcc4e08232a5b692159903acf95548b6a7492dddca5bb123d89d6325921c", size = 31558, upload-time = "2025-11-04T17:46:43.258Z" },
1006
- ]
1007
-
1008
  [[package]]
1009
  name = "httpcore"
1010
  version = "1.0.9"
@@ -1211,6 +1206,79 @@ wheels = [
1211
  { url = "https://files.pythonhosted.org/packages/3a/be/a2882fbd0915b8c56836d720e70d27b60d2594ef2485c1b6acf531912e57/inspect_ai-0.3.149-py3-none-any.whl", hash = "sha256:365142efe459bdc3f945d4742ba8e2750ddd6679d0f9ec42c7357f7222a7c4ad", size = 34604776, upload-time = "2025-11-23T18:57:04.701Z" },
1212
  ]
1213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  [[package]]
1215
  name = "jaraco-classes"
1216
  version = "3.4.0"
@@ -1244,6 +1312,18 @@ wheels = [
1244
  { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" },
1245
  ]
1246
 
 
 
 
 
 
 
 
 
 
 
 
 
1247
  [[package]]
1248
  name = "jeepney"
1249
  version = "0.9.0"
@@ -1448,15 +1528,41 @@ wheels = [
1448
  ]
1449
 
1450
  [[package]]
1451
- name = "justext"
1452
- version = "3.0.2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1453
  source = { registry = "https://pypi.org/simple" }
1454
  dependencies = [
1455
- { name = "lxml", extra = ["html-clean"] },
 
1456
  ]
1457
- sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" }
1458
  wheels = [
1459
- { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" },
 
 
 
 
 
 
 
 
 
1460
  ]
1461
 
1462
  [[package]]
@@ -1565,103 +1671,6 @@ all = [
1565
  { name = "opentelemetry-instrumentation-weaviate" },
1566
  ]
1567
 
1568
- [[package]]
1569
- name = "lxml"
1570
- version = "6.0.2"
1571
- source = { registry = "https://pypi.org/simple" }
1572
- sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
1573
- wheels = [
1574
- { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
1575
- { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
1576
- { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
1577
- { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
1578
- { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
1579
- { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
1580
- { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
1581
- { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
1582
- { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
1583
- { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
1584
- { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
1585
- { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
1586
- { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
1587
- { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
1588
- { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
1589
- { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
1590
- { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
1591
- { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
1592
- { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
1593
- { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
1594
- { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
1595
- { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
1596
- { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
1597
- { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
1598
- { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
1599
- { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
1600
- { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
1601
- { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
1602
- { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
1603
- { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
1604
- { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
1605
- { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
1606
- { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
1607
- { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
1608
- { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
1609
- { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
1610
- { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
1611
- { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
1612
- { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
1613
- { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
1614
- { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
1615
- { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
1616
- { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
1617
- { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
1618
- { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
1619
- { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
1620
- { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
1621
- { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
1622
- { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
1623
- { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
1624
- { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
1625
- { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
1626
- { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
1627
- { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
1628
- { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
1629
- { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
1630
- { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
1631
- { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
1632
- { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
1633
- { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
1634
- { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
1635
- { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
1636
- { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
1637
- { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
1638
- { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
1639
- { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
1640
- { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
1641
- { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
1642
- { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
1643
- { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
1644
- { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
1645
- { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
1646
- ]
1647
-
1648
- [package.optional-dependencies]
1649
- html-clean = [
1650
- { name = "lxml-html-clean" },
1651
- ]
1652
-
1653
- [[package]]
1654
- name = "lxml-html-clean"
1655
- version = "0.4.3"
1656
- source = { registry = "https://pypi.org/simple" }
1657
- dependencies = [
1658
- { name = "lxml" },
1659
- ]
1660
- sdist = { url = "https://files.pythonhosted.org/packages/d9/cb/c9c5bb2a9c47292e236a808dd233a03531f53b626f36259dcd32b49c76da/lxml_html_clean-0.4.3.tar.gz", hash = "sha256:c9df91925b00f836c807beab127aac82575110eacff54d0a75187914f1bd9d8c", size = 21498, upload-time = "2025-10-02T20:49:24.895Z" }
1661
- wheels = [
1662
- { url = "https://files.pythonhosted.org/packages/10/4a/63a9540e3ca73709f4200564a737d63a4c8c9c4dd032bab8535f507c190a/lxml_html_clean-0.4.3-py3-none-any.whl", hash = "sha256:63fd7b0b9c3a2e4176611c2ca5d61c4c07ffca2de76c14059a81a2825833731e", size = 14177, upload-time = "2025-10-02T20:49:23.749Z" },
1663
- ]
1664
-
1665
  [[package]]
1666
  name = "markdown-it-py"
1667
  version = "4.0.0"
@@ -1742,6 +1751,18 @@ wheels = [
1742
  { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
1743
  ]
1744
 
 
 
 
 
 
 
 
 
 
 
 
 
1745
  [[package]]
1746
  name = "mcp"
1747
  version = "1.22.0"
@@ -2843,6 +2864,15 @@ wheels = [
2843
  { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
2844
  ]
2845
 
 
 
 
 
 
 
 
 
 
2846
  [[package]]
2847
  name = "pathable"
2848
  version = "0.4.4"
@@ -2870,6 +2900,18 @@ wheels = [
2870
  { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
2871
  ]
2872
 
 
 
 
 
 
 
 
 
 
 
 
 
2873
  [[package]]
2874
  name = "platformdirs"
2875
  version = "4.5.0"
@@ -2897,6 +2939,18 @@ wheels = [
2897
  { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" },
2898
  ]
2899
 
 
 
 
 
 
 
 
 
 
 
 
 
2900
  [[package]]
2901
  name = "propcache"
2902
  version = "0.4.1"
@@ -3022,6 +3076,24 @@ wheels = [
3022
  { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
3023
  ]
3024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3025
  [[package]]
3026
  name = "py-key-value-aio"
3027
  version = "0.2.8"
@@ -3375,6 +3447,49 @@ wheels = [
3375
  { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
3376
  ]
3377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3378
  [[package]]
3379
  name = "referencing"
3380
  version = "0.36.2"
@@ -3766,6 +3881,20 @@ wheels = [
3766
  { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" },
3767
  ]
3768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3769
  [[package]]
3770
  name = "starlette"
3771
  version = "0.50.0"
@@ -3864,15 +3993,6 @@ wheels = [
3864
  { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
3865
  ]
3866
 
3867
- [[package]]
3868
- name = "tld"
3869
- version = "0.13.1"
3870
- source = { registry = "https://pypi.org/simple" }
3871
- sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" }
3872
- wheels = [
3873
- { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" },
3874
- ]
3875
-
3876
  [[package]]
3877
  name = "tokenizers"
3878
  version = "0.22.1"
@@ -3950,6 +4070,25 @@ wheels = [
3950
  { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" },
3951
  ]
3952
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3953
  [[package]]
3954
  name = "tqdm"
3955
  version = "4.67.1"
@@ -3963,21 +4102,12 @@ wheels = [
3963
  ]
3964
 
3965
  [[package]]
3966
- name = "trafilatura"
3967
- version = "2.0.0"
3968
  source = { registry = "https://pypi.org/simple" }
3969
- dependencies = [
3970
- { name = "certifi" },
3971
- { name = "charset-normalizer" },
3972
- { name = "courlan" },
3973
- { name = "htmldate" },
3974
- { name = "justext" },
3975
- { name = "lxml" },
3976
- { name = "urllib3" },
3977
- ]
3978
- sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404, upload-time = "2024-12-03T15:23:24.16Z" }
3979
  wheels = [
3980
- { url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557, upload-time = "2024-12-03T15:23:21.41Z" },
3981
  ]
3982
 
3983
  [[package]]
@@ -4053,18 +4183,6 @@ wheels = [
4053
  { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
4054
  ]
4055
 
4056
- [[package]]
4057
- name = "tzlocal"
4058
- version = "5.3.1"
4059
- source = { registry = "https://pypi.org/simple" }
4060
- dependencies = [
4061
- { name = "tzdata", marker = "sys_platform == 'win32'" },
4062
- ]
4063
- sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
4064
- wheels = [
4065
- { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
4066
- ]
4067
-
4068
  [[package]]
4069
  name = "uc-micro-py"
4070
  version = "1.0.3"
@@ -4109,6 +4227,15 @@ wheels = [
4109
  { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
4110
  ]
4111
 
 
 
 
 
 
 
 
 
 
4112
  [[package]]
4113
  name = "websockets"
4114
  version = "15.0.1"
@@ -4140,6 +4267,15 @@ wheels = [
4140
  { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
4141
  ]
4142
 
 
 
 
 
 
 
 
 
 
4143
  [[package]]
4144
  name = "wrapt"
4145
  version = "1.17.3"
 
209
  { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
210
  ]
211
 
212
+ [[package]]
213
+ name = "appnope"
214
+ version = "0.1.4"
215
+ source = { registry = "https://pypi.org/simple" }
216
+ sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
217
+ wheels = [
218
+ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
219
+ ]
220
+
221
+ [[package]]
222
+ name = "asttokens"
223
+ version = "3.0.1"
224
+ source = { registry = "https://pypi.org/simple" }
225
+ sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
226
+ wheels = [
227
+ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
228
+ ]
229
+
230
  [[package]]
231
  name = "attrs"
232
  version = "25.4.0"
 
248
  { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
249
  ]
250
 
 
 
 
 
 
 
 
 
 
251
  [[package]]
252
  name = "beartype"
253
  version = "0.22.6"
 
452
  ]
453
 
454
  [[package]]
455
+ name = "comm"
456
+ version = "0.2.3"
457
  source = { registry = "https://pypi.org/simple" }
458
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
 
 
 
 
 
459
  wheels = [
460
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
461
  ]
462
 
463
  [[package]]
 
556
  { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" },
557
  ]
558
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  [[package]]
560
  name = "debugpy"
561
  version = "1.8.17"
 
577
  { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" },
578
  ]
579
 
580
+ [[package]]
581
+ name = "decorator"
582
+ version = "5.2.1"
583
+ source = { registry = "https://pypi.org/simple" }
584
+ sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
585
+ wheels = [
586
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
587
+ ]
588
+
589
  [[package]]
590
  name = "dill"
591
  version = "0.4.0"
 
665
  { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
666
  ]
667
 
668
+ [[package]]
669
+ name = "executing"
670
+ version = "2.2.1"
671
+ source = { registry = "https://pypi.org/simple" }
672
+ sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
673
+ wheels = [
674
+ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
675
+ ]
676
+
677
  [[package]]
678
  name = "fastmcp"
679
  version = "2.13.1"
 
933
  { name = "fastmcp" },
934
  { name = "huggingface-hub" },
935
  { name = "inspect-ai" },
936
+ { name = "ipykernel" },
937
+ { name = "ipywidgets" },
938
  { name = "litellm" },
939
  { name = "lmnr", extra = ["all"] },
940
  { name = "numpy" },
941
  { name = "pandas" },
942
+ { name = "prompt-toolkit" },
943
  { name = "pydantic" },
944
  { name = "pytest" },
945
  { name = "python-dotenv" },
946
  { name = "requests" },
947
  { name = "tenacity" },
948
  { name = "torch" },
 
949
  { name = "transformers" },
950
  ]
951
 
 
955
  { name = "fastmcp", specifier = ">=2.4.0" },
956
  { name = "huggingface-hub", specifier = ">=1.0.1" },
957
  { name = "inspect-ai", specifier = ">=0.3.149" },
958
+ { name = "ipykernel", specifier = ">=7.1.0" },
959
+ { name = "ipywidgets", specifier = ">=8.1.8" },
960
  { name = "litellm", specifier = ">=1.0.0" },
961
  { name = "lmnr", extras = ["all"], specifier = ">=0.7.23" },
962
  { name = "numpy", specifier = ">=1.24.0" },
963
  { name = "pandas", specifier = ">=2.3.3" },
964
+ { name = "prompt-toolkit", specifier = ">=3.0.0" },
965
  { name = "pydantic", specifier = ">=2.12.3" },
966
  { name = "pytest", specifier = ">=9.0.2" },
967
  { name = "python-dotenv", specifier = ">=1.2.1" },
968
  { name = "requests", specifier = ">=2.32.5" },
969
  { name = "tenacity", specifier = ">=8.0.0" },
970
  { name = "torch", specifier = ">=2.9.1" },
 
971
  { name = "transformers", specifier = ">=2.3.0" },
972
  ]
973
 
 
1000
  { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
1001
  ]
1002
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  [[package]]
1004
  name = "httpcore"
1005
  version = "1.0.9"
 
1206
  { url = "https://files.pythonhosted.org/packages/3a/be/a2882fbd0915b8c56836d720e70d27b60d2594ef2485c1b6acf531912e57/inspect_ai-0.3.149-py3-none-any.whl", hash = "sha256:365142efe459bdc3f945d4742ba8e2750ddd6679d0f9ec42c7357f7222a7c4ad", size = 34604776, upload-time = "2025-11-23T18:57:04.701Z" },
1207
  ]
1208
 
1209
+ [[package]]
1210
+ name = "ipykernel"
1211
+ version = "7.1.0"
1212
+ source = { registry = "https://pypi.org/simple" }
1213
+ dependencies = [
1214
+ { name = "appnope", marker = "sys_platform == 'darwin'" },
1215
+ { name = "comm" },
1216
+ { name = "debugpy" },
1217
+ { name = "ipython" },
1218
+ { name = "jupyter-client" },
1219
+ { name = "jupyter-core" },
1220
+ { name = "matplotlib-inline" },
1221
+ { name = "nest-asyncio" },
1222
+ { name = "packaging" },
1223
+ { name = "psutil" },
1224
+ { name = "pyzmq" },
1225
+ { name = "tornado" },
1226
+ { name = "traitlets" },
1227
+ ]
1228
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" }
1229
+ wheels = [
1230
+ { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" },
1231
+ ]
1232
+
1233
+ [[package]]
1234
+ name = "ipython"
1235
+ version = "9.8.0"
1236
+ source = { registry = "https://pypi.org/simple" }
1237
+ dependencies = [
1238
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1239
+ { name = "decorator" },
1240
+ { name = "ipython-pygments-lexers" },
1241
+ { name = "jedi" },
1242
+ { name = "matplotlib-inline" },
1243
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
1244
+ { name = "prompt-toolkit" },
1245
+ { name = "pygments" },
1246
+ { name = "stack-data" },
1247
+ { name = "traitlets" },
1248
+ ]
1249
+ sdist = { url = "https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz", hash = "sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size = 4424940, upload-time = "2025-12-03T10:18:24.353Z" }
1250
+ wheels = [
1251
+ { url = "https://files.pythonhosted.org/packages/f1/df/8ee1c5dd1e3308b5d5b2f2dfea323bb2f3827da8d654abb6642051199049/ipython-9.8.0-py3-none-any.whl", hash = "sha256:ebe6d1d58d7d988fbf23ff8ff6d8e1622cfdb194daf4b7b73b792c4ec3b85385", size = 621374, upload-time = "2025-12-03T10:18:22.335Z" },
1252
+ ]
1253
+
1254
+ [[package]]
1255
+ name = "ipython-pygments-lexers"
1256
+ version = "1.1.1"
1257
+ source = { registry = "https://pypi.org/simple" }
1258
+ dependencies = [
1259
+ { name = "pygments" },
1260
+ ]
1261
+ sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
1262
+ wheels = [
1263
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
1264
+ ]
1265
+
1266
+ [[package]]
1267
+ name = "ipywidgets"
1268
+ version = "8.1.8"
1269
+ source = { registry = "https://pypi.org/simple" }
1270
+ dependencies = [
1271
+ { name = "comm" },
1272
+ { name = "ipython" },
1273
+ { name = "jupyterlab-widgets" },
1274
+ { name = "traitlets" },
1275
+ { name = "widgetsnbextension" },
1276
+ ]
1277
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" }
1278
+ wheels = [
1279
+ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" },
1280
+ ]
1281
+
1282
  [[package]]
1283
  name = "jaraco-classes"
1284
  version = "3.4.0"
 
1312
  { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" },
1313
  ]
1314
 
1315
+ [[package]]
1316
+ name = "jedi"
1317
+ version = "0.19.2"
1318
+ source = { registry = "https://pypi.org/simple" }
1319
+ dependencies = [
1320
+ { name = "parso" },
1321
+ ]
1322
+ sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
1323
+ wheels = [
1324
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
1325
+ ]
1326
+
1327
  [[package]]
1328
  name = "jeepney"
1329
  version = "0.9.0"
 
1528
  ]
1529
 
1530
  [[package]]
1531
+ name = "jupyter-client"
1532
+ version = "8.7.0"
1533
+ source = { registry = "https://pypi.org/simple" }
1534
+ dependencies = [
1535
+ { name = "jupyter-core" },
1536
+ { name = "python-dateutil" },
1537
+ { name = "pyzmq" },
1538
+ { name = "tornado" },
1539
+ { name = "traitlets" },
1540
+ ]
1541
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" }
1542
+ wheels = [
1543
+ { url = "https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" },
1544
+ ]
1545
+
1546
+ [[package]]
1547
+ name = "jupyter-core"
1548
+ version = "5.9.1"
1549
  source = { registry = "https://pypi.org/simple" }
1550
  dependencies = [
1551
+ { name = "platformdirs" },
1552
+ { name = "traitlets" },
1553
  ]
1554
+ sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
1555
  wheels = [
1556
+ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
1557
+ ]
1558
+
1559
+ [[package]]
1560
+ name = "jupyterlab-widgets"
1561
+ version = "3.0.16"
1562
+ source = { registry = "https://pypi.org/simple" }
1563
+ sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" }
1564
+ wheels = [
1565
+ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" },
1566
  ]
1567
 
1568
  [[package]]
 
1671
  { name = "opentelemetry-instrumentation-weaviate" },
1672
  ]
1673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1674
  [[package]]
1675
  name = "markdown-it-py"
1676
  version = "4.0.0"
 
1751
  { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
1752
  ]
1753
 
1754
+ [[package]]
1755
+ name = "matplotlib-inline"
1756
+ version = "0.2.1"
1757
+ source = { registry = "https://pypi.org/simple" }
1758
+ dependencies = [
1759
+ { name = "traitlets" },
1760
+ ]
1761
+ sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
1762
+ wheels = [
1763
+ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
1764
+ ]
1765
+
1766
  [[package]]
1767
  name = "mcp"
1768
  version = "1.22.0"
 
2864
  { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
2865
  ]
2866
 
2867
+ [[package]]
2868
+ name = "parso"
2869
+ version = "0.8.5"
2870
+ source = { registry = "https://pypi.org/simple" }
2871
+ sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" }
2872
+ wheels = [
2873
+ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" },
2874
+ ]
2875
+
2876
  [[package]]
2877
  name = "pathable"
2878
  version = "0.4.4"
 
2900
  { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
2901
  ]
2902
 
2903
+ [[package]]
2904
+ name = "pexpect"
2905
+ version = "4.9.0"
2906
+ source = { registry = "https://pypi.org/simple" }
2907
+ dependencies = [
2908
+ { name = "ptyprocess" },
2909
+ ]
2910
+ sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
2911
+ wheels = [
2912
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
2913
+ ]
2914
+
2915
  [[package]]
2916
  name = "platformdirs"
2917
  version = "4.5.0"
 
2939
  { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" },
2940
  ]
2941
 
2942
+ [[package]]
2943
+ name = "prompt-toolkit"
2944
+ version = "3.0.52"
2945
+ source = { registry = "https://pypi.org/simple" }
2946
+ dependencies = [
2947
+ { name = "wcwidth" },
2948
+ ]
2949
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
2950
+ wheels = [
2951
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
2952
+ ]
2953
+
2954
  [[package]]
2955
  name = "propcache"
2956
  version = "0.4.1"
 
3076
  { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
3077
  ]
3078
 
3079
+ [[package]]
3080
+ name = "ptyprocess"
3081
+ version = "0.7.0"
3082
+ source = { registry = "https://pypi.org/simple" }
3083
+ sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
3084
+ wheels = [
3085
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
3086
+ ]
3087
+
3088
+ [[package]]
3089
+ name = "pure-eval"
3090
+ version = "0.2.3"
3091
+ source = { registry = "https://pypi.org/simple" }
3092
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
3093
+ wheels = [
3094
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
3095
+ ]
3096
+
3097
  [[package]]
3098
  name = "py-key-value-aio"
3099
  version = "0.2.8"
 
3447
  { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
3448
  ]
3449
 
3450
+ [[package]]
3451
+ name = "pyzmq"
3452
+ version = "27.1.0"
3453
+ source = { registry = "https://pypi.org/simple" }
3454
+ dependencies = [
3455
+ { name = "cffi", marker = "implementation_name == 'pypy'" },
3456
+ ]
3457
+ sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
3458
+ wheels = [
3459
+ { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" },
3460
+ { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" },
3461
+ { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" },
3462
+ { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" },
3463
+ { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" },
3464
+ { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" },
3465
+ { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" },
3466
+ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
3467
+ { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
3468
+ { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
3469
+ { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" },
3470
+ { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" },
3471
+ { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" },
3472
+ { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" },
3473
+ { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" },
3474
+ { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" },
3475
+ { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" },
3476
+ { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" },
3477
+ { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" },
3478
+ { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" },
3479
+ { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" },
3480
+ { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" },
3481
+ { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" },
3482
+ { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" },
3483
+ { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" },
3484
+ { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" },
3485
+ { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" },
3486
+ { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" },
3487
+ { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" },
3488
+ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
3489
+ { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
3490
+ { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
3491
+ ]
3492
+
3493
  [[package]]
3494
  name = "referencing"
3495
  version = "0.36.2"
 
3881
  { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" },
3882
  ]
3883
 
3884
+ [[package]]
3885
+ name = "stack-data"
3886
+ version = "0.6.3"
3887
+ source = { registry = "https://pypi.org/simple" }
3888
+ dependencies = [
3889
+ { name = "asttokens" },
3890
+ { name = "executing" },
3891
+ { name = "pure-eval" },
3892
+ ]
3893
+ sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
3894
+ wheels = [
3895
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
3896
+ ]
3897
+
3898
  [[package]]
3899
  name = "starlette"
3900
  version = "0.50.0"
 
3993
  { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
3994
  ]
3995
 
 
 
 
 
 
 
 
 
 
3996
  [[package]]
3997
  name = "tokenizers"
3998
  version = "0.22.1"
 
4070
  { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" },
4071
  ]
4072
 
4073
+ [[package]]
4074
+ name = "tornado"
4075
+ version = "6.5.4"
4076
+ source = { registry = "https://pypi.org/simple" }
4077
+ sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" }
4078
+ wheels = [
4079
+ { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" },
4080
+ { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" },
4081
+ { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" },
4082
+ { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" },
4083
+ { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" },
4084
+ { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" },
4085
+ { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" },
4086
+ { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" },
4087
+ { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" },
4088
+ { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" },
4089
+ { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" },
4090
+ ]
4091
+
4092
  [[package]]
4093
  name = "tqdm"
4094
  version = "4.67.1"
 
4102
  ]
4103
 
4104
  [[package]]
4105
+ name = "traitlets"
4106
+ version = "5.14.3"
4107
  source = { registry = "https://pypi.org/simple" }
4108
+ sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
 
 
 
 
 
 
 
 
 
4109
  wheels = [
4110
+ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
4111
  ]
4112
 
4113
  [[package]]
 
4183
  { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
4184
  ]
4185
 
 
 
 
 
 
 
 
 
 
 
 
 
4186
  [[package]]
4187
  name = "uc-micro-py"
4188
  version = "1.0.3"
 
4227
  { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
4228
  ]
4229
 
4230
+ [[package]]
4231
+ name = "wcwidth"
4232
+ version = "0.2.14"
4233
+ source = { registry = "https://pypi.org/simple" }
4234
+ sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
4235
+ wheels = [
4236
+ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
4237
+ ]
4238
+
4239
  [[package]]
4240
  name = "websockets"
4241
  version = "15.0.1"
 
4267
  { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
4268
  ]
4269
 
4270
+ [[package]]
4271
+ name = "widgetsnbextension"
4272
+ version = "4.0.15"
4273
+ source = { registry = "https://pypi.org/simple" }
4274
+ sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" }
4275
+ wheels = [
4276
+ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
4277
+ ]
4278
+
4279
  [[package]]
4280
  name = "wrapt"
4281
  version = "1.17.3"