leilaghomashchi commited on
Commit
1901c07
·
verified ·
1 Parent(s): 397284d

Upload 2 files

Browse files
Files changed (2) hide show
  1. app_final_unified.py +457 -0
  2. unified_llm_sender (1).py +235 -0
app_final_unified.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import re
3
+ import os
4
+ import requests
5
+ import json
6
+ import logging
7
+ from typing import Dict, Tuple
8
+ from unified_llm_sender import UnifiedLLMSender, get_available_models, get_model_display_names
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AnonymizerAdvanced:
15
+ """ناشناس‌ساز پیشرفته با Cerebras"""
16
+
17
+ def __init__(self, cerebras_key: str = None):
18
+ self.cerebras_key = cerebras_key or os.getenv("CEREBRAS_API_KEY") or os.getenv("GR00_API_KEY")
19
+ self.mapping_table = {}
20
+ self.reverse_mapping = {}
21
+
22
+ logger.info("✅ Anonymizer Advanced مقداردهی شد")
23
+
24
+ def anonymize_with_cerebras(self, text: str) -> Tuple[str, Dict]:
25
+ """ناشناس‌سازی با Cerebras - دریافت mapping از مدل"""
26
+ logger.info("🧠 روش Cerebras...")
27
+
28
+ if not self.cerebras_key:
29
+ logger.error("❌ Cerebras API Key موجود نیست")
30
+ raise ValueError("Cerebras API Key مورد نیاز است (CEREBRAS_API_KEY یا GR00_API_KEY)")
31
+
32
+ try:
33
+ # مرحله 1: ناشناس‌سازی متن
34
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
35
+ 1. اسامی اشخاص → person-01, person-02, ...
36
+ 2. نام شرکت‌ها/سازمان‌ها → company-01, company-02, ...
37
+ 3. مقادیر پولی → amount-01, amount-02, ...
38
+ 4. درصدها → percent-01, percent-02, ...
39
+ 5. فقط این توکن‌ها استفاده کنید
40
+ 6. شماره‌های نسخه را درست حفظ کنید
41
+ 7. اگر موجودیت تکرار شود از شماره قدیمی استفاده کنید
42
+
43
+ متن:
44
+ {text}
45
+
46
+ خروجی: فقط متن ناشناس شده"""
47
+
48
+ response1 = requests.post(
49
+ "https://api.cerebras.ai/v1/chat/completions",
50
+ headers={
51
+ "Authorization": f"Bearer {self.cerebras_key}",
52
+ "Content-Type": "application/json"
53
+ },
54
+ json={
55
+ "model": "llama-3.3-70b",
56
+ "messages": [{"role": "user", "content": prompt1}],
57
+ "max_tokens": 4096,
58
+ "temperature": 0.1
59
+ },
60
+ timeout=60
61
+ )
62
+
63
+ if response1.status_code != 200:
64
+ logger.error(f"❌ Cerebras Error: {response1.status_code}")
65
+ raise Exception(f"Cerebras API Error: {response1.status_code}")
66
+
67
+ anonymized_text = response1.json()['choices'][0]['message']['content'].strip()
68
+ logger.info("✅ Cerebras: ناشناس‌سازی موفق")
69
+
70
+ # مرحله 2: استخراج mapping از مدل
71
+ prompt2 = f"""متن اصلی:
72
+ {text}
73
+
74
+ متن ناشناس شده:
75
+ {anonymized_text}
76
+
77
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
78
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
79
+
80
+ **مهم:**
81
+ - برای person-XX: نام کامل شخص (مثلاً "علی احمدی")
82
+ - برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")
83
+ - برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")
84
+ - برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")
85
+
86
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
87
+ {{
88
+ "person-01": "متن اصلی کامل",
89
+ "company-01": "متن اصلی کامل",
90
+ "amount-01": "متن اصلی کامل با واحد",
91
+ "percent-01": "عدد + درصد",
92
+ ...
93
+ }}"""
94
+
95
+ response2 = requests.post(
96
+ "https://api.cerebras.ai/v1/chat/completions",
97
+ headers={
98
+ "Authorization": f"Bearer {self.cerebras_key}",
99
+ "Content-Type": "application/json"
100
+ },
101
+ json={
102
+ "model": "llama-3.3-70b",
103
+ "messages": [{"role": "user", "content": prompt2}],
104
+ "max_tokens": 2048,
105
+ "temperature": 0.1
106
+ },
107
+ timeout=60
108
+ )
109
+
110
+ if response2.status_code == 200:
111
+ mapping_text = response2.json()['choices'][0]['message']['content'].strip()
112
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
113
+
114
+ try:
115
+ self.mapping_table = json.loads(mapping_text)
116
+ self._fix_percent_mapping()
117
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
118
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
119
+ except json.JSONDecodeError:
120
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
121
+ self._extract_mapping_from_text(text, anonymized_text)
122
+ else:
123
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
124
+ self._extract_mapping_from_text(text, anonymized_text)
125
+
126
+ return anonymized_text, self.mapping_table
127
+
128
+ except Exception as e:
129
+ logger.error(f"❌ Cerebras Exception: {e}")
130
+ raise
131
+
132
+ def _fix_percent_mapping(self):
133
+ """اصلاح mapping برای درصدها و مقادیر"""
134
+ for token, value in self.mapping_table.items():
135
+ value_str = str(value).strip()
136
+
137
+ if token.startswith('percent-'):
138
+ if not re.search(r'(درصد|%|درصدی)', value_str):
139
+ self.mapping_table[token] = f"{value_str} درصد"
140
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
141
+
142
+ elif token.startswith('amount-'):
143
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
144
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
145
+
146
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
147
+ """استخراج mapping از متن‌های اصلی و ناشناس شده"""
148
+ all_tokens = []
149
+ for entity_type in ['person', 'company', 'amount', 'percent']:
150
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
151
+ all_tokens.extend([(t, entity_type) for t in tokens])
152
+
153
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
154
+
155
+ patterns = {
156
+ 'person': r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b',
157
+ 'company': r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*',
158
+ 'amount': r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)',
159
+ 'percent': r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)',
160
+ }
161
+
162
+ original_entities = {}
163
+ for entity_type, pattern in patterns.items():
164
+ matches = list(re.finditer(pattern, original))
165
+ original_entities[entity_type] = [m.group().strip() for m in matches]
166
+
167
+ for token, entity_type in all_tokens:
168
+ if entity_type in original_entities and original_entities[entity_type]:
169
+ token_num = int(token.split('-')[1]) - 1
170
+
171
+ if token_num < len(original_entities[entity_type]):
172
+ original_text = original_entities[entity_type][token_num]
173
+ self.mapping_table[token] = original_text
174
+ self.reverse_mapping[original_text] = token
175
+ else:
176
+ original_text = original_entities[entity_type][-1]
177
+ if token not in self.mapping_table:
178
+ self.mapping_table[token] = original_text
179
+ self.reverse_mapping[original_text] = token
180
+
181
+ def analyze_with_model(self, anonymized_text: str, analysis_prompt: str, model_name: str) -> str:
182
+ """
183
+ اجرای پرامپت‌ها با مدل انتخابی
184
+ """
185
+ logger.info(f"🤖 {model_name} اجرای پرامپت...")
186
+
187
+ if not analysis_prompt or not analysis_prompt.strip():
188
+ logger.info("⚠️ پرامپتی وارد نشده - متن ناشناس‌سازی شده برگردانده می‌شود")
189
+ return anonymized_text
190
+
191
+ try:
192
+ # ساخت system message
193
+ system_msg = """شما یک تحلیلگر مالی حرفه‌ای هستید. متن حاوی کدهای ناشناس است (person-XX، company-XX، amount-XX، percent-XX).
194
+ به سوالات و درخواست‌ها با دقت پاسخ دهید و این کدها را در پاسخ خود حفظ کنید."""
195
+
196
+ # ساخت پیام کامل
197
+ full_text = f"""{analysis_prompt}
198
+
199
+ متن برای تحلیل:
200
+ {anonymized_text}"""
201
+
202
+ # استفاده از UnifiedLLMSender
203
+ sender = UnifiedLLMSender(model=model_name)
204
+ response = sender.send(
205
+ text=full_text,
206
+ system_msg=system_msg,
207
+ max_tokens=4096,
208
+ temperature=0.1,
209
+ lang='fa'
210
+ )
211
+
212
+ logger.info(f"✅ {model_name} پاسخ داد: {len(response)} کاراکتر")
213
+ return response
214
+
215
+ except Exception as e:
216
+ logger.error(f"❌ {model_name} Exception: {e}")
217
+ return f"❌ خطا در {model_name}: {str(e)}"
218
+
219
+ def restore_text(self, anonymized_text: str) -> str:
220
+ """بازگردانی متن ناشناس‌سازی شده به متن اصلی"""
221
+ logger.info("🔄 بازگردانی متن...")
222
+
223
+ if not self.mapping_table:
224
+ logger.warning("⚠️ جدول نگاشت خالی است")
225
+ return anonymized_text
226
+
227
+ restored = anonymized_text
228
+ for placeholder, original in sorted(self.mapping_table.items()):
229
+ restored = restored.replace(placeholder, original)
230
+
231
+ logger.info("✅ بازگردانی کامل")
232
+ return restored
233
+
234
+ def get_mapping_table_md(self) -> str:
235
+ """تبدیل جدول نگاشت به Markdown"""
236
+ if not self.mapping_table:
237
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
238
+
239
+ table = "### 📋 جدول نگاشت\n\n"
240
+ table += "| شناسه | متن اصلی |\n"
241
+ table += "|-------|----------|\n"
242
+
243
+ for token, original in sorted(self.mapping_table.items()):
244
+ table += f"| **{token}** | {original} |\n"
245
+
246
+ return table
247
+
248
+
249
+ # متغیر سراسری
250
+ anonymizer = None
251
+
252
+
253
+ def get_available_model_choices():
254
+ """دریافت لیست مدل‌های موجود برای Dropdown"""
255
+ available = get_available_models()
256
+ display_names = get_model_display_names()
257
+
258
+ choices = []
259
+ for model_name, info in available.items():
260
+ if info['has_key']:
261
+ choices.append(display_names.get(model_name, model_name))
262
+
263
+ # اگر هیچ مدلی موجود نیست، یک پیام نمایش بده
264
+ if not choices:
265
+ choices = ["❌ هیچ API Key موجود نیست"]
266
+
267
+ return choices
268
+
269
+
270
+ def get_model_name_from_display(display_name: str) -> str:
271
+ """تبدیل نام نمایشی به نام مدل"""
272
+ display_names = get_model_display_names()
273
+ reverse_map = {v: k for k, v in display_names.items()}
274
+ return reverse_map.get(display_name, display_name)
275
+
276
+
277
+ def process(input_text: str, analysis_prompt: str, model_choice: str):
278
+ """پردازش متن - 4 مرحله"""
279
+ global anonymizer
280
+
281
+ if not input_text.strip():
282
+ return "", "", "", ""
283
+
284
+ # دریافت نام واقعی مدل
285
+ model_name = get_model_name_from_display(model_choice)
286
+
287
+ if not anonymizer:
288
+ anonymizer = AnonymizerAdvanced()
289
+ else:
290
+ anonymizer.mapping_table = {}
291
+ anonymizer.reverse_mapping = {}
292
+
293
+ try:
294
+ logger.info("=" * 70)
295
+ logger.info(f"🚀 شروع پردازش - مدل تحلیل: {model_name}")
296
+ logger.info("=" * 70)
297
+
298
+ # مرحله 1: ناشناس‌سازی
299
+ logger.info("📝 مرحله 1: ناشناس‌سازی...")
300
+ anonymized_text, _ = anonymizer.anonymize_with_cerebras(input_text)
301
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
302
+
303
+ # مرحله 2: مدل انتخابی
304
+ logger.info(f"🤖 مرحله 2: {model_name}...")
305
+ model_response = anonymizer.analyze_with_model(anonymized_text, analysis_prompt, model_name)
306
+ logger.info(f"✅ {model_name}: {len(model_response)} کاراکتر")
307
+
308
+ # مرحله 3: بازگردانی
309
+ logger.info("🔄 مرحله 3: بازگردانی...")
310
+ restored_text = anonymizer.restore_text(model_response)
311
+ logger.info("✅ بازگردانی کامل")
312
+
313
+ # مرحله 4: جدول نگاشت
314
+ logger.info("📋 مرحله 4: جدول نگاشت...")
315
+ mapping_str = anonymizer.get_mapping_table_md()
316
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
317
+
318
+ logger.info("=" * 70)
319
+ logger.info("✅ تمام مراحل کامل!")
320
+ logger.info("=" * 70)
321
+
322
+ return restored_text, model_response, anonymized_text, mapping_str
323
+
324
+ except Exception as e:
325
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
326
+ return "", f"❌ خطا: {str(e)}", "", ""
327
+
328
+
329
+ def clear_all():
330
+ """پاک کردن همه"""
331
+ return "", "", "", "", "", ""
332
+
333
+
334
+ # Gradio Interface
335
+ css_rtl = """
336
+ .input-box { direction: rtl; text-align: right; }
337
+ .textbox textarea { direction: rtl; text-align: right; font-family: 'Tahoma', serif; }
338
+ """
339
+
340
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
341
+
342
+ gr.Markdown("# 🔐 سیستم ناشناس‌سازی متون مالی فارسی", elem_classes="input-box")
343
+ gr.Markdown("### 🌟 با پشتیبانی از مدل‌های پیشرفته AI", elem_classes="input-box")
344
+
345
+ with gr.Row():
346
+ with gr.Column(scale=1):
347
+ # منوی انتخاب مدل - بارگذاری دینامیک
348
+ model_dropdown = gr.Dropdown(
349
+ choices=get_available_model_choices(),
350
+ value=get_available_model_choices()[0] if get_available_model_choices() else None,
351
+ label="🤖 انتخاب مدل تحلیل",
352
+ info="فقط مدل‌هایی که API Key دارند نمایش داده می‌شوند",
353
+ interactive=True
354
+ )
355
+
356
+ analysis_prompt = gr.Textbox(
357
+ lines=8,
358
+ placeholder="مثال: این متن را خلاصه کن\nمثال: نقاط قوت و ضعف را استخراج کن",
359
+ label="📋 دستورات تحلیل (اختیاری)",
360
+ elem_classes="textbox"
361
+ )
362
+
363
+ gr.Markdown("---")
364
+
365
+ with gr.Column():
366
+ process_btn = gr.Button(
367
+ "▶️ پردازش",
368
+ variant="primary",
369
+ size="lg"
370
+ )
371
+
372
+ clear_btn = gr.Button(
373
+ "🗑️ پاک کردن",
374
+ variant="stop",
375
+ size="lg"
376
+ )
377
+
378
+ with gr.Column(scale=3):
379
+ input_text = gr.Textbox(
380
+ lines=14,
381
+ placeholder="متن مالی/خبری فارسی را وارد کنید...",
382
+ label="📝 متن ورودی",
383
+ elem_classes="textbox"
384
+ )
385
+
386
+ gr.Markdown("---")
387
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
388
+
389
+ with gr.Row():
390
+ with gr.Column(scale=1):
391
+ restored_text = gr.Textbox(
392
+ lines=12,
393
+ label="✅ متن بازگردانی شده",
394
+ interactive=False,
395
+ elem_classes="textbox"
396
+ )
397
+
398
+ with gr.Column(scale=1):
399
+ model_analysis = gr.Textbox(
400
+ lines=12,
401
+ label="🤖 تحلیل مدل (ناشناس)",
402
+ interactive=False,
403
+ elem_classes="textbox"
404
+ )
405
+
406
+ with gr.Column(scale=1):
407
+ anonymized_text = gr.Textbox(
408
+ lines=12,
409
+ label="🔒 متن ناشناس‌شده",
410
+ interactive=False,
411
+ elem_classes="textbox"
412
+ )
413
+
414
+ gr.Markdown("---")
415
+
416
+ mapping_table = gr.Markdown(
417
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
418
+ label="📋 جدول نگاشت",
419
+ elem_classes="input-box"
420
+ )
421
+
422
+ # Event Handlers
423
+ process_btn.click(
424
+ fn=process,
425
+ inputs=[input_text, analysis_prompt, model_dropdown],
426
+ outputs=[restored_text, model_analysis, anonymized_text, mapping_table]
427
+ )
428
+
429
+ clear_btn.click(
430
+ fn=clear_all,
431
+ outputs=[input_text, analysis_prompt, restored_text, model_analysis, anonymized_text, mapping_table]
432
+ )
433
+
434
+
435
+ if __name__ == "__main__":
436
+ print("=" * 70)
437
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
438
+ print("=" * 70)
439
+
440
+ # نمایش مدل‌های موجود
441
+ available = get_available_models()
442
+ display_names = get_model_display_names()
443
+
444
+ print("\n📋 مدل‌های موجود:\n")
445
+ for model_name, info in available.items():
446
+ status = "✅" if info['has_key'] else "❌"
447
+ display = display_names.get(model_name, model_name)
448
+ print(f" {status} {display} ({info['env_key']})")
449
+
450
+ print("\n" + "=" * 70 + "\n")
451
+
452
+ app.launch(
453
+ server_name="0.0.0.0",
454
+ server_port=7860,
455
+ share=False,
456
+ show_error=True
457
+ )
unified_llm_sender (1).py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ماژول مدیریت یکپارچه LLM ها
3
+ پشتیبانی از: ChatGPT, Grok, Gemini, DeepSeek
4
+ """
5
+
6
+ import os
7
+ import requests
8
+ import logging
9
+ from typing import Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class UnifiedLLMSender:
15
+ """کلاس یکپارچه برای ارسال به LLM های مختلف"""
16
+
17
+ MODELS = {
18
+ 'gpt-4o-mini': {
19
+ 'provider': 'openai',
20
+ 'api_base': 'https://api.openai.com/v1/chat/completions',
21
+ 'env_key': 'OPENAI_API_KEY'
22
+ },
23
+ 'gpt-4o': {
24
+ 'provider': 'openai',
25
+ 'api_base': 'https://api.openai.com/v1/chat/completions',
26
+ 'env_key': 'OPENAI_API_KEY'
27
+ },
28
+ 'o1': {
29
+ 'provider': 'openai',
30
+ 'api_base': 'https://api.openai.com/v1/chat/completions',
31
+ 'env_key': 'OPENAI_API_KEY'
32
+ },
33
+ 'o1-mini': {
34
+ 'provider': 'openai',
35
+ 'api_base': 'https://api.openai.com/v1/chat/completions',
36
+ 'env_key': 'OPENAI_API_KEY'
37
+ },
38
+ 'grok-beta': {
39
+ 'provider': 'xai',
40
+ 'api_base': 'https://api.x.ai/v1/chat/completions',
41
+ 'env_key': 'XAI_API_KEY'
42
+ },
43
+ 'grok-2-latest': {
44
+ 'provider': 'xai',
45
+ 'api_base': 'https://api.x.ai/v1/chat/completions',
46
+ 'env_key': 'XAI_API_KEY'
47
+ },
48
+ 'gemini-2.0-flash-exp': {
49
+ 'provider': 'google',
50
+ 'api_base': 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent',
51
+ 'env_key': 'GOOGLE_API_KEY'
52
+ },
53
+ 'gemini-1.5-pro': {
54
+ 'provider': 'google',
55
+ 'api_base': 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent',
56
+ 'env_key': 'GOOGLE_API_KEY'
57
+ },
58
+ 'deepseek-chat': {
59
+ 'provider': 'deepseek',
60
+ 'api_base': 'https://api.deepseek.com/v1/chat/completions',
61
+ 'env_key': 'DEEPSEEK_API_KEY'
62
+ },
63
+ 'deepseek-reasoner': {
64
+ 'provider': 'deepseek',
65
+ 'api_base': 'https://api.deepseek.com/v1/chat/completions',
66
+ 'env_key': 'DEEPSEEK_API_KEY'
67
+ }
68
+ }
69
+
70
+ def __init__(self, model: str = 'gpt-4o-mini'):
71
+ """
72
+ مقداردهی اولیه
73
+
74
+ Args:
75
+ model: نام مدل (مثلاً 'gpt-4o-mini', 'grok-beta', 'gemini-2.0-flash-exp', 'deepseek-chat')
76
+ """
77
+ self.model = model
78
+
79
+ if model not in self.MODELS:
80
+ raise ValueError(f"مدل {model} پشتیبانی نمی‌شود. مدل‌های موجود: {list(self.MODELS.keys())}")
81
+
82
+ self.config = self.MODELS[model]
83
+ self.provider = self.config['provider']
84
+ self.api_key = os.getenv(self.config['env_key'])
85
+
86
+ if not self.api_key:
87
+ logger.warning(f"⚠️ API Key برای {model} تنظیم نشده: {self.config['env_key']}")
88
+
89
+ def send(self,
90
+ text: str,
91
+ system_msg: Optional[str] = None,
92
+ max_tokens: int = 4096,
93
+ temperature: float = 0.3,
94
+ lang: str = 'fa') -> str:
95
+ """
96
+ ارسال پیام به LLM
97
+
98
+ Args:
99
+ text: متن ورودی
100
+ system_msg: پیام سیستم
101
+ max_tokens: حداکثر توکن خروجی
102
+ temperature: دما
103
+ lang: زبان
104
+
105
+ Returns:
106
+ پاسخ مدل
107
+ """
108
+ if not self.api_key:
109
+ return f"❌ API Key برای {self.model} موجود نیست. لطفاً {self.config['env_key']} را تنظیم کنید."
110
+
111
+ try:
112
+ if self.provider == 'google':
113
+ return self._send_google(text, system_msg, max_tokens, temperature)
114
+ else:
115
+ # OpenAI-compatible APIs (OpenAI, xAI, DeepSeek)
116
+ return self._send_openai_compatible(text, system_msg, max_tokens, temperature)
117
+
118
+ except Exception as e:
119
+ logger.error(f"❌ خطا در ارسال به {self.model}: {e}")
120
+ return f"❌ خطا: {str(e)}"
121
+
122
+ def _send_openai_compatible(self, text: str, system_msg: Optional[str],
123
+ max_tokens: int, temperature: float) -> str:
124
+ """ارسال به API های سازگار با OpenAI"""
125
+
126
+ messages = []
127
+ if system_msg:
128
+ messages.append({"role": "system", "content": system_msg})
129
+ messages.append({"role": "user", "content": text})
130
+
131
+ headers = {
132
+ "Authorization": f"Bearer {self.api_key}",
133
+ "Content-Type": "application/json"
134
+ }
135
+
136
+ payload = {
137
+ "model": self.model,
138
+ "messages": messages,
139
+ "max_tokens": max_tokens,
140
+ "temperature": temperature
141
+ }
142
+
143
+ logger.info(f"📤 ارسال به {self.model}...")
144
+
145
+ response = requests.post(
146
+ self.config['api_base'],
147
+ headers=headers,
148
+ json=payload,
149
+ timeout=120
150
+ )
151
+
152
+ if response.status_code == 200:
153
+ result = response.json()
154
+ content = result['choices'][0]['message']['content']
155
+ logger.info(f"✅ پاسخ از {self.model} دریافت شد ({len(content)} کاراکتر)")
156
+ return content
157
+ else:
158
+ error_msg = f"خطای API {response.status_code}: {response.text}"
159
+ logger.error(f"❌ {error_msg}")
160
+ return f"❌ {error_msg}"
161
+
162
+ def _send_google(self, text: str, system_msg: Optional[str],
163
+ max_tokens: int, temperature: float) -> str:
164
+ """ارسال به Google Gemini API"""
165
+
166
+ # ترکیب system message با user message
167
+ full_text = text
168
+ if system_msg:
169
+ full_text = f"{system_msg}\n\n{text}"
170
+
171
+ url = f"{self.config['api_base']}?key={self.api_key}"
172
+
173
+ payload = {
174
+ "contents": [{
175
+ "parts": [{
176
+ "text": full_text
177
+ }]
178
+ }],
179
+ "generationConfig": {
180
+ "maxOutputTokens": max_tokens,
181
+ "temperature": temperature
182
+ }
183
+ }
184
+
185
+ logger.info(f"📤 ارسال به {self.model}...")
186
+
187
+ response = requests.post(
188
+ url,
189
+ headers={"Content-Type": "application/json"},
190
+ json=payload,
191
+ timeout=120
192
+ )
193
+
194
+ if response.status_code == 200:
195
+ result = response.json()
196
+ content = result['candidates'][0]['content']['parts'][0]['text']
197
+ logger.info(f"✅ پاسخ از {self.model} دریافت شد ({len(content)} کاراکتر)")
198
+ return content
199
+ else:
200
+ error_msg = f"خطای API {response.status_code}: {response.text}"
201
+ logger.error(f"❌ {error_msg}")
202
+ return f"❌ {error_msg}"
203
+
204
+
205
+ # تابع کمکی برای گرفتن لیست مدل‌های موجود
206
+ def get_available_models() -> dict:
207
+ """دریافت لیست مدل‌های موجود و وضعیت API key های آن‌ها"""
208
+ available = {}
209
+
210
+ for model_name, config in UnifiedLLMSender.MODELS.items():
211
+ env_key = config['env_key']
212
+ has_key = bool(os.getenv(env_key))
213
+ available[model_name] = {
214
+ 'provider': config['provider'],
215
+ 'has_key': has_key,
216
+ 'env_key': env_key
217
+ }
218
+
219
+ return available
220
+
221
+
222
+ def get_model_display_names() -> dict:
223
+ """دریافت نام‌های نمایشی مدل‌ها"""
224
+ return {
225
+ 'gpt-4o-mini': '🤖 ChatGPT 4o-mini',
226
+ 'gpt-4o': '🤖 ChatGPT 4o',
227
+ 'o1': '🤖 ChatGPT o1',
228
+ 'o1-mini': '🤖 ChatGPT o1-mini',
229
+ 'grok-beta': '🚀 Grok Beta',
230
+ 'grok-2-latest': '🚀 Grok 2',
231
+ 'gemini-2.0-flash-exp': '✨ Gemini 2.0 Flash',
232
+ 'gemini-1.5-pro': '✨ Gemini 1.5 Pro',
233
+ 'deepseek-chat': '🧠 DeepSeek Chat',
234
+ 'deepseek-reasoner': '🧠 DeepSeek Reasoner'
235
+ }