hud-python 0.2.3__py3-none-any.whl → 0.2.5__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 (50) hide show
  1. hud/__init__.py +22 -2
  2. hud/adapters/claude/adapter.py +9 -2
  3. hud/adapters/claude/tests/__init__.py +1 -0
  4. hud/adapters/claude/tests/test_adapter.py +519 -0
  5. hud/adapters/common/types.py +5 -1
  6. hud/adapters/operator/adapter.py +4 -0
  7. hud/adapters/operator/tests/__init__.py +1 -0
  8. hud/adapters/operator/tests/test_adapter.py +370 -0
  9. hud/agent/__init__.py +4 -0
  10. hud/agent/base.py +18 -2
  11. hud/agent/claude.py +20 -17
  12. hud/agent/claude_plays_pokemon.py +282 -0
  13. hud/agent/langchain.py +12 -7
  14. hud/agent/misc/__init__.py +3 -0
  15. hud/agent/misc/response_agent.py +80 -0
  16. hud/agent/operator.py +27 -19
  17. hud/agent/tests/__init__.py +1 -0
  18. hud/agent/tests/test_base.py +202 -0
  19. hud/env/docker_client.py +28 -18
  20. hud/env/environment.py +33 -17
  21. hud/env/local_docker_client.py +83 -42
  22. hud/env/remote_client.py +1 -3
  23. hud/env/remote_docker_client.py +72 -15
  24. hud/exceptions.py +12 -0
  25. hud/gym.py +71 -53
  26. hud/job.py +52 -7
  27. hud/settings.py +6 -0
  28. hud/task.py +45 -33
  29. hud/taskset.py +44 -4
  30. hud/telemetry/__init__.py +21 -0
  31. hud/telemetry/_trace.py +173 -0
  32. hud/telemetry/context.py +193 -0
  33. hud/telemetry/exporter.py +417 -0
  34. hud/telemetry/instrumentation/__init__.py +3 -0
  35. hud/telemetry/instrumentation/mcp.py +498 -0
  36. hud/telemetry/instrumentation/registry.py +59 -0
  37. hud/telemetry/mcp_models.py +331 -0
  38. hud/telemetry/tests/__init__.py +1 -0
  39. hud/telemetry/tests/test_context.py +203 -0
  40. hud/telemetry/tests/test_trace.py +270 -0
  41. hud/types.py +10 -26
  42. hud/utils/common.py +22 -2
  43. hud/utils/misc.py +53 -0
  44. hud/utils/tests/test_version.py +1 -1
  45. hud/version.py +7 -0
  46. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/METADATA +90 -22
  47. hud_python-0.2.5.dist-info/RECORD +84 -0
  48. hud_python-0.2.3.dist-info/RECORD +0 -62
  49. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/WHEEL +0 -0
  50. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
