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,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
|
+

|
|
125
|
+

|
|
126
|
+

|
|
127
|
+
|
|
128
|
+

|
|
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.
|