memory-kg 0.5.2__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.
- memory_kg/__init__.py +38 -0
- memory_kg/__main__.py +6 -0
- memory_kg/app.py +433 -0
- memory_kg/chunker.py +630 -0
- memory_kg/cli/__init__.py +0 -0
- memory_kg/cli/cmd_analyze.py +69 -0
- memory_kg/cli/cmd_build.py +493 -0
- memory_kg/cli/cmd_hooks.py +364 -0
- memory_kg/cli/cmd_mcp.py +69 -0
- memory_kg/cli/cmd_model.py +54 -0
- memory_kg/cli/cmd_pipeline.py +352 -0
- memory_kg/cli/cmd_query.py +187 -0
- memory_kg/cli/cmd_semantic_analyze.py +69 -0
- memory_kg/cli/cmd_snapshot.py +393 -0
- memory_kg/cli/cmd_viz.py +76 -0
- memory_kg/cli/group.py +22 -0
- memory_kg/cli/main.py +39 -0
- memory_kg/cli/options.py +50 -0
- memory_kg/config.py +44 -0
- memory_kg/doc_kg.code-workspace +8 -0
- memory_kg/embedder_worker.py +273 -0
- memory_kg/entry_chunk.py +117 -0
- memory_kg/graph.py +171 -0
- memory_kg/index.py +671 -0
- memory_kg/kg.py +760 -0
- memory_kg/manifold.py +284 -0
- memory_kg/mcp_server.py +170 -0
- memory_kg/memorykg.py +645 -0
- memory_kg/memorykg_semantic_analysis.py +841 -0
- memory_kg/memorykg_thorough_analysis.py +416 -0
- memory_kg/pipeline.py +470 -0
- memory_kg/relations.py +128 -0
- memory_kg/sampler.py +340 -0
- memory_kg/semantic_builder.py +261 -0
- memory_kg/semantic_extractor.py +246 -0
- memory_kg/semantic_primitives.py +104 -0
- memory_kg/snapshots.py +483 -0
- memory_kg/store.py +454 -0
- memory_kg/topics.py +335 -0
- memory_kg-0.5.2.dist-info/METADATA +304 -0
- memory_kg-0.5.2.dist-info/RECORD +44 -0
- memory_kg-0.5.2.dist-info/WHEEL +4 -0
- memory_kg-0.5.2.dist-info/entry_points.txt +13 -0
- memory_kg-0.5.2.dist-info/licenses/LICENSE +94 -0
memory_kg/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""MemoryKG — hybrid semantic + structural knowledge graph for document corpora."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.5.2"
|
|
4
|
+
|
|
5
|
+
from memory_kg.kg import MemoryKG
|
|
6
|
+
from memory_kg.semantic_builder import SemanticBuildStats, SemanticMemoryBuilder
|
|
7
|
+
from memory_kg.semantic_extractor import (
|
|
8
|
+
AssertionCandidate,
|
|
9
|
+
AssertionExtractor,
|
|
10
|
+
EventCandidate,
|
|
11
|
+
EventExtractor,
|
|
12
|
+
)
|
|
13
|
+
from memory_kg.semantic_primitives import (
|
|
14
|
+
ASSERTION_SCHEMA,
|
|
15
|
+
EVENT_SCHEMA,
|
|
16
|
+
SEMANTIC_EDGE_RELS,
|
|
17
|
+
SEMANTIC_NODE_KINDS,
|
|
18
|
+
assertion_node_id,
|
|
19
|
+
event_node_id,
|
|
20
|
+
slugify,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ASSERTION_SCHEMA",
|
|
25
|
+
"EVENT_SCHEMA",
|
|
26
|
+
"SEMANTIC_EDGE_RELS",
|
|
27
|
+
"SEMANTIC_NODE_KINDS",
|
|
28
|
+
"AssertionCandidate",
|
|
29
|
+
"AssertionExtractor",
|
|
30
|
+
"EventCandidate",
|
|
31
|
+
"EventExtractor",
|
|
32
|
+
"MemoryKG",
|
|
33
|
+
"SemanticBuildStats",
|
|
34
|
+
"SemanticMemoryBuilder",
|
|
35
|
+
"assertion_node_id",
|
|
36
|
+
"event_node_id",
|
|
37
|
+
"slugify",
|
|
38
|
+
]
|
memory_kg/__main__.py
ADDED
memory_kg/app.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
app.py - MemoryKG Streamlit Visualizer.
|
|
4
|
+
|
|
5
|
+
Adapted from CodeKG's app structure for document-oriented nodes and edges.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# pylint: disable=import-error
|
|
17
|
+
import streamlit as st # type: ignore[import-not-found]
|
|
18
|
+
from pyvis.network import Network # type: ignore[import-not-found]
|
|
19
|
+
|
|
20
|
+
from memory_kg.kg import MemoryKG
|
|
21
|
+
from memory_kg.store import DEFAULT_RELS, GraphStore
|
|
22
|
+
|
|
23
|
+
_KIND_COLOR: dict[str, str] = {
|
|
24
|
+
"document": "#2E6BAE",
|
|
25
|
+
"section": "#B8742B",
|
|
26
|
+
"chunk": "#2F8F5B",
|
|
27
|
+
"topic": "#8D6E63",
|
|
28
|
+
"entity": "#CC5A2E",
|
|
29
|
+
"keyword": "#4E7A5E",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_KIND_SHAPE: dict[str, str] = {
|
|
33
|
+
"document": "box",
|
|
34
|
+
"section": "diamond",
|
|
35
|
+
"chunk": "ellipse",
|
|
36
|
+
"topic": "dot",
|
|
37
|
+
"entity": "triangle",
|
|
38
|
+
"keyword": "star",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_REL_COLOR: dict[str, str] = {
|
|
42
|
+
"CONTAINS": "#A8B3BE",
|
|
43
|
+
"NEXT": "#6FA8DC",
|
|
44
|
+
"REFERENCES": "#D97B5B",
|
|
45
|
+
"SIMILAR_TO": "#6A8D73",
|
|
46
|
+
"HAS_TOPIC": "#8D6E63",
|
|
47
|
+
"MENTIONS_ENTITY": "#CC5A2E",
|
|
48
|
+
"HAS_KEYWORD": "#4E7A5E",
|
|
49
|
+
"CO_OCCURS_WITH": "#9E9E9E",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_DEFAULT_DB = os.environ.get("DOCKG_DB", ".memorykg/graph.sqlite")
|
|
53
|
+
_DEFAULT_LANCEDB = os.environ.get("DOCKG_LANCEDB", ".memorykg/lancedb")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
st.set_page_config(
|
|
57
|
+
page_title="MemoryKG Explorer",
|
|
58
|
+
page_icon="\N{SPIDER WEB}",
|
|
59
|
+
layout="wide",
|
|
60
|
+
initial_sidebar_state="expanded",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_cli_db_arg() -> str:
|
|
65
|
+
"""Parse ``--db`` from Streamlit CLI args, ignoring unknown flags."""
|
|
66
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
67
|
+
parser.add_argument("--db", default=None)
|
|
68
|
+
args, _ = parser.parse_known_args()
|
|
69
|
+
return args.db
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _init_state() -> None:
|
|
73
|
+
"""Initialise Streamlit session-state keys with default values on first run."""
|
|
74
|
+
defaults = {
|
|
75
|
+
"db_path": _parse_cli_db_arg() or _DEFAULT_DB,
|
|
76
|
+
"store": None,
|
|
77
|
+
"store_loaded_path": None,
|
|
78
|
+
"query_result": None,
|
|
79
|
+
"pack_result": None,
|
|
80
|
+
}
|
|
81
|
+
for k, v in defaults.items():
|
|
82
|
+
if k not in st.session_state:
|
|
83
|
+
st.session_state[k] = v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@st.cache_resource(show_spinner="Opening SQLite store...")
|
|
87
|
+
def _load_store(db_path: str) -> GraphStore | None:
|
|
88
|
+
"""Open a :class:`~memory_kg.store.GraphStore` at *db_path*, or return ``None`` if absent."""
|
|
89
|
+
p = Path(db_path)
|
|
90
|
+
if not p.exists():
|
|
91
|
+
return None
|
|
92
|
+
return GraphStore(db_path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@st.cache_resource(show_spinner="Loading MemoryKG...")
|
|
96
|
+
def _load_kg(corpus_root: str, db_path: str, lancedb_dir: str, model: str) -> MemoryKG:
|
|
97
|
+
"""Create a cached :class:`~memory_kg.kg.MemoryKG` instance for the given paths and model."""
|
|
98
|
+
return MemoryKG(
|
|
99
|
+
corpus_root=corpus_root,
|
|
100
|
+
db_path=db_path,
|
|
101
|
+
lancedb_dir=lancedb_dir,
|
|
102
|
+
model=model,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_store() -> GraphStore | None:
|
|
107
|
+
"""Return the active :class:`~memory_kg.store.GraphStore`, reloading if the path changed."""
|
|
108
|
+
db = st.session_state.db_path
|
|
109
|
+
if st.session_state.store_loaded_path != db:
|
|
110
|
+
st.session_state.store = _load_store(db)
|
|
111
|
+
st.session_state.store_loaded_path = db
|
|
112
|
+
return st.session_state.store
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_pyvis(
|
|
116
|
+
nodes: list[dict],
|
|
117
|
+
edges: list[dict],
|
|
118
|
+
*,
|
|
119
|
+
height: str = "620px",
|
|
120
|
+
seed_ids: set[str] | None = None,
|
|
121
|
+
physics: bool = True,
|
|
122
|
+
) -> str:
|
|
123
|
+
"""Build a PyVis interactive graph and return its HTML string.
|
|
124
|
+
|
|
125
|
+
:param nodes: Node dicts from the store.
|
|
126
|
+
:param edges: Edge dicts from the store.
|
|
127
|
+
:param height: Canvas height CSS string (default ``"620px"``).
|
|
128
|
+
:param seed_ids: Node IDs highlighted as query seeds (gold border).
|
|
129
|
+
:param physics: Enable Barnes-Hut physics simulation.
|
|
130
|
+
:return: Self-contained HTML string.
|
|
131
|
+
"""
|
|
132
|
+
net = Network(
|
|
133
|
+
height=height,
|
|
134
|
+
width="100%",
|
|
135
|
+
bgcolor="#0e1117",
|
|
136
|
+
font_color="#e0e0e0",
|
|
137
|
+
directed=True,
|
|
138
|
+
notebook=False,
|
|
139
|
+
)
|
|
140
|
+
net.set_options(
|
|
141
|
+
json.dumps(
|
|
142
|
+
{
|
|
143
|
+
"physics": {
|
|
144
|
+
"enabled": physics,
|
|
145
|
+
"barnesHut": {
|
|
146
|
+
"gravitationalConstant": -8000,
|
|
147
|
+
"centralGravity": 0.25,
|
|
148
|
+
"springLength": 130,
|
|
149
|
+
"springConstant": 0.04,
|
|
150
|
+
"damping": 0.09,
|
|
151
|
+
},
|
|
152
|
+
"stabilization": {"iterations": 150},
|
|
153
|
+
},
|
|
154
|
+
"edges": {
|
|
155
|
+
"smooth": {"type": "dynamic"},
|
|
156
|
+
"arrows": {"to": {"enabled": True, "scaleFactor": 0.6}},
|
|
157
|
+
"font": {"size": 10, "color": "#aaaaaa"},
|
|
158
|
+
},
|
|
159
|
+
"interaction": {
|
|
160
|
+
"hover": True,
|
|
161
|
+
"tooltipDelay": 80,
|
|
162
|
+
"navigationButtons": True,
|
|
163
|
+
"keyboard": True,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
seeds = seed_ids or set()
|
|
170
|
+
|
|
171
|
+
for n in nodes:
|
|
172
|
+
kind = n.get("kind", "chunk")
|
|
173
|
+
color = _KIND_COLOR.get(kind, "#95A5A6")
|
|
174
|
+
shape = _KIND_SHAPE.get(kind, "dot")
|
|
175
|
+
label = n.get("title") or n.get("name") or n["id"]
|
|
176
|
+
if len(label) > 32:
|
|
177
|
+
label = label[:29] + "..."
|
|
178
|
+
|
|
179
|
+
title = (
|
|
180
|
+
f"<b>{kind}</b><br>"
|
|
181
|
+
f"id: {n.get('id', '')}<br>"
|
|
182
|
+
f"file: {n.get('file_path') or '-'}<br>"
|
|
183
|
+
f"name/title: {n.get('title') or n.get('name') or '-'}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
border_color = "#FFD700" if n["id"] in seeds else color
|
|
187
|
+
|
|
188
|
+
net.add_node(
|
|
189
|
+
n["id"],
|
|
190
|
+
label=label,
|
|
191
|
+
title=title,
|
|
192
|
+
color={
|
|
193
|
+
"background": color,
|
|
194
|
+
"border": border_color,
|
|
195
|
+
"highlight": {"background": color, "border": "#FFFFFF"},
|
|
196
|
+
},
|
|
197
|
+
shape=shape,
|
|
198
|
+
size=18 if kind in ("document", "section") else 12,
|
|
199
|
+
borderWidth=3 if n["id"] in seeds else 1,
|
|
200
|
+
font={"size": 11},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for e in edges:
|
|
204
|
+
rel = e.get("rel", "")
|
|
205
|
+
ecolor = _REL_COLOR.get(rel, "#888888")
|
|
206
|
+
net.add_edge(
|
|
207
|
+
e["src"],
|
|
208
|
+
e["dst"],
|
|
209
|
+
label=rel,
|
|
210
|
+
color=ecolor,
|
|
211
|
+
width=1.5,
|
|
212
|
+
title=rel,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode="w") as f:
|
|
216
|
+
tmp_path = f.name
|
|
217
|
+
net.save_graph(tmp_path)
|
|
218
|
+
html = Path(tmp_path).read_text(encoding="utf-8")
|
|
219
|
+
os.unlink(tmp_path)
|
|
220
|
+
return html
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _render_sidebar() -> dict:
|
|
224
|
+
"""Render the Streamlit sidebar controls and return the current settings dict."""
|
|
225
|
+
st.sidebar.title("MemoryKG Explorer")
|
|
226
|
+
st.sidebar.markdown("---")
|
|
227
|
+
|
|
228
|
+
db_path = st.sidebar.text_input("SQLite path", value=st.session_state.db_path)
|
|
229
|
+
st.session_state.db_path = db_path
|
|
230
|
+
|
|
231
|
+
store = _get_store()
|
|
232
|
+
if store is None:
|
|
233
|
+
st.sidebar.warning(f"{db_path} not found. Build your graph first.")
|
|
234
|
+
else:
|
|
235
|
+
s = store.stats()
|
|
236
|
+
st.sidebar.success(f"{s['total_nodes']} nodes / {s['total_edges']} edges")
|
|
237
|
+
|
|
238
|
+
st.sidebar.markdown("---")
|
|
239
|
+
corpus_root = st.sidebar.text_input("Corpus root", value=str(Path.cwd()))
|
|
240
|
+
lancedb_dir = st.sidebar.text_input("LanceDB dir", value=_DEFAULT_LANCEDB)
|
|
241
|
+
model = st.sidebar.selectbox(
|
|
242
|
+
"Embedding model",
|
|
243
|
+
[
|
|
244
|
+
"all-MiniLM-L6-v2",
|
|
245
|
+
"all-mpnet-base-v2",
|
|
246
|
+
"paraphrase-MiniLM-L3-v2",
|
|
247
|
+
],
|
|
248
|
+
index=0,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
k = st.sidebar.slider("Top-k seeds", min_value=1, max_value=30, value=8)
|
|
252
|
+
hop = st.sidebar.slider("Graph hops", min_value=0, max_value=4, value=1)
|
|
253
|
+
chosen_rels = st.sidebar.multiselect(
|
|
254
|
+
"Edge relations",
|
|
255
|
+
options=list(DEFAULT_RELS),
|
|
256
|
+
default=list(DEFAULT_RELS),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
st.sidebar.markdown("---")
|
|
260
|
+
max_graph_nodes = st.sidebar.slider("Max graph nodes", 20, 400, 140, step=10)
|
|
261
|
+
physics_on = st.sidebar.checkbox("Physics simulation", value=True)
|
|
262
|
+
graph_height = st.sidebar.select_slider(
|
|
263
|
+
"Graph height",
|
|
264
|
+
options=["400px", "500px", "620px", "750px"],
|
|
265
|
+
value="620px",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"db_path": db_path,
|
|
270
|
+
"store": store,
|
|
271
|
+
"corpus_root": corpus_root,
|
|
272
|
+
"lancedb_dir": lancedb_dir,
|
|
273
|
+
"model": model,
|
|
274
|
+
"k": k,
|
|
275
|
+
"hop": hop,
|
|
276
|
+
"rels": tuple(chosen_rels),
|
|
277
|
+
"max_graph_nodes": max_graph_nodes,
|
|
278
|
+
"physics_on": physics_on,
|
|
279
|
+
"graph_height": graph_height,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _load_all_nodes_edges(store: GraphStore, max_nodes: int) -> tuple[list[dict], list[dict]]:
|
|
284
|
+
"""Load up to *max_nodes* nodes and their internal edges from *store*.
|
|
285
|
+
|
|
286
|
+
:param store: Open :class:`~memory_kg.store.GraphStore`.
|
|
287
|
+
:param max_nodes: Upper bound on nodes returned (ordered by kind, file, char offset).
|
|
288
|
+
:return: ``(nodes, edges)`` tuple of node and edge dicts.
|
|
289
|
+
"""
|
|
290
|
+
rows = store.con.execute(
|
|
291
|
+
"""
|
|
292
|
+
SELECT id, kind, name, title, file_path, char_start, char_end, heading_level, text
|
|
293
|
+
FROM nodes
|
|
294
|
+
ORDER BY kind, file_path, char_start
|
|
295
|
+
LIMIT ?
|
|
296
|
+
""",
|
|
297
|
+
(max_nodes,),
|
|
298
|
+
).fetchall()
|
|
299
|
+
|
|
300
|
+
nodes = [
|
|
301
|
+
{
|
|
302
|
+
"id": r[0],
|
|
303
|
+
"kind": r[1],
|
|
304
|
+
"name": r[2],
|
|
305
|
+
"title": r[3],
|
|
306
|
+
"file_path": r[4],
|
|
307
|
+
"char_start": r[5],
|
|
308
|
+
"char_end": r[6],
|
|
309
|
+
"heading_level": r[7],
|
|
310
|
+
"text": r[8],
|
|
311
|
+
}
|
|
312
|
+
for r in rows
|
|
313
|
+
]
|
|
314
|
+
node_ids = {n["id"] for n in nodes}
|
|
315
|
+
edges = store.edges_within(node_ids)
|
|
316
|
+
return nodes, edges
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def main() -> None:
|
|
320
|
+
"""Streamlit app entry point — initialise state, render sidebar and main view."""
|
|
321
|
+
_init_state()
|
|
322
|
+
cfg = _render_sidebar()
|
|
323
|
+
|
|
324
|
+
st.title("MemoryKG Explorer")
|
|
325
|
+
st.caption("Interactive graph, query, and text-pack inspection for document corpora.")
|
|
326
|
+
|
|
327
|
+
tab_graph, tab_query, tab_pack = st.tabs(["Graph", "Query", "Pack"])
|
|
328
|
+
|
|
329
|
+
with tab_graph:
|
|
330
|
+
if cfg["store"] is None:
|
|
331
|
+
st.info("Set a valid SQLite path to explore the graph.")
|
|
332
|
+
else:
|
|
333
|
+
nodes, edges = _load_all_nodes_edges(cfg["store"], cfg["max_graph_nodes"])
|
|
334
|
+
html = _build_pyvis(
|
|
335
|
+
nodes,
|
|
336
|
+
edges,
|
|
337
|
+
height=cfg["graph_height"],
|
|
338
|
+
physics=cfg["physics_on"],
|
|
339
|
+
)
|
|
340
|
+
st.components.v1.html(html, height=int(cfg["graph_height"].replace("px", "")) + 30)
|
|
341
|
+
st.caption(f"Showing {len(nodes)} nodes and {len(edges)} edges.")
|
|
342
|
+
|
|
343
|
+
with tab_query:
|
|
344
|
+
q = st.text_input("Query", value="knowledge graph architecture")
|
|
345
|
+
if st.button("Run Query"):
|
|
346
|
+
try:
|
|
347
|
+
kg = _load_kg(
|
|
348
|
+
cfg["corpus_root"],
|
|
349
|
+
cfg["db_path"],
|
|
350
|
+
cfg["lancedb_dir"],
|
|
351
|
+
cfg["model"],
|
|
352
|
+
)
|
|
353
|
+
result = kg.query(
|
|
354
|
+
q,
|
|
355
|
+
k=cfg["k"],
|
|
356
|
+
hop=cfg["hop"],
|
|
357
|
+
rels=cfg["rels"],
|
|
358
|
+
max_nodes=cfg["max_graph_nodes"],
|
|
359
|
+
)
|
|
360
|
+
st.session_state.query_result = result
|
|
361
|
+
except (
|
|
362
|
+
AttributeError,
|
|
363
|
+
ValueError,
|
|
364
|
+
RuntimeError,
|
|
365
|
+
OSError,
|
|
366
|
+
) as exc: # pragma: no cover
|
|
367
|
+
st.error(f"Query failed: {exc}")
|
|
368
|
+
|
|
369
|
+
if st.session_state.query_result:
|
|
370
|
+
result = st.session_state.query_result
|
|
371
|
+
st.write(
|
|
372
|
+
f"Seeds: {result.seeds} | Expanded: {result.expanded_nodes}"
|
|
373
|
+
f" | Returned: {result.returned_nodes}"
|
|
374
|
+
)
|
|
375
|
+
st.json(result.to_dict())
|
|
376
|
+
|
|
377
|
+
html = _build_pyvis(
|
|
378
|
+
result.nodes,
|
|
379
|
+
result.edges,
|
|
380
|
+
height=cfg["graph_height"],
|
|
381
|
+
seed_ids={n["id"] for n in result.nodes[: cfg["k"]]},
|
|
382
|
+
physics=cfg["physics_on"],
|
|
383
|
+
)
|
|
384
|
+
st.components.v1.html(html, height=int(cfg["graph_height"].replace("px", "")) + 30)
|
|
385
|
+
|
|
386
|
+
with tab_pack:
|
|
387
|
+
pquery = st.text_input("Pack query", value="MCP setup and usage")
|
|
388
|
+
max_chars = st.slider("Max chars per excerpt", 200, 5000, 1500, step=100)
|
|
389
|
+
|
|
390
|
+
if st.button("Build Pack"):
|
|
391
|
+
try:
|
|
392
|
+
kg = _load_kg(
|
|
393
|
+
cfg["corpus_root"],
|
|
394
|
+
cfg["db_path"],
|
|
395
|
+
cfg["lancedb_dir"],
|
|
396
|
+
cfg["model"],
|
|
397
|
+
)
|
|
398
|
+
pack = kg.pack(
|
|
399
|
+
pquery,
|
|
400
|
+
k=cfg["k"],
|
|
401
|
+
hop=cfg["hop"],
|
|
402
|
+
rels=cfg["rels"],
|
|
403
|
+
max_chars=max_chars,
|
|
404
|
+
max_nodes=20,
|
|
405
|
+
)
|
|
406
|
+
st.session_state.pack_result = pack
|
|
407
|
+
except (
|
|
408
|
+
AttributeError,
|
|
409
|
+
ValueError,
|
|
410
|
+
RuntimeError,
|
|
411
|
+
OSError,
|
|
412
|
+
) as exc: # pragma: no cover
|
|
413
|
+
st.error(f"Pack failed: {exc}")
|
|
414
|
+
|
|
415
|
+
if st.session_state.pack_result:
|
|
416
|
+
pack = st.session_state.pack_result
|
|
417
|
+
st.download_button(
|
|
418
|
+
"Download Markdown",
|
|
419
|
+
data=pack.to_markdown(),
|
|
420
|
+
file_name="memorykg_pack.md",
|
|
421
|
+
mime="text/markdown",
|
|
422
|
+
)
|
|
423
|
+
st.download_button(
|
|
424
|
+
"Download JSON",
|
|
425
|
+
data=pack.to_json(),
|
|
426
|
+
file_name="memorykg_pack.json",
|
|
427
|
+
mime="application/json",
|
|
428
|
+
)
|
|
429
|
+
st.markdown(pack.to_markdown())
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
main()
|