sentienceapi 0.95.0__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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
sentience/generator.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ Script Generator - converts trace into executable code
3
+ """
4
+
5
+ from .recorder import Trace, TraceStep
6
+
7
+
8
+ class ScriptGenerator:
9
+ """Generates Python or TypeScript code from a trace"""
10
+
11
+ def __init__(self, trace: Trace):
12
+ self.trace = trace
13
+
14
+ def generate_python(self) -> str:
15
+ """Generate Python script from trace"""
16
+ lines = [
17
+ '"""',
18
+ f"Generated script from trace: {self.trace.start_url}",
19
+ f"Created: {self.trace.created_at}",
20
+ '"""',
21
+ "",
22
+ "from sentience import SentienceBrowser, snapshot, find, click, type_text, press",
23
+ "",
24
+ "def main():",
25
+ " with SentienceBrowser(headless=False) as browser:",
26
+ f' browser.page.goto("{self.trace.start_url}")',
27
+ ' browser.page.wait_for_load_state("networkidle")',
28
+ "",
29
+ ]
30
+
31
+ for step in self.trace.steps:
32
+ lines.extend(self._generate_python_step(step, indent=" "))
33
+
34
+ lines.extend(
35
+ [
36
+ "",
37
+ 'if __name__ == "__main__":',
38
+ " main()",
39
+ ]
40
+ )
41
+
42
+ return "\n".join(lines)
43
+
44
+ def generate_typescript(self) -> str:
45
+ """Generate TypeScript script from trace"""
46
+ lines = [
47
+ "/**",
48
+ f" * Generated script from trace: {self.trace.start_url}",
49
+ f" * Created: {self.trace.created_at}",
50
+ " */",
51
+ "",
52
+ "import { SentienceBrowser, snapshot, find, click, typeText, press } from './src';",
53
+ "",
54
+ "async function main() {",
55
+ " const browser = new SentienceBrowser(undefined, false);",
56
+ "",
57
+ " try {",
58
+ " await browser.start();",
59
+ f" await browser.getPage().goto('{self.trace.start_url}');",
60
+ " await browser.getPage().waitForLoadState('networkidle');",
61
+ "",
62
+ ]
63
+
64
+ for step in self.trace.steps:
65
+ lines.extend(self._generate_typescript_step(step, indent=" "))
66
+
67
+ lines.extend(
68
+ [
69
+ " } finally {",
70
+ " await browser.close();",
71
+ " }",
72
+ "}",
73
+ "",
74
+ "main().catch(console.error);",
75
+ ]
76
+ )
77
+
78
+ return "\n".join(lines)
79
+
80
+ def _generate_python_step(self, step: TraceStep, indent: str = "") -> list[str]:
81
+ """Generate Python code for a single step"""
82
+ lines = []
83
+
84
+ if step.type == "navigation":
85
+ lines.append(f"{indent}# Navigate to {step.url}")
86
+ lines.append(f'{indent}browser.page.goto("{step.url}")')
87
+ lines.append(f'{indent}browser.page.wait_for_load_state("networkidle")')
88
+
89
+ elif step.type == "click":
90
+ if step.selector:
91
+ # Use semantic selector
92
+ lines.append(f"{indent}# Click: {step.selector}")
93
+ lines.append(f"{indent}snap = snapshot(browser)")
94
+ lines.append(f'{indent}element = find(snap, "{step.selector}")')
95
+ lines.append(f"{indent}if element:")
96
+ lines.append(f"{indent} click(browser, element.id)")
97
+ lines.append(f"{indent}else:")
98
+ lines.append(f'{indent} raise Exception("Element not found: {step.selector}")')
99
+ elif step.element_id is not None:
100
+ # Fallback to element ID
101
+ lines.append(f"{indent}# TODO: replace with semantic selector")
102
+ lines.append(f"{indent}click(browser, {step.element_id})")
103
+ lines.append("")
104
+
105
+ elif step.type == "type":
106
+ if step.selector:
107
+ lines.append(f"{indent}# Type into: {step.selector}")
108
+ lines.append(f"{indent}snap = snapshot(browser)")
109
+ lines.append(f'{indent}element = find(snap, "{step.selector}")')
110
+ lines.append(f"{indent}if element:")
111
+ lines.append(f'{indent} type_text(browser, element.id, "{step.text}")')
112
+ lines.append(f"{indent}else:")
113
+ lines.append(f'{indent} raise Exception("Element not found: {step.selector}")')
114
+ elif step.element_id is not None:
115
+ lines.append(f"{indent}# TODO: replace with semantic selector")
116
+ lines.append(f'{indent}type_text(browser, {step.element_id}, "{step.text}")')
117
+ lines.append("")
118
+
119
+ elif step.type == "press":
120
+ lines.append(f"{indent}# Press key: {step.key}")
121
+ lines.append(f'{indent}press(browser, "{step.key}")')
122
+ lines.append("")
123
+
124
+ return lines
125
+
126
+ def _generate_typescript_step(self, step: TraceStep, indent: str = "") -> list[str]:
127
+ """Generate TypeScript code for a single step"""
128
+ lines = []
129
+
130
+ if step.type == "navigation":
131
+ lines.append(f"{indent}// Navigate to {step.url}")
132
+ lines.append(f"{indent}await browser.getPage().goto('{step.url}');")
133
+ lines.append(f"{indent}await browser.getPage().waitForLoadState('networkidle');")
134
+
135
+ elif step.type == "click":
136
+ if step.selector:
137
+ lines.append(f"{indent}// Click: {step.selector}")
138
+ lines.append(f"{indent}const snap = await snapshot(browser);")
139
+ lines.append(f"{indent}const element = find(snap, '{step.selector}');")
140
+ lines.append(f"{indent}if (element) {{")
141
+ lines.append(f"{indent} await click(browser, element.id);")
142
+ lines.append(f"{indent}}} else {{")
143
+ lines.append(f"{indent} throw new Error('Element not found: {step.selector}');")
144
+ lines.append(f"{indent}}}")
145
+ elif step.element_id is not None:
146
+ lines.append(f"{indent}// TODO: replace with semantic selector")
147
+ lines.append(f"{indent}await click(browser, {step.element_id});")
148
+ lines.append("")
149
+
150
+ elif step.type == "type":
151
+ if step.selector:
152
+ lines.append(f"{indent}// Type into: {step.selector}")
153
+ lines.append(f"{indent}const snap = await snapshot(browser);")
154
+ lines.append(f"{indent}const element = find(snap, '{step.selector}');")
155
+ lines.append(f"{indent}if (element) {{")
156
+ lines.append(f"{indent} await typeText(browser, element.id, '{step.text}');")
157
+ lines.append(f"{indent}}} else {{")
158
+ lines.append(f"{indent} throw new Error('Element not found: {step.selector}');")
159
+ lines.append(f"{indent}}}")
160
+ elif step.element_id is not None:
161
+ lines.append(f"{indent}// TODO: replace with semantic selector")
162
+ lines.append(f"{indent}await typeText(browser, {step.element_id}, '{step.text}');")
163
+ lines.append("")
164
+
165
+ elif step.type == "press":
166
+ lines.append(f"{indent}// Press key: {step.key}")
167
+ lines.append(f"{indent}await press(browser, '{step.key}');")
168
+ lines.append("")
169
+
170
+ return lines
171
+
172
+ def save_python(self, filepath: str) -> None:
173
+ """Generate and save Python script"""
174
+ code = self.generate_python()
175
+ with open(filepath, "w") as f:
176
+ f.write(code)
177
+
178
+ def save_typescript(self, filepath: str) -> None:
179
+ """Generate and save TypeScript script"""
180
+ code = self.generate_typescript()
181
+ with open(filepath, "w") as f:
182
+ f.write(code)
183
+
184
+
185
+ def generate(trace: Trace, language: str = "py") -> str:
186
+ """
187
+ Generate script from trace
188
+
189
+ Args:
190
+ trace: Trace object
191
+ language: 'py' or 'ts'
192
+
193
+ Returns:
194
+ Generated code as string
195
+ """
196
+ generator = ScriptGenerator(trace)
197
+ if language == "py":
198
+ return generator.generate_python()
199
+ elif language == "ts":
200
+ return generator.generate_typescript()
201
+ else:
202
+ raise ValueError(f"Unsupported language: {language}. Use 'py' or 'ts'")
sentience/inspector.py ADDED
@@ -0,0 +1,367 @@
1
+ from typing import Optional
2
+
3
+ """
4
+ Inspector tool - helps developers see what the agent "sees"
5
+ """
6
+
7
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
8
+
9
+
10
+ class Inspector:
11
+ """Inspector for debugging - shows element info on hover/click"""
12
+
13
+ def __init__(self, browser: SentienceBrowser):
14
+ self.browser = browser
15
+ self._active = False
16
+ self._last_element_id: int | None = None
17
+
18
+ def start(self) -> None:
19
+ """Start inspection mode - prints element info on mouse move/click"""
20
+ if not self.browser.page:
21
+ raise RuntimeError("Browser not started. Call browser.start() first.")
22
+
23
+ self._active = True
24
+
25
+ # Inject inspector script into page
26
+ self.browser.page.evaluate(
27
+ """
28
+ (() => {
29
+ // Remove existing inspector if any
30
+ if (window.__sentience_inspector_active) {
31
+ return;
32
+ }
33
+
34
+ window.__sentience_inspector_active = true;
35
+ window.__sentience_last_element_id = null;
36
+
37
+ // Get element at point
38
+ function getElementAtPoint(x, y) {
39
+ const el = document.elementFromPoint(x, y);
40
+ if (!el) return null;
41
+
42
+ // Find element in registry
43
+ if (window.sentience_registry) {
44
+ for (let i = 0; i < window.sentience_registry.length; i++) {
45
+ if (window.sentience_registry[i] === el) {
46
+ return i;
47
+ }
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ // Mouse move handler
54
+ function handleMouseMove(e) {
55
+ if (!window.__sentience_inspector_active) return;
56
+
57
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
58
+ if (elementId === null || elementId === window.__sentience_last_element_id) {
59
+ return;
60
+ }
61
+
62
+ window.__sentience_last_element_id = elementId;
63
+
64
+ // Get element info from snapshot if available
65
+ if (window.sentience && window.sentience_registry) {
66
+ const el = window.sentience_registry[elementId];
67
+ if (el) {
68
+ const rect = el.getBoundingClientRect();
69
+ const text = el.getAttribute('aria-label') ||
70
+ el.value ||
71
+ el.placeholder ||
72
+ el.alt ||
73
+ (el.innerText || '').substring(0, 50);
74
+
75
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
76
+
77
+ console.log(`[Sentience Inspector] Element #${elementId}: role=${role}, text="${text}", bbox=(${Math.round(rect.x)}, ${Math.round(rect.y)}, ${Math.round(rect.width)}, ${Math.round(rect.height)})`);
78
+ }
79
+ }
80
+ }
81
+
82
+ // Click handler
83
+ function handleClick(e) {
84
+ if (!window.__sentience_inspector_active) return;
85
+
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+
89
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
90
+ if (elementId === null) return;
91
+
92
+ // Get full element info
93
+ if (window.sentience && window.sentience_registry) {
94
+ const el = window.sentience_registry[elementId];
95
+ if (el) {
96
+ const rect = el.getBoundingClientRect();
97
+ const info = {
98
+ id: elementId,
99
+ tag: el.tagName.toLowerCase(),
100
+ role: el.getAttribute('role') || 'generic',
101
+ text: el.getAttribute('aria-label') ||
102
+ el.value ||
103
+ el.placeholder ||
104
+ el.alt ||
105
+ (el.innerText || '').substring(0, 100),
106
+ bbox: {
107
+ x: Math.round(rect.x),
108
+ y: Math.round(rect.y),
109
+ width: Math.round(rect.width),
110
+ height: Math.round(rect.height)
111
+ },
112
+ attributes: {
113
+ id: el.id || null,
114
+ class: el.className || null,
115
+ name: el.name || null,
116
+ type: el.type || null
117
+ }
118
+ };
119
+
120
+ console.log('[Sentience Inspector] Clicked element:', JSON.stringify(info, null, 2));
121
+
122
+ // Also try to get from snapshot if available
123
+ window.sentience.snapshot({ limit: 100 }).then(snap => {
124
+ const element = snap.elements.find(el => el.id === elementId);
125
+ if (element) {
126
+ console.log('[Sentience Inspector] Snapshot element:', JSON.stringify(element, null, 2));
127
+ }
128
+ }).catch(() => {});
129
+ }
130
+ }
131
+ }
132
+
133
+ // Add event listeners
134
+ document.addEventListener('mousemove', handleMouseMove, true);
135
+ document.addEventListener('click', handleClick, true);
136
+
137
+ // Store cleanup function
138
+ window.__sentience_inspector_cleanup = () => {
139
+ document.removeEventListener('mousemove', handleMouseMove, true);
140
+ document.removeEventListener('click', handleClick, true);
141
+ window.__sentience_inspector_active = false;
142
+ };
143
+
144
+ console.log('[Sentience Inspector] ✅ Inspection mode active. Hover elements to see info, click to see full details.');
145
+ })();
146
+ """
147
+ )
148
+
149
+ def stop(self) -> None:
150
+ """Stop inspection mode"""
151
+ if not self.browser.page:
152
+ return
153
+
154
+ self._active = False
155
+
156
+ # Cleanup inspector
157
+ self.browser.page.evaluate(
158
+ """
159
+ () => {
160
+ if (window.__sentience_inspector_cleanup) {
161
+ window.__sentience_inspector_cleanup();
162
+ }
163
+ }
164
+ """
165
+ )
166
+
167
+ def __enter__(self):
168
+ """Context manager entry"""
169
+ self.start()
170
+ return self
171
+
172
+ def __exit__(self, exc_type, exc_val, exc_tb):
173
+ """Context manager exit"""
174
+ self.stop()
175
+
176
+
177
+ def inspect(browser: SentienceBrowser) -> Inspector:
178
+ """
179
+ Create an inspector instance
180
+
181
+ Args:
182
+ browser: SentienceBrowser instance
183
+
184
+ Returns:
185
+ Inspector instance
186
+ """
187
+ return Inspector(browser)
188
+
189
+
190
+ class InspectorAsync:
191
+ """Inspector for debugging - shows element info on hover/click (async)"""
192
+
193
+ def __init__(self, browser: AsyncSentienceBrowser):
194
+ self.browser = browser
195
+ self._active = False
196
+ self._last_element_id: int | None = None
197
+
198
+ async def start(self) -> None:
199
+ """Start inspection mode - prints element info on mouse move/click (async)"""
200
+ if not self.browser.page:
201
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
202
+
203
+ self._active = True
204
+
205
+ # Inject inspector script into page
206
+ await self.browser.page.evaluate(
207
+ """
208
+ (() => {
209
+ // Remove existing inspector if any
210
+ if (window.__sentience_inspector_active) {
211
+ return;
212
+ }
213
+
214
+ window.__sentience_inspector_active = true;
215
+ window.__sentience_last_element_id = null;
216
+
217
+ // Get element at point
218
+ function getElementAtPoint(x, y) {
219
+ const el = document.elementFromPoint(x, y);
220
+ if (!el) return null;
221
+
222
+ // Find element in registry
223
+ if (window.sentience_registry) {
224
+ for (let i = 0; i < window.sentience_registry.length; i++) {
225
+ if (window.sentience_registry[i] === el) {
226
+ return i;
227
+ }
228
+ }
229
+ }
230
+ return null;
231
+ }
232
+
233
+ // Mouse move handler
234
+ function handleMouseMove(e) {
235
+ if (!window.__sentience_inspector_active) return;
236
+
237
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
238
+ if (elementId === null || elementId === window.__sentience_last_element_id) {
239
+ return;
240
+ }
241
+
242
+ window.__sentience_last_element_id = elementId;
243
+
244
+ // Get element info from snapshot if available
245
+ if (window.sentience && window.sentience_registry) {
246
+ const el = window.sentience_registry[elementId];
247
+ if (el) {
248
+ const rect = el.getBoundingClientRect();
249
+ const text = el.getAttribute('aria-label') ||
250
+ el.value ||
251
+ el.placeholder ||
252
+ el.alt ||
253
+ (el.innerText || '').substring(0, 50);
254
+
255
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
256
+
257
+ console.log(`[Sentience Inspector] Element #${elementId}: role=${role}, text="${text}", bbox=(${Math.round(rect.x)}, ${Math.round(rect.y)}, ${Math.round(rect.width)}, ${Math.round(rect.height)})`);
258
+ }
259
+ }
260
+ }
261
+
262
+ // Click handler
263
+ function handleClick(e) {
264
+ if (!window.__sentience_inspector_active) return;
265
+
266
+ e.preventDefault();
267
+ e.stopPropagation();
268
+
269
+ const elementId = getElementAtPoint(e.clientX, e.clientY);
270
+ if (elementId === null) return;
271
+
272
+ // Get full element info
273
+ if (window.sentience && window.sentience_registry) {
274
+ const el = window.sentience_registry[elementId];
275
+ if (el) {
276
+ const rect = el.getBoundingClientRect();
277
+ const info = {
278
+ id: elementId,
279
+ tag: el.tagName.toLowerCase(),
280
+ role: el.getAttribute('role') || 'generic',
281
+ text: el.getAttribute('aria-label') ||
282
+ el.value ||
283
+ el.placeholder ||
284
+ el.alt ||
285
+ (el.innerText || '').substring(0, 100),
286
+ bbox: {
287
+ x: Math.round(rect.x),
288
+ y: Math.round(rect.y),
289
+ width: Math.round(rect.width),
290
+ height: Math.round(rect.height)
291
+ },
292
+ attributes: {
293
+ id: el.id || null,
294
+ class: el.className || null,
295
+ name: el.name || null,
296
+ type: el.type || null
297
+ }
298
+ };
299
+
300
+ console.log('[Sentience Inspector] Clicked element:', JSON.stringify(info, null, 2));
301
+
302
+ // Also try to get from snapshot if available
303
+ window.sentience.snapshot({ limit: 100 }).then(snap => {
304
+ const element = snap.elements.find(el => el.id === elementId);
305
+ if (element) {
306
+ console.log('[Sentience Inspector] Snapshot element:', JSON.stringify(element, null, 2));
307
+ }
308
+ }).catch(() => {});
309
+ }
310
+ }
311
+ }
312
+
313
+ // Add event listeners
314
+ document.addEventListener('mousemove', handleMouseMove, true);
315
+ document.addEventListener('click', handleClick, true);
316
+
317
+ // Store cleanup function
318
+ window.__sentience_inspector_cleanup = () => {
319
+ document.removeEventListener('mousemove', handleMouseMove, true);
320
+ document.removeEventListener('click', handleClick, true);
321
+ window.__sentience_inspector_active = false;
322
+ };
323
+
324
+ console.log('[Sentience Inspector] ✅ Inspection mode active. Hover elements to see info, click to see full details.');
325
+ })();
326
+ """
327
+ )
328
+
329
+ async def stop(self) -> None:
330
+ """Stop inspection mode (async)"""
331
+ if not self.browser.page:
332
+ return
333
+
334
+ self._active = False
335
+
336
+ # Cleanup inspector
337
+ await self.browser.page.evaluate(
338
+ """
339
+ () => {
340
+ if (window.__sentience_inspector_cleanup) {
341
+ window.__sentience_inspector_cleanup();
342
+ }
343
+ }
344
+ """
345
+ )
346
+
347
+ async def __aenter__(self):
348
+ """Context manager entry"""
349
+ await self.start()
350
+ return self
351
+
352
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
353
+ """Context manager exit"""
354
+ await self.stop()
355
+
356
+
357
+ def inspect_async(browser: AsyncSentienceBrowser) -> InspectorAsync:
358
+ """
359
+ Create an inspector instance (async)
360
+
361
+ Args:
362
+ browser: AsyncSentienceBrowser instance
363
+
364
+ Returns:
365
+ InspectorAsync instance
366
+ """
367
+ return InspectorAsync(browser)