hebbrix 2.3.2__tar.gz → 2.4.1__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.
- {hebbrix-2.3.2 → hebbrix-2.4.1}/CHANGELOG.md +22 -0
- hebbrix-2.4.1/PKG-INFO +169 -0
- hebbrix-2.4.1/README.md +127 -0
- hebbrix-2.4.1/hebbrix/__init__.py +42 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/client.py +76 -12
- hebbrix-2.4.1/hebbrix/exceptions.py +135 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/resources.py +204 -144
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/sync_client.py +136 -26
- hebbrix-2.4.1/hebbrix.egg-info/PKG-INFO +169 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/SOURCES.txt +1 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/pyproject.toml +8 -6
- hebbrix-2.4.1/tests/test_advanced_resources.py +96 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_memories_resource.py +200 -1
- {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_openapi_parity.py +26 -0
- hebbrix-2.4.1/tests/test_sync_client.py +305 -0
- hebbrix-2.3.2/PKG-INFO +0 -205
- hebbrix-2.3.2/README.md +0 -143
- hebbrix-2.3.2/hebbrix/__init__.py +0 -74
- hebbrix-2.3.2/hebbrix/exceptions.py +0 -48
- hebbrix-2.3.2/hebbrix.egg-info/PKG-INFO +0 -205
- hebbrix-2.3.2/tests/test_sync_client.py +0 -130
- {hebbrix-2.3.2 → hebbrix-2.4.1}/LICENSE +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/MANIFEST.in +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/chat.py +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix/models.py +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/dependency_links.txt +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/requires.txt +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/hebbrix.egg-info/top_level.txt +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/setup.cfg +0 -0
- {hebbrix-2.3.2 → hebbrix-2.4.1}/tests/test_procedural_resource.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.1 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
- Make every async and sync readiness deadline preserve the original durable
|
|
6
|
+
receipt and raise top-level `IndexingTimeoutError`, including single writes,
|
|
7
|
+
batches, inference jobs, and direct updates.
|
|
8
|
+
- Normalize memory/job/status/request/outbox/idempotency recovery metadata and
|
|
9
|
+
preserve transport-only `Location`, request, retry, event, and replay headers.
|
|
10
|
+
|
|
11
|
+
## 2.4.0 — 2026-08-27
|
|
12
|
+
|
|
13
|
+
- Reconcile every exported advanced method with the canonical public OpenAPI,
|
|
14
|
+
including temporal, working-memory, consolidation, memory-tool, and RL routes.
|
|
15
|
+
- Add a route-manifest release gate and clean-wheel installation verification.
|
|
16
|
+
- Treat durable-but-indexing batch results as `202`, poll them through the SDK,
|
|
17
|
+
and raise `IndexingTimeoutError` with the durable receipt on client deadline.
|
|
18
|
+
- Preserve structured entitlement metadata in `EntitlementError`.
|
|
19
|
+
- Keep advanced resources async-only and document the sync-client boundary.
|
|
20
|
+
- Withdraw the experimental World Model from the public SDK until a trained,
|
|
21
|
+
versioned production model artifact and serving contract exist.
|
|
22
|
+
- Publish compatibility through `GET /v1/release`; replace the broken public
|
|
23
|
+
repository link with valid artifact and support links.
|
|
24
|
+
|
|
3
25
|
## 2.3.2 — 2026-08-27
|
|
4
26
|
|
|
5
27
|
- Align procedure create/list/get/update/execute/delete with the canonical
|
hebbrix-2.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hebbrix
|
|
3
|
+
Version: 2.4.1
|
|
4
|
+
Summary: Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs
|
|
5
|
+
Author-email: Hebbrix Team <support@hebbrix.com>
|
|
6
|
+
Maintainer-email: Hebbrix Team <support@hebbrix.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://hebbrix.com
|
|
9
|
+
Project-URL: Documentation, https://docs.hebbrix.com
|
|
10
|
+
Project-URL: Source, https://github.com/Hebbrix/hebbrix-python
|
|
11
|
+
Project-URL: Issues, https://github.com/Hebbrix/hebbrix-python/issues
|
|
12
|
+
Project-URL: Support, https://www.hebbrix.com/contact
|
|
13
|
+
Project-URL: API Reference, https://api.hebbrix.com/docs
|
|
14
|
+
Keywords: ai,memory,agents,llm,chatbot,assistant,ml,reinforcement-learning,knowledge-graph,vector-search,rag,temporal,procedural-memory,working-memory
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Operating System :: OS Independent
|
|
27
|
+
Classifier: Framework :: AsyncIO
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: httpx>=0.25.0
|
|
32
|
+
Requires-Dist: requests>=2.31.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
36
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: flake8<6.0.0,>=5.0.4; python_full_version < "3.8.1" and extra == "dev"
|
|
38
|
+
Requires-Dist: flake8>=6.0.0; python_full_version >= "3.8.1" and extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: cryptography>=42.0.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Hebbrix Python SDK
|
|
44
|
+
|
|
45
|
+
Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install hebbrix==2.4.1
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
|
|
54
|
+
supports the core collection, memory, search, correction, procedure, and
|
|
55
|
+
ProofLoop workflows; advanced temporal, working-memory, consolidation,
|
|
56
|
+
memory-tool, and RL resources are currently async-only.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from hebbrix import MemoryClient
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with MemoryClient(api_key="hbx_your_api_key") as client:
|
|
66
|
+
collection = await client.collections.create(name="Support memory")
|
|
67
|
+
memory = await client.memories.create(
|
|
68
|
+
collection_id=collection["id"],
|
|
69
|
+
content="Customer prefers concise replies",
|
|
70
|
+
wait_for_index=True,
|
|
71
|
+
idempotency_key="customer-42-preference-v1",
|
|
72
|
+
)
|
|
73
|
+
results = await client.search(
|
|
74
|
+
"How should replies be formatted?",
|
|
75
|
+
collection_id=collection["id"],
|
|
76
|
+
)
|
|
77
|
+
print(memory, results)
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Durable readiness
|
|
83
|
+
|
|
84
|
+
Memory writes return either a searchable completion or a durable `202` receipt.
|
|
85
|
+
A durable receipt means the database commit succeeded while indexing is still
|
|
86
|
+
converging; it is not a failure and does not justify a duplicate write.
|
|
87
|
+
|
|
88
|
+
When `wait_for_index=True`, the SDK accepts that receipt and polls the documented
|
|
89
|
+
status URL. It returns only after `searchable=true`. If the caller's deadline
|
|
90
|
+
expires, it raises `IndexingTimeoutError`; the exception retains the original
|
|
91
|
+
receipt plus normalized `memory_ids`, `job_id`, `status_url`, `request_id`,
|
|
92
|
+
`outbox_event_id`, retry timing, and idempotency replay metadata when available.
|
|
93
|
+
The synchronous and asynchronous single, batch, inference-job, and update
|
|
94
|
+
readiness paths share this behavior. The SDK never repeats the write while it
|
|
95
|
+
polls.
|
|
96
|
+
|
|
97
|
+
Catch the typed deadline without discarding the durable acceptance:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from hebbrix import IndexingTimeoutError
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
created = await client.memories.create(
|
|
104
|
+
content="Customer prefers concise replies",
|
|
105
|
+
wait_for_index=True,
|
|
106
|
+
idempotency_key="customer-42-preference-v1",
|
|
107
|
+
index_timeout=5,
|
|
108
|
+
)
|
|
109
|
+
except IndexingTimeoutError as exc:
|
|
110
|
+
# Resume observation; do not submit an unrelated second write.
|
|
111
|
+
if exc.memory_ids:
|
|
112
|
+
created = await client.memories.wait_until_searchable(exc.memory_ids[0])
|
|
113
|
+
elif exc.job_id:
|
|
114
|
+
created = await client.memory_jobs.wait(exc.job_id)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For a timed-out batch, pass `exc.receipt` to
|
|
118
|
+
`memories.wait_batch_until_searchable(...)`. Alternatively, replay the exact
|
|
119
|
+
same body with `exc.idempotency_key`; a changed body with the same key is
|
|
120
|
+
rejected by the API rather than creating a second logical write. For an update,
|
|
121
|
+
resume polling `exc.memory_ids[0]` because the relational edit already committed.
|
|
122
|
+
|
|
123
|
+
For an asynchronous batch receipt:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
receipt = await client.memories.create_batch(
|
|
127
|
+
[{"content": "First fact"}, {"content": "Second fact"}],
|
|
128
|
+
collection_id="collection-42",
|
|
129
|
+
wait_for_index=False,
|
|
130
|
+
idempotency_key="import-42",
|
|
131
|
+
)
|
|
132
|
+
completed = await client.memories.wait_batch_until_searchable(receipt)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Pagination
|
|
136
|
+
|
|
137
|
+
`collections.list()` returns the current page's collection items for backward
|
|
138
|
+
compatibility. Use `collections.list_page()` when cursor metadata is required.
|
|
139
|
+
Memory resources provide the same `list()`/`list_page()` distinction.
|
|
140
|
+
|
|
141
|
+
## Advanced capabilities and entitlements
|
|
142
|
+
|
|
143
|
+
The async client exposes the canonical `/v1` temporal, working-memory,
|
|
144
|
+
consolidation, memory-tool, and RL contracts. RL metrics and evaluation require
|
|
145
|
+
the Pro plan. Process-wide RL training and checkpoint mutation require an admin
|
|
146
|
+
role. Entitlement failures raise `EntitlementError` and preserve the stable
|
|
147
|
+
error code, current/required plan, request ID, and support action.
|
|
148
|
+
|
|
149
|
+
The experimental World Model is intentionally not exported by this public SDK.
|
|
150
|
+
It remains withdrawn until a trained, versioned production model artifact and
|
|
151
|
+
an end-to-end public serving contract are available.
|
|
152
|
+
|
|
153
|
+
The authoritative account capability matrix is available from
|
|
154
|
+
`GET /v1/users/me/capabilities`.
|
|
155
|
+
|
|
156
|
+
## Release compatibility
|
|
157
|
+
|
|
158
|
+
The production API publishes exact build and artifact compatibility at
|
|
159
|
+
[`GET /v1/release`](https://api.hebbrix.com/v1/release). The public OpenAPI is
|
|
160
|
+
[`/openapi.json`](https://api.hebbrix.com/openapi.json).
|
|
161
|
+
|
|
162
|
+
- [Documentation](https://docs.hebbrix.com)
|
|
163
|
+
- [API reference](https://api.hebbrix.com/docs)
|
|
164
|
+
- [PyPI files](https://pypi.org/project/hebbrix/#files)
|
|
165
|
+
- [Support](https://www.hebbrix.com/contact)
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT. See `LICENSE` in the distribution.
|
hebbrix-2.4.1/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Hebbrix Python SDK
|
|
2
|
+
|
|
3
|
+
Typed Python client for Hebbrix memory, retrieval, and outcome-learning APIs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hebbrix==2.4.1
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Python 3.8+ is supported. `MemoryClient` is asynchronous. `SyncMemoryClient`
|
|
12
|
+
supports the core collection, memory, search, correction, procedure, and
|
|
13
|
+
ProofLoop workflows; advanced temporal, working-memory, consolidation,
|
|
14
|
+
memory-tool, and RL resources are currently async-only.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
from hebbrix import MemoryClient
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with MemoryClient(api_key="hbx_your_api_key") as client:
|
|
24
|
+
collection = await client.collections.create(name="Support memory")
|
|
25
|
+
memory = await client.memories.create(
|
|
26
|
+
collection_id=collection["id"],
|
|
27
|
+
content="Customer prefers concise replies",
|
|
28
|
+
wait_for_index=True,
|
|
29
|
+
idempotency_key="customer-42-preference-v1",
|
|
30
|
+
)
|
|
31
|
+
results = await client.search(
|
|
32
|
+
"How should replies be formatted?",
|
|
33
|
+
collection_id=collection["id"],
|
|
34
|
+
)
|
|
35
|
+
print(memory, results)
|
|
36
|
+
|
|
37
|
+
asyncio.run(main())
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Durable readiness
|
|
41
|
+
|
|
42
|
+
Memory writes return either a searchable completion or a durable `202` receipt.
|
|
43
|
+
A durable receipt means the database commit succeeded while indexing is still
|
|
44
|
+
converging; it is not a failure and does not justify a duplicate write.
|
|
45
|
+
|
|
46
|
+
When `wait_for_index=True`, the SDK accepts that receipt and polls the documented
|
|
47
|
+
status URL. It returns only after `searchable=true`. If the caller's deadline
|
|
48
|
+
expires, it raises `IndexingTimeoutError`; the exception retains the original
|
|
49
|
+
receipt plus normalized `memory_ids`, `job_id`, `status_url`, `request_id`,
|
|
50
|
+
`outbox_event_id`, retry timing, and idempotency replay metadata when available.
|
|
51
|
+
The synchronous and asynchronous single, batch, inference-job, and update
|
|
52
|
+
readiness paths share this behavior. The SDK never repeats the write while it
|
|
53
|
+
polls.
|
|
54
|
+
|
|
55
|
+
Catch the typed deadline without discarding the durable acceptance:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from hebbrix import IndexingTimeoutError
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
created = await client.memories.create(
|
|
62
|
+
content="Customer prefers concise replies",
|
|
63
|
+
wait_for_index=True,
|
|
64
|
+
idempotency_key="customer-42-preference-v1",
|
|
65
|
+
index_timeout=5,
|
|
66
|
+
)
|
|
67
|
+
except IndexingTimeoutError as exc:
|
|
68
|
+
# Resume observation; do not submit an unrelated second write.
|
|
69
|
+
if exc.memory_ids:
|
|
70
|
+
created = await client.memories.wait_until_searchable(exc.memory_ids[0])
|
|
71
|
+
elif exc.job_id:
|
|
72
|
+
created = await client.memory_jobs.wait(exc.job_id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For a timed-out batch, pass `exc.receipt` to
|
|
76
|
+
`memories.wait_batch_until_searchable(...)`. Alternatively, replay the exact
|
|
77
|
+
same body with `exc.idempotency_key`; a changed body with the same key is
|
|
78
|
+
rejected by the API rather than creating a second logical write. For an update,
|
|
79
|
+
resume polling `exc.memory_ids[0]` because the relational edit already committed.
|
|
80
|
+
|
|
81
|
+
For an asynchronous batch receipt:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
receipt = await client.memories.create_batch(
|
|
85
|
+
[{"content": "First fact"}, {"content": "Second fact"}],
|
|
86
|
+
collection_id="collection-42",
|
|
87
|
+
wait_for_index=False,
|
|
88
|
+
idempotency_key="import-42",
|
|
89
|
+
)
|
|
90
|
+
completed = await client.memories.wait_batch_until_searchable(receipt)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Pagination
|
|
94
|
+
|
|
95
|
+
`collections.list()` returns the current page's collection items for backward
|
|
96
|
+
compatibility. Use `collections.list_page()` when cursor metadata is required.
|
|
97
|
+
Memory resources provide the same `list()`/`list_page()` distinction.
|
|
98
|
+
|
|
99
|
+
## Advanced capabilities and entitlements
|
|
100
|
+
|
|
101
|
+
The async client exposes the canonical `/v1` temporal, working-memory,
|
|
102
|
+
consolidation, memory-tool, and RL contracts. RL metrics and evaluation require
|
|
103
|
+
the Pro plan. Process-wide RL training and checkpoint mutation require an admin
|
|
104
|
+
role. Entitlement failures raise `EntitlementError` and preserve the stable
|
|
105
|
+
error code, current/required plan, request ID, and support action.
|
|
106
|
+
|
|
107
|
+
The experimental World Model is intentionally not exported by this public SDK.
|
|
108
|
+
It remains withdrawn until a trained, versioned production model artifact and
|
|
109
|
+
an end-to-end public serving contract are available.
|
|
110
|
+
|
|
111
|
+
The authoritative account capability matrix is available from
|
|
112
|
+
`GET /v1/users/me/capabilities`.
|
|
113
|
+
|
|
114
|
+
## Release compatibility
|
|
115
|
+
|
|
116
|
+
The production API publishes exact build and artifact compatibility at
|
|
117
|
+
[`GET /v1/release`](https://api.hebbrix.com/v1/release). The public OpenAPI is
|
|
118
|
+
[`/openapi.json`](https://api.hebbrix.com/openapi.json).
|
|
119
|
+
|
|
120
|
+
- [Documentation](https://docs.hebbrix.com)
|
|
121
|
+
- [API reference](https://api.hebbrix.com/docs)
|
|
122
|
+
- [PyPI files](https://pypi.org/project/hebbrix/#files)
|
|
123
|
+
- [Support](https://www.hebbrix.com/contact)
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT. See `LICENSE` in the distribution.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Typed async-first client for Hebbrix memory and outcome-learning APIs.
|
|
2
|
+
|
|
3
|
+
The public surface and its plan/role restrictions are documented by the
|
|
4
|
+
production OpenAPI and ``GET /v1/users/me/capabilities``. The experimental
|
|
5
|
+
World Model is intentionally absent from this release.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.4.1"
|
|
9
|
+
__author__ = "Hebbrix Team"
|
|
10
|
+
__license__ = "MIT"
|
|
11
|
+
|
|
12
|
+
from hebbrix.chat import MemoryChat
|
|
13
|
+
from hebbrix.client import MemoryClient
|
|
14
|
+
from hebbrix.exceptions import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
EntitlementError,
|
|
17
|
+
HebbrixError,
|
|
18
|
+
IndexingTimeoutError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
)
|
|
24
|
+
from hebbrix.models import EvidenceClaim, GroundingReceipt, SearchSafetyEnvelope
|
|
25
|
+
from hebbrix.sync_client import SyncMemoryClient
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"MemoryClient",
|
|
29
|
+
"SyncMemoryClient",
|
|
30
|
+
"MemoryChat",
|
|
31
|
+
"HebbrixError",
|
|
32
|
+
"AuthenticationError",
|
|
33
|
+
"EntitlementError",
|
|
34
|
+
"IndexingTimeoutError",
|
|
35
|
+
"ValidationError",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"RateLimitError",
|
|
38
|
+
"ServerError",
|
|
39
|
+
"GroundingReceipt",
|
|
40
|
+
"EvidenceClaim",
|
|
41
|
+
"SearchSafetyEnvelope",
|
|
42
|
+
]
|
|
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
|
|
|
10
10
|
import httpx
|
|
11
11
|
from hebbrix.exceptions import (
|
|
12
12
|
AuthenticationError,
|
|
13
|
+
EntitlementError,
|
|
13
14
|
HebbrixError,
|
|
14
15
|
NotFoundError,
|
|
15
16
|
RateLimitError,
|
|
@@ -30,7 +31,6 @@ from hebbrix.resources import (
|
|
|
30
31
|
SearchResource,
|
|
31
32
|
TemporalResource,
|
|
32
33
|
WorkingMemoryResource,
|
|
33
|
-
WorldModelResource,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
@@ -85,13 +85,12 @@ class MemoryClient:
|
|
|
85
85
|
self.working_memory = WorkingMemoryResource(self)
|
|
86
86
|
self.consolidation = ConsolidationResource(self)
|
|
87
87
|
self.memory_tools = MemoryToolsResource(self)
|
|
88
|
-
self.world_model = WorldModelResource(self)
|
|
89
88
|
|
|
90
89
|
def _get_headers(self) -> Dict[str, str]:
|
|
91
90
|
"""Get request headers."""
|
|
92
91
|
headers = {
|
|
93
92
|
"Content-Type": "application/json",
|
|
94
|
-
"User-Agent": "hebbrix-python/2.
|
|
93
|
+
"User-Agent": "hebbrix-python/2.4.1",
|
|
95
94
|
}
|
|
96
95
|
|
|
97
96
|
if self.api_key:
|
|
@@ -108,23 +107,71 @@ class MemoryClient:
|
|
|
108
107
|
|
|
109
108
|
try:
|
|
110
109
|
error_data = response.json()
|
|
111
|
-
message = error_data.get("error", {}).get("message", response.text)
|
|
112
110
|
except Exception:
|
|
113
|
-
|
|
111
|
+
error_data = {}
|
|
112
|
+
|
|
113
|
+
envelope = error_data.get("error") or error_data.get("detail") or {}
|
|
114
|
+
if not isinstance(envelope, dict):
|
|
115
|
+
envelope = {"message": str(envelope)}
|
|
116
|
+
nested = envelope.get("message")
|
|
117
|
+
details = nested if isinstance(nested, dict) else envelope
|
|
118
|
+
message = (
|
|
119
|
+
(details.get("message") if isinstance(details, dict) else None)
|
|
120
|
+
or (nested if isinstance(nested, str) else None)
|
|
121
|
+
or response.text
|
|
122
|
+
)
|
|
123
|
+
code = str(details.get("code") or envelope.get("code") or "") or None
|
|
124
|
+
request_id = (
|
|
125
|
+
details.get("request_id")
|
|
126
|
+
or envelope.get("request_id")
|
|
127
|
+
or response.headers.get("X-Request-ID")
|
|
128
|
+
)
|
|
114
129
|
|
|
115
130
|
if status_code == 401:
|
|
116
|
-
raise AuthenticationError(
|
|
131
|
+
raise AuthenticationError(
|
|
132
|
+
message, code=code, request_id=request_id, details=details
|
|
133
|
+
)
|
|
117
134
|
elif status_code == 404:
|
|
118
|
-
raise NotFoundError(
|
|
135
|
+
raise NotFoundError(
|
|
136
|
+
message, code=code, request_id=request_id, details=details
|
|
137
|
+
)
|
|
119
138
|
elif status_code == 422:
|
|
120
139
|
errors = error_data.get("error", {}).get("details", [])
|
|
121
|
-
raise ValidationError(
|
|
140
|
+
raise ValidationError(
|
|
141
|
+
message,
|
|
142
|
+
errors=errors,
|
|
143
|
+
code=code,
|
|
144
|
+
request_id=request_id,
|
|
145
|
+
details=details,
|
|
146
|
+
)
|
|
122
147
|
elif status_code == 429:
|
|
123
|
-
raise RateLimitError(
|
|
148
|
+
raise RateLimitError(
|
|
149
|
+
message, code=code, request_id=request_id, details=details
|
|
150
|
+
)
|
|
124
151
|
elif status_code >= 500:
|
|
125
|
-
raise ServerError(
|
|
152
|
+
raise ServerError(
|
|
153
|
+
message, code=code, request_id=request_id, details=details
|
|
154
|
+
)
|
|
155
|
+
elif status_code in {402, 403} and (
|
|
156
|
+
"ENTITLEMENT" in str(code or "")
|
|
157
|
+
or details.get("error")
|
|
158
|
+
in {"feature_not_available", "tier_upgrade_required"}
|
|
159
|
+
):
|
|
160
|
+
raise EntitlementError(
|
|
161
|
+
message,
|
|
162
|
+
status_code=status_code,
|
|
163
|
+
code=code,
|
|
164
|
+
request_id=request_id,
|
|
165
|
+
details=details,
|
|
166
|
+
)
|
|
126
167
|
else:
|
|
127
|
-
raise HebbrixError(
|
|
168
|
+
raise HebbrixError(
|
|
169
|
+
message,
|
|
170
|
+
status_code=status_code,
|
|
171
|
+
code=code,
|
|
172
|
+
request_id=request_id,
|
|
173
|
+
details=details,
|
|
174
|
+
)
|
|
128
175
|
|
|
129
176
|
async def request(
|
|
130
177
|
self,
|
|
@@ -153,7 +200,24 @@ class MemoryClient:
|
|
|
153
200
|
if response.status_code >= 400:
|
|
154
201
|
self._handle_error(response)
|
|
155
202
|
|
|
156
|
-
|
|
203
|
+
payload = response.json() if response.text else {}
|
|
204
|
+
if isinstance(payload, dict):
|
|
205
|
+
# Preserve transport-only recovery identifiers on durable 202
|
|
206
|
+
# receipts. The resource layer needs these values if its local
|
|
207
|
+
# readiness deadline expires after the write has committed.
|
|
208
|
+
recovery_headers = {
|
|
209
|
+
"request_id": response.headers.get("X-Request-ID"),
|
|
210
|
+
"status_url": response.headers.get("Location"),
|
|
211
|
+
"outbox_event_id": response.headers.get("X-Hebbrix-Index-Event"),
|
|
212
|
+
"retry_after": response.headers.get("Retry-After"),
|
|
213
|
+
}
|
|
214
|
+
for key, value in recovery_headers.items():
|
|
215
|
+
if value and not payload.get(key):
|
|
216
|
+
payload[key] = value
|
|
217
|
+
replay = response.headers.get("X-Idempotent-Replay")
|
|
218
|
+
if replay is not None and "idempotency_replay" not in payload:
|
|
219
|
+
payload["idempotency_replay"] = replay.casefold() == "true"
|
|
220
|
+
return payload
|
|
157
221
|
|
|
158
222
|
async def get(self, path: str, **kwargs) -> Dict[str, Any]:
|
|
159
223
|
"""Make a GET request."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hebbrix SDK Exceptions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HebbrixError(Exception):
|
|
7
|
+
"""Base exception for Hebbrix SDK."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: int = None,
|
|
13
|
+
*,
|
|
14
|
+
code: str = None,
|
|
15
|
+
request_id: str = None,
|
|
16
|
+
details: dict = None,
|
|
17
|
+
):
|
|
18
|
+
self.message = message
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.code = code
|
|
21
|
+
self.request_id = request_id
|
|
22
|
+
self.details = details or {}
|
|
23
|
+
super().__init__(self.message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EntitlementError(HebbrixError):
|
|
27
|
+
"""The authenticated account lacks the plan or role for an operation."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, *, status_code: int, **kwargs):
|
|
30
|
+
super().__init__(message, status_code=status_code, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IndexingTimeoutError(TimeoutError):
|
|
34
|
+
"""A durable write did not become searchable before the client deadline.
|
|
35
|
+
|
|
36
|
+
The write has already committed when this exception is raised. Recovery
|
|
37
|
+
metadata therefore lives on the exception so callers can resume polling or
|
|
38
|
+
safely replay the *same* request with the same idempotency key instead of
|
|
39
|
+
issuing an uncorrelated duplicate write.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
message: str,
|
|
45
|
+
receipt: dict,
|
|
46
|
+
*,
|
|
47
|
+
idempotency_key: str = None,
|
|
48
|
+
):
|
|
49
|
+
self.receipt = dict(receipt or {})
|
|
50
|
+
|
|
51
|
+
candidates = [
|
|
52
|
+
*(self.receipt.get("memory_ids") or []),
|
|
53
|
+
self.receipt.get("memory_id"),
|
|
54
|
+
self.receipt.get("id"),
|
|
55
|
+
*(
|
|
56
|
+
item.get("memory_id") or item.get("id")
|
|
57
|
+
for item in (self.receipt.get("results") or [])
|
|
58
|
+
if isinstance(item, dict)
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
self.memory_ids = list(
|
|
62
|
+
dict.fromkeys(str(value) for value in candidates if value)
|
|
63
|
+
)
|
|
64
|
+
self.job_id = self.receipt.get("job_id")
|
|
65
|
+
self.status_url = self.receipt.get("status_url")
|
|
66
|
+
if not self.status_url and self.memory_ids:
|
|
67
|
+
self.status_url = f"/v1/memories/{self.memory_ids[0]}"
|
|
68
|
+
if not self.status_url and self.job_id:
|
|
69
|
+
self.status_url = f"/v1/memory-jobs/{self.job_id}"
|
|
70
|
+
|
|
71
|
+
self.request_id = self.receipt.get("request_id")
|
|
72
|
+
self.outbox_event_id = self.receipt.get("outbox_event_id")
|
|
73
|
+
self.indexing_event_id = (
|
|
74
|
+
self.receipt.get("indexing_event_id") or self.outbox_event_id
|
|
75
|
+
)
|
|
76
|
+
self.event_id = self.receipt.get("event_id") or self.indexing_event_id
|
|
77
|
+
self.idempotency_key = idempotency_key or self.receipt.get("idempotency_key")
|
|
78
|
+
self.idempotency_replay = self.receipt.get(
|
|
79
|
+
"idempotency_replay",
|
|
80
|
+
self.receipt.get("idempotency_replayed"),
|
|
81
|
+
)
|
|
82
|
+
self.retry_after = self.receipt.get("retry_after")
|
|
83
|
+
self.recovery = {
|
|
84
|
+
key: value
|
|
85
|
+
for key, value in {
|
|
86
|
+
"memory_ids": list(self.memory_ids),
|
|
87
|
+
"job_id": self.job_id,
|
|
88
|
+
"status_url": self.status_url,
|
|
89
|
+
"request_id": self.request_id,
|
|
90
|
+
"outbox_event_id": self.outbox_event_id,
|
|
91
|
+
"indexing_event_id": self.indexing_event_id,
|
|
92
|
+
"event_id": self.event_id,
|
|
93
|
+
"idempotency_key": self.idempotency_key,
|
|
94
|
+
"idempotency_replay": self.idempotency_replay,
|
|
95
|
+
"retry_after": self.retry_after,
|
|
96
|
+
}.items()
|
|
97
|
+
if value not in (None, "", [])
|
|
98
|
+
}
|
|
99
|
+
super().__init__(message)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AuthenticationError(HebbrixError):
|
|
103
|
+
"""Raised when authentication fails."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, message: str = "Authentication failed", **kwargs):
|
|
106
|
+
super().__init__(message, status_code=401, **kwargs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ValidationError(HebbrixError):
|
|
110
|
+
"""Raised when request validation fails."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, message: str, errors: list = None, **kwargs):
|
|
113
|
+
self.errors = errors or []
|
|
114
|
+
super().__init__(message, status_code=422, **kwargs)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class NotFoundError(HebbrixError):
|
|
118
|
+
"""Raised when a resource is not found."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, message: str = "Resource not found", **kwargs):
|
|
121
|
+
super().__init__(message, status_code=404, **kwargs)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RateLimitError(HebbrixError):
|
|
125
|
+
"""Raised when rate limit is exceeded."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, message: str = "Rate limit exceeded", **kwargs):
|
|
128
|
+
super().__init__(message, status_code=429, **kwargs)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ServerError(HebbrixError):
|
|
132
|
+
"""Raised when server returns 5xx error."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, message: str = "Internal server error", **kwargs):
|
|
135
|
+
super().__init__(message, status_code=500, **kwargs)
|