Files
Artemis-Eapil/artemis/exploration/demonstrations.py
T

161 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Durable manual-demonstration commands, sharing the automation transaction."""
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)