feat: add favorite task templates and optimize evidence exports
This commit is contained in:
1 parent
e1ea099592
commit
a4a5430922
46 files changed
+1714
-188
No files matched your search
@@ -0,0 +1,406 @@
|
||||
"""Independent favorite snapshots dispatched through the existing task services."""
|
||||
|
||||
from copy import deepcopy
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from typing import Literal
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from apps.admin_console.database.repositories.session_repository import SessionRepository
|
||||
from apps.admin_console.services.automation_store import AutomationStore, identifier, store
|
||||
from apps.admin_console.services.execution_config import execution_config
|
||||
from apps.admin_console.services.model_service import model_service
|
||||
from apps.admin_console.services.task_import import TaskCase, TaskOptions, execution_title
|
||||
from artemis.config.exploration import ExplorationConfig
|
||||
from artemis.exploration.case_prompt import require_resolved_case
|
||||
from artemis.exploration.store import ExplorationStore
|
||||
|
||||
|
||||
class FavoriteCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
source_kind: Literal["task", "batch", "exploration", "exploration_case"] = "task"
|
||||
source_id: str = Field(pattern=r"^[A-Za-z0-9_-]{1,100}$")
|
||||
version: int | None = Field(default=None, ge=1)
|
||||
name: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
|
||||
|
||||
class FavoriteUpdate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
name: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
task_config: TaskOptions | None = None
|
||||
case_configs: dict[int, TaskOptions] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FavoriteRun(FavoriteUpdate):
|
||||
request_id: str = Field(pattern=r"^[A-Za-z0-9_-]{1,100}$")
|
||||
|
||||
|
||||
def _merge_options(target: dict, updates: dict) -> None:
|
||||
if isinstance(updates.get("recording"), dict):
|
||||
updates = {
|
||||
**updates,
|
||||
"recording": {**(target.get("recording") or {}), **updates["recording"]},
|
||||
}
|
||||
target.update(updates)
|
||||
|
||||
|
||||
class FavoriteService:
|
||||
def __init__(self, storage: AutomationStore = store):
|
||||
self.store = storage
|
||||
self._run_locks: WeakValueDictionary = WeakValueDictionary()
|
||||
|
||||
def get(self, key: str) -> dict:
|
||||
value = self.store.get("favorite", identifier(key))
|
||||
if value is None:
|
||||
raise KeyError("收藏不存在")
|
||||
return value
|
||||
|
||||
def _task(self, session_id: str, *, fallback: dict | None = None) -> tuple[dict, list[str]]:
|
||||
job = self.store.job(session_id) or {}
|
||||
if snapshot := job.get("execution_config"):
|
||||
return deepcopy(snapshot), []
|
||||
row = SessionRepository(self.store.path).get_session_by_id(session_id) or {}
|
||||
submission = job.get("submission") or fallback or {}
|
||||
options = {
|
||||
**(job.get("case") or {}).get("task_config", {}),
|
||||
**(submission.get("runtime_overrides") or {}),
|
||||
}
|
||||
options = {key: value for key, value in options.items() if value is not None}
|
||||
try:
|
||||
device = row.get("device_info") or {}
|
||||
device = json.loads(device) if isinstance(device, str) else device
|
||||
except (ValueError, TypeError):
|
||||
device = {}
|
||||
device = device if isinstance(device, dict) else {}
|
||||
profile = (
|
||||
options.get("profile")
|
||||
or submission.get("profile")
|
||||
or model_service.resolve_session_profile(row)
|
||||
or "flash"
|
||||
)
|
||||
options["profile"] = profile
|
||||
serial = (
|
||||
options.get("device_serial")
|
||||
or submission.get("device_serial")
|
||||
or job.get("device_serial")
|
||||
or device.get("serial")
|
||||
)
|
||||
if serial:
|
||||
options["device_serial"] = serial
|
||||
if serial == "computer:local" or device.get("execution_target") == "computer":
|
||||
options["execution_target"] = "computer"
|
||||
# Historical diagnostics describe the actual model, unlike display metadata
|
||||
# which may reflect today's defaults. Read only execution model roles.
|
||||
with self.store.connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT payload FROM traces WHERE session_id=? AND type='llm_diagnostic' ORDER BY timestamp DESC",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
for trace in rows:
|
||||
try:
|
||||
data = json.loads(trace[0])
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if str(data.get("role", "")).lower() in {
|
||||
"operator",
|
||||
"flashrunner",
|
||||
"reactiverunner",
|
||||
"planner",
|
||||
}:
|
||||
for key in ("model", "reasoning_effort"):
|
||||
if data.get(key):
|
||||
options.setdefault(key, data[key])
|
||||
if options.get("model"):
|
||||
break
|
||||
missing = [
|
||||
key
|
||||
for key in (
|
||||
"model",
|
||||
"reasoning_effort",
|
||||
"record_video",
|
||||
"recording",
|
||||
"analyze_video",
|
||||
"request_timeout_seconds",
|
||||
"max_retries",
|
||||
"retry_interval_seconds",
|
||||
"timeout_seconds",
|
||||
)
|
||||
if key not in options
|
||||
]
|
||||
snapshot = execution_config(
|
||||
{**submission, "profile": profile, "runtime_overrides": options}
|
||||
)
|
||||
return snapshot, missing
|
||||
|
||||
def create(self, value: FavoriteCreate) -> dict:
|
||||
token = f"{value.source_kind}:{value.source_id}:{value.version or ''}"
|
||||
key = hashlib.sha256(token.encode()).hexdigest()[:32]
|
||||
if existing := self.store.get("favorite", key):
|
||||
return existing
|
||||
snapshot, missing, kind, version = {}, [], value.source_kind, None
|
||||
source_id = value.source_id
|
||||
repo = ExplorationStore(self.store)
|
||||
if kind == "task":
|
||||
run = repo.run(source_id)
|
||||
if run and run["kind"] == "exploration":
|
||||
source_id = run["exploration_id"]
|
||||
kind = "exploration_case" if repo.case(source_id) else "exploration"
|
||||
if kind in {"exploration", "exploration_case"}:
|
||||
session = repo.get(source_id)
|
||||
options = deepcopy(session["task_config"])
|
||||
if kind == "exploration":
|
||||
snapshot = {
|
||||
"goal": session["goal"],
|
||||
"config": session["config"],
|
||||
"task_config": options,
|
||||
}
|
||||
name = session["goal"][:120]
|
||||
else:
|
||||
case = repo.case(source_id, value.version)
|
||||
if not case:
|
||||
raise KeyError("确认用例不存在")
|
||||
version = case["version"]
|
||||
validations = [
|
||||
r
|
||||
for r in repo.runs(source_id)
|
||||
if r["kind"] == "validation" and r.get("case_version") == version
|
||||
]
|
||||
if validations:
|
||||
config, missing = self._task(
|
||||
validations[-1]["id"],
|
||||
fallback={"runtime_overrides": validations[-1]["task_config"]},
|
||||
)
|
||||
else:
|
||||
options = {
|
||||
k: v
|
||||
for k, v in options.items()
|
||||
if k not in {"profile", "model", "reasoning_effort", "timeout_seconds"}
|
||||
}
|
||||
config = execution_config({"profile": "flash", "runtime_overrides": options})
|
||||
missing = ["首次用例复跑,默认 Flash"]
|
||||
snapshot = {"goal": require_resolved_case(case), **config}
|
||||
name = case["name"]
|
||||
elif kind == "batch":
|
||||
batch = self.store.get("batch", source_id)
|
||||
if not batch or batch.get("status") == "deleting":
|
||||
raise KeyError("批次不存在")
|
||||
cases, seen = [], set()
|
||||
for job in self.store.jobs(source_id):
|
||||
position = job.get("case_position", job["position"])
|
||||
if position in seen:
|
||||
continue
|
||||
seen.add(position)
|
||||
case = deepcopy(job["case"])
|
||||
config, inherited = self._task(
|
||||
job["session_id"],
|
||||
fallback={
|
||||
"profile": batch["profile"],
|
||||
"device_serial": batch.get("device_serial"),
|
||||
"runtime_overrides": {
|
||||
"timeout_seconds": batch.get("timeout_seconds"),
|
||||
**case.get("task_config", {}),
|
||||
},
|
||||
},
|
||||
)
|
||||
case["task_config"] = config["runtime_overrides"]
|
||||
cases.append(TaskCase.model_validate(case).model_dump(exclude_none=True))
|
||||
missing.extend(inherited)
|
||||
if not cases:
|
||||
raise ValueError("批次没有可收藏的用例")
|
||||
snapshot = {
|
||||
key: batch[key]
|
||||
for key in (
|
||||
"name",
|
||||
"formats",
|
||||
"profile",
|
||||
"device_serial",
|
||||
"timeout_seconds",
|
||||
"stop_on_failure",
|
||||
"repeat_count",
|
||||
)
|
||||
if key in batch
|
||||
}
|
||||
snapshot["cases"] = cases
|
||||
name = batch["name"]
|
||||
else:
|
||||
row = SessionRepository(self.store.path).get_session_by_id(source_id) or {}
|
||||
job = self.store.job(source_id) or {}
|
||||
goal = (
|
||||
(job.get("case") or {}).get("goal")
|
||||
or row.get("initial_goal")
|
||||
or (job.get("submission") or {}).get("goal")
|
||||
)
|
||||
if not goal:
|
||||
raise KeyError("任务不存在或没有可复用的提示词")
|
||||
config, missing = self._task(source_id)
|
||||
snapshot = {"goal": goal, **config}
|
||||
name = execution_title(job) or goal[:120]
|
||||
favorite = {
|
||||
"id": key,
|
||||
"name": (value.name or name).strip(),
|
||||
"kind": kind,
|
||||
"source_kind": value.source_kind,
|
||||
"source_id": value.source_id,
|
||||
"case_version": version,
|
||||
"snapshot": snapshot,
|
||||
"inherited_fields": sorted(set(missing)),
|
||||
"created_at": time.time(),
|
||||
"updated_at": time.time(),
|
||||
}
|
||||
if not favorite["name"]:
|
||||
raise ValueError("收藏名称不能为空")
|
||||
return self.store.put("favorite", key, favorite)
|
||||
|
||||
def updated(self, favorite: dict, value: FavoriteUpdate) -> dict:
|
||||
result = deepcopy(favorite)
|
||||
if value.name is not None:
|
||||
if not value.name.strip():
|
||||
raise ValueError("收藏名称不能为空")
|
||||
result["name"] = value.name.strip()
|
||||
snapshot = result["snapshot"]
|
||||
options = value.task_config.model_dump(exclude_none=True) if value.task_config else {}
|
||||
if result["kind"] == "batch":
|
||||
for index, case in enumerate(snapshot["cases"]):
|
||||
if override := value.case_configs.get(index):
|
||||
_merge_options(case["task_config"], override.model_dump(exclude_none=True))
|
||||
_merge_options(case["task_config"], options)
|
||||
if set(value.case_configs) - set(range(len(snapshot["cases"]))):
|
||||
raise ValueError("批次用例下标不存在")
|
||||
if options.get("device_serial"):
|
||||
snapshot["device_serial"] = options["device_serial"]
|
||||
elif result["kind"] == "exploration":
|
||||
config = snapshot["config"]
|
||||
config.update(
|
||||
{
|
||||
k: options[k]
|
||||
for k in ("model", "reasoning_effort", "timeout_seconds")
|
||||
if k in options
|
||||
}
|
||||
)
|
||||
snapshot["config"] = ExplorationConfig.model_validate(config).model_dump()
|
||||
_merge_options(snapshot["task_config"], options)
|
||||
else:
|
||||
_merge_options(snapshot["runtime_overrides"], options)
|
||||
return result
|
||||
|
||||
def update(self, key: str, value: FavoriteUpdate) -> dict:
|
||||
favorite = self.updated(self.get(key), value)
|
||||
favorite["updated_at"] = time.time()
|
||||
return self.store.put("favorite", key, favorite)
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
with self.store.connection() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM automation_records WHERE kind='favorite' AND id=?", (identifier(key),)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
async def run(self, key: str, value: FavoriteRun) -> dict:
|
||||
lock = self._run_locks.setdefault(value.request_id, asyncio.Lock())
|
||||
async with lock:
|
||||
return await self._run(key, value)
|
||||
|
||||
async def _run(self, key: str, value: FavoriteRun) -> dict:
|
||||
key = identifier(key)
|
||||
request = {"favorite_id": key, **value.model_dump(exclude_none=True)}
|
||||
signature = hashlib.sha256(json.dumps(request, sort_keys=True).encode()).hexdigest()
|
||||
# Reserve a durable snapshot before admission. Retries survive restarts,
|
||||
# edits/deletions of the favorite, and lost HTTP responses.
|
||||
with self.store.connection() as conn:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute(
|
||||
"SELECT payload FROM automation_records WHERE kind='favorite_run' AND id=?",
|
||||
(value.request_id,),
|
||||
).fetchone()
|
||||
record = json.loads(row[0]) if row else None
|
||||
if record and record["signature"] != signature:
|
||||
raise ValueError("request_id 已用于其他内容")
|
||||
if record and record.get("result"):
|
||||
return record["result"]
|
||||
if record is None:
|
||||
row = conn.execute(
|
||||
"SELECT payload FROM automation_records WHERE kind='favorite' AND id=?", (key,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise KeyError("收藏不存在")
|
||||
record = {
|
||||
"signature": signature,
|
||||
"favorite": self.updated(json.loads(row[0]), value),
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO automation_records VALUES (?,?,?,?)",
|
||||
(
|
||||
"favorite_run",
|
||||
value.request_id,
|
||||
json.dumps(record, ensure_ascii=False),
|
||||
time.time(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
favorite = record["favorite"]
|
||||
snapshot = favorite["snapshot"]
|
||||
run_id = str(uuid5(NAMESPACE_URL, "artemis-favorite:" + value.request_id))
|
||||
if favorite["kind"] == "batch":
|
||||
from apps.admin_console.services.automation_service import create_batch, batch_action
|
||||
|
||||
batch = create_batch(
|
||||
{**snapshot, "name": favorite["name"], "request_id": "favorite-" + run_id}
|
||||
)
|
||||
if batch["status"] == "draft":
|
||||
batch = batch_action(batch["id"], "resume")
|
||||
result = {"kind": "batch", "batch_id": batch["id"], "status": batch["status"]}
|
||||
elif favorite["kind"] == "exploration":
|
||||
from apps.admin_console.services.exploration_service import exploration_service
|
||||
from artemis.exploration.models import CreateExploration
|
||||
|
||||
session = exploration_service.create(CreateExploration(request_id=run_id, **snapshot))
|
||||
admission = await exploration_service.start(session["id"], run_id)
|
||||
run = admission.get("run", admission)
|
||||
rejection = admission.get("admission", {})
|
||||
outcome = run.get("outcome") or {}
|
||||
if (
|
||||
rejection.get("status") in {"error", "rejected"}
|
||||
or outcome.get("status") == "rejected"
|
||||
):
|
||||
raise ValueError(
|
||||
rejection.get("error")
|
||||
or rejection.get("message")
|
||||
or outcome.get("error")
|
||||
or "探索未入队"
|
||||
)
|
||||
result = {
|
||||
"kind": "exploration",
|
||||
"session_id": run["id"],
|
||||
"exploration_id": session["id"],
|
||||
"admission": admission,
|
||||
}
|
||||
else:
|
||||
from apps.admin_console.routers.tasks import run_task
|
||||
from apps.admin_console.schemas.task_schema import RunRequest
|
||||
|
||||
admission = await run_task(
|
||||
RunRequest(
|
||||
goal=snapshot["goal"],
|
||||
profile=snapshot["runtime_overrides"].get("profile", "flash"),
|
||||
runtime_overrides=TaskOptions.model_validate(snapshot["runtime_overrides"]),
|
||||
session_id=run_id,
|
||||
ingress="favorite",
|
||||
expected_output=snapshot.get("expected_output"),
|
||||
enable_outputter=snapshot.get("enable_outputter"),
|
||||
app_path=snapshot.get("app_path"),
|
||||
)
|
||||
)
|
||||
if not admission.get("tasks"):
|
||||
raise ValueError(admission.get("error") or admission.get("message") or "任务未入队")
|
||||
result = {"kind": "task", "session_id": run_id, "admission": admission}
|
||||
record["result"] = result
|
||||
self.store.put("favorite_run", value.request_id, record)
|
||||
return result
|
||||
|
||||
|
||||
favorites = FavoriteService()
|
||||
Reference in new issue
Block a user