superlocalmemory 4.0.4 → 4.0.6
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/CHANGELOG.md +90 -0
- package/README.md +23 -14
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -0
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +3 -2
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +2 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +6 -5
- package/plugin-src/skills/slm-graph/SKILL.md +2 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -0
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +418 -0
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +96 -28
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +88 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/core/remember_runtime.py +271 -2
- package/src/superlocalmemory/core/store_pipeline.py +100 -38
- package/src/superlocalmemory/encoding/consolidator.py +17 -47
- package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/profiles.py +19 -7
- package/src/superlocalmemory/mcp/server.py +4 -2
- package/src/superlocalmemory/mcp/tools_brain.py +54 -10
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +28 -10
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +297 -14
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +230 -24
- package/src/superlocalmemory/storage/migration_runner.py +7 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +280 -84
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Base types for the #113 bounded summary layer."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SummaryResult:
|
|
15
|
+
"""A bounded, profile-scoped, traceable summary of user memories.
|
|
16
|
+
|
|
17
|
+
Maintainer's binding constraint (issue #113 reply):
|
|
18
|
+
"views must be customizable, profile-scoped, privacy-aware, and
|
|
19
|
+
traceable back to the underlying memories rather than becoming
|
|
20
|
+
opaque generic summaries"
|
|
21
|
+
|
|
22
|
+
This dataclass enforces three of those four constraints structurally:
|
|
23
|
+
|
|
24
|
+
Traceability
|
|
25
|
+
``source_fact_ids`` carries the atomic_facts.fact_id for every fact
|
|
26
|
+
that contributed to this summary. A user can always drill back to
|
|
27
|
+
the raw memories.
|
|
28
|
+
|
|
29
|
+
Profile scope
|
|
30
|
+
``profile_id`` is mandatory; callers must never mix profiles.
|
|
31
|
+
|
|
32
|
+
Honesty / non-opaqueness
|
|
33
|
+
``coverage`` must be set to an accurate value. See the constants
|
|
34
|
+
below. A summary over 3.9% of facts that presents itself as "your
|
|
35
|
+
session" is precisely the opaque generic summary the maintainer said
|
|
36
|
+
to avoid.
|
|
37
|
+
|
|
38
|
+
Generated-by
|
|
39
|
+
``generated_by`` records whether the content is extractive
|
|
40
|
+
(deterministic, always available, Mode A default) or came from an
|
|
41
|
+
LLM (Mode B Ollama / Mode C cloud).
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
kind: "session" | "daily" | "project"
|
|
45
|
+
profile_id: Owning profile — never expose across profiles.
|
|
46
|
+
content: Human-readable summary text.
|
|
47
|
+
source_fact_ids: IDs of the atomic_facts that contributed.
|
|
48
|
+
Empty only when the underlying data does not exist.
|
|
49
|
+
coverage: One of the COVERAGE_* constants below.
|
|
50
|
+
generated_by: One of the GENERATED_BY_* constants below.
|
|
51
|
+
metadata: Extra context: date, project_path, session_id, etc.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
kind: str
|
|
55
|
+
profile_id: str
|
|
56
|
+
content: str
|
|
57
|
+
source_fact_ids: list[str]
|
|
58
|
+
coverage: str
|
|
59
|
+
generated_by: str
|
|
60
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── coverage constants ──────────────────────────────────────────────────────
|
|
64
|
+
#
|
|
65
|
+
# Use these strings; the acceptance gate checks for their presence
|
|
66
|
+
# and the values must be human-interpretable without this file.
|
|
67
|
+
|
|
68
|
+
COVERAGE_FULL = "full"
|
|
69
|
+
"""All relevant data was available and contributed to the summary."""
|
|
70
|
+
|
|
71
|
+
COVERAGE_PARTIAL = "partial"
|
|
72
|
+
"""Some data was available. Session summaries are always at most partial
|
|
73
|
+
because only ~3.9% of facts carry a session_id on a real store."""
|
|
74
|
+
|
|
75
|
+
COVERAGE_INSUFFICIENT = "insufficient"
|
|
76
|
+
"""Too few facts to produce a meaningful summary (below MIN_FACTS threshold)."""
|
|
77
|
+
|
|
78
|
+
COVERAGE_NO_SESSION = "no_session"
|
|
79
|
+
"""Session ID not found, or the session has no associated facts."""
|
|
80
|
+
|
|
81
|
+
COVERAGE_UNAVAILABLE = "unavailable"
|
|
82
|
+
"""Required data does not exist or a query error prevented access."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── generated_by constants ──────────────────────────────────────────────────
|
|
86
|
+
#
|
|
87
|
+
# extractive is the deterministic fallback, ALWAYS available.
|
|
88
|
+
# Mode A users never get anything else. Mode B/C users fall back when
|
|
89
|
+
# Ollama or the cloud is down — silence is not an option.
|
|
90
|
+
|
|
91
|
+
GENERATED_BY_EXTRACTIVE = "extractive"
|
|
92
|
+
"""Deterministic extractive summary — no LLM. Always available."""
|
|
93
|
+
|
|
94
|
+
GENERATED_BY_LLM_B = "llm_b"
|
|
95
|
+
"""Ollama local LLM (Mode B). Falls back to extractive if unavailable."""
|
|
96
|
+
|
|
97
|
+
GENERATED_BY_LLM_C = "llm_c"
|
|
98
|
+
"""Cloud LLM (Mode C). Falls back via llm_b to extractive."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_mode_str(config: object | None) -> str:
|
|
102
|
+
"""Extract the operating mode string ('a', 'b', or 'c') from a config."""
|
|
103
|
+
if config is None:
|
|
104
|
+
return "a"
|
|
105
|
+
m = getattr(config, "mode", None)
|
|
106
|
+
if m is None:
|
|
107
|
+
return "a"
|
|
108
|
+
return getattr(m, "value", str(m)).lower()
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Daily Reflection — issue #113 bounded summary.
|
|
6
|
+
|
|
7
|
+
Produces a human-readable summary of all facts recorded on a specific date,
|
|
8
|
+
grouped by time of day and sorted by importance.
|
|
9
|
+
|
|
10
|
+
MEASURED DATA REALITY
|
|
11
|
+
---------------------
|
|
12
|
+
On a real 3,294-fact store there are 48 distinct active days.
|
|
13
|
+
Per-day volumes range from 4 to 454 facts. Daily reflections are
|
|
14
|
+
structurally viable for this store.
|
|
15
|
+
|
|
16
|
+
DETERMINISTIC FALLBACK
|
|
17
|
+
-----------------------
|
|
18
|
+
The extractive path groups facts by topic entities and summarises the top-N
|
|
19
|
+
by importance. It never calls an LLM and never fails to return a result.
|
|
20
|
+
Mode B/C enrichment is attempted when configured, but falls back to extractive
|
|
21
|
+
on any failure — silence is not acceptable.
|
|
22
|
+
|
|
23
|
+
HOT PATH EXCLUSION
|
|
24
|
+
-------------------
|
|
25
|
+
This module must NEVER be imported from core/recall_pipeline.py or
|
|
26
|
+
core/store_pipeline.py.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import sqlite3
|
|
33
|
+
from datetime import date
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from .base import (
|
|
37
|
+
COVERAGE_FULL,
|
|
38
|
+
COVERAGE_INSUFFICIENT,
|
|
39
|
+
COVERAGE_UNAVAILABLE,
|
|
40
|
+
GENERATED_BY_EXTRACTIVE,
|
|
41
|
+
GENERATED_BY_LLM_B,
|
|
42
|
+
GENERATED_BY_LLM_C,
|
|
43
|
+
SummaryResult,
|
|
44
|
+
get_mode_str,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("superlocalmemory.summaries.daily")
|
|
48
|
+
|
|
49
|
+
_MIN_FACTS = 2 # Below this threshold, coverage=insufficient
|
|
50
|
+
_BODY_FACTS = 10 # Top-N facts shown in extractive summary
|
|
51
|
+
_MAX_FACT_CHARS = 300 # Per-fact character cap in the body
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_daily_reflection(
|
|
55
|
+
db_path: str | Path,
|
|
56
|
+
target_date: str | date,
|
|
57
|
+
profile_id: str = "default",
|
|
58
|
+
config: object | None = None,
|
|
59
|
+
) -> SummaryResult:
|
|
60
|
+
"""Generate a Daily Reflection for a specific date.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
db_path: Path to memory.db.
|
|
64
|
+
target_date: The date to reflect on. Accepts ``date`` objects or
|
|
65
|
+
ISO-format strings ("2026-08-17").
|
|
66
|
+
profile_id: Profile scope — never mix profiles.
|
|
67
|
+
config: Optional SLMConfig for LLM enrichment. None = Mode A
|
|
68
|
+
(extractive, always deterministic).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
SummaryResult with source_fact_ids for every contributing fact.
|
|
72
|
+
Never returns None.
|
|
73
|
+
"""
|
|
74
|
+
db_path = Path(db_path)
|
|
75
|
+
date_str = target_date.isoformat() if isinstance(target_date, date) else str(target_date)
|
|
76
|
+
|
|
77
|
+
# ── query ────────────────────────────────────────────────────────────────
|
|
78
|
+
try:
|
|
79
|
+
conn = sqlite3.connect(str(db_path), timeout=5.0)
|
|
80
|
+
conn.row_factory = sqlite3.Row
|
|
81
|
+
conn.execute("PRAGMA query_only=ON")
|
|
82
|
+
try:
|
|
83
|
+
rows = conn.execute(
|
|
84
|
+
"""
|
|
85
|
+
SELECT fact_id, content, created_at, importance,
|
|
86
|
+
canonical_entities_json, lifecycle
|
|
87
|
+
FROM atomic_facts
|
|
88
|
+
WHERE profile_id = ?
|
|
89
|
+
AND DATE(created_at) = ?
|
|
90
|
+
AND lifecycle != 'archived'
|
|
91
|
+
ORDER BY importance DESC, created_at ASC
|
|
92
|
+
""",
|
|
93
|
+
(profile_id, date_str),
|
|
94
|
+
).fetchall()
|
|
95
|
+
finally:
|
|
96
|
+
conn.close()
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.warning("daily reflection query failed for %s: %s", date_str, exc)
|
|
99
|
+
return SummaryResult(
|
|
100
|
+
kind="daily",
|
|
101
|
+
profile_id=profile_id,
|
|
102
|
+
content=f"Daily reflection for {date_str} is unavailable: data access error.",
|
|
103
|
+
source_fact_ids=[],
|
|
104
|
+
coverage=COVERAGE_UNAVAILABLE,
|
|
105
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
106
|
+
metadata={"date": date_str, "error": str(exc)},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ── coverage decision ─────────────────────────────────────────────────────
|
|
110
|
+
if not rows:
|
|
111
|
+
return SummaryResult(
|
|
112
|
+
kind="daily",
|
|
113
|
+
profile_id=profile_id,
|
|
114
|
+
content=f"No facts recorded on {date_str}.",
|
|
115
|
+
source_fact_ids=[],
|
|
116
|
+
coverage=COVERAGE_INSUFFICIENT,
|
|
117
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
118
|
+
metadata={"date": date_str, "fact_count": 0},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
facts = [dict(r) for r in rows]
|
|
122
|
+
source_fact_ids = [f["fact_id"] for f in facts]
|
|
123
|
+
fact_count = len(facts)
|
|
124
|
+
|
|
125
|
+
if fact_count < _MIN_FACTS:
|
|
126
|
+
coverage = COVERAGE_INSUFFICIENT
|
|
127
|
+
else:
|
|
128
|
+
coverage = COVERAGE_FULL
|
|
129
|
+
|
|
130
|
+
# ── extractive summary (deterministic, always available) ──────────────────
|
|
131
|
+
extractive_content = _build_extractive_content(date_str, facts, fact_count)
|
|
132
|
+
|
|
133
|
+
# ── LLM enrichment (optional) ─────────────────────────────────────────────
|
|
134
|
+
mode = get_mode_str(config)
|
|
135
|
+
if fact_count >= _MIN_FACTS and mode in ("b", "c"):
|
|
136
|
+
llm_content, llm_mode = _try_llm(date_str, facts, config, mode)
|
|
137
|
+
if llm_content:
|
|
138
|
+
return SummaryResult(
|
|
139
|
+
kind="daily",
|
|
140
|
+
profile_id=profile_id,
|
|
141
|
+
content=llm_content,
|
|
142
|
+
source_fact_ids=source_fact_ids,
|
|
143
|
+
coverage=coverage,
|
|
144
|
+
generated_by=llm_mode,
|
|
145
|
+
metadata={"date": date_str, "fact_count": fact_count},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return SummaryResult(
|
|
149
|
+
kind="daily",
|
|
150
|
+
profile_id=profile_id,
|
|
151
|
+
content=extractive_content,
|
|
152
|
+
source_fact_ids=source_fact_ids,
|
|
153
|
+
coverage=coverage,
|
|
154
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
155
|
+
metadata={"date": date_str, "fact_count": fact_count},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def _build_extractive_content(
|
|
162
|
+
date_str: str,
|
|
163
|
+
facts: list[dict],
|
|
164
|
+
fact_count: int,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Build a deterministic extractive daily reflection."""
|
|
167
|
+
import json
|
|
168
|
+
|
|
169
|
+
lines = [
|
|
170
|
+
f"Daily reflection: {date_str}",
|
|
171
|
+
f"Total facts recorded: {fact_count}",
|
|
172
|
+
"",
|
|
173
|
+
"Highlights:",
|
|
174
|
+
]
|
|
175
|
+
for f in facts[:_BODY_FACTS]:
|
|
176
|
+
content = f.get("content", "")
|
|
177
|
+
if len(content) > _MAX_FACT_CHARS:
|
|
178
|
+
content = content[:_MAX_FACT_CHARS - 3] + "..."
|
|
179
|
+
lines.append(f" - {content}")
|
|
180
|
+
if fact_count > _BODY_FACTS:
|
|
181
|
+
lines.append(f" ... and {fact_count - _BODY_FACTS} additional facts.")
|
|
182
|
+
|
|
183
|
+
# Entity summary: which entities appeared most
|
|
184
|
+
entity_counts: dict[str, int] = {}
|
|
185
|
+
for f in facts:
|
|
186
|
+
raw = f.get("canonical_entities_json") or "[]"
|
|
187
|
+
try:
|
|
188
|
+
entities = json.loads(raw)
|
|
189
|
+
except (ValueError, TypeError):
|
|
190
|
+
entities = []
|
|
191
|
+
for e in entities:
|
|
192
|
+
entity_counts[e] = entity_counts.get(e, 0) + 1
|
|
193
|
+
|
|
194
|
+
if entity_counts:
|
|
195
|
+
top_entities = sorted(entity_counts.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
196
|
+
entity_str = ", ".join(f"{e} ({c})" for e, c in top_entities)
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append(f"Active entities: {entity_str}")
|
|
199
|
+
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _try_llm(
|
|
204
|
+
date_str: str,
|
|
205
|
+
facts: list[dict],
|
|
206
|
+
config: object | None,
|
|
207
|
+
mode: str,
|
|
208
|
+
) -> tuple[str | None, str]:
|
|
209
|
+
"""Attempt LLM-based daily reflection. Returns (content, generated_by)."""
|
|
210
|
+
prompt = (
|
|
211
|
+
f"Write a concise daily reflection for {date_str} based on these facts. "
|
|
212
|
+
f"Focus on main themes and accomplishments in 3-5 sentences."
|
|
213
|
+
)
|
|
214
|
+
if mode == "c":
|
|
215
|
+
result = _call_cloud_llm(prompt, facts, config)
|
|
216
|
+
if result:
|
|
217
|
+
return result, GENERATED_BY_LLM_C
|
|
218
|
+
if mode in ("b", "c"):
|
|
219
|
+
result = _call_ollama(prompt, facts, config)
|
|
220
|
+
if result:
|
|
221
|
+
return result, GENERATED_BY_LLM_B
|
|
222
|
+
return None, GENERATED_BY_EXTRACTIVE
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _call_ollama(
|
|
226
|
+
prompt: str,
|
|
227
|
+
facts: list[dict],
|
|
228
|
+
config: object | None,
|
|
229
|
+
) -> str | None:
|
|
230
|
+
"""Mode B: call Ollama. Returns None on any failure (extractive fallback)."""
|
|
231
|
+
try:
|
|
232
|
+
import json
|
|
233
|
+
import urllib.request
|
|
234
|
+
|
|
235
|
+
api_base = "http://localhost:11434"
|
|
236
|
+
model = "llama3.2"
|
|
237
|
+
timeout = 30
|
|
238
|
+
if config and hasattr(config, "llm"):
|
|
239
|
+
api_base = getattr(config.llm, "api_base", api_base) or api_base
|
|
240
|
+
model = getattr(config.llm, "model", model) or model
|
|
241
|
+
timeout = (
|
|
242
|
+
getattr(config.llm, "timeout_seconds", None)
|
|
243
|
+
or getattr(config.llm, "timeout", None)
|
|
244
|
+
or timeout
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts[:15])
|
|
248
|
+
full_prompt = f"{prompt}\n\nFacts from {len(facts)} recorded:\n{fact_texts}"
|
|
249
|
+
payload = json.dumps({
|
|
250
|
+
"model": model,
|
|
251
|
+
"prompt": full_prompt,
|
|
252
|
+
"stream": False,
|
|
253
|
+
"options": {"num_predict": 300},
|
|
254
|
+
}).encode()
|
|
255
|
+
req = urllib.request.Request(
|
|
256
|
+
f"{api_base}/api/generate",
|
|
257
|
+
data=payload,
|
|
258
|
+
headers={"Content-Type": "application/json"},
|
|
259
|
+
)
|
|
260
|
+
resp = urllib.request.urlopen(req, timeout=timeout)
|
|
261
|
+
data = json.loads(resp.read().decode())
|
|
262
|
+
text = data.get("response", "").strip()
|
|
263
|
+
return text if text and len(text) > 20 else None
|
|
264
|
+
except Exception as exc:
|
|
265
|
+
logger.debug("Ollama daily reflection failed: %s", exc)
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _call_cloud_llm(
|
|
270
|
+
prompt: str,
|
|
271
|
+
facts: list[dict],
|
|
272
|
+
config: object | None,
|
|
273
|
+
) -> str | None:
|
|
274
|
+
"""Mode C: call the configured cloud LLM. Returns None on any failure."""
|
|
275
|
+
if not config or not hasattr(config, "llm"):
|
|
276
|
+
return None
|
|
277
|
+
try:
|
|
278
|
+
from superlocalmemory.llm.backbone import LLMBackbone
|
|
279
|
+
llm = LLMBackbone(config.llm)
|
|
280
|
+
if not llm.is_available():
|
|
281
|
+
return None
|
|
282
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts[:15])
|
|
283
|
+
full_prompt = f"{prompt}\n\nFacts:\n{fact_texts}"
|
|
284
|
+
text = llm.generate(
|
|
285
|
+
prompt=full_prompt,
|
|
286
|
+
system="You are a concise personal memory summariser.",
|
|
287
|
+
max_tokens=300,
|
|
288
|
+
temperature=0.1,
|
|
289
|
+
)
|
|
290
|
+
return text.strip() if text and len(text.strip()) > 20 else None
|
|
291
|
+
except Exception as exc:
|
|
292
|
+
logger.debug("Cloud LLM daily reflection failed: %s", exc)
|
|
293
|
+
return None
|