evalrun 0.4.0__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.
- agents/__init__.py +6 -0
- agents/auditor/__init__.py +13 -0
- agents/auditor/budget_auditor.py +91 -0
- agents/auditor/parser.py +139 -0
- agents/auditor/prompts.py +64 -0
- agents/auditor/schema.py +67 -0
- agents/base.py +20 -0
- agents/reflection/__init__.py +3 -0
- agents/reflection/agent.py +77 -0
- agents/reflection/prompts.py +18 -0
- agents/research/__init__.py +4 -0
- agents/research/agent.py +45 -0
- agents/research/planner.py +52 -0
- agents/research/prompts.py +14 -0
- agents/support/__init__.py +5 -0
- agents/support/triage_agent.py +45 -0
- agents/travel/__init__.py +11 -0
- agents/travel/agent.py +377 -0
- agents/travel/prompts.py +30 -0
- agents/travel/session.py +110 -0
- cli/__init__.py +6 -0
- cli/demo.py +47 -0
- cli/formatter.py +93 -0
- cli/html_reporter.py +647 -0
- cli/main.py +423 -0
- cli/progress.py +38 -0
- cli/resolver.py +99 -0
- evalrun-0.4.0.dist-info/METADATA +268 -0
- evalrun-0.4.0.dist-info/RECORD +100 -0
- evalrun-0.4.0.dist-info/WHEEL +5 -0
- evalrun-0.4.0.dist-info/entry_points.txt +2 -0
- evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
- evalrun-0.4.0.dist-info/top_level.txt +4 -0
- framework/__init__.py +70 -0
- framework/core/__init__.py +17 -0
- framework/core/adapters.py +118 -0
- framework/core/contracts.py +88 -0
- framework/core/suite.py +44 -0
- framework/evaluation/__init__.py +22 -0
- framework/evaluation/base.py +29 -0
- framework/evaluation/dimensions.py +7 -0
- framework/evaluation/engine.py +110 -0
- framework/evaluation/evaluators/__init__.py +7 -0
- framework/evaluation/evaluators/adaptability.py +27 -0
- framework/evaluation/evaluators/base_llm.py +104 -0
- framework/evaluation/evaluators/constraint.py +27 -0
- framework/evaluation/evaluators/information_accuracy.py +41 -0
- framework/evaluation/evaluators/personalization.py +27 -0
- framework/evaluation/evaluators/planning.py +27 -0
- framework/evaluation/evaluators/support.py +81 -0
- framework/evaluation/prompts/__init__.py +11 -0
- framework/evaluation/prompts/adaptability.py +57 -0
- framework/evaluation/prompts/base.py +52 -0
- framework/evaluation/prompts/constraint.py +41 -0
- framework/evaluation/prompts/information_accuracy.py +79 -0
- framework/evaluation/prompts/personalization.py +57 -0
- framework/evaluation/prompts/planning.py +61 -0
- framework/evaluation/runner.py +363 -0
- framework/evaluation/testing.py +25 -0
- framework/exceptions.py +49 -0
- framework/llms/__init__.py +8 -0
- framework/llms/base.py +37 -0
- framework/llms/factory.py +38 -0
- framework/llms/gemini.py +85 -0
- framework/llms/mock.py +25 -0
- framework/llms/openai.py +94 -0
- framework/llms/openai_compatible.py +139 -0
- framework/mcp/__init__.py +20 -0
- framework/mcp/client.py +62 -0
- framework/mcp/constraints.py +125 -0
- framework/mcp/revision_summary.py +122 -0
- framework/mcp/server.py +49 -0
- framework/memory/__init__.py +3 -0
- framework/memory/base.py +17 -0
- framework/models.py +83 -0
- framework/parser.py +45 -0
- framework/parsers/__init__.py +12 -0
- framework/parsers/frontmatter.py +28 -0
- framework/parsers/mapper.py +66 -0
- framework/parsers/markdown.py +122 -0
- framework/parsers/transformers.py +112 -0
- framework/profiles/__init__.py +28 -0
- framework/profiles/registry.py +104 -0
- framework/profiles/support.py +27 -0
- framework/profiles/travel.py +78 -0
- framework/regression/__init__.py +17 -0
- framework/regression/comparator.py +273 -0
- framework/regression/loader.py +145 -0
- framework/sdk.py +151 -0
- framework/utils.py +41 -0
- framework/verification/__init__.py +13 -0
- framework/verification/base.py +32 -0
- framework/verification/extractor.py +105 -0
- framework/verification/local.py +122 -0
- framework/verification/models.py +112 -0
- framework/verification/pipeline.py +36 -0
- framework/verification/prompts.py +24 -0
- framework/verification/utils.py +54 -0
- ui/__init__.py +1 -0
- ui/server.py +255 -0
framework/utils.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Shared helper utility functions for the framework."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_json_markdown(text: str) -> Any:
|
|
9
|
+
"""Extracts and parses a JSON structure from raw LLM output.
|
|
10
|
+
|
|
11
|
+
Handles wrapping blocks like ```json ... ``` robustly.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
text: Raw text generated by the model.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The parsed Python dictionary, list, or primitive.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If JSON decoding fails.
|
|
21
|
+
"""
|
|
22
|
+
cleaned = text.strip()
|
|
23
|
+
if cleaned.startswith("```json"):
|
|
24
|
+
cleaned = cleaned[7:]
|
|
25
|
+
elif cleaned.startswith("```"):
|
|
26
|
+
cleaned = cleaned[3:]
|
|
27
|
+
if cleaned.endswith("```"):
|
|
28
|
+
cleaned = cleaned[:-3]
|
|
29
|
+
normalized = cleaned.strip()
|
|
30
|
+
try:
|
|
31
|
+
return json.loads(normalized, strict=False)
|
|
32
|
+
except json.JSONDecodeError as first_error:
|
|
33
|
+
# LLM judges occasionally emit a trailing comma before `}` or `]`.
|
|
34
|
+
# Repair only that conservative, unambiguous formatting issue.
|
|
35
|
+
repaired = re.sub(r",(\s*[}\]])", r"\1", normalized)
|
|
36
|
+
if repaired == normalized:
|
|
37
|
+
raise ValueError(f"Invalid JSON string: {first_error}") from first_error
|
|
38
|
+
try:
|
|
39
|
+
return json.loads(repaired, strict=False)
|
|
40
|
+
except json.JSONDecodeError as e:
|
|
41
|
+
raise ValueError(f"Invalid JSON string: {e}") from e
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Package containing interfaces and structures for factual verification."""
|
|
2
|
+
|
|
3
|
+
from framework.verification.models import (
|
|
4
|
+
ClaimType,
|
|
5
|
+
VerificationStatus,
|
|
6
|
+
Claim,
|
|
7
|
+
Evidence,
|
|
8
|
+
VerificationReport,
|
|
9
|
+
)
|
|
10
|
+
from framework.verification.base import BaseVerifier
|
|
11
|
+
from framework.verification.local import LocalKnowledgeBaseVerifier
|
|
12
|
+
from framework.verification.extractor import ClaimExtractor
|
|
13
|
+
from framework.verification.pipeline import VerificationPipeline
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Base interface for factual claims verifiers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List
|
|
5
|
+
from .models import Claim, Evidence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseVerifier(ABC):
|
|
9
|
+
"""Abstract base class for factual claims verifiers."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def verify(self, claim: Claim) -> Evidence:
|
|
13
|
+
"""Verifies a single claim against a ground-truth source.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
claim: The claim to verify.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
An Evidence dataclass containing the outcome status and source reference.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def verify_all(self, claims: List[Claim]) -> List[Evidence]:
|
|
24
|
+
"""Batch verifies a list of claims sequentially.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
claims: The list of claims to verify.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of Evidence outcomes.
|
|
31
|
+
"""
|
|
32
|
+
return [self.verify(c) for c in claims]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Factual claim extraction from agent output using LLM."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from framework.exceptions import VerificationError
|
|
5
|
+
from framework.llms import BaseLLM, Message
|
|
6
|
+
from framework.models import AgentOutput
|
|
7
|
+
from framework.utils import parse_json_markdown
|
|
8
|
+
from .models import Claim, ClaimType
|
|
9
|
+
from .prompts import CLAIM_EXTRACTION_PROMPT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaimExtractor:
|
|
13
|
+
"""Extracts factual claims from agent output text using an LLM."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, llm: BaseLLM):
|
|
16
|
+
"""Initializes the ClaimExtractor.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
llm: The LLM client to use for structured extraction.
|
|
20
|
+
"""
|
|
21
|
+
self.llm = llm
|
|
22
|
+
|
|
23
|
+
def extract(self, output: AgentOutput) -> List[Claim]:
|
|
24
|
+
"""Extracts verifiable claims from the agent output.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
output: The output to analyze.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of extracted Claim objects.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
VerificationError: If extraction or JSON parsing fails.
|
|
34
|
+
"""
|
|
35
|
+
prompt_messages = self._build_messages(output.content)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
response = self.llm.generate(prompt_messages)
|
|
39
|
+
raw_text = response.text
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise VerificationError(
|
|
42
|
+
f"LLM claims extraction generation failed: {e}"
|
|
43
|
+
) from e
|
|
44
|
+
|
|
45
|
+
raw_claims = self._parse_response(raw_text)
|
|
46
|
+
|
|
47
|
+
claims = []
|
|
48
|
+
for idx, item in enumerate(raw_claims):
|
|
49
|
+
claim = self._validate_and_build_claim(item, idx)
|
|
50
|
+
claims.append(claim)
|
|
51
|
+
|
|
52
|
+
return claims
|
|
53
|
+
|
|
54
|
+
def _build_messages(self, content: str) -> List[Message]:
|
|
55
|
+
"""Constructs prompt messages for claims extraction."""
|
|
56
|
+
return [
|
|
57
|
+
Message(role="system", content=CLAIM_EXTRACTION_PROMPT),
|
|
58
|
+
Message(
|
|
59
|
+
role="user",
|
|
60
|
+
content=f"Extract all factual claims from the following text:\n\n{content}",
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
def _parse_response(self, raw_text: str) -> List[dict]:
|
|
65
|
+
"""Cleans and parses the LLM JSON response."""
|
|
66
|
+
try:
|
|
67
|
+
parsed = parse_json_markdown(raw_text)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise VerificationError(
|
|
70
|
+
f"Failed to parse extractor LLM output as JSON. Raw response:\n{raw_text}"
|
|
71
|
+
) from e
|
|
72
|
+
|
|
73
|
+
if not isinstance(parsed, list):
|
|
74
|
+
raise VerificationError(
|
|
75
|
+
f"Expected JSON array of claims, got: {type(parsed)}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return parsed
|
|
79
|
+
|
|
80
|
+
def _validate_and_build_claim(self, raw_item: dict, index: int) -> Claim:
|
|
81
|
+
"""Validates raw dict fields and builds a Claim object."""
|
|
82
|
+
if not isinstance(raw_item, dict):
|
|
83
|
+
raise VerificationError(
|
|
84
|
+
f"Expected claim dictionary at index {index}, got: {type(raw_item)}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for field in ["subject", "predicate", "value", "claim_type"]:
|
|
88
|
+
if field not in raw_item:
|
|
89
|
+
raise VerificationError(
|
|
90
|
+
f"Missing required field '{field}' in claim at index {index}: {raw_item}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Map type dynamically with validation fallback in python instead of LLM direct mapping
|
|
94
|
+
raw_type = str(raw_item["claim_type"]).lower()
|
|
95
|
+
try:
|
|
96
|
+
claim_type = ClaimType(raw_type)
|
|
97
|
+
except ValueError:
|
|
98
|
+
claim_type = ClaimType.OTHER
|
|
99
|
+
|
|
100
|
+
return Claim(
|
|
101
|
+
subject=str(raw_item["subject"]),
|
|
102
|
+
predicate=str(raw_item["predicate"]),
|
|
103
|
+
value=str(raw_item["value"]),
|
|
104
|
+
claim_type=claim_type,
|
|
105
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Local knowledge base claim verifier."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
from framework.exceptions import KnowledgeBaseLoadError
|
|
7
|
+
from .base import BaseVerifier
|
|
8
|
+
from .models import Claim, Evidence, VerificationStatus
|
|
9
|
+
from .utils import normalize_key, values_are_equivalent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalKnowledgeBaseVerifier(BaseVerifier):
|
|
13
|
+
"""Verifies claims against a local JSON knowledge base of truth facts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, kb_path: str):
|
|
16
|
+
"""Initializes the LocalKnowledgeBaseVerifier.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
kb_path: Path to the JSON file containing ground-truth data.
|
|
20
|
+
"""
|
|
21
|
+
self.kb_path = Path(kb_path)
|
|
22
|
+
self._kb: Dict[str, Dict[str, Any]] = {}
|
|
23
|
+
self._load_kb()
|
|
24
|
+
|
|
25
|
+
def _load_kb(self) -> None:
|
|
26
|
+
"""Loads and normalizes the knowledge base keys."""
|
|
27
|
+
if not self.kb_path.exists():
|
|
28
|
+
raise KnowledgeBaseLoadError(
|
|
29
|
+
f"Knowledge base file does not exist: {self.kb_path}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
with open(self.kb_path, "r", encoding="utf-8") as f:
|
|
34
|
+
raw_data = json.load(f)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
raise KnowledgeBaseLoadError(
|
|
37
|
+
f"Malformed JSON in knowledge base at {self.kb_path}: {e}"
|
|
38
|
+
) from e
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise KnowledgeBaseLoadError(
|
|
41
|
+
f"Failed to read knowledge base at {self.kb_path}: {e}"
|
|
42
|
+
) from e
|
|
43
|
+
|
|
44
|
+
if not isinstance(raw_data, dict):
|
|
45
|
+
raise KnowledgeBaseLoadError(
|
|
46
|
+
f"Knowledge base JSON must root-level represent a dictionary, got {type(raw_data)}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Normalize the database subject and predicate keys for robust matching
|
|
50
|
+
for subject, predicates in raw_data.items():
|
|
51
|
+
if not isinstance(predicates, dict):
|
|
52
|
+
raise KnowledgeBaseLoadError(
|
|
53
|
+
f"Expected subject '{subject}' predicates to be a dictionary, got {type(predicates)}"
|
|
54
|
+
)
|
|
55
|
+
norm_subj = normalize_key(subject)
|
|
56
|
+
if norm_subj not in self._kb:
|
|
57
|
+
self._kb[norm_subj] = {}
|
|
58
|
+
for pred, val in predicates.items():
|
|
59
|
+
self._kb[norm_subj][normalize_key(pred)] = val
|
|
60
|
+
|
|
61
|
+
def verify(self, claim: Claim) -> Evidence:
|
|
62
|
+
"""Verifies a single claim against the local normalized knowledge base.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
claim: The claim to verify.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
An Evidence object containing the outcome.
|
|
69
|
+
"""
|
|
70
|
+
norm_subj = normalize_key(claim.subject)
|
|
71
|
+
norm_pred = normalize_key(claim.predicate)
|
|
72
|
+
source_str = str(self.kb_path)
|
|
73
|
+
|
|
74
|
+
# 1. Subject check (exact normalized match)
|
|
75
|
+
if norm_subj not in self._kb:
|
|
76
|
+
return Evidence(
|
|
77
|
+
claim=claim,
|
|
78
|
+
status=VerificationStatus.NOT_FOUND,
|
|
79
|
+
expected_value=None,
|
|
80
|
+
actual_value=claim.value,
|
|
81
|
+
source=source_str,
|
|
82
|
+
# Local KB is treated as authoritative.
|
|
83
|
+
confidence=1.0,
|
|
84
|
+
reason=f"Subject '{claim.subject}' was not found in the ground-truth records.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
subject_kb = self._kb[norm_subj]
|
|
88
|
+
|
|
89
|
+
# 2. Predicate check (exact normalized match)
|
|
90
|
+
if norm_pred not in subject_kb:
|
|
91
|
+
return Evidence(
|
|
92
|
+
claim=claim,
|
|
93
|
+
status=VerificationStatus.UNKNOWN,
|
|
94
|
+
expected_value=None,
|
|
95
|
+
actual_value=claim.value,
|
|
96
|
+
source=source_str,
|
|
97
|
+
# Local KB is treated as authoritative.
|
|
98
|
+
confidence=1.0,
|
|
99
|
+
reason=f"Subject '{claim.subject}' is verified, but attribute '{claim.predicate}' is not documented in the ground-truth records.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expected_val = str(subject_kb[norm_pred])
|
|
103
|
+
actual_val = claim.value
|
|
104
|
+
|
|
105
|
+
# 3. Value check
|
|
106
|
+
if values_are_equivalent(expected_val, actual_val):
|
|
107
|
+
status = VerificationStatus.VERIFIED
|
|
108
|
+
reason_str = f"Factual claim matches ground-truth record for '{claim.subject}' ('{claim.predicate}' = '{expected_val}')."
|
|
109
|
+
else:
|
|
110
|
+
status = VerificationStatus.REFUTED
|
|
111
|
+
reason_str = f"Factual discrepancy: ground-truth records list '{expected_val}' for '{claim.subject}' attribute '{claim.predicate}', but agent claimed '{actual_val}'."
|
|
112
|
+
|
|
113
|
+
return Evidence(
|
|
114
|
+
claim=claim,
|
|
115
|
+
status=status,
|
|
116
|
+
expected_value=expected_val,
|
|
117
|
+
actual_value=actual_val,
|
|
118
|
+
source=source_str,
|
|
119
|
+
# Local KB is treated as authoritative.
|
|
120
|
+
confidence=1.0,
|
|
121
|
+
reason=reason_str,
|
|
122
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Data structures for the factual claim verification subsystem."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClaimType(str, Enum):
|
|
9
|
+
"""Supported types of factual claims for travel itineraries."""
|
|
10
|
+
|
|
11
|
+
PRICE = "price"
|
|
12
|
+
TIMING = "timing"
|
|
13
|
+
EXISTENCE = "existence"
|
|
14
|
+
DISTANCE = "distance"
|
|
15
|
+
WEATHER = "weather"
|
|
16
|
+
OTHER = "other"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VerificationStatus(str, Enum):
|
|
20
|
+
"""Possible outcomes of verifying a claim against ground-truth data."""
|
|
21
|
+
|
|
22
|
+
VERIFIED = "verified"
|
|
23
|
+
REFUTED = "refuted"
|
|
24
|
+
NOT_FOUND = "not_found"
|
|
25
|
+
UNKNOWN = "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Claim:
|
|
30
|
+
"""Represents a single testable factual assertion extracted from agent output."""
|
|
31
|
+
|
|
32
|
+
subject: str
|
|
33
|
+
predicate: str
|
|
34
|
+
value: str
|
|
35
|
+
claim_type: ClaimType
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Evidence:
|
|
40
|
+
"""Represents the outcome of verifying a Claim against a ground-truth source."""
|
|
41
|
+
|
|
42
|
+
claim: Claim
|
|
43
|
+
status: VerificationStatus
|
|
44
|
+
expected_value: Optional[str]
|
|
45
|
+
actual_value: Optional[str]
|
|
46
|
+
source: str
|
|
47
|
+
confidence: float = 1.0 # Confidence in verification result on a scale from 0.0 to 1.0.
|
|
48
|
+
reason: Optional[str] = None # Contextual reasoning explaining findings or discrepancy details.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class VerificationReport:
|
|
53
|
+
"""Consolidated verification outcomes for all claims in a scenario."""
|
|
54
|
+
|
|
55
|
+
claims: List[Claim]
|
|
56
|
+
evidence: List[Evidence]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def total_claims(self) -> int:
|
|
60
|
+
return len(self.claims)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def verified_count(self) -> int:
|
|
64
|
+
return sum(
|
|
65
|
+
1 for e in self.evidence if e.status == VerificationStatus.VERIFIED
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def refuted_count(self) -> int:
|
|
70
|
+
return sum(
|
|
71
|
+
1 for e in self.evidence if e.status == VerificationStatus.REFUTED
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def unknown_count(self) -> int:
|
|
76
|
+
return sum(
|
|
77
|
+
1 for e in self.evidence if e.status == VerificationStatus.UNKNOWN
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def not_found_count(self) -> int:
|
|
82
|
+
return sum(
|
|
83
|
+
1 for e in self.evidence if e.status == VerificationStatus.NOT_FOUND
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def to_markdown(self) -> str:
|
|
87
|
+
"""Formats verification report evidence metrics and findings into a markdown string."""
|
|
88
|
+
evidence_lines = []
|
|
89
|
+
evidence_lines.append("Verification Summary:")
|
|
90
|
+
evidence_lines.append(f"- Total Claims Extracted: {self.total_claims}")
|
|
91
|
+
evidence_lines.append(f"- Verified: {self.verified_count}")
|
|
92
|
+
evidence_lines.append(f"- Refuted: {self.refuted_count}")
|
|
93
|
+
evidence_lines.append(f"- Unknown Predicates: {self.unknown_count}")
|
|
94
|
+
evidence_lines.append(f"- Not Found Subjects: {self.not_found_count}\n")
|
|
95
|
+
evidence_lines.append("Detailed Findings:")
|
|
96
|
+
|
|
97
|
+
if not self.evidence:
|
|
98
|
+
evidence_lines.append("No testable factual claims were extracted or verified.")
|
|
99
|
+
else:
|
|
100
|
+
for idx, ev in enumerate(self.evidence):
|
|
101
|
+
line = (
|
|
102
|
+
f"- Claim {idx+1}: {ev.claim.subject} -> {ev.claim.predicate}: {ev.claim.value} "
|
|
103
|
+
f"(Type: {ev.claim.claim_type.value})\n"
|
|
104
|
+
f" Status: {ev.status.value.upper()}\n"
|
|
105
|
+
f" Expected: {ev.expected_value or 'N/A'}, Actual: {ev.actual_value or 'N/A'}\n"
|
|
106
|
+
f" Source: {ev.source} (Confidence: {ev.confidence})\n"
|
|
107
|
+
f" Reason: {ev.reason or 'N/A'}"
|
|
108
|
+
)
|
|
109
|
+
evidence_lines.append(line)
|
|
110
|
+
|
|
111
|
+
return "\n".join(evidence_lines)
|
|
112
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Verification pipeline orchestrating claim extraction and verifications."""
|
|
2
|
+
|
|
3
|
+
from framework.models import AgentOutput
|
|
4
|
+
from .base import BaseVerifier
|
|
5
|
+
from .extractor import ClaimExtractor
|
|
6
|
+
from .models import VerificationReport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VerificationPipeline:
|
|
10
|
+
"""Orchestrates factual claim extraction and ground-truth verification.
|
|
11
|
+
|
|
12
|
+
Wraps the pipeline execution: Agent Output -> Extraction -> Verifications -> VerificationReport.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, extractor: ClaimExtractor, verifier: BaseVerifier):
|
|
16
|
+
"""Initializes the VerificationPipeline.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
extractor: The extractor instance to fetch claims.
|
|
20
|
+
verifier: The verifier instance to verify claims.
|
|
21
|
+
"""
|
|
22
|
+
self.extractor = extractor
|
|
23
|
+
self.verifier = verifier
|
|
24
|
+
|
|
25
|
+
def run(self, output: AgentOutput) -> VerificationReport:
|
|
26
|
+
"""Executes the verification pipeline on the agent output.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
output: The output to analyze.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A VerificationReport containing claims and evidence metrics.
|
|
33
|
+
"""
|
|
34
|
+
claims = self.extractor.extract(output)
|
|
35
|
+
evidence = self.verifier.verify_all(claims)
|
|
36
|
+
return VerificationReport(claims=claims, evidence=evidence)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Prompts for the factual verification subsystem."""
|
|
2
|
+
|
|
3
|
+
CLAIM_EXTRACTION_PROMPT = (
|
|
4
|
+
"You are a precise factual information extraction assistant.\n"
|
|
5
|
+
"Your task is to analyze the agent's travel itinerary output and extract a focused "
|
|
6
|
+
"set of testable factual claims (LIMIT to a maximum of 10 total claims).\n\n"
|
|
7
|
+
"CRITICAL RULES:\n"
|
|
8
|
+
"1. Do NOT extract repetitive or trivial claims. Do NOT assert 'exists' or 'distance' for every "
|
|
9
|
+
"location or attraction listed in the itinerary.\n"
|
|
10
|
+
"2. Focus strictly on the most important concrete factual assertions: transit durations (e.g. Shinkansen duration), "
|
|
11
|
+
"specific ticket/hotel costs, closed days, or operating schedules.\n"
|
|
12
|
+
"3. Do NOT extract subjective opinions (e.g., 'beautiful view', 'great atmosphere').\n\n"
|
|
13
|
+
"You must return a raw JSON array of objects. Do not include markdown headers, "
|
|
14
|
+
"conversational filler, or explanations. Return ONLY the valid JSON block.\n\n"
|
|
15
|
+
"JSON Schema:\n"
|
|
16
|
+
"[\n"
|
|
17
|
+
" {\n"
|
|
18
|
+
" \"subject\": \"string (e.g., 'Shinkansen Tokyo to Kyoto', 'Gyeongbokgung Palace')\",\n"
|
|
19
|
+
" \"predicate\": \"string (e.g., 'travel_duration_minutes', 'closed_days', 'ticket_price')\",\n"
|
|
20
|
+
" \"value\": \"string (the asserted fact, e.g., '140', 'Tuesday', '3000 KRW')\",\n"
|
|
21
|
+
" \"claim_type\": \"string (must be one of: 'price', 'timing', 'existence', 'distance', 'weather', 'other')\"\n"
|
|
22
|
+
" }\n"
|
|
23
|
+
"]"
|
|
24
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Verification subsystem helper utilities."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def normalize_key(text: str) -> str:
|
|
5
|
+
"""Normalizes keys for robust string matching (lowercase alphanumeric)."""
|
|
6
|
+
return "".join(c for c in text.lower() if c.isalnum())
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _clean_numeric_value(val: str) -> str:
|
|
10
|
+
"""Removes common currency symbols, commas, and currency codes, leaving alphanumeric/numeric chars."""
|
|
11
|
+
val = val.upper()
|
|
12
|
+
for clean_target in [
|
|
13
|
+
"JPY",
|
|
14
|
+
"INR",
|
|
15
|
+
"USD",
|
|
16
|
+
"KRW",
|
|
17
|
+
"EUR",
|
|
18
|
+
"GBP",
|
|
19
|
+
"YEN",
|
|
20
|
+
"RUPEES",
|
|
21
|
+
"¥",
|
|
22
|
+
"₹",
|
|
23
|
+
"$",
|
|
24
|
+
"€",
|
|
25
|
+
"£",
|
|
26
|
+
",",
|
|
27
|
+
]:
|
|
28
|
+
val = val.replace(clean_target, "")
|
|
29
|
+
return val.strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def values_are_equivalent(expected: str, actual: str) -> bool:
|
|
33
|
+
"""Checks if two values are equivalent, handling numeric/currency normalizations."""
|
|
34
|
+
# 1. Direct normalized string check
|
|
35
|
+
norm_expected = expected.strip().lower()
|
|
36
|
+
norm_actual = actual.strip().lower()
|
|
37
|
+
if norm_expected == norm_actual:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
# 2. Try cleaning numeric values (e.g., "3000 JPY" vs "¥3,000")
|
|
41
|
+
clean_exp = _clean_numeric_value(expected)
|
|
42
|
+
clean_act = _clean_numeric_value(actual)
|
|
43
|
+
|
|
44
|
+
if clean_exp == clean_act:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
# 3. Try parsing as float if they are numeric
|
|
48
|
+
try:
|
|
49
|
+
if float(clean_exp) == float(clean_act):
|
|
50
|
+
return True
|
|
51
|
+
except ValueError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
return False
|
ui/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Guided local web UI package for EvalRun."""
|