inspect-ai 0.3.71__py3-none-any.whl → 0.3.73__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.
Files changed (114) hide show
  1. inspect_ai/_cli/eval.py +14 -3
  2. inspect_ai/_cli/sandbox.py +3 -3
  3. inspect_ai/_cli/score.py +6 -4
  4. inspect_ai/_cli/trace.py +53 -6
  5. inspect_ai/_display/core/config.py +1 -1
  6. inspect_ai/_display/core/display.py +2 -1
  7. inspect_ai/_display/core/footer.py +6 -6
  8. inspect_ai/_display/plain/display.py +11 -6
  9. inspect_ai/_display/rich/display.py +23 -13
  10. inspect_ai/_display/textual/app.py +10 -9
  11. inspect_ai/_display/textual/display.py +2 -2
  12. inspect_ai/_display/textual/widgets/footer.py +4 -0
  13. inspect_ai/_display/textual/widgets/samples.py +14 -5
  14. inspect_ai/_eval/context.py +1 -2
  15. inspect_ai/_eval/eval.py +54 -41
  16. inspect_ai/_eval/loader.py +9 -2
  17. inspect_ai/_eval/run.py +148 -81
  18. inspect_ai/_eval/score.py +13 -8
  19. inspect_ai/_eval/task/images.py +31 -21
  20. inspect_ai/_eval/task/run.py +62 -59
  21. inspect_ai/_eval/task/rundir.py +16 -9
  22. inspect_ai/_eval/task/sandbox.py +7 -8
  23. inspect_ai/_eval/task/util.py +7 -0
  24. inspect_ai/_util/_async.py +118 -10
  25. inspect_ai/_util/constants.py +0 -2
  26. inspect_ai/_util/file.py +15 -29
  27. inspect_ai/_util/future.py +37 -0
  28. inspect_ai/_util/http.py +3 -99
  29. inspect_ai/_util/httpx.py +60 -0
  30. inspect_ai/_util/interrupt.py +2 -2
  31. inspect_ai/_util/json.py +5 -52
  32. inspect_ai/_util/logger.py +30 -86
  33. inspect_ai/_util/retry.py +10 -61
  34. inspect_ai/_util/trace.py +2 -2
  35. inspect_ai/_view/server.py +86 -3
  36. inspect_ai/_view/www/dist/assets/index.js +25837 -13269
  37. inspect_ai/_view/www/log-schema.json +253 -186
  38. inspect_ai/_view/www/package.json +2 -2
  39. inspect_ai/_view/www/src/plan/PlanDetailView.tsx +8 -3
  40. inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +2 -3
  41. inspect_ai/_view/www/src/types/log.d.ts +122 -94
  42. inspect_ai/approval/_human/manager.py +6 -10
  43. inspect_ai/approval/_human/panel.py +2 -2
  44. inspect_ai/dataset/_sources/util.py +7 -6
  45. inspect_ai/log/__init__.py +4 -0
  46. inspect_ai/log/_file.py +35 -61
  47. inspect_ai/log/_log.py +18 -1
  48. inspect_ai/log/_recorders/eval.py +14 -23
  49. inspect_ai/log/_recorders/json.py +3 -18
  50. inspect_ai/log/_samples.py +27 -2
  51. inspect_ai/log/_transcript.py +8 -8
  52. inspect_ai/model/__init__.py +2 -1
  53. inspect_ai/model/_call_tools.py +60 -40
  54. inspect_ai/model/_chat_message.py +3 -2
  55. inspect_ai/model/_generate_config.py +25 -0
  56. inspect_ai/model/_model.py +74 -36
  57. inspect_ai/model/_openai.py +9 -1
  58. inspect_ai/model/_providers/anthropic.py +172 -154
  59. inspect_ai/model/_providers/azureai.py +11 -9
  60. inspect_ai/model/_providers/bedrock.py +33 -24
  61. inspect_ai/model/_providers/cloudflare.py +8 -9
  62. inspect_ai/model/_providers/goodfire.py +7 -3
  63. inspect_ai/model/_providers/google.py +47 -13
  64. inspect_ai/model/_providers/groq.py +15 -15
  65. inspect_ai/model/_providers/hf.py +24 -17
  66. inspect_ai/model/_providers/mistral.py +36 -20
  67. inspect_ai/model/_providers/openai.py +30 -25
  68. inspect_ai/model/_providers/openai_o1.py +1 -1
  69. inspect_ai/model/_providers/providers.py +1 -1
  70. inspect_ai/model/_providers/together.py +3 -4
  71. inspect_ai/model/_providers/util/__init__.py +2 -2
  72. inspect_ai/model/_providers/util/chatapi.py +6 -19
  73. inspect_ai/model/_providers/util/hooks.py +165 -0
  74. inspect_ai/model/_providers/vertex.py +20 -3
  75. inspect_ai/model/_providers/vllm.py +16 -19
  76. inspect_ai/scorer/_multi.py +5 -2
  77. inspect_ai/solver/_bridge/patch.py +31 -1
  78. inspect_ai/solver/_fork.py +5 -3
  79. inspect_ai/solver/_human_agent/agent.py +3 -2
  80. inspect_ai/tool/__init__.py +8 -2
  81. inspect_ai/tool/_tool_info.py +4 -90
  82. inspect_ai/tool/_tool_params.py +4 -34
  83. inspect_ai/tool/_tools/_computer/_common.py +117 -58
  84. inspect_ai/tool/_tools/_computer/_computer.py +80 -57
  85. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/Code/User/settings.json +7 -1
  86. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml +91 -0
  87. inspect_ai/tool/_tools/_computer/_resources/tool/.pylintrc +8 -0
  88. inspect_ai/tool/_tools/_computer/_resources/tool/.vscode/settings.json +12 -0
  89. inspect_ai/tool/_tools/_computer/_resources/tool/_args.py +78 -0
  90. inspect_ai/tool/_tools/_computer/_resources/tool/_constants.py +20 -0
  91. inspect_ai/tool/_tools/_computer/_resources/tool/_x11_client.py +175 -113
  92. inspect_ai/tool/_tools/_computer/_resources/tool/computer_tool.py +76 -20
  93. inspect_ai/tool/_tools/_computer/_resources/tool/pyproject.toml +65 -0
  94. inspect_ai/tool/_tools/_computer/test_args.py +151 -0
  95. inspect_ai/tool/_tools/_web_search.py +30 -24
  96. inspect_ai/util/__init__.py +4 -0
  97. inspect_ai/util/_concurrency.py +5 -6
  98. inspect_ai/util/_display.py +6 -0
  99. inspect_ai/util/_json.py +170 -0
  100. inspect_ai/util/_sandbox/docker/cleanup.py +13 -9
  101. inspect_ai/util/_sandbox/docker/docker.py +5 -0
  102. inspect_ai/util/_sandbox/environment.py +56 -9
  103. inspect_ai/util/_sandbox/service.py +12 -5
  104. inspect_ai/util/_subprocess.py +94 -113
  105. inspect_ai/util/_subtask.py +2 -4
  106. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/METADATA +6 -2
  107. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/RECORD +111 -103
  108. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/WHEEL +1 -1
  109. inspect_ai/_util/timeouts.py +0 -160
  110. inspect_ai/model/_providers/util/tracker.py +0 -92
  111. inspect_ai/tool/_tools/_computer/_computer_split.py +0 -198
  112. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/LICENSE +0 -0
  113. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/entry_points.txt +0 -0
  114. {inspect_ai-0.3.71.dist-info → inspect_ai-0.3.73.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Based on https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/computer.py"""
1
+ """Inspired by https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/computer.py"""
2
2
 
3
3
  import asyncio
4
4
  import base64
@@ -19,21 +19,8 @@ TYPING_GROUP_SIZE = 50
19
19
 
20
20
  ColorCount = Literal[4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4]
21
21
 
22
- Action = Literal[
23
- "key",
24
- "type",
25
- "mouse_move",
26
- "left_click",
27
- "left_click_drag",
28
- "right_click",
29
- "middle_click",
30
- "double_click",
31
- "screenshot",
32
- "cursor_position",
33
- ]
34
-
35
-
36
- class ToolError(Exception):
22
+
23
+ class X11ClientError(Exception):
37
24
  def __init__(self, message):
38
25
  self.message = message
39
26
 
@@ -83,7 +70,7 @@ class X11Client:
83
70
 
84
71
  @property
85
72
  def options(self) -> ComputerToolOptions:
86
- width, height = self.scale_coordinates("computer", self.width, self.height)
73
+ width, height = self._scale_coordinates("computer", self.width, self.height)
87
74
  return {
88
75
  "display_width_px": width,
89
76
  "display_height_px": height,
@@ -105,120 +92,191 @@ class X11Client:
105
92
 
106
93
  self.xdotool = f"{self._display_prefix}xdotool"
107
94
 
108
- async def __call__(
95
+ async def key(self, text: str) -> ToolResult:
96
+ return await self._shell(f"{self.xdotool} key -- {_key_arg_for_text(text)}")
97
+
98
+ async def hold_key(self, text: str, duration: int) -> ToolResult:
99
+ key_arg = _key_arg_for_text(text)
100
+ await self._shell(f"{self.xdotool} keydown -- {key_arg}", False)
101
+ await asyncio.sleep(duration)
102
+ return await self._shell(f"{self.xdotool} keyup -- {key_arg}")
103
+
104
+ async def type(self, text: str) -> ToolResult:
105
+ results: list[ToolResult] = []
106
+ for chunk in chunks(text, TYPING_GROUP_SIZE):
107
+ cmd = (
108
+ f"{self.xdotool} type --delay {TYPING_DELAY_MS} -- {shlex.quote(chunk)}"
109
+ )
110
+ results.append(await self._shell(cmd, take_screenshot=False))
111
+
112
+ screenshot_base64 = await self._take_screenshot_after_delay()
113
+ return ToolResult(
114
+ output="".join(result.output or "" for result in results),
115
+ error="".join(result.error or "" for result in results),
116
+ base64_image=screenshot_base64,
117
+ )
118
+
119
+ async def cursor_position(self) -> ToolResult:
120
+ result = await self._shell(
121
+ f"{self.xdotool} getmouselocation --shell",
122
+ take_screenshot=False,
123
+ )
124
+ output = result.output or ""
125
+ x, y = self._scale_coordinates(
126
+ "computer",
127
+ int(output.split("X=")[1].split("\n")[0]),
128
+ int(output.split("Y=")[1].split("\n")[0]),
129
+ )
130
+ return result.replace(output=f"X={x},Y={y}")
131
+
132
+ async def left_mouse_down(self) -> ToolResult:
133
+ return await self._shell(f"{self.xdotool} mousedown 1")
134
+
135
+ async def left_mouse_up(self) -> ToolResult:
136
+ return await self._shell(f"{self.xdotool} mouseup 1")
137
+
138
+ async def mouse_move(self, coordinate: tuple[int, int]) -> ToolResult:
139
+ return await self._mouse_move_and("mouse_move", coordinate, None)
140
+
141
+ async def left_click(
142
+ self, coordinate: tuple[int, int] | None, text: str | None
143
+ ) -> ToolResult:
144
+ return await self._mouse_move_and("left_click", coordinate, text)
145
+
146
+ async def right_click(
147
+ self, coordinate: tuple[int, int] | None, text: str | None
148
+ ) -> ToolResult:
149
+ return await self._mouse_move_and("right_click", coordinate, text)
150
+
151
+ async def middle_click(
152
+ self, coordinate: tuple[int, int] | None, text: str | None
153
+ ) -> ToolResult:
154
+ return await self._mouse_move_and("middle_click", coordinate, text)
155
+
156
+ async def double_click(
157
+ self, coordinate: tuple[int, int] | None, text: str | None
158
+ ) -> ToolResult:
159
+ return await self._mouse_move_and("double_click", coordinate, text)
160
+
161
+ async def triple_click(
162
+ self, coordinate: tuple[int, int] | None, text: str | None
163
+ ) -> ToolResult:
164
+ return await self._mouse_move_and("triple_click", coordinate, text)
165
+
166
+ async def left_click_drag(
167
+ self, start_coordinate: tuple[int, int], coordinate: tuple[int, int]
168
+ ) -> ToolResult:
169
+ await self._move_mouse_to_coordinate(start_coordinate, False)
170
+ x, y = self._scale_coordinates("api", *coordinate)
171
+ return await self._shell(
172
+ f"{self.xdotool} mousedown 1 mousemove --sync {x} {y} mouseup 1"
173
+ )
174
+
175
+ async def scroll(
109
176
  self,
110
- *,
111
- action: Action,
112
- text: str | None = None,
113
- coordinate: tuple[int, int] | None = None,
114
- **kwargs,
115
- ):
116
- if action in ("mouse_move", "left_click_drag"):
117
- if coordinate is None:
118
- raise ToolError(f"coordinate is required for {action}")
119
- if text is not None:
120
- raise ToolError(f"text is not accepted for {action}")
121
- if not isinstance(coordinate, list) or len(coordinate) != 2:
122
- raise ToolError(f"{coordinate} must be a tuple of length 2")
123
- if not all(isinstance(i, int) and i >= 0 for i in coordinate):
124
- raise ToolError(f"{coordinate} must be a tuple of non-negative ints")
125
-
126
- x, y = self.scale_coordinates("api", coordinate[0], coordinate[1])
177
+ scroll_direction: Literal["up", "down", "left", "right"],
178
+ scroll_amount: int,
179
+ coordinate: tuple[int, int] | None,
180
+ text: str | None,
181
+ ) -> ToolResult:
182
+ if coordinate:
183
+ await self._move_mouse_to_coordinate(coordinate, False)
184
+ scroll_button = {
185
+ "up": 4,
186
+ "down": 5,
187
+ "left": 6,
188
+ "right": 7,
189
+ }[scroll_direction]
190
+
191
+ if text:
192
+ key_arg = _key_arg_for_text(text)
193
+ await self._shell(f"{self.xdotool} keydown -- {key_arg}", False)
194
+ await self._shell(
195
+ f"{self.xdotool} click --repeat {scroll_amount} {scroll_button}",
196
+ False,
197
+ )
198
+ return await self._shell(f"{self.xdotool} keyup -- {key_arg}")
199
+ else:
200
+ return await self._shell(
201
+ f"{self.xdotool} click --repeat {scroll_amount} {scroll_button}"
202
+ )
127
203
 
128
- if action == "mouse_move":
129
- return await self.shell(f"{self.xdotool} mousemove --sync {x} {y}")
130
- elif action == "left_click_drag":
131
- return await self.shell(
132
- f"{self.xdotool} mousedown 1 mousemove --sync {x} {y} mouseup 1"
133
- )
134
-
135
- if action in ("key", "type"):
136
- if text is None:
137
- raise ToolError(f"text is required for {action}")
138
- if coordinate is not None:
139
- raise ToolError(f"coordinate is not accepted for {action}")
140
- if not isinstance(text, str):
141
- raise ToolError(f"{text} must be a string")
142
-
143
- if action == "key":
144
- return await self.shell(
145
- f"{self.xdotool} key -- {' '.join(shlex.quote(part) for part in text.split())}"
146
- )
147
- elif action == "type":
148
- results: list[ToolResult] = []
149
- for chunk in chunks(text, TYPING_GROUP_SIZE):
150
- cmd = f"{self.xdotool} type --delay {TYPING_DELAY_MS} -- {shlex.quote(chunk)}"
151
- results.append(await self.shell(cmd, take_screenshot=False))
152
-
153
- screenshot_base64 = await self.take_screenshot_after_delay()
154
- return ToolResult(
155
- output="".join(result.output or "" for result in results),
156
- error="".join(result.error or "" for result in results),
157
- base64_image=screenshot_base64,
158
- )
159
-
160
- if action in (
204
+ async def wait(self, duration: int) -> ToolResult:
205
+ await asyncio.sleep(duration)
206
+ return await self.screenshot()
207
+
208
+ async def screenshot(self) -> ToolResult:
209
+ return await self._screenshot()
210
+
211
+ async def _mouse_move_and(
212
+ self,
213
+ action: Literal[
214
+ "mouse_move",
161
215
  "left_click",
162
216
  "right_click",
163
- "double_click",
164
217
  "middle_click",
165
- "screenshot",
166
- "cursor_position",
167
- ):
168
- if text is not None:
169
- raise ToolError(f"text is not accepted for {action}")
170
- if coordinate is not None:
171
- raise ToolError(f"coordinate is not accepted for {action}")
172
-
173
- if action == "screenshot":
174
- return await self.screenshot()
175
- elif action == "cursor_position":
176
- result = await self.shell(
177
- f"{self.xdotool} getmouselocation --shell",
178
- take_screenshot=False,
179
- )
180
- output = result.output or ""
181
- x, y = self.scale_coordinates(
182
- "computer",
183
- int(output.split("X=")[1].split("\n")[0]),
184
- int(output.split("Y=")[1].split("\n")[0]),
185
- )
186
- return result.replace(output=f"X={x},Y={y}")
187
- else:
188
- click_arg = {
189
- "left_click": "1",
190
- "right_click": "3",
191
- "middle_click": "2",
192
- "double_click": "--repeat 2 --delay 300 1",
193
- }[action]
194
- return await self.shell(f"{self.xdotool} click {click_arg}")
195
-
196
- raise ToolError(f"Invalid action: {action}")
197
-
198
- async def screenshot(self):
218
+ "double_click",
219
+ "triple_click",
220
+ ],
221
+ coordinate: tuple[int, int] | None,
222
+ text: str | None,
223
+ ):
224
+ should_move = action == "mouse_move" or coordinate
225
+ if should_move:
226
+ assert coordinate # coding/type safety error
227
+ move_result = await self._move_mouse_to_coordinate(
228
+ coordinate, action == "mouse_move"
229
+ )
230
+ if action == "mouse_move":
231
+ return move_result
232
+ click_arg = {
233
+ "left_click": "1",
234
+ "right_click": "3",
235
+ "middle_click": "2",
236
+ "double_click": "--repeat 2 --delay 300 1",
237
+ "triple_click": "--repeat 3 --delay 300 1",
238
+ }[action]
239
+
240
+ if text:
241
+ key_arg = _key_arg_for_text(text)
242
+ await self._shell(f"{self.xdotool} keydown -- {key_arg}", False)
243
+ await self._shell(f"{self.xdotool} click {click_arg}", False)
244
+ return await self._shell(f"{self.xdotool} keyup -- {key_arg}")
245
+ else:
246
+ return await self._shell(f"{self.xdotool} click {click_arg}")
247
+
248
+ async def _move_mouse_to_coordinate(
249
+ self, coordinate: tuple[int, int], take_screenshot: bool
250
+ ):
251
+ x, y = self._scale_coordinates("api", *coordinate)
252
+ return await self._shell(
253
+ f"{self.xdotool} mousemove --sync {x} {y}", take_screenshot=take_screenshot
254
+ )
255
+
256
+ async def _screenshot(self):
199
257
  """Take a screenshot of the current screen and return the base64 encoded image."""
200
258
  output_dir = Path(OUTPUT_DIR)
201
259
  output_dir.mkdir(parents=True, exist_ok=True)
202
260
  path = output_dir / f"screenshot_{uuid4().hex}.png"
203
261
 
204
- result = await self.shell(
262
+ result = await self._shell(
205
263
  f"{self._display_prefix}scrot --silent -p {path}", take_screenshot=False
206
264
  )
207
265
  if self._scaling_enabled:
208
- x, y = self.scale_coordinates("computer", self.width, self.height)
266
+ x, y = self._scale_coordinates("computer", self.width, self.height)
209
267
  convert_cmd = f"convert {path} -resize {x}x{y}!"
210
268
  if self.color_count is not None:
211
269
  convert_cmd += f" -colors {self.color_count}"
212
270
  convert_cmd += f" {path}"
213
- await self.shell(convert_cmd, take_screenshot=False)
271
+ await self._shell(convert_cmd, take_screenshot=False)
214
272
 
215
273
  if path.exists():
216
274
  return result.replace(
217
275
  base64_image=base64.b64encode(path.read_bytes()).decode()
218
276
  )
219
- raise ToolError(f"Failed to take screenshot: {result.error}")
277
+ raise X11ClientError(f"Failed to take screenshot: {result.error}")
220
278
 
221
- async def shell(self, command: str, take_screenshot=True) -> ToolResult:
279
+ async def _shell(self, command: str, take_screenshot=True) -> ToolResult:
222
280
  """Run a shell command and return the output, error, and optionally a screenshot."""
223
281
  logging.debug(f"running shell command {command}")
224
282
  _, stdout, stderr = await run(command)
@@ -226,17 +284,17 @@ class X11Client:
226
284
  return ToolResult(
227
285
  output=stdout,
228
286
  error=stderr,
229
- base64_image=(await self.take_screenshot_after_delay())
287
+ base64_image=(await self._take_screenshot_after_delay())
230
288
  if take_screenshot
231
289
  else None,
232
290
  )
233
291
 
234
- async def take_screenshot_after_delay(self) -> str:
292
+ async def _take_screenshot_after_delay(self) -> str:
235
293
  # delay to let things settle before taking a screenshot
236
294
  await asyncio.sleep(self._screenshot_delay)
237
- return (await self.screenshot()).base64_image
295
+ return (await self._screenshot()).base64_image
238
296
 
239
- def scale_coordinates(self, source: ScalingSource, x: int, y: int):
297
+ def _scale_coordinates(self, source: ScalingSource, x: int, y: int):
240
298
  """Scale coordinates to a target maximum resolution."""
241
299
  if not self._scaling_enabled:
242
300
  return x, y
@@ -255,8 +313,12 @@ class X11Client:
255
313
  y_scaling_factor = target_dimension["height"] / self.height
256
314
  if source == "api":
257
315
  if x > self.width or y > self.height:
258
- raise ToolError(f"Coordinates {x}, {y} are out of bounds")
316
+ raise X11ClientError(f"Coordinates {x}, {y} are out of bounds")
259
317
  # scale up
260
318
  return round(x / x_scaling_factor), round(y / y_scaling_factor)
261
319
  # scale down
262
320
  return round(x * x_scaling_factor), round(y * y_scaling_factor)
321
+
322
+
323
+ def _key_arg_for_text(text: str) -> str:
324
+ return " ".join(shlex.quote(part) for part in text.split())
@@ -1,15 +1,24 @@
1
- import argparse
2
1
  import asyncio
3
2
  import json
4
3
  import logging
5
4
  import os
6
5
  import sys
7
6
  import time
7
+ from argparse import Namespace
8
+ from typing import TypeVar
8
9
 
10
+ from _args import parse_arguments
11
+ from _constants import Action
9
12
  from _logger import setup_logger
10
13
  from _tool_result import ToolResult
11
14
  from _x11_client import X11Client
12
15
 
16
+
17
+ class ComputerToolError(Exception):
18
+ def __init__(self, message):
19
+ self.message = message
20
+
21
+
13
22
  # This is a bit sketchy. We really want to use relative imports here. Using absolute imports
14
23
  # works at runtime, but it prevents intellisense from working. However, when this folder is
15
24
  # copied to the container, by default relative imports won't work if this file is launched
@@ -44,29 +53,67 @@ def main():
44
53
  sys.exit(1)
45
54
 
46
55
 
47
- def parse_arguments():
48
- parser = argparse.ArgumentParser(description="Execute computer tool action")
49
- parser.add_argument("--action", type=str, required=True, help="Action to perform")
50
- parser.add_argument("--text", type=str, help="Optional text parameter")
51
- parser.add_argument(
52
- "--coordinate",
53
- type=int,
54
- nargs=2,
55
- help="Optional coordinate parameter as a list of two integers",
56
- )
57
- return parser.parse_args()
58
-
59
-
60
- async def execute_action(args) -> ToolResult:
56
+ async def execute_action(args: Namespace) -> ToolResult:
61
57
  # we can't do anything until X11 is ready to go.
62
58
  await wait_for_file("/tmp/xfce_started")
63
59
 
64
60
  computer = X11Client()
65
- return await computer(
66
- action=args.action,
67
- text=args.text,
68
- coordinate=args.coordinate if args.coordinate else None,
69
- )
61
+ action: Action = args.action
62
+ match action:
63
+ case "key":
64
+ return await computer.key(not_none(args.text, "text"))
65
+ case "hold_key":
66
+ return await computer.hold_key(
67
+ not_none(args.text, "text"), not_none(args.duration, "duration")
68
+ )
69
+ case "type":
70
+ return await computer.type(not_none(args.text, "text"))
71
+ case "cursor_position":
72
+ return await computer.cursor_position()
73
+ case "left_mouse_down":
74
+ return await computer.left_mouse_down()
75
+ case "left_mouse_up":
76
+ return await computer.left_mouse_up()
77
+ case "mouse_move":
78
+ return await computer.mouse_move(not_none(args.coordinate, "coordinate"))
79
+ case "left_click":
80
+ return await computer.left_click(
81
+ getattr(args, "coordinate", None), getattr(args, "text", None)
82
+ )
83
+ case "right_click":
84
+ return await computer.right_click(
85
+ getattr(args, "coordinate", None), getattr(args, "text", None)
86
+ )
87
+ case "middle_click":
88
+ return await computer.middle_click(
89
+ getattr(args, "coordinate", None), getattr(args, "text", None)
90
+ )
91
+ case "double_click":
92
+ return await computer.double_click(
93
+ getattr(args, "coordinate", None), getattr(args, "text", None)
94
+ )
95
+ case "triple_click":
96
+ return await computer.triple_click(
97
+ getattr(args, "coordinate", None), getattr(args, "text", None)
98
+ )
99
+ case "left_click_drag":
100
+ return await computer.left_click_drag(
101
+ not_none(args.start_coordinate, "start_coordinate"),
102
+ not_none(args.coordinate, "coordinate"),
103
+ )
104
+ case "scroll":
105
+ return await computer.scroll(
106
+ not_none(args.scroll_direction, "scroll_direction"),
107
+ not_none(args.scroll_amount, "scroll_amount"),
108
+ getattr(args, "coordinate", None),
109
+ getattr(args, "text", None),
110
+ )
111
+ case "wait":
112
+ return await computer.wait(not_none(args.duration, "duration"))
113
+ case "screenshot":
114
+ return await computer.screenshot()
115
+
116
+ raise ComputerToolError(f"Invalid action: {action}")
70
117
 
71
118
 
72
119
  async def wait_for_file(file_path, check_interval=1):
@@ -81,5 +128,14 @@ async def wait_for_file(file_path, check_interval=1):
81
128
  )
82
129
 
83
130
 
131
+ T = TypeVar("T")
132
+
133
+
134
+ def not_none(value: T | None, name: str) -> T:
135
+ if value is None:
136
+ raise ComputerToolError(f"{name} must be provided")
137
+ return value
138
+
139
+
84
140
  if __name__ == "__main__":
85
141
  main()
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "setuptools_scm[toml]>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools_scm]
6
+
7
+ [tool.setuptools.packages.find]
8
+ where = ["."]
9
+ include = ["inspect_ai*"]
10
+
11
+ [tool.ruff]
12
+ src = ["."]
13
+
14
+ [tool.ruff.lint]
15
+ select = [
16
+ "E", # pycodestyle errors
17
+ "W", # pycodestyle warnings
18
+ "F", # flake8
19
+ "D", # pydocstyle
20
+ "I", # isort
21
+ "SIM101", # duplicate isinstance
22
+ "UP038", # non-pep604-isinstance
23
+ # "RET", # flake8-return
24
+ # "RUF", # ruff rules
25
+ ]
26
+ ignore = ["E203", "E501", "D10", "D212", "D415"]
27
+
28
+ [tool.ruff.lint.pydocstyle]
29
+ convention = "google"
30
+
31
+ [tool.pytest.ini_options]
32
+ minversion = "7.0"
33
+ addopts = "-rA --doctest-modules --color=yes"
34
+ doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"]
35
+ asyncio_mode = "auto"
36
+ asyncio_default_fixture_loop_scope = "function"
37
+ log_level = "warning"
38
+
39
+ [tool.mypy]
40
+ warn_unused_ignores = true
41
+ no_implicit_reexport = true
42
+ strict_equality = true
43
+ warn_redundant_casts = true
44
+ warn_unused_configs = true
45
+ disallow_any_explicit = true
46
+ disallow_any_generics = true
47
+ disallow_subclassing_any = true
48
+ plugins=["pydantic.mypy"]
49
+
50
+
51
+ [tool.pydantic-mypy]
52
+ init_forbid_extra = true
53
+ init_typed = true
54
+
55
+ [tool.check-wheel-contents]
56
+ ignore = ["W002", "W009"]
57
+
58
+ [project]
59
+ name = "web_browser_tool_container"
60
+ requires-python = ">=3.10"
61
+ dynamic = ["version", "dependencies"]
62
+
63
+
64
+ [project.optional-dependencies]
65
+ dev = ["pytest"]