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.
- hud/agents/base.py +55 -142
- hud/agents/claude.py +5 -6
- hud/agents/grounded_openai.py +1 -1
- hud/agents/misc/integration_test_agent.py +2 -0
- hud/agents/tests/test_base.py +2 -5
- hud/cli/__init__.py +80 -215
- hud/cli/build.py +105 -45
- hud/cli/dev.py +614 -743
- hud/cli/eval.py +14 -9
- hud/cli/flows/tasks.py +100 -21
- hud/cli/init.py +18 -14
- hud/cli/push.py +27 -9
- hud/cli/rl/local_runner.py +28 -16
- hud/cli/rl/vllm.py +2 -0
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_eval.py +574 -0
- hud/cli/tests/test_mcp_server.py +6 -95
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/source_hash.py +1 -1
- hud/datasets/parallel.py +0 -12
- hud/datasets/runner.py +1 -4
- hud/rl/actor.py +4 -2
- hud/rl/distributed.py +1 -1
- hud/rl/learner.py +2 -1
- hud/rl/train.py +1 -1
- hud/server/__init__.py +2 -1
- hud/server/router.py +160 -0
- hud/server/server.py +246 -79
- hud/telemetry/trace.py +1 -1
- hud/tools/base.py +20 -10
- hud/tools/computer/__init__.py +2 -0
- hud/tools/computer/qwen.py +431 -0
- hud/tools/computer/settings.py +16 -0
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/playwright.py +1 -1
- hud/types.py +2 -3
- hud/utils/hud_console.py +43 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/METADATA +1 -1
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/RECORD +45 -42
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/WHEEL +0 -0
- {hud_python-0.4.47.dist-info → hud_python-0.4.49.dist-info}/entry_points.txt +0 -0
- {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__(
|
|
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
|
|
465
|
+
"""Register development helper endpoints.
|
|
387
466
|
|
|
388
467
|
This adds:
|
|
389
|
-
- GET /
|
|
390
|
-
-
|
|
391
|
-
- GET /
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
"""
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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."
|
|
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
|
|
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(
|
|
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
|
"""
|
hud/tools/computer/__init__.py
CHANGED
|
@@ -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
|
]
|