memplex 3.2.5__tar.gz → 3.2.7__tar.gz
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.
- {memplex-3.2.5 → memplex-3.2.7}/PKG-INFO +5 -2
- {memplex-3.2.5 → memplex-3.2.7}/README.md +48 -5
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/scripts/hook-runner.py +27 -1
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/agent_installer.py +1 -1
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/cli.py +5 -13
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/mcp_server.py +1 -1
- {memplex-3.2.5 → memplex-3.2.7}/memplex/config.py +2 -4
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/term_mapper.py +8 -1
- {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/embedding.py +191 -8
- {memplex-3.2.5 → memplex-3.2.7}/memplex/service.py +21 -2
- memplex-3.2.7/memplex/storage/lite/search_index.py +305 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/lite/store.py +76 -63
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/PKG-INFO +5 -2
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/SOURCES.txt +3 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/requires.txt +5 -1
- {memplex-3.2.5 → memplex-3.2.7}/pyproject.toml +6 -2
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_associators.py +15 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_config.py +5 -0
- memplex-3.2.7/tests/test_e2e_robustness.py +203 -0
- memplex-3.2.7/tests/test_embedding.py +187 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_hooks.py +9 -2
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_install_scripts.py +3 -3
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_storage.py +144 -0
- {memplex-3.2.5 → memplex-3.2.7}/LICENSE +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/__main__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/.mcp.json +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/hooks/hooks.json +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/agent_runtime.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/claude_skill.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/http_api.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/base.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/benchmark_cli.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/evaluator.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/loader.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/locomo.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/memory_eval.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/memory_metrics.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/metrics.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/nq_trivia.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/popqa_hotpot.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/compaction.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/domain_classifier.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/entity_aligner.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/ref_linker.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/dictionaries/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/engine.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/docx.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/image.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/markdown.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/pdf.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/vision_mapper.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/clipboard.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/file_handler.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/url_handler.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/collector.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/hook_event.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/registry.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/enhancer.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/fallback_chain.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/injection_guard.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/provider.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/anthropic.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/local.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/rule_based.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/sanitizer.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/logging_utils.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/metrics.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/feedback.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/graph.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/memory.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/misc.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/paragraph.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/search.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/source.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/models/task.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/graph_builder.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/confidence_calculator.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/conflict_resolver.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/dedup.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/reranker.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/base.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/changelog.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/feedback.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/lite/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/vector.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/__init__.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/community.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/compiler.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/generator.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/search.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex/worker.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/dependency_links.txt +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/entry_points.txt +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/top_level.txt +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/setup.cfg +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_agent_hot_paths.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_agent_runtime.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_core_engine.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_graph_builder.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_llm.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_models.py +0 -0
- {memplex-3.2.5 → memplex-3.2.7}/tests/test_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memplex
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.7
|
|
4
4
|
Summary: Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,6 +17,9 @@ Requires-Dist: pytesseract>=0.3.10; extra == "extractors"
|
|
|
17
17
|
Provides-Extra: vector
|
|
18
18
|
Requires-Dist: chromadb>=0.4.0; extra == "vector"
|
|
19
19
|
Requires-Dist: sentence-transformers>=2.0; extra == "vector"
|
|
20
|
+
Provides-Extra: local-onnx
|
|
21
|
+
Requires-Dist: onnxruntime>=1.17.0; extra == "local-onnx"
|
|
22
|
+
Requires-Dist: tokenizers>=0.15.0; extra == "local-onnx"
|
|
20
23
|
Provides-Extra: graph
|
|
21
24
|
Requires-Dist: networkx>=3.0; extra == "graph"
|
|
22
25
|
Requires-Dist: python-louvain>=0.16; extra == "graph"
|
|
@@ -31,7 +34,7 @@ Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
|
31
34
|
Provides-Extra: neo4j
|
|
32
35
|
Requires-Dist: neo4j>=5.0.0; extra == "neo4j"
|
|
33
36
|
Provides-Extra: all
|
|
34
|
-
Requires-Dist: memplex[embedding,extractors,graph,http,llm,vector]; extra == "all"
|
|
37
|
+
Requires-Dist: memplex[embedding,extractors,graph,http,llm,local-onnx,vector]; extra == "all"
|
|
35
38
|
Provides-Extra: dev
|
|
36
39
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
40
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -35,14 +35,14 @@ npx memplex uninstall --agent all
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
The npm wrapper creates a persistent Python environment at
|
|
38
|
-
`~/.local/share/memplex/agent-venv`, installs `memplex==3.2.
|
|
38
|
+
`~/.local/share/memplex/agent-venv`, installs `memplex==3.2.7`, detects local
|
|
39
39
|
agent config directories, and registers Memplex into the selected hosts. It uses
|
|
40
40
|
`uv` when available and falls back to `python -m venv` plus `pip`.
|
|
41
41
|
|
|
42
42
|
Python-first users can skip npm:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
uv tool install memplex==3.2.
|
|
45
|
+
uv tool install memplex==3.2.7
|
|
46
46
|
memplex setup --agent all --project-path "$PWD"
|
|
47
47
|
```
|
|
48
48
|
|
|
@@ -91,12 +91,55 @@ For a no-write preview:
|
|
|
91
91
|
npx memplex setup --agent all --project-path "$PWD" --dry-run
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Offline And Mainland China
|
|
95
|
+
|
|
96
|
+
Memplex's default local retrieval uses a SQLite FTS5 sidecar index with
|
|
97
|
+
`bm25()` ranking plus generated trigram tokens for Chinese, code symbols,
|
|
98
|
+
paths, and short memory fragments. If SQLite FTS5 is unavailable, it falls back
|
|
99
|
+
to pure-Python local BM25/trigram matching. The agent hot path does not need
|
|
100
|
+
HuggingFace, so `npx memplex setup`, capture, recall, MCP tools, and hooks
|
|
101
|
+
continue to work when HuggingFace is blocked or unavailable.
|
|
102
|
+
|
|
103
|
+
The embedding fallback is also local. To force that path explicitly:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
MEMPLEX_EMBEDDING_MODEL=tfidf memplex query "local memory"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
To enhance semantic recall with a local model and no network downloads:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python -m pip install "memplex[local-onnx]"
|
|
113
|
+
export MEMPLEX_LOCAL_ONNX_MODEL=/models/bge-small/model.onnx
|
|
114
|
+
export MEMPLEX_LOCAL_ONNX_TOKENIZER=/models/bge-small/tokenizer.json
|
|
115
|
+
memplex query "local semantic memory"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`MEMPLEX_EMBEDDING_MODEL=local-onnx` or
|
|
119
|
+
`MEMPLEX_EMBEDDING_MODEL=local-onnx:/models/bge-small/model.onnx` opts in
|
|
120
|
+
explicitly and reports configuration errors. With the default auto-enhancement
|
|
121
|
+
path, a missing local ONNX runtime or model keeps the SQLite
|
|
122
|
+
FTS5/BM25+trigram retrieval path alive and falls back to local embedding.
|
|
123
|
+
|
|
124
|
+
Opt into HuggingFace only when the environment can reach it or the model is
|
|
125
|
+
already cached:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
MEMPLEX_EMBEDDING_MODEL=minilm memplex query "semantic memory"
|
|
129
|
+
MEMPLEX_EMBEDDING_MODEL=bge-m3 memplex query "中文记忆"
|
|
130
|
+
MEMPLEX_EMBEDDING_MODEL=hf:BAAI/bge-m3 memplex query "中文记忆"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If your organization provides an approved HuggingFace mirror, configure it in
|
|
134
|
+
the Python/HuggingFace environment before enabling those models. Otherwise keep
|
|
135
|
+
the default local retrieval and embedding path for agent reliability.
|
|
136
|
+
|
|
94
137
|
## Core Features
|
|
95
138
|
|
|
96
139
|
- **Automatic agent loop**: pre-turn recall, post-turn capture, background
|
|
97
140
|
consolidation.
|
|
98
141
|
- **4 memory types**: Function, Fact, Preference, Observation.
|
|
99
|
-
- **3-layer retrieval**: search, timeline, get.
|
|
142
|
+
- **3-layer retrieval**: SQLite FTS5/BM25+trigram search, timeline, get.
|
|
100
143
|
- **5-dim reranking**: raw relevance, semantic similarity, recency, source
|
|
101
144
|
authority, frequency.
|
|
102
145
|
- **5-stage compaction**: extract, dedup, summarize, prune, archive.
|
|
@@ -110,8 +153,8 @@ npx memplex setup --agent all --project-path "$PWD" --dry-run
|
|
|
110
153
|
- [Explainer](docs/explainer.md): what Memplex is and how the memory loop works.
|
|
111
154
|
- [Agent Integration Loop](docs/agent-integration.md): adapter contracts for
|
|
112
155
|
Codex, Claude Code, OpenClaw, and Hermes.
|
|
113
|
-
- [
|
|
114
|
-
|
|
156
|
+
- [Release Automation](docs/release-automation.md): npm token handling and
|
|
157
|
+
automated npm publishing.
|
|
115
158
|
|
|
116
159
|
## From Source
|
|
117
160
|
|
|
@@ -199,6 +199,31 @@ def _print_contract(content: str = "") -> None:
|
|
|
199
199
|
print(OUTPUT_CONTRACT)
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
def _package_version(memplex_module: Any) -> str:
|
|
203
|
+
"""Resolve package version, preferring source-tree pyproject metadata."""
|
|
204
|
+
try:
|
|
205
|
+
import tomllib
|
|
206
|
+
|
|
207
|
+
for parent in Path(__file__).resolve().parents:
|
|
208
|
+
pyproject = parent / "pyproject.toml"
|
|
209
|
+
if not pyproject.exists():
|
|
210
|
+
continue
|
|
211
|
+
project = tomllib.loads(pyproject.read_text(encoding="utf-8")).get(
|
|
212
|
+
"project", {}
|
|
213
|
+
)
|
|
214
|
+
if project.get("name") == "memplex" and project.get("version"):
|
|
215
|
+
return str(project["version"])
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
from importlib.metadata import version as pkg_version
|
|
221
|
+
|
|
222
|
+
return pkg_version("memplex")
|
|
223
|
+
except Exception:
|
|
224
|
+
return getattr(memplex_module, "__version__", "unknown")
|
|
225
|
+
|
|
226
|
+
|
|
202
227
|
def cmd_setup() -> None:
|
|
203
228
|
"""Check environment on plugin install."""
|
|
204
229
|
try:
|
|
@@ -207,7 +232,7 @@ def cmd_setup() -> None:
|
|
|
207
232
|
from memplex.config import load_config
|
|
208
233
|
|
|
209
234
|
# Verify memplex is importable
|
|
210
|
-
version =
|
|
235
|
+
version = _package_version(memplex)
|
|
211
236
|
|
|
212
237
|
# Check config
|
|
213
238
|
try:
|
|
@@ -219,6 +244,7 @@ def cmd_setup() -> None:
|
|
|
219
244
|
# Initialize service if config exists
|
|
220
245
|
if cfg:
|
|
221
246
|
from memplex.service import MemplexService
|
|
247
|
+
|
|
222
248
|
service = MemplexService(config=cfg)
|
|
223
249
|
health = service.health()
|
|
224
250
|
print(f"[Memplex] v{version} installed. Status: {health.get('status', 'unknown')}")
|
|
@@ -268,7 +268,7 @@ def cmd_health(args: argparse.Namespace) -> int:
|
|
|
268
268
|
try:
|
|
269
269
|
info = svc.health()
|
|
270
270
|
print(_fmt(info, args.output))
|
|
271
|
-
return 0 if info.get("status")
|
|
271
|
+
return 0 if info.get("status") in {"healthy", "warning"} else 1
|
|
272
272
|
finally:
|
|
273
273
|
svc.stop()
|
|
274
274
|
|
|
@@ -477,9 +477,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
477
477
|
# -- feedback --
|
|
478
478
|
p_fb = sub.add_parser("feedback", help="Submit feedback on a memory field")
|
|
479
479
|
p_fb.add_argument("memory_id", help="Memory ID")
|
|
480
|
-
p_fb.add_argument(
|
|
481
|
-
"--role", required=True, help="Field role (trigger|action|condition|benefit)"
|
|
482
|
-
)
|
|
480
|
+
p_fb.add_argument("--role", required=True, help="Field role (trigger|action|condition|benefit)")
|
|
483
481
|
p_fb.add_argument("--index", type=int, required=True, help="Value index")
|
|
484
482
|
p_fb.add_argument(
|
|
485
483
|
"--verdict",
|
|
@@ -508,9 +506,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
508
506
|
|
|
509
507
|
# -- agent --
|
|
510
508
|
p_agent = sub.add_parser("agent", help="Portable agent integration commands")
|
|
511
|
-
agent_sub = p_agent.add_subparsers(
|
|
512
|
-
dest="agent_command", help="Agent integration command"
|
|
513
|
-
)
|
|
509
|
+
agent_sub = p_agent.add_subparsers(dest="agent_command", help="Agent integration command")
|
|
514
510
|
agent_sub.add_parser("list", help="List supported agent profiles")
|
|
515
511
|
|
|
516
512
|
p_agent_manifest = agent_sub.add_parser("manifest", help="Show agent manifest")
|
|
@@ -520,9 +516,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
520
516
|
help="Agent id: codex | claude-code | openclaw | hermes | all",
|
|
521
517
|
)
|
|
522
518
|
|
|
523
|
-
p_agent_install = agent_sub.add_parser(
|
|
524
|
-
"install", help="Install Memplex into an agent host"
|
|
525
|
-
)
|
|
519
|
+
p_agent_install = agent_sub.add_parser("install", help="Install Memplex into an agent host")
|
|
526
520
|
p_agent_install.add_argument(
|
|
527
521
|
"--agent",
|
|
528
522
|
default="all",
|
|
@@ -565,9 +559,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
565
559
|
p_agent_recall.add_argument("--top-k", type=int, default=5)
|
|
566
560
|
p_agent_recall.add_argument("--token-budget", type=int, default=1500)
|
|
567
561
|
|
|
568
|
-
p_agent_capture = agent_sub.add_parser(
|
|
569
|
-
"capture", help="Capture a completed agent turn"
|
|
570
|
-
)
|
|
562
|
+
p_agent_capture = agent_sub.add_parser("capture", help="Capture a completed agent turn")
|
|
571
563
|
p_agent_capture.add_argument("--agent", default="codex")
|
|
572
564
|
p_agent_capture.add_argument("--user-id", default=None)
|
|
573
565
|
p_agent_capture.add_argument("--session-id", default="default")
|
|
@@ -29,7 +29,7 @@ class StorageConfig:
|
|
|
29
29
|
class EmbeddingConfig:
|
|
30
30
|
"""Embedding model configuration."""
|
|
31
31
|
|
|
32
|
-
model: str = "default" # default
|
|
32
|
+
model: str = "default" # default=local, optional local ONNX; hf:<id>=HF
|
|
33
33
|
dimension: int = 384
|
|
34
34
|
batch_size: int = 32
|
|
35
35
|
contextual_retrieval: bool = True
|
|
@@ -263,9 +263,7 @@ def _apply_env_overrides(config: MemplexConfig) -> None:
|
|
|
263
263
|
# Handle LLM fallback_chain via MEMPLEX_LLM_FALLBACK_CHAIN (comma-separated)
|
|
264
264
|
fallback_env = os.environ.get("MEMPLEX_LLM_FALLBACK_CHAIN")
|
|
265
265
|
if fallback_env is not None:
|
|
266
|
-
config.llm.fallback_chain = [
|
|
267
|
-
s.strip() for s in fallback_env.split(",") if s.strip()
|
|
268
|
-
]
|
|
266
|
+
config.llm.fallback_chain = [s.strip() for s in fallback_env.split(",") if s.strip()]
|
|
269
267
|
|
|
270
268
|
|
|
271
269
|
# ── YAML loading helpers ────────────────────────────────────────
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Term-based association using dictionary lookup."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from typing import List, Optional, Set, Tuple
|
|
4
5
|
|
|
5
6
|
from memplex.core.dictionaries import TermDictionary
|
|
6
7
|
from memplex.models.memory import Function
|
|
7
8
|
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class TermMapper:
|
|
10
13
|
"""Maps terms between documents using dictionary lookup."""
|
|
@@ -13,7 +16,7 @@ class TermMapper:
|
|
|
13
16
|
self.dictionary = dictionary or TermDictionary()
|
|
14
17
|
|
|
15
18
|
def embed_text(self, text: str) -> Optional[List[float]]:
|
|
16
|
-
"""Generate embedding vector using sentence-transformers
|
|
19
|
+
"""Generate embedding vector using sentence-transformers when available."""
|
|
17
20
|
try:
|
|
18
21
|
from sentence_transformers import SentenceTransformer
|
|
19
22
|
|
|
@@ -22,6 +25,10 @@ class TermMapper:
|
|
|
22
25
|
embedding = self._embedding_model.encode([text])[0]
|
|
23
26
|
return embedding.tolist()
|
|
24
27
|
except ImportError:
|
|
28
|
+
logger.debug("sentence-transformers not installed; skipping term embedding")
|
|
29
|
+
return None
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
logger.debug("Term embedding unavailable: %s", exc)
|
|
25
32
|
return None
|
|
26
33
|
|
|
27
34
|
def extract_terms(self, text: str) -> Set[str]:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""EmbeddingService -- vector embedding generation, storage and refresh.
|
|
2
2
|
|
|
3
|
-
Supports
|
|
4
|
-
|
|
3
|
+
Supports an offline default embedder, optional HuggingFace-backed models,
|
|
4
|
+
optional local ONNX models, configurable dimension, batch size, and Contextual
|
|
5
|
+
Retrieval (Anthropic's document-context prefix injection).
|
|
5
6
|
|
|
6
7
|
Embedding strategies::
|
|
7
8
|
|
|
@@ -20,7 +21,9 @@ Usage::
|
|
|
20
21
|
from __future__ import annotations
|
|
21
22
|
|
|
22
23
|
import logging
|
|
24
|
+
import os
|
|
23
25
|
from enum import Enum
|
|
26
|
+
from pathlib import Path
|
|
24
27
|
from typing import TYPE_CHECKING, List, Optional
|
|
25
28
|
|
|
26
29
|
from memplex.models import Function, RefreshResult
|
|
@@ -56,6 +59,7 @@ class _SentenceTransformerEmbedder:
|
|
|
56
59
|
def __init__(self, model_name: str, dimension: int) -> None:
|
|
57
60
|
from sentence_transformers import SentenceTransformer # type: ignore
|
|
58
61
|
|
|
62
|
+
self.model_name = model_name
|
|
59
63
|
self._model = SentenceTransformer(model_name)
|
|
60
64
|
self.dimension = dimension
|
|
61
65
|
|
|
@@ -68,7 +72,7 @@ class _SentenceTransformerEmbedder:
|
|
|
68
72
|
|
|
69
73
|
|
|
70
74
|
class _SimpleTFIDFEmbedder:
|
|
71
|
-
"""
|
|
75
|
+
"""Local embedder used for offline/default operation.
|
|
72
76
|
|
|
73
77
|
Uses a TF-IDF-inspired bag-of-words representation. The dimension
|
|
74
78
|
is fixed to the number of unique words seen so far (padded / truncated
|
|
@@ -117,6 +121,104 @@ class _SimpleTFIDFEmbedder:
|
|
|
117
121
|
return [self.encode(t) for t in texts]
|
|
118
122
|
|
|
119
123
|
|
|
124
|
+
class _LocalONNXEmbedder:
|
|
125
|
+
"""Offline ONNX embedding backend for locally cached models."""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
model_path: str,
|
|
130
|
+
dimension: int,
|
|
131
|
+
tokenizer_path: Optional[str] = None,
|
|
132
|
+
max_length: int = 256,
|
|
133
|
+
) -> None:
|
|
134
|
+
import numpy as np
|
|
135
|
+
import onnxruntime as ort # type: ignore
|
|
136
|
+
from tokenizers import Tokenizer # type: ignore
|
|
137
|
+
|
|
138
|
+
resolved_model = Path(model_path).expanduser()
|
|
139
|
+
if not resolved_model.exists():
|
|
140
|
+
raise FileNotFoundError(f"Local ONNX model not found: {resolved_model}")
|
|
141
|
+
|
|
142
|
+
resolved_tokenizer = (
|
|
143
|
+
Path(tokenizer_path).expanduser()
|
|
144
|
+
if tokenizer_path
|
|
145
|
+
else resolved_model.parent / "tokenizer.json"
|
|
146
|
+
)
|
|
147
|
+
if not resolved_tokenizer.exists():
|
|
148
|
+
raise FileNotFoundError(
|
|
149
|
+
f"Local ONNX tokenizer not found: {resolved_tokenizer}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.model_path = str(resolved_model)
|
|
153
|
+
self.dimension = dimension
|
|
154
|
+
self.max_length = max(1, max_length)
|
|
155
|
+
self._np = np
|
|
156
|
+
self._tokenizer = Tokenizer.from_file(str(resolved_tokenizer))
|
|
157
|
+
self._session = ort.InferenceSession(
|
|
158
|
+
str(resolved_model),
|
|
159
|
+
providers=["CPUExecutionProvider"],
|
|
160
|
+
)
|
|
161
|
+
self._input_names = [item.name for item in self._session.get_inputs()]
|
|
162
|
+
|
|
163
|
+
def encode(self, text: str) -> Vector:
|
|
164
|
+
inputs = self._encode_inputs(text)
|
|
165
|
+
outputs = self._session.run(None, inputs)
|
|
166
|
+
return self._normalize_output(outputs[0], inputs.get("attention_mask"))
|
|
167
|
+
|
|
168
|
+
def encode_batch(self, texts: List[str], batch_size: int = 32) -> List[Vector]:
|
|
169
|
+
return [self.encode(text) for text in texts]
|
|
170
|
+
|
|
171
|
+
def _encode_inputs(self, text: str) -> dict:
|
|
172
|
+
encoding = self._tokenizer.encode(text)
|
|
173
|
+
ids = list(getattr(encoding, "ids", []) or [])[: self.max_length]
|
|
174
|
+
if not ids:
|
|
175
|
+
ids = [0]
|
|
176
|
+
attention_mask = [1] * len(ids)
|
|
177
|
+
|
|
178
|
+
inputs = {}
|
|
179
|
+
if "input_ids" in self._input_names:
|
|
180
|
+
inputs["input_ids"] = self._np.array([ids], dtype=self._np.int64)
|
|
181
|
+
if "attention_mask" in self._input_names:
|
|
182
|
+
inputs["attention_mask"] = self._np.array(
|
|
183
|
+
[attention_mask],
|
|
184
|
+
dtype=self._np.int64,
|
|
185
|
+
)
|
|
186
|
+
if "token_type_ids" in self._input_names:
|
|
187
|
+
inputs["token_type_ids"] = self._np.zeros(
|
|
188
|
+
(1, len(ids)),
|
|
189
|
+
dtype=self._np.int64,
|
|
190
|
+
)
|
|
191
|
+
if not inputs:
|
|
192
|
+
inputs[self._input_names[0]] = self._np.array([ids], dtype=self._np.int64)
|
|
193
|
+
return inputs
|
|
194
|
+
|
|
195
|
+
def _normalize_output(self, output, attention_mask=None) -> Vector:
|
|
196
|
+
arr = self._np.asarray(output, dtype=float)
|
|
197
|
+
if arr.ndim == 3:
|
|
198
|
+
token_embeddings = arr[0]
|
|
199
|
+
if attention_mask is not None:
|
|
200
|
+
mask = self._np.asarray(attention_mask[0], dtype=float)[:, None]
|
|
201
|
+
denom = max(float(mask.sum()), 1.0)
|
|
202
|
+
vec = (token_embeddings * mask).sum(axis=0) / denom
|
|
203
|
+
else:
|
|
204
|
+
vec = token_embeddings.mean(axis=0)
|
|
205
|
+
elif arr.ndim == 2:
|
|
206
|
+
vec = arr[0]
|
|
207
|
+
else:
|
|
208
|
+
vec = arr.reshape(-1)
|
|
209
|
+
|
|
210
|
+
values = [float(value) for value in vec.tolist()]
|
|
211
|
+
if len(values) < self.dimension:
|
|
212
|
+
values.extend([0.0] * (self.dimension - len(values)))
|
|
213
|
+
elif len(values) > self.dimension:
|
|
214
|
+
values = values[: self.dimension]
|
|
215
|
+
|
|
216
|
+
norm = sum(value * value for value in values) ** 0.5
|
|
217
|
+
if norm > 0:
|
|
218
|
+
values = [value / norm for value in values]
|
|
219
|
+
return values
|
|
220
|
+
|
|
221
|
+
|
|
120
222
|
# ── EmbeddingService ─────────────────────────────────────────────────
|
|
121
223
|
|
|
122
224
|
|
|
@@ -126,9 +228,14 @@ class EmbeddingService:
|
|
|
126
228
|
Parameters
|
|
127
229
|
----------
|
|
128
230
|
model:
|
|
129
|
-
Embedding model name.
|
|
130
|
-
|
|
131
|
-
|
|
231
|
+
Embedding model name. ``"default"`` uses local retrieval and local
|
|
232
|
+
embeddings, and auto-enables local ONNX only when
|
|
233
|
+
``MEMPLEX_LOCAL_ONNX_MODEL`` points at an existing local model.
|
|
234
|
+
``"tfidf"``, ``"offline"``, and ``"lite"`` force local TF-IDF
|
|
235
|
+
embeddings. Use ``"local-onnx"``, ``"local-onnx:<path>``, or
|
|
236
|
+
``"onnx:<path>"`` for a local ONNX model. Use ``"minilm"``,
|
|
237
|
+
``"bge-m3"``, ``"bge-small"``, a raw sentence-transformers model id, or
|
|
238
|
+
``"hf:<model-id>"`` to opt into HuggingFace-backed embeddings.
|
|
132
239
|
dimension:
|
|
133
240
|
Embedding vector dimension.
|
|
134
241
|
storage:
|
|
@@ -137,8 +244,12 @@ class EmbeddingService:
|
|
|
137
244
|
Optional :class:`VectorStore` for upsert operations.
|
|
138
245
|
"""
|
|
139
246
|
|
|
247
|
+
_OFFLINE_MODELS = {"default", "tfidf", "offline", "lite", "local"}
|
|
248
|
+
_HF_PREFIX = "hf:"
|
|
249
|
+
_LOCAL_ONNX_MODELS = {"local-onnx", "onnx"}
|
|
250
|
+
_LOCAL_ONNX_PREFIXES = ("local-onnx:", "onnx:")
|
|
140
251
|
_MODEL_MAP = {
|
|
141
|
-
"
|
|
252
|
+
"minilm": "sentence-transformers/all-MiniLM-L6-v2",
|
|
142
253
|
"bge-m3": "BAAI/bge-m3",
|
|
143
254
|
"bge-small": "BAAI/bge-small-en-v1.5",
|
|
144
255
|
}
|
|
@@ -258,7 +369,52 @@ class EmbeddingService:
|
|
|
258
369
|
|
|
259
370
|
def _create_embedder(self, model: str, dimension: int):
|
|
260
371
|
"""Create the appropriate embedder backend."""
|
|
261
|
-
|
|
372
|
+
model_key = (model or "default").strip()
|
|
373
|
+
model_lookup_key = model_key.lower()
|
|
374
|
+
|
|
375
|
+
onnx_model_path, onnx_explicit = self._resolve_local_onnx_model_path(
|
|
376
|
+
model_key,
|
|
377
|
+
model_lookup_key,
|
|
378
|
+
)
|
|
379
|
+
if onnx_model_path:
|
|
380
|
+
try:
|
|
381
|
+
return _LocalONNXEmbedder(
|
|
382
|
+
model_path=onnx_model_path,
|
|
383
|
+
tokenizer_path=os.getenv("MEMPLEX_LOCAL_ONNX_TOKENIZER"),
|
|
384
|
+
max_length=self._local_onnx_max_length(),
|
|
385
|
+
dimension=dimension,
|
|
386
|
+
)
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
if onnx_explicit:
|
|
389
|
+
raise RuntimeError(
|
|
390
|
+
f"Failed to load explicit local ONNX embedding model "
|
|
391
|
+
f"{onnx_model_path}: {exc}"
|
|
392
|
+
) from exc
|
|
393
|
+
logger.warning(
|
|
394
|
+
"Failed to load local ONNX embedding model %s: %s. "
|
|
395
|
+
"Falling back to TF-IDF embedder",
|
|
396
|
+
onnx_model_path,
|
|
397
|
+
exc,
|
|
398
|
+
)
|
|
399
|
+
return _SimpleTFIDFEmbedder(dimension=dimension)
|
|
400
|
+
|
|
401
|
+
if onnx_explicit:
|
|
402
|
+
raise ValueError(
|
|
403
|
+
"Local ONNX embedding requested but no model path was provided. "
|
|
404
|
+
"Set MEMPLEX_LOCAL_ONNX_MODEL or use "
|
|
405
|
+
"MEMPLEX_EMBEDDING_MODEL=local-onnx:/path/to/model.onnx."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if model_lookup_key in self._OFFLINE_MODELS:
|
|
409
|
+
logger.debug(
|
|
410
|
+
"Using local TF-IDF embedder for embedding model %s", model_key
|
|
411
|
+
)
|
|
412
|
+
return _SimpleTFIDFEmbedder(dimension=dimension)
|
|
413
|
+
|
|
414
|
+
if model_lookup_key.startswith(self._HF_PREFIX):
|
|
415
|
+
model_name = model_key[len(self._HF_PREFIX) :]
|
|
416
|
+
else:
|
|
417
|
+
model_name = self._MODEL_MAP.get(model_lookup_key, model_key)
|
|
262
418
|
|
|
263
419
|
try:
|
|
264
420
|
return _SentenceTransformerEmbedder(model_name, dimension)
|
|
@@ -275,3 +431,30 @@ class EmbeddingService:
|
|
|
275
431
|
exc,
|
|
276
432
|
)
|
|
277
433
|
return _SimpleTFIDFEmbedder(dimension=dimension)
|
|
434
|
+
|
|
435
|
+
@classmethod
|
|
436
|
+
def _resolve_local_onnx_model_path(
|
|
437
|
+
cls,
|
|
438
|
+
model_key: str,
|
|
439
|
+
model_lookup_key: str,
|
|
440
|
+
) -> tuple[str, bool]:
|
|
441
|
+
"""Resolve a local ONNX model path from model string or environment."""
|
|
442
|
+
for prefix in cls._LOCAL_ONNX_PREFIXES:
|
|
443
|
+
if model_lookup_key.startswith(prefix):
|
|
444
|
+
return model_key.split(":", 1)[1].strip(), True
|
|
445
|
+
|
|
446
|
+
if model_lookup_key in cls._LOCAL_ONNX_MODELS:
|
|
447
|
+
return os.getenv("MEMPLEX_LOCAL_ONNX_MODEL", "").strip(), True
|
|
448
|
+
|
|
449
|
+
if model_lookup_key == "default":
|
|
450
|
+
return os.getenv("MEMPLEX_LOCAL_ONNX_MODEL", "").strip(), False
|
|
451
|
+
|
|
452
|
+
return "", False
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _local_onnx_max_length() -> int:
|
|
456
|
+
"""Return local ONNX tokenizer max length from environment."""
|
|
457
|
+
try:
|
|
458
|
+
return int(os.getenv("MEMPLEX_LOCAL_ONNX_MAX_LENGTH", "256"))
|
|
459
|
+
except ValueError:
|
|
460
|
+
return 256
|
|
@@ -25,7 +25,6 @@ import concurrent.futures
|
|
|
25
25
|
import logging
|
|
26
26
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
27
27
|
from datetime import datetime
|
|
28
|
-
from importlib.metadata import version as pkg_version
|
|
29
28
|
from pathlib import Path
|
|
30
29
|
from typing import Dict, List, Optional
|
|
31
30
|
|
|
@@ -62,6 +61,26 @@ logger = logging.getLogger(__name__)
|
|
|
62
61
|
# ── Helper ─────────────────────────────────────────────────────────────
|
|
63
62
|
|
|
64
63
|
|
|
64
|
+
def _package_version() -> str:
|
|
65
|
+
"""Resolve Memplex version, preferring source-tree pyproject metadata."""
|
|
66
|
+
try:
|
|
67
|
+
import tomllib
|
|
68
|
+
|
|
69
|
+
pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
70
|
+
if pyproject.exists():
|
|
71
|
+
project = tomllib.loads(pyproject.read_text(encoding="utf-8")).get(
|
|
72
|
+
"project", {}
|
|
73
|
+
)
|
|
74
|
+
if project.get("name") == "memplex" and project.get("version"):
|
|
75
|
+
return str(project["version"])
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
from importlib.metadata import version as pkg_version
|
|
80
|
+
|
|
81
|
+
return pkg_version("memplex")
|
|
82
|
+
|
|
83
|
+
|
|
65
84
|
def _detect_memory_type(text: str) -> str:
|
|
66
85
|
"""Heuristic: classify text into a memory type.
|
|
67
86
|
|
|
@@ -1051,7 +1070,7 @@ class MemplexService:
|
|
|
1051
1070
|
dead_letters_pending = self._worker.dead_letters_pending()
|
|
1052
1071
|
|
|
1053
1072
|
# Version
|
|
1054
|
-
version =
|
|
1073
|
+
version = _package_version()
|
|
1055
1074
|
|
|
1056
1075
|
return {
|
|
1057
1076
|
"status": status,
|