hud-python 0.4.47__py3-none-any.whl → 0.4.49__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (45) hide show
  1. hud/agents/base.py +55 -142
  2. hud/agents/claude.py +5 -6
  3. hud/agents/grounded_openai.py +1 -1
  4. hud/agents/misc/integration_test_agent.py +2 -0
  5. hud/agents/tests/test_base.py +2 -5
  6. hud/cli/__init__.py +80 -215
  7. hud/cli/build.py +105 -45
  8. hud/cli/dev.py +614 -743
  9. hud/cli/eval.py +14 -9
  10. hud/cli/flows/tasks.py +100 -21
  11. hud/cli/init.py +18 -14
  12. hud/cli/push.py +27 -9
  13. hud/cli/rl/local_runner.py +28 -16
  14. hud/cli/rl/vllm.py +2 -0
  15. hud/cli/tests/test_analyze_metadata.py +3 -2
  16. hud/cli/tests/test_eval.py +574 -0
  17. hud/cli/tests/test_mcp_server.py +6 -95
  18. hud/cli/tests/test_utils.py +1 -1
  19. hud/cli/utils/env_check.py +9 -9
  20. hud/cli/utils/source_hash.py +1 -1
  21. hud/datasets/parallel.py +0 -12
  22. hud/datasets/runner.py +1 -4
  23. hud/rl/actor.py +4 -2
  24. hud/rl/distributed.py +1 -1
  25. hud/rl/learner.py +2 -1
  26. hud/rl/train.py +1 -1
  27. hud/server/__init__.py +2 -1
  28. hud/server/router.py +160 -0
  29. hud/server/server.py +246 -79
  30. hud/telemetry/trace.py +1 -1
  31. hud/tools/base.py +20 -10
  32. hud/tools/computer/__init__.py +2 -0
  33. hud/tools/computer/qwen.py +431 -0
  34. hud/tools/computer/settings.py +16 -0
  35. hud/tools/executors/pyautogui.py +1 -1
  36. hud/tools/playwright.py +1 -1
  37. hud/types.py +2 -3
  38. hud/utils/hud_console.py +43 -0
  39. hud/utils/tests/test_version.py +1 -1
  40. hud/version.py +1 -1
  41. {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/METADATA +1 -1
  42. {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/RECORD +45 -42
  43. {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/WHEEL +0 -0
  44. {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/entry_points.txt +0 -0
  45. {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/licenses/LICENSE +0 -0
hud/server/server.py CHANGED
@@ -133,7 +133,9 @@ class MCPServer(FastMCP):
133
133
  FastMCP ``FunctionTool`` interface.
134
134
  """
135
135
 
136
- def __init__(self, *, name: str | None = None, **fastmcp_kwargs: Any) -> None:
136
+ def __init__(
137
+ self, name: str | None = None, instructions: str | None = None, **fastmcp_kwargs: Any
138
+ ) -> None:
137
139
  # Store shutdown function placeholder before super().__init__
138
140
  self._shutdown_fn: Callable | None = None
139
141
 
@@ -179,7 +181,7 @@ class MCPServer(FastMCP):
179
181
 
180
182
  fastmcp_kwargs["lifespan"] = _lifespan
181
183
 
182
- super().__init__(name=name, **fastmcp_kwargs)
184
+ super().__init__(name=name, instructions=instructions, **fastmcp_kwargs)
183
185
  self._initializer_fn: Callable | None = None
184
186
  self._did_init = False
185
187
  self._replaced_server = False
@@ -382,90 +384,255 @@ class MCPServer(FastMCP):
382
384
 
383
385
  return _wrapper
384
386
 
387
+ def include_router(
388
+ self,
389
+ router: FastMCP,
390
+ prefix: str | None = None,
391
+ hidden: bool = False,
392
+ **kwargs: Any,
393
+ ) -> None:
394
+ """Include a router's tools/resources with optional hidden dispatcher pattern.
395
+
396
+ Uses import_server for fast static composition (unlike mount which is slower).
397
+
398
+ Args:
399
+ router: FastMCP router to include
400
+ prefix: Optional prefix for tools/resources (ignored if hidden=True)
401
+ hidden: If True, wrap in HiddenRouter (single dispatcher tool that calls sub-tools)
402
+ **kwargs: Additional arguments passed to import_server()
403
+
404
+ Examples:
405
+ # Direct include - tools appear at top level
406
+ mcp.include_router(tools_router)
407
+
408
+ # Prefixed include - tools get prefix
409
+ mcp.include_router(admin_router, prefix="admin")
410
+
411
+ # Hidden include - single dispatcher tool
412
+ mcp.include_router(setup_router, hidden=True)
413
+ """
414
+ if not hidden:
415
+ # Synchronous composition - directly copy tools/resources
416
+ self._sync_import_router(router, hidden=False, prefix=prefix, **kwargs)
417
+ return
418
+
419
+ # Hidden pattern: wrap in HiddenRouter before importing
420
+ from .router import HiddenRouter
421
+
422
+ # Import the hidden router (synchronous)
423
+ self._sync_import_router(HiddenRouter(router), hidden=True, prefix=prefix, **kwargs)
424
+
425
+ def _sync_import_router(
426
+ self,
427
+ router: FastMCP,
428
+ hidden: bool = False,
429
+ prefix: str | None = None,
430
+ **kwargs: Any,
431
+ ) -> None:
432
+ """Synchronously import tools/resources from a router.
433
+
434
+ This is a synchronous alternative to import_server for use at module import time.
435
+ """
436
+ import re
437
+
438
+ # Import tools directly - use internal dict to preserve keys
439
+ tools = (
440
+ router._tool_manager._tools.items() if not hidden else router._sync_list_tools().items() # type: ignore
441
+ )
442
+ for key, tool in tools:
443
+ # Validate tool name
444
+ if not re.match(r"^[a-zA-Z0-9_-]{1,128}$", key):
445
+ raise ValueError(
446
+ f"Tool name '{key}' must match ^[a-zA-Z0-9_-]{{1,128}}$ "
447
+ "(letters, numbers, underscore, hyphen only, 1-128 chars)"
448
+ )
449
+
450
+ new_key = f"{prefix}_{key}" if prefix else key
451
+ self._tool_manager._tools[new_key] = tool
452
+
453
+ # Import resources directly
454
+ for key, resource in router._resource_manager._resources.items():
455
+ new_key = f"{prefix}_{key}" if prefix else key
456
+ self._resource_manager._resources[new_key] = resource
457
+
458
+ # Import prompts directly
459
+ for key, prompt in router._prompt_manager._prompts.items():
460
+ new_key = f"{prefix}_{key}" if prefix else key
461
+ self._prompt_manager._prompts[new_key] = prompt
462
+ # await self.import_server(hidden_router, prefix=None, **kwargs)
463
+
385
464
  def _register_hud_helpers(self) -> None:
386
- """Register HUD helper HTTP routes.
465
+ """Register development helper endpoints.
387
466
 
388
467
  This adds:
389
- - GET /hud - Overview of available endpoints
390
- - GET /hud/tools - List all registered tools with their schemas
391
- - GET /hud/resources - List all registered resources
392
- - GET /hud/prompts - List all registered prompts
468
+ - GET /docs - Interactive documentation and tool testing
469
+ - POST /api/tools/{name} - REST wrappers for MCP tools
470
+ - GET /openapi.json - OpenAPI spec for REST endpoints
393
471
  """
394
472
 
395
- @self.custom_route("/hud/tools", methods=["GET"])
396
- async def list_tools(request: Request) -> Response:
397
- """List all registered tools with their names, descriptions, and schemas."""
398
- tools = []
399
- # _tools is a mapping of tool_name -> FunctionTool/Tool instance
473
+ # Register REST wrapper for each tool
474
+ def create_tool_endpoint(key: str) -> Any:
475
+ """Create a REST endpoint for an MCP tool."""
476
+
477
+ async def tool_endpoint(request: Request) -> Response:
478
+ """Call MCP tool via REST endpoint."""
479
+ try:
480
+ data = await request.json()
481
+ except Exception:
482
+ data = {}
483
+
484
+ try:
485
+ result = await self._tool_manager.call_tool(key, data)
486
+
487
+ # Recursively serialize MCP objects
488
+ def serialize_obj(obj: Any) -> Any:
489
+ """Recursively serialize MCP objects to JSON-compatible format."""
490
+ if obj is None or isinstance(obj, (str, int, float, bool)):
491
+ return obj
492
+ if isinstance(obj, (list, tuple)):
493
+ return [serialize_obj(item) for item in obj]
494
+ if isinstance(obj, dict):
495
+ return {k: serialize_obj(v) for k, v in obj.items()}
496
+ if hasattr(obj, "model_dump"):
497
+ # Pydantic v2
498
+ return serialize_obj(obj.model_dump())
499
+ if hasattr(obj, "dict"):
500
+ # Pydantic v1
501
+ return serialize_obj(obj.dict())
502
+ if hasattr(obj, "__dict__"):
503
+ # Dataclass or regular class
504
+ return serialize_obj(obj.__dict__)
505
+ # Fallback: convert to string
506
+ return str(obj)
507
+
508
+ serialized = serialize_obj(result)
509
+ return JSONResponse({"success": True, "result": serialized})
510
+ except Exception as e:
511
+ return JSONResponse({"success": False, "error": str(e)}, status_code=400)
512
+
513
+ return tool_endpoint
514
+
515
+ for tool_key in self._tool_manager._tools.keys(): # noqa: SIM118
516
+ endpoint = create_tool_endpoint(tool_key)
517
+ self.custom_route(f"/api/tools/{tool_key}", methods=["POST"])(endpoint)
518
+
519
+ @self.custom_route("/openapi.json", methods=["GET"])
520
+ async def openapi_spec(request: Request) -> Response:
521
+ """Generate OpenAPI spec from MCP tools."""
522
+ spec = {
523
+ "openapi": "3.1.0",
524
+ "info": {
525
+ "title": f"{self.name or 'MCP Server'} - Testing API",
526
+ "version": "1.0.0",
527
+ "description": (
528
+ "REST API wrappers for testing MCP tools. "
529
+ "These endpoints are for development/testing only. "
530
+ "Agents should connect via MCP protocol (JSON-RPC over stdio/HTTP)."
531
+ ),
532
+ },
533
+ "paths": {},
534
+ }
535
+
536
+ # Convert each MCP tool to an OpenAPI path
400
537
  for tool_key, tool in self._tool_manager._tools.items():
401
- tool_data = {"name": tool_key}
402
538
  try:
403
- # Prefer converting to MCP model for consistent fields
404
539
  mcp_tool = tool.to_mcp_tool()
405
- tool_data["description"] = getattr(mcp_tool, "description", "")
406
- if hasattr(mcp_tool, "inputSchema") and mcp_tool.inputSchema:
407
- tool_data["input_schema"] = mcp_tool.inputSchema # type: ignore[assignment]
408
- if hasattr(mcp_tool, "outputSchema") and mcp_tool.outputSchema:
409
- tool_data["output_schema"] = mcp_tool.outputSchema # type: ignore[assignment]
410
- except Exception:
411
- # Fallback to direct attributes on FunctionTool
412
- tool_data["description"] = getattr(tool, "description", "")
413
- params = getattr(tool, "parameters", None)
414
- if params:
415
- tool_data["input_schema"] = params
416
- tools.append(tool_data)
417
-
418
- return JSONResponse({"server": self.name, "tools": tools, "count": len(tools)})
419
-
420
- @self.custom_route("/hud/resources", methods=["GET"])
421
- async def list_resources(request: Request) -> Response:
422
- """List all registered resources."""
423
- resources = []
424
- for resource_key, resource in self._resource_manager._resources.items():
425
- resource_data = {
426
- "uri": resource_key,
427
- "name": resource.name,
428
- "description": resource.description,
429
- "mimeType": resource.mime_type,
430
- }
431
- resources.append(resource_data)
432
-
433
- return JSONResponse(
434
- {"server": self.name, "resources": resources, "count": len(resources)}
435
- )
540
+ input_schema = mcp_tool.inputSchema or {"type": "object"}
541
+
542
+ spec["paths"][f"/api/tools/{tool_key}"] = {
543
+ "post": {
544
+ "summary": tool_key,
545
+ "description": mcp_tool.description or "",
546
+ "operationId": f"call_{tool_key}",
547
+ "requestBody": {
548
+ "required": True,
549
+ "content": {"application/json": {"schema": input_schema}},
550
+ },
551
+ "responses": {
552
+ "200": {
553
+ "description": "Success",
554
+ "content": {
555
+ "application/json": {
556
+ "schema": {
557
+ "type": "object",
558
+ "properties": {
559
+ "success": {"type": "boolean"},
560
+ "result": {"type": "object"},
561
+ },
562
+ }
563
+ }
564
+ },
565
+ }
566
+ },
567
+ }
568
+ }
569
+ except Exception as e:
570
+ logger.warning("Failed to generate spec for %s: %s", tool_key, e)
571
+
572
+ return JSONResponse(spec)
573
+
574
+ @self.custom_route("/docs", methods=["GET"])
575
+ async def docs_page(request: Request) -> Response:
576
+ """Interactive documentation page."""
577
+ import base64
578
+ import json
436
579
 
437
- @self.custom_route("/hud/prompts", methods=["GET"])
438
- async def list_prompts(request: Request) -> Response:
439
- """List all registered prompts."""
440
- prompts = []
441
- for prompt_key, prompt in self._prompt_manager._prompts.items():
442
- prompt_data = {
443
- "name": prompt_key,
444
- "description": prompt.description,
445
- }
446
- # Check if it has arguments
447
- if hasattr(prompt, "arguments") and prompt.arguments:
448
- prompt_data["arguments"] = [
449
- {"name": arg.name, "description": arg.description, "required": arg.required}
450
- for arg in prompt.arguments
451
- ]
452
- prompts.append(prompt_data)
453
-
454
- return JSONResponse({"server": self.name, "prompts": prompts, "count": len(prompts)})
455
-
456
- @self.custom_route("/hud", methods=["GET"])
457
- async def hud_info(request: Request) -> Response:
458
- """Show available HUD helper endpoints."""
459
580
  base_url = str(request.base_url).rstrip("/")
460
- return JSONResponse(
461
- {
462
- "name": "HUD MCP Development Helpers",
463
- "server": self.name,
464
- "endpoints": {
465
- "tools": f"{base_url}/hud/tools",
466
- "resources": f"{base_url}/hud/resources",
467
- "prompts": f"{base_url}/hud/prompts",
468
- },
469
- "description": "These endpoints help you inspect your MCP server during development.", # noqa: E501
470
- }
471
- )
581
+ tool_count = len(self._tool_manager._tools)
582
+ resource_count = len(self._resource_manager._resources)
583
+
584
+ # Generate Cursor deeplink
585
+ server_config = {"url": f"{base_url}/mcp"}
586
+ config_json = json.dumps(server_config, indent=2)
587
+ config_base64 = base64.b64encode(config_json.encode()).decode()
588
+ cursor_deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={self.name or 'mcp-server'}&config={config_base64}" # noqa: E501
589
+
590
+ html = f"""
591
+ <!DOCTYPE html>
592
+ <html lang="en">
593
+ <head>
594
+ <meta charset="UTF-8">
595
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
596
+ <title>{self.name or "MCP Server"} - Documentation</title>
597
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
598
+ <style>
599
+ body {{ margin: 0; padding: 0; font-family: monospace; }}
600
+ .header {{ padding: 1.5rem; border-bottom: 1px solid #e0e0e0; background: #fafafa; }}
601
+ .header h1 {{ margin: 0 0 0.5rem 0; font-size: 1.5rem; color: #000; }}
602
+ .header .info {{ margin: 0.25rem 0; color: #666; font-size: 0.9rem; }}
603
+ .header .warning {{ margin: 0.75rem 0 0 0; padding: 0.5rem; background: #fff3cd; border-left: 3px solid #ffc107; color: #856404; font-size: 0.85rem; }}
604
+ .header a {{ color: #000; text-decoration: underline; }}
605
+ .header a:hover {{ color: #666; }}
606
+ .topbar {{ display: none; }}
607
+ </style>
608
+ </head>
609
+ <body>
610
+ <div class="header">
611
+ <h1>{self.name or "MCP Server"} - Development Tools</h1>
612
+ <div class="info">MCP Endpoint (use this with agents): <a href="{base_url}/mcp">{base_url}/mcp</a></div>
613
+ <div class="info">Tools: {tool_count} | Resources: {resource_count}</div>
614
+ <div class="info">Add to Cursor: <a href="{cursor_deeplink}">Click here to install</a></div>
615
+ <div class="warning">
616
+ ⚠️ The REST API below is for testing only. Agents connect via MCP protocol at <code>{base_url}/mcp</code>
617
+ </div>
618
+ </div>
619
+
620
+ <div id="swagger-ui"></div>
621
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
622
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
623
+ <script>
624
+ window.onload = function() {{
625
+ SwaggerUIBundle({{
626
+ url: '/openapi.json',
627
+ dom_id: '#swagger-ui',
628
+ deepLinking: true,
629
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
630
+ layout: "StandaloneLayout",
631
+ tryItOutEnabled: true
632
+ }})
633
+ }}
634
+ </script>
635
+ </body>
636
+ </html>
637
+ """ # noqa: E501
638
+ return Response(content=html, media_type="text/html")
hud/telemetry/trace.py CHANGED
@@ -139,7 +139,7 @@ def trace(
139
139
  else:
140
140
  # Use a placeholder for custom backends
141
141
  logger.warning(
142
- "HUD API key is not set, using a placeholder for the task run ID. If this looks wrong, check your API key." # noqa: E501
142
+ "HUD API key is not set, using a placeholder for the task run ID. If this looks wrong, check your API key." # noqa: E501
143
143
  )
144
144
  task_run_id = str(uuid.uuid4())
145
145
 
hud/tools/base.py CHANGED
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any, cast, Awaitable
5
+ from typing import TYPE_CHECKING, Any, cast
5
6
 
6
7
  from fastmcp import FastMCP
7
8
 
8
9
  from hud.tools.types import ContentBlock, EvaluationResult
9
10
 
10
11
  if TYPE_CHECKING:
11
- from collections.abc import Callable
12
+ from collections.abc import Awaitable, Callable
12
13
 
13
14
  from fastmcp.tools import FunctionTool
14
15
  from fastmcp.tools.tool import Tool, ToolResult
@@ -16,9 +17,9 @@ if TYPE_CHECKING:
16
17
  # Basic result types for tools
17
18
  BaseResult = list[ContentBlock] | EvaluationResult
18
19
 
19
- import logging
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
+
22
23
  class BaseTool(ABC):
23
24
  """
24
25
  Base helper class for all MCP tools to constrain their output.
@@ -106,9 +107,9 @@ class BaseTool(ABC):
106
107
  )
107
108
  return self._mcp_tool
108
109
 
109
- def add_callback(self, event_type: str, callback: Callable[..., Awaitable[Any]]):
110
+ def add_callback(self, event_type: str, callback: Callable[..., Awaitable[Any]]) -> None:
110
111
  """Register a callback function for specific event
111
-
112
+
112
113
  Args:
113
114
  event_type: (Required) Specific event name to trigger callback
114
115
  e.g. "after_click", "before_navigate"
@@ -118,7 +119,7 @@ class BaseTool(ABC):
118
119
  self._callbacks[event_type] = []
119
120
  self._callbacks[event_type].append(callback)
120
121
 
121
- def remove_callback(self, event_type: str, callback: Callable[..., Awaitable[Any]]):
122
+ def remove_callback(self, event_type: str, callback: Callable[..., Awaitable[Any]]) -> None:
122
123
  """Remove a registered callback
123
124
  Args:
124
125
  event_type: (Required) Specific event name to trigger callback
@@ -127,22 +128,27 @@ class BaseTool(ABC):
127
128
  """
128
129
  if (event_type in self._callbacks) and (callback in self._callbacks[event_type]):
129
130
  self._callbacks[event_type].remove(callback)
130
-
131
- async def _trigger_callbacks(self, event_type: str, **kwargs):
131
+
132
+ async def _trigger_callbacks(self, event_type: str, **kwargs: Any) -> None:
132
133
  """Trigger all registered callback functions of an event type"""
133
134
  callback_list = self._callbacks.get(event_type, [])
134
135
  for callback in callback_list:
135
136
  try:
136
137
  await callback(**kwargs)
137
138
  except Exception as e:
138
- logger.warning(f"Callback failed for {event_type}: {e}")
139
+ logger.warning("Callback failed for %s: %s", event_type, e)
140
+
139
141
 
140
142
  # Prefix for internal tool names
141
143
  _INTERNAL_PREFIX = "int_"
142
144
 
143
145
 
144
146
  class BaseHub(FastMCP):
145
- """A composition-friendly FastMCP server that holds an internal tool dispatcher."""
147
+ """A composition-friendly FastMCP server that holds an internal tool dispatcher.
148
+
149
+ Note: BaseHub can be used standalone or to wrap existing routers. For the newer
150
+ FastAPI-like pattern, consider using HiddenRouter from hud.server instead.
151
+ """
146
152
 
147
153
  env: Any
148
154
 
@@ -165,6 +171,10 @@ class BaseHub(FastMCP):
165
171
  Optional long-lived environment object. Stored on the server
166
172
  instance (``layer.env``) and therefore available to every request
167
173
  via ``ctx.fastmcp.env``.
174
+ title:
175
+ Optional title for the dispatcher tool.
176
+ description:
177
+ Optional description for the dispatcher tool.
168
178
  meta:
169
179
  Metadata to include in MCP tool listing.
170
180
  """
@@ -5,11 +5,13 @@ from __future__ import annotations
5
5
  from .anthropic import AnthropicComputerTool
6
6
  from .hud import HudComputerTool
7
7
  from .openai import OpenAIComputerTool
8
+ from .qwen import QwenComputerTool
8
9
  from .settings import computer_settings
9
10
 
10
11
  __all__ = [
11
12
  "AnthropicComputerTool",
12
13
  "HudComputerTool",
13
14
  "OpenAIComputerTool",
15
+ "QwenComputerTool",
14
16
  "computer_settings",
15
17
  ]