memplex 3.2.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.
- memnex/__init__.py +31 -0
- memnex/__main__.py +6 -0
- memnex/_plugin/.claude-plugin/plugin.json +24 -0
- memnex/_plugin/.mcp.json +9 -0
- memnex/_plugin/__init__.py +0 -0
- memnex/_plugin/hooks/hooks.json +43 -0
- memnex/_plugin/scripts/hook-runner.py +166 -0
- memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
- memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
- memnex/_plugin/skills/mem-search/SKILL.md +85 -0
- memnex/_plugin/skills/mem-write/SKILL.md +78 -0
- memnex/adapters/__init__.py +14 -0
- memnex/adapters/claude_skill.py +169 -0
- memnex/adapters/cli.py +525 -0
- memnex/adapters/http_api.py +314 -0
- memnex/adapters/mcp_server.py +448 -0
- memnex/compaction.py +563 -0
- memnex/config.py +366 -0
- memnex/core/__init__.py +13 -0
- memnex/core/associator/__init__.py +8 -0
- memnex/core/associator/domain_classifier.py +75 -0
- memnex/core/associator/entity_aligner.py +127 -0
- memnex/core/associator/ref_linker.py +197 -0
- memnex/core/associator/term_mapper.py +77 -0
- memnex/core/dictionaries/__init__.py +50 -0
- memnex/core/engine.py +667 -0
- memnex/core/extractors/__init__.py +15 -0
- memnex/core/extractors/docx.py +97 -0
- memnex/core/extractors/image.py +233 -0
- memnex/core/extractors/markdown.py +139 -0
- memnex/core/extractors/pdf.py +133 -0
- memnex/core/extractors/vision_mapper.py +131 -0
- memnex/core/handlers/__init__.py +7 -0
- memnex/core/handlers/clipboard.py +40 -0
- memnex/core/handlers/file_handler.py +62 -0
- memnex/core/handlers/url_handler.py +132 -0
- memnex/llm/__init__.py +25 -0
- memnex/llm/enhancer.py +226 -0
- memnex/llm/fallback_chain.py +87 -0
- memnex/llm/injection_guard.py +178 -0
- memnex/llm/provider.py +130 -0
- memnex/llm/providers/__init__.py +22 -0
- memnex/llm/providers/anthropic.py +135 -0
- memnex/llm/providers/local.py +135 -0
- memnex/llm/providers/rule_based.py +68 -0
- memnex/llm/sanitizer.py +67 -0
- memnex/models/__init__.py +68 -0
- memnex/models/feedback.py +42 -0
- memnex/models/graph.py +33 -0
- memnex/models/memory.py +102 -0
- memnex/models/misc.py +185 -0
- memnex/models/paragraph.py +45 -0
- memnex/models/search.py +51 -0
- memnex/models/source.py +23 -0
- memnex/models/task.py +62 -0
- memnex/processing/__init__.py +1 -0
- memnex/processing/graph_builder.py +278 -0
- memnex/processing/merger/__init__.py +6 -0
- memnex/processing/merger/confidence_calculator.py +127 -0
- memnex/processing/merger/conflict_resolver.py +116 -0
- memnex/retrieval/__init__.py +1 -0
- memnex/retrieval/dedup.py +386 -0
- memnex/retrieval/embedding.py +289 -0
- memnex/retrieval/reranker.py +299 -0
- memnex/service.py +902 -0
- memnex/storage/__init__.py +65 -0
- memnex/storage/base.py +132 -0
- memnex/storage/changelog.py +106 -0
- memnex/storage/feedback.py +486 -0
- memnex/storage/lite/__init__.py +5 -0
- memnex/storage/lite/store.py +606 -0
- memnex/storage/vector.py +265 -0
- memnex/wiki/__init__.py +11 -0
- memnex/wiki/community.py +221 -0
- memnex/wiki/compiler.py +545 -0
- memnex/wiki/generator.py +270 -0
- memnex/wiki/search.py +282 -0
- memnex/worker.py +412 -0
- memplex-3.2.0.dist-info/METADATA +37 -0
- memplex-3.2.0.dist-info/RECORD +83 -0
- memplex-3.2.0.dist-info/WHEEL +5 -0
- memplex-3.2.0.dist-info/entry_points.txt +2 -0
- memplex-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""MemNex storage layer -- MemoryStore interface and backends."""
|
|
2
|
+
|
|
3
|
+
from memnex.storage.base import MemoryStore
|
|
4
|
+
from memnex.storage.lite.store import LiteMemoryStore
|
|
5
|
+
from memnex.storage.changelog import ChangelogStore
|
|
6
|
+
from memnex.storage.vector import (
|
|
7
|
+
VectorStore,
|
|
8
|
+
InMemoryVectorStore,
|
|
9
|
+
ChromaVectorStore,
|
|
10
|
+
create_vector_store,
|
|
11
|
+
)
|
|
12
|
+
from memnex.storage.feedback import (
|
|
13
|
+
FeedbackStore,
|
|
14
|
+
LiteFeedbackStore,
|
|
15
|
+
SQLiteFeedbackStore,
|
|
16
|
+
PostgresFeedbackStore,
|
|
17
|
+
create_feedback_store,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_store(
|
|
24
|
+
config=None,
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> MemoryStore:
|
|
27
|
+
"""Factory: create a MemoryStore.
|
|
28
|
+
|
|
29
|
+
Accepts either a MemNexConfig object or a backend string.
|
|
30
|
+
Falls back to 'lite' for unsupported backends.
|
|
31
|
+
"""
|
|
32
|
+
if config is not None and hasattr(config, 'storage'):
|
|
33
|
+
backend = config.storage.backend
|
|
34
|
+
storage_path = config.storage.path
|
|
35
|
+
else:
|
|
36
|
+
backend = config if isinstance(config, str) else kwargs.get('backend', 'lite')
|
|
37
|
+
storage_path = kwargs.get('path')
|
|
38
|
+
|
|
39
|
+
if backend in ("lite", "standard", "enterprise"):
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
path = Path(storage_path).expanduser() / "memory.json" if storage_path else None
|
|
42
|
+
try:
|
|
43
|
+
return LiteMemoryStore(path=path)
|
|
44
|
+
except Exception:
|
|
45
|
+
return LiteMemoryStore()
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Unknown storage backend: {backend!r}. Supported: 'lite'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"MemoryStore",
|
|
53
|
+
"LiteMemoryStore",
|
|
54
|
+
"ChangelogStore",
|
|
55
|
+
"VectorStore",
|
|
56
|
+
"InMemoryVectorStore",
|
|
57
|
+
"ChromaVectorStore",
|
|
58
|
+
"create_vector_store",
|
|
59
|
+
"FeedbackStore",
|
|
60
|
+
"LiteFeedbackStore",
|
|
61
|
+
"SQLiteFeedbackStore",
|
|
62
|
+
"PostgresFeedbackStore",
|
|
63
|
+
"create_feedback_store",
|
|
64
|
+
"create_store",
|
|
65
|
+
]
|
memnex/storage/base.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""MemoryStore abstract base class -- unified data access layer.
|
|
2
|
+
|
|
3
|
+
Pure CRUD + basic retrieval. No orchestration logic (that belongs in
|
|
4
|
+
MemNexService). Every concrete backend (Lite, Standard, Enterprise)
|
|
5
|
+
implements this interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from memnex.models import (
|
|
14
|
+
BatchResult,
|
|
15
|
+
ChangelogEvent,
|
|
16
|
+
Function,
|
|
17
|
+
GraphData,
|
|
18
|
+
MergeResult,
|
|
19
|
+
Observation,
|
|
20
|
+
SearchFilters,
|
|
21
|
+
SearchResult,
|
|
22
|
+
SourceDocument,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryStore(ABC):
|
|
27
|
+
"""Pure data access layer -- CRUD + basic retrieval, no orchestration."""
|
|
28
|
+
|
|
29
|
+
# ── Write operations ────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def add(self, func: Function, source: SourceDocument) -> None:
|
|
33
|
+
"""Add a Function. If a Function with the same *name_normalized*
|
|
34
|
+
already exists, its FieldValues are merged instead.
|
|
35
|
+
|
|
36
|
+
Concurrency (Lite backend): single-threaded, optimistic lock skipped.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def add_batch(
|
|
41
|
+
self,
|
|
42
|
+
funcs: List[Function],
|
|
43
|
+
sources: List[SourceDocument],
|
|
44
|
+
) -> BatchResult:
|
|
45
|
+
"""Batch add. Calls :meth:`add` per item; single-item failure does
|
|
46
|
+
**not** abort the rest. Failed items are recorded in
|
|
47
|
+
``BatchResult.failed_items``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def add_observation(self, observation: Observation) -> None:
|
|
52
|
+
"""Persist an Observation event."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def increment_access(self, func_id: str) -> None:
|
|
56
|
+
"""Atomically increment ``access_count`` and update
|
|
57
|
+
``last_accessed_at``. Must not depend on a prior ``get()`` to
|
|
58
|
+
avoid read-modify-write races.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# ── Retrieval ───────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def vector_search(self, text: str, top_k: int = 5) -> List[SearchResult]:
|
|
65
|
+
"""Semantic / vector similarity search."""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def fts_search(self, text: str, top_k: int = 10) -> List[SearchResult]:
|
|
69
|
+
"""Full-text / keyword search."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def filter(self, filters: SearchFilters) -> List[Function]:
|
|
73
|
+
"""Structured filter over stored Functions."""
|
|
74
|
+
|
|
75
|
+
# ── Read operations ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def get(self, func_id: str) -> Optional[Function]:
|
|
79
|
+
"""Retrieve a single Function by ID, or ``None``."""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def get_neighbors(
|
|
83
|
+
self,
|
|
84
|
+
func_id: str,
|
|
85
|
+
edge_types: Optional[List[str]] = None,
|
|
86
|
+
max_hops: int = 1,
|
|
87
|
+
) -> List[Function]:
|
|
88
|
+
"""Return neighbour Functions reachable within *max_hops* edges,
|
|
89
|
+
optionally restricted to *edge_types*.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def get_graph(
|
|
94
|
+
self,
|
|
95
|
+
func_ids: Optional[List[str]] = None,
|
|
96
|
+
) -> GraphData:
|
|
97
|
+
"""Return sub-graph. If *func_ids* is ``None`` the full graph is
|
|
98
|
+
returned.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def get_timeline(
|
|
103
|
+
self,
|
|
104
|
+
func_id: str,
|
|
105
|
+
limit: int = 20,
|
|
106
|
+
) -> List[ChangelogEvent]:
|
|
107
|
+
"""Return recent ChangelogEvents for a Function."""
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def list_functions(
|
|
111
|
+
self,
|
|
112
|
+
offset: int = 0,
|
|
113
|
+
limit: int = 1000,
|
|
114
|
+
owner: Optional[str] = None,
|
|
115
|
+
) -> List[Function]:
|
|
116
|
+
"""Paginated listing, optionally filtered by *owner*."""
|
|
117
|
+
|
|
118
|
+
# ── Delete / merge / clear ──────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def delete(self, func_id: str) -> None:
|
|
122
|
+
"""Soft-delete a Function and its associated graph edges."""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def merge(self, sub_graph: GraphData) -> MergeResult:
|
|
126
|
+
"""Incrementally fuse *sub_graph* into the main graph. Existing
|
|
127
|
+
nodes are merged; new ones are inserted.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def clear(self) -> None:
|
|
132
|
+
"""Remove all stored data (Functions, edges, changelog)."""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""ChangelogStore -- lightweight append-only event log.
|
|
2
|
+
|
|
3
|
+
Lite implementation: in-memory list persisted to a JSON file.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import tempfile
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from memnex.models import ChangelogEvent
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChangelogStore:
|
|
21
|
+
"""Append-only changelog with JSON persistence.
|
|
22
|
+
|
|
23
|
+
Data path: ``~/.memnex/changelog.json``
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Optional[Path] = None) -> None:
|
|
27
|
+
self._path = path or Path("~/.memnex/changelog.json").expanduser()
|
|
28
|
+
self._events: List[ChangelogEvent] = []
|
|
29
|
+
self._load()
|
|
30
|
+
|
|
31
|
+
# ── Public API ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def append(self, event: ChangelogEvent) -> None:
|
|
34
|
+
"""Append an event and persist."""
|
|
35
|
+
self._events.append(event)
|
|
36
|
+
self._save()
|
|
37
|
+
|
|
38
|
+
def get_timeline(
|
|
39
|
+
self,
|
|
40
|
+
func_id: str,
|
|
41
|
+
limit: int = 20,
|
|
42
|
+
) -> List[ChangelogEvent]:
|
|
43
|
+
"""Return the most recent events for *func_id*, newest first."""
|
|
44
|
+
matching = [e for e in self._events if e.func_id == func_id]
|
|
45
|
+
matching.sort(key=lambda e: e.timestamp, reverse=True)
|
|
46
|
+
return matching[:limit]
|
|
47
|
+
|
|
48
|
+
def clear(self) -> None:
|
|
49
|
+
"""Remove all events."""
|
|
50
|
+
self._events.clear()
|
|
51
|
+
self._save()
|
|
52
|
+
|
|
53
|
+
# ── Persistence helpers ─────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _serialize_event(event: ChangelogEvent) -> dict:
|
|
57
|
+
return {
|
|
58
|
+
"func_id": event.func_id,
|
|
59
|
+
"timestamp": (
|
|
60
|
+
event.timestamp.isoformat()
|
|
61
|
+
if isinstance(event.timestamp, datetime)
|
|
62
|
+
else event.timestamp
|
|
63
|
+
),
|
|
64
|
+
"event_type": event.event_type,
|
|
65
|
+
"description": event.description,
|
|
66
|
+
"source": event.source,
|
|
67
|
+
"actor": event.actor,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _deserialize_event(data: dict) -> ChangelogEvent:
|
|
72
|
+
ts = data["timestamp"]
|
|
73
|
+
if isinstance(ts, str):
|
|
74
|
+
ts = datetime.fromisoformat(ts)
|
|
75
|
+
return ChangelogEvent(
|
|
76
|
+
func_id=data["func_id"],
|
|
77
|
+
timestamp=ts,
|
|
78
|
+
event_type=data["event_type"],
|
|
79
|
+
description=data["description"],
|
|
80
|
+
source=data["source"],
|
|
81
|
+
actor=data["actor"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _load(self) -> None:
|
|
85
|
+
if not self._path.exists():
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
raw = json.loads(self._path.read_text(encoding="utf-8"))
|
|
89
|
+
self._events = [self._deserialize_event(d) for d in raw]
|
|
90
|
+
except Exception:
|
|
91
|
+
logger.warning("Failed to load changelog from %s", self._path)
|
|
92
|
+
|
|
93
|
+
def _save(self) -> None:
|
|
94
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
data = [self._serialize_event(e) for e in self._events]
|
|
96
|
+
# Atomic write: write to temp then rename
|
|
97
|
+
tmp_fd, tmp_path = tempfile.mkstemp(
|
|
98
|
+
dir=str(self._path.parent), suffix=".tmp"
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
with open(tmp_fd, "w", encoding="utf-8") as fh:
|
|
102
|
+
json.dump(data, fh, ensure_ascii=False, indent=2)
|
|
103
|
+
Path(tmp_path).replace(self._path)
|
|
104
|
+
except Exception:
|
|
105
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
106
|
+
raise
|