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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
graph/graph_store.py — Graph DB 인터페이스 (Neo4j)
|
|
3
|
+
|
|
4
|
+
Claim Graph / Evidence Graph / Temporal Graph 의 노드·엣지를 Neo4j에 저장하고
|
|
5
|
+
서브그래프를 조회한다.
|
|
6
|
+
|
|
7
|
+
[설계 — 라이브러리 배포 전제]
|
|
8
|
+
StructVerify는 라이브러리로 배포되므로 Neo4j는 *완전 옵셔널*이다.
|
|
9
|
+
- 사용자가 default.yaml의 graph.store.enabled=true + 접속정보를 넣어야 동작
|
|
10
|
+
- enabled=false / 섹션 없음 → GraphStore 비활성, 모든 호출 no-op
|
|
11
|
+
- neo4j 패키지 미설치 → 경고 후 비활성 (파이프라인은 그대로 진행)
|
|
12
|
+
- 연결/쿼리 실패 → 경고 후 해당 호출만 skip (검증 파이프라인 중단 없음)
|
|
13
|
+
|
|
14
|
+
default.yaml 예시:
|
|
15
|
+
graph:
|
|
16
|
+
store:
|
|
17
|
+
enabled: true
|
|
18
|
+
uri: bolt://localhost:7687
|
|
19
|
+
user: neo4j
|
|
20
|
+
password: ${NEO4J_PASSWORD}
|
|
21
|
+
database: neo4j # 선택, 기본 neo4j
|
|
22
|
+
|
|
23
|
+
[참고] GraphRAG (arXiv 2501.00309)
|
|
24
|
+
Graph 기반 Evidence 검색 — subgraph 조회를 통한 다중 hop 추론 지원
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from structverify.core.schemas import GraphNode, GraphEdge
|
|
29
|
+
from structverify.utils.logger import get_logger
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _safe_label(raw: str) -> str:
|
|
35
|
+
"""Cypher 라벨/관계타입에 안전한 문자열로 정제.
|
|
36
|
+
|
|
37
|
+
영숫자와 밑줄만 허용 (Cypher injection 방지). node_type/edge_type은
|
|
38
|
+
Enum value라 보통 안전하지만 방어적으로 정제한다.
|
|
39
|
+
"""
|
|
40
|
+
cleaned = "".join(
|
|
41
|
+
ch if (ch.isalnum() or ch == "_") else "_" for ch in str(raw)
|
|
42
|
+
)
|
|
43
|
+
if not cleaned or cleaned[0].isdigit():
|
|
44
|
+
cleaned = "N_" + cleaned
|
|
45
|
+
return cleaned
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _to_primitive(value):
|
|
49
|
+
"""Neo4j property로 쓸 수 있게 값을 정규화.
|
|
50
|
+
|
|
51
|
+
Neo4j는 중첩 dict/list를 property로 못 받으므로, 복합 타입은
|
|
52
|
+
문자열로 직렬화한다. None/숫자/bool/str은 그대로 통과.
|
|
53
|
+
"""
|
|
54
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
55
|
+
return value
|
|
56
|
+
if isinstance(value, (list, tuple)):
|
|
57
|
+
if all(isinstance(v, (str, int, float, bool)) for v in value):
|
|
58
|
+
return list(value)
|
|
59
|
+
return str(list(value))
|
|
60
|
+
return str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _clean_props(props: dict | None) -> dict:
|
|
64
|
+
"""properties dict의 모든 값을 Neo4j 호환 형태로 변환."""
|
|
65
|
+
if not props:
|
|
66
|
+
return {}
|
|
67
|
+
return {k: _to_primitive(v) for k, v in props.items()}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GraphStore:
|
|
71
|
+
"""
|
|
72
|
+
Neo4j 기반 Graph DB 인터페이스.
|
|
73
|
+
|
|
74
|
+
노드/엣지를 Cypher MERGE로 upsert하고, N-hop 서브그래프를 조회한다.
|
|
75
|
+
Neo4j가 없거나 비활성화면 모든 메서드가 안전하게 no-op 한다.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, config: dict | None = None):
|
|
79
|
+
"""
|
|
80
|
+
Args:
|
|
81
|
+
config: default.yaml의 graph.store 섹션
|
|
82
|
+
{enabled, uri, user, password, database}
|
|
83
|
+
"""
|
|
84
|
+
self.config = config or {}
|
|
85
|
+
self.enabled = bool(self.config.get("enabled", False))
|
|
86
|
+
self.database = self.config.get("database") or "neo4j"
|
|
87
|
+
self.driver = None
|
|
88
|
+
self._connect_failed = False
|
|
89
|
+
|
|
90
|
+
if not self.enabled:
|
|
91
|
+
logger.info(
|
|
92
|
+
"[GraphStore] graph.store.enabled=false — Neo4j 저장 비활성화 "
|
|
93
|
+
"(노드/엣지는 메모리에서만 사용)"
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
uri = self.config.get("uri")
|
|
98
|
+
user = self.config.get("user")
|
|
99
|
+
password = self.config.get("password")
|
|
100
|
+
if not uri:
|
|
101
|
+
logger.warning(
|
|
102
|
+
"[GraphStore] enabled=true 이지만 graph.store.uri 미설정 "
|
|
103
|
+
"— Neo4j 저장 skip"
|
|
104
|
+
)
|
|
105
|
+
self.enabled = False
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# neo4j 패키지는 옵셔널 의존성 — 미설치 시 graceful degrade
|
|
109
|
+
try:
|
|
110
|
+
from neo4j import AsyncGraphDatabase
|
|
111
|
+
except ImportError:
|
|
112
|
+
logger.warning(
|
|
113
|
+
"[GraphStore] 'neo4j' 패키지 미설치 — Neo4j 저장 skip. "
|
|
114
|
+
"사용하려면: pip install neo4j"
|
|
115
|
+
)
|
|
116
|
+
self.enabled = False
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
auth = (user, password) if user else None
|
|
121
|
+
self.driver = AsyncGraphDatabase.driver(uri, auth=auth)
|
|
122
|
+
logger.info(
|
|
123
|
+
f"[GraphStore] Neo4j driver 초기화: {uri} "
|
|
124
|
+
f"(database={self.database})"
|
|
125
|
+
)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(
|
|
128
|
+
f"[GraphStore] Neo4j driver 초기화 실패 — 저장 skip: {e}"
|
|
129
|
+
)
|
|
130
|
+
self.enabled = False
|
|
131
|
+
self.driver = None
|
|
132
|
+
|
|
133
|
+
def is_active(self) -> bool:
|
|
134
|
+
"""저장이 실제로 가능한 상태인지."""
|
|
135
|
+
return bool(
|
|
136
|
+
self.enabled and self.driver is not None
|
|
137
|
+
and not self._connect_failed
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def save_nodes(self, nodes: list[GraphNode]) -> int:
|
|
141
|
+
"""
|
|
142
|
+
노드 리스트를 Neo4j에 upsert한다 (Cypher MERGE).
|
|
143
|
+
|
|
144
|
+
node_type을 라벨로, node_id를 고유 키로 사용.
|
|
145
|
+
Returns: 저장된 노드 수 (비활성/실패 시 0).
|
|
146
|
+
"""
|
|
147
|
+
if not self.is_active() or not nodes:
|
|
148
|
+
return 0
|
|
149
|
+
# 라벨(node_type)별로 묶어서 UNWIND 일괄 처리
|
|
150
|
+
by_label: dict[str, list[dict]] = {}
|
|
151
|
+
for n in nodes:
|
|
152
|
+
label = (
|
|
153
|
+
n.node_type.value if hasattr(n.node_type, "value")
|
|
154
|
+
else str(n.node_type)
|
|
155
|
+
)
|
|
156
|
+
label = _safe_label(label)
|
|
157
|
+
by_label.setdefault(label, []).append({
|
|
158
|
+
"node_id": n.node_id,
|
|
159
|
+
"label": n.label,
|
|
160
|
+
"domain": getattr(n, "domain", None),
|
|
161
|
+
"props": _clean_props(getattr(n, "properties", None)),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
saved = 0
|
|
165
|
+
try:
|
|
166
|
+
async with self.driver.session(database=self.database) as session:
|
|
167
|
+
for label, rows in by_label.items():
|
|
168
|
+
query = (
|
|
169
|
+
f"UNWIND $rows AS row "
|
|
170
|
+
f"MERGE (n:{label} {{node_id: row.node_id}}) "
|
|
171
|
+
f"SET n.label = row.label, "
|
|
172
|
+
f" n.domain = row.domain, "
|
|
173
|
+
f" n += row.props"
|
|
174
|
+
)
|
|
175
|
+
await session.run(query, rows=rows)
|
|
176
|
+
saved += len(rows)
|
|
177
|
+
logger.info(
|
|
178
|
+
f"[GraphStore] 노드 {saved}건 저장 (labels={list(by_label)})"
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(f"[GraphStore] 노드 저장 실패 — skip: {e}")
|
|
182
|
+
self._connect_failed = True
|
|
183
|
+
return 0
|
|
184
|
+
return saved
|
|
185
|
+
|
|
186
|
+
async def save_edges(self, edges: list[GraphEdge]) -> int:
|
|
187
|
+
"""
|
|
188
|
+
엣지 리스트를 Neo4j에 upsert한다 (Cypher MATCH + MERGE).
|
|
189
|
+
|
|
190
|
+
from_node/to_node가 이미 저장된 노드를 가리킨다고 가정.
|
|
191
|
+
Returns: 저장된 엣지 수 (비활성/실패 시 0).
|
|
192
|
+
"""
|
|
193
|
+
if not self.is_active() or not edges:
|
|
194
|
+
return 0
|
|
195
|
+
by_type: dict[str, list[dict]] = {}
|
|
196
|
+
for e in edges:
|
|
197
|
+
etype = (
|
|
198
|
+
e.edge_type.value if hasattr(e.edge_type, "value")
|
|
199
|
+
else str(e.edge_type)
|
|
200
|
+
)
|
|
201
|
+
etype = _safe_label(etype).upper()
|
|
202
|
+
by_type.setdefault(etype, []).append({
|
|
203
|
+
"from": e.from_node,
|
|
204
|
+
"to": e.to_node,
|
|
205
|
+
"weight": getattr(e, "weight", 1.0),
|
|
206
|
+
"props": _clean_props(getattr(e, "properties", None)),
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
saved = 0
|
|
210
|
+
try:
|
|
211
|
+
async with self.driver.session(database=self.database) as session:
|
|
212
|
+
for etype, rows in by_type.items():
|
|
213
|
+
# 노드는 라벨 무관하게 node_id로 매칭
|
|
214
|
+
query = (
|
|
215
|
+
f"UNWIND $rows AS row "
|
|
216
|
+
f"MATCH (a {{node_id: row.from}}) "
|
|
217
|
+
f"MATCH (b {{node_id: row.to}}) "
|
|
218
|
+
f"MERGE (a)-[r:{etype}]->(b) "
|
|
219
|
+
f"SET r.weight = row.weight, r += row.props"
|
|
220
|
+
)
|
|
221
|
+
await session.run(query, rows=rows)
|
|
222
|
+
saved += len(rows)
|
|
223
|
+
logger.info(
|
|
224
|
+
f"[GraphStore] 엣지 {saved}건 저장 (types={list(by_type)})"
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.warning(f"[GraphStore] 엣지 저장 실패 — skip: {e}")
|
|
228
|
+
self._connect_failed = True
|
|
229
|
+
return 0
|
|
230
|
+
return saved
|
|
231
|
+
|
|
232
|
+
async def save_graph(
|
|
233
|
+
self, nodes: list[GraphNode], edges: list[GraphEdge],
|
|
234
|
+
) -> tuple[int, int]:
|
|
235
|
+
"""노드 먼저, 엣지 나중에 저장 (엣지가 노드를 참조하므로 순서 중요)."""
|
|
236
|
+
n = await self.save_nodes(nodes)
|
|
237
|
+
e = await self.save_edges(edges)
|
|
238
|
+
return n, e
|
|
239
|
+
|
|
240
|
+
async def get_subgraph(self, anchor_id: str, hops: int = 2) -> dict:
|
|
241
|
+
"""
|
|
242
|
+
특정 앵커 노드에서 N-hop 서브그래프를 조회한다.
|
|
243
|
+
|
|
244
|
+
Returns: {"nodes": [...], "edges": [...]} — 비활성/실패 시 빈 결과.
|
|
245
|
+
"""
|
|
246
|
+
if not self.is_active():
|
|
247
|
+
return {"nodes": [], "edges": []}
|
|
248
|
+
hops = max(1, min(int(hops), 5)) # 폭주 방지
|
|
249
|
+
try:
|
|
250
|
+
async with self.driver.session(database=self.database) as session:
|
|
251
|
+
query = (
|
|
252
|
+
f"MATCH path = (n {{node_id: $anchor}})-[*1..{hops}]-(m) "
|
|
253
|
+
f"WITH nodes(path) AS ns, relationships(path) AS rs "
|
|
254
|
+
f"UNWIND ns AS nd "
|
|
255
|
+
f"WITH collect(DISTINCT nd) AS nodes, rs "
|
|
256
|
+
f"UNWIND rs AS rel "
|
|
257
|
+
f"RETURN nodes, collect(DISTINCT rel) AS rels"
|
|
258
|
+
)
|
|
259
|
+
result = await session.run(query, anchor=anchor_id)
|
|
260
|
+
rec = await result.single()
|
|
261
|
+
if not rec:
|
|
262
|
+
return {"nodes": [], "edges": []}
|
|
263
|
+
nodes = [dict(nd) for nd in rec["nodes"]]
|
|
264
|
+
edges = [
|
|
265
|
+
{"type": rel.type, **dict(rel)} for rel in rec["rels"]
|
|
266
|
+
]
|
|
267
|
+
return {"nodes": nodes, "edges": edges}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.warning(f"[GraphStore] 서브그래프 조회 실패: {e}")
|
|
270
|
+
return {"nodes": [], "edges": []}
|
|
271
|
+
|
|
272
|
+
async def close(self):
|
|
273
|
+
"""드라이버 종료."""
|
|
274
|
+
if self.driver is not None:
|
|
275
|
+
try:
|
|
276
|
+
await self.driver.close()
|
|
277
|
+
logger.info("[GraphStore] Neo4j driver 종료")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.debug(f"[GraphStore] driver 종료 중 무시된 예외: {e}")
|
|
280
|
+
finally:
|
|
281
|
+
self.driver = None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
graph/provenance.py — Provenance (출처 이력) 추적
|
|
3
|
+
|
|
4
|
+
검증 결과가 어떤 커넥터 → 어떤 통계표 → 어떤 수치를 근거로 도출되었는지
|
|
5
|
+
전체 경로를 기록하고 렌더링한다.
|
|
6
|
+
|
|
7
|
+
[참고] Fact Verification on KG via Programmatic Reasoning (EMNLP Findings 2025)
|
|
8
|
+
KG 위에서 검증 경로를 프로그래밍적으로 추적하는 방법론.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from structverify.core.schemas import (
|
|
12
|
+
ProvenanceRecord, GraphNode, GraphEdge, GraphNodeType, GraphEdgeType)
|
|
13
|
+
from structverify.utils.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_provenance_subgraph(
|
|
19
|
+
provenance: ProvenanceRecord,
|
|
20
|
+
claim_node_id: str,
|
|
21
|
+
evidence_node_id: str,
|
|
22
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
23
|
+
"""
|
|
24
|
+
Provenance 정보를 그래프 노드/엣지로 변환하여 출처 경로를 기록한다.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
tuple[nodes, edges]: 출처 그래프 구성 요소
|
|
28
|
+
"""
|
|
29
|
+
source_node = GraphNode(
|
|
30
|
+
node_id=f"source:{provenance.provenance_id}",
|
|
31
|
+
node_type=GraphNodeType.SOURCE,
|
|
32
|
+
label=provenance.source_connector,
|
|
33
|
+
properties={"source_id": provenance.source_id,
|
|
34
|
+
"query_used": provenance.query_used},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
edges = [
|
|
38
|
+
GraphEdge(from_node=evidence_node_id, to_node=source_node.node_id,
|
|
39
|
+
edge_type=GraphEdgeType.SOURCED_FROM),
|
|
40
|
+
GraphEdge(from_node=claim_node_id, to_node=evidence_node_id,
|
|
41
|
+
edge_type=GraphEdgeType.VERIFIED_BY),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return [source_node], edges
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def render_provenance_text(provenance: ProvenanceRecord) -> str:
|
|
48
|
+
"""Provenance를 사람이 읽을 수 있는 텍스트로 렌더링한다."""
|
|
49
|
+
return (f"출처: {provenance.source_connector} | "
|
|
50
|
+
f"통계표 ID: {provenance.source_id} | "
|
|
51
|
+
f"검색어: {provenance.query_used} | "
|
|
52
|
+
f"조회 시각: {provenance.fetched_at.isoformat()}")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - AgentMemory 모듈 패키지 진입점 (초안)
|
|
3
|
+
# - 그래프 누적 KG 방향 — Phase 2로 보류
|
|
4
|
+
#
|
|
5
|
+
# [이수민 - 2026-05-14 — 방향 재정렬]
|
|
6
|
+
# - 작업 목표가 "정확도 개선용 사례 메모리(exemplar retrieval)"로 재조정됨
|
|
7
|
+
# 목적:
|
|
8
|
+
# · 도메인 분류 정확도 ↑ (Step 3)
|
|
9
|
+
# · 시간 해석 정확도 ↑ ("작년", "올해" 등의 해석, Step 4.5)
|
|
10
|
+
# - 신규 노출: ExemplarStore, DomainExample, TemporalExample, get_embedding
|
|
11
|
+
# - 기존 AgentMemory 골격(agent_memory.py 등)은 보존하지만 import 안 함
|
|
12
|
+
# (Phase 2에서 그래프 누적 KG 합칠 때 다시 활용 예정)
|
|
13
|
+
"""
|
|
14
|
+
structverify.memory — 정확도 개선용 사례 메모리 + (보류) 그래프 누적 KG
|
|
15
|
+
|
|
16
|
+
[현재 활성] exemplar_store.py / embedder.py
|
|
17
|
+
[Phase 2] agent_memory.py / normalizer.py / storage/ (그래프 누적)
|
|
18
|
+
"""
|
|
19
|
+
from structverify.memory.working_memory import (
|
|
20
|
+
DocumentWorkingMemory,
|
|
21
|
+
StatIdUsage,
|
|
22
|
+
)
|
|
23
|
+
from structverify.memory.exemplar_store import (
|
|
24
|
+
ExemplarStore,
|
|
25
|
+
DomainExample,
|
|
26
|
+
TemporalExample,
|
|
27
|
+
format_domain_hint,
|
|
28
|
+
format_temporal_hint,
|
|
29
|
+
)
|
|
30
|
+
from structverify.memory.embedder import get_embedding, EMBEDDING_DIM
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# ★ 현재 활성: Document-scoped Working Memory
|
|
34
|
+
"DocumentWorkingMemory",
|
|
35
|
+
"StatIdUsage",
|
|
36
|
+
# Phase 2 보류: 영속 사례 retrieval
|
|
37
|
+
"ExemplarStore",
|
|
38
|
+
"DomainExample",
|
|
39
|
+
"TemporalExample",
|
|
40
|
+
"format_domain_hint",
|
|
41
|
+
"format_temporal_hint",
|
|
42
|
+
"get_embedding",
|
|
43
|
+
"EMBEDDING_DIM",
|
|
44
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# [이수민 - 2026-05-13]
|
|
2
|
+
# - 누적 KG 영속화 + dedup의 단일 진입점 (facade)
|
|
3
|
+
# - load() : 파이프라인 시작 시 JSONL 전체 → ClaimGraph 로 복원
|
|
4
|
+
# - merge(): Step 7 종료 후 호출, 신규 노드/엣지를 누적 메모리에 머지
|
|
5
|
+
# · 공유 노드(Metric/Entity/Time/Source/Evidence) dedup
|
|
6
|
+
# · 문서별 노드(Document/Claim/Sentence) append
|
|
7
|
+
# · 같은 (Metric, Time, Entity) 다른 value → CONTRADICTS edge 자동 생성
|
|
8
|
+
# - query_metric() / query_evidence(): 과거 검증 결과 재활용용 조회 API
|
|
9
|
+
# - 메서드 본문은 Phase 2에서 구현 (현재는 시그니처+docstring만)
|
|
10
|
+
"""
|
|
11
|
+
memory/agent_memory.py — AgentMemory facade
|
|
12
|
+
|
|
13
|
+
누적 KG의 load / merge / query를 담당.
|
|
14
|
+
파이프라인 시작 시 load(), Step 7 종료 후 merge() 호출.
|
|
15
|
+
|
|
16
|
+
[Phase 1 — 구조화]
|
|
17
|
+
메서드 시그니처만 정의. 본문은 Phase 2에서 구현.
|
|
18
|
+
|
|
19
|
+
[Phase 2 — 구현 예정]
|
|
20
|
+
- load(): JSONL 전부 읽어 ClaimGraph 인스턴스 구성
|
|
21
|
+
- merge(): 신규 노드/엣지 머지 (dedup + CONTRADICTS 자동 탐지)
|
|
22
|
+
- query_metric(): 특정 Metric에 매달린 과거 Claim 조회
|
|
23
|
+
|
|
24
|
+
[Phase 3 — 통합 예정]
|
|
25
|
+
runtime_agent.py:
|
|
26
|
+
Step 0 memory = AgentMemory(); graph = await memory.load()
|
|
27
|
+
Step 7 이후 await memory.merge(all_nodes, all_edges, doc_id, run_id)
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from structverify.core.schemas import Claim, GraphEdge, GraphNode
|
|
34
|
+
from structverify.graph.claim_graph import ClaimGraph
|
|
35
|
+
from structverify.utils.logger import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AgentMemory:
|
|
41
|
+
"""
|
|
42
|
+
누적 KG의 영속화 + dedup layer.
|
|
43
|
+
|
|
44
|
+
[라이프사이클]
|
|
45
|
+
memory = AgentMemory()
|
|
46
|
+
graph = await memory.load() # Step 0
|
|
47
|
+
...
|
|
48
|
+
await memory.merge(nodes, edges, ...) # Step 7 이후
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
memory_dir: str | Path = "backend/structverify/memory",
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
memory_dir: JSONL 데이터 루트. 하위에 nodes/ edges/ 디렉터리 존재.
|
|
58
|
+
"""
|
|
59
|
+
self.memory_dir = Path(memory_dir)
|
|
60
|
+
self.nodes_dir = self.memory_dir / "nodes"
|
|
61
|
+
self.edges_dir = self.memory_dir / "edges"
|
|
62
|
+
|
|
63
|
+
# ── 1. 로드 ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async def load(self) -> ClaimGraph:
|
|
66
|
+
"""
|
|
67
|
+
JSONL 전체 → ClaimGraph 인스턴스.
|
|
68
|
+
|
|
69
|
+
파이프라인 시작 시 호출. 누적된 과거 KG를 인-메모리로 올림.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
ClaimGraph: 모든 과거 노드/엣지가 포함된 facade
|
|
73
|
+
"""
|
|
74
|
+
raise NotImplementedError # Phase 2
|
|
75
|
+
|
|
76
|
+
# ── 2. 머지 (Step 7 이후) ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
async def merge(
|
|
79
|
+
self,
|
|
80
|
+
new_nodes: list[GraphNode],
|
|
81
|
+
new_edges: list[GraphEdge],
|
|
82
|
+
doc_id: str,
|
|
83
|
+
run_id: str,
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
신규 그래프를 누적 메모리에 머지.
|
|
87
|
+
|
|
88
|
+
흐름:
|
|
89
|
+
① 공유 노드(Metric/Entity/Time/Source/Evidence) dedup by canonical_id
|
|
90
|
+
② 문서별 노드(Document/Claim/Sentence) append
|
|
91
|
+
③ 같은 (Metric, Time, Entity)에 다른 value 발견 → CONTRADICTS edge
|
|
92
|
+
④ MemoryNode/MemoryEdge로 wrap (provenance 부착)
|
|
93
|
+
⑤ JSONL append
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
new_nodes: 이번 doc의 신규 노드
|
|
97
|
+
new_edges: 이번 doc의 신규 엣지
|
|
98
|
+
doc_id: 이번 doc의 식별자
|
|
99
|
+
run_id: 이번 파이프라인 실행 식별자
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
dict: {
|
|
103
|
+
"added_nodes": int,
|
|
104
|
+
"deduped_nodes": int,
|
|
105
|
+
"added_edges": int,
|
|
106
|
+
"contradicts_found": int,
|
|
107
|
+
}
|
|
108
|
+
"""
|
|
109
|
+
raise NotImplementedError # Phase 2
|
|
110
|
+
|
|
111
|
+
# ── 3. 쿼리 ──────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async def query_metric(self, canonical_id: str) -> list[Claim]:
|
|
114
|
+
"""
|
|
115
|
+
특정 Metric에 매달린 모든 과거 Claim 조회.
|
|
116
|
+
|
|
117
|
+
verifier가 새 claim 검증 전, 같은 indicator의 과거 verified claim이
|
|
118
|
+
있는지 확인할 때 사용. cache hit이면 KOSIS 재조회 생략 가능.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
canonical_id: "metric:DT_1ES4001" 형식
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
list[Claim]: 그 metric에 매달린 모든 Claim (verified 우선)
|
|
125
|
+
"""
|
|
126
|
+
raise NotImplementedError # Phase 2
|
|
127
|
+
|
|
128
|
+
async def query_evidence(self, stat_id: str, time_period: str | None = None):
|
|
129
|
+
"""
|
|
130
|
+
특정 stat_id + 시점의 Evidence 캐시 조회.
|
|
131
|
+
|
|
132
|
+
같은 KOSIS row를 다른 doc이 이미 조회했다면 그 결과 재활용.
|
|
133
|
+
REUSED_BY 엣지 생성용.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
stat_id: KOSIS 통계표 ID (e.g. "DT_1ES4001")
|
|
137
|
+
time_period: 시점 (None이면 stat_id의 모든 시점)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Evidence | None
|
|
141
|
+
"""
|
|
142
|
+
raise NotImplementedError # Phase 2
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# [이수민 - 2026-05-14]
|
|
2
|
+
# - HCX 임베딩 API 호출 헬퍼 (사례 저장/검색 공용)
|
|
3
|
+
# - retrieval/catalog_search.py:_get_embedding 로직 재활용
|
|
4
|
+
# - exemplar_store가 직접 HTTP 호출하지 않고 이 모듈을 통하도록 일원화
|
|
5
|
+
# - KOSIS catalog와 같은 임베딩 모델 → 같은 벡터 공간에서 검색 일관성 확보
|
|
6
|
+
# - 구조화 단계 — 시그니처와 인터페이스만. 본문은 다음 단계에서 구현.
|
|
7
|
+
"""
|
|
8
|
+
memory/embedder.py — 사례용 텍스트 임베딩 헬퍼
|
|
9
|
+
|
|
10
|
+
[설계 의도]
|
|
11
|
+
- 기존 KOSIS catalog 검색이 쓰는 HCX 임베딩(v2)을 재활용
|
|
12
|
+
- 같은 임베딩 모델이어야 catalog와 같은 의미 공간에서 검색 가능
|
|
13
|
+
- 벡터 차원: HCX-emb v2 = 1024차원
|
|
14
|
+
|
|
15
|
+
[참조]
|
|
16
|
+
- 원본 구현: structverify/retrieval/catalog_search.py:_get_embedding (line 230~)
|
|
17
|
+
- 엔드포인트: https://clovastudio.stream.ntruss.com/v1/api-tools/embedding/v2
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from structverify.utils.logger import get_logger
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
EMBEDDING_DIM = 1024 # HCX-emb v2 차원
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def get_embedding(text: str, api_key: str | None = None) -> list[float] | None:
|
|
30
|
+
"""
|
|
31
|
+
텍스트 → HCX 임베딩 벡터 (1024-dim).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
text: 임베딩 대상 텍스트 (도메인 분류용 본문 일부 또는 시간 표현+문맥)
|
|
35
|
+
api_key: CLOVASTUDIO_API_KEY (None이면 환경변수에서 읽음)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
list[float] (길이 1024) 또는 None (API 실패 시)
|
|
39
|
+
"""
|
|
40
|
+
raise NotImplementedError # 다음 단계
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def get_embeddings_batch(
|
|
44
|
+
texts: list[str], api_key: str | None = None
|
|
45
|
+
) -> list[list[float] | None]:
|
|
46
|
+
"""
|
|
47
|
+
여러 텍스트 한 번에 임베딩 (배치).
|
|
48
|
+
|
|
49
|
+
초기 사례 일괄 적재 시 사용. 단건은 get_embedding() 권장.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
texts: 임베딩 대상 텍스트 리스트
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
list[list[float] | None] — 입력 순서 보존, 실패는 None
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── 보조 ────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def vector_to_pgvector_str(vec: list[float]) -> str:
|
|
63
|
+
"""
|
|
64
|
+
list[float] → pgvector INSERT용 문자열 "[1.0,2.0,...]".
|
|
65
|
+
|
|
66
|
+
pgvector는 vector 타입을 문자열 리터럴로 받음.
|
|
67
|
+
catalog_search.py:_search_pgvector의 변환 로직과 동일.
|
|
68
|
+
"""
|
|
69
|
+
raise NotImplementedError
|