Add required human review gate to the Agent pipeline.
Agent web jobs now stop after Brain consolidation and enter needs_review with a persisted review queue (blocking: high-severity, low-confidence, sensitive-category findings; audit sample of clean clusters). Humans decide confirm/reject/unsure/needs_clarification via new review API and frontend queue; a finalizer applies decisions (rejections suppressed with reason codes), performs bounded targeted reruns for clarifications, drafts RFIs only for kept issues, and only then marks the job done and sends the final email. Two-phase email (review-required, then final report), per-decision feedback labels with redacted aggregate metrics, restart recovery from job artifacts, and CLI --no-review bypass. Classic pipeline unchanged. 65 non-LLM tests.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""Persistence for human-review queue and decisions within a job output dir."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
from backend.review.schemas import validate_decision
|
||||
|
||||
|
||||
class ReviewStore:
|
||||
def __init__(self, job_out_dir: str, create: bool = True) -> None:
|
||||
self.review_dir = os.path.join(job_out_dir, "review")
|
||||
if create:
|
||||
os.makedirs(self.review_dir, exist_ok=True)
|
||||
|
||||
def _path(self, name: str) -> str:
|
||||
return os.path.join(self.review_dir, name)
|
||||
|
||||
def _write_json(self, name: str, value) -> None:
|
||||
path = self._path(name)
|
||||
tmp = f"{path}.tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(value, f, indent=2)
|
||||
os.replace(tmp, path)
|
||||
|
||||
def write_queue(self, queue: List[dict]) -> None:
|
||||
self._write_json("review_queue.json", queue)
|
||||
|
||||
def read_queue(self) -> List[dict]:
|
||||
try:
|
||||
with open(self._path("review_queue.json"), encoding="utf-8") as f:
|
||||
value = json.load(f)
|
||||
return value if isinstance(value, list) else []
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
def append_decision(self, decision: dict) -> None:
|
||||
valid = validate_decision(decision)
|
||||
if not valid or not valid["review_item_id"]:
|
||||
raise ValueError("invalid review decision")
|
||||
decisions = self.read_decisions()
|
||||
decisions[valid["review_item_id"]] = valid
|
||||
self._write_json("review_decisions.json", decisions)
|
||||
|
||||
def read_decisions(self) -> Dict[str, dict]:
|
||||
try:
|
||||
with open(self._path("review_decisions.json"), encoding="utf-8") as f:
|
||||
value = json.load(f)
|
||||
return value if isinstance(value, dict) else {}
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
def progress(self, queue: List[dict]) -> dict:
|
||||
decisions = self.read_decisions()
|
||||
required = [item for item in queue if item.get("blocking")]
|
||||
completed = [item for item in required if item.get("review_item_id") in decisions]
|
||||
return {
|
||||
"required": len(required),
|
||||
"completed": len(completed),
|
||||
"remaining": len(required) - len(completed),
|
||||
"total": len(queue),
|
||||
}
|
||||
Reference in New Issue
Block a user