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,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# 수정자: 박재윤
|
|
3
|
+
# 수정 날짜: 2026-04-27
|
|
4
|
+
# 수정 내용: KOSIS 전체 통계표 메타데이터 수집 및 pgvector INSERT 구현
|
|
5
|
+
|
|
6
|
+
# [DONE] _fetch_category KOSIS API 실제 호출 구현
|
|
7
|
+
# [DONE] save_to_db pgvector 임베딩 INSERT 구현
|
|
8
|
+
# [DONE] 주제별 통계(MT_ZTITLE) 카테고리 전체 확장
|
|
9
|
+
# [DONE] save_to_db 배치 임베딩으로 최적화 (100건 단위)
|
|
10
|
+
# [DONE] NCP 임베딩 모델로 교체 (HCX 임베딩 v2, 1024차원)
|
|
11
|
+
# [DONE] save_to_db 세마포어 기반 rate limit 제어 (asyncio.Semaphore, 3개 동시 + 재시도)
|
|
12
|
+
# [TODO] asyncpg로 마이그레이션 (현재 psycopg2 임시 사용)
|
|
13
|
+
# [TODO] 기관별 통계(MT_OTITLE) 수집 추가 (별도 배치 스크립트)
|
|
14
|
+
# [김예슬 - 2026-04-30 / v2]
|
|
15
|
+
# [DONE] is_catalog_ready(): catalog 구축 여부 확인 함수 추가
|
|
16
|
+
# [DONE] save_to_db(): embedding 미완성 행만 임베딩 처리 (skip 최적화)
|
|
17
|
+
# [DONE] builder_agent에서 catalog.rebuild=false 이면 crawl_kosis_catalog skip
|
|
18
|
+
|
|
19
|
+
adaptation/kosis_crawler.py — KOSIS 통계표 메타데이터 전량 수집 (Step 0-1)
|
|
20
|
+
|
|
21
|
+
서비스 시작 전 1회 실행하여 KOSIS의 모든 통계표 메타데이터를 수집한다.
|
|
22
|
+
수집된 데이터는 2가지 용도로 사용:
|
|
23
|
+
1) RAG — 임베딩하여 pgvector에 저장 (방법 1)
|
|
24
|
+
2) Self-Instruct — 합성 학습 데이터 생성의 seed (방법 3)
|
|
25
|
+
|
|
26
|
+
[참고] Self-Instruct (Wang et al., ACL 2023)
|
|
27
|
+
- https://github.com/yizhongw/self-instruct
|
|
28
|
+
- seed 데이터(통계표 메타)로부터 학습 데이터를 자동 생성하는 출발점
|
|
29
|
+
|
|
30
|
+
사용법:
|
|
31
|
+
python -m adaptation.kosis_crawler # CLI 실행
|
|
32
|
+
await crawl_kosis_catalog(config) # 코드에서 호출
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from structverify.utils.logger import get_logger
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── [v2] Catalog 준비 여부 확인 ──────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
async def is_catalog_ready(config: dict | None = None, min_rows: int = 1000) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
kosis_stat_catalog에 데이터가 충분히 있으면 True.
|
|
50
|
+
|
|
51
|
+
[v2 박재윤/김예슬 - 2026-04-30]
|
|
52
|
+
builder_agent.pretrain_domain()에서 호출:
|
|
53
|
+
config.kosis.catalog.rebuild=false + is_catalog_ready=True
|
|
54
|
+
→ crawl_kosis_catalog() skip (재수집 방지)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
min_rows: 이 수 이상이면 "이미 구축됨"으로 판단 (기본 1000)
|
|
58
|
+
"""
|
|
59
|
+
config = config or {}
|
|
60
|
+
try:
|
|
61
|
+
import asyncpg
|
|
62
|
+
pg_dsn = os.environ.get(
|
|
63
|
+
config.get("pgvector_dsn_env", "PGVECTOR_DSN"),
|
|
64
|
+
"postgresql://structverify:svpass123@localhost:5432/structverify",
|
|
65
|
+
)
|
|
66
|
+
conn = await asyncpg.connect(pg_dsn)
|
|
67
|
+
total = await conn.fetchval("SELECT COUNT(*) FROM kosis_stat_catalog") or 0
|
|
68
|
+
embedded = await conn.fetchval(
|
|
69
|
+
"SELECT COUNT(*) FROM kosis_stat_catalog WHERE embedding IS NOT NULL"
|
|
70
|
+
) or 0
|
|
71
|
+
await conn.close()
|
|
72
|
+
logger.info(
|
|
73
|
+
f"kosis_stat_catalog: total={total}, embedded={embedded} "
|
|
74
|
+
f"(min_rows 기준={min_rows})"
|
|
75
|
+
)
|
|
76
|
+
return int(total) >= min_rows
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.debug(f"catalog 확인 실패 (테이블 없거나 DB 미연결): {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
# [기존] - 박재윤: 기존 카테고리 (ID 오류)
|
|
82
|
+
# KOSIS_TOP_CATEGORIES = [
|
|
83
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "A"}, # 인구/가구
|
|
84
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "B"}, # 고용/노동/임금
|
|
85
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "F"}, # 농림수산식품
|
|
86
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "H"}, # 물가/가계
|
|
87
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "I"}, # 경기/기업경영
|
|
88
|
+
# {"vw_cd": "MT_ZTITLE", "parent_id": "N"}, # 국민계정/재정/금융
|
|
89
|
+
# ]
|
|
90
|
+
|
|
91
|
+
# [v1] - 박재윤: 실제 KOSIS API 확인 후 전체 카테고리로 확장
|
|
92
|
+
KOSIS_TOP_CATEGORIES = [
|
|
93
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "A"},
|
|
94
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "B"},
|
|
95
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "C"},
|
|
96
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "D"},
|
|
97
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "E"},
|
|
98
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "F"},
|
|
99
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "G"},
|
|
100
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "H1"},
|
|
101
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "H2"},
|
|
102
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "I1"},
|
|
103
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "I2"},
|
|
104
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "J1"},
|
|
105
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "J2"},
|
|
106
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "K1"},
|
|
107
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "K2"},
|
|
108
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "L"},
|
|
109
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "M1"},
|
|
110
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "M2"},
|
|
111
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "N1"},
|
|
112
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "N2"},
|
|
113
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "O"},
|
|
114
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "P1"},
|
|
115
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "P2"},
|
|
116
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "Q"},
|
|
117
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "R"},
|
|
118
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "S1"},
|
|
119
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "S2"},
|
|
120
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "T"},
|
|
121
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "U"},
|
|
122
|
+
{"vw_cd": "MT_ZTITLE", "parent_id": "V"},
|
|
123
|
+
# {"vw_cd": "MT_OTITLE", "parent_id": ""}, # 기관별 전체
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def crawl_kosis_catalog(config: dict | None = None) -> list[dict[str, Any]]:
|
|
128
|
+
from dotenv import load_dotenv
|
|
129
|
+
load_dotenv()
|
|
130
|
+
|
|
131
|
+
config = config or {}
|
|
132
|
+
kosis_config = config.get("kosis", {})
|
|
133
|
+
api_key = os.environ.get(kosis_config.get("api_key_env", "KOSIS_API_KEY"), "")
|
|
134
|
+
base_url = kosis_config.get("base_url", "https://kosis.kr/openapi")
|
|
135
|
+
timeout = kosis_config.get("timeout", 30)
|
|
136
|
+
|
|
137
|
+
if not api_key:
|
|
138
|
+
logger.error("KOSIS_API_KEY 환경변수가 설정되지 않았습니다")
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
all_tables: dict[str, dict] = {}
|
|
142
|
+
|
|
143
|
+
for category in KOSIS_TOP_CATEGORIES:
|
|
144
|
+
try:
|
|
145
|
+
tables = await _fetch_category(base_url, api_key, category, timeout)
|
|
146
|
+
for t in tables:
|
|
147
|
+
all_tables[t["stat_id"]] = t
|
|
148
|
+
logger.info(f"카테고리 {category['parent_id']} 수집: {len(tables)}건")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"카테고리 {category['parent_id']} 수집 실패: {e}")
|
|
151
|
+
|
|
152
|
+
result = list(all_tables.values())
|
|
153
|
+
logger.info(f"KOSIS 메타데이터 수집 완료: 총 {len(result)}건 (중복 제거됨)")
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def _fetch_category(base_url, api_key, category, timeout):
|
|
158
|
+
import httpx, re
|
|
159
|
+
|
|
160
|
+
results = []
|
|
161
|
+
|
|
162
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
163
|
+
async def fetch_recursive(parent_id, path_history, depth=0):
|
|
164
|
+
if depth > 7:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
url = (
|
|
168
|
+
f"https://kosis.kr/openapi/statisticsList.do?method=getList"
|
|
169
|
+
f"&apiKey={api_key}&vwCd={category['vw_cd']}"
|
|
170
|
+
f"&parentListId={parent_id}&format=json"
|
|
171
|
+
)
|
|
172
|
+
response = await client.get(url)
|
|
173
|
+
text = response.text.strip()
|
|
174
|
+
try:
|
|
175
|
+
data = response.json()
|
|
176
|
+
except Exception:
|
|
177
|
+
fixed = re.sub(r'([{,])\s*([A-Za-z_][A-Za-z0-9_]*)\s*:', r'\1"\2":', text)
|
|
178
|
+
try:
|
|
179
|
+
data = json.loads(fixed)
|
|
180
|
+
except Exception:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if not isinstance(data, list):
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
for item in data:
|
|
187
|
+
if 'TBL_ID' in item:
|
|
188
|
+
results.append({
|
|
189
|
+
"stat_id": item['TBL_ID'],
|
|
190
|
+
"stat_name": item.get('TBL_NM', ''),
|
|
191
|
+
"org_id": item.get('ORG_ID', ''),
|
|
192
|
+
"org_name": item.get('ORG_NM', ''),
|
|
193
|
+
"category_path": path_history,
|
|
194
|
+
"keywords": _extract_keywords(item.get('TBL_NM', '')),
|
|
195
|
+
"available_periods": [],
|
|
196
|
+
"description": item.get('TBL_NM', ''),
|
|
197
|
+
})
|
|
198
|
+
elif 'LIST_ID' in item:
|
|
199
|
+
current_path = f"{path_history} > {item.get('LIST_NM', '')}"
|
|
200
|
+
await fetch_recursive(item['LIST_ID'], current_path, depth+1)
|
|
201
|
+
|
|
202
|
+
await fetch_recursive(category['parent_id'], category['vw_cd'])
|
|
203
|
+
|
|
204
|
+
return results
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _extract_keywords(stat_name: str) -> list[str]:
|
|
208
|
+
stopwords = {"및", "의", "에", "별", "현황", "통계", "기준", "연도", "시도"}
|
|
209
|
+
tokens = stat_name.replace("(", " ").replace(")", " ").split()
|
|
210
|
+
return [t for t in tokens if len(t) >= 2 and t not in stopwords]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def save_to_db(catalog: list[dict], config: dict | None = None) -> int:
|
|
215
|
+
"""
|
|
216
|
+
수집된 메타데이터 → kosis_stat_catalog INSERT + 임베딩 생성.
|
|
217
|
+
|
|
218
|
+
[v2 박재윤/김예슬 - 2026-04-30]
|
|
219
|
+
embedding skip 최적화:
|
|
220
|
+
이미 embedding이 있는 stat_id는 임베딩 API 호출 skip.
|
|
221
|
+
embedding IS NULL인 행만 임베딩 처리.
|
|
222
|
+
→ 재실행 시 API 비용/시간 절감.
|
|
223
|
+
|
|
224
|
+
skip 판단:
|
|
225
|
+
1) INSERT ON CONFLICT DO UPDATE에서 embedding이 이미 있으면 기존 값 유지
|
|
226
|
+
2) catalog에 있지만 DB에 없는 신규 행만 임베딩 생성
|
|
227
|
+
"""
|
|
228
|
+
import psycopg2
|
|
229
|
+
from dotenv import load_dotenv
|
|
230
|
+
load_dotenv()
|
|
231
|
+
|
|
232
|
+
# [#67-D] 인라인 HCX 임베딩(get_embedding_safe) → 공용 EmbeddingClient.embed_batch 로 교체.
|
|
233
|
+
# config.embedding({provider, model, api_key_env}) 사용. config 없으면 {} →
|
|
234
|
+
# provider 기본 hcx + CLOVASTUDIO_API_KEY → 기존 키/동작 보존.
|
|
235
|
+
# Semaphore(3)+429 지수백오프+zero폴백([0.0]*EMBEDDING_DIM)은
|
|
236
|
+
# EmbeddingClient._embed_batch_hcx 에 그대로 이식돼 있음.
|
|
237
|
+
from structverify.utils.embedding_client import EmbeddingClient
|
|
238
|
+
embedder = EmbeddingClient((config or {}).get("embedding", {}))
|
|
239
|
+
|
|
240
|
+
conn = psycopg2.connect(
|
|
241
|
+
host=os.getenv("POSTGRES_HOST"),
|
|
242
|
+
port=os.getenv("POSTGRES_PORT"),
|
|
243
|
+
dbname=os.getenv("POSTGRES_DB"),
|
|
244
|
+
user=os.getenv("POSTGRES_USER"),
|
|
245
|
+
password=os.getenv("POSTGRES_PASSWORD")
|
|
246
|
+
)
|
|
247
|
+
cur = conn.cursor()
|
|
248
|
+
|
|
249
|
+
BATCH_SIZE = 100
|
|
250
|
+
for i in range(0, len(catalog), BATCH_SIZE):
|
|
251
|
+
batch = catalog[i:i+BATCH_SIZE]
|
|
252
|
+
texts = [f"{item['category_path']} {item['stat_name']}" for item in batch]
|
|
253
|
+
|
|
254
|
+
embeddings_list = await embedder.embed_batch(texts)
|
|
255
|
+
|
|
256
|
+
for item, embedding in zip(batch, embeddings_list):
|
|
257
|
+
# [v2 박재윤/김예슬] embedding skip 최적화:
|
|
258
|
+
# - 신규 행: embedding 포함 INSERT
|
|
259
|
+
# - 기존 행: stat_name/fetched_at만 UPDATE, embedding은 기존 값 유지
|
|
260
|
+
# (ON CONFLICT DO UPDATE에서 embedding은 COALESCE로 이미 있는 값 보존)
|
|
261
|
+
cur.execute("""
|
|
262
|
+
INSERT INTO kosis_stat_catalog
|
|
263
|
+
(stat_id, stat_name, org_id, org_name, category_path,
|
|
264
|
+
keywords, embedding, raw_meta_json)
|
|
265
|
+
VALUES (%s, %s, %s, %s, %s, %s, %s::vector, %s::jsonb)
|
|
266
|
+
ON CONFLICT (stat_id) DO UPDATE
|
|
267
|
+
SET stat_name = EXCLUDED.stat_name,
|
|
268
|
+
org_name = EXCLUDED.org_name,
|
|
269
|
+
category_path= EXCLUDED.category_path,
|
|
270
|
+
keywords = EXCLUDED.keywords,
|
|
271
|
+
raw_meta_json= EXCLUDED.raw_meta_json,
|
|
272
|
+
fetched_at = NOW(),
|
|
273
|
+
embedding = COALESCE(kosis_stat_catalog.embedding, EXCLUDED.embedding)
|
|
274
|
+
""", (
|
|
275
|
+
item["stat_id"],
|
|
276
|
+
item["stat_name"],
|
|
277
|
+
item["org_id"],
|
|
278
|
+
item["org_name"],
|
|
279
|
+
item["category_path"],
|
|
280
|
+
item["keywords"],
|
|
281
|
+
str(embedding),
|
|
282
|
+
json.dumps(item)
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
conn.commit()
|
|
286
|
+
print(f"✅ {i+len(batch)}/{len(catalog)}건 저장 완료")
|
|
287
|
+
|
|
288
|
+
cur.close()
|
|
289
|
+
conn.close()
|
|
290
|
+
return len(catalog)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
import asyncio
|
|
295
|
+
|
|
296
|
+
async def main():
|
|
297
|
+
logger.info("=== KOSIS 메타데이터 수집 시작 ===")
|
|
298
|
+
|
|
299
|
+
CACHE_FILE = "kosis_catalog_cache.json"
|
|
300
|
+
|
|
301
|
+
if os.path.exists(CACHE_FILE):
|
|
302
|
+
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
|
303
|
+
catalog = json.load(f)
|
|
304
|
+
logger.info(f"캐시에서 로드: {len(catalog)}건")
|
|
305
|
+
else:
|
|
306
|
+
catalog = await crawl_kosis_catalog()
|
|
307
|
+
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
|
308
|
+
json.dump(catalog, f, ensure_ascii=False)
|
|
309
|
+
logger.info("캐시 저장 완료")
|
|
310
|
+
|
|
311
|
+
for item in catalog[:3]:
|
|
312
|
+
print(item)
|
|
313
|
+
|
|
314
|
+
count = await save_to_db(catalog[120000:])
|
|
315
|
+
logger.info(f"=== 완료: {count}건 저장 ===")
|
|
316
|
+
|
|
317
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adaptation/sample_builder.py — 학습 샘플 포맷 변환
|
|
3
|
+
|
|
4
|
+
[김예슬 - 2026-04-24]
|
|
5
|
+
- candidate_detection 태스크 샘플 포맷 추가
|
|
6
|
+
· instruction/input/output 필드 구조로 통일
|
|
7
|
+
· HuggingFace Dataset 호환 + NCP Tuning API JSONL 호환
|
|
8
|
+
- 태스크별 샘플 수 로깅 추가
|
|
9
|
+
|
|
10
|
+
[참고] Self-Instruct (Wang et al., ACL 2023)
|
|
11
|
+
[참고] KnowLA (NAACL 2024)
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from structverify.core.schemas import FeedbackEvent
|
|
19
|
+
from structverify.utils.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_training_samples(
|
|
25
|
+
events: list[FeedbackEvent] | None = None,
|
|
26
|
+
synthetic: list[dict[str, Any]] | None = None,
|
|
27
|
+
mode: str = "finetune",
|
|
28
|
+
) -> list[dict[str, Any]]:
|
|
29
|
+
"""
|
|
30
|
+
학습 데이터 → LoRA 학습 포맷 변환.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
events: 피드백 이벤트 (mode="finetune")
|
|
34
|
+
synthetic: 합성 데이터 (mode="pretrain")
|
|
35
|
+
mode: "pretrain" | "finetune"
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
[{"task": ..., "instruction": ..., "input": ..., "output": ...}, ...]
|
|
39
|
+
"""
|
|
40
|
+
if mode == "pretrain":
|
|
41
|
+
return _build_pretrain_samples(synthetic or [])
|
|
42
|
+
elif mode == "finetune":
|
|
43
|
+
return _build_finetune_samples(events or [])
|
|
44
|
+
raise ValueError(f"미지원 모드: {mode}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_pretrain_samples(synthetic: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
합성 데이터 → instruction-tuning 포맷 변환.
|
|
50
|
+
|
|
51
|
+
생성 태스크:
|
|
52
|
+
1) claim_to_stat : 주장 → 관련 통계표
|
|
53
|
+
2) claim_to_schema : 주장 → 구조화 스키마
|
|
54
|
+
3) stat_to_claim : 통계표 → 주장 판별 (역방향)
|
|
55
|
+
4) candidate_detection: 검증 후보/비후보 분류
|
|
56
|
+
"""
|
|
57
|
+
samples: list[dict[str, Any]] = []
|
|
58
|
+
task_counts: dict[str, int] = {}
|
|
59
|
+
|
|
60
|
+
for item in synthetic:
|
|
61
|
+
task = item.get("task", "")
|
|
62
|
+
|
|
63
|
+
# ── candidate detection 샘플 ──────────────────────────────────
|
|
64
|
+
if task == "candidate_detection":
|
|
65
|
+
sentence = item.get("sentence", "")
|
|
66
|
+
label = item.get("candidate_label", False)
|
|
67
|
+
|
|
68
|
+
sample = {
|
|
69
|
+
"task": "candidate_detection",
|
|
70
|
+
"instruction": (
|
|
71
|
+
"아래 문장이 공식 통계로 검증 가능한 수치 기반 주장인지 판단하세요.\n"
|
|
72
|
+
'JSON으로만 답하세요: {"candidate_label": true 또는 false}'
|
|
73
|
+
),
|
|
74
|
+
"input": sentence,
|
|
75
|
+
"output": json.dumps({"candidate_label": label}, ensure_ascii=False),
|
|
76
|
+
}
|
|
77
|
+
samples.append(sample)
|
|
78
|
+
task_counts["candidate_detection"] = task_counts.get("candidate_detection", 0) + 1
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# ── claim/schema 샘플 ─────────────────────────────────────────
|
|
82
|
+
claim = item.get("claim", "")
|
|
83
|
+
stat_id = item.get("stat_id", "")
|
|
84
|
+
stat_name = item.get("stat_name", "")
|
|
85
|
+
indicator = item.get("indicator", "")
|
|
86
|
+
schema = item.get("schema", {})
|
|
87
|
+
|
|
88
|
+
if not claim or not stat_id:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# 태스크 1: claim_to_stat
|
|
92
|
+
samples.append({
|
|
93
|
+
"task": "claim_to_stat",
|
|
94
|
+
"instruction": "아래 주장을 검증하기 위해 필요한 KOSIS 통계표를 찾으세요.",
|
|
95
|
+
"input": claim,
|
|
96
|
+
"output": json.dumps(
|
|
97
|
+
{"stat_id": stat_id, "stat_name": stat_name, "indicator": indicator},
|
|
98
|
+
ensure_ascii=False,
|
|
99
|
+
),
|
|
100
|
+
})
|
|
101
|
+
task_counts["claim_to_stat"] = task_counts.get("claim_to_stat", 0) + 1
|
|
102
|
+
|
|
103
|
+
# 태스크 2: claim_to_schema
|
|
104
|
+
if schema and isinstance(schema, dict) and "raw" not in schema:
|
|
105
|
+
samples.append({
|
|
106
|
+
"task": "claim_to_schema",
|
|
107
|
+
"instruction": "아래 주장에서 검증에 필요한 핵심 정보를 추출하세요.",
|
|
108
|
+
"input": claim,
|
|
109
|
+
"output": json.dumps(schema, ensure_ascii=False),
|
|
110
|
+
})
|
|
111
|
+
task_counts["claim_to_schema"] = task_counts.get("claim_to_schema", 0) + 1
|
|
112
|
+
|
|
113
|
+
# 태스크 3: stat_to_claim (역방향)
|
|
114
|
+
samples.append({
|
|
115
|
+
"task": "stat_to_claim",
|
|
116
|
+
"instruction": (
|
|
117
|
+
f'통계표 "{stat_name}"({stat_id})로 아래 주장을 검증할 수 있습니까?\n'
|
|
118
|
+
'JSON으로만 답하세요: {"verifiable": true 또는 false}'
|
|
119
|
+
),
|
|
120
|
+
"input": claim,
|
|
121
|
+
"output": json.dumps({"verifiable": True}, ensure_ascii=False),
|
|
122
|
+
})
|
|
123
|
+
task_counts["stat_to_claim"] = task_counts.get("stat_to_claim", 0) + 1
|
|
124
|
+
|
|
125
|
+
logger.info(f"사전학습 샘플: 총 {len(samples)}건")
|
|
126
|
+
logger.info(f"태스크별 분포: {task_counts}")
|
|
127
|
+
return samples
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _build_finetune_samples(events: list[FeedbackEvent]) -> list[dict[str, Any]]:
|
|
131
|
+
"""피드백 이벤트 → 추가 학습 포맷 변환"""
|
|
132
|
+
samples: list[dict[str, Any]] = []
|
|
133
|
+
for ev in events:
|
|
134
|
+
if not ev.corrected_verdict:
|
|
135
|
+
continue
|
|
136
|
+
samples.append({
|
|
137
|
+
"task": "verdict_correction",
|
|
138
|
+
"instruction": "아래 주장에 대한 검증 결과를 판정하세요.",
|
|
139
|
+
"input": str(ev.claim_id),
|
|
140
|
+
"output": json.dumps(
|
|
141
|
+
{"verdict": ev.corrected_verdict.value},
|
|
142
|
+
ensure_ascii=False,
|
|
143
|
+
),
|
|
144
|
+
"original_verdict": ev.original_verdict.value if ev.original_verdict else None,
|
|
145
|
+
"reviewer_note": ev.reviewer_note,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
logger.info(f"피드백 샘플: {len(samples)}건 (원본 {len(events)}건)")
|
|
149
|
+
return samples
|