hud/__init__.py CHANGED
@@ -1,20 +1,39 @@
1
1
  """
2
- HUD Gym SDK - A Python SDK for interacting with HUD environments.
2
+ HUD SDK for interacting with the HUD evaluation platform.
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import logging
8
+
7
9
  from . import agent, env, gym, settings, task, taskset, types, utils
10
+ from .adapters import ResponseAction as Response
8
11
  from .job import create_job, load_job, run_job
9
12
  from .job import job as register_job
13
+ from .task import Task
10
14
  from .taskset import load_taskset
15
+ from .telemetry import flush, init_telemetry, trace
16
+ from .version import __version__
17
+
18
+ init_telemetry()
19
+
20
+ hud_logger = logging.getLogger("hud")
21
+ hud_logger.setLevel(logging.INFO)
11
22
 
12
- __version__ = "0.2.3"
23
+ if not hud_logger.handlers:
24
+ handler = logging.StreamHandler()
25
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
26
+ handler.setFormatter(formatter)
27
+ hud_logger.addHandler(handler)
13
28
 
14
29
  __all__ = [
30
+ "Response",
31
+ "Task",
32
+ "__version__",
15
33
  "agent",
16
34
  "create_job",
17
35
  "env",
36
+ "flush",
18
37
  "gym",
19
38
  "load_job",
20
39
  "load_taskset",
@@ -23,6 +42,7 @@ __all__ = [
23
42
  "settings",
24
43
  "task",
25
44
  "taskset",
45
+ "trace",
26
46
  "types",
27
47
  "utils",
28
48
  ]
@@ -42,6 +42,10 @@ class ClaudeAdapter(Adapter):
42
42
 
43
43
  def convert(self, data: Any) -> CLA:
44
44
  try:
45
+ # Validate input data
46
+ if not isinstance(data, dict):
47
+ raise ValueError(f"Invalid action: {data}")
48
+
45
49
  action_type = data.get("action")
46
50
 
47
51
  if action_type == "key":
@@ -81,7 +85,10 @@ class ClaudeAdapter(Adapter):
81
85
  assert len(coord) == 2
82
86
  if (
83
87
  len(self.memory) == 0
84
- or (self.memory[-1] is not MoveAction and self.memory[-1] is not ClickAction)
88
+ or (
89
+ not isinstance(self.memory[-1], MoveAction)
90
+ and not isinstance(self.memory[-1], ClickAction)
91
+ )
85
92
  or self.memory[-1].point is None
86
93
  ):
87
94
  raise ValueError("Left click drag must be preceded by a move or click action")
@@ -100,7 +107,7 @@ class ClaudeAdapter(Adapter):
100
107
  coord = data["coordinate"]
101
108
  assert isinstance(coord, list)
102
109
  assert len(coord) == 2
103
- return ClickAction(point=Point(x=coord[0], y=coord[1]), button="wheel")
110
+ return ClickAction(point=Point(x=coord[0], y=coord[1]), button="middle")
104
111
 
105
112
  elif action_type == "double_click":
106
113
  assert "coordinate" in data
@@ -0,0 +1 @@
1
+ # Tests for hud.adapters.claude module
@@ -0,0 +1,519 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from hud.adapters.claude import ClaudeAdapter
6
+ from hud.adapters.common.types import (
7
+ ClickAction,
8
+ DragAction,
9
+ MoveAction,
10
+ PositionFetch,
11
+ PressAction,
12
+ ResponseAction,
13
+ ScreenshotFetch,
14
+ ScrollAction,
15
+ TypeAction,
16
+ WaitAction,
17
+ )
18
+
19
+
20
+ class TestClaudeAdapter:
21
+ """Test the ClaudeAdapter class."""
22
+
23
+ @pytest.fixture
24
+ def adapter(self):
25
+ """Fixture providing a clean adapter instance."""
26
+ return ClaudeAdapter()
27
+
28
+ def test_init(self, adapter):
29
+ """Test adapter initialization."""
30
+ assert adapter.agent_width == 1024
31
+ assert adapter.agent_height == 768
32
+ assert adapter.env_width == 1920 # Inherited from parent
33
+ assert adapter.env_height == 1080 # Inherited from parent
34
+
35
+ def test_key_map_constants(self, adapter):
36
+ """Test KEY_MAP constants."""
37
+ assert adapter.KEY_MAP["return"] == "enter"
38
+ assert adapter.KEY_MAP["super"] == "win"
39
+ assert adapter.KEY_MAP["super_l"] == "win"
40
+ assert adapter.KEY_MAP["super_r"] == "win"
41
+ assert adapter.KEY_MAP["right shift"] == "shift"
42
+ assert adapter.KEY_MAP["left shift"] == "shift"
43
+
44
+ def test_map_key_mapped(self, adapter):
45
+ """Test _map_key with mapped keys."""
46
+ assert adapter._map_key("return") == "enter"
47
+ assert adapter._map_key("RETURN") == "enter" # Test case insensitive
48
+ assert adapter._map_key("super") == "win"
49
+ assert adapter._map_key("Super_L") == "win"
50
+
51
+ def test_map_key_unmapped(self, adapter):
52
+ """Test _map_key with unmapped keys."""
53
+ assert adapter._map_key("space") == "space"
54
+ assert adapter._map_key("CTRL") == "ctrl"
55
+ assert adapter._map_key("Unknown") == "unknown"
56
+
57
+
58
+ class TestClaudeAdapterConvert:
59
+ """Test the convert method of ClaudeAdapter."""
60
+
61
+ @pytest.fixture
62
+ def adapter(self):
63
+ """Fixture providing a clean adapter instance."""
64
+ return ClaudeAdapter()
65
+
66
+ def test_convert_key_single(self, adapter):
67
+ """Test converting single key action."""
68
+ data = {"action": "key", "text": "space"}
69
+ result = adapter.convert(data)
70
+
71
+ assert isinstance(result, PressAction)
72
+ assert result.keys == ["space"]
73
+
74
+ def test_convert_key_mapped(self, adapter):
75
+ """Test converting mapped key action."""
76
+ data = {"action": "key", "text": "return"}
77
+ result = adapter.convert(data)
78
+
79
+ assert isinstance(result, PressAction)
80
+ assert result.keys == ["enter"]
81
+
82
+ def test_convert_key_combination(self, adapter):
83
+ """Test converting key combination action."""
84
+ data = {"action": "key", "text": "ctrl+c"}
85
+ result = adapter.convert(data)
86
+
87
+ assert isinstance(result, PressAction)
88
+ assert result.keys == ["ctrl", "c"]
89
+
90
+ def test_convert_key_combination_mapped(self, adapter):
91
+ """Test converting key combination with mapped keys."""
92
+ data = {"action": "key", "text": "super+return"}
93
+ result = adapter.convert(data)
94
+
95
+ assert isinstance(result, PressAction)
96
+ assert result.keys == ["win", "enter"]
97
+
98
+ def test_convert_key_missing_text(self, adapter):
99
+ """Test converting key action with missing text."""
100
+ data = {"action": "key"}
101
+
102
+ with pytest.raises(ValueError) as exc_info:
103
+ adapter.convert(data)
104
+
105
+ assert "Invalid action" in str(exc_info.value)
106
+
107
+ def test_convert_type_action(self, adapter):
108
+ """Test converting type action."""
109
+ data = {"action": "type", "text": "Hello, World!"}
110
+ result = adapter.convert(data)
111
+
112
+ assert isinstance(result, TypeAction)
113
+ assert result.text == "Hello, World!"
114
+ assert result.enter_after is False
115
+
116
+ def test_convert_type_missing_text(self, adapter):
117
+ """Test converting type action with missing text."""
118
+ data = {"action": "type"}
119
+
120
+ with pytest.raises(ValueError) as exc_info:
121
+ adapter.convert(data)
122
+
123
+ assert "Invalid action" in str(exc_info.value)
124
+
125
+ def test_convert_mouse_move(self, adapter):
126
+ """Test converting mouse move action."""
127
+ data = {"action": "mouse_move", "coordinate": [100, 200]}
128
+ result = adapter.convert(data)
129
+
130
+ assert isinstance(result, MoveAction)
131
+ assert result.point is not None
132
+ assert result.point.x == 100
133
+ assert result.point.y == 200
134
+
135
+ def test_convert_mouse_move_invalid_coordinate(self, adapter):
136
+ """Test converting mouse move with invalid coordinate."""
137
+ # Wrong number of coordinates
138
+ data = {"action": "mouse_move", "coordinate": [100]}
139
+
140
+ with pytest.raises(ValueError) as exc_info:
141
+ adapter.convert(data)
142
+
143
+ assert "Invalid action" in str(exc_info.value)
144
+
145
+ def test_convert_mouse_move_missing_coordinate(self, adapter):
146
+ """Test converting mouse move with missing coordinate."""
147
+ data = {"action": "mouse_move"}
148
+
149
+ with pytest.raises(ValueError) as exc_info:
150
+ adapter.convert(data)
151
+
152
+ assert "Invalid action" in str(exc_info.value)
153
+
154
+ def test_convert_left_click(self, adapter):
155
+ """Test converting left click action."""
156
+ data = {"action": "left_click", "coordinate": [150, 250]}
157
+ result = adapter.convert(data)
158
+
159
+ assert isinstance(result, ClickAction)
160
+ assert result.point is not None
161
+ assert result.point.x == 150
162
+ assert result.point.y == 250
163
+ assert result.button == "left"
164
+
165
+ def test_convert_right_click(self, adapter):
166
+ """Test converting right click action."""
167
+ data = {"action": "right_click", "coordinate": [300, 400]}
168
+ result = adapter.convert(data)
169
+
170
+ assert isinstance(result, ClickAction)
171
+ assert result.point is not None
172
+ assert result.point.x == 300
173
+ assert result.point.y == 400
174
+ assert result.button == "right"
175
+
176
+ def test_convert_middle_click(self, adapter):
177
+ """Test converting middle click action."""
178
+ data = {"action": "middle_click", "coordinate": [350, 450]}
179
+ result = adapter.convert(data)
180
+
181
+ assert isinstance(result, ClickAction)
182
+ assert result.point is not None
183
+ assert result.point.x == 350
184
+ assert result.point.y == 450
185
+ assert result.button == "middle"
186
+
187
+ def test_convert_double_click(self, adapter):
188
+ """Test converting double click action."""
189
+ data = {"action": "double_click", "coordinate": [200, 300]}
190
+ result = adapter.convert(data)
191
+
192
+ assert isinstance(result, ClickAction)
193
+ assert result.point is not None
194
+ assert result.point.x == 200
195
+ assert result.point.y == 300
196
+ assert result.button == "left"
197
+ assert result.pattern == [100]
198
+
199
+ def test_convert_triple_click(self, adapter):
200
+ """Test converting triple click action."""
201
+ data = {"action": "triple_click", "coordinate": [250, 350]}
202
+ result = adapter.convert(data)
203
+
204
+ assert isinstance(result, ClickAction)
205
+ assert result.point is not None
206
+ assert result.point.x == 250
207
+ assert result.point.y == 350
208
+ assert result.button == "left"
209
+ assert result.pattern == [100, 100]
210
+
211
+ def test_convert_left_click_drag_with_move_history(self, adapter):
212
+ """Test converting left click drag with move action in history."""
213
+ # First add a move action to memory
214
+ move_data = {"action": "mouse_move", "coordinate": [100, 200]}
215
+ adapter.adapt(move_data)
216
+
217
+ # Now test drag
218
+ drag_data = {"action": "left_click_drag", "coordinate": [300, 400]}
219
+ result = adapter.convert(drag_data)
220
+
221
+ assert isinstance(result, DragAction)
222
+ assert len(result.path) == 2
223
+ assert result.path[0].x == 100
224
+ assert result.path[0].y == 200
225
+ assert result.path[1].x == 300
226
+ assert result.path[1].y == 400
227
+
228
+ def test_convert_left_click_drag_with_click_history(self, adapter):
229
+ """Test converting left click drag with click action in history."""
230
+ # First add a click action to memory
231
+ click_data = {"action": "left_click", "coordinate": [150, 250]}
232
+ adapter.adapt(click_data)
233
+
234
+ # Now test drag
235
+ drag_data = {"action": "left_click_drag", "coordinate": [350, 450]}
236
+ result = adapter.convert(drag_data)
237
+
238
+ assert isinstance(result, DragAction)
239
+ assert len(result.path) == 2
240
+ assert result.path[0].x == 150
241
+ assert result.path[0].y == 250
242
+ assert result.path[1].x == 350
243
+ assert result.path[1].y == 450
244
+
245
+ def test_convert_left_click_drag_without_history(self, adapter):
246
+ """Test converting left click drag without proper history."""
247
+ data = {"action": "left_click_drag", "coordinate": [300, 400]}
248
+
249
+ with pytest.raises(ValueError) as exc_info:
250
+ adapter.convert(data)
251
+
252
+ assert "Left click drag must be preceded by a move or click action" in str(exc_info.value)
253
+
254
+ def test_convert_left_click_drag_with_invalid_history(self, adapter):
255
+ """Test converting left click drag with invalid history."""
256
+ # Add a type action (not move or click) to memory
257
+ type_data = {"action": "type", "text": "hello"}
258
+ adapter.adapt(type_data)
259
+
260
+ # Now test drag should fail
261
+ drag_data = {"action": "left_click_drag", "coordinate": [300, 400]}
262
+
263
+ with pytest.raises(ValueError) as exc_info:
264
+ adapter.convert(drag_data)
265
+
266
+ assert "Left click drag must be preceded by a move or click action" in str(exc_info.value)
267
+
268
+ def test_convert_scroll_up(self, adapter):
269
+ """Test converting scroll up action."""
270
+ data = {
271
+ "action": "scroll",
272
+ "coordinate": [500, 600],
273
+ "scroll_direction": "up",
274
+ "scroll_amount": 3,
275
+ }
276
+ result = adapter.convert(data)
277
+
278
+ assert isinstance(result, ScrollAction)
279
+ assert result.point is not None
280
+ assert result.scroll is not None
281
+ assert result.point.x == 500
282
+ assert result.point.y == 600
283
+ assert result.scroll.x == 0
284
+ assert result.scroll.y == -3
285
+
286
+ def test_convert_scroll_down(self, adapter):
287
+ """Test converting scroll down action."""
288
+ data = {
289
+ "action": "scroll",
290
+ "coordinate": [500, 600],
291
+ "scroll_direction": "down",
292
+ "scroll_amount": 5,
293
+ }
294
+ result = adapter.convert(data)
295
+
296
+ assert isinstance(result, ScrollAction)
297
+ assert result.point is not None
298
+ assert result.scroll is not None
299
+ assert result.point.x == 500
300
+ assert result.point.y == 600
301
+ assert result.scroll.x == 0
302
+ assert result.scroll.y == 5
303
+
304
+ def test_convert_scroll_left(self, adapter):
305
+ """Test converting scroll left action."""
306
+ data = {
307
+ "action": "scroll",
308
+ "coordinate": [500, 600],
309
+ "scroll_direction": "left",
310
+ "scroll_amount": 2,
311
+ }
312
+ result = adapter.convert(data)
313
+
314
+ assert isinstance(result, ScrollAction)
315
+ assert result.point is not None
316
+ assert result.scroll is not None
317
+ assert result.point.x == 500
318
+ assert result.point.y == 600
319
+ assert result.scroll.x == -2
320
+ assert result.scroll.y == 0
321
+
322
+ def test_convert_scroll_right(self, adapter):
323
+ """Test converting scroll right action."""
324
+ data = {
325
+ "action": "scroll",
326
+ "coordinate": [500, 600],
327
+ "scroll_direction": "right",
328
+ "scroll_amount": 4,
329
+ }
330
+ result = adapter.convert(data)
331
+
332
+ assert isinstance(result, ScrollAction)
333
+ assert result.point is not None
334
+ assert result.scroll is not None
335
+ assert result.point.x == 500
336
+ assert result.point.y == 600
337
+ assert result.scroll.x == 4
338
+ assert result.scroll.y == 0
339
+
340
+ def test_convert_scroll_invalid_direction(self, adapter):
341
+ """Test converting scroll with invalid direction."""
342
+ data = {
343
+ "action": "scroll",
344
+ "coordinate": [500, 600],
345
+ "scroll_direction": "diagonal",
346
+ "scroll_amount": 3,
347
+ }
348
+
349
+ with pytest.raises(ValueError) as exc_info:
350
+ adapter.convert(data)
351
+
352
+ assert "Unsupported scroll direction: diagonal" in str(exc_info.value)
353
+
354
+ def test_convert_scroll_missing_direction(self, adapter):
355
+ """Test converting scroll with missing direction."""
356
+ data = {"action": "scroll", "coordinate": [500, 600], "scroll_amount": 3}
357
+
358
+ with pytest.raises(ValueError) as exc_info:
359
+ adapter.convert(data)
360
+
361
+ assert "Invalid action" in str(exc_info.value)
362
+
363
+ def test_convert_screenshot(self, adapter):
364
+ """Test converting screenshot action."""
365
+ data = {"action": "screenshot"}
366
+ result = adapter.convert(data)
367
+
368
+ assert isinstance(result, ScreenshotFetch)
369
+
370
+ def test_convert_cursor_position(self, adapter):
371
+ """Test converting cursor position action."""
372
+ data = {"action": "cursor_position"}
373
+ result = adapter.convert(data)
374
+
375
+ assert isinstance(result, PositionFetch)
376
+
377
+ def test_convert_wait(self, adapter):
378
+ """Test converting wait action."""
379
+ data = {"action": "wait", "duration": 2500}
380
+ result = adapter.convert(data)
381
+
382
+ assert isinstance(result, WaitAction)
383
+ assert result.time == 2500
384
+
385
+ def test_convert_wait_missing_duration(self, adapter):
386
+ """Test converting wait action with missing duration."""
387
+ data = {"action": "wait"}
388
+
389
+ with pytest.raises(ValueError) as exc_info:
390
+ adapter.convert(data)
391
+
392
+ assert "Invalid action" in str(exc_info.value)
393
+
394
+ def test_convert_response(self, adapter):
395
+ """Test converting response action."""
396
+ data = {"action": "response", "text": "Task completed successfully"}
397
+ result = adapter.convert(data)
398
+
399
+ assert isinstance(result, ResponseAction)
400
+ assert result.text == "Task completed successfully"
401
+
402
+ def test_convert_response_default_text(self, adapter):
403
+ """Test converting response action with default text."""
404
+ data = {"action": "response"}
405
+ result = adapter.convert(data)
406
+
407
+ assert isinstance(result, ResponseAction)
408
+ assert result.text == ""
409
+
410
+ def test_convert_unsupported_action(self, adapter):
411
+ """Test converting unsupported action type."""
412
+ data = {"action": "unsupported_action"}
413
+
414
+ with pytest.raises(ValueError) as exc_info:
415
+ adapter.convert(data)
416
+
417
+ assert "Unsupported action type: unsupported_action" in str(exc_info.value)
418
+
419
+ def test_convert_missing_action_field(self, adapter):
420
+ """Test converting data without action field."""
421
+ data = {"text": "hello"} # Missing action
422
+
423
+ with pytest.raises(ValueError) as exc_info:
424
+ adapter.convert(data)
425
+
426
+ assert "Unsupported action type: None" in str(exc_info.value)
427
+
428
+ def test_convert_invalid_data_structure(self, adapter):
429
+ """Test converting invalid data structure."""
430
+ with pytest.raises(ValueError) as exc_info:
431
+ adapter.convert("invalid_data")
432
+
433
+ assert "Invalid action" in str(exc_info.value)
434
+
435
+ def test_convert_none_data(self, adapter):
436
+ """Test converting None data."""
437
+ with pytest.raises(ValueError) as exc_info:
438
+ adapter.convert(None)
439
+
440
+ assert "Invalid action" in str(exc_info.value)
441
+
442
+
443
+ class TestClaudeAdapterIntegration:
444
+ """Integration tests for ClaudeAdapter."""
445
+
446
+ @pytest.fixture
447
+ def adapter(self):
448
+ """Fixture providing a clean adapter instance."""
449
+ return ClaudeAdapter()
450
+
451
+ def test_full_click_pipeline(self, adapter):
452
+ """Test full click action processing pipeline."""
453
+ # Set adapter dimensions to avoid scaling
454
+ adapter.agent_width = 1920
455
+ adapter.agent_height = 1080
456
+ adapter.env_width = 1920
457
+ adapter.env_height = 1080
458
+
459
+ raw_action = {"action": "left_click", "coordinate": [100, 200]}
460
+
461
+ result = adapter.adapt(raw_action)
462
+
463
+ assert isinstance(result, ClickAction)
464
+ assert result.point is not None
465
+ assert result.point.x == 100
466
+ assert result.point.y == 200
467
+ assert result.button == "left"
468
+
469
+ # Check that it was added to memory
470
+ assert len(adapter.memory) == 1
471
+ assert adapter.memory[0] == result
472
+
473
+ def test_drag_sequence(self, adapter):
474
+ """Test complete drag sequence."""
475
+ # Set adapter dimensions to avoid scaling
476
+ adapter.agent_width = 1920
477
+ adapter.agent_height = 1080
478
+ adapter.env_width = 1920
479
+ adapter.env_height = 1080
480
+
481
+ # First move to start position
482
+ move_action = {"action": "mouse_move", "coordinate": [100, 200]}
483
+ move_result = adapter.adapt(move_action)
484
+
485
+ # Then drag to end position
486
+ drag_action = {"action": "left_click_drag", "coordinate": [300, 400]}
487
+ drag_result = adapter.adapt(drag_action)
488
+
489
+ assert isinstance(move_result, MoveAction)
490
+ assert isinstance(drag_result, DragAction)
491
+ assert len(drag_result.path) == 2
492
+ assert drag_result.path[0] == move_result.point
493
+
494
+ # Check memory contains both actions
495
+ assert len(adapter.memory) == 2
496
+
497
+ def test_complex_action_sequence(self, adapter):
498
+ """Test complex sequence of different actions."""
499
+ actions = [
500
+ {"action": "mouse_move", "coordinate": [100, 200]},
501
+ {"action": "left_click", "coordinate": [150, 250]},
502
+ {"action": "type", "text": "Hello"},
503
+ {"action": "key", "text": "ctrl+a"},
504
+ {"action": "wait", "duration": 1000},
505
+ {"action": "screenshot"},
506
+ ]
507
+
508
+ results = adapter.adapt_list(actions)
509
+
510
+ assert len(results) == 6
511
+ assert isinstance(results[0], MoveAction)
512
+ assert isinstance(results[1], ClickAction)
513
+ assert isinstance(results[2], TypeAction)
514
+ assert isinstance(results[3], PressAction)
515
+ assert isinstance(results[4], WaitAction)
516
+ assert isinstance(results[5], ScreenshotFetch)
517
+
518
+ # Check memory
519
+ assert len(adapter.memory) == 6
@@ -20,7 +20,7 @@ class Point(BaseModel):
20
20
  class ClickAction(CLAAction):
21
21
  type: Literal["click"] = "click"
22
22
  point: Point | None = None
23
- button: Literal["left", "right", "wheel", "back", "forward"] = "left"
23
+ button: CLAButton = "left"
24
24
  pattern: list[int] | None = None # [delay_1, delay_2, ...]
25
25
  hold_keys: list[CLAKey] | None = None
26
26
 
@@ -173,6 +173,7 @@ CLAKey: TypeAlias = Literal[
173
173
  "launchmail",
174
174
  "launchmediaselect",
175
175
  "playpause",
176
+ "start",
176
177
  "stop",
177
178
  "prevtrack",
178
179
  "nexttrack",
@@ -314,3 +315,6 @@ CLAKey: TypeAlias = Literal[
314
315
  "}",
315
316
  "~",
316
317
  ]
318
+
319
+
320
+ CLAButton: TypeAlias = Literal["left", "right", "middle", "back", "forward"]
@@ -4,6 +4,7 @@ from typing import Any, ClassVar
4
4
 
5
5
  from hud.adapters.common import CLA, Adapter
6
6
  from hud.adapters.common.types import (
7
+ CLAButton,
7
8
  CLAKey,
8
9
  ClickAction,
9
10
  DragAction,
@@ -27,6 +28,8 @@ class OperatorAdapter(Adapter):
27
28
  "arrowright": "right",
28
29
  }
29
30
 
31
+ BUTTON_MAP: ClassVar[dict[str, CLAButton]] = {"wheel": "middle"}
32
+
30
33
  def __init__(self) -> None:
31
34
  super().__init__()
32
35
  # OpenAI Computer Use default dimensions
@@ -45,6 +48,7 @@ class OperatorAdapter(Adapter):
45
48
  if action_type == "click":
46
49
  x, y = data.get("x", 0), data.get("y", 0)
47
50
  button = data.get("button", "left")
51
+ button = self.BUTTON_MAP.get(button, button)
48
52
  return ClickAction(point=Point(x=x, y=y), button=button)
49
53
 
50
54
  elif action_type == "double_click":
@@ -0,0 +1 @@
1
+ # Tests for hud.adapters.operator module