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 +64 -0
- cortexos/_http.py +206 -0
- cortexos/async_client.py +254 -0
- cortexos/client.py +393 -0
- cortexos/errors.py +43 -0
- cortexos/exceptions.py +54 -0
- cortexos/integrations/__init__.py +6 -0
- cortexos/integrations/base.py +133 -0
- cortexos/integrations/mem0.py +229 -0
- cortexos/integrations/supermemory.py +211 -0
- cortexos/models.py +102 -0
- cortexos/tui/__init__.py +1 -0
- cortexos/tui/app.py +287 -0
- cortexos/tui/cli.py +33 -0
- cortexos/tui/commands.py +84 -0
- cortexos/tui/helpers.py +58 -0
- cortexos/tui/state.py +180 -0
- cortexos/tui/stream.py +88 -0
- cortexos/tui/styles.tcss +335 -0
- cortexos/tui/tabs/__init__.py +15 -0
- cortexos/tui/tabs/agents.py +112 -0
- cortexos/tui/tabs/claims.py +73 -0
- cortexos/tui/tabs/feed.py +70 -0
- cortexos/tui/tabs/inspect.py +233 -0
- cortexos/tui/tabs/memory.py +121 -0
- cortexos/tui/widgets/__init__.py +17 -0
- cortexos/tui/widgets/claim_table.py +106 -0
- cortexos/tui/widgets/confidence_bar.py +27 -0
- cortexos/tui/widgets/event_log.py +111 -0
- cortexos/tui/widgets/hi_sparkline.py +23 -0
- cortexos/tui/widgets/score_bar.py +21 -0
- cortexos/tui/widgets/stats_panel.py +51 -0
- cortexos/types.py +160 -0
- cortexos/verification.py +186 -0
- cortexos-0.2.0.dist-info/METADATA +169 -0
- cortexos-0.2.0.dist-info/RECORD +40 -0
- cortexos-0.2.0.dist-info/WHEEL +5 -0
- cortexos-0.2.0.dist-info/entry_points.txt +2 -0
- cortexos-0.2.0.dist-info/licenses/LICENSE +21 -0
- cortexos-0.2.0.dist-info/top_level.txt +1 -0
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)
|
cortexos/async_client.py
ADDED
|
@@ -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)
|