askai-python 0.3.3__tar.gz → 0.4.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.
Files changed (43) hide show
  1. {askai_python-0.3.3 → askai_python-0.4.0}/PKG-INFO +45 -17
  2. {askai_python-0.3.3 → askai_python-0.4.0}/README.md +20 -1
  3. askai_python-0.4.0/ask_ai/__init__.py +51 -0
  4. askai_python-0.4.0/ask_ai/base.py +302 -0
  5. askai_python-0.4.0/ask_ai/cli.py +107 -0
  6. askai_python-0.4.0/ask_ai/config.py +81 -0
  7. {askai_python-0.3.3 → askai_python-0.4.0}/ask_ai/exceptions.py +13 -1
  8. askai_python-0.4.0/ask_ai/media.py +90 -0
  9. {askai_python-0.3.3 → askai_python-0.4.0}/ask_ai/providers/__init__.py +1 -4
  10. askai_python-0.4.0/ask_ai/providers/anthropic_provider.py +128 -0
  11. askai_python-0.4.0/ask_ai/providers/azure_provider.py +67 -0
  12. askai_python-0.4.0/ask_ai/providers/google_provider.py +150 -0
  13. askai_python-0.4.0/ask_ai/providers/groq_provider.py +114 -0
  14. askai_python-0.4.0/ask_ai/providers/openai_provider.py +257 -0
  15. askai_python-0.4.0/ask_ai/providers/openrouter_provider.py +66 -0
  16. askai_python-0.4.0/ask_ai/utils.py +68 -0
  17. {askai_python-0.3.3 → askai_python-0.4.0}/askai_python.egg-info/PKG-INFO +45 -17
  18. {askai_python-0.3.3 → askai_python-0.4.0}/askai_python.egg-info/SOURCES.txt +1 -0
  19. askai_python-0.4.0/askai_python.egg-info/entry_points.txt +2 -0
  20. askai_python-0.4.0/askai_python.egg-info/requires.txt +26 -0
  21. {askai_python-0.3.3 → askai_python-0.4.0}/docs/PYPI_README.md +7 -7
  22. askai_python-0.4.0/pyproject.toml +83 -0
  23. {askai_python-0.3.3 → askai_python-0.4.0}/tests/test_core.py +10 -6
  24. askai_python-0.3.3/ask_ai/__init__.py +0 -26
  25. askai_python-0.3.3/ask_ai/base.py +0 -226
  26. askai_python-0.3.3/ask_ai/cli.py +0 -77
  27. askai_python-0.3.3/ask_ai/config.py +0 -56
  28. askai_python-0.3.3/ask_ai/media.py +0 -63
  29. askai_python-0.3.3/ask_ai/providers/anthropic_provider.py +0 -87
  30. askai_python-0.3.3/ask_ai/providers/azure_provider.py +0 -46
  31. askai_python-0.3.3/ask_ai/providers/google_provider.py +0 -87
  32. askai_python-0.3.3/ask_ai/providers/groq_provider.py +0 -75
  33. askai_python-0.3.3/ask_ai/providers/openai_provider.py +0 -122
  34. askai_python-0.3.3/ask_ai/providers/openrouter_provider.py +0 -44
  35. askai_python-0.3.3/ask_ai/utils.py +0 -43
  36. askai_python-0.3.3/askai_python.egg-info/requires.txt +0 -6
  37. askai_python-0.3.3/pyproject.toml +0 -33
  38. {askai_python-0.3.3 → askai_python-0.4.0}/LICENSE +0 -0
  39. {askai_python-0.3.3 → askai_python-0.4.0}/ask_ai/__main__.py +0 -0
  40. {askai_python-0.3.3 → askai_python-0.4.0}/ask_ai/requirements.txt +0 -0
  41. {askai_python-0.3.3 → askai_python-0.4.0}/askai_python.egg-info/dependency_links.txt +0 -0
  42. {askai_python-0.3.3 → askai_python-0.4.0}/askai_python.egg-info/top_level.txt +0 -0
  43. {askai_python-0.3.3 → askai_python-0.4.0}/setup.cfg +0 -0
