hud-python 0.2.1__py3-none-any.whl → 0.2.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 +5 -3
- hud/adapters/__init__.py +2 -1
- hud/adapters/claude/adapter.py +13 -17
- hud/adapters/common/adapter.py +3 -3
- hud/adapters/common/tests/__init__.py +0 -0
- hud/adapters/common/tests/test_adapter.py +277 -0
- hud/adapters/common/types.py +3 -6
- hud/adapters/operator/adapter.py +22 -29
- hud/agent/__init__.py +9 -1
- hud/agent/base.py +28 -28
- hud/agent/claude.py +69 -60
- hud/agent/langchain.py +204 -0
- hud/agent/operator.py +75 -67
- hud/env/__init__.py +5 -5
- hud/env/client.py +2 -2
- hud/env/docker_client.py +37 -39
- hud/env/environment.py +91 -66
- hud/env/local_docker_client.py +5 -7
- hud/env/remote_client.py +40 -29
- hud/env/remote_docker_client.py +13 -3
- hud/evaluators/__init__.py +2 -3
- hud/evaluators/base.py +4 -3
- hud/evaluators/inspect.py +3 -8
- hud/evaluators/judge.py +34 -58
- hud/evaluators/match.py +42 -49
- hud/evaluators/remote.py +13 -26
- hud/evaluators/tests/__init__.py +0 -0
- hud/evaluators/tests/test_inspect.py +12 -0
- hud/evaluators/tests/test_judge.py +231 -0
- hud/evaluators/tests/test_match.py +115 -0
- hud/evaluators/tests/test_remote.py +98 -0
- hud/exceptions.py +167 -0
- hud/gym.py +12 -10
- hud/job.py +525 -47
- hud/server/__init__.py +2 -2
- hud/server/requests.py +148 -186
- hud/server/tests/__init__.py +0 -0
- hud/server/tests/test_requests.py +275 -0
- hud/settings.py +3 -2
- hud/task.py +12 -22
- hud/taskset.py +44 -11
- hud/trajectory.py +6 -9
- hud/types.py +14 -9
- hud/utils/__init__.py +2 -2
- hud/utils/common.py +37 -13
- hud/utils/config.py +44 -29
- hud/utils/progress.py +149 -0
- hud/utils/telemetry.py +10 -11
- hud/utils/tests/__init__.py +0 -0
- hud/utils/tests/test_common.py +52 -0
- hud/utils/tests/test_config.py +129 -0
- hud/utils/tests/test_progress.py +225 -0
- hud/utils/tests/test_telemetry.py +37 -0
- hud/utils/tests/test_version.py +8 -0
- {hud_python-0.2.1.dist-info → hud_python-0.2.3.dist-info}/METADATA +44 -21
- hud_python-0.2.3.dist-info/RECORD +62 -0
- hud_python-0.2.1.dist-info/RECORD +0 -44
- {hud_python-0.2.1.dist-info → hud_python-0.2.3.dist-info}/WHEEL +0 -0
- {hud_python-0.2.1.dist-info → hud_python-0.2.3.dist-info}/licenses/LICENSE +0 -0
hud/__init__.py
CHANGED
|
@@ -5,19 +5,21 @@ HUD Gym SDK - A Python SDK for interacting with HUD environments.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from . import agent, env, gym, settings, task, taskset, types, utils
|
|
8
|
-
from .job import create_job,
|
|
8
|
+
from .job import create_job, load_job, run_job
|
|
9
|
+
from .job import job as register_job
|
|
9
10
|
from .taskset import load_taskset
|
|
10
11
|
|
|
11
|
-
__version__ = "0.2.
|
|
12
|
+
__version__ = "0.2.3"
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"agent",
|
|
15
16
|
"create_job",
|
|
16
17
|
"env",
|
|
17
18
|
"gym",
|
|
18
|
-
"job",
|
|
19
19
|
"load_job",
|
|
20
20
|
"load_taskset",
|
|
21
|
+
"register_job",
|
|
22
|
+
"run_job",
|
|
21
23
|
"settings",
|
|
22
24
|
"task",
|
|
23
25
|
"taskset",
|
hud/adapters/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from .claude import ClaudeAdapter
|
|
4
4
|
from .common import CLA, Adapter
|
|
5
|
+
from .common.types import ResponseAction
|
|
5
6
|
from .operator import OperatorAdapter
|
|
6
7
|
|
|
7
|
-
__all__ = ["CLA", "Adapter", "ClaudeAdapter", "OperatorAdapter"]
|
|
8
|
+
__all__ = ["CLA", "Adapter", "ClaudeAdapter", "OperatorAdapter", "ResponseAction"]
|
hud/adapters/claude/adapter.py
CHANGED
|
@@ -23,9 +23,13 @@ from hud.adapters.common.types import (
|
|
|
23
23
|
|
|
24
24
|
class ClaudeAdapter(Adapter):
|
|
25
25
|
KEY_MAP: ClassVar[dict[str, CLAKey]] = {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
26
|
+
"return": "enter",
|
|
27
|
+
"super": "win",
|
|
28
|
+
"super_l": "win",
|
|
29
|
+
"super_r": "win",
|
|
30
|
+
"right shift": "shift",
|
|
31
|
+
"left shift": "shift",
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
def __init__(self) -> None:
|
|
31
35
|
super().__init__()
|
|
@@ -34,7 +38,8 @@ class ClaudeAdapter(Adapter):
|
|
|
34
38
|
|
|
35
39
|
def _map_key(self, key: str) -> CLAKey:
|
|
36
40
|
"""Map a key to its standardized form."""
|
|
37
|
-
return self.KEY_MAP.get(key, key.lower()) # type: ignore
|
|
41
|
+
return self.KEY_MAP.get(key.lower(), key.lower()) # type: ignore
|
|
42
|
+
|
|
38
43
|
def convert(self, data: Any) -> CLA:
|
|
39
44
|
try:
|
|
40
45
|
action_type = data.get("action")
|
|
@@ -42,9 +47,7 @@ class ClaudeAdapter(Adapter):
|
|
|
42
47
|
if action_type == "key":
|
|
43
48
|
assert "text" in data
|
|
44
49
|
if "+" in data["text"]:
|
|
45
|
-
keys: list[CLAKey] = [
|
|
46
|
-
self._map_key(k) for k in (data["text"].split("+"))
|
|
47
|
-
]
|
|
50
|
+
keys: list[CLAKey] = [self._map_key(k) for k in (data["text"].split("+"))]
|
|
48
51
|
assert len(keys) > 0
|
|
49
52
|
return PressAction(keys=keys)
|
|
50
53
|
return PressAction(keys=[self._map_key(data["text"])])
|
|
@@ -78,19 +81,12 @@ class ClaudeAdapter(Adapter):
|
|
|
78
81
|
assert len(coord) == 2
|
|
79
82
|
if (
|
|
80
83
|
len(self.memory) == 0
|
|
81
|
-
or (
|
|
82
|
-
self.memory[-1] is not MoveAction
|
|
83
|
-
and self.memory[-1] is not ClickAction
|
|
84
|
-
)
|
|
84
|
+
or (self.memory[-1] is not MoveAction and self.memory[-1] is not ClickAction)
|
|
85
85
|
or self.memory[-1].point is None
|
|
86
86
|
):
|
|
87
|
-
raise ValueError(
|
|
88
|
-
"Left click drag must be preceded by a move or click action"
|
|
89
|
-
)
|
|
87
|
+
raise ValueError("Left click drag must be preceded by a move or click action")
|
|
90
88
|
else:
|
|
91
|
-
return DragAction(
|
|
92
|
-
path=[self.memory[-1].point, Point(x=coord[0], y=coord[1])]
|
|
93
|
-
)
|
|
89
|
+
return DragAction(path=[self.memory[-1].point, Point(x=coord[0], y=coord[1])])
|
|
94
90
|
|
|
95
91
|
elif action_type == "right_click":
|
|
96
92
|
assert "coordinate" in data
|
hud/adapters/common/adapter.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any, TypeAlias
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from PIL import Image
|
|
@@ -11,7 +11,7 @@ from .types import CLA
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from typing_extensions import TypeIs
|
|
13
13
|
|
|
14
|
-
ImageType = np.ndarray[Any, Any] | Image.Image | str | None
|
|
14
|
+
ImageType: TypeAlias = np.ndarray[Any, Any] | Image.Image | str | None
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _is_numpy_array(observation: Any) -> TypeIs[np.ndarray]:
|
|
@@ -164,5 +164,5 @@ class Adapter:
|
|
|
164
164
|
def adapt_list(self, actions: list[Any]) -> list[CLA]:
|
|
165
165
|
if not isinstance(actions, list):
|
|
166
166
|
raise ValueError("Please provide a list of actions")
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
return [self.adapt(action) for action in actions]
|
|
File without changes
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import io
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
from hud.adapters.common import Adapter
|
|
12
|
+
from hud.adapters.common.types import ClickAction, Point, TypeAction
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def adapter():
|
|
17
|
+
"""Fixture providing a clean adapter instance."""
|
|
18
|
+
return Adapter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def test_image():
|
|
23
|
+
"""Fixture providing test image in various formats."""
|
|
24
|
+
img = Image.new("RGB", (100, 80), color="red")
|
|
25
|
+
img_bytes = io.BytesIO()
|
|
26
|
+
img.save(img_bytes, format="PNG")
|
|
27
|
+
img_base64 = base64.b64encode(img_bytes.getvalue()).decode("utf-8")
|
|
28
|
+
img_array = np.array(img)
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"pil": img,
|
|
32
|
+
"bytes": img_bytes.getvalue(),
|
|
33
|
+
"base64": img_base64,
|
|
34
|
+
"array": img_array,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_init(adapter):
|
|
39
|
+
"""Test adapter initialization."""
|
|
40
|
+
assert adapter.agent_width == 1920
|
|
41
|
+
assert adapter.agent_height == 1080
|
|
42
|
+
assert adapter.env_width == 1920
|
|
43
|
+
assert adapter.env_height == 1080
|
|
44
|
+
assert adapter.memory == []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_preprocess(adapter):
|
|
48
|
+
"""Test preprocess method (default implementation)."""
|
|
49
|
+
action = {"type": "click", "point": {"x": 100, "y": 100}}
|
|
50
|
+
result = adapter.preprocess(action)
|
|
51
|
+
assert result == action # Default implementation returns unchanged
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_convert_valid(adapter):
|
|
55
|
+
"""Test convert method with valid action."""
|
|
56
|
+
action = ClickAction(point=Point(x=100, y=100))
|
|
57
|
+
result = adapter.convert(action)
|
|
58
|
+
# Fix: Instead of checking against CLA, check it's the same type as the input
|
|
59
|
+
assert isinstance(result, ClickAction)
|
|
60
|
+
assert result == action
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_convert_invalid(adapter):
|
|
64
|
+
"""Test convert method with invalid action."""
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
adapter.convert(None) # type: ignore
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_json_valid(adapter):
|
|
70
|
+
"""Test json method with valid action."""
|
|
71
|
+
action = ClickAction(point=Point(x=100, y=100))
|
|
72
|
+
result = adapter.json(action)
|
|
73
|
+
assert isinstance(result, dict)
|
|
74
|
+
assert result["type"] == "click"
|
|
75
|
+
assert result["point"]["x"] == 100
|
|
76
|
+
assert result["point"]["y"] == 100
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_json_invalid(adapter):
|
|
80
|
+
"""Test json method with invalid action."""
|
|
81
|
+
with pytest.raises(ValueError):
|
|
82
|
+
adapter.json(None) # type: ignore
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_rescale_pil_image(adapter, test_image):
|
|
86
|
+
"""Test rescaling PIL Image."""
|
|
87
|
+
result = adapter.rescale(test_image["pil"])
|
|
88
|
+
|
|
89
|
+
# Verify result is base64 string
|
|
90
|
+
assert isinstance(result, str)
|
|
91
|
+
|
|
92
|
+
# Verify environment dimensions were updated
|
|
93
|
+
assert adapter.env_width == 100
|
|
94
|
+
assert adapter.env_height == 80
|
|
95
|
+
|
|
96
|
+
# Decode and verify image dimensions
|
|
97
|
+
img_bytes = base64.b64decode(result)
|
|
98
|
+
img = Image.open(io.BytesIO(img_bytes))
|
|
99
|
+
assert img.size == (adapter.agent_width, adapter.agent_height)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_rescale_numpy_array(adapter, test_image):
|
|
103
|
+
"""Test rescaling numpy array."""
|
|
104
|
+
result = adapter.rescale(test_image["array"])
|
|
105
|
+
|
|
106
|
+
# Verify result is base64 string
|
|
107
|
+
assert isinstance(result, str)
|
|
108
|
+
|
|
109
|
+
# Verify environment dimensions were updated
|
|
110
|
+
assert adapter.env_width == 100
|
|
111
|
+
assert adapter.env_height == 80
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_rescale_base64(adapter, test_image):
|
|
115
|
+
"""Test rescaling base64 string."""
|
|
116
|
+
result = adapter.rescale(test_image["base64"])
|
|
117
|
+
|
|
118
|
+
# Verify result is base64 string
|
|
119
|
+
assert isinstance(result, str)
|
|
120
|
+
|
|
121
|
+
# Verify environment dimensions were updated
|
|
122
|
+
assert adapter.env_width == 100
|
|
123
|
+
assert adapter.env_height == 80
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_rescale_base64_with_header(adapter, test_image):
|
|
127
|
+
"""Test rescaling base64 string with header."""
|
|
128
|
+
base64_with_header = f"data:image/png;base64,{test_image['base64']}"
|
|
129
|
+
result = adapter.rescale(base64_with_header)
|
|
130
|
+
|
|
131
|
+
# Verify result is base64 string
|
|
132
|
+
assert isinstance(result, str)
|
|
133
|
+
|
|
134
|
+
# Verify environment dimensions were updated
|
|
135
|
+
assert adapter.env_width == 100
|
|
136
|
+
assert adapter.env_height == 80
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_rescale_invalid_type(adapter):
|
|
140
|
+
"""Test rescaling with invalid type."""
|
|
141
|
+
with pytest.raises(ValueError):
|
|
142
|
+
adapter.rescale(123) # type: ignore
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_rescale_none(adapter):
|
|
146
|
+
"""Test rescaling with None."""
|
|
147
|
+
result = adapter.rescale(None)
|
|
148
|
+
assert result is None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_postprocess_action_click(adapter):
|
|
152
|
+
"""Test postprocess_action with click action."""
|
|
153
|
+
# Set different agent and env dimensions
|
|
154
|
+
adapter.agent_width = 1000
|
|
155
|
+
adapter.agent_height = 800
|
|
156
|
+
adapter.env_width = 2000
|
|
157
|
+
adapter.env_height = 1600
|
|
158
|
+
|
|
159
|
+
action = {"type": "click", "point": {"x": 500, "y": 400}}
|
|
160
|
+
result = adapter.postprocess_action(action)
|
|
161
|
+
|
|
162
|
+
# Coordinates should be doubled
|
|
163
|
+
assert result["point"]["x"] == 1000
|
|
164
|
+
assert result["point"]["y"] == 800
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_postprocess_action_drag(adapter):
|
|
168
|
+
"""Test postprocess_action with drag action."""
|
|
169
|
+
# Set different agent and env dimensions
|
|
170
|
+
adapter.agent_width = 1000
|
|
171
|
+
adapter.agent_height = 800
|
|
172
|
+
adapter.env_width = 2000
|
|
173
|
+
adapter.env_height = 1600
|
|
174
|
+
|
|
175
|
+
action = {"type": "drag", "path": [{"x": 100, "y": 200}, {"x": 300, "y": 400}]}
|
|
176
|
+
result = adapter.postprocess_action(action)
|
|
177
|
+
|
|
178
|
+
# Coordinates should be doubled
|
|
179
|
+
assert result["path"][0]["x"] == 200
|
|
180
|
+
assert result["path"][0]["y"] == 400
|
|
181
|
+
assert result["path"][1]["x"] == 600
|
|
182
|
+
assert result["path"][1]["y"] == 800
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_postprocess_action_scroll(adapter):
|
|
186
|
+
"""Test postprocess_action with scroll action."""
|
|
187
|
+
# Set different agent and env dimensions
|
|
188
|
+
adapter.agent_width = 1000
|
|
189
|
+
adapter.agent_height = 800
|
|
190
|
+
adapter.env_width = 2000
|
|
191
|
+
adapter.env_height = 1600
|
|
192
|
+
|
|
193
|
+
action = {"type": "scroll", "point": {"x": 500, "y": 400}, "scroll": {"x": 0, "y": 10}}
|
|
194
|
+
result = adapter.postprocess_action(action)
|
|
195
|
+
|
|
196
|
+
# Point coordinates should be doubled
|
|
197
|
+
assert result["point"]["x"] == 1000
|
|
198
|
+
assert result["point"]["y"] == 800
|
|
199
|
+
# Scroll amount should be scaled
|
|
200
|
+
assert result["scroll"]["x"] == 0
|
|
201
|
+
assert result["scroll"]["y"] == 20
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_postprocess_action_empty(adapter):
|
|
205
|
+
"""Test postprocess_action with empty action."""
|
|
206
|
+
result = adapter.postprocess_action({})
|
|
207
|
+
assert result == {}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_adapt(adapter):
|
|
211
|
+
"""Test adapt method."""
|
|
212
|
+
# Mock the needed methods
|
|
213
|
+
with (
|
|
214
|
+
patch.object(adapter, "preprocess", return_value={"preprocessed": True}),
|
|
215
|
+
patch.object(adapter, "convert", return_value=TypeAction(text="test")),
|
|
216
|
+
patch.object(adapter, "json", return_value={"type": "type", "text": "test"}),
|
|
217
|
+
patch.object(adapter, "postprocess_action", return_value={"type": "type", "text": "test"}),
|
|
218
|
+
patch("hud.adapters.common.adapter.TypeAdapter") as mock_adapter,
|
|
219
|
+
):
|
|
220
|
+
mock_validator = MagicMock()
|
|
221
|
+
mock_adapter.return_value = mock_validator
|
|
222
|
+
mock_validator.validate_python.return_value = TypeAction(text="test")
|
|
223
|
+
|
|
224
|
+
adapter.adapt({"raw": "action"})
|
|
225
|
+
|
|
226
|
+
# Verify the method chain was called correctly
|
|
227
|
+
adapter.preprocess.assert_called_once_with({"raw": "action"})
|
|
228
|
+
adapter.convert.assert_called_once_with({"preprocessed": True})
|
|
229
|
+
adapter.json.assert_called_once_with(TypeAction(text="test"))
|
|
230
|
+
adapter.postprocess_action.assert_called_once_with({"type": "type", "text": "test"})
|
|
231
|
+
|
|
232
|
+
# Verify the memory was updated
|
|
233
|
+
assert len(adapter.memory) == 1
|
|
234
|
+
assert adapter.memory[0] == TypeAction(text="test")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_adapt_list(adapter):
|
|
238
|
+
"""Test adapt_list method."""
|
|
239
|
+
# Fix: Use side_effect to return different values for each call to adapt
|
|
240
|
+
click_action = ClickAction(point=Point(x=100, y=100))
|
|
241
|
+
type_action = TypeAction(text="test")
|
|
242
|
+
|
|
243
|
+
mock_adapt = MagicMock(side_effect=[click_action, type_action])
|
|
244
|
+
with patch.object(adapter, "adapt", mock_adapt):
|
|
245
|
+
actions = [{"type": "click"}, {"type": "type"}]
|
|
246
|
+
result = adapter.adapt_list(actions)
|
|
247
|
+
|
|
248
|
+
assert adapter.adapt.call_count == 2
|
|
249
|
+
assert len(result) == 2
|
|
250
|
+
assert result[0] == click_action
|
|
251
|
+
assert result[1] == type_action
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_adapt_list_invalid(adapter):
|
|
255
|
+
"""Test adapt_list with invalid input."""
|
|
256
|
+
with pytest.raises(ValueError):
|
|
257
|
+
adapter.adapt_list("not a list") # type: ignore
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_integration(adapter):
|
|
261
|
+
"""Integration test for the full adapter pipeline."""
|
|
262
|
+
adapter.agent_width = 1000
|
|
263
|
+
adapter.agent_height = 800
|
|
264
|
+
adapter.env_width = 2000
|
|
265
|
+
adapter.env_height = 1600
|
|
266
|
+
|
|
267
|
+
# Create a click action
|
|
268
|
+
action = ClickAction(point=Point(x=500, y=400))
|
|
269
|
+
|
|
270
|
+
result = adapter.adapt(action)
|
|
271
|
+
|
|
272
|
+
assert isinstance(result, ClickAction)
|
|
273
|
+
assert result.point is not None
|
|
274
|
+
assert result.point.x == 1000 # Scaled from 500 to 1000
|
|
275
|
+
assert result.point.y == 800 # Scaled from 400 to 800
|
|
276
|
+
|
|
277
|
+
assert len(adapter.memory) == 1
|
hud/adapters/common/types.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Annotated, Literal
|
|
3
|
+
from typing import Annotated, Literal, TypeAlias
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
@@ -20,7 +20,6 @@ class Point(BaseModel):
|
|
|
20
20
|
class ClickAction(CLAAction):
|
|
21
21
|
type: Literal["click"] = "click"
|
|
22
22
|
point: Point | None = None
|
|
23
|
-
selector: str | None = None
|
|
24
23
|
button: Literal["left", "right", "wheel", "back", "forward"] = "left"
|
|
25
24
|
pattern: list[int] | None = None # [delay_1, delay_2, ...]
|
|
26
25
|
hold_keys: list[CLAKey] | None = None
|
|
@@ -48,7 +47,6 @@ class KeyUpAction(CLAAction):
|
|
|
48
47
|
class TypeAction(CLAAction):
|
|
49
48
|
type: Literal["type"] = "type"
|
|
50
49
|
text: str
|
|
51
|
-
selector: str | None = None
|
|
52
50
|
enter_after: bool | None = False
|
|
53
51
|
|
|
54
52
|
|
|
@@ -64,7 +62,6 @@ class ScrollAction(CLAAction):
|
|
|
64
62
|
class MoveAction(CLAAction):
|
|
65
63
|
type: Literal["move"] = "move"
|
|
66
64
|
point: Point | None = None
|
|
67
|
-
selector: str | None = None
|
|
68
65
|
offset: Point | None = None
|
|
69
66
|
|
|
70
67
|
|
|
@@ -85,7 +82,7 @@ class DragAction(CLAAction):
|
|
|
85
82
|
# RESPONSE ACTION from agent
|
|
86
83
|
class ResponseAction(CLAAction):
|
|
87
84
|
type: Literal["response"] = "response"
|
|
88
|
-
text: str
|
|
85
|
+
text: str # The final textual response from the agent
|
|
89
86
|
|
|
90
87
|
|
|
91
88
|
# SCREENSHOT ACTION
|
|
@@ -121,7 +118,7 @@ CLA = Annotated[
|
|
|
121
118
|
]
|
|
122
119
|
|
|
123
120
|
|
|
124
|
-
CLAKey = Literal[
|
|
121
|
+
CLAKey: TypeAlias = Literal[
|
|
125
122
|
# Control keys
|
|
126
123
|
"backspace",
|
|
127
124
|
"tab",
|
hud/adapters/operator/adapter.py
CHANGED
|
@@ -20,78 +20,71 @@ from hud.adapters.common.types import (
|
|
|
20
20
|
|
|
21
21
|
class OperatorAdapter(Adapter):
|
|
22
22
|
KEY_MAP: ClassVar[dict[str, CLAKey]] = {
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
23
|
+
"return": "enter",
|
|
24
|
+
"arrowup": "up",
|
|
25
|
+
"arrowdown": "down",
|
|
26
|
+
"arrowleft": "left",
|
|
27
|
+
"arrowright": "right",
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
def __init__(self) -> None:
|
|
31
31
|
super().__init__()
|
|
32
32
|
# OpenAI Computer Use default dimensions
|
|
33
33
|
self.agent_width = 1024
|
|
34
34
|
self.agent_height = 768
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
def _map_key(self, key: str) -> CLAKey:
|
|
37
37
|
"""Map a key to its standardized form."""
|
|
38
|
-
return self.KEY_MAP.get(key, key.lower()) # type: ignore
|
|
39
|
-
|
|
38
|
+
return self.KEY_MAP.get(key.lower(), key.lower()) # type: ignore
|
|
39
|
+
|
|
40
40
|
def convert(self, data: Any) -> CLA:
|
|
41
41
|
"""Convert a Computer Use action to a HUD action"""
|
|
42
42
|
try:
|
|
43
43
|
action_type = data.get("type")
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
if action_type == "click":
|
|
46
46
|
x, y = data.get("x", 0), data.get("y", 0)
|
|
47
47
|
button = data.get("button", "left")
|
|
48
48
|
return ClickAction(point=Point(x=x, y=y), button=button)
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
elif action_type == "double_click":
|
|
51
51
|
x, y = data.get("x", 0), data.get("y", 0)
|
|
52
|
-
return ClickAction(
|
|
53
|
-
|
|
54
|
-
button="left",
|
|
55
|
-
pattern=[100]
|
|
56
|
-
)
|
|
57
|
-
|
|
52
|
+
return ClickAction(point=Point(x=x, y=y), button="left", pattern=[100])
|
|
53
|
+
|
|
58
54
|
elif action_type == "scroll":
|
|
59
55
|
x, y = data.get("x", 0), data.get("y", 0)
|
|
60
56
|
scroll_x = data.get("scroll_x", 0)
|
|
61
57
|
scroll_y = data.get("scroll_y", 0)
|
|
62
|
-
return ScrollAction(
|
|
63
|
-
|
|
64
|
-
scroll=Point(x=scroll_x, y=scroll_y)
|
|
65
|
-
)
|
|
66
|
-
|
|
58
|
+
return ScrollAction(point=Point(x=x, y=y), scroll=Point(x=scroll_x, y=scroll_y))
|
|
59
|
+
|
|
67
60
|
elif action_type == "type":
|
|
68
61
|
text = data.get("text", "")
|
|
69
62
|
return TypeAction(text=text, enter_after=False)
|
|
70
|
-
|
|
63
|
+
|
|
71
64
|
elif action_type == "wait":
|
|
72
65
|
ms = data.get("ms", 1000)
|
|
73
66
|
return WaitAction(time=ms)
|
|
74
|
-
|
|
67
|
+
|
|
75
68
|
elif action_type == "move":
|
|
76
69
|
x, y = data.get("x", 0), data.get("y", 0)
|
|
77
70
|
return MoveAction(point=Point(x=x, y=y))
|
|
78
|
-
|
|
71
|
+
|
|
79
72
|
elif action_type == "keypress":
|
|
80
73
|
keys = data.get("keys", [])
|
|
81
74
|
return PressAction(keys=[self._map_key(k) for k in keys])
|
|
82
|
-
|
|
75
|
+
|
|
83
76
|
elif action_type == "drag":
|
|
84
77
|
path = data.get("path", [])
|
|
85
78
|
points = [Point(x=p.get("x", 0), y=p.get("y", 0)) for p in path]
|
|
86
79
|
return DragAction(path=points)
|
|
87
|
-
|
|
80
|
+
|
|
88
81
|
elif action_type == "screenshot":
|
|
89
82
|
return ScreenshotFetch()
|
|
90
|
-
|
|
83
|
+
|
|
91
84
|
elif action_type == "response":
|
|
92
85
|
return ResponseAction(text=data.get("text", ""))
|
|
93
86
|
else:
|
|
94
87
|
raise ValueError(f"Unsupported action type: {action_type}")
|
|
95
|
-
|
|
88
|
+
|
|
96
89
|
except Exception as e:
|
|
97
90
|
raise ValueError(f"Invalid action: {data}. Error: {e!s}") from e
|
hud/agent/__init__.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
from .base import Agent
|
|
2
2
|
from .claude import ClaudeAgent
|
|
3
3
|
from .operator import OperatorAgent
|
|
4
|
+
from .langchain import LangchainAgent
|
|
4
5
|
|
|
5
6
|
from hud.adapters import OperatorAdapter, ClaudeAdapter
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Agent",
|
|
10
|
+
"ClaudeAgent",
|
|
11
|
+
"OperatorAgent",
|
|
12
|
+
"OperatorAdapter",
|
|
13
|
+
"ClaudeAdapter",
|
|
14
|
+
"LangchainAgent",
|
|
15
|
+
]
|