cortexdbai 0.2.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.
- cortexdb/__init__.py +79 -0
- cortexdb/client.py +962 -0
- cortexdb/exceptions.py +69 -0
- cortexdb/models.py +390 -0
- cortexdb/py.typed +0 -0
- cortexdbai-0.2.0.dist-info/METADATA +236 -0
- cortexdbai-0.2.0.dist-info/RECORD +8 -0
- cortexdbai-0.2.0.dist-info/WHEEL +4 -0
cortexdb/exceptions.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Exception hierarchy for the CortexDB Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CortexError(Exception):
|
|
7
|
+
"""Base exception for all CortexDB SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "") -> None:
|
|
10
|
+
self.message = message
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CortexAPIError(CortexError):
|
|
15
|
+
"""Raised when the CortexDB API returns an error response.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
status_code: HTTP status code from the server.
|
|
19
|
+
error_code: Application-level error code returned by the API, if any.
|
|
20
|
+
message: Human-readable error description.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
message: str,
|
|
26
|
+
status_code: int,
|
|
27
|
+
error_code: str | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.error_code = error_code
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
parts = [f"[HTTP {self.status_code}]"]
|
|
35
|
+
if self.error_code:
|
|
36
|
+
parts.append(f"[{self.error_code}]")
|
|
37
|
+
parts.append(self.message)
|
|
38
|
+
return " ".join(parts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CortexConnectionError(CortexError):
|
|
42
|
+
"""Raised when the SDK cannot connect to the CortexDB server."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CortexTimeoutError(CortexConnectionError):
|
|
46
|
+
"""Raised when a request to the CortexDB server times out."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CortexAuthError(CortexAPIError):
|
|
50
|
+
"""Raised when authentication fails (HTTP 401 or 403)."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CortexRateLimitError(CortexAPIError):
|
|
54
|
+
"""Raised when the API returns HTTP 429 Too Many Requests.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
retry_after: Number of seconds to wait before retrying, parsed from
|
|
58
|
+
the Retry-After header. None if the header was absent.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
message: str,
|
|
64
|
+
status_code: int = 429,
|
|
65
|
+
error_code: str | None = None,
|
|
66
|
+
retry_after: float | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self.retry_after = retry_after
|
|
69
|
+
super().__init__(message=message, status_code=status_code, error_code=error_code)
|
cortexdb/models.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Pydantic models for the CortexDB Python SDK.
|
|
2
|
+
|
|
3
|
+
These models mirror the Rust-side Episode schema and API request/response types.
|
|
4
|
+
All models use snake_case field names with camelCase JSON serialization for
|
|
5
|
+
wire compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import enum
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _to_camel(name: str) -> str:
|
|
19
|
+
"""Convert snake_case to camelCase for JSON serialization."""
|
|
20
|
+
parts = name.split("_")
|
|
21
|
+
return parts[0] + "".join(word.capitalize() for word in parts[1:])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SHARED_CONFIG = ConfigDict(
|
|
25
|
+
populate_by_name=True,
|
|
26
|
+
alias_generator=_to_camel,
|
|
27
|
+
use_enum_values=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Enums
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EpisodeType(str, enum.Enum):
|
|
37
|
+
"""Classification of an episode's content type."""
|
|
38
|
+
|
|
39
|
+
MESSAGE = "message"
|
|
40
|
+
CODE_CHANGE = "code_change"
|
|
41
|
+
INCIDENT = "incident"
|
|
42
|
+
DEPLOYMENT = "deployment"
|
|
43
|
+
ISSUE = "issue"
|
|
44
|
+
DOCUMENT = "document"
|
|
45
|
+
REVIEW = "review"
|
|
46
|
+
MEETING = "meeting"
|
|
47
|
+
ALERT = "alert"
|
|
48
|
+
DECISION = "decision"
|
|
49
|
+
CUSTOM = "custom"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ActorType(str, enum.Enum):
|
|
53
|
+
"""The kind of actor that produced an episode."""
|
|
54
|
+
|
|
55
|
+
PERSON = "person"
|
|
56
|
+
BOT = "bot"
|
|
57
|
+
SYSTEM = "system"
|
|
58
|
+
SERVICE = "service"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VisibilityLevel(str, enum.Enum):
|
|
62
|
+
"""Access level for an episode."""
|
|
63
|
+
|
|
64
|
+
ORGANIZATION = "organization"
|
|
65
|
+
RESTRICTED = "restricted"
|
|
66
|
+
PRIVATE = "private"
|
|
67
|
+
PUBLIC = "public"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Core domain models
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Actor(BaseModel):
|
|
76
|
+
"""Represents the entity that produced an episode."""
|
|
77
|
+
|
|
78
|
+
model_config = _SHARED_CONFIG
|
|
79
|
+
|
|
80
|
+
id: str
|
|
81
|
+
name: str = ""
|
|
82
|
+
actor_type: ActorType = ActorType.PERSON
|
|
83
|
+
email: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Source(BaseModel):
|
|
87
|
+
"""Provenance information for an episode."""
|
|
88
|
+
|
|
89
|
+
model_config = _SHARED_CONFIG
|
|
90
|
+
|
|
91
|
+
connector: str
|
|
92
|
+
external_id: str = ""
|
|
93
|
+
url: str | None = None
|
|
94
|
+
channel: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Visibility(BaseModel):
|
|
98
|
+
"""Access control specification for an episode."""
|
|
99
|
+
|
|
100
|
+
model_config = _SHARED_CONFIG
|
|
101
|
+
|
|
102
|
+
level: VisibilityLevel = VisibilityLevel.ORGANIZATION
|
|
103
|
+
allowed_groups: list[str] = Field(default_factory=list)
|
|
104
|
+
allowed_users: list[str] = Field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EntityRef(BaseModel):
|
|
108
|
+
"""A reference to a named entity mentioned in an episode."""
|
|
109
|
+
|
|
110
|
+
model_config = _SHARED_CONFIG
|
|
111
|
+
|
|
112
|
+
name: str
|
|
113
|
+
entity_type: str = ""
|
|
114
|
+
role: str | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Episode(BaseModel):
|
|
118
|
+
"""A single episode of memory stored in CortexDB.
|
|
119
|
+
|
|
120
|
+
An episode represents a discrete unit of information — a message,
|
|
121
|
+
document, code snippet, decision, or any other event worth remembering.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
model_config = _SHARED_CONFIG
|
|
125
|
+
|
|
126
|
+
id: UUID | None = None
|
|
127
|
+
tenant_id: str = "default"
|
|
128
|
+
episode_type: EpisodeType = EpisodeType.MESSAGE
|
|
129
|
+
content: str
|
|
130
|
+
actor: Actor | None = None
|
|
131
|
+
source: Source | None = None
|
|
132
|
+
visibility: Visibility | None = None
|
|
133
|
+
entities: list[EntityRef] | None = None
|
|
134
|
+
metadata: dict[str, Any] | None = None
|
|
135
|
+
created_at: datetime | None = None
|
|
136
|
+
updated_at: datetime | None = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# API request / response models
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class IngestEpisodeRequest(BaseModel):
|
|
145
|
+
"""Request body for POST /v1/episodes."""
|
|
146
|
+
|
|
147
|
+
model_config = _SHARED_CONFIG
|
|
148
|
+
|
|
149
|
+
content: str
|
|
150
|
+
tenant_id: str = "default"
|
|
151
|
+
episode_type: EpisodeType = EpisodeType.MESSAGE
|
|
152
|
+
actor: Actor | None = None
|
|
153
|
+
source: Source | None = None
|
|
154
|
+
visibility: Visibility | None = None
|
|
155
|
+
entities: list[EntityRef] | None = None
|
|
156
|
+
metadata: dict[str, Any] | None = None
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_dict(cls, data: dict[str, Any]) -> IngestEpisodeRequest:
|
|
160
|
+
"""Create an IngestEpisodeRequest from a plain dictionary.
|
|
161
|
+
|
|
162
|
+
Accepts a simplified dict with the following keys:
|
|
163
|
+
|
|
164
|
+
- ``content`` (str, required): The episode text content.
|
|
165
|
+
- ``type`` (str, optional): Episode type name (default ``"message"``).
|
|
166
|
+
- ``source`` (str, optional): Connector/system name (default ``"sdk"``).
|
|
167
|
+
- ``actor`` (str, optional): Actor name (default ``"sdk"``).
|
|
168
|
+
- ``tenant_id`` (str, optional): Tenant namespace (default ``"default"``).
|
|
169
|
+
- ``metadata`` (dict, optional): Arbitrary key-value metadata.
|
|
170
|
+
|
|
171
|
+
Any other keys are folded into the ``metadata`` dict so that callers
|
|
172
|
+
can pass domain-specific fields without worrying about the schema.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
A fully-formed ``IngestEpisodeRequest`` ready for ingestion.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ValueError: If ``content`` is missing from the dict.
|
|
179
|
+
"""
|
|
180
|
+
data = dict(data) # shallow copy to avoid mutating caller's dict
|
|
181
|
+
|
|
182
|
+
content = data.pop("content", None)
|
|
183
|
+
if content is None:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"The 'content' key is required when constructing an "
|
|
186
|
+
"IngestEpisodeRequest from a dict."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
episode_type_raw = data.pop("type", "message")
|
|
190
|
+
try:
|
|
191
|
+
episode_type = EpisodeType(episode_type_raw)
|
|
192
|
+
except ValueError:
|
|
193
|
+
episode_type = EpisodeType.MESSAGE
|
|
194
|
+
|
|
195
|
+
source_name = data.pop("source", "sdk")
|
|
196
|
+
actor_name = data.pop("actor", "sdk")
|
|
197
|
+
tenant_id = data.pop("tenant_id", "default")
|
|
198
|
+
metadata = data.pop("metadata", None)
|
|
199
|
+
|
|
200
|
+
# Remaining unknown keys get merged into metadata so nothing is lost.
|
|
201
|
+
if data:
|
|
202
|
+
if metadata is None:
|
|
203
|
+
metadata = {}
|
|
204
|
+
metadata.update(data)
|
|
205
|
+
|
|
206
|
+
return cls(
|
|
207
|
+
content=content,
|
|
208
|
+
episode_type=episode_type,
|
|
209
|
+
tenant_id=tenant_id,
|
|
210
|
+
actor=Actor(id=actor_name, name=actor_name, actor_type=ActorType.SERVICE),
|
|
211
|
+
source=Source(connector=source_name, external_id="sdk"),
|
|
212
|
+
metadata=metadata if metadata else None,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class IngestEpisodeResponse(BaseModel):
|
|
217
|
+
"""Response from POST /v1/episodes."""
|
|
218
|
+
|
|
219
|
+
model_config = _SHARED_CONFIG
|
|
220
|
+
|
|
221
|
+
episode_id: str
|
|
222
|
+
event_id: str
|
|
223
|
+
ingested_at: str
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class RememberRequest(BaseModel):
|
|
227
|
+
"""Request body for POST /v1/remember."""
|
|
228
|
+
|
|
229
|
+
model_config = _SHARED_CONFIG
|
|
230
|
+
|
|
231
|
+
content: str
|
|
232
|
+
tenant_id: str = "default"
|
|
233
|
+
scope: str | None = None
|
|
234
|
+
metadata: dict[str, Any] | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class RememberResponse(BaseModel):
|
|
238
|
+
"""Response from POST /v1/remember."""
|
|
239
|
+
|
|
240
|
+
model_config = _SHARED_CONFIG
|
|
241
|
+
|
|
242
|
+
event_id: str
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class RecallRequest(BaseModel):
|
|
246
|
+
"""Request body for POST /v1/recall."""
|
|
247
|
+
|
|
248
|
+
model_config = _SHARED_CONFIG
|
|
249
|
+
|
|
250
|
+
query: str
|
|
251
|
+
tenant_id: str = "default"
|
|
252
|
+
max_tokens: int = 4096
|
|
253
|
+
min_confidence: float = 0.0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class RecallResponse(BaseModel):
|
|
257
|
+
"""Response from POST /v1/recall."""
|
|
258
|
+
|
|
259
|
+
model_config = _SHARED_CONFIG
|
|
260
|
+
|
|
261
|
+
context: str
|
|
262
|
+
confidence: float
|
|
263
|
+
latency_ms: int = 0
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ForgetRequest(BaseModel):
|
|
267
|
+
"""Request body for POST /v1/forget."""
|
|
268
|
+
|
|
269
|
+
model_config = _SHARED_CONFIG
|
|
270
|
+
|
|
271
|
+
query: str | None = None
|
|
272
|
+
reason: str = ""
|
|
273
|
+
tenant_id: str = "default"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ForgetResponse(BaseModel):
|
|
277
|
+
"""Response from POST /v1/forget."""
|
|
278
|
+
|
|
279
|
+
model_config = _SHARED_CONFIG
|
|
280
|
+
|
|
281
|
+
forgotten_entities: int = 0
|
|
282
|
+
forgotten_edges: int = 0
|
|
283
|
+
audit_id: str = ""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class EpisodeBrief(BaseModel):
|
|
287
|
+
"""Summary of an episode returned by GET /v1/episodes.
|
|
288
|
+
|
|
289
|
+
This matches the ``EpisodeSummary`` struct in the Rust API which returns
|
|
290
|
+
a lightweight representation without the full content body.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
model_config = _SHARED_CONFIG
|
|
294
|
+
|
|
295
|
+
id: str
|
|
296
|
+
actor_name: str = ""
|
|
297
|
+
source_connector: str = ""
|
|
298
|
+
episode_type: EpisodeType = EpisodeType.MESSAGE
|
|
299
|
+
content_preview: str = ""
|
|
300
|
+
occurred_at: str = ""
|
|
301
|
+
entity_count: int = 0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ListEpisodesResponse(BaseModel):
|
|
305
|
+
"""Response from GET /v1/episodes."""
|
|
306
|
+
|
|
307
|
+
model_config = _SHARED_CONFIG
|
|
308
|
+
|
|
309
|
+
episodes: list[EpisodeBrief] = Field(default_factory=list)
|
|
310
|
+
total: int = 0
|
|
311
|
+
has_more: bool = False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class HealthResponse(BaseModel):
|
|
315
|
+
"""Response from GET /v1/admin/health."""
|
|
316
|
+
|
|
317
|
+
model_config = _SHARED_CONFIG
|
|
318
|
+
|
|
319
|
+
status: str
|
|
320
|
+
storage: str | None = None
|
|
321
|
+
version: str | None = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class MetricsResponse(BaseModel):
|
|
325
|
+
"""Response from GET /v1/admin/metrics."""
|
|
326
|
+
|
|
327
|
+
model_config = _SHARED_CONFIG
|
|
328
|
+
|
|
329
|
+
requests_total: int = 0
|
|
330
|
+
requests_active: int = 0
|
|
331
|
+
errors_total: int = 0
|
|
332
|
+
rate_limited_total: int = 0
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Entity / Link / Search models
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class EntityRelationship(BaseModel):
|
|
341
|
+
"""A relationship edge associated with an entity."""
|
|
342
|
+
|
|
343
|
+
id: str
|
|
344
|
+
source_entity_id: str
|
|
345
|
+
target_entity_id: str
|
|
346
|
+
relationship: str
|
|
347
|
+
fact: str
|
|
348
|
+
confidence: float
|
|
349
|
+
created_at: str
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class EpisodeBrief(BaseModel):
|
|
353
|
+
"""A lightweight summary of an episode."""
|
|
354
|
+
|
|
355
|
+
id: str
|
|
356
|
+
actor_name: str
|
|
357
|
+
source_connector: str
|
|
358
|
+
episode_type: str
|
|
359
|
+
content_preview: str
|
|
360
|
+
occurred_at: str
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class EntityResponse(BaseModel):
|
|
364
|
+
"""Response from GET /v1/entities/{entity_id}."""
|
|
365
|
+
|
|
366
|
+
id: str
|
|
367
|
+
name: str
|
|
368
|
+
entity_type: str
|
|
369
|
+
first_seen: str
|
|
370
|
+
last_seen: str
|
|
371
|
+
episode_count: int
|
|
372
|
+
relationships: list[EntityRelationship] = []
|
|
373
|
+
recent_episodes: list[EpisodeBrief] = []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class LinkResponse(BaseModel):
|
|
377
|
+
"""Response from POST /v1/link."""
|
|
378
|
+
|
|
379
|
+
id: str
|
|
380
|
+
created_at: str
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class SearchResponse(BaseModel):
|
|
384
|
+
"""Response from POST /v1/search."""
|
|
385
|
+
|
|
386
|
+
context: str
|
|
387
|
+
confidence: float
|
|
388
|
+
latency_ms: int
|
|
389
|
+
episodes: list[EpisodeBrief] = []
|
|
390
|
+
total_episodes: int = 0
|
cortexdb/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cortexdbai
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The Long-Term Memory Layer for AI Systems
|
|
5
|
+
Project-URL: Homepage, https://cortexdb.io
|
|
6
|
+
Project-URL: Documentation, https://docs.cortexdb.io
|
|
7
|
+
Project-URL: Repository, https://github.com/cortexdb/cortexdb
|
|
8
|
+
Project-URL: Issues, https://github.com/cortexdb/cortexdb/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/cortexdb/cortexdb/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Prashant Malik <prashant@cortexdb.io>
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
Keywords: ai,cortexdb,event-sourcing,knowledge-graph,llm,memory
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: httpx>=0.24
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: tenacity>=8.0
|
|
29
|
+
Provides-Extra: async
|
|
30
|
+
Requires-Dist: httpx[http2]; extra == 'async'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# CortexDB Python SDK
|
|
38
|
+
|
|
39
|
+
**The Long-Term Memory Layer for AI Systems.**
|
|
40
|
+
|
|
41
|
+
CortexDB gives your AI agents persistent, searchable memory. Store conversations, decisions, incidents, and domain knowledge as structured episodes. Retrieve relevant context with a single call.
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/cortexdb/)
|
|
44
|
+
[](https://pypi.org/project/cortexdb/)
|
|
45
|
+
[](https://github.com/cortexdb/cortexdb/blob/main/LICENSE)
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install cortexdb
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For HTTP/2 support (recommended for async workloads):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install cortexdb[async]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from cortexdb import Cortex
|
|
63
|
+
|
|
64
|
+
# Connect to CortexDB
|
|
65
|
+
cortex = Cortex("http://localhost:3141", api_key="your-api-key")
|
|
66
|
+
|
|
67
|
+
# Remember something
|
|
68
|
+
cortex.remember("The payments service was migrated to Kubernetes on March 15th.")
|
|
69
|
+
|
|
70
|
+
# Recall relevant memories
|
|
71
|
+
result = cortex.recall("When did we migrate payments?")
|
|
72
|
+
for match in result.matches:
|
|
73
|
+
print(f"[{match.confidence:.0%}] {match.content}")
|
|
74
|
+
|
|
75
|
+
# Forget when needed (GDPR, cleanup, etc.)
|
|
76
|
+
cortex.forget(query="old deploy logs", reason="quarterly cleanup")
|
|
77
|
+
|
|
78
|
+
# Search with filters
|
|
79
|
+
results = cortex.search(
|
|
80
|
+
"deploy failure",
|
|
81
|
+
source="github",
|
|
82
|
+
time_range="7d",
|
|
83
|
+
limit=10,
|
|
84
|
+
)
|
|
85
|
+
print(results.context)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Async Usage
|
|
89
|
+
|
|
90
|
+
Every method has an async counterpart via `AsyncCortex`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
from cortexdb import AsyncCortex
|
|
95
|
+
|
|
96
|
+
async def main():
|
|
97
|
+
async with AsyncCortex("http://localhost:3141", api_key="your-api-key") as cortex:
|
|
98
|
+
await cortex.remember("Async memory storage works great.")
|
|
99
|
+
|
|
100
|
+
result = await cortex.recall("How does async work?")
|
|
101
|
+
for match in result.matches:
|
|
102
|
+
print(match.content)
|
|
103
|
+
|
|
104
|
+
asyncio.run(main())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Episode Ingestion
|
|
108
|
+
|
|
109
|
+
For structured data, use episodes with full metadata:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cortexdb import Cortex, IngestEpisodeRequest, Actor, Source, ActorType, EpisodeType
|
|
113
|
+
|
|
114
|
+
cortex = Cortex("http://localhost:3141")
|
|
115
|
+
|
|
116
|
+
# Ingest a structured episode
|
|
117
|
+
episode = IngestEpisodeRequest(
|
|
118
|
+
content="PR #412: Add retry logic to payment processor",
|
|
119
|
+
episode_type=EpisodeType.CODE_CHANGE,
|
|
120
|
+
actor=Actor(id="ci-bot", name="CI Bot", actor_type=ActorType.SERVICE),
|
|
121
|
+
source=Source(connector="github", external_id="pr-412", url="https://github.com/org/repo/pull/412"),
|
|
122
|
+
metadata={"branch": "main", "files_changed": 3},
|
|
123
|
+
)
|
|
124
|
+
response = cortex.ingest_episode(episode)
|
|
125
|
+
print(f"Episode {response.episode_id} ingested.")
|
|
126
|
+
|
|
127
|
+
# Or use the smart store shorthand
|
|
128
|
+
cortex.store("Plain text goes through remember().")
|
|
129
|
+
cortex.store({
|
|
130
|
+
"content": "Dict data goes through ingest_episode().",
|
|
131
|
+
"type": "deployment",
|
|
132
|
+
"source": "slack",
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Entity Lookup
|
|
137
|
+
|
|
138
|
+
Explore entities and their relationships in the knowledge graph:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
cortex = Cortex("http://localhost:3141")
|
|
142
|
+
|
|
143
|
+
# Look up an entity
|
|
144
|
+
entity = cortex.entity("payments-service")
|
|
145
|
+
print(f"{entity.name} ({entity.entity_type})")
|
|
146
|
+
print(f" Episodes: {entity.episode_count}")
|
|
147
|
+
print(f" First seen: {entity.first_seen}")
|
|
148
|
+
|
|
149
|
+
# Browse relationships
|
|
150
|
+
for rel in entity.relationships:
|
|
151
|
+
print(f" {rel.relationship} -> {rel.target_entity_id} ({rel.confidence:.0%})")
|
|
152
|
+
|
|
153
|
+
# Create explicit relationships
|
|
154
|
+
cortex.link(
|
|
155
|
+
source_entity_id="payments-service",
|
|
156
|
+
target_entity_id="auth-service",
|
|
157
|
+
relationship="DEPENDS_ON",
|
|
158
|
+
fact="Payments calls auth for token validation",
|
|
159
|
+
confidence=0.95,
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
The SDK provides a structured exception hierarchy:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from cortexdb import Cortex
|
|
169
|
+
from cortexdb.exceptions import (
|
|
170
|
+
CortexError, # Base exception
|
|
171
|
+
CortexAPIError, # HTTP 4xx/5xx errors
|
|
172
|
+
CortexAuthError, # HTTP 401/403
|
|
173
|
+
CortexRateLimitError, # HTTP 429 (includes retry_after)
|
|
174
|
+
CortexConnectionError,# Network failures
|
|
175
|
+
CortexTimeoutError, # Request timeouts
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
cortex = Cortex("http://localhost:3141")
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
cortex.remember("important data")
|
|
182
|
+
except CortexRateLimitError as e:
|
|
183
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
184
|
+
except CortexAuthError as e:
|
|
185
|
+
print(f"Authentication failed: {e.message}")
|
|
186
|
+
except CortexConnectionError:
|
|
187
|
+
print("Cannot reach CortexDB server")
|
|
188
|
+
except CortexAPIError as e:
|
|
189
|
+
print(f"API error [{e.status_code}]: {e.message}")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Transient errors (429, 502, 503, 504) and connection failures are automatically retried with exponential backoff. Configure retry behavior at client initialization:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
cortex = Cortex(
|
|
196
|
+
"http://localhost:3141",
|
|
197
|
+
max_retries=5, # default: 3
|
|
198
|
+
timeout=60.0, # default: 30.0 seconds
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
### Constructor Parameters
|
|
205
|
+
|
|
206
|
+
| Parameter | Type | Default | Description |
|
|
207
|
+
|----------------|---------|--------------------------|------------------------------------|
|
|
208
|
+
| `base_url` | `str` | `http://localhost:3141` | CortexDB server URL |
|
|
209
|
+
| `api_key` | `str` | `None` | Bearer token for authentication |
|
|
210
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds |
|
|
211
|
+
| `max_retries` | `int` | `3` | Max retries for transient failures |
|
|
212
|
+
|
|
213
|
+
### Environment Variables
|
|
214
|
+
|
|
215
|
+
| Variable | Description |
|
|
216
|
+
|---------------------|----------------------------------------------|
|
|
217
|
+
| `CORTEXDB_API_KEY` | Fallback API key when none is passed directly |
|
|
218
|
+
|
|
219
|
+
## Convenience Methods
|
|
220
|
+
|
|
221
|
+
| Method | Description |
|
|
222
|
+
|-----------------------|-------------------------------------------------------|
|
|
223
|
+
| `cortex.memory(q)` | Alias for `recall()` -- the GTM one-liner |
|
|
224
|
+
| `cortex.store(data)` | Smart dispatch: `str` -> `remember()`, `dict` -> `ingest_episode()` |
|
|
225
|
+
| `cortex.context(q)` | Recall with higher token budget (8192) for LLM agents |
|
|
226
|
+
|
|
227
|
+
## Links
|
|
228
|
+
|
|
229
|
+
- [Documentation](https://docs.cortexdb.io)
|
|
230
|
+
- [GitHub Repository](https://github.com/cortexdb/cortexdb)
|
|
231
|
+
- [Issue Tracker](https://github.com/cortexdb/cortexdb/issues)
|
|
232
|
+
- [Changelog](https://github.com/cortexdb/cortexdb/blob/main/CHANGELOG.md)
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
Apache-2.0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cortexdb/__init__.py,sha256=LSlY8w1BUUlJsHxFly1AfnpQG_KplWZ08B11wh-kgHo,1616
|
|
2
|
+
cortexdb/client.py,sha256=2wPZmzwyTgIN6indk2v2HHdrtjKTY0ZmZOgDTAqtzh8,32045
|
|
3
|
+
cortexdb/exceptions.py,sha256=2ldGt_y3K2zx299VvNUVG_b9ZN0CXPbgOdJTaVnhScc,1991
|
|
4
|
+
cortexdb/models.py,sha256=kG-us0qZSKBGZ0Oq8DXVXhq6y-sYvEWw8FdBlPDpTtY,9965
|
|
5
|
+
cortexdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
cortexdbai-0.2.0.dist-info/METADATA,sha256=mYOFUmx_WBp1yhERLac-QA4TJ4ycx8NrMcvyDm9CjaA,7763
|
|
7
|
+
cortexdbai-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
cortexdbai-0.2.0.dist-info/RECORD,,
|