Skip to content

Commit 53029bd

Browse files
feat: Implement graph-first memory ingestion and reasoning pipeline
- Added `ingestion.py` for parsing text and binding graph structure to memories. - Introduced `reasoning.py` for multi-hop memory inference and relation path explanation. - Created `scheduler.py` to manage memory tier transitions based on access patterns. - Developed `self_tune.py` for online tuning of retrieval weights based on agent feedback. - Implemented `fusion.py` for multi-objective retrieval scoring and weight normalization. - Added `ranker.py` for unified retrieval ranking and explainability. - Integrated `crewai_adapter.py` for CrewAI compatibility with OMem. - Developed `llama_index.py` for LlamaIndex integration as a retrieval backend. - Created tests for graph substrate and retrieval functionalities.
1 parent 4e3b9ec commit 53029bd

26 files changed

Lines changed: 2457 additions & 433 deletions

omem/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
from .api import OMem
44
from .core.engine import DreamResult, ForgetResult
55
from .types import (
6+
Evidence,
7+
GraphNode,
68
Memory,
9+
MemoryLevel,
710
MemoryPriority,
811
MemoryStatus,
912
MemoryTier,
1013
MemoryType,
14+
Provenance,
15+
RelationEdge,
1116
RetrievalExplanation,
1217
)
1318

@@ -22,6 +27,11 @@
2227
"MemoryPriority",
2328
"MemoryStatus",
2429
"Memory",
30+
"MemoryLevel",
31+
"GraphNode",
32+
"RelationEdge",
33+
"Evidence",
34+
"Provenance",
2535
"RetrievalExplanation",
2636
"ForgetResult",
2737
"DreamResult",

