inspect-ai 0.3.75__py3-none-any.whl → 0.3.76__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 (72) hide show
  1. inspect_ai/_eval/evalset.py +3 -2
  2. inspect_ai/_eval/registry.py +3 -5
  3. inspect_ai/_eval/run.py +4 -0
  4. inspect_ai/_eval/task/run.py +4 -0
  5. inspect_ai/_util/logger.py +3 -0
  6. inspect_ai/_view/www/dist/assets/index.css +28 -16
  7. inspect_ai/_view/www/dist/assets/index.js +4801 -4615
  8. inspect_ai/_view/www/log-schema.json +79 -9
  9. inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
  10. inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
  11. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
  12. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
  13. inspect_ai/_view/www/src/types/log.d.ts +11 -5
  14. inspect_ai/log/_recorders/json.py +8 -0
  15. inspect_ai/log/_transcript.py +13 -4
  16. inspect_ai/model/_call_tools.py +13 -4
  17. inspect_ai/model/_chat_message.py +3 -0
  18. inspect_ai/model/_model.py +5 -1
  19. inspect_ai/model/_model_output.py +6 -1
  20. inspect_ai/model/_openai.py +11 -6
  21. inspect_ai/model/_providers/anthropic.py +133 -75
  22. inspect_ai/model/_providers/openai.py +11 -8
  23. inspect_ai/model/_providers/vertex.py +5 -2
  24. inspect_ai/tool/__init__.py +4 -0
  25. inspect_ai/tool/_tool_call.py +5 -2
  26. inspect_ai/tool/_tool_support_helpers.py +200 -0
  27. inspect_ai/tool/_tools/_bash_session.py +119 -0
  28. inspect_ai/tool/_tools/_computer/_computer.py +1 -1
  29. inspect_ai/tool/_tools/_text_editor.py +121 -0
  30. inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
  31. inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
  32. inspect_ai/tool/_tools/_web_search.py +1 -1
  33. inspect_ai/util/_json.py +28 -0
  34. inspect_ai/util/_sandbox/context.py +16 -7
  35. inspect_ai/util/_sandbox/docker/config.py +1 -1
  36. inspect_ai/util/_sandbox/docker/internal.py +3 -3
  37. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info}/METADATA +5 -2
  38. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info}/RECORD +42 -68
  39. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info}/WHEEL +1 -1
  40. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
  41. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
  42. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
  43. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
  44. inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
  45. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
  46. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
  47. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
  48. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
  49. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
  50. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
  51. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
  52. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
  53. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
  54. inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
  55. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
  56. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
  57. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
  58. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
  59. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
  60. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
  61. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
  62. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
  63. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
  64. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
  65. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
  66. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
  67. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
  68. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
  69. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
  70. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info}/entry_points.txt +0 -0
  71. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info/licenses}/LICENSE +0 -0
  72. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.76.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,119 @@
