askai-python 0.2.0__py3-none-any.whl → 0.3.3__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
@@ -96,7 +117,10 @@ class BaseProvider:
96
117
  last_error = None
97
118
  while attempts <= retry:
98
119
  try:
99
- return self._send_request(messages, final_config, output_type)
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
100
124
  except Exception as e:
101
125
  last_error = e
102
126
  if self._is_retryable_error(e) and attempts < retry:
@@ -112,6 +136,17 @@ class BaseProvider:
112
136
  from .exceptions import AskAINetworkError
113
137
  raise AskAINetworkError(f"Request failed after {retry} retries. Last error: {last_error}")
114
138
 
139
+ async def ask_async(self, query: str, **kwargs) -> Response:
140
+ """
141
+ Asynchronous wrapper for the .ask() method.
142
+ Perfect for Telegram bots (Aiogram) and FastAPI.
143
+ """
144
+ import asyncio
145
+ loop = asyncio.get_event_loop()
146
+ def _sync_ask():
147
+ return self.ask(query, **kwargs)
148
+ return await loop.run_in_executor(None, _sync_ask)
149
+
115
150
  def _is_retryable_error(self, e: Exception) -> bool:
116
151
  """Dynamically detect rate limits and network timeouts across different provider SDKs."""
117
152
  err_str = str(e).lower()
@@ -133,10 +168,16 @@ class BaseProvider:
133
168
 
134
169
  def _prepare_messages(self, query: str, config: AdvancedConfig) -> List[Dict[str, str]]:
135
170
  messages = []
136
- if config.system_message:
137
- messages.append({"role": "system", "content": config.system_message})
138
- elif self.persona:
139
- messages.append({"role": "system", "content": self.persona})
171
+
172
+ system_msg = config.system_message or self.persona or ""
173
+
174
+ # Smart capability: if json=True, enforce JSON system prompt
175
+ if config.extra.get('json'):
176
+ json_instruction = "You must respond with valid JSON only, without any markdown formatting or tags."
177
+ system_msg = f"{system_msg}\n{json_instruction}".strip()
178
+
179
+ if system_msg:
180
+ messages.append({"role": "system", "content": system_msg})
140
181
 
141
182
  messages.append({"role": "user", "content": query})
142
183
  return messages
ask_ai/exceptions.py CHANGED
@@ -22,5 +22,9 @@ class MediaTypeNotSupportedError(AskAIError):
22
22
  """Raised when a provider can't handle the requested media type."""
23
23
  pass
24
24
 
25
+ class AskAIParsingError(AskAIError):
26
+ """Raised when expect_json is True but the response cannot be parsed."""
27
+ pass
28
+
25
29
  # Alias for backwards compatibility internally
26
30
  APIKeyError = AskAIConfigError
@@ -0,0 +1,5 @@
1
+ openai>=1.0.0
2
+ groq
3
+ google-generativeai
4
+ anthropic
5
+ requests
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]}...")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: askai-python
3
- Version: 0.2.0
3
+ Version: 0.3.3
4
4
  Summary: AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, and more.
5
5
  Author-email: Hossein Ghorbani <hosseingh1068@gmail.com>
6
6
  License: MIT
@@ -53,16 +53,6 @@ print(Groq().ask("Explain black holes like I'm 5").text)
53
53
  - **Zero config**: Keys are pulled from the environment automatically
54
54
  - **SDK-first, not a framework**: It stays out of your way.
55
55
 
56
- ## ⚖️ How it compares
57
-
58
- | Feature | ask-ai | LangChain |
59
- | -------------- | ------ | --------- |
60
- | Setup time | 30 sec | 1 hour |
61
- | Learning curve | ⭐ | ⭐⭐⭐⭐⭐ |
62
- | Async support | ⏳ *(Coming soon)* | ⚠️ Complex |
63
- | Retry/Timeout | ✅ Built-in | ❌ Manual |
64
- | Gateway needed | ❌ No | ❌ No |
65
-
66
56
  ## 🚫 What this project is NOT
67
57
 
68
58
  > ❌ Not an AI framework
@@ -73,8 +63,43 @@ It does one thing perfectly: **Simplifying the API call to LLMs.**
73
63
 
74
64
  ---
75
65
 
