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,153 @@
1
+ """structverify.agent.indexing_agent — 색인 전략 ReAct 에이전트 (P1 재설계).
2
+
3
+ 단순 API 1회 호출이 아니라, *진짜 에이전트*:
4
+ · 파일 메모리(LocalWorkspaceBackend)에 관찰·판단을 기록(기억·추적·재개)
5
+ · 도구로 데이터를 필요할 때마다 직접 읽음 (peek/columns/sample/grep/stats)
6
+ · ReAct 루프: 생각 → 도구 호출 → 관찰 → 반복 → 최종 IndexingPlan
7
+
8
+ 기존 인프라 재사용: agent/workspace.py(메모리), utils/llm_client(추론).
9
+ 계획: docs/indexing-agent-plan.md
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import csv
14
+ import io
15
+ import json
16
+ import os
17
+ from typing import Any
18
+
19
+ from structverify.agent.indexing_planner import (
20
+ INDEXING_PLAN_SCHEMA,
21
+ _analyze_source,
22
+ _apply_heuristic_guards,
23
+ )
24
+ from structverify.agent.workspace import LocalWorkspaceBackend
25
+ from structverify.utils.llm_client import LLMClient
26
+ from structverify.utils.logger import get_logger
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ _STEP_SCHEMA: dict[str, Any] = {
31
+ "type": "object",
32
+ "properties": {
33
+ "thought": {"type": "string"},
34
+ "action": {"type": "string",
35
+ "enum": ["peek", "columns", "sample", "grep", "stats", "finish"]},
36
+ "args": {"type": "object"}, # peek:{n}, sample:{n}, grep:{pattern}
37
+ },
38
+ "required": ["thought", "action"],
39
+ }
40
+
41
+ _TOOLS_DESC = (
42
+ "peek{n}: 원문 앞 n자 · columns: CSV 헤더 · sample{n}: 표 n행/문서 조각 · "
43
+ "grep{pattern}: 패턴 매칭 줄(조항·한도 탐색) · stats: 통계(행수·수치밀도·조항수) · "
44
+ "finish: 정보 충분하면 종료(다음 단계에서 IndexingPlan 확정)"
45
+ )
46
+
47
+
48
+ class IndexingAgent:
49
+ """데이터를 도구로 탐색하며 색인 전략을 세우는 ReAct 에이전트."""
50
+
51
+ def __init__(self, config: dict[str, Any] | None = None,
52
+ workspace_root: str = "./agent_workspace/indexing"):
53
+ self.config = config or {}
54
+ self.llm = LLMClient(config=self.config.get("llm", {}))
55
+ self.ws = LocalWorkspaceBackend(workspace_root)
56
+ self.max_iters = int(self.config.get("indexing_max_iters", 6))
57
+
58
+ def _load(self, source: str) -> tuple[str, bool]:
59
+ if isinstance(source, str) and os.path.exists(source):
60
+ from structverify.retrieval.chunking import read_document
61
+ text = read_document(source) # .pdf 추출 포함
62
+ is_csv = source.lower().endswith(".csv")
63
+ else:
64
+ text = source or ""
65
+ is_csv = "," in (text.splitlines()[0] if text.splitlines() else "")
66
+ return text, is_csv
67
+
68
+ # ── 도구 (데이터를 필요할 때 읽음) ──
69
+ def _run_tool(self, action: str, args: dict, source: str, text: str, is_csv: bool) -> str:
70
+ try:
71
+ if action == "peek":
72
+ return text[: int(args.get("n", 600))]
73
+ if action == "columns":
74
+ if not is_csv:
75
+ return "(비정형 문서 — 컬럼 없음)"
76
+ return ", ".join(next(csv.reader(io.StringIO(text)), []))
77
+ if action == "sample":
78
+ n = int(args.get("n", 5))
79
+ if is_csv:
80
+ rows = list(csv.reader(io.StringIO(text)))[1:n + 1]
81
+ return "\n".join(", ".join(r) for r in rows)
82
+ return text[:200 * n][:1200]
83
+ if action == "grep":
84
+ pat = str(args.get("pattern", ""))
85
+ import re
86
+ hits = [ln for ln in text.splitlines() if pat and re.search(pat, ln)]
87
+ return "\n".join(hits[:15]) or "(매칭 없음)"
88
+ if action == "stats":
89
+ return json.dumps(_analyze_source(source), ensure_ascii=False, default=str)[:800]
90
+ except Exception as e: # noqa: BLE001
91
+ return f"(도구 오류: {e})"
92
+ return "(알 수 없는 도구)"
93
+
94
+ async def run(self, source: str, run_id: str = "run") -> dict[str, Any]:
95
+ """데이터를 탐색하며 IndexingPlan을 세운다. 관찰·판단은 workspace에 기록."""
96
+ text, is_csv = self._load(source)
97
+ trace_key = f"{run_id}/trace.md"
98
+ self.ws.write_text(trace_key, f"# 색인 전략 에이전트 trace\nsource={source}\n\n")
99
+ observations: list[str] = []
100
+
101
+ for i in range(1, self.max_iters + 1):
102
+ step = await self._decide(observations)
103
+ thought, action, args = step.get("thought", ""), step.get("action", "stats"), step.get("args") or {}
104
+ self.ws.append_text(trace_key, f"## iter {i}\n- 생각: {thought}\n- 행동: {action} {args}\n")
105
+ if action == "finish":
106
+ self.ws.append_text(trace_key, "- → 종료, plan 확정\n")
107
+ break
108
+ obs = self._run_tool(action, args, source, text, is_csv)
109
+ observations.append(f"[{action} {args}] → {obs[:400]}")
110
+ self.ws.append_text(trace_key, f"- 관찰: {obs[:300]}\n\n")
111
+
112
+ plan = await self._finalize(observations)
113
+ plan = _apply_heuristic_guards(plan, _analyze_source(source), self.config)
114
+ self.ws.write_text(f"{run_id}/plan.json", json.dumps(plan, ensure_ascii=False, indent=2))
115
+ logger.info(f"[indexing_agent] plan: datasource={plan.get('datasource')} "
116
+ f"verdict={plan.get('verdict_mode')} (iters={i}, trace={trace_key})")
117
+ return plan
118
+
119
+ async def _decide(self, observations: list[str]) -> dict[str, Any]:
120
+ hist = "\n".join(observations) or "(아직 관찰 없음)"
121
+ prompt = (
122
+ "너는 데이터 색인 전략가 에이전트다. 데이터를 도구로 조사해 색인 전략을 세운다.\n"
123
+ f"[도구]\n{_TOOLS_DESC}\n\n[지금까지 관찰]\n{hist}\n\n"
124
+ "다음 한 스텝을 정하라. 표형/문서형·수치성·조항구조·규모를 파악할 정보가 더 필요하면 "
125
+ "도구를 호출하고, 충분하면 finish. thought/action/args JSON만 출력."
126
+ )
127
+ try:
128
+ return await self.llm.generate_structured(
129
+ prompt, _STEP_SCHEMA, system_prompt="색인 전략 에이전트. JSON만.",
130
+ )
131
+ except Exception as e: # noqa: BLE001
132
+ logger.warning(f"[indexing_agent] decide 실패: {e}")
133
+ return {"thought": "폴백", "action": "finish", "args": {}}
134
+
135
+ async def _finalize(self, observations: list[str]) -> dict[str, Any]:
136
+ hist = "\n".join(observations) or "(관찰 없음)"
137
+ prompt = (
138
+ "아래 데이터 조사 관찰을 근거로 IndexingPlan(JSON)을 확정하라.\n"
139
+ "- datasource: 표형=custom_csv, 자연어 문서=custom_docs, 혼합=hybrid\n"
140
+ "- search_mode: 정형=keyword, 동의어중요=embedding, 둘다=hybrid\n"
141
+ "- verdict_mode: 값대값=numeric, 텍스트규정 준수=conformance\n"
142
+ "- chunking(문서형): 조항多=article/문단=paragraph, size/overlap\n"
143
+ "- embedding.backend: 대규모=pgvector 아니면 memory · provider: config 우선\n"
144
+ "- column_mapping(표형) · rationale(왜 이렇게 정했는지)\n\n"
145
+ f"[관찰]\n{hist}\n\nIndexingPlan JSON만 출력."
146
+ )
147
+ try:
148
+ return await self.llm.generate_structured(
149
+ prompt, INDEXING_PLAN_SCHEMA, system_prompt="색인 전략가. IndexingPlan JSON만.",
150
+ )
151
+ except Exception as e: # noqa: BLE001
152
+ logger.warning(f"[indexing_agent] finalize 실패 → 휴리스틱: {e}")
153
+ return {}
@@ -0,0 +1,169 @@
1
+ """structverify.agent.indexing_planner — 색인 전략 에이전트 (P1).
2
+
3
+ 데이터셋 샘플 + 유저 config 를 분석해 **IndexingPlan**(색인·검색·판정 전략) 초안을 만든다.
4
+ 사람이 keyword/semantic/chunk/provider/verdict를 손으로 고르지 않고, 에이전트가 결정.
5
+
6
+ 흐름: 데이터 로드/통계(휴리스틱) → LLM 구조화 판단 → IndexingPlan + rationale.
7
+ 계획: docs/indexing-agent-plan.md
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ import io
13
+ import os
14
+ import re
15
+ from typing import Any
16
+
17
+ from structverify.utils.llm_client import LLMClient
18
+ from structverify.utils.logger import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ # 에이전트가 채우는 색인 전략 스키마 (generate_structured 강제)
23
+ INDEXING_PLAN_SCHEMA: dict[str, Any] = {
24
+ "type": "object",
25
+ "properties": {
26
+ "datasource": {"type": "string", "enum": ["custom_csv", "custom_docs", "hybrid"]},
27
+ "search_mode": {"type": "string", "enum": ["keyword", "embedding", "hybrid"]},
28
+ "verdict_mode": {"type": "string", "enum": ["numeric", "conformance"]},
29
+ "chunking": {
30
+ "type": "object",
31
+ "properties": {
32
+ "strategy": {"type": "string", "enum": ["article", "paragraph", "fixed", "none"]},
33
+ "size": {"type": "integer"},
34
+ "overlap": {"type": "integer"},
35
+ },
36
+ },
37
+ "embedding": {
38
+ "type": "object",
39
+ "properties": {
40
+ "provider": {"type": "string"},
41
+ "backend": {"type": "string", "enum": ["memory", "pgvector"]},
42
+ },
43
+ },
44
+ "column_mapping": {"type": "object"},
45
+ "rationale": {"type": "string"},
46
+ },
47
+ "required": ["datasource", "search_mode", "verdict_mode", "rationale"],
48
+ }
49
+
50
+ _PGVECTOR_ROW_THRESHOLD = 20000 # 이상이면 pgvector 권장
51
+ _ARTICLE_RE = re.compile(r"제?\s*\d+\s*조")
52
+ _LIMIT_KW = ("한도", "이하", "이상", "초과", "미만", "기준", "상한", "최대", "최소")
53
+
54
+
55
+ def _analyze_source(source: str) -> dict[str, Any]:
56
+ """데이터 샘플 + 휴리스틱 통계. source = 파일 경로 또는 원시 텍스트."""
57
+ from structverify.retrieval.chunking import read_document
58
+ is_path = isinstance(source, str) and os.path.exists(source)
59
+ ext = os.path.splitext(source)[1].lower() if is_path else ""
60
+ text = read_document(source) if is_path else (source or "") # .pdf 추출 포함
61
+
62
+ stats: dict[str, Any] = {"ext": ext, "char_count": len(text)}
63
+
64
+ # CSV 판별 (확장자 또는 헤더에 콤마 다수)
65
+ first_line = text.splitlines()[0] if text.splitlines() else ""
66
+ looks_csv = ext == ".csv" or (first_line.count(",") >= 1 and ext in ("", ".csv"))
67
+ stats["is_tabular"] = looks_csv
68
+
69
+ if looks_csv:
70
+ rows = list(csv.reader(io.StringIO(text)))
71
+ header = rows[0] if rows else []
72
+ body = rows[1:]
73
+ stats["columns"] = header
74
+ stats["row_count"] = len(body)
75
+ stats["sample_rows"] = body[:5]
76
+ # 수치 컬럼 비율
77
+ num_cells = tot = 0
78
+ for r in body[:50]:
79
+ for c in r:
80
+ tot += 1
81
+ try:
82
+ float(str(c).replace(",", ""))
83
+ num_cells += 1
84
+ except ValueError:
85
+ pass
86
+ stats["numeric_density"] = round(num_cells / tot, 2) if tot else 0.0
87
+ stats["has_operator_col"] = any("operator" in (h or "").lower() for h in header)
88
+ else:
89
+ stats["article_markers"] = len(_ARTICLE_RE.findall(text))
90
+ stats["sample_text"] = text[:1200]
91
+
92
+ stats["limit_keyword_hits"] = sum(text.count(k) for k in _LIMIT_KW)
93
+ return stats
94
+
95
+
96
+ def _build_prompt(stats: dict[str, Any], config: dict[str, Any]) -> str:
97
+ emb_provider = ((config or {}).get("embedding") or {}).get("provider") \
98
+ or ((config or {}).get("llm") or {}).get("provider") or "(미지정)"
99
+ domain = ((config or {}).get("domain") or {}).get("description") or "(미지정)"
100
+ return (
101
+ "너는 데이터 색인 전략가다. 아래 데이터셋 통계와 유저 config를 보고, 이 데이터를 어떻게 "
102
+ "색인·검색·판정할지 IndexingPlan(JSON)으로 정하라.\n\n"
103
+ "[결정 기준]\n"
104
+ "- datasource: 정형 표(컬럼·값)면 custom_csv, 자연어 문서(조항·문단)면 custom_docs, 섞이면 hybrid\n"
105
+ "- search_mode: 지표 표기가 코드/정형이면 keyword, 동의어·의역 매칭이 중요하면 embedding, 둘 다면 hybrid\n"
106
+ "- verdict_mode: 값 대 값 수치비교가 되면 numeric, 텍스트 규정에 대한 준수여부면 conformance\n"
107
+ "- chunking(문서형만): 조항 마커 많으면 article, 문단 위주면 paragraph, 아니면 fixed\n"
108
+ "- embedding.backend: 행/청크가 매우 많으면(수만+) pgvector, 아니면 memory\n"
109
+ "- embedding.provider: 유저 config provider 우선\n"
110
+ "- column_mapping(표형): 컬럼→ indicator/value/unit/time/region/operator 매핑\n"
111
+ "- rationale: 왜 이렇게 정했는지 한국어로 2~3문장\n\n"
112
+ f"[데이터 통계]\n{stats}\n\n"
113
+ f"[유저 config] embedding/llm provider={emb_provider}, domain={domain}\n\n"
114
+ "IndexingPlan JSON만 출력."
115
+ )
116
+
117
+
118
+ async def plan_indexing(source: str, config: dict[str, Any] | None = None) -> dict[str, Any]:
119
+ """데이터셋(source: 경로 또는 텍스트) + config → IndexingPlan(dict).
120
+
121
+ 항상 초안을 만든다(유저가 이후 검토·확정). LLM 실패 시 휴리스틱 폴백.
122
+ """
123
+ config = config or {}
124
+ stats = _analyze_source(source)
125
+ llm = LLMClient(config=config.get("llm", {}))
126
+
127
+ try:
128
+ plan = await llm.generate_structured(
129
+ _build_prompt(stats, config),
130
+ INDEXING_PLAN_SCHEMA,
131
+ system_prompt="데이터 색인 전략가. 반드시 IndexingPlan JSON만 출력.",
132
+ )
133
+ except Exception as e: # noqa: BLE001
134
+ logger.warning(f"[indexing_planner] LLM 실패 → 휴리스틱 폴백: {e}")
135
+ plan = {}
136
+
137
+ plan = _apply_heuristic_guards(plan, stats, config)
138
+ logger.info(
139
+ f"[indexing_planner] datasource={plan.get('datasource')} "
140
+ f"search={plan.get('search_mode')} verdict={plan.get('verdict_mode')}"
141
+ )
142
+ return plan
143
+
144
+
145
+ def _apply_heuristic_guards(plan: dict[str, Any], stats: dict[str, Any],
146
+ config: dict[str, Any]) -> dict[str, Any]:
147
+ """LLM 결과에 명백한 휴리스틱 보정 (빈 값 채우기·규모/provider 강제)."""
148
+ plan = dict(plan or {})
149
+ # 형태
150
+ if not plan.get("datasource"):
151
+ plan["datasource"] = "custom_csv" if stats.get("is_tabular") else "custom_docs"
152
+ # verdict: operator 컬럼 or 수치밀도 높으면 numeric
153
+ if not plan.get("verdict_mode"):
154
+ plan["verdict_mode"] = "numeric" if stats.get("is_tabular") else "conformance"
155
+ # search_mode 기본
156
+ if not plan.get("search_mode"):
157
+ plan["search_mode"] = "keyword" if stats.get("is_tabular") else "embedding"
158
+ # 규모 → backend
159
+ n = stats.get("row_count") or stats.get("article_markers") or 0
160
+ emb = dict(plan.get("embedding") or {})
161
+ if not emb.get("backend"):
162
+ emb["backend"] = "pgvector" if n >= _PGVECTOR_ROW_THRESHOLD else "memory"
163
+ # provider: config 우선
164
+ cfg_provider = ((config.get("embedding") or {}).get("provider")
165
+ or (config.get("llm") or {}).get("provider"))
166
+ if cfg_provider:
167
+ emb["provider"] = cfg_provider
168
+ plan["embedding"] = emb
169
+ return plan
@@ -0,0 +1,182 @@
1
+ """structverify.agent.integration_example — runtime_agent에 agent loop를 끼우는 *예시 코드*.
2
+
3
+ ★ 이 파일은 *작동하는 코드가 아니라 가이드*. 사용자가 자신의 runtime_agent.py에 *수동 통합*.
4
+
5
+ ## 통합 단계
6
+
7
+ ### 1. config 확장 (`config/default.yaml`)
8
+
9
+ ```yaml
10
+ agent:
11
+ enabled: false # ← true로 켜야 agent loop 사용
12
+ llm:
13
+ plan_model: "HCX-007"
14
+ plan_temperature: 0.1
15
+ loop:
16
+ max_iterations: 10
17
+ mode: "deterministic" # phase D는 이거만. Phase E에서 "reflect" 추가.
18
+
19
+ data_sources:
20
+ enabled: ["kosis"]
21
+ default_source: "kosis"
22
+ kosis:
23
+ # 기존 KOSIS 설정 (api_key 등) 여기로 통합
24
+ api_key: null # env var에서 자동 로드 가정
25
+ ```
26
+
27
+ ### 2. runtime_agent.py에 *config 분기* 추가
28
+
29
+ 기존 Step 7-8 (retrieve_evidence + verify_claim)을 *agent loop로 대체*:
30
+
31
+ ```python
32
+ # runtime_agent.py (사용자 코드)
33
+
34
+ class RuntimeAgent:
35
+ async def process(self, sir_doc):
36
+ # ... Steps 1-6 그대로 (도메인 분류, 클레임 탐지, 스키마 유도, 그래프 빌드)
37
+
38
+ for claim in claims:
39
+ if self.config.agent.enabled:
40
+ # ★ NEW: Agent loop 경로
41
+ result = await self._verify_with_agent(claim, source_text, anchor_year)
42
+ else:
43
+ # 기존 경로 (Step 7-8)
44
+ evidence = await self._retrieve_evidence(claim)
45
+ result = await self._verify_claim(claim, evidence)
46
+ result.explanation = await generate_explanation(claim, result, self.config)
47
+
48
+ results.append(result)
49
+ # ...
50
+
51
+ async def _verify_with_agent(self, claim, source_text, anchor_year):
52
+ '''Phase D: agent loop으로 검증.'''
53
+ from structverify.agent.planner import Planner, PlannerConfig
54
+ from structverify.agent.loop import agent_loop, LoopConfig
55
+ from structverify.agent.workspace import build_workspace
56
+ from structverify.retrieval.registry import build_all_enabled
57
+
58
+ # 1. workspace 준비
59
+ workspace = build_workspace(
60
+ job_id=str(self.job_id),
61
+ config=self.config.agent.workspace.model_dump() if hasattr(self.config.agent.workspace, 'model_dump') else dict(self.config.agent.workspace),
62
+ )
63
+ if not workspace.is_initialized():
64
+ workspace.initialize(source_text=source_text)
65
+ workspace.create_claim_dir(claim.claim_id, claim_dict=claim.model_dump(mode="json"))
66
+
67
+ # 2. DataSource 등록 (KOSIS만 우선)
68
+ from structverify.retrieval import kosis_source # noqa — register_datasource 트리거
69
+ datasources = {
70
+ ds.name: ds for ds in build_all_enabled({
71
+ "enabled": ["kosis"],
72
+ "kosis": dict(self.config.data_sources.kosis) if hasattr(self.config.data_sources, 'kosis') else {},
73
+ })
74
+ }
75
+
76
+ # 3. Planner — LLM call wiring
77
+ async def llm_call_for_plan(prompt):
78
+ # ★ 사용자 wiring: HCX client 호출
79
+ # 예시 (실제 HCX 호출 방식에 맞게 수정):
80
+ from structverify.llm.hcx_client import call_hcx # ← 실제 모듈/함수 이름 확인
81
+ return await call_hcx(
82
+ prompt=prompt,
83
+ model=self.config.agent.llm.plan_model,
84
+ temperature=0.1,
85
+ )
86
+
87
+ planner = Planner(
88
+ llm_call=llm_call_for_plan,
89
+ config=PlannerConfig(
90
+ model=self.config.agent.llm.plan_model,
91
+ temperature=0.1,
92
+ ),
93
+ )
94
+
95
+ # 4. Plan 생성
96
+ plan = await planner.plan(claim, source_text=source_text, anchor_year=anchor_year)
97
+ workspace.write_plan(claim.claim_id, plan.model_dump(mode="json"))
98
+
99
+ # 5. Loop 실행
100
+ verdict = await agent_loop(
101
+ plan=plan,
102
+ claim=claim,
103
+ workspace=workspace,
104
+ datasources=datasources,
105
+ config=self.config.model_dump() if hasattr(self.config, 'model_dump') else dict(self.config),
106
+ reflect_fn=None, # Phase D = deterministic. Phase E에서 추가.
107
+ loop_config=LoopConfig(
108
+ max_iterations=self.config.agent.loop.max_iterations,
109
+ mode="deterministic",
110
+ ),
111
+ )
112
+
113
+ # 6. AgentVerdict → 기존 VerificationResult 변환
114
+ # (사용자 코드의 result schema에 맞게 변환)
115
+ result = self._agent_verdict_to_result(claim, verdict)
116
+ return result
117
+
118
+ def _agent_verdict_to_result(self, claim, agent_verdict):
119
+ '''AgentVerdict → 기존 VerificationResult 변환.
120
+
121
+ agent_verdict.verdict ('match' | 'mismatch' | ...) → result.verdict
122
+ agent_verdict.explanation → result.explanation
123
+ agent_verdict.confidence → result.confidence
124
+
125
+ evidence는 마지막 fetch_evidence observation에서 복원 가능 (workspace.read 등).
126
+ '''
127
+ # ★ 사용자 코드의 VerificationResult schema에 맞게 작성
128
+ from structverify.core.schemas import VerificationResult, VerdictType
129
+ return VerificationResult(
130
+ claim_id=claim.claim_id,
131
+ verdict=VerdictType(agent_verdict.verdict.value),
132
+ confidence=agent_verdict.confidence,
133
+ explanation=agent_verdict.explanation,
134
+ evidence=None, # 또는 마지막 observation에서 복원
135
+ )
136
+ ```
137
+
138
+ ### 3. 첫 테스트 (안전 모드)
139
+
140
+ `config.yaml`에서:
141
+ ```yaml
142
+ agent:
143
+ enabled: true # 켜기
144
+ ```
145
+
146
+ 같은 출생아 기사 재실행 후 로그 확인:
147
+ - `[planner] {claim_id}: Plan 생성 완료. type=... data_points=...`
148
+ - `[loop] {claim_id}: 시작. plan.type=growth_rate, steps=4, mode=deterministic`
149
+ - `[loop] {claim_id} iter 1: action=catalog_search`
150
+ - `[loop] {claim_id} iter 2: action=fetch_evidence`
151
+ - ... 등
152
+
153
+ 만약 *Plan은 잘 생성되는데 Loop이 fail*하면:
154
+ - KOSISDataSource의 TODO 4곳 (`structverify/retrieval/kosis_source.py`) 사용자 코드와 매핑 확인
155
+ - 특히 catalog_search/kosis_connector의 *실제 함수 이름 + 시그니처*
156
+
157
+ ## 디버깅 팁
158
+
159
+ 1. **Plan 단계 실패** → planner 직접 호출해서 LLM 응답 확인:
160
+ ```python
161
+ plan = await planner.plan(claim, ...)
162
+ print(plan.model_dump_json(indent=2))
163
+ ```
164
+
165
+ 2. **DataSource 호출 실패** → KOSIS adapter 직접 테스트:
166
+ ```python
167
+ from structverify.retrieval.kosis_source import KOSISDataSource
168
+ ds = KOSISDataSource()
169
+ cands = await ds.search_catalog(query="출생아 수")
170
+ print(cands)
171
+ ```
172
+
173
+ 3. **agent.enabled=false로 즉시 롤백** — 기존 경로로 돌아감.
174
+
175
+ ## 점진적 도입 추천
176
+
177
+ 1. **Step 1**: agent.enabled=false 유지. Phase A-C zip만 적용 — *기존 결과 그대로*.
178
+ 2. **Step 2**: *별도 테스트 스크립트*로 planner.plan() 호출 → Plan JSON 확인.
179
+ 3. **Step 3**: kosis_source.py의 TODO 4곳 채움 + KOSIS DataSource 단독 테스트.
180
+ 4. **Step 4**: agent.enabled=true로 한 claim만 처리 → 로그 분석.
181
+ 5. **Step 5**: 8 claim 전체 실행. 기존 결과와 비교.
182
+ """