memorius 0.4.0__tar.gz → 0.4.2__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 (71) hide show
  1. {memorius-0.4.0/memorius.egg-info → memorius-0.4.2}/PKG-INFO +8 -1
  2. {memorius-0.4.0 → memorius-0.4.2}/README.md +7 -0
  3. {memorius-0.4.0 → memorius-0.4.2}/memorius/__init__.py +1 -1
  4. {memorius-0.4.0 → memorius-0.4.2}/memorius/cli/main.py +158 -0
  5. {memorius-0.4.0 → memorius-0.4.2}/memorius/config.py +14 -0
  6. {memorius-0.4.0 → memorius-0.4.2}/memorius/embeddings.py +23 -0
  7. memorius-0.4.2/memorius/model_download.py +183 -0
  8. {memorius-0.4.0 → memorius-0.4.2}/memorius/rest_server.py +4 -3
  9. memorius-0.4.2/memorius/web_search.py +191 -0
  10. {memorius-0.4.0 → memorius-0.4.2/memorius.egg-info}/PKG-INFO +8 -1
  11. {memorius-0.4.0 → memorius-0.4.2}/memorius.egg-info/SOURCES.txt +5 -1
  12. {memorius-0.4.0 → memorius-0.4.2}/pyproject.toml +1 -1
  13. memorius-0.4.2/tests/test_encoding_regression.py +135 -0
  14. {memorius-0.4.0 → memorius-0.4.2}/tests/test_integration.py +27 -9
  15. memorius-0.4.2/tests/test_web_search.py +213 -0
  16. {memorius-0.4.0 → memorius-0.4.2}/LICENSE +0 -0
  17. {memorius-0.4.0 → memorius-0.4.2}/MANIFEST.in +0 -0
  18. {memorius-0.4.0 → memorius-0.4.2}/examples/quickstart.py +0 -0
  19. {memorius-0.4.0 → memorius-0.4.2}/hooks/memorius-hook.sh +0 -0
  20. {memorius-0.4.0 → memorius-0.4.2}/manifest.yaml +0 -0
  21. {memorius-0.4.0 → memorius-0.4.2}/memorius/cli/__init__.py +0 -0
  22. {memorius-0.4.0 → memorius-0.4.2}/memorius/cli/obsidian.py +0 -0
  23. {memorius-0.4.0 → memorius-0.4.2}/memorius/consolidation.py +0 -0
  24. {memorius-0.4.0 → memorius-0.4.2}/memorius/context_inject.py +0 -0
  25. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/LICENSE +0 -0
  26. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/__init__.py +0 -0
  27. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/examples/__init__.py +0 -0
  28. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/examples/quickstart.py +0 -0
  29. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/manifest.yaml +0 -0
  30. {memorius-0.4.0 → memorius-0.4.2}/memorius/data/memorius-hook.sh +0 -0
  31. {memorius-0.4.0 → memorius-0.4.2}/memorius/factcheck.py +0 -0
  32. {memorius-0.4.0 → memorius-0.4.2}/memorius/graph.py +0 -0
  33. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/__init__.py +0 -0
  34. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/__init__.py +0 -0
  35. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/base.py +0 -0
  36. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/claude_code.py +0 -0
  37. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/codex.py +0 -0
  38. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/gemini.py +0 -0
  39. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/generic.py +0 -0
  40. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/openclaude.py +0 -0
  41. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/openclaw.py +0 -0
  42. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/opencode.py +0 -0
  43. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/adapters/pi.py +0 -0
  44. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/cli.py +0 -0
  45. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/engine.py +0 -0
  46. {memorius-0.4.0 → memorius-0.4.2}/memorius/hooks/models.py +0 -0
  47. {memorius-0.4.0 → memorius-0.4.2}/memorius/llm_extract.py +0 -0
  48. {memorius-0.4.0 → memorius-0.4.2}/memorius/mcp_server.py +0 -0
  49. {memorius-0.4.0 → memorius-0.4.2}/memorius/meta_store.py +0 -0
  50. {memorius-0.4.0 → memorius-0.4.2}/memorius/models.py +0 -0
  51. {memorius-0.4.0 → memorius-0.4.2}/memorius/normalizers/__init__.py +0 -0
  52. {memorius-0.4.0 → memorius-0.4.2}/memorius/normalizers/cli.py +0 -0
  53. {memorius-0.4.0 → memorius-0.4.2}/memorius/plugin_gen/__init__.py +0 -0
  54. {memorius-0.4.0 → memorius-0.4.2}/memorius/plugin_gen/cli.py +0 -0
  55. {memorius-0.4.0 → memorius-0.4.2}/memorius/session.py +0 -0
  56. {memorius-0.4.0 → memorius-0.4.2}/memorius/temporal.py +0 -0
  57. {memorius-0.4.0 → memorius-0.4.2}/memorius/utils.py +0 -0
  58. {memorius-0.4.0 → memorius-0.4.2}/memorius/validation.py +0 -0
  59. {memorius-0.4.0 → memorius-0.4.2}/memorius/vault.py +0 -0
  60. {memorius-0.4.0 → memorius-0.4.2}/memorius/vector_store.py +0 -0
  61. {memorius-0.4.0 → memorius-0.4.2}/memorius.egg-info/dependency_links.txt +0 -0
  62. {memorius-0.4.0 → memorius-0.4.2}/memorius.egg-info/entry_points.txt +0 -0
  63. {memorius-0.4.0 → memorius-0.4.2}/memorius.egg-info/requires.txt +0 -0
  64. {memorius-0.4.0 → memorius-0.4.2}/memorius.egg-info/top_level.txt +0 -0
  65. {memorius-0.4.0 → memorius-0.4.2}/setup.cfg +0 -0
  66. {memorius-0.4.0 → memorius-0.4.2}/tests/test_core.py +0 -0
  67. {memorius-0.4.0 → memorius-0.4.2}/tests/test_features.py +0 -0
  68. {memorius-0.4.0 → memorius-0.4.2}/tests/test_graph.py +0 -0
  69. {memorius-0.4.0 → memorius-0.4.2}/tests/test_polish_regressions.py +0 -0
  70. {memorius-0.4.0 → memorius-0.4.2}/tests/test_temporal.py +0 -0
  71. {memorius-0.4.0 → memorius-0.4.2}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorius
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Self-contained memory vault for any AI agent — vector search, session diaries, and agent-agnostic hooks.
5
5
  Author-email: dimonapatrick243 <dimona.patrick@gmail.com>
