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