1
+ from pydantic import BaseModel, Field, RootModel
2
+
3
+ from inspect_ai.tool import ToolResult
4
+ from inspect_ai.tool._tool_support_helpers import (
5
+ exec_sandbox_rpc,
6
+ tool_container_sandbox,
7
+ )
8
+ from inspect_ai.util import StoreModel, store_as
9
+
10
+ from .._tool import Tool, ToolParsingError, tool
11
+ from .._tool_call import ToolCall, ToolCallContent, ToolCallView, ToolCallViewer
12
+
13
+
14
+ # These models are cloned from the container code. If/when we decide to create
15
+ # a package that is shared between the inspect and tool-container codebases, we'll
16
+ # just have to live with it.
17
+ class NewSessionResult(BaseModel):
18
+ session_name: str
19
+
20
+
21
+ class BashRestartResult(BaseModel):
22
+ pass
23
+
24
+
25
+ class BashCommandResult(BaseModel):
26
+ status: int
27
+ stdout: str
28
+ stderr: str
29
+
30
+
31
+ class BashResult(RootModel[BashRestartResult | BashCommandResult]):
32
+ pass
33
+
34
+
35
+ class BashSessionStore(StoreModel):
36
+ session_id: str = Field(default_factory=str)
37
+
38
+
39
+ # custom viewer for bash
40
+ def code_viewer(language: str, code_param: str) -> ToolCallViewer:
41
+ def viewer(tool_call: ToolCall) -> ToolCallView:
42
+ code = tool_call.arguments.get(code_param, None)
43
+ code = (code or tool_call.function).strip()
44
+ call = ToolCallContent(
45
+ title=language,
46
+ format="markdown",
47
+ content=f"```{language}\n" + code + "\n```\n",
48
+ )
49
+ return ToolCallView(call=call)
50
+
51
+ return viewer
52
+
53
+
54
+ @tool(viewer=code_viewer("bash", "command"))
55
+ def bash_session(timeout: int | None = None) -> Tool:
56
+ """Bash shell session command execution tool.
57
+
58
+ Execute bash shell commands in a long running session using a sandbox environment (e.g. "docker").
59
+
60
+ Args:
61
+ timeout: Timeout (in seconds) for command.
62
+
63
+ Returns:
64
+ String with command output (stdout) or command error (stderr).
65
+ """
66
+
67
+ async def execute(
68
+ command: str | None = None,
69
+ restart: bool | None = None,
70
+ ) -> ToolResult:
71
+ """
72
+ Use this function to execute bash commands.
73
+
74
+ Args:
75
+ command: The bash command to run. Required unless the tool is being restarted.
76
+ restart: Specifying true will restart this tool. Otherwise, leave this unspecified.
77
+
78
+ Returns:
79
+ The output of the command.
80
+ """
81
+ if not ((command is None) ^ (restart is None)):
82
+ raise ToolParsingError(
83
+ "Either 'command' or 'restart' must be specified, but not both."
84
+ )
85
+ params: dict[str, object] = {"command": command, "restart": restart}
86
+
87
+ sandbox = await tool_container_sandbox("bash session")
88
+ store = store_as(BashSessionStore)
89
+
90
+ if not store.session_id:
91
+ store.session_id = (
92
+ await exec_sandbox_rpc(
93
+ sandbox,
94
+ "bash_session_new_session",
95
+ {},
96
+ NewSessionResult,
97
+ timeout=timeout,
98
+ )
99
+ ).session_name
100
+
101
+ params["session_name"] = store.session_id
102
+
103
+ result = (
104
+ await exec_sandbox_rpc(
105
+ sandbox,
106
+ "bash_session",
107
+ params,
108
+ BashResult,
109
+ timeout=timeout,
110
+ )
111
+ ).root
112
+
113
+ if isinstance(result, BashRestartResult):
114
+ return "Bash session restarted."
115
+
116
+ # return output (including stderr if any)
117
+ return f"{result.stderr}\n{result.stdout}" if result.stderr else result.stdout
118
+
119
+ return execute
@@ -15,7 +15,7 @@ ActionFunction = Callable[[str], ToolResult | Awaitable[ToolResult]]
15
15
  def computer(max_screenshots: int | None = 1, timeout: int | None = 180) -> Tool:
