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,241 @@
|
|
|
1
|
+
# [이수민 - 2026-05-14]
|
|
2
|
+
# - 정확도 개선용 사례 메모리 (exemplar retrieval store)
|
|
3
|
+
# - 목적:
|
|
4
|
+
# · 도메인 분류 정확도 ↑ — 과거 (텍스트, 정답 도메인) 사례 retrieve → few-shot hint
|
|
5
|
+
# · 시간 해석 정확도 ↑ — 과거 (표현, 문맥, anchor_year, 정답 resolved) 사례 retrieve
|
|
6
|
+
# - 저장 단위: DomainExample, TemporalExample (둘 다 임베딩 포함)
|
|
7
|
+
# - 검색 방식: pgvector cosine 유사도 (기존 KOSIS catalog 검색과 동일 패턴)
|
|
8
|
+
# - 임베딩: catalog_search.py의 HCX 임베딩 API 재활용
|
|
9
|
+
# - 구조화 단계 — 시그니처와 인터페이스만. 본문은 다음 단계에서 구현.
|
|
10
|
+
"""
|
|
11
|
+
memory/exemplar_store.py — 사례 기반 retrieval memory
|
|
12
|
+
|
|
13
|
+
[설계 의도]
|
|
14
|
+
LLM이 매번 추론(도메인 분류, 시간 풀이)을 처음부터 하지 않고,
|
|
15
|
+
과거 검증된 사례를 retrieve해서 few-shot hint로 주입.
|
|
16
|
+
|
|
17
|
+
[라이프사이클]
|
|
18
|
+
실행 후: pipeline 끝에 정답 케이스 → store_domain() / store_temporal()
|
|
19
|
+
실행 중: Step 3 (domain), Step 4.5 (temporal)에서 retrieve_* 호출 → 프롬프트 hint
|
|
20
|
+
|
|
21
|
+
[저장 위치]
|
|
22
|
+
PostgreSQL의 별도 테이블 (pgvector extension 이미 활성화됨):
|
|
23
|
+
- domain_examples (text_embedding, domain, ...)
|
|
24
|
+
- temporal_examples (expression_embedding, context, anchor_year, resolved, ...)
|
|
25
|
+
|
|
26
|
+
[Phase 2 — 통합 예정]
|
|
27
|
+
classify_domain():
|
|
28
|
+
exemplars = await store.retrieve_domain_examples(text, top_k=3)
|
|
29
|
+
prompt += "참고 사례: ..."
|
|
30
|
+
→ LLM 호출
|
|
31
|
+
|
|
32
|
+
build_document_temporal_graph():
|
|
33
|
+
for expr in temporal_expressions:
|
|
34
|
+
exemplars = await store.retrieve_temporal_examples(expr, anchor_year, top_k=3)
|
|
35
|
+
prompt_hint += "..."
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from pydantic import BaseModel, Field
|
|
43
|
+
|
|
44
|
+
from structverify.utils.logger import get_logger
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── 사례 데이터 모델 ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
class DomainExample(BaseModel):
|
|
52
|
+
"""
|
|
53
|
+
도메인 분류 사례.
|
|
54
|
+
|
|
55
|
+
저장 시점: pipeline 끝, 검증 결과(verdict)가 정답으로 확인된 케이스만.
|
|
56
|
+
"""
|
|
57
|
+
example_id: str
|
|
58
|
+
text: str # 원문 일부 (제목 + 처음 N문장)
|
|
59
|
+
embedding: list[float] | None = None
|
|
60
|
+
domain: str # 정답 도메인 (employment, environment 등)
|
|
61
|
+
confidence: float = 1.0 # 사람 검수 / LLM 자체평가
|
|
62
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
63
|
+
source_doc: str | None = None # 출처 doc_id (추적용)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TemporalExample(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
시간 표현 해석 사례.
|
|
69
|
+
|
|
70
|
+
저장 시점: document_graph LLM agent 결과가 검증된 케이스.
|
|
71
|
+
"""
|
|
72
|
+
example_id: str
|
|
73
|
+
expression: str # 원본 표현 ("작년", "재작년 같은 기간")
|
|
74
|
+
context: str # 표현이 등장한 문장 + 앞뒤 1문장
|
|
75
|
+
embedding: list[float] | None = None # expression + context 임베딩
|
|
76
|
+
anchor_year: int # 그 문서의 anchor_year
|
|
77
|
+
resolved: str # 정답 절대 시점 ("2024", "2022-01..2022-11")
|
|
78
|
+
basis: str | None = None # 풀이 근거
|
|
79
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
80
|
+
source_doc: str | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── Exemplar Store API ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
class ExemplarStore:
|
|
86
|
+
"""
|
|
87
|
+
pgvector 기반 사례 저장/검색.
|
|
88
|
+
|
|
89
|
+
[라이프사이클]
|
|
90
|
+
store = ExemplarStore()
|
|
91
|
+
await store.connect()
|
|
92
|
+
|
|
93
|
+
# 저장 (pipeline 후)
|
|
94
|
+
await store.store_domain(text, domain, ...)
|
|
95
|
+
await store.store_temporal(expression, context, anchor, resolved, ...)
|
|
96
|
+
|
|
97
|
+
# 검색 (pipeline 중)
|
|
98
|
+
exs = await store.retrieve_domain_examples(text, top_k=3)
|
|
99
|
+
exs = await store.retrieve_temporal_examples(expr, anchor, top_k=3)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, config: dict | None = None):
|
|
103
|
+
"""
|
|
104
|
+
Args:
|
|
105
|
+
config: {
|
|
106
|
+
"pg_dsn": "postgresql://...",
|
|
107
|
+
"embedding_dim": 1024, # HCX 임베딩 차원
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
self.config = config or {}
|
|
111
|
+
|
|
112
|
+
async def connect(self) -> None:
|
|
113
|
+
"""asyncpg 풀 생성 + 테이블 존재 확인."""
|
|
114
|
+
raise NotImplementedError # 다음 단계
|
|
115
|
+
|
|
116
|
+
async def close(self) -> None:
|
|
117
|
+
"""연결 종료."""
|
|
118
|
+
raise NotImplementedError # 다음 단계
|
|
119
|
+
|
|
120
|
+
# ── 저장 ────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async def store_domain(
|
|
123
|
+
self,
|
|
124
|
+
text: str,
|
|
125
|
+
domain: str,
|
|
126
|
+
confidence: float = 1.0,
|
|
127
|
+
source_doc: str | None = None,
|
|
128
|
+
) -> str:
|
|
129
|
+
"""
|
|
130
|
+
도메인 사례 저장.
|
|
131
|
+
|
|
132
|
+
흐름:
|
|
133
|
+
① text → 임베딩 (HCX API)
|
|
134
|
+
② DomainExample 객체 생성
|
|
135
|
+
③ domain_examples 테이블에 INSERT
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
example_id
|
|
139
|
+
"""
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
async def store_temporal(
|
|
143
|
+
self,
|
|
144
|
+
expression: str,
|
|
145
|
+
context: str,
|
|
146
|
+
anchor_year: int,
|
|
147
|
+
resolved: str,
|
|
148
|
+
basis: str | None = None,
|
|
149
|
+
source_doc: str | None = None,
|
|
150
|
+
) -> str:
|
|
151
|
+
"""
|
|
152
|
+
시간 표현 사례 저장.
|
|
153
|
+
|
|
154
|
+
흐름:
|
|
155
|
+
① (expression + context) → 임베딩
|
|
156
|
+
② TemporalExample 객체 생성
|
|
157
|
+
③ temporal_examples 테이블에 INSERT
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
example_id
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError
|
|
163
|
+
|
|
164
|
+
# ── 검색 ────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async def retrieve_domain_examples(
|
|
167
|
+
self,
|
|
168
|
+
text: str,
|
|
169
|
+
top_k: int = 3,
|
|
170
|
+
) -> list[DomainExample]:
|
|
171
|
+
"""
|
|
172
|
+
주어진 텍스트와 의미적으로 가까운 도메인 사례 retrieve.
|
|
173
|
+
|
|
174
|
+
흐름:
|
|
175
|
+
① text → 임베딩
|
|
176
|
+
② pgvector cosine 유사도 검색
|
|
177
|
+
③ top_k 사례 반환 (domain 분포 다양성 확보 위해 필요 시 re-rank)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
list[DomainExample] — 유사도 높은 순
|
|
181
|
+
"""
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
async def retrieve_temporal_examples(
|
|
185
|
+
self,
|
|
186
|
+
expression: str,
|
|
187
|
+
context: str | None = None,
|
|
188
|
+
anchor_year: int | None = None,
|
|
189
|
+
top_k: int = 3,
|
|
190
|
+
) -> list[TemporalExample]:
|
|
191
|
+
"""
|
|
192
|
+
주어진 시간 표현과 비슷한 과거 사례 retrieve.
|
|
193
|
+
|
|
194
|
+
흐름:
|
|
195
|
+
① (expression + context or expression only) → 임베딩
|
|
196
|
+
② pgvector 검색 (anchor_year 가까운 케이스 우대)
|
|
197
|
+
③ top_k 사례 반환
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
expression: 새 문서에서 추출된 시간 표현
|
|
201
|
+
context: 주변 문맥 (있으면 정확도 ↑)
|
|
202
|
+
anchor_year: 새 문서의 anchor_year (있으면 비슷한 anchor 사례 우대)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
list[TemporalExample]
|
|
206
|
+
"""
|
|
207
|
+
raise NotImplementedError
|
|
208
|
+
|
|
209
|
+
# ── 유틸 ────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
async def count(self) -> dict[str, int]:
|
|
212
|
+
"""저장된 사례 수 통계. {domain: N, temporal: M}"""
|
|
213
|
+
raise NotImplementedError
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── 프롬프트 hint 변환 헬퍼 ─────────────────────────────────────────────────
|
|
217
|
+
# retrieve 결과를 LLM 프롬프트에 주입할 텍스트로 변환.
|
|
218
|
+
# 도메인 분류 / 시간 해석에서 공통으로 쓰임.
|
|
219
|
+
|
|
220
|
+
def format_domain_hint(examples: list[DomainExample]) -> str:
|
|
221
|
+
"""
|
|
222
|
+
DomainExample 리스트 → 프롬프트에 끼울 few-shot 텍스트.
|
|
223
|
+
|
|
224
|
+
예시 출력:
|
|
225
|
+
# 참고 사례 (과거 분류 결과)
|
|
226
|
+
- "쉬었음 청년이 21만..." → employment (신뢰도 0.95)
|
|
227
|
+
- "연평균기온 14.8도..." → environment (신뢰도 0.92)
|
|
228
|
+
"""
|
|
229
|
+
raise NotImplementedError
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def format_temporal_hint(examples: list[TemporalExample]) -> str:
|
|
233
|
+
"""
|
|
234
|
+
TemporalExample 리스트 → 프롬프트 hint 텍스트.
|
|
235
|
+
|
|
236
|
+
예시 출력:
|
|
237
|
+
# 참고 사례 (과거 시간 표현 풀이)
|
|
238
|
+
- "작년" (anchor=2024 문서) → 2023
|
|
239
|
+
- "재작년 같은 기간" (anchor=2024) → 2022-01..2022-11
|
|
240
|
+
"""
|
|
241
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - C안(KOSIS stat_id 기반) 정규화 헬퍼 모음
|
|
3
|
+
# - canonicalize_metric/source/evidence: stat_id → canonical node_id 문자열 생성
|
|
4
|
+
# · metric:{stat_id} 예) "metric:DT_1ES4001"
|
|
5
|
+
# · source:{stat_id}
|
|
6
|
+
# · evidence:{stat_id}:{time} 예) "evidence:DT_1ES4001:2024"
|
|
7
|
+
# - rename_metric_to_canonical(): Step 7 이후 호출
|
|
8
|
+
# · 임시 Metric 노드(LLM이 뱉은 indicator명) → canonical id로 rename
|
|
9
|
+
# · 매달려있던 BELONGS_TO 등 엣지를 canonical 노드로 redirect
|
|
10
|
+
# - 함수 본문은 Phase 2 구현 (현재는 시그니처+docstring만)
|
|
11
|
+
"""
|
|
12
|
+
memory/normalizer.py — Canonical key 생성
|
|
13
|
+
|
|
14
|
+
C안 (KOSIS stat_id 기반 정규화):
|
|
15
|
+
임시 Metric 노드 (LLM이 뱉은 indicator명)
|
|
16
|
+
→ Evidence 도착 후 stat_id로 canonical node_id 생성
|
|
17
|
+
→ 누적 메모리의 동일 stat_id Metric과 자동 dedup
|
|
18
|
+
|
|
19
|
+
[Phase 1 — 구조화]
|
|
20
|
+
함수 시그니처만 정의. 본문은 Phase 2에서 구현.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from structverify.core.schemas import Evidence, GraphEdge, GraphNode
|
|
25
|
+
from structverify.utils.logger import get_logger
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Canonical id 생성 ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def canonicalize_metric(stat_id: str) -> str:
|
|
33
|
+
"""
|
|
34
|
+
KOSIS stat_id → Metric 노드의 canonical node_id.
|
|
35
|
+
|
|
36
|
+
예시:
|
|
37
|
+
canonicalize_metric("DT_1ES4001") → "metric:DT_1ES4001"
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
stat_id: KOSIS 통계표 ID (DT_로 시작하는 문자열)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
canonical node_id 문자열
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError # Phase 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def canonicalize_source(stat_id: str) -> str:
|
|
49
|
+
"""KOSIS stat_id → Source 노드의 canonical node_id."""
|
|
50
|
+
raise NotImplementedError # Phase 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def canonicalize_evidence(stat_id: str, time_period: str | None) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Evidence 노드의 canonical node_id.
|
|
56
|
+
|
|
57
|
+
같은 stat_id라도 시점이 다르면 다른 Evidence.
|
|
58
|
+
|
|
59
|
+
예시:
|
|
60
|
+
canonicalize_evidence("DT_1ES4001", "2024") → "evidence:DT_1ES4001:2024"
|
|
61
|
+
canonicalize_evidence("DT_1ES4001", "2024-09") → "evidence:DT_1ES4001:2024-09"
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError # Phase 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Metric 노드 rename + 엣지 redirect ───────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def rename_metric_to_canonical(
|
|
69
|
+
nodes: list[GraphNode],
|
|
70
|
+
edges: list[GraphEdge],
|
|
71
|
+
evidence: Evidence,
|
|
72
|
+
temp_metric_id: str,
|
|
73
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
74
|
+
"""
|
|
75
|
+
Step 7 이후 호출. 임시 Metric 노드를 canonical id로 rename + edge redirect.
|
|
76
|
+
|
|
77
|
+
흐름:
|
|
78
|
+
① evidence.stat_table_id로 canonical id 계산
|
|
79
|
+
② nodes에서 temp_metric_id를 찾아 node_id를 canonical로 교체
|
|
80
|
+
③ edges에서 from_node/to_node가 temp_metric_id인 것을 canonical로 redirect
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
nodes: Step 6에서 만든 노드 리스트 (in-place 수정 또는 copy)
|
|
84
|
+
edges: Step 6에서 만든 엣지 리스트
|
|
85
|
+
evidence: Step 7에서 받은 Evidence (stat_table_id 보유)
|
|
86
|
+
temp_metric_id: LLM이 뱉은 indicator 기반 임시 id (e.g. "metric:쉬었음인구")
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
(정규화된 nodes, 정규화된 edges)
|
|
90
|
+
"""
|
|
91
|
+
raise NotImplementedError # Phase 2
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - AgentMemory에서 사용할 영속 데이터 모델 정의
|
|
3
|
+
# - MemoryNode/MemoryEdge: 기존 GraphNode/GraphEdge에 provenance 부착한 wrapper
|
|
4
|
+
# - Provenance: run_id + doc_id + 타임스탬프 (롤백·last-write-wins 용)
|
|
5
|
+
# - CrossDocEdgeType: cross-doc 전용 엣지(AGREES_WITH/SUPERSEDES/REUSED_BY)
|
|
6
|
+
# ※ CONTRADICTS/SUPPORTS는 core/schemas.py:GraphEdgeType에 이미 존재 — 재사용
|
|
7
|
+
# - 하단에 canonical key 컨벤션(metric:{stat_id} 등) 주석으로 명시
|
|
8
|
+
"""
|
|
9
|
+
memory/schema.py — AgentMemory 영속 스키마
|
|
10
|
+
|
|
11
|
+
기존 GraphNode/GraphEdge를 감싸는 영속 wrapper.
|
|
12
|
+
- provenance 필드 추가 (어느 doc/run에서 생성됐는지)
|
|
13
|
+
- created_at / updated_at (last-write-wins용)
|
|
14
|
+
- cross-doc 전용 엣지 타입 enum
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from structverify.core.schemas import GraphNode, GraphEdge, GraphNodeType, GraphEdgeType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── Cross-doc 전용 엣지 타입 ─────────────────────────────────────────────────
|
|
28
|
+
# core/schemas.py의 GraphEdgeType은 단일 doc 그래프용.
|
|
29
|
+
# 여기는 누적 메모리에서 doc 간 관계를 표현하는 별도 enum.
|
|
30
|
+
#
|
|
31
|
+
# 주의: GraphEdgeType.CONTRADICTS / SUPPORTS는 이미 존재 — 재사용 가능.
|
|
32
|
+
# AgentMemory가 새로 만들 수 있는 doc 간 엣지는 아래 3종.
|
|
33
|
+
|
|
34
|
+
class CrossDocEdgeType(str, Enum):
|
|
35
|
+
AGREES_WITH = "agrees_with" # 같은 (Metric, Time, Entity)에 같은 value (다른 doc)
|
|
36
|
+
SUPERSEDES = "supersedes" # 더 최근 doc이 같은 claim에 다른 값 (정정보도)
|
|
37
|
+
REUSED_BY = "reused_by" # 과거 Evidence를 새 Claim 검증에 재활용
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Provenance 메타데이터 ───────────────────────────────────────────────────
|
|
41
|
+
# 모든 MemoryNode/MemoryEdge에 부착. 잘못된 doc 발견 시 그 기여분만 롤백 가능.
|
|
42
|
+
|
|
43
|
+
class Provenance(BaseModel):
|
|
44
|
+
created_in_run: str # run_id (파이프라인 1회 실행 단위)
|
|
45
|
+
from_doc_id: str # doc_id (어느 문서에서 비롯됐는지)
|
|
46
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── 영속 노드/엣지 ───────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
class MemoryNode(BaseModel):
|
|
52
|
+
"""
|
|
53
|
+
JSONL에 저장될 영속 노드.
|
|
54
|
+
|
|
55
|
+
GraphNode를 감싸고 provenance + 타임스탬프 추가.
|
|
56
|
+
공유 노드(Metric/Entity/Time/Source/Evidence)는 canonical key로 dedup,
|
|
57
|
+
문서별 노드(Document/Claim/Sentence)는 append-only.
|
|
58
|
+
"""
|
|
59
|
+
# 기존 GraphNode 필드
|
|
60
|
+
node_id: str # canonical key (e.g. "metric:DT_1ES4001")
|
|
61
|
+
node_type: GraphNodeType
|
|
62
|
+
label: str
|
|
63
|
+
domain: str | None = None
|
|
64
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
# 영속화용 필드
|
|
67
|
+
provenance: Provenance
|
|
68
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow) # last-write-wins
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_graph_node(
|
|
72
|
+
cls,
|
|
73
|
+
node: GraphNode,
|
|
74
|
+
run_id: str,
|
|
75
|
+
doc_id: str,
|
|
76
|
+
) -> "MemoryNode":
|
|
77
|
+
"""런타임 GraphNode → 영속 MemoryNode 변환."""
|
|
78
|
+
raise NotImplementedError # Phase 2 구현
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MemoryEdge(BaseModel):
|
|
82
|
+
"""
|
|
83
|
+
JSONL에 저장될 영속 엣지.
|
|
84
|
+
|
|
85
|
+
GraphEdge를 감싸고 provenance 추가.
|
|
86
|
+
edge_type은 GraphEdgeType 또는 CrossDocEdgeType.
|
|
87
|
+
"""
|
|
88
|
+
edge_id: str
|
|
89
|
+
from_node: str
|
|
90
|
+
to_node: str
|
|
91
|
+
# edge_type은 GraphEdgeType.value 또는 CrossDocEdgeType.value 문자열
|
|
92
|
+
edge_type: str
|
|
93
|
+
weight: float = 1.0
|
|
94
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
# 영속화용 필드
|
|
97
|
+
provenance: Provenance
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_graph_edge(
|
|
101
|
+
cls,
|
|
102
|
+
edge: GraphEdge,
|
|
103
|
+
run_id: str,
|
|
104
|
+
doc_id: str,
|
|
105
|
+
) -> "MemoryEdge":
|
|
106
|
+
"""런타임 GraphEdge → 영속 MemoryEdge 변환."""
|
|
107
|
+
raise NotImplementedError # Phase 2 구현
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ── Canonical key 컨벤션 ────────────────────────────────────────────────────
|
|
111
|
+
# 공유 노드의 node_id 형식. Phase 2에서 실제 생성 시 이 규칙 따름.
|
|
112
|
+
#
|
|
113
|
+
# Metric : "metric:{stat_id}" 예) "metric:DT_1ES4001"
|
|
114
|
+
# Source : "source:{stat_id}" 예) "source:DT_1ES4001"
|
|
115
|
+
# Evidence : "evidence:{stat_id}:{time}" 예) "evidence:DT_1ES4001:2024"
|
|
116
|
+
# Entity : "entity:{normalized_name}" 예) "entity:청년"
|
|
117
|
+
# Time : "time:{iso}" 예) "time:2024", "time:2024-09"
|
|
118
|
+
#
|
|
119
|
+
# 문서별 노드(Document/Claim/Sentence)는 기존 UUID/anchor_id 사용.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - memory.storage 패키지 진입점 (I/O 전담 layer)
|
|
3
|
+
# - 로직(agent_memory.py) / 저장(storage/)을 분리하는 경계
|
|
4
|
+
# - 현재 노출: read_all_nodes / read_all_edges / append_node / append_edge
|
|
5
|
+
# - Phase 2 마이그레이션 시 jsonl_store → neo4j_store로 교체
|
|
6
|
+
# agent_memory.py 인터페이스는 그대로 유지
|
|
7
|
+
"""
|
|
8
|
+
memory.storage — 영속 layer (Phase 1: JSONL, Phase 2: Neo4j)
|
|
9
|
+
|
|
10
|
+
Phase 1 책임 분리:
|
|
11
|
+
agent_memory.py → 로직 (dedup, merge, query)
|
|
12
|
+
storage/ → I/O (read_all, append)
|
|
13
|
+
|
|
14
|
+
Phase 2 마이그레이션 시 jsonl_store를 neo4j_store로 교체.
|
|
15
|
+
agent_memory.py의 인터페이스는 그대로.
|
|
16
|
+
"""
|
|
17
|
+
from structverify.memory.storage.jsonl_store import (
|
|
18
|
+
read_all_nodes,
|
|
19
|
+
read_all_edges,
|
|
20
|
+
append_node,
|
|
21
|
+
append_edge,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"read_all_nodes",
|
|
26
|
+
"read_all_edges",
|
|
27
|
+
"append_node",
|
|
28
|
+
"append_edge",
|
|
29
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - JSONL append-only 저장소 (Phase 1)
|
|
3
|
+
# - 저장 위치:
|
|
4
|
+
# backend/structverify/memory/nodes/{type}.jsonl (노드 타입별 분리)
|
|
5
|
+
# backend/structverify/memory/edges/edges.jsonl (엣지는 단일 파일)
|
|
6
|
+
# - 원칙: append-only, 수정/삭제 안 함. dedup은 in-memory load 시 last-write-wins
|
|
7
|
+
# - 노출 함수: read_all_nodes / read_all_edges / append_node / append_edge
|
|
8
|
+
# - _NODE_FILES dict로 GraphNodeType → 파일명 매핑
|
|
9
|
+
# - 함수 본문은 Phase 2 구현 (현재는 시그니처+docstring만)
|
|
10
|
+
"""
|
|
11
|
+
memory/storage/jsonl_store.py — JSONL append-only 저장소 (Phase 1)
|
|
12
|
+
|
|
13
|
+
[저장 위치]
|
|
14
|
+
backend/structverify/memory/nodes/{type}.jsonl
|
|
15
|
+
backend/structverify/memory/edges/edges.jsonl
|
|
16
|
+
|
|
17
|
+
[원칙]
|
|
18
|
+
- Append-only: 수정/삭제 안 함. dedup은 in-memory load 시 last-write-wins.
|
|
19
|
+
- 노드 타입별 분리: metrics / entities / times / sources / evidences /
|
|
20
|
+
documents / claims / temporals
|
|
21
|
+
- 엣지는 단일 파일
|
|
22
|
+
|
|
23
|
+
[Phase 1 — 구조화]
|
|
24
|
+
함수 시그니처만 정의. 본문은 Phase 2에서 구현.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from structverify.memory.schema import MemoryEdge, MemoryNode
|
|
31
|
+
from structverify.utils.logger import get_logger
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 노드 파일 이름 컨벤션 ───────────────────────────────────────────────────
|
|
37
|
+
# GraphNodeType → JSONL 파일명 매핑
|
|
38
|
+
_NODE_FILES = {
|
|
39
|
+
"metric": "metrics.jsonl",
|
|
40
|
+
"entity": "entities.jsonl",
|
|
41
|
+
"time": "times.jsonl",
|
|
42
|
+
"source": "sources.jsonl",
|
|
43
|
+
"evidence": "evidences.jsonl",
|
|
44
|
+
"document": "documents.jsonl",
|
|
45
|
+
"claim": "claims.jsonl",
|
|
46
|
+
"sentence": "sentences.jsonl",
|
|
47
|
+
"temporal_expr": "temporals.jsonl",
|
|
48
|
+
"resolved_time": "temporals.jsonl",
|
|
49
|
+
}
|
|
50
|
+
_EDGES_FILE = "edges.jsonl"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── 읽기 ────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def read_all_nodes(nodes_dir: Path) -> list[MemoryNode]:
|
|
56
|
+
"""
|
|
57
|
+
nodes/ 디렉터리의 모든 .jsonl 파일을 읽어 MemoryNode 리스트로 반환.
|
|
58
|
+
|
|
59
|
+
같은 node_id가 여러 번 등장하면 마지막 것 채택 (last-write-wins).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
nodes_dir: Path to backend/structverify/memory/nodes/
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
list[MemoryNode]
|
|
66
|
+
"""
|
|
67
|
+
raise NotImplementedError # Phase 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_all_edges(edges_dir: Path) -> list[MemoryEdge]:
|
|
71
|
+
"""
|
|
72
|
+
edges/edges.jsonl을 읽어 MemoryEdge 리스트로 반환.
|
|
73
|
+
|
|
74
|
+
edge는 dedup 안 함 — 같은 (from, to, type)이라도 다른 doc/run에서 왔으면
|
|
75
|
+
별개 엣지로 보존. (그래야 provenance 추적 가능)
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
edges_dir: Path to backend/structverify/memory/edges/
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
list[MemoryEdge]
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError # Phase 2
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── 쓰기 (append) ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def append_node(node: MemoryNode, nodes_dir: Path) -> None:
|
|
89
|
+
"""
|
|
90
|
+
MemoryNode를 적절한 타입별 JSONL 파일에 append.
|
|
91
|
+
|
|
92
|
+
파일 결정:
|
|
93
|
+
node.node_type.value → _NODE_FILES[...]
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
node: append할 MemoryNode
|
|
97
|
+
nodes_dir: Path to backend/structverify/memory/nodes/
|
|
98
|
+
"""
|
|
99
|
+
raise NotImplementedError # Phase 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def append_edge(edge: MemoryEdge, edges_dir: Path) -> None:
|
|
103
|
+
"""
|
|
104
|
+
MemoryEdge를 edges/edges.jsonl에 append.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
edge: append할 MemoryEdge
|
|
108
|
+
edges_dir: Path to backend/structverify/memory/edges/
|
|
109
|
+
"""
|
|
110
|
+
raise NotImplementedError # Phase 2
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── 유틸 ────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def ensure_dirs(memory_dir: Path) -> None:
|
|
116
|
+
"""nodes/, edges/ 디렉터리가 없으면 생성."""
|
|
117
|
+
raise NotImplementedError # Phase 2
|