irovanai 0.2.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.
- irovanai-0.2.1/LICENSE +21 -0
- irovanai-0.2.1/MANIFEST.in +2 -0
- irovanai-0.2.1/PKG-INFO +196 -0
- irovanai-0.2.1/README.md +174 -0
- irovanai-0.2.1/pyproject.toml +41 -0
- irovanai-0.2.1/setup.cfg +4 -0
- irovanai-0.2.1/src/irovanai/__init__.py +498 -0
- irovanai-0.2.1/src/irovanai/__main__.py +5 -0
- irovanai-0.2.1/src/irovanai/py.typed +0 -0
- irovanai-0.2.1/src/irovanai.egg-info/PKG-INFO +196 -0
- irovanai-0.2.1/src/irovanai.egg-info/SOURCES.txt +13 -0
- irovanai-0.2.1/src/irovanai.egg-info/dependency_links.txt +1 -0
- irovanai-0.2.1/src/irovanai.egg-info/entry_points.txt +2 -0
- irovanai-0.2.1/src/irovanai.egg-info/top_level.txt +1 -0
- irovanai-0.2.1/tests/test_irovanai.py +136 -0
irovanai-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Irovan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
irovanai-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: irovanai
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Fast, dependency-free Python client for Irovan AI (کتابخانه پایتون آیروان)
|
|
5
|
+
Author: Irovan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/irovan/irovanai
|
|
8
|
+
Project-URL: Repository, https://github.com/irovan/irovanai.git
|
|
9
|
+
Project-URL: Issues, https://github.com/irovan/irovanai/issues
|
|
10
|
+
Keywords: irovan,ai,chatbot,persian,farsi,api,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
|
|
23
|
+
# irovanai
|
|
24
|
+
|
|
25
|
+
کتابخانه **سریع** و **بدون وابستگی** پایتون برای هوش مصنوعی آیروان (`irovan-slim` / `plus`).
|
|
26
|
+
|
|
27
|
+
- ⚡ اتصال پایدار (keep-alive): درخواستهای بعدی بدون ساخت دوبارهی اتصال
|
|
28
|
+
- 🔁 تلاش مجدد خودکار برای خطای شبکه، ۵xx و ۴۲۹
|
|
29
|
+
- 🚀 ارسال همزمان چند پیام با `chat_many`
|
|
30
|
+
- 🧠 گفتگو با حافظه (`session`)
|
|
31
|
+
- 🖼 تحلیل و ساخت تصویر
|
|
32
|
+
- ⏱ نسخهی `async`
|
|
33
|
+
- 📦 فقط کتابخانهی استاندارد پایتون؛ روی گوشی (Pydroid / Termux) هم کار میکند
|
|
34
|
+
|
|
35
|
+
## نصب
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install irovanai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
یا از گیتهاب:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install git+https://github.com/irovan/irovanai.git
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
اگر `git` ندارید (مثلاً Pydroid):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install https://github.com/irovan/irovanai/archive/refs/heads/main.zip
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
یا فقط فایل `irovanai.py` را کنار کد خودتان بگذارید؛ نیازی به نصب نیست.
|
|
54
|
+
|
|
55
|
+
## دریافت کلید API
|
|
56
|
+
|
|
57
|
+
کلید را از پلتفرم توسعهدهندگان آیروان بگیرید: https://chat.irovan.ir
|
|
58
|
+
|
|
59
|
+
## شروع سریع
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from irovanai import Irovan
|
|
63
|
+
|
|
64
|
+
ai = Irovan(api_key="YOUR_API_KEY")
|
|
65
|
+
print(ai.chat("سلام"))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
کلید را میتوانید در متغیر محیطی بگذارید:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export IROVAN_API_KEY="YOUR_API_KEY" # ویندوز: set IROVAN_API_KEY=...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
ai = Irovan()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
میانبر یکخطی:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from irovanai import ask
|
|
82
|
+
print(ask("سلام", api_key="YOUR_API_KEY"))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## مدلها
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
ai.chat("سلام", model="slim") # سریع و سبک
|
|
89
|
+
ai.chat("سلام", model="plus") # قویتر
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## ارسال همزمان (سرعت بالا)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
rs = ai.chat_many(["سلام", "پایتون چیست؟", "یک جوک بگو"], workers=8)
|
|
96
|
+
for r in rs:
|
|
97
|
+
print(r.reply)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
ترتیب خروجی همان ترتیب ورودی است. با `return_exceptions=True` خطاها بهجای متوقفکردن کار، در لیست برمیگردند.
|
|
101
|
+
|
|
102
|
+
## کش پاسخ
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
ai = Irovan(api_key="...", cache_size=100) # پیام متنی تکراری دوباره به سرور نمیرود
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## گفتگو با حافظه
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
c = ai.session(system="تو یک دستیار مودب هستی.")
|
|
112
|
+
c.send("اسم من علی است")
|
|
113
|
+
print(c.send("اسم من چیست؟"))
|
|
114
|
+
c.reset()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> تاریخچه داخل متن پیام ارسال میشود، چون API هر درخواست را مستقل دریافت میکند.
|
|
118
|
+
|
|
119
|
+
## تصویر
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
print(ai.analyze_image("photo.png", "این تصویر چه چیزی را نشان میدهد؟"))
|
|
123
|
+
|
|
124
|
+
ai.generate_image("یک خیار تازه روی پسزمینه سفید").save("cucumber.png")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Async
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import asyncio
|
|
131
|
+
from irovanai import AsyncIrovan
|
|
132
|
+
|
|
133
|
+
async def main():
|
|
134
|
+
ai = AsyncIrovan(api_key="YOUR_API_KEY")
|
|
135
|
+
rs = await ai.chat_many(["سلام", "خوبی؟"])
|
|
136
|
+
print([r.reply for r in rs])
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## خط فرمان
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
irovanai "سلام" -m slim
|
|
145
|
+
irovanai "این چیست؟" -m plus -i photo.png
|
|
146
|
+
irovanai # گفتگوی تعاملی
|
|
147
|
+
python -m irovanai "سلام" # روش دیگر
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## تنظیمات
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
Irovan(
|
|
154
|
+
api_key="...",
|
|
155
|
+
model="slim",
|
|
156
|
+
timeout=120, # مهلت هر درخواست (ثانیه)
|
|
157
|
+
retries=2, # تلاش مجدد
|
|
158
|
+
cache_size=0, # 0 = بدون کش
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## مدیریت خطا
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from irovanai import Irovan, AuthenticationError, RateLimitError, NetworkError, APIError
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
print(Irovan(api_key="...").chat("سلام"))
|
|
169
|
+
except AuthenticationError:
|
|
170
|
+
print("کلید نامعتبر است")
|
|
171
|
+
except RateLimitError:
|
|
172
|
+
print("درخواست زیاد؛ کمی بعد تلاش کنید")
|
|
173
|
+
except NetworkError as e:
|
|
174
|
+
print("مشکل شبکه:", e)
|
|
175
|
+
except APIError as e:
|
|
176
|
+
print("خطای سرور:", e, e.status_code)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## پیام خوشآمدگویی
|
|
180
|
+
|
|
181
|
+
هنگام اولین ساخت کلاینت در هر اجرا، عبارت `Welcome irovanai` چاپ میشود. برای خاموشکردن:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export IROVAN_QUIET=1 # ویندوز: set IROVAN_QUIET=1
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## تست
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -e .
|
|
191
|
+
python -m unittest discover -s tests -v
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## مجوز
|
|
195
|
+
|
|
196
|
+
MIT
|
irovanai-0.2.1/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# irovanai
|
|
2
|
+
|
|
3
|
+
کتابخانه **سریع** و **بدون وابستگی** پایتون برای هوش مصنوعی آیروان (`irovan-slim` / `plus`).
|
|
4
|
+
|
|
5
|
+
- ⚡ اتصال پایدار (keep-alive): درخواستهای بعدی بدون ساخت دوبارهی اتصال
|
|
6
|
+
- 🔁 تلاش مجدد خودکار برای خطای شبکه، ۵xx و ۴۲۹
|
|
7
|
+
- 🚀 ارسال همزمان چند پیام با `chat_many`
|
|
8
|
+
- 🧠 گفتگو با حافظه (`session`)
|
|
9
|
+
- 🖼 تحلیل و ساخت تصویر
|
|
10
|
+
- ⏱ نسخهی `async`
|
|
11
|
+
- 📦 فقط کتابخانهی استاندارد پایتون؛ روی گوشی (Pydroid / Termux) هم کار میکند
|
|
12
|
+
|
|
13
|
+
## نصب
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install irovanai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
یا از گیتهاب:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://github.com/irovan/irovanai.git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
اگر `git` ندارید (مثلاً Pydroid):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install https://github.com/irovan/irovanai/archive/refs/heads/main.zip
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
یا فقط فایل `irovanai.py` را کنار کد خودتان بگذارید؛ نیازی به نصب نیست.
|
|
32
|
+
|
|
33
|
+
## دریافت کلید API
|
|
34
|
+
|
|
35
|
+
کلید را از پلتفرم توسعهدهندگان آیروان بگیرید: https://chat.irovan.ir
|
|
36
|
+
|
|
37
|
+
## شروع سریع
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from irovanai import Irovan
|
|
41
|
+
|
|
42
|
+
ai = Irovan(api_key="YOUR_API_KEY")
|
|
43
|
+
print(ai.chat("سلام"))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
کلید را میتوانید در متغیر محیطی بگذارید:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export IROVAN_API_KEY="YOUR_API_KEY" # ویندوز: set IROVAN_API_KEY=...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
ai = Irovan()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
میانبر یکخطی:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from irovanai import ask
|
|
60
|
+
print(ask("سلام", api_key="YOUR_API_KEY"))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## مدلها
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
ai.chat("سلام", model="slim") # سریع و سبک
|
|
67
|
+
ai.chat("سلام", model="plus") # قویتر
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## ارسال همزمان (سرعت بالا)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
rs = ai.chat_many(["سلام", "پایتون چیست؟", "یک جوک بگو"], workers=8)
|
|
74
|
+
for r in rs:
|
|
75
|
+
print(r.reply)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
ترتیب خروجی همان ترتیب ورودی است. با `return_exceptions=True` خطاها بهجای متوقفکردن کار، در لیست برمیگردند.
|
|
79
|
+
|
|
80
|
+
## کش پاسخ
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
ai = Irovan(api_key="...", cache_size=100) # پیام متنی تکراری دوباره به سرور نمیرود
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## گفتگو با حافظه
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
c = ai.session(system="تو یک دستیار مودب هستی.")
|
|
90
|
+
c.send("اسم من علی است")
|
|
91
|
+
print(c.send("اسم من چیست؟"))
|
|
92
|
+
c.reset()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> تاریخچه داخل متن پیام ارسال میشود، چون API هر درخواست را مستقل دریافت میکند.
|
|
96
|
+
|
|
97
|
+
## تصویر
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
print(ai.analyze_image("photo.png", "این تصویر چه چیزی را نشان میدهد؟"))
|
|
101
|
+
|
|
102
|
+
ai.generate_image("یک خیار تازه روی پسزمینه سفید").save("cucumber.png")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Async
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from irovanai import AsyncIrovan
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
ai = AsyncIrovan(api_key="YOUR_API_KEY")
|
|
113
|
+
rs = await ai.chat_many(["سلام", "خوبی؟"])
|
|
114
|
+
print([r.reply for r in rs])
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## خط فرمان
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
irovanai "سلام" -m slim
|
|
123
|
+
irovanai "این چیست؟" -m plus -i photo.png
|
|
124
|
+
irovanai # گفتگوی تعاملی
|
|
125
|
+
python -m irovanai "سلام" # روش دیگر
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## تنظیمات
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
Irovan(
|
|
132
|
+
api_key="...",
|
|
133
|
+
model="slim",
|
|
134
|
+
timeout=120, # مهلت هر درخواست (ثانیه)
|
|
135
|
+
retries=2, # تلاش مجدد
|
|
136
|
+
cache_size=0, # 0 = بدون کش
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## مدیریت خطا
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from irovanai import Irovan, AuthenticationError, RateLimitError, NetworkError, APIError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
print(Irovan(api_key="...").chat("سلام"))
|
|
147
|
+
except AuthenticationError:
|
|
148
|
+
print("کلید نامعتبر است")
|
|
149
|
+
except RateLimitError:
|
|
150
|
+
print("درخواست زیاد؛ کمی بعد تلاش کنید")
|
|
151
|
+
except NetworkError as e:
|
|
152
|
+
print("مشکل شبکه:", e)
|
|
153
|
+
except APIError as e:
|
|
154
|
+
print("خطای سرور:", e, e.status_code)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## پیام خوشآمدگویی
|
|
158
|
+
|
|
159
|
+
هنگام اولین ساخت کلاینت در هر اجرا، عبارت `Welcome irovanai` چاپ میشود. برای خاموشکردن:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export IROVAN_QUIET=1 # ویندوز: set IROVAN_QUIET=1
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## تست
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install -e .
|
|
169
|
+
python -m unittest discover -s tests -v
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## مجوز
|
|
173
|
+
|
|
174
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "irovanai"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Fast, dependency-free Python client for Irovan AI (کتابخانه پایتون آیروان)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Irovan" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["irovan", "ai", "chatbot", "persian", "farsi", "api", "llm"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/irovan/irovanai"
|
|
28
|
+
Repository = "https://github.com/irovan/irovanai.git"
|
|
29
|
+
Issues = "https://github.com/irovan/irovanai/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
irovanai = "irovanai:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.dynamic]
|
|
35
|
+
version = { attr = "irovanai.__version__" }
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
irovanai = ["py.typed"]
|
irovanai-0.2.1/setup.cfg
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""
|
|
2
|
+
irovanai - کتابخانه پایتون برای API هوش مصنوعی آیروان (Irovan AI)
|
|
3
|
+
|
|
4
|
+
بدون هیچ وابستگی خارجی (فقط کتابخانه استاندارد پایتون)، با اتصال پایدار (keep-alive)،
|
|
5
|
+
تلاش مجدد خودکار، ارسال همزمان چند درخواست، نسخه async و حافظه گفتگو.
|
|
6
|
+
|
|
7
|
+
from irovanai import Irovan
|
|
8
|
+
ai = Irovan(api_key="YOUR_API_KEY")
|
|
9
|
+
print(ai.chat("سلام"))
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import base64
|
|
14
|
+
import functools
|
|
15
|
+
import gzip
|
|
16
|
+
import http.client
|
|
17
|
+
import json
|
|
18
|
+
import mimetypes
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import socket
|
|
22
|
+
import ssl
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
import zlib
|
|
28
|
+
from collections import OrderedDict
|
|
29
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
|
|
34
|
+
__version__ = "0.2.1"
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Irovan", "AsyncIrovan", "Chat", "Response", "encode_image", "ask",
|
|
37
|
+
"IrovanError", "AuthenticationError", "APIError", "RateLimitError", "NetworkError",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
DEFAULT_BASE_URL = "https://chat.irovan.ir"
|
|
41
|
+
DEFAULT_MODEL = "slim"
|
|
42
|
+
ENV_KEY = "IROVAN_API_KEY"
|
|
43
|
+
USER_AGENT = "irovanai/" + __version__
|
|
44
|
+
|
|
45
|
+
QUIET_ENV = "IROVAN_QUIET"
|
|
46
|
+
_banner_shown = False
|
|
47
|
+
|
|
48
|
+
_RETRY_STATUS = (429, 500, 502, 503, 504)
|
|
49
|
+
_URL_RE = re.compile(r"https?://[^\s\"'<>)\]]+")
|
|
50
|
+
_DATA_RE = re.compile(r"data:image/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _show_banner() -> None:
|
|
54
|
+
"""پیام خوشآمدگویی را یکبار در هر اجرا چاپ میکند (با IROVAN_QUIET=1 خاموش میشود)."""
|
|
55
|
+
global _banner_shown
|
|
56
|
+
if _banner_shown or os.environ.get(QUIET_ENV):
|
|
57
|
+
return
|
|
58
|
+
_banner_shown = True
|
|
59
|
+
try:
|
|
60
|
+
print("Welcome irovanai", flush=True)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ───────────────────────── خطاها ─────────────────────────
|
|
66
|
+
class IrovanError(Exception):
|
|
67
|
+
"""خطای پایهی کتابخانه."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AuthenticationError(IrovanError):
|
|
71
|
+
"""کلید API ارسال نشده یا نامعتبر است."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NetworkError(IrovanError):
|
|
75
|
+
"""خطای اتصال یا تایماوت."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class APIError(IrovanError):
|
|
79
|
+
"""سرور خطا برگردانده است."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, message, status_code=None, payload=None):
|
|
82
|
+
super().__init__(message)
|
|
83
|
+
self.status_code = status_code
|
|
84
|
+
self.payload = payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RateLimitError(APIError):
|
|
88
|
+
"""تعداد درخواستها بیش از حد مجاز است."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ───────────────────────── پاسخ ─────────────────────────
|
|
92
|
+
@dataclass
|
|
93
|
+
class Response:
|
|
94
|
+
"""پاسخ API آیروان."""
|
|
95
|
+
|
|
96
|
+
ok: bool
|
|
97
|
+
model: str
|
|
98
|
+
type: str
|
|
99
|
+
reply: str
|
|
100
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
101
|
+
|
|
102
|
+
def __str__(self) -> str:
|
|
103
|
+
return self.reply
|
|
104
|
+
|
|
105
|
+
def save(self, path: str) -> str:
|
|
106
|
+
"""اگر پاسخ یک تصویر است (data URI یا لینک)، آن را در فایل ذخیره میکند."""
|
|
107
|
+
text = (self.reply or "").strip()
|
|
108
|
+
m = _DATA_RE.search(text)
|
|
109
|
+
if m:
|
|
110
|
+
data = base64.b64decode(m.group(0).split(",", 1)[1])
|
|
111
|
+
else:
|
|
112
|
+
m = _URL_RE.search(text)
|
|
113
|
+
if not m:
|
|
114
|
+
raise IrovanError("پاسخ شامل تصویر یا لینک تصویر نیست.")
|
|
115
|
+
req = urllib.request.Request(m.group(0), headers={"User-Agent": USER_AGENT})
|
|
116
|
+
try:
|
|
117
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
118
|
+
data = r.read()
|
|
119
|
+
except OSError as e:
|
|
120
|
+
raise NetworkError(f"دانلود تصویر ناموفق بود: {e}") from e
|
|
121
|
+
with open(path, "wb") as f:
|
|
122
|
+
f.write(data)
|
|
123
|
+
return path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def encode_image(image: Union[str, bytes, os.PathLike]) -> str:
|
|
127
|
+
"""مسیر فایل، بایتها یا data URI را به data URI تبدیل میکند."""
|
|
128
|
+
if isinstance(image, (bytes, bytearray)):
|
|
129
|
+
return "data:image/png;base64," + base64.b64encode(bytes(image)).decode()
|
|
130
|
+
image = os.fspath(image)
|
|
131
|
+
if image.startswith("data:"):
|
|
132
|
+
return image
|
|
133
|
+
mime = mimetypes.guess_type(image)[0] or "image/png"
|
|
134
|
+
with open(image, "rb") as f:
|
|
135
|
+
return f"data:{mime};base64," + base64.b64encode(f.read()).decode()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ───────────────────────── انتقال (سریع) ─────────────────────────
|
|
139
|
+
class _Transport:
|
|
140
|
+
"""اتصال HTTP پایدار (keep-alive) بهازای هر thread؛ بدون نیاز به کتابخانه خارجی."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, base_url: str, timeout: float):
|
|
143
|
+
u = urlparse(base_url)
|
|
144
|
+
self.scheme = u.scheme or "https"
|
|
145
|
+
self.host = u.hostname
|
|
146
|
+
self.port = u.port
|
|
147
|
+
self.prefix = u.path.rstrip("/")
|
|
148
|
+
self.timeout = timeout
|
|
149
|
+
self._local = threading.local()
|
|
150
|
+
self._ctx = ssl.create_default_context()
|
|
151
|
+
|
|
152
|
+
def _conn(self):
|
|
153
|
+
c = getattr(self._local, "conn", None)
|
|
154
|
+
if c is None:
|
|
155
|
+
if self.scheme == "https":
|
|
156
|
+
c = http.client.HTTPSConnection(
|
|
157
|
+
self.host, self.port, timeout=self.timeout, context=self._ctx)
|
|
158
|
+
else:
|
|
159
|
+
c = http.client.HTTPConnection(self.host, self.port, timeout=self.timeout)
|
|
160
|
+
self._local.conn = c
|
|
161
|
+
return c
|
|
162
|
+
|
|
163
|
+
def close(self):
|
|
164
|
+
c = getattr(self._local, "conn", None)
|
|
165
|
+
if c is not None:
|
|
166
|
+
try:
|
|
167
|
+
c.close()
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
self._local.conn = None
|
|
171
|
+
|
|
172
|
+
def post(self, path: str, body: bytes, headers: Dict[str, str]) -> Tuple[int, bytes, Optional[float]]:
|
|
173
|
+
for attempt in (0, 1): # تلاش دوم برای اتصال قدیمی که سرور بسته است
|
|
174
|
+
try:
|
|
175
|
+
c = self._conn()
|
|
176
|
+
c.request("POST", self.prefix + path, body=body, headers=headers)
|
|
177
|
+
r = c.getresponse()
|
|
178
|
+
data = r.read()
|
|
179
|
+
enc = (r.getheader("Content-Encoding") or "").lower()
|
|
180
|
+
if enc == "gzip":
|
|
181
|
+
data = gzip.decompress(data)
|
|
182
|
+
elif enc == "deflate":
|
|
183
|
+
data = zlib.decompress(data)
|
|
184
|
+
if (r.getheader("Connection") or "").lower() == "close":
|
|
185
|
+
self.close()
|
|
186
|
+
ra = r.getheader("Retry-After")
|
|
187
|
+
try:
|
|
188
|
+
retry_after = float(ra) if ra else None
|
|
189
|
+
except ValueError:
|
|
190
|
+
retry_after = None
|
|
191
|
+
return r.status, data, retry_after
|
|
192
|
+
except socket.timeout:
|
|
193
|
+
self.close()
|
|
194
|
+
raise
|
|
195
|
+
except (http.client.HTTPException, OSError):
|
|
196
|
+
self.close()
|
|
197
|
+
if attempt == 1:
|
|
198
|
+
raise
|
|
199
|
+
raise NetworkError("اتصال برقرار نشد") # pragma: no cover
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ───────────────────────── کلاینت ─────────────────────────
|
|
203
|
+
class Irovan:
|
|
204
|
+
"""کلاینت اصلی.
|
|
205
|
+
|
|
206
|
+
پارامترها:
|
|
207
|
+
api_key: کلید API (یا متغیر محیطی IROVAN_API_KEY)
|
|
208
|
+
model: مدل پیشفرض: "slim" یا "plus"
|
|
209
|
+
timeout: مهلت هر درخواست (ثانیه)
|
|
210
|
+
retries: تعداد تلاش مجدد برای خطای شبکه/۵xx/۴۲۹
|
|
211
|
+
cache_size: اگر بیشتر از صفر باشد، پاسخ پیامهای متنی تکراری را نگه میدارد
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
api_key: Optional[str] = None,
|
|
217
|
+
model: str = DEFAULT_MODEL,
|
|
218
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
219
|
+
timeout: float = 120,
|
|
220
|
+
retries: int = 2,
|
|
221
|
+
cache_size: int = 0,
|
|
222
|
+
):
|
|
223
|
+
self.api_key = api_key or os.environ.get(ENV_KEY)
|
|
224
|
+
if not self.api_key:
|
|
225
|
+
raise AuthenticationError(
|
|
226
|
+
f"کلید API داده نشد. آن را به Irovan(api_key=...) بدهید یا متغیر {ENV_KEY} را تنظیم کنید.")
|
|
227
|
+
_show_banner()
|
|
228
|
+
self.model = model
|
|
229
|
+
self.base_url = base_url.rstrip("/")
|
|
230
|
+
self.timeout = timeout
|
|
231
|
+
self.retries = max(0, int(retries))
|
|
232
|
+
self.cache_size = max(0, int(cache_size))
|
|
233
|
+
self._cache: "OrderedDict[Tuple[str, str], Response]" = OrderedDict()
|
|
234
|
+
self._cache_lock = threading.Lock()
|
|
235
|
+
self._transport = _Transport(self.base_url, timeout)
|
|
236
|
+
self._headers = {
|
|
237
|
+
"X-API-Key": self.api_key,
|
|
238
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
239
|
+
"Accept": "application/json",
|
|
240
|
+
"Accept-Encoding": "gzip",
|
|
241
|
+
"User-Agent": USER_AGENT,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# ---- context manager ----
|
|
245
|
+
def __enter__(self):
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
def __exit__(self, *exc):
|
|
249
|
+
self.close()
|
|
250
|
+
|
|
251
|
+
def close(self):
|
|
252
|
+
"""اتصال thread فعلی را میبندد."""
|
|
253
|
+
self._transport.close()
|
|
254
|
+
|
|
255
|
+
# ---- هسته ----
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _delay(attempt: int, retry_after: Optional[float]) -> float:
|
|
258
|
+
return min(retry_after if retry_after else 0.5 * (2 ** attempt), 10.0)
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _error_message(obj: Any, status: int) -> str:
|
|
262
|
+
if isinstance(obj, dict):
|
|
263
|
+
msg = obj.get("error") or obj.get("message")
|
|
264
|
+
if msg:
|
|
265
|
+
return msg if isinstance(msg, str) else json.dumps(msg, ensure_ascii=False)
|
|
266
|
+
return f"HTTP {status}"
|
|
267
|
+
|
|
268
|
+
def _parse(self, status: int, data: bytes) -> Response:
|
|
269
|
+
try:
|
|
270
|
+
obj = json.loads(data.decode("utf-8"))
|
|
271
|
+
except ValueError:
|
|
272
|
+
raise APIError("پاسخ نامعتبر از سرور", status, data[:500].decode("utf-8", "replace"))
|
|
273
|
+
if not isinstance(obj, dict):
|
|
274
|
+
raise APIError("پاسخ نامعتبر از سرور", status, obj)
|
|
275
|
+
msg = self._error_message(obj, status)
|
|
276
|
+
if status in (401, 403):
|
|
277
|
+
raise AuthenticationError(msg)
|
|
278
|
+
if status == 429:
|
|
279
|
+
raise RateLimitError(msg, status, obj)
|
|
280
|
+
if status >= 400 or obj.get("ok") is False:
|
|
281
|
+
raise APIError(msg, status, obj)
|
|
282
|
+
return Response(
|
|
283
|
+
ok=bool(obj.get("ok", True)),
|
|
284
|
+
model=obj.get("model", ""),
|
|
285
|
+
type=obj.get("type", ""),
|
|
286
|
+
reply=obj.get("reply", ""),
|
|
287
|
+
raw=obj,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _request(self, payload: Dict[str, Any]) -> Response:
|
|
291
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
292
|
+
last: Optional[Exception] = None
|
|
293
|
+
for attempt in range(self.retries + 1):
|
|
294
|
+
try:
|
|
295
|
+
status, data, retry_after = self._transport.post("/api/chat", body, self._headers)
|
|
296
|
+
except socket.timeout as e:
|
|
297
|
+
raise NetworkError("مهلت درخواست تمام شد (timeout)") from e
|
|
298
|
+
except (http.client.HTTPException, OSError) as e:
|
|
299
|
+
last = NetworkError(f"خطا در اتصال: {e}")
|
|
300
|
+
if attempt < self.retries:
|
|
301
|
+
time.sleep(self._delay(attempt, None))
|
|
302
|
+
continue
|
|
303
|
+
raise last from e
|
|
304
|
+
if status in _RETRY_STATUS and attempt < self.retries:
|
|
305
|
+
time.sleep(self._delay(attempt, retry_after))
|
|
306
|
+
continue
|
|
307
|
+
return self._parse(status, data)
|
|
308
|
+
raise last or NetworkError("درخواست ناموفق بود") # pragma: no cover
|
|
309
|
+
|
|
310
|
+
# ---- API عمومی ----
|
|
311
|
+
def chat(
|
|
312
|
+
self,
|
|
313
|
+
message: str,
|
|
314
|
+
model: Optional[str] = None,
|
|
315
|
+
image: Optional[Union[str, bytes, os.PathLike]] = None,
|
|
316
|
+
use_cache: Optional[bool] = None,
|
|
317
|
+
) -> Response:
|
|
318
|
+
"""ارسال پیام (و در صورت نیاز تصویر) و دریافت پاسخ."""
|
|
319
|
+
model = model or self.model
|
|
320
|
+
cacheable = image is None and (self.cache_size > 0 if use_cache is None else use_cache)
|
|
321
|
+
key = (model, message)
|
|
322
|
+
if cacheable and self.cache_size > 0:
|
|
323
|
+
with self._cache_lock:
|
|
324
|
+
hit = self._cache.get(key)
|
|
325
|
+
if hit is not None:
|
|
326
|
+
self._cache.move_to_end(key)
|
|
327
|
+
return hit
|
|
328
|
+
payload: Dict[str, Any] = {"message": message, "model": model}
|
|
329
|
+
if image is not None:
|
|
330
|
+
payload["image"] = encode_image(image)
|
|
331
|
+
resp = self._request(payload)
|
|
332
|
+
if cacheable and self.cache_size > 0:
|
|
333
|
+
with self._cache_lock:
|
|
334
|
+
self._cache[key] = resp
|
|
335
|
+
while len(self._cache) > self.cache_size:
|
|
336
|
+
self._cache.popitem(last=False)
|
|
337
|
+
return resp
|
|
338
|
+
|
|
339
|
+
def analyze_image(
|
|
340
|
+
self,
|
|
341
|
+
image: Union[str, bytes, os.PathLike],
|
|
342
|
+
question: str = "این تصویر چه چیزی را نشان میدهد؟",
|
|
343
|
+
model: str = "plus",
|
|
344
|
+
) -> Response:
|
|
345
|
+
"""تحلیل تصویر (پیشفرض با مدل plus)."""
|
|
346
|
+
return self.chat(question, model=model, image=image)
|
|
347
|
+
|
|
348
|
+
def generate_image(self, prompt: str, model: Optional[str] = None) -> Response:
|
|
349
|
+
"""ساخت تصویر از روی توضیح متنی."""
|
|
350
|
+
return self.chat(f"عکس : {prompt} : بساز", model=model, use_cache=False)
|
|
351
|
+
|
|
352
|
+
def chat_many(
|
|
353
|
+
self,
|
|
354
|
+
messages: Sequence[str],
|
|
355
|
+
model: Optional[str] = None,
|
|
356
|
+
workers: int = 8,
|
|
357
|
+
return_exceptions: bool = False,
|
|
358
|
+
) -> List[Any]:
|
|
359
|
+
"""ارسال همزمان چند پیام (سریعتر از حلقه). ترتیب خروجی همان ترتیب ورودی است."""
|
|
360
|
+
messages = list(messages)
|
|
361
|
+
if not messages:
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
def one(m: str):
|
|
365
|
+
try:
|
|
366
|
+
return self.chat(m, model=model)
|
|
367
|
+
except Exception as e: # noqa: BLE001
|
|
368
|
+
if return_exceptions:
|
|
369
|
+
return e
|
|
370
|
+
raise
|
|
371
|
+
|
|
372
|
+
with ThreadPoolExecutor(max_workers=max(1, min(workers, len(messages)))) as ex:
|
|
373
|
+
return list(ex.map(one, messages))
|
|
374
|
+
|
|
375
|
+
def session(self, system: Optional[str] = None, model: Optional[str] = None, max_turns: int = 10) -> "Chat":
|
|
376
|
+
"""گفتگوی چندمرحلهای با حافظه."""
|
|
377
|
+
return Chat(self, system=system, model=model, max_turns=max_turns)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ───────────────────────── گفتگو با حافظه ─────────────────────────
|
|
381
|
+
class Chat:
|
|
382
|
+
"""نگهداری تاریخچه گفتگو. تاریخچه داخل متن پیام برای مدل ارسال میشود.
|
|
383
|
+
|
|
384
|
+
c = ai.session(system="تو یک دستیار مودب هستی")
|
|
385
|
+
c.send("اسم من علی است")
|
|
386
|
+
print(c.send("اسم من چیست؟"))
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
def __init__(self, client: Irovan, system: Optional[str] = None,
|
|
390
|
+
model: Optional[str] = None, max_turns: int = 10):
|
|
391
|
+
self.client = client
|
|
392
|
+
self.system = system
|
|
393
|
+
self.model = model
|
|
394
|
+
self.max_turns = max_turns
|
|
395
|
+
self.history: List[Tuple[str, str]] = []
|
|
396
|
+
|
|
397
|
+
def _build(self, message: str) -> str:
|
|
398
|
+
if not self.history and not self.system:
|
|
399
|
+
return message
|
|
400
|
+
parts = []
|
|
401
|
+
if self.system:
|
|
402
|
+
parts.append(self.system)
|
|
403
|
+
for u, a in self.history[-self.max_turns:]:
|
|
404
|
+
parts.append(f"کاربر: {u}\nدستیار: {a}")
|
|
405
|
+
parts.append(f"کاربر: {message}\nدستیار:")
|
|
406
|
+
return "\n\n".join(parts)
|
|
407
|
+
|
|
408
|
+
def send(self, message: str, image: Optional[Union[str, bytes, os.PathLike]] = None) -> Response:
|
|
409
|
+
resp = self.client.chat(self._build(message), model=self.model, image=image, use_cache=False)
|
|
410
|
+
self.history.append((message, resp.reply))
|
|
411
|
+
return resp
|
|
412
|
+
|
|
413
|
+
def reset(self):
|
|
414
|
+
self.history.clear()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ───────────────────────── نسخه async ─────────────────────────
|
|
418
|
+
class AsyncIrovan:
|
|
419
|
+
"""نسخه async؛ درخواستها در thread pool اجرا میشوند.
|
|
420
|
+
|
|
421
|
+
ai = AsyncIrovan(api_key="...")
|
|
422
|
+
rs = await ai.chat_many(["سلام", "خوبی؟"])
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
def __init__(self, *args, workers: int = 16, **kwargs):
|
|
426
|
+
self._client = Irovan(*args, **kwargs)
|
|
427
|
+
self._pool = ThreadPoolExecutor(max_workers=workers)
|
|
428
|
+
|
|
429
|
+
async def _run(self, fn, *args, **kwargs):
|
|
430
|
+
loop = asyncio.get_running_loop()
|
|
431
|
+
return await loop.run_in_executor(self._pool, functools.partial(fn, *args, **kwargs))
|
|
432
|
+
|
|
433
|
+
async def chat(self, message, model=None, image=None, use_cache=None) -> Response:
|
|
434
|
+
return await self._run(self._client.chat, message, model=model, image=image, use_cache=use_cache)
|
|
435
|
+
|
|
436
|
+
async def analyze_image(self, image, question="این تصویر چه چیزی را نشان میدهد؟", model="plus") -> Response:
|
|
437
|
+
return await self._run(self._client.analyze_image, image, question, model)
|
|
438
|
+
|
|
439
|
+
async def generate_image(self, prompt, model=None) -> Response:
|
|
440
|
+
return await self._run(self._client.generate_image, prompt, model)
|
|
441
|
+
|
|
442
|
+
async def chat_many(self, messages: Sequence[str], model=None, return_exceptions=False) -> List[Any]:
|
|
443
|
+
return await asyncio.gather(
|
|
444
|
+
*[self.chat(m, model=model) for m in messages], return_exceptions=return_exceptions)
|
|
445
|
+
|
|
446
|
+
def close(self):
|
|
447
|
+
self._pool.shutdown(wait=False)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ───────────────────────── میانبر ─────────────────────────
|
|
451
|
+
def ask(message: str, api_key: Optional[str] = None, model: str = DEFAULT_MODEL) -> str:
|
|
452
|
+
"""یک پیام بفرست و فقط متن پاسخ را بگیر."""
|
|
453
|
+
with Irovan(api_key=api_key, model=model) as ai:
|
|
454
|
+
return ai.chat(message).reply
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ───────────────────────── خط فرمان ─────────────────────────
|
|
458
|
+
def main(argv=None) -> int:
|
|
459
|
+
p = argparse.ArgumentParser(prog="irovanai", description="چت با هوش مصنوعی آیروان")
|
|
460
|
+
p.add_argument("message", nargs="?", help="متن پیام (اگر نداده شود، حالت گفتگوی تعاملی)")
|
|
461
|
+
p.add_argument("-m", "--model", default=DEFAULT_MODEL, help="slim یا plus")
|
|
462
|
+
p.add_argument("-i", "--image", help="مسیر تصویر برای تحلیل")
|
|
463
|
+
p.add_argument("-o", "--output", help="ذخیرهی تصویر پاسخ در این فایل")
|
|
464
|
+
p.add_argument("-k", "--key", help="کلید API (یا متغیر IROVAN_API_KEY)")
|
|
465
|
+
p.add_argument("--version", action="version", version=__version__)
|
|
466
|
+
args = p.parse_args(argv)
|
|
467
|
+
if args.message: # خروجی تمیز برای استفاده در اسکریپتها
|
|
468
|
+
global _banner_shown
|
|
469
|
+
_banner_shown = True
|
|
470
|
+
try:
|
|
471
|
+
ai = Irovan(api_key=args.key, model=args.model)
|
|
472
|
+
if args.message:
|
|
473
|
+
r = ai.chat(args.message, image=args.image)
|
|
474
|
+
if args.output:
|
|
475
|
+
print("ذخیره شد:", r.save(args.output))
|
|
476
|
+
else:
|
|
477
|
+
print(r.reply)
|
|
478
|
+
return 0
|
|
479
|
+
chat = ai.session()
|
|
480
|
+
print("گفتگو شروع شد. برای خروج: exit")
|
|
481
|
+
while True:
|
|
482
|
+
try:
|
|
483
|
+
text = input("شما: ").strip()
|
|
484
|
+
except (EOFError, KeyboardInterrupt):
|
|
485
|
+
print()
|
|
486
|
+
break
|
|
487
|
+
if text.lower() in ("exit", "quit", "خروج"):
|
|
488
|
+
break
|
|
489
|
+
if text:
|
|
490
|
+
print("ایروان:", chat.send(text).reply)
|
|
491
|
+
return 0
|
|
492
|
+
except IrovanError as e:
|
|
493
|
+
print(f"خطا: {e}", file=sys.stderr)
|
|
494
|
+
return 1
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: irovanai
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Fast, dependency-free Python client for Irovan AI (کتابخانه پایتون آیروان)
|
|
5
|
+
Author: Irovan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/irovan/irovanai
|
|
8
|
+
Project-URL: Repository, https://github.com/irovan/irovanai.git
|
|
9
|
+
Project-URL: Issues, https://github.com/irovan/irovanai/issues
|
|
10
|
+
Keywords: irovan,ai,chatbot,persian,farsi,api,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
|
|
23
|
+
# irovanai
|
|
24
|
+
|
|
25
|
+
کتابخانه **سریع** و **بدون وابستگی** پایتون برای هوش مصنوعی آیروان (`irovan-slim` / `plus`).
|
|
26
|
+
|
|
27
|
+
- ⚡ اتصال پایدار (keep-alive): درخواستهای بعدی بدون ساخت دوبارهی اتصال
|
|
28
|
+
- 🔁 تلاش مجدد خودکار برای خطای شبکه، ۵xx و ۴۲۹
|
|
29
|
+
- 🚀 ارسال همزمان چند پیام با `chat_many`
|
|
30
|
+
- 🧠 گفتگو با حافظه (`session`)
|
|
31
|
+
- 🖼 تحلیل و ساخت تصویر
|
|
32
|
+
- ⏱ نسخهی `async`
|
|
33
|
+
- 📦 فقط کتابخانهی استاندارد پایتون؛ روی گوشی (Pydroid / Termux) هم کار میکند
|
|
34
|
+
|
|
35
|
+
## نصب
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install irovanai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
یا از گیتهاب:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install git+https://github.com/irovan/irovanai.git
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
اگر `git` ندارید (مثلاً Pydroid):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install https://github.com/irovan/irovanai/archive/refs/heads/main.zip
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
یا فقط فایل `irovanai.py` را کنار کد خودتان بگذارید؛ نیازی به نصب نیست.
|
|
54
|
+
|
|
55
|
+
## دریافت کلید API
|
|
56
|
+
|
|
57
|
+
کلید را از پلتفرم توسعهدهندگان آیروان بگیرید: https://chat.irovan.ir
|
|
58
|
+
|
|
59
|
+
## شروع سریع
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from irovanai import Irovan
|
|
63
|
+
|
|
64
|
+
ai = Irovan(api_key="YOUR_API_KEY")
|
|
65
|
+
print(ai.chat("سلام"))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
کلید را میتوانید در متغیر محیطی بگذارید:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export IROVAN_API_KEY="YOUR_API_KEY" # ویندوز: set IROVAN_API_KEY=...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
ai = Irovan()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
میانبر یکخطی:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from irovanai import ask
|
|
82
|
+
print(ask("سلام", api_key="YOUR_API_KEY"))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## مدلها
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
ai.chat("سلام", model="slim") # سریع و سبک
|
|
89
|
+
ai.chat("سلام", model="plus") # قویتر
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## ارسال همزمان (سرعت بالا)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
rs = ai.chat_many(["سلام", "پایتون چیست؟", "یک جوک بگو"], workers=8)
|
|
96
|
+
for r in rs:
|
|
97
|
+
print(r.reply)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
ترتیب خروجی همان ترتیب ورودی است. با `return_exceptions=True` خطاها بهجای متوقفکردن کار، در لیست برمیگردند.
|
|
101
|
+
|
|
102
|
+
## کش پاسخ
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
ai = Irovan(api_key="...", cache_size=100) # پیام متنی تکراری دوباره به سرور نمیرود
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## گفتگو با حافظه
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
c = ai.session(system="تو یک دستیار مودب هستی.")
|
|
112
|
+
c.send("اسم من علی است")
|
|
113
|
+
print(c.send("اسم من چیست؟"))
|
|
114
|
+
c.reset()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> تاریخچه داخل متن پیام ارسال میشود، چون API هر درخواست را مستقل دریافت میکند.
|
|
118
|
+
|
|
119
|
+
## تصویر
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
print(ai.analyze_image("photo.png", "این تصویر چه چیزی را نشان میدهد؟"))
|
|
123
|
+
|
|
124
|
+
ai.generate_image("یک خیار تازه روی پسزمینه سفید").save("cucumber.png")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Async
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import asyncio
|
|
131
|
+
from irovanai import AsyncIrovan
|
|
132
|
+
|
|
133
|
+
async def main():
|
|
134
|
+
ai = AsyncIrovan(api_key="YOUR_API_KEY")
|
|
135
|
+
rs = await ai.chat_many(["سلام", "خوبی؟"])
|
|
136
|
+
print([r.reply for r in rs])
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## خط فرمان
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
irovanai "سلام" -m slim
|
|
145
|
+
irovanai "این چیست؟" -m plus -i photo.png
|
|
146
|
+
irovanai # گفتگوی تعاملی
|
|
147
|
+
python -m irovanai "سلام" # روش دیگر
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## تنظیمات
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
Irovan(
|
|
154
|
+
api_key="...",
|
|
155
|
+
model="slim",
|
|
156
|
+
timeout=120, # مهلت هر درخواست (ثانیه)
|
|
157
|
+
retries=2, # تلاش مجدد
|
|
158
|
+
cache_size=0, # 0 = بدون کش
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## مدیریت خطا
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from irovanai import Irovan, AuthenticationError, RateLimitError, NetworkError, APIError
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
print(Irovan(api_key="...").chat("سلام"))
|
|
169
|
+
except AuthenticationError:
|
|
170
|
+
print("کلید نامعتبر است")
|
|
171
|
+
except RateLimitError:
|
|
172
|
+
print("درخواست زیاد؛ کمی بعد تلاش کنید")
|
|
173
|
+
except NetworkError as e:
|
|
174
|
+
print("مشکل شبکه:", e)
|
|
175
|
+
except APIError as e:
|
|
176
|
+
print("خطای سرور:", e, e.status_code)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## پیام خوشآمدگویی
|
|
180
|
+
|
|
181
|
+
هنگام اولین ساخت کلاینت در هر اجرا، عبارت `Welcome irovanai` چاپ میشود. برای خاموشکردن:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export IROVAN_QUIET=1 # ویندوز: set IROVAN_QUIET=1
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## تست
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -e .
|
|
191
|
+
python -m unittest discover -s tests -v
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## مجوز
|
|
195
|
+
|
|
196
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/irovanai/__init__.py
|
|
6
|
+
src/irovanai/__main__.py
|
|
7
|
+
src/irovanai/py.typed
|
|
8
|
+
src/irovanai.egg-info/PKG-INFO
|
|
9
|
+
src/irovanai.egg-info/SOURCES.txt
|
|
10
|
+
src/irovanai.egg-info/dependency_links.txt
|
|
11
|
+
src/irovanai.egg-info/entry_points.txt
|
|
12
|
+
src/irovanai.egg-info/top_level.txt
|
|
13
|
+
tests/test_irovanai.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
irovanai
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import unittest
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer as HTTPServer
|
|
10
|
+
|
|
11
|
+
os.environ.setdefault("IROVAN_QUIET", "1")
|
|
12
|
+
|
|
13
|
+
import irovanai # noqa: E402
|
|
14
|
+
from irovanai import (APIError, AsyncIrovan, AuthenticationError, Irovan,
|
|
15
|
+
RateLimitError, encode_image)
|
|
16
|
+
|
|
17
|
+
STATE = {"fail": 0, "calls": 0, "conns": set()}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
protocol_version = "HTTP/1.1"
|
|
22
|
+
|
|
23
|
+
def log_message(self, *a):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def do_POST(self):
|
|
27
|
+
n = int(self.headers.get("Content-Length", 0))
|
|
28
|
+
body = json.loads(self.rfile.read(n).decode("utf-8"))
|
|
29
|
+
STATE["calls"] += 1
|
|
30
|
+
STATE["conns"].add(self.client_address)
|
|
31
|
+
key = self.headers.get("X-API-Key")
|
|
32
|
+
if key == "bad":
|
|
33
|
+
return self._send(401, {"ok": False, "error": "invalid key"})
|
|
34
|
+
if body["message"] == "limit":
|
|
35
|
+
return self._send(429, {"ok": False, "error": "slow down"})
|
|
36
|
+
if STATE["fail"] > 0:
|
|
37
|
+
STATE["fail"] -= 1
|
|
38
|
+
return self._send(503, {"ok": False, "error": "busy"})
|
|
39
|
+
self._send(200, {"ok": True, "model": "irovan-slim", "type": "ai",
|
|
40
|
+
"reply": "echo:" + body["message"], "m": body["model"],
|
|
41
|
+
"img": bool(body.get("image"))})
|
|
42
|
+
|
|
43
|
+
def _send(self, code, obj):
|
|
44
|
+
data = gzip.compress(json.dumps(obj, ensure_ascii=False).encode("utf-8"))
|
|
45
|
+
self.send_response(code)
|
|
46
|
+
self.send_header("Content-Type", "application/json")
|
|
47
|
+
self.send_header("Content-Encoding", "gzip")
|
|
48
|
+
self.send_header("Content-Length", str(len(data)))
|
|
49
|
+
self.end_headers()
|
|
50
|
+
self.wfile.write(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class T(unittest.TestCase):
|
|
54
|
+
@classmethod
|
|
55
|
+
def setUpClass(cls):
|
|
56
|
+
cls.srv = HTTPServer(("127.0.0.1", 0), H)
|
|
57
|
+
cls.srv.daemon_threads = True
|
|
58
|
+
threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
|
|
59
|
+
cls.url = f"http://127.0.0.1:{cls.srv.server_port}"
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def tearDownClass(cls):
|
|
63
|
+
cls.srv.shutdown()
|
|
64
|
+
|
|
65
|
+
def ai(self, **kw):
|
|
66
|
+
kw.setdefault("api_key", "k")
|
|
67
|
+
return Irovan(base_url=self.url, **kw)
|
|
68
|
+
|
|
69
|
+
def test_banner(self):
|
|
70
|
+
os.environ.pop("IROVAN_QUIET", None)
|
|
71
|
+
irovanai._banner_shown = False
|
|
72
|
+
buf = io.StringIO()
|
|
73
|
+
with redirect_stdout(buf):
|
|
74
|
+
self.ai()
|
|
75
|
+
self.ai()
|
|
76
|
+
os.environ["IROVAN_QUIET"] = "1"
|
|
77
|
+
self.assertEqual(buf.getvalue(), "Welcome irovanai\n") # فقط یکبار
|
|
78
|
+
|
|
79
|
+
def test_chat_and_keepalive(self):
|
|
80
|
+
STATE["conns"].clear()
|
|
81
|
+
ai = self.ai()
|
|
82
|
+
for i in range(5):
|
|
83
|
+
self.assertEqual(ai.chat(f"سلام{i}").reply, f"echo:سلام{i}")
|
|
84
|
+
self.assertEqual(len(STATE["conns"]), 1) # یک اتصال برای همه
|
|
85
|
+
|
|
86
|
+
def test_model_and_image(self):
|
|
87
|
+
r = self.ai().analyze_image(b"abc")
|
|
88
|
+
self.assertTrue(r.raw["img"])
|
|
89
|
+
self.assertEqual(r.raw["m"], "plus")
|
|
90
|
+
self.assertTrue(encode_image(b"x").startswith("data:image/png;base64,"))
|
|
91
|
+
|
|
92
|
+
def test_auth_error(self):
|
|
93
|
+
with self.assertRaises(AuthenticationError):
|
|
94
|
+
self.ai(api_key="bad").chat("hi")
|
|
95
|
+
|
|
96
|
+
def test_rate_limit(self):
|
|
97
|
+
with self.assertRaises(RateLimitError):
|
|
98
|
+
self.ai(retries=0).chat("limit")
|
|
99
|
+
|
|
100
|
+
def test_retry_on_503(self):
|
|
101
|
+
STATE["fail"] = 2
|
|
102
|
+
self.assertEqual(self.ai(retries=2).chat("x").reply, "echo:x")
|
|
103
|
+
STATE["fail"] = 1
|
|
104
|
+
with self.assertRaises(APIError):
|
|
105
|
+
self.ai(retries=0).chat("x")
|
|
106
|
+
|
|
107
|
+
def test_cache(self):
|
|
108
|
+
ai = self.ai(cache_size=4)
|
|
109
|
+
ai.chat("c1")
|
|
110
|
+
before = STATE["calls"]
|
|
111
|
+
ai.chat("c1")
|
|
112
|
+
self.assertEqual(STATE["calls"], before)
|
|
113
|
+
|
|
114
|
+
def test_chat_many_order(self):
|
|
115
|
+
msgs = [f"m{i}" for i in range(20)]
|
|
116
|
+
out = self.ai().chat_many(msgs, workers=5)
|
|
117
|
+
self.assertEqual([r.reply for r in out], ["echo:" + m for m in msgs])
|
|
118
|
+
|
|
119
|
+
def test_session_memory(self):
|
|
120
|
+
c = self.ai().session(system="SYS")
|
|
121
|
+
c.send("اسم من علی است")
|
|
122
|
+
r = c.send("اسم من چیست؟")
|
|
123
|
+
self.assertIn("علی", r.reply)
|
|
124
|
+
self.assertIn("SYS", r.reply)
|
|
125
|
+
|
|
126
|
+
def test_async(self):
|
|
127
|
+
async def go():
|
|
128
|
+
a = AsyncIrovan(api_key="k", base_url=self.url)
|
|
129
|
+
rs = await a.chat_many(["a", "b", "c"])
|
|
130
|
+
a.close()
|
|
131
|
+
return [r.reply for r in rs]
|
|
132
|
+
self.assertEqual(asyncio.run(go()), ["echo:a", "echo:b", "echo:c"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
unittest.main()
|