Skip to content

Commit 7356ffe

Browse files
author
SentienceDEV
committed
Phase 5: fixed new tests
1 parent b0788bc commit 7356ffe

File tree

3 files changed

+119
-21
lines changed

3 files changed

+119
-21
lines changed

sentience/action_executor.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ class ActionExecutor:
2626

2727
def __init__(
2828
self,
29-
browser: (
30-
SentienceBrowser | AsyncSentienceBrowser | BrowserProtocol | AsyncBrowserProtocol
31-
),
29+
browser: SentienceBrowser | AsyncSentienceBrowser | BrowserProtocol | AsyncBrowserProtocol,
3230
):
3331
"""
3432
Initialize action executor.
@@ -39,7 +37,25 @@ def __init__(
3937
"""
4038
self.browser = browser
4139
# Check if browser is async - support both concrete types and protocols
42-
self._is_async = isinstance(browser, (AsyncSentienceBrowser, AsyncBrowserProtocol))
40+
# Check concrete types first (most reliable)
41+
if isinstance(browser, AsyncSentienceBrowser):
42+
self._is_async = True
43+
elif isinstance(browser, SentienceBrowser):
44+
self._is_async = False
45+
else:
46+
# For protocol-based browsers, check if methods are actually async
47+
# This is more reliable than isinstance checks which can match both protocols
48+
import inspect
49+
50+
start_method = getattr(browser, "start", None)
51+
if start_method and inspect.iscoroutinefunction(start_method):
52+
self._is_async = True
53+
elif isinstance(browser, BrowserProtocol):
54+
# If it implements BrowserProtocol and start is not async, it's sync
55+
self._is_async = False
56+
else:
57+
# Default to sync for unknown types
58+
self._is_async = False
4359

4460
def execute(self, action_str: str, snap: Snapshot) -> dict[str, Any]:
4561
"""

sentience/agent.py

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,37 @@
3333
from .tracing import Tracer
3434

3535

36+
def _safe_tracer_call(
37+
tracer: Optional["Tracer"], method_name: str, verbose: bool, *args, **kwargs
38+
) -> None:
39+
"""
40+
Safely call tracer method, catching and logging errors without breaking execution.
41+
42+
Args:
43+
tracer: Tracer instance or None
44+
method_name: Name of tracer method to call (e.g., "emit", "emit_error")
45+
verbose: Whether to print error messages
46+
*args: Positional arguments for the tracer method
47+
**kwargs: Keyword arguments for the tracer method
48+
"""
49+
if not tracer:
50+
return
51+
try:
52+
method = getattr(tracer, method_name)
53+
if args and kwargs:
54+
method(*args, **kwargs)
55+
elif args:
56+
method(*args)
57+
elif kwargs:
58+
method(**kwargs)
59+
else:
60+
method()
61+
except Exception as tracer_error:
62+
# Tracer errors should not break agent execution
63+
if verbose:
64+
print(f"⚠️ Tracer error (non-fatal): {tracer_error}")
65+
66+
3667
class SentienceAgent(BaseAgent):
3768
"""
3869
High-level agent that combines Sentience SDK with any LLM provider.
@@ -159,7 +190,10 @@ def act( # noqa: C901
159190
# Emit step_start trace event if tracer is enabled
160191
if self.tracer:
161192
pre_url = self.browser.page.url if self.browser.page else None
162-
self.tracer.emit_step_start(
193+
_safe_tracer_call(
194+
self.tracer,
195+
"emit_step_start",
196+
self.verbose,
163197
step_id=step_id,
164198
step_index=self._step_count,
165199
goal=goal,
@@ -228,7 +262,10 @@ def act( # noqa: C901
228262
if snap.screenshot_format:
229263
snapshot_data["screenshot_format"] = snap.screenshot_format
230264

231-
self.tracer.emit(
265+
_safe_tracer_call(
266+
self.tracer,
267+
"emit",
268+
self.verbose,
232269
"snapshot",
233270
snapshot_data,
234271
step_id=step_id,
@@ -254,7 +291,10 @@ def act( # noqa: C901
254291

255292
# Emit LLM query trace event if tracer is enabled
256293
if self.tracer:
257-
self.tracer.emit(
294+
_safe_tracer_call(
295+
self.tracer,
296+
"emit",
297+
self.verbose,
258298
"llm_query",
259299
{
260300
"prompt_tokens": llm_response.prompt_tokens,
@@ -315,7 +355,10 @@ def act( # noqa: C901
315355
for el in filtered_snap.elements[:50]
316356
]
317357

318-
self.tracer.emit(
358+
_safe_tracer_call(
359+
self.tracer,
360+
"emit",
361+
self.verbose,
319362
"action",
320363
{
321364
"action": result.action,
@@ -435,14 +478,28 @@ def act( # noqa: C901
435478
verify_data=verify_data,
436479
)
437480

438-
self.tracer.emit("step_end", step_end_data, step_id=step_id)
481+
_safe_tracer_call(
482+
self.tracer,
483+
"emit",
484+
self.verbose,
485+
"step_end",
486+
step_end_data,
487+
step_id=step_id,
488+
)
439489

440490
return result
441491

442492
except Exception as e:
443493
# Emit error trace event if tracer is enabled
444494
if self.tracer:
445-
self.tracer.emit_error(step_id=step_id, error=str(e), attempt=attempt)
495+
_safe_tracer_call(
496+
self.tracer,
497+
"emit_error",
498+
self.verbose,
499+
step_id=step_id,
500+
error=str(e),
501+
attempt=attempt,
502+
)
446503

447504
if attempt < max_retries:
448505
if self.verbose:
@@ -668,7 +725,10 @@ async def act( # noqa: C901
668725
# Emit step_start trace event if tracer is enabled
669726
if self.tracer:
670727
pre_url = self.browser.page.url if self.browser.page else None
671-
self.tracer.emit_step_start(
728+
_safe_tracer_call(
729+
self.tracer,
730+
"emit_step_start",
731+
self.verbose,
672732
step_id=step_id,
673733
step_index=self._step_count,
674734
goal=goal,
@@ -740,7 +800,10 @@ async def act( # noqa: C901
740800
if snap.screenshot_format:
741801
snapshot_data["screenshot_format"] = snap.screenshot_format
742802

743-
self.tracer.emit(
803+
_safe_tracer_call(
804+
self.tracer,
805+
"emit",
806+
self.verbose,
744807
"snapshot",
745808
snapshot_data,
746809
step_id=step_id,
@@ -766,7 +829,10 @@ async def act( # noqa: C901
766829

767830
# Emit LLM query trace event if tracer is enabled
768831
if self.tracer:
769-
self.tracer.emit(
832+
_safe_tracer_call(
833+
self.tracer,
834+
"emit",
835+
self.verbose,
770836
"llm_query",
771837
{
772838
"prompt_tokens": llm_response.prompt_tokens,
@@ -827,7 +893,10 @@ async def act( # noqa: C901
827893
for el in filtered_snap.elements[:50]
828894
]
829895

830-
self.tracer.emit(
896+
_safe_tracer_call(
897+
self.tracer,
898+
"emit",
899+
self.verbose,
831900
"action",
832901
{
833902
"action": result.action,
@@ -947,14 +1016,28 @@ async def act( # noqa: C901
9471016
verify_data=verify_data,
9481017
)
9491018

950-
self.tracer.emit("step_end", step_end_data, step_id=step_id)
1019+
_safe_tracer_call(
1020+
self.tracer,
1021+
"emit",
1022+
self.verbose,
1023+
"step_end",
1024+
step_end_data,
1025+
step_id=step_id,
1026+
)
9511027

9521028
return result
9531029

9541030
except Exception as e:
9551031
# Emit error trace event if tracer is enabled
9561032
if self.tracer:
957-
self.tracer.emit_error(step_id=step_id, error=str(e), attempt=attempt)
1033+
_safe_tracer_call(
1034+
self.tracer,
1035+
"emit_error",
1036+
self.verbose,
1037+
step_id=step_id,
1038+
error=str(e),
1039+
attempt=attempt,
1040+
)
9581041

9591042
if attempt < max_retries:
9601043
if self.verbose:

tests/unit/test_agent_errors.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ def test_agent_handles_network_failure(self):
194194
agent = SentienceAgent(browser, llm, verbose=False)
195195

196196
# Mock snapshot to raise network error
197-
with patch("sentience.snapshot.snapshot") as mock_snapshot:
197+
# Patch at the agent module level since that's where it's imported
198+
with patch("sentience.agent.snapshot") as mock_snapshot:
198199
mock_snapshot.side_effect = ConnectionError("Network failure")
199200

200201
with pytest.raises(RuntimeError, match="Failed after"):
@@ -217,15 +218,13 @@ def test_agent_handles_empty_snapshot(self):
217218
)
218219

219220
with (
220-
patch("sentience.agent.snapshot") as mock_snapshot,
221+
patch("sentience.snapshot.snapshot") as mock_snapshot,
221222
patch("sentience.action_executor.click") as mock_click,
222223
):
223224
from sentience.models import ActionResult
224225

225226
mock_snapshot.return_value = empty_snap
226-
mock_click.return_value = ActionResult(
227-
success=False, duration_ms=100, outcome="Element not found"
228-
)
227+
mock_click.return_value = ActionResult(success=False, duration_ms=100, outcome="error")
229228

230229
# Agent should still attempt action even with empty snapshot
231230
result = agent.act("Click the button", max_retries=0)

0 commit comments

Comments
 (0)