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