Skip to content

Commit 13a87fb

Browse files
authored
Merge pull request #263 from hud-evals/l/def-gateway
L/def gateway
2 parents 227ea8a + 271a140 commit 13a87fb

File tree

10 files changed

+103
-33
lines changed

10 files changed

+103
-33
lines changed

docs/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"icon": "code",
3434
"versions": [
3535
{
36-
"version": "0.5.3",
36+
"version": "0.5.4",
3737
"groups": [
3838
{
3939
"group": "Get Started",

hud/agents/claude.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,18 @@ def __init__(self, params: ClaudeCreateParams | None = None, **kwargs: Any) -> N
7676

7777
model_client = self.config.model_client
7878
if model_client is None:
79-
api_key = settings.anthropic_api_key
80-
if not api_key:
81-
raise ValueError("Anthropic API key not found. Set ANTHROPIC_API_KEY.")
82-
model_client = AsyncAnthropic(api_key=api_key)
79+
# Default to HUD gateway when HUD_API_KEY is available
80+
if settings.api_key:
81+
from hud.agents.gateway import build_gateway_client
82+
83+
model_client = build_gateway_client("anthropic")
84+
elif settings.anthropic_api_key:
85+
model_client = AsyncAnthropic(api_key=settings.anthropic_api_key)
86+
else:
87+
raise ValueError(
88+
"No API key found. Set HUD_API_KEY for HUD gateway, "
89+
"or ANTHROPIC_API_KEY for direct Anthropic access."
90+
)
8391

8492
self.anthropic_client = model_client
8593
self.max_tokens = self.config.max_tokens

hud/agents/gemini.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,18 @@ def __init__(self, params: GeminiCreateParams | None = None, **kwargs: Any) -> N
6161

6262
model_client = self.config.model_client
6363
if model_client is None:
64-
api_key = settings.gemini_api_key
65-
if not api_key:
66-
raise ValueError("Gemini API key not found. Set GEMINI_API_KEY.")
67-
model_client = genai.Client(api_key=api_key)
64+
# Default to HUD gateway when HUD_API_KEY is available
65+
if settings.api_key:
66+
from hud.agents.gateway import build_gateway_client
67+
68+
model_client = build_gateway_client("gemini")
69+
elif settings.gemini_api_key:
70+
model_client = genai.Client(api_key=settings.gemini_api_key)
71+
else:
72+
raise ValueError(
73+
"No API key found. Set HUD_API_KEY for HUD gateway, "
74+
"or GEMINI_API_KEY for direct Gemini access."
75+
)
6876

6977
if self.config.validate_api_key:
7078
try:

hud/agents/openai.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,18 @@ def __init__(self, params: OpenAICreateParams | None = None, **kwargs: Any) -> N
7979

8080
model_client = self.config.model_client
8181
if model_client is None:
82-
api_key = settings.openai_api_key
83-
if not api_key:
84-
raise ValueError("OpenAI API key not found. Set OPENAI_API_KEY.")
85-
model_client = AsyncOpenAI(api_key=api_key)
82+
# Default to HUD gateway when HUD_API_KEY is available
83+
if settings.api_key:
84+
from hud.agents.gateway import build_gateway_client
85+
86+
model_client = build_gateway_client("openai")
87+
elif settings.openai_api_key:
88+
model_client = AsyncOpenAI(api_key=settings.openai_api_key)
89+
else:
90+
raise ValueError(
91+
"No API key found. Set HUD_API_KEY for HUD gateway, "
92+
"or OPENAI_API_KEY for direct OpenAI access."
93+
)
8694

8795
if self.config.validate_api_key:
8896
try:

hud/agents/tests/test_openai.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,9 @@ async def test_init_with_parameters(self, mock_openai: AsyncOpenAI) -> None:
128128
async def test_init_without_client_no_api_key(self) -> None:
129129
"""Test agent initialization fails without API key."""
130130
with patch("hud.agents.openai.settings") as mock_settings:
131+
mock_settings.api_key = None
131132
mock_settings.openai_api_key = None
132-
with pytest.raises(ValueError, match="OpenAI API key not found"):
133+
with pytest.raises(ValueError, match="No API key found"):
133134
OpenAIAgent.create()
134135

135136
@pytest.mark.asyncio

hud/environment/environment.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def __init__(
129129
super().__init__(name=name, instructions=instructions, **fastmcp_kwargs)
130130
self._connections: dict[str, Connector] = {}
131131
self._router = ToolRouter(conflict_resolution=conflict_resolution)
132+
self._routing_built = False # Track if _build_routing has been called
132133
self._in_context = False
133134

134135
# Tool call queues - run after connections established
@@ -361,6 +362,7 @@ async def __aexit__(
361362
if self._connections:
362363
await asyncio.gather(*[c.disconnect() for c in self._connections.values()])
363364
self._router.clear()
365+
self._routing_built = False
364366

365367
async def run_async(
366368
self,
@@ -389,6 +391,7 @@ async def _build_routing(self) -> None:
389391
connections=self._connections,
390392
connection_order=list(self._connections.keys()),
391393
)
394+
self._routing_built = True
392395
# Populate mock schemas for auto-generated mock values
393396
self._populate_mock_schemas()
394397

@@ -406,6 +409,8 @@ def _setup_handlers(self) -> None:
406409

407410
async def _env_list_tools(self) -> list[mcp_types.Tool]:
408411
"""Return all tools including those from connectors."""
412+
if not self._routing_built:
413+
await self._build_routing()
409414
return self._router.tools
410415

411416
async def _env_call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> list[Any]:

hud/environment/scenarios.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,23 @@ async def run_scenario_setup(self, scenario_name: str, args: dict[str, Any]) ->
199199
except Exception:
200200
available = "(could not fetch available scenarios)"
201201

202+
# Check if the prompt exists - if so, the error is something else
203+
original_error = str(e)
204+
if prompt_id in scenario_prompts:
205+
# Prompt exists but get_prompt failed for another reason
206+
raise ValueError(
207+
f"⚠️ ERROR: Scenario '{prompt_id}' exists but failed to execute.\n\n"
208+
f"The scenario was found but encountered an error during setup:\n"
209+
f" {original_error}\n\n"
210+
f"This could be caused by:\n"
211+
f" - Missing or invalid scenario arguments\n"
212+
f" - An error in the scenario's setup function\n"
213+
f" - Connection or serialization issues\n\n"
214+
f"Check the scenario definition and required arguments."
215+
) from e
216+
202217
raise ValueError(
203-
f"Scenario not found.\n\n"
218+
f"⚠️ ERROR: Scenario not found.\n\n"
204219
f"Scenario IDs have the format 'environment_name:scenario_name'.\n"
205220
f"If you only specify 'scenario_name', the SDK uses your task's env name "
206221
f"as the prefix.\n"
@@ -362,7 +377,7 @@ def decorator(
362377
# Only include JSON-serializable defaults
363378
default_val = p.default
364379
if default_val is None or isinstance(
365-
default_val, (str, int, float, bool, list, dict)
380+
default_val, (str | int | float | bool | list | dict)
366381
):
367382
arg_info["default"] = default_val
368383

@@ -413,26 +428,51 @@ async def prompt_handler(**handler_args: Any) -> list[str]:
413428

414429
# Deserialize JSON-encoded arguments using Pydantic TypeAdapter
415430
# This properly handles: Pydantic models, enums, datetime, lists, dicts
431+
# MCP prompts only support string arguments, so we JSON-serialize complex
432+
# types on the sending side and deserialize them here
416433
deserialized_args: dict[str, Any] = {}
417434
for arg_name, arg_value in handler_args.items():
418435
annotation = param_annotations.get(arg_name)
419-
if (
420-
annotation is not None
421-
and annotation is not str
422-
and isinstance(arg_value, str)
423-
):
424-
# Try TypeAdapter.validate_json for proper type coercion
436+
437+
# Only attempt deserialization on string values
438+
if not isinstance(arg_value, str):
439+
deserialized_args[arg_name] = arg_value
440+
continue
441+
442+
# If annotation is explicitly str, keep as string (no deserialization)
443+
if annotation is str:
444+
deserialized_args[arg_name] = arg_value
445+
continue
446+
447+
# If we have a non-str type annotation, use TypeAdapter
448+
if annotation is not None:
425449
try:
426450
adapter = TypeAdapter(annotation)
427451
deserialized_args[arg_name] = adapter.validate_json(arg_value)
428-
except Exception:
429-
# Fall back to plain json.loads if TypeAdapter fails
430-
try:
431-
deserialized_args[arg_name] = json.loads(arg_value)
432-
except json.JSONDecodeError:
433-
deserialized_args[arg_name] = arg_value
434-
else:
435-
deserialized_args[arg_name] = arg_value
452+
continue
453+
except Exception: # noqa: S110
454+
pass # Fall through to generic JSON decode
455+
456+
# No type annotation - try JSON decode for strings that look like JSON
457+
# (arrays, objects, numbers, booleans, null)
458+
stripped = arg_value.strip()
459+
if (stripped and stripped[0] in "[{") or stripped in ("true", "false", "null"):
460+
try:
461+
deserialized_args[arg_name] = json.loads(arg_value)
462+
continue
463+
except json.JSONDecodeError:
464+
pass # Keep as string
465+
466+
# Also try to decode if it looks like a number
467+
if stripped.lstrip("-").replace(".", "", 1).isdigit():
468+
try:
469+
deserialized_args[arg_name] = json.loads(arg_value)
470+
continue
471+
except json.JSONDecodeError:
472+
pass
473+
474+
# Keep as string
475+
deserialized_args[arg_name] = arg_value
436476

437477
# Create generator instance with deserialized args
438478
gen = scenario_fn(**deserialized_args)

hud/utils/tests/test_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ def test_import():
55
"""Test that the package can be imported."""
66
import hud
77

8-
assert hud.__version__ == "0.5.3"
8+
assert hud.__version__ == "0.5.4"

hud/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
from __future__ import annotations
66

7-
__version__ = "0.5.3"
7+
__version__ = "0.5.4"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "hud-python"
3-
version = "0.5.3"
3+
version = "0.5.4"
44
description = "SDK for the HUD platform."
55
readme = "README.md"
66
requires-python = ">=3.11, <3.13"

0 commit comments

Comments
 (0)