kiori-ai 2.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.
- kiori_ai-2.0.0/.gitignore +8 -0
- kiori_ai-2.0.0/LICENSE +21 -0
- kiori_ai-2.0.0/PKG-INFO +184 -0
- kiori_ai-2.0.0/README.md +161 -0
- kiori_ai-2.0.0/kiori.py +226 -0
- kiori_ai-2.0.0/pyproject.toml +56 -0
kiori_ai-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aras Aktas
|
|
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.
|
kiori_ai-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kiori-ai
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: The Python client for Kiori, brains for characters: memory, personality and who-knows-what for character AI. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://kiori.io
|
|
6
|
+
Project-URL: Documentation, https://kiori.io/docs
|
|
7
|
+
Project-URL: Source, https://github.com/kiori-ai/kiori/tree/main/sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/kiori-ai/kiori/issues
|
|
9
|
+
Author: Kiori
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai characters,character ai,character card,chatbot,kiori,llm,llm memory,long-term memory,memory,npc,personality,roleplay,sillytavern
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Games/Entertainment :: Role-Playing
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Kiori for JavaScript and Python
|
|
25
|
+
|
|
26
|
+
**Brains for characters.** Kiori gives every character in your app a mind of its own: a past, a personality, people they love and resent, memories that fade or stay, and a strict sense of what they could possibly know.
|
|
27
|
+
|
|
28
|
+
These are Kiori's official clients. Each is one file with no dependencies.
|
|
29
|
+
|
|
30
|
+
| | Install | Import |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| JavaScript: Node 18+ and browsers, types included | `npm install kiori` | `import { Kiori } from 'kiori'` |
|
|
33
|
+
| Python 3.8+ | `pip install kiori-ai` | `from kiori import Kiori` |
|
|
34
|
+
|
|
35
|
+
On PyPI the package is called **kiori-ai**, because the name `kiori` there belongs to an unrelated project. The module you import is still `kiori`. Don't install that other `kiori` package in the same environment: its `kiori` package would hide this module, and `from kiori import Kiori` would fail.
|
|
36
|
+
|
|
37
|
+
If you don't use a package manager, copy the single file instead: [kiori.mjs](https://kiori.io/sdk/kiori.mjs) with its types [kiori.d.ts](https://kiori.io/sdk/kiori.d.ts), or [kiori.py](https://kiori.io/sdk/kiori.py).
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
Kiori stores no brains and no chats. A brain is a JSON object you keep, in your database or on your player's device. You send it with each call, and calls that change it give it back.
|
|
42
|
+
|
|
43
|
+
- **create**: once per character, a brain from its card. Keep it as the character's template, and start each new conversation from a copy.
|
|
44
|
+
- **recall**: every turn, what the character knows right now, as a ready-to-use block for your prompt.
|
|
45
|
+
- **organize**: after each reply, the brain learns from the chat. Keep the brain that comes back.
|
|
46
|
+
- **check** (optional): the auditor reads your model's draft against what the character really knows, and fixes leaks, contradictions and invented pasts.
|
|
47
|
+
|
|
48
|
+
## Before you start
|
|
49
|
+
|
|
50
|
+
1. Ask for access on [kiori.io](https://kiori.io/#access). Your sign-in link opens the console.
|
|
51
|
+
2. In the console, paste your **brain API key** (OpenRouter, OpenAI or Gemini) and create a **Kiori key** for your app. Kiori builds and updates brains on your brain API key, and you pay your provider directly.
|
|
52
|
+
3. Keep the Kiori key on your server, for example in `KIORI_API_KEY`.
|
|
53
|
+
|
|
54
|
+
## Quickstart: JavaScript
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { Kiori } from 'kiori';
|
|
58
|
+
|
|
59
|
+
const kiori = new Kiori({ apiKey: process.env.KIORI_API_KEY });
|
|
60
|
+
|
|
61
|
+
// Once per character: a brain from its card (a Tavern/SillyTavern card, or name and description)
|
|
62
|
+
let brain = await kiori.create({ character_card: card });
|
|
63
|
+
|
|
64
|
+
// Every turn: what the character knows right now, for your prompt (the recent messages are enough)
|
|
65
|
+
const recent = messages.slice(-60);
|
|
66
|
+
const { text, check_recommended } = await kiori.recall({ brain, messages: recent });
|
|
67
|
+
let reply = await yourModel({ system: `${yourCharacterPrompt}\n\n${text}`, messages });
|
|
68
|
+
|
|
69
|
+
// Optional: when the player brings up the past, check the draft before it's shown
|
|
70
|
+
if (check_recommended) reply = (await kiori.check({ brain, messages: recent, draft: reply })).fixed;
|
|
71
|
+
|
|
72
|
+
// After the reply: the brain learns from the chat. Keep the brain that comes back
|
|
73
|
+
messages.push({ role: 'assistant', content: reply });
|
|
74
|
+
({ brain } = await kiori.organize({ brain, messages: messages.slice(-60) }));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The package is ESM only. From CommonJS, load it with `const { Kiori } = await import('kiori')`.
|
|
78
|
+
|
|
79
|
+
The client also runs in a browser, since all it needs is `fetch`. But a Kiori key inside a public web page is a key anyone can take: call Kiori from your server, or use a key that belongs to the person using the page, the way the SillyTavern extension does.
|
|
80
|
+
|
|
81
|
+
## Quickstart: Python
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import os
|
|
85
|
+
from kiori import Kiori
|
|
86
|
+
|
|
87
|
+
kiori = Kiori(os.environ["KIORI_API_KEY"])
|
|
88
|
+
|
|
89
|
+
# Once per character: a brain from its card (a dict, or a card PNG in base64)
|
|
90
|
+
brain = kiori.create(character_card=card)
|
|
91
|
+
|
|
92
|
+
# Every turn: what the character knows right now, for your prompt (the recent messages are enough)
|
|
93
|
+
recent = messages[-60:]
|
|
94
|
+
context = kiori.recall(brain=brain, messages=recent)
|
|
95
|
+
reply = your_model(system=your_character_prompt + "\n\n" + context["text"], messages=messages)
|
|
96
|
+
|
|
97
|
+
# Optional: when the player brings up the past, check the draft before it's shown
|
|
98
|
+
if context["check_recommended"]:
|
|
99
|
+
reply = kiori.check(draft=reply, brain=brain, messages=recent)["fixed"]
|
|
100
|
+
|
|
101
|
+
# After the reply: the brain learns from the chat. Keep the brain that comes back
|
|
102
|
+
messages.append({"role": "assistant", "content": reply})
|
|
103
|
+
brain = kiori.organize(brain=brain, messages=messages[-60:])["brain"]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The Python calls take keyword arguments named like the API's fields, and return plain dicts.
|
|
107
|
+
|
|
108
|
+
## Messages
|
|
109
|
+
|
|
110
|
+
Messages are OpenAI-style: `{ "id": "m41", "role": "user", "content": "..." }`, oldest first. For `organize`, send the conversation's recent messages as they are now (the last 60 is a good amount: at least the last 30, not only the new ones, and at most 400), with your own message ids if you have them, so that edits and rerolls are noticed and unlearned. Always keep the brain that comes back, even when nothing was learned.
|
|
111
|
+
|
|
112
|
+
## Group chats
|
|
113
|
+
|
|
114
|
+
Send every character's brain at once: `brains` instead of `brain`. Answers come back in the same order, and each brain learns only the scenes its character was in. Messages from characters carry their name: `{ role: "assistant", name: "Rook", content: "..." }`.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
[mira, rook] = (await kiori.organize({ brains: [mira, rook], messages, player: 'Leo' })).brains;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
mira, rook = kiori.organize(brains=[mira, rook], messages=messages, player="Leo")["brains"]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## All calls
|
|
125
|
+
|
|
126
|
+
| JavaScript | Python | |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `create({...})` | `create(...)` | a new brain: from `character_card`, or from `name`, `description` and `lore`. `depth: "full"` (default; a whole past, about a minute, on your brain API key) or `"light"` (from the card alone, free, at once) |
|
|
129
|
+
| `recall({...})` | `recall(...)` | what each character knows right now: `text` (one brain), `characters`, `check_recommended` |
|
|
130
|
+
| `organize({...})` | `organize(...)` | the brain learns from the chat: `brain` (or `brains`), `learned`, `rewound`, `waiting` |
|
|
131
|
+
| `check({...})` | `check(draft, ...)` | a draft reply checked against what each character knows: `ok`, `issues`, `fixed` |
|
|
132
|
+
| `exportObsidian(brain)` | `export_obsidian(brain)` | the brain as an Obsidian vault, as zip bytes |
|
|
133
|
+
| `personality()` | `personality()` | the 41 personality dimensions and the archetypes |
|
|
134
|
+
| `project.get()`, `project.update({...})`, `project.models()`, `project.usage(days)` | `project.get()`, `project.update(...)`, `project.models()`, `project.usage(days)` | your project, its brain API key and model, its usage |
|
|
135
|
+
| `project.keys.create(label)`, `project.keys.revoke(prefix)` | `project.create_key(label)`, `project.revoke_key(prefix)` | Kiori keys |
|
|
136
|
+
|
|
137
|
+
Changing the project (`project.update`, and making or revoking keys) takes an owner key: the one the console makes when you sign in with your link. The keys you make for your app can do everything else, so a key that leaks from your app can't move the project's calls elsewhere or take your keys away.
|
|
138
|
+
|
|
139
|
+
## Options
|
|
140
|
+
|
|
141
|
+
| JavaScript | Python | Default | |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `apiKey` | `api_key` (the first argument) | | your Kiori key |
|
|
144
|
+
| `baseUrl` | `base_url` | `https://api.kiori.io` | another Kiori, for example one you host yourself |
|
|
145
|
+
| `timeout` | `timeout` | 150000 ms in JavaScript, 150 s in Python | per request; building a full brain takes about a minute |
|
|
146
|
+
| `maxRetries` | `max_retries` | `2` | see Retries; `0` turns them off |
|
|
147
|
+
|
|
148
|
+
## Retries
|
|
149
|
+
|
|
150
|
+
Every brain call is safe to repeat, since Kiori keeps nothing between calls. So both clients ask again on their own, twice at most, when Kiori or your model provider is briefly unavailable or out of reach (408, 429, 502, 503 and the like, or a network error): after `Retry-After` when Kiori sends one (up to 20 s), else after about 1 s, then 2 s.
|
|
151
|
+
|
|
152
|
+
They don't ask again after a timeout (a full brain may still be building), for a missing or refused brain API key, when your provider is out of credits, or when the model's answers stayed unusable after Kiori's own tries (`INCOMPLETE`). Nor when Kiori asks to wait longer than 20 s: the error then carries the wait as `retryAfter` (JavaScript) or `retry_after` (Python).
|
|
153
|
+
|
|
154
|
+
`maxRetries` (JavaScript) or `max_retries` (Python) changes how many times; `0` turns retries off:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const kiori = new Kiori({ apiKey: process.env.KIORI_API_KEY, maxRetries: 0 });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
kiori = Kiori(os.environ["KIORI_API_KEY"], max_retries=0)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Errors
|
|
165
|
+
|
|
166
|
+
A failed call raises `KioriError` (`import { KioriError } from 'kiori'`, `from kiori import KioriError`), with:
|
|
167
|
+
|
|
168
|
+
- `status`: the HTTP status, or `0` when Kiori couldn't be reached (code `NETWORK`) or didn't answer in time (code `TIMEOUT`)
|
|
169
|
+
- `code`: for example `UNAUTHORIZED` (a wrong Kiori key), `NEEDS_KEY`, `BAD_KEY` and `CREDITS` (your brain API key is missing, refused, or out of credits), `RATE_LIMITED`, `PROVIDER`, `BAD_BRAIN`, and `REDIRECT` (the address answered with a redirect, which Kiori never sends: the clients don't follow one, so your key and brains don't go where it leads; check the base URL)
|
|
170
|
+
- `retryAfter` (JavaScript) or `retry_after` (Python): the seconds Kiori asked to wait, when it did
|
|
171
|
+
- `requestId` (JavaScript) or `request_id` (Python): the call's id, to quote when you write to us about it
|
|
172
|
+
|
|
173
|
+
A failed organize changes nothing: keep the brain you had, and the next call picks up where it left off. All the codes are in the [error table](https://kiori.io/docs/api#errors).
|
|
174
|
+
|
|
175
|
+
## Learn more
|
|
176
|
+
|
|
177
|
+
- [Integration guide](https://kiori.io/docs/integration): the calls, keeping brains, group chats, the drop-in, SillyTavern
|
|
178
|
+
- [Concepts](https://kiori.io/docs/concepts): the brain file, presence, organizer, auditor, sleep
|
|
179
|
+
- [API reference](https://kiori.io/docs/api), and [openapi.json](https://kiori.io/openapi.json) to generate a client in another language
|
|
180
|
+
- [kiori.io](https://kiori.io)
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT. The full text is in the LICENSE file that comes with the package. The license covers these client files, not the Kiori service.
|
kiori_ai-2.0.0/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Kiori for JavaScript and Python
|
|
2
|
+
|
|
3
|
+
**Brains for characters.** Kiori gives every character in your app a mind of its own: a past, a personality, people they love and resent, memories that fade or stay, and a strict sense of what they could possibly know.
|
|
4
|
+
|
|
5
|
+
These are Kiori's official clients. Each is one file with no dependencies.
|
|
6
|
+
|
|
7
|
+
| | Install | Import |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| JavaScript: Node 18+ and browsers, types included | `npm install kiori` | `import { Kiori } from 'kiori'` |
|
|
10
|
+
| Python 3.8+ | `pip install kiori-ai` | `from kiori import Kiori` |
|
|
11
|
+
|
|
12
|
+
On PyPI the package is called **kiori-ai**, because the name `kiori` there belongs to an unrelated project. The module you import is still `kiori`. Don't install that other `kiori` package in the same environment: its `kiori` package would hide this module, and `from kiori import Kiori` would fail.
|
|
13
|
+
|
|
14
|
+
If you don't use a package manager, copy the single file instead: [kiori.mjs](https://kiori.io/sdk/kiori.mjs) with its types [kiori.d.ts](https://kiori.io/sdk/kiori.d.ts), or [kiori.py](https://kiori.io/sdk/kiori.py).
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
Kiori stores no brains and no chats. A brain is a JSON object you keep, in your database or on your player's device. You send it with each call, and calls that change it give it back.
|
|
19
|
+
|
|
20
|
+
- **create**: once per character, a brain from its card. Keep it as the character's template, and start each new conversation from a copy.
|
|
21
|
+
- **recall**: every turn, what the character knows right now, as a ready-to-use block for your prompt.
|
|
22
|
+
- **organize**: after each reply, the brain learns from the chat. Keep the brain that comes back.
|
|
23
|
+
- **check** (optional): the auditor reads your model's draft against what the character really knows, and fixes leaks, contradictions and invented pasts.
|
|
24
|
+
|
|
25
|
+
## Before you start
|
|
26
|
+
|
|
27
|
+
1. Ask for access on [kiori.io](https://kiori.io/#access). Your sign-in link opens the console.
|
|
28
|
+
2. In the console, paste your **brain API key** (OpenRouter, OpenAI or Gemini) and create a **Kiori key** for your app. Kiori builds and updates brains on your brain API key, and you pay your provider directly.
|
|
29
|
+
3. Keep the Kiori key on your server, for example in `KIORI_API_KEY`.
|
|
30
|
+
|
|
31
|
+
## Quickstart: JavaScript
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { Kiori } from 'kiori';
|
|
35
|
+
|
|
36
|
+
const kiori = new Kiori({ apiKey: process.env.KIORI_API_KEY });
|
|
37
|
+
|
|
38
|
+
// Once per character: a brain from its card (a Tavern/SillyTavern card, or name and description)
|
|
39
|
+
let brain = await kiori.create({ character_card: card });
|
|
40
|
+
|
|
41
|
+
// Every turn: what the character knows right now, for your prompt (the recent messages are enough)
|
|
42
|
+
const recent = messages.slice(-60);
|
|
43
|
+
const { text, check_recommended } = await kiori.recall({ brain, messages: recent });
|
|
44
|
+
let reply = await yourModel({ system: `${yourCharacterPrompt}\n\n${text}`, messages });
|
|
45
|
+
|
|
46
|
+
// Optional: when the player brings up the past, check the draft before it's shown
|
|
47
|
+
if (check_recommended) reply = (await kiori.check({ brain, messages: recent, draft: reply })).fixed;
|
|
48
|
+
|
|
49
|
+
// After the reply: the brain learns from the chat. Keep the brain that comes back
|
|
50
|
+
messages.push({ role: 'assistant', content: reply });
|
|
51
|
+
({ brain } = await kiori.organize({ brain, messages: messages.slice(-60) }));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The package is ESM only. From CommonJS, load it with `const { Kiori } = await import('kiori')`.
|
|
55
|
+
|
|
56
|
+
The client also runs in a browser, since all it needs is `fetch`. But a Kiori key inside a public web page is a key anyone can take: call Kiori from your server, or use a key that belongs to the person using the page, the way the SillyTavern extension does.
|
|
57
|
+
|
|
58
|
+
## Quickstart: Python
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import os
|
|
62
|
+
from kiori import Kiori
|
|
63
|
+
|
|
64
|
+
kiori = Kiori(os.environ["KIORI_API_KEY"])
|
|
65
|
+
|
|
66
|
+
# Once per character: a brain from its card (a dict, or a card PNG in base64)
|
|
67
|
+
brain = kiori.create(character_card=card)
|
|
68
|
+
|
|
69
|
+
# Every turn: what the character knows right now, for your prompt (the recent messages are enough)
|
|
70
|
+
recent = messages[-60:]
|
|
71
|
+
context = kiori.recall(brain=brain, messages=recent)
|
|
72
|
+
reply = your_model(system=your_character_prompt + "\n\n" + context["text"], messages=messages)
|
|
73
|
+
|
|
74
|
+
# Optional: when the player brings up the past, check the draft before it's shown
|
|
75
|
+
if context["check_recommended"]:
|
|
76
|
+
reply = kiori.check(draft=reply, brain=brain, messages=recent)["fixed"]
|
|
77
|
+
|
|
78
|
+
# After the reply: the brain learns from the chat. Keep the brain that comes back
|
|
79
|
+
messages.append({"role": "assistant", "content": reply})
|
|
80
|
+
brain = kiori.organize(brain=brain, messages=messages[-60:])["brain"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The Python calls take keyword arguments named like the API's fields, and return plain dicts.
|
|
84
|
+
|
|
85
|
+
## Messages
|
|
86
|
+
|
|
87
|
+
Messages are OpenAI-style: `{ "id": "m41", "role": "user", "content": "..." }`, oldest first. For `organize`, send the conversation's recent messages as they are now (the last 60 is a good amount: at least the last 30, not only the new ones, and at most 400), with your own message ids if you have them, so that edits and rerolls are noticed and unlearned. Always keep the brain that comes back, even when nothing was learned.
|
|
88
|
+
|
|
89
|
+
## Group chats
|
|
90
|
+
|
|
91
|
+
Send every character's brain at once: `brains` instead of `brain`. Answers come back in the same order, and each brain learns only the scenes its character was in. Messages from characters carry their name: `{ role: "assistant", name: "Rook", content: "..." }`.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
[mira, rook] = (await kiori.organize({ brains: [mira, rook], messages, player: 'Leo' })).brains;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
mira, rook = kiori.organize(brains=[mira, rook], messages=messages, player="Leo")["brains"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## All calls
|
|
102
|
+
|
|
103
|
+
| JavaScript | Python | |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `create({...})` | `create(...)` | a new brain: from `character_card`, or from `name`, `description` and `lore`. `depth: "full"` (default; a whole past, about a minute, on your brain API key) or `"light"` (from the card alone, free, at once) |
|
|
106
|
+
| `recall({...})` | `recall(...)` | what each character knows right now: `text` (one brain), `characters`, `check_recommended` |
|
|
107
|
+
| `organize({...})` | `organize(...)` | the brain learns from the chat: `brain` (or `brains`), `learned`, `rewound`, `waiting` |
|
|
108
|
+
| `check({...})` | `check(draft, ...)` | a draft reply checked against what each character knows: `ok`, `issues`, `fixed` |
|
|
109
|
+
| `exportObsidian(brain)` | `export_obsidian(brain)` | the brain as an Obsidian vault, as zip bytes |
|
|
110
|
+
| `personality()` | `personality()` | the 41 personality dimensions and the archetypes |
|
|
111
|
+
| `project.get()`, `project.update({...})`, `project.models()`, `project.usage(days)` | `project.get()`, `project.update(...)`, `project.models()`, `project.usage(days)` | your project, its brain API key and model, its usage |
|
|
112
|
+
| `project.keys.create(label)`, `project.keys.revoke(prefix)` | `project.create_key(label)`, `project.revoke_key(prefix)` | Kiori keys |
|
|
113
|
+
|
|
114
|
+
Changing the project (`project.update`, and making or revoking keys) takes an owner key: the one the console makes when you sign in with your link. The keys you make for your app can do everything else, so a key that leaks from your app can't move the project's calls elsewhere or take your keys away.
|
|
115
|
+
|
|
116
|
+
## Options
|
|
117
|
+
|
|
118
|
+
| JavaScript | Python | Default | |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `apiKey` | `api_key` (the first argument) | | your Kiori key |
|
|
121
|
+
| `baseUrl` | `base_url` | `https://api.kiori.io` | another Kiori, for example one you host yourself |
|
|
122
|
+
| `timeout` | `timeout` | 150000 ms in JavaScript, 150 s in Python | per request; building a full brain takes about a minute |
|
|
123
|
+
| `maxRetries` | `max_retries` | `2` | see Retries; `0` turns them off |
|
|
124
|
+
|
|
125
|
+
## Retries
|
|
126
|
+
|
|
127
|
+
Every brain call is safe to repeat, since Kiori keeps nothing between calls. So both clients ask again on their own, twice at most, when Kiori or your model provider is briefly unavailable or out of reach (408, 429, 502, 503 and the like, or a network error): after `Retry-After` when Kiori sends one (up to 20 s), else after about 1 s, then 2 s.
|
|
128
|
+
|
|
129
|
+
They don't ask again after a timeout (a full brain may still be building), for a missing or refused brain API key, when your provider is out of credits, or when the model's answers stayed unusable after Kiori's own tries (`INCOMPLETE`). Nor when Kiori asks to wait longer than 20 s: the error then carries the wait as `retryAfter` (JavaScript) or `retry_after` (Python).
|
|
130
|
+
|
|
131
|
+
`maxRetries` (JavaScript) or `max_retries` (Python) changes how many times; `0` turns retries off:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const kiori = new Kiori({ apiKey: process.env.KIORI_API_KEY, maxRetries: 0 });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
kiori = Kiori(os.environ["KIORI_API_KEY"], max_retries=0)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Errors
|
|
142
|
+
|
|
143
|
+
A failed call raises `KioriError` (`import { KioriError } from 'kiori'`, `from kiori import KioriError`), with:
|
|
144
|
+
|
|
145
|
+
- `status`: the HTTP status, or `0` when Kiori couldn't be reached (code `NETWORK`) or didn't answer in time (code `TIMEOUT`)
|
|
146
|
+
- `code`: for example `UNAUTHORIZED` (a wrong Kiori key), `NEEDS_KEY`, `BAD_KEY` and `CREDITS` (your brain API key is missing, refused, or out of credits), `RATE_LIMITED`, `PROVIDER`, `BAD_BRAIN`, and `REDIRECT` (the address answered with a redirect, which Kiori never sends: the clients don't follow one, so your key and brains don't go where it leads; check the base URL)
|
|
147
|
+
- `retryAfter` (JavaScript) or `retry_after` (Python): the seconds Kiori asked to wait, when it did
|
|
148
|
+
- `requestId` (JavaScript) or `request_id` (Python): the call's id, to quote when you write to us about it
|
|
149
|
+
|
|
150
|
+
A failed organize changes nothing: keep the brain you had, and the next call picks up where it left off. All the codes are in the [error table](https://kiori.io/docs/api#errors).
|
|
151
|
+
|
|
152
|
+
## Learn more
|
|
153
|
+
|
|
154
|
+
- [Integration guide](https://kiori.io/docs/integration): the calls, keeping brains, group chats, the drop-in, SillyTavern
|
|
155
|
+
- [Concepts](https://kiori.io/docs/concepts): the brain file, presence, organizer, auditor, sleep
|
|
156
|
+
- [API reference](https://kiori.io/docs/api), and [openapi.json](https://kiori.io/openapi.json) to generate a client in another language
|
|
157
|
+
- [kiori.io](https://kiori.io)
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT. The full text is in the LICENSE file that comes with the package. The license covers these client files, not the Kiori service.
|
kiori_ai-2.0.0/kiori.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Kiori Python client. Python 3.8+, no dependencies.
|
|
2
|
+
|
|
3
|
+
Kiori stores no brains and no chats. A brain is a dict you keep (in your database, or on your player's device) and
|
|
4
|
+
send with each call; calls that change it give it back.
|
|
5
|
+
|
|
6
|
+
from kiori import Kiori
|
|
7
|
+
kiori = Kiori(os.environ["KIORI_API_KEY"])
|
|
8
|
+
|
|
9
|
+
# once per character: a brain from its card; keep it, and start each new conversation from a copy of it
|
|
10
|
+
brain = kiori.create(character_card=card)
|
|
11
|
+
|
|
12
|
+
# every turn: what the character knows right now, for your prompt (messages OpenAI-style, with your ids; the
|
|
13
|
+
# recent ones are enough)
|
|
14
|
+
recent = messages[-60:]
|
|
15
|
+
context = kiori.recall(brain=brain, messages=recent)
|
|
16
|
+
system_prompt += "\\n\\n" + context["text"]
|
|
17
|
+
|
|
18
|
+
# after the reply: the brain learns from the chat; keep the brain that comes back
|
|
19
|
+
brain = kiori.organize(brain=brain, messages=messages[-60:])["brain"]
|
|
20
|
+
|
|
21
|
+
# optionally, when context["check_recommended"]: let the auditor check your model's draft
|
|
22
|
+
verdict = kiori.check(brain=brain, messages=recent, draft=reply)
|
|
23
|
+
|
|
24
|
+
A call is asked again on its own (twice at most) when Kiori or the model provider is briefly unavailable or out of
|
|
25
|
+
reach. That's safe: Kiori keeps nothing between calls, so the same brain in gives the same kind of brain out.
|
|
26
|
+
"""
|
|
27
|
+
import email.utils
|
|
28
|
+
import http.client
|
|
29
|
+
import json
|
|
30
|
+
import random
|
|
31
|
+
import socket
|
|
32
|
+
import time
|
|
33
|
+
import urllib.error
|
|
34
|
+
import urllib.parse
|
|
35
|
+
import urllib.request
|
|
36
|
+
|
|
37
|
+
__all__ = ["Kiori", "KioriError"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class KioriError(Exception):
|
|
41
|
+
"""status is the HTTP status, or 0 when Kiori couldn't be reached (code NETWORK) or didn't answer in time (TIMEOUT);
|
|
42
|
+
retry_after is the seconds Kiori asked to wait (Retry-After), when it did; request_id is the call's id, to quote
|
|
43
|
+
when you write to us about it."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, status, message, code=None, retry_after=None, request_id=None):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.status = status
|
|
48
|
+
self.code = code
|
|
49
|
+
self.retry_after = retry_after
|
|
50
|
+
self.request_id = request_id
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _seconds_after(value):
|
|
54
|
+
"""Retry-After in seconds (it's seconds, or a date); None when there is none."""
|
|
55
|
+
if not value:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return max(0.0, float(value))
|
|
59
|
+
except ValueError:
|
|
60
|
+
pass
|
|
61
|
+
try:
|
|
62
|
+
return max(0.0, email.utils.parsedate_to_datetime(value).timestamp() - time.time())
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Asked again: Kiori or the model provider was busy or briefly down (or a gateway in front of them failed), or Kiori
|
|
68
|
+
# was out of reach. Not a timeout (the work may still be going on), not a key or credit problem, not answers that
|
|
69
|
+
# stayed unusable after Kiori's own tries (INCOMPLETE), and not when Kiori asks to wait more than 20 seconds. A call
|
|
70
|
+
# that isn't safe to repeat (a new key) is asked again only after a 429.
|
|
71
|
+
_AGAIN = {408, 429, 502, 503, 520, 521, 522, 523, 530}
|
|
72
|
+
_FINAL = {"NEEDS_KEY", "NO_KEY", "BAD_KEY", "CREDITS", "INCOMPLETE"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _retry_delay(e, attempt, safe):
|
|
76
|
+
if e.code == "NETWORK":
|
|
77
|
+
again = safe
|
|
78
|
+
else:
|
|
79
|
+
again = e.status in _AGAIN and e.code not in _FINAL and (safe or e.status == 429)
|
|
80
|
+
if not again:
|
|
81
|
+
return None
|
|
82
|
+
if e.retry_after is not None:
|
|
83
|
+
return e.retry_after if e.retry_after <= 20 else None
|
|
84
|
+
return 2 ** attempt * random.uniform(0.75, 1.25)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _id(value):
|
|
88
|
+
return urllib.parse.quote(str(value), safe="")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _q(**params):
|
|
92
|
+
pairs = {k: v for k, v in params.items() if v is not None}
|
|
93
|
+
return "?" + urllib.parse.urlencode(pairs) if pairs else ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _Project:
|
|
97
|
+
def __init__(self, client):
|
|
98
|
+
self._c = client
|
|
99
|
+
|
|
100
|
+
def get(self):
|
|
101
|
+
return self._c.request("GET", "/v1/project")
|
|
102
|
+
|
|
103
|
+
def update(self, **body):
|
|
104
|
+
"""name=..., llm={"key": ...} (your brain API key: the provider is recognized from it), llm={"model": ...}."""
|
|
105
|
+
return self._c.request("PATCH", "/v1/project", body)
|
|
106
|
+
|
|
107
|
+
def models(self):
|
|
108
|
+
"""The models your brain API key can use, the suggested one first."""
|
|
109
|
+
return self._c.request("GET", "/v1/project/models")
|
|
110
|
+
|
|
111
|
+
def usage(self, days=None):
|
|
112
|
+
return self._c.request("GET", "/v1/usage" + _q(days=days))
|
|
113
|
+
|
|
114
|
+
def create_key(self, label=None):
|
|
115
|
+
"""A new API key for this project (shown once)."""
|
|
116
|
+
return self._c.request("POST", "/v1/project/keys", {"label": label}, safe=False)
|
|
117
|
+
|
|
118
|
+
def revoke_key(self, prefix):
|
|
119
|
+
return self._c.request("DELETE", "/v1/project/keys/" + _id(prefix))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _brains(brain, brains):
|
|
123
|
+
if (brain is None) == (brains is None):
|
|
124
|
+
raise ValueError("Kiori: send brain= (one) or brains= (a group chat)")
|
|
125
|
+
return {"brain": brain} if brain is not None else {"brains": brains}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
129
|
+
# Kiori never redirects: a redirect means base_url leads somewhere else, and following it would send the key (and
|
|
130
|
+
# the brain) there. Turned down, it comes back as the HTTPError of its status.
|
|
131
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
_open = urllib.request.build_opener(_NoRedirect).open
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Kiori:
|
|
139
|
+
def __init__(self, api_key, base_url="https://api.kiori.io", timeout=150, max_retries=2):
|
|
140
|
+
"""timeout: seconds per request before giving up (code TIMEOUT); building a full brain takes about a minute.
|
|
141
|
+
max_retries: how many times a call is asked again when Kiori or the model provider is briefly unavailable
|
|
142
|
+
(after Retry-After, else about 1 s, then 2 s); 0 never."""
|
|
143
|
+
if not api_key:
|
|
144
|
+
raise ValueError("Kiori: api_key is required")
|
|
145
|
+
self.api_key = api_key
|
|
146
|
+
self.base_url = base_url.rstrip("/")
|
|
147
|
+
self.timeout = timeout
|
|
148
|
+
self.max_retries = max_retries
|
|
149
|
+
self.project = _Project(self)
|
|
150
|
+
|
|
151
|
+
def create(self, **body):
|
|
152
|
+
"""A new brain. character_card= takes a Tavern v1/v2/v3 card (dict, or a PNG in base64); or name=,
|
|
153
|
+
description=, lore=. depth="full" (a whole past, about a minute, on your brain API key) or "light" (free)."""
|
|
154
|
+
return self.request("POST", "/v1/brains", body)["brain"]
|
|
155
|
+
|
|
156
|
+
def recall(self, brain=None, brains=None, messages=None, **options):
|
|
157
|
+
"""What each character knows right now: {"text" (one brain), "characters", "check_recommended"}."""
|
|
158
|
+
return self.request("POST", "/v1/brains/recall", dict(options, messages=messages, **_brains(brain, brains)))
|
|
159
|
+
|
|
160
|
+
def organize(self, messages, brain=None, brains=None, **options):
|
|
161
|
+
"""The chat moved on: {"brain" (or "brains"), "learned", "rewound", "waiting"}. Send the recent messages as
|
|
162
|
+
they are now, oldest first, with your message ids if you have them, and keep what comes back.
|
|
163
|
+
Options: player=, hold= (newest messages left for later, default 4), every= (learn once this many settled
|
|
164
|
+
messages are new, default 12), now=True."""
|
|
165
|
+
return self.request("POST", "/v1/brains/organize", dict(options, messages=messages, **_brains(brain, brains)))
|
|
166
|
+
|
|
167
|
+
def check(self, draft, brain=None, brains=None, messages=None, **options):
|
|
168
|
+
"""A draft reply checked against what each character knows: {"ok", "issues", "fixed"}."""
|
|
169
|
+
return self.request("POST", "/v1/brains/check", dict(options, draft=draft, messages=messages, **_brains(brain, brains)))
|
|
170
|
+
|
|
171
|
+
def export_obsidian(self, brain):
|
|
172
|
+
"""A brain as an Obsidian vault: the zip's bytes."""
|
|
173
|
+
return self.request("POST", "/v1/brains/export", {"brain": brain}, raw=True)
|
|
174
|
+
|
|
175
|
+
def personality(self):
|
|
176
|
+
"""The 41 personality dimensions and the archetypes."""
|
|
177
|
+
return self.request("GET", "/v1/personality")
|
|
178
|
+
|
|
179
|
+
def request(self, method, path, body=None, raw=False, safe=True):
|
|
180
|
+
"""safe=False for a call that isn't safe to repeat (see _retry_delay)."""
|
|
181
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
182
|
+
attempt = 0
|
|
183
|
+
while True:
|
|
184
|
+
try:
|
|
185
|
+
payload = self._send(method, path, data)
|
|
186
|
+
break
|
|
187
|
+
except KioriError as e:
|
|
188
|
+
wait = _retry_delay(e, attempt, safe) if attempt < self.max_retries else None
|
|
189
|
+
if wait is None:
|
|
190
|
+
raise
|
|
191
|
+
time.sleep(wait)
|
|
192
|
+
attempt += 1
|
|
193
|
+
if raw:
|
|
194
|
+
return payload
|
|
195
|
+
return json.loads(payload.decode("utf-8")) if payload else None
|
|
196
|
+
|
|
197
|
+
def _send(self, method, path, data):
|
|
198
|
+
req = urllib.request.Request(self.base_url + path, data=data, method=method)
|
|
199
|
+
req.add_header("Authorization", "Bearer " + self.api_key)
|
|
200
|
+
if data is not None:
|
|
201
|
+
req.add_header("Content-Type", "application/json")
|
|
202
|
+
try:
|
|
203
|
+
with _open(req, timeout=self.timeout) as res:
|
|
204
|
+
return res.read()
|
|
205
|
+
except urllib.error.HTTPError as e:
|
|
206
|
+
if 300 <= e.code < 400:
|
|
207
|
+
raise KioriError(e.code, "%s answered with a redirect, and Kiori never redirects: check base_url"
|
|
208
|
+
% self.base_url, "REDIRECT") from None
|
|
209
|
+
try:
|
|
210
|
+
err = json.loads(e.read().decode("utf-8")).get("error") or {}
|
|
211
|
+
except Exception:
|
|
212
|
+
err = {}
|
|
213
|
+
if not isinstance(err, dict):
|
|
214
|
+
err = {}
|
|
215
|
+
message = err.get("message") or "HTTP %d" % e.code
|
|
216
|
+
raise KioriError(e.code, message, err.get("code"), _seconds_after(e.headers.get("Retry-After")),
|
|
217
|
+
err.get("request_id") or e.headers.get("X-Request-Id")) from None
|
|
218
|
+
except (socket.timeout, TimeoutError):
|
|
219
|
+
raise KioriError(0, "Kiori did not answer within %ss" % self.timeout, "TIMEOUT") from None
|
|
220
|
+
except urllib.error.URLError as e:
|
|
221
|
+
if isinstance(e.reason, (socket.timeout, TimeoutError)):
|
|
222
|
+
raise KioriError(0, "Kiori did not answer within %ss" % self.timeout, "TIMEOUT") from None
|
|
223
|
+
raise KioriError(0, "Can't reach Kiori at %s (%s)" % (self.base_url, e.reason), "NETWORK") from None
|
|
224
|
+
except (http.client.HTTPException, OSError) as e:
|
|
225
|
+
# The connection dropped after the request went out, or the answer was cut off halfway
|
|
226
|
+
raise KioriError(0, "Can't reach Kiori at %s (%s)" % (self.base_url, type(e).__name__), "NETWORK") from None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Kiori's Python client on PyPI. The distribution is "kiori-ai" (the name "kiori" on PyPI belongs to an unrelated
|
|
2
|
+
# project); the module stays kiori.py, so it's `pip install kiori-ai`, then `from kiori import Kiori`.
|
|
3
|
+
|
|
4
|
+
[build-system]
|
|
5
|
+
requires = ["hatchling>=1.27"]
|
|
6
|
+
build-backend = "hatchling.build"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "kiori-ai"
|
|
10
|
+
version = "2.0.0"
|
|
11
|
+
description = "The Python client for Kiori, brains for characters: memory, personality and who-knows-what for character AI. No dependencies."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
authors = [{ name = "Kiori" }]
|
|
17
|
+
keywords = [
|
|
18
|
+
"kiori",
|
|
19
|
+
"character ai",
|
|
20
|
+
"ai characters",
|
|
21
|
+
"memory",
|
|
22
|
+
"llm memory",
|
|
23
|
+
"long-term memory",
|
|
24
|
+
"personality",
|
|
25
|
+
"roleplay",
|
|
26
|
+
"npc",
|
|
27
|
+
"chatbot",
|
|
28
|
+
"llm",
|
|
29
|
+
"sillytavern",
|
|
30
|
+
"character card",
|
|
31
|
+
]
|
|
32
|
+
classifiers = [
|
|
33
|
+
"Development Status :: 4 - Beta",
|
|
34
|
+
"Intended Audience :: Developers",
|
|
35
|
+
"Operating System :: OS Independent",
|
|
36
|
+
"Programming Language :: Python :: 3",
|
|
37
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
38
|
+
"Topic :: Games/Entertainment :: Role-Playing",
|
|
39
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
40
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
41
|
+
]
|
|
42
|
+
dependencies = []
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://kiori.io"
|
|
46
|
+
Documentation = "https://kiori.io/docs"
|
|
47
|
+
Source = "https://github.com/kiori-ai/kiori/tree/main/sdk"
|
|
48
|
+
Issues = "https://github.com/kiori-ai/kiori/issues"
|
|
49
|
+
|
|
50
|
+
# One module, kiori.py. The JavaScript files next to it belong to the npm package and stay out of the wheel and the
|
|
51
|
+
# sdist (the sdist also always carries pyproject.toml, the readme and the license).
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
only-include = ["kiori.py"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
only-include = ["kiori.py"]
|