6
6
  License-Expression: MIT
@@ -53,6 +53,10 @@ Dynamic: license-file
53
53
  > 🧠 **Works with Claude Code, Codex CLI, Gemini CLI, OpenClaude, OpenCode, Pi, and any MCP-compatible agent.**
54
54
  >
55
55
  > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
56
+ >
57
+ > If you find this project useful, leaving a star ⭐ on the repository is the best way to support my work!
58
+
59
+
56
60
 
57
61
  ## Why Memorius?
58
62
 
@@ -99,6 +103,9 @@ memorius status
99
103
  memorius diary --session "session-001" --title "Research findings"
100
104
  ```
101
105
 
106
+ <img width="979" height="632" alt="Screenshot from 2026-06-18 15-14-39" src="https://github.com/user-attachments/assets/6cbca59b-1a3c-4b1d-85e8-dd5b256b9061" />
107
+
108
+
102
109
  ## Architecture
103
110
 
104
111
  ```
@@ -16,6 +16,10 @@
16
16
  > 🧠 **Works with Claude Code, Codex CLI, Gemini CLI, OpenClaude, OpenCode, Pi, and any MCP-compatible agent.**
17
17
  >
18
18
  > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
19
+ >
20
+ > If you find this project useful, leaving a star ⭐ on the repository is the best way to support my work!
21
+
22
+
19
23
 
20
24
  ## Why Memorius?
21
25
 
@@ -62,6 +66,9 @@ memorius status
62
66
  memorius diary --session "session-001" --title "Research findings"
