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,320 @@
1
+ """
2
+ adaptation/synthetic_generator.py — 합성 학습 데이터 자동 생성 (Step 0-2)
3
+
4
+ [김예슬 - 2026-04-24]
5
+ - CANDIDATE_DETECTION_PROMPT 추가:
6
+ · positive: 통계표에서 파생된 검증 가능 수치 주장
7
+ · negative: 의견/일정/감상 등 검증 불가 문장
8
+ - _generate_candidate_samples(): candidate detection 학습 데이터 생성
9
+ - generate_synthetic_pairs()에 candidate detection 샘플 통합
10
+ - _filter_quality(): candidate 샘플 필터 조건 추가
11
+
12
+ [참고] Self-Instruct (Wang et al., ACL 2023)
13
+ [참고] Textbooks Are All You Need (Gunasekar et al., Microsoft, 2023)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any
19
+
20
+ from structverify.utils.llm_client import LLMClient
21
+ from structverify.utils.logger import get_logger
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ # ── 프롬프트 ──────────────────────────────────────────────────────────────
26
+
27
+ CLAIM_GENERATION_PROMPT = """당신은 한국 뉴스 기자입니다.
28
+ 아래 공식 통계표 정보를 보고, 이 통계표로 검증할 수 있는 뉴스 주장 {n}개를 생성하세요.
29
+
30
+ 통계표 ID: {stat_id}
31
+ 통계표명: {stat_name}
32
+ 발행기관: {org_name}
33
+ 분류: {category_path}
34
+ 관련 키워드: {keywords}
35
+
36
+ 규칙:
37
+ - 실제 뉴스에 나올법한 자연스러운 한국어 문장
38
+ - 반드시 구체적인 수치(%, 만명, 억원, ha 등) 포함
39
+ - 주장 유형 다양하게: 증가/감소/규모/비교 중 섞어서
40
+ - 검증 가능한 사실적 주장만 (의견/전망 제외)
41
+
42
+ JSON 배열로만 답하세요:
43
+ [
44
+ {{
45
+ "claim": "뉴스에 나올법한 주장 문장 (수치 포함)",
46
+ "indicator": "검증할 핵심 지표명",
47
+ "stat_id": "{stat_id}",
48
+ "claim_type": "increase|decrease|scale|comparison",
49
+ "expected_unit": "%, 만명, ha 등"
50
+ }}
51
+ ]"""
52
+
53
+ SCHEMA_GENERATION_PROMPT = """아래 뉴스 주장에서 검증에 필요한 핵심 정보를 추출하세요.
54
+
55
+ 주장: "{claim}"
56
+ 관련 통계표: {stat_name} ({stat_id})
57
+
58
+ JSON으로만 답하세요:
59
+ {{
60
+ "indicator": "측정 지표",
61
+ "time_period": "기준 시점",
62
+ "unit": "단위",
63
+ "population": "대상 범위",
64
+ "value": 수치 또는 null,
65
+ "stat_id": "{stat_id}"
66
+ }}"""
67
+
68
+ CANDIDATE_DETECTION_PROMPT = """아래 통계표 정보를 바탕으로 candidate detection 학습 데이터를 생성하세요.
69
+
70
+ 통계표: {stat_name} ({stat_id})
71
+ 키워드: {keywords}
72
+
73
+ 다음 두 종류의 문장을 각각 {n}개씩 생성하세요:
74
+
75
+ [positive] 이 통계표로 검증 가능한 수치 기반 주장:
76
+ - 구체적인 수치/비율/규모 포함
77
+ - 공식 통계와 대조 가능한 사실 주장
78
+
79
+ [negative] 검증 불가능한 문장 (다양한 유형으로):
80
+ - 의견/감상 (예: "정책이 아쉽다")
81
+ - 단순 이벤트 일정 (예: "박람회가 10월에 열린다")
82
+ - 미래 전망 (예: "줄어들 것으로 보인다")
83
+ - 추상적 주장 (예: "문제가 심각하다")
84
+
85
+ JSON으로만 답하세요:
86
+ {{
87
+ "positives": ["문장1", "문장2", ...],
88
+ "negatives": ["문장1", "문장2", ...]
89
+ }}"""
90
+
91
+
92
+ # ── 메인 함수 ─────────────────────────────────────────────────────────────
93
+
94
+ async def generate_synthetic_pairs(
95
+ catalog: list[dict[str, Any]],
96
+ llm: LLMClient,
97
+ claims_per_table: int = 3,
98
+ max_tables: int | None = None,
99
+ ) -> list[dict[str, Any]]:
100
+ """
101
+ KOSIS 메타데이터 → LLM Self-Instruct → 학습 쌍 자동 생성.
102
+
103
+ 생성 태스크:
104
+ 1) claim_to_stat : 주장 → 관련 통계표 매핑
105
+ 2) claim_to_schema : 주장 → 구조화 스키마 추출
106
+ 3) stat_to_claim : 통계표 → 주장 판별 (역방향)
107
+ 4) candidate_pos : 검증 후보 문장 (positive)
108
+ 5) candidate_neg : 검증 비후보 문장 (negative)
109
+ """
110
+ tables = catalog[:max_tables] if max_tables else catalog
111
+ logger.info(f"합성 데이터 생성: {len(tables)}개 통계표 × {claims_per_table}쌍")
112
+
113
+ all_pairs: list[dict[str, Any]] = []
114
+ success, fail = 0, 0
115
+
116
+ for idx, table in enumerate(tables):
117
+ try:
118
+ # claim/schema 쌍 생성
119
+ claims = await _generate_claims(llm, table, claims_per_table)
120
+ for claim_data in claims:
121
+ schema = await _generate_schema(llm, claim_data, table)
122
+ all_pairs.append({
123
+ "task": "claim_to_stat",
124
+ "claim": claim_data.get("claim", ""),
125
+ "stat_id": table["stat_id"],
126
+ "stat_name": table["stat_name"],
127
+ "indicator": claim_data.get("indicator", ""),
128
+ "claim_type": claim_data.get("claim_type", ""),
129
+ "schema": schema,
130
+ "source_table": table,
131
+ })
132
+
133
+ # candidate detection 쌍 생성
134
+ candidate_samples = await _generate_candidate_samples(
135
+ llm, table, n=claims_per_table
136
+ )
137
+ all_pairs.extend(candidate_samples)
138
+
139
+ success += 1
140
+ if (idx + 1) % 50 == 0:
141
+ logger.info(f"진행: {idx + 1}/{len(tables)} ({len(all_pairs)}쌍)")
142
+
143
+ except Exception as e:
144
+ fail += 1
145
+ logger.warning(f"통계표 {table.get('stat_id')} 실패: {e}")
146
+
147
+ filtered = _filter_quality(all_pairs)
148
+ logger.info(
149
+ f"합성 데이터 완료: 성공 {success}개, 실패 {fail}개 | "
150
+ f"생성 {len(all_pairs)}쌍 → 필터 후 {len(filtered)}쌍"
151
+ )
152
+ return filtered
153
+
154
+
155
+ # ── 내부 생성 함수 ────────────────────────────────────────────────────────
156
+
157
+ async def _generate_claims(
158
+ llm: LLMClient, table: dict, n: int
159
+ ) -> list[dict[str, Any]]:
160
+ """통계표 → 뉴스 주장 N개 생성"""
161
+ prompt = CLAIM_GENERATION_PROMPT.format(
162
+ n=n,
163
+ stat_id=table.get("stat_id", ""),
164
+ stat_name=table.get("stat_name", ""),
165
+ org_name=table.get("org_name", ""),
166
+ category_path=table.get("category_path", ""),
167
+ keywords=", ".join(table.get("keywords", [])),
168
+ )
169
+ result = await llm.generate_json(
170
+ prompt=prompt,
171
+ system_prompt="한국 뉴스 기자. JSON 배열로만 답하세요.",
172
+ )
173
+ if isinstance(result, list):
174
+ return result
175
+ if isinstance(result, dict) and "raw" not in result:
176
+ return [result]
177
+ return []
178
+
179
+
180
+ async def _generate_schema(
181
+ llm: LLMClient, claim_data: dict, table: dict
182
+ ) -> dict[str, Any]:
183
+ """주장 → 검증 스키마 추출"""
184
+ prompt = SCHEMA_GENERATION_PROMPT.format(
185
+ claim=claim_data.get("claim", ""),
186
+ stat_name=table.get("stat_name", ""),
187
+ stat_id=table.get("stat_id", ""),
188
+ )
189
+ try:
190
+ return await llm.generate_json(
191
+ prompt=prompt,
192
+ system_prompt="통계 분석 전문가. JSON으로만 답하세요.",
193
+ )
194
+ except Exception:
195
+ return {}
196
+
197
+
198
+ async def _generate_candidate_samples(
199
+ llm: LLMClient, table: dict, n: int = 3
200
+ ) -> list[dict[str, Any]]:
201
+ """
202
+ 통계표 기반 candidate detection 학습 데이터 생성.
203
+ positive/negative 쌍으로 candidate_scorer 학습에 사용.
204
+ """
205
+ prompt = CANDIDATE_DETECTION_PROMPT.format(
206
+ stat_name=table.get("stat_name", ""),
207
+ stat_id=table.get("stat_id", ""),
208
+ keywords=", ".join(table.get("keywords", [])),
209
+ n=n,
210
+ )
211
+ try:
212
+ result = await llm.generate_json(
213
+ prompt=prompt,
214
+ system_prompt="학습 데이터 생성기. JSON으로만 답하세요.",
215
+ )
216
+ except Exception as e:
217
+ logger.warning(f"candidate 샘플 생성 실패 ({table.get('stat_id')}): {e}")
218
+ return []
219
+
220
+ samples = []
221
+ stat_id = table.get("stat_id", "")
222
+ stat_name = table.get("stat_name", "")
223
+
224
+ for sent in result.get("positives", []):
225
+ if isinstance(sent, str) and sent.strip():
226
+ samples.append({
227
+ "task": "candidate_detection",
228
+ "sentence": sent.strip(),
229
+ "candidate_label": True,
230
+ "stat_id": stat_id,
231
+ "stat_name": stat_name,
232
+ })
233
+
234
+ for sent in result.get("negatives", []):
235
+ if isinstance(sent, str) and sent.strip():
236
+ samples.append({
237
+ "task": "candidate_detection",
238
+ "sentence": sent.strip(),
239
+ "candidate_label": False,
240
+ "stat_id": stat_id,
241
+ "stat_name": stat_name,
242
+ })
243
+
244
+ return samples
245
+
246
+
247
+ # ── 품질 필터링 ───────────────────────────────────────────────────────────
248
+
249
+ import re
250
+ _NUMERIC_RE = re.compile(r"\d")
251
+
252
+
253
+ def _filter_quality(pairs: list[dict[str, Any]]) -> list[dict[str, Any]]:
254
+ """
255
+ 합성 데이터 품질 필터링.
256
+
257
+ claim_to_stat 필터:
258
+ - 주장 10자 미만 제거
259
+ - 수치 미포함 제거
260
+ - stat_id 누락 제거
261
+ - 중복 제거
262
+
263
+ candidate_detection 필터:
264
+ - 문장 5자 미만 제거
265
+ - positive는 수치 필수
266
+ """
267
+ seen: set[str] = set()
268
+ filtered: list[dict[str, Any]] = []
269
+
270
+ for pair in pairs:
271
+ task = pair.get("task", "")
272
+
273
+ if task == "candidate_detection":
274
+ sentence = pair.get("sentence", "").strip()
275
+ if len(sentence) < 5:
276
+ continue
277
+ if pair.get("candidate_label") is True and not _NUMERIC_RE.search(sentence):
278
+ continue
279
+ key = f"cand:{sentence}"
280
+ if key in seen:
281
+ continue
282
+ seen.add(key)
283
+ filtered.append(pair)
284
+
285
+ else:
286
+ claim = pair.get("claim", "").strip()
287
+ if len(claim) < 10:
288
+ continue
289
+ if not _NUMERIC_RE.search(claim):
290
+ continue
291
+ if not pair.get("stat_id"):
292
+ continue
293
+ key = f"claim:{claim}"
294
+ if key in seen:
295
+ continue
296
+ seen.add(key)
297
+ filtered.append(pair)
298
+
299
+ return filtered
300
+
301
+
302
+ async def save_synthetic_data(
303
+ pairs: list[dict[str, Any]],
304
+ output_path: str = "ml/data/synthetic_pretrain.jsonl",
305
+ ) -> None:
306
+ """합성 데이터 JSONL 저장"""
307
+ import os
308
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
309
+
310
+ with open(output_path, "w", encoding="utf-8") as f:
311
+ for pair in pairs:
312
+ f.write(json.dumps(pair, ensure_ascii=False) + "\n")
313
+
314
+ task_counts: dict[str, int] = {}
315
+ for p in pairs:
316
+ t = p.get("task", "unknown")
317
+ task_counts[t] = task_counts.get(t, 0) + 1
318
+
319
+ logger.info(f"합성 데이터 저장: {output_path} ({len(pairs)}건)")
320
+ logger.info(f"태스크별 분포: {task_counts}")
@@ -0,0 +1,178 @@
1
+ """
2
+ # 수정자: 박재윤
3
+ # 수정 날짜: 2026-04-30
4
+ # 수정 내용: KOSIS 딥 메타데이터 수집 및 벡터 DB 재구축
5
+
6
+ # [DONE] KOSIS 메타 API로 항목명/분류명/단위 수집 후 임베딩 텍스트 보강
7
+ # [DONE] 지자체/지역 통계 제외 (197,956건만 처리)
8
+ # [DONE] 세마포어 기반 병렬 처리 (API=5, HCX=3)
9
+ # [DONE] prdSe Y→M→Q 순회 + err:20 objL 에스컬레이션
10
+ # [DONE] 429/타임아웃 재시도 로직 (최대 5회)
11
+ # [DONE] 배치 단위 DB UPDATE (ON CONFLICT 없이 순수 UPDATE)
12
+ # [TODO] 실패 건(None 반환) 별도 재시도 스크립트 작성
13
+ # [TODO] err:21 테이블 대상 별도 파라미터 방식 시도
14
+ # [TODO] 완료 후 factcheck_test.py 재실행 및 정확도 검증
15
+ """
16
+
17
+ import os
18
+ import json
19
+ import asyncio
20
+ import httpx
21
+ import psycopg2
22
+ from psycopg2.extras import execute_values
23
+ from dotenv import load_dotenv
24
+
25
+ from structverify.core.config_loader import load_config
26
+ from structverify.utils.embedding_client import EmbeddingClient
27
+
28
+ # 환경변수 로드
29
+ load_dotenv()
30
+
31
+ # [#67-D] HCX_API_KEY 모듈 전역 제거 → EmbeddingClient(config.embedding)로 일원화.
32
+ KOSIS_API_KEY = os.getenv("KOSIS_API_KEY")
33
+ PG_CONN = {
34
+ "host": os.getenv("POSTGRES_HOST"),
35
+ "port": os.getenv("POSTGRES_PORT"),
36
+ "dbname": os.getenv("POSTGRES_DB"),
37
+ "user": os.getenv("POSTGRES_USER"),
38
+ "password": os.getenv("POSTGRES_PASSWORD")
39
+ }
40
+
41
+ CATALOG_JSON_FILE = "kosis_catalog_cache.json"
42
+
43
+ async def fetch_kosis_deep_meta(client, org_id, tbl_id):
44
+ """KOSIS API 상세 정보 조회 (Y->M->Q 순회 및 에러 핸들링 강화)"""
45
+
46
+ # 연간(Y), 월간(M), 분기(Q) 순으로 시도
47
+ for prdSe in ["Y", "M", "Q"]:
48
+ base_params = {
49
+ "method": "getList", "apiKey": KOSIS_API_KEY, "format": "json", "jsonVD": "Y",
50
+ "orgId": org_id, "tblId": tbl_id, "itmId": "ALL", "objL1": "ALL",
51
+ "prdSe": prdSe, "newEstPrdCnt": "1"
52
+ }
53
+
54
+ try:
55
+ resp = await client.get("https://kosis.kr/openapi/Param/statisticsParameterData.do", params=base_params, timeout=15)
56
+ data = resp.json()
57
+
58
+ # 에러 20 (세부항목 누락) 발생 시 파라미터 에스컬레이션
59
+ if isinstance(data, dict) and data.get("err") == "20":
60
+ for level in range(2, 9):
61
+ base_params[f"objL{level}"] = "ALL"
62
+ resp = await client.get("https://kosis.kr/openapi/Param/statisticsParameterData.do", params=base_params, timeout=15)
63
+ data = resp.json()
64
+ if not (isinstance(data, dict) and data.get("err") == "20"):
65
+ break
66
+
67
+ # 에러 30 (데이터 없음) -> 다음 시점(M, Q)으로 재시도
68
+ if isinstance(data, dict) and data.get("err") == "30":
69
+ continue
70
+
71
+ # 에러 31 (너무 큼) 등 기타 에러 -> 과감히 포기 (기본 이름으로 임베딩)
72
+ if isinstance(data, dict) and "err" in data:
73
+ # 에러 로그가 너무 많이 뜨면 불편하니 31번은 조용히 넘깁니다.
74
+ if data.get("err") != "31":
75
+ print(f"⚠️ KOSIS API 에러 [{tbl_id}]: {data.get('err')} - {data.get('errMsg')}")
76
+ return None
77
+
78
+ # 정상 데이터 파싱
79
+ items, categories, units = set(), set(), set()
80
+ for row in data:
81
+ if row.get("ITM_NM"): items.add(row["ITM_NM"])
82
+ if row.get("UNIT_NM"): units.add(row["UNIT_NM"])
83
+ for key in row.keys():
84
+ if "OBJ_NM" in key and row[key]:
85
+ categories.add(row[key])
86
+
87
+ return {
88
+ "items": ", ".join(items)[:200],
89
+ "categories": ", ".join(categories)[:300],
90
+ "units": ", ".join(units)[:50]
91
+ }
92
+
93
+ except Exception as e:
94
+ # 타임아웃 등 통신 오류 시 조용히 넘김
95
+ return None
96
+
97
+ # Y, M, Q 다 돌았는데도 없으면 None 반환
98
+ return None
99
+
100
+ async def process_single_table(client, item, semaphore_api, semaphore_hcx, embedder=None):
101
+ # [#67-D] 인라인 HCX 임베딩 → 공용 EmbeddingClient.embed 로 교체.
102
+ # embed()는 실패 시 None → 기존 'skip on failure'(None은 update_records에서 제외) 보존.
103
+ # embedder 미주입 시 기본 EmbeddingClient (hcx + CLOVASTUDIO_API_KEY) → 기존 키 동작.
104
+ # 주의: 단건 embed 경로엔 429 재시도가 없음(_embed_batch_hcx에만 이식). 재실행형
105
+ # 스크립트라 이번 run에서 누락(skip)된 표는 다음 run(embedding IS NULL)이 보강.
106
+ if embedder is None:
107
+ embedder = EmbeddingClient({})
108
+
109
+ async with semaphore_api:
110
+ meta = await fetch_kosis_deep_meta(client, item["org_id"], item["stat_id"])
111
+
112
+ embed_text = f"{item['category_path']} > {item['stat_name']}"
113
+ if meta:
114
+ embed_text += f" | 항목: {meta['items']} | 분류: {meta['categories']} | 단위: {meta['units']}"
115
+
116
+ async with semaphore_hcx:
117
+ vec = await embedder.embed(embed_text)
118
+ if vec is None:
119
+ return None
120
+ return (vec, item["stat_id"])
121
+
122
+ async def main():
123
+ print("🚀 KOSIS 딥 메타데이터 수집 및 벡터 DB 재구축 시작 (안정화 버전)")
124
+
125
+ # [#67-D] config.embedding 으로 EmbeddingClient 구성 (없으면 {} → hcx + CLOVASTUDIO_API_KEY)
126
+ embedder = EmbeddingClient(load_config().get("embedding", {}))
127
+
128
+ with open(CATALOG_JSON_FILE, 'r', encoding='utf-8') as f:
129
+ catalog = json.load(f)
130
+
131
+ # 타겟 필터링 유지
132
+ target_catalog = [
133
+ item for item in catalog
134
+ if "지자체" not in item.get("category_path", "") and "지역" not in item.get("category_path", "")
135
+ ]
136
+
137
+ # ⭐ 핵심 수정: 동시 접속 수 대폭 축소 (서버 부하 방지)
138
+ semaphore_api = asyncio.Semaphore(5) # 기존 15 -> 5 로 축소 (KOSIS 서버 과부하 방지)
139
+ semaphore_hcx = asyncio.Semaphore(3) # 기존 5 -> 3 로 축소 (안정적인 임베딩 요청)
140
+
141
+ conn = psycopg2.connect(**PG_CONN)
142
+ cur = conn.cursor()
143
+
144
+ BATCH_SIZE = 100
145
+ async with httpx.AsyncClient(timeout=30) as client:
146
+ # 이전에 실패했던 지점(예: 600번)부터 다시 시작할 수 있도록 인덱스를 직접 설정할 수 있습니다.
147
+ # 처음부터 다시 하려면 0으로 두세요.
148
+ start_index = 0
149
+
150
+ for i in range(start_index, len(target_catalog), BATCH_SIZE):
151
+ batch = target_catalog[i:i+BATCH_SIZE]
152
+
153
+ tasks = [process_single_table(client, item, semaphore_api, semaphore_hcx, embedder) for item in batch]
154
+ results = await asyncio.gather(*tasks)
155
+
156
+ update_records = [res for res in results if res is not None]
157
+
158
+ if update_records:
159
+ try:
160
+ execute_values(
161
+ cur,
162
+ "UPDATE kosis_stat_catalog SET embedding = data.vector::vector FROM (VALUES %s) AS data(vector, stat_id) WHERE kosis_stat_catalog.stat_id = data.stat_id",
163
+ update_records
164
+ )
165
+ conn.commit()
166
+ except Exception as e:
167
+ print(f"❌ DB 업데이트 실패 (배치 {i}~{i+BATCH_SIZE}): {e}")
168
+ conn.rollback() # DB 업데이트 실패 시 롤백
169
+
170
+ print(f"🔄 처리 중: {i + len(batch)} / {len(target_catalog)} 건 (성공: {len(update_records)}건)")
171
+ await asyncio.sleep(1) # ← 추가
172
+
173
+ cur.close()
174
+ conn.close()
175
+ print("🎉 완료되었습니다!")
176
+
177
+ if __name__ == "__main__":
178
+ asyncio.run(main())
@@ -0,0 +1,21 @@
1
+ """
2
+ structverify.agent — Agentic 검증 시스템.
3
+
4
+ 기존 (v6.14까지):
5
+ - builder_agent: 카탈로그 구축 agent
6
+ - runtime_agent: 검증 파이프라인 오케스트레이션 (Step 3~9)
7
+
8
+ Phase A 추가:
9
+ - workspace: Agent 작업 공간 (파일 시스템 추상화)
10
+ - memory: 멀티턴 메모리 (markdown append)
11
+ - schemas: Plan / PlanStep / Observation / AgentVerdict 데이터 모델
12
+
13
+ Phase B 예정: tools/ (catalog_search, fetch_evidence, calculate, finish)
14
+ Phase C 예정: planner.py (Plan Agent)
15
+ Phase D 예정: reflector.py + loop.py (멀티턴 실행)
16
+ Phase E 예정: verifier 확장 — 여러 data point 받아 계산
17
+ Phase F 예정: runtime_agent.py 통합 (Step 7-8을 agent_loop로 교체)
18
+ """
19
+
20
+ # 기존 export는 그대로 (수정 X)
21
+ # 새 모듈은 import하지 않음 (사용자가 명시적으로 from structverify.agent.workspace import ...)