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
@@ -1,176 +0,0 @@
1
- from absl.testing import absltest
2
- from accessibility_node import AccessibilityNode, NodeBounds
3
-
4
-
5
- class TestNodeBounds(absltest.TestCase):
6
- def test_union(self):
7
- bounds1 = NodeBounds(10, 20, 30, 40)
8
- bounds2 = NodeBounds(20, 30, 40, 50)
9
- union_bounds = bounds1.union(bounds2)
10
- self.assertEqual(union_bounds.left, 10)
11
- self.assertEqual(union_bounds.top, 20)
12
- self.assertEqual(union_bounds.right, 60)
13
- self.assertEqual(union_bounds.bottom, 80)
14
-
15
- def test_union_with_empty_bound(self):
16
- bounds = NodeBounds(10, 20, 30, 40)
17
- empty_bounds = NodeBounds(0, 0, 0, 0)
18
- union_bounds = bounds.union(empty_bounds)
19
- self.assertEqual(union_bounds.left, 10)
20
- self.assertEqual(union_bounds.top, 20)
21
- self.assertEqual(union_bounds.right, 40)
22
- self.assertEqual(union_bounds.bottom, 60)
23
-
24
- def test_is_inside(self):
25
- bounds = NodeBounds(10, 20, 30, 40)
26
- self.assertTrue(bounds.is_inside(20, 30))
27
- self.assertFalse(bounds.is_inside(5, 10))
28
- self.assertFalse(bounds.is_inside(50, 70))
29
-
30
- def test_overlaps(self):
31
- bounds1 = NodeBounds(10, 20, 30, 40)
32
- bounds2 = NodeBounds(20, 30, 40, 50)
33
- self.assertTrue(bounds1.overlaps(bounds2))
34
- bounds3 = NodeBounds(50, 60, 20, 30)
35
- self.assertFalse(bounds1.overlaps(bounds3))
36
-
37
-
38
- class TestAccessibilityNode(absltest.TestCase):
39
- def test_getitem(self):
40
- node_data = {"name": {"value": "Test"}, "role": {"value": "button"}}
41
- node = AccessibilityNode(node_data)
42
- self.assertEqual(node["name"], {"value": "Test"})
43
- self.assertEqual(node["role"], {"value": "button"})
44
- self.assertIsNone(node["invalid_key"])
45
-
46
- def test_setitem(self):
47
- node_data = {"name": {"value": "Test"}}
48
- node = AccessibilityNode(node_data)
49
- node["role"] = {"value": "button"}
50
- self.assertEqual(
51
- node._node, {"name": {"value": "Test"}, "role": {"value": "button"}}
52
- )
53
-
54
- def test_str_role_name(self):
55
- node_data = {
56
- "nodeId": "1",
57
- "role": {"value": "button"},
58
- "name": {"value": "Test Button"},
59
- }
60
- node = AccessibilityNode(node_data, bounds=NodeBounds(10, 10, 20, 20))
61
- self.assertIn('[1] button "Test Button"', str(node))
62
-
63
- def test_str_image_src(self):
64
- node_data = {
65
- "nodeId": "1",
66
- "role": {"value": "image"},
67
- "name": {"value": "Test Image"},
68
- "src": "image.png",
69
- }
70
- node = AccessibilityNode(node_data, bounds=NodeBounds(10, 10, 20, 20))
71
- self.assertIn('[1] image "Test Image" image.png', str(node))
72
-
73
- def test_str_property(self):
74
- node_data = {
75
- "nodeId": "1",
76
- "role": {"value": "link"},
77
- "name": {"value": "Test Link"},
78
- "properties": [{"name": "url", "value": {"value": "www.example.com"}}],
79
- }
80
- node = AccessibilityNode(node_data, bounds=NodeBounds(10, 10, 20, 20))
81
- self.assertIn('[1] link "Test Link" [url: www.example.com]', str(node))
82
-
83
- def test_str_empty(self):
84
- node_data = {"nodeId": "1"}
85
- node = AccessibilityNode(node_data)
86
- self.assertIn('[*] ""', str(node))
87
-
88
- def test_link_children(self):
89
- node_data = {"nodeId": "1", "childIds": [2, 3]}
90
- node = AccessibilityNode(node_data)
91
- child1 = AccessibilityNode({"nodeId": "2"})
92
- child2 = AccessibilityNode({"nodeId": "3"})
93
- node_lookup = {2: child1, 3: child2}
94
- node.link_children(node_lookup)
95
-
96
- self.assertEqual(node.children, [child1, child2])
97
- self.assertEqual(child1._parent, node)
98
- self.assertEqual(child2._parent, node)
99
-
100
- def test_get_union_bounds(self):
101
- node_data = {"nodeId": "1", "childIds": [2, 3]}
102
- node = AccessibilityNode(node_data, bounds=NodeBounds(10, 10, 20, 20))
103
- child1 = AccessibilityNode({"nodeId": "2"}, bounds=NodeBounds(20, 20, 30, 30))
104
- child2 = AccessibilityNode({"nodeId": "3"}, bounds=NodeBounds(5, 5, 10, 10))
105
- node.link_children({2: child1, 3: child2}) # Manually link children
106
-
107
- union_bounds = node.get_union_bounds()
108
- self.assertEqual(union_bounds.left, 5)
109
- self.assertEqual(union_bounds.top, 5)
110
- self.assertEqual(union_bounds.right, 50)
111
- self.assertEqual(union_bounds.bottom, 50)
112
-
113
- def test_get_property(self):
114
- node_data = {"name": {"value": "Test"}, "role": {"value": "button"}}
115
- node = AccessibilityNode(node_data)
116
- self.assertEqual(node.get_property("name"), {"value": "Test"})
117
- self.assertEqual(node.get_property("role"), {"value": "button"})
118
- self.assertIsNone(node.get_property("invalid_key"))
119
-
120
- def test_to_string(self):
121
- node_data = {
122
- "nodeId": "1",
123
- "ignored": False,
124
- "childIds": [2],
125
- "role": {"value": "button"},
126
- "name": {"value": "Test Button"},
127
- }
128
- node = AccessibilityNode(node_data, bounds=NodeBounds(20, 20, 30, 30))
129
- self.assertIn('[1] button "Test Button"', node.to_string())
130
-
131
- def test_to_string_with_children(self):
132
- node_data = {
133
- "nodeId": "1",
134
- "ignored": False,
135
- "childIds": [2, 3],
136
- "role": {"value": "button"},
137
- "name": {"value": "Test Button"},
138
- }
139
- node = AccessibilityNode(node_data, bounds=NodeBounds(20, 20, 30, 30))
140
- child_data = {
141
- "nodeId": "2",
142
- "ignored": False,
143
- "role": {"value": "text"},
144
- "name": {"value": "Child Text"},
145
- }
146
- child = AccessibilityNode(child_data, bounds=NodeBounds(20, 20, 30, 30))
147
- ignored_child_data = {
148
- "nodeId": "3",
149
- "ignored": True,
150
- "role": {"value": "text"},
151
- "name": {"value": "Child Text"},
152
- }
153
- ignored_child = AccessibilityNode(
154
- ignored_child_data, bounds=NodeBounds(20, 20, 30, 30)
155
- )
156
- node.link_children({2: child, 3: ignored_child})
157
- self.assertIn('[2] text "Child Text"', node.to_string())
158
- self.assertNotIn('[3] text "Child Text"', node.to_string())
159
-
160
- # Test with include_children=False
161
- self.assertNotIn(
162
- '[2] text "Child Text"', node.to_string(include_children=False)
163
- )
164
-
165
- def test_property_string(self):
166
- node_data = {"properties": [{"name": "checked", "value": {"value": True}}]}
167
- node = AccessibilityNode(node_data)
168
- self.assertEqual(node.property_string(), " [checked: True]")
169
-
170
- def test_property_string_with_ignored_property(self):
171
- node_data = {
172
- "properties": [{"name": "focusable", "value": {"value": "some_value"}}]
173
- }
174
- node = AccessibilityNode(node_data)
175
- # 'focusable' is in _IGNORED_ACTREE_PROPERTIES
176
- self.assertEqual(node.property_string(), "")
@@ -1,135 +0,0 @@
1
- from concurrent import futures
2
-
3
- import dm_env_servicer
4
- import grpc
5
- import mock_environment
6
- from dm_env_rpc.v1 import (
7
- compliance,
8
- dm_env_rpc_pb2,
9
- dm_env_rpc_pb2_grpc,
10
- tensor_utils,
11
- )
12
- from dm_env_rpc.v1 import connection as dm_env_rpc_connection
13
-
14
-
15
- class ServerConnection:
16
- def __init__(self):
17
- self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=1))
18
- servicer = dm_env_servicer.EnvironmentService(mock_environment.MockEnvironment)
19
- dm_env_rpc_pb2_grpc.add_EnvironmentServicer_to_server(servicer, self._server)
20
- port = self._server.add_secure_port("[::]:0", grpc.local_server_credentials())
21
- self._server.start()
22
-
23
- self._channel = grpc.secure_channel(
24
- f"[::]:{port}", grpc.local_channel_credentials()
25
- )
26
- grpc.channel_ready_future(self._channel).result()
27
-
28
- self.connection = dm_env_rpc_connection.Connection(self._channel)
29
-
30
- def close(self):
31
- self.connection.close()
32
- self._channel.close()
33
- self._server.stop(grace=None)
34
-
35
-
36
- class JoinedServerConnection(ServerConnection):
37
- def __init__(self):
38
- super().__init__()
39
- response = self.connection.send(dm_env_rpc_pb2.CreateWorldRequest())
40
- self.world_name = response.world_name
41
-
42
- response = self.connection.send(
43
- dm_env_rpc_pb2.JoinWorldRequest(world_name=self.world_name)
44
- )
45
- self.specs = response.specs
46
-
47
- def close(self):
48
- try:
49
- self.connection.send(dm_env_rpc_pb2.LeaveWorldRequest())
50
- self.connection.send(
51
- dm_env_rpc_pb2.DestroyWorldRequest(world_name=self.world_name)
52
- )
53
- finally:
54
- super().close()
55
-
56
-
57
- class DmEnvRpcStepTest(compliance.Step):
58
- @property
59
- def connection(self):
60
- return self._server_connection.connection
61
-
62
- @property
63
- def specs(self):
64
- return self._server_connection.specs
65
-
66
- def setUp(self):
67
- super().setUp()
68
- self._server_connection = JoinedServerConnection()
69
-
70
- def tearDown(self):
71
- self._server_connection.close()
72
- super().tearDown()
73
-
74
- # Overriding this test since this behaviour does not make sence
75
- # for our use case.
76
- def test_first_step_actions_are_ignored(self):
77
- pass
78
-
79
-
80
- class DmEnvRpcCreateAndDestoryWorldTest(compliance.CreateDestroyWorld):
81
- @property
82
- def connection(self):
83
- return self._server_connection.connection
84
-
85
- @property
86
- def required_world_settings(self):
87
- """A string to Tensor mapping of the minimum set of required settings."""
88
- return {}
89
-
90
- @property
91
- def invalid_world_settings(self):
92
- """World creation settings which are invalid in some way."""
93
- return {"invalid_setting": tensor_utils.pack_tensor(123)}
94
-
95
- @property
96
- def has_multiple_world_support(self):
97
- """Does the server support creating more than one world?"""
98
- return True
99
-
100
- def setUp(self):
101
- self._server_connection = ServerConnection()
102
- super().setUp()
103
-
104
- def tearDown(self):
105
- super().tearDown()
106
- self._server_connection.close()
107
-
108
-
109
- class DmEnvRpcJoinAndLeaveWorldTest(compliance.JoinLeaveWorld):
110
- @property
111
- def connection(self):
112
- return self._server_connection.connection
113
-
114
- @property
115
- def world_name(self):
116
- return self._world_name
117
-
118
- @property
119
- def invalid_join_settings(self):
120
- return {"invalid_setting": tensor_utils.pack_tensor(123)}
121
-
122
- def setUp(self):
123
- self._server_connection = ServerConnection()
124
- response = self.connection.send(dm_env_rpc_pb2.CreateWorldRequest())
125
- self._world_name = response.world_name
126
- super().setUp()
127
-
128
- def tearDown(self):
129
- super().tearDown()
130
- try:
131
- self.connection.send(
132
- dm_env_rpc_pb2.DestroyWorldRequest(world_name=self.world_name)
133
- )
134
- finally:
135
- self._server_connection.close()
@@ -1,71 +0,0 @@
1
- from unittest.mock import MagicMock, call, patch
2
-
3
- import web_environment
4
- from absl.testing import parameterized
5
- from playwright_crawler import CrawlerOutputFormat
6
-
7
-
8
- class TestWebEnvironment(parameterized.TestCase):
9
- @patch("playwright_crawler.PlaywrightCrawler")
10
- def setUp(self, MockCrawler):
11
- self._mock_crawler = MagicMock()
12
- MockCrawler.return_value = self._mock_crawler
13
- self._mock_browser_context = MagicMock()
14
- self._web_env = web_environment.WebEnvironment(self._mock_browser_context)
15
-
16
- def test_step_go_to_command(self):
17
- self._web_env.step("web_go https://en.wikipedia.org/wiki/Sun ignored_param")
18
- self._mock_crawler.go_to_page.assert_called_once_with(
19
- "https://en.wikipedia.org/wiki/Sun"
20
- )
21
-
22
- def test_step_click_command(self):
23
- self._web_env.step("web_click 1111 ignored_param")
24
- # click() might be also called later but we only check the first call
25
- self.assertEqual(self._mock_crawler.mock_calls[0], call.click("1111"))
26
-
27
- def test_step_scroll_command(self):
28
- self._web_env.step("web_scroll up ignored_param")
29
- self._mock_crawler.scroll.assert_called_once_with("up")
30
-
31
- def test_step_forward_command(self):
32
- self._web_env.step("web_forward ignored_param")
33
- self._mock_crawler.forward.assert_called_once_with()
34
-
35
- def test_step_back_command(self):
36
- self._web_env.step("web_back ignored_param")
37
- self._mock_crawler.back.assert_called_once_with()
38
-
39
- def test_step_refresh_command(self):
40
- self._web_env.step("web_refresh ignored_param")
41
- self._mock_crawler.refresh.assert_called_once_with()
42
-
43
- def test_step_type_command(self):
44
- self._web_env.step("web_type some_element_id text to type into element")
45
- self._mock_crawler.type.assert_called_once_with(
46
- "some_element_id", "text to type into element"
47
- )
48
-
49
- def test_step_type_submit_command(self):
50
- self._web_env.step("web_type_submit some_element_id text to type into element")
51
- self._mock_crawler.clear.assert_called_once()
52
- self._mock_crawler.type.assert_called_once_with(
53
- "some_element_id", "text to type into element\n"
54
- )
55
-
56
- @parameterized.parameters(
57
- ("web_go"),
58
- ("web_click"),
59
- ("web_scroll"),
60
- ("web_type"),
61
- ("web_type_submit"),
62
- ("some_random_command"),
63
- )
64
- def test_step_invalid_command(self, command):
65
- self._web_env.step(command)
66
- self.assertEqual(self._web_env._last_error, f'\n\nInvalid command: "{command}"')
67
-
68
- def test_get_observations_returns_only_required_observations(self):
69
- obs = self._web_env.get_observations(["web_at"])
70
- self.assertTrue(set(obs.keys()) == set(["web_at"]))
71
- self._mock_crawler.render.assert_called_once_with(CrawlerOutputFormat.AT)
@@ -1,184 +0,0 @@
1
- """A web dm_env (to be run in a docker container).
2
-
3
- This environment allows the agent to interact with a web browser via text
4
- commands.
5
- """
6
-
7
- import functools
8
- import json
9
- import socket
10
- import traceback
11
- from typing import Any
12
-
13
- import dm_env
14
- import playwright_crawler
15
- from dm_env import specs
16
-
17
-
18
- class WebEnvironment(dm_env.Environment):
19
- """A DM environment where an agent controls a web browser."""
20
-
21
- DEFAULT_OBSERVATIONS = ["web_url", "web_at", "error", "info"]
22
-
23
- def __init__(self, browser_context):
24
- """Initializes the environment."""
25
- super().__init__()
26
- self._web: playwright_crawler.PlaywrightCrawler = (
27
- playwright_crawler.PlaywrightCrawler(browser_context)
28
- )
29
- self._last_error = ""
30
- self._hostname = socket.gethostname()
31
- self._required_observations = self.DEFAULT_OBSERVATIONS
32
- self._selected_node_id: str | None = None
33
-
34
- def reset(self):
35
- # We're not using reset at the moment
36
- pass
37
-
38
- def step(self, action: str) -> dm_env.TimeStep:
39
- """Updates the environment according to the action and returns a `TimeStep`.
40
-
41
- Args:
42
- action: the command to execute in the web environment.
43
-
44
- Returns:
45
- A `TimeStep` namedtuple containing:
46
- step_type: A `StepType` value.
47
- reward: always 0.
48
- discount: always 1.
49
- observation: the current web browser state rendered as text.
50
- """
51
- # Process the incoming command.
52
- # Commands are always in the form [COMMAND] [args]
53
- command, *args = action.split(" ")
54
-
55
- self._last_error = ""
56
- self._selected_node_id = None
57
- command_l = command.lower()
58
-
59
- try:
60
- if not command_l: # Equivalent to case '':
61
- # Treat an empty command as a NOOP.
62
- pass
63
- elif command_l == "web_go" and len(args) > 0:
64
- self._web.go_to_page(args[0])
65
- elif command_l == "web_click" and len(args) > 0:
66
- self._web.click(args[0])
67
- elif command_l == "web_scroll" and len(args) > 0:
68
- self._web.scroll(args[0])
69
- elif command_l == "web_forward":
70
- self._web.forward()
71
- elif command_l == "web_back":
72
- self._web.back()
73
- elif command_l == "web_refresh":
74
- self._web.refresh()
75
- elif command_l == "web_type" and len(args) > 0:
76
- self._web.type(args[0], " ".join(args[1:]))
77
- elif command_l == "web_type_submit" and len(args) > 0:
78
- # Clear any existing text, type the new text, and then press enter.
79
- self._web.clear(args[0])
80
- self._web.type(args[0], " ".join(args[1:]) + "\n")
81
- else:
82
- self._last_error = f'\n\nInvalid command: "{action}"'
83
-
84
- except Exception as e:
85
- # Broard exception as we don't know what kind of error the crawler might
86
- # generate. If an error does occur pass it back as part of the
87
- # observation.
88
- traceback_info = traceback.extract_tb(e.__traceback__)[-1]
89
- self._last_error = f"\nERROR:{e}\n{traceback_info}"
90
-
91
- try:
92
- # The update might fail due to async issues.
93
- # TODO: Instead of a catch-all, make the webcrawler more
94
- # robust dynamic or malformed elements.
95
- self._web.update()
96
-
97
- # If there's a cookies message click to sort it out.
98
- self._auto_click_cookies()
99
-
100
- except Exception as e: # pylint: disable=broad-exception-caught
101
- traceback_info = traceback.extract_tb(e.__traceback__)[-1]
102
- self._last_error = f"\nUPDATE ERROR:{e}\n{traceback_info}"
103
-
104
- return dm_env.transition(
105
- reward=0.0,
106
- observation=self.get_observations(),
107
- )
108
-
109
- def observation_spec(self) -> dict[str, specs.Array]:
110
- """Defines the observations provided by the environment.
111
-
112
- Returns:
113
- The observation specification.
114
- """
115
- obs_shapes = {
116
- "web_url": specs.Array(shape=(), dtype=str, name="web_url"),
117
- "web_at": specs.Array(shape=(), dtype=str, name="web_at"),
118
- "error": specs.Array(shape=(), dtype=str, name="error"),
119
- "info": specs.Array(shape=(), dtype=str, name="info"),
120
- }
121
- return {key: obs_shapes[key] for key in self._required_observations}
122
-
123
- def action_spec(self) -> specs.Array:
124
- """Defines the actions that should be provided to `step`.
125
-
126
- Returns:
127
- The action specification.
128
- """
129
- return specs.Array(shape=(), dtype=str, name="command")
130
-
131
- def close(self) -> None:
132
- """Closes the environment."""
133
- self._web.close()
134
-
135
- @property
136
- def info(self) -> dict[str, str]:
137
- """Returns a dictionary of information about this environment."""
138
- out = {
139
- "hostname": self._hostname,
140
- }
141
- if self._selected_node_id is not None:
142
- out["node_id"] = self._selected_node_id
143
- return out
144
-
145
- def get_observations(
146
- self, required_observations: list[str] | None = None
147
- ) -> dict[str, Any]:
148
- """Returns dictionary containing each requested observations.
149
-
150
- Args:
151
- required_observations: List of observations to include in the output. If
152
- non-none overrides the default `self._requested_observations`.
153
- """
154
-
155
- def render(mode):
156
- return functools.partial(self._web.render, mode)
157
-
158
- obs_map = {
159
- "web_url": lambda: self._web.url.split("?")[0],
160
- "web_at": render(playwright_crawler.CrawlerOutputFormat.AT),
161
- "error": lambda: self._last_error,
162
- "info": lambda: json.dumps(self.info),
163
- }
164
-
165
- result = {}
166
- required_observations = (
167
- self._required_observations
168
- if required_observations is None
169
- else required_observations
170
- )
171
- for obs_name in required_observations:
172
- result[obs_name] = obs_map[obs_name]()
173
-
174
- return result
175
-
176
- def _auto_click_cookies(self):
177
- """Autoclick any cookies popup."""
178
- try:
179
- accept_node = self._web.lookup_node("<Accept all>")
180
- self._web.click(accept_node.node_id)
181
- self._web.update()
182
- except ValueError:
183
- # Node not found.
184
- pass