askai-python 0.2.0__tar.gz → 0.3.3__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 (28) hide show
  1. {askai_python-0.2.0 → askai_python-0.3.3}/PKG-INFO +36 -11
  2. {askai_python-0.2.0 → askai_python-0.3.3}/README.md +41 -12
  3. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/base.py +48 -7
  4. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/exceptions.py +4 -0
  5. askai_python-0.3.3/ask_ai/requirements.txt +5 -0
  6. askai_python-0.3.3/ask_ai/utils.py +43 -0
  7. {askai_python-0.2.0 → askai_python-0.3.3}/askai_python.egg-info/PKG-INFO +36 -11
  8. {askai_python-0.2.0 → askai_python-0.3.3}/askai_python.egg-info/SOURCES.txt +2 -0
  9. {askai_python-0.2.0 → askai_python-0.3.3}/docs/PYPI_README.md +35 -10
  10. {askai_python-0.2.0 → askai_python-0.3.3}/pyproject.toml +8 -1
  11. {askai_python-0.2.0 → askai_python-0.3.3}/tests/test_core.py +1 -2
  12. {askai_python-0.2.0 → askai_python-0.3.3}/LICENSE +0 -0
  13. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/__init__.py +0 -0
  14. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/__main__.py +0 -0
  15. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/cli.py +0 -0
  16. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/config.py +0 -0
  17. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/media.py +0 -0
  18. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/__init__.py +0 -0
  19. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/anthropic_provider.py +0 -0
  20. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/azure_provider.py +0 -0
  21. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/google_provider.py +0 -0
  22. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/groq_provider.py +0 -0
  23. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/openai_provider.py +0 -0
  24. {askai_python-0.2.0 → askai_python-0.3.3}/ask_ai/providers/openrouter_provider.py +0 -0
  25. {askai_python-0.2.0 → askai_python-0.3.3}/askai_python.egg-info/dependency_links.txt +0 -0
  26. {askai_python-0.2.0 → askai_python-0.3.3}/askai_python.egg-info/requires.txt +0 -0
  27. {askai_python-0.2.0 → askai_python-0.3.3}/askai_python.egg-info/top_level.txt +0 -0
  28. {askai_python-0.2.0 → askai_python-0.3.3}/setup.cfg +0 -0
@@ -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
@@ -12,6 +12,11 @@
12
12
  <a href="docs/README_ja.md"><img src="https://flagcdn.com/20x15/jp.png" alt="Japanese"> 日本語</a>
13
13
  </p>
14
14
 
15
+
16
+ <p align="center">
17
+ <img src="assets/banner.svg" alt="ask-ai animated banner" width="100%">
18
+ </p>
19
+
15
20
  <p align="center">
16
21
  <b>A minimal Python SDK to switch between LLM providers in one line.</b><br/>
17
22
  No frameworks. No servers. No overengineering.
@@ -21,6 +26,8 @@
21
26
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
22
27
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
23
28
 
29
+
30
+
24
31
  ---
25
32
 
26
33
  ## ⚡ Quick Start (5 seconds)
@@ -48,17 +55,6 @@ Groq().ask("Explain black holes like I'm 5").text
48
55
  - **Zero config**: Keys are pulled from the environment automatically
49
56
  - **SDK-first, not a framework**: It stays out of your way.
50
57
 
51
- ## ⚖️ How it compares
52
-
53
- | Feature | ask-ai | LangChain |
54
- | -------------- | ------ | --------- |
55
- | Setup time | 30 sec | 1 hour |
56
- | Learning curve | ⭐ | ⭐⭐⭐⭐⭐ |
57
- | Async support | ⏳ *(Coming soon)* | ⚠️ Complex |
58
- | Retry/Timeout | ✅ Built-in | ❌ Manual |
59
- | Gateway needed | ❌ No | ❌ No |
60
- | Lines to switch| **1** | 20+ |
61
-
62
58
  ## 🚫 What this project is NOT
63
59
 
64
60
  > ❌ Not an AI framework
@@ -71,7 +67,40 @@ It does one thing perfectly: **Simplifying the API call to LLMs.**
71
67
 
72
68
  ## 🛠️ Advanced Usage
73
69
 
74
- ### Built-in Retries & Timeout (Resiliency)
70
+ ### 🧰 Developer QoL Utilities (Auto-Parsing)
71
+
72
+ <p align="center">
73
+ <img src="assets/features.svg" alt="ask-ai features parsing banner" width="100%">
74
+ </p>
75
+ Stop writing Regex to clean up model outputs! `ask-ai` comes with built-in text processing flags:
76
+
77
+ ```python
78
+ from ask_ai import OpenAI
79
+ ai = OpenAI()
80
+
81
+ # 1. Clean Markdown (Removes ```json and ``` tags)
82
+ # Perfect for extracting raw data from models that wrap everything in markdown
83
+ clean_text = ai.ask("Write JSON", clean=True).text
84
+
85
+ # 2. Extract Code (Returns ONLY the code block, ignores conversational filler)
86
+ # Great for automation pipelines
87
+ code = ai.ask("Write a python ping script", code=True).text
88
+
89
+ # 3. Strip Tags (Removes <think> blocks and HTML)
90
+ # Essential for reasoning models like DeepSeek-R1
91
+ answer_only = ai.ask("What is 1+1?", strip=True).text
92
+
93
+ # 4. Enforce & Parse JSON (Directly returns a Parsed Python Dictionary)
94
+ # Adds JSON instructions to the prompt and safely runs json.loads()
95
+ data_dict = ai.ask("Extract user info", json=True).json
96
+ print(data_dict['name'])
97
+ ```
98
+
99
+ ### 🔄 Built-in Retries & Timeout (Resiliency)
100
+
101
+ <p align="center">
102
+ <img src="assets/resiliency.svg" alt="ask-ai resiliency banner" width="100%">
103
+ </p>
75
104
  Handle rate limits (`429`) and network drops automatically:
