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 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"]
@@ -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
+ ...
@@ -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