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.
Files changed (123) hide show
  1. {memplex-3.2.5 → memplex-3.2.7}/PKG-INFO +5 -2
  2. {memplex-3.2.5 → memplex-3.2.7}/README.md +48 -5
  3. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
  4. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/scripts/hook-runner.py +27 -1
  5. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/agent_installer.py +1 -1
  6. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/cli.py +5 -13
  7. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/mcp_server.py +1 -1
  8. {memplex-3.2.5 → memplex-3.2.7}/memplex/config.py +2 -4
  9. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/term_mapper.py +8 -1
  10. {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/embedding.py +191 -8
  11. {memplex-3.2.5 → memplex-3.2.7}/memplex/service.py +21 -2
  12. memplex-3.2.7/memplex/storage/lite/search_index.py +305 -0
  13. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/lite/store.py +76 -63
  14. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/PKG-INFO +5 -2
  15. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/SOURCES.txt +3 -0
  16. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/requires.txt +5 -1
  17. {memplex-3.2.5 → memplex-3.2.7}/pyproject.toml +6 -2
  18. {memplex-3.2.5 → memplex-3.2.7}/tests/test_associators.py +15 -0
  19. {memplex-3.2.5 → memplex-3.2.7}/tests/test_config.py +5 -0
  20. memplex-3.2.7/tests/test_e2e_robustness.py +203 -0
  21. memplex-3.2.7/tests/test_embedding.py +187 -0
  22. {memplex-3.2.5 → memplex-3.2.7}/tests/test_hooks.py +9 -2
  23. {memplex-3.2.5 → memplex-3.2.7}/tests/test_install_scripts.py +3 -3
  24. {memplex-3.2.5 → memplex-3.2.7}/tests/test_storage.py +144 -0
  25. {memplex-3.2.5 → memplex-3.2.7}/LICENSE +0 -0
  26. {memplex-3.2.5 → memplex-3.2.7}/memplex/__init__.py +0 -0
  27. {memplex-3.2.5 → memplex-3.2.7}/memplex/__main__.py +0 -0
  28. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/.mcp.json +0 -0
  29. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/__init__.py +0 -0
  30. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/hooks/hooks.json +0 -0
  31. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
  32. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
  33. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
  34. {memplex-3.2.5 → memplex-3.2.7}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
  35. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/__init__.py +0 -0
  36. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/agent_runtime.py +0 -0
  37. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/claude_skill.py +0 -0
  38. {memplex-3.2.5 → memplex-3.2.7}/memplex/adapters/http_api.py +0 -0
  39. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/__init__.py +0 -0
  40. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/base.py +0 -0
  41. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/benchmark_cli.py +0 -0
  42. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/evaluator.py +0 -0
  43. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/loader.py +0 -0
  44. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/locomo.py +0 -0
  45. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/memory_eval.py +0 -0
  46. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/memory_metrics.py +0 -0
  47. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/metrics.py +0 -0
  48. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/nq_trivia.py +0 -0
  49. {memplex-3.2.5 → memplex-3.2.7}/memplex/benchmarks/popqa_hotpot.py +0 -0
  50. {memplex-3.2.5 → memplex-3.2.7}/memplex/compaction.py +0 -0
  51. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/__init__.py +0 -0
  52. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/__init__.py +0 -0
  53. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/domain_classifier.py +0 -0
  54. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/entity_aligner.py +0 -0
  55. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/associator/ref_linker.py +0 -0
  56. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/dictionaries/__init__.py +0 -0
  57. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/engine.py +0 -0
  58. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/__init__.py +0 -0
  59. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/docx.py +0 -0
  60. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/image.py +0 -0
  61. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/markdown.py +0 -0
  62. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/pdf.py +0 -0
  63. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/extractors/vision_mapper.py +0 -0
  64. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/__init__.py +0 -0
  65. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/clipboard.py +0 -0
  66. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/file_handler.py +0 -0
  67. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/handlers/url_handler.py +0 -0
  68. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/__init__.py +0 -0
  69. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/collector.py +0 -0
  70. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/hook_event.py +0 -0
  71. {memplex-3.2.5 → memplex-3.2.7}/memplex/core/hooks/registry.py +0 -0
  72. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/__init__.py +0 -0
  73. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/enhancer.py +0 -0
  74. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/fallback_chain.py +0 -0
  75. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/injection_guard.py +0 -0
  76. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/provider.py +0 -0
  77. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/__init__.py +0 -0
  78. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/anthropic.py +0 -0
  79. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/local.py +0 -0
  80. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/providers/rule_based.py +0 -0
  81. {memplex-3.2.5 → memplex-3.2.7}/memplex/llm/sanitizer.py +0 -0
  82. {memplex-3.2.5 → memplex-3.2.7}/memplex/logging_utils.py +0 -0
  83. {memplex-3.2.5 → memplex-3.2.7}/memplex/metrics.py +0 -0
  84. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/__init__.py +0 -0
  85. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/feedback.py +0 -0
  86. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/graph.py +0 -0
  87. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/memory.py +0 -0
  88. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/misc.py +0 -0
  89. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/paragraph.py +0 -0
  90. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/search.py +0 -0
  91. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/source.py +0 -0
  92. {memplex-3.2.5 → memplex-3.2.7}/memplex/models/task.py +0 -0
  93. {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/__init__.py +0 -0
  94. {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/graph_builder.py +0 -0
  95. {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/__init__.py +0 -0
  96. {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/confidence_calculator.py +0 -0
  97. {memplex-3.2.5 → memplex-3.2.7}/memplex/processing/merger/conflict_resolver.py +0 -0
  98. {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/__init__.py +0 -0
  99. {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/dedup.py +0 -0
  100. {memplex-3.2.5 → memplex-3.2.7}/memplex/retrieval/reranker.py +0 -0
  101. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/__init__.py +0 -0
  102. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/base.py +0 -0
  103. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/changelog.py +0 -0
  104. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/feedback.py +0 -0
  105. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/lite/__init__.py +0 -0
  106. {memplex-3.2.5 → memplex-3.2.7}/memplex/storage/vector.py +0 -0
  107. {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/__init__.py +0 -0
  108. {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/community.py +0 -0
  109. {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/compiler.py +0 -0
  110. {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/generator.py +0 -0
  111. {memplex-3.2.5 → memplex-3.2.7}/memplex/wiki/search.py +0 -0
  112. {memplex-3.2.5 → memplex-3.2.7}/memplex/worker.py +0 -0
  113. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/dependency_links.txt +0 -0
  114. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/entry_points.txt +0 -0
  115. {memplex-3.2.5 → memplex-3.2.7}/memplex.egg-info/top_level.txt +0 -0
  116. {memplex-3.2.5 → memplex-3.2.7}/setup.cfg +0 -0
  117. {memplex-3.2.5 → memplex-3.2.7}/tests/test_agent_hot_paths.py +0 -0
  118. {memplex-3.2.5 → memplex-3.2.7}/tests/test_agent_runtime.py +0 -0
  119. {memplex-3.2.5 → memplex-3.2.7}/tests/test_core_engine.py +0 -0
  120. {memplex-3.2.5 → memplex-3.2.7}/tests/test_graph_builder.py +0 -0
  121. {memplex-3.2.5 → memplex-3.2.7}/tests/test_llm.py +0 -0
  122. {memplex-3.2.5 → memplex-3.2.7}/tests/test_models.py +0 -0
  123. {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.5
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.5`, detects local
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.5
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
- - [Hot-Path Smoke Plan](docs/agent-hot-path-smoke.md): how real agent runtime
114
- paths are tested.
156
+ - [Release Automation](docs/release-automation.md): npm token handling and
157
+ automated npm publishing.
115
158
 
116
159
  ## From Source
117
160
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memplex",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "description": "Multi-agent memory system -- persistent knowledge graph with 3-layer retrieval, compaction, and wiki",
5
5
  "author": {
6
6
  "name": "articultur"
@@ -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 = getattr(memplex, "__version__", "unknown")
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')}")
@@ -955,4 +955,4 @@ def _package_version() -> str:
955
955
  try:
956
956
  return pkg_version("memplex")
957
957
  except Exception:
958
- return "3.2.5"
958
+ return "3.2.7"
@@ -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") == "ok" else 1
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")
@@ -331,7 +331,7 @@ class MCPServer:
331
331
  },
332
332
  "serverInfo": {
333
333
  "name": "memplex",
334
- "version": "3.2.5",
334
+ "version": "3.2.7",
335
335
  },
336
336
  }
337
337
 
@@ -29,7 +29,7 @@ class StorageConfig:
29
29
  class EmbeddingConfig:
30
30
  """Embedding model configuration."""
31
31
 
32
- model: str = "default" # default | bge-m3 | bge-small | openai
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 (if available)."""
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 multiple embedding models, configurable dimension, batch size,
4
- and Contextual Retrieval (Anthropic's document-context prefix injection).
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
- """Fallback embedder when sentence-transformers is unavailable.
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. ``"default"`` maps to
130
- ``all-MiniLM-L6-v2``. Falls back to TF-IDF when
131
- sentence-transformers is not installed.
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
- "default": "all-MiniLM-L6-v2",
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
- model_name = self._MODEL_MAP.get(model, model)
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 = pkg_version("memplex")
1073
+ version = _package_version()
1055
1074
 
1056
1075
  return {
1057
1076
  "status": status,