hud-python 0.4.48__py3-none-any.whl → 0.4.50__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 +40 -34
- hud/agents/grounded_openai.py +1 -1
- hud/cli/__init__.py +78 -213
- hud/cli/build.py +105 -45
- hud/cli/dev.py +614 -743
- hud/cli/flows/tasks.py +98 -17
- hud/cli/init.py +18 -14
- hud/cli/push.py +27 -9
- hud/cli/rl/local_runner.py +3 -3
- hud/cli/tests/test_eval.py +168 -119
- hud/cli/tests/test_mcp_server.py +6 -95
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/source_hash.py +1 -1
- hud/server/__init__.py +2 -1
- hud/server/router.py +160 -0
- hud/server/server.py +246 -79
- hud/tools/base.py +9 -1
- hud/tools/bash.py +2 -2
- hud/tools/edit.py +3 -7
- hud/utils/hud_console.py +43 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/METADATA +1 -1
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/RECORD +27 -26
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/WHEEL +0 -0
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.48.dist-info → hud_python-0.4.50.dist-info}/licenses/LICENSE +0 -0
hud/cli/tests/test_mcp_server.py
CHANGED
|
@@ -2,101 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
5
|
+
from unittest.mock import patch
|
|
7
6
|
|
|
8
7
|
import pytest
|
|
9
8
|
|
|
10
9
|
from hud.cli.dev import (
|
|
11
|
-
create_proxy_server,
|
|
12
|
-
get_docker_cmd,
|
|
13
|
-
get_image_name,
|
|
14
10
|
run_mcp_dev_server,
|
|
15
|
-
update_pyproject_toml,
|
|
16
11
|
)
|
|
17
12
|
|
|
18
13
|
|
|
19
|
-
class TestCreateMCPServer:
|
|
20
|
-
"""Test MCP server creation."""
|
|
21
|
-
|
|
22
|
-
def test_create_mcp_server(self) -> None:
|
|
23
|
-
"""Test that MCP server is created with correct configuration."""
|
|
24
|
-
mcp = create_proxy_server(".", "test-image:latest")
|
|
25
|
-
assert mcp._mcp_server.name == "HUD Dev Proxy - test-image:latest"
|
|
26
|
-
# Proxy server doesn't define its own tools, it forwards to Docker containers
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TestDockerUtils:
|
|
30
|
-
"""Test Docker utility functions."""
|
|
31
|
-
|
|
32
|
-
def test_get_docker_cmd(self) -> None:
|
|
33
|
-
"""Test extracting CMD from Docker image."""
|
|
34
|
-
with patch("subprocess.run") as mock_run:
|
|
35
|
-
mock_result = MagicMock()
|
|
36
|
-
mock_result.returncode = 0
|
|
37
|
-
mock_result.stdout = '["python", "-m", "server"]'
|
|
38
|
-
mock_run.return_value = mock_result
|
|
39
|
-
|
|
40
|
-
cmd = get_docker_cmd("test-image:latest")
|
|
41
|
-
assert cmd is None
|
|
42
|
-
|
|
43
|
-
def test_get_docker_cmd_failure(self) -> None:
|
|
44
|
-
"""Test handling when Docker inspect fails."""
|
|
45
|
-
import subprocess
|
|
46
|
-
|
|
47
|
-
with patch("subprocess.run") as mock_run:
|
|
48
|
-
# check=True causes CalledProcessError on non-zero return
|
|
49
|
-
mock_run.side_effect = subprocess.CalledProcessError(1, "docker inspect")
|
|
50
|
-
|
|
51
|
-
cmd = get_docker_cmd("test-image:latest")
|
|
52
|
-
assert cmd is None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class TestImageResolution:
|
|
56
|
-
"""Test image name resolution."""
|
|
57
|
-
|
|
58
|
-
def test_get_image_name_override(self) -> None:
|
|
59
|
-
"""Test image name with override."""
|
|
60
|
-
name, source = get_image_name(".", "custom-image:v1")
|
|
61
|
-
assert name == "custom-image:v1"
|
|
62
|
-
assert source == "override"
|
|
63
|
-
|
|
64
|
-
def test_get_image_name_from_pyproject(self, tmp_path: Path) -> None:
|
|
65
|
-
"""Test image name from pyproject.toml."""
|
|
66
|
-
pyproject = tmp_path / "pyproject.toml"
|
|
67
|
-
pyproject.write_text("""
|
|
68
|
-
[tool.hud]
|
|
69
|
-
image = "my-project:latest"
|
|
70
|
-
""")
|
|
71
|
-
|
|
72
|
-
name, source = get_image_name(str(tmp_path))
|
|
73
|
-
assert name == "my-project:latest"
|
|
74
|
-
assert source == "cache"
|
|
75
|
-
|
|
76
|
-
def test_get_image_name_auto_generate(self, tmp_path: Path) -> None:
|
|
77
|
-
"""Test auto-generated image name."""
|
|
78
|
-
test_dir = tmp_path / "my_test_project"
|
|
79
|
-
test_dir.mkdir()
|
|
80
|
-
|
|
81
|
-
name, source = get_image_name(str(test_dir))
|
|
82
|
-
assert name == "my-test-project:dev"
|
|
83
|
-
assert source == "auto"
|
|
84
|
-
|
|
85
|
-
def test_update_pyproject_toml(self, tmp_path: Path) -> None:
|
|
86
|
-
"""Test updating pyproject.toml with image name."""
|
|
87
|
-
pyproject = tmp_path / "pyproject.toml"
|
|
88
|
-
pyproject.write_text("""
|
|
89
|
-
[project]
|
|
90
|
-
name = "test"
|
|
91
|
-
""")
|
|
92
|
-
|
|
93
|
-
update_pyproject_toml(str(tmp_path), "new-image:v1", silent=True)
|
|
94
|
-
|
|
95
|
-
content = pyproject.read_text()
|
|
96
|
-
assert "[tool.hud]" in content
|
|
97
|
-
assert 'image = "new-image:v1"' in content
|
|
98
|
-
|
|
99
|
-
|
|
100
14
|
class TestRunMCPDevServer:
|
|
101
15
|
"""Test the main server runner."""
|
|
102
16
|
|
|
@@ -110,16 +24,13 @@ class TestRunMCPDevServer:
|
|
|
110
24
|
pytest.raises(click.Abort),
|
|
111
25
|
):
|
|
112
26
|
run_mcp_dev_server(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
build=False,
|
|
116
|
-
no_cache=False,
|
|
117
|
-
transport="http",
|
|
27
|
+
module=".",
|
|
28
|
+
stdio=False,
|
|
118
29
|
port=8765,
|
|
119
|
-
no_reload=False,
|
|
120
30
|
verbose=False,
|
|
121
31
|
inspector=False,
|
|
122
|
-
no_logs=False,
|
|
123
|
-
docker_args=[],
|
|
124
32
|
interactive=False,
|
|
33
|
+
watch=[],
|
|
34
|
+
docker=False,
|
|
35
|
+
docker_args=[],
|
|
125
36
|
)
|
hud/cli/utils/env_check.py
CHANGED
|
@@ -175,16 +175,16 @@ def ensure_built(env_dir: Path, *, interactive: bool = True) -> dict[str, Any]:
|
|
|
175
175
|
_print_section("Added files", diffs.get("added", []))
|
|
176
176
|
_print_section("Removed files", diffs.get("removed", []))
|
|
177
177
|
|
|
178
|
-
if interactive:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
else:
|
|
185
|
-
hud_console.hint("Continuing without rebuild; this may use an outdated image.")
|
|
178
|
+
# if interactive:
|
|
179
|
+
if hud_console.confirm("Rebuild now (runs 'hud build')?", default=True):
|
|
180
|
+
require_docker_running()
|
|
181
|
+
build_environment(str(env_dir), platform="linux/amd64")
|
|
182
|
+
with open(lock_path) as f:
|
|
183
|
+
lock_data = yaml.safe_load(f) or {}
|
|
186
184
|
else:
|
|
187
|
-
hud_console.hint("
|
|
185
|
+
hud_console.hint("Continuing without rebuild; this may use an outdated image.")
|
|
186
|
+
# else:
|
|
187
|
+
# hud_console.hint("Run 'hud build' to update the image before proceeding.")
|
|
188
188
|
elif not stored_hash:
|
|
189
189
|
hud_console.dim_info(
|
|
190
190
|
"Info",
|
hud/cli/utils/source_hash.py
CHANGED
hud/server/__init__.py
CHANGED
hud/server/router.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""MCP Router utilities for FastAPI-like composition patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from hud.server import MCPServer
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
from fastmcp.tools import Tool
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# MCPRouter is just an alias to FastMCP for FastAPI-like patterns
|
|
19
|
+
MCPRouter = MCPServer
|
|
20
|
+
|
|
21
|
+
# Prefix for internal tool names
|
|
22
|
+
_INTERNAL_PREFIX = "int_"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HiddenRouter(MCPRouter):
|
|
26
|
+
"""Wraps a FastMCP router to provide a single dispatcher tool for its sub-tools.
|
|
27
|
+
|
|
28
|
+
Instead of exposing all tools at the top level, this creates a single tool
|
|
29
|
+
(named after the router) that dispatches to the router's tools internally.
|
|
30
|
+
|
|
31
|
+
Useful for setup/evaluate patterns where you want:
|
|
32
|
+
- A single 'setup' tool that can call setup_basic(), setup_advanced(), etc.
|
|
33
|
+
- A single 'evaluate' tool that can call evaluate_score(), evaluate_complete(), etc.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
# Create a router with multiple setup functions
|
|
37
|
+
setup_router = MCPRouter(name="setup")
|
|
38
|
+
|
|
39
|
+
@setup_router.tool
|
|
40
|
+
async def reset():
|
|
41
|
+
return "Environment reset"
|
|
42
|
+
|
|
43
|
+
@setup_router.tool
|
|
44
|
+
async def seed_data():
|
|
45
|
+
return "Data seeded"
|
|
46
|
+
|
|
47
|
+
# Wrap in HiddenRouter
|
|
48
|
+
hidden_setup = HiddenRouter(setup_router)
|
|
49
|
+
|
|
50
|
+
# Now you have one 'setup' tool that dispatches to reset/seed_data
|
|
51
|
+
mcp.include_router(hidden_setup)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
router: FastMCP,
|
|
57
|
+
*,
|
|
58
|
+
title: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
meta: dict[str, Any] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Wrap an existing router with a dispatcher pattern.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
router: The FastMCP router to wrap
|
|
66
|
+
title: Optional title for the dispatcher tool (defaults to "{name} Dispatcher")
|
|
67
|
+
description: Optional description for the dispatcher tool
|
|
68
|
+
meta: Optional metadata for the dispatcher tool
|
|
69
|
+
"""
|
|
70
|
+
name = router.name or "router"
|
|
71
|
+
|
|
72
|
+
# Naming scheme for hidden/internal tools
|
|
73
|
+
self._prefix_fn: Callable[[str], str] = lambda n: f"{_INTERNAL_PREFIX}{n}"
|
|
74
|
+
|
|
75
|
+
super().__init__(name=name)
|
|
76
|
+
|
|
77
|
+
# Set up dispatcher tool
|
|
78
|
+
dispatcher_title = title or f"{name.title()} Dispatcher"
|
|
79
|
+
dispatcher_desc = description or f"Call internal '{name}' functions"
|
|
80
|
+
|
|
81
|
+
# Register dispatcher that routes to hidden tools
|
|
82
|
+
async def _dispatch(
|
|
83
|
+
name: str,
|
|
84
|
+
arguments: dict | str | None = None,
|
|
85
|
+
ctx: Any | None = None,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Gateway to hidden tools.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name: Internal function name (without prefix)
|
|
91
|
+
arguments: Arguments to forward to the internal tool (dict or JSON string)
|
|
92
|
+
ctx: Request context injected by FastMCP
|
|
93
|
+
"""
|
|
94
|
+
# Handle JSON string inputs
|
|
95
|
+
if isinstance(arguments, str):
|
|
96
|
+
import json
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
arguments = json.loads(arguments)
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
arguments = {}
|
|
102
|
+
|
|
103
|
+
# Call the internal tool
|
|
104
|
+
return await self._tool_manager.call_tool(self._prefix_fn(name), arguments or {}) # type: ignore
|
|
105
|
+
|
|
106
|
+
from fastmcp.tools.tool import FunctionTool
|
|
107
|
+
|
|
108
|
+
dispatcher_tool = FunctionTool.from_function(
|
|
109
|
+
_dispatch,
|
|
110
|
+
name=name,
|
|
111
|
+
title=dispatcher_title,
|
|
112
|
+
description=dispatcher_desc,
|
|
113
|
+
tags=set(),
|
|
114
|
+
meta=meta,
|
|
115
|
+
)
|
|
116
|
+
self._tool_manager.add_tool(dispatcher_tool)
|
|
117
|
+
|
|
118
|
+
# Copy all tools from source router as hidden tools
|
|
119
|
+
for tool in router._tool_manager._tools.values():
|
|
120
|
+
tool._key = self._prefix_fn(tool.name)
|
|
121
|
+
self._tool_manager.add_tool(tool)
|
|
122
|
+
|
|
123
|
+
# Expose list of available functions via resource
|
|
124
|
+
async def _functions_catalogue() -> list[str]:
|
|
125
|
+
"""List all internal function names without prefix."""
|
|
126
|
+
return [
|
|
127
|
+
key.removeprefix(_INTERNAL_PREFIX)
|
|
128
|
+
for key in self._tool_manager._tools
|
|
129
|
+
if key.startswith(_INTERNAL_PREFIX)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
from fastmcp.resources import Resource
|
|
133
|
+
|
|
134
|
+
catalogue_resource = Resource.from_function(
|
|
135
|
+
_functions_catalogue,
|
|
136
|
+
uri=f"{name}://functions",
|
|
137
|
+
name=f"{name.title()} Functions",
|
|
138
|
+
description=f"List of available {name} functions",
|
|
139
|
+
)
|
|
140
|
+
self._resource_manager.add_resource(catalogue_resource)
|
|
141
|
+
|
|
142
|
+
# Override _list_tools to hide internal tools when mounted
|
|
143
|
+
async def _list_tools(self) -> list[Tool]:
|
|
144
|
+
"""Override _list_tools to hide internal tools when mounted."""
|
|
145
|
+
return [
|
|
146
|
+
tool
|
|
147
|
+
for key, tool in self._tool_manager._tools.items()
|
|
148
|
+
if not key.startswith(_INTERNAL_PREFIX)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
def _sync_list_tools(self) -> dict[str, Tool]:
|
|
152
|
+
"""Override _list_tools to hide internal tools when mounted."""
|
|
153
|
+
return {
|
|
154
|
+
key: tool
|
|
155
|
+
for key, tool in self._tool_manager._tools.items()
|
|
156
|
+
if not key.startswith(_INTERNAL_PREFIX)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = ["HiddenRouter", "MCPRouter"]
|