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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
structverify.retrieval.dimension_resolver — P34: KOSIS 표 차원(itmId/objL) 동적 결정.
|
|
3
|
+
|
|
4
|
+
배경:
|
|
5
|
+
KOSIS Open API는 표마다 *N차원 구조* (ITM × OBJL01 × OBJL02 × ... × PRD).
|
|
6
|
+
호출 시 itmId/objL1/objL2...를 *특정 코드*로 지정하면 그 *슬라이스*만 받음.
|
|
7
|
+
지정 안 하면 connector가 getMeta(CMMT)[0]의 ITM_ID/OBJ_ID(보통 *합계 코드*)를
|
|
8
|
+
자동 박아 *합계 row만* 받음 → 세부 항목 row가 *아예 안 옴*.
|
|
9
|
+
|
|
10
|
+
예 DT_HIRA4Q (3차원: 시군구 × 장비종류 × 분기):
|
|
11
|
+
- getMeta(CMMT)[0].ITM_ID = '00' (진단방사선·특수의료장비 합계)
|
|
12
|
+
- 호출에 itmId='00' 박힘 → 세부 ITM (체외 충격파 쇄석술기, CT, MRI...) 누락
|
|
13
|
+
- row 매칭에서 "체외 충격파 쇄석술 장비" indicator 찾을 row 0건
|
|
14
|
+
|
|
15
|
+
설계:
|
|
16
|
+
1) fetch 전에 getMeta(ITM) + getMeta(OBJL01~08) 호출 (병렬, ~1초)
|
|
17
|
+
2) 각 차원의 (코드, 이름) pair list를 LLM에 노출
|
|
18
|
+
3) LLM이 claim의 indicator/population과 *의미적 매칭*되는 코드 선택
|
|
19
|
+
4) {itmId, objL1, ...} dict 반환
|
|
20
|
+
5) fetch 호출 시 그 코드들을 params에 박음 → *정확한 슬라이스*만 받음
|
|
21
|
+
|
|
22
|
+
매칭 실패 시 None 반환 → 호출자가 기존 동작 (cmmt_rows[0]) 또는 fallback.
|
|
23
|
+
|
|
24
|
+
캐시:
|
|
25
|
+
per-job + per-stat_id cache (workspace에 저장 안 함, 메모리만). 같은 stat_id로
|
|
26
|
+
다시 호출돼도 LLM/API 1회만.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import asyncio
|
|
31
|
+
import json
|
|
32
|
+
import re
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from structverify.utils.logger import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# process-level cache: {stat_id: {itmId, objL1, ...}} (또는 None for "매칭 실패")
|
|
41
|
+
_DIM_CACHE: dict[str, dict[str, str] | None] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _extract_pairs(meta_rows: Any, code_keys: tuple[str, ...], name_keys: tuple[str, ...]) -> list[tuple[str, str]]:
|
|
45
|
+
"""KOSIS getMeta 응답에서 (code, name) pair list 추출.
|
|
46
|
+
|
|
47
|
+
응답 형식이 *type마다 다름*:
|
|
48
|
+
- ITM 응답: [{ITM_ID, ITM_NM, ...}, ...]
|
|
49
|
+
- OBJL01 응답: [{OBJ_ID, OBJ_NM, ...}, ...] 또는 [{C1, C1_NM, ...}]
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(meta_rows, list):
|
|
52
|
+
return []
|
|
53
|
+
out: list[tuple[str, str]] = []
|
|
54
|
+
seen: set[str] = set()
|
|
55
|
+
for r in meta_rows:
|
|
56
|
+
if not isinstance(r, dict):
|
|
57
|
+
continue
|
|
58
|
+
code = ""
|
|
59
|
+
for k in code_keys:
|
|
60
|
+
v = r.get(k)
|
|
61
|
+
if v:
|
|
62
|
+
code = str(v).strip()
|
|
63
|
+
break
|
|
64
|
+
name = ""
|
|
65
|
+
for k in name_keys:
|
|
66
|
+
v = r.get(k)
|
|
67
|
+
if v:
|
|
68
|
+
name = str(v).strip()
|
|
69
|
+
break
|
|
70
|
+
if code and code not in seen:
|
|
71
|
+
seen.add(code)
|
|
72
|
+
out.append((code, name or code))
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_prompt(
|
|
77
|
+
indicator: str,
|
|
78
|
+
population: str,
|
|
79
|
+
claim_text: str,
|
|
80
|
+
parent_path: str,
|
|
81
|
+
stat_name: str,
|
|
82
|
+
itm_pairs: list[tuple[str, str]],
|
|
83
|
+
obj_pairs_by_level: dict[int, list[tuple[str, str]]],
|
|
84
|
+
) -> str:
|
|
85
|
+
"""LLM prompt — 각 차원의 코드 list를 보여주고 매칭 코드 선택."""
|
|
86
|
+
lines: list[str] = []
|
|
87
|
+
if itm_pairs:
|
|
88
|
+
shown = itm_pairs[:80]
|
|
89
|
+
more = f" (외 {len(itm_pairs)-len(shown)}개)" if len(itm_pairs) > len(shown) else ""
|
|
90
|
+
pairs_str = ", ".join(f'{code!r}→{name!r}' for code, name in shown)
|
|
91
|
+
lines.append(f"### ITM (통계항목, 코드→이름):\n{pairs_str}{more}")
|
|
92
|
+
for level in sorted(obj_pairs_by_level.keys()):
|
|
93
|
+
pairs = obj_pairs_by_level[level]
|
|
94
|
+
if not pairs:
|
|
95
|
+
continue
|
|
96
|
+
shown = pairs[:80]
|
|
97
|
+
more = f" (외 {len(pairs)-len(shown)}개)" if len(pairs) > len(shown) else ""
|
|
98
|
+
pairs_str = ", ".join(f'{code!r}→{name!r}' for code, name in shown)
|
|
99
|
+
lines.append(f"### OBJL{level:02d} (분류 {level}차, 코드→이름):\n{pairs_str}{more}")
|
|
100
|
+
|
|
101
|
+
dim_block = "\n\n".join(lines) if lines else "(차원 정보 없음)"
|
|
102
|
+
|
|
103
|
+
return f"""당신은 KOSIS 통계표의 *N차원 슬라이스 코드*를 결정하는 reviewer입니다.
|
|
104
|
+
사용자 claim에 맞는 *itmId / objL1~objL8*을 각 차원의 코드 list에서 선택하세요.
|
|
105
|
+
|
|
106
|
+
[사용자 검색 의도]
|
|
107
|
+
- 표: {stat_name!r}
|
|
108
|
+
- indicator (찾는 지표): {indicator!r}
|
|
109
|
+
- population (대상 집단/지역): {population!r}
|
|
110
|
+
- claim 원문: {claim_text or '(없음)'}
|
|
111
|
+
- parent_path: {parent_path or '(없음)'}
|
|
112
|
+
|
|
113
|
+
[표의 차원 정보 — getMeta 응답]
|
|
114
|
+
{dim_block}
|
|
115
|
+
|
|
116
|
+
[판단 기준]
|
|
117
|
+
1. ITM 차원: indicator의 *핵심 키워드*와 의미적으로 매칭되는 ITM_ID 1개 선택.
|
|
118
|
+
- 예: indicator="체외 충격파 쇄석술 장비" → ITM_NM에 "체외 충격파 쇄석술기" 또는 "ESWL" 있으면 그 ITM_ID.
|
|
119
|
+
- *상위 합계 코드* (예: ITM_NM='진단방사선·특수의료장비' 같은 *전체 묶음*)는 피하기.
|
|
120
|
+
- 매칭 없으면 itmId를 "ALL"로 (모든 항목 받기).
|
|
121
|
+
2. OBJL01 차원: population의 지역/집단과 매칭되는 코드 1개 선택.
|
|
122
|
+
- 예: population="강원도" → OBJ_NM에 "강원" 또는 "강원도" 있으면 그 코드.
|
|
123
|
+
- 매칭 없으면 "ALL".
|
|
124
|
+
3. OBJL02 이상: 보통 ALL 권장 (세부 분류는 다 받기). 단 특정 분류 매칭이 명확하면 그 코드.
|
|
125
|
+
4. 매칭이 확실하지 *않으면* 해당 차원은 "ALL" — 추측 금지.
|
|
126
|
+
|
|
127
|
+
[응답 형식 — JSON only, 다른 텍스트 금지]
|
|
128
|
+
{{
|
|
129
|
+
"itmId": "<코드> 또는 ALL",
|
|
130
|
+
"objL1": "<코드> 또는 ALL",
|
|
131
|
+
"objL2": "<코드> 또는 ALL",
|
|
132
|
+
"reasoning": "한 줄 이유 — 어느 차원의 어떤 값에 매칭했는지"
|
|
133
|
+
}}
|
|
134
|
+
|
|
135
|
+
* 표에 없는 차원은 응답에서 빼도 됨. 모두 ALL인 경우도 valid.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse(raw: str) -> dict[str, str]:
|
|
140
|
+
"""LLM 응답 파싱. {itmId, objL1, ...} dict 반환. 빈 dict면 실패."""
|
|
141
|
+
try:
|
|
142
|
+
m = re.search(r"\{.*\}", raw, re.DOTALL)
|
|
143
|
+
if not m:
|
|
144
|
+
return {}
|
|
145
|
+
data = json.loads(m.group(0))
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.debug(f"[dimension_resolver] 파싱 실패: {e}")
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
out: dict[str, str] = {}
|
|
151
|
+
for key in ("itmId", "objL1", "objL2", "objL3", "objL4", "objL5", "objL6", "objL7", "objL8"):
|
|
152
|
+
v = data.get(key)
|
|
153
|
+
if v is None:
|
|
154
|
+
continue
|
|
155
|
+
s = str(v).strip().strip("'\"").strip()
|
|
156
|
+
if s:
|
|
157
|
+
out[key] = s
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def resolve_dimensions(
|
|
162
|
+
*,
|
|
163
|
+
stat_id: str,
|
|
164
|
+
stat_name: str,
|
|
165
|
+
source: Any,
|
|
166
|
+
indicator: str,
|
|
167
|
+
population: str,
|
|
168
|
+
claim_text: str,
|
|
169
|
+
parent_path: str,
|
|
170
|
+
config: dict | None,
|
|
171
|
+
use_cache: bool = True,
|
|
172
|
+
) -> dict[str, str] | None:
|
|
173
|
+
"""KOSIS 표의 차원 메타를 받아 LLM이 claim 매칭 코드 결정.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
stat_id: KOSIS 표 ID.
|
|
177
|
+
stat_name: 표 이름 (LLM 프롬프트용).
|
|
178
|
+
source: BaseDataSource (get_table_meta 지원해야).
|
|
179
|
+
indicator/population/claim_text/parent_path: claim 컨텍스트.
|
|
180
|
+
config: 전체 config dict.
|
|
181
|
+
use_cache: process-level cache 사용 여부.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
{itmId, objL1, ...} dict (값은 KOSIS 코드 또는 "ALL"). 매칭 실패/메타 없음 시 None.
|
|
185
|
+
"""
|
|
186
|
+
if not stat_id:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# 1) cache hit?
|
|
190
|
+
if use_cache and stat_id in _DIM_CACHE:
|
|
191
|
+
cached = _DIM_CACHE[stat_id]
|
|
192
|
+
if cached is None:
|
|
193
|
+
logger.info(f"[dimension_resolver] cache hit (negative): {stat_id}")
|
|
194
|
+
return None
|
|
195
|
+
logger.info(f"[dimension_resolver] cache hit: {stat_id} → {cached}")
|
|
196
|
+
return cached
|
|
197
|
+
|
|
198
|
+
# 2) 메타 병렬 호출 — ITM + OBJL01~OBJL04 (5개까지만 시도, 표 대부분 ≤3차원)
|
|
199
|
+
meta_tasks = {
|
|
200
|
+
"ITM": source.get_table_meta(stat_id, "ITM"),
|
|
201
|
+
"OBJL01": source.get_table_meta(stat_id, "OBJL01"),
|
|
202
|
+
"OBJL02": source.get_table_meta(stat_id, "OBJL02"),
|
|
203
|
+
"OBJL03": source.get_table_meta(stat_id, "OBJL03"),
|
|
204
|
+
"OBJL04": source.get_table_meta(stat_id, "OBJL04"),
|
|
205
|
+
}
|
|
206
|
+
results = await asyncio.gather(*meta_tasks.values(), return_exceptions=False)
|
|
207
|
+
metas = dict(zip(meta_tasks.keys(), results))
|
|
208
|
+
|
|
209
|
+
# 3) (code, name) pair 추출
|
|
210
|
+
itm_pairs = _extract_pairs(
|
|
211
|
+
metas.get("ITM"),
|
|
212
|
+
code_keys=("ITM_ID",),
|
|
213
|
+
name_keys=("ITM_NM",),
|
|
214
|
+
)
|
|
215
|
+
obj_pairs_by_level: dict[int, list[tuple[str, str]]] = {}
|
|
216
|
+
for level in range(1, 5):
|
|
217
|
+
key = f"OBJL{level:02d}"
|
|
218
|
+
meta = metas.get(key)
|
|
219
|
+
# OBJ 응답은 OBJ_ID/OBJ_NM 또는 C{level}/C{level}_NM 둘 다 가능 — 우선순위 시도
|
|
220
|
+
pairs = _extract_pairs(
|
|
221
|
+
meta,
|
|
222
|
+
code_keys=("OBJ_ID", f"C{level}"),
|
|
223
|
+
name_keys=("OBJ_NM", f"C{level}_NM"),
|
|
224
|
+
)
|
|
225
|
+
if pairs:
|
|
226
|
+
obj_pairs_by_level[level] = pairs
|
|
227
|
+
|
|
228
|
+
if not itm_pairs and not obj_pairs_by_level:
|
|
229
|
+
logger.info(f"[dimension_resolver] 메타 비어있음: {stat_id} → 차원 결정 skip")
|
|
230
|
+
if use_cache:
|
|
231
|
+
_DIM_CACHE[stat_id] = None
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# 4) LLM 호출
|
|
235
|
+
_rg_cfg = ((config or {}).get("kosis") or {}).get("dimension_resolver") or {}
|
|
236
|
+
model_tier = str(_rg_cfg.get("model_tier") or "light").strip().lower()
|
|
237
|
+
|
|
238
|
+
prompt = _build_prompt(
|
|
239
|
+
indicator=indicator,
|
|
240
|
+
population=population,
|
|
241
|
+
claim_text=claim_text[:400] if claim_text else "",
|
|
242
|
+
parent_path=parent_path,
|
|
243
|
+
stat_name=stat_name,
|
|
244
|
+
itm_pairs=itm_pairs,
|
|
245
|
+
obj_pairs_by_level=obj_pairs_by_level,
|
|
246
|
+
)
|
|
247
|
+
from structverify.utils.llm_client import LLMClient
|
|
248
|
+
llm = LLMClient(config=(config or {}).get("llm") or {})
|
|
249
|
+
try:
|
|
250
|
+
raw = await llm.generate(
|
|
251
|
+
prompt=prompt,
|
|
252
|
+
system_prompt="KOSIS N차원 슬라이스 코드 reviewer. JSON만 응답.",
|
|
253
|
+
model_tier=model_tier,
|
|
254
|
+
)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning(f"[dimension_resolver] LLM 호출 실패: {e}")
|
|
257
|
+
if use_cache:
|
|
258
|
+
_DIM_CACHE[stat_id] = None
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
dims = _parse(raw)
|
|
262
|
+
if not dims:
|
|
263
|
+
logger.info(
|
|
264
|
+
f"[dimension_resolver] 파싱 실패 raw={raw[:200]!r} stat_id={stat_id}"
|
|
265
|
+
)
|
|
266
|
+
if use_cache:
|
|
267
|
+
_DIM_CACHE[stat_id] = None
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
logger.info(
|
|
271
|
+
f"[dimension_resolver] {stat_id} → {dims} "
|
|
272
|
+
f"(ITM 후보 {len(itm_pairs)}개, OBJ {sum(len(v) for v in obj_pairs_by_level.values())}개)"
|
|
273
|
+
)
|
|
274
|
+
if use_cache:
|
|
275
|
+
_DIM_CACHE[stat_id] = dims
|
|
276
|
+
return dims
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def clear_cache() -> None:
|
|
280
|
+
"""테스트/디버깅용 cache 초기화."""
|
|
281
|
+
_DIM_CACHE.clear()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
retrieval/evidence_subgraph.py — Evidence 서브그래프 조립 (Step 7)
|
|
3
|
+
|
|
4
|
+
커넥터 결과를 Evidence 서브그래프로 변환하여 Claim Graph에 연결한다.
|
|
5
|
+
|
|
6
|
+
[참고] GraphRAG (arXiv 2501.00309)
|
|
7
|
+
Evidence를 그래프 구조로 조립하여 다중 hop 추론을 지원하는 패턴
|
|
8
|
+
"""
|
|
9
|
+
# 수정자: 신준수
|
|
10
|
+
# 수정 날짜: 2026-04-27
|
|
11
|
+
# 수정 내용: Evidence 필드는 StatData 정규화 필드만 사용( values 키 직접 접근 제거)
|
|
12
|
+
# [2026-05-14 | 이수민] memory/v1: StatData.category_path → Evidence.category_path 전달
|
|
13
|
+
# - 도메인 가드용 카테고리 정보를 Evidence까지 보존해 verifier가 활용
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from structverify.core.schemas import (
|
|
16
|
+
Evidence, GraphNode, GraphEdge, GraphNodeType, GraphEdgeType, ProvenanceRecord)
|
|
17
|
+
from structverify.retrieval.base_connector import StatData, ConnectorQuery
|
|
18
|
+
from structverify.retrieval.kosis_connector import KOSISConnector
|
|
19
|
+
from structverify.utils.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def build_evidence_subgraph(
|
|
25
|
+
connector: KOSISConnector,
|
|
26
|
+
query: ConnectorQuery,
|
|
27
|
+
claim_node_id: str,
|
|
28
|
+
) -> tuple[Evidence | None, list[GraphNode], list[GraphEdge]]:
|
|
29
|
+
"""
|
|
30
|
+
커넥터로 데이터 조회 → Evidence 객체 + 서브그래프 생성
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
(Evidence, [GraphNode], [GraphEdge]) 또는 (None, [], [])
|
|
34
|
+
"""
|
|
35
|
+
data = await connector.search_and_fetch(query)
|
|
36
|
+
if data is None:
|
|
37
|
+
logger.warning("Evidence 데이터 없음")
|
|
38
|
+
return None, [], []
|
|
39
|
+
|
|
40
|
+
# Evidence 객체 생성
|
|
41
|
+
graph_nodes = connector.to_graph_nodes(data)
|
|
42
|
+
provenance = connector.tag_provenance(data, query)
|
|
43
|
+
|
|
44
|
+
evidence = Evidence(
|
|
45
|
+
source_name=data.stat_name, stat_table_id=data.stat_id,
|
|
46
|
+
official_value=data.official_value,
|
|
47
|
+
unit=data.unit,
|
|
48
|
+
time_period=data.time_period,
|
|
49
|
+
raw_response=data.raw_response,
|
|
50
|
+
graph_nodes=graph_nodes, provenance=provenance,
|
|
51
|
+
# [이수민 2026-05-14] working memory 도메인 가드용
|
|
52
|
+
category_path=getattr(data, "category_path", None),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Evidence → Claim 연결 엣지
|
|
56
|
+
edges = []
|
|
57
|
+
for gn in graph_nodes:
|
|
58
|
+
edges.append(GraphEdge(
|
|
59
|
+
from_node=claim_node_id, to_node=gn.node_id,
|
|
60
|
+
edge_type=GraphEdgeType.VERIFIED_BY))
|
|
61
|
+
|
|
62
|
+
logger.info(f"Evidence 서브그래프: {len(graph_nodes)} nodes")
|
|
63
|
+
return evidence, graph_nodes, edges
|