76
105
 
77
106
  ```python
@@ -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
@@ -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
@@ -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
@@ -8,6 +8,8 @@ ask_ai/cli.py
8
8
  ask_ai/config.py
9
9
  ask_ai/exceptions.py
10
10
  ask_ai/media.py
11
+ ask_ai/requirements.txt
12
+ ask_ai/utils.py
11
13
  ask_ai/providers/__init__.py
12
14
  ask_ai/providers/anthropic_provider.py
13
15
  ask_ai/providers/azure_provider.py
@@ -34,16 +34,6 @@ print(Groq().ask("Explain black holes like I'm 5").text)
34
34
  - **Zero config**: Keys are pulled from the environment automatically
35
35
  - **SDK-first, not a framework**: It stays out of your way.
36
36
 
37
- ## ⚖️ How it compares
38
-
39
- | Feature | ask-ai | LangChain |
40
- | -------------- | ------ | --------- |
41
- | Setup time | 30 sec | 1 hour |
42
- | Learning curve | ⭐ | ⭐⭐⭐⭐⭐ |
43
- | Async support | ⏳ *(Coming soon)* | ⚠️ Complex |
44
- | Retry/Timeout | ✅ Built-in | ❌ Manual |
45
- | Gateway needed | ❌ No | ❌ No |
46
-
47
37
  ## 🚫 What this project is NOT
48
38
 
49
39
  > ❌ Not an AI framework
@@ -54,8 +44,43 @@ It does one thing perfectly: **Simplifying the API call to LLMs.**
54
44
 
55
45
  ---
56
46
 
47
+ ## 🛠️ Advanced Usage
48
+
49
+ ### 🧰 Developer QoL Utilities (Auto-Parsing)
50
+
51
+ <p align="center">
52
+ <img src="https://raw.githubusercontent.com/Hosseinghorbani0/ask-ai/main/assets/features.svg" alt="ask-ai features parsing banner" width="100%">
53
+ </p>
54
+ Stop writing Regex to clean up model outputs! `ask-ai` comes with built-in text processing flags:
55
+
56
+ ```python
57
+ from ask_ai import OpenAI
58
+ ai = OpenAI()
59
+
60
+ # 1. Clean Markdown (Removes ```json and ``` tags)
61
+ # Perfect for extracting raw data from models that wrap everything in markdown
62
+ clean_text = ai.ask("Write JSON", clean=True).text
63
+
64
+ # 2. Extract Code (Returns ONLY the code block, ignores conversational filler)
65
+ # Great for automation pipelines
66
+ code = ai.ask("Write a python ping script", code=True).text
67
+
68
+ # 3. Strip Tags (Removes <think> blocks and HTML)
69
+ # Essential for reasoning models like DeepSeek-R1
70
+ answer_only = ai.ask("What is 1+1?", strip=True).text
71
+
72
+ # 4. Enforce & Parse JSON (Directly returns a Parsed Python Dictionary)
73
+ # Adds JSON instructions to the prompt and safely runs json.loads()
74
+ data_dict = ai.ask("Extract user info", json=True).json
75
+ print(data_dict['name'])
76
+ ```
77
+
57
78
  ## 🚀 Built-in Resiliency (Retries & Timeouts)
58
79
 
80
+ <p align="center">
81
+ <img src="https://raw.githubusercontent.com/Hosseinghorbani0/ask-ai/main/assets/resiliency.svg" alt="ask-ai resiliency banner" width="100%">
82
+ </p>
83
+
59
84
  Build reliable apps without writing your own loops. `askai-python` handles rate limits (`429`) and network drops via an internal exponential backoff.
60
85
 
61
86
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "askai-python"
7
- version = "0.2.0"
7
+ version = "0.3.3"
8
8
  description = "AI Made Stupid Simple. Unified API for OpenAI, Groq, Google, and more."
9
9
  readme = "docs/PYPI_README.md"
10
10
  requires-python = ">=3.8"
@@ -24,3 +24,10 @@ dependencies = [
24
24
  [project.urls]
25
25
  "Homepage" = "https://hosseinghorbani0.ir/"
26
26
  "Bug Tracker" = "https://github.com/Hosseinghorbani0/ask-ai/issues"
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["ask_ai*"]
30
+ exclude = ["assets*", "docs*", "tests*", ".gemini*"]
31
+
32
+ [tool.setuptools.package-data]
33
+ "*" = ["*.txt", "*.md"]
@@ -17,8 +17,7 @@ def test_advanced_config_aliases():
17
17
  def test_provider_instantiation():
18
18
  # These should succeed with a fake key because clients are lazy-loaded or just stored
19
19
  # We set env var to None to ensure it uses the passed key
20
- if "OPENAI_API_KEY" in os.environ:
21
- del os.environ["OPENAI_API_KEY"]
20
+ os.environ.pop("OPENAI_API_KEY", None)
22
21
 
23
22
  ai = OpenAI(api_key="sk-fake-key-for-testing")
24
23
  assert ai.api_key == "sk-fake-key-for-testing"
File without changes
File without changes
File without changes