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.
@@ -0,0 +1,8 @@
1
+ data/local/
2
+ .env
3
+ node_modules/
4
+ .DS_Store
5
+ *.log
6
+ # the static site (kiori site)
7
+ dist/
8
+ __pycache__/
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.
@@ -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.
@@ -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.
@@ -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"]