hud-python 0.4.20__py3-none-any.whl → 0.4.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (54) hide show
  1. hud/__init__.py +7 -0
  2. hud/agents/base.py +42 -10
  3. hud/agents/claude.py +24 -14
  4. hud/agents/grounded_openai.py +280 -0
  5. hud/agents/tests/test_client.py +11 -27
  6. hud/agents/tests/test_grounded_openai_agent.py +155 -0
  7. hud/cli/__init__.py +50 -20
  8. hud/cli/build.py +3 -44
  9. hud/cli/eval.py +25 -6
  10. hud/cli/init.py +4 -4
  11. hud/cli/push.py +3 -1
  12. hud/cli/tests/test_push.py +6 -6
  13. hud/cli/utils/interactive.py +1 -1
  14. hud/clients/__init__.py +3 -2
  15. hud/clients/base.py +20 -9
  16. hud/clients/mcp_use.py +44 -22
  17. hud/datasets/task.py +6 -2
  18. hud/native/__init__.py +6 -0
  19. hud/native/comparator.py +546 -0
  20. hud/native/tests/__init__.py +1 -0
  21. hud/native/tests/test_comparator.py +539 -0
  22. hud/native/tests/test_native_init.py +79 -0
  23. hud/otel/instrumentation.py +0 -2
  24. hud/server/server.py +9 -2
  25. hud/settings.py +6 -0
  26. hud/shared/exceptions.py +204 -31
  27. hud/shared/hints.py +177 -0
  28. hud/shared/requests.py +15 -3
  29. hud/shared/tests/test_exceptions.py +385 -144
  30. hud/tools/__init__.py +2 -0
  31. hud/tools/executors/tests/test_base_executor.py +1 -1
  32. hud/tools/executors/xdo.py +1 -1
  33. hud/tools/grounding/__init__.py +13 -0
  34. hud/tools/grounding/config.py +54 -0
  35. hud/tools/grounding/grounded_tool.py +314 -0
  36. hud/tools/grounding/grounder.py +301 -0
  37. hud/tools/grounding/tests/__init__.py +1 -0
  38. hud/tools/grounding/tests/test_grounded_tool.py +196 -0
  39. hud/tools/submit.py +66 -0
  40. hud/tools/tests/test_playwright_tool.py +1 -1
  41. hud/tools/tests/test_tools_init.py +1 -1
  42. hud/tools/tests/test_utils.py +2 -2
  43. hud/types.py +33 -5
  44. hud/utils/agent_factories.py +86 -0
  45. hud/utils/design.py +57 -0
  46. hud/utils/mcp.py +6 -0
  47. hud/utils/pretty_errors.py +68 -0
  48. hud/utils/tests/test_version.py +1 -1
  49. hud/version.py +1 -1
  50. {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/METADATA +2 -4
  51. {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/RECORD +54 -37
  52. {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/WHEEL +0 -0
  53. {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/entry_points.txt +0 -0
  54. {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/licenses/LICENSE +0 -0
hud/shared/exceptions.py CHANGED
@@ -1,36 +1,179 @@
1
+ """HUD SDK Exception System.
2
+
3
+ This module provides intelligent exception handling with automatic error
4
+ classification and helpful hints for users.
5
+
6
+ Key Features:
7
+ - Auto-converts generic exceptions to specific HUD exceptions
8
+ - Attaches contextual hints based on error type
9
+ - Clean chaining syntax: raise HudException() from e
10
+
11
+ Example:
12
+ try:
13
+ client.call_tool("missing")
14
+ except Exception as e:
15
+ raise HudException() from e # Becomes HudToolNotFoundError with hints
16
+ """
17
+
1
18
  from __future__ import annotations
2
19
 
20
+ import asyncio
21
+ import json
3
22
  import logging
4
- from typing import TYPE_CHECKING, Any
23
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
5
24
 
6
25
  if TYPE_CHECKING:
7
26
  from typing import Self
8
27
 
9
28
  import httpx
10
29
 
30
+ from hud.shared.hints import (
31
+ CLIENT_NOT_INITIALIZED,
32
+ ENV_VAR_MISSING,
33
+ HUD_API_KEY_MISSING,
34
+ INVALID_CONFIG,
35
+ MCP_SERVER_ERROR,
36
+ RATE_LIMIT_HIT,
37
+ TOOL_NOT_FOUND,
38
+ Hint,
39
+ )
40
+
41
+ T = TypeVar("T", bound="HudException")
42
+
11
43
  logger = logging.getLogger(__name__)
12
44
 
13
45
 
14
46
  class HudException(Exception):
15
47
  """Base exception class for all HUD SDK errors.
16
48
 
17
- This is the parent class for all exceptions raised by the HUD SDK.
18
- Consumers should be able to catch this exception to handle any HUD-related error.
49
+ Usage:
50
+ raise HudException() from e # Auto-converts to appropriate subclass
51
+ raise HudException("Custom message") from e # With custom message
19
52
  """
20
53
 
21
- def __init__(self, message: str, response_json: dict[str, Any] | None = None) -> None:
22
- super().__init__(message)
23
- self.message = message
54
+ def __new__(cls, message: str = "", *args: Any, **kwargs: Any) -> Any:
55
+ """Auto-convert generic exceptions to specific HUD exceptions when chained."""
56
+ import sys
57
+
58
+ # Only intercept for base HudException, not subclasses
59
+ if cls is not HudException:
60
+ return super().__new__(cls)
61
+
62
+ # Check if we're in a 'raise...from' context
63
+ exc_type, exc_value, _ = sys.exc_info()
64
+ if exc_type and exc_value:
65
+ # If it's already a HudException, return it as-is
66
+ if isinstance(exc_value, HudException):
67
+ return exc_value
68
+ # Otherwise analyze if it's a regular Exception
69
+ elif isinstance(exc_value, Exception):
70
+ # Try to convert to a specific HudException
71
+ result = cls._analyze_exception(exc_value, message or str(exc_value))
72
+ # If we couldn't categorize it (still base HudException),
73
+ # just re-raise the original exception
74
+ if type(result) is HudException:
75
+ # Re-raise the original exception unchanged
76
+ raise exc_value from None
77
+ return result
78
+
79
+ # Normal creation
80
+ return super().__new__(cls)
81
+
82
+ # Subclasses can override this class attribute
83
+ default_hints: ClassVar[list[Hint]] = []
84
+
85
+ def __init__(
86
+ self,
87
+ message: str = "",
88
+ response_json: dict[str, Any] | None = None,
89
+ *,
90
+ hints: list[Hint] | None = None,
91
+ ) -> None:
92
+ # If we already have args set (from _analyze_exception), don't override them
93
+ if not self.args:
94
+ # Pass the message to the base Exception class
95
+ super().__init__(message)
96
+ self.message = message or (self.args[0] if self.args else "")
24
97
  self.response_json = response_json
98
+ # If hints not provided, use defaults defined by subclass
99
+ self.hints: list[Hint] = hints if hints is not None else list(self.default_hints)
25
100
 
26
101
  def __str__(self) -> str:
27
- parts = [self.message]
102
+ # Get the message from the exception
103
+ # First check if we have args (standard Exception message storage)
104
+ msg = str(self.args[0]) if self.args and self.args[0] else ""
105
+
106
+ # Add response JSON if available
28
107
  if self.response_json:
29
- parts.append(f"Response: {self.response_json}")
30
- return " | ".join(parts)
108
+ if msg:
109
+ return f"{msg} | Response: {self.response_json}"
110
+ else:
111
+ return f"Response: {self.response_json}"
31
112
 
113
+ return msg
32
114
 
33
- class HudRequestError(Exception):
115
+ @classmethod
116
+ def _analyze_exception(cls, e: Exception, message: str = "") -> HudException:
117
+ """Convert generic exceptions to specific HUD exceptions based on content."""
118
+ error_msg = str(e).lower()
119
+ final_msg = message or str(e)
120
+
121
+ # Map error patterns to exception types
122
+ patterns = [
123
+ # (condition_func, exception_class)
124
+ (
125
+ lambda: "not initialized" in error_msg or "not connected" in error_msg,
126
+ HudClientError,
127
+ ),
128
+ (
129
+ lambda: "invalid json" in error_msg or "config" in error_msg or "json" in error_msg,
130
+ HudConfigError,
131
+ ),
132
+ (
133
+ lambda: "tool" in error_msg
134
+ and ("not found" in error_msg or "not exist" in error_msg),
135
+ HudToolNotFoundError,
136
+ ),
137
+ (
138
+ lambda: ("api key" in error_msg or "authorization" in error_msg)
139
+ and ("hud" in error_msg or "mcp.hud.so" in error_msg),
140
+ HudAuthenticationError,
141
+ ),
142
+ (
143
+ lambda: "rate limit" in error_msg or "too many request" in error_msg,
144
+ HudRateLimitError,
145
+ ),
146
+ (lambda: isinstance(e, (TimeoutError | asyncio.TimeoutError)), HudTimeoutError),
147
+ (lambda: isinstance(e, json.JSONDecodeError), HudConfigError),
148
+ (
149
+ lambda: "environment variable" in error_msg and "required" in error_msg,
150
+ HudEnvVarError,
151
+ ),
152
+ (lambda: "event loop" in error_msg and "closed" in error_msg, HudClientError),
153
+ (
154
+ lambda: type(e).__name__ == "McpError", # Check by name to avoid import issues
155
+ HudMCPError,
156
+ ),
157
+ ]
158
+
159
+ # Find first matching pattern
160
+ for condition, exception_class in patterns:
161
+ if condition():
162
+ # Create instance directly using Exception.__new__ to bypass our custom __new__
163
+ instance = Exception.__new__(exception_class)
164
+ # Manually set args before calling __init__ to ensure proper Exception behavior
165
+ instance.args = (final_msg,)
166
+ instance.__init__(final_msg)
167
+ return instance
168
+
169
+ # No pattern matched - return base exception instance
170
+ instance = Exception.__new__(HudException)
171
+ instance.args = (final_msg,)
172
+ instance.__init__(final_msg)
173
+ return instance
174
+
175
+
176
+ class HudRequestError(HudException):
34
177
  """Any request to the HUD API can raise this exception."""
35
178
 
36
179
  def __init__(
@@ -40,13 +183,24 @@ class HudRequestError(Exception):
40
183
  response_text: str | None = None,
41
184
  response_json: dict[str, Any] | None = None,
42
185
  response_headers: dict[str, str] | None = None,
186
+ *,
187
+ hints: list[Hint] | None = None,
43
188
  ) -> None:
44
- self.message = message
45
189
  self.status_code = status_code
46
190
  self.response_text = response_text
47
- self.response_json = response_json
48
191
  self.response_headers = response_headers
49
- super().__init__(message)
192
+ # Compute default hints from status code if none provided
193
+ if hints is None and status_code in (401, 403, 429):
194
+ try:
195
+ from hud.shared.hints import HUD_API_KEY_MISSING, RATE_LIMIT_HIT # type: ignore
196
+
197
+ if status_code in (401, 403):
198
+ hints = [HUD_API_KEY_MISSING]
199
+ elif status_code == 429:
200
+ hints = [RATE_LIMIT_HIT]
201
+ except Exception as import_error:
202
+ logger.debug("Failed to attach structured hints: %s", import_error)
203
+ super().__init__(message, response_json, hints=hints)
50
204
 
51
205
  def __str__(self) -> str:
52
206
  parts = [self.message]
@@ -110,13 +264,14 @@ class HudRequestError(Exception):
110
264
  response_text[:500],
111
265
  "..." if len(response_text) > 500 else "",
112
266
  )
113
- return cls(
267
+ inst = cls(
114
268
  message=message,
115
269
  status_code=status_code,
116
270
  response_text=response_text,
117
271
  response_json=response_json,
118
272
  response_headers=response_headers,
119
273
  )
274
+ return inst
120
275
 
121
276
 
122
277
  class HudResponseError(HudException):
@@ -148,35 +303,53 @@ class HudResponseError(HudException):
148
303
 
149
304
 
150
305
  class HudAuthenticationError(HudException):
151
- """Raised when authentication with the HUD API fails.
306
+ """Missing or invalid HUD API key."""
152
307
 
153
- This exception is raised when an API key is missing, invalid, or
154
- has insufficient permissions for the requested operation.
155
- """
308
+ default_hints: ClassVar[list[Hint]] = [HUD_API_KEY_MISSING]
156
309
 
157
310
 
158
311
  class HudRateLimitError(HudException):
159
- """Raised when the rate limit for the HUD API is exceeded.
312
+ """Too many requests to the API."""
160
313
 
161
- This exception is raised when too many requests are made in a
162
- short period of time.
163
- """
314
+ default_hints: ClassVar[list[Hint]] = [RATE_LIMIT_HIT]
164
315
 
165
316
 
166
317
  class HudTimeoutError(HudException):
167
- """Raised when a request to the HUD API times out.
168
-
169
- This exception is raised when a request takes longer than the
170
- configured timeout period.
171
- """
318
+ """Request timed out."""
172
319
 
173
320
 
174
321
  class HudNetworkError(HudException):
175
- """Raised when there is a network-related error.
322
+ """Network connection issue."""
176
323
 
177
- This exception is raised when there are issues with the network
178
- connection, DNS resolution, or other network-related problems.
179
- """
324
+
325
+ class HudClientError(HudException):
326
+ """MCP client not initialized."""
327
+
328
+ default_hints: ClassVar[list[Hint]] = [CLIENT_NOT_INITIALIZED]
329
+
330
+
331
+ class HudConfigError(HudException):
332
+ """Invalid or missing configuration."""
333
+
334
+ default_hints: ClassVar[list[Hint]] = [INVALID_CONFIG]
335
+
336
+
337
+ class HudEnvVarError(HudException):
338
+ """Missing required environment variables."""
339
+
340
+ default_hints: ClassVar[list[Hint]] = [ENV_VAR_MISSING]
341
+
342
+
343
+ class HudToolNotFoundError(HudException):
344
+ """Requested tool not found."""
345
+
346
+ default_hints: ClassVar[list[Hint]] = [TOOL_NOT_FOUND]
347
+
348
+
349
+ class HudMCPError(HudException):
350
+ """MCP protocol or server error."""
351
+
352
+ default_hints: ClassVar[list[Hint]] = [MCP_SERVER_ERROR]
180
353
 
181
354
 
182
355
  class GymMakeException(HudException):
hud/shared/hints.py ADDED
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterable
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class Hint:
14
+ """Structured hint for user guidance.
15
+
16
+ Attributes:
17
+ title: Short title describing the hint.
18
+ message: Main explanatory message.
19
+ tips: Optional list of short actionable tips.
20
+ docs_url: Optional URL for documentation.
21
+ command_examples: Optional list of command examples to show.
22
+ code: Optional machine-readable code (e.g., "AUTH_API_KEY_MISSING").
23
+ context: Optional context tags (e.g., ["auth", "docker", "mcp"]).
24
+ """
25
+
26
+ title: str
27
+ message: str
28
+ tips: list[str] | None = None
29
+ docs_url: str | None = None
30
+ command_examples: list[str] | None = None
31
+ code: str | None = None
32
+ context: list[str] | None = None
33
+
34
+
35
+ # Common, reusable hints
36
+ HUD_API_KEY_MISSING = Hint(
37
+ title="HUD API key required",
38
+ message="Missing or invalid HUD_API_KEY.",
39
+ tips=[
40
+ "Set HUD_API_KEY environment variable",
41
+ "Get a key at https://app.hud.so",
42
+ "Check for whitespace or truncation",
43
+ ],
44
+ docs_url=None,
45
+ command_examples=None,
46
+ code="HUD_AUTH_MISSING",
47
+ context=["auth", "hud"],
48
+ )
49
+
50
+ RATE_LIMIT_HIT = Hint(
51
+ title="Rate limit reached",
52
+ message="Too many requests.",
53
+ tips=[
54
+ "Lower --max-concurrent",
55
+ "Add retry delay",
56
+ "Check API quotas",
57
+ ],
58
+ docs_url=None,
59
+ command_examples=None,
60
+ code="RATE_LIMIT",
61
+ context=["network"],
62
+ )
63
+
64
+ TOOL_NOT_FOUND = Hint(
65
+ title="Tool not found",
66
+ message="Requested tool doesn't exist.",
67
+ tips=[
68
+ "Check tool name spelling",
69
+ "Run: hud analyze --live <image>",
70
+ "Verify server implements tool",
71
+ ],
72
+ docs_url=None,
73
+ command_examples=None,
74
+ code="TOOL_NOT_FOUND",
75
+ context=["mcp", "tools"],
76
+ )
77
+
78
+ CLIENT_NOT_INITIALIZED = Hint(
79
+ title="Client not initialized",
80
+ message="MCP client must be initialized before use.",
81
+ tips=[
82
+ "Call client.initialize() first",
83
+ "Or use async with client:",
84
+ "Check connection succeeded",
85
+ ],
86
+ docs_url=None,
87
+ command_examples=None,
88
+ code="CLIENT_NOT_INIT",
89
+ context=["mcp", "client"],
90
+ )
91
+
92
+ INVALID_CONFIG = Hint(
93
+ title="Invalid configuration",
94
+ message="Configuration is missing or malformed.",
95
+ tips=[
96
+ "Check JSON syntax",
97
+ "Verify required fields",
98
+ "See examples in docs",
99
+ ],
100
+ docs_url=None,
101
+ command_examples=None,
102
+ code="INVALID_CONFIG",
103
+ context=["config"],
104
+ )
105
+
106
+ ENV_VAR_MISSING = Hint(
107
+ title="Environment variable required",
108
+ message="Required environment variables are missing.",
109
+ tips=[
110
+ "Set required environment variables",
111
+ "Use -e flag: hud build . -e VAR_NAME=value",
112
+ "Check Dockerfile for ENV requirements",
113
+ "Run hud debug . --build for detailed logs",
114
+ ],
115
+ docs_url=None,
116
+ command_examples=["hud build . -e BROWSER_PROVIDER=anchorbrowser"],
117
+ code="ENV_VAR_MISSING",
118
+ context=["env", "config"],
119
+ )
120
+
121
+ MCP_SERVER_ERROR = Hint(
122
+ title="MCP server error",
123
+ message="The MCP server encountered an error.",
124
+ tips=[
125
+ "Check server logs for details",
126
+ "Verify server configuration",
127
+ "Ensure all dependencies are installed",
128
+ "Run hud debug to see detailed output",
129
+ ],
130
+ docs_url=None,
131
+ command_examples=["hud debug", "hud dev --verbose"],
132
+ code="MCP_SERVER_ERROR",
133
+ context=["mcp", "server"],
134
+ )
135
+
136
+
137
+ def render_hints(hints: Iterable[Hint] | None, *, design: Any | None = None) -> None:
138
+ """Render a collection of hints using the HUD design system if available.
139
+
140
+ If design is not provided, this is a no-op to keep library use headless.
141
+ """
142
+ if not hints:
143
+ return
144
+
145
+ try:
146
+ if design is None:
147
+ from hud.utils.design import design as default_design # lazy import
148
+
149
+ design = default_design
150
+ except Exception:
151
+ # If design is unavailable (non-CLI contexts), silently skip rendering
152
+ return
153
+
154
+ for hint in hints:
155
+ try:
156
+ # Compact rendering - skip title if same as message
157
+ if hint.title and hint.title != hint.message:
158
+ design.warning(f"{hint.title}: {hint.message}")
159
+ else:
160
+ design.warning(hint.message)
161
+
162
+ # Tips as bullet points
163
+ if hint.tips:
164
+ for tip in hint.tips:
165
+ design.info(f" • {tip}")
166
+
167
+ # Only show command examples if provided
168
+ if hint.command_examples:
169
+ for cmd in hint.command_examples:
170
+ design.command_example(cmd)
171
+
172
+ # Only show docs URL if provided
173
+ if hint.docs_url:
174
+ design.link(hint.docs_url)
175
+ except Exception:
176
+ logger.warning("Failed to render hint: %s", hint)
177
+ continue
hud/shared/requests.py CHANGED
@@ -18,6 +18,7 @@ from hud.shared.exceptions import (
18
18
  HudRequestError,
19
19
  HudTimeoutError,
20
20
  )
21
+ from hud.shared.hints import HUD_API_KEY_MISSING, RATE_LIMIT_HIT
21
22
 
22
23
  # Set up logger
23
24
  logger = logging.getLogger("hud.http")
@@ -97,7 +98,10 @@ async def make_request(
97
98
  HudTimeoutError: If the request times out.
98
99
  """
99
100
  if not api_key:
100
- raise HudAuthenticationError("API key is required but not provided")
101
+ raise HudAuthenticationError(
102
+ "API key is required but not provided",
103
+ hints=[HUD_API_KEY_MISSING],
104
+ )
101
105
 
102
106
  headers = {"Authorization": f"Bearer {api_key}"}
103
107
  retry_status_codes = [502, 503, 504]
@@ -132,7 +136,11 @@ async def make_request(
132
136
  except httpx.TimeoutException as e:
133
137
  raise HudTimeoutError(f"Request timed out: {e!s}") from None
134
138
  except httpx.HTTPStatusError as e:
135
- raise HudRequestError.from_httpx_error(e) from None
139
+ err = HudRequestError.from_httpx_error(e)
140
+ if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
141
+ logger.debug("Attaching RATE_LIMIT hint to 429 error")
142
+ err.hints.append(RATE_LIMIT_HIT)
143
+ raise err from None
136
144
  except httpx.RequestError as e:
137
145
  if attempt <= max_retries:
138
146
  await _handle_retry(
@@ -225,7 +233,11 @@ def make_request_sync(
225
233
  except httpx.TimeoutException as e:
226
234
  raise HudTimeoutError(f"Request timed out: {e!s}") from None
227
235
  except httpx.HTTPStatusError as e:
228
- raise HudRequestError.from_httpx_error(e) from None
236
+ err = HudRequestError.from_httpx_error(e)
237
+ if getattr(err, "status_code", None) == 429 and RATE_LIMIT_HIT not in err.hints:
238
+ logger.debug("Attaching RATE_LIMIT hint to 429 error")
239
+ err.hints.append(RATE_LIMIT_HIT)
240
+ raise err from None
229
241
  except httpx.RequestError as e:
230
242
  if attempt <= max_retries:
231
243
  retry_time = retry_delay * (2 ** (attempt - 1))