Files
Artemis-Plugins/tests/benchmark_plugins.py
T

280 lines
11 KiB
Python

"""Opt-in sequential A/B through ARTEMIS's existing queue, never a second agent.
Usage: python tests/benchmark_plugins.py --traces <ARTEMIS traces> --output <new folder>
The user-started service must already run. Only this script's owned fixture is closed.
"""
import argparse
import json
import sqlite3
import subprocess
import time
import urllib.request
import uuid
from pathlib import Path
PROMPT = """只操作测试窗口 ARTEMIS Plugin Benchmark(进程 PID {pid}),不要观察或操作其他应用。先按此 PID 找到窗口。
1. 将 Network name 填为 ARTEMIS-LAB,勾选 Night mode,点击 Apply,确认显示 Saved: ARTEMIS-LAB; Night mode: True。
2. 只点击唯一的蓝色圆形一次,不要点击红色圆形。在下方 Scrollable settings 面板向下滚动,找到并点击 Finish,确认 Finished successfully。
3. 保存这个窗口的截图到任务 trace,文件名 proof.png,然后结束并如实说明实际结果。不要关闭窗口。
"""
def api(base, route, body=None):
request = urllib.request.Request(
base + route,
data=json.dumps(body).encode() if body is not None else None,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
def collect(traces, sid):
with sqlite3.connect(
f"file:{(traces / 'data_engine.db').as_posix()}?mode=ro", uri=True
) as db:
db.row_factory = sqlite3.Row
session = dict(
db.execute("SELECT * FROM sessions WHERE session_id=?", (sid,)).fetchone()
)
events = [
dict(row)
for row in db.execute(
"SELECT name,type,status,duration,payload FROM traces WHERE session_id=? ORDER BY timestamp",
(sid,),
)
]
steps = db.execute(
"SELECT count(*) FROM steps WHERE session_id=?", (sid,)
).fetchone()[0]
usage, calls, requests = [], [], []
for event in events:
payload = json.loads(event["payload"] or "{}")
if event["name"] == "llm_usage":
usage.append(payload)
if "plugin_id" in payload and "tool_name" in payload:
# Tool trace contains the real call receipt, not the separate log line.
calls.append(
{
"name": payload["tool_name"],
"status": event["status"],
"duration": event["duration"],
"args": payload.get("args"),
"result": payload.get("result"),
}
)
if event["name"] == "llm_request":
requests.append(payload)
return {
"session": session,
"steps": steps,
"llm_usage": usage,
"plugin_calls": calls,
"requests": requests,
}
def main():
parser = argparse.ArgumentParser(__doc__)
parser.add_argument("--base", default="http://127.0.0.1:8000")
parser.add_argument("--traces", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--model", default="mimo-v2.6-flash")
parser.add_argument("--repetitions", type=int, default=3)
parser.add_argument("--resume", action="store_true")
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=args.resume)
config = {
"model": args.model,
"reasoning_effort": "high",
"execution_target": "computer",
"record_video": True,
"recording": {"codec": "h264", "computer_scope": "window"},
"analyze_video": False,
"timeout_seconds": 480,
"request_timeout_seconds": 120,
"max_retries": 1,
}
protocol = {
"prompt": PROMPT,
"config": config,
"repetitions": args.repetitions,
"acceptance": "applied && finished && exactly one target click && no wrong clicks && verified proof.png",
"order": "OCU,Cua / Cua,OCU / OCU,Cua",
"fixture": "benchmark-fixture.ps1",
}
protocol_path = args.output / "protocol.json"
if args.resume and protocol_path.exists():
if json.loads(protocol_path.read_text("utf-8")) != protocol:
raise ValueError(
"Resume requires the original prompt, model and task settings"
)
else:
protocol_path.write_text(
json.dumps(protocol, ensure_ascii=False, indent=2), "utf-8"
)
for repetition in range(args.repetitions):
order = (
["open-computer-use", "cua-driver"]
if repetition % 2 == 0
else ["cua-driver", "open-computer-use"]
)
for plugin in order:
folder = args.output / f"{repetition + 1}-{plugin}"
if args.resume and (folder / "result.json").is_file():
continue
queue = api(args.base, "/api/status")
if queue.get("active_tasks") or queue.get("queue"):
raise RuntimeError(
"Other tasks are active/queued; benchmark will not interfere"
)
folder.mkdir()
state_file = folder / "fixture.json"
stderr = (folder / "fixture-stderr.log").open("w")
fixture = subprocess.Popen(
[
"powershell.exe",
"-NoProfile",
"-File",
str(Path(__file__).with_name("benchmark-fixture.ps1").resolve()),
"-StateFile",
str(state_file.resolve()),
],
creationflags=subprocess.CREATE_NO_WINDOW,
stdout=subprocess.DEVNULL,
stderr=stderr,
)
sid = str(uuid.uuid4())
done = False
try:
for _ in range(100):
if state_file.exists():
break
time.sleep(0.1)
if not state_file.exists():
raise RuntimeError("fixture did not start")
body = {
"goal": PROMPT.format(pid=fixture.pid),
"session_id": sid,
"profile": "flash",
"ingress": "client",
"runtime_overrides": {
**config,
"plugins": [
{
"id": plugin,
"profile": "benchmark"
if plugin == "cua-driver"
else "default",
"version": "0.1.2"
if plugin == "cua-driver"
else "1.1.0",
}
],
},
}
(folder / "request.json").write_text(
json.dumps(body, ensure_ascii=False, indent=2), "utf-8"
)
started = time.monotonic()
response = api(args.base, "/api/run", body)
(folder / "submission.json").write_text(json.dumps(response), "utf-8")
print(
json.dumps(
{
"event": "submitted",
"plugin": plugin,
"round": repetition + 1,
"trace_id": sid,
"response": response,
}
),
flush=True,
)
if response.get("status") in {"rejected", "error"}:
raise RuntimeError("task was rejected")
while time.monotonic() - started < 600:
time.sleep(15)
status = api(args.base, f"/api/sessions/{sid}")
state = json.loads(state_file.read_text("utf-8-sig"))
print(
json.dumps(
{
"event": "progress",
"trace_id": sid,
"status": status["status"],
"elapsed": round(time.monotonic() - started),
"applied": state["applied"],
"finished": state["finished"],
}
),
flush=True,
)
if status["status"] not in {
"running",
"pending",
"queued",
"stopping",
"starting",
}:
done = True
break
if not done:
api(args.base, "/api/stop", {"session_id": sid})
time.sleep(3)
elapsed = time.monotonic() - started
result = collect(args.traces.resolve(), sid)
state = json.loads(state_file.read_text("utf-8-sig"))
attachment = (
args.traces / sid / "artifacts/plugins" / plugin / "proof.png"
)
result.update(
trace_id=sid,
plugin=plugin,
repetition=repetition + 1,
fixture=state,
observed_elapsed=elapsed,
watchdog_timeout=not done,
actual_pass=bool(
state["applied"]
and state["finished"]
and state["target_clicks"] == 1
and state["wrong_clicks"] == 0
and attachment.exists()
),
attachment_exists=attachment.exists(),
)
(folder / "result.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2), "utf-8"
)
print(
json.dumps(
{
"event": "done",
"trace_id": sid,
"actual_pass": result["actual_pass"],
}
),
flush=True,
)
# Release/recording cleanup belongs to ARTEMIS, wait before next fixture.
for _ in range(15):
if not api(args.base, "/api/status").get("active_tasks"):
break
time.sleep(2)
finally:
if not done:
try:
api(args.base, "/api/stop", {"session_id": sid})
except (OSError, ValueError) as error:
print(f"Cleanup request failed for {sid}: {error}", flush=True)
if fixture.poll() is None:
fixture.terminate()
fixture.wait(10)
stderr.close()
if __name__ == "__main__":
main()