mem01-engine 0.1.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.
- mem01/__init__.py +51 -0
- mem01/api/__init__.py +1 -0
- mem01/api/app.py +247 -0
- mem01/client.py +240 -0
- mem01/embeddings/__init__.py +7 -0
- mem01/embeddings/base.py +22 -0
- mem01/embeddings/fake.py +40 -0
- mem01/embeddings/openai_embedder.py +83 -0
- mem01/env.py +52 -0
- mem01/ids.py +15 -0
- mem01/llm/__init__.py +10 -0
- mem01/llm/anthropic.py +93 -0
- mem01/llm/base.py +30 -0
- mem01/llm/fake.py +35 -0
- mem01/llm/openai_compat.py +83 -0
- mem01/metrics.py +42 -0
- mem01/read/__init__.py +15 -0
- mem01/read/conflict.py +85 -0
- mem01/read/pack.py +73 -0
- mem01/read/rank.py +100 -0
- mem01/read/recall.py +83 -0
- mem01/read/search.py +127 -0
- mem01/runtime.py +102 -0
- mem01/store/__init__.py +20 -0
- mem01/store/base.py +95 -0
- mem01/store/factory.py +68 -0
- mem01/store/memory_store.py +211 -0
- mem01/store/postgres_store.py +511 -0
- mem01/tokens.py +19 -0
- mem01/types.py +234 -0
- mem01/write/__init__.py +6 -0
- mem01/write/apply_ops.py +356 -0
- mem01/write/extractor.py +171 -0
- mem01_engine-0.1.0.dist-info/METADATA +352 -0
- mem01_engine-0.1.0.dist-info/RECORD +37 -0
- mem01_engine-0.1.0.dist-info/WHEEL +4 -0
- mem01_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
mem01/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""mem01 — belief-based agent memory.
|
|
2
|
+
|
|
3
|
+
See PRODUCT.md for product intent and IMPLEMENTATION_PLAN.md for build order.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from mem01.client import MemoryClient, RememberResult
|
|
7
|
+
from mem01.ids import new_belief_id
|
|
8
|
+
from mem01.runtime import OpenAIRuntimeSettings, build_openai_memory_client
|
|
9
|
+
from mem01.store import (
|
|
10
|
+
BeliefStore,
|
|
11
|
+
InMemoryBeliefStore,
|
|
12
|
+
ScopeFilter,
|
|
13
|
+
create_belief_store,
|
|
14
|
+
)
|
|
15
|
+
from mem01.types import (
|
|
16
|
+
Belief,
|
|
17
|
+
BeliefOp,
|
|
18
|
+
BeliefOpType,
|
|
19
|
+
BeliefSource,
|
|
20
|
+
BeliefStatus,
|
|
21
|
+
PackedMemory,
|
|
22
|
+
ScopeIds,
|
|
23
|
+
ScopeKind,
|
|
24
|
+
ScoredBelief,
|
|
25
|
+
)
|
|
26
|
+
from mem01.write.apply_ops import ApplyResult
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"__version__",
|
|
32
|
+
"Belief",
|
|
33
|
+
"ApplyResult",
|
|
34
|
+
"BeliefOp",
|
|
35
|
+
"BeliefOpType",
|
|
36
|
+
"BeliefSource",
|
|
37
|
+
"BeliefStatus",
|
|
38
|
+
"BeliefStore",
|
|
39
|
+
"InMemoryBeliefStore",
|
|
40
|
+
"MemoryClient",
|
|
41
|
+
"OpenAIRuntimeSettings",
|
|
42
|
+
"PackedMemory",
|
|
43
|
+
"RememberResult",
|
|
44
|
+
"ScopeFilter",
|
|
45
|
+
"ScopeIds",
|
|
46
|
+
"ScopeKind",
|
|
47
|
+
"ScoredBelief",
|
|
48
|
+
"create_belief_store",
|
|
49
|
+
"build_openai_memory_client",
|
|
50
|
+
"new_belief_id",
|
|
51
|
+
]
|
mem01/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP API package."""
|
mem01/api/app.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""HTTP API for self-hosted mem01 (remember / recall / correct / forget).
|
|
2
|
+
|
|
3
|
+
Run via Docker Compose or:
|
|
4
|
+
uvicorn mem01.api.app:app --host 0.0.0.0 --port 8080
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, HTTPException
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from mem01.client import MemoryClient
|
|
16
|
+
from mem01.env import load_env
|
|
17
|
+
from mem01.runtime import build_openai_memory_client
|
|
18
|
+
from mem01.store.factory import create_belief_store
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RememberBody(BaseModel):
|
|
22
|
+
messages: list[dict[str, str]]
|
|
23
|
+
user_id: str
|
|
24
|
+
project_id: str | None = None
|
|
25
|
+
session_id: str | None = None
|
|
26
|
+
agent_id: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RecallBody(BaseModel):
|
|
30
|
+
query: str
|
|
31
|
+
user_id: str
|
|
32
|
+
project_id: str | None = None
|
|
33
|
+
session_id: str | None = None
|
|
34
|
+
agent_id: str | None = None
|
|
35
|
+
max_memory_tokens: int = Field(default=800, ge=0)
|
|
36
|
+
k: int = Field(default=20, ge=1, le=200)
|
|
37
|
+
# False = current truth only; True = include superseded/invalidated (labeled)
|
|
38
|
+
include_history: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HistoryBody(BaseModel):
|
|
42
|
+
user_id: str
|
|
43
|
+
project_id: str | None = None
|
|
44
|
+
session_id: str | None = None
|
|
45
|
+
agent_id: str | None = None
|
|
46
|
+
include_invalidated: bool = True
|
|
47
|
+
limit: int = Field(default=100, ge=1, le=500)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CorrectBody(BaseModel):
|
|
51
|
+
user_id: str
|
|
52
|
+
memory_id: str
|
|
53
|
+
new_value: str
|
|
54
|
+
confidence: float = Field(default=0.95, ge=0.0, le=1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ForgetBody(BaseModel):
|
|
58
|
+
user_id: str
|
|
59
|
+
memory_id: str
|
|
60
|
+
reason: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_client() -> MemoryClient:
|
|
64
|
+
load_env()
|
|
65
|
+
# Prefer real OpenAI when key present; else Fake for health-only deploys
|
|
66
|
+
import os
|
|
67
|
+
|
|
68
|
+
key = os.environ.get("OPENAI_API_KEY", "").strip()
|
|
69
|
+
if key:
|
|
70
|
+
return build_openai_memory_client()
|
|
71
|
+
|
|
72
|
+
store = create_belief_store()
|
|
73
|
+
from mem01.embeddings.fake import FakeEmbedder
|
|
74
|
+
from mem01.llm.fake import FakeLLM
|
|
75
|
+
|
|
76
|
+
dim = int(os.environ.get("MEM01_EMBEDDING_DIM", "1536"))
|
|
77
|
+
embedder = FakeEmbedder(dimensions=dim)
|
|
78
|
+
llm = FakeLLM("[]")
|
|
79
|
+
return MemoryClient(store=store, embedder=embedder, llm=llm)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@asynccontextmanager
|
|
83
|
+
async def lifespan(app: FastAPI):
|
|
84
|
+
app.state.client = _build_client()
|
|
85
|
+
yield
|
|
86
|
+
close = getattr(app.state.client.store, "close", None)
|
|
87
|
+
if callable(close):
|
|
88
|
+
close()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
app = FastAPI(
|
|
92
|
+
title="mem01",
|
|
93
|
+
description="Belief-based agent memory (self-hosted)",
|
|
94
|
+
version="0.1.0",
|
|
95
|
+
lifespan=lifespan,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.get("/health")
|
|
100
|
+
def health() -> dict[str, str]:
|
|
101
|
+
llm = app.state.client.llm
|
|
102
|
+
extraction_model = getattr(llm, "model", None)
|
|
103
|
+
if not isinstance(extraction_model, str) or not extraction_model:
|
|
104
|
+
extraction_model = "fake" if type(llm).__name__ == "FakeLLM" else "unknown"
|
|
105
|
+
return {
|
|
106
|
+
"status": "ok",
|
|
107
|
+
"service": "mem01",
|
|
108
|
+
"extraction_model": extraction_model,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.post("/v1/remember")
|
|
113
|
+
def remember(body: RememberBody) -> dict[str, Any]:
|
|
114
|
+
client: MemoryClient = app.state.client
|
|
115
|
+
try:
|
|
116
|
+
result = client.remember(
|
|
117
|
+
body.messages,
|
|
118
|
+
user_id=body.user_id,
|
|
119
|
+
project_id=body.project_id,
|
|
120
|
+
session_id=body.session_id,
|
|
121
|
+
agent_id=body.agent_id,
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
125
|
+
return {
|
|
126
|
+
"ok": result.apply.ok,
|
|
127
|
+
"created_ids": result.apply.created_ids,
|
|
128
|
+
"updated_ids": result.apply.updated_ids,
|
|
129
|
+
"superseded_ids": result.apply.superseded_ids,
|
|
130
|
+
"invalidated_ids": result.apply.invalidated_ids,
|
|
131
|
+
"errors": result.apply.errors,
|
|
132
|
+
"llm_calls": result.llm_calls,
|
|
133
|
+
"latency_ms": result.latency_ms,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.post("/v1/recall")
|
|
138
|
+
def recall(body: RecallBody) -> dict[str, Any]:
|
|
139
|
+
client: MemoryClient = app.state.client
|
|
140
|
+
try:
|
|
141
|
+
packed = client.recall(
|
|
142
|
+
body.query,
|
|
143
|
+
user_id=body.user_id,
|
|
144
|
+
project_id=body.project_id,
|
|
145
|
+
session_id=body.session_id,
|
|
146
|
+
agent_id=body.agent_id,
|
|
147
|
+
max_memory_tokens=body.max_memory_tokens,
|
|
148
|
+
k=body.k,
|
|
149
|
+
include_history=body.include_history,
|
|
150
|
+
)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
153
|
+
return {
|
|
154
|
+
"text": packed.text,
|
|
155
|
+
"tokens_used": packed.tokens_used,
|
|
156
|
+
"max_memory_tokens": packed.max_memory_tokens,
|
|
157
|
+
"candidate_count": packed.candidate_count,
|
|
158
|
+
"latency_ms": packed.latency_ms,
|
|
159
|
+
"include_history": body.include_history,
|
|
160
|
+
"beliefs": [
|
|
161
|
+
{
|
|
162
|
+
"id": b.id,
|
|
163
|
+
"content": b.content,
|
|
164
|
+
"status": b.status.value,
|
|
165
|
+
"supersedes_id": b.supersedes_id,
|
|
166
|
+
"source": b.source.value,
|
|
167
|
+
"valid_from": b.valid_from.isoformat() if b.valid_from else None,
|
|
168
|
+
"valid_to": b.valid_to.isoformat() if b.valid_to else None,
|
|
169
|
+
"created_at": b.created_at.isoformat(),
|
|
170
|
+
"updated_at": b.updated_at.isoformat(),
|
|
171
|
+
}
|
|
172
|
+
for b in packed.beliefs
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.post("/v1/history")
|
|
178
|
+
def history(body: HistoryBody) -> dict[str, Any]:
|
|
179
|
+
"""Full belief timeline for a scope — audit / “let me see previous facts”."""
|
|
180
|
+
client: MemoryClient = app.state.client
|
|
181
|
+
try:
|
|
182
|
+
beliefs = client.history(
|
|
183
|
+
user_id=body.user_id,
|
|
184
|
+
project_id=body.project_id,
|
|
185
|
+
session_id=body.session_id,
|
|
186
|
+
agent_id=body.agent_id,
|
|
187
|
+
include_invalidated=body.include_invalidated,
|
|
188
|
+
limit=body.limit,
|
|
189
|
+
)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
192
|
+
return {
|
|
193
|
+
"count": len(beliefs),
|
|
194
|
+
"beliefs": [
|
|
195
|
+
{
|
|
196
|
+
"id": b.id,
|
|
197
|
+
"content": b.content,
|
|
198
|
+
"status": b.status.value,
|
|
199
|
+
"confidence": b.confidence,
|
|
200
|
+
"supersedes_id": b.supersedes_id,
|
|
201
|
+
"source": b.source.value,
|
|
202
|
+
"valid_from": b.valid_from.isoformat() if b.valid_from else None,
|
|
203
|
+
"valid_to": b.valid_to.isoformat() if b.valid_to else None,
|
|
204
|
+
"created_at": b.created_at.isoformat(),
|
|
205
|
+
"updated_at": b.updated_at.isoformat(),
|
|
206
|
+
}
|
|
207
|
+
for b in beliefs
|
|
208
|
+
],
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.post("/v1/correct")
|
|
213
|
+
def correct(body: CorrectBody) -> dict[str, Any]:
|
|
214
|
+
client: MemoryClient = app.state.client
|
|
215
|
+
try:
|
|
216
|
+
result = client.correct(
|
|
217
|
+
body.memory_id,
|
|
218
|
+
body.new_value,
|
|
219
|
+
user_id=body.user_id,
|
|
220
|
+
confidence=body.confidence,
|
|
221
|
+
)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
224
|
+
return {
|
|
225
|
+
"ok": result.ok,
|
|
226
|
+
"created_ids": result.created_ids,
|
|
227
|
+
"superseded_ids": result.superseded_ids,
|
|
228
|
+
"errors": result.errors,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.post("/v1/forget")
|
|
233
|
+
def forget(body: ForgetBody) -> dict[str, Any]:
|
|
234
|
+
client: MemoryClient = app.state.client
|
|
235
|
+
try:
|
|
236
|
+
result = client.forget(
|
|
237
|
+
body.memory_id,
|
|
238
|
+
user_id=body.user_id,
|
|
239
|
+
reason=body.reason,
|
|
240
|
+
)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
243
|
+
return {
|
|
244
|
+
"ok": result.ok,
|
|
245
|
+
"invalidated_ids": result.invalidated_ids,
|
|
246
|
+
"errors": result.errors,
|
|
247
|
+
}
|
mem01/client.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""MemoryClient — public facade (mem0-class four verbs).
|
|
2
|
+
|
|
3
|
+
remember / recall / correct / forget wire write + read pipelines.
|
|
4
|
+
Users and later HTTP/MCP should depend on this, not internal modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from mem01.embeddings.base import Embedder
|
|
12
|
+
from mem01.llm.base import LLMClient
|
|
13
|
+
from mem01.metrics import timer
|
|
14
|
+
from mem01.read.recall import recall as recall_pipeline
|
|
15
|
+
from mem01.store.base import BeliefStore
|
|
16
|
+
from mem01.types import (
|
|
17
|
+
Belief,
|
|
18
|
+
BeliefOp,
|
|
19
|
+
BeliefOpType,
|
|
20
|
+
BeliefSource,
|
|
21
|
+
BeliefStatus,
|
|
22
|
+
PackedMemory,
|
|
23
|
+
ScopeIds,
|
|
24
|
+
ScopeKind,
|
|
25
|
+
)
|
|
26
|
+
from mem01.write.apply_ops import ApplyResult, apply_ops
|
|
27
|
+
from mem01.write.extractor import extract_ops
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RememberResult:
|
|
32
|
+
"""Outcome of remember()."""
|
|
33
|
+
|
|
34
|
+
apply: ApplyResult
|
|
35
|
+
ops: list[BeliefOp]
|
|
36
|
+
llm_calls: int
|
|
37
|
+
latency_ms: float
|
|
38
|
+
existing_considered: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class MemoryClient:
|
|
43
|
+
"""High-level memory API.
|
|
44
|
+
|
|
45
|
+
Defaults match product direction: inject real OpenAI (or other) LLM/embedder
|
|
46
|
+
for real use; tests can pass scripted/fake implementations.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
store: BeliefStore
|
|
50
|
+
embedder: Embedder
|
|
51
|
+
llm: LLMClient
|
|
52
|
+
default_user_id: str | None = None
|
|
53
|
+
# How many existing actives to show the extractor for SUPERSEDE decisions
|
|
54
|
+
existing_for_extract_k: int = 15
|
|
55
|
+
|
|
56
|
+
def _scope_ids(
|
|
57
|
+
self,
|
|
58
|
+
user_id: str | None = None,
|
|
59
|
+
*,
|
|
60
|
+
project_id: str | None = None,
|
|
61
|
+
session_id: str | None = None,
|
|
62
|
+
agent_id: str | None = None,
|
|
63
|
+
) -> ScopeIds:
|
|
64
|
+
uid = user_id if user_id is not None else self.default_user_id
|
|
65
|
+
return ScopeIds(
|
|
66
|
+
user_id=uid,
|
|
67
|
+
project_id=project_id,
|
|
68
|
+
session_id=session_id,
|
|
69
|
+
agent_id=agent_id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def remember(
|
|
73
|
+
self,
|
|
74
|
+
messages: list[dict[str, str]],
|
|
75
|
+
*,
|
|
76
|
+
user_id: str | None = None,
|
|
77
|
+
project_id: str | None = None,
|
|
78
|
+
session_id: str | None = None,
|
|
79
|
+
agent_id: str | None = None,
|
|
80
|
+
scope: ScopeKind = ScopeKind.USER,
|
|
81
|
+
) -> RememberResult:
|
|
82
|
+
"""Extract belief ops from messages (1 LLM call) and apply them."""
|
|
83
|
+
scope_ids = self._scope_ids(
|
|
84
|
+
user_id,
|
|
85
|
+
project_id=project_id,
|
|
86
|
+
session_id=session_id,
|
|
87
|
+
agent_id=agent_id,
|
|
88
|
+
)
|
|
89
|
+
with timer() as t:
|
|
90
|
+
existing = self._existing_snippets(scope_ids)
|
|
91
|
+
ops = extract_ops(
|
|
92
|
+
messages,
|
|
93
|
+
llm=self.llm,
|
|
94
|
+
existing_beliefs=existing,
|
|
95
|
+
scope=scope,
|
|
96
|
+
scope_ids=scope_ids,
|
|
97
|
+
)
|
|
98
|
+
apply_result = apply_ops(
|
|
99
|
+
self.store,
|
|
100
|
+
ops,
|
|
101
|
+
self.embedder,
|
|
102
|
+
default_source=BeliefSource.EXTRACTION,
|
|
103
|
+
expected_user_id=scope_ids.user_id,
|
|
104
|
+
)
|
|
105
|
+
return RememberResult(
|
|
106
|
+
apply=apply_result,
|
|
107
|
+
ops=ops,
|
|
108
|
+
llm_calls=1,
|
|
109
|
+
latency_ms=t.latency_ms,
|
|
110
|
+
existing_considered=len(existing),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def recall(
|
|
114
|
+
self,
|
|
115
|
+
query: str,
|
|
116
|
+
*,
|
|
117
|
+
user_id: str | None = None,
|
|
118
|
+
project_id: str | None = None,
|
|
119
|
+
session_id: str | None = None,
|
|
120
|
+
agent_id: str | None = None,
|
|
121
|
+
max_memory_tokens: int = 800,
|
|
122
|
+
k: int = 20,
|
|
123
|
+
include_history: bool = False,
|
|
124
|
+
) -> PackedMemory:
|
|
125
|
+
"""Hot-path retrieve — 0 LLM calls.
|
|
126
|
+
|
|
127
|
+
Default: active beliefs only (current truth).
|
|
128
|
+
include_history=True: also return superseded/invalidated, labeled in text
|
|
129
|
+
(use for “before SF?” / medical audit style questions).
|
|
130
|
+
"""
|
|
131
|
+
scope_ids = self._scope_ids(
|
|
132
|
+
user_id,
|
|
133
|
+
project_id=project_id,
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
agent_id=agent_id,
|
|
136
|
+
)
|
|
137
|
+
return recall_pipeline(
|
|
138
|
+
self.store,
|
|
139
|
+
self.embedder,
|
|
140
|
+
query,
|
|
141
|
+
scope_ids,
|
|
142
|
+
max_memory_tokens=max_memory_tokens,
|
|
143
|
+
k=k,
|
|
144
|
+
include_history=include_history,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def history(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
user_id: str | None = None,
|
|
151
|
+
project_id: str | None = None,
|
|
152
|
+
session_id: str | None = None,
|
|
153
|
+
agent_id: str | None = None,
|
|
154
|
+
include_invalidated: bool = True,
|
|
155
|
+
limit: int = 100,
|
|
156
|
+
) -> list[Belief]:
|
|
157
|
+
"""Chronological belief timeline for a scope (audit / “let me see”).
|
|
158
|
+
|
|
159
|
+
Includes active + superseded (+ invalidated by default). Newest first.
|
|
160
|
+
Does not call an LLM or run vector search.
|
|
161
|
+
"""
|
|
162
|
+
scope_ids = self._scope_ids(
|
|
163
|
+
user_id,
|
|
164
|
+
project_id=project_id,
|
|
165
|
+
session_id=session_id,
|
|
166
|
+
agent_id=agent_id,
|
|
167
|
+
)
|
|
168
|
+
statuses = {
|
|
169
|
+
BeliefStatus.ACTIVE,
|
|
170
|
+
BeliefStatus.SUPERSEDED,
|
|
171
|
+
}
|
|
172
|
+
if include_invalidated:
|
|
173
|
+
statuses.add(BeliefStatus.INVALIDATED)
|
|
174
|
+
beliefs = self.store.list_by_scope(scope_ids, statuses=statuses)
|
|
175
|
+
beliefs.sort(key=lambda b: b.updated_at, reverse=True)
|
|
176
|
+
return beliefs[: max(1, limit)]
|
|
177
|
+
|
|
178
|
+
def correct(
|
|
179
|
+
self,
|
|
180
|
+
memory_id: str,
|
|
181
|
+
new_value: str,
|
|
182
|
+
*,
|
|
183
|
+
user_id: str | None = None,
|
|
184
|
+
confidence: float = 0.95,
|
|
185
|
+
) -> ApplyResult:
|
|
186
|
+
"""Human/agent fix: SUPERSEDE by id (no LLM)."""
|
|
187
|
+
op = BeliefOp(
|
|
188
|
+
op=BeliefOpType.SUPERSEDE,
|
|
189
|
+
target_id=memory_id,
|
|
190
|
+
content=new_value,
|
|
191
|
+
confidence=confidence,
|
|
192
|
+
)
|
|
193
|
+
return apply_ops(
|
|
194
|
+
self.store,
|
|
195
|
+
[op],
|
|
196
|
+
self.embedder,
|
|
197
|
+
default_source=BeliefSource.CORRECTION,
|
|
198
|
+
expected_user_id=self._scope_ids(user_id).user_id,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def forget(
|
|
202
|
+
self,
|
|
203
|
+
memory_id: str,
|
|
204
|
+
*,
|
|
205
|
+
user_id: str | None = None,
|
|
206
|
+
reason: str | None = None,
|
|
207
|
+
) -> ApplyResult:
|
|
208
|
+
"""Invalidate a belief by id (no LLM)."""
|
|
209
|
+
op = BeliefOp(
|
|
210
|
+
op=BeliefOpType.INVALIDATE,
|
|
211
|
+
target_id=memory_id,
|
|
212
|
+
reason=reason,
|
|
213
|
+
)
|
|
214
|
+
return apply_ops(
|
|
215
|
+
self.store,
|
|
216
|
+
[op],
|
|
217
|
+
self.embedder,
|
|
218
|
+
default_source=BeliefSource.FORGET,
|
|
219
|
+
expected_user_id=self._scope_ids(user_id).user_id,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def clear_user(self, *, user_id: str | None = None) -> int:
|
|
223
|
+
"""Hard-delete all stored beliefs for one user."""
|
|
224
|
+
resolved_user_id = user_id if user_id is not None else self.default_user_id
|
|
225
|
+
if resolved_user_id is None or not resolved_user_id.strip():
|
|
226
|
+
raise ValueError("user_id must be non-empty")
|
|
227
|
+
return self.store.delete_by_user(resolved_user_id)
|
|
228
|
+
|
|
229
|
+
def get(self, memory_id: str) -> Belief | None:
|
|
230
|
+
return self.store.get(memory_id)
|
|
231
|
+
|
|
232
|
+
def _existing_snippets(self, scope_ids: ScopeIds) -> list[Belief]:
|
|
233
|
+
"""Active beliefs for extractor context (SUPERSEDE targets)."""
|
|
234
|
+
active = self.store.list_by_scope(
|
|
235
|
+
scope_ids,
|
|
236
|
+
statuses={BeliefStatus.ACTIVE},
|
|
237
|
+
)
|
|
238
|
+
# Prefer recently updated
|
|
239
|
+
active.sort(key=lambda b: b.updated_at, reverse=True)
|
|
240
|
+
return active[: self.existing_for_extract_k]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Embedding providers. Read and write paths depend only on Embedder."""
|
|
2
|
+
|
|
3
|
+
from mem01.embeddings.base import Embedder
|
|
4
|
+
from mem01.embeddings.fake import FakeEmbedder
|
|
5
|
+
from mem01.embeddings.openai_embedder import OpenAIEmbedder
|
|
6
|
+
|
|
7
|
+
__all__ = ["Embedder", "FakeEmbedder", "OpenAIEmbedder"]
|
mem01/embeddings/base.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Embedder protocol — text → vector.
|
|
2
|
+
|
|
3
|
+
Why an interface:
|
|
4
|
+
- Tests use FakeEmbedder (no network, deterministic)
|
|
5
|
+
- Production can use OpenAI / local models without changing apply_ops
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class Embedder(Protocol):
|
|
15
|
+
def embed(self, text: str) -> list[float]:
|
|
16
|
+
"""Return a fixed-dimension embedding for *text*."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def dimensions(self) -> int:
|
|
21
|
+
"""Vector size this embedder produces."""
|
|
22
|
+
...
|
mem01/embeddings/fake.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Deterministic embedder for tests (no API keys).
|
|
2
|
+
|
|
3
|
+
Maps text to a stable unit-ish vector via hashing tokens into buckets.
|
|
4
|
+
Similar strings are not guaranteed close — for tests we often set expectations
|
|
5
|
+
on ops/status, not semantic neighbors. When search order matters, tests can
|
|
6
|
+
hand-seed embeddings via the store.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FakeEmbedder:
|
|
17
|
+
def __init__(self, dimensions: int = 32) -> None:
|
|
18
|
+
if dimensions < 4:
|
|
19
|
+
raise ValueError("dimensions must be >= 4")
|
|
20
|
+
self._dimensions = dimensions
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def dimensions(self) -> int:
|
|
24
|
+
return self._dimensions
|
|
25
|
+
|
|
26
|
+
def embed(self, text: str) -> list[float]:
|
|
27
|
+
tokens = re.findall(r"[a-z0-9]+", text.lower())
|
|
28
|
+
vec = [0.0] * self._dimensions
|
|
29
|
+
if not tokens:
|
|
30
|
+
# empty → tiny bias so vector is non-zero
|
|
31
|
+
vec[0] = 1.0
|
|
32
|
+
return vec
|
|
33
|
+
for tok in tokens:
|
|
34
|
+
h = hashlib.sha256(tok.encode("utf-8")).digest()
|
|
35
|
+
idx = int.from_bytes(h[:4], "big") % self._dimensions
|
|
36
|
+
sign = 1.0 if h[4] % 2 == 0 else -1.0
|
|
37
|
+
vec[idx] += sign
|
|
38
|
+
# L2 normalize
|
|
39
|
+
norm = math.sqrt(sum(x * x for x in vec)) or 1.0
|
|
40
|
+
return [x / norm for x in vec]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""OpenAI embeddings — mem0-style default embedder for real use.
|
|
2
|
+
|
|
3
|
+
Uses POST /v1/embeddings (stdlib urllib). Requires OPENAI_API_KEY.
|
|
4
|
+
Not free (usage-based); typically very cheap at small scale.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenAIEmbedder:
|
|
16
|
+
"""text-embedding-3-small by default (same class of model mem0 uses)."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
model: str = "text-embedding-3-small",
|
|
23
|
+
base_url: str = "https://api.openai.com/v1",
|
|
24
|
+
dimensions: int | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
if api_key is None:
|
|
27
|
+
try:
|
|
28
|
+
from mem01.env import load_env
|
|
29
|
+
|
|
30
|
+
load_env()
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
self.api_key = os.environ.get("OPENAI_API_KEY") or ""
|
|
34
|
+
else:
|
|
35
|
+
self.api_key = api_key
|
|
36
|
+
self.model = model
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
# text-embedding-3-small default dim is 1536; optional API matryoshka trim
|
|
39
|
+
self._dimensions = dimensions
|
|
40
|
+
self._resolved_dim: int | None = dimensions
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def dimensions(self) -> int:
|
|
44
|
+
if self._resolved_dim is not None:
|
|
45
|
+
return self._resolved_dim
|
|
46
|
+
# Known default until first call
|
|
47
|
+
return 1536
|
|
48
|
+
|
|
49
|
+
def embed(self, text: str) -> list[float]:
|
|
50
|
+
if not self.api_key:
|
|
51
|
+
raise RuntimeError("OpenAIEmbedder requires api_key or OPENAI_API_KEY")
|
|
52
|
+
body: dict = {
|
|
53
|
+
"model": self.model,
|
|
54
|
+
"input": text,
|
|
55
|
+
}
|
|
56
|
+
if self._dimensions is not None:
|
|
57
|
+
body["dimensions"] = self._dimensions
|
|
58
|
+
|
|
59
|
+
url = f"{self.base_url}/embeddings"
|
|
60
|
+
data = json.dumps(body).encode("utf-8")
|
|
61
|
+
req = urllib.request.Request(
|
|
62
|
+
url,
|
|
63
|
+
data=data,
|
|
64
|
+
method="POST",
|
|
65
|
+
headers={
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
72
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
73
|
+
except urllib.error.HTTPError as e:
|
|
74
|
+
detail = e.read().decode("utf-8", errors="replace")
|
|
75
|
+
raise RuntimeError(f"OpenAI embeddings error {e.code}: {detail}") from e
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
vec = list(payload["data"][0]["embedding"])
|
|
79
|
+
except (KeyError, IndexError, TypeError) as e:
|
|
80
|
+
raise RuntimeError(f"unexpected embeddings response: {payload!r}") from e
|
|
81
|
+
|
|
82
|
+
self._resolved_dim = len(vec)
|
|
83
|
+
return vec
|