"""Durable manual-demonstration commands, sharing the automation transaction.""" from __future__ import annotations import time from uuid import uuid4 from artemis.exploration.store import ExplorationConflict, ExplorationStore TERMINAL = {"completed", "failed", "cancelled", "timed_out"} DEMONSTRATION_SOURCE_RULE = ( "人工示范的 source 是用户选择的来源,recorded_sources 是实际录像来源" "(computer=电脑,android=安卓,both=双端)。延续示范中各步骤的设备归属;" "仅补充网址或缩小范围不表示更换平台。用户明确改端或原目标包含跨端步骤时," "按对应来源处理,不把整个任务固定在某一端。当前主设备截图不决定示范来源;" "执行、验收和用例提炼必须核对每步的平台,不能用手机上的成功证明电脑步骤完成。" ) def demonstration_provenance(demo: dict) -> dict: """Capture metadata is authoritative even when the model omits its source.""" return { "id": demo["id"], "run_id": demo["run_id"], "source": demo["source"], "recorded_sources": sorted({track["kind"] for track in demo.get("media", [])}), } def demonstration_summary(demo: dict, summary: str) -> str: labels = {"android": "安卓", "computer": "电脑", "both": "安卓与电脑"} provenance = demonstration_provenance(demo) recorded = "、".join(labels.get(kind, kind) for kind in provenance["recorded_sources"]) return f"人工示范({labels[demo['source']]};录像来源:{recorded or '未记录'})\n{summary}" class Demonstrations: def __init__(self, repository: ExplorationStore): self.repository = repository def get(self, demo_id: str) -> dict: value = self.repository.database.get("exploration_demo", demo_id) if value is None: raise KeyError("人工示范不存在") return value def list(self, session_id: str) -> list[dict]: self.repository.get(session_id) with self.repository.database.connection() as connection: rows = connection.execute( "SELECT payload FROM automation_records WHERE kind='exploration_demo' " "AND json_extract(payload,'$.exploration_id')=? ORDER BY updated_at", (session_id,), ).fetchall() import json return [json.loads(row[0]) for row in rows] def completed_context(self, session_id: str) -> list[dict]: # Keep this small: summaries already live in the conversation. Read the # durable records so legacy messages and process restarts work too. return [ demonstration_provenance(demo) for demo in self.list(session_id) if demo["status"] == "completed" ] def request(self, session_id: str, source: str, request_id: str) -> dict: repo = self.repository with repo._transaction() as connection: session = repo._require(connection, session_id) def create(): run_id = session["active_run_id"] run = repo._get(connection, "exploration_run", run_id) if run_id else None if ( not run or run["kind"] != "exploration" or run["status"] not in {"running", "waiting_user"} ): raise ExplorationConflict("请等待探索开始执行,再请求人工示范") if session["active_demonstration_id"]: raise ExplorationConflict("已有进行中的人工示范") value = { "id": str(uuid4()), "exploration_id": session_id, "run_id": run_id, "source": source, "status": "requested", "command": None, "created_at": time.time(), "deadline": None, "media": [], "error": None, "timeout_seconds": session["config"]["demonstration_timeout_seconds"], "warning_seconds": session["config"]["demonstration_warning_seconds"], } repo._put(connection, "exploration_demo", value["id"], value) session["active_demonstration_id"] = value["id"] repo._save_session(connection, session) return value return repo._operation(connection, f"{session_id}:demo", request_id, source, create) def command( self, session_id: str, demo_id: str, command: str, request_id: str, *, seconds: int = 0 ) -> dict: if command not in {"finish", "cancel", "extend"}: raise ValueError("不支持的示范操作") if command == "extend" and not 60 <= seconds <= 86400: raise ValueError("延长时长必须在 60–86400 秒之间") repo = self.repository with repo._transaction() as connection: session = repo._require(connection, session_id) demo = repo._get(connection, "exploration_demo", demo_id) if not demo or demo["exploration_id"] != session_id: raise KeyError("人工示范不存在") def update(): if session["active_demonstration_id"] != demo_id or demo["status"] not in { "requested", "recording", }: raise ExplorationConflict("示范已结束或正在分析") if command != "cancel" and demo["status"] != "recording": raise ExplorationConflict("请等待录像就绪") if demo.get("command"): raise ExplorationConflict("示范已请求结束") if demo.get("deadline") and time.time() >= demo["deadline"]: raise ExplorationConflict("示范已到时,请等待保存录像") if command == "extend": demo["deadline"] += seconds else: demo["command"] = command return repo._put(connection, "exploration_demo", demo_id, demo) return repo._operation( connection, f"{demo_id}:command", request_id, [command, seconds], update ) def transition(self, demo_id: str, status: str, **fields) -> dict: repo = self.repository with repo._transaction() as connection: demo = repo._get(connection, "exploration_demo", demo_id) if not demo: raise KeyError("人工示范不存在") session = repo._require(connection, demo["exploration_id"]) if demo["status"] in TERMINAL: return demo if session["active_run_id"] != demo["run_id"]: raise ExplorationConflict("示范所属执行已结束") demo.update(status=status, **fields) if status == "recording": demo.update(started_at=time.time(), deadline=time.time() + demo["timeout_seconds"]) if status in TERMINAL: demo["finished_at"] = time.time() session["active_demonstration_id"] = None repo._save_session(connection, session) return repo._put(connection, "exploration_demo", demo_id, demo)