inspect-ai 0.3.70__py3-none-any.whl → 0.3.71__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 (208) hide show
  1. inspect_ai/_cli/eval.py +14 -8
  2. inspect_ai/_display/core/display.py +2 -0
  3. inspect_ai/_display/core/footer.py +13 -3
  4. inspect_ai/_display/plain/display.py +6 -2
  5. inspect_ai/_display/rich/display.py +19 -6
  6. inspect_ai/_display/textual/app.py +6 -1
  7. inspect_ai/_display/textual/display.py +4 -0
  8. inspect_ai/_display/textual/widgets/transcript.py +10 -6
  9. inspect_ai/_eval/task/run.py +5 -8
  10. inspect_ai/_util/content.py +20 -1
  11. inspect_ai/_util/transcript.py +10 -4
  12. inspect_ai/_util/working.py +4 -0
  13. inspect_ai/_view/www/App.css +6 -0
  14. inspect_ai/_view/www/dist/assets/index.css +115 -87
  15. inspect_ai/_view/www/dist/assets/index.js +5324 -2276
  16. inspect_ai/_view/www/eslint.config.mjs +24 -1
  17. inspect_ai/_view/www/log-schema.json +283 -20
  18. inspect_ai/_view/www/package.json +8 -3
  19. inspect_ai/_view/www/src/App.tsx +2 -2
  20. inspect_ai/_view/www/src/components/AnsiDisplay.tsx +4 -3
  21. inspect_ai/_view/www/src/components/Card.tsx +9 -8
  22. inspect_ai/_view/www/src/components/DownloadButton.tsx +2 -1
  23. inspect_ai/_view/www/src/components/EmptyPanel.tsx +2 -2
  24. inspect_ai/_view/www/src/components/ErrorPanel.tsx +4 -3
  25. inspect_ai/_view/www/src/components/ExpandablePanel.tsx +13 -5
  26. inspect_ai/_view/www/src/components/FindBand.tsx +3 -3
  27. inspect_ai/_view/www/src/components/HumanBaselineView.tsx +3 -3
  28. inspect_ai/_view/www/src/components/LabeledValue.tsx +5 -4
  29. inspect_ai/_view/www/src/components/LargeModal.tsx +18 -13
  30. inspect_ai/_view/www/src/components/{LightboxCarousel.css → LightboxCarousel.module.css} +22 -18
  31. inspect_ai/_view/www/src/components/LightboxCarousel.tsx +36 -27
  32. inspect_ai/_view/www/src/components/MessageBand.tsx +2 -1
  33. inspect_ai/_view/www/src/components/NavPills.tsx +9 -8
  34. inspect_ai/_view/www/src/components/ProgressBar.tsx +2 -1
  35. inspect_ai/_view/www/src/components/TabSet.tsx +21 -15
  36. inspect_ai/_view/www/src/index.tsx +2 -2
  37. inspect_ai/_view/www/src/metadata/MetaDataGrid.tsx +11 -9
  38. inspect_ai/_view/www/src/metadata/MetaDataView.tsx +3 -2
  39. inspect_ai/_view/www/src/metadata/MetadataGrid.module.css +1 -0
  40. inspect_ai/_view/www/src/metadata/RenderedContent.tsx +16 -0
  41. inspect_ai/_view/www/src/plan/DatasetDetailView.tsx +3 -2
  42. inspect_ai/_view/www/src/plan/DetailStep.tsx +2 -1
  43. inspect_ai/_view/www/src/plan/PlanCard.tsx +2 -5
  44. inspect_ai/_view/www/src/plan/PlanDetailView.tsx +6 -9
  45. inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +2 -1
  46. inspect_ai/_view/www/src/plan/SolverDetailView.tsx +3 -3
  47. inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +2 -2
  48. inspect_ai/_view/www/src/samples/SampleDialog.tsx +3 -3
  49. inspect_ai/_view/www/src/samples/SampleDisplay.tsx +2 -2
  50. inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +2 -2
  51. inspect_ai/_view/www/src/samples/SamplesTools.tsx +2 -1
  52. inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +3 -19
  53. inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +2 -1
  54. inspect_ai/_view/www/src/samples/chat/ChatMessageRow.tsx +2 -1
  55. inspect_ai/_view/www/src/samples/chat/ChatView.tsx +2 -1
  56. inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +22 -7
  57. inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +35 -6
  58. inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +2 -2
  59. inspect_ai/_view/www/src/samples/chat/messages.ts +15 -2
  60. inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +13 -4
  61. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.module.css +2 -2
  62. inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +18 -19
  63. inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.module.css +1 -1
  64. inspect_ai/_view/www/src/samples/chat/tools/ToolOutput.tsx +4 -3
  65. inspect_ai/_view/www/src/samples/chat/tools/ToolTitle.tsx +2 -2
  66. inspect_ai/_view/www/src/samples/error/FlatSampleErrorView.tsx +2 -3
  67. inspect_ai/_view/www/src/samples/error/SampleErrorView.tsx +3 -2
  68. inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +2 -1
  69. inspect_ai/_view/www/src/samples/list/SampleHeader.tsx +2 -1
  70. inspect_ai/_view/www/src/samples/list/SampleList.tsx +57 -45
  71. inspect_ai/_view/www/src/samples/list/SampleRow.tsx +2 -1
  72. inspect_ai/_view/www/src/samples/list/SampleSeparator.tsx +2 -1
  73. inspect_ai/_view/www/src/samples/sample-tools/EpochFilter.tsx +2 -2
  74. inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +4 -3
  75. inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +2 -5
  76. inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +2 -2
  77. inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +2 -1
  78. inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +2 -2
  79. inspect_ai/_view/www/src/samples/transcript/ApprovalEventView.tsx +2 -1
  80. inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +2 -1
  81. inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +2 -1
  82. inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +2 -1
  83. inspect_ai/_view/www/src/samples/transcript/LoggerEventView.module.css +4 -0
  84. inspect_ai/_view/www/src/samples/transcript/LoggerEventView.tsx +12 -2
  85. inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +1 -1
  86. inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +25 -28
  87. inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +2 -1
  88. inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +5 -4
  89. inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +2 -2
  90. inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +8 -7
  91. inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +2 -2
  92. inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +3 -3
  93. inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +18 -14
  94. inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +5 -5
  95. inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +34 -15
  96. inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +2 -1
  97. inspect_ai/_view/www/src/samples/transcript/event/EventNavs.tsx +2 -1
  98. inspect_ai/_view/www/src/samples/transcript/event/EventRow.tsx +3 -2
  99. inspect_ai/_view/www/src/samples/transcript/event/EventSection.tsx +2 -2
  100. inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.module.css +28 -0
  101. inspect_ai/_view/www/src/samples/transcript/event/EventTimingPanel.tsx +115 -0
  102. inspect_ai/_view/www/src/samples/transcript/event/utils.ts +29 -0
  103. inspect_ai/_view/www/src/samples/transcript/state/StateDiffView.tsx +2 -1
  104. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +3 -3
  105. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +11 -8
  106. inspect_ai/_view/www/src/types/log.d.ts +129 -34
  107. inspect_ai/_view/www/src/usage/ModelTokenTable.tsx +6 -10
  108. inspect_ai/_view/www/src/usage/ModelUsagePanel.module.css +4 -0
  109. inspect_ai/_view/www/src/usage/ModelUsagePanel.tsx +32 -9
  110. inspect_ai/_view/www/src/usage/TokenTable.tsx +4 -6
  111. inspect_ai/_view/www/src/usage/UsageCard.tsx +2 -1
  112. inspect_ai/_view/www/src/utils/format.ts +1 -1
  113. inspect_ai/_view/www/src/utils/json.ts +24 -0
  114. inspect_ai/_view/www/src/workspace/WorkSpace.tsx +6 -5
  115. inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +9 -2
  116. inspect_ai/_view/www/src/workspace/error/TaskErrorPanel.tsx +2 -1
  117. inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +2 -1
  118. inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +3 -3
  119. inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +4 -3
  120. inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +5 -4
  121. inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +5 -8
  122. inspect_ai/_view/www/src/workspace/sidebar/EvalStatus.tsx +5 -4
  123. inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +2 -1
  124. inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +2 -1
  125. inspect_ai/_view/www/src/workspace/sidebar/SidebarLogEntry.tsx +2 -2
  126. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoreView.tsx +2 -1
  127. inspect_ai/_view/www/src/workspace/sidebar/SidebarScoresView.tsx +2 -2
  128. inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +2 -2
  129. inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +2 -5
  130. inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +12 -11
  131. inspect_ai/_view/www/yarn.lock +241 -5
  132. inspect_ai/log/_condense.py +3 -0
  133. inspect_ai/log/_recorders/eval.py +6 -1
  134. inspect_ai/log/_transcript.py +58 -1
  135. inspect_ai/model/__init__.py +2 -0
  136. inspect_ai/model/_call_tools.py +7 -0
  137. inspect_ai/model/_chat_message.py +22 -7
  138. inspect_ai/model/_conversation.py +10 -8
  139. inspect_ai/model/_generate_config.py +25 -4
  140. inspect_ai/model/_model.py +133 -57
  141. inspect_ai/model/_model_output.py +3 -0
  142. inspect_ai/model/_openai.py +106 -40
  143. inspect_ai/model/_providers/anthropic.py +134 -26
  144. inspect_ai/model/_providers/google.py +27 -8
  145. inspect_ai/model/_providers/groq.py +9 -4
  146. inspect_ai/model/_providers/openai.py +57 -4
  147. inspect_ai/model/_providers/openai_o1.py +10 -0
  148. inspect_ai/model/_providers/providers.py +1 -1
  149. inspect_ai/model/_reasoning.py +15 -2
  150. inspect_ai/scorer/_model.py +23 -19
  151. inspect_ai/solver/_human_agent/agent.py +14 -10
  152. inspect_ai/solver/_human_agent/commands/__init__.py +7 -3
  153. inspect_ai/solver/_human_agent/commands/submit.py +76 -30
  154. inspect_ai/tool/__init__.py +2 -0
  155. inspect_ai/tool/_tool.py +3 -1
  156. inspect_ai/tool/_tools/_computer/_resources/tool/_run.py +1 -1
  157. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +8 -0
  158. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +24 -0
  159. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +25 -0
  160. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +5 -6
  161. inspect_ai/tool/_tools/_web_browser/_resources/README.md +10 -11
  162. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +71 -0
  163. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +323 -0
  164. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +5 -0
  165. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +279 -0
  166. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +9 -0
  167. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +293 -0
  168. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +94 -0
  169. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +2 -0
  170. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +2 -0
  171. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +50 -0
  172. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +31 -359
  173. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +280 -0
  174. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +65 -0
  175. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +64 -0
  176. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +146 -0
  177. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +64 -0
  178. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +180 -0
  179. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +15 -9
  180. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +15 -0
  181. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +44 -0
  182. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +39 -0
  183. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +198 -48
  184. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +26 -25
  185. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +178 -39
  186. inspect_ai/tool/_tools/_web_browser/_web_browser.py +38 -19
  187. inspect_ai/util/__init__.py +2 -1
  188. inspect_ai/util/_display.py +12 -0
  189. inspect_ai/util/_sandbox/events.py +55 -21
  190. inspect_ai/util/_sandbox/self_check.py +131 -43
  191. inspect_ai/util/_subtask.py +11 -0
  192. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/METADATA +1 -1
  193. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/RECORD +197 -182
  194. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/WHEEL +1 -1
  195. inspect_ai/_view/www/node_modules/flatted/python/flatted.py +0 -149
  196. inspect_ai/_view/www/node_modules/flatted/python/test.py +0 -63
  197. inspect_ai/_view/www/src/components/VirtualList.module.css +0 -19
  198. inspect_ai/_view/www/src/components/VirtualList.tsx +0 -292
  199. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_node.py +0 -312
  200. inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +0 -275
  201. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.png +0 -0
  202. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_node.py +0 -176
  203. inspect_ai/tool/_tools/_web_browser/_resources/test_dm_env_servicer.py +0 -135
  204. inspect_ai/tool/_tools/_web_browser/_resources/test_web_environment.py +0 -71
  205. inspect_ai/tool/_tools/_web_browser/_resources/web_environment.py +0 -184
  206. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/LICENSE +0 -0
  207. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/entry_points.txt +0 -0
  208. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.71.dist-info}/top_level.txt +0 -0
