agentclaimguard 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. agentclaimguard/__init__.py +23 -0
  2. agentclaimguard/adapters/__init__.py +6 -0
  3. agentclaimguard/adapters/dify/__init__.py +2 -0
  4. agentclaimguard/adapters/dspy/__init__.py +2 -0
  5. agentclaimguard/adapters/langchain/__init__.py +18 -0
  6. agentclaimguard/adapters/langchain/runnable.py +126 -0
  7. agentclaimguard/adapters/langchain/types.py +8 -0
  8. agentclaimguard/adapters/langgraph/__init__.py +18 -0
  9. agentclaimguard/adapters/langgraph/node.py +78 -0
  10. agentclaimguard/adapters/langgraph/types.py +16 -0
  11. agentclaimguard/adapters/ragflow/__init__.py +2 -0
  12. agentclaimguard/core/__init__.py +23 -0
  13. agentclaimguard/core/claim.py +15 -0
  14. agentclaimguard/core/evidence.py +13 -0
  15. agentclaimguard/core/policy.py +83 -0
  16. agentclaimguard/core/result.py +41 -0
  17. agentclaimguard/core/runtime.py +54 -0
  18. agentclaimguard/core/tool_result.py +14 -0
  19. agentclaimguard/core/verifier.py +97 -0
  20. agentclaimguard/policies/generic_compliance.yaml +17 -0
  21. agentclaimguard/policies/generic_numeric.yaml +16 -0
  22. agentclaimguard/policies/generic_rag.yaml +14 -0
  23. agentclaimguard/policies/generic_strict.yaml +43 -0
  24. agentclaimguard/server/__init__.py +1 -0
  25. agentclaimguard/server/main.py +44 -0
  26. agentclaimguard/server/schemas.py +18 -0
  27. agentclaimguard/utils/__init__.py +1 -0
  28. agentclaimguard/utils/ids.py +6 -0
  29. agentclaimguard/utils/json_schema.py +16 -0
  30. agentclaimguard/utils/yaml_loader.py +13 -0
  31. agentclaimguard/validators/__init__.py +14 -0
  32. agentclaimguard/validators/citation_binding.py +41 -0
  33. agentclaimguard/validators/conflict_check.py +59 -0
  34. agentclaimguard/validators/evidence_required.py +37 -0
  35. agentclaimguard/validators/forbidden_verdict.py +93 -0
  36. agentclaimguard/validators/tool_required.py +59 -0
  37. agentclaimguard-0.3.1.dist-info/METADATA +254 -0
  38. agentclaimguard-0.3.1.dist-info/RECORD +41 -0
  39. agentclaimguard-0.3.1.dist-info/WHEEL +4 -0
  40. agentclaimguard-0.3.1.dist-info/licenses/LICENSE +201 -0
  41. agentclaimguard-0.3.1.dist-info/licenses/NOTICE +8 -0
