sentienceapi 0.90.16__py3-none-any.whl → 0.98.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 (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -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 +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -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 +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
sentience/ordinal.py ADDED
@@ -0,0 +1,280 @@
1
+ """
2
+ Phase 3: Ordinal Intent Detection for Semantic Search
3
+
4
+ This module provides functions to detect ordinal intent in natural language goals
5
+ and select elements based on their position within groups.
6
+
7
+ Ordinal operators supported:
8
+ - Position-based: "first", "second", "third", "1st", "2nd", "3rd", etc.
9
+ - Relative: "top", "bottom", "last", "next", "previous"
10
+ - Numeric: "#1", "#2", "number 1", "item 3"
11
+
12
+ Example usage:
13
+ from sentience.ordinal import detect_ordinal_intent, select_by_ordinal
14
+
15
+ intent = detect_ordinal_intent("click the first search result")
16
+ # OrdinalIntent(kind='nth', n=1, detected=True)
17
+
18
+ element = select_by_ordinal(elements, dominant_group_key, intent)
19
+ """
20
+
21
+ import re
22
+ from dataclasses import dataclass
23
+ from typing import Literal
24
+
25
+ from sentience.models import Element
26
+
27
+
28
+ @dataclass
29
+ class OrdinalIntent:
30
+ """Detected ordinal intent from a goal string."""
31
+
32
+ detected: bool
33
+ kind: Literal["first", "last", "nth", "top_k", "next", "previous"] | None = None
34
+ n: int | None = None # For "nth" kind: 1-indexed position (1=first, 2=second)
35
+ k: int | None = None # For "top_k" kind: number of items
36
+
37
+
38
+ # Ordinal word to number mapping
39
+ ORDINAL_WORDS = {
40
+ "first": 1,
41
+ "second": 2,
42
+ "third": 3,
43
+ "fourth": 4,
44
+ "fifth": 5,
45
+ "sixth": 6,
46
+ "seventh": 7,
47
+ "eighth": 8,
48
+ "ninth": 9,
49
+ "tenth": 10,
50
+ "1st": 1,
51
+ "2nd": 2,
52
+ "3rd": 3,
53
+ "4th": 4,
54
+ "5th": 5,
55
+ "6th": 6,
56
+ "7th": 7,
57
+ "8th": 8,
58
+ "9th": 9,
59
+ "10th": 10,
60
+ }
61
+
62
+ # Patterns for detecting ordinal intent
63
+ ORDINAL_PATTERNS = [
64
+ # "first", "second", etc.
65
+ (
66
+ r"\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\b",
67
+ "ordinal_word",
68
+ ),
69
+ # "1st", "2nd", "3rd", etc.
70
+ (r"\b(\d+)(st|nd|rd|th)\b", "ordinal_suffix"),
71
+ # "#1", "#2", etc.
72
+ (r"#(\d+)\b", "hash_number"),
73
+ # "number 1", "item 3", "result 5"
74
+ (r"\b(?:number|item|result|option|choice)\s*(\d+)\b", "labeled_number"),
75
+ # "top" (implies first/best)
76
+ (r"\btop\b(?!\s*\d)", "top"),
77
+ # "top 3", "top 5"
78
+ (r"\btop\s+(\d+)\b", "top_k"),
79
+ # "last", "final", "bottom"
80
+ (r"\b(last|final|bottom)\b", "last"),
81
+ # "next", "following"
82
+ (r"\b(next|following)\b", "next"),
83
+ # "previous", "preceding", "prior"
84
+ (r"\b(previous|preceding|prior)\b", "previous"),
85
+ ]
86
+
87
+
88
+ def detect_ordinal_intent(goal: str) -> OrdinalIntent:
89
+ """
90
+ Detect ordinal intent from a goal string.
91
+
92
+ Args:
93
+ goal: Natural language goal (e.g., "click the first search result")
94
+
95
+ Returns:
96
+ OrdinalIntent with detected=True if ordinal intent found, False otherwise.
97
+
98
+ Examples:
99
+ >>> detect_ordinal_intent("click the first item")
100
+ OrdinalIntent(detected=True, kind='nth', n=1)
101
+
102
+ >>> detect_ordinal_intent("select the 3rd option")
103
+ OrdinalIntent(detected=True, kind='nth', n=3)
104
+
105
+ >>> detect_ordinal_intent("show top 5 results")
106
+ OrdinalIntent(detected=True, kind='top_k', k=5)
107
+
108
+ >>> detect_ordinal_intent("click the last button")
109
+ OrdinalIntent(detected=True, kind='last')
110
+
111
+ >>> detect_ordinal_intent("find the submit button")
112
+ OrdinalIntent(detected=False)
113
+ """
114
+ goal_lower = goal.lower()
115
+
116
+ for pattern, pattern_type in ORDINAL_PATTERNS:
117
+ match = re.search(pattern, goal_lower, re.IGNORECASE)
118
+ if match:
119
+ if pattern_type == "ordinal_word":
120
+ word = match.group(1).lower()
121
+ n = ORDINAL_WORDS.get(word)
122
+ if n:
123
+ return OrdinalIntent(detected=True, kind="nth", n=n)
124
+
125
+ elif pattern_type == "ordinal_suffix":
126
+ n = int(match.group(1))
127
+ return OrdinalIntent(detected=True, kind="nth", n=n)
128
+
129
+ elif pattern_type == "hash_number":
130
+ n = int(match.group(1))
131
+ return OrdinalIntent(detected=True, kind="nth", n=n)
132
+
133
+ elif pattern_type == "labeled_number":
134
+ n = int(match.group(1))
135
+ return OrdinalIntent(detected=True, kind="nth", n=n)
136
+
137
+ elif pattern_type == "top":
138
+ # "top" without a number means "first/best"
139
+ return OrdinalIntent(detected=True, kind="first")
140
+
141
+ elif pattern_type == "top_k":
142
+ k = int(match.group(1))
143
+ return OrdinalIntent(detected=True, kind="top_k", k=k)
144
+
145
+ elif pattern_type == "last":
146
+ return OrdinalIntent(detected=True, kind="last")
147
+
148
+ elif pattern_type == "next":
149
+ return OrdinalIntent(detected=True, kind="next")
150
+
151
+ elif pattern_type == "previous":
152
+ return OrdinalIntent(detected=True, kind="previous")
153
+
154
+ return OrdinalIntent(detected=False)
155
+
156
+
157
+ def select_by_ordinal(
158
+ elements: list[Element],
159
+ dominant_group_key: str | None,
160
+ intent: OrdinalIntent,
161
+ current_element_id: int | None = None,
162
+ ) -> Element | list[Element] | None:
163
+ """
164
+ Select element(s) from a list based on ordinal intent.
165
+
166
+ Uses the dominant_group_key to filter to the "main content" group,
167
+ then selects by group_index based on the ordinal intent.
168
+
169
+ Args:
170
+ elements: List of elements with group_key and group_index populated
171
+ dominant_group_key: The most common group key (main content group)
172
+ intent: Detected ordinal intent
173
+ current_element_id: Current element ID (for next/previous navigation)
174
+
175
+ Returns:
176
+ Single Element for nth/first/last, list of Elements for top_k,
177
+ or None if no matching element found.
178
+
179
+ Examples:
180
+ >>> intent = OrdinalIntent(detected=True, kind='nth', n=1)
181
+ >>> element = select_by_ordinal(elements, "x5-w2-h1", intent)
182
+ # Returns element with group_key="x5-w2-h1" and group_index=0
183
+ """
184
+ if not intent.detected:
185
+ return None
186
+
187
+ # Filter to dominant group if available
188
+ if dominant_group_key:
189
+ group_elements = [e for e in elements if e.group_key == dominant_group_key]
190
+ else:
191
+ # Fallback: use all elements with group_index
192
+ group_elements = [e for e in elements if e.group_index is not None]
193
+
194
+ if not group_elements:
195
+ return None
196
+
197
+ # Sort by group_index to ensure correct ordering
198
+ group_elements.sort(key=lambda e: e.group_index if e.group_index is not None else 0)
199
+
200
+ if intent.kind == "first" or (intent.kind == "nth" and intent.n == 1):
201
+ # First element (group_index=0)
202
+ return group_elements[0] if group_elements else None
203
+
204
+ elif intent.kind == "nth" and intent.n is not None:
205
+ # Nth element (1-indexed, so n=2 means group_index=1)
206
+ target_index = intent.n - 1
207
+ if 0 <= target_index < len(group_elements):
208
+ return group_elements[target_index]
209
+ return None
210
+
211
+ elif intent.kind == "last":
212
+ # Last element
213
+ return group_elements[-1] if group_elements else None
214
+
215
+ elif intent.kind == "top_k" and intent.k is not None:
216
+ # Top K elements
217
+ return group_elements[: intent.k]
218
+
219
+ elif intent.kind == "next" and current_element_id is not None:
220
+ # Next element after current
221
+ for i, elem in enumerate(group_elements):
222
+ if elem.id == current_element_id and i + 1 < len(group_elements):
223
+ return group_elements[i + 1]
224
+ return None
225
+
226
+ elif intent.kind == "previous" and current_element_id is not None:
227
+ # Previous element before current
228
+ for i, elem in enumerate(group_elements):
229
+ if elem.id == current_element_id and i > 0:
230
+ return group_elements[i - 1]
231
+ return None
232
+
233
+ return None
234
+
235
+
236
+ def boost_ordinal_elements(
237
+ elements: list[Element],
238
+ dominant_group_key: str | None,
239
+ intent: OrdinalIntent,
240
+ boost_factor: int = 10000,
241
+ ) -> list[Element]:
242
+ """
243
+ Boost the importance of elements matching ordinal intent.
244
+
245
+ This is useful for integrating ordinal selection with existing
246
+ importance-based ranking. Elements matching the ordinal intent
247
+ get a significant importance boost.
248
+
249
+ Args:
250
+ elements: List of elements (not modified)
251
+ dominant_group_key: The most common group key
252
+ intent: Detected ordinal intent
253
+ boost_factor: Amount to add to importance (default: 10000)
254
+
255
+ Returns:
256
+ A new list with copies of elements, with boosted importance for matches.
257
+ """
258
+ if not intent.detected or not dominant_group_key:
259
+ return [e.model_copy() for e in elements]
260
+
261
+ target = select_by_ordinal(elements, dominant_group_key, intent)
262
+
263
+ if target is None:
264
+ return [e.model_copy() for e in elements]
265
+
266
+ # Handle single element or list
267
+ if isinstance(target, list):
268
+ target_ids = {e.id for e in target}
269
+ else:
270
+ target_ids = {target.id}
271
+
272
+ # Create copies and boost matching elements
273
+ result = []
274
+ for elem in elements:
275
+ copy = elem.model_copy()
276
+ if copy.id in target_ids:
277
+ copy.importance = (copy.importance or 0) + boost_factor
278
+ result.append(copy)
279
+
280
+ return result
sentience/overlay.py CHANGED
@@ -2,9 +2,9 @@
2
2
  Visual overlay utilities - show/clear element highlights in browser
3
3
  """
4
4
 
5
- from typing import Any
5
+ from typing import Any, Optional
6
6
 
7
- from .browser import SentienceBrowser
7
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
8
8
  from .models import Element, Snapshot
9
9
 
10
10
 
@@ -113,3 +113,110 @@ def clear_overlay(browser: SentienceBrowser) -> None:
113
113
  }
114
114
  """
115
115
  )
