feat: release 1.5.1 with human review and batch filters
This commit is contained in:
1 parent
53c5680260
commit
bdd1aa2c14
36 files changed
+672
-57
No files matched your search
@@ -13,7 +13,7 @@ from apps.admin_console.services import automation_service as service
|
||||
from apps.admin_console.services import batch_archive as archive_service
|
||||
from apps.admin_console.services import report_export, task_import
|
||||
from artemis.config.settings import settings
|
||||
from artemis.runtime.case_result import read_case_result
|
||||
from apps.admin_console.services.case_review import read_reviewed_result
|
||||
|
||||
router = APIRouter(prefix="/api/automation", tags=["automation"])
|
||||
|
||||
@@ -24,6 +24,12 @@ class ReportPreferences(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class CaseReviewRequest(BaseModel):
|
||||
model_config = {"extra": "forbid"}
|
||||
status: Literal["passed", "failed"]
|
||||
reason: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class BatchRequest(ReportPreferences):
|
||||
request_id: str | None = Field(default=None, pattern=r"^[A-Za-z0-9_-]{1,100}$")
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
@@ -125,8 +131,8 @@ def batch_by_request(request_id: str):
|
||||
def batch_detail(batch_id: str):
|
||||
jobs = store.jobs(batch_id)
|
||||
for job in jobs:
|
||||
job["test_result"] = read_case_result(
|
||||
job["session_id"], job["status"], job.get("error") or ""
|
||||
job["test_result"] = read_reviewed_result(
|
||||
job["session_id"], job["status"], job.get("error") or "", storage=store
|
||||
)
|
||||
return {**require("batch", batch_id), "jobs": jobs}
|
||||
|
||||
@@ -207,6 +213,42 @@ async def download_report(
|
||||
raise HTTPException(404, str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/reports/{session_id}/review")
|
||||
def review_case(session_id: str, value: CaseReviewRequest):
|
||||
report(session_id)
|
||||
job = store.job(session_id) or {}
|
||||
batch_id = job.get("batch_id")
|
||||
if (
|
||||
batch_id
|
||||
and archive_service.status(batch_id, require("batch", batch_id))["status"] == "preparing"
|
||||
):
|
||||
raise HTTPException(409, "批次报告正在打包,请完成后再保存人工复验")
|
||||
with report_export.EXPORT_LOCK:
|
||||
current = report(session_id)
|
||||
if current["status"] not in {
|
||||
"completed",
|
||||
"success",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"interrupted",
|
||||
"skipped",
|
||||
}:
|
||||
raise HTTPException(409, "请在任务结束后进行人工复验")
|
||||
try:
|
||||
store.save_case_review(session_id, value.status, value.reason)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(409, str(exc)) from exc
|
||||
if batch_id:
|
||||
archive_service.forget(batch_id)
|
||||
# A finishing worker can publish report_ready just after export;
|
||||
# removing only the generated ZIP also prevents that stale cache hit.
|
||||
try:
|
||||
archive_service.archive_path(batch_id).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass # An open download may hold the file; report_ready is already invalidated.
|
||||
return report(session_id)
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
def history(
|
||||
q: str = Query(default="", max_length=200),
|
||||
@@ -215,6 +257,7 @@ def history(
|
||||
end: float | None = None,
|
||||
page: int = Query(default=1, ge=1),
|
||||
size: int = Query(default=30, ge=1, le=100),
|
||||
batch_id: str = Query(default="", max_length=100),
|
||||
):
|
||||
# SQL filtering/pagination searches the entire persisted history, not a UI cache.
|
||||
query = """WITH history AS (
|
||||
@@ -226,7 +269,7 @@ def history(
|
||||
COALESCE(json_extract(j.payload,'$.case.category'),'') category,
|
||||
COALESCE(json_extract(j.payload,'$.case.subcategory'),'') subcategory,
|
||||
COALESCE(json_extract(j.payload,'$.case.preconditions'),'') preconditions,
|
||||
COALESCE(json_extract(j.payload,'$.error'),'') error
|
||||
COALESCE(json_extract(j.payload,'$.error'),'') error, j.batch_id
|
||||
FROM sessions s LEFT JOIN automation_jobs j ON s.session_id=j.session_id
|
||||
UNION ALL
|
||||
SELECT j.session_id,json_extract(j.payload,'$.case.goal'),
|
||||
@@ -237,11 +280,22 @@ def history(
|
||||
COALESCE(json_extract(j.payload,'$.case.category'),''),
|
||||
COALESCE(json_extract(j.payload,'$.case.subcategory'),''),
|
||||
COALESCE(json_extract(j.payload,'$.case.preconditions'),''),
|
||||
COALESCE(json_extract(j.payload,'$.error'),'')
|
||||
COALESCE(json_extract(j.payload,'$.error'),''), j.batch_id
|
||||
FROM automation_jobs j WHERE NOT EXISTS (SELECT 1 FROM sessions s WHERE s.session_id=j.session_id)
|
||||
) """
|
||||
where = " WHERE status IN ('completed','success','failed','cancelled','interrupted','skipped')"
|
||||
args = []
|
||||
if batch_id == "__batch__":
|
||||
where += " AND batch_id IS NOT NULL"
|
||||
elif batch_id == "__single__":
|
||||
where += " AND batch_id IS NULL"
|
||||
elif batch_id:
|
||||
try:
|
||||
identifier(batch_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
where += " AND batch_id = ?"
|
||||
args.append(batch_id)
|
||||
if q:
|
||||
where += " AND (name LIKE ? ESCAPE '\\' OR initial_goal LIKE ? ESCAPE '\\' OR session_id LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\' OR case_id LIKE ? ESCAPE '\\' OR category LIKE ? ESCAPE '\\' OR subcategory LIKE ? ESCAPE '\\' OR preconditions LIKE ? ESCAPE '\\')"
|
||||
escaped = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
||||
@@ -266,8 +320,8 @@ def history(
|
||||
items = [dict(row) for row in rows]
|
||||
jobs = store.jobs_for_sessions([item["session_id"] for item in items])
|
||||
for item in items:
|
||||
item["test_result"] = read_case_result(
|
||||
item["session_id"], item["status"], item.get("error") or ""
|
||||
item["test_result"] = read_reviewed_result(
|
||||
item["session_id"], item["status"], item.get("error") or "", storage=store
|
||||
)
|
||||
if job := jobs.get(item["session_id"]):
|
||||
item["name"] = task_import.execution_title(job)
|
||||
|
||||
@@ -20,7 +20,7 @@ from fastapi import APIRouter, HTTPException
|
||||
|
||||
from artemis.config import DB_PATH, TRACES_PATH
|
||||
from artemis.runtime import trace_store
|
||||
from artemis.runtime.case_result import read_case_result
|
||||
from apps.admin_console.services.case_review import read_reviewed_result
|
||||
|
||||
try:
|
||||
from admin_console.core.state import state
|
||||
@@ -83,6 +83,7 @@ def _list_sessions_sync():
|
||||
job = jobs_by_id.get(s_id)
|
||||
if job:
|
||||
case = job.get("case") or {}
|
||||
row_dict["batch_id"] = job.get("batch_id")
|
||||
row_dict["case_id"] = case.get("case_id") or ""
|
||||
row_dict["display_name"] = execution_title(job) or row_dict.get("initial_goal")
|
||||
row_dict["iteration"] = job.get("iteration", 1)
|
||||
@@ -131,8 +132,12 @@ def _list_sessions_sync():
|
||||
else:
|
||||
row_dict["video_url"] = None
|
||||
row_dict["recording_status"] = recording_status
|
||||
row_dict["test_result"] = read_case_result(
|
||||
s_id, row_dict.get("status") or "", row_dict.get("error") or "", root=TRACES_PATH
|
||||
row_dict["test_result"] = read_reviewed_result(
|
||||
s_id,
|
||||
row_dict.get("status") or "",
|
||||
row_dict.get("error") or "",
|
||||
root=TRACES_PATH,
|
||||
storage=store,
|
||||
)
|
||||
|
||||
agent_names = agent_names_by_session.get(s_id, [])
|
||||
@@ -194,8 +199,10 @@ async def get_session_details(session_id: str):
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
|
||||
result = dict(row)
|
||||
result["test_result"] = read_case_result(
|
||||
session_id, result.get("status") or "", root=TRACES_PATH
|
||||
job = store.job(session_id) or {}
|
||||
result["batch_id"] = job.get("batch_id")
|
||||
result["test_result"] = read_reviewed_result(
|
||||
session_id, result.get("status") or "", root=TRACES_PATH, storage=store
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from artemis.core.diagnostics import readiness_engine
|
||||
from apps.admin_console.services.automation_store import store
|
||||
from apps.admin_console.services.task_import import execution_title
|
||||
from artemis.runtime import DeviceExecutionLock, device_pool
|
||||
from artemis.runtime.task_control import (
|
||||
add_instruction,
|
||||
@@ -423,6 +425,15 @@ async def get_status():
|
||||
seen_ids.add(sid)
|
||||
queue_data.append(g_item)
|
||||
|
||||
batch_jobs = store.jobs_for_sessions(
|
||||
[str(item["session_id"]) for item in [*queue_data, *active_tasks] if item.get("session_id")]
|
||||
)
|
||||
queue_data = [{**item} for item in queue_data]
|
||||
for item in [*queue_data, *active_tasks]:
|
||||
if job := batch_jobs.get(str(item.get("session_id"))):
|
||||
item["batch_id"] = job.get("batch_id")
|
||||
item["display_name"] = execution_title(job)
|
||||
|
||||
if is_running:
|
||||
is_paused = state.is_paused
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user