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,68 @@
|
|
|
1
|
+
"""Base connector mixin with shared helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
|
|
10
|
+
from fastmcp.tools.tool import Tool
|
|
11
|
+
|
|
12
|
+
from hud.environment.connection import ConnectionType, Connector
|
|
13
|
+
|
|
14
|
+
__all__ = ["BaseConnectorMixin"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseConnectorMixin:
|
|
18
|
+
"""Base mixin providing connection helper.
|
|
19
|
+
|
|
20
|
+
Requires:
|
|
21
|
+
_connections: dict[str, Connector]
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_connections: dict[str, Connector]
|
|
25
|
+
|
|
26
|
+
def _add_connection(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
transport: Any,
|
|
30
|
+
*,
|
|
31
|
+
connection_type: ConnectionType,
|
|
32
|
+
auth: str | None = None,
|
|
33
|
+
prefix: str | None = None,
|
|
34
|
+
include: list[str] | None = None,
|
|
35
|
+
exclude: list[str] | None = None,
|
|
36
|
+
transform: Callable[[Tool], Tool | None] | None = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Add a connection to the environment.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Connection name/alias.
|
|
42
|
+
transport: FastMCP transport (URL, config dict, etc.).
|
|
43
|
+
connection_type: LOCAL or REMOTE - determines parallelization.
|
|
44
|
+
auth: Authorization header value.
|
|
45
|
+
prefix: Prefix for tool names.
|
|
46
|
+
include: Only include these tools.
|
|
47
|
+
exclude: Exclude these tools.
|
|
48
|
+
transform: Transform function for tools.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
self for chaining.
|
|
52
|
+
"""
|
|
53
|
+
from hud.environment.connection import ConnectionConfig, Connector
|
|
54
|
+
|
|
55
|
+
config = ConnectionConfig(
|
|
56
|
+
prefix=prefix,
|
|
57
|
+
include=include,
|
|
58
|
+
exclude=exclude,
|
|
59
|
+
transform=transform,
|
|
60
|
+
)
|
|
61
|
+
self._connections[name] = Connector(
|
|
62
|
+
transport,
|
|
63
|
+
config,
|
|
64
|
+
name,
|
|
65
|
+
connection_type=connection_type,
|
|
66
|
+
auth=auth,
|
|
67
|
+
)
|
|
68
|
+
return self
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Local connection connectors - Docker image, FastAPI, MCPServer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from fastmcp.tools.tool import Tool
|
|
13
|
+
|
|
14
|
+
__all__ = ["LocalConnectorMixin"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalConnectorMixin(MCPConfigConnectorMixin):
|
|
18
|
+
"""Mixin providing local connection methods.
|
|
19
|
+
|
|
20
|
+
Methods:
|
|
21
|
+
connect_image(image) - Run Docker image via stdio
|
|
22
|
+
connect_fastapi(app) - Mount FastAPI app as MCP server
|
|
23
|
+
connect_server(server) - Mount any MCPServer/FastMCP directly
|
|
24
|
+
|
|
25
|
+
Inherits connect_mcp() from MCPConfigConnectorMixin.
|
|
26
|
+
|
|
27
|
+
Note: include_router() is inherited from MCPServer (via FastMCP).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def connect_image(
|
|
31
|
+
self,
|
|
32
|
+
image: str,
|
|
33
|
+
*,
|
|
34
|
+
alias: str | None = None,
|
|
35
|
+
docker_args: list[str] | None = None,
|
|
36
|
+
env_vars: dict[str, str] | None = None,
|
|
37
|
+
prefix: str | None = None,
|
|
38
|
+
include: list[str] | None = None,
|
|
39
|
+
exclude: list[str] | None = None,
|
|
40
|
+
transform: Callable[[Tool], Tool | None] | None = None,
|
|
41
|
+
) -> Any:
|
|
42
|
+
"""Connect to a Docker image via stdio.
|
|
43
|
+
|
|
44
|
+
Creates an MCP config that runs: docker run -i --rm {image}
|
|
45
|
+
Environment variables from `.env` files are auto-injected.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
```python
|
|
49
|
+
env = Environment("my-env")
|
|
50
|
+
env.connect_image("mcp/fetch")
|
|
51
|
+
|
|
52
|
+
async with env:
|
|
53
|
+
result = await env.call_tool("fetch", url="https://example.com")
|
|
54
|
+
```
|
|
55
|
+
"""
|
|
56
|
+
from hud.cli.utils.docker import create_docker_run_command
|
|
57
|
+
|
|
58
|
+
cmd = create_docker_run_command(
|
|
59
|
+
image=image,
|
|
60
|
+
docker_args=docker_args,
|
|
61
|
+
extra_env=env_vars,
|
|
62
|
+
interactive=True,
|
|
63
|
+
remove=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
name = alias or image
|
|
67
|
+
mcp_config = {
|
|
68
|
+
name: {
|
|
69
|
+
"command": cmd[0],
|
|
70
|
+
"args": cmd[1:],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return self.connect_mcp(
|
|
74
|
+
mcp_config,
|
|
75
|
+
alias=name,
|
|
76
|
+
prefix=prefix,
|
|
77
|
+
include=include,
|
|
78
|
+
exclude=exclude,
|
|
79
|
+
transform=transform,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def connect_fastapi(
|
|
83
|
+
self,
|
|
84
|
+
app: Any,
|
|
85
|
+
*,
|
|
86
|
+
name: str | None = None,
|
|
87
|
+
prefix: str | None = None,
|
|
88
|
+
include_hidden: bool = True,
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Import a FastAPI application's routes as MCP tools.
|
|
91
|
+
|
|
92
|
+
Uses FastMCP's from_fastapi() to convert FastAPI endpoints to MCP tools,
|
|
93
|
+
then imports them synchronously so they're available immediately.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
app: FastAPI application instance
|
|
97
|
+
name: Custom name for the server (defaults to app.title)
|
|
98
|
+
prefix: Optional prefix for tool names
|
|
99
|
+
include_hidden: If True (default), includes routes with include_in_schema=False
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
```python
|
|
103
|
+
from fastapi import FastAPI
|
|
104
|
+
|
|
105
|
+
api = FastAPI()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@api.get("/users/{user_id}", operation_id="get_user")
|
|
109
|
+
def get_user(user_id: int):
|
|
110
|
+
return {"id": user_id, "name": "Alice"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
env = Environment("my-env")
|
|
114
|
+
env.connect_fastapi(api)
|
|
115
|
+
|
|
116
|
+
async with env:
|
|
117
|
+
result = await env.call_tool("get_user", user_id=1)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Tip: Use operation_id in FastAPI decorators for cleaner tool names.
|
|
121
|
+
"""
|
|
122
|
+
from fastmcp import FastMCP
|
|
123
|
+
|
|
124
|
+
# Temporarily enable hidden routes for OpenAPI generation
|
|
125
|
+
hidden_routes: list[Any] = []
|
|
126
|
+
if include_hidden:
|
|
127
|
+
for route in getattr(app, "routes", []):
|
|
128
|
+
if hasattr(route, "include_in_schema") and not route.include_in_schema:
|
|
129
|
+
hidden_routes.append(route)
|
|
130
|
+
route.include_in_schema = True
|
|
131
|
+
# Clear cached openapi schema so it regenerates
|
|
132
|
+
if hasattr(app, "openapi_schema"):
|
|
133
|
+
app.openapi_schema = None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
server_name = name or getattr(app, "title", None) or "fastapi"
|
|
137
|
+
mcp_server = FastMCP.from_fastapi(app=app, name=server_name)
|
|
138
|
+
# Use include_router for synchronous import (tools available immediately)
|
|
139
|
+
self.include_router(mcp_server, prefix=prefix) # type: ignore
|
|
140
|
+
finally:
|
|
141
|
+
# Restore original states
|
|
142
|
+
for route in hidden_routes:
|
|
143
|
+
route.include_in_schema = False
|
|
144
|
+
if hidden_routes and hasattr(app, "openapi_schema"):
|
|
145
|
+
app.openapi_schema = None # Clear cache again
|
|
146
|
+
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def connect_server(
|
|
150
|
+
self,
|
|
151
|
+
server: Any,
|
|
152
|
+
*,
|
|
153
|
+
prefix: str | None = None,
|
|
154
|
+
) -> Any:
|
|
155
|
+
"""Import an MCPServer or FastMCP instance's tools directly.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
```python
|
|
159
|
+
from fastmcp import FastMCP
|
|
160
|
+
|
|
161
|
+
tools = FastMCP("tools")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@tools.tool
|
|
165
|
+
def greet(name: str) -> str:
|
|
166
|
+
return f"Hello, {name}!"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
env = Environment("my-env")
|
|
170
|
+
env.connect_server(tools)
|
|
171
|
+
|
|
172
|
+
async with env:
|
|
173
|
+
result = await env.call_tool("greet", name="World")
|
|
174
|
+
```
|
|
175
|
+
"""
|
|
176
|
+
self.include_router(server, prefix=prefix) # type: ignore
|
|
177
|
+
return self
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""MCP config connection connectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from hud.environment.connectors.base import BaseConnectorMixin
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from fastmcp.tools.tool import Tool
|
|
13
|
+
|
|
14
|
+
__all__ = ["MCPConfigConnectorMixin"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPConfigConnectorMixin(BaseConnectorMixin):
|
|
18
|
+
"""Mixin providing mcp_config connection methods."""
|
|
19
|
+
|
|
20
|
+
def connect_mcp(
|
|
21
|
+
self,
|
|
22
|
+
config: dict[str, dict[str, Any]],
|
|
23
|
+
*,
|
|
24
|
+
alias: str | None = None,
|
|
25
|
+
prefix: str | None = None,
|
|
26
|
+
include: list[str] | None = None,
|
|
27
|
+
exclude: list[str] | None = None,
|
|
28
|
+
transform: Callable[[Tool], Tool | None] | None = None,
|
|
29
|
+
) -> Any:
|
|
30
|
+
"""Connect using an mcp_config dictionary (single server).
|
|
31
|
+
|
|
32
|
+
Auto-detects LOCAL (stdio) vs REMOTE (URL) based on config.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
env = Environment("my-env")
|
|
37
|
+
|
|
38
|
+
# Stdio server
|
|
39
|
+
env.connect_mcp(
|
|
40
|
+
{
|
|
41
|
+
"filesystem": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async with env:
|
|
49
|
+
await env.call_tool("read_file", path="/tmp/test.txt")
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
from hud.environment.connection import ConnectionType
|
|
53
|
+
|
|
54
|
+
name = alias or next(iter(config.keys()), "mcp")
|
|
55
|
+
server_config = next(iter(config.values()), {})
|
|
56
|
+
|
|
57
|
+
is_local = "command" in server_config or "args" in server_config
|
|
58
|
+
conn_type = ConnectionType.LOCAL if is_local else ConnectionType.REMOTE
|
|
59
|
+
|
|
60
|
+
return self._add_connection(
|
|
61
|
+
name,
|
|
62
|
+
config,
|
|
63
|
+
connection_type=conn_type,
|
|
64
|
+
prefix=prefix,
|
|
65
|
+
include=include,
|
|
66
|
+
exclude=exclude,
|
|
67
|
+
transform=transform,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def connect_mcp_config(
|
|
71
|
+
self,
|
|
72
|
+
mcp_config: dict[str, dict[str, Any]],
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
) -> Any:
|
|
75
|
+
"""Connect multiple servers from an mcp_config dictionary.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
```python
|
|
79
|
+
env = Environment("my-env")
|
|
80
|
+
|
|
81
|
+
# Claude Desktop style config
|
|
82
|
+
env.connect_mcp_config(
|
|
83
|
+
{
|
|
84
|
+
"filesystem": {
|
|
85
|
+
"command": "npx",
|
|
86
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
87
|
+
},
|
|
88
|
+
"github": {
|
|
89
|
+
"command": "npx",
|
|
90
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
91
|
+
"env": {"GITHUB_TOKEN": "..."},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async with env:
|
|
97
|
+
await env.call_tool("read_file", path="/tmp/test.txt")
|
|
98
|
+
await env.call_tool("search_repositories", query="mcp")
|
|
99
|
+
```
|
|
100
|
+
"""
|
|
101
|
+
# Store mcp_config for serialization (v4 format)
|
|
102
|
+
# Merge with existing if called multiple times
|
|
103
|
+
if not hasattr(self, "_mcp_config") or self._mcp_config is None:
|
|
104
|
+
self._mcp_config = {}
|
|
105
|
+
self._mcp_config.update(mcp_config)
|
|
106
|
+
|
|
107
|
+
for server_name, server_config in mcp_config.items():
|
|
108
|
+
self.connect_mcp({server_name: server_config}, alias=server_name, **kwargs)
|
|
109
|
+
return self
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""OpenAI Agents SDK connectors - import tools from OpenAI agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = ["OpenAIConnectorMixin"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenAIConnectorMixin:
|
|
12
|
+
"""Mixin providing OpenAI Agents SDK connector methods."""
|
|
13
|
+
|
|
14
|
+
# These are defined on Environment/MCPServer
|
|
15
|
+
_tool_manager: Any
|
|
16
|
+
|
|
17
|
+
def connect_function_tools(
|
|
18
|
+
self,
|
|
19
|
+
tools: list[Any],
|
|
20
|
+
*,
|
|
21
|
+
prefix: str | None = None,
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""Import FunctionTools from the OpenAI Agents SDK.
|
|
24
|
+
|
|
25
|
+
Wraps each tool so calls go through HUD with telemetry.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from agents import function_tool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@function_tool
|
|
33
|
+
def search(query: str) -> str:
|
|
34
|
+
'''Search for information.'''
|
|
35
|
+
return f"Results for {query}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@function_tool
|
|
39
|
+
def calculate(expression: str) -> float:
|
|
40
|
+
'''Evaluate a math expression.'''
|
|
41
|
+
return eval(expression)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
env = Environment("my-env")
|
|
45
|
+
env.connect_function_tools([search, calculate])
|
|
46
|
+
|
|
47
|
+
async with env:
|
|
48
|
+
result = await env.call_tool("search", query="MCP protocol")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
Requires `openai-agents`: pip install openai-agents
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
from agents import FunctionTool
|
|
56
|
+
except ImportError as e:
|
|
57
|
+
raise ImportError(
|
|
58
|
+
"openai-agents is required for connect_function_tools. "
|
|
59
|
+
"Install with: pip install openai-agents"
|
|
60
|
+
) from e
|
|
61
|
+
|
|
62
|
+
for tool in tools:
|
|
63
|
+
if isinstance(tool, FunctionTool):
|
|
64
|
+
self._add_openai_function_tool(tool, prefix)
|
|
65
|
+
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def _add_openai_function_tool(self, tool: Any, prefix: str | None) -> None:
|
|
69
|
+
"""Convert OpenAI FunctionTool to local MCP tool."""
|
|
70
|
+
name = f"{prefix}_{tool.name}" if prefix else tool.name
|
|
71
|
+
|
|
72
|
+
# Get the original invoke function
|
|
73
|
+
original_invoke = tool.on_invoke_tool
|
|
74
|
+
|
|
75
|
+
# Create wrapper that calls the original
|
|
76
|
+
async def invoke(**arguments: Any) -> Any:
|
|
77
|
+
# OpenAI's on_invoke_tool expects (ToolContext, str_json_args)
|
|
78
|
+
# We need to create a minimal context
|
|
79
|
+
from agents.tool_context import ToolContext
|
|
80
|
+
|
|
81
|
+
ctx = ToolContext(context=None)
|
|
82
|
+
result = await original_invoke(ctx, json.dumps(arguments))
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
# Set function metadata for FastMCP
|
|
86
|
+
invoke.__name__ = name
|
|
87
|
+
invoke.__doc__ = tool.description
|
|
88
|
+
|
|
89
|
+
# Register using FastMCP's tool decorator mechanism
|
|
90
|
+
# We access the internal _tool_manager from MCPServer
|
|
91
|
+
from fastmcp.tools import Tool as FastMCPTool
|
|
92
|
+
|
|
93
|
+
fastmcp_tool = FastMCPTool.from_function(
|
|
94
|
+
fn=invoke,
|
|
95
|
+
name=name,
|
|
96
|
+
description=tool.description,
|
|
97
|
+
)
|
|
98
|
+
# Override the schema with OpenAI's (more accurate)
|
|
99
|
+
fastmcp_tool.parameters = tool.params_json_schema
|
|
100
|
+
|
|
101
|
+
self._tool_manager.add_tool(fastmcp_tool)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Remote connection connectors - HUD Hub, URL, OpenAPI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from fastmcp.tools.tool import Tool
|
|
14
|
+
|
|
15
|
+
__all__ = ["RemoteConnectorMixin"]
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RemoteConnectorMixin(MCPConfigConnectorMixin):
|
|
21
|
+
"""Mixin providing remote connection methods.
|
|
22
|
+
|
|
23
|
+
Note: include_router() is inherited from MCPServer (via FastMCP).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def connect_hub(
|
|
27
|
+
self,
|
|
28
|
+
slug: str,
|
|
29
|
+
*,
|
|
30
|
+
alias: str | None = None,
|
|
31
|
+
prefix: str | None = None,
|
|
32
|
+
include: list[str] | None = None,
|
|
33
|
+
exclude: list[str] | None = None,
|
|
34
|
+
transform: Callable[[Tool], Tool | None] | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Connect to a HUD Hub environment.
|
|
37
|
+
|
|
38
|
+
Creates an MCP connection to the HUD API with the hub slug in headers.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
env = Environment("my-env")
|
|
43
|
+
env.connect_hub("browser")
|
|
44
|
+
|
|
45
|
+
async with env:
|
|
46
|
+
await env.call_tool("navigate", url="https://google.com")
|
|
47
|
+
```
|
|
48
|
+
"""
|
|
49
|
+
from hud.settings import settings
|
|
50
|
+
|
|
51
|
+
logger.info("Connecting to hub environment: %s", slug)
|
|
52
|
+
|
|
53
|
+
# Store hub config for serialization (v5 format)
|
|
54
|
+
# Note: Only first hub is stored for serialization (task configs use single hub)
|
|
55
|
+
if not hasattr(self, "_hub_config") or self._hub_config is None:
|
|
56
|
+
hub_config: dict[str, Any] = {"name": slug}
|
|
57
|
+
if include:
|
|
58
|
+
hub_config["include"] = include
|
|
59
|
+
if exclude:
|
|
60
|
+
hub_config["exclude"] = exclude
|
|
61
|
+
self._hub_config = hub_config
|
|
62
|
+
|
|
63
|
+
# Create mcp_config with standard MCP URL and hub slug in headers
|
|
64
|
+
# Note: Authorization is injected at request time by httpx/aiohttp hooks
|
|
65
|
+
# in hud.eval.instrument (uses contextvar for api_key).
|
|
66
|
+
mcp_config = {
|
|
67
|
+
"hud": {
|
|
68
|
+
"url": settings.hud_mcp_url,
|
|
69
|
+
"headers": {"Environment-Name": slug},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self.connect_mcp_config(
|
|
74
|
+
mcp_config, prefix=prefix, include=include, exclude=exclude, transform=transform
|
|
75
|
+
)
|
|
76
|
+
logger.info("Hub connected: %s", slug)
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def connect_url(
|
|
80
|
+
self,
|
|
81
|
+
url: str,
|
|
82
|
+
*,
|
|
83
|
+
headers: dict[str, str] | None = None,
|
|
84
|
+
alias: str | None = None,
|
|
85
|
+
prefix: str | None = None,
|
|
86
|
+
include: list[str] | None = None,
|
|
87
|
+
exclude: list[str] | None = None,
|
|
88
|
+
transform: Callable[[Tool], Tool | None] | None = None,
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Connect to an MCP server via URL.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
```python
|
|
94
|
+
env = Environment("my-env")
|
|
95
|
+
env.connect_url(
|
|
96
|
+
"https://mcp.example.com",
|
|
97
|
+
headers={"Authorization": "Bearer token"},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async with env:
|
|
101
|
+
await env.call_tool("search", query="hello")
|
|
102
|
+
```
|
|
103
|
+
"""
|
|
104
|
+
from hud.environment.connection import ConnectionType
|
|
105
|
+
|
|
106
|
+
auth = headers.get("Authorization") if headers else None
|
|
107
|
+
return self._add_connection(
|
|
108
|
+
alias or url,
|
|
109
|
+
url,
|
|
110
|
+
connection_type=ConnectionType.REMOTE,
|
|
111
|
+
auth=auth,
|
|
112
|
+
prefix=prefix,
|
|
113
|
+
include=include,
|
|
114
|
+
exclude=exclude,
|
|
115
|
+
transform=transform,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def connect_openapi(
|
|
119
|
+
self,
|
|
120
|
+
openapi_spec: dict[str, Any] | str,
|
|
121
|
+
*,
|
|
122
|
+
base_url: str | None = None,
|
|
123
|
+
headers: dict[str, str] | None = None,
|
|
124
|
+
name: str | None = None,
|
|
125
|
+
prefix: str | None = None,
|
|
126
|
+
timeout: float = 30.0,
|
|
127
|
+
) -> Any:
|
|
128
|
+
"""Mount an OpenAPI specification as an MCP server.
|
|
129
|
+
|
|
130
|
+
Converts REST API endpoints to MCP tools. Base URL is auto-inferred
|
|
131
|
+
from the spec URL when possible.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
```python
|
|
135
|
+
env = Environment("my-env")
|
|
136
|
+
env.connect_openapi("https://petstore.swagger.io/v2/swagger.json")
|
|
137
|
+
|
|
138
|
+
async with env:
|
|
139
|
+
result = await env.call_tool("getPetById", petId=1)
|
|
140
|
+
```
|
|
141
|
+
"""
|
|
142
|
+
from urllib.parse import urlparse
|
|
143
|
+
|
|
144
|
+
import httpx
|
|
145
|
+
from fastmcp import FastMCP
|
|
146
|
+
|
|
147
|
+
if isinstance(openapi_spec, str):
|
|
148
|
+
if openapi_spec.startswith(("http://", "https://")):
|
|
149
|
+
if base_url is None:
|
|
150
|
+
parsed = urlparse(openapi_spec)
|
|
151
|
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
152
|
+
|
|
153
|
+
resp = httpx.get(openapi_spec, headers=headers)
|
|
154
|
+
resp.raise_for_status()
|
|
155
|
+
openapi_spec = resp.json()
|
|
156
|
+
else:
|
|
157
|
+
import json
|
|
158
|
+
|
|
159
|
+
with open(openapi_spec) as f:
|
|
160
|
+
openapi_spec = json.load(f)
|
|
161
|
+
|
|
162
|
+
if base_url is None:
|
|
163
|
+
raise ValueError("base_url is required when openapi_spec is a dict or file")
|
|
164
|
+
|
|
165
|
+
client = httpx.AsyncClient(base_url=base_url, headers=headers or {}, timeout=timeout)
|
|
166
|
+
mcp_server = FastMCP.from_openapi(
|
|
167
|
+
openapi_spec=cast("dict[str, Any]", openapi_spec),
|
|
168
|
+
client=client,
|
|
169
|
+
name=name or "openapi",
|
|
170
|
+
)
|
|
171
|
+
self.include_router(mcp_server, prefix=prefix) # type: ignore
|
|
172
|
+
return self
|