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.
- inspect_ai/_cli/eval.py +16 -0
- inspect_ai/_display/core/results.py +6 -1
- inspect_ai/_eval/eval.py +8 -1
- inspect_ai/_eval/evalset.py +6 -2
- inspect_ai/_eval/registry.py +3 -5
- inspect_ai/_eval/run.py +7 -2
- inspect_ai/_eval/task/run.py +4 -0
- inspect_ai/_util/content.py +3 -0
- inspect_ai/_util/logger.py +3 -0
- inspect_ai/_view/www/dist/assets/index.css +28 -16
- inspect_ai/_view/www/dist/assets/index.js +4811 -4609
- inspect_ai/_view/www/log-schema.json +79 -9
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +22 -4
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +1 -1
- inspect_ai/_view/www/src/samples/descriptor/score/CategoricalScoreDescriptor.tsx +1 -1
- inspect_ai/_view/www/src/samples/descriptor/score/NumericScoreDescriptor.tsx +2 -2
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +1 -1
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +2 -2
- inspect_ai/_view/www/src/types/log.d.ts +11 -5
- inspect_ai/log/_recorders/json.py +8 -0
- inspect_ai/log/_transcript.py +13 -4
- inspect_ai/model/_call_tools.py +13 -4
- inspect_ai/model/_chat_message.py +3 -0
- inspect_ai/model/_model.py +5 -1
- inspect_ai/model/_model_output.py +6 -1
- inspect_ai/model/_openai.py +78 -10
- inspect_ai/model/_openai_responses.py +277 -0
- inspect_ai/model/_providers/anthropic.py +134 -75
- inspect_ai/model/_providers/azureai.py +2 -2
- inspect_ai/model/_providers/mistral.py +29 -13
- inspect_ai/model/_providers/openai.py +64 -57
- inspect_ai/model/_providers/openai_responses.py +177 -0
- inspect_ai/model/_providers/openrouter.py +52 -2
- inspect_ai/model/_providers/providers.py +1 -1
- inspect_ai/model/_providers/vertex.py +5 -2
- inspect_ai/tool/__init__.py +6 -0
- inspect_ai/tool/_tool.py +23 -3
- inspect_ai/tool/_tool_call.py +5 -2
- inspect_ai/tool/_tool_support_helpers.py +200 -0
- inspect_ai/tool/_tools/_bash_session.py +119 -0
- inspect_ai/tool/_tools/_computer/_computer.py +1 -1
- inspect_ai/tool/_tools/_text_editor.py +121 -0
- inspect_ai/tool/_tools/_think.py +48 -0
- inspect_ai/tool/_tools/_web_browser/_back_compat.py +150 -0
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +75 -130
- inspect_ai/tool/_tools/_web_search.py +1 -1
- inspect_ai/util/_json.py +28 -0
- inspect_ai/util/_sandbox/context.py +16 -7
- inspect_ai/util/_sandbox/docker/config.py +1 -1
- inspect_ai/util/_sandbox/docker/internal.py +3 -3
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/METADATA +5 -2
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/RECORD +56 -80
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/WHEEL +1 -1
- inspect_ai/model/_image.py +0 -15
- inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +0 -8
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +0 -24
- inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +0 -25
- inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +0 -22
- inspect_ai/tool/_tools/_web_browser/_resources/README.md +0 -63
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +0 -71
- inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +0 -323
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +0 -5
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +0 -279
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +0 -9
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +0 -293
- inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +0 -94
- inspect_ai/tool/_tools/_web_browser/_resources/constants.py +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +0 -2
- inspect_ai/tool/_tools/_web_browser/_resources/mock_environment.py +0 -45
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +0 -50
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +0 -48
- inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +0 -280
- inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +0 -65
- inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +0 -146
- inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +0 -64
- inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +0 -180
- inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +0 -99
- inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +0 -15
- inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +0 -44
- inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +0 -39
- inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +0 -214
- inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +0 -35
- inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +0 -192
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.75.dist-info → inspect_ai-0.3.77.dist-info/licenses}/LICENSE +0 -0
- {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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
354
|
+
return await _web_browser_cmd("web_refresh", locals())
|
338
355
|
|
339
356
|
return execute
|
340
357
|
|
341
358
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
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
|
-
|
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
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
-
|
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
|
-
|
375
|
-
|
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
|
-
|
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
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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))
|