sentienceapi 0.90.11__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 (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 +150 -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.11.dist-info/METADATA +878 -0
  42. sentienceapi-0.90.11.dist-info/RECORD +46 -0
  43. sentienceapi-0.90.11.dist-info/WHEEL +5 -0
  44. sentienceapi-0.90.11.dist-info/entry_points.txt +2 -0
  45. sentienceapi-0.90.11.dist-info/licenses/LICENSE.md +43 -0
  46. sentienceapi-0.90.11.dist-info/top_level.txt +1 -0
@@ -0,0 +1,509 @@
1
+ """
2
+ Conversational Agent: Natural language interface for Sentience SDK
3
+ Enables end users to control web automation using plain English
4
+ """
5
+
6
+ import json
7
+ import time
8
+ from typing import Any
9
+
10
+ from .agent import SentienceAgent
11
+ from .browser import SentienceBrowser
12
+ from .llm_provider import LLMProvider
13
+ from .models import Snapshot
14
+ from .snapshot import snapshot
15
+
16
+
17
+ class ConversationalAgent:
18
+ """
19
+ Natural language agent that translates user intent into SDK actions
20
+ and returns human-readable results.
21
+
22
+ This is Layer 4 - the highest abstraction level for non-technical users.
23
+
24
+ Example:
25
+ >>> agent = ConversationalAgent(browser, llm)
26
+ >>> result = agent.execute("Search for magic mouse on google.com")
27
+ >>> print(result)
28
+ "I searched for 'magic mouse' on Google and found several results.
29
+ The top result is from amazon.com selling the Apple Magic Mouse 2 for $79."
30
+ """
31
+
32
+ def __init__(self, browser: SentienceBrowser, llm: LLMProvider, verbose: bool = True):
33
+ """
34
+ Initialize conversational agent
35
+
36
+ Args:
37
+ browser: SentienceBrowser instance
38
+ llm: LLM provider (OpenAI, Anthropic, LocalLLM, etc.)
39
+ verbose: Print step-by-step execution logs (default: True)
40
+ """
41
+ self.browser = browser
42
+ self.llm = llm
43
+ self.verbose = verbose
44
+
45
+ # Underlying technical agent
46
+ self.technical_agent = SentienceAgent(browser, llm, verbose=False)
47
+
48
+ # Conversation history and context
49
+ self.conversation_history: list[dict[str, Any]] = []
50
+ self.execution_context: dict[str, Any] = {
51
+ "current_url": None,
52
+ "last_action": None,
53
+ "discovered_elements": [],
54
+ "session_data": {},
55
+ }
56
+
57
+ def execute(self, user_input: str) -> str:
58
+ """
59
+ Execute a natural language command and return natural language result
60
+
61
+ Args:
62
+ user_input: Natural language instruction (e.g., "Search for magic mouse")
63
+
64
+ Returns:
65
+ Human-readable result description
66
+
67
+ Example:
68
+ >>> agent.execute("Go to google.com and search for magic mouse")
69
+ "I navigated to google.com, searched for 'magic mouse', and found 10 results.
70
+ The top result is from amazon.com selling Magic Mouse 2 for $79."
71
+ """
72
+ if self.verbose:
73
+ print(f"\n{'=' * 70}")
74
+ print(f"👤 User: {user_input}")
75
+ print(f"{'=' * 70}")
76
+
77
+ start_time = time.time()
78
+
79
+ # Step 1: Plan the execution (break down into atomic steps)
80
+ plan = self._create_plan(user_input)
81
+
82
+ if self.verbose:
83
+ print("\n📋 Execution Plan:")
84
+ for i, step in enumerate(plan["steps"], 1):
85
+ print(f" {i}. {step['description']}")
86
+
87
+ # Step 2: Execute each step
88
+ execution_results = []
89
+ for step in plan["steps"]:
90
+ step_result = self._execute_step(step)
91
+ execution_results.append(step_result)
92
+
93
+ if not step_result.get("success", False):
94
+ # Early exit on failure
95
+ if self.verbose:
96
+ print(f"⚠️ Step failed: {step['description']}")
97
+ break
98
+
99
+ # Step 3: Synthesize natural language response
100
+ response = self._synthesize_response(user_input, plan, execution_results)
101
+
102
+ duration_ms = int((time.time() - start_time) * 1000)
103
+
104
+ # Step 4: Update conversation history
105
+ self.conversation_history.append(
106
+ {
107
+ "user_input": user_input,
108
+ "plan": plan,
109
+ "results": execution_results,
110
+ "response": response,
111
+ "duration_ms": duration_ms,
112
+ }
113
+ )
114
+
115
+ if self.verbose:
116
+ print(f"\n🤖 Agent: {response}")
117
+ print(f"⏱️ Completed in {duration_ms}ms\n")
118
+
119
+ return response
120
+
121
+ def _create_plan(self, user_input: str) -> dict[str, Any]:
122
+ """
123
+ Use LLM to break down user input into atomic executable steps
124
+
125
+ Args:
126
+ user_input: Natural language command
127
+
128
+ Returns:
129
+ Plan dictionary with list of atomic steps
130
+ """
131
+ # Get current page context
132
+ current_url = self.browser.page.url if self.browser.page else "None"
133
+
134
+ system_prompt = """You are a web automation planning assistant.
135
+
136
+ Your job is to analyze a natural language request and break it down into atomic steps
137
+ that can be executed by a web automation agent.
138
+
139
+ AVAILABLE ACTIONS:
140
+ 1. NAVIGATE - Go to a URL
141
+ 2. FIND_AND_CLICK - Find and click an element by description
142
+ 3. FIND_AND_TYPE - Find input field and type text
143
+ 4. PRESS_KEY - Press a keyboard key (Enter, Escape, etc.)
144
+ 5. WAIT - Wait for page to load or element to appear
145
+ 6. EXTRACT_INFO - Extract specific information from the page
146
+ 7. VERIFY - Verify a condition is met
147
+
148
+ RESPONSE FORMAT (JSON):
149
+ {
150
+ "intent": "brief summary of user intent",
151
+ "steps": [
152
+ {
153
+ "action": "NAVIGATE" | "FIND_AND_CLICK" | "FIND_AND_TYPE" | "PRESS_KEY" | "WAIT" | "EXTRACT_INFO" | "VERIFY",
154
+ "description": "human-readable description",
155
+ "parameters": {
156
+ "url": "https://...",
157
+ "element_description": "search box",
158
+ "text": "magic mouse",
159
+ "key": "Enter",
160
+ "duration": 2.0,
161
+ "info_type": "product link",
162
+ "condition": "page contains results"
163
+ }
164
+ }
165
+ ],
166
+ "expected_outcome": "what success looks like"
167
+ }
168
+
169
+ IMPORTANT: Return ONLY valid JSON, no markdown, no code blocks."""
170
+
171
+ user_prompt = f"""Current URL: {current_url}
172
+
173
+ User Request: {user_input}
174
+
175
+ Create a step-by-step execution plan."""
176
+
177
+ try:
178
+ response = self.llm.generate(
179
+ system_prompt,
180
+ user_prompt,
181
+ json_mode=self.llm.supports_json_mode(),
182
+ temperature=0.0,
183
+ )
184
+
185
+ # Parse JSON response
186
+ plan = json.loads(response.content)
187
+ return plan
188
+
189
+ except json.JSONDecodeError as e:
190
+ # Fallback: create simple plan
191
+ if self.verbose:
192
+ print(f"⚠️ JSON parsing failed, using fallback plan: {e}")
193
+
194
+ return {
195
+ "intent": user_input,
196
+ "steps": [
197
+ {
198
+ "action": "FIND_AND_CLICK",
199
+ "description": user_input,
200
+ "parameters": {"element_description": user_input},
201
+ }
202
+ ],
203
+ "expected_outcome": "Complete user request",
204
+ }
205
+
206
+ def _execute_step(self, step: dict[str, Any]) -> dict[str, Any]:
207
+ """
208
+ Execute a single atomic step from the plan
209
+
210
+ Args:
211
+ step: Step dictionary with action and parameters
212
+
213
+ Returns:
214
+ Execution result with success status and data
215
+ """
216
+ action = step["action"]
217
+ params = step.get("parameters", {})
218
+
219
+ if self.verbose:
220
+ print(f"\n⚙️ Executing: {step['description']}")
221
+
222
+ try:
223
+ if action == "NAVIGATE":
224
+ url = params["url"]
225
+ # Add https:// if missing
226
+ if not url.startswith(("http://", "https://")):
227
+ url = "https://" + url
228
+
229
+ self.browser.page.goto(url, wait_until="domcontentloaded")
230
+ self.execution_context["current_url"] = url
231
+ time.sleep(1) # Brief wait for page to settle
232
+
233
+ return {"success": True, "action": action, "data": {"url": url}}
234
+
235
+ elif action == "FIND_AND_CLICK":
236
+ element_desc = params["element_description"]
237
+ # Use technical agent to find and click (returns AgentActionResult)
238
+ result = self.technical_agent.act(f"Click the {element_desc}")
239
+ return {
240
+ "success": result.success, # Use attribute access
241
+ "action": action,
242
+ "data": result.model_dump(), # Convert to dict for flexibility
243
+ }
244
+
245
+ elif action == "FIND_AND_TYPE":
246
+ element_desc = params["element_description"]
247
+ text = params["text"]
248
+ # Use technical agent to find input and type (returns AgentActionResult)
249
+ result = self.technical_agent.act(f"Type '{text}' into {element_desc}")
250
+ return {
251
+ "success": result.success, # Use attribute access
252
+ "action": action,
253
+ "data": {"text": text, "result": result.model_dump()},
254
+ }
255
+
256
+ elif action == "PRESS_KEY":
257
+ key = params["key"]
258
+ result = self.technical_agent.act(f"Press {key} key")
259
+ return {
260
+ "success": result.success, # Use attribute access
261
+ "action": action,
262
+ "data": {"key": key, "result": result.model_dump()},
263
+ }
264
+
265
+ elif action == "WAIT":
266
+ duration = params.get("duration", 2.0)
267
+ time.sleep(duration)
268
+ return {
269
+ "success": True,
270
+ "action": action,
271
+ "data": {"duration": duration},
272
+ }
273
+
274
+ elif action == "EXTRACT_INFO":
275
+ info_type = params["info_type"]
276
+ # Get current page snapshot and extract info
277
+ snap = snapshot(self.browser, limit=50)
278
+
279
+ # Use LLM to extract specific information
280
+ extracted = self._extract_information(snap, info_type)
281
+
282
+ return {
283
+ "success": True,
284
+ "action": action,
285
+ "data": {"extracted": extracted, "info_type": info_type},
286
+ }
287
+
288
+ elif action == "VERIFY":
289
+ condition = params["condition"]
290
+ # Verify condition using current page state
291
+ is_verified = self._verify_condition(condition)
292
+ return {
293
+ "success": is_verified,
294
+ "action": action,
295
+ "data": {"condition": condition, "verified": is_verified},
296
+ }
297
+
298
+ else:
299
+ raise ValueError(f"Unknown action: {action}")
300
+
301
+ except Exception as e:
302
+ if self.verbose:
303
+ print(f"❌ Step failed: {e}")
304
+ return {"success": False, "action": action, "error": str(e)}
305
+
306
+ def _extract_information(self, snap: Snapshot, info_type: str) -> dict[str, Any]:
307
+ """
308
+ Extract specific information from snapshot using LLM
309
+
310
+ Args:
311
+ snap: Snapshot object
312
+ info_type: Type of info to extract (e.g., "product link", "price")
313
+
314
+ Returns:
315
+ Extracted information dictionary
316
+ """
317
+ # Build context from snapshot
318
+ elements_text = "\n".join(
319
+ [
320
+ f"[{el.id}] {el.role}: {el.text} (importance: {el.importance})"
321
+ for el in snap.elements[:30] # Top 30 elements
322
+ ]
323
+ )
324
+
325
+ system_prompt = f"""Extract {info_type} from the following page elements.
326
+
327
+ ELEMENTS:
328
+ {elements_text}
329
+
330
+ Return JSON with extracted information:
331
+ {{
332
+ "found": true/false,
333
+ "data": {{
334
+ // extracted information fields
335
+ }},
336
+ "summary": "brief description of what was found"
337
+ }}"""
338
+
339
+ user_prompt = f"Extract {info_type} from the elements above."
340
+
341
+ try:
342
+ response = self.llm.generate(
343
+ system_prompt, user_prompt, json_mode=self.llm.supports_json_mode()
344
+ )
345
+ return json.loads(response.content)
346
+ except:
347
+ return {
348
+ "found": False,
349
+ "data": {},
350
+ "summary": "Failed to extract information",
351
+ }
352
+
353
+ def _verify_condition(self, condition: str) -> bool:
354
+ """
355
+ Verify a condition is met on current page
356
+
357
+ Args:
358
+ condition: Natural language condition to verify
359
+
360
+ Returns:
361
+ True if condition is met, False otherwise
362
+ """
363
+ try:
364
+ snap = snapshot(self.browser, limit=30)
365
+
366
+ # Build context
367
+ elements_text = "\n".join([f"{el.role}: {el.text}" for el in snap.elements[:20]])
368
+
369
+ system_prompt = f"""Verify if the following condition is met based on page elements.
370
+
371
+ CONDITION: {condition}
372
+
373
+ PAGE ELEMENTS:
374
+ {elements_text}
375
+
376
+ Return JSON:
377
+ {{
378
+ "verified": true/false,
379
+ "reasoning": "explanation"
380
+ }}"""
381
+
382
+ response = self.llm.generate(system_prompt, "", json_mode=self.llm.supports_json_mode())
383
+ result = json.loads(response.content)
384
+ return result.get("verified", False)
385
+ except:
386
+ return False
387
+
388
+ def _synthesize_response(
389
+ self,
390
+ user_input: str,
391
+ plan: dict[str, Any],
392
+ execution_results: list[dict[str, Any]],
393
+ ) -> str:
394
+ """
395
+ Synthesize a natural language response from execution results
396
+
397
+ Args:
398
+ user_input: Original user input
399
+ plan: Execution plan
400
+ execution_results: List of step execution results
401
+
402
+ Returns:
403
+ Human-readable response string
404
+ """
405
+ # Build summary of what happened
406
+ successful_steps = [r for r in execution_results if r.get("success")]
407
+ failed_steps = [r for r in execution_results if not r.get("success")]
408
+
409
+ # Extract key data
410
+ extracted_data = []
411
+ for result in execution_results:
412
+ if result.get("action") == "EXTRACT_INFO":
413
+ extracted_data.append(result.get("data", {}).get("extracted", {}))
414
+
415
+ # Use LLM to create natural response
416
+ system_prompt = """You are a helpful assistant that summarizes web automation results
417
+ in natural, conversational language.
418
+
419
+ Your job is to take technical execution results and convert them into a friendly,
420
+ human-readable response that answers the user's original request.
421
+
422
+ Be concise but informative. Include key findings or data discovered.
423
+ If the task failed, explain what went wrong in simple terms.
424
+
425
+ IMPORTANT: Return only the natural language response, no JSON, no markdown."""
426
+
427
+ results_summary = {
428
+ "user_request": user_input,
429
+ "plan_intent": plan.get("intent"),
430
+ "total_steps": len(execution_results),
431
+ "successful_steps": len(successful_steps),
432
+ "failed_steps": len(failed_steps),
433
+ "extracted_data": extracted_data,
434
+ "final_url": self.browser.page.url if self.browser.page else None,
435
+ }
436
+
437
+ user_prompt = f"""Summarize these automation results in 1-3 natural sentences:
438
+
439
+ {json.dumps(results_summary, indent=2)}
440
+
441
+ Respond as if you're talking to a user, not listing technical details."""
442
+
443
+ try:
444
+ response = self.llm.generate(system_prompt, user_prompt, temperature=0.3)
445
+ return response.content.strip()
446
+ except:
447
+ # Fallback response
448
+ if failed_steps:
449
+ return f"I attempted to {user_input}, but encountered an error during execution."
450
+ else:
451
+ return f"I completed your request: {user_input}"
452
+
453
+ def chat(self, message: str) -> str:
454
+ """
455
+ Conversational interface with context awareness
456
+
457
+ Args:
458
+ message: User message (can reference previous context)
459
+
460
+ Returns:
461
+ Agent response
462
+
463
+ Example:
464
+ >>> agent.chat("Go to google.com")
465
+ "I've navigated to google.com"
466
+ >>> agent.chat("Search for magic mouse") # Contextual
467
+ "I searched for 'magic mouse' and found 10 results"
468
+ """
469
+ return self.execute(message)
470
+
471
+ def get_summary(self) -> str:
472
+ """
473
+ Get a summary of the entire conversation/session
474
+
475
+ Returns:
476
+ Natural language summary of all actions taken
477
+ """
478
+ if not self.conversation_history:
479
+ return "No actions have been performed yet."
480
+
481
+ system_prompt = """Summarize this web automation session in a brief, natural paragraph.
482
+ Focus on what was accomplished and key findings."""
483
+
484
+ session_data = {
485
+ "total_interactions": len(self.conversation_history),
486
+ "actions": [
487
+ {"request": h["user_input"], "outcome": h["response"]}
488
+ for h in self.conversation_history
489
+ ],
490
+ }
491
+
492
+ user_prompt = f"Summarize this session:\n{json.dumps(session_data, indent=2)}"
493
+
494
+ try:
495
+ summary = self.llm.generate(system_prompt, user_prompt)
496
+ return summary.content.strip()
497
+ except Exception as ex:
498
+ return f"Session with {len(self.conversation_history)} interactions completed with exception: {ex}"
499
+
500
+ def clear_history(self):
501
+ """Clear conversation history"""
502
+ self.conversation_history.clear()
503
+ self.technical_agent.clear_history()
504
+ self.execution_context = {
505
+ "current_url": None,
506
+ "last_action": None,
507
+ "discovered_elements": [],
508
+ "session_data": {},
509
+ }
sentience/expect.py ADDED
@@ -0,0 +1,92 @@
1
+ """
2
+ Expect/Assert functionality
3
+ """
4
+
5
+ import time
6
+
7
+ from .browser import SentienceBrowser
8
+ from .models import Element
9
+ from .query import query
10
+ from .wait import wait_for
11
+
12
+
13
+ class Expectation:
14
+ """Assertion helper for element expectations"""
15
+
16
+ def __init__(self, browser: SentienceBrowser, selector: str | dict):
17
+ self.browser = browser
18
+ self.selector = selector
19
+
20
+ def to_be_visible(self, timeout: float = 10.0) -> Element:
21
+ """Assert element is visible (exists and in viewport)"""
22
+ result = wait_for(self.browser, self.selector, timeout=timeout)
23
+
24
+ if not result.found:
25
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
26
+
27
+ element = result.element
28
+ if not element.in_viewport:
29
+ raise AssertionError(f"Element found but not visible in viewport: {self.selector}")
30
+
31
+ return element
32
+
33
+ def to_exist(self, timeout: float = 10.0) -> Element:
34
+ """Assert element exists"""
35
+ result = wait_for(self.browser, self.selector, timeout=timeout)
36
+
37
+ if not result.found:
38
+ raise AssertionError(f"Element does not exist: {self.selector} (timeout: {timeout}s)")
39
+
40
+ return result.element
41
+
42
+ def to_have_text(self, expected_text: str, timeout: float = 10.0) -> Element:
43
+ """Assert element has specific text"""
44
+ result = wait_for(self.browser, self.selector, timeout=timeout)
45
+
46
+ if not result.found:
47
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
48
+
49
+ element = result.element
50
+ if not element.text or expected_text not in element.text:
51
+ raise AssertionError(
52
+ f"Element text mismatch. Expected '{expected_text}', got '{element.text}'"
53
+ )
54
+
55
+ return element
56
+
57
+ def to_have_count(self, expected_count: int, timeout: float = 10.0) -> None:
58
+ """Assert selector matches exactly N elements"""
59
+ from .snapshot import snapshot
60
+
61
+ start_time = time.time()
62
+ while time.time() - start_time < timeout:
63
+ snap = snapshot(self.browser)
64
+ matches = query(snap, self.selector)
65
+
66
+ if len(matches) == expected_count:
67
+ return
68
+
69
+ time.sleep(0.25)
70
+
71
+ # Final check
72
+ snap = snapshot(self.browser)
73
+ matches = query(snap, self.selector)
74
+ actual_count = len(matches)
75
+
76
+ raise AssertionError(
77
+ f"Element count mismatch. Expected {expected_count}, got {actual_count}"
78
+ )
79
+
80
+
81
+ def expect(browser: SentienceBrowser, selector: str | dict) -> Expectation:
82
+ """
83
+ Create expectation helper for assertions
84
+
85
+ Args:
86
+ browser: SentienceBrowser instance
87
+ selector: String DSL or dict query
88
+
89
+ Returns:
90
+ Expectation helper
91
+ """
92
+ return Expectation(browser, selector)