@@ -0,0 +1,23 @@
1
+ from agentclaimguard.core.claim import Claim
2
+ from agentclaimguard.core.evidence import Evidence
3
+ from agentclaimguard.core.policy import Policy
4
+ from agentclaimguard.core.result import (
5
+ ClaimVerificationResult,
6
+ VerificationResult,
7
+ Violation,
8
+ )
9
+ from agentclaimguard.core.runtime import AgentClaimGuard, verify_claims
10
+ from agentclaimguard.core.tool_result import ToolResult
11
+
12
+ __all__ = [
13
+ "Claim",
14
+ "AgentClaimGuard",
15
+ "ClaimVerificationResult",
16
+ "Evidence",
17
+ "Policy",
18
+ "ToolResult",
19
+ "VerificationResult",
20
+ "Violation",
21
+ "verify_claims",
22
+ ]
23
+
@@ -0,0 +1,6 @@
1
+ """Framework adapter namespace.
2
+
3
+ The v0.1 release focuses on the core SDK and OpenAPI server. Framework-specific
4
+ adapters will live under this package as they are added.
5
+ """
6
+
@@ -0,0 +1,2 @@
1
+ """Dify adapter placeholder."""
2
+
@@ -0,0 +1,2 @@
1
+ """DSPy adapter placeholder."""
2
+
@@ -0,0 +1,18 @@
1
+ """LangChain adapter for claim-level evidence gating."""
2
+
3
+ from .runnable import GuardedRunnable, create_guarded_runnable
4
+ from .types import (
5
+ FieldExtractor,
6
+ FieldMapper,
7
+ LangChainAdapterInput,
8
+ LangChainAdapterOutput,
9
+ )
10
+
11
+ __all__ = [
12
+ "FieldExtractor",
13
+ "FieldMapper",
14
+ "GuardedRunnable",
15
+ "LangChainAdapterInput",
16
+ "LangChainAdapterOutput",
17
+ "create_guarded_runnable",
18
+ ]
@@ -0,0 +1,126 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ from langchain_core.runnables import Runnable, RunnableSerializable
5
+ from pydantic import ConfigDict
6
+
7
+ from agentclaimguard.core.policy import Policy
8
+ from agentclaimguard.core.result import VerificationResult
9
+ from agentclaimguard.core.runtime import AgentClaimGuard
10
+
11
+ from .types import FieldExtractor, FieldMapper
12
+
13
+
14
+ _DEFAULT_FIELD_MAP: dict[str, str] = {
15
+ "claims": "claims",
16
+ "evidence": "evidence",
17
+ "tool_results": "tool_results",
18
+ }
19
+
20
+
21
+ class GuardedRunnable(RunnableSerializable[Any, Any]):
22
+ model_config = ConfigDict(arbitrary_types_allowed=True)
23
+
24
+ runnable: Runnable[Any, Any]
25
+ policy: Policy
26
+ field_map: FieldMapper = None
27
+ result_key: str = "guard_result"
28
+ output_key: str = "output"
29
+ overwrite_result: bool = False
30
+
31
+ def invoke(self, input: Any, config=None, **kwargs: Any) -> Any:
32
+ output = self.runnable.invoke(input, config=config, **kwargs)
33
+ guard_result = self._run_guard(input=input, output=output)
34
+ return self._merge_output(output=output, guard_result=guard_result)
35
+
36
+ async def ainvoke(self, input: Any, config=None, **kwargs: Any) -> Any:
37
+ output = await self.runnable.ainvoke(input, config=config, **kwargs)
38
+ guard_result = self._run_guard(input=input, output=output)
39
+ return self._merge_output(output=output, guard_result=guard_result)
40
+
41
+ def _run_guard(self, *, input: Any, output: Any) -> VerificationResult:
42
+ guard = AgentClaimGuard(policy=self.policy)
43
+ field_map = dict(_DEFAULT_FIELD_MAP)
44
+ if self.field_map:
45
+ field_map.update(self.field_map)
46
+
47
+ claims = _resolve_field(field_map["claims"], input=input, output=output) or []
48
+ evidence = (
49
+ _resolve_field(field_map["evidence"], input=input, output=output) or []
50
+ )
51
+ tool_results = (
52
+ _resolve_field(field_map["tool_results"], input=input, output=output) or []
53
+ )
54
+
55
+ return guard.verify(
56
+ claims=claims,
57
+ evidence=evidence,
58
+ tool_results=tool_results,
59
+ )
60
+
61
+ def _merge_output(self, *, output: Any, guard_result: VerificationResult) -> Any:
62
+ if isinstance(output, Mapping):
63
+ if self.result_key in output and not self.overwrite_result:
64
+ raise ValueError(
65
+ f"LangChain adapter output already contains '{self.result_key}'. "
66
+ "Use a different result_key or set overwrite_result=True."
67
+ )
68
+ merged = dict(output)
69
+ merged[self.result_key] = guard_result
70
+ return merged
71
+
72
+ return {
73
+ self.output_key: output,
74
+ self.result_key: guard_result,
75
+ }
76
+
77
+
78
+ def create_guarded_runnable(
79
+ runnable: Runnable[Any, Any],
80
+ policy: Policy,
81
+ *,
82
+ field_map: FieldMapper = None,
83
+ result_key: str = "guard_result",
84
+ output_key: str = "output",
85
+ overwrite_result: bool = False,
86
+ ) -> GuardedRunnable:
87
+ """Wrap a LangChain Runnable and attach AgentClaimGuard verification."""
88
+ return GuardedRunnable(
89
+ runnable=runnable,
90
+ policy=policy,
91
+ field_map=field_map,
92
+ result_key=result_key,
93
+ output_key=output_key,
94
+ overwrite_result=overwrite_result,
95
+ )
96
+
97
+
98
+ def _resolve_field(
99
+ extractor: FieldExtractor,
100
+ *,
101
+ input: Any,
102
+ output: Any,
103
+ ) -> list[Any] | None:
104
+ if callable(extractor):
105
+ value = extractor(input, output)
106
+ elif isinstance(extractor, str):
107
+ value = _lookup_value(output, extractor)
108
+ if value is None:
109
+ value = _lookup_value(input, extractor)
110
+ else:
111
+ value = None
112
+
113
+ if value is None:
114
+ return None
115
+ if isinstance(value, list):
116
+ return value
117
+ raise TypeError(
118
+ "LangChain adapter field extractors must resolve to a list or None. "
119
+ f"Got {type(value).__name__}."
120
+ )
121
+
122
+
123
+ def _lookup_value(source: Any, key: str) -> Any:
124
+ if isinstance(source, Mapping):
125
+ return source.get(key)
126
+ return None
@@ -0,0 +1,8 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any, TypeAlias
3
+
4
+
5
+ LangChainAdapterInput: TypeAlias = Any
6
+ LangChainAdapterOutput: TypeAlias = Any
7
+ FieldExtractor: TypeAlias = str | Callable[[Any, Any], list[Any] | None]
8
+ FieldMapper: TypeAlias = Mapping[str, FieldExtractor] | None
@@ -0,0 +1,18 @@
1
+ """LangGraph adapter for claim-level evidence gating."""
2
+
3
+ from .node import create_evidence_guard_node, route_by_guard_status
4
+ from .types import (
5
+ EvidenceGuardNode,
6
+ GuardRoute,
7
+ LangGraphGuardState,
8
+ LangGraphGuardUpdate,
9
+ )
10
+
11
+ __all__ = [
12
+ "EvidenceGuardNode",
13
+ "GuardRoute",
14
+ "LangGraphGuardState",
15
+ "LangGraphGuardUpdate",
16
+ "create_evidence_guard_node",
17
+ "route_by_guard_status",
18
+ ]
@@ -0,0 +1,78 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ from pydantic import ValidationError
5
+
6
+ from agentclaimguard.core.policy import Policy
7
+ from agentclaimguard.core.result import VerificationResult
8
+ from agentclaimguard.core.runtime import AgentClaimGuard
9
+
10
+ from .types import EvidenceGuardNode, GuardRoute
11
+
12
+
13
+ _ROUTABLE_CLAIM_STATUSES: set[GuardRoute] = {
14
+ "need_check",
15
+ "insufficient_evidence",
16
+ "conflicting_evidence",
17
+ }
18
+
19
+
20
+ def create_evidence_guard_node(
21
+ policy: Policy,
22
+ *,
23
+ claims_key: str = "claims",
24
+ evidence_key: str = "evidence",
25
+ tool_results_key: str = "tool_results",
26
+ result_key: str = "guard_result",
27
+ ) -> EvidenceGuardNode:
28
+ """Create a LangGraph-compatible node for AgentClaimGuard verification."""
29
+ guard = AgentClaimGuard(policy=policy)
30
+
31
+ def evidence_guard_node(state: Mapping[str, Any]) -> dict[str, VerificationResult]:
32
+ result = guard.verify(
33
+ claims=state.get(claims_key, []),
34
+ evidence=state.get(evidence_key, []),
35
+ tool_results=state.get(tool_results_key, []),
36
+ )
37
+ return {result_key: result}
38
+
39
+ return evidence_guard_node
40
+
41
+
42
+ def route_by_guard_status(
43
+ state: Mapping[str, Any],
44
+ *,
45
+ result_key: str = "guard_result",
46
+ ) -> GuardRoute:
47
+ result = _coerce_result(state.get(result_key))
48
+ if result is None:
49
+ return "need_check"
50
+
51
+ if result.status == "passed":
52
+ return "passed"
53
+
54
+ claim_status = _first_non_passed_claim_status(result)
55
+ if claim_status in _ROUTABLE_CLAIM_STATUSES:
56
+ return claim_status
57
+
58
+ return "blocked"
59
+
60
+
61
+ def _coerce_result(value: Any) -> VerificationResult | None:
62
+ if value is None:
63
+ return None
64
+ if isinstance(value, VerificationResult):
65
+ return value
66
+ if isinstance(value, Mapping):
67
+ try:
68
+ return VerificationResult.model_validate(value)
69
+ except ValidationError:
70
+ return None
71
+ return None
72
+
73
+
74
+ def _first_non_passed_claim_status(result: VerificationResult) -> str | None:
75
+ for claim_result in result.claim_results:
76
+ if claim_result.status != "passed":
77
+ return claim_result.status
78
+ return None
@@ -0,0 +1,16 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any, Literal, TypeAlias
3
+
4
+ from agentclaimguard.core.result import VerificationResult
5
+
6
+
7
+ LangGraphGuardState: TypeAlias = Mapping[str, Any]
8
+ LangGraphGuardUpdate: TypeAlias = dict[str, VerificationResult]
9
+ EvidenceGuardNode: TypeAlias = Callable[[LangGraphGuardState], LangGraphGuardUpdate]
10
+ GuardRoute: TypeAlias = Literal[
11
+ "passed",
12
+ "blocked",
13
+ "need_check",
14
+ "insufficient_evidence",
15
+ "conflicting_evidence",
16
+ ]
@@ -0,0 +1,2 @@
1
+ """RAGFlow adapter placeholder."""
2
+
@@ -0,0 +1,23 @@
1
+ from agentclaimguard.core.claim import Claim
2
+ from agentclaimguard.core.evidence import Evidence
3
+ from agentclaimguard.core.policy import Policy
4
+ from agentclaimguard.core.result import (
5
+ ClaimVerificationResult,
6
+ VerificationResult,
7
+ Violation,
8
+ )
9
+ from agentclaimguard.core.runtime import AgentClaimGuard, verify_claims
10
+ from agentclaimguard.core.tool_result import ToolResult
11
+
12
+ __all__ = [
13
+ "Claim",
14
+ "AgentClaimGuard",
15
+ "ClaimVerificationResult",
16
+ "Evidence",
17
+ "Policy",
18
+ "ToolResult",
19
+ "VerificationResult",
20
+ "Violation",
21
+ "verify_claims",
22
+ ]
23
+
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class Claim(BaseModel):
7
+ id: str
8
+ text: str
9
+ type: str
10
+ verdict: str | None = None
11
+ evidence_refs: list[str] = Field(default_factory=list)
12
+ tool_result_refs: list[str] = Field(default_factory=list)
13
+ confidence: float | None = Field(default=None, ge=0.0, le=1.0)
14
+ metadata: dict[str, Any] = Field(default_factory=dict)
15
+
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class Evidence(BaseModel):
7
+ id: str
8
+ type: str
9
+ source: str | None = None
10
+ locator: str | None = None
11
+ content: str
12
+ metadata: dict[str, Any] = Field(default_factory=dict)
13
+
@@ -0,0 +1,83 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+
6
+ from agentclaimguard.utils.yaml_loader import load_yaml
7
+
8
+
9
+ class EvidenceRequirement(BaseModel):
10
+ type: str
11
+ min_count: int = Field(default=1, ge=1)
12
+
13
+
14
+ class ToolRequirement(BaseModel):
15
+ tool_name: str
16
+ min_count: int = Field(default=1, ge=1)
17
+
18
+ @model_validator(mode="before")
19
+ @classmethod
20
+ def parse_string_requirement(cls, value: Any) -> Any:
21
+ if isinstance(value, str):
22
+ return {"tool_name": value}
23
+ if isinstance(value, dict) and "name" in value and "tool_name" not in value:
24
+ return {**value, "tool_name": value["name"]}
25
+ return value
26
+
27
+
28
+ class FallbackRule(BaseModel):
29
+ verdict: str = "need_check"
30
+ reason: str | None = None
31
+
32
+
33
+ class ClaimTypePolicy(BaseModel):
34
+ required_evidence: list[EvidenceRequirement] = Field(default_factory=list)
35
+ required_tool_results: list[ToolRequirement] = Field(default_factory=list)
36
+ forbidden: list[str] = Field(default_factory=list)
37
+ fallback: FallbackRule | None = None
38
+ metadata: dict[str, Any] = Field(default_factory=dict)
39
+
40
+
41
+ class Policy(BaseModel):
42
+ name: str = "default"
43
+ version: str = "0.1"
44
+ claim_types: dict[str, ClaimTypePolicy] = Field(default_factory=dict)
45
+ default_fallback: FallbackRule = Field(
46
+ default_factory=lambda: FallbackRule(
47
+ verdict="need_check",
48
+ reason="The claim could not be verified by the active policy.",
49
+ )
50
+ )
51
+ metadata: dict[str, Any] = Field(default_factory=dict)
52
+
53
+ @classmethod
54
+ def load(cls, path: str | Path) -> "Policy":
55
+ data = load_yaml(path)
56
+ return cls.model_validate(data)
57
+
58
+ @classmethod
59
+ def load_builtin(cls, name: str) -> "Policy":
60
+ policy_name = name.removesuffix(".yaml")
61
+ if "/" in policy_name or "\\" in policy_name:
62
+ raise ValueError("Built-in policy names must not include path separators.")
63
+
64
+ path = Path(__file__).resolve().parents[1] / "policies" / f"{policy_name}.yaml"
65
+ if not path.exists():
66
+ available = ", ".join(cls.list_builtin())
67
+ raise ValueError(
68
+ f"Unknown built-in policy '{name}'. Available policies: {available}."
69
+ )
70
+
71
+ return cls.load(path)
72
+
73
+ @classmethod
74
+ def list_builtin(cls) -> list[str]:
75
+ policy_dir = Path(__file__).resolve().parents[1] / "policies"
76
+ return sorted(path.stem for path in policy_dir.glob("*.yaml"))
77
+
78
+ def policy_for_claim_type(self, claim_type: str) -> ClaimTypePolicy:
79
+ return self.claim_types.get(claim_type, ClaimTypePolicy())
80
+
81
+ def fallback_for_claim_type(self, claim_type: str) -> FallbackRule:
82
+ claim_policy = self.policy_for_claim_type(claim_type)
83
+ return claim_policy.fallback or self.default_fallback
@@ -0,0 +1,41 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ ClaimStatus = Literal[
7
+ "passed",
8
+ "blocked",
9
+ "need_check",
10
+ "insufficient_evidence",
11
+ "conflicting_evidence",
12
+ "tool_required",
13
+ "tool_error",
14
+ "repair_required",
15
+ ]
16
+
17
+
18
+ class Violation(BaseModel):
19
+ claim_id: str
20
+ type: str
21
+ message: str
22
+ required: str | None = None
23
+ found: int | None = None
24
+ refs: list[str] = Field(default_factory=list)
25
+ details: dict[str, Any] = Field(default_factory=dict)
26
+
27
+
28
+ class ClaimVerificationResult(BaseModel):
29
+ claim_id: str
30
+ status: ClaimStatus
31
+ violations: list[Violation] = Field(default_factory=list)
32
+ safe_verdict: str | None = None
33
+ reason: str | None = None
34
+
35
+
36
+ class VerificationResult(BaseModel):
37
+ status: Literal["passed", "blocked"]
38
+ claim_results: list[ClaimVerificationResult] = Field(default_factory=list)
39
+ violations: list[Violation] = Field(default_factory=list)
40
+ safe_output: dict[str, Any] = Field(default_factory=dict)
41
+
@@ -0,0 +1,54 @@
1
+ from collections.abc import Iterable
2
+
3
+ from agentclaimguard.core.claim import Claim
4
+ from agentclaimguard.core.evidence import Evidence
5
+ from agentclaimguard.core.policy import Policy
6
+ from agentclaimguard.core.result import VerificationResult
7
+ from agentclaimguard.core.tool_result import ToolResult
8
+ from agentclaimguard.core.verifier import verify_claims as run_verifier
9
+
10
+
11
+ class AgentClaimGuard:
12
+ def __init__(self, policy: Policy):
13
+ self.policy = policy
14
+
15
+ def verify(
16
+ self,
17
+ claims: Iterable[Claim | dict],
18
+ evidence: Iterable[Evidence | dict] | None = None,
19
+ tool_results: Iterable[ToolResult | dict] | None = None,
20
+ ) -> VerificationResult:
21
+ parsed_claims = [Claim.model_validate(item) for item in claims]
22
+ parsed_evidence = [Evidence.model_validate(item) for item in evidence or []]
23
+ parsed_tool_results = [
24
+ ToolResult.model_validate(item) for item in tool_results or []
25
+ ]
26
+ return run_verifier(
27
+ claims=parsed_claims,
28
+ evidence=parsed_evidence,
29
+ tool_results=parsed_tool_results,
30
+ policy=self.policy,
31
+ )
32
+
33
+ def repair(
34
+ self,
35
+ claims: Iterable[Claim | dict],
36
+ evidence: Iterable[Evidence | dict] | None = None,
37
+ tool_results: Iterable[ToolResult | dict] | None = None,
38
+ ) -> dict:
39
+ result = self.verify(claims=claims, evidence=evidence, tool_results=tool_results)
40
+ return result.safe_output
41
+
42
+
43
+ def verify_claims(
44
+ claims: Iterable[Claim | dict],
45
+ evidence: Iterable[Evidence | dict],
46
+ tool_results: Iterable[ToolResult | dict],
47
+ policy: Policy,
48
+ ) -> VerificationResult:
49
+ return AgentClaimGuard(policy=policy).verify(
50
+ claims=claims,
51
+ evidence=evidence,
52
+ tool_results=tool_results,
53
+ )
54
+
@@ -0,0 +1,14 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ToolResult(BaseModel):
7
+ id: str
8
+ tool_name: str
9
+ status: Literal["success", "error", "skipped"] = "success"
10
+ input: dict[str, Any] = Field(default_factory=dict)
11
+ output: Any = None
12
+ evidence_refs: list[str] = Field(default_factory=list)
13
+ metadata: dict[str, Any] = Field(default_factory=dict)
14
+
@@ -0,0 +1,97 @@
1
+ from collections.abc import Iterable
2
+
3
+ from agentclaimguard.core.claim import Claim
4
+ from agentclaimguard.core.evidence import Evidence
5
+ from agentclaimguard.core.policy import Policy
6
+ from agentclaimguard.core.result import (
7
+ ClaimVerificationResult,
8
+ ClaimStatus,
9
+ VerificationResult,
10
+ Violation,
11
+ )
12
+ from agentclaimguard.core.tool_result import ToolResult
13
+ from agentclaimguard.validators.citation_binding import validate_citation_binding
14
+ from agentclaimguard.validators.conflict_check import validate_conflicting_evidence
15
+ from agentclaimguard.validators.evidence_required import validate_required_evidence
16
+ from agentclaimguard.validators.forbidden_verdict import validate_forbidden_rules
17
+ from agentclaimguard.validators.tool_required import validate_required_tools
18
+
19
+
20
+ def verify_claims(
21
+ claims: Iterable[Claim],
22
+ evidence: Iterable[Evidence],
23
+ tool_results: Iterable[ToolResult],
24
+ policy: Policy,
25
+ ) -> VerificationResult:
26
+ evidence_by_id = {item.id: item for item in evidence}
27
+ tool_results_by_id = {item.id: item for item in tool_results}
28
+
29
+ claim_results: list[ClaimVerificationResult] = []
30
+ all_violations: list[Violation] = []
31
+
32
+ for claim in claims:
33
+ claim_policy = policy.policy_for_claim_type(claim.type)
34
+ violations: list[Violation] = []
35
+ violations.extend(validate_citation_binding(claim, evidence_by_id, tool_results_by_id))
36
+ violations.extend(validate_required_evidence(claim, evidence_by_id, claim_policy))
37
+ violations.extend(validate_required_tools(claim, tool_results_by_id, claim_policy))
38
+ violations.extend(validate_forbidden_rules(claim, evidence_by_id, tool_results_by_id, claim_policy))
39
+ violations.extend(validate_conflicting_evidence(claim, evidence_by_id))
40
+
41
+ if violations:
42
+ fallback = policy.fallback_for_claim_type(claim.type)
43
+ status = _status_from_violations(violations)
44
+ claim_result = ClaimVerificationResult(
45
+ claim_id=claim.id,
46
+ status=status,
47
+ violations=violations,
48
+ safe_verdict=fallback.verdict,
49
+ reason=fallback.reason,
50
+ )
51
+ all_violations.extend(violations)
52
+ else:
53
+ claim_result = ClaimVerificationResult(claim_id=claim.id, status="passed")
54
+
55
+ claim_results.append(claim_result)
56
+
57
+ status = "passed" if not all_violations else "blocked"
58
+ return VerificationResult(
59
+ status=status,
60
+ claim_results=claim_results,
61
+ violations=all_violations,
62
+ safe_output=_safe_output(claim_results),
63
+ )
64
+
65
+
66
+ def _status_from_violations(violations: list[Violation]) -> ClaimStatus:
67
+ violation_types = {violation.type for violation in violations}
68
+ if "conflicting_evidence" in violation_types:
69
+ return "conflicting_evidence"
70
+ if "required_tool_error" in violation_types:
71
+ return "tool_error"
72
+ if "missing_required_tool_result" in violation_types:
73
+ return "tool_required"
74
+ if {
75
+ "missing_required_evidence",
76
+ "missing_citation",
77
+ "invalid_evidence_ref",
78
+ } & violation_types:
79
+ return "insufficient_evidence"
80
+ return "blocked"
81
+
82
+
83
+ def _safe_output(
84
+ claim_results: list[ClaimVerificationResult],
85
+ ) -> dict[str, list[dict[str, str | None]]]:
86
+ blocked = [
87
+ {
88
+ "claim_id": result.claim_id,
89
+ "safe_verdict": result.safe_verdict,
90
+ "reason": result.reason,
91
+ "status": result.status,
92
+ }
93
+ for result in claim_results
94
+ if result.status != "passed"
95
+ ]
96
+ return {"blocked_claims": blocked}
97
+
@@ -0,0 +1,17 @@
1
+ name: generic_compliance
2
+ version: "0.1"
3
+
4
+ claim_types:
5
+ compliance_judgement:
6
+ required_evidence:
7
+ - type: regulation
8
+ min_count: 1
9
+ - type: source_fact
10
+ min_count: 1
11
+ forbidden:
12
+ - use_model_memory_as_authority
13
+ - unsupported_pass_fail
14
+ fallback:
15
+ verdict: need_check
16
+ reason: Compliance judgments require rule evidence and source facts.
17
+