mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 03:35:40 +02:00
196 lines
7.7 KiB
Python
196 lines
7.7 KiB
Python
"""Screenshot pruning: keep first + previous + current, collapse the rest.
|
|
|
|
Vision images are ~1.3-2k tokens each and the model re-reads every one on every
|
|
turn; pruning to 3 anchors (first/previous/current) and stubbing the rest cuts the
|
|
re-prefilled image tokens without losing the agent's memory (URL + ReportProgress
|
|
text stay). These pin the keep-set, the in-place mutation, and tool_result safety.
|
|
"""
|
|
|
|
from backend.apps.agents.browser.browser_history import (
|
|
prune_old_screenshots,
|
|
OMITTED_SCREENSHOT_STUB,
|
|
)
|
|
|
|
|
|
def p_img(tag):
|
|
return {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": tag}}
|
|
|
|
|
|
def p_shot_turn(tag, url):
|
|
# mirrors _format_tool_result for BrowserScreenshot: [image, text(url)]
|
|
return {"role": "user", "content": [{
|
|
"type": "tool_result", "tool_use_id": f"t_{tag}",
|
|
"content": [p_img(tag), {"type": "text", "text": f"Screenshot captured. URL: {url}"}],
|
|
}]}
|
|
|
|
|
|
def p_count_images(messages):
|
|
n = 0
|
|
for m in messages:
|
|
for b in m.get("content", []):
|
|
if isinstance(b, dict):
|
|
if b.get("type") == "image":
|
|
n += 1
|
|
elif b.get("type") == "tool_result":
|
|
n += sum(1 for x in b.get("content", []) if isinstance(x, dict) and x.get("type") == "image")
|
|
return n
|
|
|
|
|
|
def test_keeps_first_and_last_two_collapses_middle():
|
|
msgs = [p_shot_turn(str(i), f"https://site/{i}") for i in range(5)] # images 0..4
|
|
collapsed = prune_old_screenshots(msgs)
|
|
assert collapsed == 2 # 5 images - (first + last 2) = 2 stubbed
|
|
assert p_count_images(msgs) == 3
|
|
# image tags that survive are 0 (first), 3 and 4 (last two)
|
|
surviving = [b["source"]["data"] for m in msgs for tr in m["content"]
|
|
for b in tr["content"] if b.get("type") == "image"]
|
|
assert surviving == ["0", "3", "4"]
|
|
|
|
|
|
def test_three_or_fewer_is_a_noop():
|
|
msgs = [p_shot_turn(str(i), f"u{i}") for i in range(3)]
|
|
assert prune_old_screenshots(msgs) == 0
|
|
assert p_count_images(msgs) == 3
|
|
|
|
|
|
def test_stub_preserves_the_url_text_block():
|
|
msgs = [p_shot_turn(str(i), f"https://site/{i}") for i in range(4)]
|
|
prune_old_screenshots(msgs)
|
|
# the collapsed shot (#1) keeps its "URL:" text, only the image became a stub
|
|
collapsed_tr = msgs[1]["content"][0]["content"]
|
|
assert any(b.get("text") == OMITTED_SCREENSHOT_STUB for b in collapsed_tr)
|
|
assert any("URL: https://site/1" in b.get("text", "") for b in collapsed_tr)
|
|
|
|
|
|
def test_handles_direct_image_blocks_too():
|
|
msgs = [
|
|
{"role": "user", "content": [p_img("a"), {"type": "text", "text": "hi"}]},
|
|
{"role": "user", "content": [p_img("b")]},
|
|
{"role": "user", "content": [p_img("c")]},
|
|
{"role": "user", "content": [p_img("d")]},
|
|
]
|
|
collapsed = prune_old_screenshots(msgs)
|
|
assert collapsed == 1 # keep a (first), c+d (last two); stub b
|
|
assert msgs[1]["content"][0] == {"type": "text", "text": OMITTED_SCREENSHOT_STUB}
|
|
|
|
|
|
def test_keep_recent_is_tunable():
|
|
msgs = [p_shot_turn(str(i), f"u{i}") for i in range(6)]
|
|
prune_old_screenshots(msgs, keep_first=False, keep_recent=1)
|
|
# only the most recent survives
|
|
assert p_count_images(msgs) == 1
|
|
|
|
|
|
def p_tool_use_msg(tu_id, name):
|
|
return {"role": "assistant", "content": [
|
|
{"type": "tool_use", "id": tu_id, "name": name, "input": {}},
|
|
]}
|
|
|
|
|
|
def p_tool_result_msg(tu_id, text):
|
|
return {"role": "user", "content": [
|
|
{"type": "tool_result", "tool_use_id": tu_id,
|
|
"content": [{"type": "text", "text": text}]},
|
|
]}
|
|
|
|
|
|
def test_prune_stale_page_state_keeps_last_two_attachments():
|
|
from backend.apps.agents.browser.browser_history import (
|
|
PAGE_STATE_MARKER, prune_stale_page_state,
|
|
)
|
|
msgs = []
|
|
for i in range(4):
|
|
msgs.append(p_tool_use_msg(f"t{i}", "BrowserClickIndex"))
|
|
msgs.append(p_tool_result_msg(
|
|
f"t{i}", f"Clicked [{i}]\n\n{PAGE_STATE_MARKER}\n[1]<button \"A{i}\">",
|
|
))
|
|
pruned = prune_stale_page_state(msgs)
|
|
assert pruned == 2
|
|
texts = [m["content"][0]["content"][0]["text"] for m in msgs if m["role"] == "user"]
|
|
assert PAGE_STATE_MARKER not in texts[0] and "Clicked [0]" in texts[0]
|
|
assert PAGE_STATE_MARKER not in texts[1]
|
|
assert PAGE_STATE_MARKER in texts[2] and PAGE_STATE_MARKER in texts[3]
|
|
# idempotent: a second pass finds nothing new to prune
|
|
assert prune_stale_page_state(msgs) == 0
|
|
|
|
|
|
def test_prune_stale_page_state_collapses_old_heavy_reads_only():
|
|
from backend.apps.agents.browser.browser_history import prune_stale_page_state
|
|
big = "28 interactive elements\n" + "\n".join(f"[{i}]<button \"x\">" for i in range(60))
|
|
msgs = []
|
|
for i in range(3):
|
|
msgs.append(p_tool_use_msg(f"r{i}", "BrowserListInteractives"))
|
|
msgs.append(p_tool_result_msg(f"r{i}", big))
|
|
msgs.append(p_tool_use_msg("nav", "BrowserNavigate"))
|
|
msgs.append(p_tool_result_msg("nav", "Navigated to https://example.com"))
|
|
pruned = prune_stale_page_state(msgs)
|
|
assert pruned == 1
|
|
first = msgs[1]["content"][0]["content"][0]["text"]
|
|
assert first.startswith("28 interactive elements") and "pruned" in first
|
|
assert msgs[3]["content"][0]["content"][0]["text"] == big
|
|
assert msgs[7]["content"][0]["content"][0]["text"] == "Navigated to https://example.com"
|
|
|
|
|
|
# --- incremental cache marker ---------------------------------------------------
|
|
def test_cache_marker_places_one_at_depth_and_strips_old():
|
|
from backend.apps.agents.browser.browser_history import place_cache_marker
|
|
msgs = []
|
|
for i in range(12):
|
|
msgs.append(p_tool_use_msg(f"t{i}", "BrowserClickIndex"))
|
|
msgs.append(p_tool_result_msg(f"t{i}", f"Clicked [{i}]"))
|
|
place_cache_marker(msgs)
|
|
place_cache_marker(msgs) # second pass must not accumulate markers
|
|
marked = [
|
|
(mi, b) for mi, m in enumerate(msgs)
|
|
for b in (m["content"] if isinstance(m["content"], list) else [])
|
|
if isinstance(b, dict) and "cache_control" in b
|
|
]
|
|
assert len(marked) == 1
|
|
assert marked[0][0] == len(msgs) - 8 - 1 # last markable message before the tail zone
|
|
|
|
|
|
def test_cache_marker_skips_short_conversations():
|
|
from backend.apps.agents.browser.browser_history import place_cache_marker
|
|
msgs = [p_tool_use_msg("t0", "BrowserClickIndex"), p_tool_result_msg("t0", "Clicked")]
|
|
place_cache_marker(msgs)
|
|
assert all(
|
|
"cache_control" not in b
|
|
for m in msgs for b in m["content"]
|
|
if isinstance(b, dict)
|
|
)
|
|
|
|
|
|
def test_cache_marker_prefix_stays_stable_across_a_simulated_turn():
|
|
from backend.apps.agents.browser.browser_history import (
|
|
PAGE_STATE_MARKER, place_cache_marker, prune_stale_page_state,
|
|
)
|
|
import copy as p_copy
|
|
import json as p_json
|
|
|
|
def p_stripped(ms):
|
|
ms = p_copy.deepcopy(ms)
|
|
for m in ms:
|
|
for b in (m["content"] if isinstance(m["content"], list) else []):
|
|
if isinstance(b, dict):
|
|
b.pop("cache_control", None)
|
|
return p_json.dumps(ms, sort_keys=True)
|
|
|
|
msgs = []
|
|
for i in range(10):
|
|
msgs.append(p_tool_use_msg(f"t{i}", "BrowserClickIndex"))
|
|
msgs.append(p_tool_result_msg(
|
|
f"t{i}", f"Clicked [{i}]\n\n{PAGE_STATE_MARKER}\n[1]<button \"A{i}\">",
|
|
))
|
|
prune_stale_page_state(msgs)
|
|
place_cache_marker(msgs)
|
|
cut = len(msgs) - 8
|
|
before = p_stripped(msgs[:cut])
|
|
# next turn: a new attachment arrives, pruning collapses the one falling out
|
|
msgs.append(p_tool_use_msg("t10", "BrowserClickIndex"))
|
|
msgs.append(p_tool_result_msg(
|
|
"t10", f"Clicked [10]\n\n{PAGE_STATE_MARKER}\n[1]<button \"A10\">",
|
|
))
|
|
prune_stale_page_state(msgs)
|
|
place_cache_marker(msgs)
|
|
assert p_stripped(msgs[:cut]) == before
|