structverify 0.3.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.
- structverify/__init__.py +83 -0
- structverify/adaptation/__init__.py +0 -0
- structverify/adaptation/adapter_trainer.py +341 -0
- structverify/adaptation/feedback_store.py +31 -0
- structverify/adaptation/kosis_crawler.py +317 -0
- structverify/adaptation/sample_builder.py +149 -0
- structverify/adaptation/synthetic_generator.py +320 -0
- structverify/adaptation/update_embeddings.py +178 -0
- structverify/agent/__init__.py +21 -0
- structverify/agent/builder_agent.py +226 -0
- structverify/agent/conformance_agent.py +171 -0
- structverify/agent/dependency_planner.py +151 -0
- structverify/agent/indexing_agent.py +153 -0
- structverify/agent/indexing_planner.py +169 -0
- structverify/agent/integration_example.py +182 -0
- structverify/agent/loop.py +1165 -0
- structverify/agent/memory.py +207 -0
- structverify/agent/planner.py +817 -0
- structverify/agent/prompts/__init__.py +15 -0
- structverify/agent/prompts/planner_prompts.py +219 -0
- structverify/agent/prompts/reflect_prompts.py +387 -0
- structverify/agent/reflect.py +227 -0
- structverify/agent/runtime_agent.py +1272 -0
- structverify/agent/schemas.py +262 -0
- structverify/agent/source_profiler.py +229 -0
- structverify/agent/tools/__init__.py +64 -0
- structverify/agent/tools/base.py +222 -0
- structverify/agent/tools/calculate.py +244 -0
- structverify/agent/tools/catalog_search.py +859 -0
- structverify/agent/tools/deep_explore.py +293 -0
- structverify/agent/tools/explore_catalog.py +423 -0
- structverify/agent/tools/fetch_evidence.py +922 -0
- structverify/agent/tools/finish.py +423 -0
- structverify/agent/tools/meta_explore.py +267 -0
- structverify/agent/tools/query_rewriter.py +134 -0
- structverify/agent/tools/read_original.py +144 -0
- structverify/agent/tools/replan.py +365 -0
- structverify/agent/workspace.py +958 -0
- structverify/api.py +804 -0
- structverify/config/default.yaml +350 -0
- structverify/core/__init__.py +0 -0
- structverify/core/config_loader.py +30 -0
- structverify/core/pipeline.py +280 -0
- structverify/core/schemas.py +362 -0
- structverify/detection/__init__.py +26 -0
- structverify/detection/_config.py +163 -0
- structverify/detection/_llm.py +24 -0
- structverify/detection/candidate/__init__.py +1 -0
- structverify/detection/candidate/heuristic.py +60 -0
- structverify/detection/candidate/llm.py +51 -0
- structverify/detection/candidate_scorer.py +81 -0
- structverify/detection/claim_detector.py +164 -0
- structverify/detection/claims/__init__.py +1 -0
- structverify/detection/claims/worthiness.py +142 -0
- structverify/detection/domain/__init__.py +1 -0
- structverify/detection/domain/classify.py +84 -0
- structverify/detection/domain/preview.py +36 -0
- structverify/detection/domain/registry.py +99 -0
- structverify/detection/domain_classifier.py +75 -0
- structverify/detection/prompts/__init__.py +1 -0
- structverify/detection/prompts/candidate.py +38 -0
- structverify/detection/prompts/claim_worthiness.py +48 -0
- structverify/detection/prompts/domain.py +41 -0
- structverify/detection/prompts/schema.py +508 -0
- structverify/detection/prompts_loader.py +167 -0
- structverify/detection/schema/__init__.py +1 -0
- structverify/detection/schema/expand.py +83 -0
- structverify/detection/schema/induce.py +441 -0
- structverify/detection/schema/regenerate.py +162 -0
- structverify/detection/schema/temporal_hints.py +130 -0
- structverify/detection/schema/validate.py +193 -0
- structverify/detection/schema_inductor.py +112 -0
- structverify/detection/synthetic_generator.py +270 -0
- structverify/explanation/__init__.py +0 -0
- structverify/explanation/_config.py +18 -0
- structverify/explanation/_llm.py +25 -0
- structverify/explanation/explainer.py +183 -0
- structverify/explanation/fallback.py +29 -0
- structverify/explanation/formatters.py +75 -0
- structverify/explanation/prompts/__init__.py +1 -0
- structverify/explanation/prompts/match.py +27 -0
- structverify/explanation/prompts/mismatch.py +20 -0
- structverify/explanation/prompts/multihop.py +16 -0
- structverify/explanation/prompts/unverifiable.py +17 -0
- structverify/graph/__init__.py +0 -0
- structverify/graph/claim_graph.py +226 -0
- structverify/graph/document_graph.py +487 -0
- structverify/graph/graph_builder.py +238 -0
- structverify/graph/graph_multihop.py +335 -0
- structverify/graph/graph_store.py +281 -0
- structverify/graph/provenance.py +52 -0
- structverify/memory/__init__.py +44 -0
- structverify/memory/agent_memory.py +142 -0
- structverify/memory/embedder.py +69 -0
- structverify/memory/exemplar_store.py +241 -0
- structverify/memory/normalizer.py +91 -0
- structverify/memory/schema.py +119 -0
- structverify/memory/storage/__init__.py +29 -0
- structverify/memory/storage/jsonl_store.py +117 -0
- structverify/memory/working_memory.py +370 -0
- structverify/preprocessing/Dockerfile.scraper +27 -0
- structverify/preprocessing/__init__.py +0 -0
- structverify/preprocessing/extractor.py +574 -0
- structverify/preprocessing/pdf/__init__.py +16 -0
- structverify/preprocessing/pdf/fields.py +95 -0
- structverify/preprocessing/pdf/markdown.py +107 -0
- structverify/preprocessing/pdf/models.py +34 -0
- structverify/preprocessing/pdf/ocr.py +172 -0
- structverify/preprocessing/pdf/pipeline.py +74 -0
- structverify/preprocessing/pdf/reader.py +119 -0
- structverify/preprocessing/pdf/scoring.py +61 -0
- structverify/preprocessing/scraper_sandbox.py +561 -0
- structverify/preprocessing/segmenter.py +48 -0
- structverify/preprocessing/sir_builder.py +240 -0
- structverify/progress.py +591 -0
- structverify/retrieval/__init__.py +0 -0
- structverify/retrieval/base.py +208 -0
- structverify/retrieval/base_connector.py +85 -0
- structverify/retrieval/catalog_ranker.py +300 -0
- structverify/retrieval/catalog_search.py +583 -0
- structverify/retrieval/chunking.py +92 -0
- structverify/retrieval/custom_csv_source.py +386 -0
- structverify/retrieval/custom_db_source.py +396 -0
- structverify/retrieval/custom_docs_source.py +152 -0
- structverify/retrieval/dimension_resolver.py +281 -0
- structverify/retrieval/evidence_subgraph.py +63 -0
- structverify/retrieval/kosis_connector.py +1192 -0
- structverify/retrieval/kosis_relevance.py +142 -0
- structverify/retrieval/kosis_source.py +1541 -0
- structverify/retrieval/query_builder.py +72 -0
- structverify/retrieval/registry.py +133 -0
- structverify/retrieval/relevance_judge.py +141 -0
- structverify/retrieval/row_matcher.py +267 -0
- structverify/storage/__init__.py +0 -0
- structverify/storage/db_manager.py +157 -0
- structverify/storage/dwh_manager.py +92 -0
- structverify/storage/init_db.py +99 -0
- structverify/storage/raw_storage.py +29 -0
- structverify/training/__init__.py +26 -0
- structverify/training/curator.py +124 -0
- structverify/training/dataset.py +134 -0
- structverify/training/doctor.py +99 -0
- structverify/training/evalgate.py +96 -0
- structverify/training/generate.py +101 -0
- structverify/training/loop.py +116 -0
- structverify/training/recipe/train_mlx.py +99 -0
- structverify/training/recipe/train_qlora.py +104 -0
- structverify/training/tasks.py +79 -0
- structverify/utils/__init__.py +0 -0
- structverify/utils/embedding_client.py +248 -0
- structverify/utils/llm_client.py +809 -0
- structverify/utils/logger.py +81 -0
- structverify/verification/__init__.py +0 -0
- structverify/verification/_config.py +45 -0
- structverify/verification/adapters.py +405 -0
- structverify/verification/conformance.py +117 -0
- structverify/verification/decide_verdict.py +216 -0
- structverify/verification/decide_verdict_agent.py +454 -0
- structverify/verification/growth_diff.py +267 -0
- structverify/verification/row_match.py +345 -0
- structverify/verification/units.py +64 -0
- structverify/verification/verdict_thresholds.py +232 -0
- structverify/verification/verifier.py +84 -0
- structverify-0.3.0.dist-info/METADATA +903 -0
- structverify-0.3.0.dist-info/RECORD +168 -0
- structverify-0.3.0.dist-info/WHEEL +5 -0
- structverify-0.3.0.dist-info/licenses/LICENSE +21 -0
- structverify-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""[리팩] Step 8 판정 메인 진입 — profile별 분기 (fallback 우선)"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from structverify.core.schemas import (
|
|
7
|
+
Claim,
|
|
8
|
+
Evidence,
|
|
9
|
+
MismatchType,
|
|
10
|
+
VerificationResult,
|
|
11
|
+
VerdictType,
|
|
12
|
+
)
|
|
13
|
+
from structverify.utils.logger import get_logger
|
|
14
|
+
|
|
15
|
+
from ._config import VerificationProfile
|
|
16
|
+
from .adapters import (
|
|
17
|
+
AgentCalculateInput,
|
|
18
|
+
AgentFetchInput,
|
|
19
|
+
NormalizedInput,
|
|
20
|
+
VerdictDecision,
|
|
21
|
+
)
|
|
22
|
+
from .decide_verdict_agent import (
|
|
23
|
+
decide_verdict_agent_calculate,
|
|
24
|
+
decide_verdict_agent_fetch,
|
|
25
|
+
)
|
|
26
|
+
from .growth_diff import verify_growth_or_diff
|
|
27
|
+
from .row_match import extract_numeric_values, find_best_match
|
|
28
|
+
from .units import is_same_unit_type, normalize_value
|
|
29
|
+
from .verdict_thresholds import verdict_from_error
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def decide_verdict(
|
|
35
|
+
claim: Claim,
|
|
36
|
+
normalized: NormalizedInput | AgentFetchInput | AgentCalculateInput,
|
|
37
|
+
config: dict | None = None,
|
|
38
|
+
profile: VerificationProfile = "fallback",
|
|
39
|
+
) -> VerificationResult | VerdictDecision:
|
|
40
|
+
"""claim + 정규화된 입력 → VerificationResult(fallback) 또는 VerdictDecision(agent)."""
|
|
41
|
+
config = config or {}
|
|
42
|
+
if profile == "fallback":
|
|
43
|
+
if not isinstance(normalized, NormalizedInput):
|
|
44
|
+
raise TypeError("profile='fallback' requires NormalizedInput")
|
|
45
|
+
return _decide_verdict_fallback(claim, normalized, config)
|
|
46
|
+
if profile == "agent":
|
|
47
|
+
if isinstance(normalized, AgentFetchInput):
|
|
48
|
+
return decide_verdict_agent_fetch(claim, normalized, config)
|
|
49
|
+
if isinstance(normalized, AgentCalculateInput):
|
|
50
|
+
return decide_verdict_agent_calculate(claim, normalized, config)
|
|
51
|
+
raise TypeError("profile='agent' requires AgentFetchInput or AgentCalculateInput")
|
|
52
|
+
raise NotImplementedError(f"profile={profile!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _decide_verdict_fallback(
|
|
56
|
+
claim: Claim,
|
|
57
|
+
normalized: NormalizedInput,
|
|
58
|
+
config: dict,
|
|
59
|
+
) -> VerificationResult:
|
|
60
|
+
evidence = normalized.evidence
|
|
61
|
+
claim_year = normalized.claim_year
|
|
62
|
+
claim_year_month = normalized.claim_year_month
|
|
63
|
+
|
|
64
|
+
claimed = claim.schema.value if claim.schema else None
|
|
65
|
+
claim_unit = (claim.schema.unit or "") if claim.schema else ""
|
|
66
|
+
|
|
67
|
+
prev_value = getattr(claim.schema, "prev_value", None) if claim.schema else None
|
|
68
|
+
# [v6.14 C2] 증가율/차이 자동 계산 분기
|
|
69
|
+
if prev_value is not None and prev_value != 0:
|
|
70
|
+
indicator = (claim.schema.indicator or "") if claim.schema else ""
|
|
71
|
+
is_ratio_schema = claim_unit and (
|
|
72
|
+
"%" in claim_unit
|
|
73
|
+
or "퍼센트" in claim_unit
|
|
74
|
+
or "율" in claim_unit
|
|
75
|
+
or "비율" in claim_unit
|
|
76
|
+
)
|
|
77
|
+
is_diff_schema = (
|
|
78
|
+
"차이" in indicator or "증감" in indicator or "변화량" in indicator
|
|
79
|
+
)
|
|
80
|
+
if is_ratio_schema or is_diff_schema:
|
|
81
|
+
calc_result = verify_growth_or_diff(
|
|
82
|
+
claim,
|
|
83
|
+
evidence,
|
|
84
|
+
claim_year,
|
|
85
|
+
claim_year_month,
|
|
86
|
+
prev_value,
|
|
87
|
+
is_ratio_schema,
|
|
88
|
+
config,
|
|
89
|
+
classify_mismatch=_classify_mismatch,
|
|
90
|
+
)
|
|
91
|
+
if calc_result is not None:
|
|
92
|
+
return calc_result
|
|
93
|
+
|
|
94
|
+
raw = evidence.raw_response if isinstance(evidence.raw_response, dict) else {}
|
|
95
|
+
rows = raw.get("row", [])
|
|
96
|
+
if isinstance(rows, list) and rows:
|
|
97
|
+
kosis_values = extract_numeric_values(rows)
|
|
98
|
+
if kosis_values:
|
|
99
|
+
best_match, best_error = find_best_match(
|
|
100
|
+
claimed,
|
|
101
|
+
claim_unit,
|
|
102
|
+
claim_year,
|
|
103
|
+
kosis_values,
|
|
104
|
+
claim_year_month=claim_year_month,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if best_match is None:
|
|
108
|
+
return VerificationResult(
|
|
109
|
+
claim_id=claim.claim_id,
|
|
110
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
111
|
+
confidence=0.3,
|
|
112
|
+
evidence=evidence,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
evidence = evidence.model_copy(update={
|
|
116
|
+
"official_value": best_match.get("value"),
|
|
117
|
+
"unit": best_match.get("unit") or evidence.unit,
|
|
118
|
+
"time_period": best_match.get("period") or evidence.time_period,
|
|
119
|
+
})
|
|
120
|
+
logger.info(
|
|
121
|
+
f"[verifier] evidence 동기화 (F2): official_value={evidence.official_value} "
|
|
122
|
+
f"unit={evidence.unit!r} time_period={evidence.time_period!r}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return verdict_from_error(
|
|
126
|
+
claim,
|
|
127
|
+
evidence,
|
|
128
|
+
best_error,
|
|
129
|
+
best_match,
|
|
130
|
+
config,
|
|
131
|
+
_classify_mismatch,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
kosis_unit = evidence.unit or ""
|
|
135
|
+
|
|
136
|
+
if not is_same_unit_type(claim_unit, kosis_unit):
|
|
137
|
+
logger.info(f"단위 타입 불일치: claim={claim_unit!r} kosis={kosis_unit!r}")
|
|
138
|
+
return VerificationResult(
|
|
139
|
+
claim_id=claim.claim_id,
|
|
140
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
141
|
+
confidence=0.3,
|
|
142
|
+
evidence=evidence,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
official = normalize_value(evidence.official_value, kosis_unit)
|
|
146
|
+
denom = max(abs(official), abs(claimed), 1e-9)
|
|
147
|
+
diff_pct = abs(claimed - official) / denom * 100
|
|
148
|
+
|
|
149
|
+
return verdict_from_error(
|
|
150
|
+
claim, evidence, diff_pct / 100, None, config, _classify_mismatch,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _primary_year_from_period(text: str | None) -> str | None:
|
|
155
|
+
if not text or not str(text).strip():
|
|
156
|
+
return None
|
|
157
|
+
m = re.search(r"(?:19|20)\d{2}", str(text))
|
|
158
|
+
return m.group(0) if m else None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _norm_token(s: str | None) -> str:
|
|
162
|
+
if not s:
|
|
163
|
+
return ""
|
|
164
|
+
return " ".join(str(s).split()).lower()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _population_incompatible(claim_pop: str | None, ev_pop: str | None) -> bool:
|
|
168
|
+
c = _norm_token(claim_pop)
|
|
169
|
+
e = _norm_token(ev_pop)
|
|
170
|
+
if not c or not e:
|
|
171
|
+
return False
|
|
172
|
+
if c in e or e in c:
|
|
173
|
+
return False
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _classify_mismatch(
|
|
178
|
+
claim: Claim,
|
|
179
|
+
evidence: Evidence,
|
|
180
|
+
diff_pct: float,
|
|
181
|
+
config: dict,
|
|
182
|
+
) -> MismatchType:
|
|
183
|
+
"""
|
|
184
|
+
MISMATCH 세부 유형 분류 (fallback 프로필, LLM 미사용).
|
|
185
|
+
우선순위: TIME_PERIOD → POPULATION → EXAGGERATION → VALUE
|
|
186
|
+
"""
|
|
187
|
+
vconf = config.get("verification", {}) if config else {}
|
|
188
|
+
exaggeration_pct = float(vconf.get("exaggeration_diff_percent", 20.0))
|
|
189
|
+
|
|
190
|
+
schema = claim.schema
|
|
191
|
+
if schema is None:
|
|
192
|
+
return (
|
|
193
|
+
MismatchType.EXAGGERATION
|
|
194
|
+
if diff_pct > exaggeration_pct
|
|
195
|
+
else MismatchType.VALUE
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# 시점
|
|
199
|
+
cy = _primary_year_from_period(schema.time_period)
|
|
200
|
+
ey = _primary_year_from_period(evidence.time_period)
|
|
201
|
+
if cy and ey and cy != ey:
|
|
202
|
+
return MismatchType.TIME_PERIOD
|
|
203
|
+
|
|
204
|
+
# 집단
|
|
205
|
+
raw = evidence.raw_response if isinstance(evidence.raw_response, dict) else {}
|
|
206
|
+
ev_pop = raw.get("population") or raw.get("population_label")
|
|
207
|
+
if isinstance(ev_pop, (list, tuple)):
|
|
208
|
+
ev_pop = " ".join(str(x) for x in ev_pop)
|
|
209
|
+
if schema.population and _population_incompatible(schema.population, ev_pop):
|
|
210
|
+
return MismatchType.POPULATION
|
|
211
|
+
|
|
212
|
+
# 과장
|
|
213
|
+
if diff_pct > exaggeration_pct:
|
|
214
|
+
return MismatchType.EXAGGERATION
|
|
215
|
+
|
|
216
|
+
return MismatchType.VALUE
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""[리팩] agent 프로필 판정 — loop._synthesize_verdict_* 로직 추출 (이동만)"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from structverify.agent.schemas import ClaimType
|
|
8
|
+
from structverify.core.schemas import Claim, VerdictType
|
|
9
|
+
from structverify.utils.logger import get_logger
|
|
10
|
+
|
|
11
|
+
from .adapters import AgentCalculateInput, AgentFetchInput, VerdictDecision
|
|
12
|
+
from .growth_diff import try_difference_from_rows, try_growth_rate_from_rows
|
|
13
|
+
from .verdict_thresholds import (
|
|
14
|
+
classify_atomic_ratio_agent,
|
|
15
|
+
classify_calculate_simple_agent,
|
|
16
|
+
classify_difference_gap_agent,
|
|
17
|
+
classify_growth_rate_pp_agent,
|
|
18
|
+
detect_threshold_direction,
|
|
19
|
+
growth_rate_direction_mismatch,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
_COMPLEX_TYPES = {ClaimType.GROWTH_RATE, ClaimType.DIFFERENCE, ClaimType.RANKING}
|
|
25
|
+
|
|
26
|
+
# 한국어 큰 수 단위 배수 (선두 접두어). "만 건"→10⁴·"건", "억달러"→10⁸·"달러".
|
|
27
|
+
_KOR_MULT = {"천": 1e3, "만": 1e4, "억": 1e8, "조": 1e12}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _kor_unit_scale(unit: str | None) -> tuple[float, str]:
|
|
31
|
+
"""단위 문자열 선두의 만/억/조/천 배수와 기본단위를 분리.
|
|
32
|
+
|
|
33
|
+
스키마 유도가 "228만 건"을 (228, "만 건")처럼 *값은 접두어 앞 숫자, 단위엔 접두어*
|
|
34
|
+
로 뽑을 때가 있다("150만 명"은 (1500000,"명")으로 뽑기도 함 — LLM 비일관). 어느
|
|
35
|
+
쪽이든 '값이 단위 표기와 일치'하므로, 비교 직전 단위 배수를 값에 반영하면 일관돼진다.
|
|
36
|
+
|
|
37
|
+
Returns: (배수, 기본단위) 예: "만 건"→(10000.0,"건"), "건"→(1.0,"건").
|
|
38
|
+
"""
|
|
39
|
+
u = (unit or "").strip()
|
|
40
|
+
m = re.match(r"^\s*(천|만|억|조)\s*(.*)$", u)
|
|
41
|
+
if m:
|
|
42
|
+
return _KOR_MULT[m.group(1)], m.group(2).strip()
|
|
43
|
+
return 1.0, u
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def decide_verdict_agent_fetch(
|
|
47
|
+
claim: Claim,
|
|
48
|
+
normalized: AgentFetchInput,
|
|
49
|
+
config: dict,
|
|
50
|
+
) -> VerdictDecision:
|
|
51
|
+
"""fetch observation 기반 agent 판정 (loop._synthesize_verdict_from_observation).
|
|
52
|
+
|
|
53
|
+
합성 규칙:
|
|
54
|
+
- fetch 성공 + claim에 값 있음 → 값 비교 (tolerance, 기본 5%)
|
|
55
|
+
- growth_rate/difference/ranking → 두 시점 비교 필요인데 단일 fetch 뿐 → UNVERIFIABLE
|
|
56
|
+
(단, GROWTH_RATE/DIFFERENCE는 rows pool에서 직접 계산 시도)
|
|
57
|
+
- fetch 실패 또는 값 없음 → UNVERIFIABLE
|
|
58
|
+
"""
|
|
59
|
+
claim_id = normalized.claim_id
|
|
60
|
+
evidence = normalized.evidence
|
|
61
|
+
tolerance = normalized.tolerance
|
|
62
|
+
claim_actual_type = normalized.claim_actual_type
|
|
63
|
+
|
|
64
|
+
fetched_value = evidence.get("value")
|
|
65
|
+
fetched_unit = evidence.get("unit", "") or ""
|
|
66
|
+
fetched_time = evidence.get("time_period", "") or ""
|
|
67
|
+
stat_table_id = evidence.get("stat_table_id", "") or ""
|
|
68
|
+
stat_name = evidence.get("stat_name", "") or ""
|
|
69
|
+
|
|
70
|
+
# 소스 인식 라벨 (KOSIS 전용 하드코딩 제거 — custom_csv 등도 지원). 모든 branch에서 사용.
|
|
71
|
+
_src = str(evidence.get("source") or "").strip()
|
|
72
|
+
_src_disp = {"kosis": "KOSIS", "custom_csv": "기준 데이터", "custom_db": "기준 데이터"}.get(
|
|
73
|
+
_src, _src or "기준 데이터"
|
|
74
|
+
)
|
|
75
|
+
src_label = _src_disp + (f"({stat_table_id})" if stat_table_id else "")
|
|
76
|
+
if stat_name:
|
|
77
|
+
src_label += f" {stat_name}"
|
|
78
|
+
|
|
79
|
+
schema = claim.schema
|
|
80
|
+
claim_value = schema.value if schema is not None else None
|
|
81
|
+
claim_unit = (schema.unit or "") if schema is not None else ""
|
|
82
|
+
claim_time = (schema.time_period or "") if schema is not None else ""
|
|
83
|
+
claim_indicator = (schema.indicator or "") if schema is not None else ""
|
|
84
|
+
|
|
85
|
+
# 복합 claim type: 두 시점 비교 필요인데 plan은 단일 fetch
|
|
86
|
+
# ★ plan.claim_type은 Planner LLM이 source_text 의미로 일괄 분류해서 부정확함
|
|
87
|
+
# → claim.schema에서 직접 추론한 type을 더 신뢰
|
|
88
|
+
if isinstance(claim_actual_type, ClaimType) and claim_actual_type in _COMPLEX_TYPES:
|
|
89
|
+
# ── [v6.17] GROWTH_RATE 직접 계산 시도 ──────────────────────────
|
|
90
|
+
if claim_actual_type == ClaimType.GROWTH_RATE:
|
|
91
|
+
result = _try_growth_rate_verdict(
|
|
92
|
+
claim, claim_id, evidence, schema, claim_value, claim_indicator,
|
|
93
|
+
stat_table_id, normalized.all_fetch_observations, config,
|
|
94
|
+
)
|
|
95
|
+
if result is not None:
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
if claim_actual_type == ClaimType.DIFFERENCE:
|
|
99
|
+
result = _try_difference_verdict(
|
|
100
|
+
claim, claim_id, evidence, schema, claim_value,
|
|
101
|
+
stat_table_id, normalized.all_fetch_observations, config,
|
|
102
|
+
)
|
|
103
|
+
if result is not None:
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
logger.info(
|
|
107
|
+
f"[loop] {claim_id}: claim type={claim_actual_type.value} "
|
|
108
|
+
f"(planner type={normalized.plan_claim_type.value}) — 단일 fetch로 검증 불가"
|
|
109
|
+
)
|
|
110
|
+
return VerdictDecision(
|
|
111
|
+
claim_id=claim_id,
|
|
112
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
113
|
+
confidence=0.3,
|
|
114
|
+
explanation=(
|
|
115
|
+
f"{claim_actual_type.value} 유형은 두 시점 비교 필요. "
|
|
116
|
+
f"{src_label} 현재값 {fetched_value!r}{fetched_unit} "
|
|
117
|
+
f"(시점 {fetched_time}) 확보. "
|
|
118
|
+
f"이전 시점 데이터 부재로 검증 불가."
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if fetched_value is None or claim_value is None:
|
|
123
|
+
return VerdictDecision(
|
|
124
|
+
claim_id=claim_id,
|
|
125
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
126
|
+
confidence=0.3,
|
|
127
|
+
explanation=(
|
|
128
|
+
f"비교 불가: 주장값={claim_value!r}{claim_unit}, "
|
|
129
|
+
f"{src_label} 조회값={fetched_value!r}{fetched_unit}."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
fv = float(fetched_value)
|
|
135
|
+
cv = float(claim_value)
|
|
136
|
+
except (TypeError, ValueError):
|
|
137
|
+
return VerdictDecision(
|
|
138
|
+
claim_id=claim_id,
|
|
139
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
140
|
+
confidence=0.3,
|
|
141
|
+
explanation=(
|
|
142
|
+
f"값 숫자 변환 실패 — 주장값={claim_value!r}, 조회값={fetched_value!r}."
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# 한국어 큰 수 단위(만/억/조) 스케일 정규화 — "228만 건" vs "건" 같은 단위 스케일
|
|
147
|
+
# 불일치 구제. 단, *스케일링이 두 값을 더 가깝게 만들 때만* 적용한다.
|
|
148
|
+
# - 오히려 멀어지면: 단위 표기가 어긋났거나(예: 스키마가 "억"을 떨궈 잘못된 단위)
|
|
149
|
+
# 값이 이미 완전 환산된 것(이중스케일) → 건드리면 오판. 그대로 둔다.
|
|
150
|
+
# 이 가드로 [만 건 vs 건] 구제와 [잘못된 단위·이중스케일] 회귀 방지를 동시에.
|
|
151
|
+
_cs, _cbase = _kor_unit_scale(claim_unit)
|
|
152
|
+
_fs, _fbase = _kor_unit_scale(fetched_unit)
|
|
153
|
+
if (_cs != 1.0 or _fs != 1.0) and (_cbase == _fbase or not _cbase or not _fbase):
|
|
154
|
+
def _dr(a: float, b: float) -> float:
|
|
155
|
+
return abs(b - a) / abs(a) if abs(a) > 1e-9 else (0.0 if abs(b) < 1e-9 else 1.0)
|
|
156
|
+
if _dr(cv * _cs, fv * _fs) < _dr(cv, fv):
|
|
157
|
+
_cv0, _fv0 = cv, fv
|
|
158
|
+
cv, fv = cv * _cs, fv * _fs
|
|
159
|
+
claim_unit, fetched_unit = (_cbase or claim_unit), (_fbase or fetched_unit)
|
|
160
|
+
logger.info(
|
|
161
|
+
f"[loop] {claim_id}: 단위 스케일 정규화(값이 가까워짐) "
|
|
162
|
+
f"주장 {_cv0:.4g}×{_cs:g}={cv:.4g}{claim_unit}, "
|
|
163
|
+
f"조회 {_fv0:.4g}×{_fs:g}={fv:.4g}{fetched_unit}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if abs(cv) < 1e-9:
|
|
167
|
+
diff_ratio = 0.0 if abs(fv) < 1e-9 else 1.0
|
|
168
|
+
else:
|
|
169
|
+
diff_ratio = abs(fv - cv) / abs(cv)
|
|
170
|
+
|
|
171
|
+
time_aligned = True
|
|
172
|
+
if claim_time and fetched_time:
|
|
173
|
+
ct_norm = str(claim_time).replace("-", "").replace(".", "")
|
|
174
|
+
ft_norm = str(fetched_time).replace("-", "").replace(".", "")
|
|
175
|
+
time_aligned = (ct_norm in ft_norm) or (ft_norm in ct_norm)
|
|
176
|
+
|
|
177
|
+
# [규정/예산] evidence에 규칙 operator가 실려오면 *규칙 기준 준수* 판정.
|
|
178
|
+
# fv=기준값(한도/최소), cv=신청/주장값. 예: operator '<=' → 신청 cv ≤ 한도 fv 이면 승인(MATCH).
|
|
179
|
+
# (detect_threshold_direction은 '주장 자체가 부등식'인 경우 — 여긴 규칙이 부등식이라 별도)
|
|
180
|
+
_rule_op = str(evidence.get("operator") or "").strip()
|
|
181
|
+
if _rule_op and time_aligned:
|
|
182
|
+
import operator as _opmod
|
|
183
|
+
_OPS = {"<=": _opmod.le, "≤": _opmod.le, "<": _opmod.lt,
|
|
184
|
+
">=": _opmod.ge, "≥": _opmod.ge, ">": _opmod.gt,
|
|
185
|
+
"==": _opmod.eq, "=": _opmod.eq}
|
|
186
|
+
_fn = _OPS.get(_rule_op)
|
|
187
|
+
if _fn is not None:
|
|
188
|
+
_rel = {"<=": "이하", "≤": "이하", "<": "미만", ">=": "이상",
|
|
189
|
+
"≥": "이상", ">": "초과", "==": "일치", "=": "일치"}[_rule_op]
|
|
190
|
+
# 단위 스케일 정규화 — 신청/기준 단위가 둘 다 통화 단위면 '원' 기준으로 맞춤.
|
|
191
|
+
# (스키마 유도가 "6000만원"→6000/만원, 규칙은 50000000/원 처럼 스케일이 달라 오판 방지)
|
|
192
|
+
_WON = {"원": 1, "천원": 1000, "만원": 10000, "십만원": 100000,
|
|
193
|
+
"백만원": 1_000_000, "천만원": 10_000_000, "억원": 100_000_000}
|
|
194
|
+
_cu, _fu = _WON.get((claim_unit or "").strip()), _WON.get((fetched_unit or "").strip())
|
|
195
|
+
_cvn, _fvn = (cv * _cu, fv * _fu) if (_cu and _fu) else (cv, fv)
|
|
196
|
+
_ok = _fn(_cvn, _fvn)
|
|
197
|
+
logger.info(
|
|
198
|
+
f"[loop] {claim_id}: 규칙 operator 판정 신청={cv:.4g} {_rule_op} "
|
|
199
|
+
f"기준={fv:.4g} → {'승인' if _ok else '반려'}"
|
|
200
|
+
)
|
|
201
|
+
return VerdictDecision(
|
|
202
|
+
claim_id=claim_id,
|
|
203
|
+
verdict=VerdictType.MATCH if _ok else VerdictType.MISMATCH,
|
|
204
|
+
confidence=0.9,
|
|
205
|
+
explanation=(
|
|
206
|
+
f"신청/주장값 {cv:.4g}{claim_unit}이(가) 기준 {fv:.4g}{fetched_unit} "
|
|
207
|
+
f"{_rel} 규칙을 {'충족 → 승인' if _ok else '위반 → 반려'} "
|
|
208
|
+
f"({src_label}, 시점 {fetched_time or claim_time})."
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# [v6.20] 부등식 주장 — threshold 방향 감지 후 실측값 vs 기준값 비교
|
|
213
|
+
thr_dir = detect_threshold_direction(claim)
|
|
214
|
+
if thr_dir is not None and time_aligned:
|
|
215
|
+
if thr_dir == "gte":
|
|
216
|
+
satisfied = fv >= cv
|
|
217
|
+
rel_txt = "이상"
|
|
218
|
+
else:
|
|
219
|
+
satisfied = fv <= cv
|
|
220
|
+
rel_txt = "이하"
|
|
221
|
+
logger.info(
|
|
222
|
+
f"[loop] {claim_id}: threshold 판정 dir={thr_dir} "
|
|
223
|
+
f"기준값={cv:.4g} 실측={fv:.4g} → {'충족' if satisfied else '미충족'}"
|
|
224
|
+
)
|
|
225
|
+
if satisfied:
|
|
226
|
+
return VerdictDecision(
|
|
227
|
+
claim_id=claim_id,
|
|
228
|
+
verdict=VerdictType.MATCH,
|
|
229
|
+
confidence=0.8,
|
|
230
|
+
explanation=(
|
|
231
|
+
f"주장은 '{cv:.4g}{claim_unit} {rel_txt}'(부등식)이고, "
|
|
232
|
+
f"{src_label} 조회값은 {fv:.4g}{fetched_unit} "
|
|
233
|
+
f"(시점 {fetched_time or claim_time})이므로 주장이 성립합니다."
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
return VerdictDecision(
|
|
237
|
+
claim_id=claim_id,
|
|
238
|
+
verdict=VerdictType.MISMATCH,
|
|
239
|
+
confidence=0.7,
|
|
240
|
+
explanation=(
|
|
241
|
+
f"주장은 '{cv:.4g}{claim_unit} {rel_txt}'(부등식)이지만, "
|
|
242
|
+
f"{src_label} 조회값은 {fv:.4g}{fetched_unit} "
|
|
243
|
+
f"(시점 {fetched_time or claim_time})이므로 주장이 성립하지 않습니다."
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if diff_ratio < tolerance and time_aligned:
|
|
248
|
+
return VerdictDecision(
|
|
249
|
+
claim_id=claim_id,
|
|
250
|
+
verdict=VerdictType.MATCH,
|
|
251
|
+
confidence=0.85,
|
|
252
|
+
explanation=(
|
|
253
|
+
f"주장값 {cv:.4g}{claim_unit}과 {src_label} 조회값 "
|
|
254
|
+
f"{fv:.4g}{fetched_unit}이 일치 (오차 {diff_ratio*100:.2f}%, "
|
|
255
|
+
f"시점 주장={claim_time or '?'}, 조회={fetched_time or '?'})."
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if not time_aligned:
|
|
260
|
+
return VerdictDecision(
|
|
261
|
+
claim_id=claim_id,
|
|
262
|
+
verdict=VerdictType.UNVERIFIABLE,
|
|
263
|
+
confidence=0.35,
|
|
264
|
+
explanation=(
|
|
265
|
+
f"시점 불일치 — 주장은 {claim_time}, {src_label} 조회는 {fetched_time}. "
|
|
266
|
+
f"동일 시점 데이터 미확보로 검증 불가 "
|
|
267
|
+
f"(조회값 {fv:.4g}{fetched_unit}, 주장값 {cv:.4g}{claim_unit})."
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
verdict_t, conf = classify_atomic_ratio_agent(diff_ratio, config)
|
|
272
|
+
return VerdictDecision(
|
|
273
|
+
claim_id=claim_id,
|
|
274
|
+
verdict=verdict_t,
|
|
275
|
+
confidence=conf,
|
|
276
|
+
explanation=(
|
|
277
|
+
f"주장값 {cv:.4g}{claim_unit}과 {src_label} 조회값 "
|
|
278
|
+
f"{fv:.4g}{fetched_unit}이 {diff_ratio*100:.1f}% 차이 "
|
|
279
|
+
f"(시점 {fetched_time or claim_time})."
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def decide_verdict_agent_calculate(
|
|
285
|
+
claim: Claim,
|
|
286
|
+
normalized: AgentCalculateInput,
|
|
287
|
+
config: dict,
|
|
288
|
+
) -> VerdictDecision:
|
|
289
|
+
"""calculate observation 기반 agent 판정 (loop._synthesize_verdict_from_calculate).
|
|
290
|
+
|
|
291
|
+
LLM이 prev/current를 계산했지만 finish를 안 부르고 다시 같은 액션 반복
|
|
292
|
+
→ 중복차단 → 강제 unverifiable로 죽는 케이스 회복. calculate output의
|
|
293
|
+
result 값을 claim.schema.value와 비교해 자동 verdict 생성한다.
|
|
294
|
+
"""
|
|
295
|
+
claim_id = normalized.claim_id
|
|
296
|
+
calc_value = normalized.calc_value
|
|
297
|
+
claim_actual_type = normalized.claim_actual_type
|
|
298
|
+
calc_summary = normalized.calc_summary
|
|
299
|
+
|
|
300
|
+
schema = claim.schema
|
|
301
|
+
claim_value = schema.value if schema is not None else None
|
|
302
|
+
claim_unit = (schema.unit or "") if schema is not None else ""
|
|
303
|
+
cv = float(claim_value)
|
|
304
|
+
|
|
305
|
+
if isinstance(claim_actual_type, ClaimType) and claim_actual_type == ClaimType.GROWTH_RATE:
|
|
306
|
+
diff = abs(abs(calc_value) - abs(cv))
|
|
307
|
+
verdict_t, conf = classify_growth_rate_pp_agent(diff, config)
|
|
308
|
+
label = "일치" if verdict_t == VerdictType.MATCH else (
|
|
309
|
+
"오차 큼" if verdict_t == VerdictType.UNVERIFIABLE else "불일치"
|
|
310
|
+
)
|
|
311
|
+
diff_desc = f"차이 {diff:.2f}%p"
|
|
312
|
+
elif isinstance(claim_actual_type, ClaimType) and claim_actual_type == ClaimType.DIFFERENCE:
|
|
313
|
+
gap = abs(abs(calc_value) - abs(cv))
|
|
314
|
+
verdict_t, conf = classify_difference_gap_agent(gap, cv, config)
|
|
315
|
+
tol = max(abs(cv) * 0.10, 0.02)
|
|
316
|
+
label = "일치" if verdict_t == VerdictType.MATCH else (
|
|
317
|
+
"오차 큼" if verdict_t == VerdictType.UNVERIFIABLE else "불일치"
|
|
318
|
+
)
|
|
319
|
+
diff_desc = f"차이 {gap:.4f}, 허용 {tol:.4f}"
|
|
320
|
+
else:
|
|
321
|
+
if abs(cv) < 1e-9:
|
|
322
|
+
diff_ratio = 0.0 if abs(calc_value) < 1e-9 else 1.0
|
|
323
|
+
else:
|
|
324
|
+
diff_ratio = abs(calc_value - cv) / abs(cv)
|
|
325
|
+
verdict_t, conf = classify_calculate_simple_agent(diff_ratio, config)
|
|
326
|
+
label = "일치" if verdict_t == VerdictType.MATCH else "불일치"
|
|
327
|
+
diff_desc = f"오차 {diff_ratio*100:.2f}%"
|
|
328
|
+
|
|
329
|
+
logger.info(
|
|
330
|
+
f"[loop] {claim_id}: calculate 합성 판정={label} "
|
|
331
|
+
f"(기사 {cv}{claim_unit} vs 계산 {calc_value:.4g}, {diff_desc})"
|
|
332
|
+
)
|
|
333
|
+
return VerdictDecision(
|
|
334
|
+
claim_id=claim_id,
|
|
335
|
+
verdict=verdict_t,
|
|
336
|
+
confidence=conf,
|
|
337
|
+
explanation=(
|
|
338
|
+
f"Agent가 직접 계산한 결과로 검증: 기사 주장 {cv}{claim_unit}, "
|
|
339
|
+
f"산출된 값 {calc_value:.4g} ({diff_desc}). "
|
|
340
|
+
f"계산식: {calc_summary[:200]}"
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _try_growth_rate_verdict(
|
|
346
|
+
claim: Claim,
|
|
347
|
+
claim_id: str,
|
|
348
|
+
evidence: dict,
|
|
349
|
+
schema: Any,
|
|
350
|
+
claim_value: Any,
|
|
351
|
+
claim_indicator: str,
|
|
352
|
+
stat_table_id: str,
|
|
353
|
+
all_fetch_observations: list | None,
|
|
354
|
+
config: dict,
|
|
355
|
+
) -> VerdictDecision | None:
|
|
356
|
+
calc = try_growth_rate_from_rows(
|
|
357
|
+
evidence, schema, claim_id, all_fetch_observations=all_fetch_observations,
|
|
358
|
+
)
|
|
359
|
+
if calc is None or claim_value is None:
|
|
360
|
+
return None
|
|
361
|
+
calc_rate, _cur_v, _prev_v, calc_desc = calc
|
|
362
|
+
try:
|
|
363
|
+
claimed_rate = float(claim_value)
|
|
364
|
+
except (TypeError, ValueError):
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
if growth_rate_direction_mismatch(claim_indicator, calc_rate):
|
|
368
|
+
# [패치 J] 증가/감소 방향 불일치 — 부호 가드
|
|
369
|
+
diff = abs(abs(calc_rate) - abs(claimed_rate))
|
|
370
|
+
logger.warning(
|
|
371
|
+
f"[loop] {claim_id}: growth_rate 부호 방향 불일치 "
|
|
372
|
+
f"(indicator={claim_indicator!r}, 기사 {claimed_rate:+.2f}% 방향, "
|
|
373
|
+
f"계산 {calc_rate:+.2f}% 반대 방향) → MISMATCH 강제"
|
|
374
|
+
)
|
|
375
|
+
return VerdictDecision(
|
|
376
|
+
claim_id=claim_id,
|
|
377
|
+
verdict=VerdictType.MISMATCH,
|
|
378
|
+
confidence=0.75,
|
|
379
|
+
explanation=(
|
|
380
|
+
f"증가율 방향 불일치: 기사는 '{claim_indicator}' "
|
|
381
|
+
f"{claimed_rate}% (양의 방향), "
|
|
382
|
+
f"KOSIS({stat_table_id}) 표 계산값 "
|
|
383
|
+
f"{calc_rate:.2f}% ({'감소' if calc_rate < 0 else '증가'} 방향). "
|
|
384
|
+
f"{calc_desc}"
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
diff = abs(abs(calc_rate) - abs(claimed_rate))
|
|
389
|
+
verdict_t, conf = classify_growth_rate_pp_agent(diff, config)
|
|
390
|
+
v_label = {
|
|
391
|
+
VerdictType.MATCH: "일치",
|
|
392
|
+
VerdictType.UNVERIFIABLE: "오차 큼",
|
|
393
|
+
VerdictType.MISMATCH: "불일치",
|
|
394
|
+
}[verdict_t]
|
|
395
|
+
logger.info(
|
|
396
|
+
f"[loop] {claim_id}: growth_rate 직접계산 판정={v_label} "
|
|
397
|
+
f"(기사 {claimed_rate}% vs 계산 {calc_rate:.2f}%, 차이 {diff:.2f}%p)"
|
|
398
|
+
)
|
|
399
|
+
return VerdictDecision(
|
|
400
|
+
claim_id=claim_id,
|
|
401
|
+
verdict=verdict_t,
|
|
402
|
+
confidence=conf,
|
|
403
|
+
explanation=(
|
|
404
|
+
f"증가율 직접 검증: 기사 주장 {claimed_rate}%, "
|
|
405
|
+
f"KOSIS({stat_table_id}) 표에서 계산한 값 "
|
|
406
|
+
f"{calc_rate:.2f}% (차이 {diff:.2f}%p). {calc_desc}"
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _try_difference_verdict(
|
|
412
|
+
claim: Claim,
|
|
413
|
+
claim_id: str,
|
|
414
|
+
evidence: dict,
|
|
415
|
+
schema: Any,
|
|
416
|
+
claim_value: Any,
|
|
417
|
+
stat_table_id: str,
|
|
418
|
+
all_fetch_observations: list | None,
|
|
419
|
+
config: dict,
|
|
420
|
+
) -> VerdictDecision | None:
|
|
421
|
+
calc = try_difference_from_rows(
|
|
422
|
+
evidence, schema, claim_id, all_fetch_observations=all_fetch_observations,
|
|
423
|
+
)
|
|
424
|
+
if calc is None or claim_value is None:
|
|
425
|
+
return None
|
|
426
|
+
calc_diff, _cur_v, _prev_v, calc_desc = calc
|
|
427
|
+
try:
|
|
428
|
+
claimed_diff = float(claim_value)
|
|
429
|
+
except (TypeError, ValueError):
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
gap = abs(abs(calc_diff) - abs(claimed_diff))
|
|
433
|
+
verdict_t, conf = classify_difference_gap_agent(gap, claimed_diff, config)
|
|
434
|
+
tol = max(abs(claimed_diff) * 0.10, 0.02)
|
|
435
|
+
v_label = {
|
|
436
|
+
VerdictType.MATCH: "일치",
|
|
437
|
+
VerdictType.UNVERIFIABLE: "오차 큼",
|
|
438
|
+
VerdictType.MISMATCH: "불일치",
|
|
439
|
+
}[verdict_t]
|
|
440
|
+
logger.info(
|
|
441
|
+
f"[loop] {claim_id}: difference 직접계산 판정={v_label} "
|
|
442
|
+
f"(기사 {claimed_diff} vs 계산 {calc_diff:.4f}, "
|
|
443
|
+
f"차이 {gap:.4f}, 허용 {tol:.4f})"
|
|
444
|
+
)
|
|
445
|
+
return VerdictDecision(
|
|
446
|
+
claim_id=claim_id,
|
|
447
|
+
verdict=verdict_t,
|
|
448
|
+
confidence=conf,
|
|
449
|
+
explanation=(
|
|
450
|
+
f"차이값 직접 검증: 기사 주장 {claimed_diff}, "
|
|
451
|
+
f"KOSIS({stat_table_id}) 표에서 계산한 값 "
|
|
452
|
+
f"{calc_diff:.4f} (차이 {gap:.4f}). {calc_desc}"
|
|
453
|
+
),
|
|
454
|
+
)
|