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,293 @@
1
+ """
2
+ structverify.agent.tools.deep_explore — P28: row-aware deep exploration helper.
3
+
4
+ 배경:
5
+ KOSIS pgvector catalog은 *표 이름*만 임베딩한다. 표 본문(C2_NM 등)에만 있는
6
+ *세부 항목* keyword (예: "체외 충격파 쇄석술", "치료 가능 사망률")는 cosine 검색
7
+ 에서 잘 잡히지 않아 정답 표가 top N 밖으로 밀려난다.
8
+
9
+ P21B의 row_preview rerank는 *비슷한 시도*였지만 (a) 한 표당 1 row만 보여줘
10
+ 패턴 파악 불가, (b) prompt가 "best 골라"라 LLM이 자기 prior로 오선택,
11
+ (c) 5 candidates × 3 prdSe = 15 동시 KOSIS 호출로 API 폭주 → P25에서 disable.
12
+
13
+ 설계 (P28):
14
+ - top N (기본 3) 후보에 대해 *Y prdSe만, 5 row*씩 sample fetch.
15
+ - LLM에 row 패턴 + claim을 던지고 "이 row가 보이면 더 파볼 가치가 있는 표"를
16
+ *외삽적으로* 추천하게 한다. 단순 "best 고르기"가 아니라 "row 단서를 근거로
17
+ 한 reasoning".
18
+ - P20 KOSIS cache hit이면 비용 0. cold 시작 시 최악 3건 호출 (Y만).
19
+ - LLM이 "none" 답하면 호출자에게 신호 → reflect에 query refinement 권유.
20
+
21
+ 호출자:
22
+ - catalog_search.py (T1 — top1 점수 낮을 때 사전 보강)
23
+ - loop.py / reflect (T2 — fetch 실패 후 회복용, force_explore=True 전달)
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import asyncio
28
+ import json
29
+ import re
30
+ from dataclasses import dataclass
31
+ from typing import Any
32
+
33
+ from structverify.utils.logger import get_logger
34
+
35
+ logger = get_logger(__name__)
36
+
37
+
38
+ @dataclass
39
+ class ExplorationResult:
40
+ """deep_explore 반환 형식."""
41
+ best_table_id: str | None
42
+ """LLM이 추천한 best 표 ID. None이면 모두 부적합 OR LLM 호출 실패."""
43
+
44
+ reasoning: str
45
+ """LLM이 제시한 한 줄 이유."""
46
+
47
+ none_signal: bool
48
+ """LLM이 명시적으로 "none of these"라 답한 경우 True.
49
+ 호출자(loop)는 이 신호를 받아 reflect에 *catalog query 재정의* hint를 줘야 함."""
50
+
51
+ previewed_ids: list[str]
52
+ """실제로 preview fetch 성공한 stat_id 목록 (디버깅/로깅용)."""
53
+
54
+ used: bool
55
+ """deep_explore가 실제 LLM 호출까지 갔는지 (False면 후보 부족/error로 skip)."""
56
+
57
+
58
+ _PICK_COLS = ("ITM_NM", "C1_NM", "C2_NM", "C3_NM", "C4_NM", "PRD_DE", "DT", "UNIT_NM")
59
+
60
+
61
+ async def _preview_fetch(
62
+ candidate_id: str,
63
+ source: Any,
64
+ workspace: Any,
65
+ rows_per_table: int,
66
+ ) -> dict | None:
67
+ """한 표의 sample row 가져오기. P20 cache hit 시 즉시 반환."""
68
+ try:
69
+ ev = await source.fetch_evidence(
70
+ candidate_id=candidate_id,
71
+ params={
72
+ "newEstPrdCnt": "1", # 최신 1 시점만 (한 시점에 다수 row 포함)
73
+ "prdSe": "Y", # Y만 — M/Q fallback X (KOSIS 부하 ↓)
74
+ "_preview": True,
75
+ },
76
+ workspace=workspace,
77
+ )
78
+ if ev is None:
79
+ return None
80
+ rows = ev.get("rows") or []
81
+ if not rows:
82
+ return None
83
+ # 핵심 컬럼만 + rows_per_table 개로 trim
84
+ sample = []
85
+ for r in rows[:rows_per_table]:
86
+ picked = {k: r.get(k) for k in _PICK_COLS if r.get(k) not in (None, "")}
87
+ if picked:
88
+ sample.append(picked)
89
+ return {
90
+ "stat_name": ev.get("stat_name") or "",
91
+ "rows_count": len(rows),
92
+ "sample_rows": sample,
93
+ }
94
+ except Exception as e:
95
+ logger.debug(f"[deep_explore] preview {candidate_id} 실패: {e}")
96
+ return None
97
+
98
+
99
+ def _build_prompt(
100
+ query: str,
101
+ claim_info: dict,
102
+ candidates_with_preview: list[dict],
103
+ ) -> str:
104
+ """LLM prompt 생성. "best 고르기"가 아니라 "row 단서로 reasoning"."""
105
+ lines = []
106
+ for i, c in enumerate(candidates_with_preview, start=1):
107
+ cid = c.get("id", "")
108
+ cname = (c.get("name", "") or "").strip()
109
+ score = c.get("score")
110
+ head = f"{i}. [{cid}] {cname}"
111
+ if isinstance(score, (int, float)):
112
+ head += f" (catalog_score={score:.3f})"
113
+ lines.append(head)
114
+ prev = c.get("_preview")
115
+ if prev and prev.get("sample_rows"):
116
+ lines.append(f" - rows_count={prev.get('rows_count')}")
117
+ for j, row in enumerate(prev["sample_rows"], start=1):
118
+ row_str = ", ".join(f"{k}={v!r}" for k, v in row.items())
119
+ lines.append(f" - row{j}: {row_str}")
120
+ else:
121
+ lines.append(" - (sample row 없음 — preview 실패 또는 빈 표)")
122
+
123
+ return f"""당신은 통계표 *탐색 reviewer*입니다. 사용자가 찾는 *구체 항목*이
124
+ 어느 표의 row에 들어있을 가능성이 높은지, sample row 패턴을 보고 *외삽적으로* 판단하세요.
125
+
126
+ [사용자 검색 의도]
127
+ - query: {query!r}
128
+ - indicator (찾는 지표): {claim_info.get('indicator')!r}
129
+ - population (대상 집단/지역): {claim_info.get('population')!r}
130
+ - time_period: {claim_info.get('time_period')!r}
131
+ - unit: {claim_info.get('unit')!r}
132
+
133
+ [후보 표 + sample rows]
134
+ {chr(10).join(lines)}
135
+
136
+ [판단 기준]
137
+ 1. ITM_NM / C1_NM~C4_NM 어느 컬럼에 indicator의 *키워드*가 직접/유사 매칭되는 표 우선.
138
+ 2. 매칭이 안 보여도 row 분류 체계로 보아 "이 표를 더 깊이 파면 (다른 row에) 해당 항목이 있을 가능성"이 높으면 그 표를 추천. *외삽 OK*.
139
+ 예: indicator="체외 충격파 쇄석술 장비" + 어떤 표 sample row가 ITM_NM="CT", "MRI" 등 *의료장비 분류*면 → 이 표 더 깊은 row에 쇄석술 장비도 있을 가능성 ↑.
140
+ 3. row가 명백히 *다른 도메인*(예: indicator=장비인데 row=인구통계)이면 배제.
141
+ 4. 어느 표에도 단서가 없으면 best_stat_id를 "none"으로 응답. *억지로 고르지 말 것*.
142
+
143
+ [응답 형식 — JSON only, 다른 텍스트 금지]
144
+ {{
145
+ "best_stat_id": "DT_XXX" or "none",
146
+ "reasoning": "row 단서 기반 한 줄 이유 (외삽 reasoning이면 명시)",
147
+ "confidence": 0.0~1.0
148
+ }}
149
+ """
150
+
151
+
152
+ def _parse_response(raw: str, candidate_ids: list[str]) -> tuple[str | None, str, bool]:
153
+ """LLM 응답 파싱. (best_id, reasoning, none_signal)."""
154
+ try:
155
+ m = re.search(r"\{[^{}]*\}", raw, re.DOTALL)
156
+ if not m:
157
+ return None, "", False
158
+ data = json.loads(m.group(0))
159
+ except Exception as e:
160
+ logger.debug(f"[deep_explore] JSON 파싱 실패: {e}")
161
+ return None, "", False
162
+
163
+ raw_best = (data.get("best_stat_id") or "").strip()
164
+ reasoning = str(data.get("reasoning") or "").strip()
165
+
166
+ # "none" 신호 — case-insensitive
167
+ if raw_best.lower() in ("none", "null", "", "n/a"):
168
+ return None, reasoning, True
169
+
170
+ # brackets/quotes strip (P24와 동일한 normalize)
171
+ best = raw_best.strip().strip("[]").strip("'\"").strip()
172
+ if best in candidate_ids:
173
+ return best, reasoning, False
174
+
175
+ # substring fallback
176
+ for cid in candidate_ids:
177
+ if cid and (best in cid or cid in best):
178
+ logger.info(f"[deep_explore] best={best!r} → substring 매칭 {cid!r}")
179
+ return cid, reasoning, False
180
+
181
+ logger.info(f"[deep_explore] best={best!r}가 후보 list에 없음 — 무효 처리")
182
+ return None, reasoning, False
183
+
184
+
185
+ async def deep_explore(
186
+ *,
187
+ query: str,
188
+ candidates: list[dict[str, Any]],
189
+ claim: Any,
190
+ source: Any,
191
+ workspace: Any,
192
+ config: dict | None,
193
+ ) -> ExplorationResult:
194
+ """top N 후보에 sample row preview + LLM reasoning으로 best table 추천.
195
+
196
+ 호출자가 trigger 조건 (T1/T2) 검사 후 호출. 본 함수 자체는 무조건 실행.
197
+
198
+ Args:
199
+ query: 원본 catalog 검색 쿼리 (LLM에 전달).
200
+ candidates: catalog 검색 결과 후보 list (이미 점수순 정렬됨).
201
+ 각 dict는 최소 {"id": str, "name": str, "score": float}.
202
+ claim: 현재 처리 중인 Claim 객체 (claim.schema 추출용).
203
+ source: BaseDataSource. fetch_evidence(candidate_id, params={_preview:True}) 지원.
204
+ workspace: P20 KOSIS cache 활용.
205
+ config: 전체 config dict — catalog_search.deep_explore.* + agent.llm.* 사용.
206
+
207
+ Returns:
208
+ ExplorationResult.
209
+ """
210
+ _cfg = (config or {}).get("catalog_search") or {}
211
+ _dx = _cfg.get("deep_explore") or {}
212
+ top_n = int(_dx.get("top_n") or 3)
213
+ rows_per_table = int(_dx.get("rows_per_table") or 5)
214
+
215
+ if not candidates:
216
+ return ExplorationResult(None, "", False, [], used=False)
217
+
218
+ top_candidates = candidates[:top_n]
219
+ candidate_ids = [c.get("id", "") for c in top_candidates if c.get("id")]
220
+
221
+ if not candidate_ids:
222
+ return ExplorationResult(None, "", False, [], used=False)
223
+
224
+ # 1) sample row preview (병렬, KOSIS 부하 고려 — gather)
225
+ preview_tasks = [
226
+ _preview_fetch(cid, source, workspace, rows_per_table)
227
+ for cid in candidate_ids
228
+ ]
229
+ preview_results = await asyncio.gather(*preview_tasks, return_exceptions=False)
230
+
231
+ previewed_ids: list[str] = []
232
+ enriched: list[dict[str, Any]] = []
233
+ for c, prev in zip(top_candidates, preview_results):
234
+ d = dict(c)
235
+ if prev is not None:
236
+ d["_preview"] = prev
237
+ previewed_ids.append(c.get("id", ""))
238
+ enriched.append(d)
239
+
240
+ if not previewed_ids:
241
+ logger.info("[deep_explore] preview 0건 — LLM 호출 skip")
242
+ return ExplorationResult(None, "", False, [], used=False)
243
+
244
+ # 2) claim info 추출
245
+ _schema = getattr(claim, "schema", None) if claim is not None else None
246
+ claim_info = {
247
+ "indicator": (getattr(_schema, "indicator", None) or "") if _schema else "",
248
+ "population": (getattr(_schema, "population", None) or "") if _schema else "",
249
+ "time_period": (getattr(_schema, "time_period", None) or "") if _schema else "",
250
+ "unit": (getattr(_schema, "unit", None) or "") if _schema else "",
251
+ }
252
+
253
+ # 3) LLM 호출 — model_tier는 config.catalog_search.deep_explore.model_tier
254
+ prompt = _build_prompt(query, claim_info, enriched)
255
+ model_tier = str(_dx.get("model_tier") or "light").strip().lower()
256
+
257
+ from structverify.utils.llm_client import LLMClient
258
+ llm = LLMClient(config=(config or {}).get("llm") or {})
259
+ try:
260
+ raw = await llm.generate(
261
+ prompt=prompt,
262
+ system_prompt=(
263
+ "KOSIS 통계표 탐색 reviewer. row 패턴 기반 외삽 reasoning. JSON만 응답."
264
+ ),
265
+ model_tier=model_tier,
266
+ )
267
+ except Exception as e:
268
+ logger.warning(f"[deep_explore] LLM 호출 실패 (model_tier={model_tier}): {e}")
269
+ return ExplorationResult(None, "", False, previewed_ids, used=False)
270
+
271
+ # 4) 파싱 + 결과
272
+ best, reasoning, none_signal = _parse_response(raw, candidate_ids)
273
+ if none_signal:
274
+ logger.info(
275
+ f"[deep_explore] LLM none_signal — reasoning={reasoning[:120]!r}"
276
+ )
277
+ elif best:
278
+ logger.info(
279
+ f"[deep_explore] LLM 추천 best={best!r} reasoning={reasoning[:120]!r}"
280
+ )
281
+ else:
282
+ logger.info(
283
+ f"[deep_explore] LLM 응답 파싱 결과 best=None (none_signal=False) — "
284
+ f"무효 응답으로 처리"
285
+ )
286
+
287
+ return ExplorationResult(
288
+ best_table_id=best,
289
+ reasoning=reasoning,
290
+ none_signal=none_signal,
291
+ previewed_ids=previewed_ids,
292
+ used=True,
293
+ )