KashefTech/Data-Anonymization
0
1"""2🤖 LLM Sender Unified Module3ماژول یکپارچه برای ارسال به ChatGPT و Grok4✨ با پشتیبانی از GPT-5 models و رفع مشکل temperature5"""6 7import requests8import os9import logging10from typing import Optional11import time12from abc import ABC, abstractmethod13 14logging.basicConfig(level=logging.INFO)15logger = logging.getLogger(__name__)16 17 18class LLMSender(ABC):19 """کلاس پایه برای ارسال به مدلهای مختلف LLM"""20 21 def __init__(self, api_key: Optional[str] = None, model: str = None):22 self.api_key = api_key23 self.model = model24 self.base_url = ""25 26 @abstractmethod27 def get_default_model(self) -> str:28 """مدل پیشفرض"""29 pass30 31 @abstractmethod32 def get_base_url(self) -> str:33 """URL پایه API"""34 pass35 36 def set_api_key(self, api_key: str):37 """تنظیم کلید API"""38 self.api_key = api_key39 logger.info("✅ کلید API تنظیم شد")40 41 def set_model(self, model: str):42 """تغییر مدل"""43 self.model = model44 logger.info(f"✅ مدل تغییر یافت به: {model}")45 46 def _uses_max_completion_tokens(self) -> bool:47 """بررسی اینکه آیا مدل از max_completion_tokens استفاده میکند"""48 models_with_completion_tokens = [49 'gpt-5', # تمام مدلهای GPT-550 'gpt-5.1' # GPT-5.151 ]52 return any(self.model.startswith(prefix) for prefix in models_with_completion_tokens)53 54 def _requires_default_temperature(self) -> bool:55 """بررسی اینکه آیا مدل فقط temperature=1 را قبول میکند"""56 models_requiring_default_temp = [57 'gpt-5', # تمام مدلهای GPT-558 'o1' # تمام مدلهای O159 ]60 return any(self.model.startswith(prefix) for prefix in models_requiring_default_temp)61 62 def send_simple(self, text: str, lang: str = 'fa') -> str:63 """ارسال ساده بدون system message سفارشی"""64 system_msg = (65 "شما یک تحلیلگر متخصص هستید. متن حاوی کدهای ناشناس است. "66 "به درخواستها با دقت و حرفهای پاسخ دهید."67 if lang == 'fa' 68 else "You are a professional analyst. The text contains anonymous codes. "69 "Answer requests accurately and professionally."70 )71 72 return self.send(text, system_msg=system_msg, lang=lang)73 74 def send(75 self,76 text: str,77 system_msg: Optional[str] = None,78 max_tokens: int = 2000,79 temperature: float = 0.2, # ✅ کاهش از 0.7 به 0.2 برای دقت بیشتر80 timeout: int = 60,81 lang: str = 'fa',82 retry_count: int = 383 ) -> str:84 """ارسال متن به LLM با کنترل کامل"""85 try:86 # بررسی اولیه87 if not text or not text.strip():88 error_msg = "متن خالی است!" if lang == 'fa' else "Text is empty!"89 logger.error(f"❌ {error_msg}")90 return f"❌ {error_msg}"91 92 if not self.api_key:93 error_msg = "کلید API تنظیم نشده است!" if lang == 'fa' else "API Key not configured!"94 logger.error(f"❌ {error_msg}")95 return f"❌ {error_msg}"96 97 # تنظیم system message پیشفرض98 if system_msg is None:99 system_msg = (100 "شما یک تحلیلگر مالی حرفهای هستید. متن حاوی کدهای ناشناس است. "101 "به سوالات با دقت پاسخ دهید."102 if lang == 'fa'103 else "You are a professional financial analyst. The text contains anonymous codes. "104 "Answer questions accurately."105 )106 107 # تهیه headers108 headers = {109 "Authorization": f"Bearer {self.api_key}",110 "Content-Type": "application/json"111 }112 113 # ✨ تنظیم temperature مناسب114 if self._requires_default_temperature():115 actual_temperature = 1.0116 if temperature != 1.0:117 logger.info(f"⚠️ مدل {self.model} فقط temperature=1 را قبول میکند")118 else:119 actual_temperature = temperature120 121 # ساخت request body122 data = {123 "model": self.model,124 "messages": [125 {"role": "system", "content": system_msg},126 {"role": "user", "content": text}127 ],128 "temperature": actual_temperature129 }130 131 # ✨ انتخاب پارامتر مناسب برای max tokens132 if self._uses_max_completion_tokens():133 data["max_completion_tokens"] = max_tokens134 else:135 data["max_tokens"] = max_tokens136 137 # ارسال با retry mechanism138 for attempt in range(retry_count):139 try:140 logger.info(f"📤 ارسال درخواست به {self.__class__.__name__} (تلاش {attempt + 1}/{retry_count})...")141 142 response = requests.post(143 self.base_url,144 headers=headers,145 json=data,146 timeout=timeout147 )148 149 # پردازش پاسخ موفق150 if response.status_code == 200:151 result = response.json()152 llm_response = result['choices'][0]['message']['content']153 # ✅ حذف بلوکهای thinking برای DeepInfra/Qwen3154 if isinstance(self, DeepInfraSender):155 llm_response = DeepInfraSender.strip_thinking(llm_response)156 logger.info("✅ پاسخ دریافت شد")157 return llm_response158 159 # پردازش خطاهای مختلف160 elif response.status_code == 429: # Rate limiting161 wait_time = 5 * (attempt + 1)162 logger.warning(f"⚠️ Rate limit | صبر: {wait_time} ثانیه")163 if attempt < retry_count - 1:164 time.sleep(wait_time)165 continue166 else:167 return (168 "❌ سهمیه API تمام شده است. لطفاً بعداً تلاش کنید."169 if lang == 'fa'170 else "❌ API quota exceeded. Please try later."171 )172 173 elif response.status_code == 401:174 return (175 "❌ کلید API نامعتبر است!"176 if lang == 'fa'177 else "❌ Invalid API key!"178 )179 180 elif response.status_code in [502, 503, 504]: # Server errors181 wait_time = 2 * (attempt + 1)182 logger.warning(f"⚠️ Server error {response.status_code} | صبر: {wait_time} ثانیه")183 if attempt < retry_count - 1:184 time.sleep(wait_time)185 continue186 else:187 return (188 f"❌ خطای سرور: {response.status_code}"189 if lang == 'fa'190 else f"❌ Server error: {response.status_code}"191 )192 193 else:194 # خطای دیگر195 try:196 error_data = response.json() if response.content else {}197 if isinstance(error_data, dict):198 error_msg = error_data.get('error', {}).get('message', response.text)199 else:200 error_msg = str(error_data)201 except:202 error_msg = response.text[:200]203 204 logger.error(f"❌ API Error: {error_msg}")205 return f"❌ API Error: {error_msg}"206 207 except requests.exceptions.Timeout:208 logger.warning("⚠️ Timeout | صبر: 3 ثانیه و تلاش مجدد")209 if attempt < retry_count - 1:210 time.sleep(3)211 continue212 else:213 return (214 "❌ خطای اتصال: timeout"215 if lang == 'fa'216 else "❌ Connection error: timeout"217 )218 219 except requests.exceptions.ConnectionError as e:220 logger.warning("⚠️ Connection error | صبر: 2 ثانیه و تلاش مجدد")221 if attempt < retry_count - 1:222 time.sleep(2)223 continue224 else:225 return (226 f"❌ خطای اتصال: {str(e)}"227 if lang == 'fa'228 else f"❌ Connection error: {str(e)}"229 )230 231 except Exception as e:232 logger.error(f"❌ خطای غیرمنتظره: {str(e)}")233 return (234 f"❌ خطا در ارتباط با LLM: {str(e)}"235 if lang == 'fa'236 else f"❌ Error connecting to LLM: {str(e)}"237 )238 239 240class ChatGPTSender(LLMSender):241 """کلاس برای ارسال به ChatGPT"""242 243 def __init__(self, api_key: Optional[str] = None, model: str = "gpt-4o-mini"):244 raw_key = api_key or os.getenv("OPENAI_API_KEY", "")245 cleaned_key = raw_key.strip() if raw_key else ""246 247 super().__init__(cleaned_key, model)248 self.base_url = self.get_base_url()249 250 if not self.api_key:251 logger.warning("⚠️ کلید OpenAI API تنظیم نشده است!")252 253 def get_default_model(self) -> str:254 return "gpt-4o-mini"255 256 def get_base_url(self) -> str:257 return "https://api.openai.com/v1/chat/completions"258 259 260class GrokSender(LLMSender):261 """کلاس برای ارسال به Grok (xAI)"""262 263 def __init__(self, api_key: Optional[str] = None, model: str = "grok-beta"):264 raw_key = api_key or os.getenv("XAI_API_KEY", "")265 cleaned_key = raw_key.strip() if raw_key else ""266 267 super().__init__(cleaned_key, model)268 self.base_url = self.get_base_url()269 270 if not self.api_key:271 logger.warning("⚠️ کلید xAI API تنظیم نشده است!")272 273 def get_default_model(self) -> str:274 return "grok-beta"275 276 def get_base_url(self) -> str:277 return "https://api.x.ai/v1/chat/completions"278 279 280class DeepInfraSender(LLMSender):281 """کلاس برای ارسال به DeepInfra"""282 283 def __init__(self, api_key: Optional[str] = None, model: str = "Qwen/Qwen3-14B"):284 raw_key = api_key or os.getenv("DEEPINFRA_API_KEY", "")285 cleaned_key = raw_key.strip() if raw_key else ""286 287 super().__init__(cleaned_key, model)288 self.base_url = self.get_base_url()289 290 if not self.api_key:291 logger.warning("⚠️ کلید DeepInfra API تنظیم نشده است!")292 293 def get_default_model(self) -> str:294 return "Qwen/Qwen3-14B"295 296 def get_base_url(self) -> str:297 return "https://api.deepinfra.com/v1/openai/chat/completions"298 299 def _uses_max_completion_tokens(self) -> bool:300 return False301 302 def _requires_default_temperature(self) -> bool:303 return False304 305 @staticmethod306 def strip_thinking(text: str) -> str:307 """✅ حذف بلوکهای <think>...</think> که Qwen3 تولید میکند"""308 if not text:309 return text310 cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)311 return cleaned.strip()312 313 314def create_llm_sender(315 provider: str = "chatgpt",316 api_key: Optional[str] = None,317 model: Optional[str] = None318) -> LLMSender:319 """ایجاد LLM sender بر اساس provider"""320 provider = provider.lower()321 322 if provider == "chatgpt":323 if model is None:324 model = "gpt-4o-mini"325 return ChatGPTSender(api_key=api_key, model=model)326 327 elif provider == "grok":328 if model is None:329 model = "grok-beta"330 return GrokSender(api_key=api_key, model=model)331 332 elif provider == "deepinfra":333 if model is None:334 model = "Qwen/Qwen3-14B"335 return DeepInfraSender(api_key=api_key, model=model)336 337 else:338 raise ValueError(f"Provider نامعتبر: {provider}")339 340 341# ✅ مدلهای موجود (بهروزرسانی شده با DeepInfra)342AVAILABLE_MODELS = {343 "chatgpt": [344 "gpt-5.1",345 "gpt-5",346 "gpt-4.1",347 "gpt-4o-mini",348 "gpt-4o",349 "gpt-4-turbo",350 "gpt-3.5-turbo"351 ],352 "grok": [353 "grok-3-mini",354 "grok-3",355 "grok-2-1212"356 ],357 "deepinfra": [358 "Qwen/Qwen3-14B",359 "Qwen/Qwen3-32B",360 "Qwen/Qwen3-30B-A3B",361 "Qwen/Qwen2.5-72B-Instruct",362 "Qwen/Qwen2.5-14B-Instruct",363 ]364}365 366 367if __name__ == "__main__":368 print("=" * 60)369 print("🤖 LLM Sender - نسخه اصلاح شده")370 print("✨ رفع مشکل temperature برای GPT-5")371 print("=" * 60)372 373 # تست374 print("\n🧪 تست مدلها:")375 test_models = ['gpt-5', 'gpt-5.1', 'gpt-4o']376 for model in test_models:377 sender = create_llm_sender("chatgpt", model=model)378 uses_completion = sender._uses_max_completion_tokens()379 requires_default_temp = sender._requires_default_temperature()380 381 print(f"\n مدل: {model}")382 print(f" • max_tokens: {'max_completion_tokens' if uses_completion else 'max_tokens'}")383 print(f" • temperature: {'1.0 (default only)' if requires_default_temp else '0.7 (custom)'}")384 