superlocalmemory 3.3.19 → 3.3.20
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.
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +4 -3
- package/src/superlocalmemory/cli/main.py +2 -2
- package/src/superlocalmemory/core/config.py +4 -3
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/retrieval/agentic.py +89 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.20",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -252,10 +252,11 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
252
252
|
from superlocalmemory.core.config import SLMConfig
|
|
253
253
|
|
|
254
254
|
use_json = getattr(args, 'json', False)
|
|
255
|
-
|
|
255
|
+
sync_mode = getattr(args, 'sync_mode', False)
|
|
256
256
|
|
|
257
|
-
# V3.3.19:
|
|
258
|
-
|
|
257
|
+
# V3.3.19: Async by default — return instantly, process in background.
|
|
258
|
+
# Use --sync to wait for completion (e.g., when you need fact_ids back).
|
|
259
|
+
if not sync_mode:
|
|
259
260
|
import subprocess
|
|
260
261
|
cmd = [sys.executable, "-m", "superlocalmemory.cli.main", "remember", args.content]
|
|
261
262
|
if args.tags:
|
|
@@ -135,8 +135,8 @@ def main() -> None:
|
|
|
135
135
|
remember_p.add_argument("--tags", default="", help="Comma-separated tags")
|
|
136
136
|
remember_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
137
137
|
remember_p.add_argument(
|
|
138
|
-
"--
|
|
139
|
-
help="
|
|
138
|
+
"--sync", dest="sync_mode", action="store_true",
|
|
139
|
+
help="Wait for completion (default: async background processing)",
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
recall_p = sub.add_parser("recall", help="Semantic search with 4-channel retrieval")
|
|
@@ -740,9 +740,10 @@ class SLMConfig:
|
|
|
740
740
|
retrieval=RetrievalConfig(
|
|
741
741
|
# V3.3.2: ONNX cross-encoder enabled for all modes (~200MB)
|
|
742
742
|
use_cross_encoder=True,
|
|
743
|
-
#
|
|
744
|
-
#
|
|
745
|
-
|
|
743
|
+
# V3.3.19: Enable 1 round of rule-based query decomposition.
|
|
744
|
+
# The enhanced _heuristic_expand generates entity+action
|
|
745
|
+
# sub-queries that dramatically improve multi-hop retrieval.
|
|
746
|
+
agentic_max_rounds=1,
|
|
746
747
|
),
|
|
747
748
|
math=MathConfig(
|
|
748
749
|
sheaf_contradiction_threshold=0.45, # 768d threshold
|
|
@@ -157,13 +157,17 @@ def run_recall(
|
|
|
157
157
|
response = retrieval_engine.recall(query, profile_id, m, limit)
|
|
158
158
|
|
|
159
159
|
# Agentic sufficiency verification
|
|
160
|
+
# V3.3.19: Only trigger for multi_hop queries in Mode A (rule-based).
|
|
161
|
+
# Single-hop/factual/temporal queries get WORSE with decomposition —
|
|
162
|
+
# sub-query noise dilutes precision. Mode C (LLM) can trigger broadly.
|
|
160
163
|
agentic_rounds = config.retrieval.agentic_max_rounds
|
|
161
164
|
if agentic_rounds > 0 and response.results:
|
|
162
165
|
max_score = max((r.score for r in response.results), default=0.0)
|
|
166
|
+
has_llm = llm is not None and getattr(llm, "is_available", False)
|
|
163
167
|
should_trigger = (
|
|
164
|
-
|
|
165
|
-
or
|
|
166
|
-
or len(response.results) < 3
|
|
168
|
+
response.query_type == "multi_hop"
|
|
169
|
+
or (has_llm and max_score < config.retrieval.agentic_confidence_threshold)
|
|
170
|
+
or (has_llm and len(response.results) < 3)
|
|
167
171
|
)
|
|
168
172
|
if should_trigger:
|
|
169
173
|
try:
|
|
@@ -31,7 +31,10 @@ logger = logging.getLogger(__name__)
|
|
|
31
31
|
|
|
32
32
|
_MAX_ROUNDS = 2
|
|
33
33
|
_SUFFICIENCY_SCORE_THRESHOLD = 0.6
|
|
34
|
-
|
|
34
|
+
# V3.3.19: Removed "temporal" from skip list. S15's lesson was with
|
|
35
|
+
# weak alias expansion. The new rule-based decomposer (v3.3.19) helps
|
|
36
|
+
# temporal queries by generating entity+action sub-queries.
|
|
37
|
+
_SKIP_TYPES: frozenset[str] = frozenset() # No types skipped
|
|
35
38
|
|
|
36
39
|
_SUFFICIENCY_SYSTEM = (
|
|
37
40
|
"You evaluate whether retrieved context is sufficient to answer a query. "
|
|
@@ -241,22 +244,91 @@ class AgenticRetriever:
|
|
|
241
244
|
def _heuristic_expand(
|
|
242
245
|
self, query: str, profile_id: str,
|
|
243
246
|
) -> list[str]:
|
|
244
|
-
"""Mode A:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
for
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
247
|
+
"""Mode A: rule-based query decomposition (no LLM).
|
|
248
|
+
|
|
249
|
+
V3.3.19: Full rewrite. Generates targeted sub-queries by:
|
|
250
|
+
1. Extracting person/place names (real proper nouns only)
|
|
251
|
+
2. Extracting action/event keywords (non-stopwords minus entities)
|
|
252
|
+
3. Combining entity + action for focused retrieval
|
|
253
|
+
4. Entity-only and action-only lookups for broader context
|
|
254
|
+
|
|
255
|
+
For LoCoMo "When did [Person] [Action]?" patterns, this generates:
|
|
256
|
+
"Caroline LGBTQ support group" (entity + action)
|
|
257
|
+
"Caroline" (entity only)
|
|
258
|
+
"LGBTQ support group" (action only)
|
|
259
|
+
"""
|
|
260
|
+
sub_queries: list[str] = []
|
|
261
|
+
|
|
262
|
+
# Extract REAL proper nouns from original query (not title-cased)
|
|
263
|
+
# This avoids the extract_query_entities trap where "Support Group"
|
|
264
|
+
# from title-casing gets treated as entities.
|
|
265
|
+
_STARTERS = {
|
|
266
|
+
"What", "Where", "Who", "Which", "How", "When", "Does", "Did",
|
|
267
|
+
"Can", "Could", "Would", "Should", "Are", "Is", "Was", "Were",
|
|
268
|
+
"Has", "Have", "The", "Tell", "Do",
|
|
269
|
+
}
|
|
270
|
+
entities = [
|
|
271
|
+
m for m in re.findall(r"\b[A-Z][a-z]{2,}\b", query)
|
|
272
|
+
if m not in _STARTERS
|
|
273
|
+
]
|
|
274
|
+
# Also grab all-caps abbreviations (LGBTQ, MIT, NYC)
|
|
275
|
+
abbrevs = re.findall(r"\b[A-Z]{2,}\b", query)
|
|
276
|
+
entities.extend(abbrevs)
|
|
277
|
+
|
|
278
|
+
# Extract action/event keywords (remove question words + entity names)
|
|
279
|
+
_STOP = {
|
|
280
|
+
"when", "did", "does", "do", "what", "where", "who", "which",
|
|
281
|
+
"how", "is", "was", "were", "are", "has", "have", "had",
|
|
282
|
+
"the", "a", "an", "to", "for", "of", "in", "on", "at",
|
|
283
|
+
"and", "or", "but", "with", "from", "about", "that", "this",
|
|
284
|
+
"it", "they", "she", "he", "her", "his", "their", "its",
|
|
285
|
+
"been", "being", "would", "could", "should", "will", "can",
|
|
286
|
+
"may", "might", "not", "no", "so", "if", "by", "up",
|
|
287
|
+
"go", "going", "went", "get", "got", "ago",
|
|
288
|
+
"many", "much", "some", "any", "ever",
|
|
289
|
+
}
|
|
290
|
+
entity_lower = {e.lower() for e in entities}
|
|
291
|
+
words = re.sub(r"[^\w\s]", "", query.lower()).split()
|
|
292
|
+
action_words = [
|
|
293
|
+
w for w in words
|
|
294
|
+
if w not in _STOP and w not in entity_lower and len(w) > 2
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
# Strategy 1: Entity + action keywords (most targeted)
|
|
298
|
+
if entities and action_words:
|
|
299
|
+
action_phrase = " ".join(action_words)
|
|
300
|
+
for ent in entities[:2]:
|
|
301
|
+
sub_queries.append(f"{ent} {action_phrase}")
|
|
302
|
+
|
|
303
|
+
# Strategy 2: Action keywords only (finds the event regardless of entity)
|
|
304
|
+
if action_words:
|
|
305
|
+
sub_queries.append(" ".join(action_words))
|
|
306
|
+
|
|
307
|
+
# Strategy 3: Entity-only lookup (broad context)
|
|
308
|
+
for ent in entities[:2]:
|
|
309
|
+
sub_queries.append(ent)
|
|
310
|
+
|
|
311
|
+
# Strategy 4: Alias expansion (original approach, still useful)
|
|
312
|
+
if self._db is not None:
|
|
313
|
+
for name in entities[:2]:
|
|
314
|
+
entity = self._db.get_entity_by_name(name, profile_id)
|
|
315
|
+
if entity:
|
|
316
|
+
try:
|
|
317
|
+
aliases = self._db.get_aliases_for_entity(entity.entity_id)
|
|
318
|
+
for a in aliases[:2]:
|
|
319
|
+
sub_queries.append(f"{a.alias} {' '.join(action_words)}")
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
# Deduplicate, limit to 3 sub-queries (keep round 2 fast)
|
|
324
|
+
seen: set[str] = set()
|
|
325
|
+
unique: list[str] = []
|
|
326
|
+
for sq in sub_queries:
|
|
327
|
+
sq_lower = sq.strip().lower()
|
|
328
|
+
if sq_lower and sq_lower not in seen and sq_lower != query.lower():
|
|
329
|
+
seen.add(sq_lower)
|
|
330
|
+
unique.append(sq.strip())
|
|
331
|
+
return unique[:3]
|
|
260
332
|
|
|
261
333
|
|
|
262
334
|
# ---------------------------------------------------------------------------
|