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.
@@ -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.
@@ -0,0 +1,3 @@
1
+ recursive-include assets *
2
+ include LICENSE
3
+ include README.md
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.
@@ -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"]
@@ -0,0 +1,2 @@
1
+ # This directory is reserved for non-code assets such as images, icons,
2
+ # or data files bundled with the drekai package.
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
@@ -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,5 @@
1
+ """Backwards-compatible setup shim — all config lives in pyproject.toml."""
2
+
3
+ from setuptools import setup
4
+
5
+ setup()
@@ -0,0 +1,4 @@
1
+ """### DrekAI Tools Wrapper"""
2
+ from .tools import Tool, ToolParameter
3
+
4
+ __all__ = ["Tool", "ToolParameter"]
@@ -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")