askai-python 0.1.6__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.
ask_ai/base.py CHANGED
@@ -9,9 +9,30 @@ class Response:
9
9
  """
10
10
  Unified response object for all easyai requests.
11
11
  """
12
- def __init__(self, text: str = "", media: Union[ImageObject, AudioObject, None] = None):
13
- self.text = text
12
+ def __init__(self, text: str = "", media: Union[ImageObject, AudioObject, None] = None, **kwargs):
13
+ self._raw_text = text
14
14
  self.media = media
15
+ self._kwargs = kwargs
16
+
17
+ @property
18
+ def text(self) -> str:
19
+ text = self._raw_text
20
+ if self._kwargs.get('strip'):
21
+ from .utils import strip_tags
22
+ text = strip_tags(text)
23
+ if self._kwargs.get('clean'):
24
+ from .utils import clean_markdown
25
+ text = clean_markdown(text)
26
+ if self._kwargs.get('code'):
27
+ from .utils import extract_code
28
+ text = extract_code(text)
29
+ return text
30
+
31
+ @property
32
+ def json(self) -> Union[Dict[str, Any], list]:
33
+ """Returns parsed JSON. If 'json=True' was passed, this is highly reliable."""
34
+ from .utils import parse_json
35
+ return parse_json(self._raw_text)
15
36
 
16
37
  def __str__(self):
17
38
  return self.text
@@ -82,18 +103,70 @@ class BaseProvider:
82
103
  # 3. Check for specific output_type override (e.g. user forced image)
83
104
  output_type = kwargs.get('output_type')
84
105
 
85
- # 4. Call Provider Implementation
86
- return self._send_request(messages, final_config, output_type)
106
+ # 4. Extract timeout and retry safely
107
+ timeout_val = kwargs.get('timeout', final_config.extra.get('timeout', 30))
108
+ retry_val = kwargs.get('retry', final_config.extra.get('retry', 0))
109
+
110
+ timeout = float(timeout_val) if timeout_val is not None else 30.0
111
+ retry = int(retry_val) if retry_val is not None else 0
112
+
113
+ final_config.extra['timeout'] = timeout # Pass down to providers
114
+
115
+ # 5. Execute with Retry Logic
116
+ attempts = 0
117
+ last_error = None
118
+ while attempts <= retry:
119
+ try:
120
+ response = self._send_request(messages, final_config, output_type)
121
+ # Attach parsing arguments to the response object dynamically
122
+ response._kwargs.update(kwargs)
123
+ return response
124
+ except Exception as e:
125
+ last_error = e
126
+ if self._is_retryable_error(e) and attempts < retry:
127
+ attempts += 1
128
+ delay = 2 ** attempts # 2s, 4s, 8s...
129
+ print(f"[ask-ai] Transient error ({e.__class__.__name__}). Retrying in {delay}s... ({attempts}/{retry})")
130
+ import time
131
+ time.sleep(delay)
132
+ else:
133
+ raise e
134
+
135
+ # Fallback if loop exits without returning
136
+ from .exceptions import AskAINetworkError
137
+ raise AskAINetworkError(f"Request failed after {retry} retries. Last error: {last_error}")
138
+
139
+ def _is_retryable_error(self, e: Exception) -> bool:
140
+ """Dynamically detect rate limits and network timeouts across different provider SDKs."""
141
+ err_str = str(e).lower()
142
+ err_cls = e.__class__.__name__.lower()
143
+
144
+ # Explicit ask-ai errors
145
+ from .exceptions import AskAINetworkError, AskAIRateLimitError
146
+ if isinstance(e, (AskAINetworkError, AskAIRateLimitError)): return True
147
+
148
+ # Heuristics for underlying provider SDKs (OpenAI, Anthropic, etc.)
149
+ if "ratelimit" in err_cls or "rate_limit" in err_cls or "429" in err_str: return True
150
+ if "timeout" in err_cls or "timeout" in err_str: return True
151
+ if "connection" in err_cls or "connecterror" in err_cls: return True
152
+
153
+ return False
87
154
 
