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.
- cortexos-0.2.0/LICENSE +21 -0
- cortexos-0.2.0/MANIFEST.in +3 -0
- cortexos-0.2.0/PKG-INFO +169 -0
- cortexos-0.2.0/README.md +138 -0
- cortexos-0.2.0/cortexos/__init__.py +64 -0
- cortexos-0.2.0/cortexos/_http.py +206 -0
- cortexos-0.2.0/cortexos/async_client.py +254 -0
- cortexos-0.2.0/cortexos/client.py +393 -0
- cortexos-0.2.0/cortexos/errors.py +43 -0
- cortexos-0.2.0/cortexos/exceptions.py +54 -0
- cortexos-0.2.0/cortexos/integrations/__init__.py +6 -0
- cortexos-0.2.0/cortexos/integrations/base.py +133 -0
- cortexos-0.2.0/cortexos/integrations/mem0.py +229 -0
- cortexos-0.2.0/cortexos/integrations/supermemory.py +211 -0
- cortexos-0.2.0/cortexos/models.py +102 -0
- cortexos-0.2.0/cortexos/tui/__init__.py +1 -0
- cortexos-0.2.0/cortexos/tui/app.py +287 -0
- cortexos-0.2.0/cortexos/tui/cli.py +33 -0
- cortexos-0.2.0/cortexos/tui/commands.py +84 -0
- cortexos-0.2.0/cortexos/tui/helpers.py +58 -0
- cortexos-0.2.0/cortexos/tui/state.py +180 -0
- cortexos-0.2.0/cortexos/tui/stream.py +88 -0
- cortexos-0.2.0/cortexos/tui/styles.tcss +335 -0
- cortexos-0.2.0/cortexos/tui/tabs/__init__.py +15 -0
- cortexos-0.2.0/cortexos/tui/tabs/agents.py +112 -0
- cortexos-0.2.0/cortexos/tui/tabs/claims.py +73 -0
- cortexos-0.2.0/cortexos/tui/tabs/feed.py +70 -0
- cortexos-0.2.0/cortexos/tui/tabs/inspect.py +233 -0
- cortexos-0.2.0/cortexos/tui/tabs/memory.py +121 -0
- cortexos-0.2.0/cortexos/tui/widgets/__init__.py +17 -0
- cortexos-0.2.0/cortexos/tui/widgets/claim_table.py +106 -0
- cortexos-0.2.0/cortexos/tui/widgets/confidence_bar.py +27 -0
- cortexos-0.2.0/cortexos/tui/widgets/event_log.py +111 -0
- cortexos-0.2.0/cortexos/tui/widgets/hi_sparkline.py +23 -0
- cortexos-0.2.0/cortexos/tui/widgets/score_bar.py +21 -0
- cortexos-0.2.0/cortexos/tui/widgets/stats_panel.py +51 -0
- cortexos-0.2.0/cortexos/types.py +160 -0
- cortexos-0.2.0/cortexos/verification.py +186 -0
- cortexos-0.2.0/cortexos.egg-info/PKG-INFO +169 -0
- cortexos-0.2.0/cortexos.egg-info/SOURCES.txt +48 -0
- cortexos-0.2.0/cortexos.egg-info/dependency_links.txt +1 -0
- cortexos-0.2.0/cortexos.egg-info/entry_points.txt +2 -0
- cortexos-0.2.0/cortexos.egg-info/requires.txt +12 -0
- cortexos-0.2.0/cortexos.egg-info/top_level.txt +1 -0
- cortexos-0.2.0/pyproject.toml +55 -0
- cortexos-0.2.0/setup.cfg +4 -0
- cortexos-0.2.0/tests/test_client.py +429 -0
- cortexos-0.2.0/tests/test_integration.py +309 -0
- cortexos-0.2.0/tests/test_integrations.py +622 -0
- 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.
|
cortexos-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
cortexos-0.2.0/README.md
ADDED
|
@@ -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)
|