feat(memory): add incremental transcript compaction
This commit is contained in:
1 parent
cbe8f4f0c6
commit
41ae386a3a
65 files changed
+9971
-815
No files matched your search
@@ -0,0 +1,116 @@
|
||||
# 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 record-time action semantics enrichment (element hit testing)."""
|
||||
|
||||
from artemis.utils.element_hit_test import find_element_at_point, hit_test_semantics
|
||||
|
||||
|
||||
def _xml_el(text, bounds, resource_id=None, cls="android.widget.TextView"):
|
||||
return {
|
||||
"text": text,
|
||||
"bounds": bounds,
|
||||
"class": cls,
|
||||
"resource_id": resource_id,
|
||||
"is_ocr": False,
|
||||
}
|
||||
|
||||
|
||||
def test_smallest_covering_element_wins():
|
||||
elements = [
|
||||
_xml_el("Whole screen", [0, 0, 1080, 2400]),
|
||||
_xml_el("Card", [100, 100, 500, 500]),
|
||||
_xml_el("Button", [200, 200, 300, 260], resource_id="btn_ok"),
|
||||
]
|
||||
el, source = find_element_at_point(elements, 250, 230)
|
||||
assert source == "hit_test"
|
||||
assert el["text"] == "Button"
|
||||
|
||||
semantics = hit_test_semantics(elements, 250, 230)
|
||||
assert semantics == {
|
||||
"target_label_source": "hit_test",
|
||||
"target_text": "Button",
|
||||
"target_class": "android.widget.TextView",
|
||||
"target_resource_id": "btn_ok",
|
||||
}
|
||||
|
||||
|
||||
def test_xml_element_preferred_over_smaller_ocr_element():
|
||||
elements = [
|
||||
_xml_el("Login", [100, 100, 500, 300]),
|
||||
{
|
||||
"text": "Login OCR",
|
||||
"bounds": [200, 150, 260, 180],
|
||||
"class": None,
|
||||
"resource_id": None,
|
||||
"is_ocr": True,
|
||||
},
|
||||
]
|
||||
el, source = find_element_at_point(elements, 220, 160)
|
||||
assert source == "hit_test"
|
||||
assert el["text"] == "Login"
|
||||
|
||||
|
||||
def test_ocr_fallback_when_no_xml_element_covers_point():
|
||||
elements = [
|
||||
_xml_el("Elsewhere", [800, 800, 900, 900]),
|
||||
{
|
||||
"text": "OCR Label",
|
||||
"bounds": [100, 100, 300, 200],
|
||||
"class": None,
|
||||
"resource_id": None,
|
||||
"is_ocr": True,
|
||||
},
|
||||
]
|
||||
semantics = hit_test_semantics(elements, 150, 150)
|
||||
assert semantics["target_label_source"] == "ocr"
|
||||
assert semantics["target_text"] == "OCR Label"
|
||||
assert "target_class" not in semantics
|
||||
assert "target_resource_id" not in semantics
|
||||
|
||||
|
||||
def test_string_bounds_are_parsed():
|
||||
elements = [_xml_el("Item", "[100,100][300,500]")]
|
||||
el, source = find_element_at_point(elements, 200, 300)
|
||||
assert source == "hit_test"
|
||||
assert el["text"] == "Item"
|
||||
|
||||
|
||||
def test_graceful_degradation_to_none():
|
||||
# No elements at all
|
||||
assert hit_test_semantics(None, 100, 100) == {"target_label_source": "none"}
|
||||
assert hit_test_semantics([], 100, 100) == {"target_label_source": "none"}
|
||||
|
||||
# Point outside every element
|
||||
elements = [_xml_el("Item", [0, 0, 50, 50])]
|
||||
assert hit_test_semantics(elements, 500, 500) == {"target_label_source": "none"}
|
||||
|
||||
# Malformed element data must not raise
|
||||
malformed = [
|
||||
{"text": "no bounds"},
|
||||
{"text": "bad bounds", "bounds": "garbage"},
|
||||
{"text": "short bounds", "bounds": [1, 2]},
|
||||
{"text": "non-numeric", "bounds": ["a", "b", "c", "d"]},
|
||||
"not-a-dict",
|
||||
None,
|
||||
]
|
||||
assert hit_test_semantics(malformed, 10, 10) == {"target_label_source": "none"}
|
||||
|
||||
|
||||
def test_element_without_optional_fields_only_reports_source():
|
||||
elements = [
|
||||
{"text": "", "bounds": [0, 0, 100, 100], "class": None, "resource_id": None}
|
||||
]
|
||||
semantics = hit_test_semantics(elements, 50, 50)
|
||||
assert semantics == {"target_label_source": "hit_test"}
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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.
|
||||
|
||||
"""Perceptual-hash stability and distance behavior (M4 similarity hint)."""
|
||||
|
||||
import io
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from artemis.utils.image_hash import HASH_SIZE, dhash_hex, hamming_distance_hex
|
||||
|
||||
|
||||
def _screen(draw_fn, size=(270, 600), quality=85) -> bytes:
|
||||
img = Image.new("RGB", size, "white")
|
||||
draw_fn(ImageDraw.Draw(img))
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=quality)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _login_screen(d):
|
||||
d.rectangle([20, 40, 250, 90], fill="#3355ff")
|
||||
d.rectangle([30, 200, 240, 250], outline="#333333", width=4)
|
||||
d.rectangle([30, 300, 240, 350], outline="#333333", width=4)
|
||||
d.rectangle([60, 450, 210, 510], fill="#22aa66")
|
||||
|
||||
|
||||
def _settings_screen(d):
|
||||
for y in range(60, 560, 80):
|
||||
d.rectangle([10, y, 260, y + 50], fill="#dddddd")
|
||||
d.ellipse([200, 20, 250, 45], fill="#aa2222")
|
||||
|
||||
|
||||
def test_dhash_is_stable_for_identical_bytes():
|
||||
img = _screen(_login_screen)
|
||||
assert dhash_hex(img) == dhash_hex(img)
|
||||
assert len(dhash_hex(img)) == HASH_SIZE * HASH_SIZE // 4
|
||||
|
||||
|
||||
def test_dhash_survives_jpeg_reencoding_with_small_distance():
|
||||
original = _screen(_login_screen, quality=95)
|
||||
with Image.open(io.BytesIO(original)) as img:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=60)
|
||||
reencoded = buf.getvalue()
|
||||
|
||||
distance = hamming_distance_hex(dhash_hex(original), dhash_hex(reencoded))
|
||||
assert distance is not None and distance <= 4
|
||||
|
||||
|
||||
def test_dhash_separates_different_screens():
|
||||
distance = hamming_distance_hex(
|
||||
dhash_hex(_screen(_login_screen)), dhash_hex(_screen(_settings_screen))
|
||||
)
|
||||
assert distance is not None and distance > 8
|
||||
|
||||
|
||||
def test_dhash_best_effort_on_garbage():
|
||||
assert dhash_hex(None) is None
|
||||
assert dhash_hex(b"") is None
|
||||
assert dhash_hex(b"not an image at all") is None
|
||||
|
||||
|
||||
def test_hamming_distance_edge_cases():
|
||||
assert hamming_distance_hex(None, "ff") is None
|
||||
assert hamming_distance_hex("ff", None) is None
|
||||
assert hamming_distance_hex("ff", "ffff") is None # length mismatch
|
||||
assert hamming_distance_hex("zz", "aa") is None # not hex
|
||||
assert hamming_distance_hex("00ff", "00ff") == 0
|
||||
assert hamming_distance_hex("0000", "000f") == 4
|
||||
@@ -365,7 +365,7 @@ def test_build_plan_and_history_concatenates_thoughts():
|
||||
assert " - Raw thought" in output
|
||||
|
||||
|
||||
def test_build_plan_and_history_chronological_last_step():
|
||||
def test_build_plan_and_history_interleaved_decision_loop():
|
||||
plan = "- [ ] Subgoal 1"
|
||||
steps = [
|
||||
{
|
||||
@@ -390,9 +390,7 @@ def test_build_plan_and_history_chronological_last_step():
|
||||
],
|
||||
}
|
||||
]
|
||||
output = build_plan_and_history(
|
||||
plan, steps, "default", last_n_detailed=1, chronological_last_step=True
|
||||
)
|
||||
output = build_plan_and_history(plan, steps, "default", last_n_detailed=1)
|
||||
|
||||
assert plan in output
|
||||
assert "--- Execution History ---" in output
|
||||
@@ -464,9 +462,7 @@ def test_build_plan_and_history_safety_net_and_failure_analyzer():
|
||||
],
|
||||
}
|
||||
]
|
||||
output = build_plan_and_history(
|
||||
plan, steps, "default", last_n_detailed=1, chronological_last_step=True
|
||||
)
|
||||
output = build_plan_and_history(plan, steps, "default", last_n_detailed=1)
|
||||
|
||||
assert "- **Step 1 (Most Recent Step, Start: 5.0s)**" in output
|
||||
assert "* [Operator Decision Loop]:" in output
|
||||
|
||||
Reference in new issue
Block a user