hud-python 0.4.45__py3-none-any.whl → 0.5.13__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 +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -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 +381 -214
- 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 +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- 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/pull.py +1 -1
- hud/cli/push.py +38 -13
- 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 +110 -8
- 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.py +1 -1
- 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 +70 -1
- 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 +45 -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 +326 -0
- hud/datasets/runner.py +198 -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 +52 -0
- hud/environment/connection.py +258 -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 +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -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 +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -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 +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -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 +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -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 +165 -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 +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- 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_agent_tool.py +355 -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 +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- 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.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.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.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/licenses/LICENSE +0 -0
hud/utils/hud_console.py
CHANGED
|
@@ -20,9 +20,8 @@ import time
|
|
|
20
20
|
import traceback
|
|
21
21
|
from typing import TYPE_CHECKING, Any, Literal, Self
|
|
22
22
|
|
|
23
|
-
import questionary
|
|
24
|
-
import typer
|
|
25
23
|
from rich.console import Console
|
|
24
|
+
from rich.markup import escape
|
|
26
25
|
from rich.panel import Panel
|
|
27
26
|
from rich.table import Table
|
|
28
27
|
|
|
@@ -38,9 +37,26 @@ TEXT = "bright_white" # Off-white that's readable on dark, not too bright on li
|
|
|
38
37
|
SECONDARY = "rgb(108,113,196)" # Muted blue-purple for secondary text
|
|
39
38
|
|
|
40
39
|
|
|
40
|
+
# HUD Symbol System - Minimal 3-category system with default colors
|
|
41
|
+
class Symbols:
|
|
42
|
+
"""Unicode symbols for consistent CLI output with default colors."""
|
|
43
|
+
|
|
44
|
+
# Info/Items - Use for all informational lines (gold)
|
|
45
|
+
ITEM = f"[{GOLD}]•[/{GOLD}]"
|
|
46
|
+
|
|
47
|
+
# Status - Use for state/completion (green)
|
|
48
|
+
SUCCESS = f"[{GREEN}]●[/{GREEN}]"
|
|
49
|
+
|
|
50
|
+
# Flow/Special - Use for transitions and important notes (gold)
|
|
51
|
+
FLOW = f"[{GOLD}]⟿[/{GOLD}]"
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
class HUDConsole:
|
|
42
55
|
"""Design system for HUD CLI output."""
|
|
43
56
|
|
|
57
|
+
# Make symbols easily accessible
|
|
58
|
+
sym = Symbols
|
|
59
|
+
|
|
44
60
|
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
45
61
|
"""Initialize the design system.
|
|
46
62
|
|
|
@@ -80,7 +96,7 @@ class HUDConsole:
|
|
|
80
96
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
81
97
|
"""
|
|
82
98
|
console = self._stderr_console if stderr else self._stdout_console
|
|
83
|
-
console.print(f"[{GREEN}]✅ {message}[/{GREEN}]")
|
|
99
|
+
console.print(f"[{GREEN}]✅ {escape(message)}[/{GREEN}]")
|
|
84
100
|
|
|
85
101
|
def error(self, message: str, stderr: bool = True) -> None:
|
|
86
102
|
"""Print an error message.
|
|
@@ -91,10 +107,12 @@ class HUDConsole:
|
|
|
91
107
|
"""
|
|
92
108
|
console = self._stderr_console if stderr else self._stdout_console
|
|
93
109
|
tb = traceback.format_exc()
|
|
110
|
+
escaped_message = escape(message)
|
|
94
111
|
if "NoneType: None" not in tb:
|
|
95
|
-
|
|
112
|
+
escaped_tb = escape(tb)
|
|
113
|
+
console.print(f"[{RED} not bold]❌ {escaped_message}\n{escaped_tb}[/{RED} not bold]")
|
|
96
114
|
else:
|
|
97
|
-
console.print(f"[{RED} not bold]❌ {
|
|
115
|
+
console.print(f"[{RED} not bold]❌ {escaped_message}[/{RED} not bold]")
|
|
98
116
|
|
|
99
117
|
def warning(self, message: str, stderr: bool = True) -> None:
|
|
100
118
|
"""Print a warning message.
|
|
@@ -104,7 +122,7 @@ class HUDConsole:
|
|
|
104
122
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
105
123
|
"""
|
|
106
124
|
console = self._stderr_console if stderr else self._stdout_console
|
|
107
|
-
console.print(f"⚠️ [{YELLOW} not bold]{message}[/{YELLOW} not bold]")
|
|
125
|
+
console.print(f"⚠️ [{YELLOW} not bold]{escape(message)}[/{YELLOW} not bold]")
|
|
108
126
|
|
|
109
127
|
def info(self, message: str, stderr: bool = True) -> None:
|
|
110
128
|
"""Print an info message.
|
|
@@ -114,7 +132,7 @@ class HUDConsole:
|
|
|
114
132
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
115
133
|
"""
|
|
116
134
|
console = self._stderr_console if stderr else self._stdout_console
|
|
117
|
-
console.print(f"[{TEXT} not bold]{message}[/{TEXT} not bold]")
|
|
135
|
+
console.print(f"[{TEXT} not bold]{escape(message)}[/{TEXT} not bold]")
|
|
118
136
|
|
|
119
137
|
def print(self, message: str, stderr: bool = True) -> None:
|
|
120
138
|
"""Print a message.
|
|
@@ -136,7 +154,7 @@ class HUDConsole:
|
|
|
136
154
|
"""
|
|
137
155
|
console = self._stderr_console if stderr else self._stdout_console
|
|
138
156
|
console.print(
|
|
139
|
-
f"[{DIM} not bold][default]{label}[/default][/{DIM} not bold] [default]{value}[/default]" # noqa: E501
|
|
157
|
+
f"[{DIM} not bold][default]{escape(label)}[/default][/{DIM} not bold] [default]{escape(value)}[/default]" # noqa: E501
|
|
140
158
|
)
|
|
141
159
|
|
|
142
160
|
def link(self, url: str, stderr: bool = True) -> None:
|
|
@@ -147,7 +165,7 @@ class HUDConsole:
|
|
|
147
165
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
148
166
|
"""
|
|
149
167
|
console = self._stderr_console if stderr else self._stdout_console
|
|
150
|
-
console.print(f"[{SECONDARY} underline]{url}[/{SECONDARY} underline]")
|
|
168
|
+
console.print(f"[{SECONDARY} underline]{escape(url)}[/{SECONDARY} underline]")
|
|
151
169
|
|
|
152
170
|
def json_config(self, json_str: str, stderr: bool = True) -> None:
|
|
153
171
|
"""Print JSON configuration with neutral theme.
|
|
@@ -158,7 +176,7 @@ class HUDConsole:
|
|
|
158
176
|
"""
|
|
159
177
|
# Print JSON with neutral grey text
|
|
160
178
|
console = self._stderr_console if stderr else self._stdout_console
|
|
161
|
-
console.print(f"[{TEXT}]{json_str}[/{TEXT}]")
|
|
179
|
+
console.print(f"[{TEXT}]{escape(json_str)}[/{TEXT}]")
|
|
162
180
|
|
|
163
181
|
def key_value_table(
|
|
164
182
|
self, data: dict[str, str | int | float], show_header: bool = False, stderr: bool = True
|
|
@@ -188,7 +206,7 @@ class HUDConsole:
|
|
|
188
206
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
189
207
|
"""
|
|
190
208
|
console = self._stderr_console if stderr else self._stdout_console
|
|
191
|
-
console.print(f"[{DIM}]{message}[/{DIM}]")
|
|
209
|
+
console.print(f"[{DIM}]{escape(message)}[/{DIM}]")
|
|
192
210
|
|
|
193
211
|
def phase(self, phase_num: int, title: str, stderr: bool = True) -> None:
|
|
194
212
|
"""Print a phase header (for debug command).
|
|
@@ -221,7 +239,7 @@ class HUDConsole:
|
|
|
221
239
|
stderr: If True, output to stderr (default), otherwise stdout
|
|
222
240
|
"""
|
|
223
241
|
console = self._stderr_console if stderr else self._stdout_console
|
|
224
|
-
console.print(f"[rgb(181,137,0)]💡 Hint: {hint}[/rgb(181,137,0)]")
|
|
242
|
+
console.print(f"[rgb(181,137,0)]💡 Hint: {escape(hint)}[/rgb(181,137,0)]")
|
|
225
243
|
|
|
226
244
|
def status_item(
|
|
227
245
|
self,
|
|
@@ -250,10 +268,14 @@ class HUDConsole:
|
|
|
250
268
|
indicator = indicators.get(status, indicators["info"])
|
|
251
269
|
console = self._stderr_console if stderr else self._stdout_console
|
|
252
270
|
|
|
271
|
+
escaped_label = escape(label)
|
|
272
|
+
escaped_value = escape(value)
|
|
253
273
|
if primary:
|
|
254
|
-
console.print(
|
|
274
|
+
console.print(
|
|
275
|
+
f"{indicator} {escaped_label}: [bold {SECONDARY}]{escaped_value}[/bold {SECONDARY}]"
|
|
276
|
+
)
|
|
255
277
|
else:
|
|
256
|
-
console.print(f"{indicator} {
|
|
278
|
+
console.print(f"{indicator} {escaped_label}: [{TEXT}]{escaped_value}[/{TEXT}]")
|
|
257
279
|
|
|
258
280
|
def command_example(
|
|
259
281
|
self, command: str, description: str | None = None, stderr: bool = True
|
|
@@ -470,6 +492,9 @@ class HUDConsole:
|
|
|
470
492
|
Returns:
|
|
471
493
|
The selected choice value
|
|
472
494
|
"""
|
|
495
|
+
import questionary
|
|
496
|
+
from questionary import Style
|
|
497
|
+
|
|
473
498
|
# Convert choices to questionary format
|
|
474
499
|
q_choices = []
|
|
475
500
|
|
|
@@ -481,15 +506,27 @@ class HUDConsole:
|
|
|
481
506
|
else:
|
|
482
507
|
q_choices.append(choice)
|
|
483
508
|
|
|
509
|
+
# Custom style for better visibility of selection
|
|
510
|
+
custom_style = Style(
|
|
511
|
+
[
|
|
512
|
+
("qmark", "fg:cyan bold"),
|
|
513
|
+
("question", "bold"),
|
|
514
|
+
("pointer", "fg:cyan bold"),
|
|
515
|
+
("highlighted", "fg:cyan bold"),
|
|
516
|
+
]
|
|
517
|
+
)
|
|
518
|
+
|
|
484
519
|
result = questionary.select(
|
|
485
520
|
message,
|
|
486
521
|
choices=q_choices,
|
|
487
|
-
default=q_choices[default] if default is not None else None,
|
|
488
522
|
instruction="(Use ↑/↓ arrows, Enter to select)",
|
|
523
|
+
style=custom_style,
|
|
489
524
|
).ask()
|
|
490
525
|
|
|
491
526
|
# If no selection made (Ctrl+C or ESC), exit
|
|
492
527
|
if result is None:
|
|
528
|
+
import typer
|
|
529
|
+
|
|
493
530
|
raise typer.Exit(1)
|
|
494
531
|
|
|
495
532
|
return result
|
|
@@ -516,7 +553,12 @@ class HUDConsole:
|
|
|
516
553
|
except (TypeError, ValueError):
|
|
517
554
|
args_str = str(arguments)[:60]
|
|
518
555
|
|
|
519
|
-
|
|
556
|
+
escaped_name = escape(name)
|
|
557
|
+
escaped_args = escape(args_str)
|
|
558
|
+
return (
|
|
559
|
+
f"[{GOLD}]→[/{GOLD}] [bold {TEXT}]{escaped_name}[/bold {TEXT}]"
|
|
560
|
+
f"[{DIM}]({escaped_args})[/{DIM}]"
|
|
561
|
+
)
|
|
520
562
|
|
|
521
563
|
def format_tool_result(self, content: str, is_error: bool = False) -> str:
|
|
522
564
|
"""Format a tool result in compact HUD style.
|
|
@@ -532,11 +574,12 @@ class HUDConsole:
|
|
|
532
574
|
if len(content) > 80:
|
|
533
575
|
content = content[:77] + "..."
|
|
534
576
|
|
|
577
|
+
escaped_content = escape(content)
|
|
535
578
|
# Format with status using HUD colors
|
|
536
579
|
if is_error:
|
|
537
|
-
return f" [{RED}]✗[/{RED}] [{DIM}]{
|
|
580
|
+
return f" [{RED}]✗[/{RED}] [{DIM}]{escaped_content}[/{DIM}]"
|
|
538
581
|
else:
|
|
539
|
-
return f" [{GREEN}]✓[/{GREEN}] [{TEXT}]{
|
|
582
|
+
return f" [{GREEN}]✓[/{GREEN}] [{TEXT}]{escaped_content}[/{TEXT}]"
|
|
540
583
|
|
|
541
584
|
def confirm(self, message: str, default: bool = True) -> bool:
|
|
542
585
|
"""Print a confirmation message.
|
|
@@ -545,8 +588,36 @@ class HUDConsole:
|
|
|
545
588
|
message: The confirmation message
|
|
546
589
|
default: If True, the default choice is True
|
|
547
590
|
"""
|
|
591
|
+
import questionary
|
|
592
|
+
|
|
548
593
|
return questionary.confirm(message, default=default).ask()
|
|
549
594
|
|
|
595
|
+
# Symbol-based output methods
|
|
596
|
+
def symbol(self, symbol: str, message: str, color: str = GOLD, stderr: bool = True) -> None:
|
|
597
|
+
"""Print a message with a colored symbol prefix.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
symbol: Symbol to use (use Symbols.* constants)
|
|
601
|
+
message: Message text
|
|
602
|
+
color: Color for the symbol (default: gold)
|
|
603
|
+
stderr: If True, output to stderr
|
|
604
|
+
"""
|
|
605
|
+
console = self._stderr_console if stderr else self._stdout_console
|
|
606
|
+
console.print(f"[{color}]{symbol}[/{color}] {escape(message)}")
|
|
607
|
+
|
|
608
|
+
def detail(self, message: str, stderr: bool = True) -> None:
|
|
609
|
+
"""Print an indented detail line with gold pointer symbol."""
|
|
610
|
+
console = self._stderr_console if stderr else self._stdout_console
|
|
611
|
+
console.print(f" [{GOLD}]{Symbols.ITEM}[/{GOLD}] {escape(message)}")
|
|
612
|
+
|
|
613
|
+
def flow(self, message: str, stderr: bool = True) -> None:
|
|
614
|
+
"""Print a flow/transition message with wave symbol."""
|
|
615
|
+
self.symbol(Symbols.FLOW, message, GOLD, stderr)
|
|
616
|
+
|
|
617
|
+
def note(self, message: str, stderr: bool = True) -> None:
|
|
618
|
+
"""Print an important note with asterism symbol."""
|
|
619
|
+
self.symbol(Symbols.ITEM, message, GOLD, stderr)
|
|
620
|
+
|
|
550
621
|
|
|
551
622
|
# Global design instance for convenience
|
|
552
623
|
class _ProgressContext:
|
hud/utils/mcp.py
CHANGED
|
@@ -5,8 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
-
from hud.settings import settings
|
|
9
|
-
|
|
10
8
|
logger = logging.getLogger(__name__)
|
|
11
9
|
|
|
12
10
|
|
|
@@ -17,15 +15,27 @@ class MCPConfigPatch(BaseModel):
|
|
|
17
15
|
meta: dict[str, Any] | None = Field(default_factory=dict, alias="meta")
|
|
18
16
|
|
|
19
17
|
|
|
18
|
+
def _is_hud_server(url: str) -> bool:
|
|
19
|
+
"""Check if a URL is a HUD MCP server.
|
|
20
|
+
|
|
21
|
+
Matches:
|
|
22
|
+
- Any mcp.hud.* domain (including .ai, .so, and future domains)
|
|
23
|
+
- Staging servers (orcstaging.hud.so)
|
|
24
|
+
- Any *.hud.ai or *.hud.so domain
|
|
25
|
+
"""
|
|
26
|
+
if not url:
|
|
27
|
+
return False
|
|
28
|
+
url_lower = url.lower()
|
|
29
|
+
return "mcp.hud." in url_lower or ".hud.ai" in url_lower or ".hud.so" in url_lower
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def patch_mcp_config(mcp_config: dict[str, dict[str, Any]], patch: MCPConfigPatch) -> None:
|
|
21
33
|
"""Patch MCP config with additional values."""
|
|
22
|
-
hud_mcp_url = settings.hud_mcp_url
|
|
23
|
-
|
|
24
34
|
for server_cfg in mcp_config.values():
|
|
25
35
|
url = server_cfg.get("url", "")
|
|
26
36
|
|
|
27
37
|
# 1) HTTP header lane (only for hud MCP servers)
|
|
28
|
-
if
|
|
38
|
+
if _is_hud_server(url) and patch.headers:
|
|
29
39
|
for key, value in patch.headers.items():
|
|
30
40
|
headers = server_cfg.setdefault("headers", {})
|
|
31
41
|
headers.setdefault(key, value)
|
|
@@ -35,56 +45,3 @@ def patch_mcp_config(mcp_config: dict[str, dict[str, Any]], patch: MCPConfigPatc
|
|
|
35
45
|
for key, value in patch.meta.items():
|
|
36
46
|
meta = server_cfg.setdefault("meta", {})
|
|
37
47
|
meta.setdefault(key, value)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def setup_hud_telemetry(
|
|
41
|
-
mcp_config: dict[str, dict[str, Any]], auto_trace: bool = True
|
|
42
|
-
) -> Any | None:
|
|
43
|
-
"""Setup telemetry for hud servers.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
The auto-created trace context manager if one was created, None otherwise.
|
|
47
|
-
Caller is responsible for exiting the context manager.
|
|
48
|
-
"""
|
|
49
|
-
if not mcp_config:
|
|
50
|
-
raise ValueError("Please run initialize() before setting up client-side telemetry")
|
|
51
|
-
|
|
52
|
-
# Check if there are any HUD servers to setup telemetry for
|
|
53
|
-
hud_mcp_url = settings.hud_mcp_url
|
|
54
|
-
has_hud_servers = any(
|
|
55
|
-
hud_mcp_url in server_cfg.get("url", "") for server_cfg in mcp_config.values()
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# If no HUD servers, no need for telemetry setup
|
|
59
|
-
if not has_hud_servers:
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
from hud.otel import get_current_task_run_id
|
|
63
|
-
from hud.telemetry import trace
|
|
64
|
-
|
|
65
|
-
run_id = get_current_task_run_id()
|
|
66
|
-
auto_trace_cm = None
|
|
67
|
-
|
|
68
|
-
if not run_id and auto_trace:
|
|
69
|
-
# Start an auto trace and capture its ID for headers/metadata
|
|
70
|
-
auto_trace_cm = trace("My Trace")
|
|
71
|
-
_trace_obj = auto_trace_cm.__enter__()
|
|
72
|
-
try:
|
|
73
|
-
run_id = getattr(_trace_obj, "id", None) or str(_trace_obj)
|
|
74
|
-
except Exception: # pragma: no cover - fallback shouldn't fail lint
|
|
75
|
-
run_id = None
|
|
76
|
-
|
|
77
|
-
# Patch HUD servers with run-id (works whether auto or user trace)
|
|
78
|
-
if run_id:
|
|
79
|
-
patch_mcp_config(
|
|
80
|
-
mcp_config,
|
|
81
|
-
MCPConfigPatch(headers={"Run-Id": run_id}, meta={"run_id": run_id}),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
if settings.api_key:
|
|
85
|
-
patch_mcp_config(
|
|
86
|
-
mcp_config,
|
|
87
|
-
MCPConfigPatch(headers={"Authorization": f"Bearer {settings.api_key}"}),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
return auto_trace_cm
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Utilities to convert JSON schemas into OpenAI's strict format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeGuard
|
|
6
|
+
|
|
7
|
+
_EMPTY_SCHEMA = {
|
|
8
|
+
"additionalProperties": False,
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {},
|
|
11
|
+
"required": [],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
"""Ensure a JSON schema conforms to OpenAI's strict requirements.
|
|
17
|
+
|
|
18
|
+
This mutates the provided schema in-place and returns it for convenience.
|
|
19
|
+
"""
|
|
20
|
+
if schema == {}:
|
|
21
|
+
return _EMPTY_SCHEMA.copy()
|
|
22
|
+
return _ensure_strict_json_schema(schema, path=(), root=schema)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ensure_strict_json_schema(
|
|
26
|
+
json_schema: object,
|
|
27
|
+
*,
|
|
28
|
+
path: tuple[str, ...],
|
|
29
|
+
root: dict[str, Any],
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
if not _is_dict(json_schema):
|
|
32
|
+
raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")
|
|
33
|
+
|
|
34
|
+
defs = json_schema.get("$defs")
|
|
35
|
+
if _is_dict(defs):
|
|
36
|
+
for def_name, def_schema in defs.items():
|
|
37
|
+
_ensure_strict_json_schema(def_schema, path=(*path, "$defs", def_name), root=root)
|
|
38
|
+
|
|
39
|
+
definitions = json_schema.get("definitions")
|
|
40
|
+
if _is_dict(definitions):
|
|
41
|
+
for definition_name, definition_schema in definitions.items():
|
|
42
|
+
_ensure_strict_json_schema(
|
|
43
|
+
definition_schema, path=(*path, "definitions", definition_name), root=root
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
typ = json_schema.get("type")
|
|
47
|
+
if typ == "object":
|
|
48
|
+
if "additionalProperties" not in json_schema or json_schema["additionalProperties"] is True:
|
|
49
|
+
json_schema["additionalProperties"] = False
|
|
50
|
+
elif (
|
|
51
|
+
json_schema["additionalProperties"] and json_schema["additionalProperties"] is not False
|
|
52
|
+
):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"additionalProperties should not be set for object types in strict mode."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
properties = json_schema.get("properties")
|
|
58
|
+
if _is_dict(properties):
|
|
59
|
+
json_schema["required"] = list(properties.keys())
|
|
60
|
+
json_schema["properties"] = {
|
|
61
|
+
key: _ensure_strict_json_schema(prop_schema, path=(*path, "properties", key), root=root)
|
|
62
|
+
for key, prop_schema in properties.items()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
items = json_schema.get("items")
|
|
66
|
+
if _is_dict(items):
|
|
67
|
+
json_schema["items"] = _ensure_strict_json_schema(items, path=(*path, "items"), root=root)
|
|
68
|
+
|
|
69
|
+
prefix_items = json_schema.get("prefixItems")
|
|
70
|
+
if _is_list(prefix_items) and prefix_items:
|
|
71
|
+
item_types = set()
|
|
72
|
+
for item in prefix_items:
|
|
73
|
+
if _is_dict(item) and "type" in item:
|
|
74
|
+
item_types.add(item["type"])
|
|
75
|
+
|
|
76
|
+
if len(item_types) == 1:
|
|
77
|
+
item_type = item_types.pop()
|
|
78
|
+
json_schema["items"] = {"type": item_type}
|
|
79
|
+
else:
|
|
80
|
+
json_schema["items"] = {"type": "integer"}
|
|
81
|
+
|
|
82
|
+
tuple_length = len(prefix_items)
|
|
83
|
+
json_schema["minItems"] = tuple_length
|
|
84
|
+
json_schema["maxItems"] = tuple_length
|
|
85
|
+
json_schema.pop("prefixItems")
|
|
86
|
+
|
|
87
|
+
any_of = json_schema.get("anyOf")
|
|
88
|
+
if _is_list(any_of):
|
|
89
|
+
json_schema["anyOf"] = [
|
|
90
|
+
_ensure_strict_json_schema(variant, path=(*path, "anyOf", str(i)), root=root)
|
|
91
|
+
for i, variant in enumerate(any_of)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
one_of = json_schema.get("oneOf")
|
|
95
|
+
if _is_list(one_of):
|
|
96
|
+
existing_any_of = json_schema.get("anyOf", [])
|
|
97
|
+
if not _is_list(existing_any_of):
|
|
98
|
+
existing_any_of = []
|
|
99
|
+
json_schema["anyOf"] = existing_any_of + [
|
|
100
|
+
_ensure_strict_json_schema(variant, path=(*path, "oneOf", str(i)), root=root)
|
|
101
|
+
for i, variant in enumerate(one_of)
|
|
102
|
+
]
|
|
103
|
+
json_schema.pop("oneOf")
|
|
104
|
+
|
|
105
|
+
all_of = json_schema.get("allOf")
|
|
106
|
+
if _is_list(all_of):
|
|
107
|
+
if len(all_of) == 1:
|
|
108
|
+
json_schema.update(
|
|
109
|
+
_ensure_strict_json_schema(all_of[0], path=(*path, "allOf", "0"), root=root)
|
|
110
|
+
)
|
|
111
|
+
json_schema.pop("allOf")
|
|
112
|
+
else:
|
|
113
|
+
json_schema["allOf"] = [
|
|
114
|
+
_ensure_strict_json_schema(entry, path=(*path, "allOf", str(i)), root=root)
|
|
115
|
+
for i, entry in enumerate(all_of)
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
if "default" in json_schema:
|
|
119
|
+
json_schema.pop("default")
|
|
120
|
+
|
|
121
|
+
for keyword in ("title", "examples", "format"):
|
|
122
|
+
json_schema.pop(keyword, None)
|
|
123
|
+
|
|
124
|
+
ref = json_schema.get("$ref")
|
|
125
|
+
if ref and _has_more_than_n_keys(json_schema, 1):
|
|
126
|
+
if not isinstance(ref, str):
|
|
127
|
+
raise ValueError(f"Received non-string $ref - {ref}")
|
|
128
|
+
resolved = _resolve_ref(root=root, ref=ref)
|
|
129
|
+
if not _is_dict(resolved):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Expected `$ref: {ref}` to resolve to a dictionary but got {resolved}"
|
|
132
|
+
)
|
|
133
|
+
json_schema.update({**resolved, **json_schema})
|
|
134
|
+
json_schema.pop("$ref")
|
|
135
|
+
return _ensure_strict_json_schema(json_schema, path=path, root=root)
|
|
136
|
+
|
|
137
|
+
return json_schema
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resolve_ref(*, root: dict[str, Any], ref: str) -> object:
|
|
141
|
+
if not ref.startswith("#/"):
|
|
142
|
+
raise ValueError(f"Unexpected $ref format {ref!r}; does not start with #/")
|
|
143
|
+
|
|
144
|
+
path = ref[2:].split("/")
|
|
145
|
+
resolved: object = root
|
|
146
|
+
for key in path:
|
|
147
|
+
assert _is_dict(resolved), f"Encountered non-dictionary entry while resolving {ref}"
|
|
148
|
+
resolved = resolved[key]
|
|
149
|
+
|
|
150
|
+
return resolved
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_dict(obj: object) -> TypeGuard[dict[str, Any]]:
|
|
154
|
+
return isinstance(obj, dict)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _is_list(obj: object) -> TypeGuard[list[object]]:
|
|
158
|
+
return isinstance(obj, list)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
|
|
162
|
+
return any(count > n for count, _ in enumerate(obj, start=1))
|
hud/utils/tests/test_init.py
CHANGED
hud/utils/tests/test_mcp.py
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
from hud.utils.mcp import MCPConfigPatch, patch_mcp_config, setup_hud_telemetry
|
|
5
|
+
from hud.utils.mcp import MCPConfigPatch, patch_mcp_config
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
class TestPatchMCPConfig:
|
|
@@ -85,28 +83,3 @@ class TestPatchMCPConfig:
|
|
|
85
83
|
# Existing meta should be preserved, new one added
|
|
86
84
|
assert mcp_config["test_server"]["meta"]["existing_key"] == "existing_value"
|
|
87
85
|
assert mcp_config["test_server"]["meta"]["test_key"] == "test_value"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class TestSetupHUDTelemetry:
|
|
91
|
-
"""Tests for setup_hud_telemetry function."""
|
|
92
|
-
|
|
93
|
-
def test_empty_config_raises_error(self):
|
|
94
|
-
"""Test that empty config raises ValueError."""
|
|
95
|
-
with pytest.raises(
|
|
96
|
-
ValueError, match="Please run initialize\\(\\) before setting up client-side telemetry"
|
|
97
|
-
):
|
|
98
|
-
setup_hud_telemetry({})
|
|
99
|
-
|
|
100
|
-
def test_none_config_raises_error(self):
|
|
101
|
-
"""Test that None config raises ValueError."""
|
|
102
|
-
with pytest.raises(
|
|
103
|
-
ValueError, match="Please run initialize\\(\\) before setting up client-side telemetry"
|
|
104
|
-
):
|
|
105
|
-
setup_hud_telemetry(None) # type: ignore[arg-type]
|
|
106
|
-
|
|
107
|
-
def test_valid_config_returns_none_when_no_hud_servers(self):
|
|
108
|
-
"""Test that valid config with no HUD servers returns None."""
|
|
109
|
-
mcp_config = {"test_server": {"url": "http://example.com"}}
|
|
110
|
-
|
|
111
|
-
result = setup_hud_telemetry(mcp_config)
|
|
112
|
-
assert result is None
|