147 lines
6.1 KiB
Python
147 lines
6.1 KiB
Python
# 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.
|
||
|
||
"""Pro-graph step summarizer node (M2: visual-transition lens dispatch).
|
||
|
||
Per the history-module redesign §5 (single step-level lens, confirmed
|
||
2026-08-31) this node no longer generates a per-step text capsule with its own
|
||
LLM call. It dispatches the step to the shared
|
||
:class:`~artemis.memory.step_memory.StepMemoryService` visual-transition lens
|
||
(``ctx.step_memory``): pre/post screenshots are pulled from the DataEngine by
|
||
step id, the red action-overlay and the ``flash_summarizer.md`` neutral-wording
|
||
contract apply unchanged, and the result lands as a versioned
|
||
``source="visual_transition"`` summary write (M0 semantics).
|
||
|
||
The auxiliary agents' compiled view (``build_plan_and_history``) is untouched:
|
||
a step whose summary is not ready yet falls back to detailed rendering
|
||
(``task_tree.py`` semantics), so the transition is safe. The intent/strategy
|
||
capsule moves up to the chunk-level ``StepCapsuleLens`` in M3.
|
||
"""
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from artemis.context import ArtemisContext
|
||
from artemis.data_engine.trace import trace
|
||
from artemis.graph.state import State
|
||
from artemis.graph.visibility import strict_state
|
||
from artemis.memory import ensure_step_memory
|
||
from artemis.utils.logger import get_logger
|
||
from artemis.utils.task_tree import format_result_clean
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
|
||
class SummarizerNode:
|
||
def __init__(self, ctx: ArtemisContext):
|
||
self.ctx = ctx
|
||
|
||
@trace(type="agent", name="summarizer")
|
||
async def __call__(self, state: State):
|
||
state = strict_state(state, "summarizer")
|
||
if not (self.ctx.data_engine and state.current_step_id):
|
||
return {}
|
||
try:
|
||
self._dispatch_visual_summary(state)
|
||
except Exception as e:
|
||
logger.error(f"Failed to dispatch visual step summary: {e}")
|
||
return {}
|
||
|
||
def _dispatch_visual_summary(self, state: State) -> None:
|
||
engine = self.ctx.data_engine
|
||
step_id = state.current_step_id
|
||
|
||
step_number = None
|
||
try:
|
||
step_number = engine.get_step_number(step_id)
|
||
except Exception as e:
|
||
logger.warning(f"Could not resolve step number for {step_id}: {e}")
|
||
|
||
record = None
|
||
if isinstance(step_number, int):
|
||
try:
|
||
record = engine.get_step_record(step_number)
|
||
except Exception as e:
|
||
logger.warning(f"Could not load step record {step_number}: {e}")
|
||
|
||
# Graceful degradation: a missing pre/post image simply dispatches with
|
||
# None bytes — the lens renders a text-only transition account.
|
||
pre_bytes = self._read_step_image(step_number, "pre")
|
||
post_bytes = self._read_step_image(step_number, "post")
|
||
|
||
action_name, action_args = self._extract_action(record, state, ctx=self.ctx)
|
||
result = getattr(state, "last_execution_result", None)
|
||
# format_result_clean reports only errors/repairs; a clean run keeps
|
||
# the controller's bare status word.
|
||
exec_outcome = format_result_clean(result)
|
||
if not exec_outcome:
|
||
if isinstance(result, dict) and result.get("status"):
|
||
exec_outcome = str(result["status"])
|
||
else:
|
||
exec_outcome = "unknown"
|
||
|
||
service = ensure_step_memory(self.ctx)
|
||
service.dispatch(
|
||
step_number=step_number if isinstance(step_number, int) else 0,
|
||
action_name=action_name,
|
||
action_args=action_args,
|
||
pre_img_bytes=pre_bytes,
|
||
post_img_bytes=post_bytes,
|
||
exec_outcome=exec_outcome,
|
||
data_engine_step_id=step_id,
|
||
)
|
||
logger.info(f"Dispatched visual transition summary for step {step_number} ({step_id})")
|
||
|
||
def _read_step_image(self, step_number: int | None, which: str) -> bytes | None:
|
||
if not isinstance(step_number, int):
|
||
return None
|
||
try:
|
||
path = self.ctx.data_engine.get_step_image_path(step_number, which)
|
||
if path is None:
|
||
return None
|
||
return path.read_bytes()
|
||
except Exception as e:
|
||
logger.warning(f"Could not read {which} image for step {step_number}: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def _extract_action(record: Any, state: State, *, ctx: Any = None) -> tuple[str, dict]:
|
||
actions = getattr(record, "action_taken", None) if record is not None else None
|
||
if not actions:
|
||
decisions = getattr(state, "structured_decisions", None)
|
||
if decisions:
|
||
try:
|
||
actions = json.loads(decisions)
|
||
except Exception:
|
||
actions = None
|
||
if isinstance(actions, dict):
|
||
actions = [actions]
|
||
if not isinstance(actions, list) or not actions or not isinstance(actions[0], dict):
|
||
return "no_op", {}
|
||
# The visual-transition lens is a model: it sees 0–1000 coordinates
|
||
# only. Pro records pixels (space-marked), so convert once here; a
|
||
# record already marked normalized passes through untouched.
|
||
from artemis.utils.coordinates import normalize_any_structure
|
||
|
||
device = getattr(ctx, "device", None) if ctx is not None else None
|
||
width = getattr(device, "device_width", None) or 1080
|
||
height = getattr(device, "device_height", None) or 2400
|
||
actions = normalize_any_structure(actions, width, height)
|
||
first = actions[0]
|
||
action_name = first.get("action") or "unknown"
|
||
action_args = {k: v for k, v in first.items() if k != "action"}
|
||
if len(actions) > 1:
|
||
action_args["additional_actions"] = actions[1:]
|
||
return action_name, action_args
|