smartmemory 1.4.26__tar.gz → 1.4.28__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.
- {smartmemory-1.4.26 → smartmemory-1.4.28}/CHANGELOG.md +20 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/PKG-INFO +2 -2
- {smartmemory-1.4.26 → smartmemory-1.4.28}/pyproject.toml +2 -2
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/cli.py +46 -1
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/storage.py +9 -0
- smartmemory-1.4.28/smartmemory_app/warm.py +84 -0
- smartmemory-1.4.28/tests/integration/test_warm.py +104 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/.github/dependabot.yml +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/.github/workflows/publish.yml +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/.github/workflows/sync-core.yml +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/.gitignore +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/LICENSE +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/LICENSE.agpl-v3 +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/LICENSE.header +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/README.md +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/plugin.json +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/scripts/generate_seed_patterns.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/__init__.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/__main__.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/async_enrichment.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/cli_code.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/cli_mcp.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/config.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/daemon.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/ai.smartmemory.daemon.plist +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/ai.smartmemory.worker.plist +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seed_patterns.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/ai-model.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/concept.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/database.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/framework.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/language.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/manifest.json +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/organization.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/platform.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/protocol.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/service.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/data/seeds/tool.jsonl +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/enrichment_queue.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/enrichment_worker.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/event_sink.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/events_server.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/distill.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/learn.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/observe.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/orient.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/persist.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/hooks/recall.sh +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/launch_metrics.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/lifecycle.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/lifecycle_api.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/lifecycle_config.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/local_api.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/patterns.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/recall_format.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/remote_backend.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/setup.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/setup_tui.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/skills/ingest.md +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/skills/orient.md +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/skills/remember.md +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/skills/search.md +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/assets/local-Clt8rYLQ.js +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/assets/local-ZCcXPKXN.css +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/icons/icon-192x192.svg +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/icons/icon-512x512.svg +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/index.html +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/logo.svg +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/static/viewer-logo.svg +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/sync.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/smartmemory_app/viewer_server.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/__init__.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/conftest.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/__init__.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_async_enrichment_e2e.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_async_enrichment_sqlite_regression.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_cli_new_subcommands.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_daemon.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_entity_ruler_patterns.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_hook_recall.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_hook_scripts.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_local_api_integration.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_recall_confidence.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_recall_flow.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/integration/test_viewer_server.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/__init__.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_async_enrichment.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_cli.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_config.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_events_server.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_launchd.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_lifecycle.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_lite_api_contract.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_local_api.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_patterns.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_recall_format.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_remote_backend.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_server_tools.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_setup.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_setup_tui.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_stale_markers.py +0 -0
- {smartmemory-1.4.26 → smartmemory-1.4.28}/tests/unit/test_storage.py +0 -0
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
Notable, **user-facing** changes to the `smartmemory` distribution package. The wrapper is thin — it pins an exact `smartmemory-core` version and the two move in lockstep — so entries here highlight what a release *delivers* (features, fixes, security), not routine version-pin bumps. For full internal detail, see [`smartmemory-core`'s CHANGELOG](https://github.com/smart-memory/smart-memory-core/blob/main/CHANGELOG.md). Loosely follows [Keep a Changelog](https://keepachangelog.com); not every patch release gets an entry.
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
|
+
### Changed (auto, lockstep) — track smartmemory-core==1.4.28 (1.4.28)
|
|
7
|
+
- Version copied from smartmemory-core 1.4.28 release (single-source lockstep).
|
|
8
|
+
|
|
9
|
+
### Changed (auto, lockstep) — track smartmemory-core==1.4.27 (1.4.27)
|
|
10
|
+
- Version copied from smartmemory-core 1.4.27 release (single-source lockstep).
|
|
11
|
+
|
|
12
|
+
### Added — `smartmemory warm` + background model warming (DIST-LITE-WARMSTART-1)
|
|
13
|
+
- New `smartmemory warm` CLI command pre-loads the local embedder (and reranker) so the
|
|
14
|
+
first `add`/`search` is instant instead of paying a cold model load (~12s, or ~38s the
|
|
15
|
+
first time the model downloads). Run once after install or before a demo. `--no-reranker`
|
|
16
|
+
warms the embedder only.
|
|
17
|
+
- The local backend now **warms the embedder in the background at construction** (daemon
|
|
18
|
+
thread) so the user's first `add()` overlaps the model load instead of paying it inline.
|
|
19
|
+
Opt out with `SMARTMEMORY_NO_WARM=1`.
|
|
20
|
+
- Direct (no-daemon) CLI ops (`add`, `recall`) now print a one-time "First run: loading
|
|
21
|
+
local models…" notice before a cold load, so first-run isn't a silent hang. (The daemon
|
|
22
|
+
path already prints "loading models" at `start`.)
|
|
23
|
+
- Paired with the core reranker fix (non-blocking model load), this removes the
|
|
24
|
+
multi-second first-run hang from the local FREE path.
|
|
25
|
+
|
|
6
26
|
### Changed (auto, lockstep) — track smartmemory-core==1.4.26 (1.4.26)
|
|
7
27
|
- Version copied from smartmemory-core 1.4.26 release (single-source lockstep).
|
|
8
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartmemory
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.28
|
|
4
4
|
License-File: LICENSE
|
|
5
5
|
License-File: LICENSE.agpl-v3
|
|
6
6
|
License-File: LICENSE.header
|
|
@@ -10,7 +10,7 @@ Requires-Dist: fastapi>=0.110
|
|
|
10
10
|
Requires-Dist: filelock>=3.12
|
|
11
11
|
Requires-Dist: httpx>=0.27
|
|
12
12
|
Requires-Dist: keyring>=24.0
|
|
13
|
-
Requires-Dist: smartmemory-core[lite]==1.4.
|
|
13
|
+
Requires-Dist: smartmemory-core[lite]==1.4.28
|
|
14
14
|
Requires-Dist: smartmemory-mcp>=0.2.0
|
|
15
15
|
Requires-Dist: textual>=8.0
|
|
16
16
|
Requires-Dist: tomli-w>=1.0
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smartmemory"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.28"
|
|
8
8
|
requires-python = ">=3.11"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"smartmemory-core[lite]==1.4.
|
|
10
|
+
"smartmemory-core[lite]==1.4.28", # EXACT pin — wrapper and core versions move in lockstep
|
|
11
11
|
"filelock>=3.12", # cross-process SQLite write locking
|
|
12
12
|
"smartmemory-mcp>=0.2.0", # unified MCP server (PLAT-MCP-UNIFY-1)
|
|
13
13
|
"httpx>=0.27", # remote API calls (DIST-LITE-5)
|
|
@@ -148,6 +148,26 @@ def restart_cmd(num_workers: int) -> None:
|
|
|
148
148
|
click.echo("Daemon ready.")
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
@cli.command("warm")
|
|
152
|
+
@click.option("--no-reranker", is_flag=True, help="Warm only the embedder, skip the reranker model.")
|
|
153
|
+
def warm_cmd(no_reranker: bool) -> None:
|
|
154
|
+
"""Pre-load the local models so the first add/search is instant.
|
|
155
|
+
|
|
156
|
+
The first add() otherwise pays a cold embedder load (~12s, or ~38s the first
|
|
157
|
+
time the model downloads) and the first search() pays a cold reranker load.
|
|
158
|
+
Run this once after install — or before a demo — to move that cost off the
|
|
159
|
+
user's first real call (DIST-LITE-WARMSTART-1).
|
|
160
|
+
"""
|
|
161
|
+
import time
|
|
162
|
+
|
|
163
|
+
from smartmemory_app.warm import warm_models
|
|
164
|
+
|
|
165
|
+
click.echo("Warming local models (one-time; subsequent runs are cached)...")
|
|
166
|
+
t0 = time.perf_counter()
|
|
167
|
+
warm_models(reranker=not no_reranker)
|
|
168
|
+
click.echo(f"Models warm in {time.perf_counter() - t0:.1f}s. First add/search will now be fast.")
|
|
169
|
+
|
|
170
|
+
|
|
151
171
|
@cli.command("status")
|
|
152
172
|
def status_cmd() -> None:
|
|
153
173
|
"""Show SmartMemory daemon status."""
|
|
@@ -229,6 +249,28 @@ def _validate_memory_type(ctx, param, value: str) -> str:
|
|
|
229
249
|
return value
|
|
230
250
|
|
|
231
251
|
|
|
252
|
+
_warm_notice_shown = False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _warm_notice() -> None:
|
|
256
|
+
"""Show a one-time notice if a direct (no-daemon) op is about to pay a cold model
|
|
257
|
+
load, so first-run isn't a silent multi-second hang (DIST-LITE-WARMSTART-1). The
|
|
258
|
+
daemon path already prints "loading models" at start; this covers direct CLI ops.
|
|
259
|
+
"""
|
|
260
|
+
global _warm_notice_shown
|
|
261
|
+
if _warm_notice_shown:
|
|
262
|
+
return
|
|
263
|
+
from smartmemory_app.warm import is_warm
|
|
264
|
+
|
|
265
|
+
if not is_warm():
|
|
266
|
+
click.echo(
|
|
267
|
+
"First run: loading local models (~10–40s, one-time). "
|
|
268
|
+
"Tip: run 'smartmemory warm' to pre-load.",
|
|
269
|
+
err=True,
|
|
270
|
+
)
|
|
271
|
+
_warm_notice_shown = True
|
|
272
|
+
|
|
273
|
+
|
|
232
274
|
@cli.command(
|
|
233
275
|
"add",
|
|
234
276
|
context_settings=dict(
|
|
@@ -278,7 +320,7 @@ def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
|
|
|
278
320
|
chunks = (
|
|
279
321
|
[raw.strip()]
|
|
280
322
|
if as_whole
|
|
281
|
-
else [
|
|
323
|
+
else [ln.strip() for ln in raw.splitlines() if ln.strip()]
|
|
282
324
|
)
|
|
283
325
|
if not chunks:
|
|
284
326
|
raise click.ClickException("Content cannot be empty.")
|
|
@@ -294,6 +336,7 @@ def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
|
|
|
294
336
|
else:
|
|
295
337
|
from smartmemory_app.storage import ingest
|
|
296
338
|
|
|
339
|
+
_warm_notice()
|
|
297
340
|
ids.append(ingest(chunk, memory_type, properties=props))
|
|
298
341
|
click.echo(f"Added {len(ids)} memories")
|
|
299
342
|
for item_id in ids:
|
|
@@ -311,6 +354,7 @@ def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
|
|
|
311
354
|
else:
|
|
312
355
|
from smartmemory_app.storage import ingest
|
|
313
356
|
|
|
357
|
+
_warm_notice()
|
|
314
358
|
click.echo(ingest(text, memory_type, properties=props))
|
|
315
359
|
|
|
316
360
|
|
|
@@ -353,6 +397,7 @@ def recall_cmd(
|
|
|
353
397
|
else:
|
|
354
398
|
from smartmemory_app.storage import recall
|
|
355
399
|
|
|
400
|
+
_warm_notice()
|
|
356
401
|
context = recall(
|
|
357
402
|
cwd,
|
|
358
403
|
top_k,
|
|
@@ -107,6 +107,15 @@ def _get_local_memory(data_dir: str | None = None) -> "SmartMemory":
|
|
|
107
107
|
event_sink=get_event_sink(), # DIST-LITE-3
|
|
108
108
|
)
|
|
109
109
|
atexit.register(_shutdown)
|
|
110
|
+
# DIST-LITE-WARMSTART-1: warm the local embedder in the background so the
|
|
111
|
+
# user's first add() overlaps the ~12s model load instead of paying it inline.
|
|
112
|
+
# Daemon thread, idempotent, opt out with SMARTMEMORY_NO_WARM=1.
|
|
113
|
+
try:
|
|
114
|
+
from smartmemory_app.warm import warm_models_background
|
|
115
|
+
|
|
116
|
+
warm_models_background()
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
110
119
|
return _memory
|
|
111
120
|
|
|
112
121
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Model pre-warming for the local FREE path (DIST-LITE-WARMSTART-1).
|
|
2
|
+
|
|
3
|
+
The first ``add()`` pays a cold embedder load (~12s, or ~38s the first time the
|
|
4
|
+
model is downloaded) and the first ``search()`` would pay a cold reranker load.
|
|
5
|
+
These helpers load the models ahead of (or in parallel with) the user's first real
|
|
6
|
+
call so the one-command "wow" moment isn't a multi-second hang.
|
|
7
|
+
|
|
8
|
+
- ``warm_models()`` — foreground; used by the ``smartmemory warm`` CLI and install
|
|
9
|
+
prefetch, where a visible one-time wait is acceptable.
|
|
10
|
+
- ``warm_models_background()`` — fire-and-forget daemon thread; used at local
|
|
11
|
+
construction so the embedder load overlaps construction + user think-time. Opt out
|
|
12
|
+
with ``SMARTMEMORY_NO_WARM=1``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_warm_started = False
|
|
24
|
+
_warm_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_warm() -> bool:
|
|
28
|
+
"""True if the local embedder model is already resident in this process.
|
|
29
|
+
|
|
30
|
+
Used to decide whether a foreground op is about to pay a cold load, so the
|
|
31
|
+
caller can show a progress notice instead of a silent hang.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
from smartmemory.plugins.embedding import EmbeddingService
|
|
35
|
+
|
|
36
|
+
return EmbeddingService._st_model is not None or EmbeddingService._pinned_local_model is not None
|
|
37
|
+
except Exception:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def warm_models(*, reranker: bool = True) -> None:
|
|
42
|
+
"""Synchronously load the local embedder (and optionally the reranker).
|
|
43
|
+
|
|
44
|
+
Never raises — a warm failure must not break the caller; the model will simply
|
|
45
|
+
load lazily on first use as before.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
from smartmemory.plugins.embedding import EmbeddingService
|
|
49
|
+
|
|
50
|
+
if EmbeddingService().warm():
|
|
51
|
+
logger.debug("Embedder warmed")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.debug("Embedder warm skipped: %s", e)
|
|
54
|
+
|
|
55
|
+
if reranker:
|
|
56
|
+
try:
|
|
57
|
+
from smartmemory.search.rerank import CrossEncoderReranker
|
|
58
|
+
|
|
59
|
+
# block=True here is intentional: the CLI/prefetch path WANTS to wait.
|
|
60
|
+
CrossEncoderReranker.get_model(block=True)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.debug("Reranker warm skipped: %s", e)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def warm_models_background(*, reranker: bool = False) -> None:
|
|
66
|
+
"""Kick a one-time daemon thread to warm models without blocking the caller.
|
|
67
|
+
|
|
68
|
+
Defaults to embedder-only (every ``add()`` needs it; the reranker already
|
|
69
|
+
background-loads lazily on first search). Opt out with ``SMARTMEMORY_NO_WARM=1``.
|
|
70
|
+
Idempotent — only the first call per process starts a thread.
|
|
71
|
+
"""
|
|
72
|
+
global _warm_started
|
|
73
|
+
if os.environ.get("SMARTMEMORY_NO_WARM"):
|
|
74
|
+
return
|
|
75
|
+
with _warm_lock:
|
|
76
|
+
if _warm_started:
|
|
77
|
+
return
|
|
78
|
+
_warm_started = True
|
|
79
|
+
threading.Thread(
|
|
80
|
+
target=warm_models,
|
|
81
|
+
kwargs={"reranker": reranker},
|
|
82
|
+
name="smartmemory-warm",
|
|
83
|
+
daemon=True,
|
|
84
|
+
).start()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""DIST-LITE-WARMSTART-1 — background model warming orchestration.
|
|
2
|
+
|
|
3
|
+
Tests the *scheduling* contract (opt-out + warm-once), not the model load itself
|
|
4
|
+
(the embedder/reranker loads are verified separately and would require a model
|
|
5
|
+
download here). Marked integration: exercises real threads.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import smartmemory_app.warm as warm
|
|
13
|
+
|
|
14
|
+
pytestmark = pytest.mark.integration
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _join_warm_threads(timeout=5):
|
|
18
|
+
for t in threading.enumerate():
|
|
19
|
+
if t.name == "smartmemory-warm":
|
|
20
|
+
t.join(timeout=timeout)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_background_warm_opts_out(monkeypatch):
|
|
24
|
+
"""SMARTMEMORY_NO_WARM=1 schedules nothing — no thread, no state change."""
|
|
25
|
+
monkeypatch.setenv("SMARTMEMORY_NO_WARM", "1")
|
|
26
|
+
warm._warm_started = False
|
|
27
|
+
before = threading.active_count()
|
|
28
|
+
|
|
29
|
+
warm.warm_models_background()
|
|
30
|
+
|
|
31
|
+
assert warm._warm_started is False
|
|
32
|
+
assert threading.active_count() == before
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_background_warm_runs_once(monkeypatch):
|
|
36
|
+
"""Repeated calls schedule the warm work exactly once per process."""
|
|
37
|
+
monkeypatch.delenv("SMARTMEMORY_NO_WARM", raising=False)
|
|
38
|
+
warm._warm_started = False
|
|
39
|
+
calls = []
|
|
40
|
+
# Substitute the downstream loader (verified for real elsewhere) so this test
|
|
41
|
+
# asserts the SCHEDULER, deterministically and without a model download.
|
|
42
|
+
monkeypatch.setattr(warm, "warm_models", lambda **kwargs: calls.append(kwargs))
|
|
43
|
+
|
|
44
|
+
warm.warm_models_background()
|
|
45
|
+
warm.warm_models_background()
|
|
46
|
+
warm.warm_models_background()
|
|
47
|
+
_join_warm_threads()
|
|
48
|
+
|
|
49
|
+
assert warm._warm_started is True
|
|
50
|
+
assert len(calls) == 1 # only the first call did work
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_warm_models_never_raises(monkeypatch):
|
|
54
|
+
"""warm_models() must swallow loader failures — a warm error can't break callers."""
|
|
55
|
+
import smartmemory.plugins.embedding as emb
|
|
56
|
+
|
|
57
|
+
def boom(self):
|
|
58
|
+
raise RuntimeError("simulated model load failure")
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(emb.EmbeddingService, "warm", boom)
|
|
61
|
+
# reranker=False so we don't trigger a real cross-encoder download here.
|
|
62
|
+
warm.warm_models(reranker=False) # must not raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_is_warm_false_when_unloaded(monkeypatch):
|
|
66
|
+
"""is_warm() reports False when no local model is resident."""
|
|
67
|
+
import smartmemory.plugins.embedding as emb
|
|
68
|
+
|
|
69
|
+
monkeypatch.setattr(emb.EmbeddingService, "_st_model", None, raising=False)
|
|
70
|
+
monkeypatch.setattr(emb.EmbeddingService, "_pinned_local_model", None, raising=False)
|
|
71
|
+
assert warm.is_warm() is False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_cli_warm_notice_prints_once_when_cold(monkeypatch, capsys):
|
|
75
|
+
"""The direct-CLI cold-load notice fires exactly once, only when not warm."""
|
|
76
|
+
from smartmemory_app import cli
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr("smartmemory_app.warm.is_warm", lambda: False)
|
|
79
|
+
cli._warm_notice_shown = False
|
|
80
|
+
|
|
81
|
+
cli._warm_notice()
|
|
82
|
+
cli._warm_notice()
|
|
83
|
+
|
|
84
|
+
err = capsys.readouterr().err
|
|
85
|
+
assert err.count("First run: loading local models") == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_add_cmd_direct_path_emits_notice(monkeypatch):
|
|
89
|
+
"""The real `add` command, on the direct (no-daemon) path with a cold model,
|
|
90
|
+
emits the warm notice before the blocking ingest."""
|
|
91
|
+
from click.testing import CliRunner
|
|
92
|
+
|
|
93
|
+
from smartmemory_app import cli
|
|
94
|
+
|
|
95
|
+
monkeypatch.setattr(cli, "_daemon_request", lambda *a, **k: None) # force direct path
|
|
96
|
+
monkeypatch.setattr("smartmemory_app.storage.ingest", lambda *a, **k: "id-123")
|
|
97
|
+
monkeypatch.setattr("smartmemory_app.warm.is_warm", lambda: False)
|
|
98
|
+
cli._warm_notice_shown = False
|
|
99
|
+
|
|
100
|
+
r = CliRunner(mix_stderr=False).invoke(cli.cli, ["add", "hello world"])
|
|
101
|
+
|
|
102
|
+
assert r.exit_code == 0, r.output
|
|
103
|
+
assert "id-123" in r.stdout
|
|
104
|
+
assert "First run: loading local models" in r.stderr
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|