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.
Files changed (30) hide show
  1. {askai_python-0.4.0 → askai_python-0.4.1}/PKG-INFO +62 -6
  2. {askai_python-0.4.0 → askai_python-0.4.1}/README.md +94 -8
  3. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/__init__.py +1 -1
  4. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/base.py +147 -5
  5. askai_python-0.4.1/ask_ai/providers/openrouter_provider.py +123 -0
  6. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/PKG-INFO +62 -6
  7. {askai_python-0.4.0 → askai_python-0.4.1}/docs/PYPI_README.md +61 -5
  8. {askai_python-0.4.0 → askai_python-0.4.1}/pyproject.toml +1 -1
  9. askai_python-0.4.0/ask_ai/providers/openrouter_provider.py +0 -66
  10. {askai_python-0.4.0 → askai_python-0.4.1}/LICENSE +0 -0
  11. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/__main__.py +0 -0
  12. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/cli.py +0 -0
  13. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/config.py +0 -0
  14. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/exceptions.py +0 -0
  15. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/media.py +0 -0
  16. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/__init__.py +0 -0
  17. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/anthropic_provider.py +0 -0
  18. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/azure_provider.py +0 -0
  19. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/google_provider.py +0 -0
  20. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/groq_provider.py +0 -0
  21. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/providers/openai_provider.py +0 -0
  22. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/requirements.txt +0 -0
  23. {askai_python-0.4.0 → askai_python-0.4.1}/ask_ai/utils.py +0 -0
  24. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/SOURCES.txt +0 -0
  25. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/dependency_links.txt +0 -0
  26. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/entry_points.txt +0 -0
  27. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/requires.txt +0 -0
  28. {askai_python-0.4.0 → askai_python-0.4.1}/askai_python.egg-info/top_level.txt +0 -0
  29. {askai_python-0.4.0 → askai_python-0.4.1}/setup.cfg +0 -0
  30. {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.0
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
- - [x] Baseline multiple providers
162
- - [x] Automated Retry & Timeout controls
163
- - [ ] Async API (`await ask_async`)
164
- - [ ] Provider Fallback (`fallback=[Groq()]`)
165
- - [ ] Structured Outputs (Pydantic Support)
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
  [![PyPI version](https://img.shields.io/pypi/v/askai-python.svg)](https://pypi.org/project/askai-python/)
26
26
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
27
- [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
27
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](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
- - [x] OpenAI, Groq, Gemini, Anthropic, Azure, OpenRouter support
150
- - [x] Text to Image (DALL-E) and Text to Audio
151
- - [x] Automated Retry & Timeout controls
152
- - [x] Async Support (`await ask_async`)
153
- - [ ] Provider Fallback (`ask(..., providers=[OpenAI, Groq])`)
154
- - [ ] Streaming Support (`ask_stream`)
155
- - [ ] Structured Output (Pydantic Support)
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
 
@@ -2,7 +2,7 @@
2
2
  ask-ai: A minimal Python SDK to switch between LLM providers in one line.
3
3
  """
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.4.1"
6
6
 
7
7
  from .providers import (
8
8
  OpenAI,
@@ -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
- return self._execute_with_retry(
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
- return await self._execute_with_retry_async(
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, enforce JSON system prompt
162
- if config.extra.get('json'):
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.0
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
- - [x] Baseline multiple providers
162
- - [x] Automated Retry & Timeout controls
163
- - [ ] Async API (`await ask_async`)
164
- - [ ] Provider Fallback (`fallback=[Groq()]`)
165
- - [ ] Structured Outputs (Pydantic Support)
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
- - [x] Baseline multiple providers
115
- - [x] Automated Retry & Timeout controls
116
- - [ ] Async API (`await ask_async`)
117
- - [ ] Provider Fallback (`fallback=[Groq()]`)
118
- - [ ] Structured Outputs (Pydantic Support)
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.0"
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