inspect-ai 0.3.75__py3-none-any.whl → 0.3.77__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 (87) hide show
  1. inspect_ai/_cli/eval.py +16 -0
  2. inspect_ai/_display/core/results.py +6 -1
  3. inspect_ai/_eval/eval.py +8 -1
  4. inspect_ai/_eval/evalset.py +6 -2
  5. inspect_ai/_eval/registry.py +3 -5
  6. inspect_ai/_eval/run.py +7 -2
  7. inspect_ai/_eval/task/run.py +4 -0
  8. inspect_ai/_util/content.py +3 -0
  9. inspect_ai/_util/logger.py +3 -0
  10. inspect_ai/_view/www/dist/assets/index.css +28 -16
  11. inspect_ai/_view/www/dist/assets/index.js +4811 -4609
  12. inspect_ai/_view/www/log-schema.json +79 -9
  13. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +22 -4
  14. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +1 -1
  15. inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
  16. inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
  17. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
  18. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
  19. inspect_ai/_view/www/src/types/log.d.ts +11 -5
  20. inspect_ai/log/_recorders/json.py +8 -0
  21. inspect_ai/log/_transcript.py +13 -4
  22. inspect_ai/model/_call_tools.py +13 -4
  23. inspect_ai/model/_chat_message.py +3 -0
  24. inspect_ai/model/_model.py +5 -1
  25. inspect_ai/model/_model_output.py +6 -1
  26. inspect_ai/model/_openai.py +78 -10
  27. inspect_ai/model/_openai_responses.py +277 -0
  28. inspect_ai/model/_providers/anthropic.py +134 -75
  29. inspect_ai/model/_providers/azureai.py +2 -2
  30. inspect_ai/model/_providers/mistral.py +29 -13
  31. inspect_ai/model/_providers/openai.py +64 -57
  32. inspect_ai/model/_providers/openai_responses.py +177 -0
  33. inspect_ai/model/_providers/openrouter.py +52 -2
  34. inspect_ai/model/_providers/providers.py +1 -1
  35. inspect_ai/model/_providers/vertex.py +5 -2
  36. inspect_ai/tool/__init__.py +6 -0
  37. inspect_ai/tool/_tool.py +23 -3
  38. inspect_ai/tool/_tool_call.py +5 -2
  39. inspect_ai/tool/_tool_support_helpers.py +200 -0
  40. inspect_ai/tool/_tools/_bash_session.py +119 -0
  41. inspect_ai/tool/_tools/_computer/_computer.py +1 -1
  42. inspect_ai/tool/_tools/_text_editor.py +121 -0
  43. inspect_ai/tool/_tools/_think.py +48 -0
  44. inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
  45. inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
  46. inspect_ai/tool/_tools/_web_search.py +1 -1
  47. inspect_ai/util/_json.py +28 -0
  48. inspect_ai/util/_sandbox/context.py +16 -7
  49. inspect_ai/util/_sandbox/docker/config.py +1 -1
  50. inspect_ai/util/_sandbox/docker/internal.py +3 -3
  51. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/METADATA +5 -2
  52. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/RECORD +56 -80
  53. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/WHEEL +1 -1
  54. inspect_ai/model/_image.py +0 -15
  55. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
  56. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
  57. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
  58. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
  59. inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
  60. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
  61. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
  62. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
  63. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
  64. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
  65. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
  66. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
  67. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
  68. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
  69. inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
  70. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
  71. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
  72. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
  73. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
  74. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
  75. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
  76. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
  77. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
  78. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
  79. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
  80. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
  81. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
  82. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
  83. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
  84. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
  85. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/entry_points.txt +0 -0
  86. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info/licenses}/LICENSE +0 -0
  87. {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/top_level.txt +0 -0
@@ -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,48 @@
1
+ from .._tool import Tool, tool
2
+ from .._tool_call import ToolCall, ToolCallContent, ToolCallView, ToolCallViewer
3
+ from .._tool_def import ToolDef
4
+
5
+
6
+ @tool
7
+ def think(
8
+ description: str | None = None,
9
+ thought_description: str | None = None,
10
+ ) -> Tool:
11
+ """Think tool for extra thinking.
12
+
13
+ Tool that provides models with the ability to include an additional thinking step as part of getting to its final answer.
14
+
15
+ Note that the `think()` tool is not a substitute for reasoning and extended thinking, but rather an an alternate way of letting models express thinking that is better suited to some tool use scenarios. Please see the documentation on using the [think tool](https://inspect.aisi.org.uk/tools-standard.html#sec-think) before using it in your evaluations.
16
+
17
+ Args:
18
+ description: Override the default description of the think tool.
19
+ thought_description: Override the default description of the thought parameter.
20
+ """
21
+
22
+ async def execute(thought: str) -> str:
23
+ """Use the tool to think about something.
24
+
25
+ The will not obtain new information or change the environment, but just append the thought to the log. Use it when complex reasoning or some cache memory is needed."
26
+
27
+ Args:
28
+ thought: A thought to think about.
29
+ """
30
+ return ""
31
+
32
+ return ToolDef(
33
+ execute,
34
+ name="think",
35
+ description=description,
36
+ parameters=(dict(thought=thought_description) if thought_description else None),
37
+ viewer=think_tool_viewer(),
38
+ ).as_tool()
39
+
40
+
41
+ def think_tool_viewer() -> ToolCallViewer:
42
+ def viewer(tool_call: ToolCall) -> ToolCallView:
43
+ call = ToolCallContent(
44
+ format="markdown", content=tool_call.arguments["thought"]
45
+ )
46
+ return ToolCallView(call=call)
47
+
48
+ return viewer
@@ -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
@@ -52,7 +52,7 @@ def web_search(
52
52
  A web search is conducted using the specified provider, the results are parsed for relevance
53
53
  using the specified model, and the top 'num_results' relevant pages are returned.
54
54
 
55
- See further documentation at <https://inspect.aisi.org.uk/tools.html#sec-web-search>.
55
+ See further documentation at <https://inspect.aisi.org.uk/tools-standard.html#sec-web-search>.
56
56
 
57
57
  Args:
58
58
  provider: Search provider (defaults to "google", currently
inspect_ai/util/_json.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import types
2
2
  import typing
3
+ from copy import deepcopy
3
4
  from dataclasses import is_dataclass
4
5
  from typing import (
5
6
  Any,
@@ -10,6 +11,7 @@ from typing import (
10
11
  Tuple,
11
12
  Type,
12
13
  Union,
14
+ cast,
13
15
  get_args,
14
16
  get_origin,
15
17
  get_type_hints,
@@ -127,6 +129,7 @@ def cls_json_schema(cls: Type[Any]) -> JSONSchema:
127
129
  required.append(name)
128
130
  elif isinstance(cls, type) and issubclass(cls, BaseModel):
129
131
  schema = cls.model_json_schema()
132
+ schema = resolve_schema_references(schema)
130
133
  for name, prop in schema.get("properties", {}).items():
131
134
  properties[name] = JSONSchema(**prop)
132
135
  required = schema.get("required", [])
@@ -168,3 +171,28 @@ def python_type_to_json_type(python_type: str | None) -> JSONType:
168
171
  raise ValueError(
169
172
  f"Unsupported type: {python_type} for Python to JSON conversion."
170
173
  )
174
+
175
+
176
+ def resolve_schema_references(schema: dict[str, Any]) -> dict[str, Any]:
177
+ """Resolves all $ref references in a JSON schema by inlining the definitions."""
178
+ schema = deepcopy(schema)
179
+ definitions = schema.pop("$defs", {})
180
+
181
+ def _resolve_refs(obj: Any) -> Any:
182
+ if isinstance(obj, dict):
183
+ if "$ref" in obj and obj["$ref"].startswith("#/$defs/"):
184
+ ref_key = obj["$ref"].split("/")[-1]
185
+ if ref_key in definitions:
186
+ # Replace with a deep copy of the definition
187
+ resolved = deepcopy(definitions[ref_key])
188
+ # Process any nested references in the definition
189
+ return _resolve_refs(resolved)
190
+
191
+ # Process all entries in the dictionary
192
+ return {k: _resolve_refs(v) for k, v in obj.items()}
193
+ elif isinstance(obj, list):
194
+ return [_resolve_refs(item) for item in obj]
195
+ else:
196
+ return obj
197
+
198
+ return cast(dict[str, Any], _resolve_refs(schema))