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.
Files changed (44) hide show
  1. memory_kg/__init__.py +38 -0
  2. memory_kg/__main__.py +6 -0
  3. memory_kg/app.py +433 -0
  4. memory_kg/chunker.py +630 -0
  5. memory_kg/cli/__init__.py +0 -0
  6. memory_kg/cli/cmd_analyze.py +69 -0
  7. memory_kg/cli/cmd_build.py +493 -0
  8. memory_kg/cli/cmd_hooks.py +364 -0
  9. memory_kg/cli/cmd_mcp.py +69 -0
  10. memory_kg/cli/cmd_model.py +54 -0
  11. memory_kg/cli/cmd_pipeline.py +352 -0
  12. memory_kg/cli/cmd_query.py +187 -0
  13. memory_kg/cli/cmd_semantic_analyze.py +69 -0
  14. memory_kg/cli/cmd_snapshot.py +393 -0
  15. memory_kg/cli/cmd_viz.py +76 -0
  16. memory_kg/cli/group.py +22 -0
  17. memory_kg/cli/main.py +39 -0
  18. memory_kg/cli/options.py +50 -0
  19. memory_kg/config.py +44 -0
  20. memory_kg/doc_kg.code-workspace +8 -0
  21. memory_kg/embedder_worker.py +273 -0
  22. memory_kg/entry_chunk.py +117 -0
  23. memory_kg/graph.py +171 -0
  24. memory_kg/index.py +671 -0
  25. memory_kg/kg.py +760 -0
  26. memory_kg/manifold.py +284 -0
  27. memory_kg/mcp_server.py +170 -0
  28. memory_kg/memorykg.py +645 -0
  29. memory_kg/memorykg_semantic_analysis.py +841 -0
  30. memory_kg/memorykg_thorough_analysis.py +416 -0
  31. memory_kg/pipeline.py +470 -0
  32. memory_kg/relations.py +128 -0
  33. memory_kg/sampler.py +340 -0
  34. memory_kg/semantic_builder.py +261 -0
  35. memory_kg/semantic_extractor.py +246 -0
  36. memory_kg/semantic_primitives.py +104 -0
  37. memory_kg/snapshots.py +483 -0
  38. memory_kg/store.py +454 -0
  39. memory_kg/topics.py +335 -0
  40. memory_kg-0.5.2.dist-info/METADATA +304 -0
  41. memory_kg-0.5.2.dist-info/RECORD +44 -0
  42. memory_kg-0.5.2.dist-info/WHEEL +4 -0
  43. memory_kg-0.5.2.dist-info/entry_points.txt +13 -0
  44. 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
@@ -0,0 +1,6 @@
1
+ """Entry point for ``python -m memory_kg``."""
2
+
3
+ from memory_kg.cli.main import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
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()