inspect-ai 0.3.70__py3-none-any.whl → 0.3.72__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 (219) 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 +281 -153
  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/_common.py +117 -58
  157. inspect_ai/tool/_tools/_computer/_computer.py +80 -57
  158. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/Code/User/settings.json +7 -1
  159. inspect_ai/tool/_tools/_computer/_resources/image_home_dir/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml +91 -0
  160. inspect_ai/tool/_tools/_computer/_resources/tool/.pylintrc +8 -0
  161. inspect_ai/tool/_tools/_computer/_resources/tool/.vscode/settings.json +12 -0
  162. inspect_ai/tool/_tools/_computer/_resources/tool/_args.py +78 -0
  163. inspect_ai/tool/_tools/_computer/_resources/tool/_constants.py +20 -0
  164. inspect_ai/tool/_tools/_computer/_resources/tool/_run.py +1 -1
  165. inspect_ai/tool/_tools/_computer/_resources/tool/_x11_client.py +175 -113
  166. inspect_ai/tool/_tools/_computer/_resources/tool/computer_tool.py +76 -20
  167. inspect_ai/tool/_tools/_computer/_resources/tool/pyproject.toml +65 -0
  168. inspect_ai/tool/_tools/_computer/test_args.py +151 -0
  169. inspect_ai/tool/_tools/_web_browser/_resources/.pylintrc +8 -0
  170. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/launch.json +24 -0
  171. inspect_ai/tool/_tools/_web_browser/_resources/.vscode/settings.json +25 -0
  172. inspect_ai/tool/_tools/_web_browser/_resources/Dockerfile +5 -6
  173. inspect_ai/tool/_tools/_web_browser/_resources/README.md +10 -11
  174. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree.py +71 -0
  175. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_tree_node.py +323 -0
  176. inspect_ai/tool/_tools/_web_browser/_resources/cdp/__init__.py +5 -0
  177. inspect_ai/tool/_tools/_web_browser/_resources/cdp/a11y.py +279 -0
  178. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom.py +9 -0
  179. inspect_ai/tool/_tools/_web_browser/_resources/cdp/dom_snapshot.py +293 -0
  180. inspect_ai/tool/_tools/_web_browser/_resources/cdp/page.py +94 -0
  181. inspect_ai/tool/_tools/_web_browser/_resources/constants.py +2 -0
  182. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.svg +2 -0
  183. inspect_ai/tool/_tools/_web_browser/_resources/playwright_browser.py +50 -0
  184. inspect_ai/tool/_tools/_web_browser/_resources/playwright_crawler.py +31 -359
  185. inspect_ai/tool/_tools/_web_browser/_resources/playwright_page_crawler.py +280 -0
  186. inspect_ai/tool/_tools/_web_browser/_resources/pyproject.toml +65 -0
  187. inspect_ai/tool/_tools/_web_browser/_resources/rectangle.py +64 -0
  188. inspect_ai/tool/_tools/_web_browser/_resources/rpc_client_helpers.py +146 -0
  189. inspect_ai/tool/_tools/_web_browser/_resources/scale_factor.py +64 -0
  190. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_tree_node.py +180 -0
  191. inspect_ai/tool/_tools/_web_browser/_resources/test_playwright_crawler.py +15 -9
  192. inspect_ai/tool/_tools/_web_browser/_resources/test_rectangle.py +15 -0
  193. inspect_ai/tool/_tools/_web_browser/_resources/test_web_client.py +44 -0
  194. inspect_ai/tool/_tools/_web_browser/_resources/web_browser_rpc_types.py +39 -0
  195. inspect_ai/tool/_tools/_web_browser/_resources/web_client.py +198 -48
  196. inspect_ai/tool/_tools/_web_browser/_resources/web_client_new_session.py +26 -25
  197. inspect_ai/tool/_tools/_web_browser/_resources/web_server.py +178 -39
  198. inspect_ai/tool/_tools/_web_browser/_web_browser.py +38 -19
  199. inspect_ai/util/__init__.py +2 -1
  200. inspect_ai/util/_display.py +12 -0
  201. inspect_ai/util/_sandbox/events.py +55 -21
  202. inspect_ai/util/_sandbox/self_check.py +131 -43
  203. inspect_ai/util/_subtask.py +11 -0
  204. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/METADATA +1 -1
  205. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/RECORD +209 -186
  206. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/WHEEL +1 -1
  207. inspect_ai/_view/www/src/components/VirtualList.module.css +0 -19
  208. inspect_ai/_view/www/src/components/VirtualList.tsx +0 -292
  209. inspect_ai/tool/_tools/_computer/_computer_split.py +0 -198
  210. inspect_ai/tool/_tools/_web_browser/_resources/accessibility_node.py +0 -312
  211. inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +0 -275
  212. inspect_ai/tool/_tools/_web_browser/_resources/images/usage_diagram.png +0 -0
  213. inspect_ai/tool/_tools/_web_browser/_resources/test_accessibility_node.py +0 -176
  214. inspect_ai/tool/_tools/_web_browser/_resources/test_dm_env_servicer.py +0 -135
  215. inspect_ai/tool/_tools/_web_browser/_resources/test_web_environment.py +0 -71
  216. inspect_ai/tool/_tools/_web_browser/_resources/web_environment.py +0 -184
  217. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/LICENSE +0 -0
  218. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/entry_points.txt +0 -0
  219. {inspect_ai-0.3.70.dist-info → inspect_ai-0.3.72.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
1
+ from cdp.dom_snapshot import (
2
+ DocumentSnapshot,
3
+ DOMSnapshot,
4
+ LayoutTreeSnapshot,
5
+ NodeTreeSnapshot,
6
+ RareBooleanData,
7
+ StringIndex,
8
+ TextBoxSnapshot,
9
+ )
10
+
11
+ simple_node_tree = NodeTreeSnapshot()
12
+ simple_layout_tree = LayoutTreeSnapshot(
13
+ nodeIndex=(),
14
+ styles=(),
15
+ bounds=(),
16
+ text=(),
17
+ stackingContexts=RareBooleanData(index=()),
18
+ )
19
+ simple_text_boxes = TextBoxSnapshot(layoutIndex=(), bounds=(), start=(), length=())
20
+ simple_doc_snapshot = DocumentSnapshot(
21
+ documentURL=StringIndex(0),
22
+ title=StringIndex(1),
23
+ baseURL=StringIndex(0),
24
+ contentLanguage=StringIndex(2),
25
+ encodingName=StringIndex(3),
26
+ publicId=StringIndex(666),
27
+ systemId=StringIndex(666),
28
+ frameId=StringIndex(666),
29
+ nodes=simple_node_tree,
30
+ layout=simple_layout_tree,
31
+ textBoxes=simple_text_boxes,
32
+ )
33
+ simple_dom_snapshot = DOMSnapshot(
34
+ documents=(), strings=("documentUrl", "title", "contentLanguage", "encoding")
35
+ )
36
+
37
+ # THIS TEST MODULE IS DISABLED FOR NOW. IT WILL COME BACK
38
+
39
+ # def test_foo():
40
+ # ax_node = cast(
41
+ # AXNode,
42
+ # {"nodeId": "666", "name": {"value": "Test"}, "role": {"value": "button"}},
43
+ # )
44
+ # ax_nodes = {ax_node.nodeId: ax_node}
45
+ # nodes: dict[AXNodeId, AccessibilityTreeNode] = {}
46
+ # snapshot_context = create_snapshot_context(simple_dom_snapshot)
47
+ # window_bounds = Rectangle(0, 0, 1024, 768)
48
+
49
+ # node = AccessibilityTreeNode(
50
+ # ax_node={"name": {"value": "Test"}, "role": {"value": "button"}},
51
+ # ax_nodes=ax_nodes,
52
+ # parent=None,
53
+ # all_accessibility_tree_nodes=nodes,
54
+ # snapshot_context=snapshot_context,
55
+ # device_scale_factor=1,
56
+ # window_bounds=window_bounds,
57
+ # )
58
+
59
+
60
+ # class TestAccessibilityTreeNode(absltest.TestCase):
61
+ # def test_getitem(self):
62
+ # node_data = {"name": {"value": "Test"}, "role": {"value": "button"}}
63
+ # node = AccessibilityTreeNode(node_data)
64
+ # self.assertEqual(node["name"], {"value": "Test"})
65
+ # self.assertEqual(node["role"], {"value": "button"})
66
+ # self.assertIsNone(node["invalid_key"])
67
+
68
+ # def test_setitem(self):
69
+ # node_data = {"name": {"value": "Test"}}
70
+ # node = AccessibilityTreeNode(node_data)
71
+ # node["role"] = {"value": "button"}
72
+ # self.assertEqual(
73
+ # node._node, {"name": {"value": "Test"}, "role": {"value": "button"}}
74
+ # )
75
+
76
+ # def test_str_role_name(self):
77
+ # node_data = {
78
+ # "nodeId": "1",
79
+ # "role": {"value": "button"},
80
+ # "name": {"value": "Test Button"},
81
+ # }
82
+ # node = AccessibilityTreeNode(node_data, bounds=Rectangle(10, 10, 20, 20))
83
+ # self.assertIn('[1] button "Test Button"', str(node))
84
+
85
+ # def test_str_image_src(self):
86
+ # node_data = {
87
+ # "nodeId": "1",
88
+ # "role": {"value": "image"},
89
+ # "name": {"value": "Test Image"},
90
+ # "src": "image.png",
91
+ # }
92
+ # node = AccessibilityTreeNode(node_data, bounds=Rectangle(10, 10, 20, 20))
93
+ # self.assertIn('[1] image "Test Image" image.png', str(node))
94
+
95
+ # def test_str_property(self):
96
+ # node_data = {
97
+ # "nodeId": "1",
98
+ # "role": {"value": "link"},
99
+ # "name": {"value": "Test Link"},
100
+ # "properties": [{"name": "url", "value": {"value": "www.example.com"}}],
101
+ # }
102
+ # node = AccessibilityTreeNode(node_data, bounds=Rectangle(10, 10, 20, 20))
103
+ # self.assertIn('[1] link "Test Link" [url: www.example.com]', str(node))
104
+
105
+ # def test_str_empty(self):
106
+ # node_data = {"nodeId": "1"}
107
+ # node = AccessibilityTreeNode(node_data)
108
+ # self.assertIn('[*] ""', str(node))
109
+
110
+ # def test_link_children(self):
111
+ # node_data = {"nodeId": "1", "childIds": [2, 3]}
112
+ # node = AccessibilityTreeNode(node_data)
113
+ # child1 = AccessibilityTreeNode({"nodeId": "2"})
114
+ # child2 = AccessibilityTreeNode({"nodeId": "3"})
115
+ # node_lookup = {2: child1, 3: child2}
116
+ # node.link_children(node_lookup)
117
+
118
+ # self.assertEqual(node.children, [child1, child2])
119
+ # self.assertEqual(child1._parent, node)
120
+ # self.assertEqual(child2._parent, node)
121
+
122
+ # def test_get_property(self):
123
+ # node_data = {"name": {"value": "Test"}, "role": {"value": "button"}}
124
+ # node = AccessibilityTreeNode(node_data)
125
+ # self.assertEqual(node.get_property("name"), {"value": "Test"})
126
+ # self.assertEqual(node.get_property("role"), {"value": "button"})
127
+ # self.assertIsNone(node.get_property("invalid_key"))
128
+
129
+ # def test_render(self):
130
+ # node_data = {
131
+ # "nodeId": "1",
132
+ # "ignored": False,
133
+ # "childIds": [2],
134
+ # "role": {"value": "button"},
135
+ # "name": {"value": "Test Button"},
136
+ # }
137
+ # node = AccessibilityTreeNode(node_data, bounds=Rectangle(20, 20, 30, 30))
138
+ # self.assertIn('[1] button "Test Button"', node.render())
139
+
140
+ # def test_to_render_with_children(self):
141
+ # node_data = {
142
+ # "nodeId": "1",
143
+ # "ignored": False,
144
+ # "childIds": [2, 3],
145
+ # "role": {"value": "button"},
146
+ # "name": {"value": "Test Button"},
147
+ # }
148
+ # node = AccessibilityTreeNode(node_data, bounds=Rectangle(20, 20, 30, 30))
149
+ # child_data = {
150
+ # "nodeId": "2",
151
+ # "ignored": False,
152
+ # "role": {"value": "text"},
153
+ # "name": {"value": "Child Text"},
154
+ # }
155
+ # child = AccessibilityTreeNode(child_data, bounds=Rectangle(20, 20, 30, 30))
156
+ # ignored_child_data = {
157
+ # "nodeId": "3",
158
+ # "ignored": True,
159
+ # "role": {"value": "text"},
160
+ # "name": {"value": "Child Text"},
161
+ # }
162
+ # ignored_child = AccessibilityTreeNode(
163
+ # ignored_child_data, bounds=Rectangle(20, 20, 30, 30)
164
+ # )
165
+ # node.link_children({2: child, 3: ignored_child})
166
+ # self.assertIn('[2] text "Child Text"', node.render())
167
+ # self.assertNotIn('[3] text "Child Text"', node.render())
168
+
169
+ # def test_property_string(self):
170
+ # node_data = {"properties": [{"name": "checked", "value": {"value": True}}]}
171
+ # node = AccessibilityTreeNode(node_data)
172
+ # self.assertEqual(node.property_string(), " [checked: True]")
173
+
174
+ # def test_property_string_with_ignored_property(self):
175
+ # node_data = {
176
+ # "properties": [{"name": "focusable", "value": {"value": "some_value"}}]
177
+ # }
178
+ # node = AccessibilityTreeNode(node_data)
179
+ # # 'focusable' is in _IGNORED_ACTREE_PROPERTIES
180
+ # self.assertEqual(node.property_string(), "")
@@ -1,10 +1,12 @@
1
- import playwright_crawler
2
1
  from absl.testing import parameterized
3
2
 
3
+ import playwright_browser
4
+ import playwright_crawler
5
+
4
6
 
5
7
  class TestPlaywrightCrawler(parameterized.TestCase):
6
8
  def setUp(self):
7
- self._browser = playwright_crawler.PlaywrightBrowser()
9
+ self._browser = playwright_browser.PlaywrightBrowser()
8
10
  self._crawler = playwright_crawler.PlaywrightCrawler(
9
11
  self._browser.get_new_context()
10
12
  )
@@ -26,12 +28,14 @@ class TestPlaywrightCrawler(parameterized.TestCase):
26
28
 
27
29
  def test_render_accessibility_tree(self):
28
30
  self._crawler.go_to_page("https://www.example.com")
29
- at_no_update = self._crawler.render(playwright_crawler.CrawlerOutputFormat.AT)
30
- self.assertEqual(at_no_update, "<empty>")
31
+ at_no_update = self._crawler.render_at(
32
+ playwright_crawler.CrawlerOutputFormat.AT
33
+ )
34
+ self.assertEqual(at_no_update, "")
31
35
 
32
36
  self._crawler.update()
33
37
 
34
- at_update = self._crawler.render(playwright_crawler.CrawlerOutputFormat.AT)
38
+ at_update = self._crawler.render_at(playwright_crawler.CrawlerOutputFormat.AT)
35
39
  nodes = at_update.splitlines()
36
40
  self.assertEqual(len(nodes), 3)
37
41
  self.assertTrue(
@@ -72,9 +76,9 @@ class TestPlaywrightCrawler(parameterized.TestCase):
72
76
  </body>
73
77
  </html>
74
78
  """
75
- self._crawler._page.set_content(test_html)
79
+ self._crawler._page_future.set_content(test_html)
76
80
  self._crawler.update()
77
- at_before_scroll = self._crawler.render(
81
+ at_before_scroll = self._crawler.render_at(
78
82
  playwright_crawler.CrawlerOutputFormat.AT
79
83
  )
80
84
  self.assertIn("Scrolling Test Page", at_before_scroll)
@@ -82,12 +86,14 @@ class TestPlaywrightCrawler(parameterized.TestCase):
82
86
 
83
87
  self._crawler.scroll("down")
84
88
  self._crawler.update()
85
- at_after_scroll = self._crawler.render(
89
+ at_after_scroll = self._crawler.render_at(
86
90
  playwright_crawler.CrawlerOutputFormat.AT
87
91
  )
88
92
  self.assertIn("Click Me", at_after_scroll)
89
93
 
90
94
  self._crawler.click("17")
91
95
  self._crawler.update()
92
- at_after_click = self._crawler.render(playwright_crawler.CrawlerOutputFormat.AT)
96
+ at_after_click = self._crawler.render_at(
97
+ playwright_crawler.CrawlerOutputFormat.AT
98
+ )
93
99
  self.assertIn("Text Changed!", at_after_click)
@@ -0,0 +1,15 @@
1
+ from rectangle import Rectangle
2
+
3
+
4
+ def test_overlaps():
5
+ bounds1 = Rectangle(10, 20, 30, 40)
6
+ bounds2 = Rectangle(20, 30, 40, 50)
7
+ assert bounds1.overlaps(bounds2)
8
+ bounds3 = Rectangle(50, 60, 20, 30)
9
+ assert not bounds1.overlaps(bounds3)
10
+
11
+
12
+ def test_within():
13
+ bounds1 = Rectangle(231, 167, 2, 9)
14
+ bounds2 = Rectangle(231, 167, 26, 9)
15
+ assert bounds1.within(bounds2)
@@ -0,0 +1,44 @@
1
+ import sys
2
+
3
+ from web_browser_rpc_types import GoArgs
4
+ from web_client import _parse_args
5
+
6
+
7
+ def test_parse_args_session_name_handling() -> None:
8
+ test_cases: list[tuple[list[str], str, object]] = [
9
+ (
10
+ ["cli", "--session_name=my_session", "web_go", "boston.com"],
11
+ "web_go",
12
+ GoArgs(session_name="my_session", url="boston.com"),
13
+ ),
14
+ (
15
+ ["cli", "web_go", "boston.com", "--session_name=my_session"],
16
+ "web_go",
17
+ GoArgs(session_name="my_session", url="boston.com"),
18
+ ),
19
+ (
20
+ ["cli", "web_go", "--session_name=my_session", "boston.com"],
21
+ "web_go",
22
+ GoArgs(session_name="my_session", url="boston.com"),
23
+ ),
24
+ (
25
+ ["cli", "--session_name", "my_session", "web_go", "boston.com"],
26
+ "web_go",
27
+ GoArgs(session_name="my_session", url="boston.com"),
28
+ ),
29
+ (
30
+ ["cli", "web_go", "boston.com", "--session_name", "my_session"],
31
+ "web_go",
32
+ GoArgs(session_name="my_session", url="boston.com"),
33
+ ),
34
+ (
35
+ ["cli", "web_go", "--session_name", "my_session", "boston.com"],
36
+ "web_go",
37
+ GoArgs(session_name="my_session", url="boston.com"),
38
+ ),
39
+ ]
40
+ for argv, expected_cmd, expected_params in test_cases:
41
+ sys.argv = argv
42
+ cmd, params = _parse_args()
43
+ assert cmd == expected_cmd
44
+ assert params == expected_params
@@ -0,0 +1,39 @@
1
+ from typing import Literal, TypedDict
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class NewSessionArgs(TypedDict):
7
+ headful: bool
8
+
9
+
10
+ class CrawlerBaseArgs(TypedDict):
11
+ session_name: str
12
+
13
+
14
+ class GoArgs(CrawlerBaseArgs):
15
+ url: str
16
+
17
+
18
+ class ClickArgs(CrawlerBaseArgs):
19
+ element_id: str
20
+
21
+
22
+ class ScrollArgs(CrawlerBaseArgs):
23
+ direction: Literal["up", "down"]
24
+
25
+
26
+ class TypeOrSubmitArgs(CrawlerBaseArgs):
27
+ element_id: str
28
+ text: str
29
+
30
+
31
+ class NewSessionResponse(BaseModel):
32
+ session_name: str
33
+
34
+
35
+ class CrawlerResponse(BaseModel):
36
+ web_url: str
37
+ main_content: str | None = None
38
+ web_at: str
39
+ error: str | None = None
@@ -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__":