116
+
117
+
118
+ async def show_overlay_async(
119
+ browser: AsyncSentienceBrowser,
120
+ elements: list[Element] | list[dict[str, Any]] | Snapshot,
121
+ target_element_id: int | None = None,
122
+ ) -> None:
123
+ """
124
+ Display visual overlay highlighting elements in the browser (async)
125
+
126
+ This function shows a Shadow DOM overlay with color-coded borders around
127
+ detected elements. Useful for debugging, learning, and validating element detection.
128
+
129
+ Args:
130
+ browser: AsyncSentienceBrowser instance
131
+ elements: Can be:
132
+ - List of Element objects (from snapshot.elements)
133
+ - List of raw element dicts (from snapshot result or API response)
134
+ - Snapshot object (will use snapshot.elements)
135
+ target_element_id: Optional ID of element to highlight in red (default: None)
136
+
137
+ Color Coding:
138
+ - Red: Target element (when target_element_id is specified)
139
+ - Blue: Primary elements (is_primary=true)
140
+ - Green: Regular interactive elements
141
+
142
+ Visual Indicators:
143
+ - Border thickness and opacity scale with importance score
144
+ - Semi-transparent fill for better visibility
145
+ - Importance badges showing scores
146
+ - Star icon for primary elements
147
+ - Target emoji for the target element
148
+
149
+ Auto-clear: Overlay automatically disappears after 5 seconds
150
+
151
+ Example:
152
+ # Show overlay from snapshot
153
+ snap = await snapshot_async(browser)
154
+ await show_overlay_async(browser, snap)
155
+
156
+ # Show overlay with custom elements
157
+ elements = [{"id": 1, "bbox": {"x": 100, "y": 100, "width": 200, "height": 50}, ...}]
158
+ await show_overlay_async(browser, elements)
159
+
160
+ # Show overlay with target element highlighted in red
161
+ await show_overlay_async(browser, snap, target_element_id=42)
162
+
163
+ # Clear overlay manually before 5 seconds
164
+ await clear_overlay_async(browser)
165
+ """
166
+ if not browser.page:
167
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
168
+
169
+ # Handle different input types
170
+ if isinstance(elements, Snapshot):
171
+ # Extract elements from Snapshot object
172
+ elements_list = [el.model_dump() for el in elements.elements]
173
+ elif isinstance(elements, list) and len(elements) > 0:
174
+ # Check if it's a list of Element objects or dicts
175
+ if hasattr(elements[0], "model_dump"):
176
+ # List of Element objects
177
+ elements_list = [el.model_dump() for el in elements]
178
+ else:
179
+ # Already a list of dicts
180
+ elements_list = elements
181
+ else:
182
+ raise ValueError("elements must be a Snapshot, list of Element objects, or list of dicts")
183
+
184
+ # Call extension API
185
+ await browser.page.evaluate(
186
+ """
187
+ (args) => {
188
+ if (window.sentience && window.sentience.showOverlay) {
189
+ window.sentience.showOverlay(args.elements, args.targetId);
190
+ } else {
191
+ console.warn('[Sentience SDK] showOverlay not available - is extension loaded?');
192
+ }
193
+ }
194
+ """,
195
+ {"elements": elements_list, "targetId": target_element_id},
196
+ )
197
+
198
+
199
+ async def clear_overlay_async(browser: AsyncSentienceBrowser) -> None:
200
+ """
201
+ Clear the visual overlay manually (before 5-second auto-clear) (async)
202
+
203
+ Args:
204
+ browser: AsyncSentienceBrowser instance
205
+
206
+ Example:
207
+ await show_overlay_async(browser, snap)
208
+ # ... inspect overlay ...
209
+ await clear_overlay_async(browser) # Remove immediately
210
+ """
211
+ if not browser.page:
212
+ raise RuntimeError("Browser not started. Call await browser.start() first.")
213
+
214
+ await browser.page.evaluate(
215
+ """
216
+ () => {
217
+ if (window.sentience && window.sentience.clearOverlay) {
218
+ window.sentience.clearOverlay();
219
+ }
220
+ }
221
+ """
222
+ )
sentience/protocols.py ADDED
@@ -0,0 +1,228 @@
1
+ """
2
+ Protocol definitions for testability and dependency injection.
3
+
4
+ These protocols define the minimal interface required by agent classes,
5
+ enabling better testability through mocking while maintaining type safety.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
9
+
10
+ if TYPE_CHECKING:
11
+ from playwright.async_api import Page as AsyncPage
12
+ from playwright.sync_api import Page
13
+
14
+ from .models import Snapshot
15
+
16
+
17
+ @runtime_checkable
18
+ class PageProtocol(Protocol):
19
+ """
20
+ Protocol for Playwright Page operations used by agents.
21
+
22
+ This protocol defines the minimal interface required from Playwright's Page object.
23
+ Agents use this interface to interact with the browser page.
24
+ """
25
+
26
+ @property
27
+ def url(self) -> str:
28
+ """Current page URL."""
29
+ ...
30
+
31
+ def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
32
+ """
33
+ Evaluate JavaScript in the page context.
34
+
35
+ Args:
36
+ script: JavaScript code to evaluate
37
+ *args: Arguments to pass to the script
38
+ **kwargs: Keyword arguments to pass to the script
39
+
40
+ Returns:
41
+ Result of the JavaScript evaluation
42
+ """
43
+ ...
44
+
45
+ def goto(self, url: str, **kwargs: Any) -> Any | None:
46
+ """
47
+ Navigate to a URL.
48
+
49
+ Args:
50
+ url: URL to navigate to
51
+ **kwargs: Additional navigation options
52
+
53
+ Returns:
54
+ Response object or None
55
+ """
56
+ ...
57
+
58
+ def wait_for_timeout(self, timeout: int) -> None:
59
+ """
60
+ Wait for a specified timeout.
61
+
62
+ Args:
63
+ timeout: Timeout in milliseconds
64
+ """
65
+ ...
66
+
67
+ def wait_for_load_state(self, state: str = "load", timeout: int | None = None) -> None:
68
+ """
69
+ Wait for page load state.
70
+
71
+ Args:
72
+ state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
73
+ timeout: Optional timeout in milliseconds
74
+ """
75
+ ...
76
+
77
+
78
+ @runtime_checkable
79
+ class BrowserProtocol(Protocol):
80
+ """
81
+ Protocol for browser operations used by agents.
82
+
83
+ This protocol defines the minimal interface required from SentienceBrowser.
84
+ Agents use this interface to interact with the browser and take snapshots.
85
+
86
+ Note: SentienceBrowser naturally implements this protocol, so no changes
87
+ are required to existing code. This protocol enables better testability
88
+ through mocking.
89
+ """
90
+
91
+ @property
92
+ def page(self) -> PageProtocol | None:
93
+ """
94
+ Current Playwright Page object.
95
+
96
+ Returns:
97
+ Page object if browser is started, None otherwise
98
+ """
99
+ ...
100
+
101
+ def start(self) -> None:
102
+ """Start the browser session."""
103
+ ...
104
+
105
+ def close(self, output_path: str | None = None) -> str | None:
106
+ """
107
+ Close the browser session.
108
+
109
+ Args:
110
+ output_path: Optional path to save browser state/output
111
+
112
+ Returns:
113
+ Path to saved output or None
114
+ """
115
+ ...
116
+
117
+ def goto(self, url: str) -> None:
118
+ """
119
+ Navigate to a URL.
120
+
121
+ Args:
122
+ url: URL to navigate to
123
+ """
124
+ ...
125
+
126
+
127
+ @runtime_checkable
128
+ class AsyncPageProtocol(Protocol):
129
+ """
130
+ Protocol for async Playwright Page operations.
131
+
132
+ Similar to PageProtocol but for async operations.
133
+ """
134
+
135
+ @property
136
+ def url(self) -> str:
137
+ """Current page URL."""
138
+ ...
139
+
140
+ async def evaluate(self, script: str, *args: Any, **kwargs: Any) -> Any:
141
+ """
142
+ Evaluate JavaScript in the page context (async).
143
+
144
+ Args:
145
+ script: JavaScript code to evaluate
146
+ *args: Arguments to pass to the script
147
+ **kwargs: Keyword arguments to pass to the script
148
+
149
+ Returns:
150
+ Result of the JavaScript evaluation
151
+ """
152
+ ...
153
+
154
+ async def goto(self, url: str, **kwargs: Any) -> Any | None:
155
+ """
156
+ Navigate to a URL (async).
157
+
158
+ Args:
159
+ url: URL to navigate to
160
+ **kwargs: Additional navigation options
161
+
162
+ Returns:
163
+ Response object or None
164
+ """
165
+ ...
166
+
167
+ async def wait_for_timeout(self, timeout: int) -> None:
168
+ """
169
+ Wait for a specified timeout (async).
170
+
171
+ Args:
172
+ timeout: Timeout in milliseconds
173
+ """
174
+ ...
175
+
176
+ async def wait_for_load_state(self, state: str = "load", timeout: int | None = None) -> None:
177
+ """
178
+ Wait for page load state (async).
179
+
180
+ Args:
181
+ state: Load state to wait for (e.g., "load", "domcontentloaded", "networkidle")
182
+ timeout: Optional timeout in milliseconds
183
+ """
184
+ ...
185
+
186
+
187
+ @runtime_checkable
188
+ class AsyncBrowserProtocol(Protocol):
189
+ """
190
+ Protocol for async browser operations.
191
+
192
+ Similar to BrowserProtocol but for async operations.
193
+ """
194
+
195
+ @property
196
+ def page(self) -> AsyncPageProtocol | None:
197
+ """
198
+ Current Playwright AsyncPage object.
199
+
200
+ Returns:
201
+ AsyncPage object if browser is started, None otherwise
202
+ """
203
+ ...
204
+
205
+ async def start(self) -> None:
206
+ """Start the browser session (async)."""
207
+ ...
208
+
209
+ async def close(self, output_path: str | None = None) -> str | None:
210
+ """
211
+ Close the browser session (async).
212
+
213
+ Args:
214
+ output_path: Optional path to save browser state/output
215
+
216
+ Returns:
217
+ Path to saved output or None
218
+ """
219
+ ...
220
+
221
+ async def goto(self, url: str) -> None:
222
+ """
223
+ Navigate to a URL (async).
224
+
225
+ Args:
226
+ url: URL to navigate to
227
+ """
228
+ ...