kirha 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.
- kirha-0.1.0/LICENSE +21 -0
- kirha-0.1.0/PKG-INFO +185 -0
- kirha-0.1.0/README.md +160 -0
- kirha-0.1.0/pyproject.toml +63 -0
- kirha-0.1.0/pyproject.toml.orig +43 -0
- kirha-0.1.0/src/kirha/__init__.py +44 -0
- kirha-0.1.0/src/kirha/_client.py +427 -0
- kirha-0.1.0/src/kirha/_errors.py +46 -0
- kirha-0.1.0/src/kirha/_models.py +137 -0
- kirha-0.1.0/src/kirha/py.typed +0 -0
kirha-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kirha
|
|
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.
|
kirha-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kirha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Kirha Search and Tools API
|
|
5
|
+
Keywords: kirha,search,ai,api,sdk
|
|
6
|
+
Author: Kirha
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: httpx>=0.27,<1
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Project-URL: Homepage, https://kirha.com
|
|
22
|
+
Project-URL: Documentation, https://docs.kirha.com
|
|
23
|
+
Project-URL: Repository, https://github.com/kirha-ai/kirha-sdk-python
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://raw.githubusercontent.com/kirha-ai/kirha-sdk-python/main/assets/banner.png" alt="KIRHA SDK" />
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://pypi.org/project/kirha/"><img src="https://img.shields.io/pypi/v/kirha" alt="PyPI version" /></a>
|
|
32
|
+
<a href="https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/ci.yml"><img src="https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
33
|
+
<a href="https://pypi.org/project/kirha/"><img src="https://img.shields.io/pypi/pyversions/kirha" alt="Python versions" /></a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<a href="https://kirha.com"><b>🦋 KIRHA</b></a> •
|
|
38
|
+
<a href="https://docs.kirha.com"><b>📚 Documentation</b></a> •
|
|
39
|
+
<a href="https://app.kirha.com/auth/register"><b>🔑 Get an API key</b></a>
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
# KIRHA SDK
|
|
43
|
+
|
|
44
|
+
Python SDK for the KIRHA Search and Tools APIs.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install kirha
|
|
50
|
+
# or
|
|
51
|
+
uv add kirha
|
|
52
|
+
# or
|
|
53
|
+
poetry add kirha
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.10 or later.
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from kirha import Kirha
|
|
62
|
+
|
|
63
|
+
kirha = Kirha()
|
|
64
|
+
|
|
65
|
+
result = kirha.search("What does the boxed warning on Ozempic say?")
|
|
66
|
+
|
|
67
|
+
print(result.data)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`Kirha()` reads your API key from the `KIRHA_API_KEY` environment variable. Pass `api_key="..."` to set it in code.
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Summarization
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
kirha = Kirha(api_key="your-api-key")
|
|
78
|
+
|
|
79
|
+
result = kirha.search("Summarize latest NVIDIA earning calls", summarization=True)
|
|
80
|
+
|
|
81
|
+
print(result.summary)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Summarization with instruction
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
kirha = Kirha(api_key="your-api-key")
|
|
88
|
+
|
|
89
|
+
result = kirha.search(
|
|
90
|
+
"List top 10 stablecoins yield on Ethereum",
|
|
91
|
+
summarization={
|
|
92
|
+
"instruction": "Format the response as a table with columns: Stablecoin, Protocol, APY",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
print(result.summary)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Plan mode
|
|
100
|
+
|
|
101
|
+
Review the steps and the estimated cost before running the query, yourself or with an agent. Planning charges nothing until the plan runs.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
kirha = Kirha(api_key="your-api-key")
|
|
105
|
+
|
|
106
|
+
plan = kirha.plan(
|
|
107
|
+
"Find engineers working at companies behind the latest patents in Machine Learning",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
print(plan.steps)
|
|
111
|
+
print(plan.usage)
|
|
112
|
+
|
|
113
|
+
result = plan.execute()
|
|
114
|
+
print(result.data)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A plan expires 5 minutes after it is created. To run it from another process, after an approval step for example, call `kirha.execute_plan(plan_id)`.
|
|
118
|
+
|
|
119
|
+
### Direct tool calling
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
tools = kirha.tools(focus="crypto")
|
|
123
|
+
print([tool.name for tool in tools])
|
|
124
|
+
|
|
125
|
+
result = kirha.execute_tool("coingecko_searchCoin", {"query": "avax", "limit": 1})
|
|
126
|
+
print(result.output)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Async
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import asyncio
|
|
133
|
+
|
|
134
|
+
from kirha import AsyncKirha
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def main() -> None:
|
|
138
|
+
async with AsyncKirha() as kirha:
|
|
139
|
+
result = await kirha.search("What does the boxed warning on Ozempic say?")
|
|
140
|
+
print(result.data)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
asyncio.run(main())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`AsyncKirha` has the same methods as `Kirha`, as coroutines. Both clients keep a connection pool open, so close them with `close()` or use them as context managers.
|
|
147
|
+
|
|
148
|
+
## Documentation
|
|
149
|
+
|
|
150
|
+
- [Search API](https://github.com/kirha-ai/kirha-sdk-python/blob/main/docs/search-api.md) - Full API reference
|
|
151
|
+
- [Examples](https://github.com/kirha-ai/kirha-sdk-python/tree/main/examples) - Runnable scripts
|
|
152
|
+
|
|
153
|
+
## Errors
|
|
154
|
+
|
|
155
|
+
| Error | Description |
|
|
156
|
+
|-------|-------------|
|
|
157
|
+
| `KirhaError` | Base error class |
|
|
158
|
+
| `ConfigurationError` | No API key passed and `KIRHA_API_KEY` not set |
|
|
159
|
+
| `ApiError` | API returned an error, with `status_code`, `code` and `details` |
|
|
160
|
+
| `AuthenticationError` | Invalid or missing API key (401) |
|
|
161
|
+
| `RateLimitError` | Rate limit exceeded (429), with `retry_after` in seconds |
|
|
162
|
+
| `PlanExpiredError` | Plan expired (5 minute limit) |
|
|
163
|
+
| `NetworkError` | Network/connection failure or timeout |
|
|
164
|
+
|
|
165
|
+
`AuthenticationError` and `RateLimitError` extend `ApiError`.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv sync
|
|
171
|
+
uv run pytest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`uv run pytest -m integration` runs the tests against the live API with your `KIRHA_API_KEY`. They spend credits.
|
|
175
|
+
|
|
176
|
+
### Releasing
|
|
177
|
+
|
|
178
|
+
1. Bump `version` in `pyproject.toml` (for example `uv version 0.2.0`) and merge it to `main`.
|
|
179
|
+
2. Create a GitHub release with a tag matching that version, prefixed with `v` (for example `v0.2.0`).
|
|
180
|
+
|
|
181
|
+
Publishing the release runs the [publish workflow](https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/publish.yml), which checks that the tag matches the package version, runs the tests, builds the package and uploads it to [PyPI](https://pypi.org/project/kirha/) through trusted publishing. No PyPI token is stored in the repository.
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
kirha-0.1.0/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/kirha-ai/kirha-sdk-python/main/assets/banner.png" alt="KIRHA SDK" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://pypi.org/project/kirha/"><img src="https://img.shields.io/pypi/v/kirha" alt="PyPI version" /></a>
|
|
7
|
+
<a href="https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/ci.yml"><img src="https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
8
|
+
<a href="https://pypi.org/project/kirha/"><img src="https://img.shields.io/pypi/pyversions/kirha" alt="Python versions" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://kirha.com"><b>🦋 KIRHA</b></a> •
|
|
13
|
+
<a href="https://docs.kirha.com"><b>📚 Documentation</b></a> •
|
|
14
|
+
<a href="https://app.kirha.com/auth/register"><b>🔑 Get an API key</b></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
# KIRHA SDK
|
|
18
|
+
|
|
19
|
+
Python SDK for the KIRHA Search and Tools APIs.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install kirha
|
|
25
|
+
# or
|
|
26
|
+
uv add kirha
|
|
27
|
+
# or
|
|
28
|
+
poetry add kirha
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Python 3.10 or later.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from kirha import Kirha
|
|
37
|
+
|
|
38
|
+
kirha = Kirha()
|
|
39
|
+
|
|
40
|
+
result = kirha.search("What does the boxed warning on Ozempic say?")
|
|
41
|
+
|
|
42
|
+
print(result.data)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Kirha()` reads your API key from the `KIRHA_API_KEY` environment variable. Pass `api_key="..."` to set it in code.
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Summarization
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
kirha = Kirha(api_key="your-api-key")
|
|
53
|
+
|
|
54
|
+
result = kirha.search("Summarize latest NVIDIA earning calls", summarization=True)
|
|
55
|
+
|
|
56
|
+
print(result.summary)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Summarization with instruction
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
kirha = Kirha(api_key="your-api-key")
|
|
63
|
+
|
|
64
|
+
result = kirha.search(
|
|
65
|
+
"List top 10 stablecoins yield on Ethereum",
|
|
66
|
+
summarization={
|
|
67
|
+
"instruction": "Format the response as a table with columns: Stablecoin, Protocol, APY",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print(result.summary)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Plan mode
|
|
75
|
+
|
|
76
|
+
Review the steps and the estimated cost before running the query, yourself or with an agent. Planning charges nothing until the plan runs.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
kirha = Kirha(api_key="your-api-key")
|
|
80
|
+
|
|
81
|
+
plan = kirha.plan(
|
|
82
|
+
"Find engineers working at companies behind the latest patents in Machine Learning",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
print(plan.steps)
|
|
86
|
+
print(plan.usage)
|
|
87
|
+
|
|
88
|
+
result = plan.execute()
|
|
89
|
+
print(result.data)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A plan expires 5 minutes after it is created. To run it from another process, after an approval step for example, call `kirha.execute_plan(plan_id)`.
|
|
93
|
+
|
|
94
|
+
### Direct tool calling
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
tools = kirha.tools(focus="crypto")
|
|
98
|
+
print([tool.name for tool in tools])
|
|
99
|
+
|
|
100
|
+
result = kirha.execute_tool("coingecko_searchCoin", {"query": "avax", "limit": 1})
|
|
101
|
+
print(result.output)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Async
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import asyncio
|
|
108
|
+
|
|
109
|
+
from kirha import AsyncKirha
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def main() -> None:
|
|
113
|
+
async with AsyncKirha() as kirha:
|
|
114
|
+
result = await kirha.search("What does the boxed warning on Ozempic say?")
|
|
115
|
+
print(result.data)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
asyncio.run(main())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`AsyncKirha` has the same methods as `Kirha`, as coroutines. Both clients keep a connection pool open, so close them with `close()` or use them as context managers.
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
- [Search API](https://github.com/kirha-ai/kirha-sdk-python/blob/main/docs/search-api.md) - Full API reference
|
|
126
|
+
- [Examples](https://github.com/kirha-ai/kirha-sdk-python/tree/main/examples) - Runnable scripts
|
|
127
|
+
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
| Error | Description |
|
|
131
|
+
|-------|-------------|
|
|
132
|
+
| `KirhaError` | Base error class |
|
|
133
|
+
| `ConfigurationError` | No API key passed and `KIRHA_API_KEY` not set |
|
|
134
|
+
| `ApiError` | API returned an error, with `status_code`, `code` and `details` |
|
|
135
|
+
| `AuthenticationError` | Invalid or missing API key (401) |
|
|
136
|
+
| `RateLimitError` | Rate limit exceeded (429), with `retry_after` in seconds |
|
|
137
|
+
| `PlanExpiredError` | Plan expired (5 minute limit) |
|
|
138
|
+
| `NetworkError` | Network/connection failure or timeout |
|
|
139
|
+
|
|
140
|
+
`AuthenticationError` and `RateLimitError` extend `ApiError`.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv sync
|
|
146
|
+
uv run pytest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`uv run pytest -m integration` runs the tests against the live API with your `KIRHA_API_KEY`. They spend credits.
|
|
150
|
+
|
|
151
|
+
### Releasing
|
|
152
|
+
|
|
153
|
+
1. Bump `version` in `pyproject.toml` (for example `uv version 0.2.0`) and merge it to `main`.
|
|
154
|
+
2. Create a GitHub release with a tag matching that version, prefixed with `v` (for example `v0.2.0`).
|
|
155
|
+
|
|
156
|
+
Publishing the release runs the [publish workflow](https://github.com/kirha-ai/kirha-sdk-python/actions/workflows/publish.yml), which checks that the tag matches the package version, runs the tests, builds the package and uploads it to [PyPI](https://pypi.org/project/kirha/) through trusted publishing. No PyPI token is stored in the repository.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kirha"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Kirha Search and Tools API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"kirha",
|
|
12
|
+
"search",
|
|
13
|
+
"ai",
|
|
14
|
+
"api",
|
|
15
|
+
"sdk",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Programming Language :: Python :: 3.15",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "Kirha"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://kirha.com"
|
|
35
|
+
Documentation = "https://docs.kirha.com"
|
|
36
|
+
Repository = "https://github.com/kirha-ai/kirha-sdk-python"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.13.0,<0.14"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"mypy>=2.4.0",
|
|
45
|
+
"pytest>=9.1.1",
|
|
46
|
+
"respx>=0.23.1",
|
|
47
|
+
"ruff>=0.17.0",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
strict = true
|
|
52
|
+
files = [
|
|
53
|
+
"src",
|
|
54
|
+
"tests",
|
|
55
|
+
"examples",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.pytest]
|
|
59
|
+
addopts = [
|
|
60
|
+
"-m",
|
|
61
|
+
"not integration",
|
|
62
|
+
]
|
|
63
|
+
markers = ["integration: calls the live Kirha API, needs KIRHA_API_KEY"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kirha"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Kirha Search and Tools API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Kirha" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
11
|
+
keywords = ["kirha", "search", "ai", "api", "sdk"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Programming Language :: Python :: 3.15",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://kirha.com"
|
|
27
|
+
Documentation = "https://docs.kirha.com"
|
|
28
|
+
Repository = "https://github.com/kirha-ai/kirha-sdk-python"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.13.0,<0.14"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = ["mypy>=2.4.0", "pytest>=9.1.1", "respx>=0.23.1", "ruff>=0.17.0"]
|
|
36
|
+
|
|
37
|
+
[tool.mypy]
|
|
38
|
+
strict = true
|
|
39
|
+
files = ["src", "tests", "examples"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest]
|
|
42
|
+
addopts = ["-m", "not integration"]
|
|
43
|
+
markers = ["integration: calls the live Kirha API, needs KIRHA_API_KEY"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from ._client import AsyncKirha, AsyncPlan, Kirha, Plan
|
|
2
|
+
from ._errors import (
|
|
3
|
+
ApiError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConfigurationError,
|
|
6
|
+
KirhaError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
PlanExpiredError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
)
|
|
11
|
+
from ._models import (
|
|
12
|
+
Account,
|
|
13
|
+
Planning,
|
|
14
|
+
PlanningRuntime,
|
|
15
|
+
PlanStep,
|
|
16
|
+
SearchResult,
|
|
17
|
+
Summarization,
|
|
18
|
+
Tool,
|
|
19
|
+
ToolExecutionResult,
|
|
20
|
+
Usage,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Account",
|
|
25
|
+
"ApiError",
|
|
26
|
+
"AsyncKirha",
|
|
27
|
+
"AsyncPlan",
|
|
28
|
+
"AuthenticationError",
|
|
29
|
+
"ConfigurationError",
|
|
30
|
+
"Kirha",
|
|
31
|
+
"KirhaError",
|
|
32
|
+
"NetworkError",
|
|
33
|
+
"Plan",
|
|
34
|
+
"PlanExpiredError",
|
|
35
|
+
"PlanStep",
|
|
36
|
+
"Planning",
|
|
37
|
+
"PlanningRuntime",
|
|
38
|
+
"RateLimitError",
|
|
39
|
+
"SearchResult",
|
|
40
|
+
"Summarization",
|
|
41
|
+
"Tool",
|
|
42
|
+
"ToolExecutionResult",
|
|
43
|
+
"Usage",
|
|
44
|
+
]
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from ._errors import (
|
|
14
|
+
ApiError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
NetworkError,
|
|
18
|
+
PlanExpiredError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
)
|
|
21
|
+
from ._models import (
|
|
22
|
+
Account,
|
|
23
|
+
PlanningRuntime,
|
|
24
|
+
PlanStep,
|
|
25
|
+
SearchResult,
|
|
26
|
+
Summarization,
|
|
27
|
+
Tool,
|
|
28
|
+
ToolExecutionResult,
|
|
29
|
+
Usage,
|
|
30
|
+
parse_plan,
|
|
31
|
+
parse_search_result,
|
|
32
|
+
parse_tool_execution_result,
|
|
33
|
+
parse_tools,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
BASE_URL = "https://api.kirha.com"
|
|
37
|
+
DEFAULT_TIMEOUT = 120.0
|
|
38
|
+
PLAN_EXPIRED_CODE = "CONVERSATION_TURN_TIMEOUT"
|
|
39
|
+
|
|
40
|
+
T = TypeVar("T")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, kw_only=True)
|
|
44
|
+
class _PlanFields:
|
|
45
|
+
id: str
|
|
46
|
+
query: str
|
|
47
|
+
focus: str | None
|
|
48
|
+
status: str
|
|
49
|
+
steps: list[PlanStep]
|
|
50
|
+
usage: Usage
|
|
51
|
+
account: Account
|
|
52
|
+
reason: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, kw_only=True)
|
|
56
|
+
class Plan(_PlanFields):
|
|
57
|
+
_client: Kirha = field(repr=False, compare=False)
|
|
58
|
+
_summarization: bool | Summarization | None = field(
|
|
59
|
+
default=None, repr=False, compare=False
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def execute(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
summarization: bool | Summarization | None = None,
|
|
66
|
+
include_data: bool | None = None,
|
|
67
|
+
include_planning: bool | None = None,
|
|
68
|
+
) -> SearchResult:
|
|
69
|
+
if summarization is None:
|
|
70
|
+
summarization = self._summarization
|
|
71
|
+
return self._client.execute_plan(
|
|
72
|
+
self.id,
|
|
73
|
+
summarization=summarization,
|
|
74
|
+
include_data=include_data,
|
|
75
|
+
include_planning=include_planning,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, kw_only=True)
|
|
80
|
+
class AsyncPlan(_PlanFields):
|
|
81
|
+
_client: AsyncKirha = field(repr=False, compare=False)
|
|
82
|
+
_summarization: bool | Summarization | None = field(
|
|
83
|
+
default=None, repr=False, compare=False
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def execute(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
summarization: bool | Summarization | None = None,
|
|
90
|
+
include_data: bool | None = None,
|
|
91
|
+
include_planning: bool | None = None,
|
|
92
|
+
) -> SearchResult:
|
|
93
|
+
if summarization is None:
|
|
94
|
+
summarization = self._summarization
|
|
95
|
+
return await self._client.execute_plan(
|
|
96
|
+
self.id,
|
|
97
|
+
summarization=summarization,
|
|
98
|
+
include_data=include_data,
|
|
99
|
+
include_planning=include_planning,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _BaseClient:
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
api_key: str | None,
|
|
107
|
+
focus: str | None,
|
|
108
|
+
summarization: bool | Summarization | None,
|
|
109
|
+
) -> None:
|
|
110
|
+
api_key = api_key or os.environ.get("KIRHA_API_KEY")
|
|
111
|
+
if not api_key:
|
|
112
|
+
raise ConfigurationError(
|
|
113
|
+
"api_key is required: pass it or set KIRHA_API_KEY"
|
|
114
|
+
)
|
|
115
|
+
self._headers = {"Authorization": f"Bearer {api_key}"}
|
|
116
|
+
self._focus = focus
|
|
117
|
+
self._summarization = summarization
|
|
118
|
+
|
|
119
|
+
def _search_body(
|
|
120
|
+
self,
|
|
121
|
+
query: str,
|
|
122
|
+
focus: str | None,
|
|
123
|
+
summarization: bool | Summarization | None,
|
|
124
|
+
include_data: bool | None,
|
|
125
|
+
include_planning: bool | None,
|
|
126
|
+
planning_runtime: PlanningRuntime | None,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
return _compact(
|
|
129
|
+
query=query,
|
|
130
|
+
focus=self._resolve_focus(focus),
|
|
131
|
+
summarization=self._summarization_body(summarization),
|
|
132
|
+
include_raw_data=include_data,
|
|
133
|
+
include_planning=include_planning,
|
|
134
|
+
planning_runtime=planning_runtime,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _plan_body(
|
|
138
|
+
self, query: str, focus: str | None, planning_runtime: PlanningRuntime | None
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
return _compact(
|
|
141
|
+
query=query,
|
|
142
|
+
focus=self._resolve_focus(focus),
|
|
143
|
+
planning_runtime=planning_runtime,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _run_body(
|
|
147
|
+
self,
|
|
148
|
+
plan_id: str,
|
|
149
|
+
summarization: bool | Summarization | None,
|
|
150
|
+
include_data: bool | None,
|
|
151
|
+
include_planning: bool | None,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
return _compact(
|
|
154
|
+
id=plan_id,
|
|
155
|
+
summarization=self._summarization_body(summarization),
|
|
156
|
+
include_raw_data=include_data,
|
|
157
|
+
include_planning=include_planning,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _resolve_focus(self, focus: str | None) -> str | None:
|
|
161
|
+
return (self._focus if focus is None else focus) or None
|
|
162
|
+
|
|
163
|
+
def _summarization_body(
|
|
164
|
+
self, summarization: bool | Summarization | None
|
|
165
|
+
) -> dict[str, Any] | None:
|
|
166
|
+
value = self._summarization if summarization is None else summarization
|
|
167
|
+
if isinstance(value, dict):
|
|
168
|
+
return _compact(enable=True, instruction=value.get("instruction"))
|
|
169
|
+
return {"enable": True} if value else None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Kirha(_BaseClient):
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
api_key: str | None = None,
|
|
176
|
+
*,
|
|
177
|
+
focus: str | None = None,
|
|
178
|
+
summarization: bool | Summarization | None = None,
|
|
179
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
180
|
+
) -> None:
|
|
181
|
+
super().__init__(api_key, focus, summarization)
|
|
182
|
+
self._http = httpx.Client(
|
|
183
|
+
base_url=BASE_URL, headers=self._headers, timeout=timeout
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def search(
|
|
187
|
+
self,
|
|
188
|
+
query: str,
|
|
189
|
+
*,
|
|
190
|
+
focus: str | None = None,
|
|
191
|
+
summarization: bool | Summarization | None = None,
|
|
192
|
+
include_data: bool | None = None,
|
|
193
|
+
include_planning: bool | None = None,
|
|
194
|
+
planning_runtime: PlanningRuntime | None = None,
|
|
195
|
+
) -> SearchResult:
|
|
196
|
+
body = self._search_body(
|
|
197
|
+
query,
|
|
198
|
+
focus,
|
|
199
|
+
summarization,
|
|
200
|
+
include_data,
|
|
201
|
+
include_planning,
|
|
202
|
+
planning_runtime,
|
|
203
|
+
)
|
|
204
|
+
return self._request("POST", "/search/v1", parse_search_result, json=body)
|
|
205
|
+
|
|
206
|
+
def plan(
|
|
207
|
+
self,
|
|
208
|
+
query: str,
|
|
209
|
+
*,
|
|
210
|
+
focus: str | None = None,
|
|
211
|
+
summarization: bool | Summarization | None = None,
|
|
212
|
+
planning_runtime: PlanningRuntime | None = None,
|
|
213
|
+
) -> Plan:
|
|
214
|
+
body = self._plan_body(query, focus, planning_runtime)
|
|
215
|
+
fields = self._request("POST", "/search/v1/plan", parse_plan, json=body)
|
|
216
|
+
return Plan(
|
|
217
|
+
**fields,
|
|
218
|
+
query=query,
|
|
219
|
+
focus=body.get("focus"),
|
|
220
|
+
_client=self,
|
|
221
|
+
_summarization=summarization,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def execute_plan(
|
|
225
|
+
self,
|
|
226
|
+
plan_id: str,
|
|
227
|
+
*,
|
|
228
|
+
summarization: bool | Summarization | None = None,
|
|
229
|
+
include_data: bool | None = None,
|
|
230
|
+
include_planning: bool | None = None,
|
|
231
|
+
) -> SearchResult:
|
|
232
|
+
body = self._run_body(plan_id, summarization, include_data, include_planning)
|
|
233
|
+
try:
|
|
234
|
+
return self._request(
|
|
235
|
+
"POST", "/search/v1/plan/run", parse_search_result, json=body
|
|
236
|
+
)
|
|
237
|
+
except ApiError as error:
|
|
238
|
+
if error.code == PLAN_EXPIRED_CODE:
|
|
239
|
+
raise PlanExpiredError(plan_id) from error
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
def tools(self, *, focus: str) -> list[Tool]:
|
|
243
|
+
return self._request(
|
|
244
|
+
"GET", "/tools/v1", parse_tools, params={"vertical_id": focus}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def execute_tool(
|
|
248
|
+
self, tool_name: str, input: dict[str, Any] | None = None
|
|
249
|
+
) -> ToolExecutionResult:
|
|
250
|
+
body = {"tool_name": tool_name, "input": input or {}}
|
|
251
|
+
return self._request(
|
|
252
|
+
"POST", "/tools/v1/execute", parse_tool_execution_result, json=body
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def close(self) -> None:
|
|
256
|
+
self._http.close()
|
|
257
|
+
|
|
258
|
+
def __enter__(self) -> Self:
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def __exit__(self, *_: object) -> None:
|
|
262
|
+
self.close()
|
|
263
|
+
|
|
264
|
+
def _request(
|
|
265
|
+
self, method: str, path: str, parse: Callable[[Any], T], **kwargs: Any
|
|
266
|
+
) -> T:
|
|
267
|
+
try:
|
|
268
|
+
response = self._http.request(method, path, **kwargs)
|
|
269
|
+
except httpx.TransportError as error:
|
|
270
|
+
raise NetworkError(
|
|
271
|
+
f"Request to {BASE_URL}{path} failed: {error!r}"
|
|
272
|
+
) from error
|
|
273
|
+
return _handle(response, parse)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class AsyncKirha(_BaseClient):
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
api_key: str | None = None,
|
|
280
|
+
*,
|
|
281
|
+
focus: str | None = None,
|
|
282
|
+
summarization: bool | Summarization | None = None,
|
|
283
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
284
|
+
) -> None:
|
|
285
|
+
super().__init__(api_key, focus, summarization)
|
|
286
|
+
self._http = httpx.AsyncClient(
|
|
287
|
+
base_url=BASE_URL, headers=self._headers, timeout=timeout
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def search(
|
|
291
|
+
self,
|
|
292
|
+
query: str,
|
|
293
|
+
*,
|
|
294
|
+
focus: str | None = None,
|
|
295
|
+
summarization: bool | Summarization | None = None,
|
|
296
|
+
include_data: bool | None = None,
|
|
297
|
+
include_planning: bool | None = None,
|
|
298
|
+
planning_runtime: PlanningRuntime | None = None,
|
|
299
|
+
) -> SearchResult:
|
|
300
|
+
body = self._search_body(
|
|
301
|
+
query,
|
|
302
|
+
focus,
|
|
303
|
+
summarization,
|
|
304
|
+
include_data,
|
|
305
|
+
include_planning,
|
|
306
|
+
planning_runtime,
|
|
307
|
+
)
|
|
308
|
+
return await self._request("POST", "/search/v1", parse_search_result, json=body)
|
|
309
|
+
|
|
310
|
+
async def plan(
|
|
311
|
+
self,
|
|
312
|
+
query: str,
|
|
313
|
+
*,
|
|
314
|
+
focus: str | None = None,
|
|
315
|
+
summarization: bool | Summarization | None = None,
|
|
316
|
+
planning_runtime: PlanningRuntime | None = None,
|
|
317
|
+
) -> AsyncPlan:
|
|
318
|
+
body = self._plan_body(query, focus, planning_runtime)
|
|
319
|
+
fields = await self._request("POST", "/search/v1/plan", parse_plan, json=body)
|
|
320
|
+
return AsyncPlan(
|
|
321
|
+
**fields,
|
|
322
|
+
query=query,
|
|
323
|
+
focus=body.get("focus"),
|
|
324
|
+
_client=self,
|
|
325
|
+
_summarization=summarization,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
async def execute_plan(
|
|
329
|
+
self,
|
|
330
|
+
plan_id: str,
|
|
331
|
+
*,
|
|
332
|
+
summarization: bool | Summarization | None = None,
|
|
333
|
+
include_data: bool | None = None,
|
|
334
|
+
include_planning: bool | None = None,
|
|
335
|
+
) -> SearchResult:
|
|
336
|
+
body = self._run_body(plan_id, summarization, include_data, include_planning)
|
|
337
|
+
try:
|
|
338
|
+
return await self._request(
|
|
339
|
+
"POST", "/search/v1/plan/run", parse_search_result, json=body
|
|
340
|
+
)
|
|
341
|
+
except ApiError as error:
|
|
342
|
+
if error.code == PLAN_EXPIRED_CODE:
|
|
343
|
+
raise PlanExpiredError(plan_id) from error
|
|
344
|
+
raise
|
|
345
|
+
|
|
346
|
+
async def tools(self, *, focus: str) -> list[Tool]:
|
|
347
|
+
return await self._request(
|
|
348
|
+
"GET", "/tools/v1", parse_tools, params={"vertical_id": focus}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
async def execute_tool(
|
|
352
|
+
self, tool_name: str, input: dict[str, Any] | None = None
|
|
353
|
+
) -> ToolExecutionResult:
|
|
354
|
+
body = {"tool_name": tool_name, "input": input or {}}
|
|
355
|
+
return await self._request(
|
|
356
|
+
"POST", "/tools/v1/execute", parse_tool_execution_result, json=body
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
async def close(self) -> None:
|
|
360
|
+
await self._http.aclose()
|
|
361
|
+
|
|
362
|
+
async def __aenter__(self) -> Self:
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
async def __aexit__(self, *_: object) -> None:
|
|
366
|
+
await self.close()
|
|
367
|
+
|
|
368
|
+
async def _request(
|
|
369
|
+
self, method: str, path: str, parse: Callable[[Any], T], **kwargs: Any
|
|
370
|
+
) -> T:
|
|
371
|
+
try:
|
|
372
|
+
response = await self._http.request(method, path, **kwargs)
|
|
373
|
+
except httpx.TransportError as error:
|
|
374
|
+
raise NetworkError(
|
|
375
|
+
f"Request to {BASE_URL}{path} failed: {error!r}"
|
|
376
|
+
) from error
|
|
377
|
+
return _handle(response, parse)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _compact(**fields: Any) -> dict[str, Any]:
|
|
381
|
+
return {key: value for key, value in fields.items() if value is not None}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _handle(response: httpx.Response, parse: Callable[[Any], T]) -> T:
|
|
385
|
+
if not response.is_success:
|
|
386
|
+
raise _api_error(response)
|
|
387
|
+
try:
|
|
388
|
+
return parse(response.json())
|
|
389
|
+
except (ValueError, KeyError, TypeError, AttributeError) as error:
|
|
390
|
+
raise ApiError(
|
|
391
|
+
"Unexpected response from the Kirha API",
|
|
392
|
+
response.status_code,
|
|
393
|
+
"invalid_response",
|
|
394
|
+
response.text,
|
|
395
|
+
) from error
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _api_error(response: httpx.Response) -> ApiError:
|
|
399
|
+
status = response.status_code
|
|
400
|
+
try:
|
|
401
|
+
body = response.json()
|
|
402
|
+
except ValueError:
|
|
403
|
+
body = None
|
|
404
|
+
|
|
405
|
+
nested = body.get("error") if isinstance(body, dict) else None
|
|
406
|
+
if isinstance(nested, dict) and "message" in nested:
|
|
407
|
+
message = nested["message"]
|
|
408
|
+
code = nested.get("code", "unknown_error")
|
|
409
|
+
details = nested.get("details")
|
|
410
|
+
elif isinstance(body, dict) and "reason" in body:
|
|
411
|
+
message, code, details = body["reason"], body.get("type", "unknown_error"), None
|
|
412
|
+
else:
|
|
413
|
+
message = f"HTTP {status}: {response.reason_phrase}"
|
|
414
|
+
code, details = "unknown_error", body
|
|
415
|
+
|
|
416
|
+
if status == 401:
|
|
417
|
+
return AuthenticationError(message, status, code, details)
|
|
418
|
+
if status == 429:
|
|
419
|
+
retry_after = response.headers.get("Retry-After", "")
|
|
420
|
+
return RateLimitError(
|
|
421
|
+
message,
|
|
422
|
+
status,
|
|
423
|
+
code,
|
|
424
|
+
details,
|
|
425
|
+
int(retry_after) if retry_after.isdigit() else None,
|
|
426
|
+
)
|
|
427
|
+
return ApiError(message, status, code, details)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KirhaError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigurationError(KirhaError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApiError(KirhaError):
|
|
13
|
+
def __init__(
|
|
14
|
+
self, message: str, status_code: int, code: str, details: Any = None
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.code = code
|
|
19
|
+
self.details = details
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthenticationError(ApiError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RateLimitError(ApiError):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
status_code: int,
|
|
31
|
+
code: str,
|
|
32
|
+
details: Any = None,
|
|
33
|
+
retry_after: int | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(message, status_code, code, details)
|
|
36
|
+
self.retry_after = retry_after
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlanExpiredError(KirhaError):
|
|
40
|
+
def __init__(self, plan_id: str) -> None:
|
|
41
|
+
super().__init__(f"Plan {plan_id} has expired (plans are valid for 5 minutes)")
|
|
42
|
+
self.plan_id = plan_id
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NetworkError(KirhaError):
|
|
46
|
+
pass
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Literal, TypedDict
|
|
3
|
+
|
|
4
|
+
PlanningRuntime = Literal["standard", "fast"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Summarization(TypedDict, total=False):
|
|
8
|
+
instruction: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
12
|
+
class Usage:
|
|
13
|
+
estimated: float
|
|
14
|
+
consumed: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
18
|
+
class Account:
|
|
19
|
+
balance: float
|
|
20
|
+
balance_timestamp: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
24
|
+
class PlanStep:
|
|
25
|
+
id: str
|
|
26
|
+
status: str
|
|
27
|
+
tool_name: str
|
|
28
|
+
parameters: dict[str, Any]
|
|
29
|
+
reasoning: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
33
|
+
class Planning:
|
|
34
|
+
status: str
|
|
35
|
+
steps: list[PlanStep]
|
|
36
|
+
reason: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
40
|
+
class SearchResult:
|
|
41
|
+
id: str
|
|
42
|
+
usage: Usage
|
|
43
|
+
account: Account
|
|
44
|
+
summary: str | None = None
|
|
45
|
+
data: list[dict[str, Any]] | None = None
|
|
46
|
+
planning: Planning | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
50
|
+
class Tool:
|
|
51
|
+
id: str
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
input_schema: dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
output_schema: dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
59
|
+
class ToolExecutionResult:
|
|
60
|
+
tool_name: str
|
|
61
|
+
output: Any
|
|
62
|
+
is_error: bool
|
|
63
|
+
usage: Usage
|
|
64
|
+
error_message: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_usage(data: Any) -> Usage:
|
|
68
|
+
return Usage(estimated=data["estimated"], consumed=data["consumed"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_account(data: Any) -> Account:
|
|
72
|
+
return Account(balance=data["balance"], balance_timestamp=data["balance_timestamp"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_steps(data: Any) -> list[PlanStep]:
|
|
76
|
+
return [
|
|
77
|
+
PlanStep(
|
|
78
|
+
id=step["id"],
|
|
79
|
+
status=step["status"],
|
|
80
|
+
tool_name=step["tool_name"],
|
|
81
|
+
parameters=step["parameters"],
|
|
82
|
+
reasoning=step.get("reasoning"),
|
|
83
|
+
)
|
|
84
|
+
for step in data
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_search_result(data: Any) -> SearchResult:
|
|
89
|
+
planning = data.get("planning")
|
|
90
|
+
return SearchResult(
|
|
91
|
+
id=data["id"],
|
|
92
|
+
usage=parse_usage(data["usage"]),
|
|
93
|
+
account=parse_account(data["account"]),
|
|
94
|
+
summary=data.get("summary"),
|
|
95
|
+
data=data.get("raw_data"),
|
|
96
|
+
planning=Planning(
|
|
97
|
+
status=planning["status"],
|
|
98
|
+
steps=parse_steps(planning["steps"]),
|
|
99
|
+
reason=planning.get("reason"),
|
|
100
|
+
)
|
|
101
|
+
if planning
|
|
102
|
+
else None,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_plan(data: Any) -> dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
"id": data["id"],
|
|
109
|
+
"status": data["status"],
|
|
110
|
+
"steps": parse_steps(data["steps"]),
|
|
111
|
+
"usage": parse_usage(data["usage"]),
|
|
112
|
+
"account": parse_account(data["account"]),
|
|
113
|
+
"reason": data.get("reason"),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_tools(data: Any) -> list[Tool]:
|
|
118
|
+
return [
|
|
119
|
+
Tool(
|
|
120
|
+
id=tool["id"],
|
|
121
|
+
name=tool["name"],
|
|
122
|
+
description=tool["description"],
|
|
123
|
+
input_schema=tool.get("input_schema") or {},
|
|
124
|
+
output_schema=tool.get("output_schema") or {},
|
|
125
|
+
)
|
|
126
|
+
for tool in data
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parse_tool_execution_result(data: Any) -> ToolExecutionResult:
|
|
131
|
+
return ToolExecutionResult(
|
|
132
|
+
tool_name=data["tool_name"],
|
|
133
|
+
output=data["output"],
|
|
134
|
+
is_error=data["is_error"],
|
|
135
|
+
usage=parse_usage(data["usage"]),
|
|
136
|
+
error_message=data.get("error_message"),
|
|
137
|
+
)
|
|
File without changes
|