feat(live): refactor AI self-heal to use keyword-based ReAct loop and improve locator logging#303
Conversation
bf85cfe to
80e721e
Compare
efbe0f8 to
555d18c
Compare
Add ElementSourceInterface.capture_screenshot_bytes(), an optional fast path that returns the device's natively-encoded screenshot (PNG/JPEG) so callers needing only bytes skip the numpy decode -> re-encode round-trip. Implement it across every backend: - Appium / Selenium: base64 endpoint, with a backoff retry for truncated responses. - Playwright: returns page.screenshot()'s PNG bytes verbatim (no decode at all). Each backend's capture_screenshot_as_numpy() now reuses capture_screenshot_bytes(), so the bytes and numpy paths share one fetch+retry implementation. StrategyManager.capture_screenshot_bytes() prefers any source implementing the native path and otherwise encodes the numpy frame, so every backend keeps working. LiveController.screenshot_png_bytes() (the NL agent's screenshot provider) now delegates to it, keeping the live layer driver-agnostic.
…back StrategyManager prefers native bytes, falls back to numpy encode, and skips a failing native source. Per-backend coverage: Appium/Selenium base64 decode with retry, Playwright raw-PNG passthrough, and capture_screenshot_as_numpy reusing the bytes path.
…erface default - ElementSourceInterface.capture_screenshot_bytes() is now concrete: the default encodes capture()'s numpy frame via cv2.imencode, so every source automatically supports the bytes path without raising NotImplementedError. Backends with a native encoded path (Appium, Playwright, Selenium) still override for efficiency. - StrategyManager.capture_screenshot_bytes() is simplified: the NotImplementedError branch is removed (no longer needed), and a final OpticsError is raised if all strategies fail rather than silently falling back to a secondary numpy encode path that could itself fail. - execution_tracer.log_attempt() is now called for both success and failure in capture_screenshot_bytes(), consistent with capture_screenshot() and _try_strategy_locate(). - Tests updated to reflect new semantics: exhausted strategies raise OpticsError, and a new test verifies the interface default encode path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…clean up locator logging
97943f0 to
e8b0d7d
Compare
|
|
|
||
| try: | ||
| results_list = list(results) | ||
| except OpticsError: |
There was a problem hiding this comment.
why is it swallowing OpticsError? Wouldn't this make debugging harder?
| for attempt in range(2): | ||
| try: | ||
| return base64.b64decode(driver.get_screenshot_as_base64()) | ||
| except Exception as e: |
There was a problem hiding this comment.
This exception is too wide as well
| """ | ||
| driver = self._require_driver() | ||
| last_exc: Optional[Exception] = None | ||
| for attempt in range(2): |
There was a problem hiding this comment.
I would suggest not using magic numbers like 2. Use a named constant for this
| for attempt in range(2): | ||
| try: | ||
| return base64.b64decode(driver.get_screenshot_as_base64()) | ||
| except Exception as e: |
| """ | ||
| driver = self._require_driver() | ||
| last_exc: Optional[Exception] = None | ||
| for attempt in range(2): |
There was a problem hiding this comment.
We should use a named constant instead of 2 here
| except NoSuchElementException: | ||
| return None | ||
| except Exception as e: | ||
| internal_logger.debug("Error finding element by index: %s: %s", xpath, e) |
There was a problem hiding this comment.
This should be an exception. Its for an error
| internal_logger.exception("SeleniumScreenshot does not support getting interactive elements.") | ||
| raise NotImplementedError("SeleniumScreenshot does not support getting interactive elements.") | ||
|
|
||
| def capture_screenshot_bytes(self) -> bytes: |
There was a problem hiding this comment.
this and capture_screenshot_bytes() of appium_screenshot look similar
This should be extracted to a common place
| from optics_framework.common.logging_config import internal_logger | ||
|
|
||
| # Delay between screenshot retries (Selenium Grid nodes can be remote/flaky too). | ||
| _SCREENSHOT_RETRY_BACKOFF_S = 0.5 |
There was a problem hiding this comment.
This constant and the one in appium_screenshot.py are duplicated
| try: | ||
| return utils.encode_numpy_to_png_bytes(image) | ||
| return self._action_keyword.strategy_manager.capture_screenshot_bytes() | ||
| except ValueError as exc: # pragma: no cover - defensive |
There was a problem hiding this comment.
the capture_screenshot_bytes() never raises a ValueError. It will raise a OpticsError with code E303
| @@ -1,14 +1,16 @@ | |||
| from functools import wraps | |||



Summary
This PR refactors the AI self-heal mechanism from coordinate-guessing (which frequently tapped blind/guessed screen coordinates and often missed targets) to a keyword-based execution model. The AI now acts as a ReAct loop over the framework's own action keywords, utilizing more reliable, targeted actions that route through the locating strategies ladder (XPath, text, OCR, image-matching).
Additionally, it cleans up noisy expected element-not-found log output from Appium element sources to prevent the live TUI silent error checker from overriding successful self-heals with failure statuses.
Changes
1. AI Self-Heal Refactor (
optics_framework/common/ai_self_heal.py)AISelfHealHandlerto run a keyword-based loop (similar to the NL agent but lighter).percent_x,percent_y,text,direction) inHEAL_ACTION_SCHEMAwithkeywordname, parameter string list, and acompletedstatus flag.press_elementinstead of guessing percentages.2. Action Keyword Integration (
optics_framework/api/action_keyword.py)_heal_execute()to parse and execute keywords generated by the LLM during the self-heal process._heal_catalog()to serve a focused keyword catalog:press_element,enter_text,scroll,swipe_by_percentage,press_keycode,press_by_percentage.3. Log Cleanup for Silent Error Checker
optics_framework/engines/elementsources/appium_find_element.pyoptics_framework/engines/elementsources/appium_page_source.pyNoSuchElementExceptionspecifically and raisedOpticsErrorwithout logging stack tracebacks viainternal_logger.exception(which is treated as a silent error/failure inlive.py).4. Tests
tests/units/common/test_ai_self_heal.pyFakeExecutorinstead ofFakeDriver.ActionKeywordintegrations.Verification