drekai 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.
- drekai-0.1.0/.gitignore +35 -0
- drekai-0.1.0/LICENSE +21 -0
- drekai-0.1.0/MANIFEST.in +3 -0
- drekai-0.1.0/PKG-INFO +242 -0
- drekai-0.1.0/README.md +212 -0
- drekai-0.1.0/__init__.py +27 -0
- drekai-0.1.0/assets/.gitkeep +2 -0
- drekai-0.1.0/chat.py +255 -0
- drekai-0.1.0/messaging.py +32 -0
- drekai-0.1.0/model.py +59 -0
- drekai-0.1.0/pyproject.toml +57 -0
- drekai-0.1.0/settings.py +8 -0
- drekai-0.1.0/setup.py +5 -0
- drekai-0.1.0/tools/__init__.py +4 -0
- drekai-0.1.0/tools/tools.py +108 -0
drekai-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Byte-compiled / optimized
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# Testing / coverage
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
htmlcov/
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
|
28
|
+
|
|
29
|
+
# mypy / ruff
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
drekai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drek124
|
|
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.
|
drekai-0.1.0/MANIFEST.in
ADDED
drekai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: drekai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modern, async-first Python wrapper for OpenAI-compatible LLM APIs with built-in tool use.
|
|
5
|
+
Project-URL: Homepage, https://github.com/drek124/drekai
|
|
6
|
+
Project-URL: Repository, https://github.com/drek124/drekai
|
|
7
|
+
Project-URL: Issues, https://github.com/drek124/drekai/issues
|
|
8
|
+
Author-email: drek <drek.dev124@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,async,chat,llm,openai,tools
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: openai>=1.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# DrekAI
|
|
32
|
+
|
|
33
|
+
A modern, **async-first** Python wrapper for OpenAI-compatible LLM APIs with built-in **tool/function calling** support.
|
|
34
|
+
|
|
35
|
+
> **Works with OpenAI, Gemini (via proxy), and any OpenAI-compatible endpoint.**
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Features
|
|
40
|
+
|
|
41
|
+
- **Async-native** – built on `httpx` / `openai` async client
|
|
42
|
+
- **Tool use** – define Python functions as AI-callable tools with automatic parameter injection
|
|
43
|
+
- **Streaming** – stream responses token-by-token with a simple callback
|
|
44
|
+
- **Sandboxed parameters** – keep secrets invisible to the LLM while still passing them to tools
|
|
45
|
+
- **Multi-turn chats** – first-class conversation history management
|
|
46
|
+
- **Image support** – send local files, URLs, or base64 images inline
|
|
47
|
+
- **Thinking / reasoning** – support for extended-thinking models with effort control
|
|
48
|
+
- **Gemini compatibility** – automatic tool-call shimming for Gemini models behind OpenAI proxies
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install drekai
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or install from source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/drek124/drekai.git
|
|
62
|
+
cd drekai
|
|
63
|
+
pip install .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🚀 Quick Start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
from drekai import Model, Chatbot
|
|
73
|
+
|
|
74
|
+
# 1. Define a model endpoint
|
|
75
|
+
model = Model("gpt-4o", "https://api.openai.com/v1", api_key="sk-...")
|
|
76
|
+
|
|
77
|
+
# 2. Create a chatbot
|
|
78
|
+
bot = Chatbot(
|
|
79
|
+
name="Helper",
|
|
80
|
+
system_prompt="You are a helpful assistant.",
|
|
81
|
+
parent_model=model,
|
|
82
|
+
temperature=0.7,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def main():
|
|
86
|
+
# One-shot generation
|
|
87
|
+
response = await bot.generate_text("Hello!")
|
|
88
|
+
print(response.choices[0].message.content)
|
|
89
|
+
|
|
90
|
+
# Multi-turn chat
|
|
91
|
+
chat = bot.start_chat()
|
|
92
|
+
r1 = await chat.generate_reply("What's the weather in Tokyo?")
|
|
93
|
+
r2 = await chat.generate_reply("And in London?")
|
|
94
|
+
print(r2.choices[0].message.content)
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🛠️ Tool Use
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from drekai import Model, Chatbot
|
|
105
|
+
from drekai.tools import Tool, ToolParameter
|
|
106
|
+
|
|
107
|
+
model = Model("gpt-4o", "https://api.openai.com/v1", api_key="sk-...")
|
|
108
|
+
bot = Chatbot("Assistant", "You are a helpful assistant.", model)
|
|
109
|
+
|
|
110
|
+
async def get_weather(city: str) -> str:
|
|
111
|
+
"""Get the current weather for a city."""
|
|
112
|
+
# In a real app, call a weather API here
|
|
113
|
+
return f"The weather in {city} is sunny, 25°C."
|
|
114
|
+
|
|
115
|
+
tools = [
|
|
116
|
+
Tool(
|
|
117
|
+
"get_weather",
|
|
118
|
+
[ToolParameter("city", "City name", type=str)],
|
|
119
|
+
callback=get_weather,
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
async def main():
|
|
124
|
+
chat = bot.start_chat()
|
|
125
|
+
response = await chat.generate_reply(
|
|
126
|
+
"What's the weather in Paris?",
|
|
127
|
+
tools=tools,
|
|
128
|
+
)
|
|
129
|
+
print(response.choices[0].message.content)
|
|
130
|
+
|
|
131
|
+
asyncio.run(main())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Sandboxed Parameters
|
|
135
|
+
|
|
136
|
+
Keep secrets like user IDs invisible to the LLM:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
async def get_friends(user, limit: int = 50) -> str:
|
|
140
|
+
"""Fetch the user's friends list."""
|
|
141
|
+
return str(user.get_friends(limit=limit))
|
|
142
|
+
|
|
143
|
+
tools = [
|
|
144
|
+
Tool(
|
|
145
|
+
"get_friends",
|
|
146
|
+
[ToolParameter("limit", "Max friends to return", required=False, type=int)],
|
|
147
|
+
callback=get_friends,
|
|
148
|
+
sandbox_params=["user"], # hidden from the LLM
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
await chat.generate_reply(
|
|
153
|
+
"Who are my friends?",
|
|
154
|
+
tools=tools,
|
|
155
|
+
sandbox_params={"user": current_user}, # injected at call time
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🖼️ Images
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from drekai.messaging import Image
|
|
165
|
+
|
|
166
|
+
# Local file
|
|
167
|
+
await chat.generate_reply(
|
|
168
|
+
"Describe this image.",
|
|
169
|
+
items=[Image("/path/to/photo.jpg")],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# URL
|
|
173
|
+
await chat.generate_reply(
|
|
174
|
+
"Describe this image.",
|
|
175
|
+
items=[Image("https://example.com/photo.jpg")],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Base64
|
|
179
|
+
await chat.generate_reply(
|
|
180
|
+
"Describe this image.",
|
|
181
|
+
items=[Image(base64_data, b64=True)],
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 📖 API Reference
|
|
188
|
+
|
|
189
|
+
### `Model(model_id, base_url, *, api_key)`
|
|
190
|
+
Root model representing an API endpoint.
|
|
191
|
+
|
|
192
|
+
### `Chatbot(name, system_prompt, parent_model, *, api_key, temperature, thinking, reasoning_effort)`
|
|
193
|
+
A named chatbot built on a model.
|
|
194
|
+
|
|
195
|
+
### `Chat`
|
|
196
|
+
Created via `chatbot.start_chat()`. Manages conversation history.
|
|
197
|
+
|
|
198
|
+
| Method | Description |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `generate_reply(...)` | Generate the next response |
|
|
201
|
+
| `add_message_to_context(content, role)` | Manually add a message |
|
|
202
|
+
| `clear()` | Reset history (keeps system prompt) |
|
|
203
|
+
| `delete_first_message(role)` | Remove first message with given role |
|
|
204
|
+
| `stop_live_generation()` | Stop an active stream mid-generation |
|
|
205
|
+
|
|
206
|
+
### `ChatSettings(max_tokens, show_tool_error_type)`
|
|
207
|
+
Per-chat configuration.
|
|
208
|
+
|
|
209
|
+
### `Tool(name, params, callback, sandbox_params)`
|
|
210
|
+
An AI-callable function.
|
|
211
|
+
|
|
212
|
+
### `ToolParameter(name, description, type, required)`
|
|
213
|
+
Describes a tool parameter.
|
|
214
|
+
|
|
215
|
+
### `MessageItem` / `Image`
|
|
216
|
+
Chat message attachments.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 🧪 Development
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Clone and install in editable mode with dev deps
|
|
224
|
+
git clone https://github.com/drek124/drekai.git
|
|
225
|
+
cd drekai
|
|
226
|
+
pip install -e ".[dev]"
|
|
227
|
+
|
|
228
|
+
# Lint
|
|
229
|
+
ruff check .
|
|
230
|
+
|
|
231
|
+
# Type check
|
|
232
|
+
mypy .
|
|
233
|
+
|
|
234
|
+
# Test
|
|
235
|
+
pytest
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 📄 License
|
|
241
|
+
|
|
242
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
drekai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# DrekAI
|
|
2
|
+
|
|
3
|
+
A modern, **async-first** Python wrapper for OpenAI-compatible LLM APIs with built-in **tool/function calling** support.
|
|
4
|
+
|
|
5
|
+
> **Works with OpenAI, Gemini (via proxy), and any OpenAI-compatible endpoint.**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- **Async-native** – built on `httpx` / `openai` async client
|
|
12
|
+
- **Tool use** – define Python functions as AI-callable tools with automatic parameter injection
|
|
13
|
+
- **Streaming** – stream responses token-by-token with a simple callback
|
|
14
|
+
- **Sandboxed parameters** – keep secrets invisible to the LLM while still passing them to tools
|
|
15
|
+
- **Multi-turn chats** – first-class conversation history management
|
|
16
|
+
- **Image support** – send local files, URLs, or base64 images inline
|
|
17
|
+
- **Thinking / reasoning** – support for extended-thinking models with effort control
|
|
18
|
+
- **Gemini compatibility** – automatic tool-call shimming for Gemini models behind OpenAI proxies
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install drekai
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install from source:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/drek124/drekai.git
|
|
32
|
+
cd drekai
|
|
33
|
+
pip install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from drekai import Model, Chatbot
|
|
43
|
+
|
|
44
|
+
# 1. Define a model endpoint
|
|
45
|
+
model = Model("gpt-4o", "https://api.openai.com/v1", api_key="sk-...")
|
|
46
|
+
|
|
47
|
+
# 2. Create a chatbot
|
|
48
|
+
bot = Chatbot(
|
|
49
|
+
name="Helper",
|
|
50
|
+
system_prompt="You are a helpful assistant.",
|
|
51
|
+
parent_model=model,
|
|
52
|
+
temperature=0.7,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
# One-shot generation
|
|
57
|
+
response = await bot.generate_text("Hello!")
|
|
58
|
+
print(response.choices[0].message.content)
|
|
59
|
+
|
|
60
|
+
# Multi-turn chat
|
|
61
|
+
chat = bot.start_chat()
|
|
62
|
+
r1 = await chat.generate_reply("What's the weather in Tokyo?")
|
|
63
|
+
r2 = await chat.generate_reply("And in London?")
|
|
64
|
+
print(r2.choices[0].message.content)
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🛠️ Tool Use
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from drekai import Model, Chatbot
|
|
75
|
+
from drekai.tools import Tool, ToolParameter
|
|
76
|
+
|
|
77
|
+
model = Model("gpt-4o", "https://api.openai.com/v1", api_key="sk-...")
|
|
78
|
+
bot = Chatbot("Assistant", "You are a helpful assistant.", model)
|
|
79
|
+
|
|
80
|
+
async def get_weather(city: str) -> str:
|
|
81
|
+
"""Get the current weather for a city."""
|
|
82
|
+
# In a real app, call a weather API here
|
|
83
|
+
return f"The weather in {city} is sunny, 25°C."
|
|
84
|
+
|
|
85
|
+
tools = [
|
|
86
|
+
Tool(
|
|
87
|
+
"get_weather",
|
|
88
|
+
[ToolParameter("city", "City name", type=str)],
|
|
89
|
+
callback=get_weather,
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
async def main():
|
|
94
|
+
chat = bot.start_chat()
|
|
95
|
+
response = await chat.generate_reply(
|
|
96
|
+
"What's the weather in Paris?",
|
|
97
|
+
tools=tools,
|
|
98
|
+
)
|
|
99
|
+
print(response.choices[0].message.content)
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Sandboxed Parameters
|
|
105
|
+
|
|
106
|
+
Keep secrets like user IDs invisible to the LLM:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
async def get_friends(user, limit: int = 50) -> str:
|
|
110
|
+
"""Fetch the user's friends list."""
|
|
111
|
+
return str(user.get_friends(limit=limit))
|
|
112
|
+
|
|
113
|
+
tools = [
|
|
114
|
+
Tool(
|
|
115
|
+
"get_friends",
|
|
116
|
+
[ToolParameter("limit", "Max friends to return", required=False, type=int)],
|
|
117
|
+
callback=get_friends,
|
|
118
|
+
sandbox_params=["user"], # hidden from the LLM
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
await chat.generate_reply(
|
|
123
|
+
"Who are my friends?",
|
|
124
|
+
tools=tools,
|
|
125
|
+
sandbox_params={"user": current_user}, # injected at call time
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🖼️ Images
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from drekai.messaging import Image
|
|
135
|
+
|
|
136
|
+
# Local file
|
|
137
|
+
await chat.generate_reply(
|
|
138
|
+
"Describe this image.",
|
|
139
|
+
items=[Image("/path/to/photo.jpg")],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# URL
|
|
143
|
+
await chat.generate_reply(
|
|
144
|
+
"Describe this image.",
|
|
145
|
+
items=[Image("https://example.com/photo.jpg")],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Base64
|
|
149
|
+
await chat.generate_reply(
|
|
150
|
+
"Describe this image.",
|
|
151
|
+
items=[Image(base64_data, b64=True)],
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📖 API Reference
|
|
158
|
+
|
|
159
|
+
### `Model(model_id, base_url, *, api_key)`
|
|
160
|
+
Root model representing an API endpoint.
|
|
161
|
+
|
|
162
|
+
### `Chatbot(name, system_prompt, parent_model, *, api_key, temperature, thinking, reasoning_effort)`
|
|
163
|
+
A named chatbot built on a model.
|
|
164
|
+
|
|
165
|
+
### `Chat`
|
|
166
|
+
Created via `chatbot.start_chat()`. Manages conversation history.
|
|
167
|
+
|
|
168
|
+
| Method | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `generate_reply(...)` | Generate the next response |
|
|
171
|
+
| `add_message_to_context(content, role)` | Manually add a message |
|
|
172
|
+
| `clear()` | Reset history (keeps system prompt) |
|
|
173
|
+
| `delete_first_message(role)` | Remove first message with given role |
|
|
174
|
+
| `stop_live_generation()` | Stop an active stream mid-generation |
|
|
175
|
+
|
|
176
|
+
### `ChatSettings(max_tokens, show_tool_error_type)`
|
|
177
|
+
Per-chat configuration.
|
|
178
|
+
|
|
179
|
+
### `Tool(name, params, callback, sandbox_params)`
|
|
180
|
+
An AI-callable function.
|
|
181
|
+
|
|
182
|
+
### `ToolParameter(name, description, type, required)`
|
|
183
|
+
Describes a tool parameter.
|
|
184
|
+
|
|
185
|
+
### `MessageItem` / `Image`
|
|
186
|
+
Chat message attachments.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🧪 Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Clone and install in editable mode with dev deps
|
|
194
|
+
git clone https://github.com/drek124/drekai.git
|
|
195
|
+
cd drekai
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
|
|
198
|
+
# Lint
|
|
199
|
+
ruff check .
|
|
200
|
+
|
|
201
|
+
# Type check
|
|
202
|
+
mypy .
|
|
203
|
+
|
|
204
|
+
# Test
|
|
205
|
+
pytest
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 📄 License
|
|
211
|
+
|
|
212
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
drekai-0.1.0/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""### DrekAI OpenAI API Wrapper
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drek124
|
|
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.
|
|
22
|
+
"""
|
|
23
|
+
from .chat import Chat
|
|
24
|
+
from .model import Model, Chatbot
|
|
25
|
+
from .settings import ChatSettings
|
|
26
|
+
|
|
27
|
+
__all__ = ["Chat", "Chatbot", "Model", "ChatSettings"]
|
drekai-0.1.0/chat.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
import json
|
|
5
|
+
import traceback
|
|
6
|
+
import logging
|
|
7
|
+
from .tools import Tool
|
|
8
|
+
from .messaging import MessageItem
|
|
9
|
+
from .settings import ChatSettings
|
|
10
|
+
from openai import HttpxBinaryResponseContent
|
|
11
|
+
DEFAULT_OPTIONS = ChatSettings()
|
|
12
|
+
|
|
13
|
+
class Chat:
|
|
14
|
+
def __init__(self, model, options):
|
|
15
|
+
self.model = model
|
|
16
|
+
self.messages: list[dict, dict] = [{
|
|
17
|
+
"role": "system",
|
|
18
|
+
"content": model.system_prompt
|
|
19
|
+
}]
|
|
20
|
+
self.total_tokens: int = 0
|
|
21
|
+
self.options: ChatSettings = options or DEFAULT_OPTIONS
|
|
22
|
+
self._stop_event: asyncio.Event | None = None
|
|
23
|
+
self.id: str = (self.model.id + '-' + ''.join(random.choice(string.ascii_lowercase) for _ in range(15))).upper()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_message_to_context(self, content: str, role: str = 'user') -> list[dict, dict]:
|
|
28
|
+
"""Adds a message to the messages context."""
|
|
29
|
+
self.messages.append({
|
|
30
|
+
"role": role,
|
|
31
|
+
"content": str(content)
|
|
32
|
+
})
|
|
33
|
+
return self.messages
|
|
34
|
+
|
|
35
|
+
async def generate_reply(
|
|
36
|
+
self, prompt: str = None, prompt_role: str = 'user', *, thinking_callback = None, return_response_data: bool = True,
|
|
37
|
+
tools: list[Tool] = None, sandbox_params = {}, stream_callback = None, options: ChatSettings = None,
|
|
38
|
+
items: list[MessageItem] = [],
|
|
39
|
+
) -> HttpxBinaryResponseContent:
|
|
40
|
+
"""## Response Generation
|
|
41
|
+
### Parameters
|
|
42
|
+
- prompt: Add a prompt before generating
|
|
43
|
+
- prompt_role: The role of the prompt; such as `'user'`, `'system'` & `'assistant'`
|
|
44
|
+
- thinking_callback: The `async callback(thought: str)`; executed when a tool is called
|
|
45
|
+
- return_response_data: Whether to directly return the AI's response or the full API response.
|
|
46
|
+
- tools: The list of available tools
|
|
47
|
+
- sandbox_params: The sandboxed parameters that are invisible to the model; if a tool call from this request requires a certain sandboxed parameter, it will be fetched from here. (e.g. `{'user_id': user.id}`)
|
|
48
|
+
- stream_callback: `async callback(chunk: str)`; sets `stream` to `True`
|
|
49
|
+
- items: The list of chat items to insert in the message (e.g. images)
|
|
50
|
+
- options: The overwrite of the chat settings
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
contents = [] + [item.value for item in items]
|
|
54
|
+
if prompt:
|
|
55
|
+
contents.append({"type": "text", "text": str(prompt)})
|
|
56
|
+
|
|
57
|
+
if contents:
|
|
58
|
+
self.messages.append({
|
|
59
|
+
"role": prompt_role,
|
|
60
|
+
"content": contents
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
params = {}
|
|
64
|
+
|
|
65
|
+
if not options:
|
|
66
|
+
options = self.options
|
|
67
|
+
|
|
68
|
+
if options.max_tokens:
|
|
69
|
+
params['max_tokens'] = options.max_tokens
|
|
70
|
+
if self.model.parent_model.tool_compatability == 'gemini':
|
|
71
|
+
|
|
72
|
+
for tool in tools:
|
|
73
|
+
if 'required' not in tool.raw.get('function', {}):
|
|
74
|
+
continue
|
|
75
|
+
preq = tool.raw['function']['required']
|
|
76
|
+
tool.raw['function']['parameters']['required'] = preq
|
|
77
|
+
tool.raw['function'].pop('required')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if tools:
|
|
81
|
+
params['tools'] = [tool.raw for tool in tools]
|
|
82
|
+
|
|
83
|
+
if not self.model.thinking:
|
|
84
|
+
params["extra_body"] = {
|
|
85
|
+
"thinking": {"type": "disabled"}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stream = True if stream_callback else False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
r = await self.model.client.chat.completions.create(
|
|
92
|
+
model=self.model.parent_model.id,
|
|
93
|
+
messages=self.messages,
|
|
94
|
+
stream=stream,
|
|
95
|
+
reasoning_effort=self.model.reasoning_effort,
|
|
96
|
+
temperature=self.model.temperature,
|
|
97
|
+
**params
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if stream_callback:
|
|
101
|
+
full_content = ""
|
|
102
|
+
reasoning_content = ""
|
|
103
|
+
tool_calls_dict = {}
|
|
104
|
+
self._stop_event = asyncio.Event()
|
|
105
|
+
async for chunk in r:
|
|
106
|
+
if self._stop_event.is_set():
|
|
107
|
+
await r.close()
|
|
108
|
+
full_content += '\n[GENERATION_STOPPED]'
|
|
109
|
+
assistant_msg = {"role": "assistant", "content": full_content}
|
|
110
|
+
self.messages.append(assistant_msg)
|
|
111
|
+
return assistant_msg if return_response_data else full_content
|
|
112
|
+
# Track token usage if included in stream
|
|
113
|
+
if chunk.usage:
|
|
114
|
+
self.total_tokens += chunk.usage.total_tokens
|
|
115
|
+
|
|
116
|
+
if not chunk.choices:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
delta = chunk.choices[0].delta
|
|
120
|
+
|
|
121
|
+
# 1. Accumulate text content & stream it
|
|
122
|
+
if delta.content:
|
|
123
|
+
full_content += delta.content
|
|
124
|
+
await stream_callback(delta.content)
|
|
125
|
+
|
|
126
|
+
if hasattr(delta, 'reasoning_content') and delta.reasoning_content:
|
|
127
|
+
reasoning_content += delta.reasoning_content
|
|
128
|
+
if thinking_callback:
|
|
129
|
+
await thinking_callback(delta.reasoning_content)
|
|
130
|
+
|
|
131
|
+
# 3. Assemble streamed tool calls
|
|
132
|
+
if delta.tool_calls:
|
|
133
|
+
for tc in delta.tool_calls:
|
|
134
|
+
idx = tc.index
|
|
135
|
+
if idx not in tool_calls_dict:
|
|
136
|
+
tool_calls_dict[idx] = tc
|
|
137
|
+
else:
|
|
138
|
+
# Concatenate incoming argument chunks
|
|
139
|
+
if tc.function and tc.function.arguments:
|
|
140
|
+
tool_calls_dict[idx].function.arguments += tc.function.arguments
|
|
141
|
+
|
|
142
|
+
# Convert assembled tool calls to list
|
|
143
|
+
tool_calls = list(tool_calls_dict.values()) if tool_calls_dict else None
|
|
144
|
+
text_response = full_content
|
|
145
|
+
|
|
146
|
+
# Append assembled message to history
|
|
147
|
+
assistant_msg = {"role": "assistant", "content": full_content or None}
|
|
148
|
+
if tool_calls:
|
|
149
|
+
assistant_msg["tool_calls"] = [
|
|
150
|
+
{
|
|
151
|
+
"id": tc.id,
|
|
152
|
+
"type": "function",
|
|
153
|
+
"function": {
|
|
154
|
+
"name": tc.function.name,
|
|
155
|
+
"arguments": tc.function.arguments
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for tc in tool_calls
|
|
159
|
+
]
|
|
160
|
+
self.messages.append(assistant_msg)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
# Non-streaming path
|
|
164
|
+
self.messages.append(r.choices[0].message)
|
|
165
|
+
self.total_tokens += r.usage.total_tokens
|
|
166
|
+
text_response = r.choices[0].message.content
|
|
167
|
+
tool_calls = r.choices[0].message.tool_calls
|
|
168
|
+
reasoning_content = getattr(r.choices[0].message, 'reasoning_content', None)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Tool Execution Phase
|
|
174
|
+
if tool_calls:
|
|
175
|
+
if getattr(self.model.parent_model, 'tool_compatability', None) == 'gemini':
|
|
176
|
+
# Convert Pydantic SDK object to dict if non-streaming
|
|
177
|
+
if not isinstance(self.messages[-1], dict) and hasattr(self.messages[-1], "model_dump"):
|
|
178
|
+
self.messages[-1] = self.messages[-1].model_dump()
|
|
179
|
+
|
|
180
|
+
last_msg = self.messages[-1]
|
|
181
|
+
|
|
182
|
+
if isinstance(last_msg, dict) and "tool_calls" in last_msg:
|
|
183
|
+
for tc in last_msg["tool_calls"]:
|
|
184
|
+
# 1. Attach directly to tool call dict
|
|
185
|
+
tc["thought_signature"] = "skip_thought_signature_validator"
|
|
186
|
+
|
|
187
|
+
# 2. Attach to provider_specific_fields / extra_content (OpenAI proxy target)
|
|
188
|
+
tc["extra_content"] = {"google": {"thought_signature": "skip_thought_signature_validator"}}
|
|
189
|
+
for ii, tc in enumerate(tool_calls, start=1):
|
|
190
|
+
tool = None
|
|
191
|
+
sandbox_params['_tc_index'] = ii # Add tool call index incase a tool needs it
|
|
192
|
+
for t in tools:
|
|
193
|
+
if t.name == tc.function.name:
|
|
194
|
+
tool = t
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
args = json.loads(tc.function.arguments)
|
|
198
|
+
for sp in tool.sandbox_params:
|
|
199
|
+
args[sp] = sandbox_params[sp]
|
|
200
|
+
|
|
201
|
+
if reasoning_content and thinking_callback:
|
|
202
|
+
await thinking_callback(reasoning_content)
|
|
203
|
+
try:
|
|
204
|
+
tool_response = await tool.callback(**args)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
tb = traceback.format_exc()
|
|
207
|
+
tool_response = f"ERROR: {e}" if options.show_tool_error_type else "TOOL ERROR"
|
|
208
|
+
logging.error(tb)
|
|
209
|
+
last_tool = (ii == len(tool_calls))
|
|
210
|
+
response = await self.tool_response(
|
|
211
|
+
tc.id,
|
|
212
|
+
tool_response,
|
|
213
|
+
stream_callback=stream_callback,
|
|
214
|
+
generate_response=last_tool,
|
|
215
|
+
sandbox_params=sandbox_params,
|
|
216
|
+
tools=tools
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not last_tool:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
if return_response_data:
|
|
223
|
+
return response
|
|
224
|
+
|
|
225
|
+
return text_response
|
|
226
|
+
|
|
227
|
+
return r if return_response_data else text_response
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def stop_live_generation(self):
|
|
231
|
+
"Stops the live generation"
|
|
232
|
+
self._stop_event.set()
|
|
233
|
+
|
|
234
|
+
def clear(self):
|
|
235
|
+
"""Wipes the chat; the system prompt doesn't get removed."""
|
|
236
|
+
self.messages = []
|
|
237
|
+
if self.model.system_prompt:
|
|
238
|
+
self.messages.append({
|
|
239
|
+
"role": "system",
|
|
240
|
+
"content": self.model.system_prompt
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
def delete_first_message(self, role: str):
|
|
244
|
+
"""Deletes the first message in the context with the targeted role. System prompt cannot be deleted."""
|
|
245
|
+
for msg in self.messages:
|
|
246
|
+
if msg == self.messages[0]: continue
|
|
247
|
+
if msg['role'] != role: continue
|
|
248
|
+
self.messages.remove(msg)
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
async def tool_response(self, call_id: str, content, *, stream_callback, generate_response: bool = True, sandbox_params: dict = {}, tools = []):
|
|
252
|
+
self.messages.append({'role': 'tool', 'tool_call_id': call_id, 'content': str(content)})
|
|
253
|
+
if not generate_response: return
|
|
254
|
+
response = await self.generate_reply(return_response_data=True, tools=tools, sandbox_params=sandbox_params, stream_callback=stream_callback)
|
|
255
|
+
return response
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
class MessageItem:
|
|
5
|
+
def __init__(self, value: dict):
|
|
6
|
+
self.value: dict = value
|
|
7
|
+
|
|
8
|
+
class Image(MessageItem):
|
|
9
|
+
def __init__(self, fp: str, *, b64: bool = False):
|
|
10
|
+
"""Create an image ready to be appended to the chat
|
|
11
|
+
- fp: The local path of the image, or image URL.
|
|
12
|
+
- b64: `fp` representing the Base64 value of the image, not a path."""
|
|
13
|
+
|
|
14
|
+
if b64:
|
|
15
|
+
path = f"data:image/jpeg;base64,{fp}"
|
|
16
|
+
|
|
17
|
+
elif 'http' in fp.lower():
|
|
18
|
+
path = fp
|
|
19
|
+
else:
|
|
20
|
+
self.path: Path | str = Path(fp)
|
|
21
|
+
if not self.path.is_file():
|
|
22
|
+
raise ValueError(f"The file \"{self.path.name}\" not found")
|
|
23
|
+
with open(self.path, "rb") as image_file:
|
|
24
|
+
b64 = base64.b64encode(image_file.read()).decode("utf-8")
|
|
25
|
+
path = f"data:image/jpeg;base64,{b64}"
|
|
26
|
+
image_file.close()
|
|
27
|
+
|
|
28
|
+
super().__init__(value={
|
|
29
|
+
"type": "image_url",
|
|
30
|
+
"image_url": {
|
|
31
|
+
"url": path
|
|
32
|
+
}})
|
drekai-0.1.0/model.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from . import Chat
|
|
3
|
+
from openai import AsyncOpenAI, HttpxBinaryResponseContent
|
|
4
|
+
MISSING = object()
|
|
5
|
+
|
|
6
|
+
class Model:
|
|
7
|
+
def __init__(self, model_id: str, base_url: str, *, api_key = MISSING):
|
|
8
|
+
"""Create a new root model
|
|
9
|
+
- id: The ID identified by the API endpoint, example: `drek-v6.7-pro`
|
|
10
|
+
- base_url: The URL of the OpenAI-supported endpoint
|
|
11
|
+
- api_key: The global API key for all child chatbots."""
|
|
12
|
+
self.id: str = model_id
|
|
13
|
+
self.base_url: str = base_url
|
|
14
|
+
if 'gemini' in self.id:
|
|
15
|
+
self.tool_compatability: str | None = 'gemini'
|
|
16
|
+
else:
|
|
17
|
+
self.tool_compatability: str = None
|
|
18
|
+
if api_key is not MISSING:
|
|
19
|
+
self.api_key = api_key
|
|
20
|
+
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
21
|
+
else:
|
|
22
|
+
self.api_key = None
|
|
23
|
+
self.client: AsyncOpenAI = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Chatbot:
|
|
27
|
+
def __init__(
|
|
28
|
+
self, name: str, system_prompt: str, parent_model: Model, *,
|
|
29
|
+
api_key = MISSING, temperature: float = 1.0, thinking: bool = True, reasoning_effort: Literal['low', 'medium', 'high', 'max'] = 'low',
|
|
30
|
+
id = None
|
|
31
|
+
):
|
|
32
|
+
"""Create your new chatbot
|
|
33
|
+
## Parameters
|
|
34
|
+
- name: The name of the chatbot; invisible to the LLM
|
|
35
|
+
- system_prompt: The system prompt for the chatbot
|
|
36
|
+
- parent_model: The root model that will be utilized
|
|
37
|
+
- temperature: The temperature represents the creativity/charm level of the model.
|
|
38
|
+
- api_key: Your authorized API key to the endpoint; if not present, `parent_model.api_key` will be utilized instead.
|
|
39
|
+
- thinking: The ability for the LLM to think
|
|
40
|
+
- reasoning_effort: When `thinking=True`, specify the effort the LLM would utilize to think
|
|
41
|
+
- id: Use it when needed for your own backend development"""
|
|
42
|
+
self.name: str = name
|
|
43
|
+
self.id = id if id is not None else self.name.lower().strip().replace(' ', '-')
|
|
44
|
+
self.parent_model: Model = parent_model
|
|
45
|
+
self.system_prompt = system_prompt
|
|
46
|
+
self.temperature: float = temperature
|
|
47
|
+
self.reasoning_effort: str = reasoning_effort
|
|
48
|
+
self.client = self.parent_model.client or AsyncOpenAI(api_key=api_key, base_url=parent_model.base_url)
|
|
49
|
+
self.thinking: bool = thinking
|
|
50
|
+
|
|
51
|
+
async def generate_text(self, prompt: str, role: str = 'user', **kwargs) -> HttpxBinaryResponseContent:
|
|
52
|
+
"""Generate a text response."""
|
|
53
|
+
chat = self.start_chat()
|
|
54
|
+
return await chat.generate_reply(prompt, role, **kwargs)
|
|
55
|
+
|
|
56
|
+
def start_chat(self) -> Chat:
|
|
57
|
+
"Start a new chat with the chatbot"
|
|
58
|
+
return Chat(self)
|
|
59
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "drekai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A modern, async-first Python wrapper for OpenAI-compatible LLM APIs with built-in tool use."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "drek", email = "drek.dev124@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["openai", "llm", "chat", "tools", "async", "ai"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
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
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"openai>=1.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/drek124/drekai"
|
|
34
|
+
Repository = "https://github.com/drek124/drekai"
|
|
35
|
+
Issues = "https://github.com/drek124/drekai/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=7.0",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
"ruff",
|
|
42
|
+
"mypy",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["."]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
line-length = 100
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "N", "UP", "B"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.10"
|
|
57
|
+
strict = true
|
drekai-0.1.0/settings.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class ChatSettings:
|
|
2
|
+
def __init__(self, *, max_tokens: int = None, show_tool_error_type: bool = True):
|
|
3
|
+
"""## DrekAI Chat Settings
|
|
4
|
+
### Parameters
|
|
5
|
+
- max_tokens: The maximum input + output completion tokens allowed per response
|
|
6
|
+
- show_tool_error_type: Shows the LLM the exception that occured during the tool's execution; if `False`, the LLM recieves "TOOL ERROR". If `True`, the LLM recieves the exception message (Not traceback)."""
|
|
7
|
+
self.max_tokens = max_tokens
|
|
8
|
+
self.show_tool_error_type = show_tool_error_type
|
drekai-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from types import NoneType
|
|
3
|
+
from typing import Literal, Any
|
|
4
|
+
|
|
5
|
+
TOOL_PARAM_CLASSES = {
|
|
6
|
+
str: "string",
|
|
7
|
+
float: "number",
|
|
8
|
+
int: "integer",
|
|
9
|
+
bool: "boolean",
|
|
10
|
+
dict: "object",
|
|
11
|
+
list: "array",
|
|
12
|
+
None: "null",
|
|
13
|
+
NoneType: "null"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ToolParameter:
|
|
17
|
+
def __init__(self, name: str, description: str, *, required: bool = True,
|
|
18
|
+
type: Any | Literal['string', 'number', 'integer', 'boolean',
|
|
19
|
+
'object', 'array', 'null'] = str):
|
|
20
|
+
"""## Tool Parameter
|
|
21
|
+
## Parameters
|
|
22
|
+
- name: The name of the parameter; must match the present parameter in the callback.
|
|
23
|
+
- description: The description of this parameter that will be given to the LLM.
|
|
24
|
+
- type: The instance/object type of this parameter; Supported data types:
|
|
25
|
+
```python
|
|
26
|
+
str, int, float, bool, dict, list, None
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
self.name: str = name
|
|
30
|
+
self.description: str = description
|
|
31
|
+
self.required: bool = required
|
|
32
|
+
if isinstance(type, str):
|
|
33
|
+
self.type: str = type
|
|
34
|
+
else:
|
|
35
|
+
if type not in TOOL_PARAM_CLASSES:
|
|
36
|
+
raise ValueError(f"Unknown tool parameter type \"{type}\"")
|
|
37
|
+
self.type: str = TOOL_PARAM_CLASSES[type]
|
|
38
|
+
|
|
39
|
+
class Tool:
|
|
40
|
+
def __init__(
|
|
41
|
+
self, name: str, params: list[ToolParameter] = [], *,
|
|
42
|
+
callback = None, sandbox_params: list[str] = []
|
|
43
|
+
):
|
|
44
|
+
"""## Create an AI Tool
|
|
45
|
+
### Parameters
|
|
46
|
+
- name: The name of the tool
|
|
47
|
+
- params: The parameters of the tool
|
|
48
|
+
- callback: The callback of the tool; the description of the tool is decided by the callback's docs
|
|
49
|
+
- sandbox_params: The parameters hidden from the model and passed into the tool parameters.
|
|
50
|
+
By using sandboxed parameters, it makes it impossible for the AI to manage data it isn't supposed to manage.
|
|
51
|
+
Built-in sandbox parameters:
|
|
52
|
+
- `_tc_index`: The index of the tool call
|
|
53
|
+
### Example
|
|
54
|
+
*Creating a tool that fetches a user's friends*
|
|
55
|
+
```python
|
|
56
|
+
from DrekAI import Chatbot
|
|
57
|
+
from DrekAI.tools import Tool, ToolParameter
|
|
58
|
+
|
|
59
|
+
assistant = Chatbot("Assistant", "You are a helpful assistant")
|
|
60
|
+
chat = assistant.start_chat()
|
|
61
|
+
|
|
62
|
+
async def get_friends(user, limit: int = 100) -> str:
|
|
63
|
+
"Fetch the user's friends list" # The description passed to the LLM
|
|
64
|
+
result: str | Any = user.get_friends(limit=limit)
|
|
65
|
+
return "Friends list:" + result # The data returned to the LLM
|
|
66
|
+
tools = [
|
|
67
|
+
Tool(
|
|
68
|
+
"get_friends", [ToolParameter("limit", "The limit of the fetched users", required=False, type=int)],
|
|
69
|
+
callback=get_friends,
|
|
70
|
+
sandbox_params=["user"] # Accepted sandboxed parameters
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
user = ...
|
|
75
|
+
async def main():
|
|
76
|
+
response = await chat.generate_reply("Who is in my friend's list?",
|
|
77
|
+
tools=tools,
|
|
78
|
+
sandbox_params={"user": user} # Impossible for the LLM to fetch another user's friends
|
|
79
|
+
)
|
|
80
|
+
print(response.choices[0].message.content)
|
|
81
|
+
|
|
82
|
+
```"""
|
|
83
|
+
self.name: str = name
|
|
84
|
+
self.params: list[ToolParameter] = params
|
|
85
|
+
self.sandbox_params: list[str] = sandbox_params
|
|
86
|
+
if callback:
|
|
87
|
+
self.callback = callback
|
|
88
|
+
self.description: str = inspect.getdoc(self.callback)
|
|
89
|
+
self.raw: dict = {
|
|
90
|
+
'type': 'function',
|
|
91
|
+
'function': {
|
|
92
|
+
'name': self.name,
|
|
93
|
+
'description': self.description,
|
|
94
|
+
'parameters': {
|
|
95
|
+
'type': 'object',
|
|
96
|
+
'properties': {}
|
|
97
|
+
},
|
|
98
|
+
'required': [p.name for p in self.params if p.required]
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for tool in self.params:
|
|
103
|
+
self.raw['function']['parameters']['properties'][tool.name] = {'type': tool.type, 'description': tool.description}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def callback(self, **kwargs) -> str:
|
|
108
|
+
raise NotImplementedError(f"Tool callback for \"{self.name}\" not implemented")
|