askai-python 0.4.0__tar.gz → 0.4.1__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.
- {askai_python-0.4.0 → askai_python-0.4.1}/PKG-INFO +62 -6
- {askai_python-0.4.0 → askai_python-0.4.1}/README.md +94 -8
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/__init__.py +1 -1
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/base.py +147 -5
- askai_python-0.4.1/ask_ai/providers/openrouter_provider.py +123 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/PKG-INFO +62 -6
- {askai_python-0.4.0 → askai_python-0.4.1}/docs/PYPI_README.md +61 -5
- {askai_python-0.4.0 → askai_python-0.4.1}/pyproject.toml +1 -1
- askai_python-0.4.0/ask_ai/providers/openrouter_provider.py +0 -66
- {askai_python-0.4.0 → askai_python-0.4.1}/LICENSE +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/__main__.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/cli.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/config.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/exceptions.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/media.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/__init__.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/anthropic_provider.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/azure_provider.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/google_provider.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/groq_provider.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/openai_provider.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/requirements.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/utils.py +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/SOURCES.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/dependency_links.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/entry_points.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/requires.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/top_level.txt +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/setup.cfg +0 -0
- {askai_python-0.4.0 → askai_python-0.4.1}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: askai-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, Anthropic, and more.
|
|
5
5
|
Author-email: Hossein Ghorbani <hosseingh1068@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -143,6 +143,54 @@ response = ai.ask(
|
|
|
143
143
|
)
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
## 🎨 Media Generation (Image & Audio)
|
|
147
|
+
|
|
148
|
+
Generate images or speech with compatible providers:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from ask_ai import OpenAI
|
|
152
|
+
|
|
153
|
+
ai = OpenAI()
|
|
154
|
+
|
|
155
|
+
# Generate an Image (DALL-E)
|
|
156
|
+
img_response = ai.ask("A majestic lion in a neon city", output_type="image")
|
|
157
|
+
img_response.save("lion.png")
|
|
158
|
+
|
|
159
|
+
# Generate Speech (TTS)
|
|
160
|
+
audio_response = ai.ask("Hello, this is a voice.", output_type="audio")
|
|
161
|
+
audio_response.save("welcome.mp3")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🔗 Provider Fallback
|
|
165
|
+
|
|
166
|
+
Never experience downtime by supplying fallback providers:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from ask_ai import OpenAI, Groq
|
|
170
|
+
|
|
171
|
+
ai = OpenAI()
|
|
172
|
+
response = ai.ask("Explain quantum physics", providers=[ai, Groq])
|
|
173
|
+
print(response.text)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 📋 Structured Output (Pydantic Support)
|
|
177
|
+
|
|
178
|
+
Force LLMs to strictly respond matching a Pydantic schema:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from pydantic import BaseModel
|
|
182
|
+
from ask_ai import OpenAI
|
|
183
|
+
|
|
184
|
+
class User(BaseModel):
|
|
185
|
+
name: str
|
|
186
|
+
age: int
|
|
187
|
+
|
|
188
|
+
ai = OpenAI()
|
|
189
|
+
response = ai.ask("Extract name: Alice is 30.", response_model=User)
|
|
190
|
+
user = response.pydantic
|
|
191
|
+
print(user.name) # "Alice"
|
|
192
|
+
```
|
|
193
|
+
|
|
146
194
|
## 🔌 Supported Providers
|
|
147
195
|
|
|
148
196
|
| Provider | Class | Capabilities |
|
|
@@ -158,11 +206,19 @@ response = ai.ask(
|
|
|
158
206
|
|
|
159
207
|
## 🗺️ Roadmap
|
|
160
208
|
|
|
161
|
-
|
|
162
|
-
- [x]
|
|
163
|
-
- [ ]
|
|
164
|
-
- [ ]
|
|
165
|
-
- [ ]
|
|
209
|
+
### 🚀 Roadmap 2.0 (Active)
|
|
210
|
+
- [x] Provider Fallback chaining & Pydantic Structured Output support
|
|
211
|
+
- [ ] Multi-Modal Vision input (image to text) for all key providers
|
|
212
|
+
- [ ] Native support for tools/function calling execution
|
|
213
|
+
- [ ] Memory buffer / session-based conversation management
|
|
214
|
+
|
|
215
|
+
### 🏁 Roadmap 1.0 (Completed)
|
|
216
|
+
- ~~[x] Baseline multiple providers~~
|
|
217
|
+
- ~~[x] Automated Retry & Timeout controls~~
|
|
218
|
+
- ~~[x] Async API (`await ask_async`)~~
|
|
219
|
+
- ~~[x] Streaming Support (`ask_stream` / `ask_stream_async`)~~
|
|
220
|
+
- ~~[x] Provider Fallback (`fallback=[Groq()]`)~~
|
|
221
|
+
- ~~[x] Structured Outputs (Pydantic Support)~~
|
|
166
222
|
|
|
167
223
|
---
|
|
168
224
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
[](https://pypi.org/project/askai-python/)
|
|
26
26
|
[](https://opensource.org/licenses/MIT)
|
|
27
|
-
[](https://www.python.org/downloads/)
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
|
|
@@ -144,15 +144,101 @@ asyncio.run(main())
|
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
+
### 🌊 Streaming Support (Sync & Async)
|
|
148
|
+
|
|
149
|
+
Stream responses token-by-token easily:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from ask_ai import OpenAI
|
|
153
|
+
|
|
154
|
+
ai = OpenAI()
|
|
155
|
+
|
|
156
|
+
# Sync Streaming
|
|
157
|
+
for chunk in ai.ask_stream("Write a short story about a blue dragon"):
|
|
158
|
+
print(chunk, end="", flush=True)
|
|
159
|
+
|
|
160
|
+
# Async Streaming
|
|
161
|
+
async def stream_example():
|
|
162
|
+
async for chunk in await ai.ask_stream_async("Write a short story"):
|
|
163
|
+
print(chunk, end="", flush=True)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### 🎨 Media Generation (Image & Audio)
|
|
169
|
+
|
|
170
|
+
Generate images or speech with compatible providers:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from ask_ai import OpenAI
|
|
174
|
+
|
|
175
|
+
ai = OpenAI()
|
|
176
|
+
|
|
177
|
+
# Generate an Image (DALL-E)
|
|
178
|
+
img_response = ai.ask("A majestic lion in a neon city", output_type="image")
|
|
179
|
+
img_response.save("lion.png")
|
|
180
|
+
|
|
181
|
+
# Generate Speech (TTS)
|
|
182
|
+
audio_response = ai.ask("Hello, this is a generated voice.", output_type="audio")
|
|
183
|
+
audio_response.save("welcome.mp3")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 🔗 Provider Fallback
|
|
189
|
+
|
|
190
|
+
Never experience downtime by supplying alternative fallback providers:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from ask_ai import OpenAI, Groq
|
|
194
|
+
|
|
195
|
+
ai = OpenAI()
|
|
196
|
+
|
|
197
|
+
# If OpenAI fails or rate-limits, it automatically switches to Groq
|
|
198
|
+
response = ai.ask("Explain quantum physics", providers=[ai, Groq])
|
|
199
|
+
print(response.text)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### 📋 Structured Output (Pydantic Support)
|
|
205
|
+
|
|
206
|
+
Force LLMs to strictly respond matching a Pydantic schema:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from pydantic import BaseModel
|
|
210
|
+
from ask_ai import OpenAI
|
|
211
|
+
|
|
212
|
+
class User(BaseModel):
|
|
213
|
+
name: str
|
|
214
|
+
age: int
|
|
215
|
+
|
|
216
|
+
ai = OpenAI()
|
|
217
|
+
response = ai.ask("Extract name: Alice is 30 years old.", response_model=User)
|
|
218
|
+
|
|
219
|
+
user = response.pydantic
|
|
220
|
+
print(user.name) # "Alice"
|
|
221
|
+
print(user.age) # 30
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
147
226
|
## 🗺️ Roadmap
|
|
148
227
|
|
|
149
|
-
|
|
150
|
-
- [x]
|
|
151
|
-
- [
|
|
152
|
-
- [
|
|
153
|
-
- [ ]
|
|
154
|
-
|
|
155
|
-
|
|
228
|
+
### 🚀 Roadmap 2.0 (Active)
|
|
229
|
+
- [x] Provider Fallback chaining & Pydantic Structured Output support
|
|
230
|
+
- [ ] Multi-Modal Vision input (image to text) for all key providers
|
|
231
|
+
- [ ] Native support for tools/function calling execution
|
|
232
|
+
- [ ] Memory buffer / session-based conversation management
|
|
233
|
+
|
|
234
|
+
### 🏁 Roadmap 1.0 (Completed)
|
|
235
|
+
- ~~[x] OpenAI, Groq, Gemini, Anthropic, Azure, OpenRouter support~~
|
|
236
|
+
- ~~[x] Text to Image (DALL-E) and Text to Audio~~
|
|
237
|
+
- ~~[x] Automated Retry & Timeout controls~~
|
|
238
|
+
- ~~[x] Async Support (`await ask_async`)~~
|
|
239
|
+
- ~~[x] Streaming Support (`ask_stream` / `ask_stream_async`)~~
|
|
240
|
+
- ~~[x] Provider Fallback (`ask(..., providers=[OpenAI, Groq])`)~~
|
|
241
|
+
- ~~[x] Structured Output (Pydantic Support)~~
|
|
156
242
|
|
|
157
243
|
---
|
|
158
244
|
|
|
@@ -15,9 +15,10 @@ class Response:
|
|
|
15
15
|
"""
|
|
16
16
|
Unified response object for all ask-ai requests.
|
|
17
17
|
"""
|
|
18
|
-
def __init__(self, text: str = "", media: Union[ImageObject, AudioObject, None] = None, **kwargs):
|
|
18
|
+
def __init__(self, text: str = "", media: Union[ImageObject, AudioObject, None] = None, response_model: Any = None, **kwargs):
|
|
19
19
|
self._raw_text = text
|
|
20
20
|
self.media = media
|
|
21
|
+
self.response_model = response_model
|
|
21
22
|
self._kwargs = kwargs
|
|
22
23
|
|
|
23
24
|
@property
|
|
@@ -40,6 +41,25 @@ class Response:
|
|
|
40
41
|
from .utils import parse_json
|
|
41
42
|
return parse_json(self._raw_text)
|
|
42
43
|
|
|
44
|
+
@property
|
|
45
|
+
def pydantic(self) -> Any:
|
|
46
|
+
"""Returns parsed Pydantic model instance if response_model was provided."""
|
|
47
|
+
if not self.response_model:
|
|
48
|
+
raise ValueError("No response_model was specified for this request.")
|
|
49
|
+
return self.to_model(self.response_model)
|
|
50
|
+
|
|
51
|
+
def to_model(self, model_class: Any) -> Any:
|
|
52
|
+
"""Parse the response text into a specified Pydantic model."""
|
|
53
|
+
text_content = self.text
|
|
54
|
+
if hasattr(model_class, "model_validate_json"):
|
|
55
|
+
return model_class.model_validate_json(text_content)
|
|
56
|
+
elif hasattr(model_class, "parse_raw"):
|
|
57
|
+
return model_class.parse_raw(text_content)
|
|
58
|
+
else:
|
|
59
|
+
import json
|
|
60
|
+
data = json.loads(text_content)
|
|
61
|
+
return model_class(**data)
|
|
62
|
+
|
|
43
63
|
def __str__(self) -> str:
|
|
44
64
|
return self.text
|
|
45
65
|
|
|
@@ -97,19 +117,68 @@ class BaseProvider:
|
|
|
97
117
|
The main entry point. Sync version.
|
|
98
118
|
Detects intent, manages config, and returns a unified Response.
|
|
99
119
|
"""
|
|
120
|
+
providers = kwargs.pop("providers", None) or kwargs.pop("fallback", None)
|
|
121
|
+
if providers:
|
|
122
|
+
if not isinstance(providers, (list, tuple)):
|
|
123
|
+
providers = [providers]
|
|
124
|
+
errors = []
|
|
125
|
+
for p in providers:
|
|
126
|
+
if isinstance(p, type):
|
|
127
|
+
try:
|
|
128
|
+
p_inst = p()
|
|
129
|
+
except Exception as e:
|
|
130
|
+
errors.append(f"{p.__name__} init failed: {e}")
|
|
131
|
+
continue
|
|
132
|
+
else:
|
|
133
|
+
p_inst = p
|
|
134
|
+
try:
|
|
135
|
+
return p_inst.ask(query, **kwargs)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
errors.append(f"{p_inst.__class__.__name__}: {e}")
|
|
138
|
+
logger.warning("Provider fallback failed for %s: %s", p_inst.__class__.__name__, e)
|
|
139
|
+
from .exceptions import ProviderError
|
|
140
|
+
raise ProviderError(f"All fallback providers failed: {errors}")
|
|
141
|
+
|
|
100
142
|
final_config = self._build_final_config(kwargs)
|
|
101
143
|
messages = self._prepare_messages(query, final_config)
|
|
102
144
|
output_type = kwargs.get('output_type')
|
|
103
145
|
retry = int(kwargs.get('retry', final_config.extra.get('retry', 0)) or 0)
|
|
146
|
+
response_model = kwargs.get('response_model')
|
|
104
147
|
|
|
105
|
-
|
|
148
|
+
response = self._execute_with_retry(
|
|
106
149
|
self._send_request, messages, final_config, output_type, retry
|
|
107
150
|
)
|
|
151
|
+
if response_model:
|
|
152
|
+
response.response_model = response_model
|
|
153
|
+
return response
|
|
108
154
|
|
|
109
155
|
def ask_stream(self, query: str, **kwargs) -> Iterator[str]:
|
|
110
156
|
"""
|
|
111
157
|
Streaming text generation. Yields text chunks.
|
|
112
158
|
"""
|
|
159
|
+
providers = kwargs.pop("providers", None) or kwargs.pop("fallback", None)
|
|
160
|
+
if providers:
|
|
161
|
+
if not isinstance(providers, (list, tuple)):
|
|
162
|
+
providers = [providers]
|
|
163
|
+
errors = []
|
|
164
|
+
for p in providers:
|
|
165
|
+
if isinstance(p, type):
|
|
166
|
+
try:
|
|
167
|
+
p_inst = p()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
errors.append(f"{p.__name__} init failed: {e}")
|
|
170
|
+
continue
|
|
171
|
+
else:
|
|
172
|
+
p_inst = p
|
|
173
|
+
try:
|
|
174
|
+
yield from p_inst.ask_stream(query, **kwargs)
|
|
175
|
+
return
|
|
176
|
+
except Exception as e:
|
|
177
|
+
errors.append(f"{p_inst.__class__.__name__}: {e}")
|
|
178
|
+
logger.warning("Provider fallback failed for %s: %s", p_inst.__class__.__name__, e)
|
|
179
|
+
from .exceptions import ProviderError
|
|
180
|
+
raise ProviderError(f"All fallback providers failed: {errors}")
|
|
181
|
+
|
|
113
182
|
final_config = self._build_final_config(kwargs)
|
|
114
183
|
messages = self._prepare_messages(query, final_config)
|
|
115
184
|
yield from self._send_request_stream(messages, final_config)
|
|
@@ -121,19 +190,69 @@ class BaseProvider:
|
|
|
121
190
|
Native async version of ask().
|
|
122
191
|
Uses provider's async client directly — no thread pool wrapping.
|
|
123
192
|
"""
|
|
193
|
+
providers = kwargs.pop("providers", None) or kwargs.pop("fallback", None)
|
|
194
|
+
if providers:
|
|
195
|
+
if not isinstance(providers, (list, tuple)):
|
|
196
|
+
providers = [providers]
|
|
197
|
+
errors = []
|
|
198
|
+
for p in providers:
|
|
199
|
+
if isinstance(p, type):
|
|
200
|
+
try:
|
|
201
|
+
p_inst = p()
|
|
202
|
+
except Exception as e:
|
|
203
|
+
errors.append(f"{p.__name__} init failed: {e}")
|
|
204
|
+
continue
|
|
205
|
+
else:
|
|
206
|
+
p_inst = p
|
|
207
|
+
try:
|
|
208
|
+
return await p_inst.ask_async(query, **kwargs)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
errors.append(f"{p_inst.__class__.__name__}: {e}")
|
|
211
|
+
logger.warning("Provider fallback failed for %s: %s", p_inst.__class__.__name__, e)
|
|
212
|
+
from .exceptions import ProviderError
|
|
213
|
+
raise ProviderError(f"All fallback providers failed: {errors}")
|
|
214
|
+
|
|
124
215
|
final_config = self._build_final_config(kwargs)
|
|
125
216
|
messages = self._prepare_messages(query, final_config)
|
|
126
217
|
output_type = kwargs.get('output_type')
|
|
127
218
|
retry = int(kwargs.get('retry', final_config.extra.get('retry', 0)) or 0)
|
|
219
|
+
response_model = kwargs.get('response_model')
|
|
128
220
|
|
|
129
|
-
|
|
221
|
+
response = await self._execute_with_retry_async(
|
|
130
222
|
self._send_request_async, messages, final_config, output_type, retry
|
|
131
223
|
)
|
|
224
|
+
if response_model:
|
|
225
|
+
response.response_model = response_model
|
|
226
|
+
return response
|
|
132
227
|
|
|
133
228
|
async def ask_stream_async(self, query: str, **kwargs) -> AsyncIterator[str]:
|
|
134
229
|
"""
|
|
135
230
|
Async streaming text generation. Yields text chunks.
|
|
136
231
|
"""
|
|
232
|
+
providers = kwargs.pop("providers", None) or kwargs.pop("fallback", None)
|
|
233
|
+
if providers:
|
|
234
|
+
if not isinstance(providers, (list, tuple)):
|
|
235
|
+
providers = [providers]
|
|
236
|
+
errors = []
|
|
237
|
+
for p in providers:
|
|
238
|
+
if isinstance(p, type):
|
|
239
|
+
try:
|
|
240
|
+
p_inst = p()
|
|
241
|
+
except Exception as e:
|
|
242
|
+
errors.append(f"{p.__name__} init failed: {e}")
|
|
243
|
+
continue
|
|
244
|
+
else:
|
|
245
|
+
p_inst = p
|
|
246
|
+
try:
|
|
247
|
+
async for chunk in await p_inst.ask_stream_async(query, **kwargs):
|
|
248
|
+
yield chunk
|
|
249
|
+
return
|
|
250
|
+
except Exception as e:
|
|
251
|
+
errors.append(f"{p_inst.__class__.__name__}: {e}")
|
|
252
|
+
logger.warning("Provider fallback failed for %s: %s", p_inst.__class__.__name__, e)
|
|
253
|
+
from .exceptions import ProviderError
|
|
254
|
+
raise ProviderError(f"All fallback providers failed: {errors}")
|
|
255
|
+
|
|
137
256
|
final_config = self._build_final_config(kwargs)
|
|
138
257
|
messages = self._prepare_messages(query, final_config)
|
|
139
258
|
async for chunk in self._send_request_stream_async(messages, final_config):
|
|
@@ -158,8 +277,31 @@ class BaseProvider:
|
|
|
158
277
|
|
|
159
278
|
system_msg = config.system_message or self.persona or ""
|
|
160
279
|
|
|
161
|
-
# Smart capability: if json=True
|
|
162
|
-
|
|
280
|
+
# Smart capability: if json=True or response_model is specified
|
|
281
|
+
response_model = config.extra.get('response_model')
|
|
282
|
+
if response_model:
|
|
283
|
+
if hasattr(response_model, "model_json_schema"):
|
|
284
|
+
schema = response_model.model_json_schema()
|
|
285
|
+
elif hasattr(response_model, "schema"):
|
|
286
|
+
schema = response_model.schema()
|
|
287
|
+
else:
|
|
288
|
+
schema = None
|
|
289
|
+
|
|
290
|
+
if schema:
|
|
291
|
+
import json
|
|
292
|
+
schema_str = json.dumps(schema)
|
|
293
|
+
json_instruction = (
|
|
294
|
+
f"You must respond with valid JSON matching the following JSON schema:\n"
|
|
295
|
+
f"{schema_str}\n"
|
|
296
|
+
f"Do not include any markdown markup, explanation, or HTML tags."
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
json_instruction = "You must respond with valid JSON only."
|
|
300
|
+
|
|
301
|
+
system_msg = f"{system_msg}\n{json_instruction}".strip()
|
|
302
|
+
config.extra['json'] = True
|
|
303
|
+
|
|
304
|
+
elif config.extra.get('json'):
|
|
163
305
|
json_instruction = "You must respond with valid JSON only, without any markdown formatting or tags."
|
|
164
306
|
system_msg = f"{system_msg}\n{json_instruction}".strip()
|
|
165
307
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import openai
|
|
6
|
+
except ImportError:
|
|
7
|
+
openai = None
|
|
8
|
+
|
|
9
|
+
from ..config import AdvancedConfig
|
|
10
|
+
from ..exceptions import APIKeyError, ProviderError
|
|
11
|
+
from .openai_provider import OpenAIProvider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OpenRouterProvider(OpenAIProvider):
|
|
15
|
+
"""OpenRouter provider — routes to multiple models via OpenAI-compatible API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str = None, model: str = None, **kwargs):
|
|
18
|
+
if openai is None:
|
|
19
|
+
raise ProviderError("OpenAI client library not installed. Run: pip install openai")
|
|
20
|
+
|
|
21
|
+
self.api_key = api_key or os.environ.get("OPENROUTER_API_KEY")
|
|
22
|
+
if not self.api_key:
|
|
23
|
+
raise APIKeyError("OpenRouter API Key is missing. Set OPENROUTER_API_KEY env var or pass api_key=")
|
|
24
|
+
|
|
25
|
+
# Sync client with OpenRouter base URL
|
|
26
|
+
self.client = openai.OpenAI(
|
|
27
|
+
base_url="https://openrouter.ai/api/v1",
|
|
28
|
+
api_key=self.api_key,
|
|
29
|
+
)
|
|
30
|
+
self._async_client = None
|
|
31
|
+
|
|
32
|
+
self.model = model or "openai/gpt-4o"
|
|
33
|
+
self.persona = None
|
|
34
|
+
self.config = AdvancedConfig(**kwargs)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def async_client(self):
|
|
38
|
+
"""Lazy-load async client for OpenRouter."""
|
|
39
|
+
if self._async_client is None:
|
|
40
|
+
self._async_client = openai.AsyncOpenAI(
|
|
41
|
+
base_url="https://openrouter.ai/api/v1",
|
|
42
|
+
api_key=self.api_key,
|
|
43
|
+
)
|
|
44
|
+
return self._async_client
|
|
45
|
+
|
|
46
|
+
def _get_api_key_env_var(self):
|
|
47
|
+
return "OPENROUTER_API_KEY"
|
|
48
|
+
|
|
49
|
+
def _get_default_model(self):
|
|
50
|
+
return "openai/gpt-4o"
|
|
51
|
+
|
|
52
|
+
def _get_media_tools(self) -> List[Dict[str, Any]]:
|
|
53
|
+
# OpenRouter does not support media endpoints
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
def _generate_image(self, messages, config):
|
|
57
|
+
prompt = messages[-1]["content"]
|
|
58
|
+
try:
|
|
59
|
+
import httpx
|
|
60
|
+
except ImportError:
|
|
61
|
+
raise ProviderError("httpx client library is required for OpenRouter Image Generation. Run: pip install httpx")
|
|
62
|
+
|
|
63
|
+
from ..media import ImageObject
|
|
64
|
+
from .openai_provider import _download_bytes
|
|
65
|
+
|
|
66
|
+
headers = {
|
|
67
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
68
|
+
"Content-Type": "application/json"
|
|
69
|
+
}
|
|
70
|
+
payload = {
|
|
71
|
+
"model": self.model,
|
|
72
|
+
"prompt": prompt
|
|
73
|
+
}
|
|
74
|
+
try:
|
|
75
|
+
response = httpx.post("https://openrouter.ai/api/v1/images", json=payload, headers=headers, timeout=60.0)
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
data = response.json()
|
|
78
|
+
image_url = data["data"][0]["url"]
|
|
79
|
+
img_data = _download_bytes(image_url)
|
|
80
|
+
return Response(
|
|
81
|
+
text=f"Generated image via OpenRouter ({self.model}) for: {prompt}",
|
|
82
|
+
media=ImageObject(img_data)
|
|
83
|
+
)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ProviderError(f"OpenRouter Image Generation Error: {e}") from e
|
|
86
|
+
|
|
87
|
+
async def _generate_image_async(self, messages, config):
|
|
88
|
+
prompt = messages[-1]["content"]
|
|
89
|
+
try:
|
|
90
|
+
import httpx
|
|
91
|
+
except ImportError:
|
|
92
|
+
raise ProviderError("httpx client library is required for OpenRouter Image Generation. Run: pip install httpx")
|
|
93
|
+
|
|
94
|
+
from ..media import ImageObject
|
|
95
|
+
from .openai_provider import _download_bytes_async
|
|
96
|
+
|
|
97
|
+
headers = {
|
|
98
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
99
|
+
"Content-Type": "application/json"
|
|
100
|
+
}
|
|
101
|
+
payload = {
|
|
102
|
+
"model": self.model,
|
|
103
|
+
"prompt": prompt
|
|
104
|
+
}
|
|
105
|
+
try:
|
|
106
|
+
async with httpx.AsyncClient() as client:
|
|
107
|
+
response = await client.post("https://openrouter.ai/api/v1/images", json=payload, headers=headers, timeout=60.0)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
data = response.json()
|
|
110
|
+
image_url = data["data"][0]["url"]
|
|
111
|
+
img_data = await _download_bytes_async(image_url)
|
|
112
|
+
return Response(
|
|
113
|
+
text=f"Generated image via OpenRouter ({self.model}) for: {prompt}",
|
|
114
|
+
media=ImageObject(img_data)
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise ProviderError(f"OpenRouter Image Generation Error: {e}") from e
|
|
118
|
+
|
|
119
|
+
def _generate_audio(self, messages, config):
|
|
120
|
+
raise ProviderError("OpenRouter does not support Audio Generation via this library.")
|
|
121
|
+
|
|
122
|
+
async def _generate_audio_async(self, messages, config):
|
|
123
|
+
raise ProviderError("OpenRouter does not support Audio Generation via this library.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: askai-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, Anthropic, and more.
|
|
5
5
|
Author-email: Hossein Ghorbani <hosseingh1068@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -143,6 +143,54 @@ response = ai.ask(
|
|
|
143
143
|
)
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
## 🎨 Media Generation (Image & Audio)
|
|
147
|
+
|
|
148
|
+
Generate images or speech with compatible providers:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from ask_ai import OpenAI
|
|
152
|
+
|
|
153
|
+
ai = OpenAI()
|
|
154
|
+
|
|
155
|
+
# Generate an Image (DALL-E)
|
|
156
|
+
img_response = ai.ask("A majestic lion in a neon city", output_type="image")
|
|
157
|
+
img_response.save("lion.png")
|
|
158
|
+
|
|
159
|
+
# Generate Speech (TTS)
|
|
160
|
+
audio_response = ai.ask("Hello, this is a voice.", output_type="audio")
|
|
161
|
+
audio_response.save("welcome.mp3")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🔗 Provider Fallback
|
|
165
|
+
|
|
166
|
+
Never experience downtime by supplying fallback providers:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from ask_ai import OpenAI, Groq
|
|
170
|
+
|
|
171
|
+
ai = OpenAI()
|
|
172
|
+
response = ai.ask("Explain quantum physics", providers=[ai, Groq])
|
|
173
|
+
print(response.text)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 📋 Structured Output (Pydantic Support)
|
|
177
|
+
|
|
178
|
+
Force LLMs to strictly respond matching a Pydantic schema:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from pydantic import BaseModel
|
|
182
|
+
from ask_ai import OpenAI
|
|
183
|
+
|
|
184
|
+
class User(BaseModel):
|
|
185
|
+
name: str
|
|
186
|
+
age: int
|
|
187
|
+
|
|
188
|
+
ai = OpenAI()
|
|
189
|
+
response = ai.ask("Extract name: Alice is 30.", response_model=User)
|
|
190
|
+
user = response.pydantic
|
|
191
|
+
print(user.name) # "Alice"
|
|
192
|
+
```
|
|
193
|
+
|
|
146
194
|
## 🔌 Supported Providers
|
|
147
195
|
|
|
148
196
|
| Provider | Class | Capabilities |
|
|
@@ -158,11 +206,19 @@ response = ai.ask(
|
|
|
158
206
|
|
|
159
207
|
## 🗺️ Roadmap
|
|
160
208
|
|
|
161
|
-
|
|
162
|
-
- [x]
|
|
163
|
-
- [ ]
|
|
164
|
-
- [ ]
|
|
165
|
-
- [ ]
|
|
209
|
+
### 🚀 Roadmap 2.0 (Active)
|
|
210
|
+
- [x] Provider Fallback chaining & Pydantic Structured Output support
|
|
211
|
+
- [ ] Multi-Modal Vision input (image to text) for all key providers
|
|
212
|
+
- [ ] Native support for tools/function calling execution
|
|
213
|
+
- [ ] Memory buffer / session-based conversation management
|
|
214
|
+
|
|
215
|
+
### 🏁 Roadmap 1.0 (Completed)
|
|
216
|
+
- ~~[x] Baseline multiple providers~~
|
|
217
|
+
- ~~[x] Automated Retry & Timeout controls~~
|
|
218
|
+
- ~~[x] Async API (`await ask_async`)~~
|
|
219
|
+
- ~~[x] Streaming Support (`ask_stream` / `ask_stream_async`)~~
|
|
220
|
+
- ~~[x] Provider Fallback (`fallback=[Groq()]`)~~
|
|
221
|
+
- ~~[x] Structured Outputs (Pydantic Support)~~
|
|
166
222
|
|
|
167
223
|
---
|
|
168
224
|
|
|
@@ -96,6 +96,54 @@ response = ai.ask(
|
|
|
96
96
|
)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
## 🎨 Media Generation (Image & Audio)
|
|
100
|
+
|
|
101
|
+
Generate images or speech with compatible providers:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from ask_ai import OpenAI
|
|
105
|
+
|
|
106
|
+
ai = OpenAI()
|
|
107
|
+
|
|
108
|
+
# Generate an Image (DALL-E)
|
|
109
|
+
img_response = ai.ask("A majestic lion in a neon city", output_type="image")
|
|
110
|
+
img_response.save("lion.png")
|
|
111
|
+
|
|
112
|
+
# Generate Speech (TTS)
|
|
113
|
+
audio_response = ai.ask("Hello, this is a voice.", output_type="audio")
|
|
114
|
+
audio_response.save("welcome.mp3")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 🔗 Provider Fallback
|
|
118
|
+
|
|
119
|
+
Never experience downtime by supplying fallback providers:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ask_ai import OpenAI, Groq
|
|
123
|
+
|
|
124
|
+
ai = OpenAI()
|
|
125
|
+
response = ai.ask("Explain quantum physics", providers=[ai, Groq])
|
|
126
|
+
print(response.text)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 📋 Structured Output (Pydantic Support)
|
|
130
|
+
|
|
131
|
+
Force LLMs to strictly respond matching a Pydantic schema:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pydantic import BaseModel
|
|
135
|
+
from ask_ai import OpenAI
|
|
136
|
+
|
|
137
|
+
class User(BaseModel):
|
|
138
|
+
name: str
|
|
139
|
+
age: int
|
|
140
|
+
|
|
141
|
+
ai = OpenAI()
|
|
142
|
+
response = ai.ask("Extract name: Alice is 30.", response_model=User)
|
|
143
|
+
user = response.pydantic
|
|
144
|
+
print(user.name) # "Alice"
|
|
145
|
+
```
|
|
146
|
+
|
|
99
147
|
## 🔌 Supported Providers
|
|
100
148
|
|
|
101
149
|
| Provider | Class | Capabilities |
|
|
@@ -111,11 +159,19 @@ response = ai.ask(
|
|
|
111
159
|
|
|
112
160
|
## 🗺️ Roadmap
|
|
113
161
|
|
|
114
|
-
|
|
115
|
-
- [x]
|
|
116
|
-
- [ ]
|
|
117
|
-
- [ ]
|
|
118
|
-
- [ ]
|
|
162
|
+
### 🚀 Roadmap 2.0 (Active)
|
|
163
|
+
- [x] Provider Fallback chaining & Pydantic Structured Output support
|
|
164
|
+
- [ ] Multi-Modal Vision input (image to text) for all key providers
|
|
165
|
+
- [ ] Native support for tools/function calling execution
|
|
166
|
+
- [ ] Memory buffer / session-based conversation management
|
|
167
|
+
|
|
168
|
+
### 🏁 Roadmap 1.0 (Completed)
|
|
169
|
+
- ~~[x] Baseline multiple providers~~
|
|
170
|
+
- ~~[x] Automated Retry & Timeout controls~~
|
|
171
|
+
- ~~[x] Async API (`await ask_async`)~~
|
|
172
|
+
- ~~[x] Streaming Support (`ask_stream` / `ask_stream_async`)~~
|
|
173
|
+
- ~~[x] Provider Fallback (`fallback=[Groq()]`)~~
|
|
174
|
+
- ~~[x] Structured Outputs (Pydantic Support)~~
|
|
119
175
|
|
|
120
176
|
---
|
|
121
177
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "askai-python"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, Anthropic, and more."
|
|
9
9
|
readme = "docs/PYPI_README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing import List, Dict, Any
|
|
3
|
-
|
|
4
|
-
try:
|
|
5
|
-
import openai
|
|
6
|
-
except ImportError:
|
|
7
|
-
openai = None
|
|
8
|
-
|
|
9
|
-
from ..config import AdvancedConfig
|
|
10
|
-
from ..exceptions import APIKeyError, ProviderError
|
|
11
|
-
from .openai_provider import OpenAIProvider
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class OpenRouterProvider(OpenAIProvider):
|
|
15
|
-
"""OpenRouter provider — routes to multiple models via OpenAI-compatible API."""
|
|
16
|
-
|
|
17
|
-
def __init__(self, api_key: str = None, model: str = None, **kwargs):
|
|
18
|
-
if openai is None:
|
|
19
|
-
raise ProviderError("OpenAI client library not installed. Run: pip install openai")
|
|
20
|
-
|
|
21
|
-
self.api_key = api_key or os.environ.get("OPENROUTER_API_KEY")
|
|
22
|
-
if not self.api_key:
|
|
23
|
-
raise APIKeyError("OpenRouter API Key is missing. Set OPENROUTER_API_KEY env var or pass api_key=")
|
|
24
|
-
|
|
25
|
-
# Sync client with OpenRouter base URL
|
|
26
|
-
self.client = openai.OpenAI(
|
|
27
|
-
base_url="https://openrouter.ai/api/v1",
|
|
28
|
-
api_key=self.api_key,
|
|
29
|
-
)
|
|
30
|
-
self._async_client = None
|
|
31
|
-
|
|
32
|
-
self.model = model or "openai/gpt-4o"
|
|
33
|
-
self.persona = None
|
|
34
|
-
self.config = AdvancedConfig(**kwargs)
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def async_client(self):
|
|
38
|
-
"""Lazy-load async client for OpenRouter."""
|
|
39
|
-
if self._async_client is None:
|
|
40
|
-
self._async_client = openai.AsyncOpenAI(
|
|
41
|
-
base_url="https://openrouter.ai/api/v1",
|
|
42
|
-
api_key=self.api_key,
|
|
43
|
-
)
|
|
44
|
-
return self._async_client
|
|
45
|
-
|
|
46
|
-
def _get_api_key_env_var(self):
|
|
47
|
-
return "OPENROUTER_API_KEY"
|
|
48
|
-
|
|
49
|
-
def _get_default_model(self):
|
|
50
|
-
return "openai/gpt-4o"
|
|
51
|
-
|
|
52
|
-
def _get_media_tools(self) -> List[Dict[str, Any]]:
|
|
53
|
-
# OpenRouter does not support media endpoints
|
|
54
|
-
return []
|
|
55
|
-
|
|
56
|
-
def _generate_image(self, messages, config):
|
|
57
|
-
raise ProviderError("OpenRouter does not support Image Generation via this library.")
|
|
58
|
-
|
|
59
|
-
def _generate_audio(self, messages, config):
|
|
60
|
-
raise ProviderError("OpenRouter does not support Audio Generation via this library.")
|
|
61
|
-
|
|
62
|
-
async def _generate_image_async(self, messages, config):
|
|
63
|
-
raise ProviderError("OpenRouter does not support Image Generation via this library.")
|
|
64
|
-
|
|
65
|
-
async def _generate_audio_async(self, messages, config):
|
|
66
|
-
raise ProviderError("OpenRouter does not support Audio Generation via this library.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|