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.
Files changed (168) hide show
  1. structverify/__init__.py +83 -0
  2. structverify/adaptation/__init__.py +0 -0
  3. structverify/adaptation/adapter_trainer.py +341 -0
  4. structverify/adaptation/feedback_store.py +31 -0
  5. structverify/adaptation/kosis_crawler.py +317 -0
  6. structverify/adaptation/sample_builder.py +149 -0
  7. structverify/adaptation/synthetic_generator.py +320 -0
  8. structverify/adaptation/update_embeddings.py +178 -0
  9. structverify/agent/__init__.py +21 -0
  10. structverify/agent/builder_agent.py +226 -0
  11. structverify/agent/conformance_agent.py +171 -0
  12. structverify/agent/dependency_planner.py +151 -0
  13. structverify/agent/indexing_agent.py +153 -0
  14. structverify/agent/indexing_planner.py +169 -0
  15. structverify/agent/integration_example.py +182 -0
  16. structverify/agent/loop.py +1165 -0
  17. structverify/agent/memory.py +207 -0
  18. structverify/agent/planner.py +817 -0
  19. structverify/agent/prompts/__init__.py +15 -0
  20. structverify/agent/prompts/planner_prompts.py +219 -0
  21. structverify/agent/prompts/reflect_prompts.py +387 -0
  22. structverify/agent/reflect.py +227 -0
  23. structverify/agent/runtime_agent.py +1272 -0
  24. structverify/agent/schemas.py +262 -0
  25. structverify/agent/source_profiler.py +229 -0
  26. structverify/agent/tools/__init__.py +64 -0
  27. structverify/agent/tools/base.py +222 -0
  28. structverify/agent/tools/calculate.py +244 -0
  29. structverify/agent/tools/catalog_search.py +859 -0
  30. structverify/agent/tools/deep_explore.py +293 -0
  31. structverify/agent/tools/explore_catalog.py +423 -0
  32. structverify/agent/tools/fetch_evidence.py +922 -0
  33. structverify/agent/tools/finish.py +423 -0
  34. structverify/agent/tools/meta_explore.py +267 -0
  35. structverify/agent/tools/query_rewriter.py +134 -0
  36. structverify/agent/tools/read_original.py +144 -0
  37. structverify/agent/tools/replan.py +365 -0
  38. structverify/agent/workspace.py +958 -0
  39. structverify/api.py +804 -0
  40. structverify/config/default.yaml +350 -0
  41. structverify/core/__init__.py +0 -0
  42. structverify/core/config_loader.py +30 -0
  43. structverify/core/pipeline.py +280 -0
  44. structverify/core/schemas.py +362 -0
  45. structverify/detection/__init__.py +26 -0
  46. structverify/detection/_config.py +163 -0
  47. structverify/detection/_llm.py +24 -0
  48. structverify/detection/candidate/__init__.py +1 -0
  49. structverify/detection/candidate/heuristic.py +60 -0
  50. structverify/detection/candidate/llm.py +51 -0
  51. structverify/detection/candidate_scorer.py +81 -0
  52. structverify/detection/claim_detector.py +164 -0
  53. structverify/detection/claims/__init__.py +1 -0
  54. structverify/detection/claims/worthiness.py +142 -0
  55. structverify/detection/domain/__init__.py +1 -0
  56. structverify/detection/domain/classify.py +84 -0
  57. structverify/detection/domain/preview.py +36 -0
  58. structverify/detection/domain/registry.py +99 -0
  59. structverify/detection/domain_classifier.py +75 -0
  60. structverify/detection/prompts/__init__.py +1 -0
  61. structverify/detection/prompts/candidate.py +38 -0
  62. structverify/detection/prompts/claim_worthiness.py +48 -0
  63. structverify/detection/prompts/domain.py +41 -0
  64. structverify/detection/prompts/schema.py +508 -0
  65. structverify/detection/prompts_loader.py +167 -0
  66. structverify/detection/schema/__init__.py +1 -0
  67. structverify/detection/schema/expand.py +83 -0
  68. structverify/detection/schema/induce.py +441 -0
  69. structverify/detection/schema/regenerate.py +162 -0
  70. structverify/detection/schema/temporal_hints.py +130 -0
  71. structverify/detection/schema/validate.py +193 -0
  72. structverify/detection/schema_inductor.py +112 -0
  73. structverify/detection/synthetic_generator.py +270 -0
  74. structverify/explanation/__init__.py +0 -0
  75. structverify/explanation/_config.py +18 -0
  76. structverify/explanation/_llm.py +25 -0
  77. structverify/explanation/explainer.py +183 -0
  78. structverify/explanation/fallback.py +29 -0
  79. structverify/explanation/formatters.py +75 -0
  80. structverify/explanation/prompts/__init__.py +1 -0
  81. structverify/explanation/prompts/match.py +27 -0
  82. structverify/explanation/prompts/mismatch.py +20 -0
  83. structverify/explanation/prompts/multihop.py +16 -0
  84. structverify/explanation/prompts/unverifiable.py +17 -0
  85. structverify/graph/__init__.py +0 -0
  86. structverify/graph/claim_graph.py +226 -0
  87. structverify/graph/document_graph.py +487 -0
  88. structverify/graph/graph_builder.py +238 -0
  89. structverify/graph/graph_multihop.py +335 -0
  90. structverify/graph/graph_store.py +281 -0
  91. structverify/graph/provenance.py +52 -0
  92. structverify/memory/__init__.py +44 -0
  93. structverify/memory/agent_memory.py +142 -0
  94. structverify/memory/embedder.py +69 -0
  95. structverify/memory/exemplar_store.py +241 -0
  96. structverify/memory/normalizer.py +91 -0
  97. structverify/memory/schema.py +119 -0
  98. structverify/memory/storage/__init__.py +29 -0
  99. structverify/memory/storage/jsonl_store.py +117 -0
  100. structverify/memory/working_memory.py +370 -0
  101. structverify/preprocessing/Dockerfile.scraper +27 -0
  102. structverify/preprocessing/__init__.py +0 -0
  103. structverify/preprocessing/extractor.py +574 -0
  104. structverify/preprocessing/pdf/__init__.py +16 -0
  105. structverify/preprocessing/pdf/fields.py +95 -0
  106. structverify/preprocessing/pdf/markdown.py +107 -0
  107. structverify/preprocessing/pdf/models.py +34 -0
  108. structverify/preprocessing/pdf/ocr.py +172 -0
  109. structverify/preprocessing/pdf/pipeline.py +74 -0
  110. structverify/preprocessing/pdf/reader.py +119 -0
  111. structverify/preprocessing/pdf/scoring.py +61 -0
  112. structverify/preprocessing/scraper_sandbox.py +561 -0
  113. structverify/preprocessing/segmenter.py +48 -0
  114. structverify/preprocessing/sir_builder.py +240 -0
  115. structverify/progress.py +591 -0
  116. structverify/retrieval/__init__.py +0 -0
  117. structverify/retrieval/base.py +208 -0
  118. structverify/retrieval/base_connector.py +85 -0
  119. structverify/retrieval/catalog_ranker.py +300 -0
  120. structverify/retrieval/catalog_search.py +583 -0
  121. structverify/retrieval/chunking.py +92 -0
  122. structverify/retrieval/custom_csv_source.py +386 -0
  123. structverify/retrieval/custom_db_source.py +396 -0
  124. structverify/retrieval/custom_docs_source.py +152 -0
  125. structverify/retrieval/dimension_resolver.py +281 -0
  126. structverify/retrieval/evidence_subgraph.py +63 -0
  127. structverify/retrieval/kosis_connector.py +1192 -0
  128. structverify/retrieval/kosis_relevance.py +142 -0
  129. structverify/retrieval/kosis_source.py +1541 -0
  130. structverify/retrieval/query_builder.py +72 -0
  131. structverify/retrieval/registry.py +133 -0
  132. structverify/retrieval/relevance_judge.py +141 -0
  133. structverify/retrieval/row_matcher.py +267 -0
  134. structverify/storage/__init__.py +0 -0
  135. structverify/storage/db_manager.py +157 -0
  136. structverify/storage/dwh_manager.py +92 -0
  137. structverify/storage/init_db.py +99 -0
  138. structverify/storage/raw_storage.py +29 -0
  139. structverify/training/__init__.py +26 -0
  140. structverify/training/curator.py +124 -0
  141. structverify/training/dataset.py +134 -0
  142. structverify/training/doctor.py +99 -0
  143. structverify/training/evalgate.py +96 -0
  144. structverify/training/generate.py +101 -0
  145. structverify/training/loop.py +116 -0
  146. structverify/training/recipe/train_mlx.py +99 -0
  147. structverify/training/recipe/train_qlora.py +104 -0
  148. structverify/training/tasks.py +79 -0
  149. structverify/utils/__init__.py +0 -0
  150. structverify/utils/embedding_client.py +248 -0
  151. structverify/utils/llm_client.py +809 -0
  152. structverify/utils/logger.py +81 -0
  153. structverify/verification/__init__.py +0 -0
  154. structverify/verification/_config.py +45 -0
  155. structverify/verification/adapters.py +405 -0
  156. structverify/verification/conformance.py +117 -0
  157. structverify/verification/decide_verdict.py +216 -0
  158. structverify/verification/decide_verdict_agent.py +454 -0
  159. structverify/verification/growth_diff.py +267 -0
  160. structverify/verification/row_match.py +345 -0
  161. structverify/verification/units.py +64 -0
  162. structverify/verification/verdict_thresholds.py +232 -0
  163. structverify/verification/verifier.py +84 -0
  164. structverify-0.3.0.dist-info/METADATA +903 -0
  165. structverify-0.3.0.dist-info/RECORD +168 -0
  166. structverify-0.3.0.dist-info/WHEEL +5 -0
  167. structverify-0.3.0.dist-info/licenses/LICENSE +21 -0
  168. 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