khai-sdk 0.1.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,166 @@
1
+ name: Release
2
+
3
+ # Publish flow for the khai-sdk package. Triggered only by version tags (v1.2.3).
4
+ #
5
+ # build ─► publish-testpypi ─► smoke-testpypi ─► publish-pypi ─► github-release
6
+ #
7
+ # Both publish jobs use PyPI Trusted Publishing: the job requests a short-lived
8
+ # OIDC token (permissions: id-token: write) and PyPI accepts it because a
9
+ # publisher for owner KHAI-BE / repo khai-python / workflow release.yml /
10
+ # environment {testpypi,pypi} is registered there. No API token exists to leak.
11
+ # The `pypi` environment requires a human reviewer, so a real release always
12
+ # has an explicit approval step after the TestPyPI rehearsal has passed.
13
+
14
+ on:
15
+ push:
16
+ tags: ["v*"]
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ env:
22
+ PACKAGE: khai-sdk
23
+
24
+ jobs:
25
+ build:
26
+ name: Build sdist and wheel
27
+ runs-on: ubuntu-latest
28
+ timeout-minutes: 10
29
+ outputs:
30
+ version: ${{ steps.version.outputs.version }}
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.12"
37
+
38
+ - name: Check tag matches package version
39
+ id: version
40
+ run: |
41
+ tag="${GITHUB_REF_NAME#v}"
42
+ pkg="$(python -c 'import re,pathlib; print(re.search(r"__version__\s*=\s*\"([^\"]+)\"", pathlib.Path("src/khai/_version.py").read_text()).group(1))')"
43
+ echo "tag=$tag package=$pkg"
44
+ if [ "$tag" != "$pkg" ]; then
45
+ echo "::error::Tag v$tag does not match src/khai/_version.py ($pkg)"; exit 1
46
+ fi
47
+ echo "version=$pkg" >> "$GITHUB_OUTPUT"
48
+
49
+ - name: Run tests
50
+ run: |
51
+ python -m pip install --upgrade pip
52
+ pip install -e ".[dev]"
53
+ pytest -q
54
+
55
+ - name: Build
56
+ run: |
57
+ pip install build
58
+ python -m build
59
+ ls -l dist
60
+
61
+ - uses: actions/upload-artifact@v4
62
+ with:
63
+ name: dist
64
+ path: dist/
65
+ if-no-files-found: error
66
+
67
+ publish-testpypi:
68
+ name: Publish to TestPyPI
69
+ needs: build
70
+ runs-on: ubuntu-latest
71
+ timeout-minutes: 10
72
+ environment:
73
+ name: testpypi
74
+ url: https://test.pypi.org/project/khai-sdk/${{ needs.build.outputs.version }}/
75
+ permissions:
76
+ id-token: write
77
+ steps:
78
+ - uses: actions/download-artifact@v4
79
+ with:
80
+ name: dist
81
+ path: dist/
82
+ - uses: pypa/gh-action-pypi-publish@release/v1
83
+ with:
84
+ repository-url: https://test.pypi.org/legacy/
85
+
86
+ smoke-testpypi:
87
+ name: Install from TestPyPI and import
88
+ needs: [build, publish-testpypi]
89
+ runs-on: ubuntu-latest
90
+ timeout-minutes: 15
91
+ steps:
92
+ - uses: actions/setup-python@v5
93
+ with:
94
+ python-version: "3.12"
95
+ - name: Install the just-published version
96
+ env:
97
+ VERSION: ${{ needs.build.outputs.version }}
98
+ run: |
99
+ # The TestPyPI simple index can lag the upload by a minute; retry.
100
+ for i in 1 2 3 4 5 6 7 8 9 10; do
101
+ if pip install --no-cache-dir \
102
+ --index-url https://test.pypi.org/simple/ \
103
+ --extra-index-url https://pypi.org/simple/ \
104
+ "${PACKAGE}==${VERSION}"; then
105
+ break
106
+ fi
107
+ echo "not indexed yet, retry $i"; sleep 20
108
+ done
109
+ - name: Import and sanity-check
110
+ env:
111
+ VERSION: ${{ needs.build.outputs.version }}
112
+ run: |
113
+ python - <<'PY'
114
+ import os
115
+ import khai
116
+ from khai import KhaiClient, KhaiConfigError, IngestTurn
117
+ assert khai.__version__ == os.environ["VERSION"], khai.__version__
118
+ t = IngestTurn("q", "a", "s")
119
+ assert t.response_id and t.timestamp
120
+ try:
121
+ KhaiClient(api_key="x", base_url="http://api.getkhai.ai")
122
+ except KhaiConfigError:
123
+ pass
124
+ else:
125
+ raise SystemExit("http base_url should be rejected")
126
+ print("ok", khai.__version__)
127
+ PY
128
+
129
+ publish-pypi:
130
+ name: Publish to PyPI
131
+ needs: [build, smoke-testpypi]
132
+ runs-on: ubuntu-latest
133
+ timeout-minutes: 10
134
+ environment:
135
+ name: pypi
136
+ url: https://pypi.org/project/khai-sdk/${{ needs.build.outputs.version }}/
137
+ permissions:
138
+ id-token: write
139
+ steps:
140
+ - uses: actions/download-artifact@v4
141
+ with:
142
+ name: dist
143
+ path: dist/
144
+ - uses: pypa/gh-action-pypi-publish@release/v1
145
+
146
+ github-release:
147
+ name: Create GitHub Release
148
+ needs: [build, publish-pypi]
149
+ runs-on: ubuntu-latest
150
+ timeout-minutes: 5
151
+ permissions:
152
+ contents: write
153
+ steps:
154
+ - uses: actions/checkout@v4
155
+ - uses: actions/download-artifact@v4
156
+ with:
157
+ name: dist
158
+ path: dist/
159
+ - name: Create release with artifacts
160
+ env:
161
+ GH_TOKEN: ${{ github.token }}
162
+ run: |
163
+ gh release create "$GITHUB_REF_NAME" dist/* \
164
+ --title "$GITHUB_REF_NAME" \
165
+ --generate-notes \
166
+ --verify-tag
@@ -0,0 +1,30 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ pytest:
13
+ runs-on: ubuntu-latest
14
+ timeout-minutes: 10
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ cache: pip
25
+ - name: Install
26
+ run: python -m pip install --upgrade pip && pip install -e ".[dev]"
27
+ - name: Test
28
+ run: pytest -q
29
+ - name: Build sdist and wheel
30
+ run: pip install build && python -m build
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .venv/
8
+ .idea/
9
+ .vscode/
10
+ .DS_Store
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Khai Python SDK. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
5
+
6
+ ## [Unreleased]
7
+
8
+ ### Added
9
+ - Release pipeline (`.github/workflows/release.yml`): a `v*` tag builds once,
10
+ publishes to TestPyPI, smoke-installs from there, then publishes to PyPI via
11
+ trusted publishing (OIDC, no stored tokens) behind a required reviewer, and
12
+ creates a GitHub Release with the artifacts.
13
+ - `IngestTurn` now fills `response_id` (UUID4) and `timestamp` (UTC ISO-8601) at
14
+ creation when the caller omits them, so retries are deduplicated server-side
15
+ instead of stored twice.
16
+ - `KHAI_API_KEY` and `KHAI_BASE_URL` environment fallbacks; `api_key` and
17
+ `base_url` are optional constructor arguments.
18
+ - `max_queue_size` (default 10,000) bounds the fire-and-forget queue and the
19
+ retry buffer. Drops are reported via `on_error` as the new `KhaiQueueFullError`.
20
+ - Open clients are flushed once at interpreter exit.
21
+ - `khai.__version__` is the single source of the version (hatch dynamic
22
+ version from `src/khai/_version.py`) and is stamped into the `User-Agent`.
23
+
24
+ ### Changed
25
+ - Distribution name is `khai-sdk` (install with `pip install khai-sdk`, then
26
+ `import khai`). PyPI rejects `khai` as confusable with the existing `khal`
27
+ project, so the shorter name is not available.
28
+ - `base_url` must be `https://`; `http://` is accepted only for localhost.
29
+ - `close()` is idempotent.
30
+
31
+ ### Fixed
32
+ - Error messages from the backend's auth and rate-limit layer were lost: those
33
+ responses use FastAPI's `{"detail": ...}` envelope, and the SDK only read
34
+ `{"error": ...}`. A rejected key surfaced as "request failed"; it now
35
+ surfaces as "Invalid or inactive API key." 422 validation lists are
36
+ flattened into one message.
37
+
38
+ ## [0.1.0] - 2026-08-04
39
+
40
+ Initial hand-written SDK: `ingest_turn`, `ingest_batch`, `ingest_turn_async`,
41
+ `set_redactor`, typed errors, retries with exponential backoff.
khai_sdk-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brise.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.5
2
+ Name: khai-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for streaming chatbot turns to Khai for evaluation.
5
+ Project-URL: Homepage, https://getkhai.ai
6
+ Project-URL: Source, https://github.com/KHAI-BE/khai-python
7
+ Project-URL: Changelog, https://github.com/KHAI-BE/khai-python/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/KHAI-BE/khai-python/issues
9
+ Author-email: Khai <support@getkhai.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: chatbot,evaluation,khai,llm,observability
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.24
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Requires-Dist: respx>=0.20; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Khai Python SDK
30
+
31
+ Stream chatbot turns to Khai for evaluation from any cloud — no access granted to Khai.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install khai-sdk
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ Set your key in the environment rather than in source:
42
+
43
+ ```bash
44
+ export KHAI_API_KEY="khai_..."
45
+ ```
46
+
47
+ ```python
48
+ from khai import KhaiClient
49
+
50
+ khai = KhaiClient() # reads KHAI_API_KEY; base_url defaults to https://api.getkhai.ai
51
+
52
+ resp = khai.ingest_turn(
53
+ user_query="How do I pay my bill?",
54
+ agent_response="You can pay online at ...",
55
+ session_id="chat-123",
56
+ agent_id="YOUR_AGENT_ID",
57
+ )
58
+ print(resp.status, resp.result.trust_score if resp.evaluated else resp.reason)
59
+ ```
60
+
61
+ ## Every turn is idempotent
62
+
63
+ You do not have to think about retries. When you omit them, the SDK fills in a
64
+ `timestamp` (UTC, ISO-8601) and a UUID `response_id` **when the turn is created**,
65
+ and keeps them for every retry. The backend deduplicates on `response_id`, so a
66
+ turn resent after a timeout, a 5xx, or a network outage is answered with
67
+ `status: duplicate` instead of being stored twice.
68
+
69
+ If your platform already has a per-reply id (Dialogflow `responseId`, Lex,
70
+ Copilot Studio), pass it as `response_id` and the same guarantee holds even
71
+ across restarts of your own process.
72
+
73
+ ## Fire-and-forget
74
+
75
+ `ingest_turn_async` queues the turn and returns immediately, so it never blocks
76
+ your chatbot's response path. Failed deliveries are buffered and retried. Both
77
+ the queue and the buffer are capped at `max_queue_size` (default 10,000 turns);
78
+ when full, the SDK drops a turn and reports it through `on_error` as
79
+ `KhaiQueueFullError` rather than growing memory during an outage. Open clients
80
+ are flushed once at interpreter exit.
81
+
82
+ ```python
83
+ with KhaiClient() as khai:
84
+ khai.ingest_turn_async(
85
+ user_query=user_msg,
86
+ agent_response=bot_reply,
87
+ session_id=session_id,
88
+ agent_id="YOUR_AGENT_ID",
89
+ )
90
+ # ... keep serving; turns flush in the background, and on close().
91
+ ```
92
+
93
+ ## Batch
94
+
95
+ ```python
96
+ from khai import IngestTurn
97
+
98
+ khai.ingest_batch([
99
+ IngestTurn(user_query="...", agent_response="...", session_id="s1", agent_id="a1"),
100
+ IngestTurn(user_query="...", agent_response="...", session_id="s1", agent_id="a1"),
101
+ ])
102
+ ```
103
+
104
+ ## Redact PII before sending
105
+
106
+ ```python
107
+ def redact(payload: dict) -> dict:
108
+ payload["user_query"] = mask_emails(payload["user_query"])
109
+ return payload
110
+
111
+ khai.set_redactor(redact)
112
+ ```
113
+
114
+ ## Errors
115
+
116
+ All errors derive from `KhaiError`:
117
+
118
+ | Error | Meaning |
119
+ |---|---|
120
+ | `KhaiAuthError` | API key missing/invalid (401/403) |
121
+ | `KhaiRateLimitError` | Per-key rate limit exceeded (429) |
122
+ | `KhaiAPIError` | Other non-success status |
123
+ | `KhaiConnectionError` | Network/timeout (already retried) |
124
+ | `KhaiQueueFullError` | Fire-and-forget queue/buffer full; a turn was dropped (via `on_error`) |
125
+ | `KhaiConfigError` | Bad client configuration (missing key, non-HTTPS base URL) |
126
+
127
+ ## Configuration
128
+
129
+ | Setting | Argument | Environment variable | Default |
130
+ |---|---|---|---|
131
+ | API key | `api_key` | `KHAI_API_KEY` | required |
132
+ | Base URL | `base_url` | `KHAI_BASE_URL` | `https://api.getkhai.ai` |
133
+ | Request timeout (s) | `timeout` | — | `10.0` |
134
+ | Retries on 429/5xx/network | `max_retries` | — | `3` |
135
+ | Queue / buffer cap | `max_queue_size` | — | `10000` |
136
+
137
+ `base_url` must be `https://`; plain `http://` is accepted only for `localhost`.
138
+
139
+ ## Releasing (maintainers)
140
+
141
+ Releases are cut from tags and published by CI through PyPI trusted publishing;
142
+ no one uploads from a laptop and no API token is stored anywhere.
143
+
144
+ 1. Bump `__version__` in `src/khai/_version.py` and move the `Unreleased`
145
+ entries in `CHANGELOG.md` under the new version. Merge via pull request.
146
+ 2. Tag the merged commit and push the tag:
147
+
148
+ ```bash
149
+ git tag v0.1.0 && git push origin v0.1.0
150
+ ```
151
+
152
+ 3. The `Release` workflow builds the sdist and wheel, publishes to TestPyPI,
153
+ installs from TestPyPI and imports the package, then waits for a reviewer to
154
+ approve the `pypi` environment before publishing to PyPI and creating the
155
+ GitHub Release. A tag whose version does not match `__version__` fails fast.
@@ -0,0 +1,127 @@
1
+ # Khai Python SDK
2
+
3
+ Stream chatbot turns to Khai for evaluation from any cloud — no access granted to Khai.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install khai-sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ Set your key in the environment rather than in source:
14
+
15
+ ```bash
16
+ export KHAI_API_KEY="khai_..."
17
+ ```
18
+
19
+ ```python
20
+ from khai import KhaiClient
21
+
22
+ khai = KhaiClient() # reads KHAI_API_KEY; base_url defaults to https://api.getkhai.ai
23
+
24
+ resp = khai.ingest_turn(
25
+ user_query="How do I pay my bill?",
26
+ agent_response="You can pay online at ...",
27
+ session_id="chat-123",
28
+ agent_id="YOUR_AGENT_ID",
29
+ )
30
+ print(resp.status, resp.result.trust_score if resp.evaluated else resp.reason)
31
+ ```
32
+
33
+ ## Every turn is idempotent
34
+
35
+ You do not have to think about retries. When you omit them, the SDK fills in a
36
+ `timestamp` (UTC, ISO-8601) and a UUID `response_id` **when the turn is created**,
37
+ and keeps them for every retry. The backend deduplicates on `response_id`, so a
38
+ turn resent after a timeout, a 5xx, or a network outage is answered with
39
+ `status: duplicate` instead of being stored twice.
40
+
41
+ If your platform already has a per-reply id (Dialogflow `responseId`, Lex,
42
+ Copilot Studio), pass it as `response_id` and the same guarantee holds even
43
+ across restarts of your own process.
44
+
45
+ ## Fire-and-forget
46
+
47
+ `ingest_turn_async` queues the turn and returns immediately, so it never blocks
48
+ your chatbot's response path. Failed deliveries are buffered and retried. Both
49
+ the queue and the buffer are capped at `max_queue_size` (default 10,000 turns);
50
+ when full, the SDK drops a turn and reports it through `on_error` as
51
+ `KhaiQueueFullError` rather than growing memory during an outage. Open clients
52
+ are flushed once at interpreter exit.
53
+
54
+ ```python
55
+ with KhaiClient() as khai:
56
+ khai.ingest_turn_async(
57
+ user_query=user_msg,
58
+ agent_response=bot_reply,
59
+ session_id=session_id,
60
+ agent_id="YOUR_AGENT_ID",
61
+ )
62
+ # ... keep serving; turns flush in the background, and on close().
63
+ ```
64
+
65
+ ## Batch
66
+
67
+ ```python
68
+ from khai import IngestTurn
69
+
70
+ khai.ingest_batch([
71
+ IngestTurn(user_query="...", agent_response="...", session_id="s1", agent_id="a1"),
72
+ IngestTurn(user_query="...", agent_response="...", session_id="s1", agent_id="a1"),
73
+ ])
74
+ ```
75
+
76
+ ## Redact PII before sending
77
+
78
+ ```python
79
+ def redact(payload: dict) -> dict:
80
+ payload["user_query"] = mask_emails(payload["user_query"])
81
+ return payload
82
+
83
+ khai.set_redactor(redact)
84
+ ```
85
+
86
+ ## Errors
87
+
88
+ All errors derive from `KhaiError`:
89
+
90
+ | Error | Meaning |
91
+ |---|---|
92
+ | `KhaiAuthError` | API key missing/invalid (401/403) |
93
+ | `KhaiRateLimitError` | Per-key rate limit exceeded (429) |
94
+ | `KhaiAPIError` | Other non-success status |
95
+ | `KhaiConnectionError` | Network/timeout (already retried) |
96
+ | `KhaiQueueFullError` | Fire-and-forget queue/buffer full; a turn was dropped (via `on_error`) |
97
+ | `KhaiConfigError` | Bad client configuration (missing key, non-HTTPS base URL) |
98
+
99
+ ## Configuration
100
+
101
+ | Setting | Argument | Environment variable | Default |
102
+ |---|---|---|---|
103
+ | API key | `api_key` | `KHAI_API_KEY` | required |
104
+ | Base URL | `base_url` | `KHAI_BASE_URL` | `https://api.getkhai.ai` |
105
+ | Request timeout (s) | `timeout` | — | `10.0` |
106
+ | Retries on 429/5xx/network | `max_retries` | — | `3` |
107
+ | Queue / buffer cap | `max_queue_size` | — | `10000` |
108
+
109
+ `base_url` must be `https://`; plain `http://` is accepted only for `localhost`.
110
+
111
+ ## Releasing (maintainers)
112
+
113
+ Releases are cut from tags and published by CI through PyPI trusted publishing;
114
+ no one uploads from a laptop and no API token is stored anywhere.
115
+
116
+ 1. Bump `__version__` in `src/khai/_version.py` and move the `Unreleased`
117
+ entries in `CHANGELOG.md` under the new version. Merge via pull request.
118
+ 2. Tag the merged commit and push the tag:
119
+
120
+ ```bash
121
+ git tag v0.1.0 && git push origin v0.1.0
122
+ ```
123
+
124
+ 3. The `Release` workflow builds the sdist and wheel, publishes to TestPyPI,
125
+ installs from TestPyPI and imports the package, then waits for a reviewer to
126
+ approve the `pypi` environment before publishing to PyPI and creating the
127
+ GitHub Release. A tag whose version does not match `__version__` fails fast.
@@ -0,0 +1,10 @@
1
+ # Security policy
2
+
3
+ Report vulnerabilities in this SDK privately to admin@brise.ai. Do not open
4
+ a public issue for security reports. We acknowledge reports within three
5
+ business days.
6
+
7
+ Only the latest minor release receives security fixes.
8
+
9
+ The SDK never logs or prints your API key. Pass it via the `KHAI_API_KEY`
10
+ environment variable rather than embedding it in source.