16
16
  """Desktop computer tool.
17
17
 
18
- See documentation at <https://inspect.aisi.org.uk/tools.html#sec-computer>.
18
+ See documentation at <https://inspect.aisi.org.uk/tools-standard.html#sec-computer>.
19
19
 
20
20
  Args:
21
21
  max_screenshots: The maximum number of screenshots to play
@@ -0,0 +1,121 @@
1
+ import inspect
2
+ from typing import Annotated, Literal
3
+
4
+ from pydantic import BaseModel, Discriminator, RootModel
5
+
6
+ from inspect_ai.tool import ToolResult
7
+ from inspect_ai.tool._tool_support_helpers import (
8
+ exec_sandbox_rpc,
9
+ tool_container_sandbox,
10
+ )
11
+
12
+ from .._tool import Tool, tool
13
+
14
+ # These models are cloned from the container code. If/when we decide to create
15
+ # a package that is shared between the inspect and tool-container codebases, we'll
16
+ # just have to live with it.
17
+
18
+
19
+ class BaseParams(BaseModel):
20
+ path: str
21
+
22
+
23
+ class ViewParams(BaseParams):
24
+ command: Literal["view"] = "view"
25
+ view_range: list[int] | None = None
26
+
27
+
28
+ class CreateParams(BaseParams):
29
+ command: Literal["create"] = "create"
30
+ file_text: str
31
+
32
+
33
+ class StrReplaceParams(BaseParams):
34
+ command: Literal["str_replace"] = "str_replace"
35
+ old_str: str
36
+ new_str: str | None = None
37
+
38
+
39
+ class InsertParams(BaseParams):
40
+ command: Literal["insert"] = "insert"
41
+ insert_line: int
42
+ new_str: str
43
+
44
+
45
+ class UndoEditParams(BaseParams):
46
+ command: Literal["undo_edit"] = "undo_edit"
47
+
48
+
49
+ class TextEditorParams(
50
+ RootModel[
51
+ ViewParams | CreateParams | StrReplaceParams | InsertParams | UndoEditParams
52
+ ]
53
+ ):
54
+ root: Annotated[
55
+ ViewParams | CreateParams | StrReplaceParams | InsertParams | UndoEditParams,
56
+ Discriminator("command"),
57
+ ]
58
+
59
+
60
+ TextEditorResult = str
61
+
62
+
63
+ @tool()
64
+ def text_editor(timeout: int | None = None, user: str | None = None) -> Tool:
65
+ """Custom editing tool for viewing, creating and editing files.
66
+
67
+ Perform text editor operations using a sandbox environment (e.g. "docker").
68
+
69
+ IMPORTANT: This tool does not currently support Subtask isolation. This means
70
+ that a change made to a file by on Subtask will be visible to another Subtask.
71
+
72
+ Args:
73
+ timeout: Timeout (in seconds) for command.
74
+ user: User to execute commands as.
75
+
76
+ Returns:
77
+ String with command output (stdout) or command error (stderr).
78
+ """
79
+
80
+ async def execute(
81
+ command: Literal["view", "create", "str_replace", "insert", "undo_edit"],
82
+ path: str,
83
+ file_text: str | None = None,
84
+ insert_line: int | None = None,
85
+ new_str: str | None = None,
86
+ old_str: str | None = None,
87
+ view_range: list[int] | None = None,
88
+ ) -> ToolResult:
89
+ """
90
+ Use this function to execute text editing commands.
91
+
92
+ Args:
93
+ command: The command to execute.
94
+ path: Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.
95
+ file_text: Required parameter of `create` command, with the content of the file to be created.
96
+ insert_line: Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.
97
+ new_str: Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.
98
+ old_str: Required parameter of `str_replace` command containing the string in `path` to replace.
99
+ view_range: Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.
100
+
101
+ Returns:
102
+ The output of the command.
103
+ """
104
+ sandbox = await tool_container_sandbox("editor")
105
+
106
+ # Create a dictionary of the parameters
107
+ params = {
108
+ k: v
109
+ for k, v in locals().items()
110
+ if k in inspect.signature(execute).parameters
111
+ }
112
+
113
+ return await exec_sandbox_rpc(
114
+ sandbox,
115
+ "text_editor",
116
+ params,
117
+ TextEditorResult,
118
+ timeout=timeout,
119
+ )
120
+
121
+ return execute
@@ -0,0 +1,150 @@
1
+ """This module provides the "old" client code for running against the, now deprecated, `aisiuk/inspect-web-browser-tool` image."""
2
+
3
+ import re
4
+ from logging import getLogger
5
+ from textwrap import dedent
6
+
7
+ from pydantic import Field
8
+
9
+ from inspect_ai._util.content import ContentText
10
+ from inspect_ai._util.error import PrerequisiteError
11
+ from inspect_ai._util.logger import warn_once
12
+ from inspect_ai.tool import ToolError, ToolResult
13
+ from inspect_ai.util import SandboxEnvironment, StoreModel, sandbox_with, store_as
14
+ from inspect_ai.util._sandbox.docker.internal import (
15
+ INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB_DEPRECATED,
16
+ )
17
+
18
+ logger = getLogger("web_browser")
19
+
20
+ WEB_CLIENT_REQUEST = "/app/web_browser/web_client.py"
21
+ WEB_CLIENT_NEW_SESSION = "/app/web_browser/web_client_new_session.py"
22
+
23
+
24
+ class WebBrowserStore(StoreModel):
25
+ main_content: str = Field(default_factory=str)
26
+ web_at: str = Field(default_factory=str)
27
+ session_id: str = Field(default_factory=str)
28
+
29
+
30
+ async def old_web_browser_cmd(cmd: str, *args: str) -> ToolResult:
31
+ sandbox_env = await _web_browser_sandbox()
32
+ warn_once(
33
+ logger,
34
+ "WARNING: Use of the `aisiuk/inspect-web-browser-tool` image is deprecated. Please update your configuration to use the `aisiuk/inspect-tool-support` image or install the `inspect-tool-support` package into your own image.",
35
+ )
36
+
37
+ store = store_as(WebBrowserStore)
38
+ if not store.session_id:
39
+ result = await sandbox_env.exec(
40
+ ["python3", WEB_CLIENT_NEW_SESSION], timeout=180
41
+ )
42
+
43
+ if not result.success:
44
+ raise RuntimeError(
45
+ f"Error creating new web browser session: {result.stderr}"
46
+ )
47
+
48
+ store.session_id = result.stdout.strip("\n")
49
+
50
+ session_flag = f"--session_name={store.session_id}"
51
+
52
+ arg_list = None
53
+ if session_flag:
54
+ arg_list = ["python3", WEB_CLIENT_REQUEST, session_flag, cmd] + list(args)
55
+ else:
56
+ arg_list = ["python3", WEB_CLIENT_REQUEST, cmd] + list(args)
57
+
58
+ result = await sandbox_env.exec(arg_list, timeout=180)
59
+ if not result.success:
60
+ raise RuntimeError(
61
+ f"Error executing web browser command {cmd}({', '.join(args)}): {result.stderr}"
62
+ )
63
+ else:
64
+ response = _parse_web_browser_output(result.stdout)
65
+ if "error" in response and response.get("error", "").strip() != "":
66
+ raise ToolError(str(response.get("error")) or "(unknown error)")
67
+ elif "web_at" in response:
68
+ main_content = str(response.get("main_content")) or None
69
+ web_at = (
70
+ str(response.get("web_at")) or "(no web accessibility tree available)"
71
+ )
72
+ # Remove base64 data from images.
73
+ web_at_lines = web_at.split("\n")
74
+ web_at_lines = [
75
+ line.partition("data:image/png;base64")[0] for line in web_at_lines
76
+ ]
77
+
78
+ store_as(WebBrowserStore).main_content = (
79
+ main_content or "(no main text summary)"
80
+ )
81
+ store_as(WebBrowserStore).web_at = web_at
82
+
83
+ web_at = "\n".join(web_at_lines)
84
+ return (
85
+ [
86
+ ContentText(text=f"main content:\n{main_content}\n\n"),
87
+ ContentText(text=f"accessibility tree:\n{web_at}"),
88
+ ]
89
+ if main_content
90
+ else web_at
91
+ )
92
+ else:
93
+ raise RuntimeError(
94
+ f"web_browser output must contain either 'error' or 'web_at' field: {result.stdout}"
95
+ )
96
+
97
+
98
+ async def _web_browser_sandbox() -> SandboxEnvironment:
99
+ sb = await sandbox_with(WEB_CLIENT_REQUEST)
100
+ if sb:
101
+ return sb
102
+ else:
103
+ msg = dedent(f"""
104
+ The web browser service was not found in any of the sandboxes for this sample. Please add the web browser service to your configuration. For example, the following Docker compose file uses the {INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB_DEPRECATED} image as its default sandbox:
105
+
106
+ services:
107
+ default:
108
+ image: "{INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB_DEPRECATED}"
109
+ init: true
110
+
111
+ Alternatively, this Docker compose file creates a dedicated image for the web browser service:
112
+
113
+ services:
114
+ default:
115
+ image: "python:3.12-bookworm"
116
+ init: true
117
+ command: "tail -f /dev/null"
118
+
119
+ web_browser:
120
+ image: "{INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB_DEPRECATED}"
121
+ init: true
122
+ """).strip()
123
+ raise PrerequisiteError(msg)
124
+
125
+
126
+ def _parse_web_browser_output(output: str) -> dict[str, str]:
127
+ response: dict[str, str] = dict(
128
+ web_url="", main_content="", web_at="", info="", error=""
129
+ )
130
+ active_field: str | None = None
131
+ active_field_lines: list[str] = []
132
+
133
+ def collect_active_field() -> None:
134
+ if active_field is not None:
135
+ response[active_field] = "\n".join(active_field_lines)
136
+ active_field_lines.clear()
137
+
138
+ for line in output.splitlines():
139
+ field_match = re.match(
140
+ r"^(error|main_content|web_at|web_url|info)\s*:\s*(.+)$", line
141
+ )
142
+ if field_match:
143
+ collect_active_field()
144
+ active_field = field_match.group(1)
145
+ active_field_lines.append(field_match.group(2))
146
+ else:
147
+ active_field_lines.append(line)
148
+ collect_active_field()
149
+
150
+ return response
@@ -1,23 +1,40 @@
1
1
  import re
2
- from textwrap import dedent
3
2
 
4
- from pydantic import Field
3
+ from pydantic import BaseModel, Field
5
4
 
6
5
  from inspect_ai._util.content import ContentText
7
6
  from inspect_ai._util.error import PrerequisiteError
8
7
  from inspect_ai.tool._tool import Tool, ToolError, ToolResult, tool
9
8
  from inspect_ai.tool._tool_call import ToolCall, ToolCallContent, ToolCallView
10
9
  from inspect_ai.tool._tool_info import parse_tool_info
10
+ from inspect_ai.tool._tool_support_helpers import (
11
+ exec_sandbox_rpc,
12
+ tool_container_sandbox,
13
+ )
11
14
  from inspect_ai.tool._tool_with import tool_with
12
- from inspect_ai.util._sandbox import SandboxEnvironment, sandbox_with
13
- from inspect_ai.util._sandbox.docker.internal import INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB
14
15
  from inspect_ai.util._store_model import StoreModel, store_as
15
16
 
17
+ from ._back_compat import old_web_browser_cmd
18
+
19
+
20
+ # These two models are cloned from the container code. If/when we decide to create
21
+ # a package that is shared between the inspect and tool-container codebases, we'll
22
+ # just have to live with it.
23
+ class NewSessionResult(BaseModel):
24
+ session_name: str
25
+
26
+
27
+ class CrawlerResult(BaseModel):
28
+ web_url: str
29
+ main_content: str | None = None
30
+ web_at: str
31
+ error: str | None = None
32
+
16
33
 
17
34
  def web_browser(interactive: bool = True) -> list[Tool]:
18
35
  """Tools used for web browser navigation.
