hud-python 0.4.1__py3-none-any.whl → 0.4.3__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.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
"""Generic OpenAI chat-completions agent.
|
|
2
|
-
|
|
3
|
-
This class provides the minimal glue required to connect any endpoint that
|
|
4
|
-
implements the OpenAI compatible *chat.completions* API with MCP tool calling
|
|
5
|
-
through the existing :class:`hud.agent.MCPAgent` scaffolding.
|
|
6
|
-
|
|
7
|
-
Key points:
|
|
8
|
-
- Stateless, no special server-side conversation state is assumed.
|
|
9
|
-
- Accepts an :class:`openai.AsyncOpenAI` client, caller can supply their own
|
|
10
|
-
base_url / api_key (e.g. ART, llama.cpp, together.ai, …)
|
|
11
|
-
- All HUD features (step_count, OTel spans, tool filtering, screenshots, …)
|
|
12
|
-
come from the ``MCPAgent`` base class, we only implement the three abstract
|
|
13
|
-
methods
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import logging
|
|
20
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
21
|
-
|
|
22
|
-
import mcp.types as types
|
|
23
|
-
|
|
24
|
-
from hud.types import AgentResponse, MCPToolCall, MCPToolResult
|
|
25
|
-
|
|
26
|
-
from .base import MCPAgent
|
|
27
|
-
|
|
28
|
-
if TYPE_CHECKING:
|
|
29
|
-
from openai import AsyncOpenAI
|
|
30
|
-
from openai.types.chat import ChatCompletionToolParam
|
|
31
|
-
|
|
32
|
-
from hud.clients import AgentMCPClient
|
|
33
|
-
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class GenericOpenAIChatAgent(MCPAgent):
|
|
38
|
-
"""MCP-enabled agent that speaks the OpenAI *chat.completions* protocol."""
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
mcp_client: AgentMCPClient,
|
|
43
|
-
*,
|
|
44
|
-
openai_client: AsyncOpenAI,
|
|
45
|
-
model_name: str = "gpt-4o-mini",
|
|
46
|
-
parallel_tool_calls: bool = False,
|
|
47
|
-
logprobs: bool = False,
|
|
48
|
-
**agent_kwargs: Any,
|
|
49
|
-
) -> None:
|
|
50
|
-
super().__init__(mcp_client=mcp_client, **agent_kwargs)
|
|
51
|
-
self.oai = openai_client
|
|
52
|
-
self.model_name = model_name
|
|
53
|
-
self.parallel_tool_calls = parallel_tool_calls
|
|
54
|
-
self.logprobs = logprobs
|
|
55
|
-
|
|
56
|
-
@staticmethod
|
|
57
|
-
def _oai_to_mcp(tool_call: Any) -> MCPToolCall: # type: ignore[valid-type]
|
|
58
|
-
"""Convert an OpenAI ``tool_call`` to :class:`MCPToolCall`."""
|
|
59
|
-
return MCPToolCall(
|
|
60
|
-
id=tool_call.id,
|
|
61
|
-
name=tool_call.function.name,
|
|
62
|
-
arguments=json.loads(tool_call.function.arguments or "{}"),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
async def get_system_messages(self) -> list[Any]:
|
|
66
|
-
"""Get system messages for OpenAI."""
|
|
67
|
-
return [
|
|
68
|
-
{"role": "system", "content": self.system_prompt},
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
async def format_blocks(self, blocks: list[types.ContentBlock]) -> list[Any]:
|
|
72
|
-
"""Format blocks for OpenAI."""
|
|
73
|
-
return [
|
|
74
|
-
{
|
|
75
|
-
"role": "user",
|
|
76
|
-
"content": [
|
|
77
|
-
{"type": "text", "text": block.text}
|
|
78
|
-
for block in blocks
|
|
79
|
-
if isinstance(block, types.TextContent)
|
|
80
|
-
],
|
|
81
|
-
},
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
def get_tool_schemas(self) -> list[dict]:
|
|
85
|
-
tool_schemas = super().get_tool_schemas()
|
|
86
|
-
openai_tools = []
|
|
87
|
-
for schema in tool_schemas:
|
|
88
|
-
openai_tool = {
|
|
89
|
-
"type": "function",
|
|
90
|
-
"function": {
|
|
91
|
-
"name": schema["name"],
|
|
92
|
-
"description": schema.get("description", ""),
|
|
93
|
-
"parameters": schema.get("parameters", {"type": "object", "properties": {}}),
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
openai_tools.append(openai_tool)
|
|
97
|
-
return openai_tools
|
|
98
|
-
|
|
99
|
-
async def get_response(self, messages: list[Any]) -> AgentResponse:
|
|
100
|
-
"""Send chat request to OpenAI and convert the response."""
|
|
101
|
-
# Convert MCP tool schemas to OpenAI format
|
|
102
|
-
mcp_schemas = self.get_tool_schemas()
|
|
103
|
-
|
|
104
|
-
response = await self.oai.chat.completions.create(
|
|
105
|
-
model=self.model_name,
|
|
106
|
-
messages=messages,
|
|
107
|
-
tools=cast("list[ChatCompletionToolParam]", mcp_schemas),
|
|
108
|
-
parallel_tool_calls=self.parallel_tool_calls,
|
|
109
|
-
logprobs=self.logprobs,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
choice = response.choices[0]
|
|
113
|
-
msg = choice.message
|
|
114
|
-
|
|
115
|
-
tool_calls = []
|
|
116
|
-
if msg.tool_calls:
|
|
117
|
-
for tc in msg.tool_calls:
|
|
118
|
-
if tc.function.name is not None: # type: ignore
|
|
119
|
-
tool_calls.append(self._oai_to_mcp(tc))
|
|
120
|
-
if not self.parallel_tool_calls:
|
|
121
|
-
break
|
|
122
|
-
|
|
123
|
-
return AgentResponse(
|
|
124
|
-
content=msg.content or "",
|
|
125
|
-
tool_calls=tool_calls,
|
|
126
|
-
done=choice.finish_reason == "stop",
|
|
127
|
-
raw=response, # Include raw response for access to Choice objects
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
async def format_tool_results(
|
|
131
|
-
self,
|
|
132
|
-
tool_calls: list[MCPToolCall],
|
|
133
|
-
tool_results: list[MCPToolResult],
|
|
134
|
-
) -> list[Any]:
|
|
135
|
-
"""Render MCP tool results as OpenAI ``role=tool`` messages."""
|
|
136
|
-
rendered: list[dict[str, Any]] = []
|
|
137
|
-
for call, res in zip(tool_calls, tool_results, strict=False):
|
|
138
|
-
if res.structuredContent:
|
|
139
|
-
content = json.dumps(res.structuredContent)
|
|
140
|
-
else:
|
|
141
|
-
# Concatenate any TextContent blocks
|
|
142
|
-
content = "".join(
|
|
143
|
-
c.text # type: ignore[attr-defined]
|
|
144
|
-
for c in res.content
|
|
145
|
-
if hasattr(c, "text")
|
|
146
|
-
)
|
|
147
|
-
rendered.append(
|
|
148
|
-
{
|
|
149
|
-
"role": "tool",
|
|
150
|
-
"tool_call_id": call.id,
|
|
151
|
-
"content": content or "", # Ensure content is never None
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
return rendered
|
|
1
|
+
"""Generic OpenAI chat-completions agent.
|
|
2
|
+
|
|
3
|
+
This class provides the minimal glue required to connect any endpoint that
|
|
4
|
+
implements the OpenAI compatible *chat.completions* API with MCP tool calling
|
|
5
|
+
through the existing :class:`hud.agent.MCPAgent` scaffolding.
|
|
6
|
+
|
|
7
|
+
Key points:
|
|
8
|
+
- Stateless, no special server-side conversation state is assumed.
|
|
9
|
+
- Accepts an :class:`openai.AsyncOpenAI` client, caller can supply their own
|
|
10
|
+
base_url / api_key (e.g. ART, llama.cpp, together.ai, …)
|
|
11
|
+
- All HUD features (step_count, OTel spans, tool filtering, screenshots, …)
|
|
12
|
+
come from the ``MCPAgent`` base class, we only implement the three abstract
|
|
13
|
+
methods
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
21
|
+
|
|
22
|
+
import mcp.types as types
|
|
23
|
+
|
|
24
|
+
from hud.types import AgentResponse, MCPToolCall, MCPToolResult
|
|
25
|
+
|
|
26
|
+
from .base import MCPAgent
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from openai import AsyncOpenAI
|
|
30
|
+
from openai.types.chat import ChatCompletionToolParam
|
|
31
|
+
|
|
32
|
+
from hud.clients import AgentMCPClient
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GenericOpenAIChatAgent(MCPAgent):
|
|
38
|
+
"""MCP-enabled agent that speaks the OpenAI *chat.completions* protocol."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
mcp_client: AgentMCPClient,
|
|
43
|
+
*,
|
|
44
|
+
openai_client: AsyncOpenAI,
|
|
45
|
+
model_name: str = "gpt-4o-mini",
|
|
46
|
+
parallel_tool_calls: bool = False,
|
|
47
|
+
logprobs: bool = False,
|
|
48
|
+
**agent_kwargs: Any,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(mcp_client=mcp_client, **agent_kwargs)
|
|
51
|
+
self.oai = openai_client
|
|
52
|
+
self.model_name = model_name
|
|
53
|
+
self.parallel_tool_calls = parallel_tool_calls
|
|
54
|
+
self.logprobs = logprobs
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _oai_to_mcp(tool_call: Any) -> MCPToolCall: # type: ignore[valid-type]
|
|
58
|
+
"""Convert an OpenAI ``tool_call`` to :class:`MCPToolCall`."""
|
|
59
|
+
return MCPToolCall(
|
|
60
|
+
id=tool_call.id,
|
|
61
|
+
name=tool_call.function.name,
|
|
62
|
+
arguments=json.loads(tool_call.function.arguments or "{}"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def get_system_messages(self) -> list[Any]:
|
|
66
|
+
"""Get system messages for OpenAI."""
|
|
67
|
+
return [
|
|
68
|
+
{"role": "system", "content": self.system_prompt},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
async def format_blocks(self, blocks: list[types.ContentBlock]) -> list[Any]:
|
|
72
|
+
"""Format blocks for OpenAI."""
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
"role": "user",
|
|
76
|
+
"content": [
|
|
77
|
+
{"type": "text", "text": block.text}
|
|
78
|
+
for block in blocks
|
|
79
|
+
if isinstance(block, types.TextContent)
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
def get_tool_schemas(self) -> list[dict]:
|
|
85
|
+
tool_schemas = super().get_tool_schemas()
|
|
86
|
+
openai_tools = []
|
|
87
|
+
for schema in tool_schemas:
|
|
88
|
+
openai_tool = {
|
|
89
|
+
"type": "function",
|
|
90
|
+
"function": {
|
|
91
|
+
"name": schema["name"],
|
|
92
|
+
"description": schema.get("description", ""),
|
|
93
|
+
"parameters": schema.get("parameters", {"type": "object", "properties": {}}),
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
openai_tools.append(openai_tool)
|
|
97
|
+
return openai_tools
|
|
98
|
+
|
|
99
|
+
async def get_response(self, messages: list[Any]) -> AgentResponse:
|
|
100
|
+
"""Send chat request to OpenAI and convert the response."""
|
|
101
|
+
# Convert MCP tool schemas to OpenAI format
|
|
102
|
+
mcp_schemas = self.get_tool_schemas()
|
|
103
|
+
|
|
104
|
+
response = await self.oai.chat.completions.create(
|
|
105
|
+
model=self.model_name,
|
|
106
|
+
messages=messages,
|
|
107
|
+
tools=cast("list[ChatCompletionToolParam]", mcp_schemas),
|
|
108
|
+
parallel_tool_calls=self.parallel_tool_calls,
|
|
109
|
+
logprobs=self.logprobs,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
choice = response.choices[0]
|
|
113
|
+
msg = choice.message
|
|
114
|
+
|
|
115
|
+
tool_calls = []
|
|
116
|
+
if msg.tool_calls:
|
|
117
|
+
for tc in msg.tool_calls:
|
|
118
|
+
if tc.function.name is not None: # type: ignore
|
|
119
|
+
tool_calls.append(self._oai_to_mcp(tc))
|
|
120
|
+
if not self.parallel_tool_calls:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
return AgentResponse(
|
|
124
|
+
content=msg.content or "",
|
|
125
|
+
tool_calls=tool_calls,
|
|
126
|
+
done=choice.finish_reason == "stop",
|
|
127
|
+
raw=response, # Include raw response for access to Choice objects
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def format_tool_results(
|
|
131
|
+
self,
|
|
132
|
+
tool_calls: list[MCPToolCall],
|
|
133
|
+
tool_results: list[MCPToolResult],
|
|
134
|
+
) -> list[Any]:
|
|
135
|
+
"""Render MCP tool results as OpenAI ``role=tool`` messages."""
|
|
136
|
+
rendered: list[dict[str, Any]] = []
|
|
137
|
+
for call, res in zip(tool_calls, tool_results, strict=False):
|
|
138
|
+
if res.structuredContent:
|
|
139
|
+
content = json.dumps(res.structuredContent)
|
|
140
|
+
else:
|
|
141
|
+
# Concatenate any TextContent blocks
|
|
142
|
+
content = "".join(
|
|
143
|
+
c.text # type: ignore[attr-defined]
|
|
144
|
+
for c in res.content
|
|
145
|
+
if hasattr(c, "text")
|
|
146
|
+
)
|
|
147
|
+
rendered.append(
|
|
148
|
+
{
|
|
149
|
+
"role": "tool",
|
|
150
|
+
"tool_call_id": call.id,
|
|
151
|
+
"content": content or "", # Ensure content is never None
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return rendered
|
hud/agents/tests/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Tests for MCP Agent module."""
|
|
1
|
+
"""Tests for MCP Agent module."""
|