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