omem/api.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .backends.sqlite import SQLiteBackend
1212
from .core.engine import BrainTrace, DreamResult, ForgetResult
1313
from .security.audit import AuditLogger
14-
from .types import Memory, MemoryType, RetrievalExplanation
14+
from .types import Memory, MemoryTier, MemoryType, RetrievalExplanation
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -101,6 +101,57 @@ def add(
101101
self._audit.log("add", memory_id=memory_id_result, namespace=namespace, source=source)
102102
return memory_id_result
103103

104+
def add_experience(
105+
self,
106+
content: str,
107+
namespace: str = "default",
108+
source: str = "experience",
109+
confidence: float = 1.0,
110+
importance: Optional[float] = None,
111+
metadata: Optional[Dict] = None,
112+
) -> str:
113+
"""Graph-first ingestion for unstructured experience text."""
114+
memory_id = self.brain.add_experience(
115+
content,
116+
namespace=namespace,
117+
source=source,
118+
confidence=confidence,
119+
importance=importance,
120+
metadata=metadata,
121+
)
122+
self._audit.log("add_experience", memory_id=memory_id, namespace=namespace)
123+
return memory_id
124+
125+
def link_entities(
126+
self,
127+
source: str,
128+
target: str,
129+
relation: str = "related_to",
130+
memory_id: str = "",
131+
confidence: float = 1.0,
132+
) -> str:
133+
"""Explicitly link two entities in the knowledge graph."""
134+
return self.brain.link_entities(
135+
source, target, relation, memory_id=memory_id, confidence=confidence
136+
)
137+
138+
def assert_fact(
139+
self,
140+
subject: str,
141+
relation: str,
142+
obj: str,
143+
memory_id: str = "",
144+
confidence: float = 0.9,
145+
) -> Dict:
146+
"""Assert a structured fact as a high-confidence graph relation."""
147+
return self.brain.assert_fact(
148+
subject, relation, obj, memory_id=memory_id, confidence=confidence
149+
)
150+
151+
def query_graph(self, entity_name: str, depth: int = 2) -> Dict:
152+
"""Structured graph query: nodes, edges, and related memory IDs."""
153+
return self.brain.query_graph(entity_name, depth=depth)
154+
104155
def add_batch(
105156
self,
106157
contents: List[str],
@@ -143,16 +194,27 @@ def recall(
143194
time_range: Optional[str] = None,
144195
namespace: Optional[str] = None,
145196
project_only: bool = False,
197+
level: Optional[str] = None,
198+
tiers: Optional[List[MemoryTier]] = None,
199+
explain: bool = False,
200+
weight_overrides: Optional[Dict] = None,
201+
include_archive: bool = False,
146202
) -> List[Memory]:
147203
"""Advanced retrieval with context-type boosting and temporal filtering.
148204
149205
Args:
150206
query: The search query.
151207
k: Number of results.
152208
context_type: One of 'architecture', 'bugs', 'decisions', etc.
209+
mode: Retrieval mode profile ('default', 'planning', 'coding', 'chat', 'recall').
153210
time_range: One of 'today', 'recent', 'last_week'.
154211
namespace: Specific namespace.
155212
project_only: If True, only searches the provided namespace (doesn't mix global).
213+
level: Hierarchy filter ('working', 'short_term', 'long_term', 'archive').
214+
tiers: Optional list of MemoryTier enums to restrict search.
215+
explain: If True, populate explanations via get_explanations().
216+
weight_overrides: Optional dict to override fusion weight components.
217+
include_archive: Include archived memories in search.
156218
"""
157219
import time
158220

@@ -231,7 +293,17 @@ def recall(
231293
)
232294

233295
results = self.brain.rag(
234-
query, top_k=top_k, namespace=search_namespace, type_boosts=type_boosts
296+
query,
297+
top_k=top_k,
298+
namespace=search_namespace,
299+
type_boosts=type_boosts,
300+
mode=mode or "default",
301+
level=level,
302+
tiers=tiers,
303+
explain=explain,
304+
weight_overrides=weight_overrides,
305+
include_archive=include_archive,
306+
include_inactive=include_archive,
235307
)
236308

237309
# Post-filter for namespace if we searched wide
@@ -323,9 +395,33 @@ def inspect(
323395
query: str,
324396
top_k: int = 5,
325397
namespace: Optional[str] = None,
398+
mode: str = "default",
399+
weight_overrides: Optional[Dict] = None,
326400
) -> List[RetrievalExplanation]:
327-
"""Explain the scoring for a given query."""
328-
return self.brain.inspect(query, top_k, namespace)
401+
"""Explain the fusion scoring breakdown for a given query."""
402+
return self.brain.inspect(
403+
query,
404+
top_k,
405+
namespace,
406+
mode=mode,
407+
weight_overrides=weight_overrides,
408+
)
409+
410+
def get_explanations(self) -> List[RetrievalExplanation]:
411+
"""Return explanations from the last explain=True recall."""
412+
return self.brain.get_last_explanations()
413+
414+
def set_fusion_weights(self, weights: Dict[str, float]) -> None:
415+
"""Set default fusion weights for retrieval scoring."""
416+
from .core.retrieval.fusion import FusionWeights
417+
418+
current = self.brain.get_fusion_weights().as_dict()
419+
current.update(weights)
420+
self.brain.set_fusion_weights(FusionWeights(**current))
421+
422+
def get_fusion_weights(self) -> Dict[str, float]:
423+
"""Return current fusion weight configuration."""
424+
return self.brain.get_fusion_weights().as_dict()
329425

330426
def namespace_stats(self, namespace: str) -> Dict:
331427
"""Get stats for a specific namespace."""

omem/codebase/retriever.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,10 @@
1313
from typing import Any, Dict, List
1414

1515
from ..api import OMem
16+
from ..core.retrieval.fusion import fuse_score
17+
from ..core.retrieval.ranker import weights_for_mode
1618

17-
# Default ranking weights – these can be tuned later via a config file.
18-
DEFAULT_WEIGHTS = {
19-
"semantic": 0.5,
20-
"graph": 0.2,
21-
"recency": 0.15,
22-
"importance": 0.15,
23-
}
19+
DEFAULT_WEIGHTS = weights_for_mode("coding").as_dict()
2420

2521
class CodeRetriever:
2622
"""Retrieve code symbols for a natural‑language query.
@@ -104,10 +100,13 @@ def retrieve(
104100
# Pick the primary result (highest combined weight without graph yet)
105101
def combined(s):
106102
w = self.weights
107-
return (
108-
w["semantic"] * s["semantic"]
109-
+ w["recency"] * s["recency"]
110-
+ w["importance"] * s["importance"]
103+
return fuse_score(
104+
semantic=s["semantic"],
105+
keyword=0.0,
106+
recency=s["recency"],
107+
importance=s["importance"],
108+
graph=s["graph"],
109+
weights=weights_for_mode("coding"),
111110
)
112111

113112
scored.sort(key=combined, reverse=True)
@@ -120,11 +119,13 @@ def combined(s):
120119
entry["graph"] = 1.0 / (1 + self._graph_hops(primary_id, entry["mem"].id))
121120
# Re‑rank with graph component
122121
primary.sort(
123-
key=lambda s: (
124-
self.weights["semantic"] * s["semantic"]
125-
+ self.weights["graph"] * s["graph"]
126-
+ self.weights["recency"] * s["recency"]
127-
+ self.weights["importance"] * s["importance"]
122+
key=lambda s: fuse_score(
123+
semantic=s["semantic"],
124+
keyword=0.0,
125+
recency=s["recency"],
126+
importance=s["importance"],
127+
graph=s["graph"],
128+
weights=weights_for_mode("coding"),
128129
),
129130
reverse=True,
130131
)

omem/core/brain/dream.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,13 @@ def dream_consolidate(
259259
metadata={
260260
"source_count": len(cluster),
261261
"cluster_types": list(set(m.type.name for m in cluster)),
262+
"abstract": True,
262263
},
263264
insight_sources=cluster_source_ids,
264265
consolidation_count=len(cluster),
266+
confidence_score=min(0.6 + len(cluster) * 0.05, 0.95),
267+
evidence_count=len(cluster),
268+
level="long_term",
265269
)
266270
insight_memories.append(insight)
267271
result.insight_ids.append(insight_id)

omem/core/brain/forgetting.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ def compute_health(memory: Memory, now: Optional[float] = None) -> float:
8383
utility = getattr(memory, "utility_score", 0.0)
8484
utility_factor = 1.0 + min(utility, 1.0) * (_MAX_UTILITY_BOOST - 1.0)
8585

86-
return base * recency * usage * utility_factor
86+
confidence_factor = 0.5 + 0.5 * getattr(memory, "confidence_score", 1.0)
87+
evidence_factor = min(1.0 + getattr(memory, "evidence_count", 1) * 0.05, 1.5)
88+
89+
return base * recency * usage * utility_factor * confidence_factor * evidence_factor
8790

8891

8992
@dataclass

omem/core/brain/importance.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,23 @@ def run_decay_sweep(memories: List[Memory], now: Optional[float] = None) -> List
356356
mem.active = False
357357
deactivated.append(mem.id)
358358
return deactivated
359+
360+
361+
def reinforce_on_access(memory: Memory, now: Optional[float] = None) -> None:
362+
"""Online learning signal: boost importance and confidence on successful retrieval."""
363+
now = now or time.time()
364+
memory.access_count += 1
365+
memory.last_accessed = now
366+
memory.freshness = now
367+
368+
# Diminishing boost — frequently accessed memories stabilize
369+
boost = 0.02 / (1.0 + memory.access_count * 0.1)
370+
memory.importance = min(memory.importance + boost, 1.0)
371+
memory.base_score = min(memory.base_score + boost * 0.5, 2.0)
372+
memory.confidence_score = min(
373+
compute_confidence_score(memory) + boost * 0.5, 1.0
374+
)
375+
376+
# Promote working → short_term on repeated access
377+
if memory.level == "working" and memory.access_count >= 2:
378+
memory.level = "short_term"

omem/core/brain/ingestion.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Graph-first memory ingestion pipeline.
2+
3+
Experience → Understand → Connect
4+
5+
Parses text, extracts entities/relations, and binds graph structure to memories.
6+
"""
7+
8+
from dataclasses import dataclass, field
9+
from typing import List
10+
11+
from ...types import Memory
12+
from ..graph.knowledge import KnowledgeGraph
13+
14+
15+
@dataclass
16+
class IngestResult:
17+
"""Structured output from graph-first ingestion."""
18+
19+
memory_id: str
20+
node_ids: List[str] = field(default_factory=list)
21+
edge_ids: List[str] = field(default_factory=list)
22+
entities: List[str] = field(default_factory=list)
23+
confidence: float = 1.0
24+
evidence_count: int = 1
25+
relation_types: List[str] = field(default_factory=list)
26+
27+
28+
def ingest_experience(
29+
graph: KnowledgeGraph,
30+
memory_id: str,
31+
content: str,
32+
source: str = "user",
33+
confidence: float = 1.0,
34+
namespace: str = "default",
35+
user_name: str = "",
36+
) -> IngestResult:
37+
"""Run the graph ingestion pipeline for a new memory."""
38+
payload = graph.ingest_experience(
39+
memory_id=memory_id,
40+
content=content,
41+
source=source,
42+
confidence=confidence,
43+
namespace=namespace,
44+
user_name=user_name,
45+
)
46+
return IngestResult(
47+
memory_id=memory_id,
48+
node_ids=payload.get("node_ids", []),
49+
edge_ids=payload.get("edge_ids", []),
50+
entities=payload.get("entities", []),
51+
confidence=payload.get("confidence", confidence),
52+
evidence_count=payload.get("evidence_count", 1),
53+
relation_types=payload.get("relation_types", []),
54+
)
55+
56+
57+
def apply_ingest_to_memory(memory: Memory, ingest: IngestResult) -> None:
58+
"""Attach graph substrate metadata to a Memory record."""
59+
memory.node_ids = ingest.node_ids
60+
memory.edge_ids = ingest.edge_ids
61+
memory.entities = ingest.entities
62+
memory.confidence_score = ingest.confidence
63+
memory.evidence_count = ingest.evidence_count
64+
if not memory.provenance:
65+
memory.provenance = memory.source or "user"

0 commit comments

Comments
 (0)