19
36
 
20
- See documentation at <https://inspect.aisi.org.uk/tools.html#sec-web-browser>.
37
+ See documentation at <https://inspect.aisi.org.uk/tools-standard.html#sec-web-browser>.
21
38
 
22
39
  Args:
23
40
  interactive: Provide interactive tools (enable
@@ -85,7 +102,7 @@ def web_browser_go() -> Tool:
85
102
  Returns:
86
103
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
87
104
  """
88
- return await web_browser_cmd("web_go", url)
105
+ return await _web_browser_cmd("web_go", locals())
89
106
 
90
107
  return execute
91
108
 
@@ -165,7 +182,7 @@ def web_browser_click() -> Tool:
165
182
  Returns:
166
183
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
167
184
  """
168
- return await web_browser_cmd("web_click", str(element_id))
185
+ return await _web_browser_cmd("web_click", locals())
169
186
 
170
187
  return execute
171
188
 
@@ -203,7 +220,7 @@ def web_browser_type_submit() -> Tool:
203
220
  Returns:
204
221
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
205
222
  """
206
- return await web_browser_cmd("web_type_submit", str(element_id), text)
223
+ return await _web_browser_cmd("web_type_submit", locals())
207
224
 
208
225
  return execute
209
226
 
@@ -241,7 +258,7 @@ def web_browser_type() -> Tool:
241
258
  Returns:
242
259
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
243
260
  """
244
- return await web_browser_cmd("web_type", str(element_id), text)
261
+ return await _web_browser_cmd("web_type", locals())
245
262
 
246
263
  return execute
247
264
 
@@ -271,7 +288,7 @@ def web_browser_scroll() -> Tool:
271
288
  Returns:
272
289
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
273
290
  """
274
- return await web_browser_cmd("web_scroll", direction)
291
+ return await _web_browser_cmd("web_scroll", locals())
275
292
 
276
293
  return execute
277
294
 
@@ -292,7 +309,7 @@ def web_browser_back() -> Tool:
292
309
  Returns:
293
310
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
294
311
  """
295
- return await web_browser_cmd("web_back")
312
+ return await _web_browser_cmd("web_back", locals())
296
313
 
297
314
  return execute
298
315
 
@@ -313,7 +330,7 @@ def web_browser_forward() -> Tool:
313
330
  Returns:
314
331
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
315
332
  """
316
- return await web_browser_cmd("web_forward")
333
+ return await _web_browser_cmd("web_forward", locals())
317
334
 
318
335
  return execute
319
336
 
@@ -334,133 +351,61 @@ def web_browser_refresh() -> Tool:
334
351
  Returns:
335
352
  Web accessibility tree of the visible elements of the web page. The element_id of each element is displayed in brackets at the beginning of the line.
336
353
  """
337
- return await web_browser_cmd("web_refresh")
354
+ return await _web_browser_cmd("web_refresh", locals())
338
355
 
339
356
  return execute
340
357
 
341
358
 
342
- WEB_CLIENT_REQUEST = "/app/web_browser/web_client.py"
343
- WEB_CLIENT_NEW_SESSION = "/app/web_browser/web_client_new_session.py"
344
-
345
-
346
- async def web_browser_cmd(cmd: str, *args: str) -> ToolResult:
347
- sandbox_env = await sandbox_with(WEB_CLIENT_NEW_SESSION)
348
- session_flag = ""
349
- if sandbox_env:
350
- store = store_as(WebBrowserStore)
351
- if not store.session_id:
352
- result = await sandbox_env.exec(
353
- ["python3", WEB_CLIENT_NEW_SESSION], timeout=180
359
+ async def _web_browser_cmd(tool_name: str, params: dict[str, object]) -> ToolResult:
360
+ try:
361
+ sandbox_env = await tool_container_sandbox("web browser")
362
+ except PrerequisiteError as e:
363
+ # The user may have the old, incompatible, sandbox. If so, use that and
364
+ # execute the old compatible code.
365
+ try:
366
+ return await old_web_browser_cmd(tool_name, *params)
367
+ except PrerequisiteError:
368
+ raise e
369
+
370
+ store = store_as(WebBrowserStore)
371
+
372
+ if not store.session_id:
373
+ store.session_id = (
374
+ await exec_sandbox_rpc(
375
+ sandbox_env,
376
+ "web_new_session",
377
+ {"headful": False},
378
+ NewSessionResult,
354
379
  )
380
+ ).session_name
355
381
 
356
- if not result.success:
357
- raise RuntimeError(
358
- f"Error creating new web browser session: {result.stderr}"
359
- )
360
-
361
- store.session_id = result.stdout.strip("\n")
382
+ params["session_name"] = store.session_id
362
383
 
363
- session_flag = f"--session_name={store.session_id}"
364
-
365
- else:
366
- sandbox_env = await web_browser_sandbox()
367
-
368
- arg_list = None
369
- if session_flag:
370
- arg_list = ["python3", WEB_CLIENT_REQUEST, session_flag, cmd] + list(args)
384
+ crawler_result = await exec_sandbox_rpc(
385
+ sandbox_env, tool_name, params, CrawlerResult
386
+ )
387
+ if crawler_result.error and crawler_result.error.strip() != "":
388
+ raise ToolError(crawler_result.error)
371
389
  else:
372
- arg_list = ["python3", WEB_CLIENT_REQUEST, cmd] + list(args)
390
+ main_content = crawler_result.main_content
391
+ web_at = crawler_result.web_at or "(no web accessibility tree available)"
392
+ # Remove base64 data from images.
393
+ web_at_lines = web_at.split("\n")
394
+ web_at_lines = [
395
+ line.partition("data:image/png;base64")[0] for line in web_at_lines
396
+ ]
373
397
 
374
- result = await sandbox_env.exec(arg_list, timeout=180)
375
- if not result.success:
376
- raise RuntimeError(
377
- f"Error executing web browser command {cmd}({', '.join(args)}): {result.stderr}"
398
+ store_as(WebBrowserStore).main_content = (
399
+ main_content or "(no main text summary)"
378
400
  )
379
- else:
380
- response = parse_web_browser_output(result.stdout)
381
- if "error" in response and response.get("error", "").strip() != "":
382
- raise ToolError(str(response.get("error")) or "(unknown error)")
383
- elif "web_at" in response:
384
- main_content = str(response.get("main_content")) or None
385
- web_at = (
386
- str(response.get("web_at")) or "(no web accessibility tree available)"
387
- )
388
- # Remove base64 data from images.
389
- web_at_lines = web_at.split("\n")
390
- web_at_lines = [
391
- line.partition("data:image/png;base64")[0] for line in web_at_lines
392
- ]
401
+ store_as(WebBrowserStore).web_at = web_at
393
402
 
394
- store_as(WebBrowserStore).main_content = (
395
- main_content or "(no main text summary)"
396
- )
397
- store_as(WebBrowserStore).web_at = web_at
398
-
399
- web_at = "\n".join(web_at_lines)
400
- return (
401
- [
402
- ContentText(text=f"main content:\n{main_content}\n\n"),
403
- ContentText(text=f"accessibility tree:\n{web_at}"),
404
- ]
405
- if main_content
406
- else web_at
407
- )
408
- else:
409
- raise RuntimeError(
410
- f"web_browser output must contain either 'error' or 'web_at' field: {result.stdout}"
411
- )
412
-
413
-
414
- async def web_browser_sandbox() -> SandboxEnvironment:
415
- sb = await sandbox_with(WEB_CLIENT_REQUEST)
416
- if sb:
417
- return sb
418
- else:
419
- msg = dedent(f"""
420
- The web browser service was not found in any of the sandboxes for this sample. Please add the web browser service to your configuration. For example, the following Docker compose file uses the {INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB} image as its default sandbox:
421
-
422
- services:
423
- default:
424
- image: "{INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB}"
425
- init: true
426
-
427
- Alternatively, this Docker compose file creates a dedicated image for the web browser service:
428
-
429
- services:
430
- default:
431
- image: "python:3.12-bookworm"
432
- init: true
433
- command: "tail -f /dev/null"
434
-
435
- web_browser:
436
- image: "{INSPECT_WEB_BROWSER_IMAGE_DOCKERHUB}"
437
- init: true
438
- """).strip()
439
- raise PrerequisiteError(msg)
440
-
441
-
442
- def parse_web_browser_output(output: str) -> dict[str, str]:
443
- response: dict[str, str] = dict(
444
- web_url="", main_content="", web_at="", info="", error=""
445
- )
446
- active_field: str | None = None
447
- active_field_lines: list[str] = []
448
-
449
- def collect_active_field() -> None:
450
- if active_field is not None:
451
- response[active_field] = "\n".join(active_field_lines)
452
- active_field_lines.clear()
453
-
454
- for line in output.splitlines():
455
- field_match = re.match(
456
- r"^(error|main_content|web_at|web_url|info)\s*:\s*(.+)$", line
403
+ web_at = "\n".join(web_at_lines)
404
+ return (
405
+ [
406
+ ContentText(text=f"main content:\n{main_content}\n\n"),
407
+ ContentText(text=f"accessibility tree:\n{web_at}"),
408
+ ]
409
+ if main_content
410
+ else web_at
457
411
  )
458
- if field_match:
459
- collect_active_field()
460
- active_field = field_match.group(1)
461
- active_field_lines.append(field_match.group(2))
462
- else:
463
- active_field_lines.append(line)
464
- collect_active_field()
465
-
466
- return response