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.

Files changed (59) hide show
  1. hud/__init__.py +5 -3
  2. hud/adapters/__init__.py +2 -1
  3. hud/adapters/claude/adapter.py +13 -17
  4. hud/adapters/common/adapter.py +3 -3
  5. hud/adapters/common/tests/__init__.py +0 -0
  6. hud/adapters/common/tests/test_adapter.py +277 -0
  7. hud/adapters/common/types.py +3 -6
  8. hud/adapters/operator/adapter.py +22 -29
  9. hud/agent/__init__.py +9 -1
  10. hud/agent/base.py +28 -28
  11. hud/agent/claude.py +69 -60
  12. hud/agent/langchain.py +204 -0
  13. hud/agent/operator.py +75 -67
  14. hud/env/__init__.py +5 -5
  15. hud/env/client.py +2 -2
  16. hud/env/docker_client.py +37 -39
  17. hud/env/environment.py +91 -66
  18. hud/env/local_docker_client.py +5 -7
  19. hud/env/remote_client.py +40 -29
  20. hud/env/remote_docker_client.py +13 -3
  21. hud/evaluators/__init__.py +2 -3
  22. hud/evaluators/base.py +4 -3
  23. hud/evaluators/inspect.py +3 -8
  24. hud/evaluators/judge.py +34 -58
  25. hud/evaluators/match.py +42 -49
  26. hud/evaluators/remote.py +13 -26
  27. hud/evaluators/tests/__init__.py +0 -0
  28. hud/evaluators/tests/test_inspect.py +12 -0
  29. hud/evaluators/tests/test_judge.py +231 -0
  30. hud/evaluators/tests/test_match.py +115 -0
  31. hud/evaluators/tests/test_remote.py +98 -0
  32. hud/exceptions.py +167 -0
  33. hud/gym.py +12 -10
  34. hud/job.py +525 -47
  35. hud/server/__init__.py +2 -2
  36. hud/server/requests.py +148 -186
  37. hud/server/tests/__init__.py +0 -0
  38. hud/server/tests/test_requests.py +275 -0
  39. hud/settings.py +3 -2
  40. hud/task.py +12 -22
  41. hud/taskset.py +44 -11
  42. hud/trajectory.py +6 -9
  43. hud/types.py +14 -9
  44. hud/utils/__init__.py +2 -2
  45. hud/utils/common.py +37 -13
  46. hud/utils/config.py +44 -29
  47. hud/utils/progress.py +149 -0
  48. hud/utils/telemetry.py +10 -11
  49. hud/utils/tests/__init__.py +0 -0
  50. hud/utils/tests/test_common.py +52 -0
  51. hud/utils/tests/test_config.py +129 -0
  52. hud/utils/tests/test_progress.py +225 -0
  53. hud/utils/tests/test_telemetry.py +37 -0
  54. hud/utils/tests/test_version.py +8 -0
  55. {hud_python-0.2.1.dist-info → hud_python-0.2.3.dist-info}/METADATA +44 -21
  56. hud_python-0.2.3.dist-info/RECORD +62 -0
  57. hud_python-0.2.1.dist-info/RECORD +0 -44
  58. {hud_python-0.2.1.dist-info → hud_python-0.2.3.dist-info}/WHEEL +0 -0
  59. {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, job, load_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.1"
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"]
@@ -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
- "Return": "enter",
27
- "Super": "win",
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
@@ -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
@@ -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 # The final textual response from the agent
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",
@@ -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
- "Return": "enter",
24
- "ArrowUp": "up",
25
- "ArrowDown": "down",
26
- "ArrowLeft": "left",
27
- "ArrowRight": "right",
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
- point=Point(x=x, y=y),
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
- point=Point(x=x, y=y),
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__ = ["Agent", "ClaudeAgent", "OperatorAgent", "OperatorAdapter", "ClaudeAdapter"]
8
+ __all__ = [
9
+ "Agent",
10
+ "ClaudeAgent",
11
+ "OperatorAgent",
12
+ "OperatorAdapter",
13
+ "ClaudeAdapter",
14
+ "LangchainAgent",
15
+ ]