outhad-contextkit-sdk 1.0.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.
- outhad_contextkit_sdk-1.0.0/.gitignore +196 -0
- outhad_contextkit_sdk-1.0.0/LICENSE +17 -0
- outhad_contextkit_sdk-1.0.0/PKG-INFO +129 -0
- outhad_contextkit_sdk-1.0.0/README.md +98 -0
- outhad_contextkit_sdk-1.0.0/outhad_contextkit_sdk/__init__.py +44 -0
- outhad_contextkit_sdk-1.0.0/outhad_contextkit_sdk/client/__init__.py +18 -0
- outhad_contextkit_sdk-1.0.0/outhad_contextkit_sdk/client/errors.py +36 -0
- outhad_contextkit_sdk-1.0.0/outhad_contextkit_sdk/client/main.py +496 -0
- outhad_contextkit_sdk-1.0.0/pyproject.toml +66 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
**/node_modules/
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
venv_test_clap/
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
cover/
|
|
55
|
+
|
|
56
|
+
# Translations
|
|
57
|
+
*.mo
|
|
58
|
+
*.pot
|
|
59
|
+
|
|
60
|
+
# Django stuff:
|
|
61
|
+
*.log
|
|
62
|
+
local_settings.py
|
|
63
|
+
db.sqlite3
|
|
64
|
+
db.sqlite3-journal
|
|
65
|
+
|
|
66
|
+
# Flask stuff:
|
|
67
|
+
instance/
|
|
68
|
+
.webassets-cache
|
|
69
|
+
|
|
70
|
+
# Scrapy stuff:
|
|
71
|
+
.scrapy
|
|
72
|
+
|
|
73
|
+
# Sphinx documentation
|
|
74
|
+
docs/_build/
|
|
75
|
+
|
|
76
|
+
# PyBuilder
|
|
77
|
+
.pybuilder/
|
|
78
|
+
target/
|
|
79
|
+
|
|
80
|
+
# Jupyter Notebook
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# poetry
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
+
#poetry.lock
|
|
104
|
+
|
|
105
|
+
# pdm
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
+
#pdm.lock
|
|
108
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended not to include it
|
|
109
|
+
# in version control.
|
|
110
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
111
|
+
.pdm.toml
|
|
112
|
+
|
|
113
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
114
|
+
__pypackages__/
|
|
115
|
+
|
|
116
|
+
# Celery stuff
|
|
117
|
+
celerybeat-schedule
|
|
118
|
+
celerybeat.pid
|
|
119
|
+
|
|
120
|
+
# SageMath parsed files
|
|
121
|
+
*.sage.py
|
|
122
|
+
|
|
123
|
+
# Environments
|
|
124
|
+
.env
|
|
125
|
+
.venv
|
|
126
|
+
env/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
env.bak/
|
|
130
|
+
venv.bak/
|
|
131
|
+
pyenv/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
163
|
+
|
|
164
|
+
.ideas.md
|
|
165
|
+
.todos.md
|
|
166
|
+
|
|
167
|
+
# Database
|
|
168
|
+
db
|
|
169
|
+
test-db
|
|
170
|
+
!embedchain/embedchain/core/db/
|
|
171
|
+
|
|
172
|
+
.vscode
|
|
173
|
+
.idea/
|
|
174
|
+
|
|
175
|
+
.DS_Store
|
|
176
|
+
|
|
177
|
+
notebooks/*.yaml
|
|
178
|
+
.ipynb_checkpoints/
|
|
179
|
+
|
|
180
|
+
!configs/*.yaml
|
|
181
|
+
|
|
182
|
+
# cache db
|
|
183
|
+
*.db
|
|
184
|
+
|
|
185
|
+
# local directories for testing
|
|
186
|
+
eval/
|
|
187
|
+
qdrant_storage/
|
|
188
|
+
.crossnote
|
|
189
|
+
testing.ipynb
|
|
190
|
+
#CHUNKING_AND_RETRIEVAL
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# Backup documentation and test data
|
|
194
|
+
test_dataset/data/
|
|
195
|
+
|
|
196
|
+
Readme2.md
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Outhad
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: outhad-contextkit-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Thin Python client for the Outhad ContextKit cloud — paywalled by API key minted at https://contextkit.outhad.com
|
|
5
|
+
Project-URL: Homepage, https://contextkit.outhad.com
|
|
6
|
+
Project-URL: Documentation, https://contextkit.outhad.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/outhad/contextkit
|
|
8
|
+
Project-URL: Issues, https://github.com/outhad/contextkit/issues
|
|
9
|
+
Author-email: Outhad <idrisitanzil@gmail.com>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,contextkit,long-term-memory,memory,outhad,sdk
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: typing-extensions>=4.8.0; python_version < '3.11'
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23.7; extra == 'test'
|
|
28
|
+
Requires-Dist: pytest>=8.2.2; extra == 'test'
|
|
29
|
+
Requires-Dist: respx>=0.21.0; extra == 'test'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# outhad-contextkit-sdk
|
|
33
|
+
|
|
34
|
+
Thin Python client for the **Outhad ContextKit** cloud — long-term memory
|
|
35
|
+
for AI agents, with multimodal + temporal-causal recall, personalised
|
|
36
|
+
retrieval, and tenant isolation built in.
|
|
37
|
+
|
|
38
|
+
This package ships **client only**. There is no local memory engine, no
|
|
39
|
+
vector store driver, no LLM provider. Calls go to the Outhad ContextKit
|
|
40
|
+
cloud where the heavy lifting happens — billed against the API key you
|
|
41
|
+
mint at https://contextkit.outhad.com.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install outhad-contextkit-sdk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from outhad_contextkit_sdk import MemoryClient
|
|
53
|
+
|
|
54
|
+
client = MemoryClient(api_key="ock_live_...") # or set OUTHAD_CONTEXTKIT_API_KEY
|
|
55
|
+
|
|
56
|
+
client.add(
|
|
57
|
+
messages=[
|
|
58
|
+
{"role": "user", "content": "Hi, my name is Alice and I love sushi."},
|
|
59
|
+
],
|
|
60
|
+
user_id="alice",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
hits = client.search(query="What food does Alice like?", user_id="alice")
|
|
64
|
+
for hit in hits["results"]:
|
|
65
|
+
print(hit["score"], hit["memory"])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Get an API key
|
|
69
|
+
|
|
70
|
+
1. Sign up at <https://contextkit.outhad.com>.
|
|
71
|
+
2. Open the dashboard → **API keys**.
|
|
72
|
+
3. Click **Create key**, copy the one-shot secret (looks like
|
|
73
|
+
`ock_live_<8-char-prefix>_<32-byte-secret>`).
|
|
74
|
+
4. Either pass it as `MemoryClient(api_key=…)` or export it in your
|
|
75
|
+
shell as `OUTHAD_CONTEXTKIT_API_KEY`.
|
|
76
|
+
|
|
77
|
+
## Auth header
|
|
78
|
+
|
|
79
|
+
The SDK sends `Authorization: Token <api-key>` on every request. The
|
|
80
|
+
cloud dispatcher also accepts `Bearer` and `X-API-Key` if you prefer to
|
|
81
|
+
hit the HTTP API directly (e.g. from `curl`).
|
|
82
|
+
|
|
83
|
+
## Host resolution
|
|
84
|
+
|
|
85
|
+
| Resolution order | Default |
|
|
86
|
+
|------------------|---------|
|
|
87
|
+
| `host=` constructor arg | (none) |
|
|
88
|
+
| `OUTHAD_CONTEXTKIT_HOST` env var | (none) |
|
|
89
|
+
| Production default | `https://contextkit.outhad.com` |
|
|
90
|
+
| Dev fallback (transparent retry on connection error) | `http://localhost:8000` |
|
|
91
|
+
|
|
92
|
+
When the production default is unreachable (DNS failure / connection
|
|
93
|
+
refused) the client transparently retries against
|
|
94
|
+
`http://localhost:8000` and pins itself there for the rest of the
|
|
95
|
+
session — useful while you are working against `make cloud-up` locally.
|
|
96
|
+
The fallback is disabled when you pass `host=` or set
|
|
97
|
+
`OUTHAD_CONTEXTKIT_HOST` explicitly.
|
|
98
|
+
|
|
99
|
+
## Async
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from outhad_contextkit_sdk import AsyncMemoryClient
|
|
103
|
+
|
|
104
|
+
async def main():
|
|
105
|
+
async with AsyncMemoryClient(api_key="ock_live_...") as client:
|
|
106
|
+
await client.add(
|
|
107
|
+
messages=[{"role": "user", "content": "I prefer dark mode."}],
|
|
108
|
+
user_id="alice",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API surface
|
|
113
|
+
|
|
114
|
+
* `add(messages, *, user_id, agent_id, run_id, metadata, infer)`
|
|
115
|
+
* `get_all(*, user_id, agent_id, run_id, limit)`
|
|
116
|
+
* `get(memory_id)`
|
|
117
|
+
* `search(query, *, user_id, agent_id, run_id, filters, limit)`
|
|
118
|
+
* `update(memory_id, *, text)`
|
|
119
|
+
* `history(memory_id)`
|
|
120
|
+
* `delete(memory_id)`
|
|
121
|
+
* `delete_all(*, user_id, agent_id, run_id)`
|
|
122
|
+
|
|
123
|
+
For dashboard-style operations the same client also exposes
|
|
124
|
+
`list_api_keys()`, `create_api_key()`, `revoke_api_key()` — those
|
|
125
|
+
require a Clerk session JWT and are intended for interactive tooling.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
Apache-2.0.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# outhad-contextkit-sdk
|
|
2
|
+
|
|
3
|
+
Thin Python client for the **Outhad ContextKit** cloud — long-term memory
|
|
4
|
+
for AI agents, with multimodal + temporal-causal recall, personalised
|
|
5
|
+
retrieval, and tenant isolation built in.
|
|
6
|
+
|
|
7
|
+
This package ships **client only**. There is no local memory engine, no
|
|
8
|
+
vector store driver, no LLM provider. Calls go to the Outhad ContextKit
|
|
9
|
+
cloud where the heavy lifting happens — billed against the API key you
|
|
10
|
+
mint at https://contextkit.outhad.com.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install outhad-contextkit-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from outhad_contextkit_sdk import MemoryClient
|
|
22
|
+
|
|
23
|
+
client = MemoryClient(api_key="ock_live_...") # or set OUTHAD_CONTEXTKIT_API_KEY
|
|
24
|
+
|
|
25
|
+
client.add(
|
|
26
|
+
messages=[
|
|
27
|
+
{"role": "user", "content": "Hi, my name is Alice and I love sushi."},
|
|
28
|
+
],
|
|
29
|
+
user_id="alice",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
hits = client.search(query="What food does Alice like?", user_id="alice")
|
|
33
|
+
for hit in hits["results"]:
|
|
34
|
+
print(hit["score"], hit["memory"])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Get an API key
|
|
38
|
+
|
|
39
|
+
1. Sign up at <https://contextkit.outhad.com>.
|
|
40
|
+
2. Open the dashboard → **API keys**.
|
|
41
|
+
3. Click **Create key**, copy the one-shot secret (looks like
|
|
42
|
+
`ock_live_<8-char-prefix>_<32-byte-secret>`).
|
|
43
|
+
4. Either pass it as `MemoryClient(api_key=…)` or export it in your
|
|
44
|
+
shell as `OUTHAD_CONTEXTKIT_API_KEY`.
|
|
45
|
+
|
|
46
|
+
## Auth header
|
|
47
|
+
|
|
48
|
+
The SDK sends `Authorization: Token <api-key>` on every request. The
|
|
49
|
+
cloud dispatcher also accepts `Bearer` and `X-API-Key` if you prefer to
|
|
50
|
+
hit the HTTP API directly (e.g. from `curl`).
|
|
51
|
+
|
|
52
|
+
## Host resolution
|
|
53
|
+
|
|
54
|
+
| Resolution order | Default |
|
|
55
|
+
|------------------|---------|
|
|
56
|
+
| `host=` constructor arg | (none) |
|
|
57
|
+
| `OUTHAD_CONTEXTKIT_HOST` env var | (none) |
|
|
58
|
+
| Production default | `https://contextkit.outhad.com` |
|
|
59
|
+
| Dev fallback (transparent retry on connection error) | `http://localhost:8000` |
|
|
60
|
+
|
|
61
|
+
When the production default is unreachable (DNS failure / connection
|
|
62
|
+
refused) the client transparently retries against
|
|
63
|
+
`http://localhost:8000` and pins itself there for the rest of the
|
|
64
|
+
session — useful while you are working against `make cloud-up` locally.
|
|
65
|
+
The fallback is disabled when you pass `host=` or set
|
|
66
|
+
`OUTHAD_CONTEXTKIT_HOST` explicitly.
|
|
67
|
+
|
|
68
|
+
## Async
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from outhad_contextkit_sdk import AsyncMemoryClient
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
async with AsyncMemoryClient(api_key="ock_live_...") as client:
|
|
75
|
+
await client.add(
|
|
76
|
+
messages=[{"role": "user", "content": "I prefer dark mode."}],
|
|
77
|
+
user_id="alice",
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## API surface
|
|
82
|
+
|
|
83
|
+
* `add(messages, *, user_id, agent_id, run_id, metadata, infer)`
|
|
84
|
+
* `get_all(*, user_id, agent_id, run_id, limit)`
|
|
85
|
+
* `get(memory_id)`
|
|
86
|
+
* `search(query, *, user_id, agent_id, run_id, filters, limit)`
|
|
87
|
+
* `update(memory_id, *, text)`
|
|
88
|
+
* `history(memory_id)`
|
|
89
|
+
* `delete(memory_id)`
|
|
90
|
+
* `delete_all(*, user_id, agent_id, run_id)`
|
|
91
|
+
|
|
92
|
+
For dashboard-style operations the same client also exposes
|
|
93
|
+
`list_api_keys()`, `create_api_key()`, `revoke_api_key()` — those
|
|
94
|
+
require a Clerk session JWT and are intended for interactive tooling.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
Apache-2.0.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Outhad ContextKit — thin Python SDK.
|
|
2
|
+
|
|
3
|
+
This distribution intentionally ships **only** the cloud HTTP client.
|
|
4
|
+
There is no local memory engine, no vector store driver, no LLM
|
|
5
|
+
provider — those live in the upstream ``outhad_contextkit`` library
|
|
6
|
+
which only operators of the cloud platform need.
|
|
7
|
+
|
|
8
|
+
End-user flow:
|
|
9
|
+
|
|
10
|
+
1. Sign up at https://contextkit.outhad.com.
|
|
11
|
+
2. Mint an API key in the dashboard.
|
|
12
|
+
3. ``pip install outhad-contextkit-sdk``
|
|
13
|
+
4. ``from outhad_contextkit_sdk import MemoryClient``
|
|
14
|
+
|
|
15
|
+
Example::
|
|
16
|
+
|
|
17
|
+
from outhad_contextkit_sdk import MemoryClient
|
|
18
|
+
|
|
19
|
+
client = MemoryClient(api_key="ock_live_...") # picks up env var if omitted
|
|
20
|
+
client.add(
|
|
21
|
+
messages=[{"role": "user", "content": "I love sushi"}],
|
|
22
|
+
user_id="alice",
|
|
23
|
+
)
|
|
24
|
+
hits = client.search(query="What food do I like?", user_id="alice")
|
|
25
|
+
"""
|
|
26
|
+
from outhad_contextkit_sdk.client.errors import APIError, AuthenticationError
|
|
27
|
+
from outhad_contextkit_sdk.client.main import (
|
|
28
|
+
DEFAULT_HOST,
|
|
29
|
+
DEV_HOST,
|
|
30
|
+
AsyncMemoryClient,
|
|
31
|
+
MemoryClient,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__version__ = "1.0.0"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"APIError",
|
|
38
|
+
"AsyncMemoryClient",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"DEFAULT_HOST",
|
|
41
|
+
"DEV_HOST",
|
|
42
|
+
"MemoryClient",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Public client surface for ``outhad_contextkit_sdk``."""
|
|
2
|
+
|
|
3
|
+
from outhad_contextkit_sdk.client.errors import APIError, AuthenticationError
|
|
4
|
+
from outhad_contextkit_sdk.client.main import (
|
|
5
|
+
DEFAULT_HOST,
|
|
6
|
+
DEV_HOST,
|
|
7
|
+
AsyncMemoryClient,
|
|
8
|
+
MemoryClient,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"APIError",
|
|
13
|
+
"AsyncMemoryClient",
|
|
14
|
+
"AuthenticationError",
|
|
15
|
+
"DEFAULT_HOST",
|
|
16
|
+
"DEV_HOST",
|
|
17
|
+
"MemoryClient",
|
|
18
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Typed exceptions raised by the SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class APIError(Exception):
|
|
8
|
+
"""Raised on any non-2xx response from the cloud backend.
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
status: HTTP status code returned by the server.
|
|
12
|
+
body: Decoded JSON / text body, when available.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
status: Optional[int] = None,
|
|
20
|
+
body: Any = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.status = status
|
|
24
|
+
self.body = body
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str: # pragma: no cover - cosmetic
|
|
27
|
+
if self.status is None:
|
|
28
|
+
return super().__str__()
|
|
29
|
+
return f"[{self.status}] {super().__str__()}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthenticationError(APIError):
|
|
33
|
+
"""Raised when the API key is missing, malformed, revoked or expired."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["APIError", "AuthenticationError"]
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Thin Python client for the Outhad ContextKit cloud.
|
|
2
|
+
|
|
3
|
+
This module is the **only** public surface of ``outhad-contextkit-sdk``.
|
|
4
|
+
It speaks HTTP to the cloud backend and never imports any model
|
|
5
|
+
provider, vector store driver, or local memory engine. All authenticated
|
|
6
|
+
calls require an ``ock_live_…`` / ``ock_test_…`` API key minted at
|
|
7
|
+
https://contextkit.outhad.com/dashboard/api-keys.
|
|
8
|
+
|
|
9
|
+
Host resolution
|
|
10
|
+
---------------
|
|
11
|
+
1. Explicit ``host=`` constructor arg, when provided.
|
|
12
|
+
2. ``OUTHAD_CONTEXTKIT_HOST`` env var, when set.
|
|
13
|
+
3. Production default (``DEFAULT_HOST = "https://contextkit.outhad.com"``).
|
|
14
|
+
4. On a connection error against the production default the client
|
|
15
|
+
transparently retries against the local-dev fallback
|
|
16
|
+
(``DEV_HOST = "http://localhost:8000"``) and pins itself there for
|
|
17
|
+
the rest of the session — useful while the production deployment
|
|
18
|
+
is still spinning up. The fallback is only triggered when the
|
|
19
|
+
default URL is in use; explicit ``host=`` / env wins.
|
|
20
|
+
|
|
21
|
+
Auth header
|
|
22
|
+
-----------
|
|
23
|
+
``Authorization: Token <api_key>`` — the format the cloud dispatcher
|
|
24
|
+
already accepts (mem0-compat). Set once on the underlying httpx client
|
|
25
|
+
so every request carries it.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
import httpx
|
|
34
|
+
|
|
35
|
+
from outhad_contextkit_sdk.client.errors import APIError, AuthenticationError
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Production cloud host. Override via ``host=`` arg or
|
|
40
|
+
# ``OUTHAD_CONTEXTKIT_HOST`` env var. Pinned to the public dashboard
|
|
41
|
+
# domain so the SDK works out of the box for paying customers.
|
|
42
|
+
DEFAULT_HOST = "https://contextkit.outhad.com"
|
|
43
|
+
|
|
44
|
+
# Local dev host used by ``server/docker-compose.cloud.yaml``. The
|
|
45
|
+
# client falls back to this transparently when the default URL is
|
|
46
|
+
# unreachable (DNS failure / connection refused) — see the docstring.
|
|
47
|
+
DEV_HOST = "http://localhost:8000"
|
|
48
|
+
|
|
49
|
+
# How long a single HTTP call may take, in seconds. Generous because
|
|
50
|
+
# memory operations include OpenAI fact extraction on the server side.
|
|
51
|
+
DEFAULT_TIMEOUT = 30.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _select_host(explicit: Optional[str]) -> str:
|
|
55
|
+
if explicit:
|
|
56
|
+
return explicit.rstrip("/")
|
|
57
|
+
env = os.environ.get("OUTHAD_CONTEXTKIT_HOST")
|
|
58
|
+
if env:
|
|
59
|
+
return env.rstrip("/")
|
|
60
|
+
return DEFAULT_HOST
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _decode_error(resp: httpx.Response) -> APIError:
|
|
64
|
+
try:
|
|
65
|
+
body = resp.json()
|
|
66
|
+
msg = (
|
|
67
|
+
body.get("detail")
|
|
68
|
+
if isinstance(body, dict) and "detail" in body
|
|
69
|
+
else str(body)
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
body = resp.text
|
|
73
|
+
msg = body
|
|
74
|
+
if resp.status_code == 401:
|
|
75
|
+
return AuthenticationError(msg, status=resp.status_code, body=body)
|
|
76
|
+
return APIError(msg, status=resp.status_code, body=body)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_connection_failure(exc: BaseException) -> bool:
|
|
80
|
+
"""Heuristic — whether ``exc`` looks like the prod URL being down.
|
|
81
|
+
|
|
82
|
+
httpx raises ``httpx.ConnectError`` for DNS / connection-refused, and
|
|
83
|
+
``httpx.ReadTimeout`` when the server hangs. We treat both as
|
|
84
|
+
"unreachable" and trigger the dev-host fallback.
|
|
85
|
+
"""
|
|
86
|
+
return isinstance(
|
|
87
|
+
exc,
|
|
88
|
+
(
|
|
89
|
+
httpx.ConnectError,
|
|
90
|
+
httpx.ConnectTimeout,
|
|
91
|
+
httpx.ReadTimeout,
|
|
92
|
+
httpx.RemoteProtocolError,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
# Sync client
|
|
99
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MemoryClient:
|
|
103
|
+
"""Synchronous client for the Outhad ContextKit cloud.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
api_key: ``ock_live_…`` or ``ock_test_…`` token minted at the
|
|
107
|
+
dashboard. Falls back to the ``OUTHAD_CONTEXTKIT_API_KEY``
|
|
108
|
+
env var when omitted; raises :class:`AuthenticationError`
|
|
109
|
+
when neither is provided.
|
|
110
|
+
host: Override the cloud host. Defaults to
|
|
111
|
+
:data:`DEFAULT_HOST` (production). Pass ``DEV_HOST`` for a
|
|
112
|
+
local backend.
|
|
113
|
+
timeout: Per-request timeout in seconds.
|
|
114
|
+
client: Pre-built ``httpx.Client``. When supplied,
|
|
115
|
+
``base_url`` and ``Authorization`` headers are overwritten
|
|
116
|
+
so the rest of the SDK works as-expected.
|
|
117
|
+
allow_dev_fallback: When True (default), a connection failure
|
|
118
|
+
against the production default URL retries once against
|
|
119
|
+
``DEV_HOST`` and pins the client to it for the session.
|
|
120
|
+
Disabled automatically when the host was set explicitly.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
api_key: Optional[str] = None,
|
|
126
|
+
*,
|
|
127
|
+
host: Optional[str] = None,
|
|
128
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
129
|
+
client: Optional[httpx.Client] = None,
|
|
130
|
+
allow_dev_fallback: bool = True,
|
|
131
|
+
) -> None:
|
|
132
|
+
self.api_key = api_key or os.environ.get("OUTHAD_CONTEXTKIT_API_KEY")
|
|
133
|
+
if not self.api_key:
|
|
134
|
+
raise AuthenticationError(
|
|
135
|
+
"Outhad ContextKit API key not provided. Pass api_key=… or "
|
|
136
|
+
"set OUTHAD_CONTEXTKIT_API_KEY in your environment. "
|
|
137
|
+
"Mint a key at https://contextkit.outhad.com/dashboard/api-keys.",
|
|
138
|
+
status=401,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
explicit_host = host is not None or "OUTHAD_CONTEXTKIT_HOST" in os.environ
|
|
142
|
+
self.host = _select_host(host)
|
|
143
|
+
self.timeout = timeout
|
|
144
|
+
self._allow_fallback = allow_dev_fallback and not explicit_host
|
|
145
|
+
self._fell_back = False
|
|
146
|
+
self._owns_client = client is None
|
|
147
|
+
|
|
148
|
+
if client is None:
|
|
149
|
+
self._client = httpx.Client(
|
|
150
|
+
base_url=self.host,
|
|
151
|
+
timeout=self.timeout,
|
|
152
|
+
headers=self._auth_headers(),
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
self._client = client
|
|
156
|
+
self._client.base_url = httpx.URL(self.host)
|
|
157
|
+
self._client.headers.update(self._auth_headers())
|
|
158
|
+
|
|
159
|
+
# Surface identity on construction (and trigger dev fallback
|
|
160
|
+
# when the production default is unreachable).
|
|
161
|
+
self.identity: Dict[str, Any] = self._validate()
|
|
162
|
+
self.user_email = self.identity.get("user_email")
|
|
163
|
+
self.org_id = self.identity.get("org_id")
|
|
164
|
+
self.project_id = self.identity.get("project_id")
|
|
165
|
+
|
|
166
|
+
# ----- private --------------------------------------------------------
|
|
167
|
+
def _auth_headers(self) -> Dict[str, str]:
|
|
168
|
+
return {
|
|
169
|
+
"Authorization": f"Token {self.api_key}",
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"User-Agent": "outhad-contextkit-sdk-python/1.0.0",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def _retarget(self, host: str) -> None:
|
|
175
|
+
self.host = host.rstrip("/")
|
|
176
|
+
self._client.base_url = httpx.URL(self.host)
|
|
177
|
+
self._fell_back = True
|
|
178
|
+
|
|
179
|
+
def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
180
|
+
try:
|
|
181
|
+
resp = self._client.request(method, path, **kwargs)
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
if (
|
|
184
|
+
self._allow_fallback
|
|
185
|
+
and not self._fell_back
|
|
186
|
+
and self.host == DEFAULT_HOST
|
|
187
|
+
and _is_connection_failure(exc)
|
|
188
|
+
):
|
|
189
|
+
logger.warning(
|
|
190
|
+
"Production host %s unreachable (%s) — retrying against %s",
|
|
191
|
+
DEFAULT_HOST,
|
|
192
|
+
exc.__class__.__name__,
|
|
193
|
+
DEV_HOST,
|
|
194
|
+
)
|
|
195
|
+
self._retarget(DEV_HOST)
|
|
196
|
+
return self._client.request(method, path, **kwargs)
|
|
197
|
+
raise
|
|
198
|
+
return resp
|
|
199
|
+
|
|
200
|
+
def _json(self, method: str, path: str, **kwargs) -> Any:
|
|
201
|
+
resp = self._request(method, path, **kwargs)
|
|
202
|
+
if not resp.is_success:
|
|
203
|
+
raise _decode_error(resp)
|
|
204
|
+
if resp.status_code == 204 or not resp.content:
|
|
205
|
+
return None
|
|
206
|
+
return resp.json()
|
|
207
|
+
|
|
208
|
+
def _validate(self) -> Dict[str, Any]:
|
|
209
|
+
# ``GET /v1/ping/`` returns identity when a valid API key is
|
|
210
|
+
# supplied — that is the canonical handshake the cloud backend
|
|
211
|
+
# implements.
|
|
212
|
+
try:
|
|
213
|
+
resp = self._request("GET", "/v1/ping/")
|
|
214
|
+
except Exception:
|
|
215
|
+
raise
|
|
216
|
+
if resp.status_code == 401:
|
|
217
|
+
raise AuthenticationError(
|
|
218
|
+
"API key rejected — verify it on the dashboard.",
|
|
219
|
+
status=401,
|
|
220
|
+
body=resp.text,
|
|
221
|
+
)
|
|
222
|
+
if not resp.is_success:
|
|
223
|
+
raise APIError(
|
|
224
|
+
f"Failed to reach Outhad ContextKit cloud at {self.host}",
|
|
225
|
+
status=resp.status_code,
|
|
226
|
+
body=resp.text,
|
|
227
|
+
)
|
|
228
|
+
try:
|
|
229
|
+
data = resp.json()
|
|
230
|
+
except Exception:
|
|
231
|
+
data = {}
|
|
232
|
+
if isinstance(data, dict) and data.get("auth") == "invalid_api_key":
|
|
233
|
+
raise AuthenticationError(
|
|
234
|
+
"API key rejected by the cloud.",
|
|
235
|
+
status=401,
|
|
236
|
+
body=data,
|
|
237
|
+
)
|
|
238
|
+
return data if isinstance(data, dict) else {}
|
|
239
|
+
|
|
240
|
+
# ----- memory CRUD ----------------------------------------------------
|
|
241
|
+
def add(
|
|
242
|
+
self,
|
|
243
|
+
messages: List[Dict[str, str]],
|
|
244
|
+
*,
|
|
245
|
+
user_id: Optional[str] = None,
|
|
246
|
+
agent_id: Optional[str] = None,
|
|
247
|
+
run_id: Optional[str] = None,
|
|
248
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
249
|
+
infer: bool = True,
|
|
250
|
+
) -> Any:
|
|
251
|
+
payload = {
|
|
252
|
+
"messages": messages,
|
|
253
|
+
"user_id": user_id,
|
|
254
|
+
"agent_id": agent_id,
|
|
255
|
+
"run_id": run_id,
|
|
256
|
+
"metadata": metadata,
|
|
257
|
+
"infer": infer,
|
|
258
|
+
}
|
|
259
|
+
return self._json("POST", "/v1/memories/", json=payload)
|
|
260
|
+
|
|
261
|
+
def get_all(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
user_id: Optional[str] = None,
|
|
265
|
+
agent_id: Optional[str] = None,
|
|
266
|
+
run_id: Optional[str] = None,
|
|
267
|
+
limit: int = 100,
|
|
268
|
+
) -> Any:
|
|
269
|
+
params = {
|
|
270
|
+
k: v
|
|
271
|
+
for k, v in {
|
|
272
|
+
"user_id": user_id,
|
|
273
|
+
"agent_id": agent_id,
|
|
274
|
+
"run_id": run_id,
|
|
275
|
+
"limit": limit,
|
|
276
|
+
}.items()
|
|
277
|
+
if v is not None
|
|
278
|
+
}
|
|
279
|
+
return self._json("GET", "/v1/memories/", params=params)
|
|
280
|
+
|
|
281
|
+
def get(self, memory_id: str) -> Any:
|
|
282
|
+
return self._json("GET", f"/v1/memories/{memory_id}/")
|
|
283
|
+
|
|
284
|
+
def search(
|
|
285
|
+
self,
|
|
286
|
+
query: str,
|
|
287
|
+
*,
|
|
288
|
+
user_id: Optional[str] = None,
|
|
289
|
+
agent_id: Optional[str] = None,
|
|
290
|
+
run_id: Optional[str] = None,
|
|
291
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
292
|
+
limit: int = 10,
|
|
293
|
+
) -> Any:
|
|
294
|
+
payload = {
|
|
295
|
+
"query": query,
|
|
296
|
+
"user_id": user_id,
|
|
297
|
+
"agent_id": agent_id,
|
|
298
|
+
"run_id": run_id,
|
|
299
|
+
"filters": filters,
|
|
300
|
+
"limit": limit,
|
|
301
|
+
}
|
|
302
|
+
return self._json("POST", "/v1/memories/search/", json=payload)
|
|
303
|
+
|
|
304
|
+
def update(self, memory_id: str, *, text: Optional[str] = None) -> Any:
|
|
305
|
+
if text is None:
|
|
306
|
+
raise ValueError("update(): provide text=...")
|
|
307
|
+
return self._json("PUT", f"/v1/memories/{memory_id}/", json={"text": text})
|
|
308
|
+
|
|
309
|
+
def history(self, memory_id: str) -> Any:
|
|
310
|
+
return self._json("GET", f"/v1/memories/{memory_id}/history/")
|
|
311
|
+
|
|
312
|
+
def delete(self, memory_id: str) -> Any:
|
|
313
|
+
return self._json("DELETE", f"/v1/memories/{memory_id}/")
|
|
314
|
+
|
|
315
|
+
def delete_all(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
user_id: Optional[str] = None,
|
|
319
|
+
agent_id: Optional[str] = None,
|
|
320
|
+
run_id: Optional[str] = None,
|
|
321
|
+
) -> Any:
|
|
322
|
+
params = {
|
|
323
|
+
k: v
|
|
324
|
+
for k, v in {
|
|
325
|
+
"user_id": user_id,
|
|
326
|
+
"agent_id": agent_id,
|
|
327
|
+
"run_id": run_id,
|
|
328
|
+
}.items()
|
|
329
|
+
if v is not None
|
|
330
|
+
}
|
|
331
|
+
return self._json("DELETE", "/v1/memories/", params=params)
|
|
332
|
+
|
|
333
|
+
# ----- API-key management (Clerk JWT only — for dashboard parity) ----
|
|
334
|
+
def list_api_keys(self) -> Any:
|
|
335
|
+
return self._json("GET", "/v1/api-keys")
|
|
336
|
+
|
|
337
|
+
def create_api_key(
|
|
338
|
+
self,
|
|
339
|
+
name: str,
|
|
340
|
+
*,
|
|
341
|
+
scopes: Optional[List[str]] = None,
|
|
342
|
+
environment: str = "live",
|
|
343
|
+
) -> Any:
|
|
344
|
+
return self._json(
|
|
345
|
+
"POST",
|
|
346
|
+
"/v1/api-keys",
|
|
347
|
+
json={"name": name, "scopes": scopes, "environment": environment},
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def revoke_api_key(self, api_key_id: str) -> Any:
|
|
351
|
+
return self._json("DELETE", f"/v1/api-keys/{api_key_id}")
|
|
352
|
+
|
|
353
|
+
# ----- lifecycle ------------------------------------------------------
|
|
354
|
+
def close(self) -> None:
|
|
355
|
+
if self._owns_client:
|
|
356
|
+
self._client.close()
|
|
357
|
+
|
|
358
|
+
def __enter__(self) -> "MemoryClient":
|
|
359
|
+
return self
|
|
360
|
+
|
|
361
|
+
def __exit__(self, *args: Any) -> None:
|
|
362
|
+
self.close()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
366
|
+
# Async twin
|
|
367
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class AsyncMemoryClient:
|
|
371
|
+
"""Async equivalent of :class:`MemoryClient` (httpx.AsyncClient under the hood)."""
|
|
372
|
+
|
|
373
|
+
def __init__(
|
|
374
|
+
self,
|
|
375
|
+
api_key: Optional[str] = None,
|
|
376
|
+
*,
|
|
377
|
+
host: Optional[str] = None,
|
|
378
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
379
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
380
|
+
allow_dev_fallback: bool = True,
|
|
381
|
+
) -> None:
|
|
382
|
+
self.api_key = api_key or os.environ.get("OUTHAD_CONTEXTKIT_API_KEY")
|
|
383
|
+
if not self.api_key:
|
|
384
|
+
raise AuthenticationError(
|
|
385
|
+
"Outhad ContextKit API key not provided.",
|
|
386
|
+
status=401,
|
|
387
|
+
)
|
|
388
|
+
explicit_host = host is not None or "OUTHAD_CONTEXTKIT_HOST" in os.environ
|
|
389
|
+
self.host = _select_host(host)
|
|
390
|
+
self.timeout = timeout
|
|
391
|
+
self._allow_fallback = allow_dev_fallback and not explicit_host
|
|
392
|
+
self._fell_back = False
|
|
393
|
+
self._owns_client = client is None
|
|
394
|
+
|
|
395
|
+
headers = {
|
|
396
|
+
"Authorization": f"Token {self.api_key}",
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"User-Agent": "outhad-contextkit-sdk-python-async/1.0.0",
|
|
399
|
+
}
|
|
400
|
+
if client is None:
|
|
401
|
+
self._client = httpx.AsyncClient(
|
|
402
|
+
base_url=self.host, timeout=timeout, headers=headers
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
self._client = client
|
|
406
|
+
self._client.base_url = httpx.URL(self.host)
|
|
407
|
+
self._client.headers.update(headers)
|
|
408
|
+
|
|
409
|
+
# ----- private --------------------------------------------------------
|
|
410
|
+
def _retarget(self, host: str) -> None:
|
|
411
|
+
self.host = host.rstrip("/")
|
|
412
|
+
self._client.base_url = httpx.URL(self.host)
|
|
413
|
+
self._fell_back = True
|
|
414
|
+
|
|
415
|
+
async def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
416
|
+
try:
|
|
417
|
+
return await self._client.request(method, path, **kwargs)
|
|
418
|
+
except Exception as exc:
|
|
419
|
+
if (
|
|
420
|
+
self._allow_fallback
|
|
421
|
+
and not self._fell_back
|
|
422
|
+
and self.host == DEFAULT_HOST
|
|
423
|
+
and _is_connection_failure(exc)
|
|
424
|
+
):
|
|
425
|
+
logger.warning(
|
|
426
|
+
"Production host %s unreachable (%s) — retrying against %s",
|
|
427
|
+
DEFAULT_HOST,
|
|
428
|
+
exc.__class__.__name__,
|
|
429
|
+
DEV_HOST,
|
|
430
|
+
)
|
|
431
|
+
self._retarget(DEV_HOST)
|
|
432
|
+
return await self._client.request(method, path, **kwargs)
|
|
433
|
+
raise
|
|
434
|
+
|
|
435
|
+
async def _json(self, method: str, path: str, **kwargs) -> Any:
|
|
436
|
+
resp = await self._request(method, path, **kwargs)
|
|
437
|
+
if not resp.is_success:
|
|
438
|
+
raise _decode_error(resp)
|
|
439
|
+
if resp.status_code == 204 or not resp.content:
|
|
440
|
+
return None
|
|
441
|
+
return resp.json()
|
|
442
|
+
|
|
443
|
+
async def validate(self) -> Dict[str, Any]:
|
|
444
|
+
resp = await self._request("GET", "/v1/ping/")
|
|
445
|
+
if resp.status_code == 401:
|
|
446
|
+
raise AuthenticationError("API key rejected.", status=401)
|
|
447
|
+
if not resp.is_success:
|
|
448
|
+
raise APIError(
|
|
449
|
+
f"Failed to reach Outhad ContextKit cloud at {self.host}",
|
|
450
|
+
status=resp.status_code,
|
|
451
|
+
body=resp.text,
|
|
452
|
+
)
|
|
453
|
+
return resp.json() if resp.content else {}
|
|
454
|
+
|
|
455
|
+
# ----- memory CRUD ----------------------------------------------------
|
|
456
|
+
async def add(self, messages, **kw) -> Any:
|
|
457
|
+
return await self._json("POST", "/v1/memories/", json={"messages": messages, **kw})
|
|
458
|
+
|
|
459
|
+
async def get_all(self, **kw) -> Any:
|
|
460
|
+
return await self._json("GET", "/v1/memories/", params=kw)
|
|
461
|
+
|
|
462
|
+
async def get(self, memory_id: str) -> Any:
|
|
463
|
+
return await self._json("GET", f"/v1/memories/{memory_id}/")
|
|
464
|
+
|
|
465
|
+
async def search(self, query: str, **kw) -> Any:
|
|
466
|
+
return await self._json("POST", "/v1/memories/search/", json={"query": query, **kw})
|
|
467
|
+
|
|
468
|
+
async def update(self, memory_id: str, *, text: str) -> Any:
|
|
469
|
+
return await self._json("PUT", f"/v1/memories/{memory_id}/", json={"text": text})
|
|
470
|
+
|
|
471
|
+
async def history(self, memory_id: str) -> Any:
|
|
472
|
+
return await self._json("GET", f"/v1/memories/{memory_id}/history/")
|
|
473
|
+
|
|
474
|
+
async def delete(self, memory_id: str) -> Any:
|
|
475
|
+
return await self._json("DELETE", f"/v1/memories/{memory_id}/")
|
|
476
|
+
|
|
477
|
+
async def delete_all(self, **kw) -> Any:
|
|
478
|
+
return await self._json("DELETE", "/v1/memories/", params=kw)
|
|
479
|
+
|
|
480
|
+
async def aclose(self) -> None:
|
|
481
|
+
if self._owns_client:
|
|
482
|
+
await self._client.aclose()
|
|
483
|
+
|
|
484
|
+
async def __aenter__(self) -> "AsyncMemoryClient":
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
488
|
+
await self.aclose()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
__all__ = [
|
|
492
|
+
"DEFAULT_HOST",
|
|
493
|
+
"DEV_HOST",
|
|
494
|
+
"AsyncMemoryClient",
|
|
495
|
+
"MemoryClient",
|
|
496
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "outhad-contextkit-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Thin Python client for the Outhad ContextKit cloud — paywalled by API key minted at https://contextkit.outhad.com"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Outhad", email = "idrisitanzil@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "Apache-2.0" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
keywords = [
|
|
16
|
+
"memory",
|
|
17
|
+
"ai",
|
|
18
|
+
"agents",
|
|
19
|
+
"long-term-memory",
|
|
20
|
+
"outhad",
|
|
21
|
+
"contextkit",
|
|
22
|
+
"sdk",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"License :: OSI Approved :: Apache Software License",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Development Status :: 4 - Beta",
|
|
33
|
+
"Intended Audience :: Developers",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Intentionally lean — the whole point of this distribution is "thin
|
|
38
|
+
# client only". No torch, no langchain, no vector store drivers, no
|
|
39
|
+
# embedders. Just an HTTP client + JSON.
|
|
40
|
+
dependencies = [
|
|
41
|
+
"httpx>=0.27.0",
|
|
42
|
+
"typing-extensions>=4.8.0; python_version<'3.11'",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
test = [
|
|
47
|
+
"pytest>=8.2.2",
|
|
48
|
+
"pytest-asyncio>=0.23.7",
|
|
49
|
+
"respx>=0.21.0",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://contextkit.outhad.com"
|
|
54
|
+
Documentation = "https://contextkit.outhad.com/docs"
|
|
55
|
+
Source = "https://github.com/outhad/contextkit"
|
|
56
|
+
Issues = "https://github.com/outhad/contextkit/issues"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build]
|
|
59
|
+
include = [
|
|
60
|
+
"outhad_contextkit_sdk/**/*.py",
|
|
61
|
+
"README.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.build.targets.wheel]
|
|
66
|
+
packages = ["outhad_contextkit_sdk"]
|