@@ -1,20 +1,48 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: askai-python
3
- Version: 0.3.3
4
- Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, and more.
3
+ Version: 0.4.0
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
7
- Project-URL: Homepage, https://hosseinghorbani0.ir/
7
+ Project-URL: Homepage, https://github.com/Hosseinghorbani0/ask-ai
8
+ Project-URL: Documentation, https://github.com/Hosseinghorbani0/ask-ai#readme
8
9
  Project-URL: Bug Tracker, https://github.com/Hosseinghorbani0/ask-ai/issues
9
- Requires-Python: >=3.8
10
+ Project-URL: Changelog, https://github.com/Hosseinghorbani0/ask-ai/releases
11
+ Keywords: ai,llm,openai,anthropic,gemini,groq,sdk,api
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
10
25
  Description-Content-Type: text/markdown
11
26
  License-File: LICENSE
12
- Requires-Dist: openai>=1.0.0
13
- Requires-Dist: groq
14
- Requires-Dist: google-generativeai
15
- Requires-Dist: anthropic
16
- Requires-Dist: requests
17
- Requires-Dist: Pillow
27
+ Provides-Extra: openai
28
+ Requires-Dist: openai>=1.0.0; extra == "openai"
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
31
+ Provides-Extra: google
32
+ Requires-Dist: google-generativeai>=0.5.0; extra == "google"
33
+ Provides-Extra: groq
34
+ Requires-Dist: groq>=0.4.0; extra == "groq"
35
+ Provides-Extra: media
36
+ Requires-Dist: Pillow>=9.0.0; extra == "media"
37
+ Provides-Extra: httpx
38
+ Requires-Dist: httpx>=0.24.0; extra == "httpx"
39
+ Provides-Extra: all
40
+ Requires-Dist: openai>=1.0.0; extra == "all"
41
+ Requires-Dist: anthropic>=0.20.0; extra == "all"
42
+ Requires-Dist: google-generativeai>=0.5.0; extra == "all"
43
+ Requires-Dist: groq>=0.4.0; extra == "all"
44
+ Requires-Dist: Pillow>=9.0.0; extra == "all"
45
+ Requires-Dist: httpx>=0.24.0; extra == "all"
18
46
  Dynamic: license-file
19
47
 
20
48
  # askai-python 🚀
@@ -24,7 +52,7 @@ No frameworks. No servers. No overengineering.
24
52
 
