sentienceapi 0.90.9__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 (46) hide show
  1. sentience/__init__.py +153 -0
  2. sentience/actions.py +439 -0
  3. sentience/agent.py +687 -0
  4. sentience/agent_config.py +43 -0
  5. sentience/base_agent.py +101 -0
  6. sentience/browser.py +409 -0
  7. sentience/cli.py +130 -0
  8. sentience/cloud_tracing.py +292 -0
  9. sentience/conversational_agent.py +509 -0
  10. sentience/expect.py +92 -0
  11. sentience/extension/background.js +233 -0
  12. sentience/extension/content.js +298 -0
  13. sentience/extension/injected_api.js +1473 -0
  14. sentience/extension/manifest.json +36 -0
  15. sentience/extension/pkg/sentience_core.d.ts +51 -0
  16. sentience/extension/pkg/sentience_core.js +529 -0
  17. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  18. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  19. sentience/extension/release.json +115 -0
  20. sentience/extension/test-content.js +4 -0
  21. sentience/formatting.py +59 -0
  22. sentience/generator.py +202 -0
  23. sentience/inspector.py +185 -0
  24. sentience/llm_provider.py +431 -0
  25. sentience/models.py +406 -0
  26. sentience/overlay.py +115 -0
  27. sentience/query.py +303 -0
  28. sentience/read.py +96 -0
  29. sentience/recorder.py +369 -0
  30. sentience/schemas/trace_v1.json +216 -0
  31. sentience/screenshot.py +54 -0
  32. sentience/snapshot.py +282 -0
  33. sentience/text_search.py +107 -0
  34. sentience/trace_indexing/__init__.py +27 -0
  35. sentience/trace_indexing/index_schema.py +111 -0
  36. sentience/trace_indexing/indexer.py +363 -0
  37. sentience/tracer_factory.py +211 -0
  38. sentience/tracing.py +285 -0
  39. sentience/utils.py +296 -0
  40. sentience/wait.py +73 -0
  41. sentienceapi-0.90.9.dist-info/METADATA +878 -0
  42. sentienceapi-0.90.9.dist-info/RECORD +46 -0
  43. sentienceapi-0.90.9.dist-info/WHEEL +5 -0
  44. sentienceapi-0.90.9.dist-info/entry_points.txt +2 -0
  45. sentienceapi-0.90.9.dist-info/licenses/LICENSE.md +43 -0
  46. sentienceapi-0.90.9.dist-info/top_level.txt +1 -0
