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,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
structverify.agent.schemas — Agent 시스템의 데이터 모델.
|
|
3
|
+
|
|
4
|
+
핵심 개념:
|
|
5
|
+
- DataPointSpec : claim 검증에 필요한 *하나의 데이터 점* (지표 + 시점 + 인구)
|
|
6
|
+
- Plan : 검증 계획 — 어떤 데이터 점들이 필요한지 + 어떻게 찾을지
|
|
7
|
+
- PlanStep : 다음 행동 한 단위 (action + input + rationale)
|
|
8
|
+
- Observation : action 실행 결과
|
|
9
|
+
- ReflectDecision : Reflect Agent가 결정한 다음 행동
|
|
10
|
+
- AgentVerdict : Agent loop 최종 결과
|
|
11
|
+
|
|
12
|
+
Phase A에서는 *모델 정의*만. 실제 사용은:
|
|
13
|
+
- Plan → Phase C (Plan Agent)
|
|
14
|
+
- Observation → Phase D (Loop)
|
|
15
|
+
- ReflectDecision → Phase D (Reflect Agent)
|
|
16
|
+
- AgentVerdict → Phase E (Verifier 확장)
|
|
17
|
+
|
|
18
|
+
사용 패턴:
|
|
19
|
+
from structverify.agent.schemas import Plan, DataPointSpec, ClaimType
|
|
20
|
+
|
|
21
|
+
plan = Plan(
|
|
22
|
+
claim_id="abc-123",
|
|
23
|
+
claim_type=ClaimType.COMPARISON,
|
|
24
|
+
required_data=[
|
|
25
|
+
DataPointSpec(indicator="평균소비성향", time="2014"),
|
|
26
|
+
DataPointSpec(indicator="평균소비성향", time="2024"),
|
|
27
|
+
],
|
|
28
|
+
initial_steps=[...],
|
|
29
|
+
)
|
|
30
|
+
workspace.write_plan(claim_id, plan.model_dump())
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from enum import Enum
|
|
36
|
+
from typing import Any
|
|
37
|
+
from pydantic import BaseModel, Field
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Enums ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
class ClaimType(str, Enum):
|
|
43
|
+
"""Claim의 검증 유형 — Plan Agent가 분류한다."""
|
|
44
|
+
|
|
45
|
+
ABSOLUTE = "absolute"
|
|
46
|
+
"""단일 시점 절대값. 예: '출생아 수 20,171명' (1개 데이터 점)."""
|
|
47
|
+
|
|
48
|
+
DIFFERENCE = "difference"
|
|
49
|
+
"""두 시점 사이 차이값. 예: '0.04명 증가' (2개 데이터 점 + 차이 계산)."""
|
|
50
|
+
|
|
51
|
+
GROWTH_RATE = "growth_rate"
|
|
52
|
+
"""두 시점 사이 증가율(%). 예: '6.7% 증가' (2개 데이터 점 + 비율 계산)."""
|
|
53
|
+
|
|
54
|
+
COMPARISON = "comparison"
|
|
55
|
+
"""두 시점 직접 비교 (X→Y 형식). 예: '73.6% → 70.3%' (2개 데이터 점)."""
|
|
56
|
+
|
|
57
|
+
RANKING = "ranking"
|
|
58
|
+
"""순위/상대 비교. 예: '1위', '하락폭이 가장 컸다' (여러 데이터 점)."""
|
|
59
|
+
|
|
60
|
+
AGGREGATION = "aggregation"
|
|
61
|
+
"""[2026-05-21] 다년/다기간 집계 — 평균/총합/최대/최소 등.
|
|
62
|
+
예: '최근 3년 평균 …', '2022~2024년 총합 …' (N개 데이터 점 + 집계 계산).
|
|
63
|
+
도메인 무관, schema_inductor가 LLM으로 신호 감지. 시퀀스:
|
|
64
|
+
catalog_search → fetch_evidence × N → calculate(agg) → finish."""
|
|
65
|
+
|
|
66
|
+
UNKNOWN = "unknown"
|
|
67
|
+
"""분류 불가."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ActionType(str, Enum):
|
|
71
|
+
"""Agent가 실행 가능한 action."""
|
|
72
|
+
|
|
73
|
+
CATALOG_SEARCH = "catalog_search"
|
|
74
|
+
"""지정된 데이터 소스의 카탈로그(표 목록)에서 검색."""
|
|
75
|
+
|
|
76
|
+
EXPLORE_CATALOG = "explore_catalog"
|
|
77
|
+
"""[패치 Q] 카탈로그의 카테고리 분포 + 대표 표를 탐색.
|
|
78
|
+
|
|
79
|
+
LLM이 어떤 카테고리 어휘를 써야 할지 모를 때(예: '기상관측통계' vs '기후 변화')
|
|
80
|
+
먼저 이 도구를 호출해 카탈로그가 실제로 어떤 분류·어휘를 쓰는지 파악한 뒤,
|
|
81
|
+
그 정보를 바탕으로 정확한 catalog_search query/category를 만든다.
|
|
82
|
+
룰베이스 도메인 매핑 없이도 DataSource 어휘를 자가학습."""
|
|
83
|
+
|
|
84
|
+
FETCH_EVIDENCE = "fetch_evidence"
|
|
85
|
+
"""카탈로그에서 찾은 후보의 실제 데이터 조회."""
|
|
86
|
+
|
|
87
|
+
READ_ORIGINAL = "read_original"
|
|
88
|
+
"""원문 기사 읽기 (claim 외 정보 필요할 때)."""
|
|
89
|
+
|
|
90
|
+
CALCULATE = "calculate"
|
|
91
|
+
"""확보한 데이터로 수식 계산 (증가율, 차이 등)."""
|
|
92
|
+
|
|
93
|
+
REPLAN = "replan"
|
|
94
|
+
"""[2026-05-26] plan 자체가 틀린 경우 — claim 값이 표에 직접 row로 없고
|
|
95
|
+
*계산해야 하는* 경우 (예: '증가 수 52'는 row가 아니라 current-prev delta).
|
|
96
|
+
기존 fallback(try_ids/catalog retry/row_matcher 등)이 모두 같은 plan 안에서
|
|
97
|
+
답 찾는 거라면, replan은 *plan을 통째로 새로 만든다*.
|
|
98
|
+
|
|
99
|
+
호출 조건:
|
|
100
|
+
- 모든 catalog 후보 fetch 실패 + catalog retry도 소진
|
|
101
|
+
- 표 sample을 보고 LLM이 'claim 값이 row로 없는 *계산 대상*'이라 판단할 때
|
|
102
|
+
- per-claim 최대 2회 (무한 replan 방지)
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
FINISH = "finish"
|
|
106
|
+
"""판정 확정 후 종료."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class VerdictType(str, Enum):
|
|
110
|
+
"""Agent loop 결과 verdict."""
|
|
111
|
+
MATCH = "match"
|
|
112
|
+
MISMATCH = "mismatch"
|
|
113
|
+
PARTIAL = "partial"
|
|
114
|
+
UNVERIFIABLE = "unverifiable"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class StopReason(str, Enum):
|
|
118
|
+
"""Agent loop 종료 이유."""
|
|
119
|
+
COMPLETED = "completed"
|
|
120
|
+
"""모든 데이터 확보 + 계산 + finish 결정."""
|
|
121
|
+
|
|
122
|
+
MAX_ITERATIONS = "max_iterations"
|
|
123
|
+
"""max_iter 초과 — 미완료 상태로 종료."""
|
|
124
|
+
|
|
125
|
+
GIVE_UP = "give_up"
|
|
126
|
+
"""agent가 명시적으로 포기 (충분히 시도했으나 데이터 없음)."""
|
|
127
|
+
|
|
128
|
+
TOKEN_BUDGET = "token_budget"
|
|
129
|
+
"""LLM 토큰 예산 초과."""
|
|
130
|
+
|
|
131
|
+
ERROR = "error"
|
|
132
|
+
"""예외 발생."""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Data point ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
class DataPointSpec(BaseModel):
|
|
138
|
+
"""
|
|
139
|
+
검증에 필요한 *하나의 데이터 점* 명세.
|
|
140
|
+
|
|
141
|
+
Plan Agent가 claim에서 추출하고, agent loop이 *resolved_value*를 채움.
|
|
142
|
+
"""
|
|
143
|
+
indicator: str
|
|
144
|
+
"""지표명. '출생아 수', '평균소비성향' 등."""
|
|
145
|
+
|
|
146
|
+
time: str
|
|
147
|
+
"""시점. 'YYYY' 또는 'YYYY-MM' 형식 권장."""
|
|
148
|
+
|
|
149
|
+
population: str | None = None
|
|
150
|
+
"""대상 인구. '전체', '30대 이하' 등."""
|
|
151
|
+
|
|
152
|
+
unit_hint: str | None = None
|
|
153
|
+
"""단위 힌트. '%', '명', '원' (LLM이 추론 — 절대 강제 X)."""
|
|
154
|
+
|
|
155
|
+
# ── 채워지는 필드 (agent loop 진행 중) ────────────────────────
|
|
156
|
+
resolved_value: float | None = None
|
|
157
|
+
"""확보된 수치값. None이면 *아직 못 찾음*."""
|
|
158
|
+
|
|
159
|
+
resolved_unit: str | None = None
|
|
160
|
+
"""실제 fetch된 데이터의 단위."""
|
|
161
|
+
|
|
162
|
+
source: str | None = None
|
|
163
|
+
"""데이터 출처. 'KOSIS:DT_1L9U108', 'CSV:my_sales.csv' 등."""
|
|
164
|
+
|
|
165
|
+
source_time: str | None = None
|
|
166
|
+
"""실제 fetch된 시점 (요청 시점과 다를 수 있음 — 가까운 row 매칭)."""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── Plan ──────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
class PlanStep(BaseModel):
|
|
172
|
+
"""Plan에 포함된 *예상 행동 단계*. Reflect Agent가 변경/건너뛸 수 있다."""
|
|
173
|
+
|
|
174
|
+
action: ActionType
|
|
175
|
+
input: dict[str, Any]
|
|
176
|
+
rationale: str
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class FallbackStrategy(BaseModel):
|
|
180
|
+
"""1차 탐색 실패 시 대안."""
|
|
181
|
+
|
|
182
|
+
use_original_text: bool = False
|
|
183
|
+
"""원문에서 직접 수치 추출 시도."""
|
|
184
|
+
|
|
185
|
+
alternative_keywords: list[str] = Field(default_factory=list)
|
|
186
|
+
"""다른 검색어 후보."""
|
|
187
|
+
|
|
188
|
+
give_up_after_attempts: int = 5
|
|
189
|
+
"""이 횟수 시도 후에도 데이터 못 찾으면 unverifiable."""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Plan(BaseModel):
|
|
193
|
+
"""Plan Agent가 생성하는 검증 계획."""
|
|
194
|
+
|
|
195
|
+
claim_id: str
|
|
196
|
+
claim_type: ClaimType
|
|
197
|
+
required_data: list[DataPointSpec]
|
|
198
|
+
initial_steps: list[PlanStep] = Field(default_factory=list)
|
|
199
|
+
fallback: FallbackStrategy = Field(default_factory=FallbackStrategy)
|
|
200
|
+
calculation_formula: str | None = None
|
|
201
|
+
"""예: '(current - prev) / prev * 100' (증가율), 'X - Y' (차이)."""
|
|
202
|
+
|
|
203
|
+
notes: str | None = None
|
|
204
|
+
"""Plan Agent의 자유 형식 메모 (디버깅용)."""
|
|
205
|
+
|
|
206
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Observation + Reflect ────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
class Observation(BaseModel):
|
|
212
|
+
"""Action 실행 결과."""
|
|
213
|
+
|
|
214
|
+
iter_num: int
|
|
215
|
+
action: ActionType
|
|
216
|
+
input: dict[str, Any]
|
|
217
|
+
output: dict[str, Any]
|
|
218
|
+
"""Tool 호출 raw 결과. observations/*.json에도 저장됨."""
|
|
219
|
+
|
|
220
|
+
summary: str
|
|
221
|
+
"""memory.md에 들어갈 한 줄 요약. agent가 다음 턴에 참고."""
|
|
222
|
+
|
|
223
|
+
success: bool = True
|
|
224
|
+
error: str | None = None
|
|
225
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class ReflectDecision(BaseModel):
|
|
229
|
+
"""Reflect Agent의 다음 행동 결정."""
|
|
230
|
+
|
|
231
|
+
thought: str
|
|
232
|
+
"""추론 과정 (디버깅 + 사용자에게 보일 수도)."""
|
|
233
|
+
|
|
234
|
+
action: ActionType
|
|
235
|
+
input: dict[str, Any]
|
|
236
|
+
|
|
237
|
+
confidence_so_far: float = 0.0
|
|
238
|
+
"""0~1. finish 결정에 영향."""
|
|
239
|
+
|
|
240
|
+
# finish action일 때만 채워짐
|
|
241
|
+
proposed_verdict: VerdictType | None = None
|
|
242
|
+
proposed_explanation: str | None = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── Verdict ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
class AgentVerdict(BaseModel):
|
|
248
|
+
"""Agent loop 최종 결과."""
|
|
249
|
+
|
|
250
|
+
claim_id: str
|
|
251
|
+
verdict: VerdictType
|
|
252
|
+
confidence: float
|
|
253
|
+
explanation: str
|
|
254
|
+
|
|
255
|
+
data_points: list[DataPointSpec] = Field(default_factory=list)
|
|
256
|
+
"""확보된 데이터 점들. verifier.py가 이걸로 *최종 검산*."""
|
|
257
|
+
|
|
258
|
+
iterations_used: int = 0
|
|
259
|
+
total_tokens: int = 0
|
|
260
|
+
stop_reason: StopReason = StopReason.COMPLETED
|
|
261
|
+
|
|
262
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""structverify.agent.source_profiler — 연결된 데이터소스의 '검증 프로파일' 생성.
|
|
2
|
+
|
|
3
|
+
목적: 탐지·스키마 파이프라인이 KOSIS를 가정하지 않고, *연결된 소스*로 검증
|
|
4
|
+
가능한 것을 기준으로 삼게 한다.
|
|
5
|
+
|
|
6
|
+
에이전트 관점:
|
|
7
|
+
· inspect — 소스를 조회해 지표/단위/샘플을 수집 (도구)
|
|
8
|
+
· reason — LLM이 도메인·검증 범위를 요약
|
|
9
|
+
· remember — SourceProfile(의미기억)로 캐시 → 탐지 프롬프트에 주입
|
|
10
|
+
|
|
11
|
+
메모리 구조: SourceProfile 은 소스당 1회 만들어 캐시하는 *semantic memory*.
|
|
12
|
+
(같은 소스로 여러 문서를 검증해도 재생성 안 함.)
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field, asdict
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from structverify.utils.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 지표가 이보다 많으면 "방대"로 보고 임베딩+랭커 전략을 쓴다. (KOSIS 26만 표 = 방대)
|
|
25
|
+
_LARGE_THRESHOLD = 200
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SourceProfile:
|
|
30
|
+
"""연결된 데이터소스가 '무엇을 검증할 수 있는지'에 대한 의미기억."""
|
|
31
|
+
source_name: str
|
|
32
|
+
domain: str = "" # 추론된 도메인 (예: 'corporate_finance')
|
|
33
|
+
description: str = "" # 이 데이터가 다루는 것 한 문장
|
|
34
|
+
indicators: list[str] = field(default_factory=list) # 검증 가능한 지표들
|
|
35
|
+
units: list[str] = field(default_factory=list)
|
|
36
|
+
samples: list[str] = field(default_factory=list) # 예시 사실 몇 개
|
|
37
|
+
# ★ 검색 전략 — 프로파일러가 데이터 규모를 보고 결정 (소스 이름 분기 대체).
|
|
38
|
+
# {scale, indicator_count, method: keyword|embedding, use_ranker: bool, ranker_context}
|
|
39
|
+
retrieval_plan: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
def is_empty(self) -> bool:
|
|
45
|
+
return not self.indicators
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _decide_plan(indicators: list[str], domain: str, description: str) -> dict[str, Any]:
|
|
49
|
+
"""데이터 특성을 보고 검색 전략을 정한다.
|
|
50
|
+
|
|
51
|
+
규모-적응은 *검색 방식*에만 적용:
|
|
52
|
+
작음 → 키워드 매칭 (후보가 적어 임베딩 비용 회피).
|
|
53
|
+
방대 → 임베딩 의미검색 (KOSIS 26만 표처럼 키워드로 못 좁힘).
|
|
54
|
+
|
|
55
|
+
판별(랭커)은 *규모와 무관하게 항상 ON* — KOSIS 수준의 자동 disambiguation.
|
|
56
|
+
소규모라도 "총 주문건수 vs 연간 주문건수", "특수의료장비 vs 의료장비"처럼
|
|
57
|
+
claim에 맞는 표를 고르는 모호성은 늘 존재한다. 후보가 2개 이상이면 LLM 랭커가
|
|
58
|
+
claim(지표·시점·대상)에 가장 맞는 표를 고른다. (후보 1개면 호출자가 스킵.)
|
|
59
|
+
"""
|
|
60
|
+
n = len(indicators)
|
|
61
|
+
large = n > _LARGE_THRESHOLD
|
|
62
|
+
ctx = f"{domain}: {description}".strip(" :") if (domain or description) else ""
|
|
63
|
+
return {
|
|
64
|
+
"indicator_count": n,
|
|
65
|
+
"scale": "large" if large else "small",
|
|
66
|
+
"method": "embedding" if large else "keyword",
|
|
67
|
+
"use_ranker": n > 1, # 후보 판별이 필요할 수 있으면 항상 (KOSIS 파리티)
|
|
68
|
+
"ranker_context": ctx[:200],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# 소스명 → SourceProfile (프로세스 캐시). 같은 소스 재프로파일 방지.
|
|
73
|
+
_CACHE: dict[str, SourceProfile] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _inspect_tabular(ds: Any) -> tuple[list[str], list[str], list[str]]:
|
|
77
|
+
"""csv/db 소스에서 (지표목록, 단위목록, 샘플문장). _read_rows 를 쓰는 소스용."""
|
|
78
|
+
rows = ds._read_rows()
|
|
79
|
+
cm = getattr(ds, "column_mapping", {})
|
|
80
|
+
ind_c, unit_c, val_c = cm.get("indicator", "indicator"), cm.get("unit", "unit"), cm.get("value", "value")
|
|
81
|
+
inds, units, samples = [], [], []
|
|
82
|
+
seen_i, seen_u = set(), set()
|
|
83
|
+
for r in rows:
|
|
84
|
+
i = (r.get(ind_c) or "").strip()
|
|
85
|
+
if i and i not in seen_i:
|
|
86
|
+
seen_i.add(i); inds.append(i)
|
|
87
|
+
u = (r.get(unit_c) or "").strip()
|
|
88
|
+
if u and u not in seen_u:
|
|
89
|
+
seen_u.add(u); units.append(u)
|
|
90
|
+
for r in rows[:6]:
|
|
91
|
+
i, v, u = (r.get(ind_c) or ""), (r.get(val_c) or ""), (r.get(unit_c) or "")
|
|
92
|
+
if i:
|
|
93
|
+
samples.append(f"{i} = {v}{u}")
|
|
94
|
+
# 지표가 많은 소스(예: 부품유형 150+)에서 프로파일 상한에 잘려 'headline' 지표
|
|
95
|
+
# (총/지역/연간/평균 등)가 빠지는 문제 방지 — 짧고 일반적인 이름을 앞으로 정렬.
|
|
96
|
+
# (짧은 지표명 = 대개 종합/대표 지표; 긴 이름 = 세부 카테고리.)
|
|
97
|
+
inds.sort(key=lambda s: (len(s), s))
|
|
98
|
+
return inds, units, samples
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _inspect_docs(ds: Any) -> tuple[list[str], list[str], list[str]]:
|
|
102
|
+
"""custom_docs 소스에서 조각 미리보기 → 주제(지표 대용)."""
|
|
103
|
+
chunks = getattr(ds, "_chunks", None) or getattr(ds, "chunks", None) or []
|
|
104
|
+
previews = []
|
|
105
|
+
for ch in list(chunks)[:8]:
|
|
106
|
+
txt = ch if isinstance(ch, str) else (ch.get("text", "") if isinstance(ch, dict) else "")
|
|
107
|
+
txt = " ".join(str(txt).split())[:60]
|
|
108
|
+
if txt:
|
|
109
|
+
previews.append(txt)
|
|
110
|
+
return previews, [], previews
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_PROFILE_PROMPT = """아래는 사실 검증의 '정답 기준'으로 쓸 데이터 소스의 내용이다.
|
|
114
|
+
이 소스로 어떤 주장을 검증할 수 있는지 파악하라.
|
|
115
|
+
|
|
116
|
+
[소스에 있는 지표/항목]
|
|
117
|
+
{indicators}
|
|
118
|
+
|
|
119
|
+
[단위]
|
|
120
|
+
{units}
|
|
121
|
+
|
|
122
|
+
[샘플]
|
|
123
|
+
{samples}
|
|
124
|
+
|
|
125
|
+
JSON으로만 답하라:
|
|
126
|
+
{{"domain": "이 데이터의 도메인 한 단어(영문 스네이크, 예: corporate_finance)",
|
|
127
|
+
"description": "이 데이터가 다루는 내용을 한 문장으로"}}"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def profile_source(
|
|
131
|
+
ds: Any,
|
|
132
|
+
source_name: str,
|
|
133
|
+
config: dict | None = None,
|
|
134
|
+
*,
|
|
135
|
+
max_indicators: int = 60,
|
|
136
|
+
) -> SourceProfile:
|
|
137
|
+
"""데이터소스를 프로파일링해 SourceProfile 반환 (캐시)."""
|
|
138
|
+
# 캐시 키: 소스명 + 실제 데이터 식별자(테이블/경로) — 같은 custom_db라도 테이블이
|
|
139
|
+
# 다르면 다른 프로파일이어야 한다.
|
|
140
|
+
_ident = (
|
|
141
|
+
getattr(ds, "table", None) or getattr(ds, "query", None)
|
|
142
|
+
or getattr(ds, "csv_path", None) or getattr(ds, "docs_path", None)
|
|
143
|
+
or (",".join(getattr(ds, "tables", []) or []) if getattr(ds, "agentic", False) else "")
|
|
144
|
+
or ""
|
|
145
|
+
)
|
|
146
|
+
cache_key = f"{source_name}:{_ident}"
|
|
147
|
+
if cache_key in _CACHE:
|
|
148
|
+
return _CACHE[cache_key]
|
|
149
|
+
|
|
150
|
+
# ── 에이전틱(text-to-SQL) 소스: 스키마 조사 → LLM이 지표 후보 제안 ──
|
|
151
|
+
if getattr(ds, "agentic", False) and hasattr(ds, "propose_metrics"):
|
|
152
|
+
try:
|
|
153
|
+
meta = await ds.propose_metrics(config)
|
|
154
|
+
except Exception as e: # noqa: BLE001
|
|
155
|
+
logger.warning(f"[source-profiler] propose_metrics 실패({source_name}): {e}")
|
|
156
|
+
meta = {}
|
|
157
|
+
profile = SourceProfile(
|
|
158
|
+
source_name=source_name,
|
|
159
|
+
domain=str(meta.get("domain", "") or ""),
|
|
160
|
+
description=str(meta.get("description", "") or ""),
|
|
161
|
+
indicators=list(meta.get("indicators", []) or [])[:max_indicators],
|
|
162
|
+
units=list(meta.get("units", []) or [])[:20],
|
|
163
|
+
samples=list(meta.get("samples", []) or [])[:6],
|
|
164
|
+
)
|
|
165
|
+
if profile.indicators:
|
|
166
|
+
profile.retrieval_plan = _decide_plan(
|
|
167
|
+
profile.indicators, profile.domain, profile.description,
|
|
168
|
+
)
|
|
169
|
+
profile.retrieval_plan["agentic"] = True # SQL로 임의 집계 가능 → worthiness 관대
|
|
170
|
+
logger.info(
|
|
171
|
+
f"[source-profiler] {source_name}(에이전틱): domain={profile.domain!r} "
|
|
172
|
+
f"제안지표 {len(profile.indicators)}개 · text-to-SQL 모드"
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
logger.info(f"[source-profiler] {source_name}(에이전틱): 지표 제안 없음")
|
|
176
|
+
_CACHE[cache_key] = profile
|
|
177
|
+
return profile
|
|
178
|
+
|
|
179
|
+
# ── inspect ──
|
|
180
|
+
try:
|
|
181
|
+
if hasattr(ds, "_read_rows"):
|
|
182
|
+
inds, units, samples = _inspect_tabular(ds)
|
|
183
|
+
else:
|
|
184
|
+
inds, units, samples = _inspect_docs(ds)
|
|
185
|
+
except Exception as e: # noqa: BLE001 — 프로파일 실패는 빈 프로파일로(파이프라인 보호)
|
|
186
|
+
logger.warning(f"[source-profiler] inspect 실패({source_name}): {e}")
|
|
187
|
+
inds = units = samples = []
|
|
188
|
+
|
|
189
|
+
profile = SourceProfile(
|
|
190
|
+
source_name=source_name,
|
|
191
|
+
indicators=inds[:max_indicators],
|
|
192
|
+
units=units[:20],
|
|
193
|
+
samples=samples[:6],
|
|
194
|
+
)
|
|
195
|
+
if not inds:
|
|
196
|
+
logger.info(f"[source-profiler] {source_name}: 지표 없음 — 빈 프로파일")
|
|
197
|
+
_CACHE[cache_key] = profile
|
|
198
|
+
return profile
|
|
199
|
+
|
|
200
|
+
# ── reason (LLM 요약) ──
|
|
201
|
+
try:
|
|
202
|
+
from structverify.utils.llm_client import LLMClient
|
|
203
|
+
llm = LLMClient(config=(config or {}).get("llm", {}))
|
|
204
|
+
out = await llm.generate_json_light(_PROFILE_PROMPT.format(
|
|
205
|
+
indicators="\n".join(f"- {i}" for i in inds[:40]),
|
|
206
|
+
units=", ".join(units[:20]) or "(미상)",
|
|
207
|
+
samples="\n".join(samples) or "(없음)",
|
|
208
|
+
))
|
|
209
|
+
profile.domain = str((out or {}).get("domain", "") or "").strip()
|
|
210
|
+
profile.description = str((out or {}).get("description", "") or "").strip()
|
|
211
|
+
except Exception as e: # noqa: BLE001
|
|
212
|
+
logger.warning(f"[source-profiler] reason 실패({source_name}): {e}")
|
|
213
|
+
|
|
214
|
+
# ★ 규모 보고 검색 전략 결정 (키워드 vs 임베딩+랭커)
|
|
215
|
+
profile.retrieval_plan = _decide_plan(
|
|
216
|
+
profile.indicators, profile.domain, profile.description,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
logger.info(
|
|
220
|
+
f"[source-profiler] {source_name} 프로파일: domain={profile.domain!r} "
|
|
221
|
+
f"지표 {len(profile.indicators)}개 · 전략={profile.retrieval_plan['method']}"
|
|
222
|
+
f"(랭커 {'ON' if profile.retrieval_plan['use_ranker'] else 'OFF'})"
|
|
223
|
+
)
|
|
224
|
+
_CACHE[cache_key] = profile
|
|
225
|
+
return profile
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def clear_cache() -> None:
|
|
229
|
+
_CACHE.clear()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
structverify.agent.tools — Agent가 호출하는 Tool 모음.
|
|
3
|
+
|
|
4
|
+
각 Tool은 *@register_tool(ActionType.X)* 데코레이터로 자동 등록됨.
|
|
5
|
+
이 패키지를 import하면 *모든 Tool이 registry에 등록됨*.
|
|
6
|
+
|
|
7
|
+
Phase B Tool 목록:
|
|
8
|
+
- catalog_search : DataSource 카탈로그(표) 검색
|
|
9
|
+
- fetch_evidence : 후보의 실제 수치 조회
|
|
10
|
+
- read_original : workspace의 원문 기사 읽기
|
|
11
|
+
- calculate : 안전한 수식 계산
|
|
12
|
+
- finish : 검증 종료 + Verdict 생성
|
|
13
|
+
|
|
14
|
+
회사 자체 Tool 추가:
|
|
15
|
+
from structverify.agent.tools import register_tool, ToolBase
|
|
16
|
+
from structverify.agent.schemas import ActionType
|
|
17
|
+
|
|
18
|
+
# ActionType에 새 항목 추가 후 (또는 기존 사용):
|
|
19
|
+
@register_tool(ActionType.YOUR_ACTION)
|
|
20
|
+
class YourTool(ToolBase):
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
Usage (Phase D Loop에서):
|
|
24
|
+
from structverify.agent.tools import get_tool_class, list_tools, render_all_help
|
|
25
|
+
|
|
26
|
+
# LLM prompt에 모든 Tool 설명 삽입
|
|
27
|
+
prompt = f"... Available tools:\n{render_all_help()} ..."
|
|
28
|
+
|
|
29
|
+
# LLM 응답 (decision)에서 action 받아서 실행
|
|
30
|
+
tool_cls = get_tool_class(decision.action)
|
|
31
|
+
tool = tool_cls()
|
|
32
|
+
result = await tool.execute(decision.input, context)
|
|
33
|
+
"""
|
|
34
|
+
from .base import (
|
|
35
|
+
ToolBase,
|
|
36
|
+
ToolContext,
|
|
37
|
+
ToolResult,
|
|
38
|
+
register_tool,
|
|
39
|
+
get_tool_class,
|
|
40
|
+
list_tools,
|
|
41
|
+
build_tool,
|
|
42
|
+
render_all_help,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 모든 Tool 모듈 import — register_tool 데코레이터 실행 트리거.
|
|
46
|
+
# (import 순서 무관 — registry는 ActionType 키 기반)
|
|
47
|
+
from . import calculate # noqa: F401
|
|
48
|
+
from . import finish # noqa: F401
|
|
49
|
+
from . import read_original # noqa: F401
|
|
50
|
+
from . import catalog_search # noqa: F401
|
|
51
|
+
from . import fetch_evidence # noqa: F401
|
|
52
|
+
from . import explore_catalog # noqa: F401
|
|
53
|
+
from . import replan # noqa: F401
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"ToolBase",
|
|
57
|
+
"ToolContext",
|
|
58
|
+
"ToolResult",
|
|
59
|
+
"register_tool",
|
|
60
|
+
"get_tool_class",
|
|
61
|
+
"list_tools",
|
|
62
|
+
"build_tool",
|
|
63
|
+
"render_all_help",
|
|
64
|
+
]
|