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,903 @@
1
+ Metadata-Version: 2.4
2
+ Name: structverify
3
+ Version: 0.3.0
4
+ Summary: Compliance & fact verification for documents — check your files against your own rulebook (PDF) or a data source, and read a plain True/False.
5
+ Author-email: "김예슬 (Yeseul Kim)" <yesul0718@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 김예슬 (Yeseul Kim) and StructVerify contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/yeseul-kim01/structverify
29
+ Project-URL: Repository, https://github.com/yeseul-kim01/structverify
30
+ Project-URL: Issues, https://github.com/yeseul-kim01/structverify/issues
31
+ Keywords: compliance,fact-checking,verification,llm,rag,regulation,conformance
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
39
+ Classifier: Topic :: Text Processing :: Linguistic
40
+ Requires-Python: >=3.10
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: pydantic>=2.0
44
+ Requires-Dist: httpx>=0.25
45
+ Requires-Dist: pyyaml>=6.0
46
+ Requires-Dist: openai>=1.10
47
+ Requires-Dist: pdfplumber>=0.10
48
+ Requires-Dist: python-dotenv
49
+ Requires-Dist: json5>=0.9.0
50
+ Provides-Extra: kosis
51
+ Requires-Dist: asyncpg>=0.29; extra == "kosis"
52
+ Provides-Extra: korean
53
+ Requires-Dist: kss>=6.0; extra == "korean"
54
+ Provides-Extra: url
55
+ Requires-Dist: trafilatura>=1.8; extra == "url"
56
+ Requires-Dist: beautifulsoup4>=4.12; extra == "url"
57
+ Provides-Extra: db
58
+ Requires-Dist: sqlalchemy>=2.0; extra == "db"
59
+ Provides-Extra: docs
60
+ Requires-Dist: python-docx>=1.1; extra == "docs"
61
+ Provides-Extra: pdf-ocr
62
+ Requires-Dist: pymupdf>=1.23; extra == "pdf-ocr"
63
+ Provides-Extra: graph
64
+ Requires-Dist: neo4j>=5.0; extra == "graph"
65
+ Provides-Extra: adaptation
66
+ Requires-Dist: mlflow>=2.10; extra == "adaptation"
67
+ Requires-Dist: boto3>=1.34; extra == "adaptation"
68
+ Requires-Dist: peft>=0.8; extra == "adaptation"
69
+ Requires-Dist: transformers>=4.38; extra == "adaptation"
70
+ Requires-Dist: torch>=2.1; extra == "adaptation"
71
+ Provides-Extra: training
72
+ Requires-Dist: unsloth; extra == "training"
73
+ Requires-Dist: trl>=0.8; extra == "training"
74
+ Requires-Dist: peft>=0.8; extra == "training"
75
+ Requires-Dist: bitsandbytes>=0.43; extra == "training"
76
+ Requires-Dist: accelerate>=0.27; extra == "training"
77
+ Requires-Dist: datasets>=2.16; extra == "training"
78
+ Requires-Dist: transformers>=4.40; extra == "training"
79
+ Requires-Dist: torch>=2.2; extra == "training"
80
+ Provides-Extra: training-mac
81
+ Requires-Dist: mlx-lm>=0.18; extra == "training-mac"
82
+ Provides-Extra: platform
83
+ Requires-Dist: fastapi>=0.109; extra == "platform"
84
+ Requires-Dist: uvicorn>=0.27; extra == "platform"
85
+ Requires-Dist: sqlalchemy>=2.0; extra == "platform"
86
+ Requires-Dist: asyncpg>=0.29; extra == "platform"
87
+ Requires-Dist: celery>=5.3; extra == "platform"
88
+ Requires-Dist: redis>=5.0; extra == "platform"
89
+ Requires-Dist: snowflake-connector-python>=3.6; extra == "platform"
90
+ Requires-Dist: langfuse>=2.0; extra == "platform"
91
+ Requires-Dist: python-docx>=1.1; extra == "platform"
92
+ Provides-Extra: all
93
+ Requires-Dist: asyncpg>=0.29; extra == "all"
94
+ Requires-Dist: kss>=6.0; extra == "all"
95
+ Requires-Dist: trafilatura>=1.8; extra == "all"
96
+ Requires-Dist: python-docx>=1.1; extra == "all"
97
+ Requires-Dist: neo4j>=5.0; extra == "all"
98
+ Requires-Dist: mlflow>=2.10; extra == "all"
99
+ Requires-Dist: boto3>=1.34; extra == "all"
100
+ Requires-Dist: fastapi>=0.109; extra == "all"
101
+ Requires-Dist: uvicorn>=0.27; extra == "all"
102
+ Requires-Dist: sqlalchemy>=2.0; extra == "all"
103
+ Requires-Dist: celery>=5.3; extra == "all"
104
+ Requires-Dist: redis>=5.0; extra == "all"
105
+ Requires-Dist: snowflake-connector-python>=3.6; extra == "all"
106
+ Requires-Dist: langfuse>=2.0; extra == "all"
107
+ Provides-Extra: dev
108
+ Requires-Dist: pytest>=8.0; extra == "dev"
109
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
110
+ Requires-Dist: ruff>=0.2; extra == "dev"
111
+ Requires-Dist: mypy>=1.8; extra == "dev"
112
+ Provides-Extra: peft
113
+ Requires-Dist: peft>=0.8; extra == "peft"
114
+ Requires-Dist: transformers>=4.38; extra == "peft"
115
+ Requires-Dist: torch>=2.1; extra == "peft"
116
+ Dynamic: license-file
117
+
118
+ # StructVerify
119
+
120
+ **문서가 규칙을 지켰는지 한 줄로 검사하는 라이브러리.** 내 규정집(PDF)이나 데이터 소스에
121
+ 문서를 대조하고, 결과에서 평범한 `True`/`False`를 읽는다. `transformers` 처럼 — 설정하고,
122
+ `.` 찍어 부르고, 판정을 받는다.
123
+
124
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
125
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
126
+ ![Providers](https://img.shields.io/badge/LLM-upstage%20%7C%20openai%20%7C%20gemini%20%7C%20hcx-orange)
127
+
128
+ ![StructVerify 데모 — 규정집으로 문서를 검사하고 준수/위반을 판정](docs/media/structverify_demo.gif)
129
+
130
+ ```python
131
+ import structverify as sv
132
+
133
+ rules = sv.Ruleset.from_file("safety_standard.pdf", provider="upstage")
134
+ v = rules.check("총납 함량은 120mg/kg으로 측정되었다")
135
+
136
+ v.compliant # False ← 평범한 True/False
137
+ v.article # "제3조 (총납) … 100mg/kg 이하"
138
+ v.rule_value, v.claim_value, v.unit # (100.0, 120.0, 'mg/kg')
139
+ ```
140
+
141
+ ## 설치
142
+
143
+ ```bash
144
+ pip install structverify # 코어 — 규정 준수 검사 (의존성 7개)
145
+ pip install "structverify[kosis]" # + 공공통계(KOSIS) 사실검증
146
+ pip install "structverify[all]" # 전부
147
+ ```
148
+
149
+ > 코어는 `pydantic·httpx·pyyaml·openai·pdfplumber` 정도만 받는다. KOSIS(pgvector)·
150
+ > URL 추출·Neo4j·FastAPI 플랫폼 등 무거운 건 전부 **필요할 때만** extra로.
151
+
152
+ ## 왜 StructVerify인가
153
+
154
+ - **규정 준수 + 사실 검증을 하나의 API로.** `Ruleset`(내 규정집)과 `Verifier`(데이터 대조).
155
+ - **내 데이터를 정답으로 (BYO ground truth).** 회사 규정 PDF·참조 CSV를 올려 검증.
156
+ - **큰 규정집엔 ReAct 에이전트 루프.** 검색→판정→쿼리 재구성→재검색을 반복해 적용 조항을 찾음.
157
+ - **provider 무관.** `upstage` \| `openai` \| `gemini` \| `hcx` — `api_key` 한 줄이면 끝.
158
+ - **결과가 곧 boolean.** `.ok` `.compliant` `.violated` / `bool(result)` / iterable / `.to_dict()`.
159
+
160
+ ## 30초 시작
161
+
162
+ ```python
163
+ import structverify as sv
164
+
165
+ # ① 규정 준수 — 내 규정집으로
166
+ rules = sv.Ruleset.from_file("policy.pdf", provider="upstage", api_key="up_...")
167
+ for v in rules.check_file("report.pdf"): # 측정값 줄마다 자동 판정
168
+ print("✓" if v.compliant else "✗", v.article, v.rule_value, v.claim_value)
169
+
170
+ # ② 사실 검증 — 회사 CSV를 기준 데이터로
171
+ engine = sv.Verifier(provider="upstage", data=sv.DataSource.csv("reference.csv"))
172
+ report = engine.verify("한국전력공사의 2023년 부채비율은 600%에 달했다.")
173
+ report.ok # 거짓 주장 없으면 True
174
+ report.mismatches # 반박된 주장만
175
+ ```
176
+
177
+ 전체 API는 아래 [라이브러리 사용](#라이브러리-사용) 참조.
178
+
179
+ ---
180
+
181
+ ## 프로젝트 구조
182
+
183
+ `backend/` 는 두 레이어로 나뉜다.
184
+
185
+ | 디렉토리 | 역할 |
186
+ |---|---|
187
+ | `structverify/` | **검증 라이브러리** — Pipeline, Agent, Tools, Retrieval 등 핵심 로직 (본 문서) |
188
+ | `sv_platform/` | **FastAPI 플랫폼** — REST API, 인증, Job, DB. `structverify/`를 wrap (`pip install "structverify[platform]"`) |
189
+
190
+ 플랫폼은 [sv_platform/README.md](sv_platform/README.md) 참조.
191
+
192
+ ---
193
+
194
+ ## 환경
195
+
196
+ - Python **3.13** (`.venv`의 pyvenv.cfg는 3.13.2)
197
+ - PostgreSQL 16 + pgvector
198
+ - Redis 7
199
+ - (옵션) Neo4j 5, Snowflake, Elasticsearch
200
+ - (옵션) Docker — URL extraction의 LLM scraper sandbox 격리용
201
+
202
+ ```bash
203
+ cd backend
204
+ python -m venv .venv && source .venv/bin/activate
205
+ pip install -e ".[dev]"
206
+ ```
207
+
208
+ > ⚠️ venv는 `python -m venv` 또는 `uv venv --seed` 로 생성 (`uv venv` 는 pip 미포함이라 `pip install -e .` 가 실패)
209
+
210
+ API 키 / DB 설정 (env 또는 `.env`):
211
+ ```
212
+ NCP_API_KEY=<NCP CLOVA Studio key — HCX-003/007/DASH-002/EMB-V2 공용>
213
+ KOSIS_API_KEY=<KOSIS Open API key>
214
+ PGVECTOR_DSN=postgresql://structverify:svpass123@localhost:5432/structverify
215
+ ```
216
+
217
+ > ⚠️ NCP_API_KEY 하나로 LLM 호출(HCX-003/DASH-002/007) + 임베딩(HCX-EMB-V2) + reranker(HCX-RERANKER)를 모두 처리한다. `config/default.yaml`의 `llm.api_key_env`, `embedding.api_key_env`, `reranker.api_key_env`가 전부 `NCP_API_KEY` 가리킴.
218
+
219
+ ---
220
+
221
+ ## 디렉토리 구조
222
+
223
+ ```
224
+ backend/structverify/
225
+ ├── core/
226
+ │ ├── pipeline.py 13단계 통합 (입력 → SIR → claim → schema → agent → 검증 → 설명)
227
+ │ ├── schemas.py 전체 데이터 모델 (Pydantic)
228
+ │ └── config_loader.py YAML 설정 로더
229
+
230
+ ├── preprocessing/ Step 1~2
231
+ │ ├── extractor.py URL(trafilatura + LLMScraper fallback) / PDF / DOCX / TEXT 추출
232
+ │ ├── segmenter.py kss 문장 분리 + 정규표현식 폴백 + surface signal
233
+ │ ├── sandbox_backend.py LLMScraper 동적 Python 실행 격리 (docker/local)
234
+ │ └── sir_builder.py SIR Tree 빌더 (blocks + sentences)
235
+
236
+ ├── detection/ Step 3~5
237
+ │ ├── domain_classifier.py LLM 도메인 분류 (HCX-DASH-002, label 16종 + general fallback)
238
+ │ ├── candidate_scorer.py sentence candidate scoring (LLM + surface heuristic fallback)
239
+ │ ├── claim_detector.py check-worthiness 판별 (HCX-003) — 순위/예측/주관 표현 자동 필터
240
+ │ ├── schema_inductor.py Dynamic Schema Induction (HCX-007 Structured Outputs)
241
+ │ │ + value=null 폴백 / value_role 자동 추론
242
+ │ │ + aggregation_window / aggregation_time_range 추출
243
+ │ └── synthetic_generator.py (Builder/Adaptation용 — KOSIS Self-Instruct 생성)
244
+
245
+ ├── graph/ Step 4.5, 6
246
+ │ ├── document_graph.py anchor_year + temporal expression 그래프
247
+ │ ├── graph_builder.py Claim/Evidence Graph 조립 + COMPARE 엣지
248
+ │ ├── graph_store.py Neo4j 인터페이스 (옵션, 기본 비활성)
249
+ │ └── provenance.py 출처 경로 렌더
250
+
251
+ ├── retrieval/ Step 7 (catalog + fetch는 agent tools가 직접 호출)
252
+ │ ├── base.py BaseDataSource 인터페이스
253
+ │ ├── base_connector.py ConnectorQuery dataclass (keyword/indicator/time_period/population/extra_params)
254
+ │ ├── catalog_search.py pgvector 카탈로그 의미 검색 (HCX-EMB-V2 임베딩)
255
+ │ ├── kosis_source.py KOSIS DataSource (search_catalog + fetch_evidence + _select_best_row)
256
+ │ ├── kosis_connector.py KOSIS Open API HTTP 호출 + PRD_SE-aware strategy pruning
257
+ │ ├── catalog_ranker.py ★ LLM batch ranking (후보 N개 → 점수 순위, P26)
258
+ │ ├── relevance_judge.py ★ per-table relevance LLM 판단 (P32, 룰 거부 시 fallback)
259
+ │ ├── row_matcher.py ★ row 매칭 LLM rescue (P33c, _select_best_row 0건일 때)
260
+ │ ├── dimension_resolver.py ★ KOSIS 표 차원(itmId/objL) 동적 결정 (P34, cache 있음)
261
+ │ ├── evidence_subgraph.py Evidence 서브그래프
262
+ │ ├── query_builder.py Schema → KOSIS 파라미터
263
+ │ └── registry.py DataSource 레지스트리
264
+
265
+ ├── verification/ Step 8 (deterministic 백업 — Phase D에서는 agent loop이 대체)
266
+ │ └── verifier.py 수치 비교 + 불일치 유형
267
+
268
+ ├── explanation/ Step 9
269
+ │ └── explainer.py LLM 자연어 설명 생성 (HCX-003)
270
+
271
+ ├── agent/ ★ Phase D Multi-Agent 시스템
272
+ │ ├── runtime_agent.py claim별 process_one_claim 병렬 실행 + Level 분리
273
+ │ ├── planner.py ★ Plan 생성 (claim → 검증 전략) — HCX-007
274
+ │ ├── reflect.py ★ 매 iter 다음 action 결정 — HCX-DASH-002
275
+ │ ├── loop.py ★ ReAct loop 본체 (tool 실행 + verdict 합성 + 중복 가드)
276
+ │ ├── dependency_planner.py ★ sub-claim 실행 레벨 분리 (base=L1, derived=L2)
277
+ │ ├── workspace.py job별 상태 파일 시스템
278
+ │ │ (verified_facts / sibling_evidence / fetched_values /
279
+ │ │ successful_stat_ids / failed_stat_ids / memory)
280
+ │ ├── memory.py memory.md 조작 헬퍼
281
+ │ ├── schemas.py ClaimType / ActionType / VerdictType / Plan / AgentVerdict
282
+ │ ├── builder_agent.py (추후 개발) 사전학습 + 피드백 학습
283
+ │ ├── prompts/
284
+ │ │ ├── planner_prompts.py Plan LLM 프롬프트 템플릿
285
+ │ │ └── reflect_prompts.py Reflect LLM 프롬프트 + verdict 가이드
286
+ │ └── tools/
287
+ │ ├── base.py ToolBase + ToolContext + @register_tool 데코레이터
288
+ │ ├── catalog_search.py catalog 검색 + job-success prepend + deep/meta_explore 자동 발동
289
+ │ ├── fetch_evidence.py 표 fetch + row 매칭 + 후보 폴백 + LLM ranker 적용
290
+ │ ├── calculate.py 수식 계산 (증가율/차이/집계, eval 화이트리스트)
291
+ │ ├── finish.py verdict 결정 + 합성 정정 + loop 종료
292
+ │ ├── read_original.py 원문 재독
293
+ │ ├── explore_catalog.py 카탈로그 카테고리 분포 탐색 (LLM 어휘 학습용)
294
+ │ ├── deep_explore.py ★ T1/T2 — top 표들 sample row preview → LLM row 단서 reasoning
295
+ │ ├── meta_explore.py ★ T1/T2 (기본 모드) — KOSIS getMeta(ITM/OBJ) → 빠른 식별
296
+ │ ├── query_rewriter.py ★ catalog_search query LLM 변형 (row-level keyword → table-friendly)
297
+ │ └── replan.py ★ Plan 자체 갈아끼우기 (per-claim 최대 2회, [[replan_max=2]])
298
+
299
+ ├── adaptation/ ★ Builder 학습 파이프라인 (자동 데이터셋 생성)
300
+ │ ├── kosis_crawler.py KOSIS 카탈로그 크롤
301
+ │ ├── synthetic_generator.py Self-Instruct 합성 claim 생성
302
+ │ ├── sample_builder.py train/eval split + 정제
303
+ │ ├── adapter_trainer.py LoRA fine-tuning (추후 활성화)
304
+ │ ├── feedback_store.py 사용자 피드백 적재
305
+ │ └── update_embeddings.py catalog 임베딩 재구축 스크립트
306
+
307
+ ├── storage/
308
+ │ ├── raw_storage.py S3/MinIO 원본 보존 (옵션)
309
+ │ ├── db_manager.py PostgreSQL Claims/Results CRUD
310
+ │ ├── dwh_manager.py Snowflake/BigQuery DWH (옵션)
311
+ │ └── init_db.py 초기 스키마 부트스트랩
312
+
313
+ ├── memory/ DocumentWorkingMemory (이수민 main, 도메인/지표 누적)
314
+ │ └── ...
315
+
316
+ └── utils/
317
+ ├── logger.py
318
+ └── llm_client.py LLMClient (HCX v1/v3/Structured Outputs) + 전역 rate limiter
319
+ ```
320
+
321
+ ---
322
+
323
+ ## 라이브러리 사용
324
+
325
+ `import structverify as sv` 한 줄로 시작하는 **인체공학적 고수준 API**를 제공한다.
326
+ `transformers` 처럼 — provider 를 한 번 설정하고, 메서드를 부르면, 결과 객체에서
327
+ `.ok` / `.compliant` 같은 **평범한 True/False** 를 바로 읽는다. 모든 sync 메서드는
328
+ `a` 접두사 async 쌍을 가진다 (`check`/`acheck`, `verify`/`averify`).
329
+
330
+ ### ① 규정 준수 검사 — `Ruleset` (내 규정집으로 검증)
331
+
332
+ PDF/txt 규정집(안전기준·사내정책·표시기준 등)을 색인하고, 문장이 그 규정을 지키는지 판정한다.
333
+ 규정집만 있으면 되므로 외부 데이터 없이 바로 돌아간다.
334
+
335
+ ```python
336
+ import structverify as sv
337
+
338
+ rules = sv.Ruleset.from_file("safety_standard.pdf", provider="upstage", api_key="up_...")
339
+ len(rules) # 색인된 조항 수
340
+
341
+ v = rules.check("총납 함량은 120mg/kg으로 측정되었다")
342
+ v.compliant # False ← 평범한 True/False
343
+ v.violated # True
344
+ v.article # "제3조 (총납) … 100mg/kg 이하"
345
+ v.rule_value, v.claim_value, v.unit # (100.0, 120.0, 'mg/kg')
346
+ v.reason # 판정 근거 (자연어)
347
+
348
+ # 문서 전체 — 측정값이 있는 줄마다 자동 판정
349
+ for v in rules.check_file("시험성적서.pdf"):
350
+ print("✓" if v.compliant else "✗", v.article, v.rule_value, v.claim_value)
351
+ ```
352
+
353
+ **큰 규정집 → ReAct 에이전트 루프 (`agent=True`).** 조항이 많아 한 번의 top-k 검색으로
354
+ 적용 조항을 놓칠 수 있을 때, KOSIS 사실검증과 같은 방식으로 **검색 → 판정 → (애매하면)
355
+ 쿼리 재구성 → 재검색**을 반복한다. 판정이 확정(준수/위반)될 때까지 스스로 조항을 찾아간다.
356
+
357
+ ```python
358
+ rules = sv.Ruleset.from_file("big_rulebook.pdf", provider="upstage", agent=True)
359
+ v = rules.check("화장품 납 함량은 30㎍/g으로 측정되었다")
360
+ v.violated # True
361
+ v.iterations # 몇 바퀴 만에 확정했는지 (1 = 한 번에, >1 = 재검색함)
362
+
363
+ rules.check("…", agent=True) # 규정집은 direct로 만들고 이 문장만 에이전트로
364
+ ```
365
+
366
+ ### ② 사실 검증 — `verify` / `Verifier` (데이터 소스 대조)
367
+
368
+ 문서의 수치·사실 주장을 설정된 데이터 소스(통계 카탈로그 등)와 대조한다.
369
+
370
+ ```python
371
+ import structverify as sv
372
+
373
+ # 한 줄
374
+ report = sv.verify("과수농가 65세 이상 비율은 64.2%다", provider="upstage")
375
+ report.ok # 거짓 주장이 없으면 True
376
+ if report: # Report 자체가 True/False
377
+ print("거짓 주장 없음")
378
+
379
+ # 엔진 재사용
380
+ engine = sv.Verifier(provider="upstage", api_key="up_...")
381
+ report = engine.verify(long_text)
382
+ report.mismatches # 반박된 주장만
383
+ for r in report: # 순회 가능 (len(report) == 주장 수)
384
+ print(r.verdict, r.confidence, r.value, r.reason)
385
+ ```
386
+
387
+ **기준 데이터 연결 — `DataSource`.** 기본은 내장 KOSIS 공공통계지만, `data=` 로
388
+ **회사 데이터**(참조 통계 CSV 등)를 기준으로 삼을 수 있다. 검증은 그 소스를 대상으로
389
+ ReAct 에이전트가 검색→조회→판정한다.
390
+
391
+ ```python
392
+ # 회사 참조 통계 CSV (indicator,year,region,value,unit 행)
393
+ engine = sv.Verifier(provider="upstage", data=sv.DataSource.csv("부채비율.csv"))
394
+ engine.verify("한국전력공사의 2023년 부채비율은 600%에 달했다.").mismatches
395
+
396
+ sv.DataSource.csv("t.csv", columns={"value": "amount"}) # 컬럼명 커스텀
397
+ sv.DataSource.docs("policies/") # 회사 문서(의미검색)
398
+ sv.DataSource.db("postgresql://…", table="kpi") # 회사 DB (sqlite/postgres/snowflake …, [db] extra)
399
+ sv.DataSource.kosis() # 내장 공공통계(기본)
400
+ sv.Verifier(provider="upstage", data="부채비율.csv") # 경로 문자열도 OK(확장자 추론)
401
+ ```
402
+
403
+ > 예산·한도처럼 *규정 준수* 성격의 회사 데이터는 위 사실검증보다 **`Ruleset`**(①)이 적합하다.
404
+
405
+ ### 결과 객체 요약
406
+
407
+ | 객체 | 핵심 boolean | 주요 필드 |
408
+ |---|---|---|
409
+ | `Verdict` (규정 준수) | `.compliant` · `.violated` · `bool(v)` | `.article` `.rule_value` `.claim_value` `.unit` `.reason` |
410
+ | `Report` (문서) | `.ok` · `bool(report)` | `.results` `.matches` `.mismatches` `.unverifiable` (iterable, `len()`) |
411
+ | `Result` (주장) | `.ok` · `.is_match` · `bool(r)` | `.verdict` `.claim` `.reason` `.confidence` `.value` `.source` |
412
+
413
+ 모든 결과 객체는 `.to_dict()`(JSON 직렬화)와 사람이 읽는 `repr` 을 지원한다.
414
+
415
+ ### provider 설정
416
+
417
+ `provider` 는 `upstage` \| `openai` \| `gemini` \| `hcx`. `api_key` 를 직접 넘기면 환경변수
418
+ 없이 동작하고, 생략하면 provider별 환경변수(`UPSTAGE_API_KEY` · `OPENAI_API_KEY` ·
419
+ `GEMINI_API_KEY` · `NCP_API_KEY`)를 사용한다.
420
+
421
+ ```python
422
+ sv.build_config(provider="upstage", api_key="up_...", tolerance=1.0) # 전체 엔진 config dict
423
+ ```
424
+
425
+ ### 저수준 API (파이프라인 직접 제어)
426
+
427
+ ```python
428
+ from structverify.core.pipeline import VerificationPipeline
429
+
430
+ pipeline = VerificationPipeline() # config/default.yaml 자동 로드
431
+ report = await pipeline.run("...", source_type="text") # url | pdf | docx | text
432
+ for r in report.results:
433
+ print(r.verdict, r.confidence, r.evidence.official_value, r.explanation)
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Phase D Agent Loop 상세
439
+
440
+ ### 1) Document → claim 추출 (Step 1~5)
441
+
442
+ `core/pipeline.py`의 `run()` 진입:
443
+ 1. `extract_text(source, source_type)` — URL/PDF/DOCX/TEXT → markdown raw_text
444
+ 2. `build_sir(raw_text, src)` — SIR Tree (blocks + sentences)
445
+ 3. `runtime_agent.process(sir_doc)` 호출
446
+
447
+ `runtime_agent.process()` 내부:
448
+ - Step 3: `classify_domain` (HCX-DASH-002)
449
+ - Step 4: `detect_claims` (candidate scoring → check-worthiness)
450
+ - Step 4.5: `build_document_temporal_graph` (anchor_year + temporal expression)
451
+ - Step 5: `induce_schemas` (HCX-007 Structured Outputs → ClaimSchema)
452
+ - 한 claim → N sub-claim 분기 (지역별, base/derived 등)
453
+ - **value=null 폴백** — `source_phrase`에서 숫자 max() 복원
454
+ - **value_role 자동 추론** — base / derived_rate / derived_difference / aggregation
455
+ - **aggregation 필드 추출** — `aggregation_window` (예: "최근 3년" → 3), `aggregation_time_range` (예: ["2022","2023","2024"])
456
+ - Step 6: `build_claim_graph` (Claim 그래프 + COMPARE 엣지)
457
+
458
+ ### 2) Sub-claim 실행 레벨 분리 (Dependency Planner)
459
+
460
+ `agent/dependency_planner.py::build_execution_levels(claims)`:
461
+
462
+ | Level | 포함 | 이유 |
463
+ |---|---|---|
464
+ | L1 | `value_role in {base, aggregation, None}` | 단독 fetch로 검증 가능. catalog 캐시 공유. |
465
+ | L2 | `value_role in {derived_rate, derived_difference}` | base 결과(sibling cache) 의존. L1 끝난 후 실행. |
466
+
467
+ → L1 병렬 처리 후 L2 병렬 처리. L1의 `verified_facts` / `sibling_evidence` / `successful_stat_ids`가 L2로 전파됨.
468
+
469
+ ### 3) Per-claim Agent Loop (Step 7~8)
470
+
471
+ 각 claim마다 `_verify_with_agent()` → `agent/loop.py:agent_loop()`:
472
+
473
+ ```
474
+ claim
475
+
476
+
477
+ [1] Planner LLM (HCX-007)
478
+ │ - 입력: claim.schema, 본문, anchor_year, 도메인
479
+ │ - 출력: Plan {
480
+ │ claim_type, # ABSOLUTE/GROWTH_RATE/DIFFERENCE/COMPARISON/RANKING/AGGREGATION
481
+ │ required_data, # 필요한 데이터 점 명세
482
+ │ initial_steps, # 권장 액션 시퀀스
483
+ │ fallback, # 1차 실패 시 대안
484
+ │ calculation_formula, # 수식 (derived/aggregation)
485
+ │ }
486
+ │ - value_role 후처리: schema.value_role 기반 claim_type 강제 정정
487
+
488
+ [2] Reflect Loop (max_iter=10, mode=reflect)
489
+ 매 iter:
490
+ a) workspace.read_memory(claim_id) + sibling_evidence inject (iter 1만)
491
+ b) reflect_fn(plan, memory, last_observation, iter_num) → ReflectDecision
492
+ - LLM (HCX-DASH-002) 호출
493
+ - action ∈ {catalog_search, fetch_evidence, calculate, finish,
494
+ read_original, explore_catalog, replan}
495
+ c) 중복 action 차단 — 같은 (action, input) 2회 연속 → 헛돌이로 판단,
496
+ 3회 연속이면 강제 unverifiable 종료
497
+ d) Tool 실행 → Observation 기록 (claims/<id>/observations/iterNNN_*.json)
498
+ e) Auto-finish 트리거:
499
+ - calculate 성공 후 fetch ≥2 (sibling base 있으면 ≥1) → 즉시 finish
500
+ - LLM이 finish 미호출하고 다음 fetch 시도하는 헛돌이 차단
501
+ f) FINISH 신호 → loop 종료
502
+
503
+ [3] AgentVerdict 생성
504
+ - LLM verdict vs 합성 verdict 일치 검증 → 불일치면 합성으로 자동 정정 (N 패치)
505
+ - verdict='comparison' 같은 claim_type 오입 → unverifiable 강등 (P17)
506
+ - fetch 성공 0건인데 match/mismatch 보고 → unverifiable 강등 (hallucination 가드)
507
+
508
+ [4] runtime_agent에서 primary/supporting Evidence 분리 (P7)
509
+ - primary: claim.schema.time_period와 매칭되는 fetch (없으면 dps[0])
510
+ - supporting: derived claim의 prev 시점 등 (base는 빈 list)
511
+ ```
512
+
513
+ ### ClaimType별 권장 시퀀스
514
+
515
+ | claim_type | 시퀀스 |
516
+ |---|---|
517
+ | `absolute` | catalog_search → fetch_evidence → finish |
518
+ | `growth_rate` | catalog_search → fetch×2 (current+prev) → calculate → finish |
519
+ | `difference` | catalog_search → fetch×2 → calculate → finish |
520
+ | `comparison` | catalog_search → fetch×2 (두 시점/대상) → finish |
521
+ | `ranking` | catalog_search → fetch×N (지역별) → 시스템 합성 비교 → finish |
522
+ | `aggregation` | catalog_search → fetch×N (N개 시점) → calculate(mean/sum/...) → finish |
523
+
524
+ ---
525
+
526
+ ## Tool 시스템 (Action 11종)
527
+
528
+ ### 검색·탐색
529
+
530
+ #### `catalog_search`
531
+ ```python
532
+ input: {
533
+ query: str,
534
+ category?: list[str],
535
+ top_k?: int = 15, # 기본 15 (cosine 6~15위 정답 포섭)
536
+ force_explore?: bool, # deep/meta_explore 강제 발동
537
+ explore_mode?: "meta" | "row_preview", # 기본 "meta" (빠름)
538
+ query_rewrite?: bool, # LLM이 query 변형 → 합집합 검색
539
+ source?: str,
540
+ }
541
+ output: {candidates: [{id, name, score, ...}, ...]}
542
+ ```
543
+ - pgvector 의미 검색 + KOSIS 통합검색 API union
544
+ - `prior_success` stat_id를 score=1.5로 prepend (P5, 라벨로 indicator 역추적)
545
+ - LLM `catalog_ranker` 활성 시 metadata 포함 batch ranking으로 try_ids 재정렬 (P26)
546
+ - top1 score 낮거나 top1-top2 gap 작으면 **deep_explore / meta_explore 자동 발동** (T1)
547
+
548
+ #### `explore_catalog`
549
+ - 카탈로그가 실제 어떤 카테고리 어휘를 쓰는지 LLM이 파악 (룰 매핑 없이 self-discover)
550
+
551
+ #### `deep_explore` (내부 헬퍼 — catalog_search가 자동 호출)
552
+ - top N 표의 sample row preview → LLM이 row 단서로 best 표 외삽 추천 (`mode="row_preview"`, 표당 ~22s)
553
+
554
+ #### `meta_explore` (내부 헬퍼 — catalog_search 기본 모드)
555
+ - top N 표의 `getMeta(ITM/OBJ)` 받아 LLM이 정답 표 식별 (표당 ~1s, 권장)
556
+
557
+ #### `query_rewriter` (내부 헬퍼)
558
+ - "체외충격파쇄석기 강원도" 같은 row-level keyword → "시군구별 의료장비"로 변형
559
+
560
+ ### 데이터 조회
561
+
562
+ #### `fetch_evidence`
563
+ ```python
564
+ input: {
565
+ candidate_id: str,
566
+ params: {indicator, time_period, population, unit_hint, match_criteria?, ...},
567
+ _candidate_fallbacks?: list[str], # LLM 미제공 시 catalog observation에서 자동 주입
568
+ }
569
+ output: {evidence: {value, unit, time_period, stat_table_id, rows, matched_row}}
570
+ ```
571
+ - claim.schema에서 params 자동 보강 (population은 LLM 값보다 schema가 우선 — L 패치)
572
+ - `match_criteria` carry-over 가드 (P15) — schema.population과 충돌하면 폐기
573
+ - 후보 순회 — value=None 응답이어도 다음 후보로 폴백 (P6, _candidate_fallbacks 자동 주입)
574
+ - `catalog_ranker` 활성 시 후보 pool을 LLM이 재정렬해 try_ids 결정 (P26)
575
+ - `relevance_judge` (P32) — 표 이름과 indicator의 의미 일치 LLM 판단 (룰 거부 시 fallback)
576
+ - `dimension_resolver` (P34) — KOSIS getMeta 보고 itmId/objL을 LLM이 동적 결정
577
+ - `_select_best_row` — 1차 strict 매칭 실패 시 `row_matcher` LLM rescue (P33c)
578
+ - 성공 시 `sibling_evidence` + `verified_facts` + `fetched_values` + `successful_stat_ids` 저장
579
+
580
+ ### 계산
581
+
582
+ #### `calculate`
583
+ ```python
584
+ input: {
585
+ expression: str, # 또는 alias: formula / expr / equation
586
+ variables: {var_name: number, ...},
587
+ }
588
+ output: {result: float, expression: str, variables: dict}
589
+ ```
590
+ - **eval 화이트리스트**: `+ - * / % **`, `abs`, `round`, `min`, `max`, `sqrt`, `log`, `log10`
591
+ - 증가율: `(current - prev) / prev * 100`
592
+ - 차이: `current - prev`
593
+ - 집계: `mean / sum / max / min / median` (aggregation claim용)
594
+
595
+ ### 종료·재계획
596
+
597
+ #### `finish`
598
+ ```python
599
+ input: {
600
+ verdict: "match" | "mismatch" | "partial" | "unverifiable",
601
+ confidence: 0.0~1.0,
602
+ explanation: str,
603
+ data_points?: [{indicator, time, resolved_value, source}],
604
+ }
605
+ output: {verdict, ...}
606
+ ```
607
+ - verdict가 enum 외 값 → `unverifiable`로 강등 (P17, hallucination 가드)
608
+ - evidence 없는데 match/mismatch → `unverifiable`로 강등
609
+ - workspace에 `verdict.json` + `memory.md` final 섹션 저장
610
+
611
+ #### `replan`
612
+ ```python
613
+ input: {reason: str}
614
+ output: {replan_count, new_plan?}
615
+ ```
616
+ - **호출 조건**: 모든 fetch 후보 실패 + catalog retry/query_rewrite/force_explore 다 시도한 후만
617
+ - **호출 효과**: planner LLM이 observation 보고 *완전히 새 plan* 생성 (claim_type 변경 가능)
618
+ - 예: `absolute` → `difference` 변경 (claim이 "증가 수 52"인데 표엔 절대값만)
619
+ - **per-claim 최대 2회** (`_REPLAN_MAX_PER_CLAIM=2`)
620
+ - replan 후 새 plan 따라 `fetch_evidence`/`calculate` 다시 진행
621
+
622
+ ### `read_original`
623
+ - 원문 기사 재독 (`{context_chars: 500}`) — claim 외 정보 필요할 때
624
+
625
+ ---
626
+
627
+ ## Workspace 시스템
628
+
629
+ 매 검증 job마다 `agent_workspace/job_<id>/` 디렉토리 생성. `id`는 `agent.workspace.scope`에 따라:
630
+ - `doc_hash` (default): `md5(raw_text)` — 같은 본문 재검증 시 캐시 공유
631
+ - `job_id`: API job_id — 매 요청 cold start (멀티-테넌트 격리에 안전)
632
+
633
+ ### 파일 구조
634
+ ```
635
+ agent_workspace/job_<id>/
636
+ ├── meta.json
637
+ ├── source.txt 원본 raw_text (markdown 포함, P18)
638
+ ├── memory.md job-level 메모리 (LLM 입력)
639
+ ├── verified_facts.json (indicator, time, population) 키 KOSIS 캐시 (verdict 확정값)
640
+ ├── fetched_values.json (stat_id, indicator, time, population) 키 raw fetch 캐시
641
+ ├── successful_stat_ids.json catalog prepend + fetch prior 1순위 stat_id 목록
642
+ ├── sibling_evidence.json sent_id 기반 base→derived 공유
643
+ ├── summary.json
644
+ └── claims/<claim_id>/
645
+ ├── claim.json
646
+ ├── plan.json Planner 출력 (replan 시 덮어씀)
647
+ ├── memory.md
648
+ ├── observations/ 매 iter raw 결과
649
+ │ ├── iter001_catalog_search.json
650
+ │ ├── iter002_fetch_DT_xxxx.json
651
+ │ ├── iter003_deep_explore.json
652
+ │ └── ...
653
+ ├── failed_stat_ids.json per-claim 실패 표 블랙리스트 (P33b, 무한 반복 차단)
654
+ ├── verdict.json 최종 AgentVerdict
655
+ └── log.jsonl
656
+ ```
657
+
658
+ ### 캐시 메커니즘
659
+
660
+ | 캐시 | 키 | 저장 시점 | 사용처 |
661
+ |---|---|---|---|
662
+ | **verified_facts** | (indicator, time_period, population) | finish의 verdict=match/mismatch | 다음 claim의 fetch lookup 직전. unit 호환 검사 + 파생 접미사 strip (v6.22) |
663
+ | **fetched_values** | (stat_id, indicator, time, population) | fetch_evidence 성공 직후 | 같은 claim의 다음 iter 또는 다른 claim이 동일 (stat_id, indicator) 재요청 시 KOSIS 호출 skip (2026-05-26) |
664
+ | **successful_stat_ids** | stat_id list | fetch 성공 1회 이상 | catalog_search 결과 맨 앞 prepend + fetch 후보 1순위 |
665
+ | **failed_stat_ids** | per-claim stat_id list | fetch가 거부/매칭 실패 | 같은 claim의 다음 catalog_search에서 제외 → 무한 반복 차단 (P33b) |
666
+ | **sibling_evidence** | sent_id → {role: evidence} | fetch 성공 직후 | 같은 sent_id의 base가 받은 값을 derived가 prev 없이 calculate에 inject |
667
+
668
+ ---
669
+
670
+ ## 설정 (`config/default.yaml`)
671
+
672
+ 핵심 섹션만 발췌. 자세한 내용은 파일 참조.
673
+
674
+ ### Agent
675
+ ```yaml
676
+ agent:
677
+ enabled: true
678
+ workspace:
679
+ backend: "local"
680
+ local_path: "./agent_workspace"
681
+ scope: "doc_hash" # "doc_hash" | "job_id"
682
+ external_job_id: null # sv_platform이 자동 주입
683
+ persist_after_job: true
684
+ cleanup_after_days: 7
685
+ loop:
686
+ mode: "reflect" # "reflect" | "deterministic"
687
+ max_iterations: 10
688
+ enable_reflection: true
689
+ single_pass_fallback: true
690
+ early_stop_on_confidence: 0.9
691
+ llm:
692
+ plan_model: "structured" # HCX-007
693
+ reflect_model: "light" # HCX-DASH-002
694
+ explain_model: "heavy" # HCX-003
695
+ budget:
696
+ max_tokens_per_job: 100000
697
+ max_concurrent_claims: 3
698
+ ```
699
+
700
+ ### LLM (전역 rate limit 포함)
701
+ ```yaml
702
+ llm:
703
+ provider: "hcx"
704
+ models:
705
+ heavy: "HCX-003"
706
+ light: "HCX-DASH-002"
707
+ structured: "HCX-007"
708
+ temperature: 0.1
709
+ max_tokens: 4096 # 2048→4096 (finish의 explanation 잘림 방지, 2026-05-21)
710
+ api_key_env: "NCP_API_KEY"
711
+ min_call_interval_ms: 600 # 전역 HCX rate limit (~1.6 req/s, 429 대응)
712
+
713
+ embedding:
714
+ provider: "hcx"
715
+ model: "HCX-EMB-V2" # 1024-dim
716
+ api_key_env: "NCP_API_KEY"
717
+
718
+ reranker:
719
+ provider: "hcx"
720
+ model: "HCX-RERANKER"
721
+ api_key_env: "NCP_API_KEY"
722
+ ```
723
+
724
+ ### Candidate Detection
725
+ ```yaml
726
+ candidate_detection:
727
+ enabled: true
728
+ threshold: 0.65
729
+ use_surface_signals: true
730
+ teacher_llm_fallback: true
731
+ concurrency: 2 # LLM 동시 호출 상한
732
+ ```
733
+
734
+ ### KOSIS DataSource
735
+ ```yaml
736
+ data_sources:
737
+ enabled: ["kosis"]
738
+ default_source: "kosis"
739
+ kosis: # KOSISDataSource(config=...)에 전달
740
+ catalog_ranker: # P26 — LLM batch ranking
741
+ enabled: true
742
+ score_threshold: 0.15
743
+ pool_limit: 20
744
+ max_try: 10
745
+ model_tier: "light"
746
+ relevance_guard:
747
+ enabled: true
748
+ llm_fallback: true # P32 — 룰 거부 시 LLM 의미 판단 1회
749
+ model_tier: "light"
750
+ row_match_llm_fallback: true # P33c — _select_best_row 매칭 0건 LLM rescue
751
+ row_match_model_tier: "light"
752
+ dimension_resolver: # P34 — itmId/objL 동적 결정
753
+ # (config는 default.yaml 참조)
754
+ ```
755
+
756
+ ### KOSIS (top-level — API URL, catalog 구축용)
757
+ ```yaml
758
+ kosis:
759
+ base_url: "https://kosis.kr/openapi"
760
+ api_key_env: "KOSIS_API_KEY"
761
+ pgvector_dsn_env: "PGVECTOR_DSN"
762
+ catalog:
763
+ rebuild: false
764
+ embed_batch_size: 100
765
+ min_rows: 1000
766
+ ```
767
+
768
+ > ⚠️ `data_sources.kosis`(소스별 ranker/guard 설정)와 top-level `kosis`(API URL + catalog 빌드)는 **별개**.
769
+
770
+ ---
771
+
772
+ ## 데이터 모델 (`core/schemas.py`)
773
+
774
+ ### Claim
775
+ ```python
776
+ class ClaimSchema:
777
+ indicator: str | None
778
+ time_period: str | None # "YYYY" | "YYYY-MM"
779
+ unit: str | None
780
+ population: str | None
781
+ value: float | None # P4 폴백 후
782
+ parent_path: str | None
783
+ # derived 지원
784
+ prev_value: float | None
785
+ prev_time_period: str | None
786
+ prev_phrase: str | None
787
+ value_role: str | None # "base" | "derived_rate" | "derived_difference" | "aggregation"
788
+ # aggregation 지원 (2026-05-21)
789
+ aggregation: str | None # "mean" | "sum" | "max" | "min" | "median"
790
+ aggregation_window: int | None # "최근 N년" 의 N
791
+ aggregation_time_range: list[str] | None # 명시 시점 리스트
792
+ ```
793
+
794
+ ### ClaimType / ActionType / VerdictType
795
+ ```python
796
+ class ClaimType(str, Enum):
797
+ ABSOLUTE / DIFFERENCE / GROWTH_RATE / COMPARISON / RANKING / AGGREGATION / UNKNOWN
798
+
799
+ class ActionType(str, Enum):
800
+ CATALOG_SEARCH / EXPLORE_CATALOG / FETCH_EVIDENCE / READ_ORIGINAL /
801
+ CALCULATE / REPLAN / FINISH
802
+
803
+ class VerdictType(str, Enum):
804
+ MATCH / MISMATCH / PARTIAL / UNVERIFIABLE
805
+ ```
806
+
807
+ ### VerificationResult
808
+ ```python
809
+ class VerificationResult:
810
+ claim_id: UUID
811
+ verdict: VerdictType
812
+ confidence: float
813
+ evidence: Evidence | None # primary
814
+ supporting_evidence: list[Evidence] # derived claim의 prev 등 (P7)
815
+ explanation: str | None
816
+ computed_value: float | None # calculate 결과
817
+ formula: str | None
818
+ ```
819
+
820
+ ---
821
+
822
+ ## 테스트
823
+
824
+ ```bash
825
+ cd backend
826
+ pytest # 전체
827
+ pytest structverify/agent/ # agent 모듈만
828
+ pytest -k "schema_inductor" # 특정 키워드
829
+ ```
830
+
831
+ ---
832
+
833
+ ## 운영 노트
834
+
835
+ ### LLM 호출 빈도가 신경 쓰일 때
836
+ - `config.llm.min_call_interval_ms` (default 600 → ≈1.6 req/s)
837
+ - `config.candidate_detection.concurrency` (default 2)
838
+ - HCX 쿼터 늘렸으면 둘 다 하향 가능 (200ms + 4 정도)
839
+ - 429 폭주 시 자동 jitter (0~0.7s) + exponential backoff (1/2/4초)
840
+
841
+ ### Workspace 캐시가 stale일 때
842
+ - `scope: "doc_hash"`라 같은 본문이면 직전 검증 결과 재사용 (정상 동작)
843
+ - 완전 cold 검증 필요 시:
844
+ - `config.agent.workspace.scope: "job_id"`로 전환
845
+ - 또는 `rm -rf agent_workspace/`
846
+ - 특정 캐시만 무효화하고 싶으면 해당 JSON 파일만 삭제:
847
+ - `agent_workspace/job_<id>/verified_facts.json` (verdict 캐시)
848
+ - `agent_workspace/job_<id>/fetched_values.json` (raw fetch 캐시)
849
+
850
+ ### URL 추출이 실패할 때
851
+ - 1차 `trafilatura`가 본문 200자 미만이면 자동 LLMScraper 폴백
852
+ - LLMScraper는 Docker sandbox에서 동적 Python 코드 실행 — `preprocessing.sandbox_backend` 설정 확인
853
+ - 둘 다 실패하면 빈 문자열 → claim 0건 (pipeline 정상 종료, 결과 없음)
854
+
855
+ ### KOSIS API 429 / 데이터 lag
856
+ - KOSIS 응답이 1~2년 지연되는 경우가 많음 (예: 2024-06 기사에서 "최신 데이터"는 2022~2023)
857
+ - `_select_best_row`는 명시 시점(prd_target) 정확 매칭만 허용 — 시점 누락 row를 default로 잡지 않음 (패치 3-2). 안전 우선이라 unverifiable로 떨어질 수 있음.
858
+
859
+ ### 신규 DataSource 추가 (KOSIS 외)
860
+ 1. `retrieval/`에 `BaseDataSource` 상속 클래스 작성
861
+ 2. `@register_datasource("name")` 데코레이터
862
+ 3. `config/default.yaml`의 `data_sources.enabled`에 `"name"` 추가
863
+ 4. `data_sources.<name>` 설정 섹션 추가 (LLM key 등)
864
+
865
+ ---
866
+
867
+ ## 알려진 한계 / 추후 개발
868
+
869
+ ### 미구현
870
+ - **Builder Agent (`agent/builder_agent.py`)** — placeholder 상태. KOSIS Self-Instruct → LoRA fine-tuning 파이프라인 자체는 `adaptation/`에 있지만 운영 데이터 축적 후 활성화 예정.
871
+ - **PDF/DOCX 업로드** — sv_platform 라우트는 받지만 multipart 처리는 Phase 3 예정 (현재 400 응답).
872
+ - **Custom DataSource API** — `custom_csv` / `custom_db` config 자리는 있지만 활성화 미완 (Phase 4).
873
+ - **Neo4j 활성화** — 현재 `graph.store.enabled=false` 기본. 멀티홉 검증 강화 시 활성.
874
+
875
+ ### 한계 (설계상)
876
+ - **지역명 변경 대응** — "강원도" ↔ "강원특별자치도" (2023 행정구역 개명)는 substring 매칭만 — `'강원도' in '강원특별자치도'`는 False라 매칭 실패 가능. 일부 케이스에서 unverifiable.
877
+ - **historical claim (1990년대)** — catalog 임베딩이 표 이름만 기반이라 historical 시리즈 표가 cosine 깊이 묻혀 surface 못 함.
878
+ - **외국/지자체 자체 통계** — KOSIS에 없는 데이터는 검증 불가 (의도된 거부).
879
+ - **순위/예측만 있는 표현** — claim_detector가 의도적으로 필터 (검증 가능한 수치 없음).
880
+ - **LLM hallucination** — planner/reflect가 가짜 candidate_id 박는 케이스. 현재는 fetch 후 next-candidate fallback만 있고 placeholder 자체 필터링은 없음.
881
+ - **prev_time fetch 실패** — growth_rate/difference의 prev 시점이 catalog에서 못 찾으면 max_iter 후 unverifiable.
882
+
883
+ ### 시연 추천
884
+ - ✅ 최근 1~3년 + 전국/시도 단위 + 단순 absolute / growth_rate
885
+ - ✅ "출생아 수", "고용률", "실업률", "소비자물가지수" 등 KOSIS 핵심 통계
886
+ - ⚠️ 강원도/전라북도/제주도 시도 (개명 영향), 1990년대 이전, 외국 통계는 피하거나 사전 해명
887
+
888
+ ---
889
+
890
+ ## 라이선스
891
+
892
+ **StructVerify는 [MIT 라이선스](LICENSE)** — 자유롭게 사용·수정·배포 가능.
893
+
894
+ 의존성도 모두 **permissive 라이선스**만 사용한다 (copyleft 오염 없음):
895
+
896
+ | 범위 | 패키지 | 라이선스 |
897
+ |---|---|---|
898
+ | **코어** | pydantic, pyyaml · httpx, python-dotenv, uvicorn · openai, json5, asyncpg, trafilatura, neo4j, mlflow, boto3, snowflake · pdfplumber, kss, redis, fastapi, sqlalchemy, langfuse, python-docx | MIT · BSD-3 · Apache-2.0 |
899
+ | **격리(opt-in)** | PyMuPDF (`[pdf-ocr]` extra) | ⚠️ **AGPL-3.0** |
900
+
901
+ > 유일한 copyleft인 **PyMuPDF(AGPL)** 는 고급 OCR PDF 파이프라인 전용으로 `[pdf-ocr]`
902
+ > extra에 격리했다. 기본 PDF 처리는 **pdfplumber(MIT)** 를 쓰므로, `pip install structverify`
903
+ > 및 `[all]` 어떤 경로로도 AGPL이 딸려오지 않는다. AGPL이 괜찮은 경우에만 직접 opt-in.