sentience/models.py ADDED
@@ -0,0 +1,406 @@
1
+ """
2
+ Pydantic models for Sentience SDK - matches spec/snapshot.schema.json
3
+ """
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class BBox(BaseModel):
11
+ """Bounding box coordinates"""
12
+
13
+ x: float
14
+ y: float
15
+ width: float
16
+ height: float
17
+
18
+
19
+ class Viewport(BaseModel):
20
+ """Viewport dimensions"""
21
+
22
+ width: float
23
+ height: float
24
+
25
+
26
+ class VisualCues(BaseModel):
27
+ """Visual analysis cues"""
28
+
29
+ is_primary: bool
30
+ background_color_name: str | None = None
31
+ is_clickable: bool
32
+
33
+
34
+ class Element(BaseModel):
35
+ """Element from snapshot"""
36
+
37
+ id: int
38
+ role: str
39
+ text: str | None = None
40
+ importance: int
41
+ bbox: BBox
42
+ visual_cues: VisualCues
43
+ in_viewport: bool = True
44
+ is_occluded: bool = False
45
+ z_index: int = 0
46
+
47
+
48
+ class Snapshot(BaseModel):
49
+ """Snapshot response from extension"""
50
+
51
+ status: Literal["success", "error"]
52
+ timestamp: str | None = None
53
+ url: str
54
+ viewport: Viewport | None = None
55
+ elements: list[Element]
56
+ screenshot: str | None = None
57
+ screenshot_format: Literal["png", "jpeg"] | None = None
58
+ error: str | None = None
59
+ requires_license: bool | None = None
60
+
61
+ def save(self, filepath: str) -> None:
62
+ """Save snapshot as JSON file"""
63
+ import json
64
+
65
+ with open(filepath, "w") as f:
66
+ json.dump(self.model_dump(), f, indent=2)
67
+
68
+
69
+ class ActionResult(BaseModel):
70
+ """Result of an action (click, type, press)"""
71
+
72
+ success: bool
73
+ duration_ms: int
74
+ outcome: Literal["navigated", "dom_updated", "no_change", "error"] | None = None
75
+ url_changed: bool | None = None
76
+ snapshot_after: Snapshot | None = None
77
+ error: dict | None = None
78
+
79
+
80
+ class WaitResult(BaseModel):
81
+ """Result of wait_for operation"""
82
+
83
+ found: bool
84
+ element: Element | None = None
85
+ duration_ms: int
86
+ timeout: bool
87
+
88
+
89
+ # ========== Agent Layer Models ==========
90
+
91
+
92
+ class ScreenshotConfig(BaseModel):
93
+ """Screenshot format configuration"""
94
+
95
+ format: Literal["png", "jpeg"] = "png"
96
+ quality: int | None = Field(None, ge=1, le=100) # Only for JPEG (1-100)
97
+
98
+
99
+ class SnapshotFilter(BaseModel):
100
+ """Filter options for snapshot elements"""
101
+
102
+ min_area: int | None = Field(None, ge=0)
103
+ allowed_roles: list[str] | None = None
104
+ min_z_index: int | None = None
105
+
106
+
107
+ class SnapshotOptions(BaseModel):
108
+ """
109
+ Configuration for snapshot calls.
110
+ Matches TypeScript SnapshotOptions interface from sdk-ts/src/snapshot.ts
111
+ """
112
+
113
+ screenshot: bool | ScreenshotConfig = False # Union type: boolean or config
114
+ limit: int = Field(50, ge=1, le=500)
115
+ filter: SnapshotFilter | None = None
116
+ use_api: bool | None = None # Force API vs extension
117
+ save_trace: bool = False # Save raw_elements to JSON for benchmarking/training
118
+ trace_path: str | None = None # Path to save trace (default: "trace_{timestamp}.json")
119
+ goal: str | None = None # Optional goal/task description for the snapshot
120
+ show_overlay: bool = False # Show visual overlay highlighting elements in browser
121
+
122
+ class Config:
123
+ arbitrary_types_allowed = True
124
+
125
+
126
+ class AgentActionResult(BaseModel):
127
+ """Result of a single agent action (from agent.act())"""
128
+
129
+ success: bool
130
+ action: Literal["click", "type", "press", "finish", "error"]
131
+ goal: str
132
+ duration_ms: int
133
+ attempt: int
134
+
135
+ # Optional fields based on action type
136
+ element_id: int | None = None
137
+ text: str | None = None
138
+ key: str | None = None
139
+ outcome: Literal["navigated", "dom_updated", "no_change", "error"] | None = None
140
+ url_changed: bool | None = None
141
+ error: str | None = None
142
+ message: str | None = None # For FINISH action
143
+
144
+ def __getitem__(self, key):
145
+ """
146
+ Support dict-style access for backward compatibility.
147
+ This allows existing code using result["success"] to continue working.
148
+ """
149
+ import warnings
150
+
151
+ warnings.warn(
152
+ f"Dict-style access result['{key}'] is deprecated. Use result.{key} instead.",
153
+ DeprecationWarning,
154
+ stacklevel=2,
155
+ )
156
+ return getattr(self, key)
157
+
158
+
159
+ class ActionTokenUsage(BaseModel):
160
+ """Token usage for a single action"""
161
+
162
+ goal: str
163
+ prompt_tokens: int
164
+ completion_tokens: int
165
+ total_tokens: int
166
+ model: str
167
+
168
+
169
+ class TokenStats(BaseModel):
170
+ """Token usage statistics for an agent session"""
171
+
172
+ total_prompt_tokens: int
173
+ total_completion_tokens: int
174
+ total_tokens: int
175
+ by_action: list[ActionTokenUsage]
176
+
177
+
178
+ class ActionHistory(BaseModel):
179
+ """Single history entry from agent execution"""
180
+
181
+ goal: str
182
+ action: str # The raw action string from LLM
183
+ result: dict # Will be AgentActionResult but stored as dict for flexibility
184
+ success: bool
185
+ attempt: int
186
+ duration_ms: int
187
+
188
+
189
+ class ProxyConfig(BaseModel):
190
+ """
191
+ Proxy configuration for browser networking.
192
+
193
+ Supports HTTP, HTTPS, and SOCKS5 proxies with optional authentication.
194
+ """
195
+
196
+ server: str = Field(
197
+ ...,
198
+ description="Proxy server URL including scheme and port (e.g., 'http://proxy.example.com:8080')",
199
+ )
200
+ username: str | None = Field(
201
+ None,
202
+ description="Username for proxy authentication (optional)",
203
+ )
204
+ password: str | None = Field(
205
+ None,
206
+ description="Password for proxy authentication (optional)",
207
+ )
208
+
209
+ def to_playwright_dict(self) -> dict:
210
+ """
211
+ Convert to Playwright proxy configuration format.
212
+
213
+ Returns:
214
+ Dict compatible with Playwright's proxy parameter
215
+ """
216
+ config = {"server": self.server}
217
+ if self.username and self.password:
218
+ config["username"] = self.username
219
+ config["password"] = self.password
220
+ return config
221
+
222
+
223
+ # ========== Storage State Models (Auth Injection) ==========
224
+
225
+
226
+ class Cookie(BaseModel):
227
+ """
228
+ Cookie definition for storage state injection.
229
+
230
+ Matches Playwright's cookie format for storage_state.
231
+ """
232
+
233
+ name: str = Field(..., description="Cookie name")
234
+ value: str = Field(..., description="Cookie value")
235
+ domain: str = Field(..., description="Cookie domain (e.g., '.example.com')")
236
+ path: str = Field(default="/", description="Cookie path")
237
+ expires: float | None = Field(None, description="Expiration timestamp (Unix epoch)")
238
+ httpOnly: bool = Field(default=False, description="HTTP-only flag")
239
+ secure: bool = Field(default=False, description="Secure (HTTPS-only) flag")
240
+ sameSite: Literal["Strict", "Lax", "None"] = Field(
241
+ default="Lax", description="SameSite attribute"
242
+ )
243
+
244
+
245
+ class LocalStorageItem(BaseModel):
246
+ """
247
+ LocalStorage item for a specific origin.
248
+
249
+ Playwright stores localStorage as an array of {name, value} objects.
250
+ """
251
+
252
+ name: str = Field(..., description="LocalStorage key")
253
+ value: str = Field(..., description="LocalStorage value")
254
+
255
+
256
+ class OriginStorage(BaseModel):
257
+ """
258
+ Storage state for a specific origin (localStorage).
259
+
260
+ Represents localStorage data for a single domain.
261
+ """
262
+
263
+ origin: str = Field(..., description="Origin URL (e.g., 'https://example.com')")
264
+ localStorage: list[LocalStorageItem] = Field(
265
+ default_factory=list, description="LocalStorage items for this origin"
266
+ )
267
+
268
+
269
+ class StorageState(BaseModel):
270
+ """
271
+ Complete browser storage state (cookies + localStorage).
272
+
273
+ This is the format used by Playwright's storage_state() method.
274
+ Can be saved to/loaded from JSON files for session injection.
275
+ """
276
+
277
+ cookies: list[Cookie] = Field(
278
+ default_factory=list, description="Cookies to inject (global scope)"
279
+ )
280
+ origins: list[OriginStorage] = Field(
281
+ default_factory=list, description="LocalStorage data per origin"
282
+ )
283
+
284
+ @classmethod
285
+ def from_dict(cls, data: dict) -> "StorageState":
286
+ """
287
+ Create StorageState from dictionary (e.g., loaded from JSON).
288
+
289
+ Args:
290
+ data: Dictionary with 'cookies' and/or 'origins' keys
291
+
292
+ Returns:
293
+ StorageState instance
294
+ """
295
+ cookies = [
296
+ Cookie(**cookie) if isinstance(cookie, dict) else cookie
297
+ for cookie in data.get("cookies", [])
298
+ ]
299
+ origins = []
300
+ for origin_data in data.get("origins", []):
301
+ if isinstance(origin_data, dict):
302
+ # Handle localStorage as array of {name, value} or as dict
303
+ localStorage_data = origin_data.get("localStorage", [])
304
+ if isinstance(localStorage_data, dict):
305
+ # Convert dict to list of LocalStorageItem
306
+ localStorage_items = [
307
+ LocalStorageItem(name=k, value=v) for k, v in localStorage_data.items()
308
+ ]
309
+ else:
310
+ # Already a list
311
+ localStorage_items = [
312
+ LocalStorageItem(**item) if isinstance(item, dict) else item
313
+ for item in localStorage_data
314
+ ]
315
+ origins.append(
316
+ OriginStorage(
317
+ origin=origin_data.get("origin", ""),
318
+ localStorage=localStorage_items,
319
+ )
320
+ )
321
+ else:
322
+ origins.append(origin_data)
323
+ return cls(cookies=cookies, origins=origins)
324
+
325
+ def to_playwright_dict(self) -> dict:
326
+ """
327
+ Convert to Playwright-compatible dictionary format.
328
+
329
+ Returns:
330
+ Dictionary compatible with Playwright's storage_state parameter
331
+ """
332
+ return {
333
+ "cookies": [cookie.model_dump() for cookie in self.cookies],
334
+ "origins": [
335
+ {
336
+ "origin": origin.origin,
337
+ "localStorage": [item.model_dump() for item in origin.localStorage],
338
+ }
339
+ for origin in self.origins
340
+ ],
341
+ }
342
+
343
+
344
+ # ========== Text Search Models (findTextRect) ==========
345
+
346
+
347
+ class TextRect(BaseModel):
348
+ """
349
+ Rectangle coordinates for text occurrence.
350
+ Includes both absolute (page) and viewport-relative coordinates.
351
+ """
352
+
353
+ x: float = Field(..., description="Absolute X coordinate (page coordinate with scroll offset)")
354
+ y: float = Field(..., description="Absolute Y coordinate (page coordinate with scroll offset)")
355
+ width: float = Field(..., description="Rectangle width in pixels")
356
+ height: float = Field(..., description="Rectangle height in pixels")
357
+ left: float = Field(..., description="Absolute left position (same as x)")
358
+ top: float = Field(..., description="Absolute top position (same as y)")
359
+ right: float = Field(..., description="Absolute right position (x + width)")
360
+ bottom: float = Field(..., description="Absolute bottom position (y + height)")
361
+
362
+
363
+ class ViewportRect(BaseModel):
364
+ """Viewport-relative rectangle coordinates (without scroll offset)"""
365
+
366
+ x: float = Field(..., description="Viewport-relative X coordinate")
367
+ y: float = Field(..., description="Viewport-relative Y coordinate")
368
+ width: float = Field(..., description="Rectangle width in pixels")
369
+ height: float = Field(..., description="Rectangle height in pixels")
370
+
371
+
372
+ class TextContext(BaseModel):
373
+ """Context text surrounding a match"""
374
+
375
+ before: str = Field(..., description="Text before the match (up to 20 chars)")
376
+ after: str = Field(..., description="Text after the match (up to 20 chars)")
377
+
378
+
379
+ class TextMatch(BaseModel):
380
+ """A single text match with its rectangle and context"""
381
+
382
+ text: str = Field(..., description="The matched text")
383
+ rect: TextRect = Field(..., description="Absolute rectangle coordinates (with scroll offset)")
384
+ viewport_rect: ViewportRect = Field(
385
+ ..., description="Viewport-relative rectangle (without scroll offset)"
386
+ )
387
+ context: TextContext = Field(..., description="Surrounding text context")
388
+ in_viewport: bool = Field(..., description="Whether the match is currently visible in viewport")
389
+
390
+
391
+ class TextRectSearchResult(BaseModel):
392
+ """
393
+ Result of findTextRect operation.
394
+ Returns all occurrences of text on the page with their exact pixel coordinates.
395
+ """
396
+
397
+ status: Literal["success", "error"]
398
+ query: str | None = Field(None, description="The search text that was queried")
399
+ case_sensitive: bool | None = Field(None, description="Whether search was case-sensitive")
400
+ whole_word: bool | None = Field(None, description="Whether whole-word matching was used")
401
+ matches: int | None = Field(None, description="Number of matches found")
402
+ results: list[TextMatch] | None = Field(
403
+ None, description="List of text matches with coordinates"
404
+ )
405
+ viewport: Viewport | None = Field(None, description="Current viewport dimensions")
406
+ error: str | None = Field(None, description="Error message if status is 'error'")
sentience/overlay.py ADDED
@@ -0,0 +1,115 @@
1
+ """
2
+ Visual overlay utilities - show/clear element highlights in browser
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from .browser import SentienceBrowser
8
+ from .models import Element, Snapshot
9
+
10
+
11
+ def show_overlay(
12
+ browser: SentienceBrowser,
13
+ elements: list[Element] | list[dict[str, Any]] | Snapshot,
14
+ target_element_id: int | None = None,
15
+ ) -> None:
16
+ """
17
+ Display visual overlay highlighting elements in the browser
18
+
19
+ This function shows a Shadow DOM overlay with color-coded borders around
20
+ detected elements. Useful for debugging, learning, and validating element detection.
21
+
22
+ Args:
23
+ browser: SentienceBrowser instance
24
+ elements: Can be:
25
+ - List of Element objects (from snapshot.elements)
26
+ - List of raw element dicts (from snapshot result or API response)
27
+ - Snapshot object (will use snapshot.elements)
28
+ target_element_id: Optional ID of element to highlight in red (default: None)
29
+
30
+ Color Coding:
31
+ - Red: Target element (when target_element_id is specified)
32
+ - Blue: Primary elements (is_primary=true)
33
+ - Green: Regular interactive elements
34
+
35
+ Visual Indicators:
36
+ - Border thickness and opacity scale with importance score
37
+ - Semi-transparent fill for better visibility
38
+ - Importance badges showing scores
39
+ - Star icon for primary elements
40
+ - Target emoji for the target element
41
+
42
+ Auto-clear: Overlay automatically disappears after 5 seconds
43
+
44
+ Example:
45
+ # Show overlay from snapshot
46
+ snap = snapshot(browser)
47
+ show_overlay(browser, snap)
48
+
49
+ # Show overlay with custom elements
50
+ elements = [{"id": 1, "bbox": {"x": 100, "y": 100, "width": 200, "height": 50}, ...}]
51
+ show_overlay(browser, elements)
52
+
53
+ # Show overlay with target element highlighted in red
54
+ show_overlay(browser, snap, target_element_id=42)
55
+
56
+ # Clear overlay manually before 5 seconds
57
+ clear_overlay(browser)
58
+ """
59
+ if not browser.page:
60
+ raise RuntimeError("Browser not started. Call browser.start() first.")
61
+
62
+ # Handle different input types
63
+ if isinstance(elements, Snapshot):
64
+ # Extract elements from Snapshot object
65
+ elements_list = [el.model_dump() for el in elements.elements]
66
+ elif isinstance(elements, list) and len(elements) > 0:
67
+ # Check if it's a list of Element objects or dicts
68
+ if hasattr(elements[0], "model_dump"):
69
+ # List of Element objects
70
+ elements_list = [el.model_dump() for el in elements]
71
+ else:
72
+ # Already a list of dicts
73
+ elements_list = elements
74
+ else:
75
+ raise ValueError("elements must be a Snapshot, list of Element objects, or list of dicts")
76
+
77
+ # Call extension API
78
+ browser.page.evaluate(
79
+ """
80
+ (args) => {
81
+ if (window.sentience && window.sentience.showOverlay) {
82
+ window.sentience.showOverlay(args.elements, args.targetId);
83
+ } else {
84
+ console.warn('[Sentience SDK] showOverlay not available - is extension loaded?');
85
+ }
86
+ }
87
+ """,
88
+ {"elements": elements_list, "targetId": target_element_id},
89
+ )
90
+
91
+
92
+ def clear_overlay(browser: SentienceBrowser) -> None:
93
+ """
94
+ Clear the visual overlay manually (before 5-second auto-clear)
95
+
96
+ Args:
97
+ browser: SentienceBrowser instance
98
+
99
+ Example:
100
+ show_overlay(browser, snap)
101
+ # ... inspect overlay ...
102
+ clear_overlay(browser) # Remove immediately
103
+ """
104
+ if not browser.page:
105
+ raise RuntimeError("Browser not started. Call browser.start() first.")
106
+
107
+ browser.page.evaluate(
108
+ """
109
+ () => {
110
+ if (window.sentience && window.sentience.clearOverlay) {
111
+ window.sentience.clearOverlay();
112
+ }
113
+ }
114
+ """
115
+ )