@@ -1,63 +1,213 @@
1
- """A client to interract with the web server."""
2
-
1
+ import argparse
3
2
  import sys
3
+ from typing import Literal
4
4
 
5
- import grpc
6
- from dm_env_rpc.v1 import connection as dm_env_connection
7
- from dm_env_rpc.v1 import dm_env_adaptor, dm_env_rpc_pb2
5
+ from constants import DEFAULT_SESSION_NAME, SERVER_PORT
6
+ from rpc_client_helpers import RPCError, rpc_call
7
+ from web_browser_rpc_types import (
8
+ ClickArgs,
9
+ CrawlerBaseArgs,
10
+ CrawlerResponse,
11
+ GoArgs,
12
+ NewSessionArgs,
13
+ NewSessionResponse,
14
+ ScrollArgs,
15
+ TypeOrSubmitArgs,
16
+ )
8
17
 
9
- _DM_ENV_BASE_PORT = 9443
10
- _WORLD_NAME = "WebBrowser"
11
- _SESSION_FLAG = "--session_name="
18
+ _SERVER_URL = f"http://localhost:{SERVER_PORT}/"
12
19
 
13
20
 
14
- def parse_args(cli_args: list[str]) -> tuple[str, str]:
15
- if not cli_args:
16
- return _WORLD_NAME, ""
21
+ def main() -> None:
22
+ if len(sys.argv) > 1:
23
+ command, params = _parse_args()
24
+ _execute_command(command, params)
25
+ else:
26
+ _interactive_mode()
17
27
 
