fix: preserve pixel safety target context

This commit is contained in:
somew1nd committed 2026-09-03 10:58:30 -07:00
1 parent 4bc79fdec8
commit d7c64f571d
3 files changed
+141 -9

No files matched your search

+8 -5
View File
@@ -1,11 +1,14 @@
Role
UI State Verification Expert. Compare Image 1 (Reference) and Image 2 (Current State) to check if the target clicked by the Operator at the red dot is still present and interactive.
UI State Verification Expert. The Operator decided to act at the red dot while looking at Image 1 (Reference). Image 2 (Current State) is the live screen now. Decide whether the action can still land on its intended target.
Read the `[Target]` block first to determine which rule applies.
Rules
1. Check Intent: Read the `[Planned Action & Original Thinking]` to identify what Action the Agent intended to perform and why.
2. Specific UI Elements (Priority): If targeting a specific UI control (e.g., button, icon, text, slider handle) — even if it overlays a dynamic video, map, or live stream — focus ONLY on that element's shape. Output `{"is_present": false, ...}` if that exact element has disappeared, shifted away, or is blocked. Ignore background media changes underneath.
3. Blank Space, Backgrounds & Media Surfaces: If targeting open/blank space or the body of a playing video / animation / map / live stream itself rather than a specific control drawn on top of it (e.g., tapping a full-screen video to wake its hidden controls, tapping the backdrop to dismiss a dialog), the frames are expected to differ: ignore natural video/background frame changes and the appearance or fading of overlay controls. Output `{"is_present": true, "confidence": 1.0}` unless an unexpected blocking popup now covers the tap point.
1. `Kind: specific UI control` — the target has a label, resource id, class or bounds. Judge whether that exact control remains visible at the red dot in Image 2. Ignore video, map, animation or background changes beneath it. A control that has hidden, faded, collapsed, moved or become covered is not present, even if tapping the same spot could restore it.
2. `Kind: coordinates only` — no UI hierarchy element was found at the point. Identify what appears under the red dot in both images. If Image 1 shows a distinct button, icon, link or handle, apply Rule 1. Apply the surface rule only when Image 1 shows blank space, a backdrop, or the body of a video, animation, map or live stream.
3. Surface rule. Typical intents: tap the video body to wake its hidden controls, tap the backdrop to dismiss a dialog. Surfaces are expected to change between frames: ignore natural frame changes and the appearance or fading of overlay controls. Output `{"is_present": true, "confidence": 1.0}` unless an unexpected blocking popup now covers the red dot.
4. Use `[Planned Action & Original Thinking]` to resolve whether the target was a control or its underlying surface. Do not treat a missing control as present because another tap could restore it.
Output
Return EXACTLY this raw JSON and nothing else. Keep reasoning strictly under 25 words.
Return EXACTLY this raw JSON and nothing else. Keep reasoning strictly under 35 words.
{"reasoning": "brief explanation", "is_present": boolean, "confidence": float}
+33 -4
View File
@@ -66,18 +66,47 @@ def _init_llm_and_prompt(ctx: ArtemisContext, get_llm_fn):
return llm, prompt
def _describe_target(action_item: dict) -> str:
"""Describe the structured action target for the pixel judge."""
label = action_item.get("target_text")
resource_id = action_item.get("target_resource_id")
class_name = action_item.get("target_class")
bounds = action_item.get("target_bounds")
lines = ["[Target]", f"Action: {action_item.get('action')}"]
if label or resource_id or class_name or bounds:
lines.append("Kind: specific UI control")
if label:
lines.append(f"Label: {label}")
if resource_id:
lines.append(f"Resource ID: {resource_id}")
if class_name:
lines.append(f"Class: {class_name}")
if not label and not resource_id:
lines.append("Label: (unlabelled; identify it by its shape in Image 1)")
if bounds:
lines.append(f"Bounds at decision time: {bounds}")
else:
lines.append(
"Kind: coordinates only (no UI hierarchy element was found under the point;"
" inspect Image 1 for a control at the red dot)"
)
return "\n".join(lines)
def _build_messages(
prompt: str,
orig_crop_bytes: bytes,
live_crop_bytes: bytes,
action_name,
action_item: dict,
state: State | None,
) -> list:
"""Formulates the multi-modal verification message pair."""
orig_b64 = base64.b64encode(orig_crop_bytes).decode("utf-8")
live_b64 = base64.b64encode(live_crop_bytes).decode("utf-8")
action_name = action_item.get("action")
user_content = [
{"type": "text", "text": _describe_target(action_item)},
{"type": "text", "text": "[Image 1 (Reference)]"},
{
"type": "image_url",
@@ -119,7 +148,7 @@ async def _run_attempt(
prompt: str,
orig_crop_bytes: bytes,
current_coords,
action_name,
action_item: dict,
state: State | None,
attempt: int,
) -> tuple[bool, ValidationErrorCategory, str] | None:
@@ -147,7 +176,7 @@ async def _run_attempt(
)
# C. Formulate multi-modal verification message
messages = _build_messages(prompt, orig_crop_bytes, live_crop_bytes, action_name, state)
messages = _build_messages(prompt, orig_crop_bytes, live_crop_bytes, action_item, state)
# D. Invoke Universal VLM. Parsing (fences, repair) and one
# corrective re-ask on unparseable JSON are handled by the
@@ -257,7 +286,7 @@ async def validate_action_precondition_pixel(
prompt,
orig_crop_bytes,
current_coords,
action_name,
action_item,
state,
attempt,
)
@@ -0,0 +1,100 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for pixel safety-net target descriptions."""
from pathlib import Path
from unittest.mock import MagicMock
from artemis.agents.validator import precondition_pixel as pp
from langchain_core.messages import HumanMessage, SystemMessage
_PNG = b"fake-image-bytes"
def _texts(messages: list) -> list[str]:
human = messages[1]
return [part["text"] for part in human.content if part["type"] == "text"]
def test_named_control_target_block_precedes_images():
item = {
"action": "tap",
"coordinates": [1001, 718],
"target_text": "进入全屏模式",
"target_resource_id": "com.google.android.youtube:id/fullscreen_button",
"target_class": "android.widget.ImageView",
"target_bounds": [922, 655, 1080, 781],
}
state = MagicMock()
state.operator_native_thinking = "identified 进入全屏模式"
state.operator_raw_thinking = None
messages = pp._build_messages("SYSTEM", _PNG, _PNG, item, state)
assert isinstance(messages[0], SystemMessage)
assert isinstance(messages[1], HumanMessage)
first_text = messages[1].content[0]["text"]
assert first_text.startswith("[Target]")
assert "Kind: specific UI control" in first_text
assert "Label: 进入全屏模式" in first_text
assert "Resource ID: com.google.android.youtube:id/fullscreen_button" in first_text
assert "Class: android.widget.ImageView" in first_text
assert "[922, 655, 1080, 781]" in first_text
assert messages[1].content[1]["text"] == "[Image 1 (Reference)]"
assert any("Original Thinking" in t for t in _texts(messages))
assert any("Action: tap" in t for t in _texts(messages))
def test_coordinates_only_target_is_labelled_as_surface():
item = {"action": "tap", "coordinates": [540, 400]}
messages = pp._build_messages("SYSTEM", _PNG, _PNG, item, state=None)
first_text = messages[1].content[0]["text"]
assert "Kind: coordinates only" in first_text
assert "Label:" not in first_text
assert "Resource ID:" not in first_text
assert not any("Original Thinking" in t for t in _texts(messages))
def test_label_alone_counts_as_named_control():
item = {"action": "tap", "coordinates": [10, 10], "target_text": "Submit"}
text = pp._describe_target(item)
assert "Kind: specific UI control" in text
assert "Label: Submit" in text
assert "Resource ID" not in text
def test_unlabelled_hit_test_element_is_still_a_control():
item = {
"action": "tap",
"coordinates": [1001, 718],
"target_class": "android.widget.ImageView",
"target_bounds": [922, 655, 1080, 781],
"target_label_source": "hit_test",
}
text = pp._describe_target(item)
assert "Kind: specific UI control" in text
assert "unlabelled" in text
assert "Class: android.widget.ImageView" in text
assert "[922, 655, 1080, 781]" in text
def test_prompt_rules_are_keyed_on_target_kind():
prompt = Path(pp.__file__).with_name("pixel_safety_net.md").read_text(encoding="utf-8")
assert "`Kind: specific UI control`" in prompt
assert "`Kind: coordinates only`" in prompt
assert "hidden" in prompt
assert "Identify what appears under the red dot" in prompt