Skip to content

Commit 39727db

Browse files
author
SentienceDEV
committed
incorporate boilerplate code
1 parent 6e307d2 commit 39727db

File tree

3 files changed

+960
-16
lines changed

3 files changed

+960
-16
lines changed

predicate/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
# Ordinal support (Phase 3)
119119
from .ordinal import OrdinalIntent, boost_ordinal_elements, detect_ordinal_intent, select_by_ordinal
120120
from .overlay import clear_overlay, show_overlay
121+
from .overlay_dismissal import OverlayDismissResult, dismiss_overlays, dismiss_overlays_before_agent
121122
from .permissions import PermissionPolicy
122123
from .pruning import (
123124
CategoryDetectionResult,
@@ -259,6 +260,10 @@
259260
"screenshot",
260261
"show_overlay",
261262
"clear_overlay",
263+
# Overlay dismissal (proactive popup/banner removal)
264+
"OverlayDismissResult",
265+
"dismiss_overlays",
266+
"dismiss_overlays_before_agent",
262267
# Text Search
263268
"find_text_rect",
264269
"TextRectSearchResult",

predicate/agents/planner_executor_agent.py

Lines changed: 238 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,12 @@ def build_executor_prompt(
17521752
(intent and any(kw in intent.lower() for kw in product_keywords))
17531753
or any(kw in goal.lower() for kw in product_keywords)
17541754
)
1755+
# Check if this is an Add to Cart action
1756+
add_to_cart_keywords = ["add to cart", "add to bag", "add to basket", "buy now"]
1757+
is_add_to_cart_action = (
1758+
(intent and any(kw in intent.lower() for kw in add_to_cart_keywords))
1759+
or any(kw in goal.lower() for kw in add_to_cart_keywords)
1760+
)
17551761
# Check if intent asks to match text (e.g., "Click element with text matching [keyword]")
17561762
is_text_matching_action = intent and "matching" in intent.lower()
17571763
# Check if input_text specifies a target to match (for CLICK actions, input_text is target text)
@@ -1799,6 +1805,24 @@ def build_executor_prompt(
17991805
"Output MUST match exactly: CLICK(<digits>) with no spaces.\n"
18001806
"Example: CLICK(1268)"
18011807
)
1808+
elif is_add_to_cart_action:
1809+
# Add to Cart action - may need to click product first if on search results page
1810+
system = (
1811+
"You are an executor for browser automation.\n"
1812+
"Return ONLY a single-line CLICK(id) action.\n"
1813+
"If you output anything else, the action fails.\n"
1814+
"Do NOT output <think> or any reasoning.\n"
1815+
"ADD TO CART HINTS:\n"
1816+
"- FIRST: Look for buttons with text: 'Add to Cart', 'Add to Bag', 'Add to Basket', 'Buy Now'\n"
1817+
"- If found, click that button directly\n"
1818+
"- FALLBACK: If NO 'Add to Cart' button is visible, you are likely on a SEARCH RESULTS page\n"
1819+
" - In this case, click a PRODUCT LINK to go to the product details page first\n"
1820+
" - Look for LINK elements with product IDs in href (e.g., /7027762, /dp/B...)\n"
1821+
" - Prefer links with product names, prices, or delivery info\n"
1822+
"- AVOID: 'Search' buttons, category buttons, filter buttons, pagination\n"
1823+
"Output MUST match exactly: CLICK(<digits>) with no spaces.\n"
1824+
"Example: CLICK(42)"
1825+
)
18021826
else:
18031827
system = (
18041828
"You are an executor for browser automation.\n"
@@ -2940,41 +2964,54 @@ async def replan(
29402964
Return ONLY a JSON object with mode="patch" and replace_steps array.
29412965
29422966
IMPORTANT - Alternative approaches when CLICK fails:
2943-
- If a direct product/element click failed, try a DIFFERENT approach:
2944-
* Use search: Add a TYPE_AND_SUBMIT step to search for the product
2945-
* Use category navigation: Click a category link instead of the product directly
2946-
* Click a different element: Try "Quick Shop" or "View Details" buttons
2967+
- If a product/category navigation failed, USE SITE SEARCH instead:
2968+
* Replace the failed CLICK with a TYPE_AND_SUBMIT to search for the product
2969+
* This is the MOST RELIABLE fallback - site search works on all websites
2970+
- If clicking a specific element failed:
2971+
* Try a different selector or button (e.g., "Quick Shop", "View Details")
29472972
- Don't just retry the same approach with minor changes"""
29482973

2974+
# Extract product/item name from step for search suggestion
2975+
product_hint = ""
2976+
step_labels = " ".join([
2977+
failed_step.goal or "",
2978+
failed_step.target or "",
2979+
failed_step.intent or "",
2980+
]).lower()
2981+
# Common patterns to extract product name
2982+
for pattern in [r"snow\s*blower", r"product\s+(\w+)", r"click\s+(?:on\s+)?(.+?)(?:\s+product|\s+category)?$"]:
2983+
match = re.search(pattern, step_labels, re.IGNORECASE)
2984+
if match:
2985+
product_hint = match.group(0) if match.lastindex is None else match.group(1)
2986+
break
2987+
if not product_hint and failed_step.target:
2988+
product_hint = failed_step.target
2989+
29492990
user = f"""Task: {task}
29502991
29512992
Failure:
29522993
- Step ID: {failed_step.id}
29532994
- Step goal: {failed_step.goal}
29542995
- Reason: {failure_reason}
29552996
2956-
IMPORTANT: The element could not be clicked or the verification failed.
2957-
Try a DIFFERENT approach - pick ONE of these alternatives:
2958-
2959-
1. If clicking a category link failed (e.g., "Rally Home Goods" in sidebar):
2960-
- Try clicking "Catalog" or "Shop All" link instead
2961-
- Or click "SHOP NOW" button to browse all products
2962-
2963-
2. If clicking a product failed:
2964-
- Try clicking a category first, then look for the product
2965-
- Or try clicking "Quick Shop" button near the product
2997+
IMPORTANT: The element could not be found or clicked. The current page likely doesn't have the target.
2998+
The BEST approach is to USE SITE SEARCH to find the product directly.
29662999
2967-
Example - replace failed category click with Catalog link:
3000+
RECOMMENDED: Replace the failed step with a site search:
29683001
{{
29693002
"mode": "patch",
29703003
"replace_steps": [
29713004
{{
29723005
"id": {failed_step.id},
2973-
"step": {{ "id": {failed_step.id}, "goal": "Click Catalog to browse products", "action": "CLICK", "intent": "Click Catalog or Shop Now link", "verify": [{{"predicate": "url_contains", "args": ["/collections"]}}] }}
3006+
"step": {{ "id": {failed_step.id}, "goal": "Search for {product_hint or 'the product'}", "action": "TYPE_AND_SUBMIT", "input": "{product_hint or 'product name'}", "intent": "Type in search box and submit", "verify": [{{"predicate": "url_contains", "args": ["search"]}}] }}
29743007
}}
29753008
]
29763009
}}
29773010
3011+
Alternative approaches (if search doesn't apply):
3012+
1. Click "Catalog" or "Shop All" to browse products
3013+
2. Click "Quick Shop" or "View Details" buttons
3014+
29783015
Return JSON patch:"""
29793016

29803017
for attempt in range(1, max_attempts + 1):
@@ -3492,6 +3529,102 @@ def _looks_like_search_submission(self, step: PlanStep, element: Any | None) ->
34923529
).lower()
34933530
return "search" in labels
34943531

3532+
def _is_add_to_cart_step(self, step: PlanStep) -> bool:
3533+
"""Detect if a step is an Add to Cart action."""
3534+
add_to_cart_keywords = ["add to cart", "add to bag", "add to basket", "buy now"]
3535+
labels = " ".join(
3536+
str(part or "").lower()
3537+
for part in (step.goal, step.intent, step.input)
3538+
)
3539+
return any(kw in labels for kw in add_to_cart_keywords)
3540+
3541+
def _is_search_results_url(self, url: str) -> bool:
3542+
"""Check if URL looks like a search results page."""
3543+
url_lower = url.lower()
3544+
# Common patterns for search results pages
3545+
search_patterns = [
3546+
"search",
3547+
"query=",
3548+
"q=",
3549+
"s=",
3550+
"/s?",
3551+
"keyword=",
3552+
"keywords=",
3553+
"results",
3554+
]
3555+
return any(pattern in url_lower for pattern in search_patterns)
3556+
3557+
def _is_category_navigation_step(self, step: PlanStep) -> bool:
3558+
"""Check if this step is navigating to a category/section."""
3559+
nav_keywords = [
3560+
"navigate to", "go to", "click category", "category link",
3561+
"click on", "browse", "section", "department"
3562+
]
3563+
labels = " ".join(
3564+
str(part or "").lower()
3565+
for part in (step.goal, step.intent)
3566+
)
3567+
return any(kw in labels for kw in nav_keywords)
3568+
3569+
def _url_change_matches_intent(self, step: PlanStep, pre_url: str, post_url: str) -> bool:
3570+
"""
3571+
Check if URL change actually matches the step's intent.
3572+
3573+
For category navigation, the new URL should contain keywords from the target.
3574+
This prevents accepting unrelated URL changes as successful navigation.
3575+
"""
3576+
# Extract target keywords from step
3577+
target = step.target or ""
3578+
intent = step.intent or ""
3579+
goal = step.goal or ""
3580+
3581+
post_url_lower = post_url.lower()
3582+
3583+
# Special case: checkout/cart related steps
3584+
# These steps may go to /cart first before /checkout, which is valid
3585+
checkout_keywords = ["checkout", "proceed to checkout", "cart", "view cart"]
3586+
step_labels = f"{goal} {intent}".lower()
3587+
is_checkout_step = any(kw in step_labels for kw in checkout_keywords)
3588+
if is_checkout_step:
3589+
# Accept cart or checkout URLs as valid for checkout steps
3590+
checkout_url_patterns = ["cart", "checkout", "basket", "bag"]
3591+
if any(pattern in post_url_lower for pattern in checkout_url_patterns):
3592+
return True
3593+
3594+
# Get keywords from target (e.g., "Outdoor Power Equipment" -> ["outdoor", "power", "equipment"])
3595+
target_words = set(
3596+
word.lower() for word in re.split(r'[\s\-_]+', target)
3597+
if len(word) >= 3 # Skip short words like "to", "and"
3598+
)
3599+
3600+
# Also check predicates for expected URL patterns
3601+
expected_patterns = []
3602+
for pred in (step.verify or []):
3603+
if pred.predicate == "url_contains" and pred.args:
3604+
expected_patterns.append(pred.args[0].lower())
3605+
3606+
# If predicates specify URL patterns, check those
3607+
if expected_patterns:
3608+
if any(pattern in post_url_lower for pattern in expected_patterns):
3609+
return True
3610+
# For non-checkout steps, reject URL changes that don't match predicates
3611+
# But only if we have a target to validate against
3612+
if target_words:
3613+
return False
3614+
# No target and no predicate match - be permissive
3615+
return True
3616+
3617+
# Otherwise check if target keywords appear in URL
3618+
if target_words:
3619+
# At least one target word should appear in URL
3620+
if any(word in post_url_lower for word in target_words):
3621+
return True
3622+
# URL doesn't contain any target keywords - suspicious
3623+
return False
3624+
3625+
# No target specified - can't validate, allow fallback
3626+
return True
3627+
34953628
def _find_submit_button_for_type_and_submit(
34963629
self,
34973630
*,
@@ -4702,6 +4835,95 @@ async def _execute_step(
47024835
element=typed_element,
47034836
typed_text=executor_text or step.input or "",
47044837
)
4838+
elif original_action == "CLICK" and is_meaningful_change:
4839+
# For CLICK actions, validate URL change matches step intent
4840+
# This prevents accepting wrong category navigations
4841+
url_matches_intent = self._url_change_matches_intent(
4842+
step=step,
4843+
pre_url=pre_url,
4844+
post_url=current_url,
4845+
)
4846+
if not url_matches_intent:
4847+
fallback_ok = False
4848+
if self.config.verbose:
4849+
print(f" [VERIFY] URL changed but doesn't match step intent", flush=True)
4850+
print(f" [VERIFY] Step target: {step.target}, URL: {current_url}", flush=True)
4851+
4852+
# Special handling for Add to Cart steps: if we were on search results
4853+
# and navigated to a product page, retry the step on the new page
4854+
# instead of accepting URL change as success
4855+
is_add_to_cart = self._is_add_to_cart_step(step)
4856+
was_on_search_results = self._is_search_results_url(pre_url)
4857+
now_on_product_page = not self._is_search_results_url(current_url)
4858+
4859+
if is_add_to_cart and was_on_search_results and now_on_product_page and is_meaningful_change:
4860+
# We clicked a product link instead of Add to Cart
4861+
# Retry the step on the product page
4862+
if self.config.verbose:
4863+
print(f" [ADD-TO-CART] Navigated from search results to product page: {pre_url} -> {current_url}", flush=True)
4864+
print(f" [ADD-TO-CART] Retrying Add to Cart action on product page...", flush=True)
4865+
4866+
# Get fresh snapshot on the product page
4867+
await asyncio.sleep(0.5) # Brief wait for page to load
4868+
try:
4869+
retry_ctx = await self._get_execution_context(
4870+
runtime, step, step_index
4871+
)
4872+
# Build prompt for retry - looking for Add to Cart on product page
4873+
retry_prompt = self.build_executor_prompt(
4874+
goal=step.goal,
4875+
elements=retry_ctx.snapshot.elements or [],
4876+
intent=step.intent,
4877+
task_category=retry_ctx.task_category,
4878+
input_text=step.input,
4879+
)
4880+
if self.config.verbose:
4881+
print(f" [ADD-TO-CART] Asking executor to find Add to Cart button...", flush=True)
4882+
4883+
retry_resp = self.executor.generate(
4884+
retry_prompt["system"],
4885+
retry_prompt["user"],
4886+
max_tokens=self.config.executor_max_tokens,
4887+
)
4888+
self._usage.record(role="executor", resp=retry_resp)
4889+
retry_action = retry_resp.content.strip()
4890+
if self.config.verbose:
4891+
print(f" [ADD-TO-CART] Retry executor output: {retry_action}", flush=True)
4892+
4893+
# Parse and execute retry action
4894+
retry_match = re.match(r"CLICK\((\d+)\)", retry_action)
4895+
if retry_match:
4896+
retry_element_id = int(retry_match.group(1))
4897+
await runtime.click(retry_element_id)
4898+
if self.config.verbose:
4899+
print(f" [ADD-TO-CART] Clicked element {retry_element_id}", flush=True)
4900+
4901+
# Wait and verify
4902+
await asyncio.sleep(0.5)
4903+
verification_passed = await self._verify_step(runtime, step)
4904+
if verification_passed:
4905+
if self.config.verbose:
4906+
print(f" [ADD-TO-CART] Add to Cart successful after retry!", flush=True)
4907+
else:
4908+
# Check for DOM change (cart drawer/modal)
4909+
post_retry_snap = await runtime.snapshot(SnapshotOptions(limit=50))
4910+
if post_retry_snap and hasattr(post_retry_snap, "elements"):
4911+
post_els = post_retry_snap.elements or []
4912+
cart_indicators = ["cart", "bag", "basket", "checkout", "added", "item"]
4913+
has_cart_indicator = any(
4914+
any(ind in (getattr(el, "text", "") or "").lower() for ind in cart_indicators)
4915+
for el in post_els[:30]
4916+
)
4917+
if has_cart_indicator:
4918+
if self.config.verbose:
4919+
print(f" [ADD-TO-CART] Cart indicator detected, accepting as success", flush=True)
4920+
verification_passed = True
4921+
except Exception as retry_err:
4922+
if self.config.verbose:
4923+
print(f" [ADD-TO-CART] Retry failed: {retry_err}", flush=True)
4924+
4925+
# Skip the normal URL fallback since we handled Add to Cart specially
4926+
fallback_ok = False
47054927

47064928
if fallback_ok:
47074929
if self.config.verbose:

0 commit comments

Comments
 (0)