18
- if cli_args[0].startswith(_SESSION_FLAG):
19
- world_name = cli_args[0][len(_SESSION_FLAG) :]
20
28
 
21
- if len(cli_args) == 1:
22
- return world_name, ""
29
+ def _execute_command(
30
+ command: str,
31
+ params: NewSessionArgs
32
+ | GoArgs
33
+ | ClickArgs
34
+ | TypeOrSubmitArgs
35
+ | ScrollArgs
36
+ | CrawlerBaseArgs,
37
+ ) -> None:
38
+ try:
39
+ if command == "new_session":
40
+ print(
41
+ rpc_call(
42
+ _SERVER_URL, command, dict(params), NewSessionResponse
43
+ ).session_name
44
+ )
23
45
  else:
24
- return world_name, " ".join(cli_args[1:])
46
+ response = rpc_call(
47
+ _SERVER_URL,
48
+ command,
49
+ dict(params),
50
+ CrawlerResponse,
51
+ )
52
+ for key, value in vars(response).items():
53
+ if value is not None:
54
+ print(key, ": ", value)
25
55
 
26
- else:
27
- return _WORLD_NAME, " ".join(cli_args)
56
+ except RPCError as rpc_error:
57
+ _return_error(f"error: {rpc_error}")
28
58
 
29
59
 
