Skip to content

feat(live): refactor AI self-heal to use keyword-based ReAct loop and improve locator logging#303

Open
chinmayajha wants to merge 7 commits into
mozarkai:mainfrom
chinmayajha:live-native-screenshot-bytes
Open

feat(live): refactor AI self-heal to use keyword-based ReAct loop and improve locator logging#303
chinmayajha wants to merge 7 commits into
mozarkai:mainfrom
chinmayajha:live-native-screenshot-bytes

Conversation

@chinmayajha

@chinmayajha chinmayajha commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

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)

  • Rewrote AISelfHealHandler to run a keyword-based loop (similar to the NL agent but lighter).
  • Replaced the coordinate schema (percent_x, percent_y, text, direction) in HEAL_ACTION_SCHEMA with keyword name, parameter string list, and a completed status flag.
  • Added catalog introspection to the prompt so the LLM knows available signatures.
  • Instructed the LLM to prefer targeting elements by visible text (using the condensed UI hierarchy) via press_element instead of guessing percentages.
  • Added explicit prompt guidelines for gesture directions to align the LLM's spatial logical scroll actions with the physical swipe inputs expected by the driver.

2. Action Keyword Integration (optics_framework/api/action_keyword.py)

  • Implemented _heal_execute() to parse and execute keywords generated by the LLM during the self-heal process.
  • Temporarily disables the self-heal toggle during internal keyword calls to prevent infinite recursion.
  • Added _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.py
  • optics_framework/engines/elementsources/appium_page_source.py
    • Caught NoSuchElementException specifically and raised OpticsError without logging stack tracebacks via internal_logger.exception (which is treated as a silent error/failure in live.py).

4. Tests

  • tests/units/common/test_ai_self_heal.py
    • Updated unit tests to use FakeExecutor instead of FakeDriver.
    • Added new coverage for keyword execution, multi-step navigation, catalog prompts, done/give_up loops, and ActionKeyword integrations.

Verification

  • Ran full test suite:
    poetry run pytest --ignore=tests/feature

@chinmayajha chinmayajha requested a review from thakur-patel June 15, 2026 16:37
@chinmayajha chinmayajha self-assigned this Jun 22, 2026
Comment thread optics_framework/common/elementsource_interface.py
Comment thread optics_framework/common/strategies.py
@chinmayajha chinmayajha changed the title feat(live): native screenshot-bytes fast path for the NL LLM agent (driver-agnostic) feat(live): refactor AI self-heal to use keyword-based ReAct loop and improve locator logging Jun 29, 2026
@chinmayajha chinmayajha force-pushed the live-native-screenshot-bytes branch 2 times, most recently from bf85cfe to 80e721e Compare June 29, 2026 18:27
@chinmayajha chinmayajha force-pushed the live-native-screenshot-bytes branch from efbe0f8 to 555d18c Compare July 1, 2026 03:15
chinmayajha and others added 7 commits July 2, 2026 09:25
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>
@chinmayajha chinmayajha force-pushed the live-native-screenshot-bytes branch from 97943f0 to e8b0d7d Compare July 2, 2026 03:55
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown


try:
results_list = list(results)
except OpticsError:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception is too wide as well

"""
driver = self._require_driver()
last_exc: Optional[Exception] = None
for attempt in range(2):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception is too wide

"""
driver = self._require_driver()
last_exc: Optional[Exception] = None
for attempt in range(2):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the capture_screenshot_bytes() never raises a ValueError. It will raise a OpticsError with code E303

@@ -1,14 +1,16 @@
from functools import wraps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing wrong with the code but commits f079ea1 and 2b545fa do not use conventional commits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants