remora-ai 0.1.0__tar.gz

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.
@@ -0,0 +1,29 @@
1
+ # Node dependencies & outputs
2
+ node_modules/
3
+ .npm
4
+ dist/
5
+
6
+ # Python caches & build artifacts
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.egg-info/
11
+ .eggs/
12
+ build/
13
+ .pytest_cache/
14
+
15
+ # Environment files
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+
20
+ # OS / IDE
21
+ .DS_Store
22
+ Thumbs.db
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+ *.swo
27
+
28
+ # Bun / Test logs
29
+ *.log
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: remora-ai
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for Remora AI Memory Infrastructure
5
+ Author-email: Remora Team <hello@remora.ai>
6
+ License: MIT
7
+ Keywords: agents,ai,crewai,embeddings,langchain,llm,mem0,memory,pgvector,vector-search
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: httpx>=0.25.0
10
+ Requires-Dist: pydantic>=2.0.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
13
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Remora Python SDK (`remora-ai`)
17
+
18
+ > **Developer-first AI Memory Infrastructure Client** for persistent, evolving agent memory.
19
+
20
+ ---
21
+
22
+ ## 📦 Installation
23
+
24
+ ```bash
25
+ pip install remora-ai
26
+ ```
27
+
28
+ *(Note: The client uses Python standard library `urllib` internally, requiring zero extra dependencies!)*
29
+
30
+ ---
31
+
32
+ ## 🚀 Quickstart
33
+
34
+ ```python
35
+ from remora import RemoraClient
36
+
37
+ remora = RemoraClient(
38
+ api_key="rl_live_your_key_here",
39
+ base_url="http://localhost:3000" # Defaults to http://localhost:3000
40
+ )
41
+
42
+ # 1. Ingest a conversation (async extraction with zero chatbot latency)
43
+ remora.ingest(
44
+ user_id="usr_alice",
45
+ messages=[
46
+ {"role": "user", "content": "I am building a SaaS using Python and PostgreSQL."}
47
+ ]
48
+ )
49
+
50
+ # 2. Retrieve compact prompt-ready context before replying
51
+ ctx = remora.context(
52
+ user_id="usr_alice",
53
+ query="What tech stack should I recommend?"
54
+ )
55
+
56
+ print(ctx.context_text)
57
+ # Output:
58
+ # Relevant user memories:
59
+ # - User is building a SaaS using Python and PostgreSQL.
60
+ # Use these memories only when relevant.
61
+
62
+ # 3. Direct memory search with hybrid ranking scores
63
+ memories = remora.search(
64
+ user_id="usr_alice",
65
+ query="programming language and database"
66
+ )
67
+
68
+ for m in memories:
69
+ print(f"- {m.content} (Score: {m.final_score})")
70
+ print(f" Similarity: {m.score_breakdown.semantic_similarity}")
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 🤖 Integration with OpenAI / LangChain
76
+
77
+ ```python
78
+ from openai import OpenAI
79
+ from remora import RemoraClient
80
+
81
+ remora = RemoraClient(api_key="rl_live_...")
82
+ openai = OpenAI()
83
+
84
+ user_id = "usr_alice"
85
+ user_message = "What database should I choose?"
86
+
87
+ # 1. Fetch relevant persistent memories
88
+ ctx = remora.context(user_id=user_id, query=user_message)
89
+
90
+ # 2. Inject context into system prompt
91
+ system_prompt = f"You are a helpful coding copilot.\n\n{ctx.context_text}"
92
+
93
+ response = openai.chat.completions.create(
94
+ model="gpt-4o",
95
+ messages=[
96
+ {"role": "system", "content": system_prompt},
97
+ {"role": "user", "content": user_message}
98
+ ]
99
+ )
100
+
101
+ print(response.choices[0].message.content)
102
+
103
+ # 3. Asynchronously ingest the turn for future memory formation
104
+ remora.ingest(
105
+ user_id=user_id,
106
+ messages=[
107
+ {"role": "user", "content": user_message},
108
+ {"role": "assistant", "content": response.choices[0].message.content}
109
+ ]
110
+ )
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 📚 API Reference
116
+
117
+ - `remora.add(user_id, content, namespace="default", type="FACT", importance=0.5)`
118
+ - `remora.search(user_id, query, limit=5)`
119
+ - `remora.context(user_id, query, limit=5)`
120
+ - `remora.ingest(user_id, messages, sync=False)`
121
+ - `remora.get(memory_id)`
122
+ - `remora.update(memory_id, content="...")`
123
+ - `remora.delete(memory_id)`
124
+ - `remora.delete_user_memories(user_id)`: GDPR / Privacy wipe
125
+ - `remora.export_user_memories(user_id)`: Data portability export
@@ -0,0 +1,110 @@
1
+ # Remora Python SDK (`remora-ai`)
2
+
3
+ > **Developer-first AI Memory Infrastructure Client** for persistent, evolving agent memory.
4
+
5
+ ---
6
+
7
+ ## 📦 Installation
8
+
9
+ ```bash
10
+ pip install remora-ai
11
+ ```
12
+
13
+ *(Note: The client uses Python standard library `urllib` internally, requiring zero extra dependencies!)*
14
+
15
+ ---
16
+
17
+ ## 🚀 Quickstart
18
+
19
+ ```python
20
+ from remora import RemoraClient
21
+
22
+ remora = RemoraClient(
23
+ api_key="rl_live_your_key_here",
24
+ base_url="http://localhost:3000" # Defaults to http://localhost:3000
25
+ )
26
+
27
+ # 1. Ingest a conversation (async extraction with zero chatbot latency)
28
+ remora.ingest(
29
+ user_id="usr_alice",
30
+ messages=[
31
+ {"role": "user", "content": "I am building a SaaS using Python and PostgreSQL."}
32
+ ]
33
+ )
34
+
35
+ # 2. Retrieve compact prompt-ready context before replying
36
+ ctx = remora.context(
37
+ user_id="usr_alice",
38
+ query="What tech stack should I recommend?"
39
+ )
40
+
41
+ print(ctx.context_text)
42
+ # Output:
43
+ # Relevant user memories:
44
+ # - User is building a SaaS using Python and PostgreSQL.
45
+ # Use these memories only when relevant.
46
+
47
+ # 3. Direct memory search with hybrid ranking scores
48
+ memories = remora.search(
49
+ user_id="usr_alice",
50
+ query="programming language and database"
51
+ )
52
+
53
+ for m in memories:
54
+ print(f"- {m.content} (Score: {m.final_score})")
55
+ print(f" Similarity: {m.score_breakdown.semantic_similarity}")
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 🤖 Integration with OpenAI / LangChain
61
+
62
+ ```python
63
+ from openai import OpenAI
64
+ from remora import RemoraClient
65
+
66
+ remora = RemoraClient(api_key="rl_live_...")
67
+ openai = OpenAI()
68
+
69
+ user_id = "usr_alice"
70
+ user_message = "What database should I choose?"
71
+
72
+ # 1. Fetch relevant persistent memories
73
+ ctx = remora.context(user_id=user_id, query=user_message)
74
+
75
+ # 2. Inject context into system prompt
76
+ system_prompt = f"You are a helpful coding copilot.\n\n{ctx.context_text}"
77
+
78
+ response = openai.chat.completions.create(
79
+ model="gpt-4o",
80
+ messages=[
81
+ {"role": "system", "content": system_prompt},
82
+ {"role": "user", "content": user_message}
83
+ ]
84
+ )
85
+
86
+ print(response.choices[0].message.content)
87
+
88
+ # 3. Asynchronously ingest the turn for future memory formation
89
+ remora.ingest(
90
+ user_id=user_id,
91
+ messages=[
92
+ {"role": "user", "content": user_message},
93
+ {"role": "assistant", "content": response.choices[0].message.content}
94
+ ]
95
+ )
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 📚 API Reference
101
+
102
+ - `remora.add(user_id, content, namespace="default", type="FACT", importance=0.5)`
103
+ - `remora.search(user_id, query, limit=5)`
104
+ - `remora.context(user_id, query, limit=5)`
105
+ - `remora.ingest(user_id, messages, sync=False)`
106
+ - `remora.get(memory_id)`
107
+ - `remora.update(memory_id, content="...")`
108
+ - `remora.delete(memory_id)`
109
+ - `remora.delete_user_memories(user_id)`: GDPR / Privacy wipe
110
+ - `remora.export_user_memories(user_id)`: Data portability export
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "remora-ai"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for Remora AI Memory Infrastructure"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Remora Team", email = "hello@remora.ai" }
14
+ ]
15
+ keywords = [
16
+ "ai",
17
+ "memory",
18
+ "agents",
19
+ "llm",
20
+ "mem0",
21
+ "langchain",
22
+ "crewai",
23
+ "embeddings",
24
+ "vector-search",
25
+ "pgvector"
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.25.0",
29
+ "pydantic>=2.0.0"
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0.0",
35
+ "pytest-asyncio>=0.21.0"
36
+ ]
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["remora"]
@@ -0,0 +1,38 @@
1
+ """
2
+ Remora Python SDK
3
+ ~~~~~~~~~~~~~~~~~
4
+ Official Python client library for Remora AI Memory Infrastructure.
5
+ """
6
+
7
+ from .client import RemoraClient
8
+ from .models import (
9
+ Memory,
10
+ RankedMemory,
11
+ ContextResult,
12
+ IngestResult,
13
+ ScoreBreakdown,
14
+ MemoryType,
15
+ MemoryStatus
16
+ )
17
+ from .exceptions import (
18
+ RemoraError,
19
+ AuthenticationError,
20
+ NotFoundError,
21
+ RateLimitError
22
+ )
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = [
26
+ "RemoraClient",
27
+ "Memory",
28
+ "RankedMemory",
29
+ "ContextResult",
30
+ "IngestResult",
31
+ "ScoreBreakdown",
32
+ "MemoryType",
33
+ "MemoryStatus",
34
+ "RemoraError",
35
+ "AuthenticationError",
36
+ "NotFoundError",
37
+ "RateLimitError"
38
+ ]
@@ -0,0 +1,214 @@
1
+ import json
2
+ import urllib.request
3
+ import urllib.error
4
+ from typing import Optional, List, Dict, Any, Union
5
+
6
+ from .models import Memory, RankedMemory, ContextResult, IngestResult, MemoryType, MemoryStatus
7
+ from .exceptions import RemoraError, AuthenticationError, NotFoundError, RateLimitError
8
+
9
+ class RemoraClient:
10
+ """
11
+ Synchronous Python Client for Remora AI Memory Infrastructure.
12
+ Zero external dependencies required (uses standard library urllib).
13
+ """
14
+
15
+ def __init__(self, api_key: str, base_url: str = "http://localhost:3000", timeout: float = 30.0):
16
+ if not api_key:
17
+ raise AuthenticationError("RemoraClient: api_key is required.")
18
+ self.api_key = api_key.strip()
19
+ self.base_url = base_url.rstrip("/")
20
+ self.timeout = timeout
21
+
22
+ def _request(self, method: str, path: str, payload: Optional[Dict[str, Any]] = None) -> Any:
23
+ url = f"{self.base_url}{path}"
24
+ headers = {
25
+ "Content-Type": "application/json",
26
+ "Authorization": f"Bearer {self.api_key}",
27
+ "User-Agent": "remora-python-sdk/0.1.0"
28
+ }
29
+
30
+ data_bytes = None
31
+ if payload is not None:
32
+ data_bytes = json.dumps(payload).encode("utf-8")
33
+
34
+ req = urllib.request.Request(url, data=data_bytes, headers=headers, method=method)
35
+
36
+ try:
37
+ with urllib.request.urlopen(req, timeout=self.timeout) as response:
38
+ status_code = response.getcode()
39
+ body = response.read().decode("utf-8")
40
+ parsed = json.loads(body) if body else {}
41
+ return parsed.get("data", {})
42
+ except urllib.error.HTTPError as e:
43
+ raw_body = e.read().decode("utf-8") if e.fp else ""
44
+ error_json = {}
45
+ try:
46
+ error_json = json.loads(raw_body)
47
+ except Exception:
48
+ pass
49
+
50
+ message = error_json.get("message", f"HTTP Error {e.code}: {e.reason}")
51
+
52
+ if e.code == 401:
53
+ raise AuthenticationError(message, raw_response=error_json)
54
+ elif e.code == 404:
55
+ raise NotFoundError(message, raw_response=error_json)
56
+ elif e.code == 429:
57
+ retry_after = e.headers.get("Retry-After")
58
+ retry_sec = int(retry_after) if retry_after and retry_after.isdigit() else None
59
+ raise RateLimitError(message, retry_after=retry_sec, raw_response=error_json)
60
+ else:
61
+ raise RemoraError(message, status_code=e.code, raw_response=error_json)
62
+ except urllib.error.URLError as e:
63
+ raise RemoraError(f"Connection error to Remora at {url}: {e.reason}")
64
+
65
+ def add(
66
+ self,
67
+ user_id: str,
68
+ content: str,
69
+ namespace: str = "default",
70
+ type: MemoryType = "FACT",
71
+ importance: float = 0.5,
72
+ confidence: float = 1.0,
73
+ metadata: Optional[Dict[str, Any]] = None
74
+ ) -> Memory:
75
+ """Store a durable memory for an end-user."""
76
+ payload = {
77
+ "user_id": user_id,
78
+ "content": content,
79
+ "namespace": namespace,
80
+ "type": type,
81
+ "importance": importance,
82
+ "confidence": confidence,
83
+ "metadata": metadata or {}
84
+ }
85
+ res = self._request("POST", "/v1/memories", payload)
86
+ return Memory.from_dict(res)
87
+
88
+ def search(
89
+ self,
90
+ user_id: str,
91
+ query: str,
92
+ namespace: Optional[str] = None,
93
+ limit: int = 5
94
+ ) -> List[RankedMemory]:
95
+ """Search memories with semantic vector similarity and hybrid ranking."""
96
+ payload: Dict[str, Any] = {
97
+ "user_id": user_id,
98
+ "query": query,
99
+ "limit": limit
100
+ }
101
+ if namespace:
102
+ payload["namespace"] = namespace
103
+
104
+ res = self._request("POST", "/v1/memories/search", payload)
105
+ if isinstance(res, list):
106
+ return [RankedMemory.from_dict(m) for m in res]
107
+ return []
108
+
109
+ def context(
110
+ self,
111
+ user_id: str,
112
+ query: str,
113
+ namespace: Optional[str] = None,
114
+ limit: int = 5
115
+ ) -> ContextResult:
116
+ """Retrieve prompt-ready formatted context text to inject directly into an LLM."""
117
+ payload: Dict[str, Any] = {
118
+ "user_id": user_id,
119
+ "query": query,
120
+ "limit": limit
121
+ }
122
+ if namespace:
123
+ payload["namespace"] = namespace
124
+
125
+ res = self._request("POST", "/v1/memories/context", payload)
126
+ return ContextResult.from_dict(res)
127
+
128
+ def ingest(
129
+ self,
130
+ user_id: str,
131
+ messages: List[Dict[str, str]],
132
+ namespace: str = "default",
133
+ sync: bool = False
134
+ ) -> IngestResult:
135
+ """
136
+ Extract memories automatically from raw conversation turns.
137
+ Defaults to sync=False (returns 202 Accepted + jobId with zero chatbot latency).
138
+ """
139
+ payload = {
140
+ "user_id": user_id,
141
+ "messages": messages,
142
+ "namespace": namespace,
143
+ "sync": sync
144
+ }
145
+ res = self._request("POST", "/v1/memories/ingest", payload)
146
+ return IngestResult.from_dict(res)
147
+
148
+ def get(self, memory_id: str) -> Memory:
149
+ """Get single memory by ID along with its evolution versions."""
150
+ res = self._request("GET", f"/v1/memories/{memory_id}")
151
+ return Memory.from_dict(res)
152
+
153
+ def update(
154
+ self,
155
+ memory_id: str,
156
+ content: Optional[str] = None,
157
+ importance: Optional[float] = None,
158
+ confidence: Optional[float] = None,
159
+ type: Optional[MemoryType] = None,
160
+ status: Optional[MemoryStatus] = None
161
+ ) -> Memory:
162
+ """Update a memory (automatically logs a MemoryVersion audit entry)."""
163
+ payload: Dict[str, Any] = {}
164
+ if content is not None:
165
+ payload["content"] = content
166
+ if importance is not None:
167
+ payload["importance"] = importance
168
+ if confidence is not None:
169
+ payload["confidence"] = confidence
170
+ if type is not None:
171
+ payload["type"] = type
172
+ if status is not None:
173
+ payload["status"] = status
174
+
175
+ res = self._request("PATCH", f"/v1/memories/{memory_id}", payload)
176
+ return Memory.from_dict(res)
177
+
178
+ def delete(self, memory_id: str) -> Memory:
179
+ """Soft-delete a memory."""
180
+ res = self._request("DELETE", f"/v1/memories/{memory_id}")
181
+ return Memory.from_dict(res)
182
+
183
+ def delete_user_memories(self, user_id: str) -> Dict[str, Any]:
184
+ """GDPR / Privacy: Delete all memories for an external user."""
185
+ return self._request("DELETE", f"/v1/memories/users/{user_id}")
186
+
187
+ def export_user_memories(self, user_id: str) -> Dict[str, Any]:
188
+ """Data Portability: Export all memories and version history for a user."""
189
+ return self._request("GET", f"/v1/memories/users/{user_id}/export")
190
+
191
+ def list(
192
+ self,
193
+ user_id: Optional[str] = None,
194
+ namespace: Optional[str] = None,
195
+ limit: int = 20,
196
+ offset: int = 0
197
+ ) -> Dict[str, Any]:
198
+ """List memories with optional pagination and filters."""
199
+ params = []
200
+ if user_id:
201
+ params.append(f"user_id={user_id}")
202
+ if namespace:
203
+ params.append(f"namespace={namespace}")
204
+ params.append(f"limit={limit}")
205
+ params.append(f"offset={offset}")
206
+
207
+ query = f"?{'&'.join(params)}" if params else ""
208
+ res = self._request("GET", f"/v1/memories{query}")
209
+ result = res if isinstance(res, dict) else {}
210
+ memories = [Memory.from_dict(m) for m in result.get("memories", [])]
211
+ return {
212
+ "total": result.get("total", 0),
213
+ "memories": memories
214
+ }
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+ class RemoraError(Exception):
4
+ """Base exception for all Remora errors."""
5
+ def __init__(self, message: str, status_code: int = 500, raw_response: Optional[dict] = None):
6
+ super().__init__(message)
7
+ self.status_code = status_code
8
+ self.raw_response = raw_response or {}
9
+
10
+ class AuthenticationError(RemoraError):
11
+ """Raised when the API key is missing, invalid, expired, or revoked."""
12
+ def __init__(self, message: str = "Unauthorized: Invalid or missing API key.", raw_response: Optional[dict] = None):
13
+ super().__init__(message, status_code=401, raw_response=raw_response)
14
+
15
+ class NotFoundError(RemoraError):
16
+ """Raised when a requested resource (e.g. memory ID) does not exist."""
17
+ def __init__(self, message: str = "Resource not found.", raw_response: Optional[dict] = None):
18
+ super().__init__(message, status_code=404, raw_response=raw_response)
19
+
20
+ class RateLimitError(RemoraError):
21
+ """Raised when the API key exceeds its sliding window rate limit."""
22
+ def __init__(self, message: str = "Rate limit exceeded.", retry_after: Optional[int] = None, raw_response: Optional[dict] = None):
23
+ super().__init__(message, status_code=429, raw_response=raw_response)
24
+ self.retry_after = retry_after
@@ -0,0 +1,142 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional, List, Dict, Any, Literal
3
+
4
+ MemoryType = Literal[
5
+ "FACT",
6
+ "PREFERENCE",
7
+ "GOAL",
8
+ "INSTRUCTION",
9
+ "DECISION",
10
+ "PROFILE",
11
+ "RELATIONSHIP",
12
+ "PROJECT_CONTEXT",
13
+ "BEHAVIOR",
14
+ "TEMPORARY",
15
+ "OTHER"
16
+ ]
17
+
18
+ MemoryStatus = Literal[
19
+ "ACTIVE",
20
+ "SUPERSEDED",
21
+ "ARCHIVED",
22
+ "DELETED",
23
+ "EXPIRED"
24
+ ]
25
+
26
+ @dataclass
27
+ class ScoreBreakdown:
28
+ semantic_similarity: float
29
+ importance: float
30
+ confidence: float
31
+ recency: float
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: Dict[str, Any]) -> "ScoreBreakdown":
35
+ return cls(
36
+ semantic_similarity=data.get("semanticSimilarity", 0.0),
37
+ importance=data.get("importance", 0.0),
38
+ confidence=data.get("confidence", 0.0),
39
+ recency=data.get("recency", 0.0),
40
+ )
41
+
42
+ @dataclass
43
+ class Memory:
44
+ id: str
45
+ organization_id: str
46
+ project_id: str
47
+ external_user_id: str
48
+ namespace: str
49
+ content: str
50
+ type: str
51
+ status: str
52
+ importance: float
53
+ confidence: float
54
+ created_at: str
55
+ updated_at: str
56
+ last_accessed_at: Optional[str] = None
57
+ metadata: Optional[Dict[str, Any]] = None
58
+ versions: Optional[List[Dict[str, Any]]] = None
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: Dict[str, Any]) -> "Memory":
62
+ return cls(
63
+ id=data.get("id", ""),
64
+ organization_id=data.get("organizationId", ""),
65
+ project_id=data.get("projectId", ""),
66
+ external_user_id=data.get("externalUserId", ""),
67
+ namespace=data.get("namespace", "default"),
68
+ content=data.get("content", ""),
69
+ type=data.get("type", "FACT"),
70
+ status=data.get("status", "ACTIVE"),
71
+ importance=float(data.get("importance", 0.5)),
72
+ confidence=float(data.get("confidence", 1.0)),
73
+ last_accessed_at=data.get("lastAccessedAt"),
74
+ created_at=data.get("createdAt", ""),
75
+ updated_at=data.get("updatedAt", ""),
76
+ metadata=data.get("metadata"),
77
+ versions=data.get("versions"),
78
+ )
79
+
80
+ @dataclass
81
+ class RankedMemory(Memory):
82
+ final_score: float = 0.0
83
+ score_breakdown: Optional[ScoreBreakdown] = None
84
+
85
+ @classmethod
86
+ def from_dict(cls, data: Dict[str, Any]) -> "RankedMemory":
87
+ base = super().from_dict(data)
88
+ breakdown = None
89
+ if "scoreBreakdown" in data and isinstance(data["scoreBreakdown"], dict):
90
+ breakdown = ScoreBreakdown.from_dict(data["scoreBreakdown"])
91
+
92
+ return cls(
93
+ id=base.id,
94
+ organization_id=base.organization_id,
95
+ project_id=base.project_id,
96
+ external_user_id=base.external_user_id,
97
+ namespace=base.namespace,
98
+ content=base.content,
99
+ type=base.type,
100
+ status=base.status,
101
+ importance=base.importance,
102
+ confidence=base.confidence,
103
+ last_accessed_at=base.last_accessed_at,
104
+ created_at=base.created_at,
105
+ updated_at=base.updated_at,
106
+ metadata=base.metadata,
107
+ versions=base.versions,
108
+ final_score=float(data.get("finalScore", 0.0)),
109
+ score_breakdown=breakdown
110
+ )
111
+
112
+ @dataclass
113
+ class ContextResult:
114
+ context_text: str
115
+ memories: List[RankedMemory] = field(default_factory=list)
116
+
117
+ @classmethod
118
+ def from_dict(cls, data: Dict[str, Any]) -> "ContextResult":
119
+ memories_raw = data.get("memories", [])
120
+ memories = [RankedMemory.from_dict(m) for m in memories_raw]
121
+ return cls(
122
+ context_text=data.get("contextText", ""),
123
+ memories=memories
124
+ )
125
+
126
+ @dataclass
127
+ class IngestResult:
128
+ ingested: Optional[int] = None
129
+ job_id: Optional[str] = None
130
+ status: Optional[str] = None
131
+ memories: List[Memory] = field(default_factory=list)
132
+
133
+ @classmethod
134
+ def from_dict(cls, data: Dict[str, Any]) -> "IngestResult":
135
+ memories_raw = data.get("memories", [])
136
+ memories = [Memory.from_dict(m) for m in memories_raw]
137
+ return cls(
138
+ ingested=data.get("ingested"),
139
+ job_id=data.get("jobId"),
140
+ status=data.get("status"),
141
+ memories=memories
142
+ )
@@ -0,0 +1,78 @@
1
+ import unittest
2
+ from remora.models import Memory, RankedMemory, ContextResult, ScoreBreakdown
3
+ from remora.client import RemoraClient
4
+ from remora.exceptions import AuthenticationError
5
+
6
+ class TestRemoraSDK(unittest.TestCase):
7
+ def test_client_init_requires_key(self):
8
+ with self.assertRaises(AuthenticationError):
9
+ RemoraClient(api_key="")
10
+
11
+ def test_memory_model_deserialization(self):
12
+ raw = {
13
+ "id": "mem_123",
14
+ "organizationId": "org_abc",
15
+ "projectId": "proj_xyz",
16
+ "externalUserId": "usr_alice",
17
+ "namespace": "personal",
18
+ "content": "User prefers dark mode.",
19
+ "type": "PREFERENCE",
20
+ "status": "ACTIVE",
21
+ "importance": 0.8,
22
+ "confidence": 0.95,
23
+ "createdAt": "2026-10-20T12:00:00Z",
24
+ "updatedAt": "2026-10-20T12:00:00Z"
25
+ }
26
+
27
+ memory = Memory.from_dict(raw)
28
+ self.assertEqual(memory.id, "mem_123")
29
+ self.assertEqual(memory.content, "User prefers dark mode.")
30
+ self.assertEqual(memory.importance, 0.8)
31
+ self.assertEqual(memory.confidence, 0.95)
32
+
33
+ def test_ranked_memory_deserialization(self):
34
+ raw = {
35
+ "id": "mem_123",
36
+ "organizationId": "org_abc",
37
+ "projectId": "proj_xyz",
38
+ "externalUserId": "usr_alice",
39
+ "namespace": "default",
40
+ "content": "User prefers Python.",
41
+ "type": "PREFERENCE",
42
+ "status": "ACTIVE",
43
+ "importance": 0.8,
44
+ "confidence": 0.9,
45
+ "finalScore": 0.895,
46
+ "scoreBreakdown": {
47
+ "semanticSimilarity": 0.92,
48
+ "importance": 0.8,
49
+ "confidence": 0.9,
50
+ "recency": 0.95
51
+ },
52
+ "createdAt": "2026-10-20T12:00:00Z",
53
+ "updatedAt": "2026-10-20T12:00:00Z"
54
+ }
55
+
56
+ ranked = RankedMemory.from_dict(raw)
57
+ self.assertEqual(ranked.final_score, 0.895)
58
+ self.assertIsNotNone(ranked.score_breakdown)
59
+ self.assertEqual(ranked.score_breakdown.semantic_similarity, 0.92)
60
+
61
+ def test_context_result_deserialization(self):
62
+ raw = {
63
+ "contextText": "Relevant user memories:\n- User likes Python.",
64
+ "memories": [
65
+ {
66
+ "id": "mem_1",
67
+ "content": "User likes Python.",
68
+ "finalScore": 0.9
69
+ }
70
+ ]
71
+ }
72
+
73
+ ctx = ContextResult.from_dict(raw)
74
+ self.assertIn("Relevant user memories:", ctx.context_text)
75
+ self.assertEqual(len(ctx.memories), 1)
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()