cortexos 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.
cortexos/__init__.py ADDED
@@ -0,0 +1,64 @@
1
+ """CortexOS Python SDK — developer-facing interface to the CortexOS memory engine."""
2
+
3
+ from cortexos.async_client import AsyncCortex
4
+ from cortexos.client import Cortex
5
+ from cortexos.errors import (
6
+ AuthError,
7
+ CortexError,
8
+ MemoryNotFoundError,
9
+ RateLimitError,
10
+ ServerError,
11
+ ValidationError,
12
+ )
13
+ from cortexos.exceptions import CortexOSError, MemoryBlockedError
14
+ from cortexos.models import CheckResult, ClaimResult, GateResult, ShieldResult
15
+ from cortexos.types import (
16
+ Attribution,
17
+ CAMAAttribution,
18
+ CAMAClaim,
19
+ CAMAClaimSource,
20
+ EASScore,
21
+ Memory,
22
+ Page,
23
+ RecallAndAttributeResult,
24
+ RecallResult,
25
+ )
26
+ from cortexos.verification import VerificationClient
27
+
28
+ from importlib.metadata import version, PackageNotFoundError
29
+
30
+ try:
31
+ __version__ = version("cortexos")
32
+ except PackageNotFoundError:
33
+ __version__ = "0.2.0" # fallback for editable installs
34
+
35
+ __all__ = [
36
+ # Clients
37
+ "Cortex",
38
+ "AsyncCortex",
39
+ "VerificationClient",
40
+ # Types
41
+ "Memory",
42
+ "Attribution",
43
+ "CAMAAttribution",
44
+ "CAMAClaim",
45
+ "CAMAClaimSource",
46
+ "EASScore",
47
+ "RecallResult",
48
+ "RecallAndAttributeResult",
49
+ "Page",
50
+ # Verification models
51
+ "CheckResult",
52
+ "ClaimResult",
53
+ "GateResult",
54
+ "ShieldResult",
55
+ # Errors
56
+ "CortexError",
57
+ "CortexOSError",
58
+ "MemoryBlockedError",
59
+ "AuthError",
60
+ "RateLimitError",
61
+ "MemoryNotFoundError",
62
+ "ValidationError",
63
+ "ServerError",
64
+ ]
cortexos/_http.py ADDED
@@ -0,0 +1,206 @@
1
+ """Internal HTTP layer — sync and async httpx clients with retry and auth."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from cortexos.errors import (
11
+ AuthError,
12
+ CortexError,
13
+ MemoryNotFoundError,
14
+ RateLimitError,
15
+ ServerError,
16
+ ValidationError,
17
+ )
18
+
19
+ _DEFAULT_RETRIES = 3
20
+ _RETRY_STATUSES = {429, 502, 503, 504}
21
+ _BACKOFF_BASE = 0.5 # seconds
22
+
23
+
24
+ def _build_headers(api_key: str | None) -> dict[str, str]:
25
+ headers: dict[str, str] = {"Content-Type": "application/json"}
26
+ if api_key:
27
+ headers["Authorization"] = f"Bearer {api_key}"
28
+ return headers
29
+
30
+
31
+ def _raise_for_status(resp: httpx.Response, memory_id: str | None = None) -> None:
32
+ if resp.status_code < 400:
33
+ return
34
+ body = resp.text
35
+
36
+ if resp.status_code == 401 or resp.status_code == 403:
37
+ raise AuthError("Invalid or missing API key", status_code=resp.status_code, response_body=body)
38
+
39
+ if resp.status_code == 404:
40
+ if memory_id:
41
+ raise MemoryNotFoundError(memory_id)
42
+ raise CortexError("Resource not found", status_code=404, response_body=body)
43
+
44
+ if resp.status_code == 422:
45
+ raise ValidationError(f"Validation error: {body[:300]}", status_code=422, response_body=body)
46
+
47
+ if resp.status_code == 429:
48
+ retry_after: float | None = None
49
+ try:
50
+ retry_after = float(resp.headers.get("Retry-After", ""))
51
+ except (ValueError, TypeError):
52
+ pass
53
+ raise RateLimitError(retry_after=retry_after, status_code=429, response_body=body)
54
+
55
+ if resp.status_code >= 500:
56
+ raise ServerError(f"Server error {resp.status_code}: {body[:300]}", status_code=resp.status_code, response_body=body)
57
+
58
+ raise CortexError(f"Unexpected HTTP {resp.status_code}: {body[:300]}", status_code=resp.status_code, response_body=body)
59
+
60
+
61
+ # ── Sync HTTP client ───────────────────────────────────────────────────────
62
+
63
+
64
+ class SyncHTTP:
65
+ def __init__(
66
+ self,
67
+ base_url: str,
68
+ api_key: str | None,
69
+ timeout: float,
70
+ max_retries: int,
71
+ ):
72
+ self._client = httpx.Client(
73
+ base_url=base_url,
74
+ headers=_build_headers(api_key),
75
+ timeout=timeout,
76
+ )
77
+ self._max_retries = max_retries
78
+
79
+ def close(self) -> None:
80
+ self._client.close()
81
+
82
+ def __enter__(self) -> "SyncHTTP":
83
+ return self
84
+
85
+ def __exit__(self, *_: Any) -> None:
86
+ self.close()
87
+
88
+ def request(
89
+ self,
90
+ method: str,
91
+ path: str,
92
+ *,
93
+ memory_id: str | None = None,
94
+ **kwargs: Any,
95
+ ) -> httpx.Response:
96
+ last_exc: Exception | None = None
97
+ for attempt in range(self._max_retries):
98
+ try:
99
+ resp = self._client.request(method, path, **kwargs)
100
+ if resp.status_code in _RETRY_STATUSES and attempt < self._max_retries - 1:
101
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
102
+ continue
103
+ _raise_for_status(resp, memory_id=memory_id)
104
+ return resp
105
+ except (AuthError, MemoryNotFoundError, ValidationError):
106
+ raise # Never retry these
107
+ except (RateLimitError, ServerError, CortexError) as exc:
108
+ last_exc = exc
109
+ if attempt < self._max_retries - 1:
110
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
111
+ except httpx.TimeoutException as exc:
112
+ last_exc = CortexError(f"Request timed out: {exc}")
113
+ if attempt < self._max_retries - 1:
114
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
115
+ except httpx.RequestError as exc:
116
+ last_exc = CortexError(f"Connection error: {exc}")
117
+ if attempt < self._max_retries - 1:
118
+ time.sleep(_BACKOFF_BASE * (2 ** attempt))
119
+ raise last_exc or CortexError("Request failed after retries")
120
+
121
+ def get(self, path: str, **kwargs: Any) -> httpx.Response:
122
+ return self.request("GET", path, **kwargs)
123
+
124
+ def post(self, path: str, **kwargs: Any) -> httpx.Response:
125
+ return self.request("POST", path, **kwargs)
126
+
127
+ def patch(self, path: str, **kwargs: Any) -> httpx.Response:
128
+ return self.request("PATCH", path, **kwargs)
129
+
130
+ def delete(self, path: str, **kwargs: Any) -> httpx.Response:
131
+ return self.request("DELETE", path, **kwargs)
132
+
133
+
134
+ # ── Async HTTP client ──────────────────────────────────────────────────────
135
+
136
+
137
+ class AsyncHTTP:
138
+ def __init__(
139
+ self,
140
+ base_url: str,
141
+ api_key: str | None,
142
+ timeout: float,
143
+ max_retries: int,
144
+ ):
145
+ self._client = httpx.AsyncClient(
146
+ base_url=base_url,
147
+ headers=_build_headers(api_key),
148
+ timeout=timeout,
149
+ )
150
+ self._max_retries = max_retries
151
+
152
+ async def aclose(self) -> None:
153
+ await self._client.aclose()
154
+
155
+ async def __aenter__(self) -> "AsyncHTTP":
156
+ return self
157
+
158
+ async def __aexit__(self, *_: Any) -> None:
159
+ await self.aclose()
160
+
161
+ async def request(
162
+ self,
163
+ method: str,
164
+ path: str,
165
+ *,
166
+ memory_id: str | None = None,
167
+ **kwargs: Any,
168
+ ) -> httpx.Response:
169
+ import asyncio
170
+
171
+ last_exc: Exception | None = None
172
+ for attempt in range(self._max_retries):
173
+ try:
174
+ resp = await self._client.request(method, path, **kwargs)
175
+ if resp.status_code in _RETRY_STATUSES and attempt < self._max_retries - 1:
176
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
177
+ continue
178
+ _raise_for_status(resp, memory_id=memory_id)
179
+ return resp
180
+ except (AuthError, MemoryNotFoundError, ValidationError):
181
+ raise
182
+ except (RateLimitError, ServerError, CortexError) as exc:
183
+ last_exc = exc
184
+ if attempt < self._max_retries - 1:
185
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
186
+ except httpx.TimeoutException as exc:
187
+ last_exc = CortexError(f"Request timed out: {exc}")
188
+ if attempt < self._max_retries - 1:
189
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
190
+ except httpx.RequestError as exc:
191
+ last_exc = CortexError(f"Connection error: {exc}")
192
+ if attempt < self._max_retries - 1:
193
+ await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
194
+ raise last_exc or CortexError("Request failed after retries")
195
+
196
+ async def get(self, path: str, **kwargs: Any) -> httpx.Response:
197
+ return await self.request("GET", path, **kwargs)
198
+
199
+ async def post(self, path: str, **kwargs: Any) -> httpx.Response:
200
+ return await self.request("POST", path, **kwargs)
201
+
202
+ async def patch(self, path: str, **kwargs: Any) -> httpx.Response:
203
+ return await self.request("PATCH", path, **kwargs)
204
+
205
+ async def delete(self, path: str, **kwargs: Any) -> httpx.Response:
206
+ return await self.request("DELETE", path, **kwargs)
@@ -0,0 +1,254 @@
1
+ """Asynchronous CortexOS client — same interface as Cortex but fully async."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from cortexos._http import AsyncHTTP
8
+ from cortexos.client import _memory_payload, _DEFAULT_BASE_URL, _API_PREFIX, _DEFAULT_TIMEOUT, _DEFAULT_RETRIES
9
+ from cortexos.types import Attribution, CAMAAttribution, Memory, Page, RecallAndAttributeResult, RecallResult
10
+
11
+
12
+ class AsyncCortex:
13
+ """
14
+ Asynchronous CortexOS SDK client.
15
+
16
+ Usage::
17
+
18
+ from cortexos import AsyncCortex
19
+
20
+ async with AsyncCortex(api_key="sk-...", agent_id="my-agent") as cx:
21
+ mem = await cx.remember("User prefers dark mode", importance=0.8)
22
+ results = await cx.recall("UI preferences?", top_k=5)
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ agent_id: str,
28
+ api_key: str | None = None,
29
+ base_url: str = _DEFAULT_BASE_URL,
30
+ timeout: float = _DEFAULT_TIMEOUT,
31
+ max_retries: int = _DEFAULT_RETRIES,
32
+ ):
33
+ """
34
+ Args:
35
+ agent_id: The default agent namespace for all operations.
36
+ api_key: Bearer token sent as ``Authorization: Bearer <key>``.
37
+ base_url: Root URL of the CortexOS engine.
38
+ timeout: Per-request timeout in seconds.
39
+ max_retries: Number of retry attempts on transient errors.
40
+ """
41
+ self.agent_id = agent_id
42
+ self._http = AsyncHTTP(base_url, api_key, timeout, max_retries)
43
+ self._prefix = _API_PREFIX
44
+
45
+ # ── Context manager support ────────────────────────────────────────────
46
+
47
+ async def __aenter__(self) -> "AsyncCortex":
48
+ return self
49
+
50
+ async def __aexit__(self, *_: Any) -> None:
51
+ await self.aclose()
52
+
53
+ async def aclose(self) -> None:
54
+ """Close the underlying async HTTP connection pool."""
55
+ await self._http.aclose()
56
+
57
+ # ── Memory CRUD ────────────────────────────────────────────────────────
58
+
59
+ async def remember(
60
+ self,
61
+ content: str,
62
+ *,
63
+ importance: float = 0.5,
64
+ tags: list[str] | None = None,
65
+ metadata: dict[str, Any] | None = None,
66
+ tier: str = "warm",
67
+ ttl: int | None = None,
68
+ agent_id: str | None = None,
69
+ ) -> Memory:
70
+ """Store a new memory. See :meth:`~cortexos.Cortex.remember` for full docs."""
71
+ payload = _memory_payload(
72
+ content=content,
73
+ agent_id=agent_id or self.agent_id,
74
+ importance=importance,
75
+ tags=tags or [],
76
+ metadata=metadata or {},
77
+ tier=tier,
78
+ ttl=ttl,
79
+ )
80
+ resp = await self._http.post(f"{self._prefix}/memories", json=payload)
81
+ return Memory._from_api(resp.json())
82
+
83
+ async def get(self, memory_id: str) -> Memory:
84
+ """Fetch a memory by ID. Raises :class:`~cortexos.errors.MemoryNotFoundError` if missing."""
85
+ resp = await self._http.get(
86
+ f"{self._prefix}/memories/{memory_id}",
87
+ memory_id=memory_id,
88
+ )
89
+ return Memory._from_api(resp.json()["memory"])
90
+
91
+ async def update(
92
+ self,
93
+ memory_id: str,
94
+ *,
95
+ importance: float | None = None,
96
+ tags: list[str] | None = None,
97
+ metadata: dict[str, Any] | None = None,
98
+ tier: str | None = None,
99
+ ) -> Memory:
100
+ """Update memory fields. See :meth:`~cortexos.Cortex.update` for full docs."""
101
+ values: dict[str, Any] = {}
102
+ if importance is not None:
103
+ values["criticality"] = importance
104
+ if tier is not None:
105
+ values["tier"] = tier
106
+ if tags is not None or metadata is not None:
107
+ current = await self.get(memory_id)
108
+ merged_meta = dict(current.metadata)
109
+ if tags is not None:
110
+ merged_meta["_tags"] = tags
111
+ if metadata is not None:
112
+ merged_meta.update(metadata)
113
+ values["metadata"] = merged_meta
114
+ resp = await self._http.patch(
115
+ f"{self._prefix}/memories/{memory_id}",
116
+ json=values,
117
+ memory_id=memory_id,
118
+ )
119
+ return Memory._from_api(resp.json())
120
+
121
+ async def forget(self, memory_id: str) -> None:
122
+ """Soft-delete a memory. Raises :class:`~cortexos.errors.MemoryNotFoundError` if missing."""
123
+ await self._http.delete(
124
+ f"{self._prefix}/memories/{memory_id}",
125
+ memory_id=memory_id,
126
+ )
127
+
128
+ async def list(
129
+ self,
130
+ *,
131
+ limit: int = 50,
132
+ offset: int = 0,
133
+ tier: str | None = None,
134
+ sort_by: str = "created_at",
135
+ order: str = "desc",
136
+ agent_id: str | None = None,
137
+ ) -> Page:
138
+ """List memories with pagination. See :meth:`~cortexos.Cortex.list` for full docs."""
139
+ params: dict[str, Any] = {
140
+ "agent_id": agent_id or self.agent_id,
141
+ "limit": limit,
142
+ "offset": offset,
143
+ "sort_by": sort_by,
144
+ "order": order,
145
+ }
146
+ if tier is not None:
147
+ params["tier"] = tier
148
+ resp = await self._http.get(f"{self._prefix}/memories", params=params)
149
+ data = resp.json()
150
+ return Page(
151
+ items=[Memory._from_api(m) for m in data["items"]],
152
+ total=data["total"],
153
+ offset=data["offset"],
154
+ limit=data["limit"],
155
+ )
156
+
157
+ # ── Recall ─────────────────────────────────────────────────────────────
158
+
159
+ async def recall(
160
+ self,
161
+ query: str,
162
+ *,
163
+ top_k: int = 10,
164
+ min_score: float = 0.0,
165
+ tags: list[str] | None = None,
166
+ agent_id: str | None = None,
167
+ ) -> list[RecallResult]:
168
+ """Semantic search. See :meth:`~cortexos.Cortex.recall` for full docs."""
169
+ params: dict[str, Any] = {
170
+ "q": query,
171
+ "agent_id": agent_id or self.agent_id,
172
+ "top_k": top_k,
173
+ }
174
+ resp = await self._http.get(f"{self._prefix}/memories/search", params=params)
175
+ results = [
176
+ RecallResult(memory=Memory._from_api(item["memory"]), score=item["similarity"])
177
+ for item in resp.json()
178
+ if item["similarity"] >= min_score
179
+ ]
180
+ if tags:
181
+ tag_set = set(tags)
182
+ results = [r for r in results if tag_set.intersection(r.memory.tags)]
183
+ return results
184
+
185
+ # ── Attribution ─────────────────────────────────────────────────────────
186
+
187
+ async def attribute(
188
+ self,
189
+ query: str,
190
+ response: str,
191
+ memory_ids: list[str],
192
+ *,
193
+ agent_id: str | None = None,
194
+ ) -> Attribution:
195
+ """Run EAS attribution. See :meth:`~cortexos.Cortex.attribute` for full docs."""
196
+ payload = {
197
+ "query_text": query,
198
+ "response_text": response,
199
+ "retrieved_memory_ids": memory_ids,
200
+ "agent_id": agent_id or self.agent_id,
201
+ }
202
+ resp = await self._http.post(f"{self._prefix}/transactions", json=payload)
203
+ return Attribution._from_api(resp.json(), query=query, response=response)
204
+
205
+ async def cama_attribute(
206
+ self,
207
+ transaction_id: str,
208
+ ) -> CAMAAttribution:
209
+ """Run CAMA attribution on an existing transaction.
210
+
211
+ See :meth:`~cortexos.Cortex.cama_attribute` for full docs.
212
+ """
213
+ resp = await self._http.post(f"{self._prefix}/attribution/{transaction_id}/cama")
214
+ return CAMAAttribution._from_api(resp.json(), transaction_id=transaction_id)
215
+
216
+ async def check(
217
+ self,
218
+ query: str,
219
+ response: str,
220
+ memory_ids: list[str],
221
+ *,
222
+ agent_id: str | None = None,
223
+ ) -> CAMAAttribution:
224
+ """One-shot hallucination check. See :meth:`~cortexos.Cortex.check` for full docs."""
225
+ attr = await self.attribute(query, response, memory_ids, agent_id=agent_id)
226
+ return await self.cama_attribute(attr.transaction_id)
227
+
228
+ async def forget_all(self, *, agent_id: str | None = None) -> int:
229
+ """Delete all memories for the given agent. Returns count deleted."""
230
+ count = 0
231
+ while True:
232
+ page = await self.list(agent_id=agent_id, limit=50, offset=0)
233
+ if not page.items:
234
+ break
235
+ for mem in page.items:
236
+ await self.forget(mem.id)
237
+ count += 1
238
+ return count
239
+
240
+ async def recall_and_attribute(
241
+ self,
242
+ query: str,
243
+ response: str,
244
+ *,
245
+ top_k: int = 10,
246
+ min_score: float = 0.0,
247
+ agent_id: str | None = None,
248
+ ) -> RecallAndAttributeResult:
249
+ """Recall then attribute. See :meth:`~cortexos.Cortex.recall_and_attribute` for full docs."""
250
+ aid = agent_id or self.agent_id
251
+ recall_results = await self.recall(query, top_k=top_k, min_score=min_score, agent_id=aid)
252
+ memory_ids = [r.memory.id for r in recall_results]
253
+ attribution = await self.attribute(query, response, memory_ids, agent_id=aid)
254
+ return RecallAndAttributeResult(memories=recall_results, attribution=attribution)