loop-memory 0.4.0__py3-none-any.whl
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.
- loop_memory/__init__.py +62 -0
- loop_memory/backends/__init__.py +13 -0
- loop_memory/backends/embedding.py +82 -0
- loop_memory/backends/sentence_embedder.py +30 -0
- loop_memory/backends/vector_store.py +139 -0
- loop_memory/cli/__init__.py +0 -0
- loop_memory/cli/_common.py +68 -0
- loop_memory/cli/commands/__init__.py +13 -0
- loop_memory/cli/commands/cognitive.py +205 -0
- loop_memory/cli/commands/diag.py +346 -0
- loop_memory/cli/commands/graph.py +21 -0
- loop_memory/cli/commands/hooks.py +212 -0
- loop_memory/cli/commands/read.py +362 -0
- loop_memory/cli/commands/serve.py +147 -0
- loop_memory/cli/commands/write.py +138 -0
- loop_memory/cli/main.py +115 -0
- loop_memory/engine/__init__.py +0 -0
- loop_memory/engine/loop.py +247 -0
- loop_memory/engine/reflect.py +89 -0
- loop_memory/examples/__init__.py +0 -0
- loop_memory/examples/demo.py +39 -0
- loop_memory/export/__init__.py +39 -0
- loop_memory/export/memory_md.py +629 -0
- loop_memory/graph/__init__.py +0 -0
- loop_memory/graph/build.py +259 -0
- loop_memory/graph/extract.py +197 -0
- loop_memory/ingest/__init__.py +0 -0
- loop_memory/ingest/loader.py +782 -0
- loop_memory/ingest/pipeline.py +458 -0
- loop_memory/jobs/__init__.py +0 -0
- loop_memory/jobs/cognitive.py +353 -0
- loop_memory/jobs/compact.py +371 -0
- loop_memory/jobs/consolidate.py +95 -0
- loop_memory/jobs/contradiction.py +281 -0
- loop_memory/jobs/evolution.py +2021 -0
- loop_memory/jobs/graph.py +395 -0
- loop_memory/jobs/llm_compact_pass.py +24 -0
- loop_memory/jobs/llm_consolidate.py +980 -0
- loop_memory/jobs/scheduler.py +495 -0
- loop_memory/llm/__init__.py +0 -0
- loop_memory/llm/base.py +80 -0
- loop_memory/llm/openai_adapter.py +31 -0
- loop_memory/llm/providers.py +517 -0
- loop_memory/mcp/__init__.py +804 -0
- loop_memory/memory/__init__.py +0 -0
- loop_memory/memory/types.py +199 -0
- loop_memory/privacy/__init__.py +22 -0
- loop_memory/privacy/private.py +46 -0
- loop_memory/privacy/redact.py +188 -0
- loop_memory/py.typed +0 -0
- loop_memory/sdk.py +875 -0
- loop_memory/sdk_extensions.py +384 -0
- loop_memory/security/__init__.py +20 -0
- loop_memory/security/secrets.py +464 -0
- loop_memory/serve/__init__.py +0 -0
- loop_memory/serve/app.py +506 -0
- loop_memory/serve/handlers.py +316 -0
- loop_memory/serve/routes/_shared.py +59 -0
- loop_memory/serve/routes/admin.py +970 -0
- loop_memory/serve/routes/cognitive.py +64 -0
- loop_memory/serve/routes/export.py +65 -0
- loop_memory/serve/routes/graph.py +101 -0
- loop_memory/serve/routes/insights.py +702 -0
- loop_memory/serve/routes/memories.py +435 -0
- loop_memory/serve/routes/sessions.py +75 -0
- loop_memory/serve/routes/system.py +493 -0
- loop_memory/serve/routes/wiki.py +812 -0
- loop_memory/serve/static/__init__.py +0 -0
- loop_memory/serve/static/index.html +15 -0
- loop_memory/serve/watcher.py +451 -0
- loop_memory/storage/__init__.py +5 -0
- loop_memory/storage/retrieval.py +365 -0
- loop_memory/storage/sqlite_store.py +3627 -0
- loop_memory/wiki/__init__.py +41 -0
- loop_memory/wiki/backfill.py +143 -0
- loop_memory/wiki/classifier.py +238 -0
- loop_memory/wiki/prompts.py +295 -0
- loop_memory/wiki/scope.py +227 -0
- loop_memory-0.4.0.dist-info/METADATA +627 -0
- loop_memory-0.4.0.dist-info/RECORD +84 -0
- loop_memory-0.4.0.dist-info/WHEEL +5 -0
- loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
- loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
- loop_memory-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Graph memory + 3D adaptive scoring for the Universal Agent Memory
|
|
2
|
+
contract.
|
|
3
|
+
|
|
4
|
+
This module has three responsibilities, all additive on top of the
|
|
5
|
+
existing ``KnowledgeGraph`` in ``graph/build.py``:
|
|
6
|
+
|
|
7
|
+
1. **Semantic graph edges** — ``upsert_semantic_edge(src, dst,
|
|
8
|
+
kind, weight, evidence_id)`` lets any Agent push high-value
|
|
9
|
+
relationships like ``(User)-[LIVES_IN]->(Hangzhou)`` that
|
|
10
|
+
vector search can never reconstruct on its own. Mem0's
|
|
11
|
+
differentiator; loop-memory now has it.
|
|
12
|
+
|
|
13
|
+
2. **Graph-aware recall** — ``graph_boost(store, query, memory_ids)``
|
|
14
|
+
returns a per-memory ``[0, 1.5]`` multiplier based on the density
|
|
15
|
+
of the subgraph the memory shares with the query's entities. The
|
|
16
|
+
hybrid recall path multiplies its RRF score by this to push
|
|
17
|
+
connected memories to the top.
|
|
18
|
+
|
|
19
|
+
3. **3D adaptive scoring** — ``adaptive_score(recall_count, last_recalled,
|
|
20
|
+
importance, graph_degree)`` blends usage / importance / graph
|
|
21
|
+
connectivity into a single [0, 1] number that callers can use to
|
|
22
|
+
rank "what's worth surfacing right now". Article 7 calls this
|
|
23
|
+
"the third dimension" Mem0 added on top of plain vector recall.
|
|
24
|
+
|
|
25
|
+
The module is zero-dep and only uses ``MemoryStore``; it can be
|
|
26
|
+
called from the SDK, the HTTP route, the CLI, or the MCP server
|
|
27
|
+
without any extra setup.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import math
|
|
34
|
+
import time
|
|
35
|
+
from collections.abc import Iterable
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from ..storage.sqlite_store import MemoryStore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Semantic graph edges
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Relation kinds we care about. Kept short so the SQL filter stays
|
|
48
|
+
# cheap. ``co_occurs_with`` is the legacy kind the original
|
|
49
|
+
# ``KnowledgeGraph`` writes; the new ones below are the "high-signal"
|
|
50
|
+
# predicates an Agent pushes explicitly.
|
|
51
|
+
HIGH_SIGNAL_KINDS: frozenset[str] = frozenset({
|
|
52
|
+
"lives_in", "works_on", "uses", "prefers", "decided",
|
|
53
|
+
"depends_on", "owns", "manages", "blocks", "replaces",
|
|
54
|
+
"caused", "fixed_by", "documented_in", "person",
|
|
55
|
+
"项目", "决策", "偏好", "工具", "技术栈",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def upsert_semantic_edge(
|
|
60
|
+
store: MemoryStore,
|
|
61
|
+
src: str,
|
|
62
|
+
dst: str,
|
|
63
|
+
*,
|
|
64
|
+
kind: str = "relates_to",
|
|
65
|
+
weight: float = 0.5,
|
|
66
|
+
evidence_id: str | None = None,
|
|
67
|
+
) -> dict[str, Any]:
|
|
68
|
+
"""Push a high-signal relation between two entities.
|
|
69
|
+
|
|
70
|
+
``src`` and ``dst`` are entity names (not ids). The store upserts
|
|
71
|
+
the underlying ``entities`` rows and the ``relations`` row in
|
|
72
|
+
one call so the graph stays consistent even if the entities
|
|
73
|
+
have never been seen before.
|
|
74
|
+
|
|
75
|
+
Returns a small dict describing what was written so the caller
|
|
76
|
+
can log it via ``record_audit``.
|
|
77
|
+
"""
|
|
78
|
+
src = (src or "").strip()
|
|
79
|
+
dst = (dst or "").strip()
|
|
80
|
+
if not src or not dst or src == dst:
|
|
81
|
+
raise ValueError("upsert_semantic_edge needs two distinct non-empty names")
|
|
82
|
+
# Ensure both entities exist
|
|
83
|
+
store.upsert_entity(src, _kind_for(src), bump_weight=weight)
|
|
84
|
+
store.upsert_entity(dst, _kind_for(dst), bump_weight=weight)
|
|
85
|
+
store.upsert_relation(src, dst, kind=kind, weight=weight, evidence_id=evidence_id)
|
|
86
|
+
return {
|
|
87
|
+
"src": src,
|
|
88
|
+
"dst": dst,
|
|
89
|
+
"kind": kind,
|
|
90
|
+
"weight": weight,
|
|
91
|
+
"evidence_id": evidence_id,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _kind_for(name: str) -> str:
|
|
96
|
+
"""Best-effort guess at the entity ``kind`` for a name.
|
|
97
|
+
|
|
98
|
+
The legacy ``extract_entities`` only emits a small set of kinds
|
|
99
|
+
(``concept``, ``person``, ``tag``, ``project``, ``tool``,
|
|
100
|
+
``company``). When the Agent pushes a name it didn't extract
|
|
101
|
+
from text we want it stored under the most useful existing
|
|
102
|
+
kind so the UI can colour it correctly.
|
|
103
|
+
"""
|
|
104
|
+
n = (name or "").lower()
|
|
105
|
+
if any(tok in n for tok in ("项目", "project", "repo", "service", "系统", "模块")):
|
|
106
|
+
return "project"
|
|
107
|
+
if any(tok in n for tok in ("工具", "tool", "framework", "库", "lib")):
|
|
108
|
+
return "tool"
|
|
109
|
+
if any(tok in n for tok in ("公司", "company", "inc", "团队", "team")):
|
|
110
|
+
return "company"
|
|
111
|
+
if n[:1].isupper() and n[1:2].islower():
|
|
112
|
+
return "concept"
|
|
113
|
+
return "concept"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Graph-aware recall boost
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class GraphBoost:
|
|
123
|
+
"""Per-memory graph connectivity score in [0, 1.5]."""
|
|
124
|
+
|
|
125
|
+
memory_id: str
|
|
126
|
+
boost: float
|
|
127
|
+
matched_entities: list[str]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def graph_boost(
|
|
131
|
+
store: MemoryStore,
|
|
132
|
+
query: str,
|
|
133
|
+
memory_ids: list[str],
|
|
134
|
+
*,
|
|
135
|
+
max_per_memory: int = 8,
|
|
136
|
+
) -> dict[str, GraphBoost]:
|
|
137
|
+
"""Return a ``{memory_id: GraphBoost}`` map for the given candidates.
|
|
138
|
+
|
|
139
|
+
The boost is the density of the subgraph the memory shares with
|
|
140
|
+
the query's entities. A memory whose entities connect to the
|
|
141
|
+
query's entities (either directly or via one hop) gets a boost;
|
|
142
|
+
isolated memories get 0. The maximum is 1.5 so it can dominate
|
|
143
|
+
the RRF score when the connection is strong.
|
|
144
|
+
|
|
145
|
+
The implementation is deliberately simple:
|
|
146
|
+
|
|
147
|
+
1. Extract entities from the query text (using the same
|
|
148
|
+
``extract_entities`` the graph builder uses).
|
|
149
|
+
2. Find every relation (in either direction) where one endpoint
|
|
150
|
+
is a query entity.
|
|
151
|
+
3. Look up which memory ids back each matched relation via the
|
|
152
|
+
``entity_mentions`` table.
|
|
153
|
+
4. Count matches per memory id; map count → boost with a
|
|
154
|
+
saturating log curve.
|
|
155
|
+
|
|
156
|
+
The lookup is bounded to ``max_per_memory`` hits per memory so
|
|
157
|
+
a runaway relation doesn't lock the score to its cap.
|
|
158
|
+
"""
|
|
159
|
+
if not memory_ids or not query.strip():
|
|
160
|
+
return {}
|
|
161
|
+
from ..graph.extract import extract_entities
|
|
162
|
+
query_entities = [n for (n, _) in extract_entities(query) if n]
|
|
163
|
+
if not query_entities:
|
|
164
|
+
return {}
|
|
165
|
+
# All related entities (1-hop away from query entities)
|
|
166
|
+
related: dict[str, list[str]] = {n: [] for n in query_entities}
|
|
167
|
+
for ent in query_entities:
|
|
168
|
+
rels = store.search_entities_by_names([ent], limit=64)
|
|
169
|
+
for r in rels:
|
|
170
|
+
# ``search_entities_by_names`` returns canonical name in
|
|
171
|
+
# ``name``; we want the *other* side of each relation.
|
|
172
|
+
others = store.related_entities(r["name"], limit=64)
|
|
173
|
+
for o in others:
|
|
174
|
+
if o != ent:
|
|
175
|
+
related.setdefault(ent, []).append(o)
|
|
176
|
+
if not any(related.values()):
|
|
177
|
+
return {}
|
|
178
|
+
# Map: memory_id -> set of matched query entities (de-duplicated)
|
|
179
|
+
mem_to_matches: dict[str, set[str]] = {m: set() for m in memory_ids}
|
|
180
|
+
for qent, others in related.items():
|
|
181
|
+
if not others:
|
|
182
|
+
continue
|
|
183
|
+
# Which memory ids back `qent`?
|
|
184
|
+
mems_for_qent = store.memory_ids_for_entity(qent, limit=512)
|
|
185
|
+
# Which memory ids back each of the 1-hop neighbours?
|
|
186
|
+
for nb in others:
|
|
187
|
+
mems_for_nb = store.memory_ids_for_entity(nb, limit=512)
|
|
188
|
+
for mid in set(mems_for_qent) & set(mems_for_nb) & set(memory_ids):
|
|
189
|
+
mem_to_matches[mid].add(qent)
|
|
190
|
+
if len(mem_to_matches[mid]) >= max_per_memory:
|
|
191
|
+
break
|
|
192
|
+
out: dict[str, GraphBoost] = {}
|
|
193
|
+
for mid, matches in mem_to_matches.items():
|
|
194
|
+
if not matches:
|
|
195
|
+
continue
|
|
196
|
+
# Saturating log so 1 hit = 0.45, 2 = 0.7, 4+ → 1.0+, capped
|
|
197
|
+
# at 1.5.
|
|
198
|
+
n = len(matches)
|
|
199
|
+
boost = min(1.5, 0.45 + 0.25 * math.log1p(n))
|
|
200
|
+
out[mid] = GraphBoost(memory_id=mid, boost=round(boost, 3),
|
|
201
|
+
matched_entities=sorted(matches))
|
|
202
|
+
return out
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# 3D adaptive scoring
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass
|
|
211
|
+
class AdaptiveScore:
|
|
212
|
+
"""The three dimensions Mem0 fuses, plus the blended score.
|
|
213
|
+
|
|
214
|
+
All numbers in [0, 1]. ``blended`` is a weighted average with
|
|
215
|
+
weights that bias toward ``importance`` so the highest-quality
|
|
216
|
+
memories still dominate even when their usage is low.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
importance: float
|
|
220
|
+
recency: float
|
|
221
|
+
usage: float
|
|
222
|
+
graph: float
|
|
223
|
+
blended: float
|
|
224
|
+
|
|
225
|
+
def to_dict(self) -> dict[str, float]:
|
|
226
|
+
return {
|
|
227
|
+
"importance": round(self.importance, 4),
|
|
228
|
+
"recency": round(self.recency, 4),
|
|
229
|
+
"usage": round(self.usage, 4),
|
|
230
|
+
"graph": round(self.graph, 4),
|
|
231
|
+
"blended": round(self.blended, 4),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def adaptive_score(
|
|
236
|
+
*,
|
|
237
|
+
importance: float,
|
|
238
|
+
created_at: float,
|
|
239
|
+
now: float | None = None,
|
|
240
|
+
recall_count: int = 0,
|
|
241
|
+
last_recalled_at: float | None = None,
|
|
242
|
+
graph_degree: int = 0,
|
|
243
|
+
half_life_days: float = 30.0,
|
|
244
|
+
weights: dict[str, float] | None = None,
|
|
245
|
+
) -> AdaptiveScore:
|
|
246
|
+
"""Compute the 3D adaptive score.
|
|
247
|
+
|
|
248
|
+
The four components are:
|
|
249
|
+
|
|
250
|
+
* ``importance`` — the LLM/original importance (already [0, 1]).
|
|
251
|
+
* ``recency`` — half-life decay on the memory's age.
|
|
252
|
+
* ``usage`` — log-saturated recall_count, further discounted by
|
|
253
|
+
the age of the most recent recall (a memory that was hot two
|
|
254
|
+
months ago is not as hot as one that was hot yesterday).
|
|
255
|
+
* ``graph`` — log-saturated graph degree, capped at 1.0 (a
|
|
256
|
+
memory connected to 8+ entities is a "hub" worth promoting).
|
|
257
|
+
|
|
258
|
+
``blended`` = w_i*importance + w_r*recency + w_u*usage + w_g*graph,
|
|
259
|
+
with default weights ``{importance: 0.40, recency: 0.20, usage:
|
|
260
|
+
0.25, graph: 0.15}``. Callers can override via ``weights=``.
|
|
261
|
+
|
|
262
|
+
Pure function — no DB access — so the unit tests can hammer it
|
|
263
|
+
with crafted inputs.
|
|
264
|
+
"""
|
|
265
|
+
w = weights or {"importance": 0.40, "recency": 0.20, "usage": 0.25, "graph": 0.15}
|
|
266
|
+
now = now if now is not None else time.time()
|
|
267
|
+
age = max(0.0, now - created_at)
|
|
268
|
+
half_life = half_life_days * 86400.0
|
|
269
|
+
recency = (0.5 ** (age / half_life)) if half_life else 1.0
|
|
270
|
+
recency = max(0.0, min(1.0, recency))
|
|
271
|
+
|
|
272
|
+
if recall_count > 0:
|
|
273
|
+
log_recall = math.log1p(recall_count) / math.log1p(100)
|
|
274
|
+
log_recall = max(0.0, min(1.0, log_recall))
|
|
275
|
+
if last_recalled_at:
|
|
276
|
+
age_recall = max(0.0, now - last_recalled_at)
|
|
277
|
+
usage_recency = (0.5 ** (age_recall / half_life)) if half_life else 1.0
|
|
278
|
+
else:
|
|
279
|
+
usage_recency = 1.0
|
|
280
|
+
usage = log_recall * (0.25 + 0.75 * usage_recency)
|
|
281
|
+
else:
|
|
282
|
+
usage = 0.0
|
|
283
|
+
usage = max(0.0, min(1.0, usage))
|
|
284
|
+
|
|
285
|
+
graph = 0.0
|
|
286
|
+
if graph_degree > 0:
|
|
287
|
+
graph = math.log1p(graph_degree) / math.log1p(8) # 8 hops = full credit
|
|
288
|
+
graph = max(0.0, min(1.0, graph))
|
|
289
|
+
|
|
290
|
+
imp = max(0.0, min(1.0, float(importance or 0)))
|
|
291
|
+
blended = (
|
|
292
|
+
w.get("importance", 0.40) * imp
|
|
293
|
+
+ w.get("recency", 0.20) * recency
|
|
294
|
+
+ w.get("usage", 0.25) * usage
|
|
295
|
+
+ w.get("graph", 0.15) * graph
|
|
296
|
+
)
|
|
297
|
+
blended = max(0.0, min(1.0, blended))
|
|
298
|
+
return AdaptiveScore(
|
|
299
|
+
importance=imp,
|
|
300
|
+
recency=recency,
|
|
301
|
+
usage=usage,
|
|
302
|
+
graph=graph,
|
|
303
|
+
blended=blended,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# Convenience: small subgraph snapshot
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass
|
|
313
|
+
class Subgraph:
|
|
314
|
+
"""A trimmed-down view of the graph for an Agent prompt."""
|
|
315
|
+
|
|
316
|
+
nodes: list[dict[str, Any]]
|
|
317
|
+
edges: list[dict[str, Any]]
|
|
318
|
+
memory_ids: list[str]
|
|
319
|
+
|
|
320
|
+
def to_dict(self) -> dict[str, Any]:
|
|
321
|
+
return {
|
|
322
|
+
"nodes": self.nodes,
|
|
323
|
+
"edges": self.edges,
|
|
324
|
+
"memory_ids": self.memory_ids,
|
|
325
|
+
"node_count": len(self.nodes),
|
|
326
|
+
"edge_count": len(self.edges),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def subgraph_for(
|
|
331
|
+
store: MemoryStore,
|
|
332
|
+
query: str,
|
|
333
|
+
*,
|
|
334
|
+
max_hops: int = 1,
|
|
335
|
+
max_nodes: int = 32,
|
|
336
|
+
max_edges: int = 64,
|
|
337
|
+
) -> Subgraph:
|
|
338
|
+
"""Build a small subgraph relevant to ``query``.
|
|
339
|
+
|
|
340
|
+
1. Extract entities from the query.
|
|
341
|
+
2. Pull every relation (1 hop) that touches a query entity.
|
|
342
|
+
3. Look up the backing memory ids for each entity.
|
|
343
|
+
4. Trim to ``max_nodes`` / ``max_edges``.
|
|
344
|
+
|
|
345
|
+
Returns a :class:`Subgraph` dataclass so callers can render it
|
|
346
|
+
however they like (Mermaid, JSON for the SDK, etc.).
|
|
347
|
+
"""
|
|
348
|
+
from ..graph.extract import extract_entities
|
|
349
|
+
ents = [n for (n, _) in extract_entities(query) if n]
|
|
350
|
+
if not ents:
|
|
351
|
+
return Subgraph(nodes=[], edges=[], memory_ids=[])
|
|
352
|
+
nodes: dict[str, dict[str, Any]] = {}
|
|
353
|
+
edges: list[dict[str, Any]] = []
|
|
354
|
+
memory_ids: set[str] = set()
|
|
355
|
+
for ent in ents:
|
|
356
|
+
info = store.entity_by_name(ent)
|
|
357
|
+
if info:
|
|
358
|
+
nodes[ent] = {
|
|
359
|
+
"name": ent,
|
|
360
|
+
"kind": info.get("kind") or "concept",
|
|
361
|
+
"weight": float(info.get("weight") or 0),
|
|
362
|
+
"mention_count": int(info.get("mention_count") or 0),
|
|
363
|
+
}
|
|
364
|
+
else:
|
|
365
|
+
nodes[ent] = {"name": ent, "kind": "concept", "weight": 0.5, "mention_count": 0}
|
|
366
|
+
for nb in store.related_entities(ent, limit=32):
|
|
367
|
+
if nb not in nodes:
|
|
368
|
+
nb_info = store.entity_by_name(nb) or {}
|
|
369
|
+
nodes[nb] = {
|
|
370
|
+
"name": nb,
|
|
371
|
+
"kind": nb_info.get("kind") or "concept",
|
|
372
|
+
"weight": float(nb_info.get("weight") or 0),
|
|
373
|
+
"mention_count": int(nb_info.get("mention_count") or 0),
|
|
374
|
+
}
|
|
375
|
+
edge = {"src": ent, "dst": nb, "kind": "related"}
|
|
376
|
+
if edge not in edges:
|
|
377
|
+
edges.append(edge)
|
|
378
|
+
for mid in store.memory_ids_for_entity(ent, limit=32):
|
|
379
|
+
memory_ids.add(mid)
|
|
380
|
+
# Trim
|
|
381
|
+
n_nodes = list(nodes.values())[:max_nodes]
|
|
382
|
+
n_edges = edges[:max_edges]
|
|
383
|
+
return Subgraph(nodes=n_nodes, edges=n_edges, memory_ids=sorted(memory_ids))
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = [
|
|
387
|
+
"AdaptiveScore",
|
|
388
|
+
"GraphBoost",
|
|
389
|
+
"HIGH_SIGNAL_KINDS",
|
|
390
|
+
"Subgraph",
|
|
391
|
+
"adaptive_score",
|
|
392
|
+
"graph_boost",
|
|
393
|
+
"subgraph_for",
|
|
394
|
+
"upsert_semantic_edge",
|
|
395
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Optional LLM-driven memory fusion.
|
|
2
|
+
|
|
3
|
+
Placeholder for the moment — the wiki consolidator already produces
|
|
4
|
+
high-quality distilled pages. Re-implementing memory-layer fusion here
|
|
5
|
+
would burn the same context twice. Keep this module around so the
|
|
6
|
+
``Compactor`` config knob can switch modes without an import error.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..storage.sqlite_store import MemoryStore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def llm_fuse_pass(store: MemoryStore, *, force: bool = False) -> int:
|
|
16
|
+
"""Stub. Returns 0. The real implementation lives in
|
|
17
|
+
:mod:`jobs.evolution` — this pass is intentionally a no-op until
|
|
18
|
+
we have evidence the heuristic layer is leaving valuable signal
|
|
19
|
+
on the floor.
|
|
20
|
+
"""
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["llm_fuse_pass"]
|