hud-python 0.4.45__py3-none-any.whl → 0.5.1__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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +11 -5
- hud/agents/base.py +220 -500
- hud/agents/claude.py +200 -240
- hud/agents/gemini.py +275 -0
- hud/agents/gemini_cua.py +335 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +41 -36
- hud/agents/openai.py +291 -292
- hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
- hud/agents/operator.py +211 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +379 -210
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +376 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/cli/__init__.py +461 -545
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +664 -110
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +882 -734
- hud/cli/eval.py +782 -668
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/push.py +29 -11
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +108 -6
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +69 -0
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +40 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +327 -0
- hud/datasets/runner.py +192 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +50 -0
- hud/environment/connection.py +206 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +109 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +694 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +112 -0
- hud/environment/scenarios.py +493 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +218 -0
- hud/environment/tests/test_environment.py +161 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +201 -0
- hud/environment/tests/test_scenarios.py +280 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +674 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +185 -0
- hud/eval/manager.py +466 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +340 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +145 -0
- hud/eval/types.py +63 -0
- hud/eval/utils.py +183 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +151 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +158 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +16 -2
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +4 -0
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +167 -57
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +61 -3
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.1.dist-info/METADATA +264 -0
- hud_python-0.5.1.dist-info/RECORD +299 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Tests for format integrations - OpenAI, Anthropic, Gemini."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import mcp.types as mcp_types
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_mock_tool(
|
|
11
|
+
name: str, description: str = "", schema: dict | None = None
|
|
12
|
+
) -> mcp_types.Tool:
|
|
13
|
+
"""Create a mock MCP tool for testing."""
|
|
14
|
+
return mcp_types.Tool(
|
|
15
|
+
name=name,
|
|
16
|
+
description=description,
|
|
17
|
+
inputSchema=schema or {"type": "object", "properties": {}},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestOpenAIMixin:
|
|
22
|
+
"""Tests for OpenAI format conversion."""
|
|
23
|
+
|
|
24
|
+
def test_as_openai_chat_tools_basic(self) -> None:
|
|
25
|
+
"""as_openai_chat_tools converts MCP tools to OpenAI format."""
|
|
26
|
+
from hud.environment.integrations.openai import OpenAIMixin
|
|
27
|
+
|
|
28
|
+
class TestEnv(OpenAIMixin):
|
|
29
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
30
|
+
return [
|
|
31
|
+
create_mock_tool(
|
|
32
|
+
"navigate",
|
|
33
|
+
"Navigate to URL",
|
|
34
|
+
{
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {"url": {"type": "string"}},
|
|
37
|
+
"required": ["url"],
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
env = TestEnv()
|
|
46
|
+
tools = env.as_openai_chat_tools()
|
|
47
|
+
|
|
48
|
+
assert len(tools) == 1
|
|
49
|
+
assert tools[0]["type"] == "function"
|
|
50
|
+
assert tools[0]["function"]["name"] == "navigate" # type: ignore[typeddict-item]
|
|
51
|
+
assert tools[0]["function"]["description"] == "Navigate to URL" # type: ignore[typeddict-item]
|
|
52
|
+
assert "url" in tools[0]["function"]["parameters"]["properties"] # type: ignore[typeddict-item, operator]
|
|
53
|
+
|
|
54
|
+
def test_as_openai_chat_tools_strict_mode(self) -> None:
|
|
55
|
+
"""as_openai_chat_tools with strict=True adds strict flag."""
|
|
56
|
+
from hud.environment.integrations.openai import OpenAIMixin
|
|
57
|
+
|
|
58
|
+
class TestEnv(OpenAIMixin):
|
|
59
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
60
|
+
return [create_mock_tool("test_tool")]
|
|
61
|
+
|
|
62
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
env = TestEnv()
|
|
66
|
+
tools = env.as_openai_chat_tools(strict=True)
|
|
67
|
+
|
|
68
|
+
assert tools[0]["function"]["strict"] is True # type: ignore[typeddict-item]
|
|
69
|
+
|
|
70
|
+
def test_as_openai_chat_tools_empty(self) -> None:
|
|
71
|
+
"""as_openai_chat_tools returns empty list when no tools."""
|
|
72
|
+
from hud.environment.integrations.openai import OpenAIMixin
|
|
73
|
+
|
|
74
|
+
class TestEnv(OpenAIMixin):
|
|
75
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
env = TestEnv()
|
|
82
|
+
tools = env.as_openai_chat_tools()
|
|
83
|
+
|
|
84
|
+
assert tools == []
|
|
85
|
+
|
|
86
|
+
def test_as_openai_responses_tools(self) -> None:
|
|
87
|
+
"""as_openai_responses_tools converts to Responses API format."""
|
|
88
|
+
from hud.environment.integrations.openai import OpenAIMixin
|
|
89
|
+
|
|
90
|
+
class TestEnv(OpenAIMixin):
|
|
91
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
92
|
+
return [create_mock_tool("search", "Search the web")]
|
|
93
|
+
|
|
94
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
env = TestEnv()
|
|
98
|
+
tools = env.as_openai_responses_tools()
|
|
99
|
+
|
|
100
|
+
assert len(tools) == 1
|
|
101
|
+
assert tools[0]["type"] == "function"
|
|
102
|
+
assert tools[0]["name"] == "search"
|
|
103
|
+
assert tools[0]["description"] == "Search the web"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestAnthropicMixin:
|
|
107
|
+
"""Tests for Anthropic/Claude format conversion."""
|
|
108
|
+
|
|
109
|
+
def test_as_claude_tools_basic(self) -> None:
|
|
110
|
+
"""as_claude_tools converts MCP tools to Claude format."""
|
|
111
|
+
from hud.environment.integrations.anthropic import AnthropicMixin
|
|
112
|
+
|
|
113
|
+
class TestEnv(AnthropicMixin):
|
|
114
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
115
|
+
return [
|
|
116
|
+
create_mock_tool(
|
|
117
|
+
"click",
|
|
118
|
+
"Click element",
|
|
119
|
+
{
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {"selector": {"type": "string"}},
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
env = TestEnv()
|
|
130
|
+
tools = env.as_claude_tools()
|
|
131
|
+
|
|
132
|
+
assert len(tools) == 1
|
|
133
|
+
assert tools[0]["name"] == "click"
|
|
134
|
+
assert tools[0]["description"] == "Click element"
|
|
135
|
+
assert "input_schema" in tools[0]
|
|
136
|
+
assert "cache_control" not in tools[0]
|
|
137
|
+
|
|
138
|
+
def test_as_claude_tools_with_cache_control(self) -> None:
|
|
139
|
+
"""as_claude_tools with cache_control=True adds cache field."""
|
|
140
|
+
from hud.environment.integrations.anthropic import AnthropicMixin
|
|
141
|
+
|
|
142
|
+
class TestEnv(AnthropicMixin):
|
|
143
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
144
|
+
return [create_mock_tool("test")]
|
|
145
|
+
|
|
146
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
env = TestEnv()
|
|
150
|
+
tools = env.as_claude_tools(cache_control=True)
|
|
151
|
+
|
|
152
|
+
assert tools[0]["cache_control"] == {"type": "ephemeral"}
|
|
153
|
+
|
|
154
|
+
def test_as_claude_programmatic_tools(self) -> None:
|
|
155
|
+
"""as_claude_programmatic_tools includes allowed_callers."""
|
|
156
|
+
from hud.environment.integrations.anthropic import AnthropicMixin
|
|
157
|
+
|
|
158
|
+
class TestEnv(AnthropicMixin):
|
|
159
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
160
|
+
return [create_mock_tool("analyze")]
|
|
161
|
+
|
|
162
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
env = TestEnv()
|
|
166
|
+
tools = env.as_claude_programmatic_tools()
|
|
167
|
+
|
|
168
|
+
assert tools[0]["allowed_callers"] == ["code_execution_20250825"]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestGeminiMixin:
|
|
172
|
+
"""Tests for Google/Gemini format conversion."""
|
|
173
|
+
|
|
174
|
+
def test_as_gemini_tools_basic(self) -> None:
|
|
175
|
+
"""as_gemini_tools converts MCP tools to Gemini format."""
|
|
176
|
+
from hud.environment.integrations.gemini import GeminiMixin
|
|
177
|
+
|
|
178
|
+
class TestEnv(GeminiMixin):
|
|
179
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
180
|
+
return [
|
|
181
|
+
create_mock_tool(
|
|
182
|
+
"search",
|
|
183
|
+
"Search query",
|
|
184
|
+
{
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {"query": {"type": "string"}},
|
|
187
|
+
},
|
|
188
|
+
),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
env = TestEnv()
|
|
192
|
+
tools = env.as_gemini_tools()
|
|
193
|
+
|
|
194
|
+
assert len(tools) == 1
|
|
195
|
+
assert "function_declarations" in tools[0]
|
|
196
|
+
declarations = tools[0]["function_declarations"]
|
|
197
|
+
assert len(declarations) == 1
|
|
198
|
+
assert declarations[0]["name"] == "search"
|
|
199
|
+
assert declarations[0]["description"] == "Search query"
|
|
200
|
+
|
|
201
|
+
def test_as_gemini_tools_multiple(self) -> None:
|
|
202
|
+
"""as_gemini_tools wraps multiple tools in single declaration list."""
|
|
203
|
+
from hud.environment.integrations.gemini import GeminiMixin
|
|
204
|
+
|
|
205
|
+
class TestEnv(GeminiMixin):
|
|
206
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
207
|
+
return [
|
|
208
|
+
create_mock_tool("tool1"),
|
|
209
|
+
create_mock_tool("tool2"),
|
|
210
|
+
create_mock_tool("tool3"),
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
env = TestEnv()
|
|
214
|
+
tools = env.as_gemini_tools()
|
|
215
|
+
|
|
216
|
+
assert len(tools) == 1 # Single wrapper object
|
|
217
|
+
assert len(tools[0]["function_declarations"]) == 3
|
|
218
|
+
|
|
219
|
+
def test_as_gemini_tool_config_auto(self) -> None:
|
|
220
|
+
"""as_gemini_tool_config with AUTO mode."""
|
|
221
|
+
from hud.environment.integrations.gemini import GeminiMixin
|
|
222
|
+
|
|
223
|
+
class TestEnv(GeminiMixin):
|
|
224
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
env = TestEnv()
|
|
228
|
+
config = env.as_gemini_tool_config(mode="AUTO")
|
|
229
|
+
|
|
230
|
+
assert config["function_calling_config"]["mode"] == "AUTO"
|
|
231
|
+
|
|
232
|
+
def test_as_gemini_tool_config_any_with_allowed(self) -> None:
|
|
233
|
+
"""as_gemini_tool_config with ANY mode and allowed tools."""
|
|
234
|
+
from hud.environment.integrations.gemini import GeminiMixin
|
|
235
|
+
|
|
236
|
+
class TestEnv(GeminiMixin):
|
|
237
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
env = TestEnv()
|
|
241
|
+
config = env.as_gemini_tool_config(mode="ANY", allowed_tools=["search", "navigate"])
|
|
242
|
+
|
|
243
|
+
assert config["function_calling_config"]["mode"] == "ANY"
|
|
244
|
+
assert config["function_calling_config"]["allowed_function_names"] == ["search", "navigate"]
|
|
245
|
+
|
|
246
|
+
def test_as_gemini_tool_config_none(self) -> None:
|
|
247
|
+
"""as_gemini_tool_config with NONE mode disables tools."""
|
|
248
|
+
from hud.environment.integrations.gemini import GeminiMixin
|
|
249
|
+
|
|
250
|
+
class TestEnv(GeminiMixin):
|
|
251
|
+
def as_tools(self) -> list[mcp_types.Tool]:
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
env = TestEnv()
|
|
255
|
+
config = env.as_gemini_tool_config(mode="NONE")
|
|
256
|
+
|
|
257
|
+
assert config["function_calling_config"]["mode"] == "NONE"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Tests for local connectors - connect_image, connect_server, connect_fastapi."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
from hud.environment.connection import ConnectionType, Connector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestConnectImage:
|
|
12
|
+
"""Tests for LocalConnectorMixin.connect_image."""
|
|
13
|
+
|
|
14
|
+
@patch("hud.cli.utils.docker.create_docker_run_command")
|
|
15
|
+
def test_connect_image_creates_local_connection(self, mock_docker_cmd: MagicMock) -> None:
|
|
16
|
+
"""connect_image creates LOCAL connection with docker command."""
|
|
17
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
18
|
+
|
|
19
|
+
mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
|
|
20
|
+
|
|
21
|
+
class TestEnv(LocalConnectorMixin):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._connections: dict[str, Connector] = {}
|
|
24
|
+
|
|
25
|
+
def mount(self, server: Any, *, prefix: str | None = None) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
env = TestEnv()
|
|
29
|
+
env.connect_image("mcp/fetch")
|
|
30
|
+
|
|
31
|
+
assert "mcp/fetch" in env._connections
|
|
32
|
+
conn = env._connections["mcp/fetch"]
|
|
33
|
+
assert conn.connection_type == ConnectionType.LOCAL
|
|
34
|
+
mock_docker_cmd.assert_called_once()
|
|
35
|
+
|
|
36
|
+
@patch("hud.cli.utils.docker.create_docker_run_command")
|
|
37
|
+
def test_connect_image_with_alias(self, mock_docker_cmd: MagicMock) -> None:
|
|
38
|
+
"""connect_image uses alias for connection name."""
|
|
39
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
40
|
+
|
|
41
|
+
mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
|
|
42
|
+
|
|
43
|
+
class TestEnv(LocalConnectorMixin):
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._connections: dict[str, Connector] = {}
|
|
46
|
+
|
|
47
|
+
def mount(self, server: Any, *, prefix: str | None = None) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
env = TestEnv()
|
|
51
|
+
env.connect_image("mcp/fetch", alias="fetcher")
|
|
52
|
+
|
|
53
|
+
assert "fetcher" in env._connections
|
|
54
|
+
assert "mcp/fetch" not in env._connections
|
|
55
|
+
|
|
56
|
+
@patch("hud.cli.utils.docker.create_docker_run_command")
|
|
57
|
+
def test_connect_image_with_prefix(self, mock_docker_cmd: MagicMock) -> None:
|
|
58
|
+
"""connect_image passes prefix to config."""
|
|
59
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
60
|
+
|
|
61
|
+
mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
|
|
62
|
+
|
|
63
|
+
class TestEnv(LocalConnectorMixin):
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
self._connections: dict[str, Connector] = {}
|
|
66
|
+
|
|
67
|
+
def mount(self, server: Any, *, prefix: str | None = None) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
env = TestEnv()
|
|
71
|
+
env.connect_image("mcp/fetch", prefix="fetch")
|
|
72
|
+
|
|
73
|
+
conn = env._connections["mcp/fetch"]
|
|
74
|
+
assert conn.config.prefix == "fetch"
|
|
75
|
+
|
|
76
|
+
@patch("hud.cli.utils.docker.create_docker_run_command")
|
|
77
|
+
def test_connect_image_returns_self(self, mock_docker_cmd: MagicMock) -> None:
|
|
78
|
+
"""connect_image returns self for chaining."""
|
|
79
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
80
|
+
|
|
81
|
+
mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
|
|
82
|
+
|
|
83
|
+
class TestEnv(LocalConnectorMixin):
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
self._connections: dict[str, Connector] = {}
|
|
86
|
+
|
|
87
|
+
def mount(self, server: Any, *, prefix: str | None = None) -> None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
env = TestEnv()
|
|
91
|
+
result = env.connect_image("mcp/fetch")
|
|
92
|
+
|
|
93
|
+
assert result is env
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestConnectServer:
|
|
97
|
+
"""Tests for LocalConnectorMixin.connect_server."""
|
|
98
|
+
|
|
99
|
+
def test_connect_server_calls_include_router(self) -> None:
|
|
100
|
+
"""connect_server calls include_router with server and prefix."""
|
|
101
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
102
|
+
|
|
103
|
+
class TestEnv(LocalConnectorMixin):
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
self._connections: dict[str, Connector] = {}
|
|
106
|
+
self.routers: list[tuple[Any, str | None]] = []
|
|
107
|
+
|
|
108
|
+
def include_router(self, server: Any, *, prefix: str | None = None) -> None:
|
|
109
|
+
self.routers.append((server, prefix))
|
|
110
|
+
|
|
111
|
+
env = TestEnv()
|
|
112
|
+
mock_server = MagicMock()
|
|
113
|
+
env.connect_server(mock_server, prefix="tools")
|
|
114
|
+
|
|
115
|
+
assert len(env.routers) == 1
|
|
116
|
+
assert env.routers[0] == (mock_server, "tools")
|
|
117
|
+
|
|
118
|
+
def test_connect_server_returns_self(self) -> None:
|
|
119
|
+
"""connect_server returns self for chaining."""
|
|
120
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
121
|
+
|
|
122
|
+
class TestEnv(LocalConnectorMixin):
|
|
123
|
+
def __init__(self) -> None:
|
|
124
|
+
self._connections: dict[str, Connector] = {}
|
|
125
|
+
|
|
126
|
+
def include_router(self, server: Any, *, prefix: str | None = None) -> None:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
env = TestEnv()
|
|
130
|
+
result = env.connect_server(MagicMock())
|
|
131
|
+
|
|
132
|
+
assert result is env
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestConnectFastAPI:
|
|
136
|
+
"""Tests for LocalConnectorMixin.connect_fastapi."""
|
|
137
|
+
|
|
138
|
+
@patch("fastmcp.FastMCP")
|
|
139
|
+
def test_connect_fastapi_creates_mcp_server(self, mock_fastmcp: MagicMock) -> None:
|
|
140
|
+
"""connect_fastapi converts FastAPI app to MCP server."""
|
|
141
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
142
|
+
|
|
143
|
+
mock_mcp_server = MagicMock()
|
|
144
|
+
mock_fastmcp.from_fastapi.return_value = mock_mcp_server
|
|
145
|
+
|
|
146
|
+
class TestEnv(LocalConnectorMixin):
|
|
147
|
+
def __init__(self) -> None:
|
|
148
|
+
self._connections: dict[str, Connector] = {}
|
|
149
|
+
self.routers: list[tuple[Any, str | None]] = []
|
|
150
|
+
|
|
151
|
+
def include_router(self, server: Any, *, prefix: str | None = None) -> None:
|
|
152
|
+
self.routers.append((server, prefix))
|
|
153
|
+
|
|
154
|
+
env = TestEnv()
|
|
155
|
+
mock_app = MagicMock()
|
|
156
|
+
mock_app.title = "My API"
|
|
157
|
+
env.connect_fastapi(mock_app)
|
|
158
|
+
|
|
159
|
+
mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="My API")
|
|
160
|
+
assert len(env.routers) == 1
|
|
161
|
+
assert env.routers[0] == (mock_mcp_server, None)
|
|
162
|
+
|
|
163
|
+
@patch("fastmcp.FastMCP")
|
|
164
|
+
def test_connect_fastapi_with_custom_name(self, mock_fastmcp: MagicMock) -> None:
|
|
165
|
+
"""connect_fastapi uses custom name if provided."""
|
|
166
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
167
|
+
|
|
168
|
+
mock_fastmcp.from_fastapi.return_value = MagicMock()
|
|
169
|
+
|
|
170
|
+
class TestEnv(LocalConnectorMixin):
|
|
171
|
+
def __init__(self) -> None:
|
|
172
|
+
self._connections: dict[str, Connector] = {}
|
|
173
|
+
|
|
174
|
+
def include_router(self, server: Any, *, prefix: str | None = None) -> None:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
env = TestEnv()
|
|
178
|
+
mock_app = MagicMock()
|
|
179
|
+
mock_app.title = "Original"
|
|
180
|
+
env.connect_fastapi(mock_app, name="custom-api")
|
|
181
|
+
|
|
182
|
+
mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="custom-api")
|
|
183
|
+
|
|
184
|
+
@patch("fastmcp.FastMCP")
|
|
185
|
+
def test_connect_fastapi_returns_self(self, mock_fastmcp: MagicMock) -> None:
|
|
186
|
+
"""connect_fastapi returns self for chaining."""
|
|
187
|
+
from hud.environment.connectors.local import LocalConnectorMixin
|
|
188
|
+
|
|
189
|
+
mock_fastmcp.from_fastapi.return_value = MagicMock()
|
|
190
|
+
|
|
191
|
+
class TestEnv(LocalConnectorMixin):
|
|
192
|
+
def __init__(self) -> None:
|
|
193
|
+
self._connections: dict[str, Connector] = {}
|
|
194
|
+
|
|
195
|
+
def include_router(self, server: Any, *, prefix: str | None = None) -> None:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
env = TestEnv()
|
|
199
|
+
result = env.connect_fastapi(MagicMock())
|
|
200
|
+
|
|
201
|
+
assert result is env
|