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