superlocalmemory 3.4.0 → 3.4.3
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/README.md +7 -8
- package/docs/screenshots/01-dashboard-main.png +0 -0
- package/docs/screenshots/02-knowledge-graph.png +0 -0
- package/docs/screenshots/03-patterns-learning.png +0 -0
- package/docs/screenshots/04-learning-dashboard.png +0 -0
- package/docs/screenshots/05-behavioral-analysis.png +0 -0
- package/docs/screenshots/06-graph-communities.png +0 -0
- package/package.json +2 -2
- package/pyproject.toml +11 -2
- package/scripts/postinstall.js +26 -7
- package/src/superlocalmemory/cli/commands.py +42 -60
- package/src/superlocalmemory/cli/daemon.py +107 -47
- package/src/superlocalmemory/cli/main.py +10 -0
- package/src/superlocalmemory/cli/setup_wizard.py +137 -9
- package/src/superlocalmemory/core/config.py +28 -0
- package/src/superlocalmemory/core/consolidation_engine.py +38 -1
- package/src/superlocalmemory/core/engine.py +9 -0
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/graph_analyzer.py +254 -12
- package/src/superlocalmemory/core/health_monitor.py +313 -0
- package/src/superlocalmemory/core/reranker_worker.py +19 -5
- package/src/superlocalmemory/ingestion/__init__.py +13 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
- package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
- package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
- package/src/superlocalmemory/ingestion/credentials.py +118 -0
- package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
- package/src/superlocalmemory/ingestion/parsers.py +100 -0
- package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +287 -53
- package/src/superlocalmemory/learning/entity_compiler.py +377 -0
- package/src/superlocalmemory/mesh/__init__.py +12 -0
- package/src/superlocalmemory/mesh/broker.py +344 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +141 -4
- package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
- package/src/superlocalmemory/server/api.py +15 -8
- package/src/superlocalmemory/server/routes/behavioral.py +8 -4
- package/src/superlocalmemory/server/routes/chat.py +320 -0
- package/src/superlocalmemory/server/routes/entity.py +95 -0
- package/src/superlocalmemory/server/routes/ingest.py +110 -0
- package/src/superlocalmemory/server/routes/insights.py +368 -0
- package/src/superlocalmemory/server/routes/learning.py +106 -6
- package/src/superlocalmemory/server/routes/memories.py +20 -9
- package/src/superlocalmemory/server/routes/mesh.py +186 -0
- package/src/superlocalmemory/server/routes/stats.py +25 -3
- package/src/superlocalmemory/server/routes/timeline.py +252 -0
- package/src/superlocalmemory/server/routes/v3_api.py +161 -0
- package/src/superlocalmemory/server/ui.py +8 -0
- package/src/superlocalmemory/server/unified_daemon.py +691 -0
- package/src/superlocalmemory/storage/schema_v343.py +229 -0
- package/src/superlocalmemory/ui/index.html +168 -58
- package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
- package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
- package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
- package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -594
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -279
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -47
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -73,7 +73,38 @@ class ConsolidationWorker:
|
|
|
73
73
|
except Exception as exc:
|
|
74
74
|
logger.debug("Pattern generation failed: %s", exc)
|
|
75
75
|
|
|
76
|
-
# 4.
|
|
76
|
+
# 4. Recompute graph intelligence (v3.4.2: wired into learning pipeline)
|
|
77
|
+
try:
|
|
78
|
+
from superlocalmemory.core.graph_analyzer import GraphAnalyzer
|
|
79
|
+
conn_ga = sqlite3.connect(self._memory_db, timeout=10)
|
|
80
|
+
conn_ga.execute("PRAGMA busy_timeout=5000")
|
|
81
|
+
conn_ga.row_factory = sqlite3.Row
|
|
82
|
+
|
|
83
|
+
class _DBProxy:
|
|
84
|
+
"""Minimal DB proxy for GraphAnalyzer compatibility."""
|
|
85
|
+
def __init__(self, connection: sqlite3.Connection) -> None:
|
|
86
|
+
self._conn = connection
|
|
87
|
+
def execute(self, sql: str, params: tuple = ()) -> list:
|
|
88
|
+
cursor = self._conn.execute(sql, params)
|
|
89
|
+
if sql.strip().upper().startswith(("INSERT", "UPDATE", "DELETE", "ALTER", "CREATE")):
|
|
90
|
+
self._conn.commit()
|
|
91
|
+
return []
|
|
92
|
+
return cursor.fetchall()
|
|
93
|
+
|
|
94
|
+
ga = GraphAnalyzer(_DBProxy(conn_ga))
|
|
95
|
+
if not dry_run:
|
|
96
|
+
ga_result = ga.compute_and_store(profile_id)
|
|
97
|
+
stats["graph_nodes"] = ga_result.get("node_count", 0)
|
|
98
|
+
stats["graph_communities"] = ga_result.get("community_count", 0)
|
|
99
|
+
logger.info(
|
|
100
|
+
"Graph analysis: %d nodes, %d communities",
|
|
101
|
+
stats["graph_nodes"], stats["graph_communities"],
|
|
102
|
+
)
|
|
103
|
+
conn_ga.close()
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
logger.debug("Graph analysis failed: %s", exc)
|
|
106
|
+
|
|
107
|
+
# 5. Check if ranker should retrain
|
|
77
108
|
try:
|
|
78
109
|
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
79
110
|
collector = FeedbackCollector(Path(self._learning_db))
|
|
@@ -88,6 +119,21 @@ class ConsolidationWorker:
|
|
|
88
119
|
except Exception as exc:
|
|
89
120
|
logger.debug("Retrain check failed: %s", exc)
|
|
90
121
|
|
|
122
|
+
# 6. Entity compilation (v3.4.3: compiled truth per entity)
|
|
123
|
+
if not dry_run:
|
|
124
|
+
try:
|
|
125
|
+
from superlocalmemory.learning.entity_compiler import EntityCompiler
|
|
126
|
+
from superlocalmemory.core.config import SLMConfig
|
|
127
|
+
config = SLMConfig.load()
|
|
128
|
+
compiler = EntityCompiler(self._memory_db, config)
|
|
129
|
+
ec_result = compiler.compile_all(profile_id)
|
|
130
|
+
stats["entities_compiled"] = ec_result.get("compiled", 0)
|
|
131
|
+
if ec_result["compiled"] > 0:
|
|
132
|
+
logger.info("Entity compilation: %d entities compiled",
|
|
133
|
+
ec_result["compiled"])
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
logger.debug("Entity compilation failed: %s", exc)
|
|
136
|
+
|
|
91
137
|
return stats
|
|
92
138
|
|
|
93
139
|
def _deduplicate(self, profile_id: str, dry_run: bool) -> int:
|
|
@@ -133,127 +179,315 @@ class ConsolidationWorker:
|
|
|
133
179
|
return 0
|
|
134
180
|
|
|
135
181
|
def _generate_patterns(self, profile_id: str, dry_run: bool) -> int:
|
|
136
|
-
"""Mine behavioral patterns from
|
|
182
|
+
"""Mine behavioral patterns from ALL memory sources.
|
|
137
183
|
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
- Temporal patterns (time-of-day activity)
|
|
184
|
+
v3.4.1: Expanded from 3 to 7 pattern types. No 500-fact cap.
|
|
185
|
+
Analyzes: facts, signals, co-retrieval edges, channel credits,
|
|
186
|
+
entities, sessions, graph communities.
|
|
142
187
|
"""
|
|
143
188
|
try:
|
|
144
189
|
from superlocalmemory.learning.behavioral import BehavioralPatternStore
|
|
145
190
|
import re
|
|
146
|
-
from collections import Counter
|
|
191
|
+
from collections import Counter, defaultdict
|
|
147
192
|
|
|
148
193
|
conn = sqlite3.connect(self._memory_db, timeout=10)
|
|
149
194
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
150
195
|
conn.row_factory = sqlite3.Row
|
|
151
196
|
|
|
197
|
+
# v3.4.1: No cap — analyze ALL facts
|
|
152
198
|
facts = conn.execute(
|
|
153
|
-
"SELECT content, created_at
|
|
154
|
-
"
|
|
199
|
+
"SELECT fact_id, content, fact_type, created_at, session_id, "
|
|
200
|
+
"confidence, canonical_entities_json "
|
|
201
|
+
"FROM atomic_facts "
|
|
202
|
+
"WHERE profile_id = ? AND lifecycle = 'active' "
|
|
203
|
+
"ORDER BY created_at DESC",
|
|
155
204
|
(profile_id,),
|
|
156
205
|
).fetchall()
|
|
157
|
-
conn.close()
|
|
158
206
|
|
|
159
|
-
if len(facts) <
|
|
207
|
+
if len(facts) < 5:
|
|
208
|
+
conn.close()
|
|
160
209
|
return 0
|
|
161
210
|
|
|
162
211
|
store = BehavioralPatternStore(self._learning_db)
|
|
163
212
|
generated = 0
|
|
164
213
|
|
|
165
|
-
# Tech
|
|
214
|
+
# ── 1. Tech Preferences (expanded keyword list) ───────────
|
|
166
215
|
tech_keywords = {
|
|
167
|
-
"python": "Python", "javascript": "JavaScript",
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
216
|
+
"python": "Python", "javascript": "JavaScript",
|
|
217
|
+
"typescript": "TypeScript", "react": "React",
|
|
218
|
+
"vue": "Vue", "angular": "Angular",
|
|
219
|
+
"postgresql": "PostgreSQL", "mysql": "MySQL",
|
|
220
|
+
"sqlite": "SQLite", "docker": "Docker",
|
|
221
|
+
"kubernetes": "Kubernetes", "aws": "AWS",
|
|
222
|
+
"azure": "Azure", "gcp": "GCP",
|
|
223
|
+
"node": "Node.js", "fastapi": "FastAPI",
|
|
224
|
+
"django": "Django", "flask": "Flask",
|
|
173
225
|
"rust": "Rust", "go": "Go", "java": "Java",
|
|
174
226
|
"git": "Git", "npm": "npm", "pip": "pip",
|
|
175
|
-
"langchain": "LangChain", "ollama": "Ollama",
|
|
176
|
-
"
|
|
227
|
+
"langchain": "LangChain", "ollama": "Ollama",
|
|
228
|
+
"pytorch": "PyTorch", "claude": "Claude",
|
|
229
|
+
"openai": "OpenAI", "anthropic": "Anthropic",
|
|
230
|
+
"redis": "Redis", "mongodb": "MongoDB",
|
|
231
|
+
"graphql": "GraphQL", "nextjs": "Next.js",
|
|
232
|
+
"terraform": "Terraform", "nginx": "Nginx",
|
|
233
|
+
"linux": "Linux", "macos": "macOS",
|
|
234
|
+
"vscode": "VS Code", "neovim": "Neovim",
|
|
177
235
|
}
|
|
178
236
|
|
|
179
|
-
tech_counts = Counter()
|
|
237
|
+
tech_counts: Counter = Counter()
|
|
180
238
|
for f in facts:
|
|
181
239
|
content = dict(f)["content"].lower()
|
|
182
240
|
for keyword, label in tech_keywords.items():
|
|
183
241
|
if keyword in content:
|
|
184
242
|
tech_counts[label] += 1
|
|
185
243
|
|
|
186
|
-
for tech, count in tech_counts.most_common(
|
|
187
|
-
if count >=
|
|
188
|
-
confidence = min(1.0, count /
|
|
244
|
+
for tech, count in tech_counts.most_common(20):
|
|
245
|
+
if count >= 2 and not dry_run:
|
|
246
|
+
confidence = min(1.0, count / max(len(facts) * 0.1, 10))
|
|
189
247
|
store.record_pattern(
|
|
190
248
|
profile_id=profile_id,
|
|
191
249
|
pattern_type="tech_preference",
|
|
192
|
-
data={"topic": tech, "pattern_key": tech,
|
|
193
|
-
|
|
250
|
+
data={"topic": tech, "pattern_key": tech,
|
|
251
|
+
"value": tech, "key": "tech",
|
|
252
|
+
"evidence": count},
|
|
194
253
|
success_rate=confidence,
|
|
195
254
|
confidence=confidence,
|
|
196
255
|
)
|
|
197
256
|
generated += 1
|
|
198
257
|
|
|
199
|
-
# Topic
|
|
200
|
-
word_counts = Counter()
|
|
258
|
+
# ── 2. Topic Interests (word frequency) ───────────────────
|
|
201
259
|
stopwords = frozenset({
|
|
202
|
-
"the", "is", "a", "an", "in", "on", "at", "to", "for",
|
|
203
|
-
"and", "or", "not", "with", "that", "this", "was",
|
|
204
|
-
"
|
|
260
|
+
"the", "is", "a", "an", "in", "on", "at", "to", "for",
|
|
261
|
+
"of", "and", "or", "not", "with", "that", "this", "was",
|
|
262
|
+
"are", "be", "has", "had", "have", "from", "by", "it",
|
|
263
|
+
"its", "as", "but", "were", "been", "being", "would",
|
|
264
|
+
"could", "should", "will", "may", "might", "can", "do",
|
|
265
|
+
"does", "did", "about", "into", "over", "after", "before",
|
|
266
|
+
"then", "than", "also", "just", "like", "more", "some",
|
|
267
|
+
"only", "other", "such", "each", "every", "both", "most",
|
|
205
268
|
})
|
|
269
|
+
word_counts: Counter = Counter()
|
|
206
270
|
for f in facts:
|
|
207
271
|
words = re.findall(r'\b[a-zA-Z]{4,}\b', dict(f)["content"].lower())
|
|
208
272
|
for w in words:
|
|
209
273
|
if w not in stopwords:
|
|
210
274
|
word_counts[w] += 1
|
|
211
275
|
|
|
212
|
-
for topic, count in word_counts.most_common(
|
|
213
|
-
if count >=
|
|
214
|
-
confidence = min(1.0, count /
|
|
276
|
+
for topic, count in word_counts.most_common(15):
|
|
277
|
+
if count >= 3 and not dry_run:
|
|
278
|
+
confidence = min(1.0, count / max(len(facts) * 0.05, 15))
|
|
215
279
|
store.record_pattern(
|
|
216
280
|
profile_id=profile_id,
|
|
217
281
|
pattern_type="interest",
|
|
218
282
|
data={"topic": topic, "pattern_key": topic,
|
|
219
|
-
|
|
283
|
+
"count": count, "evidence": count},
|
|
220
284
|
success_rate=confidence,
|
|
221
285
|
confidence=confidence,
|
|
222
286
|
)
|
|
223
287
|
generated += 1
|
|
224
288
|
|
|
225
|
-
# Temporal
|
|
226
|
-
hour_counts = Counter()
|
|
289
|
+
# ── 3. Temporal Activity Patterns ─────────────────────────
|
|
290
|
+
hour_counts: Counter = Counter()
|
|
227
291
|
for f in facts:
|
|
228
292
|
created = dict(f).get("created_at", "")
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
try:
|
|
294
|
+
if "T" in created:
|
|
231
295
|
hour = int(created.split("T")[1][:2])
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
296
|
+
elif " " in created:
|
|
297
|
+
hour = int(created.split(" ")[1][:2])
|
|
298
|
+
else:
|
|
299
|
+
continue
|
|
300
|
+
period = ("morning" if 6 <= hour < 12 else
|
|
301
|
+
"afternoon" if 12 <= hour < 18 else
|
|
302
|
+
"evening" if 18 <= hour < 22 else "night")
|
|
303
|
+
hour_counts[period] += 1
|
|
304
|
+
except (ValueError, IndexError):
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
total_hours = sum(hour_counts.values())
|
|
239
308
|
for period, count in hour_counts.most_common():
|
|
240
|
-
if count >=
|
|
241
|
-
|
|
242
|
-
pct = round(count / total * 100)
|
|
309
|
+
if count >= 2 and total_hours > 0 and not dry_run:
|
|
310
|
+
pct = round(count / total_hours * 100)
|
|
243
311
|
store.record_pattern(
|
|
244
312
|
profile_id=profile_id,
|
|
245
313
|
pattern_type="temporal",
|
|
246
314
|
data={"topic": period, "pattern_key": period,
|
|
247
|
-
|
|
248
|
-
|
|
315
|
+
"value": f"{period} ({pct}%)",
|
|
316
|
+
"evidence": count, "key": period,
|
|
317
|
+
"distribution": dict(hour_counts)},
|
|
249
318
|
success_rate=pct / 100,
|
|
250
|
-
confidence=min(1.0, count /
|
|
319
|
+
confidence=min(1.0, count / max(total_hours * 0.1, 5)),
|
|
320
|
+
)
|
|
321
|
+
generated += 1
|
|
322
|
+
|
|
323
|
+
# ── 4. Entity Preferences (v3.4.1 NEW) ───────────────────
|
|
324
|
+
import json as _json
|
|
325
|
+
entity_counts: Counter = Counter()
|
|
326
|
+
for f in facts:
|
|
327
|
+
raw = dict(f).get("canonical_entities_json", "")
|
|
328
|
+
if raw:
|
|
329
|
+
try:
|
|
330
|
+
for ent in _json.loads(raw):
|
|
331
|
+
entity_counts[ent] += 1
|
|
332
|
+
except (ValueError, TypeError):
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
for entity, count in entity_counts.most_common(15):
|
|
336
|
+
if count >= 3 and not dry_run:
|
|
337
|
+
confidence = min(1.0, count / max(len(facts) * 0.05, 10))
|
|
338
|
+
store.record_pattern(
|
|
339
|
+
profile_id=profile_id,
|
|
340
|
+
pattern_type="interest",
|
|
341
|
+
data={"topic": entity, "pattern_key": f"entity:{entity}",
|
|
342
|
+
"value": entity, "evidence": count,
|
|
343
|
+
"source": "entity_frequency"},
|
|
344
|
+
success_rate=confidence,
|
|
345
|
+
confidence=confidence,
|
|
346
|
+
)
|
|
347
|
+
generated += 1
|
|
348
|
+
|
|
349
|
+
# ── 5. Session Activity Patterns (v3.4.1 NEW) ────────────
|
|
350
|
+
session_counts: Counter = Counter()
|
|
351
|
+
for f in facts:
|
|
352
|
+
sid = dict(f).get("session_id", "")
|
|
353
|
+
if sid:
|
|
354
|
+
session_counts[sid] += 1
|
|
355
|
+
|
|
356
|
+
if session_counts:
|
|
357
|
+
avg_facts_per_session = sum(session_counts.values()) / len(session_counts)
|
|
358
|
+
heavy_sessions = [s for s, c in session_counts.items() if c > avg_facts_per_session * 2]
|
|
359
|
+
if heavy_sessions and not dry_run:
|
|
360
|
+
store.record_pattern(
|
|
361
|
+
profile_id=profile_id,
|
|
362
|
+
pattern_type="workflow",
|
|
363
|
+
data={"pattern_key": "heavy_session_usage",
|
|
364
|
+
"value": f"{len(heavy_sessions)} intensive sessions",
|
|
365
|
+
"evidence": len(heavy_sessions),
|
|
366
|
+
"avg_facts": round(avg_facts_per_session, 1),
|
|
367
|
+
"total_sessions": len(session_counts)},
|
|
368
|
+
success_rate=0.8,
|
|
369
|
+
confidence=min(1.0, len(heavy_sessions) / 5),
|
|
370
|
+
)
|
|
371
|
+
generated += 1
|
|
372
|
+
|
|
373
|
+
# ── 6. Fact Type Distribution (v3.4.1 NEW) ────────────────
|
|
374
|
+
type_counts: Counter = Counter()
|
|
375
|
+
for f in facts:
|
|
376
|
+
ft = dict(f).get("fact_type", "semantic")
|
|
377
|
+
type_counts[ft] += 1
|
|
378
|
+
|
|
379
|
+
total_ft = sum(type_counts.values())
|
|
380
|
+
if total_ft > 0 and not dry_run:
|
|
381
|
+
dominant_type = type_counts.most_common(1)[0]
|
|
382
|
+
pct = round(dominant_type[1] / total_ft * 100)
|
|
383
|
+
store.record_pattern(
|
|
384
|
+
profile_id=profile_id,
|
|
385
|
+
pattern_type="style",
|
|
386
|
+
data={"pattern_key": "memory_style",
|
|
387
|
+
"value": f"{dominant_type[0]} dominant ({pct}%)",
|
|
388
|
+
"evidence": dominant_type[1],
|
|
389
|
+
"distribution": dict(type_counts)},
|
|
390
|
+
success_rate=pct / 100,
|
|
391
|
+
confidence=min(1.0, dominant_type[1] / 20),
|
|
392
|
+
)
|
|
393
|
+
generated += 1
|
|
394
|
+
|
|
395
|
+
# ── 7. Channel Performance (v3.4.1 NEW — from signals) ────
|
|
396
|
+
try:
|
|
397
|
+
learn_conn = sqlite3.connect(self._learning_db, timeout=10)
|
|
398
|
+
learn_conn.row_factory = sqlite3.Row
|
|
399
|
+
|
|
400
|
+
# Retrieval usage patterns from learning_feedback
|
|
401
|
+
channel_rows = learn_conn.execute(
|
|
402
|
+
"SELECT channel, COUNT(*) AS cnt, "
|
|
403
|
+
"AVG(signal_value) AS avg_signal "
|
|
404
|
+
"FROM learning_feedback "
|
|
405
|
+
"WHERE profile_id = ? "
|
|
406
|
+
"GROUP BY channel ORDER BY cnt DESC",
|
|
407
|
+
(profile_id,),
|
|
408
|
+
).fetchall()
|
|
409
|
+
|
|
410
|
+
for row in channel_rows:
|
|
411
|
+
d = dict(row)
|
|
412
|
+
ch = d.get("channel", "unknown")
|
|
413
|
+
cnt = d.get("cnt", 0)
|
|
414
|
+
avg_sig = round(float(d.get("avg_signal", 0) or 0), 3)
|
|
415
|
+
if cnt >= 5 and not dry_run:
|
|
416
|
+
store.record_pattern(
|
|
417
|
+
profile_id=profile_id,
|
|
418
|
+
pattern_type="style",
|
|
419
|
+
data={"pattern_key": f"channel:{ch}",
|
|
420
|
+
"value": f"{ch} ({cnt} hits, {avg_sig} avg)",
|
|
421
|
+
"evidence": cnt,
|
|
422
|
+
"avg_signal": avg_sig},
|
|
423
|
+
success_rate=avg_sig,
|
|
424
|
+
confidence=min(1.0, cnt / 50),
|
|
425
|
+
)
|
|
426
|
+
generated += 1
|
|
427
|
+
|
|
428
|
+
# Co-retrieval cluster patterns
|
|
429
|
+
try:
|
|
430
|
+
coret_rows = learn_conn.execute(
|
|
431
|
+
"SELECT fact_a, fact_b, co_access_count "
|
|
432
|
+
"FROM co_retrieval_edges "
|
|
433
|
+
"WHERE profile_id = ? AND co_access_count >= 3 "
|
|
434
|
+
"ORDER BY co_access_count DESC LIMIT 20",
|
|
435
|
+
(profile_id,),
|
|
436
|
+
).fetchall()
|
|
437
|
+
if coret_rows and not dry_run:
|
|
438
|
+
store.record_pattern(
|
|
439
|
+
profile_id=profile_id,
|
|
440
|
+
pattern_type="workflow",
|
|
441
|
+
data={"pattern_key": "co_retrieval_clusters",
|
|
442
|
+
"value": f"{len(coret_rows)} strong fact pairs",
|
|
443
|
+
"evidence": len(coret_rows),
|
|
444
|
+
"top_pair_count": dict(coret_rows[0]).get("co_access_count", 0) if coret_rows else 0},
|
|
445
|
+
success_rate=0.7,
|
|
446
|
+
confidence=min(1.0, len(coret_rows) / 10),
|
|
447
|
+
)
|
|
448
|
+
generated += 1
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
learn_conn.close()
|
|
453
|
+
except Exception as exc:
|
|
454
|
+
logger.debug("Signal pattern mining failed: %s", exc)
|
|
455
|
+
|
|
456
|
+
# ── 8. Community Membership (v3.4.1 NEW — from graph) ─────
|
|
457
|
+
try:
|
|
458
|
+
comm_rows = conn.execute(
|
|
459
|
+
"SELECT community_id, COUNT(*) AS cnt "
|
|
460
|
+
"FROM fact_importance "
|
|
461
|
+
"WHERE profile_id = ? AND community_id IS NOT NULL "
|
|
462
|
+
"GROUP BY community_id ORDER BY cnt DESC",
|
|
463
|
+
(profile_id,),
|
|
464
|
+
).fetchall()
|
|
465
|
+
if comm_rows and not dry_run:
|
|
466
|
+
total_comm = sum(dict(r)["cnt"] for r in comm_rows)
|
|
467
|
+
store.record_pattern(
|
|
468
|
+
profile_id=profile_id,
|
|
469
|
+
pattern_type="style",
|
|
470
|
+
data={"pattern_key": "knowledge_structure",
|
|
471
|
+
"value": f"{len(comm_rows)} topic communities, {total_comm} classified facts",
|
|
472
|
+
"evidence": total_comm,
|
|
473
|
+
"community_count": len(comm_rows)},
|
|
474
|
+
success_rate=0.8,
|
|
475
|
+
confidence=min(1.0, len(comm_rows) / 5),
|
|
251
476
|
)
|
|
252
477
|
generated += 1
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
253
480
|
|
|
481
|
+
conn.close()
|
|
482
|
+
|
|
483
|
+
logger.info(
|
|
484
|
+
"Pattern mining: %d patterns generated for profile %s "
|
|
485
|
+
"from %d facts",
|
|
486
|
+
generated, profile_id, len(facts),
|
|
487
|
+
)
|
|
254
488
|
return generated
|
|
255
489
|
except Exception as exc:
|
|
256
|
-
logger.
|
|
490
|
+
logger.warning("Pattern generation error: %s", exc)
|
|
257
491
|
return 0
|
|
258
492
|
|
|
259
493
|
def _retrain_ranker(self, profile_id: str, signal_count: int) -> bool:
|