63
67
  ```
64
68
 
69
+ <img width="979" height="632" alt="Screenshot from 2026-06-18 15-14-39" src="https://github.com/user-attachments/assets/6cbca59b-1a3c-4b1d-85e8-dd5b256b9061" />
70
+
71
+
65
72
  ## Architecture
66
73
 
67
74
  ```
@@ -26,4 +26,4 @@ Architecture:
26
26
  memorius: Main CLI — init, mine, search, diary, status, serve, hook
27
27
  """
28
28
 
29
- __version__ = "0.4.0"
29
+ __version__ = "0.4.2"
@@ -2,6 +2,7 @@
2
2
 
3
3
  Usage:
4
4
  memorius init Initialize a new vault
5
+ memorius setup Download ONNX model and initialize vault
5
6
  memorius status Show vault status
6
7
  memorius store <text> Store a memory
7
8
  memorius search <query> Semantic search across memories
@@ -15,6 +16,7 @@ Usage:
15
16
  memorius obsidian list List notes in an Obsidian vault
16
17
  memorius obsidian import Import Obsidian notes as memories
17
18
  memorius obsidian export Export memories as Obsidian notes
19
+ memorius web <query> Search the internet (web fallback)
18
20
  """
19
21
 
20
22
  from __future__ import annotations
@@ -28,6 +30,7 @@ from pathlib import Path
28
30
  from memorius import __version__
29
31
  from memorius.config import load_config, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH
30
32
  from memorius.vault import VaultEngine
33
+ from memorius.web_search import web_fallback_enabled, should_fallback
31
34
 
