generated from coulomb/repo-seed
Validate structured agentic review decisions
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
from repo_registry.acceptance.agentic import AgenticReviewer, AgenticReviewRequest
|
||||
from repo_registry.acceptance.agentic import (
|
||||
AgenticReviewer,
|
||||
AgenticReviewDecision,
|
||||
AgenticReviewRequest,
|
||||
validate_agentic_review_decision,
|
||||
validate_agentic_review_decisions,
|
||||
)
|
||||
from repo_registry.acceptance.criteria import (
|
||||
active_quality_criteria_version,
|
||||
criteria_registry_dict,
|
||||
@@ -15,6 +21,7 @@ from repo_registry.acceptance.gates import (
|
||||
|
||||
__all__ = [
|
||||
"active_quality_criteria_version",
|
||||
"AgenticReviewDecision",
|
||||
"AgenticReviewer",
|
||||
"AgenticReviewRequest",
|
||||
"blocking_quality_gate_outcomes",
|
||||
@@ -25,4 +32,6 @@ __all__ = [
|
||||
"evaluate_candidate_graph_quality",
|
||||
"load_quality_criteria",
|
||||
"quality_gate_outcome_dicts",
|
||||
"validate_agentic_review_decision",
|
||||
"validate_agentic_review_decisions",
|
||||
]
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Protocol
|
||||
|
||||
from repo_registry.acceptance.gates import QualityGateOutcome
|
||||
from repo_registry.core.models import CandidateGraph, Repository
|
||||
|
||||
AGENTIC_REVIEW_ACTIONS = {
|
||||
"approve",
|
||||
"approve_with_edits",
|
||||
"reject",
|
||||
"downgrade",
|
||||
"request_human_review",
|
||||
"propose_edit",
|
||||
"relink",
|
||||
}
|
||||
AGENTIC_APPROVAL_ACTIONS = {"approve", "approve_with_edits"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgenticReviewRequest:
|
||||
@@ -16,9 +28,46 @@ class AgenticReviewRequest:
|
||||
context: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgenticReviewDecision:
|
||||
action: str
|
||||
target_type: str
|
||||
target_id: int
|
||||
rationale: str
|
||||
criterion_ids: list[str]
|
||||
evidence_refs: list[str]
|
||||
notes: str = ""
|
||||
proposed_changes: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AgenticReviewer(Protocol):
|
||||
reviewer_id: str
|
||||
policy_version: str
|
||||
|
||||
def review(self, request: AgenticReviewRequest) -> None:
|
||||
"""Review a candidate graph and record decisions through the caller."""
|
||||
def review(self, request: AgenticReviewRequest) -> list[AgenticReviewDecision]:
|
||||
"""Review a candidate graph and return structured decisions."""
|
||||
|
||||
|
||||
def validate_agentic_review_decision(decision: AgenticReviewDecision) -> None:
|
||||
if decision.action not in AGENTIC_REVIEW_ACTIONS:
|
||||
raise ValueError(f"unsupported agentic review action: {decision.action}")
|
||||
if not decision.target_type:
|
||||
raise ValueError("agentic review decision target_type is required")
|
||||
if decision.target_id < 0:
|
||||
raise ValueError("agentic review decision target_id must be non-negative")
|
||||
if not decision.rationale.strip():
|
||||
raise ValueError("agentic review decision rationale is required")
|
||||
if not decision.criterion_ids:
|
||||
raise ValueError("agentic review decision criterion_ids are required")
|
||||
if decision.action in AGENTIC_APPROVAL_ACTIONS and not decision.evidence_refs:
|
||||
raise ValueError(
|
||||
"agentic approval requires evidence refs tied to the rationale"
|
||||
)
|
||||
|
||||
|
||||
def validate_agentic_review_decisions(
|
||||
decisions: list[AgenticReviewDecision],
|
||||
) -> list[AgenticReviewDecision]:
|
||||
for decision in decisions:
|
||||
validate_agentic_review_decision(decision)
|
||||
return decisions
|
||||
|
||||
@@ -5,12 +5,14 @@ from dataclasses import asdict, replace
|
||||
from typing import Any
|
||||
|
||||
from repo_registry.acceptance import (
|
||||
AgenticReviewDecision,
|
||||
AgenticReviewer,
|
||||
AgenticReviewRequest,
|
||||
active_quality_criteria_version,
|
||||
blocking_quality_gate_outcomes,
|
||||
evaluate_candidate_capability_quality,
|
||||
evaluate_candidate_graph_quality,
|
||||
validate_agentic_review_decisions,
|
||||
)
|
||||
from repo_registry.core.models import (
|
||||
AbilitySummary,
|
||||
@@ -646,7 +648,18 @@ class RegistryService:
|
||||
quality_gate_outcomes=gate_outcomes,
|
||||
context="candidate-characteristic-acceptance",
|
||||
)
|
||||
self.agentic_reviewer.review(request)
|
||||
decisions = validate_agentic_review_decisions(
|
||||
self.agentic_reviewer.review(request) or []
|
||||
)
|
||||
for decision in decisions:
|
||||
self._apply_agentic_review_decision(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision,
|
||||
reviewer_id=self.agentic_reviewer.reviewer_id,
|
||||
policy_version=self.agentic_reviewer.policy_version,
|
||||
criteria_version=criteria_version,
|
||||
)
|
||||
self.store.create_review_decision(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
@@ -655,11 +668,170 @@ class RegistryService:
|
||||
f"{notes} reviewer={self.agentic_reviewer.reviewer_id}; "
|
||||
f"policy_version={self.agentic_reviewer.policy_version}; "
|
||||
f"criteria_version={criteria_version}; "
|
||||
f"quality_gate_outcomes={len(gate_outcomes)}."
|
||||
f"quality_gate_outcomes={len(gate_outcomes)}; "
|
||||
f"decisions={len(decisions)}."
|
||||
).strip(),
|
||||
)
|
||||
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||
|
||||
def _apply_agentic_review_decision(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
decision: AgenticReviewDecision,
|
||||
*,
|
||||
reviewer_id: str,
|
||||
policy_version: str,
|
||||
criteria_version: str,
|
||||
) -> None:
|
||||
notes = self._agentic_review_decision_notes(
|
||||
decision,
|
||||
reviewer_id=reviewer_id,
|
||||
policy_version=policy_version,
|
||||
criteria_version=criteria_version,
|
||||
)
|
||||
if decision.action in {"approve", "approve_with_edits"}:
|
||||
self._apply_agentic_approval(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision,
|
||||
notes,
|
||||
)
|
||||
return
|
||||
if decision.action == "reject":
|
||||
self._apply_agentic_rejection(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision,
|
||||
notes,
|
||||
)
|
||||
return
|
||||
self.store.create_review_decision(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
action=f"agentic_{decision.action}",
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
def _apply_agentic_approval(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
decision: AgenticReviewDecision,
|
||||
notes: str,
|
||||
) -> None:
|
||||
action = f"agentic_{decision.action}"
|
||||
if decision.target_type == "candidate_graph":
|
||||
self.approve_candidate_graph(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
notes=notes,
|
||||
action=f"{action}_candidate_graph",
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_ability":
|
||||
self.accept_candidate_ability(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_capability":
|
||||
self.accept_candidate_capability(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_feature":
|
||||
self.accept_candidate_feature(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_evidence":
|
||||
self.accept_candidate_evidence(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
raise ValueError(f"unsupported agentic approval target: {decision.target_type}")
|
||||
|
||||
def _apply_agentic_rejection(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
decision: AgenticReviewDecision,
|
||||
notes: str,
|
||||
) -> None:
|
||||
if decision.target_type == "candidate_ability":
|
||||
self.reject_candidate_ability(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_capability":
|
||||
self.reject_candidate_capability(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_feature":
|
||||
self.reject_candidate_feature(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
if decision.target_type == "candidate_evidence":
|
||||
self.reject_candidate_evidence(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
decision.target_id,
|
||||
notes=notes,
|
||||
)
|
||||
return
|
||||
self.store.create_review_decision(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
action="agentic_reject",
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
def _agentic_review_decision_notes(
|
||||
self,
|
||||
decision: AgenticReviewDecision,
|
||||
*,
|
||||
reviewer_id: str,
|
||||
policy_version: str,
|
||||
criteria_version: str,
|
||||
) -> str:
|
||||
evidence = ", ".join(decision.evidence_refs) or "none"
|
||||
criteria = ", ".join(decision.criterion_ids)
|
||||
notes = (
|
||||
f"reviewer={reviewer_id}; policy_version={policy_version}; "
|
||||
f"criteria_version={criteria_version}; action={decision.action}; "
|
||||
f"target={decision.target_type}:{decision.target_id}; "
|
||||
f"criteria={criteria}; evidence={evidence}; "
|
||||
f"rationale={decision.rationale.strip()}"
|
||||
)
|
||||
if decision.proposed_changes:
|
||||
notes += f"; proposed_changes={decision.proposed_changes}"
|
||||
if decision.notes.strip():
|
||||
notes += f"; notes={decision.notes.strip()}"
|
||||
return notes
|
||||
|
||||
def _trusted_auto_approve_capability_safe(
|
||||
self,
|
||||
capability: CandidateCapability,
|
||||
|
||||
Reference in New Issue
Block a user