Files
Artemis-Eapil/artemis/agents/checker/evidence.py
T

103 lines
4.3 KiB
Python

"""Bounded, source-labelled end-of-run evidence from the existing step ledger."""
import base64
from pathlib import Path
import re
from artemis.data_engine.step_evidence import step_image_name
def final_evidence(engine, *, max_sources: int = 8) -> list[dict]:
"""Attach the last two independently captured frames per resource.
These are historical captures, never advertised as live device state.
Equal hashes retain both capture labels but share one image block. No
screenshots from another resource are substituted for missing evidence.
"""
if engine is None:
return []
steps = engine.storage.get_steps(engine.current_session_id) or []
sources = {}
for step in steps:
result = step.last_execution_result
execution = result.get("execution") if isinstance(result, dict) else None
actions = execution if isinstance(execution, list) else step.action_taken
actions = actions if isinstance(actions, list) else [actions]
for action in actions:
if not isinstance(action, dict):
continue
plugin = action.get("plugin_id")
source = action.get("source") or {}
if plugin:
identity = (plugin, source.get("kind", "other"), source.get("target_id", ""))
name = action.get("post_image_name")
if len(actions) == 1:
name = name or step_image_name(step, "post")
pre = action.get("pre_image_name") if action.get("pre_image_valid") else None
else:
# A pure helper/report's decision frame is not a fresh post-action observation.
if str(action.get("action", "")).startswith("plug_"):
continue
identity = ("device", "android", "primary")
name, pre = step.post_image_name, step.pre_image_name
if not name and not plugin:
continue
label = f"Step {step.step_number} post-action; {identity}; {source.get('label', '')}"
label += f"; captured_at={source.get('captured_at') or step.timestamp}"
if pre and pre == name:
label += (
"; fresh post-capture identical to pre-capture (not proof of success by itself)"
)
frames = sources.pop(identity, [])
sources[identity] = [*frames[-1:], (name, label)]
selected = list(sources.values())[-max_sources:]
blocks = []
attached = set()
for frames in selected:
for name, label in frames[-2:]:
if not name:
blocks.append(
{
"type": "text",
"text": f"Recorded evidence: {label}; no post-action image. Earlier captures do not prove the state after this call.",
}
)
continue
if not isinstance(name, str) or not re.fullmatch(r"[0-9a-f]{64}", name):
continue
path = Path(engine.get_image_path(name))
blocks.append({"type": "text", "text": f"Recorded evidence: {label}; image={name}"})
if name in attached:
blocks.append(
{"type": "text", "text": "Same image as the preceding capture with this hash."}
)
continue
try:
image = path.read_bytes()
except OSError:
blocks.append(
{
"type": "text",
"text": "Recorded image unavailable; do not infer its contents.",
}
)
continue
mime = "image/png" if image.startswith(b"\x89PNG") else "image/jpeg"
if image.startswith(b"RIFF") and image[8:12] == b"WEBP":
mime = "image/webp"
blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{base64.b64encode(image).decode()}"},
}
)
attached.add(name)
if len(sources) > max_sources:
blocks.append(
{
"type": "text",
"text": "Additional resources omitted from this bounded preview; inspect them with get_step_screenshot before judging.",
}
)
return blocks