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.
@@ -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"]