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.
- agentclaimguard/__init__.py +23 -0
- agentclaimguard/adapters/__init__.py +6 -0
- agentclaimguard/adapters/dify/__init__.py +2 -0
- agentclaimguard/adapters/dspy/__init__.py +2 -0
- agentclaimguard/adapters/langchain/__init__.py +18 -0
- agentclaimguard/adapters/langchain/runnable.py +126 -0
- agentclaimguard/adapters/langchain/types.py +8 -0
- agentclaimguard/adapters/langgraph/__init__.py +18 -0
- agentclaimguard/adapters/langgraph/node.py +78 -0
- agentclaimguard/adapters/langgraph/types.py +16 -0
- agentclaimguard/adapters/ragflow/__init__.py +2 -0
- agentclaimguard/core/__init__.py +23 -0
- agentclaimguard/core/claim.py +15 -0
- agentclaimguard/core/evidence.py +13 -0
- agentclaimguard/core/policy.py +83 -0
- agentclaimguard/core/result.py +41 -0
- agentclaimguard/core/runtime.py +54 -0
- agentclaimguard/core/tool_result.py +14 -0
- agentclaimguard/core/verifier.py +97 -0
- agentclaimguard/policies/generic_compliance.yaml +17 -0
- agentclaimguard/policies/generic_numeric.yaml +16 -0
- agentclaimguard/policies/generic_rag.yaml +14 -0
- agentclaimguard/policies/generic_strict.yaml +43 -0
- agentclaimguard/server/__init__.py +1 -0
- agentclaimguard/server/main.py +44 -0
- agentclaimguard/server/schemas.py +18 -0
- agentclaimguard/utils/__init__.py +1 -0
- agentclaimguard/utils/ids.py +6 -0
- agentclaimguard/utils/json_schema.py +16 -0
- agentclaimguard/utils/yaml_loader.py +13 -0
- agentclaimguard/validators/__init__.py +14 -0
- agentclaimguard/validators/citation_binding.py +41 -0
- agentclaimguard/validators/conflict_check.py +59 -0
- agentclaimguard/validators/evidence_required.py +37 -0
- agentclaimguard/validators/forbidden_verdict.py +93 -0
- agentclaimguard/validators/tool_required.py +59 -0
- agentclaimguard-0.3.1.dist-info/METADATA +254 -0
- agentclaimguard-0.3.1.dist-info/RECORD +41 -0
- agentclaimguard-0.3.1.dist-info/WHEEL +4 -0
- agentclaimguard-0.3.1.dist-info/licenses/LICENSE +201 -0
- 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,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,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,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
|
+
|