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,362 @@
|
|
|
1
|
+
# [2026-05-14 | 이수민] memory/v1: working memory 도메인 가드용 필드 추가
|
|
2
|
+
# - Evidence.category_path: KOSIS catalog의 카테고리 경로 (예: "인구 > 출생")
|
|
3
|
+
# - MismatchType.DOMAIN_MISMATCH: stat_id 카테고리가 문서 도메인과 어긋난 경우
|
|
4
|
+
"""
|
|
5
|
+
core/schemas.py — v3 전체 파이프라인 데이터 모델
|
|
6
|
+
|
|
7
|
+
v2 변경점
|
|
8
|
+
- Sentence에 regex 중심 has_numeric 대신
|
|
9
|
+
has_numeric_surface + candidate_score/candidate_label 추가
|
|
10
|
+
- claim candidate detection을 독립 태스크로 다루기 위한 필드 추가
|
|
11
|
+
- 기존 has_numeric는 하위 호환을 위해 제거하지 않고 property처럼 대체 가능하게 설계
|
|
12
|
+
|
|
13
|
+
설계 의도
|
|
14
|
+
- surface rule은 보조 신호로만 사용
|
|
15
|
+
- 실제 검증 후보 여부는 candidate_score / candidate_label이 담당
|
|
16
|
+
|
|
17
|
+
v3 변경점 [김예슬]
|
|
18
|
+
- GraphEdgeType에 4개 추가
|
|
19
|
+
- COMPARE 엣지: indicator가 같은 Claim 쌍을 전부 연결
|
|
20
|
+
"쉬었음인구"를 indicator로 가진 C1과 C2가 COMPARE로 연결되면,
|
|
21
|
+
"2.6배" 검증 시 MetricNode 2-hop으로 C1+C2를 함께 KOSIS에 조회해서 비율 계산이 가능
|
|
22
|
+
- 문맥 엣지: sir_doc이 넘어오면 extract_context_edges()를 호출해서 NEXT_SENT/IN_BLOCK/IN_DOC를 GraphEdge로 변환해 반환값에 포함
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Any
|
|
30
|
+
from uuid import UUID, uuid4
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Enums ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
class SourceType(str, Enum):
|
|
38
|
+
URL = "url"
|
|
39
|
+
PDF = "pdf"
|
|
40
|
+
DOCX = "docx"
|
|
41
|
+
TEXT = "text"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BlockType(str, Enum):
|
|
45
|
+
PARAGRAPH = "paragraph"
|
|
46
|
+
TABLE = "table"
|
|
47
|
+
HEADING = "heading"
|
|
48
|
+
LIST = "list"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ClaimType(str, Enum):
|
|
52
|
+
"""주장 유형 — ClaimBuster 계열 분류 확장"""
|
|
53
|
+
INCREASE = "increase"
|
|
54
|
+
DECREASE = "decrease"
|
|
55
|
+
SCALE = "scale"
|
|
56
|
+
COMPARISON = "comparison"
|
|
57
|
+
FORECAST = "forecast"
|
|
58
|
+
# [2026-05-21] aggregation claim 지원 — "최근 N년 평균/총합" 류
|
|
59
|
+
AGGREGATION = "aggregation"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class VerdictType(str, Enum):
|
|
63
|
+
"""판정 결과 — FEVER 3단계 매핑"""
|
|
64
|
+
MATCH = "match"
|
|
65
|
+
MISMATCH = "mismatch"
|
|
66
|
+
UNVERIFIABLE = "unverifiable"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MismatchType(str, Enum):
|
|
70
|
+
VALUE = "value"
|
|
71
|
+
TIME_PERIOD = "time_period"
|
|
72
|
+
POPULATION = "population"
|
|
73
|
+
EXAGGERATION = "exaggeration"
|
|
74
|
+
# [이수민 2026-05-14] working memory 도메인 가드용
|
|
75
|
+
DOMAIN_MISMATCH = "domain_mismatch" # stat_id 카테고리가 문서 도메인과 어긋남
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class FeedbackType(str, Enum):
|
|
79
|
+
HUMAN_REVIEW = "human_review"
|
|
80
|
+
LOW_CONFIDENCE = "low_confidence"
|
|
81
|
+
FAILURE = "failure"
|
|
82
|
+
DRIFT = "drift"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class GraphNodeType(str, Enum):
|
|
86
|
+
CLAIM = "claim"
|
|
87
|
+
ENTITY = "entity"
|
|
88
|
+
METRIC = "metric"
|
|
89
|
+
TIME = "time"
|
|
90
|
+
EVIDENCE = "evidence"
|
|
91
|
+
SOURCE = "source"
|
|
92
|
+
# ── 멀티홉 시간 그래프용 (document_graph.py) ──────────────────────────
|
|
93
|
+
DOCUMENT = "document" # 문서 메타 (anchor_year property 보유)
|
|
94
|
+
SENTENCE = "sentence" # 문장 (REFERS_TO 타겟, sir_doc 호환)
|
|
95
|
+
TEMPORAL_EXPR = "temporal_expr" # "작년", "9월", "재작년 같은 기간"
|
|
96
|
+
RESOLVED_TIME = "resolved_time" # 절대 시점 (2023, 2024-09 등)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class GraphEdgeType(str, Enum):
|
|
100
|
+
MEASURED_AT = "measured_at"
|
|
101
|
+
BELONGS_TO = "belongs_to"
|
|
102
|
+
VERIFIED_BY = "verified_by"
|
|
103
|
+
SOURCED_FROM = "sourced_from"
|
|
104
|
+
CONTRADICTS = "contradicts"
|
|
105
|
+
SUPPORTS = "supports"
|
|
106
|
+
# GraphRAG 문맥 엣지 (sir_builder.extract_context_edges → graph_builder)
|
|
107
|
+
NEXT_SENT = "next_sent" # 문장 → 다음 문장 (문맥 흐름)
|
|
108
|
+
IN_BLOCK = "in_block" # 문장 → 소속 문단
|
|
109
|
+
IN_DOC = "in_doc" # 문단 → 소속 문서
|
|
110
|
+
# 복합 주장 검증용 (같은 지표를 공유하는 Claim 간)
|
|
111
|
+
COMPARE = "compare" # C1 ↔ C2 (2.6배 같은 파생 주장 검증)
|
|
112
|
+
# ── 멀티홉 시간 그래프용 (document_graph.py) ──────────────────────────
|
|
113
|
+
HAS_TEMPORAL = "has_temporal" # Sentence/Claim → TemporalExpr
|
|
114
|
+
RELATIVE_TO = "relative_to" # TemporalExpr → Document (anchor 의존)
|
|
115
|
+
RESOLVES_TO = "resolves_to" # TemporalExpr → ResolvedTime
|
|
116
|
+
REFERS_TO = "refers_to" # TemporalExpr → 다른 Sentence (coref)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── SIR Tree ─────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
class SourceOffset(BaseModel):
|
|
122
|
+
"""원문 역추적용 절대 위치 정보"""
|
|
123
|
+
page: int | None = None
|
|
124
|
+
char_start: int = 0
|
|
125
|
+
char_end: int = 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Sentence(BaseModel):
|
|
129
|
+
"""
|
|
130
|
+
개별 문장 + candidate detection 결과
|
|
131
|
+
|
|
132
|
+
필드 설명
|
|
133
|
+
- has_numeric_surface:
|
|
134
|
+
정규식 기반의 약한 표면 신호. 최종 candidate 판단이 아님.
|
|
135
|
+
- candidate_score:
|
|
136
|
+
검증 후보 점수. 0~1 범위.
|
|
137
|
+
- candidate_label:
|
|
138
|
+
threshold를 적용한 최종 후보 여부.
|
|
139
|
+
- candidate_source:
|
|
140
|
+
점수의 출처 ("surface_rule", "weak_supervision", "teacher_llm" 등)
|
|
141
|
+
- candidate_signals:
|
|
142
|
+
디버깅/분석용 보조 신호
|
|
143
|
+
"""
|
|
144
|
+
sent_id: str
|
|
145
|
+
text: str
|
|
146
|
+
char_offset_start: int = 0
|
|
147
|
+
char_offset_end: int = 0
|
|
148
|
+
|
|
149
|
+
# 기존 regex 탐지는 하위 호환을 위해 surface signal로 격하
|
|
150
|
+
has_numeric_surface: bool = False
|
|
151
|
+
|
|
152
|
+
# 논문형 candidate detection 결과
|
|
153
|
+
candidate_score: float = 0.0
|
|
154
|
+
candidate_label: bool = False
|
|
155
|
+
candidate_source: str | None = None
|
|
156
|
+
candidate_signals: dict[str, Any] = Field(default_factory=dict)
|
|
157
|
+
|
|
158
|
+
graph_anchor_id: str | None = None
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def has_numeric(self) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
하위 호환용 property.
|
|
164
|
+
기존 코드가 s.has_numeric를 참조하더라도 surface signal로 동작하게 한다.
|
|
165
|
+
"""
|
|
166
|
+
return self.has_numeric_surface
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SIRBlock(BaseModel):
|
|
170
|
+
block_id: str
|
|
171
|
+
type: BlockType
|
|
172
|
+
level: int | None = None
|
|
173
|
+
content: str | None = None
|
|
174
|
+
sentences: list[Sentence] = Field(default_factory=list)
|
|
175
|
+
headers: list[str] | None = None
|
|
176
|
+
rows: list[list[str]] | None = None
|
|
177
|
+
entity_refs: list[str] = Field(default_factory=list)
|
|
178
|
+
event_refs: list[str] = Field(default_factory=list)
|
|
179
|
+
graph_anchor_ids: list[str] = Field(default_factory=list)
|
|
180
|
+
source_offset: SourceOffset = Field(default_factory=SourceOffset)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SIRDocument(BaseModel):
|
|
184
|
+
doc_id: UUID = Field(default_factory=uuid4)
|
|
185
|
+
source_type: SourceType
|
|
186
|
+
source_uri: str | None = None
|
|
187
|
+
extracted_at: datetime = Field(default_factory=datetime.utcnow)
|
|
188
|
+
blocks: list[SIRBlock] = Field(default_factory=list)
|
|
189
|
+
detected_domain: str | None = None
|
|
190
|
+
# [2026-05-21] URL/PDF 입력 시 추출된 본문 텍스트. 프론트 "원문" 패널에서
|
|
191
|
+
# source_data 대체로 사용. text 입력은 source_data == raw_text이라 중복이지만
|
|
192
|
+
# 일관성 유지를 위해 항상 채움.
|
|
193
|
+
raw_text: str | None = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── Claim ────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
class ClaimSchema(BaseModel):
|
|
199
|
+
indicator: str | None = None
|
|
200
|
+
time_period: str | None = None
|
|
201
|
+
unit: str | None = None
|
|
202
|
+
population: str | None = None
|
|
203
|
+
value: float | None = None
|
|
204
|
+
comparison_type: ClaimType | None = None
|
|
205
|
+
source_reference: str | None = None
|
|
206
|
+
graph_schema_candidates: list[dict[str, str]] = Field(default_factory=list)
|
|
207
|
+
parent_path: str | None = None # "노동 > 청년 > 쉬었음 인구" (계층 카테고리)
|
|
208
|
+
is_approximate: bool = False # "안팎" 인지
|
|
209
|
+
modifier: str | None = None # 근사 표현 원문
|
|
210
|
+
prev_value: float | None = None
|
|
211
|
+
prev_time_period: str | None = None
|
|
212
|
+
prev_phrase: str | None = None
|
|
213
|
+
|
|
214
|
+
# [2026-05-21] schema_inductor가 한 claim → N sub-claim 분기 시,
|
|
215
|
+
# 각 sub-claim의 *검증 역할*을 명시. planner LLM이 같은 claim_text를 보고
|
|
216
|
+
# 둘 다 동일 plan_type으로 잘못 분류하는 걸 방지하는 1차 신호.
|
|
217
|
+
# - "base" : 단일 값 단순 확인 (예: 출생아 수 = 20717명)
|
|
218
|
+
# → plan_type=absolute, 시퀀스: catalog → fetch → finish
|
|
219
|
+
# - "derived_rate" : 비율/증가율/감소율 계산 (단위 %, ~증가율 류)
|
|
220
|
+
# → plan_type=growth_rate
|
|
221
|
+
# - "derived_difference": 절대 차이/변화량 (단위 절대단위, 차이값)
|
|
222
|
+
# → plan_type=difference
|
|
223
|
+
# - "aggregation" : 다년/다기간 집계 (평균/합계/최대/최소 류)
|
|
224
|
+
# → plan_type=aggregation, 시퀀스: catalog → fetch×N → calc(agg) → finish
|
|
225
|
+
# - None : 명시 안 됨 — planner LLM이 claim_text/schema로 추론
|
|
226
|
+
value_role: str | None = None
|
|
227
|
+
|
|
228
|
+
# [2026-05-21] aggregation claim 지원 — "최근 3년 평균 …", "총합 …" 류
|
|
229
|
+
# 도메인 무관, 모두 Optional. None이면 downstream에서 일반 base/derived 흐름으로 폴백.
|
|
230
|
+
# LLM(schema_inductor)이 claim_text에서 신호를 감지해 채우며, 한국어 키워드 하드코딩 X.
|
|
231
|
+
aggregation: str | None = None
|
|
232
|
+
"""집계 연산자 — "mean" | "sum" | "max" | "min" | "median" 등. None이면 단일값 처리."""
|
|
233
|
+
aggregation_window: int | None = None
|
|
234
|
+
"""집계 윈도우 크기 — "최근 N년/N분기/N개월" → N. None이면 explicit range 사용 또는 미사용."""
|
|
235
|
+
aggregation_time_range: list[str] | None = None
|
|
236
|
+
"""집계 대상 시점 명시 — ["2022", "2023", "2024"]. None이면 window/anchor로 derive."""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Claim(BaseModel):
|
|
241
|
+
claim_id: UUID = Field(default_factory=uuid4)
|
|
242
|
+
doc_id: UUID
|
|
243
|
+
block_id: str
|
|
244
|
+
sent_id: str
|
|
245
|
+
claim_text: str
|
|
246
|
+
claim_type: str | None = None # 자유 문자열
|
|
247
|
+
canonical_type: ClaimType | None = None
|
|
248
|
+
schema: ClaimSchema | None = None
|
|
249
|
+
source_offset: SourceOffset = Field(default_factory=SourceOffset)
|
|
250
|
+
check_worthy_score: float = 0.0
|
|
251
|
+
graph_anchor_id: str | None = None
|
|
252
|
+
# [v4 김예슬] 앞뒤 문맥 (SIR Tree에서 추출, runtime_agent가 부착)
|
|
253
|
+
# "이는 2.6배" 같은 대명사 참조 해소 + schema_inductor/query_builder에서 활용
|
|
254
|
+
context_text: str | None = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── Graph ────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
class GraphNode(BaseModel):
|
|
260
|
+
node_id: str
|
|
261
|
+
node_type: GraphNodeType
|
|
262
|
+
label: str
|
|
263
|
+
domain: str | None = None
|
|
264
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class GraphEdge(BaseModel):
|
|
268
|
+
edge_id: str = Field(default_factory=lambda: str(uuid4())[:8])
|
|
269
|
+
from_node: str
|
|
270
|
+
to_node: str
|
|
271
|
+
edge_type: GraphEdgeType
|
|
272
|
+
weight: float = 1.0
|
|
273
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ProvenanceRecord(BaseModel):
|
|
277
|
+
provenance_id: str = Field(default_factory=lambda: str(uuid4())[:8])
|
|
278
|
+
source_connector: str
|
|
279
|
+
source_id: str | None = None
|
|
280
|
+
query_used: str | None = None
|
|
281
|
+
fetched_at: datetime = Field(default_factory=datetime.utcnow)
|
|
282
|
+
raw_snapshot: dict[str, Any] = Field(default_factory=dict)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── Evidence / Verification ──────────────────────────────────
|
|
286
|
+
|
|
287
|
+
class Evidence(BaseModel):
|
|
288
|
+
source_name: str
|
|
289
|
+
stat_table_id: str | None = None
|
|
290
|
+
official_value: float | None = None
|
|
291
|
+
unit: str | None = None
|
|
292
|
+
time_period: str | None = None
|
|
293
|
+
raw_response: dict[str, Any] = Field(default_factory=dict)
|
|
294
|
+
graph_nodes: list[GraphNode] = Field(default_factory=list)
|
|
295
|
+
provenance: ProvenanceRecord | None = None
|
|
296
|
+
# [이수민 2026-05-14] working memory 도메인 가드용
|
|
297
|
+
category_path: str | None = None # KOSIS catalog의 "인구 > 출생" 등
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class VerificationResult(BaseModel):
|
|
301
|
+
result_id: UUID = Field(default_factory=uuid4)
|
|
302
|
+
claim_id: UUID
|
|
303
|
+
verdict: VerdictType
|
|
304
|
+
confidence: float = 0.0
|
|
305
|
+
evidence: Evidence | None = None
|
|
306
|
+
# [2026-05-21] derived claim (growth_rate/difference) 검증에 함께 쓰인 *보조 데이터*.
|
|
307
|
+
# primary evidence는 claim.schema.time_period와 매칭되는 시점값(current).
|
|
308
|
+
# supporting_evidence는 그 외 — 예: difference 검증의 prev 시점 KOSIS 값.
|
|
309
|
+
# base claim은 보통 비어있음 (단일 시점만 필요).
|
|
310
|
+
# 프론트는 "공식 통계 출처" 아래에 "함께 참조한 데이터" 섹션으로 렌더.
|
|
311
|
+
supporting_evidence: list[Evidence] = Field(default_factory=list)
|
|
312
|
+
mismatch_type: MismatchType | None = None
|
|
313
|
+
explanation: str | None = None
|
|
314
|
+
provenance_summary: str | None = None
|
|
315
|
+
reviewer_verdict: VerdictType | None = None
|
|
316
|
+
# ★ Phase E: growth_rate/diff 같은 계산 검증값
|
|
317
|
+
computed_value: float | None = None
|
|
318
|
+
"""Calculate tool 결과값 — 주장값 vs 이 값 비교로 verdict 결정."""
|
|
319
|
+
formula: str | None = None
|
|
320
|
+
"""사용된 수식 (예: '(current - prev) / prev * 100')."""
|
|
321
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
322
|
+
# [Multi-hop GraphRAG - 김예슬 2026-05-08]
|
|
323
|
+
# 파생 주장(2.6배 등)을 COMPARE 엣지 2-hop으로 검증한 경우 표시
|
|
324
|
+
multihop_used: bool = False
|
|
325
|
+
multihop_detail: dict | None = None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ── Feedback / Adaptation ────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
class FeedbackEvent(BaseModel):
|
|
331
|
+
event_id: UUID = Field(default_factory=uuid4)
|
|
332
|
+
claim_id: UUID
|
|
333
|
+
feedback_type: FeedbackType
|
|
334
|
+
original_verdict: VerdictType | None = None
|
|
335
|
+
corrected_verdict: VerdictType | None = None
|
|
336
|
+
reviewer_note: str | None = None
|
|
337
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class DomainPack(BaseModel):
|
|
341
|
+
pack_id: str
|
|
342
|
+
domain: str
|
|
343
|
+
version: str
|
|
344
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
345
|
+
adapter_path: str | None = None
|
|
346
|
+
eval_score: float | None = None
|
|
347
|
+
is_active: bool = True
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ── Report ───────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
class VerificationReport(BaseModel):
|
|
353
|
+
report_id: UUID = Field(default_factory=uuid4)
|
|
354
|
+
document: SIRDocument
|
|
355
|
+
claims: list[Claim] = Field(default_factory=list)
|
|
356
|
+
results: list[VerificationResult] = Field(default_factory=list)
|
|
357
|
+
graph_nodes: list[GraphNode] = Field(default_factory=list)
|
|
358
|
+
graph_edges: list[GraphEdge] = Field(default_factory=list)
|
|
359
|
+
feedbacks: list[FeedbackEvent] = Field(default_factory=list)
|
|
360
|
+
domain_pack_used: str | None = None
|
|
361
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
362
|
+
summary: str | None = None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""structverify.detection — Step 3~5 public API.
|
|
2
|
+
|
|
3
|
+
domain classify → claim detect → schema induce
|
|
4
|
+
"""
|
|
5
|
+
from structverify.detection.claim_detector import detect_claims
|
|
6
|
+
from structverify.detection.candidate_scorer import score_candidate
|
|
7
|
+
from structverify.detection.domain_classifier import (
|
|
8
|
+
CONFIDENCE_THRESHOLD,
|
|
9
|
+
DEFAULT_SEED_DOMAINS,
|
|
10
|
+
DOMAIN_NAME_PATTERN,
|
|
11
|
+
DomainRegistry,
|
|
12
|
+
classify_domain,
|
|
13
|
+
)
|
|
14
|
+
from structverify.detection.schema_inductor import induce_schemas, regenerate_schema
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CONFIDENCE_THRESHOLD",
|
|
18
|
+
"DEFAULT_SEED_DOMAINS",
|
|
19
|
+
"DOMAIN_NAME_PATTERN",
|
|
20
|
+
"DomainRegistry",
|
|
21
|
+
"classify_domain",
|
|
22
|
+
"detect_claims",
|
|
23
|
+
"induce_schemas",
|
|
24
|
+
"regenerate_schema",
|
|
25
|
+
"score_candidate",
|
|
26
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""[리팩] Step 3~5 설정 로드 — detection/config.yaml (default.yaml 미수정).
|
|
2
|
+
|
|
3
|
+
런타임: config.detection.*
|
|
4
|
+
레거시: candidate_detection, verification.min_confidence 등 (fallback, 변경 없음)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
_CONFIG_PATH = Path(__file__).with_name("config.yaml")
|
|
14
|
+
_CONFIG_CACHE: dict[str, Any] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_module_config() -> dict[str, Any]:
|
|
18
|
+
global _CONFIG_CACHE
|
|
19
|
+
if _CONFIG_CACHE is None:
|
|
20
|
+
with open(_CONFIG_PATH, encoding="utf-8") as f:
|
|
21
|
+
data = yaml.safe_load(f) or {}
|
|
22
|
+
_CONFIG_CACHE = data if isinstance(data, dict) else {}
|
|
23
|
+
return dict(_CONFIG_CACHE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
out = dict(base)
|
|
28
|
+
for key, value in override.items():
|
|
29
|
+
if (
|
|
30
|
+
key in out
|
|
31
|
+
and isinstance(out[key], dict)
|
|
32
|
+
and isinstance(value, dict)
|
|
33
|
+
):
|
|
34
|
+
out[key] = _deep_merge(out[key], value)
|
|
35
|
+
else:
|
|
36
|
+
out[key] = value
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _root(config: dict | None) -> dict[str, Any]:
|
|
41
|
+
return config or {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _runtime_detection(config: dict | None) -> dict[str, Any]:
|
|
45
|
+
block = _root(config).get("detection")
|
|
46
|
+
return block if isinstance(block, dict) else {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def detection_defaults() -> dict[str, Any]:
|
|
50
|
+
"""detection/config.yaml 내용 (테스트·디버그용)."""
|
|
51
|
+
return _load_module_config()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _detection_effective(config: dict | None) -> dict[str, Any]:
|
|
55
|
+
"""config.yaml + config.detection 병합 (레거시 키 제외)."""
|
|
56
|
+
return _deep_merge(_load_module_config(), _runtime_detection(config))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def candidate_detection_config(config: dict | None = None) -> dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
candidate detection 설정 병합.
|
|
62
|
+
|
|
63
|
+
우선순위: candidate_detection (레거시) > config.detection > config.yaml
|
|
64
|
+
"""
|
|
65
|
+
det = _detection_effective(config)
|
|
66
|
+
legacy = _root(config).get("candidate_detection") or {}
|
|
67
|
+
if not isinstance(legacy, dict):
|
|
68
|
+
legacy = {}
|
|
69
|
+
base = det.get("candidate_detection") or det.get("candidate") or {}
|
|
70
|
+
if not isinstance(base, dict):
|
|
71
|
+
base = {}
|
|
72
|
+
merged = dict(base)
|
|
73
|
+
merged.update(legacy)
|
|
74
|
+
return merged
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def domain_packs_dir(config: dict | None = None) -> str:
|
|
78
|
+
root = _root(config)
|
|
79
|
+
if root.get("domain_packs_dir"):
|
|
80
|
+
return str(root["domain_packs_dir"])
|
|
81
|
+
runtime = _runtime_detection(config)
|
|
82
|
+
if runtime.get("domain_packs_dir"):
|
|
83
|
+
return str(runtime["domain_packs_dir"])
|
|
84
|
+
return str(_load_module_config().get("domain_packs_dir", "domain-packs"))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def domain_registry_path(config: dict | None = None) -> str:
|
|
88
|
+
root = _root(config)
|
|
89
|
+
if root.get("domain_registry_path"):
|
|
90
|
+
return str(root["domain_registry_path"])
|
|
91
|
+
runtime = _runtime_detection(config)
|
|
92
|
+
if runtime.get("domain_registry_path"):
|
|
93
|
+
return str(runtime["domain_registry_path"])
|
|
94
|
+
return str(_load_module_config().get("domain_registry_path", "domain-packs/registry.yaml"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def domain_confidence_threshold(config: dict | None = None) -> float:
|
|
98
|
+
det = _detection_effective(config)
|
|
99
|
+
domain = det.get("domain") or {}
|
|
100
|
+
if isinstance(domain, dict) and domain.get("confidence_threshold") is not None:
|
|
101
|
+
return float(domain["confidence_threshold"])
|
|
102
|
+
if det.get("confidence_threshold") is not None:
|
|
103
|
+
return float(det["confidence_threshold"])
|
|
104
|
+
return 0.6
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def claim_min_confidence(config: dict | None = None) -> float:
|
|
108
|
+
"""claim 채택 최소 confidence. 레거시 verification.min_confidence fallback."""
|
|
109
|
+
root = _root(config)
|
|
110
|
+
vconf = root.get("verification") or {}
|
|
111
|
+
if isinstance(vconf, dict) and vconf.get("min_confidence") is not None:
|
|
112
|
+
return float(vconf["min_confidence"])
|
|
113
|
+
runtime = _runtime_detection(config)
|
|
114
|
+
claim = runtime.get("claim") or {}
|
|
115
|
+
if isinstance(claim, dict) and claim.get("min_confidence") is not None:
|
|
116
|
+
return float(claim["min_confidence"])
|
|
117
|
+
if runtime.get("min_confidence") is not None:
|
|
118
|
+
return float(runtime["min_confidence"])
|
|
119
|
+
defaults_claim = _load_module_config().get("claim") or {}
|
|
120
|
+
if isinstance(defaults_claim, dict) and defaults_claim.get("min_confidence") is not None:
|
|
121
|
+
return float(defaults_claim["min_confidence"])
|
|
122
|
+
return 0.7
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def claim_worthy_score_floor(config: dict | None = None) -> float:
|
|
126
|
+
"""check-worthy true인데 score=0일 때 보정값."""
|
|
127
|
+
det = _detection_effective(config)
|
|
128
|
+
claim = det.get("claim") or {}
|
|
129
|
+
if isinstance(claim, dict) and claim.get("worthy_score_floor") is not None:
|
|
130
|
+
return float(claim["worthy_score_floor"])
|
|
131
|
+
return 0.8
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def candidate_llm_label_floor(config: dict | None = None) -> float:
|
|
135
|
+
det = _detection_effective(config)
|
|
136
|
+
cand = det.get("candidate_detection") or det.get("candidate") or {}
|
|
137
|
+
if isinstance(cand, dict) and cand.get("llm_label_floor_score") is not None:
|
|
138
|
+
return float(cand["llm_label_floor_score"])
|
|
139
|
+
return 0.75
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def model_tier_for(config: dict | None, step: str, *, default: str = "heavy") -> str:
|
|
143
|
+
"""Step별 LLM model_tier (domain_classify, candidate_score, ...)."""
|
|
144
|
+
det = _detection_effective(config)
|
|
145
|
+
tiers = det.get("model_tier") or {}
|
|
146
|
+
if isinstance(tiers, dict) and tiers.get(step):
|
|
147
|
+
return str(tiers[step])
|
|
148
|
+
return default
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def detected_domain(config: dict | None = None, *, default: str = "general") -> str:
|
|
152
|
+
det = _detection_effective(config)
|
|
153
|
+
root = _root(config)
|
|
154
|
+
value = det.get("detected_domain") or root.get("detected_domain")
|
|
155
|
+
return value if value else default
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def llm_config(config: dict | None = None) -> dict[str, Any]:
|
|
159
|
+
base = dict(_root(config).get("llm") or {})
|
|
160
|
+
det_llm = _detection_effective(config).get("llm") or {}
|
|
161
|
+
if isinstance(det_llm, dict):
|
|
162
|
+
base.update(det_llm)
|
|
163
|
+
return base
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""detection/_llm.py — Step 3~5 LLM thin wrapper.
|
|
2
|
+
|
|
3
|
+
structverify.utils.llm_client.LLMClient를 detection 모듈에서 직접 생성하지 않고
|
|
4
|
+
여기를 경유한다. (utils/llm_client.py 대수술 없음)
|
|
5
|
+
|
|
6
|
+
[리팩 Phase C #14] detection 내 LLMClient 생성은 get_llm_client()로 통일.
|
|
7
|
+
[리팩 Phase C #15] llm 서브설정은 detection._config.llm_config() 경유.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from structverify.detection._config import llm_config
|
|
14
|
+
from structverify.utils.llm_client import LLMClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_llm_client(config: dict | None = None) -> LLMClient:
|
|
18
|
+
"""detection._config.llm_config()로 LLMClient 생성 (레거시 config.llm 호환)."""
|
|
19
|
+
return LLMClient(config=llm_config(config))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def llm_config_from(config: dict | None) -> dict[str, Any]:
|
|
23
|
+
"""전체 config dict에서 llm 서브 dict 추출 (detection.llm 병합 포함)."""
|
|
24
|
+
return llm_config(config)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""detection/candidate — Step 4 candidate scoring (LLM·heuristic)."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""detection/candidate/heuristic.py — candidate scoring heuristic fallback.
|
|
2
|
+
|
|
3
|
+
candidate_scorer.py에서 분리 (로직 move-only).
|
|
4
|
+
|
|
5
|
+
LLM 실패 시만 사용 — rule 기반 1차 후보 결정 용도 아님.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# ── heuristic fallback 패턴 (LLM 실패 시만 사용) ────────────────────────
|
|
13
|
+
# 아래 패턴들은 LLM이 호출 불가능할 때만 사용하는 fallback입니다.
|
|
14
|
+
# 운영 환경에서는 LLM 판단이 우선입니다.
|
|
15
|
+
TIME_PATTERN = re.compile(r"\d{4}년|\d+월|\d+분기|전년|지난해|올해")
|
|
16
|
+
COMPARISON_PATTERN = re.compile(r"증가|감소|상승|하락|올랐다|내렸다|대비|비율|점유율|이상|이하|안팎")
|
|
17
|
+
POPULATION_PATTERN = re.compile(r"국내|전국|가구|가계|농가|학생|청년|고령자|근로자|기업|미국|일본|유럽|한국")
|
|
18
|
+
NUMBER_PATTERN = re.compile(r"\d")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _score_candidate_heuristic(
|
|
22
|
+
sentence: str,
|
|
23
|
+
threshold: float = 0.65,
|
|
24
|
+
) -> tuple[float, bool, str, dict[str, Any]]:
|
|
25
|
+
"""
|
|
26
|
+
최소한의 fallback heuristic.
|
|
27
|
+
|
|
28
|
+
TODO [김예슬]: 논문 실험 baseline으로도 활용 가능
|
|
29
|
+
- 이 함수의 성능(F1, precision, recall)을 측정하고
|
|
30
|
+
teacher LLM 및 fine-tuned 모델과 비교
|
|
31
|
+
|
|
32
|
+
주의: 이 heuristic은 LLM 호출 실패 시만 사용합니다.
|
|
33
|
+
Rule 기반으로 검증 후보를 결정하는 용도로 사용하지 마세요.
|
|
34
|
+
"""
|
|
35
|
+
has_quantity = bool(NUMBER_PATTERN.search(sentence))
|
|
36
|
+
has_time_expr = bool(TIME_PATTERN.search(sentence))
|
|
37
|
+
has_population = bool(POPULATION_PATTERN.search(sentence))
|
|
38
|
+
has_comparison_expr = bool(COMPARISON_PATTERN.search(sentence))
|
|
39
|
+
|
|
40
|
+
score = 0.0
|
|
41
|
+
if has_quantity:
|
|
42
|
+
score += 0.35
|
|
43
|
+
if has_time_expr:
|
|
44
|
+
score += 0.20
|
|
45
|
+
if has_population:
|
|
46
|
+
score += 0.20
|
|
47
|
+
if has_comparison_expr:
|
|
48
|
+
score += 0.25
|
|
49
|
+
|
|
50
|
+
score = min(score, 1.0)
|
|
51
|
+
label = score >= threshold
|
|
52
|
+
|
|
53
|
+
signals = {
|
|
54
|
+
"has_quantity": has_quantity,
|
|
55
|
+
"has_time_expr": has_time_expr,
|
|
56
|
+
"has_population": has_population,
|
|
57
|
+
"has_comparison_expr": has_comparison_expr,
|
|
58
|
+
"reason": "heuristic_fallback",
|
|
59
|
+
}
|
|
60
|
+
return score, label, "heuristic_fallback", signals
|