irovanai 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.
irovanai/__init__.py 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())
irovanai/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from . import main
4
+
5
+ sys.exit(main())
irovanai/py.typed ADDED
File without changes
@@ -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.
@@ -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,9 @@
1
+ irovanai/__init__.py,sha256=-z3VkbzvvPyUfT9Ede8RuI__ppEd_oBEDHCwrdaVgfs,20161
2
+ irovanai/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
3
+ irovanai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ irovanai-0.2.1.dist-info/LICENSE,sha256=n5uUTIkQzN33a03dDT6hEvMT9UKO-CDD1MVX70b3Yfo,1063
5
+ irovanai-0.2.1.dist-info/METADATA,sha256=7IxuRkLTVbW2FY1o-IFCfzWfq1jkqs1NQxFpMPHfHeo,5335
6
+ irovanai-0.2.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
7
+ irovanai-0.2.1.dist-info/entry_points.txt,sha256=y6euHT-J-HRjsBbkoFm1yNcfpvmEMBI1UpBdV7SGd9k,43
8
+ irovanai-0.2.1.dist-info/top_level.txt,sha256=waUsOec-HEJD_lhTqrotUQJWntEYkHlytCXEdo4Q1jk,9
9
+ irovanai-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ irovanai = irovanai:main
@@ -0,0 +1 @@
1
+ irovanai