66
+ ## 🛠️ Advanced Usage
67
+
68
+ ### 🧰 Developer QoL Utilities (Auto-Parsing)
69
+
70
+ <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%">
72
+ </p>
73
+ Stop writing Regex to clean up model outputs! `ask-ai` comes with built-in text processing flags:
74
+
75
+ ```python
76
+ from ask_ai import OpenAI
77
+ ai = OpenAI()
78
+
79
+ # 1. Clean Markdown (Removes ```json and ``` tags)
80
+ # Perfect for extracting raw data from models that wrap everything in markdown
81
+ clean_text = ai.ask("Write JSON", clean=True).text
82
+
83
+ # 2. Extract Code (Returns ONLY the code block, ignores conversational filler)
84
+ # Great for automation pipelines
85
+ code = ai.ask("Write a python ping script", code=True).text
86
+
87
+ # 3. Strip Tags (Removes <think> blocks and HTML)
88
+ # Essential for reasoning models like DeepSeek-R1
89
+ answer_only = ai.ask("What is 1+1?", strip=True).text
90
+
91
+ # 4. Enforce & Parse JSON (Directly returns a Parsed Python Dictionary)
92
+ # Adds JSON instructions to the prompt and safely runs json.loads()
93
+ data_dict = ai.ask("Extract user info", json=True).json
94
+ print(data_dict['name'])
95
+ ```
96
+
76
97
  ## 🚀 Built-in Resiliency (Retries & Timeouts)
77
98
 
99
+ <p align="center">
100
+ <img src="https://raw.githubusercontent.com/Hosseinghorbani0/ask-ai/main/assets/resiliency.svg" alt="ask-ai resiliency banner" width="100%">
101
+ </p>
102
+
78
103
  Build reliable apps without writing your own loops. `askai-python` handles rate limits (`429`) and network drops via an internal exponential backoff.
79
104
 
80
105
  ```python
@@ -1,10 +1,12 @@
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=N3U_O_alfhTyiIz_ts-z7YYdoJlR3yJcQxvD3Z9NNpY,7402
3
+ ask_ai/base.py,sha256=Hxk7hFJqK8uBSabVBAMrLziPdEo0QWrGrGgpIlIEcN8,8934
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=V7IVUL-oNkUYXnek0J4Bt66MaxgvJQCVk8bBTpqsMk4,769
6
+ ask_ai/exceptions.py,sha256=jUP1tJJvvx8ymFXU19Em3JBbTPixEnO_uj6ZC5zDyEM,893
7
7
  ask_ai/media.py,sha256=cxPZ0XoY0iU3Y8bY-YV3MGPlNFOJ_FjjwMATgo-jcFw,1821
8
+ ask_ai/requirements.txt,sha256=Nvs7a0Ol8J6bTUWUJ02VqjNo9jazd6houEmDLqu-Qcw,58
9
+ ask_ai/utils.py,sha256=cRokmKtoNOgQsGrKTdKBjv9vdG3sXpiA6W9HtcjNIho,1582
8
10
  ask_ai/providers/__init__.py,sha256=tVSW-FfSMPzCYK_TZy93W5ckdVe2PBPyzogpKXH_jhg,341
9
11
  ask_ai/providers/anthropic_provider.py,sha256=NL_6E7eNTYAoYpym4ewD8Vj8tF_ewuFjFkRR0Jj0L-U,3670
10
12
  ask_ai/providers/azure_provider.py,sha256=bY8MlHIjiH_H00Q78_2ooCgN1OPeM3ajpvJ2AkxNsTI,2086
@@ -12,8 +14,8 @@ ask_ai/providers/google_provider.py,sha256=DsIgu_cBC7ta0lKclAlge1PYfcp3zXKPLjWSe
12
14
  ask_ai/providers/groq_provider.py,sha256=iU7BTpvmBniE78VdTQETrCXrJzdXTpWH2oN9_sUgnFk,3268
13
15
  ask_ai/providers/openai_provider.py,sha256=b6grPbsUIY19HkABDrUIAZAIcJCKQup5tooU2LFlkvo,4793
14
16
  ask_ai/providers/openrouter_provider.py,sha256=LCGK-vNxy_maY2Fle6xh_i8MVm7a_bhjCaF7Uj0H3AM,1986
15
- askai_python-0.2.0.dist-info/licenses/LICENSE,sha256=SUSbAUsh_Uqw7NDEKwAGRVPS7sojihBHIBGBx6WZxvg,1073
16
- askai_python-0.2.0.dist-info/METADATA,sha256=EHGGvSOedlST4YqUG8KD3uT0rmmK3VSJt3A35dAfjqM,3609
17
- askai_python-0.2.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
18
- askai_python-0.2.0.dist-info/top_level.txt,sha256=fW2GapXaz6f-2UAuzVVPUPNfZUK_ZS1dpLyCllcE03s,7
19
- askai_python-0.2.0.dist-info/RECORD,,
17
+ askai_python-0.3.3.dist-info/licenses/LICENSE,sha256=SUSbAUsh_Uqw7NDEKwAGRVPS7sojihBHIBGBx6WZxvg,1073
18
+ askai_python-0.3.3.dist-info/METADATA,sha256=o3baxmEPjdg7wflgM-aLxRL-FVuw5Q4tIJL8bVCCh90,4558
19
+ askai_python-0.3.3.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
20
+ askai_python-0.3.3.dist-info/top_level.txt,sha256=fW2GapXaz6f-2UAuzVVPUPNfZUK_ZS1dpLyCllcE03s,7
21
+ askai_python-0.3.3.dist-info/RECORD,,