25
53
  [![PyPI version](https://img.shields.io/pypi/v/askai-python.svg)](https://pypi.org/project/askai-python/)
26
54
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
27
- [![GitHub](https://img.shields.io/badge/GitHub-View%20Source-blue.svg)](https://github.com/Hosseinghorbani0/ask-ai)
55
+ [![GitHub](https://img.shields.io/badge/GitHub-View%20Source-blue.svg)](https://github.com/Hosseinghorbani0/askai-python)
28
56
 
29
57
  ---
30
58
 
@@ -46,7 +74,7 @@ print(Groq().ask("Explain black holes like I'm 5").text)
46
74
 
47
75
  ---
48
76
 
49
- ## 🧐 Why ask-ai?
77
+ ## 🧐 Why askai-python?
50
78
 
51
79
  - **One function**: Just call `.ask()`
52
80
  - **Multiple providers**: OpenAI, Anthropic, Google Gemini, Groq, Azure, OpenRouter
@@ -68,9 +96,9 @@ It does one thing perfectly: **Simplifying the API call to LLMs.**
68
96
  ### 🧰 Developer QoL Utilities (Auto-Parsing)
69
97
 
70
98
  <p align="center">
71
- <img src="https://raw.githubusercontent.com/Hosseinghorbani0/ask-ai/main/assets/features.svg" alt="ask-ai features parsing banner" width="100%">
99
+ <img src="https://raw.githubusercontent.com/Hosseinghorbani0/askai-python/main/assets/features.svg" alt="askai-python features parsing banner" width="100%">
72
100
  </p>
73
- Stop writing Regex to clean up model outputs! `ask-ai` comes with built-in text processing flags:
101
+ Stop writing Regex to clean up model outputs! `askai-python` comes with built-in text processing flags:
74
102
 
75
103
  ```python
76
104
  from ask_ai import OpenAI
@@ -97,7 +125,7 @@ print(data_dict['name'])
97
125
  ## 🚀 Built-in Resiliency (Retries & Timeouts)
98
126
 
99
127
  <p align="center">
100
- <img src="https://raw.githubusercontent.com/Hosseinghorbani0/ask-ai/main/assets/resiliency.svg" alt="ask-ai resiliency banner" width="100%">
128
+ <img src="https://raw.githubusercontent.com/Hosseinghorbani0/askai-python/main/assets/resiliency.svg" alt="askai-python resiliency banner" width="100%">
101
129
  </p>
102
130
 
103
131
  Build reliable apps without writing your own loops. `askai-python` handles rate limits (`429`) and network drops via an internal exponential backoff.
@@ -140,6 +168,6 @@ response = ai.ask(
140
168
 
141
169
  ## 🔗 Important Links
142
170
 
143
- - **GitHub Repository**: [Hosseinghorbani0/ask-ai](https://github.com/Hosseinghorbani0/ask-ai) (Star us! ⭐)
171
+ - **GitHub Repository**: [Hosseinghorbani0/askai-python](https://github.com/Hosseinghorbani0/askai-python) (Star us! ⭐)
144
172
  - **Official Website**: [hosseinghorbani0.ir](https://hosseinghorbani0.ir/)
145
- - **Bug Tracker**: [Report an Issue](https://github.com/Hosseinghorbani0/ask-ai/issues)
173
+ - **Bug Tracker**: [Report an Issue](https://github.com/Hosseinghorbani0/askai-python/issues)
@@ -125,12 +125,31 @@ print(ai.ask("How do I optimize a Dockerfile?").text)
125
125
 
126
126
  ---
127
127
 
128
+ ### 🚀 Async Support (FastAPI / Aiogram)
129
+
130
+ `ask-ai` works perfectly in asynchronous environments:
131
+
132
+ ```python
133
+ import asyncio
134
+ from ask_ai import OpenAI
135
+
136
+ async def main():
137
+ ai = OpenAI()
138
+ # Runs non-blocking in your event loop
139
+ response = await ai.ask_async("What is the capital of France?")
140
+ print(response.text)
141
+
142
+ asyncio.run(main())
143
+ ```
144
+
145
+ ---
146
+
128
147
  ## 🗺️ Roadmap
129
148
 
130
149
  - [x] OpenAI, Groq, Gemini, Anthropic, Azure, OpenRouter support
131
150
  - [x] Text to Image (DALL-E) and Text to Audio
132
151
  - [x] Automated Retry & Timeout controls
133
- - [ ] Async Support (`await ask_async`)
152
+ - [x] Async Support (`await ask_async`)
134
153
  - [ ] Provider Fallback (`ask(..., providers=[OpenAI, Groq])`)
135
154
  - [ ] Streaming Support (`ask_stream`)
136
155
  - [ ] Structured Output (Pydantic Support)
@@ -0,0 +1,51 @@
1
+ """
2
+ ask-ai: A minimal Python SDK to switch between LLM providers in one line.
3
+ """
4
+
5
+ __version__ = "0.4.0"
6
+
7
+ from .providers import (
8
+ OpenAI,
9
+ Anthropic,
10
+ Google,
11
+ Groq,
12
+ Azure,
13
+ OpenRouter
14
+ )
15
+ from .config import AdvancedConfig
16
+ from .base import Response
17
+ from .media import ImageObject, AudioObject
18
+ from .exceptions import (
19
+ AskAIError,
20
+ AskAIConfigError,
21
+ AskAINetworkError,
22
+ AskAIRateLimitError,
23
+ AskAIParsingError,
24
+ ProviderError,
25
+ MediaTypeNotSupportedError,
26
+ StreamNotSupportedError,
27
+ )
28
+
29
+ __all__ = [
30
+ # Providers
31
+ "OpenAI",
32
+ "Anthropic",
33
+ "Google",
34
+ "Groq",
35
+ "Azure",
36
+ "OpenRouter",
37
+ # Core
38
+ "AdvancedConfig",
39
+ "Response",
40
+ "ImageObject",
41
+ "AudioObject",
42
+ # Exceptions
43
+ "AskAIError",
44
+ "AskAIConfigError",
45
+ "AskAINetworkError",
46
+ "AskAIRateLimitError",
47
+ "AskAIParsingError",
48
+ "ProviderError",
49
+ "MediaTypeNotSupportedError",
50
+ "StreamNotSupportedError",
51
+ ]
@@ -0,0 +1,302 @@
1
+ import os
2
+ import time
3
+ import random
4
+ import asyncio
5
+ import logging
6
+ from typing import Union, List, Dict, Any, Iterator, AsyncIterator
7
+
8
+ from .config import AdvancedConfig
9
+ from .media import ImageObject, AudioObject
10
+
11
+ logger = logging.getLogger("ask_ai")
12
+
13
+
14
+ class Response:
15
+ """
16
+ Unified response object for all ask-ai requests.
17
+ """
18
+ def __init__(self, text: str = "", media: Union[ImageObject, AudioObject, None] = None, **kwargs):
19
+ self._raw_text = text
20
+ self.media = media
21
+ self._kwargs = kwargs
22
+
23
+ @property
24
+ def text(self) -> str:
25
+ text = self._raw_text
26
+ if self._kwargs.get('strip'):
27
+ from .utils import strip_tags
28
+ text = strip_tags(text)
29
+ if self._kwargs.get('clean'):
30
+ from .utils import clean_markdown
31
+ text = clean_markdown(text)
32
+ if self._kwargs.get('code'):
33
+ from .utils import extract_code
34
+ text = extract_code(text)
35
+ return text
36
+
37
+ @property
38
+ def json(self) -> Union[Dict[str, Any], list]:
39
+ """Returns parsed JSON. If 'json=True' was passed, this is highly reliable."""
40
+ from .utils import parse_json
41
+ return parse_json(self._raw_text)
42
+
43
+ def __str__(self) -> str:
44
+ return self.text
45
+
46
+ def __repr__(self) -> str:
47
+ preview = self._raw_text[:80] + "..." if len(self._raw_text) > 80 else self._raw_text
48
+ return f'<Response text="{preview}">'
49
+
50
+ def save(self, path: str) -> None:
51
+ """Smart save based on content type."""
52
+ if self.media:
53
+ self.media.save(path)
54
+ else:
55
+ with open(path, "w", encoding="utf-8") as f:
56
+ f.write(self.text)
57
+
58
+
59
+ class BaseProvider:
60
+ """
61
+ Abstract base class for all AI providers.
62
+ Implements the core 'ask' logic and configuration management.
63
+ """
64
+ def __init__(self, api_key: str = None, model: str = None, persona: str = None, **kwargs):
65
+ # Zero-Config: Try env var if key not provided
66
+ self.api_key = api_key or os.environ.get(self._get_api_key_env_var())
67
+ self.model = model or self._get_default_model()
68
+ self.persona = persona
69
+ self.config = AdvancedConfig(**kwargs)
70
+
71
+ def _get_api_key_env_var(self) -> str:
72
+ """Subclasses should return the env var name, e.g. 'OPENAI_API_KEY'"""
73
+ raise NotImplementedError
74
+
75
+ def _get_default_model(self) -> str:
76
+ """Subclasses should return a default model"""
77
+ raise NotImplementedError
78
+
79
+ def advanced(self, **kwargs) -> None:
80
+ """
81
+ Update global advanced settings for this instance.
82
+ Merges new settings with existing ones.
83
+ """
84
+ new_conf = AdvancedConfig(**kwargs)
85
+ for k, v in new_conf.__dict__.items():
86
+ if k == 'extra':
87
+ continue
88
+ if v is not None:
89
+ setattr(self.config, k, v)
90
+ if new_conf.extra:
91
+ self.config.extra.update(new_conf.extra)
92
+
93
+ # ─── Sync API ───────────────────────────────────────────
94
+
95
+ def ask(self, query: str, **kwargs) -> Response:
96
+ """
97
+ The main entry point. Sync version.
98
+ Detects intent, manages config, and returns a unified Response.
99
+ """
100
+ final_config = self._build_final_config(kwargs)
101
+ messages = self._prepare_messages(query, final_config)
102
+ output_type = kwargs.get('output_type')
103
+ retry = int(kwargs.get('retry', final_config.extra.get('retry', 0)) or 0)
104
+
105
+ return self._execute_with_retry(
106
+ self._send_request, messages, final_config, output_type, retry
107
+ )
108
+
109
+ def ask_stream(self, query: str, **kwargs) -> Iterator[str]:
110
+ """
111
+ Streaming text generation. Yields text chunks.
112
+ """
113
+ final_config = self._build_final_config(kwargs)
114
+ messages = self._prepare_messages(query, final_config)
115
+ yield from self._send_request_stream(messages, final_config)
116
+
117
+ # ─── Async API ──────────────────────────────────────────
118
+
119
+ async def ask_async(self, query: str, **kwargs) -> Response:
120
+ """
121
+ Native async version of ask().
122
+ Uses provider's async client directly — no thread pool wrapping.
123
+ """
124
+ final_config = self._build_final_config(kwargs)
125
+ messages = self._prepare_messages(query, final_config)
126
+ output_type = kwargs.get('output_type')
127
+ retry = int(kwargs.get('retry', final_config.extra.get('retry', 0)) or 0)
128
+
129
+ return await self._execute_with_retry_async(
130
+ self._send_request_async, messages, final_config, output_type, retry
131
+ )
132
+
133
+ async def ask_stream_async(self, query: str, **kwargs) -> AsyncIterator[str]:
134
+ """
135
+ Async streaming text generation. Yields text chunks.
136
+ """
137
+ final_config = self._build_final_config(kwargs)
138
+ messages = self._prepare_messages(query, final_config)
139
+ async for chunk in self._send_request_stream_async(messages, final_config):
140
+ yield chunk
141
+
142
+ # ─── Config Helpers ─────────────────────────────────────
143
+
144
+ def _build_final_config(self, kwargs: Dict[str, Any]) -> AdvancedConfig:
145
+ """Build final config by merging global config with per-request kwargs."""
146
+ request_config = AdvancedConfig(**kwargs)
147
+ final_config = self.config.merge(request_config)
148
+
149
+ # Ensure timeout has a default
150
+ timeout = kwargs.get('timeout', final_config.extra.get('timeout', 30))
151
+ final_config.extra['timeout'] = float(timeout) if timeout is not None else 30.0
152
+
153
+ return final_config
154
+
155
+ def _prepare_messages(self, query: str, config: AdvancedConfig) -> List[Dict[str, str]]:
156
+ """Build the messages list from query, persona, and config."""
157
+ messages = []
158
+
159
+ system_msg = config.system_message or self.persona or ""
160
+
161
+ # Smart capability: if json=True, enforce JSON system prompt
162
+ if config.extra.get('json'):
163
+ json_instruction = "You must respond with valid JSON only, without any markdown formatting or tags."
164
+ system_msg = f"{system_msg}\n{json_instruction}".strip()
165
+
166
+ if system_msg:
167
+ messages.append({"role": "system", "content": system_msg})
168
+
169
+ messages.append({"role": "user", "content": query})
170
+ return messages
171
+
172
+ # ─── Retry Logic ────────────────────────────────────────
173
+
174
+ def _execute_with_retry(self, func, messages, config, output_type, retry):
175
+ """Execute a sync function with exponential backoff + jitter."""
176
+ attempts = 0
177
+ last_error = None
178
+
179
+ while attempts <= retry:
180
+ try:
181
+ response = func(messages, config, output_type)
182
+ return response
183
+ except Exception as e:
184
+ last_error = e
185
+ if self._is_retryable_error(e) and attempts < retry:
186
+ attempts += 1
187
+ delay = min(2 ** attempts + random.uniform(0, 1), 30)
188
+ logger.warning(
189
+ "Transient error (%s). Retrying in %.1fs... (%d/%d)",
190
+ e.__class__.__name__, delay, attempts, retry
191
+ )
192
+ time.sleep(delay)
193
+ else:
194
+ raise
195
+
196
+ from .exceptions import AskAINetworkError
197
+ raise AskAINetworkError(f"Request failed after {retry} retries. Last error: {last_error}")
198
+
199
+ async def _execute_with_retry_async(self, func, messages, config, output_type, retry):
200
+ """Execute an async function with exponential backoff + jitter."""
201
+ attempts = 0
202
+ last_error = None
203
+
204
+ while attempts <= retry:
205
+ try:
206
+ response = await func(messages, config, output_type)
207
+ return response
208
+ except Exception as e:
209
+ last_error = e
210
+ if self._is_retryable_error(e) and attempts < retry:
211
+ attempts += 1
212
+ delay = min(2 ** attempts + random.uniform(0, 1), 30)
213
+ logger.warning(
214
+ "Transient error (%s). Retrying in %.1fs... (%d/%d)",
215
+ e.__class__.__name__, delay, attempts, retry
216
+ )
217
+ await asyncio.sleep(delay)
218
+ else:
219
+ raise
220
+
221
+ from .exceptions import AskAINetworkError
222
+ raise AskAINetworkError(f"Request failed after {retry} retries. Last error: {last_error}")
223
+
224
+ def _is_retryable_error(self, e: Exception) -> bool:
225
+ """Dynamically detect rate limits and network timeouts across different provider SDKs."""
226
+ from .exceptions import AskAINetworkError, AskAIRateLimitError
227
+ if isinstance(e, (AskAINetworkError, AskAIRateLimitError)):
228
+ return True
229
+
230
+ err_str = str(e).lower()
231
+ err_cls = e.__class__.__name__.lower()
232
+
233
+ retryable_indicators = ("ratelimit", "rate_limit", "429", "timeout", "connection", "connecterror")
234
+ return any(indicator in err_cls or indicator in err_str for indicator in retryable_indicators)
235
+
236
+ # ─── Abstract Methods (Subclasses Must Implement) ──────
237
+
238
+ def _send_request(self, messages: List[Dict[str, str]], config: AdvancedConfig, output_type: str = None) -> Response:
239
+ """Sync request — subclasses must implement."""
240
+ raise NotImplementedError
241
+
242
+ async def _send_request_async(self, messages: List[Dict[str, str]], config: AdvancedConfig, output_type: str = None) -> Response:
243
+ """Async request — subclasses should override for native async."""
244
+ # Default fallback: run sync in thread pool
245
+ loop = asyncio.get_running_loop()
246
+ return await loop.run_in_executor(None, self._send_request, messages, config, output_type)
247
+
248
+ def _send_request_stream(self, messages: List[Dict[str, str]], config: AdvancedConfig) -> Iterator[str]:
249
+ """Sync streaming — subclasses should override."""
250
+ from .exceptions import StreamNotSupportedError
251
+ raise StreamNotSupportedError(f"{self.__class__.__name__} does not support streaming.")
252
+
253
+ async def _send_request_stream_async(self, messages: List[Dict[str, str]], config: AdvancedConfig) -> AsyncIterator[str]:
254
+ """Async streaming — subclasses should override for native async."""
255
+ # Default fallback: wrap sync stream
256
+ loop = asyncio.get_running_loop()
257
+ for chunk in await loop.run_in_executor(None, lambda: list(self._send_request_stream(messages, config))):
258
+ yield chunk
259
+
260
+ # ─── Tool Definitions for Smart Intent ─────────────────
261
+
262
+ def _get_media_tools(self) -> List[Dict[str, Any]]:
263
+ return [
264
+ {
265
+ "type": "function",
266
+ "function": {
267
+ "name": "generate_image",
268
+ "description": (
269
+ "Generate an image based on a prompt. Use this when the user asks to draw, create, or show an image."
270
+ ),
271
+ "parameters": {
272
+ "type": "object",
273
+ "properties": {
274
+ "prompt": {
275
+ "type": "string",
276
+ "description": "The detailed description of the image to generate."
277
+ }
278
+ },
279
+ "required": ["prompt"]
280
+ }
281
+ }
282
+ },
283
+ {
284
+ "type": "function",
285
+ "function": {
286
+ "name": "generate_speech",
287
+ "description": (
288
+ "Generate audio speech from text. Use this when the user asks to say something, speak, or read aloud."
289
+ ),
290
+ "parameters": {
291
+ "type": "object",
292
+ "properties": {
293
+ "text": {
294
+ "type": "string",
295
+ "description": "The text to speak."
296
+ }
297
+ },
298
+ "required": ["text"]
299
+ }
300
+ }
301
+ }
302
+ ]
@@ -0,0 +1,107 @@
1
+ import sys
2
+ import argparse
3
+ from . import __version__
4
+ from . import (
5
+ OpenAI,
6
+ Groq,
7
+ Google,
8
+ OpenRouter,
9
+ Azure,
10
+ Anthropic,
11
+ AskAIError
12
+ )
13
+
14
+
15
+ PROVIDER_MAP = {
16
+ "openai": OpenAI,
17
+ "groq": Groq,
18
+ "google": Google,
19
+ "openrouter": OpenRouter,
20
+ "azure": Azure,
21
+ "anthropic": Anthropic,
22
+ }
23
+
24
+
25
+ def main():
26
+ parser = argparse.ArgumentParser(
27
+ description="ask-ai CLI: Ask AI anything directly from your terminal."
28
+ )
29
+ parser.add_argument("query", nargs="?", help="The text query to ask the AI.")
30
+ parser.add_argument(
31
+ "--provider", "-p",
32
+ default="openai",
33
+ choices=list(PROVIDER_MAP.keys()),
34
+ help="The AI provider to use (default: openai)."
35
+ )
36
+ parser.add_argument("--model", "-m", help="Specific model to use.")
37
+ parser.add_argument("--temp", type=float, help="Temperature setting.")
38
+ parser.add_argument("--system", "-s", help="System prompt.")
39
+ parser.add_argument("--json", action="store_true", help="Request JSON output.")
40
+ parser.add_argument("--stream", action="store_true", help="Enable streaming output.")
41
+ parser.add_argument("--version", "-v", action="version", version=f"ask-ai {__version__}")
42
+
43
+ args = parser.parse_args()
44
+
45
+ if not args.query:
46
+ parser.print_help()
47
+ sys.exit(0)
48
+
49
+ provider_class = PROVIDER_MAP.get(args.provider.lower())
50
+ if not provider_class:
51
+ print(f"Error: Provider '{args.provider}' not found.")
52
+ sys.exit(1)
53
+
54
+ try:
55
+ # Init provider
56
+ init_kwargs = {}
57
+ if args.model:
58
+ init_kwargs["model"] = args.model
59
+
60
+ ai = provider_class(**init_kwargs)
61
+
62
+ # Build ask kwargs
63
+ ask_kwargs = {}
64
+ if args.temp is not None:
65
+ ask_kwargs["temperature"] = args.temp
66
+ if args.system:
67
+ ask_kwargs["system_message"] = args.system
68
+ if args.json:
69
+ ask_kwargs["json"] = True
70
+
71
+ # Stream or regular
72
+ if args.stream:
73
+ try:
74
+ for chunk in ai.ask_stream(args.query, **ask_kwargs):
75
+ print(chunk, end="", flush=True)
76
+ print() # newline at end
77
+ except Exception:
78
+ # Fallback to non-streaming if provider doesn't support it
79
+ response = ai.ask(args.query, **ask_kwargs)
80
+ print(response.text)
81
+ else:
82
+ response = ai.ask(args.query, **ask_kwargs)
83
+
84
+ if response.media:
85
+ print(f"Generated Media: {response.media.type}")
86
+ filename = f"output.{'png' if response.media.type == 'image' else 'mp3'}"
87
+ response.media.save(filename)
88
+ print(f"Saved to {filename}")
89
+ elif args.json:
90
+ import json
91
+ print(json.dumps(response.json, indent=2, ensure_ascii=False))
92
+ else:
93
+ print(response.text)
94
+
95
+ except AskAIError as e:
96
+ print(f"Error: {e}", file=sys.stderr)
97
+ sys.exit(1)
98
+ except KeyboardInterrupt:
99
+ print("\nInterrupted.")
100
+ sys.exit(130)
101
+ except Exception as e:
102
+ print(f"Unexpected Error: {e}", file=sys.stderr)
103
+ sys.exit(1)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,81 @@
1
+ from typing import Optional, Dict, Any, List
2
+
3
+
4
+ class AdvancedConfig:
5
+ """
6
+ Configuration class for advanced AI settings.
7
+ Supports both direct init and aliased parameters for usability.
8
+ """
9
+ def __init__(
10
+ self,
11
+ temperature: Optional[float] = None,
12
+ max_tokens: Optional[int] = None,
13
+ top_p: Optional[float] = None,
14
+ frequency_penalty: Optional[float] = None,
15
+ presence_penalty: Optional[float] = None,
16
+ stop: Optional[List[str]] = None,
17
+ system_message: Optional[str] = None,
18
+ safe_mode: bool = False,
19
+ **kwargs
20
+ ):
21
+ self.temperature = temperature
22
+ self.max_tokens = max_tokens
23
+ self.top_p = top_p
24
+ self.frequency_penalty = frequency_penalty
25
+ self.presence_penalty = presence_penalty
26
+ self.stop = stop
27
+
28
+ # Smart Aliases for 10/10 Usability
29
+ # If user passes 'prompt' or 'system', treat it as 'system_message'
30
+ if system_message is None:
31
+ system_message = kwargs.pop("prompt", None) or kwargs.pop("system", None)
32
+ else:
33
+ # Remove aliases from kwargs if system_message was explicitly set
34
+ kwargs.pop("prompt", None)
35
+ kwargs.pop("system", None)
36
+
37
+ self.system_message = system_message
38
+ self.safe_mode = safe_mode
39
+ self.extra: Dict[str, Any] = kwargs
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ """Convert config to a dictionary, filtering None values."""
43
+ result = {}
44
+ for k, v in self.__dict__.items():
45
+ if k == 'extra':
46
+ continue
47
+ if v is not None:
48
+ result[k] = v
49
+ return result
50
+
51
+ def merge(self, other: 'AdvancedConfig') -> 'AdvancedConfig':
52
+ """
53
+ Merge another config into this one (other overrides self).
54
+ Returns a new AdvancedConfig without mutating either input.
55
+ """
56
+ merged = AdvancedConfig()
57
+
58
+ # Copy self values
59
+ for k, v in self.__dict__.items():
60
+ if k == 'extra':
61
+ continue
62
+ setattr(merged, k, v)
63
+
64
+ # Override with other values (only non-None)
65
+ for k, v in other.__dict__.items():
66
+ if k == 'extra':
67
+ continue
68
+ if v is not None:
69
+ setattr(merged, k, v)
70
+
71
+ # Deep merge extra dicts
72
+ merged_extra = dict(self.extra)
73
+ merged_extra.update(other.extra)
74
+ merged.extra = merged_extra
75
+
76
+ return merged
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: Dict[str, Any]) -> 'AdvancedConfig':
80
+ """Create an AdvancedConfig from a dictionary."""
81
+ return cls(**data)