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,424 @@
|
|
|
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
|
+
"""Project Work Log — issue #113 bounded summary.
|
|
6
|
+
|
|
7
|
+
Produces a human-readable log of tool activity and recorded facts for a
|
|
8
|
+
specific project, identified by tool_events.project_path.
|
|
9
|
+
|
|
10
|
+
CRITICAL IMPLEMENTATION NOTE — DO NOT USE entity_profiles.project_name
|
|
11
|
+
-----------------------------------------------------------------------
|
|
12
|
+
entity_profiles.project_name has 1,148 rows and EXACTLY ONE distinct value
|
|
13
|
+
on a real store. Grouping by it yields a single meaningless bucket for all
|
|
14
|
+
projects. This is measured fact, not assumption.
|
|
15
|
+
|
|
16
|
+
Project scope MUST come from tool_events.project_path.
|
|
17
|
+
- 1,899 rows across 13 real projects on the same store.
|
|
18
|
+
- Top project: ".../testing - automation" (336 events).
|
|
19
|
+
- This is the only reliable project discriminator in the schema.
|
|
20
|
+
|
|
21
|
+
DETERMINISTIC FALLBACK
|
|
22
|
+
-----------------------
|
|
23
|
+
The extractive path aggregates tool events and the top facts for the project.
|
|
24
|
+
It never calls an LLM and never fails to return a result. Mode B/C
|
|
25
|
+
enrichment is attempted when configured, but falls back to extractive on any
|
|
26
|
+
failure.
|
|
27
|
+
|
|
28
|
+
HOT PATH EXCLUSION
|
|
29
|
+
-------------------
|
|
30
|
+
This module must NEVER be imported from core/recall_pipeline.py or
|
|
31
|
+
core/store_pipeline.py.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
import sqlite3
|
|
38
|
+
from collections import Counter
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from .base import (
|
|
42
|
+
COVERAGE_FULL,
|
|
43
|
+
COVERAGE_INSUFFICIENT,
|
|
44
|
+
COVERAGE_UNAVAILABLE,
|
|
45
|
+
GENERATED_BY_EXTRACTIVE,
|
|
46
|
+
GENERATED_BY_LLM_B,
|
|
47
|
+
GENERATED_BY_LLM_C,
|
|
48
|
+
SummaryResult,
|
|
49
|
+
get_mode_str,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("superlocalmemory.summaries.project")
|
|
53
|
+
|
|
54
|
+
_MIN_EVENTS = 1 # Minimum tool_events rows to attempt a summary
|
|
55
|
+
_MIN_FACTS = 1 # Minimum atomic_facts rows to include facts section
|
|
56
|
+
_TOP_TOOLS = 8 # How many tools to list in the summary
|
|
57
|
+
_TOP_FACTS = 8 # How many facts to include in the extractive body
|
|
58
|
+
_MAX_FACT_CHARS = 300 # Per-fact character cap in the body
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_project_work_log(
|
|
62
|
+
db_path: str | Path,
|
|
63
|
+
project_path: str,
|
|
64
|
+
profile_id: str = "default",
|
|
65
|
+
config: object | None = None,
|
|
66
|
+
) -> SummaryResult:
|
|
67
|
+
"""Generate a Project Work Log for a specific project.
|
|
68
|
+
|
|
69
|
+
Scope is determined by tool_events.project_path — NOT by
|
|
70
|
+
entity_profiles.project_name (which has 1 distinct value on a real store
|
|
71
|
+
and is therefore useless for scoping).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
db_path: Path to memory.db.
|
|
75
|
+
project_path: The project path as stored in tool_events.project_path.
|
|
76
|
+
Exact match — callers may pass a prefix and use
|
|
77
|
+
generate_project_work_log_by_prefix() for fuzzy matching.
|
|
78
|
+
profile_id: Profile scope — never mix profiles.
|
|
79
|
+
config: Optional SLMConfig for LLM enrichment. None = Mode A.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
SummaryResult with source_fact_ids. Never returns None.
|
|
83
|
+
"""
|
|
84
|
+
db_path = Path(db_path)
|
|
85
|
+
|
|
86
|
+
# ── query tool events ────────────────────────────────────────────────────
|
|
87
|
+
tool_rows, facts_rows, query_error = _query_project_data(
|
|
88
|
+
db_path, project_path, profile_id
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if query_error:
|
|
92
|
+
return SummaryResult(
|
|
93
|
+
kind="project",
|
|
94
|
+
profile_id=profile_id,
|
|
95
|
+
content=(
|
|
96
|
+
f"Project work log for '{project_path}' is unavailable: "
|
|
97
|
+
f"data access error."
|
|
98
|
+
),
|
|
99
|
+
source_fact_ids=[],
|
|
100
|
+
coverage=COVERAGE_UNAVAILABLE,
|
|
101
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
102
|
+
metadata={"project_path": project_path, "error": query_error},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not tool_rows and not facts_rows:
|
|
106
|
+
return SummaryResult(
|
|
107
|
+
kind="project",
|
|
108
|
+
profile_id=profile_id,
|
|
109
|
+
content=(
|
|
110
|
+
f"No tool events or facts found for project '{project_path}'.\n"
|
|
111
|
+
f"Note: project scope is matched by tool_events.project_path "
|
|
112
|
+
f"(exact match)."
|
|
113
|
+
),
|
|
114
|
+
source_fact_ids=[],
|
|
115
|
+
coverage=COVERAGE_INSUFFICIENT,
|
|
116
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
117
|
+
metadata={"project_path": project_path, "event_count": 0},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
source_fact_ids = [f["fact_id"] for f in facts_rows]
|
|
121
|
+
event_count = len(tool_rows)
|
|
122
|
+
fact_count = len(facts_rows)
|
|
123
|
+
|
|
124
|
+
coverage = COVERAGE_FULL if (event_count >= _MIN_EVENTS or fact_count >= _MIN_FACTS) \
|
|
125
|
+
else COVERAGE_INSUFFICIENT
|
|
126
|
+
|
|
127
|
+
extractive_content = _build_extractive_content(
|
|
128
|
+
project_path, tool_rows, facts_rows, event_count, fact_count
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ── LLM enrichment (optional) ─────────────────────────────────────────────
|
|
132
|
+
mode = get_mode_str(config)
|
|
133
|
+
if mode in ("b", "c"):
|
|
134
|
+
llm_content, llm_mode = _try_llm(
|
|
135
|
+
project_path, tool_rows, facts_rows, config, mode
|
|
136
|
+
)
|
|
137
|
+
if llm_content:
|
|
138
|
+
return SummaryResult(
|
|
139
|
+
kind="project",
|
|
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={
|
|
146
|
+
"project_path": project_path,
|
|
147
|
+
"event_count": event_count,
|
|
148
|
+
"fact_count": fact_count,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return SummaryResult(
|
|
153
|
+
kind="project",
|
|
154
|
+
profile_id=profile_id,
|
|
155
|
+
content=extractive_content,
|
|
156
|
+
source_fact_ids=source_fact_ids,
|
|
157
|
+
coverage=coverage,
|
|
158
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
159
|
+
metadata={
|
|
160
|
+
"project_path": project_path,
|
|
161
|
+
"event_count": event_count,
|
|
162
|
+
"fact_count": fact_count,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def generate_project_work_log_by_prefix(
|
|
168
|
+
db_path: str | Path,
|
|
169
|
+
project_prefix: str,
|
|
170
|
+
profile_id: str = "default",
|
|
171
|
+
config: object | None = None,
|
|
172
|
+
) -> list[SummaryResult]:
|
|
173
|
+
"""Generate work logs for all projects whose path starts with project_prefix.
|
|
174
|
+
|
|
175
|
+
Useful when the user knows only the parent directory, not the exact path.
|
|
176
|
+
Returns one SummaryResult per distinct project_path found.
|
|
177
|
+
|
|
178
|
+
Each result uses the EXACT project_path as the key, so source_fact_ids
|
|
179
|
+
and tool events are correctly scoped per sub-project.
|
|
180
|
+
"""
|
|
181
|
+
db_path = Path(db_path)
|
|
182
|
+
try:
|
|
183
|
+
conn = sqlite3.connect(str(db_path), timeout=5.0)
|
|
184
|
+
conn.row_factory = sqlite3.Row
|
|
185
|
+
conn.execute("PRAGMA query_only=ON")
|
|
186
|
+
try:
|
|
187
|
+
rows = conn.execute(
|
|
188
|
+
"""
|
|
189
|
+
SELECT DISTINCT project_path
|
|
190
|
+
FROM tool_events
|
|
191
|
+
WHERE profile_id = ?
|
|
192
|
+
AND project_path LIKE ?
|
|
193
|
+
AND project_path != ''
|
|
194
|
+
ORDER BY project_path
|
|
195
|
+
""",
|
|
196
|
+
(profile_id, f"{project_prefix}%"),
|
|
197
|
+
).fetchall()
|
|
198
|
+
finally:
|
|
199
|
+
conn.close()
|
|
200
|
+
except Exception:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
results = []
|
|
204
|
+
for row in rows:
|
|
205
|
+
path = dict(row)["project_path"]
|
|
206
|
+
result = generate_project_work_log(db_path, path, profile_id, config)
|
|
207
|
+
results.append(result)
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ── internal helpers ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
def _query_project_data(
|
|
214
|
+
db_path: Path,
|
|
215
|
+
project_path: str,
|
|
216
|
+
profile_id: str,
|
|
217
|
+
) -> tuple[list[dict], list[dict], str | None]:
|
|
218
|
+
"""Query tool events and associated facts for the project.
|
|
219
|
+
|
|
220
|
+
Returns (tool_rows, facts_rows, error_message_or_None).
|
|
221
|
+
Uses tool_events.project_path for project scoping — not project_name.
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
conn = sqlite3.connect(str(db_path), timeout=5.0)
|
|
225
|
+
conn.row_factory = sqlite3.Row
|
|
226
|
+
conn.execute("PRAGMA query_only=ON")
|
|
227
|
+
try:
|
|
228
|
+
# Tool events scoped by project_path (the correct column).
|
|
229
|
+
# Using tool_events.project_path — NOT entity_profiles.project_name.
|
|
230
|
+
tool_rows = conn.execute(
|
|
231
|
+
"""
|
|
232
|
+
SELECT tool_name, event_type, session_id,
|
|
233
|
+
input_summary, output_summary, created_at, duration_ms
|
|
234
|
+
FROM tool_events
|
|
235
|
+
WHERE profile_id = ?
|
|
236
|
+
AND project_path = ?
|
|
237
|
+
ORDER BY created_at ASC
|
|
238
|
+
""",
|
|
239
|
+
(profile_id, project_path),
|
|
240
|
+
).fetchall()
|
|
241
|
+
|
|
242
|
+
# Facts recorded during sessions that touched this project.
|
|
243
|
+
facts_rows = conn.execute(
|
|
244
|
+
"""
|
|
245
|
+
SELECT DISTINCT af.fact_id, af.content, af.created_at,
|
|
246
|
+
af.importance, af.canonical_entities_json
|
|
247
|
+
FROM atomic_facts af
|
|
248
|
+
JOIN tool_events te
|
|
249
|
+
ON te.session_id = af.session_id
|
|
250
|
+
AND te.profile_id = af.profile_id
|
|
251
|
+
WHERE af.profile_id = ?
|
|
252
|
+
AND te.project_path = ?
|
|
253
|
+
AND af.lifecycle != 'archived'
|
|
254
|
+
ORDER BY af.importance DESC, af.created_at ASC
|
|
255
|
+
""",
|
|
256
|
+
(profile_id, project_path),
|
|
257
|
+
).fetchall()
|
|
258
|
+
finally:
|
|
259
|
+
conn.close()
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
logger.warning("project work log query failed for %s: %s", project_path, exc)
|
|
262
|
+
return [], [], str(exc)
|
|
263
|
+
|
|
264
|
+
return [dict(r) for r in tool_rows], [dict(r) for r in facts_rows], None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _build_extractive_content(
|
|
268
|
+
project_path: str,
|
|
269
|
+
tool_rows: list[dict],
|
|
270
|
+
facts_rows: list[dict],
|
|
271
|
+
event_count: int,
|
|
272
|
+
fact_count: int,
|
|
273
|
+
) -> str:
|
|
274
|
+
"""Build a deterministic extractive project work log."""
|
|
275
|
+
# Readable project name: last 2 path components.
|
|
276
|
+
parts = project_path.rstrip("/").split("/")
|
|
277
|
+
display_name = "/".join(parts[-2:]) if len(parts) >= 2 else project_path
|
|
278
|
+
|
|
279
|
+
lines = [
|
|
280
|
+
f"Project Work Log: {display_name}",
|
|
281
|
+
f"Full path: {project_path}",
|
|
282
|
+
f"Tool events: {event_count}",
|
|
283
|
+
f"Associated facts: {fact_count}",
|
|
284
|
+
"",
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
# Tool usage breakdown.
|
|
288
|
+
if tool_rows:
|
|
289
|
+
tool_counter: Counter = Counter()
|
|
290
|
+
for r in tool_rows:
|
|
291
|
+
tool_counter[r.get("tool_name") or "unknown"] += 1
|
|
292
|
+
top_tools = tool_counter.most_common(_TOP_TOOLS)
|
|
293
|
+
lines.append("Tool usage:")
|
|
294
|
+
for tool, count in top_tools:
|
|
295
|
+
lines.append(f" {tool}: {count} event(s)")
|
|
296
|
+
if len(tool_counter) > _TOP_TOOLS:
|
|
297
|
+
lines.append(f" ... and {len(tool_counter) - _TOP_TOOLS} other tools.")
|
|
298
|
+
lines.append("")
|
|
299
|
+
|
|
300
|
+
# Top facts by importance.
|
|
301
|
+
if facts_rows:
|
|
302
|
+
lines.append("Key facts from project sessions:")
|
|
303
|
+
for f in facts_rows[:_TOP_FACTS]:
|
|
304
|
+
content = f.get("content", "")
|
|
305
|
+
if len(content) > _MAX_FACT_CHARS:
|
|
306
|
+
content = content[:_MAX_FACT_CHARS - 3] + "..."
|
|
307
|
+
lines.append(f" - {content}")
|
|
308
|
+
if fact_count > _TOP_FACTS:
|
|
309
|
+
lines.append(f" ... and {fact_count - _TOP_FACTS} more facts.")
|
|
310
|
+
|
|
311
|
+
return "\n".join(lines)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _try_llm(
|
|
315
|
+
project_path: str,
|
|
316
|
+
tool_rows: list[dict],
|
|
317
|
+
facts_rows: list[dict],
|
|
318
|
+
config: object | None,
|
|
319
|
+
mode: str,
|
|
320
|
+
) -> tuple[str | None, str]:
|
|
321
|
+
"""Attempt LLM-based project summary. Returns (content, generated_by)."""
|
|
322
|
+
parts = project_path.rstrip("/").split("/")
|
|
323
|
+
display_name = "/".join(parts[-2:]) if len(parts) >= 2 else project_path
|
|
324
|
+
prompt = (
|
|
325
|
+
f"Write a concise project work log for '{display_name}' based on "
|
|
326
|
+
f"{len(tool_rows)} tool events and {len(facts_rows)} recorded facts."
|
|
327
|
+
)
|
|
328
|
+
if mode == "c":
|
|
329
|
+
result = _call_cloud_llm(prompt, tool_rows, facts_rows, config)
|
|
330
|
+
if result:
|
|
331
|
+
return result, GENERATED_BY_LLM_C
|
|
332
|
+
if mode in ("b", "c"):
|
|
333
|
+
result = _call_ollama(prompt, tool_rows, facts_rows, config)
|
|
334
|
+
if result:
|
|
335
|
+
return result, GENERATED_BY_LLM_B
|
|
336
|
+
return None, GENERATED_BY_EXTRACTIVE
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _call_ollama(
|
|
340
|
+
prompt: str,
|
|
341
|
+
tool_rows: list[dict],
|
|
342
|
+
facts_rows: list[dict],
|
|
343
|
+
config: object | None,
|
|
344
|
+
) -> str | None:
|
|
345
|
+
"""Mode B: call Ollama. Returns None on any failure."""
|
|
346
|
+
try:
|
|
347
|
+
import json
|
|
348
|
+
import urllib.request
|
|
349
|
+
|
|
350
|
+
api_base = "http://localhost:11434"
|
|
351
|
+
model = "llama3.2"
|
|
352
|
+
timeout = 30
|
|
353
|
+
if config and hasattr(config, "llm"):
|
|
354
|
+
api_base = getattr(config.llm, "api_base", api_base) or api_base
|
|
355
|
+
model = getattr(config.llm, "model", model) or model
|
|
356
|
+
timeout = (
|
|
357
|
+
getattr(config.llm, "timeout_seconds", None)
|
|
358
|
+
or getattr(config.llm, "timeout", None)
|
|
359
|
+
or timeout
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
tool_summary = ", ".join(
|
|
363
|
+
f"{r['tool_name']}({r.get('event_type', '')})"
|
|
364
|
+
for r in tool_rows[:10]
|
|
365
|
+
)
|
|
366
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts_rows[:8])
|
|
367
|
+
full_prompt = (
|
|
368
|
+
f"{prompt}\n\n"
|
|
369
|
+
f"Tools used: {tool_summary}\n\n"
|
|
370
|
+
f"Key facts:\n{fact_texts}\n\n"
|
|
371
|
+
f"Respond in 3-5 sentences."
|
|
372
|
+
)
|
|
373
|
+
payload = json.dumps({
|
|
374
|
+
"model": model,
|
|
375
|
+
"prompt": full_prompt,
|
|
376
|
+
"stream": False,
|
|
377
|
+
"options": {"num_predict": 300},
|
|
378
|
+
}).encode()
|
|
379
|
+
req = urllib.request.Request(
|
|
380
|
+
f"{api_base}/api/generate",
|
|
381
|
+
data=payload,
|
|
382
|
+
headers={"Content-Type": "application/json"},
|
|
383
|
+
)
|
|
384
|
+
resp = urllib.request.urlopen(req, timeout=timeout)
|
|
385
|
+
data = json.loads(resp.read().decode())
|
|
386
|
+
text = data.get("response", "").strip()
|
|
387
|
+
return text if text and len(text) > 20 else None
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
logger.debug("Ollama project work log failed: %s", exc)
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _call_cloud_llm(
|
|
394
|
+
prompt: str,
|
|
395
|
+
tool_rows: list[dict],
|
|
396
|
+
facts_rows: list[dict],
|
|
397
|
+
config: object | None,
|
|
398
|
+
) -> str | None:
|
|
399
|
+
"""Mode C: call the configured cloud LLM. Returns None on any failure."""
|
|
400
|
+
if not config or not hasattr(config, "llm"):
|
|
401
|
+
return None
|
|
402
|
+
try:
|
|
403
|
+
from superlocalmemory.llm.backbone import LLMBackbone
|
|
404
|
+
llm = LLMBackbone(config.llm)
|
|
405
|
+
if not llm.is_available():
|
|
406
|
+
return None
|
|
407
|
+
tool_summary = ", ".join(
|
|
408
|
+
f"{r['tool_name']}({r.get('event_type', '')})"
|
|
409
|
+
for r in tool_rows[:10]
|
|
410
|
+
)
|
|
411
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts_rows[:8])
|
|
412
|
+
full_prompt = (
|
|
413
|
+
f"{prompt}\n\nTools: {tool_summary}\n\nFacts:\n{fact_texts}"
|
|
414
|
+
)
|
|
415
|
+
text = llm.generate(
|
|
416
|
+
prompt=full_prompt,
|
|
417
|
+
system="You are a concise project activity summariser.",
|
|
418
|
+
max_tokens=300,
|
|
419
|
+
temperature=0.1,
|
|
420
|
+
)
|
|
421
|
+
return text.strip() if text and len(text.strip()) > 20 else None
|
|
422
|
+
except Exception as exc:
|
|
423
|
+
logger.debug("Cloud LLM project work log failed: %s", exc)
|
|
424
|
+
return None
|