|
|
@@ -231,6 +231,7 @@ class ScopeValidationResult(_StrictModel):
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
def validate_outcome(self) -> "ScopeValidationResult":
|
|
|
+ _require_scope_outcome_matches_checks(self.outcome, self.checks)
|
|
|
if self.outcome == "passed" and self.retry_from is not None:
|
|
|
raise ValueError("passed requires retry_from=null")
|
|
|
if self.outcome in {"failed", "unknown"} and self.retry_from is None:
|
|
|
@@ -240,6 +241,34 @@ class ScopeValidationResult(_StrictModel):
|
|
|
return self
|
|
|
|
|
|
|
|
|
+def _require_scope_outcome_matches_checks(
|
|
|
+ outcome: ValidationOutcome,
|
|
|
+ checks: Sequence[ValidationCheck],
|
|
|
+) -> None:
|
|
|
+ """防止 Scope 总结论与逐项状态相互矛盾。"""
|
|
|
+ statuses = {check.status for check in checks}
|
|
|
+ if not statuses:
|
|
|
+ raise ValueError("scope result must contain at least one check")
|
|
|
+ if outcome == "passed":
|
|
|
+ if statuses != {"passed"}:
|
|
|
+ raise ValueError("passed scope requires every check to pass")
|
|
|
+ return
|
|
|
+ if outcome == "failed":
|
|
|
+ if "failed" not in statuses:
|
|
|
+ raise ValueError("failed scope requires at least one failed check")
|
|
|
+ return
|
|
|
+ if outcome == "unknown":
|
|
|
+ if "failed" in statuses or "unknown" not in statuses:
|
|
|
+ raise ValueError(
|
|
|
+ "unknown scope requires an unknown check and no failed checks"
|
|
|
+ )
|
|
|
+ return
|
|
|
+ if "failed" in statuses or "unknown" not in statuses:
|
|
|
+ raise ValueError(
|
|
|
+ "error scope requires an unknown check and no failed checks"
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
class ValidationResult(_StrictModel):
|
|
|
"""所有有效 Scope 的聚合结果;直接父级只审核这一份。"""
|
|
|
|
|
|
@@ -277,7 +306,7 @@ class ValidationResult(_StrictModel):
|
|
|
class ValidationPolicy(_StrictModel):
|
|
|
"""由可信应用注入并在根 Trace 固化的最低验收规则。"""
|
|
|
|
|
|
- policy_version: str = "recursive-validator-2.1"
|
|
|
+ policy_version: str = "recursive-validator-2.2"
|
|
|
global_rules: str = _GLOBAL_RULES
|
|
|
rubrics: dict[ValidationScope, list[str]] = Field(
|
|
|
default_factory=lambda: {
|
|
|
@@ -360,14 +389,14 @@ class ValidationPolicy(_StrictModel):
|
|
|
check_id=f"task.criterion.{index}",
|
|
|
scope=scope,
|
|
|
criterion=str(criterion),
|
|
|
- method="deterministic_and_llm",
|
|
|
+ method="llm",
|
|
|
))
|
|
|
for index, expected in enumerate(brief.get("expected_outputs", []), start=1):
|
|
|
checks.append(ValidationCheckSpec(
|
|
|
check_id=f"task.expected_output.{index}",
|
|
|
scope=scope,
|
|
|
criterion=f"Expected output is actually delivered: {expected}",
|
|
|
- method="deterministic_and_llm",
|
|
|
+ method="llm",
|
|
|
))
|
|
|
if scope == "root":
|
|
|
for index, criterion in enumerate(anchor.get("completion_criteria", []), start=1):
|
|
|
@@ -375,14 +404,14 @@ class ValidationPolicy(_StrictModel):
|
|
|
check_id=f"root.criterion.{index}",
|
|
|
scope=scope,
|
|
|
criterion=str(criterion),
|
|
|
- method="deterministic_and_llm",
|
|
|
+ method="llm",
|
|
|
))
|
|
|
for index, constraint in enumerate(anchor.get("constraints", []), start=1):
|
|
|
checks.append(ValidationCheckSpec(
|
|
|
check_id=f"root.constraint.{index}",
|
|
|
scope=scope,
|
|
|
criterion=f"Global constraint is satisfied: {constraint}",
|
|
|
- method="deterministic_and_llm",
|
|
|
+ method="llm",
|
|
|
))
|
|
|
scoped_issues = [
|
|
|
issue for issue in material_issues if scope in issue.scopes
|
|
|
@@ -682,7 +711,7 @@ def parse_scope_validation_result(
|
|
|
raise ValueError(
|
|
|
f"{expected_scope} non-pass must use retry_from={expected_retry}"
|
|
|
)
|
|
|
- return ScopeValidationResult(
|
|
|
+ result = ScopeValidationResult(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=expected_scope,
|
|
|
outcome=decision.outcome,
|
|
|
@@ -691,24 +720,116 @@ def parse_scope_validation_result(
|
|
|
retry_from=decision.retry_from,
|
|
|
plan_hash=plan.plan_hash,
|
|
|
)
|
|
|
+ return _require_scope_result_integrity(
|
|
|
+ result,
|
|
|
+ plan=plan,
|
|
|
+ expected_scope=expected_scope,
|
|
|
+ )
|
|
|
|
|
|
|
|
|
-def scope_validation_error(
|
|
|
+def _require_scope_result_integrity(
|
|
|
+ result: ScopeValidationResult,
|
|
|
+ *,
|
|
|
+ plan: ValidationPlan,
|
|
|
+ expected_scope: ValidationScope,
|
|
|
+) -> ScopeValidationResult:
|
|
|
+ """对 LLM、框架生成和断点恢复结果执行同一份 Plan 约束。"""
|
|
|
+ if result.scope != expected_scope:
|
|
|
+ raise ValueError(
|
|
|
+ f"scope result has scope {result.scope!r}, expected {expected_scope!r}"
|
|
|
+ )
|
|
|
+ if result.plan_hash != plan.plan_hash:
|
|
|
+ raise ValueError("scope result plan_hash does not match validation plan")
|
|
|
+ _require_scope_outcome_matches_checks(result.outcome, result.checks)
|
|
|
+
|
|
|
+ planned_ids = [item.check_id for item in plan.checks_for_scope(expected_scope)]
|
|
|
+ returned_ids = [item.check_id for item in result.checks]
|
|
|
+ if len(returned_ids) != len(set(returned_ids)):
|
|
|
+ raise ValueError("scope result contains duplicate check_id values")
|
|
|
+ extras = set(returned_ids) - set(planned_ids)
|
|
|
+ missing = set(planned_ids) - set(returned_ids)
|
|
|
+ if extras or missing:
|
|
|
+ raise ValueError(
|
|
|
+ "scope result checks do not match plan: "
|
|
|
+ f"extras={sorted(extras)}, missing={sorted(missing)}"
|
|
|
+ )
|
|
|
+ by_id = {item.check_id: item for item in result.checks}
|
|
|
+ required_statuses = {
|
|
|
+ by_id[item.check_id].status
|
|
|
+ for item in plan.checks_for_scope(expected_scope)
|
|
|
+ if item.required
|
|
|
+ }
|
|
|
+ if result.outcome == "passed" and required_statuses != {"passed"}:
|
|
|
+ raise ValueError("passed scope requires every required plan check to pass")
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
+def _framework_scope_result(
|
|
|
*,
|
|
|
validator_trace_id: str,
|
|
|
scope: ValidationScope,
|
|
|
- plan_hash: str,
|
|
|
+ plan: ValidationPlan,
|
|
|
+ outcome: Literal["failed", "unknown", "error"],
|
|
|
reason: str,
|
|
|
+ primary_check_id: str | None = None,
|
|
|
) -> ScopeValidationResult:
|
|
|
+ """用 Plan 内的全部 check 表达框架失败,不制造计划外 ID。"""
|
|
|
detail = reason.strip() or "Validator failed without an error description"
|
|
|
- return ScopeValidationResult(
|
|
|
+ planned = plan.checks_for_scope(scope)
|
|
|
+ planned_ids = {item.check_id for item in planned}
|
|
|
+ if not planned:
|
|
|
+ raise ValueError(f"validation plan has no checks for scope {scope!r}")
|
|
|
+ if primary_check_id is not None and primary_check_id not in planned_ids:
|
|
|
+ raise ValueError(
|
|
|
+ f"primary check {primary_check_id!r} is not planned for scope {scope!r}"
|
|
|
+ )
|
|
|
+ primary = primary_check_id or planned[0].check_id
|
|
|
+ checks: list[ValidationCheck] = []
|
|
|
+ for spec in planned:
|
|
|
+ is_primary = spec.check_id == primary
|
|
|
+ status: CheckStatus = (
|
|
|
+ "failed" if outcome == "failed" and is_primary else "unknown"
|
|
|
+ )
|
|
|
+ if is_primary:
|
|
|
+ issue = detail
|
|
|
+ elif outcome == "error":
|
|
|
+ issue = f"Not evaluated because Validator execution failed: {detail}"
|
|
|
+ else:
|
|
|
+ issue = f"Not evaluated because {primary} blocked this scope: {detail}"
|
|
|
+ checks.append(ValidationCheck(
|
|
|
+ check_id=spec.check_id,
|
|
|
+ status=status,
|
|
|
+ issue=issue,
|
|
|
+ ))
|
|
|
+ result = ScopeValidationResult(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=scope,
|
|
|
- outcome="error",
|
|
|
- checks=[],
|
|
|
+ outcome=outcome,
|
|
|
+ checks=checks,
|
|
|
reason=detail,
|
|
|
- retry_from=None,
|
|
|
- plan_hash=plan_hash,
|
|
|
+ retry_from=None if outcome == "error" else _SCOPE_RETRY[scope],
|
|
|
+ plan_hash=plan.plan_hash,
|
|
|
+ )
|
|
|
+ return _require_scope_result_integrity(
|
|
|
+ result,
|
|
|
+ plan=plan,
|
|
|
+ expected_scope=scope,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def scope_validation_error(
|
|
|
+ *,
|
|
|
+ validator_trace_id: str,
|
|
|
+ scope: ValidationScope,
|
|
|
+ plan: ValidationPlan,
|
|
|
+ reason: str,
|
|
|
+) -> ScopeValidationResult:
|
|
|
+ return _framework_scope_result(
|
|
|
+ validator_trace_id=validator_trace_id,
|
|
|
+ scope=scope,
|
|
|
+ plan=plan,
|
|
|
+ outcome="error",
|
|
|
+ reason=reason,
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -721,6 +842,12 @@ def aggregate_validation_results(
|
|
|
"""按固定优先级聚合Scope结果,并选择最早可靠回退层。"""
|
|
|
if [item.scope for item in scope_results] != plan.effective_scopes:
|
|
|
raise ValueError("scope results must match plan order exactly")
|
|
|
+ for scope, result in zip(plan.effective_scopes, scope_results):
|
|
|
+ _require_scope_result_integrity(
|
|
|
+ result,
|
|
|
+ plan=plan,
|
|
|
+ expected_scope=scope,
|
|
|
+ )
|
|
|
outcome = max(
|
|
|
(item.outcome for item in scope_results),
|
|
|
key=lambda item: _OUTCOME_PRIORITY[item],
|
|
|
@@ -729,11 +856,7 @@ def aggregate_validation_results(
|
|
|
check.issue or result.reason
|
|
|
for result in scope_results
|
|
|
if result.outcome != "passed"
|
|
|
- for check in (result.checks or [ValidationCheck(
|
|
|
- check_id="framework.error",
|
|
|
- status="failed" if result.outcome == "failed" else "unknown",
|
|
|
- issue=result.reason,
|
|
|
- )])
|
|
|
+ for check in result.checks
|
|
|
if check.status != "passed"
|
|
|
]
|
|
|
issues = list(dict.fromkeys(item for item in issues if item))
|
|
|
@@ -760,14 +883,13 @@ def validation_error(
|
|
|
evaluated_trace_id: str,
|
|
|
scope: ValidationScope,
|
|
|
reason: str,
|
|
|
- plan_hash: str | None = None,
|
|
|
+ plan: ValidationPlan,
|
|
|
) -> ValidationResult:
|
|
|
"""兼容入口:把一个Scope错误包装为一份聚合失败关闭结果。"""
|
|
|
- resolved_hash = plan_hash or ("0" * 64)
|
|
|
scope_result = scope_validation_error(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=scope,
|
|
|
- plan_hash=resolved_hash,
|
|
|
+ plan=plan,
|
|
|
reason=reason,
|
|
|
)
|
|
|
return ValidationResult(
|
|
|
@@ -776,7 +898,7 @@ def validation_error(
|
|
|
scope_results=[scope_result],
|
|
|
issues=[scope_result.reason],
|
|
|
retry_from=None,
|
|
|
- plan_hash=resolved_hash,
|
|
|
+ plan_hash=plan.plan_hash,
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -826,11 +948,15 @@ class LLMValidator:
|
|
|
"""顺序执行所有Scope;每完成一项即可回调持久化断点。"""
|
|
|
started = time.monotonic()
|
|
|
runs: list[_ScopeRun] = []
|
|
|
- resumed = {
|
|
|
- item.scope: item
|
|
|
- for item in resume_scope_results
|
|
|
- if item.plan_hash == plan.plan_hash
|
|
|
- }
|
|
|
+ resumed: dict[ValidationScope, ScopeValidationResult] = {}
|
|
|
+ for item in resume_scope_results:
|
|
|
+ if item.plan_hash != plan.plan_hash:
|
|
|
+ continue
|
|
|
+ resumed[item.scope] = _require_scope_result_integrity(
|
|
|
+ item,
|
|
|
+ plan=plan,
|
|
|
+ expected_scope=item.scope,
|
|
|
+ )
|
|
|
for scope in plan.effective_scopes:
|
|
|
if scope in resumed:
|
|
|
result = resumed[scope]
|
|
|
@@ -1072,7 +1198,7 @@ class LLMValidator:
|
|
|
result = scope_validation_error(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=scope,
|
|
|
- plan_hash=plan.plan_hash,
|
|
|
+ plan=plan,
|
|
|
reason=f"Validator failed: {exc}",
|
|
|
)
|
|
|
await self._finish_trace(
|
|
|
@@ -1114,7 +1240,7 @@ class LLMValidator:
|
|
|
result = scope_validation_error(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=scope,
|
|
|
- plan_hash=plan.plan_hash,
|
|
|
+ plan=plan,
|
|
|
reason=reason,
|
|
|
)
|
|
|
else:
|
|
|
@@ -1125,22 +1251,15 @@ class LLMValidator:
|
|
|
),
|
|
|
None,
|
|
|
)
|
|
|
- check = ValidationCheck(
|
|
|
- check_id=(
|
|
|
- deterministic_check.check_id
|
|
|
- if deterministic_check else f"{scope}.deterministic"
|
|
|
- ),
|
|
|
- status=outcome,
|
|
|
- issue=reason,
|
|
|
- )
|
|
|
- result = ScopeValidationResult(
|
|
|
+ result = _framework_scope_result(
|
|
|
validator_trace_id=validator_trace_id,
|
|
|
scope=scope,
|
|
|
+ plan=plan,
|
|
|
outcome=outcome,
|
|
|
- checks=[check],
|
|
|
reason=reason,
|
|
|
- retry_from=_SCOPE_RETRY[scope],
|
|
|
- plan_hash=plan.plan_hash,
|
|
|
+ primary_check_id=(
|
|
|
+ deterministic_check.check_id if deterministic_check else None
|
|
|
+ ),
|
|
|
)
|
|
|
await self._store_terminal_message(trace, result)
|
|
|
return _ScopeRun(result=result, trace_id=validator_trace_id)
|
|
|
@@ -1152,18 +1271,13 @@ class LLMValidator:
|
|
|
scope: ValidationScope,
|
|
|
reason: str,
|
|
|
) -> _ScopeRun:
|
|
|
- result = ScopeValidationResult(
|
|
|
+ detail = f"Required validation material is unavailable: {reason}"
|
|
|
+ result = _framework_scope_result(
|
|
|
validator_trace_id=trace.trace_id,
|
|
|
scope=scope,
|
|
|
+ plan=plan,
|
|
|
outcome="unknown",
|
|
|
- checks=[ValidationCheck(
|
|
|
- check_id=f"{scope}.input_limit",
|
|
|
- status="unknown",
|
|
|
- issue=f"Required validation material is unavailable: {reason}",
|
|
|
- )],
|
|
|
- reason=f"Required validation material is unavailable: {reason}",
|
|
|
- retry_from=_SCOPE_RETRY[scope],
|
|
|
- plan_hash=plan.plan_hash,
|
|
|
+ reason=detail,
|
|
|
)
|
|
|
await self._store_terminal_message(trace, result)
|
|
|
return _ScopeRun(result=result, trace_id=trace.trace_id)
|