88
155
  def _merge_configs(self, global_conf: AdvancedConfig, req_conf: AdvancedConfig) -> AdvancedConfig:
89
156
  return global_conf.merge(req_conf) # We need to implement merge logic in AdvancedConfig properly or here
90
157
 
91
158
  def _prepare_messages(self, query: str, config: AdvancedConfig) -> List[Dict[str, str]]:
92
159
  messages = []
93
- if config.system_message:
94
- messages.append({"role": "system", "content": config.system_message})
95
- elif self.persona:
96
- messages.append({"role": "system", "content": self.persona})
160
+
161
+ system_msg = config.system_message or self.persona or ""
162
+
163
+ # Smart capability: if json=True, enforce JSON system prompt
164
+ if config.extra.get('json'):
165
+ json_instruction = "You must respond with valid JSON only, without any markdown formatting or tags."
166
+ system_msg = f"{system_msg}\n{json_instruction}".strip()
167
+
168
+ if system_msg:
169
+ messages.append({"role": "system", "content": system_msg})
97
170
 
98
171
  messages.append({"role": "user", "content": query})
99
172
  return messages
ask_ai/exceptions.py CHANGED
@@ -2,14 +2,29 @@ class AskAIError(Exception):
2
2
  """Base exception for all ask-ai errors."""
3
3
  pass
4
4
 
5
- class APIKeyError(AskAIError):
6
- """Raised when API key is missing or invalid."""
5
+ class AskAIConfigError(AskAIError):
6
+ """Raised when configuration (like API keys) is missing or invalid."""
7
+ pass
8
+
9
+ class AskAINetworkError(AskAIError):
10
+ """Raised when a network connection fails (timeout, disconnect)."""
11
+ pass
12
+
13
+ class AskAIRateLimitError(AskAIError):
14
+ """Raised when the provider rate limits the request (429)."""
7
15
  pass
8
16
 
9
17
  class ProviderError(AskAIError):
10
- """Raised when the provider API fails (e.g. 500 error)."""
18
+ """Raised when the provider API fails (e.g. 500 error or bad request)."""
11
19
  pass
12
20
 
13
21
  class MediaTypeNotSupportedError(AskAIError):
14
22
  """Raised when a provider can't handle the requested media type."""
15
23
  pass
