cortexos 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. cortexos-0.2.0/LICENSE +21 -0
  2. cortexos-0.2.0/MANIFEST.in +3 -0
  3. cortexos-0.2.0/PKG-INFO +169 -0
  4. cortexos-0.2.0/README.md +138 -0
  5. cortexos-0.2.0/cortexos/__init__.py +64 -0
  6. cortexos-0.2.0/cortexos/_http.py +206 -0
  7. cortexos-0.2.0/cortexos/async_client.py +254 -0
  8. cortexos-0.2.0/cortexos/client.py +393 -0
  9. cortexos-0.2.0/cortexos/errors.py +43 -0
  10. cortexos-0.2.0/cortexos/exceptions.py +54 -0
  11. cortexos-0.2.0/cortexos/integrations/__init__.py +6 -0
  12. cortexos-0.2.0/cortexos/integrations/base.py +133 -0
  13. cortexos-0.2.0/cortexos/integrations/mem0.py +229 -0
  14. cortexos-0.2.0/cortexos/integrations/supermemory.py +211 -0
  15. cortexos-0.2.0/cortexos/models.py +102 -0
  16. cortexos-0.2.0/cortexos/tui/__init__.py +1 -0
  17. cortexos-0.2.0/cortexos/tui/app.py +287 -0
  18. cortexos-0.2.0/cortexos/tui/cli.py +33 -0
  19. cortexos-0.2.0/cortexos/tui/commands.py +84 -0
  20. cortexos-0.2.0/cortexos/tui/helpers.py +58 -0
  21. cortexos-0.2.0/cortexos/tui/state.py +180 -0
  22. cortexos-0.2.0/cortexos/tui/stream.py +88 -0
  23. cortexos-0.2.0/cortexos/tui/styles.tcss +335 -0
  24. cortexos-0.2.0/cortexos/tui/tabs/__init__.py +15 -0
  25. cortexos-0.2.0/cortexos/tui/tabs/agents.py +112 -0
  26. cortexos-0.2.0/cortexos/tui/tabs/claims.py +73 -0
  27. cortexos-0.2.0/cortexos/tui/tabs/feed.py +70 -0
  28. cortexos-0.2.0/cortexos/tui/tabs/inspect.py +233 -0
  29. cortexos-0.2.0/cortexos/tui/tabs/memory.py +121 -0
  30. cortexos-0.2.0/cortexos/tui/widgets/__init__.py +17 -0
  31. cortexos-0.2.0/cortexos/tui/widgets/claim_table.py +106 -0
  32. cortexos-0.2.0/cortexos/tui/widgets/confidence_bar.py +27 -0
  33. cortexos-0.2.0/cortexos/tui/widgets/event_log.py +111 -0
  34. cortexos-0.2.0/cortexos/tui/widgets/hi_sparkline.py +23 -0
  35. cortexos-0.2.0/cortexos/tui/widgets/score_bar.py +21 -0
  36. cortexos-0.2.0/cortexos/tui/widgets/stats_panel.py +51 -0
  37. cortexos-0.2.0/cortexos/types.py +160 -0
  38. cortexos-0.2.0/cortexos/verification.py +186 -0
  39. cortexos-0.2.0/cortexos.egg-info/PKG-INFO +169 -0
  40. cortexos-0.2.0/cortexos.egg-info/SOURCES.txt +48 -0
  41. cortexos-0.2.0/cortexos.egg-info/dependency_links.txt +1 -0
  42. cortexos-0.2.0/cortexos.egg-info/entry_points.txt +2 -0
  43. cortexos-0.2.0/cortexos.egg-info/requires.txt +12 -0
  44. cortexos-0.2.0/cortexos.egg-info/top_level.txt +1 -0
  45. cortexos-0.2.0/pyproject.toml +55 -0
  46. cortexos-0.2.0/setup.cfg +4 -0
  47. cortexos-0.2.0/tests/test_client.py +429 -0
  48. cortexos-0.2.0/tests/test_integration.py +309 -0
  49. cortexos-0.2.0/tests/test_integrations.py +622 -0
  50. cortexos-0.2.0/tests/test_tui.py +343 -0