30
- def main() -> None:
31
- world_name, command = parse_args(sys.argv[1:])
32
-
33
- # Keep connection open even when empty data pings are received. This is
34
- # required to avoid a "too many pings" error.
35
- options = [
36
- ("grpc.keepalive_permit_without_calls", 0),
37
- ("grpc.keepalive_timeout_ms", 20000),
38
- ("grpc.http2.max_pings_without_data", 0),
39
- ("grpc.http2.max_pings_without_data", 0),
40
- ("grpc.http2.max_ping_strikes", 0),
41
- ("grpc.http2.min_recv_ping_interval_without_data_ms", 0),
42
- ]
43
- channel = grpc.secure_channel(
44
- f"localhost:{_DM_ENV_BASE_PORT}",
45
- grpc.local_channel_credentials(),
46
- options=options,
60
+ def _interactive_mode() -> None:
61
+ print(
62
+ "Welcome to the Playwright Crawler interactive mode!\n"
63
+ "commands:\n"
64
+ " web_go <URL> - goes to the specified url.\n"
65
+ " web_click <ELEMENT_ID> - clicks on a given element.\n"
66
+ " web_scroll <up/down> - scrolls up or down one page.\n"
67
+ " web_forward - navigates forward a page.\n"
68
+ " web_back - navigates back a page.\n"
69
+ " web_refresh - reloads current page (F5).\n"
70
+ " web_type <ELEMENT_ID> <TEXT> - types the specified text into the input with the specified id.\n"
71
+ " web_type_submit <ELEMENT_ID> <TEXT> - types the specified text into the input with the specified id and presses ENTER to submit the form."
72
+ )
73
+
74
+ session_created = False
75
+ while True:
76
+ try:
77
+ user_input = input("Enter command: ").strip()
78
+ if user_input.lower() in {"exit", "quit"}:
79
+ break
80
+ args = user_input.split()
81
+ sys.argv = ["cli"] + args
82
+ command, params = _parse_args()
83
+ print(f"command: {command}, params: {params}")
84
+ if not session_created:
85
+ _execute_command("new_session", NewSessionArgs(headful=True))
86
+ session_created = True
87
+ _execute_command(command, params)
88
+ except Exception as e: # pylint: disable=broad-exception-caught
89
+ print(f"Error: {e}")
90
+
91
+
92
+ def _return_error(error: str) -> None:
93
+ print(error, file=sys.stderr)
94
+ sys.exit(1)
95
+
96
+
97
+ def _create_main_parser() -> argparse.ArgumentParser:
98
+ parser = argparse.ArgumentParser(prog="web_client")
99
+ parser.add_argument(
100
+ "--session_name",
101
+ type=str,
102
+ required=False,
103
+ default=DEFAULT_SESSION_NAME,
104
+ help="Session name",
47
105
  )
48
- connection = dm_env_connection.Connection(channel)
49
-
50
- specs = connection.send(
51
- dm_env_rpc_pb2.JoinWorldRequest(world_name=world_name)
52
- ).specs
53
-
54
- with dm_env_adaptor.DmEnvAdaptor(
55
- connection=connection, specs=specs, nested_tensors=False
56
- ) as env:
57
- time_step = env.step({"command": command})
58
- for key, value in time_step.observation.items():
59
- print(key, ": ", value)
60
- env.close()
106
+ return parser
107
+
108
+
109
+ def _create_command_parser() -> argparse.ArgumentParser:
110
+ result = argparse.ArgumentParser(prog="web_client")
111
+
112
+ subparsers = result.add_subparsers(dest="command", required=True)
113
+
114
+ go_parser = subparsers.add_parser("web_go")
115
+ go_parser.add_argument("url", type=str, help="URL to navigate to")
116
+
117
+ click_parser = subparsers.add_parser("web_click")
118
+ click_parser.add_argument("element_id", type=str, help="ID of the element to click")
119
+
120
+ scroll_parser = subparsers.add_parser("web_scroll")
121
+ scroll_parser.add_argument(
122
+ "direction",
123
+ type=str,
124
+ choices=["up", "down"],
125
+ help="Direction to scroll (up or down)",
126
+ )
127
+ subparsers.add_parser("web_forward")
128
+ subparsers.add_parser("web_back")
129
+ subparsers.add_parser("web_refresh")
130
+
131
+ type_parser = subparsers.add_parser("web_type")
132
+ type_parser.add_argument(
133
+ "element_id", type=str, help="ID of the element to type into"
134
+ )
135
+ type_parser.add_argument("text", type=str, help="The text to type")
136
+
137
+ submit_parser = subparsers.add_parser("web_type_submit")
138
+ submit_parser.add_argument(
139
+ "element_id",
140
+ type=str,
141
+ help="ID of the element to type into and submit",
142
+ )
143
+ submit_parser.add_argument("text", type=str, help="The text to type")
144
+
145
+ # Add common argument to all subparsers
146
+ for name, subparser in subparsers.choices.items():
147
+ if name != "new_session":
148
+ subparser.add_argument(
149
+ "--session_name",
150
+ type=str,
151
+ nargs="?",
152
+ required=False,
153
+ default=DEFAULT_SESSION_NAME,
154
+ help="Session name",
155
+ )
156
+
157
+ return result
158
+
159
+
160
+ main_parser = _create_main_parser()
161
+ command_parser = _create_command_parser()
162
+
163
+
164
+ def _parse_args() -> (
165
+ tuple[Literal["web_go"], GoArgs]
166
+ | tuple[Literal["web_click"], ClickArgs]
167
+ | tuple[Literal["web_type", "web_type_submit"], TypeOrSubmitArgs]
168
+ | tuple[Literal["web_scroll"], ScrollArgs]
169
+ | tuple[Literal["web_forward", "web_back", "web_refresh"], CrawlerBaseArgs]
170
+ ):
171
+ # web_client.py supports a very non-standard command line. It has a required named
172
+ # parameter, --session_name, before the command.
173
+ # Unfortunately, because we can't break backwards compatibility, we're stuck
174
+ # with that. To properly parse it, we'll be forced to have a separate parser
175
+ # for --session_name and merge the results with the normal command parser.
176
+
177
+ main_args, remaining_args = main_parser.parse_known_args()
178
+ session_name = main_args.session_name or DEFAULT_SESSION_NAME
179
+
180
+ command_args = command_parser.parse_args(remaining_args)
181
+ command_args_dict = vars(command_args)
182
+
183
+ match command_args.command:
184
+ case "web_go":
185
+ return command_args_dict["command"], GoArgs(
186
+ url=command_args_dict["url"],
187
+ session_name=session_name,
188
+ )
189
+ case "web_click":
190
+ return command_args_dict["command"], ClickArgs(
191
+ element_id=command_args_dict["element_id"],
192
+ session_name=session_name,
193
+ )
194
+ case "web_type" | "web_type_submit":
195
+ return command_args_dict["command"], TypeOrSubmitArgs(
196
+ element_id=command_args_dict["element_id"],
197
+ text=command_args_dict["text"],
198
+ session_name=session_name,
199
+ )
200
+ case "web_scroll":
201
+ return command_args_dict["command"], ScrollArgs(
202
+ direction=command_args_dict["direction"],
203
+ session_name=session_name,
204
+ )
205
+ case "web_forward" | "web_back" | "web_refresh":
206
+ return command_args_dict["command"], CrawlerBaseArgs(
207
+ session_name=session_name,
208
+ )
209
+ case _:
210
+ raise ValueError("Unexpected command")
61
211
 
62
212
 
63
213
  if __name__ == "__main__":
@@ -1,33 +1,34 @@
1
- """Simple script to run and test the RPC server."""
1
+ import argparse
2
+ import sys
2
3
 
3
- import grpc
4
- from dm_env_rpc.v1 import connection as dm_env_connection
5
- from dm_env_rpc.v1 import dm_env_rpc_pb2
4
+ from constants import SERVER_PORT
5
+ from rpc_client_helpers import RPCError, rpc_call
6
+ from web_browser_rpc_types import NewSessionArgs, NewSessionResponse
6
7
 
7
- _DM_ENV_BASE_PORT = 9443
8
8
 
9
-
10
- def main():
11
- # Keep connection open even when empty data pings are received. This is
12
- # required to avoid a "too many pings" error.
13
- options = [
14
- ("grpc.keepalive_permit_without_calls", 0),
15
- ("grpc.keepalive_timeout_ms", 20000),
16
- ("grpc.http2.max_pings_without_data", 0),
17
- ("grpc.http2.max_ping_strikes", 0),
18
- ("grpc.http2.min_recv_ping_interval_without_data_ms", 0),
19
- ]
20
- # Creating a world with the headless browser.
21
- channel = grpc.secure_channel(
22
- f"localhost:{_DM_ENV_BASE_PORT}",
23
- grpc.local_channel_credentials(),
24
- options=options,
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(prog="web_client_new_session")
11
+ parser.add_argument(
12
+ "--headful", action="store_true", help="Run in headful mode for testing"
25
13
  )
26
- connection = dm_env_connection.Connection(channel)
27
- world_name = connection.send(dm_env_rpc_pb2.CreateWorldRequest()).world_name
28
- print(world_name)
14
+ args_class = parser.parse_args()
15
+ args_dict = vars(args_class)
16
+ # TODO: Frick. this does no validation
17
+ params_typed_dict = NewSessionArgs(headful=args_dict["headful"])
18
+ params = dict(params_typed_dict)
29
19
 
30
- connection.close()
20
+ try:
21
+ print(
22
+ rpc_call(
23
+ f"http://localhost:{SERVER_PORT}/",
24
+ "new_session",
25
+ params,
26
+ NewSessionResponse,
27
+ ).session_name
28
+ )
29
+ except RPCError as rpc_error:
30
+ print(rpc_error, file=sys.stderr)
31
+ sys.exit(1)
31
32
 
32
33
 
33
34
  if __name__ == "__main__":
@@ -1,52 +1,191 @@
1
- """Simple script to run and test the RPC server."""
1
+ import threading
2
+ from typing import Awaitable, Callable, Unpack
2
3
 
3
- from concurrent import futures
4
+ from aiohttp.web import Application, Request, Response, run_app
5
+ from jsonrpcserver import Result, Success, async_dispatch, method
4
6
 
5
- import dm_env_servicer
6
- import grpc
7
- import web_environment
8
- from dm_env_rpc.v1 import connection as dm_env_connection
9
- from dm_env_rpc.v1 import dm_env_rpc_pb2, dm_env_rpc_pb2_grpc
7
+ from constants import DEFAULT_SESSION_NAME, SERVER_PORT
8
+ from playwright_browser import PlaywrightBrowser
9
+ from playwright_crawler import PlaywrightCrawler
10
+ from scale_factor import get_screen_scale_factor
11
+ from web_browser_rpc_types import (
12
+ ClickArgs,
13
+ CrawlerBaseArgs,
14
+ CrawlerResponse,
15
+ GoArgs,
16
+ NewSessionArgs,
17
+ NewSessionResponse,
18
+ ScrollArgs,
19
+ TypeOrSubmitArgs,
20
+ )
10
21
 
11
- _DM_ENV_BASE_PORT = 9443
12
22
 
23
+ class Sessions:
24
+ def __init__(self) -> None:
25
+ self._lock = threading.Lock()
26
+ self._browser: PlaywrightBrowser | None = None
27
+ self._sessions: dict[str, PlaywrightCrawler] = {}
13
28
 
14
- def main():
15
- # Keep connection open even when empty data pings are received. This is
16
- # required to avoid a "too many pings" error.
17
- options = [
18
- ("grpc.keepalive_permit_without_calls", 0),
19
- ("grpc.keepalive_timeout_ms", 20000),
20
- ("grpc.http2.max_pings_without_data", 0),
21
- ("grpc.http2.max_ping_strikes", 0),
22
- ("grpc.http2.min_recv_ping_interval_without_data_ms", 0),
23
- ]
24
- grpc_server = grpc.server(
25
- # We must have a single worker thread since the web environment is not
26
- # thread safe.
27
- futures.ThreadPoolExecutor(max_workers=1),
28
- options=options,
29
- )
30
- env_service = dm_env_servicer.EnvironmentService(web_environment.WebEnvironment)
31
- dm_env_rpc_pb2_grpc.add_EnvironmentServicer_to_server(env_service, grpc_server)
29
+ async def new_session(self, headful: bool) -> str:
30
+ with self._lock:
31
+ if not self._browser:
32
+ self._browser = await PlaywrightBrowser.create(headless=not headful)
33
+ current_count = len(self._sessions)
34
+ name = (
35
+ DEFAULT_SESSION_NAME
36
+ if current_count == 0
37
+ else f"{DEFAULT_SESSION_NAME}_{current_count}"
38
+ )
39
+ crawler = await PlaywrightCrawler.create(
40
+ await self._browser.get_new_context(),
41
+ device_scale_factor=get_screen_scale_factor() if headful else 1,
42
+ )
43
+ self._sessions[name] = crawler
44
+ return name
32
45
 
33
- grpc_server.add_secure_port(
34
- f"localhost:{_DM_ENV_BASE_PORT}", grpc.local_server_credentials()
35
- )
46
+ async def get_crawler_for_session(self, name: str) -> PlaywrightCrawler:
47
+ if not self._sessions:
48
+ await self.new_session(False)
49
+ return self._sessions[name]
36
50
 
37
- grpc_server.start()
38
51
 
39
- # Creating a world with the headless browser after the server started.
40
- channel = grpc.secure_channel(
41
- f"localhost:{_DM_ENV_BASE_PORT}",
42
- grpc.local_channel_credentials(),
43
- options=options,
52
+ sessions = Sessions()
53
+
54
+
55
+ @method
56
+ async def new_session(**kwargs: Unpack[NewSessionArgs]) -> NewSessionResponse:
57
+ return Success(
58
+ NewSessionResponse(
59
+ session_name=await sessions.new_session(kwargs.get("headful", False))
60
+ ).model_dump()
44
61
  )
45
- connection = dm_env_connection.Connection(channel)
46
- connection.send(dm_env_rpc_pb2.CreateWorldRequest())
47
- connection.close()
48
62
 
49
- grpc_server.wait_for_termination()
63
+
64
+ @method
65
+ async def web_go(**kwargs: Unpack[GoArgs]) -> Result:
66
+ async def handler(crawler: PlaywrightCrawler):
67
+ await (await crawler.current_page).go_to_url(kwargs["url"])
68
+
69
+ return await _execute_crawler_command(kwargs["session_name"], handler)
70
+
71
+
72
+ @method
73
+ async def web_click(**kwargs: Unpack[ClickArgs]) -> Result:
74
+ async def handler(crawler: PlaywrightCrawler):
75
+ await (await crawler.current_page).click(kwargs["element_id"])
76
+
77
+ return await _execute_crawler_command(kwargs["session_name"], handler)
78
+
79
+
80
+ @method
81
+ async def web_scroll(**kwargs: Unpack[ScrollArgs]) -> Result:
82
+ async def handler(crawler: PlaywrightCrawler):
83
+ await (await crawler.current_page).scroll(kwargs["direction"])
84
+
85
+ return await _execute_crawler_command(kwargs["session_name"], handler)
86
+
87
+
88
+ @method
89
+ async def web_forward(**kwargs: Unpack[CrawlerBaseArgs]) -> Result:
90
+ async def handler(crawler: PlaywrightCrawler):
91
+ await (await crawler.current_page).forward()
92
+
93
+ return await _execute_crawler_command(kwargs["session_name"], handler)
94
+
95
+
96
+ @method
97
+ async def web_back(**kwargs: Unpack[CrawlerBaseArgs]) -> Result:
98
+ async def handler(crawler: PlaywrightCrawler):
99
+ await (await crawler.current_page).back()
100
+
101
+ return await _execute_crawler_command(kwargs["session_name"], handler)
102
+
103
+
104
+ @method
105
+ async def web_refresh(**kwargs: Unpack[CrawlerBaseArgs]) -> Result:
106
+ async def handler(crawler: PlaywrightCrawler):
107
+ await (await crawler.current_page).refresh()
108
+
109
+ return await _execute_crawler_command(kwargs["session_name"], handler)
110
+
111
+
112
+ @method
113
+ async def web_type(**kwargs: Unpack[TypeOrSubmitArgs]) -> Result:
114
+ async def handler(crawler: PlaywrightCrawler):
115
+ await (await crawler.current_page).type(
116
+ kwargs["element_id"], _str_from_str_or_list(kwargs["text"])
117
+ )
118
+
119
+ return await _execute_crawler_command(kwargs["session_name"], handler)
120
+
121
+
122
+ @method
123
+ async def web_type_submit(**kwargs: Unpack[TypeOrSubmitArgs]) -> Result:
124
+ async def handler(crawler: PlaywrightCrawler):
125
+ await (await crawler.current_page).clear(kwargs["element_id"])
126
+ await (await crawler.current_page).type(
127
+ kwargs["element_id"], _str_from_str_or_list(kwargs["text"]) + "\n"
128
+ )
129
+
130
+ return await _execute_crawler_command(kwargs["session_name"], handler)
131
+
132
+
133
+ async def _execute_crawler_command(
134
+ session_name: str, handler: Callable[[PlaywrightCrawler], Awaitable[None]]
135
+ ) -> Result:
136
+ if not sessions:
137
+ await new_session()
138
+ try:
139
+ crawler = await sessions.get_crawler_for_session(session_name)
140
+ await handler(crawler)
141
+ await (await crawler.current_page).update()
142
+
143
+ # If there's a cookies message click to sort it out.
144
+ await _auto_click_cookies(crawler)
145
+
146
+ return Success(
147
+ CrawlerResponse(
148
+ web_url=(await crawler.current_page).url.split("?")[0],
149
+ main_content=(await crawler.current_page).render_main_content(),
150
+ web_at=(await crawler.current_page).render_at(),
151
+ error=None,
152
+ ).model_dump()
153
+ )
154
+ except Exception as e: # pylint: disable=broad-exception-caught
155
+ return Success(
156
+ CrawlerResponse(
157
+ web_url=(await crawler.current_page).url.split("?")[0],
158
+ web_at="encountered error",
159
+ error=str(e),
160
+ ).model_dump()
161
+ )
162
+
163
+
164
+ def _str_from_str_or_list(str_or_list: str | list[str]) -> str:
165
+ return str_or_list if isinstance(str_or_list, str) else " ".join(str_or_list)
166
+
167
+
168
+ async def _auto_click_cookies(crawler: PlaywrightCrawler):
169
+ """Autoclick any cookies popup."""
170
+ try:
171
+ accept_node = (await crawler.current_page).lookup_node("<Accept all>")
172
+ except LookupError:
173
+ return
174
+ await (await crawler.current_page).click(accept_node.node_id)
175
+ await (await crawler.current_page).update()
176
+
177
+
178
+ def main():
179
+ async def handle_request(request: Request) -> Response:
180
+ return Response(
181
+ text=await async_dispatch(await request.text()),
182
+ content_type="application/json",
183
+ )
184
+
185
+ app = Application()
186
+ app.router.add_post("/", handle_request)
187
+
188
+ run_app(app, port=SERVER_PORT)
50
189
 
51
190
 
52
191
  if __name__ == "__main__":