24
+
25
+ class AskAIParsingError(AskAIError):
26
+ """Raised when expect_json is True but the response cannot be parsed."""
27
+ pass
28
+
29
+ # Alias for backwards compatibility internally
30
+ APIKeyError = AskAIConfigError
ask_ai/utils.py ADDED
@@ -0,0 +1,43 @@
1
+ import re
2
+ import json
3
+ from typing import Dict, Any, Union
4
+
5
+ def clean_markdown(text: str) -> str:
6
+ """Removes markdown code blocks like ```json ... ```"""
7
+ text = text.strip()
8
+ if text.startswith("```"):
9
+ # find the first newline
10
+ first_newline = text.find("\n")
11
+ if first_newline != -1:
12
+ text = text[first_newline+1:]
13
+ if text.endswith("```"):
14
+ text = text[:-3]
15
+ return text.strip()
16
+
17
+ def extract_code(text: str) -> str:
18
+ """Extracts code blocks from text, ignoring conversational filler."""
19
+ text = str(text)
20
+ matches = re.findall(r'```(?:[\w]*\n)?(.*?)```', text, re.DOTALL)
21
+ if matches:
22
+ return "\n\n".join(m.strip() for m in matches)
23
+ return text
24
+
25
+ def strip_tags(text: str) -> str:
26
+ """Removes XML/HTML tags, including <think>...</think> blocks."""
27
+ # First remove <think>...</think> entirely if we don't want the thought process
28
+ text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
29
+ # Remove any other HTML tags
30
+ text = re.sub(r'<[^>]+>', '', text)
31
+ return text.strip()
32
+
33
+ def parse_json(text: str) -> Union[Dict[str, Any], list]:
34
+ """Tries to parse JSON from text, employing cleaning if necessary."""
35
+ try:
36
+ return json.loads(text)
37
+ except json.JSONDecodeError:
38
+ cleaned = clean_markdown(text)
39
+ try:
40
+ return json.loads(cleaned)
41
+ except json.JSONDecodeError as e:
42
+ from .exceptions import AskAIParsingError
43
+ raise AskAIParsingError(f"Failed to parse JSON: {e}\nContent snippet: {text[:100]}...")
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: askai-python
3
+ Version: 0.2.1
4
+ Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, and more.
5
+ Author-email: Hossein Ghorbani <hosseingh1068@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://hosseinghorbani0.ir/
8
+ Project-URL: Bug Tracker, https://github.com/Hosseinghorbani0/ask-ai/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ 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
18
+ Dynamic: license-file
19
+
20
+ # askai-python 🚀
21
+
22
+ **A minimal Python SDK to switch between LLM providers in one line.**
23
+ No frameworks. No servers. No overengineering.
24
+
25
+ [![PyPI version](https://img.shields.io/pypi/v/askai-python.svg)](https://pypi.org/project/askai-python/)
26
+ [![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)
28
+
29
+ ---
30
+
31
+ ## ⚡ Quick Start (5 seconds)
32
+
33
+ ```bash
34
+ pip install askai-python
35
+ ```
36
+
37
+ ```python
38
+ from ask_ai import OpenAI, Groq
39
+
40
+ # Auto-detects OPENAI_API_KEY from environment
41
+ print(OpenAI().ask("Explain black holes like I'm 5").text)
42
+
43
+ # Switch provider instantly
44
+ print(Groq().ask("Explain black holes like I'm 5").text)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🧐 Why ask-ai?
50
+
51
+ - **One function**: Just call `.ask()`
52
+ - **Multiple providers**: OpenAI, Anthropic, Google Gemini, Groq, Azure, OpenRouter
53
+ - **Zero config**: Keys are pulled from the environment automatically
54
+ - **SDK-first, not a framework**: It stays out of your way.
55
+
56
+ ## 🚫 What this project is NOT
57
+
58
+ > ❌ Not an AI framework
59
+ > ❌ Not an API gateway
60
+ > ❌ Not an agent memory system
61
+
62
+ It does one thing perfectly: **Simplifying the API call to LLMs.**
63
+
64
+ ---
65
+
66
+ ## 🛠️ Advanced Usage
67
+
68
+ ### 🧰 Developer QoL Utilities (Auto-Parsing)
69
+ Stop writing Regex to clean up model outputs! `ask-ai` comes with built-in text processing flags:
70
+
71
+ ```python
72
+ from ask_ai import OpenAI
73
+ ai = OpenAI()
74
+
75
+ # 1. Clean Markdown (Removes ```json and ``` tags)
76
+ # Perfect for extracting raw data from models that wrap everything in markdown
77
+ clean_text = ai.ask("Write JSON", clean=True).text
78
+
79
+ # 2. Extract Code (Returns ONLY the code block, ignores conversational filler)
80
+ # Great for automation pipelines
81
+ code = ai.ask("Write a python ping script", code=True).text
82
+
83
+ # 3. Strip Tags (Removes <think> blocks and HTML)
84
+ # Essential for reasoning models like DeepSeek-R1
85
+ answer_only = ai.ask("What is 1+1?", strip=True).text
86
+
87
+ # 4. Enforce & Parse JSON (Directly returns a Parsed Python Dictionary)
88
+ # Adds JSON instructions to the prompt and safely runs json.loads()
89
+ data_dict = ai.ask("Extract user info", json=True).json
90
+ print(data_dict['name'])
91
+ ```
92
+
93
+ ## 🚀 Built-in Resiliency (Retries & Timeouts)
94
+
95
+ Build reliable apps without writing your own loops. `askai-python` handles rate limits (`429`) and network drops via an internal exponential backoff.
96
+
97
+ ```python
98
+ from ask_ai import OpenAI
99
+
100
+ ai = OpenAI()
101
+
102
+ # Automatically retries up to 3 times on transient errors, with a 15-second timeout
103
+ response = ai.ask(
104
+ "Write a complex python script",
105
+ retry=3,
106
+ timeout=15
107
+ )
108
+ ```
109
+
110
+ ## 🔌 Supported Providers
111
+
112
+ | Provider | Class | Capabilities |
113
+ |----------|-------|-------------|
114
+ | **OpenAI** | `OpenAI` | Text, Images (DALL-E), Vision |
115
+ | **Anthropic** | `Anthropic` | Text, Vision (Claude 3.5) |
116
+ | **Google** | `Google` | Text, Images, Video, Audio |
117
+ | **Groq** | `Groq` | Ultra-fast Llama 3 & Mixtral |
118
+ | **Azure** | `Azure` | Enterprise-grade OpenAI |
119
+ | **OpenRouter**| `OpenRouter`| 100+ community models |
120
+
121
+ ---
122
+
123
+ ## 🗺️ Roadmap
124
+
125
+ - [x] Baseline multiple providers
126
+ - [x] Automated Retry & Timeout controls
127
+ - [ ] Async API (`await ask_async`)
128
+ - [ ] Provider Fallback (`fallback=[Groq()]`)
129
+ - [ ] Structured Outputs (Pydantic Support)
130
+
131
+ ---
132
+
133
+ ## 🔗 Important Links
134
+
135
+ - **GitHub Repository**: [Hosseinghorbani0/ask-ai](https://github.com/Hosseinghorbani0/ask-ai) (Star us! ⭐)
136
+ - **Official Website**: [hosseinghorbani0.ir](https://hosseinghorbani0.ir/)
137
+ - **Bug Tracker**: [Report an Issue](https://github.com/Hosseinghorbani0/ask-ai/issues)
@@ -1,10 +1,11 @@
1
1
  ask_ai/__init__.py,sha256=i08NdBtFsjV3vT9ZqhEsroXd7vvZrWDojsOW-79iTV4,439
2
2
  ask_ai/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
- ask_ai/base.py,sha256=2-SnoQiy-RAmjCceiAsx-rqwbh20xAMpLdtPiiJdNUk,5389
3
+ ask_ai/base.py,sha256=pMNc6YhHiWasi1IH6Qe6xdtNwoxzQu68JPW5KBrZCsE,8543
4
4
  ask_ai/cli.py,sha256=8oeRLsT2lrcuG0vDq_a4CVqvzzmwDG3HxHW4BYg3IxM,2708
5
5
  ask_ai/config.py,sha256=oP5RjdbbdDve8G-FlcbNaMXPorBw96GxhCbfFWnfbIw,1934
6
- ask_ai/exceptions.py,sha256=JT2XbhsaWCFn3bPJD37RVH9HehV1ngus5s05v6xc1Tg,414
6
+ ask_ai/exceptions.py,sha256=jUP1tJJvvx8ymFXU19Em3JBbTPixEnO_uj6ZC5zDyEM,893
7
7
  ask_ai/media.py,sha256=cxPZ0XoY0iU3Y8bY-YV3MGPlNFOJ_FjjwMATgo-jcFw,1821
8
+ ask_ai/utils.py,sha256=cRokmKtoNOgQsGrKTdKBjv9vdG3sXpiA6W9HtcjNIho,1582
8
9
  ask_ai/providers/__init__.py,sha256=tVSW-FfSMPzCYK_TZy93W5ckdVe2PBPyzogpKXH_jhg,341
9
10
  ask_ai/providers/anthropic_provider.py,sha256=NL_6E7eNTYAoYpym4ewD8Vj8tF_ewuFjFkRR0Jj0L-U,3670
10
11
  ask_ai/providers/azure_provider.py,sha256=bY8MlHIjiH_H00Q78_2ooCgN1OPeM3ajpvJ2AkxNsTI,2086
@@ -12,8 +13,8 @@ ask_ai/providers/google_provider.py,sha256=DsIgu_cBC7ta0lKclAlge1PYfcp3zXKPLjWSe
12
13
  ask_ai/providers/groq_provider.py,sha256=iU7BTpvmBniE78VdTQETrCXrJzdXTpWH2oN9_sUgnFk,3268
13
14
  ask_ai/providers/openai_provider.py,sha256=b6grPbsUIY19HkABDrUIAZAIcJCKQup5tooU2LFlkvo,4793
14
15
  ask_ai/providers/openrouter_provider.py,sha256=LCGK-vNxy_maY2Fle6xh_i8MVm7a_bhjCaF7Uj0H3AM,1986
15
- askai_python-0.1.6.dist-info/licenses/LICENSE,sha256=SUSbAUsh_Uqw7NDEKwAGRVPS7sojihBHIBGBx6WZxvg,1073
16
- askai_python-0.1.6.dist-info/METADATA,sha256=auX_qAPfA8IY8g38KL-IQDxBddvRdIFiVDzGkzQHd3o,3850
17
- askai_python-0.1.6.dist-info/WHEEL,sha256=YLJXdYXQ2FQ0Uqn2J-6iEIC-3iOey8lH3xCtvFLkd8Q,91
18
- askai_python-0.1.6.dist-info/top_level.txt,sha256=fW2GapXaz6f-2UAuzVVPUPNfZUK_ZS1dpLyCllcE03s,7
19
- askai_python-0.1.6.dist-info/RECORD,,
16
+ askai_python-0.2.1.dist-info/licenses/LICENSE,sha256=SUSbAUsh_Uqw7NDEKwAGRVPS7sojihBHIBGBx6WZxvg,1073
17
+ askai_python-0.2.1.dist-info/METADATA,sha256=sDAgOTv2w_wTOBcJrOp5-c8io7eMgHi13kjcv2h_myE,4218
18
+ askai_python-0.2.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
19
+ askai_python-0.2.1.dist-info/top_level.txt,sha256=fW2GapXaz6f-2UAuzVVPUPNfZUK_ZS1dpLyCllcE03s,7
20
+ askai_python-0.2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (81.0.0)
2
+ Generator: setuptools (82.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,135 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: askai-python
3
- Version: 0.1.6
4
- Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, and more.
5
- Author-email: Hossein Ghorbani <hosseingh1068@gmail.com>
6
- License: MIT
7
- Project-URL: Homepage, https://hosseinghorbani0.ir/
8
- Project-URL: Bug Tracker, https://github.com/Hosseinghorbani0/ask-ai/issues
9
- Requires-Python: >=3.8
10
- Description-Content-Type: text/markdown
11
- 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
18
- Dynamic: license-file
19
-
20
- # askai-python 🚀
21
-
22
- **AI Made Stupid Simple.** One unified Python client for OpenAI, Claude, Gemini, Groq, and Azure.
23
-
24
- [![PyPI version](https://img.shields.io/pypi/v/askai-python.svg)](https://pypi.org/project/askai-python/)
25
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
26
- [![GitHub](https://img.shields.io/badge/GitHub-View%20Source-blue.svg)](https://github.com/Hosseinghorbani0/ask-ai)
27
-
28
- ---
29
-
30
- ## ⚡ What is askai-python?
31
-
32
- `askai-python` is a lightweight, zero-dependency-logic wrapper that provides a **single API** for all major AI providers. Stop learning multiple SDKs; just call `.ask()` and get your answer.
33
-
34
- - **Unified Interface**: One syntax for OpenAI, Anthropic, Google, and more.
35
- - **Zero-Config**: Seamlessly uses environment variables for API keys.
36
- - **Smart Media**: Easy handling of images and audio (Provider permitting).
37
- - **Production Grade**: Built-in robust error handling and type hints.
38
-
39
- ---
40
-
41
- ## 📦 Installation
42
-
43
- ```bash
44
- pip install askai-python
45
- ```
46
-
47
- ---
48
-
49
- ## 🚀 Examples & Tutorials
50
-
51
- ### 1. Basic Usage (The 3-Line Magic)
52
- ```python
53
- from ask_ai import OpenAI
54
-
55
- ai = OpenAI() # Auto-loads OPENAI_API_KEY from env
56
- print(ai.ask("What is the capital of France?").text)
57
- ```
58
-
59
- ### 2. Multi-Provider Mastery
60
- Switching between top-tier models has never been easier:
61
-
62
- ```python
63
- from ask_ai import OpenAI, Anthropic, Google
64
-
65
- prompt = "Explain quantum computing to a 5-year-old."
66
-
67
- # OpenAI (GPT-4o)
68
- print(OpenAI().ask(prompt).text)
69
-
70
- # Anthropic (Claude 3.5 Sonnet)
71
- print(Anthropic().ask(prompt).text)
72
-
73
- # Google (Gemini 1.5 Pro)
74
- print(Google().ask(prompt).text)
75
- ```
76
-
77
- ### 3. Advanced Configuration (System Personas)
78
- You can set a system prompt and temperature globally for an instance or per-request.
79
-
80
- ```python
81
- from ask_ai import OpenAI
82
-
83
- ai = OpenAI()
84
-
85
- # Global config
86
- ai.advanced(
87
- prompt="You are a sarcastic robot from the year 3000.",
88
- temperature=0.9
89
- )
90
-
91
- print(ai.ask("What is love?").text)
92
-
93
- # Per-request override
94
- print(ai.ask("What is 1+1?", temperature=0.1).text)
95
- ```
96
-
97
- ### 4. Handling Structured Exceptions
98
- Build reliable apps by catching specific AI errors:
99
-
100
- ```python
101
- from ask_ai import OpenAI
102
- from ask_ai.exceptions import AskAIError, APIKeyError
103
-
104
- try:
105
- ai = OpenAI(api_key="invalid-key")
106
- ai.ask("Hello")
107
- except APIKeyError:
108
- print("Check your API keys!")
109
- except AskAIError as e:
110
- print(f"An AI error occurred: {e}")
111
- ```
112
-
113
- ---
114
-
115
- ## 🔌 Supported Providers
116
-
117
- | Provider | Class | Capabilities |
118
- |----------|-------|-------------|
119
- | **OpenAI** | `OpenAI` | Text, Images (DALL-E), Vision |
120
- | **Anthropic** | `Anthropic` | Text, Vision (Claude 3.5) |
121
- | **Google** | `Google` | Text, Images, Video, Audio |
122
- | **Groq** | `Groq` | Ultra-fast Llama 3 & Mixtral |
123
- | **Azure** | `Azure` | Enterprise-grade OpenAI |
124
- | **OpenRouter**| `OpenRouter`| 100+ community models |
125
-
126
- ---
127
-
128
- ## 🔗 Important Links
129
-
130
- - **GitHub Repository**: [Hosseinghorbani0/ask-ai](https://github.com/Hosseinghorbani0/ask-ai) (Star us! ⭐)
131
- - **Official Website**: [hosseinghorbani0.ir](https://hosseinghorbani0.ir/)
132
- - **Bug Tracker**: [Report an Issue](https://github.com/Hosseinghorbani0/ask-ai/issues)
133
-
134
- ---
135
- *Built with ❤️ by [Hossein Ghorbani](https://github.com/Hosseinghorbani0).*