cortexos-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CortexOS
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,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include cortexos *.py *.tcss
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexos
3
+ Version: 0.2.0
4
+ Summary: CortexOS Python SDK — hallucination detection for LLM agents
5
+ License: MIT
6
+ Project-URL: Homepage, https://cortexa.ink
7
+ Project-URL: Documentation, https://cortexa.ink/docs
8
+ Project-URL: Repository, https://github.com/Tactacion/cortexos-sdk
9
+ Project-URL: Bug Tracker, https://github.com/Tactacion/cortexos-sdk/issues
10
+ Keywords: llm,memory,attribution,rag,ai,hallucination
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Requires-Dist: pydantic<3,>=2.7
22
+ Provides-Extra: tui
23
+ Requires-Dist: textual<1,>=0.80; extra == "tui"
24
+ Requires-Dist: httpx-sse<1,>=0.4; extra == "tui"
25
+ Requires-Dist: click<9,>=8; extra == "tui"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest<9,>=8; extra == "dev"
28
+ Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
29
+ Requires-Dist: respx<1,>=0.21; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # CortexOS Python SDK
33
+
34
+ The official Python SDK for [Cortexa](https://cortexa.ink) — hallucination detection and verification for LLM agents.
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ pip install cortexos
40
+ ```
41
+
42
+ ```python
43
+ import cortexos
44
+
45
+ cortexos.configure(api_key="your-key")
46
+
47
+ result = cortexos.check(
48
+ response="The return window is 30 days",
49
+ sources=["Return policy: 30-day window for all items."]
50
+ )
51
+ print(f"Hallucination Index: {result.hallucination_index}")
52
+ # → Hallucination Index: 0.0
53
+ ```
54
+
55
+ Get an API key at [cortexa.ink](https://cortexa.ink)
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install cortexos
61
+ ```
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ from cortexos import Cortex
67
+
68
+ cx = Cortex(api_key="sk-...", agent_id="support-bot")
69
+
70
+ # Store a memory
71
+ mem = cx.remember(
72
+ "User prefers email over Slack for all communications",
73
+ importance=0.85,
74
+ tags=["preferences", "communication"],
75
+ metadata={"source": "conversation_123"},
76
+ )
77
+
78
+ # Semantic recall
79
+ results = cx.recall("how does the user want to be contacted?", top_k=5)
80
+ for r in results:
81
+ print(f"[{r.score:.2f}] {r.memory.content}")
82
+
83
+ # EAS attribution — score which memories contributed to your LLM response
84
+ attr = cx.attribute(
85
+ query="How should I contact the user?",
86
+ response="Based on their preferences, contact them via email.",
87
+ memory_ids=[mem.id],
88
+ )
89
+ print(attr.scores) # {"mem_xxx": 0.91}
90
+
91
+ # Combined recall + attribution in one call
92
+ result = cx.recall_and_attribute(
93
+ query="How should I reach the user?",
94
+ response="Contact via email.",
95
+ top_k=10,
96
+ )
97
+ ```
98
+
99
+ ## Async usage
100
+
101
+ ```python
102
+ import asyncio
103
+ from cortexos import AsyncCortex
104
+
105
+ async def main():
106
+ async with AsyncCortex(api_key="sk-...", agent_id="my-agent") as cx:
107
+ mem = await cx.remember("User lives in Tokyo", importance=0.7)
108
+ results = await cx.recall("where does the user live?")
109
+ await cx.forget(mem.id)
110
+
111
+ asyncio.run(main())
112
+ ```
113
+
114
+ ## API reference
115
+
116
+ ### `Cortex` / `AsyncCortex`
117
+
118
+ | Method | Description |
119
+ |--------|-------------|
120
+ | `remember(content, *, importance, tags, metadata, tier, ttl)` | Store a new memory |
121
+ | `get(memory_id)` | Fetch a memory by ID |
122
+ | `update(memory_id, *, importance, tags, metadata, tier)` | Update memory fields |
123
+ | `forget(memory_id)` | Soft-delete a memory |
124
+ | `list(*, limit, offset, tier, sort_by, order)` | Paginated memory listing |
125
+ | `recall(query, *, top_k, min_score, tags)` | Semantic search |
126
+ | `attribute(query, response, memory_ids)` | Run EAS attribution |
127
+ | `recall_and_attribute(query, response, *, top_k, min_score)` | Recall + attribute |
128
+
129
+ ### Types
130
+
131
+ - **`Memory`** — `id`, `content`, `agent_id`, `tier`, `importance`, `tags`, `metadata`, `retrieval_count`, `created_at`
132
+ - **`RecallResult`** — `memory: Memory`, `score: float`
133
+ - **`Attribution`** — `transaction_id`, `query`, `response`, `scores: dict[str, float]`
134
+ - **`RecallAndAttributeResult`** — `memories: list[RecallResult]`, `attribution: Attribution`
135
+ - **`Page`** — `items: list[Memory]`, `total`, `offset`, `limit`, `has_more`
136
+
137
+ ### Errors
138
+
139
+ | Exception | When |
140
+ |-----------|------|
141
+ | `CortexError` | Base class for all SDK errors |
142
+ | `AuthError` | Invalid or missing API key (401/403) |
143
+ | `RateLimitError` | Too many requests (429); has `.retry_after` |
144
+ | `MemoryNotFoundError` | Memory ID does not exist (404) |
145
+ | `ValidationError` | Invalid request payload (422) |
146
+ | `ServerError` | Unexpected 5xx from the server |
147
+
148
+ ## Configuration
149
+
150
+ ```python
151
+ cx = Cortex(
152
+ agent_id="my-agent",
153
+ api_key="sk-...", # Optional if server has no auth
154
+ base_url="https://api.cortexa.ink",
155
+ timeout=30.0, # Per-request timeout (seconds)
156
+ max_retries=3, # Retries on 429/502/503/504
157
+ )
158
+ ```
159
+
160
+ ## Running tests
161
+
162
+ ```bash
163
+ # Unit tests (no server required)
164
+ pip install "cortexos[dev]"
165
+ pytest tests/test_client.py -v
166
+
167
+ # Integration tests (requires a running cortex-engine)
168
+ pytest tests/test_integration.py -v
169
+ ```
@@ -0,0 +1,138 @@
1
+ # CortexOS Python SDK
2
+
3
+ The official Python SDK for [Cortexa](https://cortexa.ink) — hallucination detection and verification for LLM agents.
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ pip install cortexos
9
+ ```
10
+
11
+ ```python
12
+ import cortexos
13
+
14
+ cortexos.configure(api_key="your-key")
15
+
16
+ result = cortexos.check(
17
+ response="The return window is 30 days",
18
+ sources=["Return policy: 30-day window for all items."]
19
+ )
20
+ print(f"Hallucination Index: {result.hallucination_index}")
21
+ # → Hallucination Index: 0.0
22
+ ```
23
+
24
+ Get an API key at [cortexa.ink](https://cortexa.ink)
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install cortexos
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from cortexos import Cortex
36
+
37
+ cx = Cortex(api_key="sk-...", agent_id="support-bot")
38
+
39
+ # Store a memory
40
+ mem = cx.remember(
41
+ "User prefers email over Slack for all communications",
42
+ importance=0.85,
43
+ tags=["preferences", "communication"],
44
+ metadata={"source": "conversation_123"},
45
+ )
46
+
47
+ # Semantic recall
48
+ results = cx.recall("how does the user want to be contacted?", top_k=5)
49
+ for r in results:
50
+ print(f"[{r.score:.2f}] {r.memory.content}")
51
+
52
+ # EAS attribution — score which memories contributed to your LLM response
53
+ attr = cx.attribute(
54
+ query="How should I contact the user?",
55
+ response="Based on their preferences, contact them via email.",
56
+ memory_ids=[mem.id],
57
+ )
58
+ print(attr.scores) # {"mem_xxx": 0.91}
59
+
60
+ # Combined recall + attribution in one call
61
+ result = cx.recall_and_attribute(
62
+ query="How should I reach the user?",
63
+ response="Contact via email.",
64
+ top_k=10,
65
+ )
66
+ ```
67
+
68
+ ## Async usage
69
+
70
+ ```python
71
+ import asyncio
72
+ from cortexos import AsyncCortex
73
+
74
+ async def main():
75
+ async with AsyncCortex(api_key="sk-...", agent_id="my-agent") as cx:
76
+ mem = await cx.remember("User lives in Tokyo", importance=0.7)
77
+ results = await cx.recall("where does the user live?")
78
+ await cx.forget(mem.id)
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ## API reference
84
+
85
+ ### `Cortex` / `AsyncCortex`
86
+
87
+ | Method | Description |
88
+ |--------|-------------|
89
+ | `remember(content, *, importance, tags, metadata, tier, ttl)` | Store a new memory |
90
+ | `get(memory_id)` | Fetch a memory by ID |
91
+ | `update(memory_id, *, importance, tags, metadata, tier)` | Update memory fields |
92
+ | `forget(memory_id)` | Soft-delete a memory |
93
+ | `list(*, limit, offset, tier, sort_by, order)` | Paginated memory listing |
94
+ | `recall(query, *, top_k, min_score, tags)` | Semantic search |
95
+ | `attribute(query, response, memory_ids)` | Run EAS attribution |
96
+ | `recall_and_attribute(query, response, *, top_k, min_score)` | Recall + attribute |
97
+
98
+ ### Types
99
+
100
+ - **`Memory`** — `id`, `content`, `agent_id`, `tier`, `importance`, `tags`, `metadata`, `retrieval_count`, `created_at`
101
+ - **`RecallResult`** — `memory: Memory`, `score: float`
102
+ - **`Attribution`** — `transaction_id`, `query`, `response`, `scores: dict[str, float]`
103
+ - **`RecallAndAttributeResult`** — `memories: list[RecallResult]`, `attribution: Attribution`
104
+ - **`Page`** — `items: list[Memory]`, `total`, `offset`, `limit`, `has_more`
105
+
106
+ ### Errors
107
+
108
+ | Exception | When |
109
+ |-----------|------|
110
+ | `CortexError` | Base class for all SDK errors |
111
+ | `AuthError` | Invalid or missing API key (401/403) |
112
+ | `RateLimitError` | Too many requests (429); has `.retry_after` |
113
+ | `MemoryNotFoundError` | Memory ID does not exist (404) |
114
+ | `ValidationError` | Invalid request payload (422) |
115
+ | `ServerError` | Unexpected 5xx from the server |
116
+
117
+ ## Configuration
118
+
119
+ ```python
120
+ cx = Cortex(
121
+ agent_id="my-agent",
122
+ api_key="sk-...", # Optional if server has no auth
123
+ base_url="https://api.cortexa.ink",
124
+ timeout=30.0, # Per-request timeout (seconds)
125
+ max_retries=3, # Retries on 429/502/503/504
126
+ )
127
+ ```
128
+
129
+ ## Running tests
130
+
131
+ ```bash
132
+ # Unit tests (no server required)
133
+ pip install "cortexos[dev]"
134
+ pytest tests/test_client.py -v
135
+
136
+ # Integration tests (requires a running cortex-engine)
137
+ pytest tests/test_integration.py -v
138
+ ```
@@ -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
+ ]
@@ -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)