32
35
  logging.basicConfig(
33
36
  level=logging.WARNING,
@@ -36,7 +39,21 @@ logging.basicConfig(
36
39
  logger = logging.getLogger("memorius.cli")
37
40
 
38
41
 
42
+ def _ensure_utf8_streams():
43
+ """Force UTF-8 on stdout/stderr so emoji/icons (e.g. the factcheck verdict
44
+ glyphs) don't crash on Windows cp1252 consoles. Best-effort: a stream
45
+ that can't be reconfigured (e.g. piped to a file) is left untouched."""
46
+ for stream in (sys.stdout, sys.stderr):
47
+ reconfigure = getattr(stream, "reconfigure", None)
48
+ if callable(reconfigure):
49
+ try:
50
+ reconfigure(encoding="utf-8", errors="replace")
51
+ except (ValueError, OSError):
52
+ pass
53
+
54
+
39
55
  def main():
56
+ _ensure_utf8_streams()
40
57
  parser = argparse.ArgumentParser(
41
58
  "memorius",
42
59
  description="Memory vault for any AI agent — store, search, and organize memories.",
@@ -48,6 +65,11 @@ def main():
48
65
  subparsers = parser.add_subparsers(dest="command")
49
66
 
50
67
  subparsers.add_parser("init", help="Initialize a new vault")
68
+
69
+ setup_p = subparsers.add_parser("setup", help="Download ONNX model and initialize vault")
70
+ setup_p.add_argument("--force", action="store_true", help="Re-download model even if exists")
71
+ setup_p.add_argument("--skip-model", action="store_true", help="Skip model download, only init vault")
72
+
51
73
  subparsers.add_parser("status", help="Show vault status")
52
74
 
53
75
  store_p = subparsers.add_parser("store", help="Store a memory")
@@ -62,6 +84,7 @@ def main():
62
84
  search_p.add_argument("--n", type=int, default=10, help="Number of results")
63
85
  search_p.add_argument("--vault", default=None, help="Filter by vault")
64
86
  search_p.add_argument("--shelf", default=None, help="Filter by shelf")
87
+ search_p.add_argument("--web", action="store_true", help="Fall back to web search if local recall is thin")
65
88
 
66
89
  mine_p = subparsers.add_parser("mine", help="Mine memories from a transcript")
67
90
  mine_p.add_argument("file", nargs="?", default=None, help="Transcript file path")
@@ -106,11 +129,18 @@ def main():
106
129
  factcheck_p = subparsers.add_parser("factcheck", help="Fact-check against stored memories")
107
130
  factcheck_p.add_argument("statement", nargs="?", default=None, help="Statement to verify")
108
131
  factcheck_p.add_argument("--vault", default=None, help="Filter by vault")
132
+ factcheck_p.add_argument("--web", action="store_true", help="Cross-check uncertain claims against the web")
109
133
 
110
134
  context_p = subparsers.add_parser("context", help="Get memory context for injection")
111
135
  context_p.add_argument("query", nargs="?", default=None, help="Topic to search for")
112
136
  context_p.add_argument("--vault", default=None, help="Filter by vault")
113
137
  context_p.add_argument("--max", type=int, default=5, help="Max items")
138
+ context_p.add_argument("--web", action="store_true", help="Augment with web results if no local context")
139
+
140
+ web_p = subparsers.add_parser("web", help="Search the internet (web fallback)")
141
+ web_p.add_argument("query", nargs="?", default=None, help="Search query")
142
+ web_p.add_argument("--max", type=int, default=5, help="Max results")
143
+ web_p.add_argument("--provider", default=None, help="Provider override (duckduckgo|mock)")
114
144
 
115
145
  profile_p = subparsers.add_parser("profile", help="Build session memory profile")
116
146
  profile_p.add_argument("session_id", nargs="?", default=None, help="Session ID")
@@ -158,6 +188,7 @@ def main():
158
188
  # Dispatch
159
189
  commands = {
160
190
  "init": cmd_init,
191
+ "setup": cmd_setup,
161
192
  "status": cmd_status,
162
193
  "store": cmd_store,
163
194
  "search": cmd_search,
@@ -173,6 +204,7 @@ def main():
173
204
  "extract": cmd_extract,
174
205
  "factcheck": cmd_factcheck,
175
206
  "context": cmd_context,
207
+ "web": cmd_web,
176
208
  "profile": cmd_profile,
177
209
  "stats": cmd_stats,
178
210
  }
@@ -199,6 +231,56 @@ def cmd_init(engine, args, config):
199
231
  print(f"Embeddings: {config.get('embeddings', {}).get('provider', 'chroma-default')}")
200
232
 
201
233
 
234
+ def cmd_setup(engine, args, config):
235
+ """Download ONNX model and initialize vault."""
236
+ from memorius.model_download import is_model_downloaded, setup_model, get_model_path
237
+
238
+ print("memorius setup")
239
+ print("=" * 50)
240
+
241
+ # Step 1: Initialize vault
242
+ print("\n1. Initializing vault...")
243
+ DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
244
+ if not DEFAULT_CONFIG_PATH.exists():
245
+ from memorius.config import DEFAULT_CONFIG_YAML
246
+ DEFAULT_CONFIG_PATH.write_text(DEFAULT_CONFIG_YAML)
247
+ print(f" Created config: {DEFAULT_CONFIG_PATH}")
248
+ else:
249
+ print(f" Config exists: {DEFAULT_CONFIG_PATH}")
250
+
251
+ engine._meta.ensure_vault("main", "Main vault")
252
+ print(" Vault initialized: main")
253
+
254
+ # Step 2: Download ONNX model
255
+ if args.skip_model:
256
+ print("\n2. Skipping model download (--skip-model)")
257
+ else:
258
+ print("\n2. Setting up ONNX model...")
259
+ if is_model_downloaded() and not args.force:
260
+ print(f" Model already downloaded: {get_model_path()}")
261
+ else:
262
+ if args.force:
263
+ print(" Force mode: re-downloading model...")
264
+ success = setup_model(force=args.force)
265
+ if not success:
266
+ print("\nSetup completed with errors.")
267
+ print("The vault is ready, but the ONNX model failed to download.")
268
+ print("You can try again later with: memorius setup --force")
269
+ return
270
+
271
+ # Step 3: Verify
272
+ print("\n3. Verification...")
273
+ status = engine.status()
274
+ print(f" Embeddings: {status['embedding_provider']} (dim={status['embedding_dimension']})")
275
+
276
+ print("\n" + "=" * 50)
277
+ print("Setup complete!")
278
+ print("\nNext steps:")
279
+ print(" memorius store 'My first memory' # Store a memory")
280
+ print(" memorius search 'first memory' # Search for it")
281
+ print(" memorius status # Check vault status")
282
+
283
+
202
284
  def cmd_status(engine, args, config):
203
285
  """Show vault status."""
204
286
  status = engine.status()
@@ -260,6 +342,24 @@ def cmd_search(engine, args, config):
260
342
  print(" ...")
261
343
  print()
262
344
 
345
+ # Web fallback — only if local recall is thin ("if needed").
346
+ if web_fallback_enabled(args, config) and should_fallback(len(results), config):
347
+ from memorius.web_search import get_web_provider
348
+ provider = get_web_provider(config)
349
+ if provider:
350
+ web = provider.search(
351
+ query,
352
+ max_results=config.get("retrieval", {}).get("web_max_results", 5),
353
+ )
354
+ print("Web results (from internet):")
355
+ if not web:
356
+ print(" (no web results)")
357
+ for r in web:
358
+ print(f" - {r.title}")
359
+ print(f" {r.url}")
360
+ if r.snippet:
361
+ print(f" {r.snippet[:160]}")
362
+
263
363
 
264
364
  def cmd_mine(engine, args, config):
265
365
  """Mine memories from a transcript."""
@@ -432,6 +532,47 @@ def cmd_factcheck(engine, args, config):
432
532
  for m in result.contradicting_memories:
433
533
  print(f" - [{m['vault']}/{m['shelf']}] {m['content'][:100]}")
434
534
 
535
+ # Web cross-check when the vault can't settle the claim.
536
+ if result.verdict in ("uncertain", "no_match") and web_fallback_enabled(args, config):
537
+ from memorius.web_search import get_web_provider
538
+ provider = get_web_provider(config)
539
+ if provider:
540
+ web = provider.search(
541
+ result.statement,
542
+ max_results=config.get("retrieval", {}).get("web_max_results", 5),
543
+ )
544
+ if web:
545
+ print(" Web cross-check:")
546
+ for r in web[:3]:
547
+ print(f" - {r.title}: {r.url}")
548
+
549
+
550
+ def cmd_web(engine, args, config):
551
+ """Search the internet (web fallback primitive)."""
552
+ from memorius.web_search import get_web_provider
553
+
554
+ query = args.query
555
+ if not query:
556
+ query = sys.stdin.read().strip()
557
+ if not query:
558
+ print("Error: query required (pass as argument or pipe to stdin)")
559
+ return
560
+
561
+ provider = get_web_provider(config, provider=args.provider)
562
+ if not provider:
563
+ print("Error: no web provider available")
564
+ return
565
+
566
+ results = provider.search(query, max_results=args.max)
567
+ print(f'Web search: "{query}"')
568
+ print(f"Results: {len(results)}")
569
+ print()
570
+ for i, r in enumerate(results, 1):
571
+ print(f"{i}. {r.title}")
572
+ print(f" {r.url}")
573
+ if r.snippet:
574
+ print(f" {r.snippet[:200]}")
575
+
435
576
 
436
577
  def cmd_context(engine, args, config):
437
578
  """Get memory context for injection."""
@@ -445,6 +586,23 @@ def cmd_context(engine, args, config):
445
586
  context = engine.get_context(query, vault=args.vault, max_items=args.max)
446
587
  if context:
447
588
  print(context)
589
+ elif web_fallback_enabled(args, config):
590
+ from memorius.web_search import get_web_provider
591
+ provider = get_web_provider(config)
592
+ if provider:
593
+ web = provider.search(
594
+ query,
595
+ max_results=config.get("retrieval", {}).get("web_max_results", 5),
596
+ )
597
+ if web:
598
+ print("No matching memories. Web results (from internet):")
599
+ for r in web:
600
+ print(f"- {r.title}")
601
+ print(f" {r.url}")
602
+ if r.snippet:
603
+ print(f" {r.snippet[:160]}")
604
+ else:
605
+ print("No relevant memories or web results found.")
448
606
  else:
449
607
  print("No relevant memories found.")
450
608
 
@@ -37,6 +37,12 @@ vault:
37
37
  hooks:
38
38
  enabled: true
39
39
  config: ~/.memorius/hooks.yaml
40
+
41
+ retrieval:
42
+ web_fallback: false # opt-in: augment thin local recall with web (local-first / privacy)
43
+ web_provider: duckduckgo # duckduckgo (keyless) | mock (tests)
44
+ web_min_results: 1 # if ZERO local hits, fall back to web
45
+ web_max_results: 5 # max web results to return
40
46
  """
41
47
 
42
48
 
@@ -98,6 +104,12 @@ def _ensure_defaults(config: dict[str, Any]):
98
104
  vault_cfg.setdefault("default", "main")
99
105
  vault_cfg.setdefault("max_note_size", 1000)
100
106
 
107
+ retrieval = config.setdefault("retrieval", {})
108
+ retrieval.setdefault("web_fallback", False)
109
+ retrieval.setdefault("web_provider", "duckduckgo")
110
+ retrieval.setdefault("web_min_results", 1)
111
+ retrieval.setdefault("web_max_results", 5)
112
+
101
113
  hooks = config.setdefault("hooks", {})
102
114
  hooks.setdefault("enabled", True)
103
115
  hooks.setdefault("config", "~/.memorius/hooks.yaml")
@@ -112,6 +124,8 @@ def _apply_env_overrides(config: dict[str, Any]):
112
124
  "MEMORIUS_REST_PORT": ("server", "rest_port"),
113
125
  "MEMORIUS_HOST": ("server", "host"),
114
126
  "MEMORIUS_DEFAULT_VAULT": ("vault", "default"),
127
+ "MEMORIUS_WEB_FALLBACK": ("retrieval", "web_fallback"),
128
+ "MEMORIUS_WEB_PROVIDER": ("retrieval", "web_provider"),
115
129
  }
116
130
  for env_key, (section, key) in overrides.items():
117
131
  val = os.environ.get(env_key)
@@ -112,6 +112,7 @@ class ChromaDefaultProvider(EmbeddingProvider):
112
112
  """ChromaDB's built-in embedding function (ONNX all-MiniLM-L6-v2).
113
113
 
114
114
  No extra dependencies — ChromaDB ships its own ONNX runtime.
115
+ Auto-downloads the ONNX model on first use if not already present.
115
116
  """
116
117
  def __init__(self):
117
118
  try:
@@ -122,9 +123,31 @@ class ChromaDefaultProvider(EmbeddingProvider):
122
123
  " pip install memorius (includes chromadb)\n"
123
124
  " or: pip install chromadb"
124
125
  )
126
+
127
+ # Auto-download ONNX model if not present
128
+ self._ensure_model_downloaded()
129
+
125
130
  self._fn = ef.DefaultEmbeddingFunction()
126
131
  self._dim = 384
127
132
 
133
+ def _ensure_model_downloaded(self):
134
+ """Ensure the ONNX model is downloaded before initializing.
135
+
136
+ Non-fatal: if the pre-download fails (e.g. no network), we warn
137
+ and continue — chromadb's DefaultEmbeddingFunction fetches its own
138
+ copy on first use.
139
+ """
140
+ from memorius.model_download import is_model_downloaded, setup_model
141
+
142
+ if not is_model_downloaded():
143
+ print("ONNX model not found. Downloading now...")
144
+ if not setup_model():
145
+ print(
146
+ "Warning: could not pre-download the ONNX model via "
147
+ "'memorius setup'. chromadb will attempt its own "
148
+ "download on first use — ensure network access."
149
+ )
150
+
128
151
  def embed(self, texts: list[str]) -> list[list[float]]:
129
152
  result = self._fn(texts)
130
153
  return result
@@ -0,0 +1,183 @@
1
+ """ONNX model download utility for memorius.
2
+
3
+ Downloads the all-MiniLM-L6-v2 ONNX model for ChromaDB's default embedding function.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import os
10
+ import sys
11
+ import tarfile
12
+ import tempfile
13
+ from pathlib import Path
14
+ from typing import Callable, Optional
15
+
16
+ import httpx
17
+
18
+ # Model configuration
19
+ MODEL_NAME = "all-MiniLM-L6-v2"
20
+ MODEL_URL = "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz"
21
+ MODEL_SHA256 = "913d7300ceae3b2dbc2c50d1de4baacab4be7b9380491c27fab7418616a16ec3"
22
+ EXTRACTED_FOLDER_NAME = "onnx"
23
+ ARCHIVE_FILENAME = "onnx.tar.gz"
24
+
25
+ # Default cache directory
26
+ DEFAULT_CACHE_DIR = Path.home() / ".cache" / "memorius" / "onnx_models"
27
+
28
+
29
+ def get_model_cache_dir() -> Path:
30
+ """Get the model cache directory from env var or default."""
31
+ env_dir = os.environ.get("MEMORIUS_MODEL_CACHE_DIR")
32
+ if env_dir:
33
+ return Path(env_dir)
34
+ return DEFAULT_CACHE_DIR
35
+
36
+
37
+ def get_model_path() -> Path:
38
+ """Get the path to the extracted ONNX model."""
39
+ cache_dir = get_model_cache_dir()
40
+ return cache_dir / MODEL_NAME / EXTRACTED_FOLDER_NAME
41
+
42
+
43
+ def is_model_downloaded() -> bool:
44
+ """Check if the ONNX model is already downloaded."""
45
+ model_path = get_model_path()
46
+ onnx_file = model_path / "model.onnx"
47
+ return onnx_file.exists()
48
+
49
+
50
+ def _verify_sha256(file_path: Path, expected_hash: str) -> bool:
51
+ """Verify the SHA256 hash of a file."""
52
+ sha256 = hashlib.sha256()
53
+ with open(file_path, "rb") as f:
54
+ for chunk in iter(lambda: f.read(8192), b""):
55
+ sha256.update(chunk)
56
+ return sha256.hexdigest() == expected_hash
57
+
58
+
59
+ def download_model(
60
+ force: bool = False,
61
+ progress_callback: Optional[Callable] = None,
62
+ ) -> Path:
63
+ """Download the ONNX model if not already present.
64
+
65
+ Args:
66
+ force: If True, re-download even if model exists
67
+ progress_callback: Optional callback for progress updates (bytes_downloaded, total_bytes)
68
+
69
+ Returns:
70
+ Path to the extracted model directory
71
+
72
+ Raises:
73
+ RuntimeError: If download or extraction fails
74
+ """
75
+ cache_dir = get_model_cache_dir()
76
+ model_dir = cache_dir / MODEL_NAME
77
+ archive_path = model_dir / ARCHIVE_FILENAME
78
+ extracted_path = model_dir / EXTRACTED_FOLDER_NAME
79
+
80
+ # Check if already downloaded
81
+ if not force and is_model_downloaded():
82
+ print(f"ONNX model already downloaded: {extracted_path}")
83
+ return extracted_path
84
+
85
+ # Create directories
86
+ model_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ print(f"Downloading ONNX model ({MODEL_NAME})...")
89
+ print(f" URL: {MODEL_URL}")
90
+ print(f" Destination: {model_dir}")
91
+
92
+ try:
93
+ # Download with progress
94
+ with httpx.stream("GET", MODEL_URL, timeout=None) as response:
95
+ response.raise_for_status()
96
+ total_bytes = int(response.headers.get("content-length", 0))
97
+
98
+ downloaded = 0
99
+ with open(archive_path, "wb") as f:
100
+ for chunk in response.iter_bytes(chunk_size=8192):
101
+ f.write(chunk)
102
+ downloaded += len(chunk)
103
+ if progress_callback:
104
+ progress_callback(downloaded, total_bytes)
105
+ elif total_bytes > 0:
106
+ percent = (downloaded / total_bytes) * 100
107
+ print(f"\r Progress: {percent:.1f}% ({downloaded}/{total_bytes} bytes)", end="", flush=True)
108
+
109
+ if total_bytes > 0:
110
+ print() # New line after progress
111
+
112
+ print("Download complete. Verifying hash...")
113
+
114
+ # Verify hash
115
+ if not _verify_sha256(archive_path, MODEL_SHA256):
116
+ raise RuntimeError(
117
+ f"Hash verification failed. Expected: {MODEL_SHA256}\n"
118
+ "The downloaded file may be corrupted. Please try again with --force."
119
+ )
120
+
121
+ print("Hash verified. Extracting...")
122
+
123
+ # Extract
124
+ with tarfile.open(archive_path, "r:gz") as tar:
125
+ tar.extractall(path=model_dir)
126
+
127
+ # Clean up archive
128
+ archive_path.unlink()
129
+
130
+ # Verify extraction
131
+ if not is_model_downloaded():
132
+ raise RuntimeError(
133
+ f"Extraction failed. Model file not found at: {extracted_path / 'model.onnx'}"
134
+ )
135
+
136
+ print(f"Model downloaded successfully: {extracted_path}")
137
+ return extracted_path
138
+
139
+ except httpx.HTTPError as e:
140
+ raise RuntimeError(
141
+ f"Failed to download ONNX model: {e}\n"
142
+ "Check your internet connection and try again."
143
+ )
144
+ except tarfile.TarError as e:
145
+ raise RuntimeError(
146
+ f"Failed to extract ONNX model: {e}\n"
147
+ "The downloaded file may be corrupted. Please try again with --force."
148
+ )
149
+ finally:
150
+ # Clean up partial downloads on failure
151
+ if archive_path.exists() and not is_model_downloaded():
152
+ archive_path.unlink()
153
+
154
+
155
+ def setup_model(force: bool = False) -> bool:
156
+ """Setup the ONNX model. Returns True if successful."""
157
+ try:
158
+ download_model(force=force)
159
+ return True
160
+ except RuntimeError as e:
161
+ print(f"Error: {e}", file=sys.stderr)
162
+ return False
163
+
164
+
165
+ if __name__ == "__main__":
166
+ # Allow running as a script for testing
167
+ import argparse
168
+
169
+ parser = argparse.ArgumentParser(description="Download ONNX model for memorius")
170
+ parser.add_argument("--force", action="store_true", help="Re-download even if model exists")
171
+ parser.add_argument("--check", action="store_true", help="Check if model is downloaded")
172
+ args = parser.parse_args()
173
+
174
+ if args.check:
175
+ if is_model_downloaded():
176
+ print(f"Model is downloaded: {get_model_path()}")
177
+ sys.exit(0)
178
+ else:
179
+ print("Model is not downloaded.")
180
+ sys.exit(1)
181
+
182
+ success = setup_model(force=args.force)
183
+ sys.exit(0 if success else 1)
@@ -28,7 +28,7 @@ class MemoriusAPI:
28
28
  def __init__(self, engine):
29
29
  self._engine = engine
30
30
  self._request_counts: dict[str, list[float]] = {} # IP -> [timestamps]
31
- self._rate_limit_max = 100 # requests per minute
31
+ self._rate_limit_max = 500 # requests per minute
32
32
  self._rate_limit_window = 60 # seconds
33
33
 
34
34
  def create_app(self):
@@ -46,9 +46,10 @@ class MemoriusAPI:
46
46
  # CORS — restrictive by default
47
47
  app.add_middleware(
48
48
  CORSMiddleware,
49
- allow_origins=["http://127.0.0.1", "http://localhost"],
50
- allow_methods=["GET", "POST"],
49
+ allow_origins=["http://127.0.0.1", "http://localhost", "http://127.0.0.1:*", "http://localhost:*", "app://obsidian.md", "file://"],
50
+ allow_methods=["GET", "POST", "OPTIONS"],
51
51
  allow_headers=["Content-Type", "Authorization"],
52
+ allow_credentials=True,
52
53
  )
53
54
 
54
55
  # API key authentication