cuemap 0.4.1__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.
Potentially problematic release.
This version of cuemap might be problematic. Click here for more details.
- cuemap/__init__.py +33 -0
- cuemap/client.py +336 -0
- cuemap/exceptions.py +16 -0
- cuemap/models.py +25 -0
- cuemap-0.4.1.dist-info/METADATA +337 -0
- cuemap-0.4.1.dist-info/RECORD +9 -0
- cuemap-0.4.1.dist-info/WHEEL +5 -0
- cuemap-0.4.1.dist-info/licenses/LICENSE +21 -0
- cuemap-0.4.1.dist-info/top_level.txt +1 -0
cuemap/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CueMap: Redis for AI Agents
|
|
3
|
+
|
|
4
|
+
A high-performance, temporal-associative memory store.
|
|
5
|
+
You give us the cues, we give you the right memory at the right time.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from cuemap import CueMap
|
|
9
|
+
>>>
|
|
10
|
+
>>> client = CueMap()
|
|
11
|
+
>>>
|
|
12
|
+
>>> # Add a memory with cues
|
|
13
|
+
>>> client.add("The server password is abc123", cues=["server", "password"])
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Recall by cues
|
|
16
|
+
>>> results = client.recall(["server", "password"])
|
|
17
|
+
>>> print(results[0].content)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import CueMap, AsyncCueMap
|
|
21
|
+
from .models import Memory, RecallResult
|
|
22
|
+
from .exceptions import CueMapError, ConnectionError, AuthenticationError
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CueMap",
|
|
27
|
+
"AsyncCueMap",
|
|
28
|
+
"Memory",
|
|
29
|
+
"RecallResult",
|
|
30
|
+
"CueMapError",
|
|
31
|
+
"ConnectionError",
|
|
32
|
+
"AuthenticationError",
|
|
33
|
+
]
|
cuemap/client.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Pure CueMap client - no magic, just speed."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
from .models import Memory, RecallResult
|
|
7
|
+
from .exceptions import CueMapError, ConnectionError, AuthenticationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CueMap:
|
|
11
|
+
"""
|
|
12
|
+
Pure CueMap client.
|
|
13
|
+
|
|
14
|
+
No auto-cue extraction. No semantic matching. Just fast memory storage.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> client = CueMap()
|
|
18
|
+
>>> client.add("Important note", cues=["work", "urgent"])
|
|
19
|
+
>>> results = client.recall(["work"])
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
url: str = "http://localhost:8080",
|
|
25
|
+
api_key: Optional[str] = None,
|
|
26
|
+
project_id: Optional[str] = None,
|
|
27
|
+
timeout: float = 30.0
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize CueMap client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
url: CueMap server URL
|
|
34
|
+
api_key: Optional API key for authentication
|
|
35
|
+
project_id: Optional project ID for multi-tenancy
|
|
36
|
+
timeout: Request timeout in seconds
|
|
37
|
+
"""
|
|
38
|
+
self.url = url
|
|
39
|
+
self.api_key = api_key
|
|
40
|
+
self.project_id = project_id
|
|
41
|
+
|
|
42
|
+
self.client = httpx.Client(
|
|
43
|
+
base_url=url,
|
|
44
|
+
timeout=timeout
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def _headers(self) -> Dict[str, str]:
|
|
48
|
+
"""Get request headers."""
|
|
49
|
+
headers = {}
|
|
50
|
+
if self.api_key:
|
|
51
|
+
headers["X-API-Key"] = self.api_key
|
|
52
|
+
if self.project_id:
|
|
53
|
+
headers["X-Project-ID"] = self.project_id
|
|
54
|
+
return headers
|
|
55
|
+
|
|
56
|
+
def add(
|
|
57
|
+
self,
|
|
58
|
+
content: str,
|
|
59
|
+
cues: List[str],
|
|
60
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
61
|
+
) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Add a memory.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
content: Memory content
|
|
67
|
+
cues: List of cues (tags) for retrieval
|
|
68
|
+
metadata: Optional metadata
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Memory ID
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> client.add(
|
|
75
|
+
... "Meeting with John at 3pm",
|
|
76
|
+
... cues=["meeting", "john", "calendar"]
|
|
77
|
+
... )
|
|
78
|
+
"""
|
|
79
|
+
response = self.client.post(
|
|
80
|
+
"/memories",
|
|
81
|
+
json={
|
|
82
|
+
"content": content,
|
|
83
|
+
"cues": cues,
|
|
84
|
+
"metadata": metadata or {}
|
|
85
|
+
},
|
|
86
|
+
headers=self._headers()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if response.status_code == 401:
|
|
90
|
+
raise AuthenticationError("Invalid API key")
|
|
91
|
+
elif response.status_code != 200:
|
|
92
|
+
raise CueMapError(f"Failed to add memory: {response.status_code}")
|
|
93
|
+
|
|
94
|
+
return response.json()["id"]
|
|
95
|
+
|
|
96
|
+
def recall(
|
|
97
|
+
self,
|
|
98
|
+
cues: List[str],
|
|
99
|
+
limit: int = 10,
|
|
100
|
+
auto_reinforce: bool = False,
|
|
101
|
+
min_intersection: Optional[int] = None,
|
|
102
|
+
projects: Optional[List[str]] = None
|
|
103
|
+
) -> List[RecallResult]:
|
|
104
|
+
"""
|
|
105
|
+
Recall memories by cues.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cues: List of cues to search for
|
|
109
|
+
limit: Maximum results to return
|
|
110
|
+
auto_reinforce: Automatically reinforce retrieved memories
|
|
111
|
+
min_intersection: Minimum number of cues that must match (for strict AND logic)
|
|
112
|
+
projects: List of project IDs for cross-domain queries (multi-tenant only)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of recall results
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> # OR logic (default): matches any cue
|
|
119
|
+
>>> results = client.recall(["meeting", "john"])
|
|
120
|
+
|
|
121
|
+
>>> # AND logic: requires both cues
|
|
122
|
+
>>> results = client.recall(["meeting", "john"], min_intersection=2)
|
|
123
|
+
|
|
124
|
+
>>> # Cross-domain query (multi-tenant)
|
|
125
|
+
>>> results = client.recall(["urgent"], projects=["sales", "support"])
|
|
126
|
+
"""
|
|
127
|
+
payload = {
|
|
128
|
+
"cues": cues,
|
|
129
|
+
"limit": limit,
|
|
130
|
+
"auto_reinforce": auto_reinforce
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if min_intersection is not None:
|
|
134
|
+
payload["min_intersection"] = min_intersection
|
|
135
|
+
|
|
136
|
+
if projects is not None:
|
|
137
|
+
payload["projects"] = projects
|
|
138
|
+
|
|
139
|
+
response = self.client.post(
|
|
140
|
+
"/recall",
|
|
141
|
+
json=payload,
|
|
142
|
+
headers=self._headers()
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if response.status_code != 200:
|
|
146
|
+
raise CueMapError(f"Failed to recall: {response.status_code}")
|
|
147
|
+
|
|
148
|
+
results = response.json()["results"]
|
|
149
|
+
return [RecallResult(**r) for r in results]
|
|
150
|
+
|
|
151
|
+
def reinforce(self, memory_id: str, cues: List[str]) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Reinforce a memory on specific cue pathways.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
memory_id: Memory ID
|
|
157
|
+
cues: Cues to reinforce on
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Success status
|
|
161
|
+
"""
|
|
162
|
+
response = self.client.patch(
|
|
163
|
+
f"/memories/{memory_id}/reinforce",
|
|
164
|
+
json={"cues": cues},
|
|
165
|
+
headers=self._headers()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return response.status_code == 200
|
|
169
|
+
|
|
170
|
+
def get(self, memory_id: str) -> Memory:
|
|
171
|
+
"""Get a memory by ID."""
|
|
172
|
+
response = self.client.get(
|
|
173
|
+
f"/memories/{memory_id}",
|
|
174
|
+
headers=self._headers()
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if response.status_code == 404:
|
|
178
|
+
raise CueMapError(f"Memory not found: {memory_id}")
|
|
179
|
+
elif response.status_code != 200:
|
|
180
|
+
raise CueMapError(f"Failed to get memory: {response.status_code}")
|
|
181
|
+
|
|
182
|
+
return Memory(**response.json())
|
|
183
|
+
|
|
184
|
+
def stats(self) -> Dict[str, Any]:
|
|
185
|
+
"""Get server statistics."""
|
|
186
|
+
response = self.client.get(
|
|
187
|
+
"/stats",
|
|
188
|
+
headers=self._headers()
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return response.json()
|
|
192
|
+
|
|
193
|
+
def close(self):
|
|
194
|
+
"""Close the client."""
|
|
195
|
+
self.client.close()
|
|
196
|
+
|
|
197
|
+
def __enter__(self):
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
201
|
+
self.close()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AsyncCueMap:
|
|
205
|
+
"""
|
|
206
|
+
Async CueMap client.
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> async with AsyncCueMap() as client:
|
|
210
|
+
... await client.add("Note", cues=["work"])
|
|
211
|
+
... results = await client.recall(["work"])
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
url: str = "http://localhost:8080",
|
|
217
|
+
api_key: Optional[str] = None,
|
|
218
|
+
project_id: Optional[str] = None,
|
|
219
|
+
timeout: float = 30.0
|
|
220
|
+
):
|
|
221
|
+
self.url = url
|
|
222
|
+
self.api_key = api_key
|
|
223
|
+
self.project_id = project_id
|
|
224
|
+
|
|
225
|
+
self.client = httpx.AsyncClient(
|
|
226
|
+
base_url=url,
|
|
227
|
+
timeout=timeout
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _headers(self) -> Dict[str, str]:
|
|
231
|
+
headers = {}
|
|
232
|
+
if self.api_key:
|
|
233
|
+
headers["X-API-Key"] = self.api_key
|
|
234
|
+
if self.project_id:
|
|
235
|
+
headers["X-Project-ID"] = self.project_id
|
|
236
|
+
return headers
|
|
237
|
+
|
|
238
|
+
async def add(
|
|
239
|
+
self,
|
|
240
|
+
content: str,
|
|
241
|
+
cues: List[str],
|
|
242
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
243
|
+
) -> str:
|
|
244
|
+
"""Add a memory (async)."""
|
|
245
|
+
response = await self.client.post(
|
|
246
|
+
"/memories",
|
|
247
|
+
json={
|
|
248
|
+
"content": content,
|
|
249
|
+
"cues": cues,
|
|
250
|
+
"metadata": metadata or {}
|
|
251
|
+
},
|
|
252
|
+
headers=self._headers()
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if response.status_code == 401:
|
|
256
|
+
raise AuthenticationError("Invalid API key")
|
|
257
|
+
elif response.status_code != 200:
|
|
258
|
+
raise CueMapError(f"Failed to add memory: {response.status_code}")
|
|
259
|
+
|
|
260
|
+
return response.json()["id"]
|
|
261
|
+
|
|
262
|
+
async def recall(
|
|
263
|
+
self,
|
|
264
|
+
cues: List[str],
|
|
265
|
+
limit: int = 10,
|
|
266
|
+
auto_reinforce: bool = False,
|
|
267
|
+
min_intersection: Optional[int] = None,
|
|
268
|
+
projects: Optional[List[str]] = None
|
|
269
|
+
) -> List[RecallResult]:
|
|
270
|
+
"""Recall memories (async)."""
|
|
271
|
+
payload = {
|
|
272
|
+
"cues": cues,
|
|
273
|
+
"limit": limit,
|
|
274
|
+
"auto_reinforce": auto_reinforce
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if min_intersection is not None:
|
|
278
|
+
payload["min_intersection"] = min_intersection
|
|
279
|
+
|
|
280
|
+
if projects is not None:
|
|
281
|
+
payload["projects"] = projects
|
|
282
|
+
|
|
283
|
+
response = await self.client.post(
|
|
284
|
+
"/recall",
|
|
285
|
+
json=payload,
|
|
286
|
+
headers=self._headers()
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if response.status_code != 200:
|
|
290
|
+
raise CueMapError(f"Failed to recall: {response.status_code}")
|
|
291
|
+
|
|
292
|
+
results = response.json()["results"]
|
|
293
|
+
return [RecallResult(**r) for r in results]
|
|
294
|
+
|
|
295
|
+
async def reinforce(self, memory_id: str, cues: List[str]) -> bool:
|
|
296
|
+
"""Reinforce a memory (async)."""
|
|
297
|
+
response = await self.client.patch(
|
|
298
|
+
f"/memories/{memory_id}/reinforce",
|
|
299
|
+
json={"cues": cues},
|
|
300
|
+
headers=self._headers()
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return response.status_code == 200
|
|
304
|
+
|
|
305
|
+
async def get(self, memory_id: str) -> Memory:
|
|
306
|
+
"""Get a memory by ID (async)."""
|
|
307
|
+
response = await self.client.get(
|
|
308
|
+
f"/memories/{memory_id}",
|
|
309
|
+
headers=self._headers()
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if response.status_code == 404:
|
|
313
|
+
raise CueMapError(f"Memory not found: {memory_id}")
|
|
314
|
+
elif response.status_code != 200:
|
|
315
|
+
raise CueMapError(f"Failed to get memory: {response.status_code}")
|
|
316
|
+
|
|
317
|
+
return Memory(**response.json())
|
|
318
|
+
|
|
319
|
+
async def stats(self) -> Dict[str, Any]:
|
|
320
|
+
"""Get server statistics (async)."""
|
|
321
|
+
response = await self.client.get(
|
|
322
|
+
"/stats",
|
|
323
|
+
headers=self._headers()
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return response.json()
|
|
327
|
+
|
|
328
|
+
async def close(self):
|
|
329
|
+
"""Close the client."""
|
|
330
|
+
await self.client.aclose()
|
|
331
|
+
|
|
332
|
+
async def __aenter__(self):
|
|
333
|
+
return self
|
|
334
|
+
|
|
335
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
336
|
+
await self.close()
|
cuemap/exceptions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CueMapError(Exception):
|
|
5
|
+
"""Base exception."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConnectionError(CueMapError):
|
|
10
|
+
"""Connection failed."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(CueMapError):
|
|
15
|
+
"""Authentication failed."""
|
|
16
|
+
pass
|
cuemap/models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Data models."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Memory(BaseModel):
|
|
8
|
+
"""A memory object."""
|
|
9
|
+
|
|
10
|
+
id: str
|
|
11
|
+
content: str
|
|
12
|
+
cues: List[str] = Field(default_factory=list)
|
|
13
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RecallResult(BaseModel):
|
|
17
|
+
"""Result from a recall operation."""
|
|
18
|
+
|
|
19
|
+
memory_id: str
|
|
20
|
+
content: str
|
|
21
|
+
score: float
|
|
22
|
+
intersection_count: int
|
|
23
|
+
recency_score: float
|
|
24
|
+
reinforcement_score: float
|
|
25
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cuemap
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Redis for AI Agents - High-performance temporal-associative memory
|
|
5
|
+
Author-email: CueMap Team <hello@cuemap.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cuemap-dev/sdks
|
|
8
|
+
Project-URL: Documentation, https://github.com/cuemap-dev/sdks/blob/main/python/README.md
|
|
9
|
+
Project-URL: Repository, https://github.com/cuemap-dev/sdks
|
|
10
|
+
Project-URL: Issues, https://github.com/cuemap-dev/sdks/issues
|
|
11
|
+
Project-URL: Engine, https://github.com/cuemap-dev/engine
|
|
12
|
+
Keywords: ai,memory,agents,redis,temporal,associative,llm,rag
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: httpx>=0.25.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# CueMap: Redis for AI Agents
|
|
37
|
+
|
|
38
|
+
**High-performance, temporal-associative memory store.**
|
|
39
|
+
|
|
40
|
+
You give us the cues, we give you the right memory at the right time.
|
|
41
|
+
|
|
42
|
+
## Why CueMap?
|
|
43
|
+
|
|
44
|
+
Redis doesn't auto-serialize your data. It doesn't guess what you mean. It just stores keys and values **blazing fast**.
|
|
45
|
+
|
|
46
|
+
CueMap is the same philosophy for AI agent memory:
|
|
47
|
+
- ✅ **You control the cues** (tags)
|
|
48
|
+
- ✅ **We handle the speed** (sub-millisecond)
|
|
49
|
+
- ✅ **No magic** (predictable behavior)
|
|
50
|
+
- ✅ **No dependencies** (5KB SDK)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install cuemap
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That's it. No ML models. No transformers. Just pure speed.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### 1. Start the Engine
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
docker run -p 8080:8080 cuemap/engine:latest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Use the SDK
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from cuemap import CueMap
|
|
72
|
+
|
|
73
|
+
client = CueMap()
|
|
74
|
+
|
|
75
|
+
# Add a memory with cues
|
|
76
|
+
client.add(
|
|
77
|
+
"The server password is abc123",
|
|
78
|
+
cues=["server", "password", "credentials"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Recall by cues
|
|
82
|
+
results = client.recall(["server", "password"])
|
|
83
|
+
print(results[0].content)
|
|
84
|
+
# Output: "The server password is abc123"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Core API
|
|
88
|
+
|
|
89
|
+
### Add Memory
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
memory_id = client.add(
|
|
93
|
+
content="Meeting with John at 3pm",
|
|
94
|
+
cues=["meeting", "john", "calendar", "today"]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Recall Memories
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# OR logic (default): matches any cue
|
|
102
|
+
results = client.recall(
|
|
103
|
+
cues=["meeting", "john"],
|
|
104
|
+
limit=10
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for result in results:
|
|
108
|
+
print(f"{result.content} (score: {result.score})")
|
|
109
|
+
|
|
110
|
+
# AND logic: requires all cues to match
|
|
111
|
+
results = client.recall(
|
|
112
|
+
cues=["meeting", "john"],
|
|
113
|
+
min_intersection=2 # Both cues must match
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Cross-domain query (multi-tenant mode)
|
|
117
|
+
results = client.recall(
|
|
118
|
+
cues=["urgent"],
|
|
119
|
+
projects=["sales", "support", "engineering"]
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Reinforce Memory
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Make a memory more accessible
|
|
127
|
+
client.reinforce(memory_id, cues=["important", "urgent"])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## How It Works
|
|
131
|
+
|
|
132
|
+
### Temporal-Associative Retrieval
|
|
133
|
+
|
|
134
|
+
CueMap uses **Iterative Deepening Intersection**:
|
|
135
|
+
|
|
136
|
+
1. **Intersection**: Memories matching multiple cues rank higher
|
|
137
|
+
2. **Recency**: Recent memories are more accessible
|
|
138
|
+
3. **Reinforcement**: Frequently accessed memories stay "front of mind"
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Add memories
|
|
142
|
+
client.add("Pizza recipe", cues=["food", "italian"])
|
|
143
|
+
client.add("Pasta recipe", cues=["food", "italian"])
|
|
144
|
+
client.add("Sushi recipe", cues=["food", "japanese"])
|
|
145
|
+
|
|
146
|
+
# Query with multiple cues
|
|
147
|
+
results = client.recall(["food", "italian"])
|
|
148
|
+
# Returns: Pizza and Pasta (both match 2 cues)
|
|
149
|
+
# Sushi is filtered out (only matches 1 cue)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Performance (~1M memories)
|
|
153
|
+
|
|
154
|
+
- **Write P99**: 0.33ms
|
|
155
|
+
- **Read P99**: 0.37ms
|
|
156
|
+
- **Throughput**: 2,900+ ops/sec
|
|
157
|
+
- **Accuracy**: 100% (validated on 120 test scenarios)
|
|
158
|
+
|
|
159
|
+
## Recipes
|
|
160
|
+
|
|
161
|
+
### Recipe 1: Use with OpenAI
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from cuemap import CueMap
|
|
165
|
+
import openai
|
|
166
|
+
|
|
167
|
+
client = CueMap()
|
|
168
|
+
|
|
169
|
+
def store_with_ai_tags(content: str):
|
|
170
|
+
# Let OpenAI extract the cues
|
|
171
|
+
response = openai.chat.completions.create(
|
|
172
|
+
model="gpt-4",
|
|
173
|
+
messages=[{
|
|
174
|
+
"role": "system",
|
|
175
|
+
"content": "Extract 3-5 search tags from the text. Return as JSON array."
|
|
176
|
+
}, {
|
|
177
|
+
"role": "user",
|
|
178
|
+
"content": content
|
|
179
|
+
}]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
cues = response.choices[0].message.content # ["tag1", "tag2", ...]
|
|
183
|
+
|
|
184
|
+
# Store in CueMap
|
|
185
|
+
return client.add(content, cues=cues)
|
|
186
|
+
|
|
187
|
+
# Usage
|
|
188
|
+
store_with_ai_tags("I need to buy groceries this weekend")
|
|
189
|
+
# OpenAI extracts: ["shopping", "groceries", "weekend", "todo"]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Recipe 2: Use with LangChain
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from cuemap import CueMap
|
|
196
|
+
from langchain.memory import BaseMemory
|
|
197
|
+
|
|
198
|
+
class CueMapMemory(BaseMemory):
|
|
199
|
+
def __init__(self, cue_extractor):
|
|
200
|
+
self.client = CueMap()
|
|
201
|
+
self.extract_cues = cue_extractor
|
|
202
|
+
|
|
203
|
+
def save_context(self, inputs, outputs):
|
|
204
|
+
context = f"User: {inputs['input']}\nAI: {outputs['output']}"
|
|
205
|
+
cues = self.extract_cues(context)
|
|
206
|
+
self.client.add(context, cues=cues)
|
|
207
|
+
|
|
208
|
+
def load_memory_variables(self, inputs):
|
|
209
|
+
cues = self.extract_cues(inputs['input'])
|
|
210
|
+
results = self.client.recall(cues, limit=5)
|
|
211
|
+
return {"history": "\n".join([r.content for r in results])}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Recipe 3: Manual Cues (Production)
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
# For production: explicit, predictable cues
|
|
218
|
+
client.add(
|
|
219
|
+
"Deploy command: kubectl apply -f deployment.yaml",
|
|
220
|
+
cues=["deployment", "kubernetes", "commands", "devops"]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
client.add(
|
|
224
|
+
"API endpoint: https://api.example.com/v1/users",
|
|
225
|
+
cues=["api", "endpoint", "users", "documentation"]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Query with specific cues
|
|
229
|
+
client.recall(["deployment", "kubernetes"])
|
|
230
|
+
client.recall(["api", "users"])
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Running the Engine
|
|
234
|
+
|
|
235
|
+
CueMap requires a running engine. Choose your deployment:
|
|
236
|
+
|
|
237
|
+
### Option 1: Docker (Recommended)
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
docker run -p 8080:8080 cuemap/engine:latest
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Option 2: From Source
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git clone https://github.com/cuemap-dev/engine
|
|
247
|
+
cd engine
|
|
248
|
+
cargo build --release
|
|
249
|
+
./target/release/cuemap-rust --port 8080
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Configuration
|
|
253
|
+
|
|
254
|
+
### Connect to Engine
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# Default (localhost)
|
|
258
|
+
client = CueMap()
|
|
259
|
+
|
|
260
|
+
# Custom URL
|
|
261
|
+
client = CueMap(url="http://your-server:8080")
|
|
262
|
+
|
|
263
|
+
# With authentication
|
|
264
|
+
client = CueMap(
|
|
265
|
+
url="http://your-server:8080",
|
|
266
|
+
api_key="your-secret-key"
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Multi-tenancy
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# Use project isolation
|
|
274
|
+
client = CueMap(
|
|
275
|
+
url="http://your-server:8080",
|
|
276
|
+
project_id="my-project"
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Async Support
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from cuemap import AsyncCueMap
|
|
284
|
+
|
|
285
|
+
async with AsyncCueMap() as client:
|
|
286
|
+
await client.add("Note", cues=["work"])
|
|
287
|
+
results = await client.recall(["work"])
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Philosophy
|
|
291
|
+
|
|
292
|
+
### What CueMap Does
|
|
293
|
+
|
|
294
|
+
✅ **Fast storage** - Sub-millisecond retrieval
|
|
295
|
+
✅ **Temporal ordering** - Recent memories prioritized
|
|
296
|
+
✅ **Intersection scoring** - Multi-cue matching
|
|
297
|
+
✅ **Reinforcement** - Move-to-front operation
|
|
298
|
+
|
|
299
|
+
### What CueMap Doesn't Do
|
|
300
|
+
|
|
301
|
+
❌ **Auto-tagging** - You provide the cues
|
|
302
|
+
❌ **Semantic search** - Use your own embeddings
|
|
303
|
+
❌ **LLM integration** - Bring your own model
|
|
304
|
+
❌ **Magic** - Explicit and predictable
|
|
305
|
+
|
|
306
|
+
## Why This Approach?
|
|
307
|
+
|
|
308
|
+
**Redis Philosophy**: Don't guess what the user wants. Provide primitives. Let them build.
|
|
309
|
+
|
|
310
|
+
**CueMap Philosophy**: Don't auto-extract cues. Don't auto-embed. Just store and retrieve **fast**.
|
|
311
|
+
|
|
312
|
+
**Benefits**:
|
|
313
|
+
- 🚀 **5KB SDK** (vs 500MB with ML models)
|
|
314
|
+
- ⚡ **Instant install** (1 second vs 5 minutes)
|
|
315
|
+
- 🎯 **Predictable** (no ML black boxes)
|
|
316
|
+
- 🔧 **Flexible** (works with any LLM/embedding model)
|
|
317
|
+
|
|
318
|
+
## Comparison
|
|
319
|
+
|
|
320
|
+
| Feature | CueMap | Vector DBs |
|
|
321
|
+
|---------|--------|------------|
|
|
322
|
+
| **Speed** | 0.37ms P99 | 200-500ms |
|
|
323
|
+
| **SDK Size** | 5KB | 500MB+ |
|
|
324
|
+
| **Dependencies** | 2 | 50+ |
|
|
325
|
+
| **Install Time** | 1 sec | 5 min |
|
|
326
|
+
| **Cue Control** | Explicit | Auto (black box) |
|
|
327
|
+
| **Temporal** | ✅ Built-in | ❌ None |
|
|
328
|
+
|
|
329
|
+
## Documentation
|
|
330
|
+
|
|
331
|
+
- [Engine Repository](https://github.com/cuemap-dev/engine)
|
|
332
|
+
- [SDKs Repository](https://github.com/cuemap-dev/sdks)
|
|
333
|
+
- [Examples](https://github.com/cuemap-dev/sdks/tree/main/python/examples)
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cuemap/__init__.py,sha256=jy_21twtKaRLJq2AunVstNgFCIfe8QCjPpH5quxTz3c,817
|
|
2
|
+
cuemap/client.py,sha256=HzLzPAYjHKPVJstBIH7G3QNZ3k_D5Obt_hPLKJPQ3rw,9819
|
|
3
|
+
cuemap/exceptions.py,sha256=LPwk-pXDBWP-aA1m6w9BNqfM-zmT3-ZPZcyXdPDE9PE,245
|
|
4
|
+
cuemap/models.py,sha256=YgnKNtWJLad_s02koDhCSbqdm1MuN7cp_ih98J6gojY,566
|
|
5
|
+
cuemap-0.4.1.dist-info/licenses/LICENSE,sha256=XroXVr5iCchm6KhTUtnrjVAhcErBwmvhl7EwcjeI_yI,1063
|
|
6
|
+
cuemap-0.4.1.dist-info/METADATA,sha256=v1VekU6uNZWyIwfB2gBIFLJNtGr1UZsDo2yLuC4ShXU,8461
|
|
7
|
+
cuemap-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
cuemap-0.4.1.dist-info/top_level.txt,sha256=bZ0CmDM_glT73Eeo0pHCyjpEThjesQ0Zx9r3D4_N9LU,7
|
|
9
|
+
cuemap-0.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 CueMap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cuemap
|