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
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from sentience.browser import AsyncSentienceBrowser
6
+ from sentience.tracing import Tracer
7
+
8
+
9
+ @dataclass
10
+ class SentienceLangChainContext:
11
+ """
12
+ Context for LangChain/LangGraph integrations.
13
+
14
+ We keep this small and explicit; it mirrors the PydanticAI deps object.
15
+ """
16
+
17
+ browser: AsyncSentienceBrowser
18
+ tracer: Tracer | None = None
@@ -0,0 +1,326 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import re
5
+ import time
6
+ from typing import Any, Literal
7
+
8
+ from sentience.actions import (
9
+ click_async,
10
+ click_rect_async,
11
+ press_async,
12
+ scroll_to_async,
13
+ type_text_async,
14
+ )
15
+ from sentience.integrations.models import AssertionResult, BrowserState, ElementSummary
16
+ from sentience.models import ReadResult, SnapshotOptions, TextRectSearchResult
17
+ from sentience.read import read_async
18
+ from sentience.snapshot import snapshot_async
19
+ from sentience.text_search import find_text_rect_async
20
+ from sentience.trace_event_builder import TraceEventBuilder
21
+
22
+ from .context import SentienceLangChainContext
23
+
24
+
25
+ class SentienceLangChainCore:
26
+ """
27
+ Framework-agnostic (LangChain-friendly) async wrappers around Sentience SDK.
28
+
29
+ - No LangChain imports
30
+ - Optional Sentience tracing (local/cloud) if ctx.tracer is provided
31
+ """
32
+
33
+ def __init__(self, ctx: SentienceLangChainContext):
34
+ self.ctx = ctx
35
+ self._step_counter = 0
36
+
37
+ def _safe_tracer_call(self, method_name: str, *args, **kwargs) -> None:
38
+ tracer = self.ctx.tracer
39
+ if not tracer:
40
+ return
41
+ try:
42
+ getattr(tracer, method_name)(*args, **kwargs)
43
+ except Exception:
44
+ # Tracing must be non-fatal
45
+ pass
46
+
47
+ async def _trace(self, tool_name: str, exec_coro, exec_meta: dict[str, Any]):
48
+ tracer = self.ctx.tracer
49
+ browser = self.ctx.browser
50
+
51
+ pre_url = getattr(getattr(browser, "page", None), "url", None)
52
+
53
+ # Emit run_start once (best-effort)
54
+ if tracer and getattr(tracer, "started_at", None) is None:
55
+ self._safe_tracer_call(
56
+ "emit_run_start",
57
+ agent="LangChain+SentienceTools",
58
+ llm_model=None,
59
+ config={"integration": "langchain"},
60
+ )
61
+
62
+ step_id = None
63
+ step_index = None
64
+ start = time.time()
65
+ if tracer:
66
+ self._step_counter += 1
67
+ step_index = self._step_counter
68
+ step_id = f"tool-{step_index}:{tool_name}"
69
+ self._safe_tracer_call(
70
+ "emit_step_start",
71
+ step_id=step_id,
72
+ step_index=step_index,
73
+ goal=f"tool:{tool_name}",
74
+ attempt=0,
75
+ pre_url=pre_url,
76
+ )
77
+
78
+ try:
79
+ result = await exec_coro()
80
+
81
+ if tracer and step_id and step_index:
82
+ post_url = getattr(getattr(browser, "page", None), "url", pre_url)
83
+ duration_ms = int((time.time() - start) * 1000)
84
+
85
+ success: bool | None = None
86
+ if hasattr(result, "success"):
87
+ success = bool(getattr(result, "success"))
88
+ elif hasattr(result, "status"):
89
+ success = getattr(result, "status") == "success"
90
+ elif isinstance(result, dict):
91
+ if "success" in result:
92
+ try:
93
+ success = bool(result.get("success"))
94
+ except Exception:
95
+ success = None
96
+ elif "status" in result:
97
+ success = result.get("status") == "success"
98
+
99
+ exec_data = {"tool": tool_name, "duration_ms": duration_ms, **exec_meta}
100
+ if success is not None:
101
+ exec_data["success"] = success
102
+
103
+ verify_data = {
104
+ "passed": bool(success) if success is not None else True,
105
+ "signals": {},
106
+ }
107
+
108
+ step_end_data = TraceEventBuilder.build_step_end_event(
109
+ step_id=step_id,
110
+ step_index=step_index,
111
+ goal=f"tool:{tool_name}",
112
+ attempt=0,
113
+ pre_url=pre_url or "",
114
+ post_url=post_url or "",
115
+ snapshot_digest=None,
116
+ llm_data={},
117
+ exec_data=exec_data,
118
+ verify_data=verify_data,
119
+ )
120
+ self._safe_tracer_call("emit", "step_end", step_end_data, step_id=step_id)
121
+
122
+ return result
123
+ except Exception as e:
124
+ if tracer and step_id:
125
+ self._safe_tracer_call("emit_error", step_id=step_id, error=str(e), attempt=0)
126
+ raise
127
+
128
+ # ===== Observe =====
129
+ async def snapshot_state(
130
+ self, limit: int = 50, include_screenshot: bool = False
131
+ ) -> BrowserState:
132
+ async def _run():
133
+ opts = SnapshotOptions(limit=limit, screenshot=include_screenshot)
134
+ snap = await snapshot_async(self.ctx.browser, opts)
135
+ if getattr(snap, "status", "success") != "success":
136
+ raise RuntimeError(getattr(snap, "error", None) or "snapshot failed")
137
+ elements = [
138
+ ElementSummary(
139
+ id=e.id,
140
+ role=e.role,
141
+ text=e.text,
142
+ importance=e.importance,
143
+ bbox=e.bbox,
144
+ )
145
+ for e in snap.elements
146
+ ]
147
+ return BrowserState(url=snap.url, elements=elements)
148
+
149
+ return await self._trace(
150
+ "snapshot_state",
151
+ _run,
152
+ {"limit": limit, "include_screenshot": include_screenshot},
153
+ )
154
+
155
+ async def read_page(
156
+ self,
157
+ format: Literal["raw", "text", "markdown"] = "text",
158
+ enhance_markdown: bool = True,
159
+ ) -> ReadResult:
160
+ async def _run():
161
+ return await read_async(
162
+ self.ctx.browser, output_format=format, enhance_markdown=enhance_markdown
163
+ )
164
+
165
+ return await self._trace(
166
+ "read_page",
167
+ _run,
168
+ {"format": format, "enhance_markdown": enhance_markdown},
169
+ )
170
+
171
+ # ===== Act =====
172
+ async def click(self, element_id: int):
173
+ return await self._trace(
174
+ "click",
175
+ lambda: click_async(self.ctx.browser, element_id),
176
+ {"element_id": element_id},
177
+ )
178
+
179
+ async def type_text(self, element_id: int, text: str):
180
+ # avoid tracing text (PII)
181
+ return await self._trace(
182
+ "type_text",
183
+ lambda: type_text_async(self.ctx.browser, element_id, text),
184
+ {"element_id": element_id},
185
+ )
186
+
187
+ async def press_key(self, key: str):
188
+ return await self._trace(
189
+ "press_key", lambda: press_async(self.ctx.browser, key), {"key": key}
190
+ )
191
+
192
+ async def scroll_to(
193
+ self,
194
+ element_id: int,
195
+ behavior: Literal["smooth", "instant", "auto"] = "smooth",
196
+ block: Literal["start", "center", "end", "nearest"] = "center",
197
+ ):
198
+ return await self._trace(
199
+ "scroll_to",
200
+ lambda: scroll_to_async(self.ctx.browser, element_id, behavior=behavior, block=block),
201
+ {"element_id": element_id, "behavior": behavior, "block": block},
202
+ )
203
+
204
+ async def navigate(self, url: str) -> dict[str, Any]:
205
+ async def _run():
206
+ await self.ctx.browser.goto(url)
207
+ post_url = getattr(getattr(self.ctx.browser, "page", None), "url", None)
208
+ return {"success": True, "url": post_url or url}
209
+
210
+ return await self._trace("navigate", _run, {"url": url})
211
+
212
+ async def click_rect(
213
+ self,
214
+ *,
215
+ x: float,
216
+ y: float,
217
+ width: float,
218
+ height: float,
219
+ button: Literal["left", "right", "middle"] = "left",
220
+ click_count: int = 1,
221
+ ):
222
+ async def _run():
223
+ return await click_rect_async(
224
+ self.ctx.browser,
225
+ {"x": x, "y": y, "w": width, "h": height},
226
+ button=button,
227
+ click_count=click_count,
228
+ )
229
+
230
+ return await self._trace(
231
+ "click_rect",
232
+ _run,
233
+ {
234
+ "x": x,
235
+ "y": y,
236
+ "width": width,
237
+ "height": height,
238
+ "button": button,
239
+ "click_count": click_count,
240
+ },
241
+ )
242
+
243
+ async def find_text_rect(
244
+ self,
245
+ text: str,
246
+ case_sensitive: bool = False,
247
+ whole_word: bool = False,
248
+ max_results: int = 10,
249
+ ) -> TextRectSearchResult:
250
+ async def _run():
251
+ return await find_text_rect_async(
252
+ self.ctx.browser,
253
+ text,
254
+ case_sensitive=case_sensitive,
255
+ whole_word=whole_word,
256
+ max_results=max_results,
257
+ )
258
+
259
+ return await self._trace(
260
+ "find_text_rect",
261
+ _run,
262
+ {
263
+ "query": text,
264
+ "case_sensitive": case_sensitive,
265
+ "whole_word": whole_word,
266
+ "max_results": max_results,
267
+ },
268
+ )
269
+
270
+ # ===== Verify / guard =====
271
+ async def verify_url_matches(self, pattern: str, flags: int = 0) -> AssertionResult:
272
+ async def _run():
273
+ page = getattr(self.ctx.browser, "page", None)
274
+ if not page:
275
+ return AssertionResult(passed=False, reason="Browser not started (page is None)")
276
+ url = page.url
277
+ ok = re.search(pattern, url, flags) is not None
278
+ return AssertionResult(
279
+ passed=ok,
280
+ reason="" if ok else f"URL did not match pattern. url={url!r} pattern={pattern!r}",
281
+ details={"url": url, "pattern": pattern},
282
+ )
283
+
284
+ return await self._trace("verify_url_matches", _run, {"pattern": pattern})
285
+
286
+ async def verify_text_present(
287
+ self,
288
+ text: str,
289
+ *,
290
+ format: Literal["text", "markdown", "raw"] = "text",
291
+ case_sensitive: bool = False,
292
+ ) -> AssertionResult:
293
+ async def _run():
294
+ result = await read_async(self.ctx.browser, output_format=format, enhance_markdown=True)
295
+ if result.status != "success":
296
+ return AssertionResult(
297
+ passed=False, reason=f"read failed: {result.error}", details={}
298
+ )
299
+
300
+ haystack = result.content if case_sensitive else result.content.lower()
301
+ needle = text if case_sensitive else text.lower()
302
+ ok = needle in haystack
303
+ return AssertionResult(
304
+ passed=ok,
305
+ reason="" if ok else f"Text not present: {text!r}",
306
+ details={"format": format, "query": text, "length": result.length},
307
+ )
308
+
309
+ return await self._trace("verify_text_present", _run, {"query": text, "format": format})
310
+
311
+ async def assert_eventually_url_matches(
312
+ self,
313
+ pattern: str,
314
+ *,
315
+ timeout_s: float = 10.0,
316
+ poll_s: float = 0.25,
317
+ flags: int = 0,
318
+ ) -> AssertionResult:
319
+ deadline = time.monotonic() + timeout_s
320
+ last: AssertionResult | None = None
321
+ while time.monotonic() <= deadline:
322
+ last = await self.verify_url_matches(pattern, flags)
323
+ if last.passed:
324
+ return last
325
+ await asyncio.sleep(poll_s)
326
+ return last or AssertionResult(passed=False, reason="No attempts executed", details={})
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .context import SentienceLangChainContext
8
+ from .core import SentienceLangChainCore
9
+
10
+
11
+ def build_sentience_langchain_tools(ctx: SentienceLangChainContext) -> list[Any]:
12
+ """
13
+ Build LangChain tools backed by Sentience.
14
+
15
+ LangChain is an optional dependency; imports are done lazily here so that
16
+ `import sentience` works without LangChain installed.
17
+ """
18
+
19
+ try:
20
+ from langchain_core.tools import StructuredTool
21
+ except Exception: # pragma: no cover
22
+ from langchain.tools import StructuredTool # type: ignore
23
+
24
+ core = SentienceLangChainCore(ctx)
25
+
26
+ # ---- Schemas ----
27
+ class SnapshotStateArgs(BaseModel):
28
+ limit: int = Field(50, ge=1, le=500, description="Max elements to return (default 50)")
29
+ include_screenshot: bool = Field(
30
+ False, description="Include screenshot in snapshot (default false)"
31
+ )
32
+
33
+ class ReadPageArgs(BaseModel):
34
+ format: Literal["raw", "text", "markdown"] = Field("text", description="Output format")
35
+ enhance_markdown: bool = Field(
36
+ True, description="Enhance markdown conversion (default true)"
37
+ )
38
+
39
+ class ClickArgs(BaseModel):
40
+ element_id: int = Field(..., description="Sentience element id from snapshot_state()")
41
+
42
+ class TypeTextArgs(BaseModel):
43
+ element_id: int = Field(..., description="Sentience element id from snapshot_state()")
44
+ text: str = Field(..., description="Text to type")
45
+
46
+ class PressKeyArgs(BaseModel):
47
+ key: str = Field(..., description="Key to press (e.g., Enter, Escape, Tab)")
48
+
49
+ class ScrollToArgs(BaseModel):
50
+ element_id: int = Field(..., description="Sentience element id from snapshot_state()")
51
+ behavior: Literal["smooth", "instant", "auto"] = Field(
52
+ "smooth", description="Scroll behavior"
53
+ )
54
+ block: Literal["start", "center", "end", "nearest"] = Field(
55
+ "center", description="Vertical alignment"
56
+ )
57
+
58
+ class NavigateArgs(BaseModel):
59
+ url: str = Field(..., description="URL to navigate to")
60
+
61
+ class ClickRectArgs(BaseModel):
62
+ x: float = Field(..., description="Rect x (px)")
63
+ y: float = Field(..., description="Rect y (px)")
64
+ width: float = Field(..., description="Rect width (px)")
65
+ height: float = Field(..., description="Rect height (px)")
66
+ button: Literal["left", "right", "middle"] = Field("left", description="Mouse button")
67
+ click_count: int = Field(1, ge=1, le=3, description="Click count")
68
+
69
+ class FindTextRectArgs(BaseModel):
70
+ text: str = Field(..., description="Text to search for")
71
+ case_sensitive: bool = Field(False, description="Case sensitive search")
72
+ whole_word: bool = Field(False, description="Whole-word match only")
73
+ max_results: int = Field(10, ge=1, le=100, description="Max matches (capped at 100)")
74
+
75
+ class VerifyUrlMatchesArgs(BaseModel):
76
+ pattern: str = Field(..., description="Regex pattern to match against current URL")
77
+
78
+ class VerifyTextPresentArgs(BaseModel):
79
+ text: str = Field(..., description="Text to check for in read_page output")
80
+ format: Literal["text", "markdown", "raw"] = Field("text", description="Read format")
81
+ case_sensitive: bool = Field(False, description="Case sensitive check")
82
+
83
+ class AssertEventuallyUrlMatchesArgs(BaseModel):
84
+ pattern: str = Field(..., description="Regex pattern to match against current URL")
85
+ timeout_s: float = Field(10.0, ge=0.1, description="Timeout seconds")
86
+ poll_s: float = Field(0.25, ge=0.05, description="Polling interval seconds")
87
+
88
+ # ---- Sync wrappers (explicitly unsupported) ----
89
+ def _sync_unsupported(*args, **kwargs):
90
+ raise RuntimeError(
91
+ "Sentience LangChain tools are async-only. Use an async LangChain agent/runner."
92
+ )
93
+
94
+ # ---- Tools ----
95
+ return [
96
+ StructuredTool(
97
+ name="sentience_snapshot_state",
98
+ description="Observe: take a bounded Sentience snapshot and return a typed BrowserState (url + elements).",
99
+ args_schema=SnapshotStateArgs,
100
+ func=_sync_unsupported,
101
+ coroutine=lambda **kw: core.snapshot_state(**kw),
102
+ ),
103
+ StructuredTool(
104
+ name="sentience_read_page",
105
+ description="Observe: read page content as text/markdown/raw HTML.",
106
+ args_schema=ReadPageArgs,
107
+ func=_sync_unsupported,
108
+ coroutine=lambda **kw: core.read_page(**kw),
109
+ ),
110
+ StructuredTool(
111
+ name="sentience_click",
112
+ description="Act: click an element by element_id from snapshot_state.",
113
+ args_schema=ClickArgs,
114
+ func=_sync_unsupported,
115
+ coroutine=lambda **kw: core.click(**kw),
116
+ ),
117
+ StructuredTool(
118
+ name="sentience_type_text",
119
+ description="Act: type text into an element by element_id from snapshot_state.",
120
+ args_schema=TypeTextArgs,
121
+ func=_sync_unsupported,
122
+ coroutine=lambda **kw: core.type_text(**kw),
123
+ ),
124
+ StructuredTool(
125
+ name="sentience_press_key",
126
+ description="Act: press a keyboard key (Enter/Escape/Tab/etc.).",
127
+ args_schema=PressKeyArgs,
128
+ func=_sync_unsupported,
129
+ coroutine=lambda **kw: core.press_key(**kw),
130
+ ),
131
+ StructuredTool(
132
+ name="sentience_scroll_to",
133
+ description="Act: scroll an element into view by element_id from snapshot_state.",
134
+ args_schema=ScrollToArgs,
135
+ func=_sync_unsupported,
136
+ coroutine=lambda **kw: core.scroll_to(**kw),
137
+ ),
138
+ StructuredTool(
139
+ name="sentience_navigate",
140
+ description="Act: navigate to a URL using the underlying Playwright page.goto.",
141
+ args_schema=NavigateArgs,
142
+ func=_sync_unsupported,
143
+ coroutine=lambda **kw: core.navigate(**kw),
144
+ ),
145
+ StructuredTool(
146
+ name="sentience_click_rect",
147
+ description="Act: click a rectangle by pixel coordinates (useful with find_text_rect).",
148
+ args_schema=ClickRectArgs,
149
+ func=_sync_unsupported,
150
+ coroutine=lambda **kw: core.click_rect(**kw),
151
+ ),
152
+ StructuredTool(
153
+ name="sentience_find_text_rect",
154
+ description="Locate: find text occurrences on the page and return pixel coordinates.",
155
+ args_schema=FindTextRectArgs,
156
+ func=_sync_unsupported,
157
+ coroutine=lambda **kw: core.find_text_rect(**kw),
158
+ ),
159
+ StructuredTool(
160
+ name="sentience_verify_url_matches",
161
+ description="Verify: check current URL matches a regex pattern (post-action guard).",
162
+ args_schema=VerifyUrlMatchesArgs,
163
+ func=_sync_unsupported,
164
+ coroutine=lambda **kw: core.verify_url_matches(**kw),
165
+ ),
166
+ StructuredTool(
167
+ name="sentience_verify_text_present",
168
+ description="Verify: check that a text substring is present in read_page output.",
169
+ args_schema=VerifyTextPresentArgs,
170
+ func=_sync_unsupported,
171
+ coroutine=lambda **kw: core.verify_text_present(**kw),
172
+ ),
173
+ StructuredTool(
174
+ name="sentience_assert_eventually_url_matches",
175
+ description="Verify: retry URL regex match until timeout (use for delayed navigation/redirects).",
176
+ args_schema=AssertEventuallyUrlMatchesArgs,
177
+ func=_sync_unsupported,
178
+ coroutine=lambda **kw: core.assert_eventually_url_matches(**kw),
179
+ ),
180
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Shared typed models for integrations (internal).
3
+
4
+ These are intentionally small, framework-friendly return types for tool wrappers.
5
+ They wrap/derive from existing Sentience SDK types while keeping payloads bounded
6
+ and predictable for LLM tool calls.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel
14
+
15
+ from sentience.models import BBox
16
+
17
+
18
+ class ElementSummary(BaseModel):
19
+ """A small, stable subset of `sentience.models.Element` suitable for tool returns."""
20
+
21
+ id: int
22
+ role: str
23
+ text: str | None = None
24
+ importance: int | None = None
25
+ bbox: BBox | None = None
26
+
27
+
28
+ class BrowserState(BaseModel):
29
+ """
30
+ Minimal browser state for integrations.
31
+
32
+ Notes:
33
+ - Keep this payload bounded: prefer `snapshot(limit=50)` and summarize elements.
34
+ - Integrations can extend this in their own packages without changing core SDK.
35
+ """
36
+
37
+ url: str
38
+ elements: list[ElementSummary]
39
+
40
+
41
+ class AssertionResult(BaseModel):
42
+ """Framework-friendly assertion/guard result."""
43
+
44
+ passed: bool
45
+ reason: str = ""
46
+ details: dict[str, Any] = {}
@@ -0,0 +1,15 @@
1
+ """
2
+ PydanticAI integration helpers (optional).
3
+
4
+ This module does NOT import `pydantic_ai` at import time so the base SDK can be
5
+ installed without the optional dependency. Users should install:
6
+
7
+ pip install sentienceapi[pydanticai]
8
+
9
+ and then use `register_sentience_tools(...)` with a PydanticAI `Agent`.
10
+ """
11
+
12
+ from .deps import SentiencePydanticDeps
13
+ from .toolset import register_sentience_tools
14
+
15
+ __all__ = ["SentiencePydanticDeps", "register_sentience_tools"]
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from sentience.browser import AsyncSentienceBrowser
7
+ from sentience.tracing import Tracer
8
+
9
+
10
+ @dataclass
11
+ class SentiencePydanticDeps:
12
+ """
13
+ Dependencies passed into PydanticAI tools via ctx.deps.
14
+
15
+ At minimum we carry the live `AsyncSentienceBrowser`.
16
+ """
17
+
18
+ browser: AsyncSentienceBrowser
19
+ runtime: Any | None = None
20
+ tracer: Tracer | None = None