leilaghomashchi commited on
Commit
d8ec3e2
·
verified ·
1 Parent(s): 5c093de

Upload 2 files

Browse files
Files changed (2) hide show
  1. app_unified (2).py +534 -0
  2. llm_sender_unified (2).py +312 -0
app_unified (2).py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, List, Tuple
8
+ from llm_sender_unified import create_llm_sender, AVAILABLE_MODELS # ✅ import ماژول جدید
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class AnonymizerAdvanced:
14
+ """ناشناس‌ساز پیشرفته با روش‌های متعدد"""
15
+
16
+ def __init__(self, cerebras_key: str = None, llm_provider: str = "chatgpt", llm_model: str = None):
17
+ self.cerebras_key = cerebras_key or os.getenv("CEREBRAS_API_KEY")
18
+ self.llm_provider = llm_provider
19
+ self.llm_model = llm_model
20
+ self.mapping_table = {} # {placeholder: original_text}
21
+ self.reverse_mapping = {} # {original_text: placeholder}
22
+
23
+ # ✅ ایجاد LLM sender بر اساس provider انتخابی
24
+ self._create_llm_sender()
25
+
26
+ logger.info(f"✅ Anonymizer Advanced مقداردهی شد با {llm_provider}")
27
+
28
+ def _create_llm_sender(self):
29
+ """ایجاد LLM sender مناسب"""
30
+ try:
31
+ # گرفتن API key مناسب
32
+ if self.llm_provider == "chatgpt":
33
+ api_key = os.getenv("OPENAI_API_KEY")
34
+ elif self.llm_provider == "grok":
35
+ api_key = os.getenv("XAI_API_KEY")
36
+ else:
37
+ api_key = None
38
+
39
+ # ایجاد sender
40
+ self.llm_sender = create_llm_sender(
41
+ provider=self.llm_provider,
42
+ api_key=api_key,
43
+ model=self.llm_model
44
+ )
45
+
46
+ logger.info(f"✅ LLM Sender ایجاد شد: {self.llm_provider} - {self.llm_sender.model}")
47
+
48
+ except Exception as e:
49
+ logger.error(f"❌ خطا در ایجاد LLM Sender: {e}")
50
+ # fallback to ChatGPT
51
+ self.llm_sender = create_llm_sender("chatgpt")
52
+
53
+ def set_llm_provider(self, provider: str, model: str = None):
54
+ """تغییر provider و مدل LLM"""
55
+ self.llm_provider = provider
56
+ self.llm_model = model
57
+ self._create_llm_sender()
58
+ logger.info(f"✅ LLM تغییر یافت به: {provider} - {model}")
59
+
60
+ def anonymize_with_cerebras(self, text: str) -> Tuple[str, Dict]:
61
+ """ناشناس‌سازی با Cerebras - دریافت mapping از مدل"""
62
+ logger.info("🧠 روش Cerebras...")
63
+
64
+ if not self.cerebras_key:
65
+ logger.error("❌ Cerebras API Key موجود نیست")
66
+ raise ValueError("Cerebras API Key مورد نیاز است")
67
+
68
+ try:
69
+ # مرحله 1: ناشناس‌سازی متن
70
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
71
+ 1. اسامی اشخاص → person-01, person-02, ...
72
+ 2. نام شرکت‌ها/سازمان‌ها → company-01, company-02, ...
73
+ 3. مقادیر پولی → amount-01, amount-02, ...
74
+ 4. درصدها → percent-01, percent-02, ...
75
+ 5. فقط این توکن‌ها استفاده کنید
76
+ 6. شماره‌های نسخه را درست حفظ کنید
77
+ 7. اگر موجودیت تکرار شود از شماره قدیمی استفاده کنید
78
+
79
+ متن:
80
+ {text}
81
+
82
+ خروجی: فقط متن ناشناس شده"""
83
+
84
+ response1 = requests.post(
85
+ "https://api.cerebras.ai/v1/chat/completions",
86
+ headers={
87
+ "Authorization": f"Bearer {self.cerebras_key}",
88
+ "Content-Type": "application/json"
89
+ },
90
+ json={
91
+ "model": "llama-3.3-70b",
92
+ "messages": [{"role": "user", "content": prompt1}],
93
+ "max_tokens": 4096,
94
+ "temperature": 0.1
95
+ },
96
+ timeout=60
97
+ )
98
+
99
+ if response1.status_code != 200:
100
+ logger.error(f"❌ Cerebras Error: {response1.status_code}")
101
+ raise Exception(f"Cerebras API Error: {response1.status_code}")
102
+
103
+ anonymized_text = response1.json()['choices'][0]['message']['content'].strip()
104
+ logger.info("✅ Cerebras: ناشناس‌سازی موفق")
105
+
106
+ # مرحله 2: استخراج mapping از مدل
107
+ prompt2 = f"""متن اصلی:
108
+ {text}
109
+
110
+ متن ناشناس شده:
111
+ {anonymized_text}
112
+
113
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
114
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
115
+
116
+ **مهم:**
117
+ - برای person-XX: نام کامل شخص (مثلاً "علی احمدی")
118
+ - برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")
119
+ - برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")
120
+ - برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")
121
+
122
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
123
+ {{
124
+ "person-01": "متن اصلی کامل",
125
+ "company-01": "متن اصلی کامل",
126
+ "amount-01": "متن اصلی کامل با واحد",
127
+ "percent-01": "عدد + درصد",
128
+ ...
129
+ }}"""
130
+
131
+ response2 = requests.post(
132
+ "https://api.cerebras.ai/v1/chat/completions",
133
+ headers={
134
+ "Authorization": f"Bearer {self.cerebras_key}",
135
+ "Content-Type": "application/json"
136
+ },
137
+ json={
138
+ "model": "llama-3.3-70b",
139
+ "messages": [{"role": "user", "content": prompt2}],
140
+ "max_tokens": 2048,
141
+ "temperature": 0.1
142
+ },
143
+ timeout=60
144
+ )
145
+
146
+ if response2.status_code == 200:
147
+ mapping_text = response2.json()['choices'][0]['message']['content'].strip()
148
+
149
+ # پاک‌سازی و parse کردن JSON
150
+ # حذف markdown code blocks اگر وجود داشته باشه
151
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
152
+
153
+ try:
154
+ self.mapping_table = json.loads(mapping_text)
155
+
156
+ # پست-پروسسینگ: اصلاح mapping برای percent ها
157
+ self._fix_percent_mapping()
158
+
159
+ # ساخت reverse mapping
160
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
161
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
162
+ except json.JSONDecodeError:
163
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
164
+ self._extract_mapping_from_text(text, anonymized_text)
165
+ else:
166
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
167
+ self._extract_mapping_from_text(text, anonymized_text)
168
+
169
+ return anonymized_text, self.mapping_table
170
+
171
+ except Exception as e:
172
+ logger.error(f"❌ Cerebras Exception: {e}")
173
+ raise
174
+
175
+ def _fix_percent_mapping(self):
176
+ """اصلاح mapping برای درصدها و مقادیر - اضافه کردن واحدها اگر فقط عدد باشد"""
177
+ for token, value in self.mapping_table.items():
178
+ value_str = str(value).strip()
179
+
180
+ if token.startswith('percent-'):
181
+ # چک کنیم آیا کلمه "درصد" یا "%" در value هست
182
+ if not re.search(r'(درصد|%|درصدی)', value_str):
183
+ # فقط عدد هست، کلمه "درصد" رو اضافه کن
184
+ self.mapping_table[token] = f"{value_str} درصد"
185
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
186
+
187
+ elif token.startswith('amount-'):
188
+ # چک کنیم آیا واحد پولی در value هست
189
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
190
+ # فقط عدد هست، احتمالاً باید واحد اضافه بشه
191
+ # اما نمی‌دونیم چه واحدی، پس warning بده
192
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
193
+
194
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
195
+ """استخراج mapping از متن‌های اصلی و ناشناس شده - نسخه بهبود یافته"""
196
+
197
+ # استخراج همه توکن‌های ناشناس از متن ناشناس‌سازی شده
198
+ all_tokens = []
199
+ for entity_type in ['person', 'company', 'amount', 'percent']:
200
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
201
+ all_tokens.extend([(t, entity_type) for t in tokens])
202
+
203
+ # حذف تکراری‌ها و مرتب‌سازی
204
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
205
+
206
+ # الگوهای موجودیت در متن اصلی
207
+ patterns = {
208
+ 'person': r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b',
209
+ 'company': r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*',
210
+ 'amount': r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)',
211
+ 'percent': r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)',
212
+ }
213
+
214
+ # استخراج موجودیت‌های اصلی
215
+ original_entities = {}
216
+ for entity_type, pattern in patterns.items():
217
+ matches = list(re.finditer(pattern, original))
218
+ original_entities[entity_type] = [m.group().strip() for m in matches]
219
+
220
+ # نگاشت توکن‌ها به موجودیت‌های اصلی
221
+ for token, entity_type in all_tokens:
222
+ if entity_type in original_entities and original_entities[entity_type]:
223
+ # گرفتن شماره توکن (مثلاً از person-01 عدد 1 رو میگیریم)
224
+ token_num = int(token.split('-')[1]) - 1
225
+
226
+ if token_num < len(original_entities[entity_type]):
227
+ original_text = original_entities[entity_type][token_num]
228
+ self.mapping_table[token] = original_text
229
+ self.reverse_mapping[original_text] = token
230
+ else:
231
+ # اگر شماره توکن بیشتر از تعداد موجودیت‌ها بود
232
+ # از آخرین موجودیت استفاده کن
233
+ original_text = original_entities[entity_type][-1]
234
+ if token not in self.mapping_table:
235
+ self.mapping_table[token] = original_text
236
+ self.reverse_mapping[original_text] = token
237
+
238
+ def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
239
+ """
240
+ ✅ استفاده از LLM یکپارچه (ChatGPT یا Grok)
241
+ اجرای پرامپت‌های درون متن ناشناس‌سازی شده
242
+ """
243
+ logger.info(f"🤖 {self.llm_provider.upper()} اجرای پرامپت...")
244
+
245
+ # اگر پرامپتی نیست، فقط متن ناشناس‌سازی شده برگردان
246
+ if not analysis_prompt or not analysis_prompt.strip():
247
+ logger.info("⚠️ پرامپت خالی - بدون تحلیل")
248
+ return "⚠️ هیچ دستور تحلیل داده نشده است"
249
+
250
+ # ترکیب متن ناشناس‌سازی شده + پرامپت کاربر
251
+ combined_text = f"""متن ناشناس‌سازی شده:
252
+ {anonymized_text}
253
+
254
+ دستورات:
255
+ {analysis_prompt}
256
+
257
+ توجه: در پاسخ از همان کدهای ناشناس (person-XX, company-XX, amount-XX, percent-XX) استفاده کن."""
258
+
259
+ try:
260
+ # ✅ ارسال به LLM انتخابی
261
+ response = self.llm_sender.send_simple(combined_text, lang='fa')
262
+
263
+ logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
264
+ return response
265
+
266
+ except Exception as e:
267
+ logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
268
+ return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
269
+
270
+ def restore_text(self, anonymized_text: str) -> str:
271
+ """بازگردانی متن ناشناس‌سازی شده به اصلی"""
272
+ logger.info("🔄 بازگردانی متن...")
273
+
274
+ if not self.mapping_table:
275
+ logger.warning("⚠️ جدول نگاشت خالی است")
276
+ return anonymized_text
277
+
278
+ restored = anonymized_text
279
+
280
+ # جایگزینی placeholder ها با متن اصلی
281
+ for placeholder, original in sorted(self.mapping_table.items()):
282
+ restored = restored.replace(placeholder, original)
283
+
284
+ logger.info("✅ بازگردانی کامل")
285
+ return restored
286
+
287
+ def get_mapping_table_md(self) -> str:
288
+ """تبدیل جدول نگاشت به Markdown"""
289
+ if not self.mapping_table:
290
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
291
+
292
+ table = "### 📋 جدول نگاشت\n\n"
293
+ table += "| شناسه | متن اصلی |\n"
294
+ table += "|-------|----------|\n"
295
+
296
+ for token, original in sorted(self.mapping_table.items()):
297
+ table += f"| **{token}** | {original} |\n"
298
+
299
+ return table
300
+
301
+ # متغیر سراسری
302
+ anonymizer = None
303
+
304
+ def process(
305
+ input_text: str,
306
+ analysis_prompt: str,
307
+ llm_provider: str,
308
+ llm_model: str
309
+ ):
310
+ """پردازش متن - 4 مرحله"""
311
+ global anonymizer
312
+
313
+ if not input_text.strip():
314
+ return "", "", "", ""
315
+
316
+ cerebras_key = os.getenv("CEREBRAS_API_KEY")
317
+
318
+ # ✅ ایجاد یا آپدیت anonymizer با provider و model جدید
319
+ if not anonymizer:
320
+ anonymizer = AnonymizerAdvanced(
321
+ cerebras_key,
322
+ llm_provider=llm_provider,
323
+ llm_model=llm_model
324
+ )
325
+ else:
326
+ # آپدیت provider و model
327
+ anonymizer.set_llm_provider(llm_provider, llm_model)
328
+ anonymizer.mapping_table = {}
329
+ anonymizer.reverse_mapping = {}
330
+
331
+ try:
332
+ logger.info("=" * 70)
333
+ logger.info(f"🚀 ��روع پردازش - LLM: {llm_provider} ({llm_model})")
334
+ logger.info("=" * 70)
335
+
336
+ # مرحله 1: ناشناس‌سازی
337
+ logger.info("📝 مرحله 1: ناشناس‌سازی...")
338
+ anonymized_text, _ = anonymizer.anonymize_with_cerebras(input_text)
339
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
340
+
341
+ # مرحله 2: LLM با متن ناشناس‌سازی شده + دستورات
342
+ logger.info(f"🤖 مرحله 2: {llm_provider.upper()}...")
343
+ llm_response = anonymizer.analyze_with_llm(anonymized_text, analysis_prompt)
344
+ logger.info(f"✅ {llm_provider.upper()}: {len(llm_response)} کاراکتر")
345
+
346
+ # مرحله 3: بازگردانی پاسخ LLM
347
+ logger.info("🔄 مرحله 3: بازگردانی...")
348
+ restored_text = anonymizer.restore_text(llm_response)
349
+ logger.info("✅ بازگردانی کامل")
350
+
351
+ # مرحله 4: جدول نگاشت
352
+ logger.info("📋 مرحله 4: جدول نگاشت...")
353
+ mapping_str = anonymizer.get_mapping_table_md()
354
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
355
+
356
+ logger.info("=" * 70)
357
+ logger.info("✅ تمام مراحل کامل!")
358
+ logger.info("=" * 70)
359
+
360
+ return restored_text, llm_response, anonymized_text, mapping_str
361
+
362
+ except Exception as e:
363
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
364
+ return "", f"❌ خطا: {str(e)}", "", ""
365
+
366
+ def clear_all():
367
+ """پاک کردن همه"""
368
+ return "", "", "", "", "", ""
369
+
370
+ def update_model_choices(provider: str):
371
+ """آپدیت لیست مدل‌ها بر اساس provider انتخابی"""
372
+ models = AVAILABLE_MODELS.get(provider, [])
373
+ return gr.Dropdown(choices=models, value=models[0] if models else None)
374
+
375
+ # Gradio Interface
376
+ css_rtl = """
377
+ .input-box { direction: rtl; text-align: right; }
378
+ .textbox textarea { direction: rtl; text-align: right; font-family: 'Tahoma', serif; }
379
+ """
380
+
381
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
382
+
383
+ gr.Markdown("# 🔐 سیستم ناشناس‌سازی متون مالی فارسی", elem_classes="input-box")
384
+
385
+ # ============================================
386
+ # صفحه اول: دکمه‌ها (راست) + ورودی (چپ)
387
+ # ============================================
388
+ with gr.Row():
389
+ # سمت راست: دکمه‌ها و دستورات
390
+ with gr.Column(scale=1):
391
+ # ✅ انتخاب LLM Provider
392
+ with gr.Group():
393
+ gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
394
+
395
+ llm_provider = gr.Dropdown(
396
+ choices=["chatgpt", "grok"],
397
+ value="chatgpt",
398
+ label="🤖 انتخاب LLM",
399
+ interactive=True
400
+ )
401
+
402
+ llm_model = gr.Dropdown(
403
+ choices=AVAILABLE_MODELS["chatgpt"],
404
+ value="gpt-4o-mini",
405
+ label="📦 انتخاب مدل",
406
+ interactive=True
407
+ )
408
+
409
+ gr.Markdown("---")
410
+
411
+ analysis_prompt = gr.Textbox(
412
+ lines=6,
413
+ placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
414
+ label="📋 دستورات LLM (اختیاری)",
415
+ elem_classes="textbox"
416
+ )
417
+
418
+ gr.Markdown("---")
419
+
420
+ with gr.Column():
421
+ process_btn = gr.Button(
422
+ "▶️ پردازش",
423
+ variant="primary",
424
+ size="lg"
425
+ )
426
+
427
+ clear_btn = gr.Button(
428
+ "🗑️ پاک کردن",
429
+ variant="stop",
430
+ size="lg"
431
+ )
432
+
433
+ # سمت چپ: متن ورودی (بزرگ‌تر)
434
+ with gr.Column(scale=3):
435
+ input_text = gr.Textbox(
436
+ lines=18,
437
+ placeholder="متن مالی/خبری را وارد کنید...",
438
+ label="📝 متن ورودی",
439
+ elem_classes="textbox"
440
+ )
441
+
442
+ # ============================================
443
+ # صفحه دوم: 3 باکس نتایج (وسط)
444
+ # ============================================
445
+ gr.Markdown("---")
446
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
447
+
448
+ with gr.Row():
449
+ # باکس 1: متن بازگردانی شده (راست)
450
+ with gr.Column(scale=1):
451
+ restored_text = gr.Textbox(
452
+ lines=12,
453
+ label="✅ متن بازگردانی شده",
454
+ interactive=False,
455
+ elem_classes="textbox"
456
+ )
457
+
458
+ # باکس 2: تحلیل LLM (وسط)
459
+ with gr.Column(scale=1):
460
+ llm_analysis = gr.Textbox(
461
+ lines=12,
462
+ label="🤖 تحلیل LLM",
463
+ interactive=False,
464
+ elem_classes="textbox"
465
+ )
466
+
467
+ # باکس 3: متن ناشناس‌شده (چپ)
468
+ with gr.Column(scale=1):
469
+ anonymized_text = gr.Textbox(
470
+ lines=12,
471
+ label="🔒 متن ناشناس‌شده",
472
+ interactive=False,
473
+ elem_classes="textbox"
474
+ )
475
+
476
+ # ============================================
477
+ # پایین: جدول نگاشت (Markdown)
478
+ # ============================================
479
+ gr.Markdown("---")
480
+
481
+ mapping_table = gr.Markdown(
482
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
483
+ label="📋 جدول نگاشت",
484
+ elem_classes="input-box"
485
+ )
486
+
487
+ # ============================================
488
+ # Event Handlers
489
+ # ============================================
490
+
491
+ # ✅ آپدیت مدل‌ها هنگام تغییر provider
492
+ llm_provider.change(
493
+ fn=update_model_choices,
494
+ inputs=[llm_provider],
495
+ outputs=[llm_model]
496
+ )
497
+
498
+ # پردازش
499
+ process_btn.click(
500
+ fn=process,
501
+ inputs=[input_text, analysis_prompt, llm_provider, llm_model],
502
+ outputs=[restored_text, llm_analysis, anonymized_text, mapping_table]
503
+ )
504
+
505
+ # پاک کردن
506
+ clear_btn.click(
507
+ fn=clear_all,
508
+ outputs=[input_text, analysis_prompt, restored_text, llm_analysis, anonymized_text, mapping_table]
509
+ )
510
+
511
+ if __name__ == "__main__":
512
+ print("=" * 70)
513
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
514
+ print("=" * 70)
515
+ print("\n📋 نحوه استفاده:\n")
516
+ print("1. کلیدهای API را تنظیم کنید:")
517
+ print(" - CEREBRAS_API_KEY (ضروری)")
518
+ print(" - OPENAI_API_KEY (برای ChatGPT)")
519
+ print(" - XAI_API_KEY (برای Grok)")
520
+ print("2. http://localhost:7860 را باز کنید")
521
+ print("3. LLM و مدل را انتخاب کنید")
522
+ print("4. متن را وارد کنید")
523
+ print("5. 'پردازش' را کلیک کنید\n")
524
+ print("LLM‌های پشتیبانی‌شده:")
525
+ print(" 🤖 ChatGPT: gpt-4o-mini, gpt-4o, gpt-4-turbo")
526
+ print(" 🤖 Grok: grok-beta (رایگان), grok-3-mini, grok-3")
527
+ print("=" * 70 + "\n")
528
+
529
+ app.launch(
530
+ server_name="0.0.0.0",
531
+ server_port=7860,
532
+ share=False,
533
+ show_error=True
534
+ )
llm_sender_unified (2).py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🤖 LLM Sender Unified Module
3
+ ماژول یکپارچه برای ارسال به ChatGPT و Grok
4
+ """
5
+
6
+ import requests
7
+ import os
8
+ import logging
9
+ from typing import Optional
10
+ import time
11
+ from abc import ABC, abstractmethod
12
+
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LLMSender(ABC):
18
+ """کلاس پایه برای ارسال به مدل‌های مختلف LLM"""
19
+
20
+ def __init__(self, api_key: Optional[str] = None, model: str = None):
21
+ self.api_key = api_key
22
+ self.model = model
23
+ self.base_url = ""
24
+
25
+ @abstractmethod
26
+ def get_default_model(self) -> str:
27
+ """مدل پیش‌فرض"""
28
+ pass
29
+
30
+ @abstractmethod
31
+ def get_base_url(self) -> str:
32
+ """URL پایه API"""
33
+ pass
34
+
35
+ def set_api_key(self, api_key: str):
36
+ """تنظیم کلید API"""
37
+ self.api_key = api_key
38
+ logger.info("✅ کلید API تنظیم شد")
39
+
40
+ def set_model(self, model: str):
41
+ """تغییر مدل"""
42
+ self.model = model
43
+ logger.info(f"✅ مدل تغییر یافت به: {model}")
44
+
45
+ def send_simple(self, text: str, lang: str = 'fa') -> str:
46
+ """ارسال ساده بدون system message سفارشی"""
47
+ system_msg = (
48
+ "شما یک تحلیلگر متخصص هستید. متن حاوی کدهای ناشناس است. "
49
+ "به درخواست‌ها با دقت و حرفه‌ای پاسخ دهید."
50
+ if lang == 'fa'
51
+ else "You are a professional analyst. The text contains anonymous codes. "
52
+ "Answer requests accurately and professionally."
53
+ )
54
+
55
+ return self.send(text, system_msg=system_msg, lang=lang)
56
+
57
+ def send(
58
+ self,
59
+ text: str,
60
+ system_msg: Optional[str] = None,
61
+ max_tokens: int = 2000,
62
+ temperature: float = 0.7,
63
+ timeout: int = 60,
64
+ lang: str = 'fa',
65
+ retry_count: int = 3
66
+ ) -> str:
67
+ """ارسال متن به LLM با کنترل کامل"""
68
+ try:
69
+ # بررسی اولیه
70
+ if not text or not text.strip():
71
+ error_msg = "متن خالی است!" if lang == 'fa' else "Text is empty!"
72
+ logger.error(f"❌ {error_msg}")
73
+ return f"❌ {error_msg}"
74
+
75
+ if not self.api_key:
76
+ error_msg = "کلید API تنظیم نشده است!" if lang == 'fa' else "API Key not configured!"
77
+ logger.error(f"❌ {error_msg}")
78
+ return f"❌ {error_msg}"
79
+
80
+ # تنظیم system message پیش‌فرض
81
+ if system_msg is None:
82
+ system_msg = (
83
+ "شما یک تحلیلگر مالی حرفه‌ای هستید. متن حاوی کدهای ناشناس است. "
84
+ "به سوالات با دقت پاسخ دهید."
85
+ if lang == 'fa'
86
+ else "You are a professional financial analyst. The text contains anonymous codes. "
87
+ "Answer questions accurately."
88
+ )
89
+
90
+ # تهیه headers
91
+ headers = {
92
+ "Authorization": f"Bearer {self.api_key}",
93
+ "Content-Type": "application/json"
94
+ }
95
+
96
+ # ساخت request body
97
+ data = {
98
+ "model": self.model,
99
+ "messages": [
100
+ {"role": "system", "content": system_msg},
101
+ {"role": "user", "content": text}
102
+ ],
103
+ "max_tokens": max_tokens,
104
+ "temperature": temperature
105
+ }
106
+
107
+ # ارسال با retry mechanism
108
+ for attempt in range(retry_count):
109
+ try:
110
+ logger.info(f"📤 ارسال درخواست به {self.__class__.__name__} (تلاش {attempt + 1}/{retry_count})...")
111
+
112
+ response = requests.post(
113
+ self.base_url,
114
+ headers=headers,
115
+ json=data,
116
+ timeout=timeout
117
+ )
118
+
119
+ # پردازش پاسخ موفق
120
+ if response.status_code == 200:
121
+ result = response.json()
122
+ llm_response = result['choices'][0]['message']['content']
123
+ logger.info("✅ پاسخ دریافت شد")
124
+ return llm_response
125
+
126
+ # پردازش خطاهای مختلف
127
+ elif response.status_code == 429: # Rate limiting
128
+ wait_time = 5 * (attempt + 1)
129
+ logger.warning(f"⚠️ Rate limit | صبر: {wait_time} ثانیه")
130
+ if attempt < retry_count - 1:
131
+ time.sleep(wait_time)
132
+ continue
133
+ else:
134
+ return (
135
+ "❌ سهمیه API تمام شده است. لطفاً بعداً تلاش کنید."
136
+ if lang == 'fa'
137
+ else "❌ API quota exceeded. Please try later."
138
+ )
139
+
140
+ elif response.status_code == 401:
141
+ return (
142
+ "❌ کلید API نامعتبر است!"
143
+ if lang == 'fa'
144
+ else "❌ Invalid API key!"
145
+ )
146
+
147
+ elif response.status_code in [502, 503, 504]: # Server errors
148
+ wait_time = 2 * (attempt + 1)
149
+ logger.warning(f"⚠️ Server error {response.status_code} | صبر: {wait_time} ثانیه")
150
+ if attempt < retry_count - 1:
151
+ time.sleep(wait_time)
152
+ continue
153
+ else:
154
+ return (
155
+ f"❌ خطای سرور: {response.status_code}"
156
+ if lang == 'fa'
157
+ else f"❌ Server error: {response.status_code}"
158
+ )
159
+
160
+ else:
161
+ # خطای دیگر
162
+ error_data = response.json() if response.content else {}
163
+ error_msg = error_data.get('error', {}).get('message', response.text)
164
+ logger.error(f"❌ API Error: {error_msg}")
165
+ return f"❌ API Error: {error_msg}"
166
+
167
+ except requests.exceptions.Timeout:
168
+ logger.warning("⚠️ Timeout | صبر: 3 ثانیه و تلاش مجدد")
169
+ if attempt < retry_count - 1:
170
+ time.sleep(3)
171
+ continue
172
+ else:
173
+ return (
174
+ "❌ خطای اتصال: timeout"
175
+ if lang == 'fa'
176
+ else "❌ Connection error: timeout"
177
+ )
178
+
179
+ except requests.exceptions.ConnectionError as e:
180
+ logger.warning("⚠️ Connection error | صبر: 2 ثانیه و تلاش مجدد")
181
+ if attempt < retry_count - 1:
182
+ time.sleep(2)
183
+ continue
184
+ else:
185
+ return (
186
+ f"❌ خطای اتصال: {str(e)}"
187
+ if lang == 'fa'
188
+ else f"❌ Connection error: {str(e)}"
189
+ )
190
+
191
+ except Exception as e:
192
+ logger.error(f"❌ خطای غیرمنتظره: {str(e)}")
193
+ return (
194
+ f"❌ خطا در ارتباط با LLM: {str(e)}"
195
+ if lang == 'fa'
196
+ else f"❌ Error connecting to LLM: {str(e)}"
197
+ )
198
+
199
+
200
+ class ChatGPTSender(LLMSender):
201
+ """کلاس برای ارسال به ChatGPT"""
202
+
203
+ def __init__(self, api_key: Optional[str] = None, model: str = "gpt-4o-mini"):
204
+ super().__init__(api_key or os.getenv("OPENAI_API_KEY", ""), model)
205
+ self.base_url = self.get_base_url()
206
+
207
+ if not self.api_key:
208
+ logger.warning("⚠️ کلید OpenAI API تنظیم نشده است!")
209
+
210
+ def get_default_model(self) -> str:
211
+ return "gpt-4o-mini"
212
+
213
+ def get_base_url(self) -> str:
214
+ return "https://api.openai.com/v1/chat/completions"
215
+
216
+
217
+ class GrokSender(LLMSender):
218
+ """کلاس برای ارسال به Grok (xAI)"""
219
+
220
+ def __init__(self, api_key: Optional[str] = None, model: str = "grok-beta"):
221
+ super().__init__(api_key or os.getenv("XAI_API_KEY", ""), model)
222
+ self.base_url = self.get_base_url()
223
+
224
+ if not self.api_key:
225
+ logger.warning("⚠️ کلید xAI API تنظیم نشده است!")
226
+
227
+ def get_default_model(self) -> str:
228
+ return "grok-beta"
229
+
230
+ def get_base_url(self) -> str:
231
+ return "https://api.x.ai/v1/chat/completions"
232
+
233
+
234
+ # Factory function برای ایجاد sender مناسب
235
+ def create_llm_sender(
236
+ provider: str = "chatgpt",
237
+ api_key: Optional[str] = None,
238
+ model: Optional[str] = None
239
+ ) -> LLMSender:
240
+ """
241
+ ایجاد LLM sender بر اساس provider
242
+
243
+ Args:
244
+ provider: "chatgpt" یا "grok"
245
+ api_key: کلید API (اختیاری)
246
+ model: مدل (اختیاری، از پیش‌فرض استفاده می‌شود)
247
+
248
+ Returns:
249
+ instance از ChatGPTSender یا GrokSender
250
+ """
251
+ provider = provider.lower()
252
+
253
+ if provider == "chatgpt":
254
+ if model is None:
255
+ model = "gpt-4o-mini"
256
+ return ChatGPTSender(api_key=api_key, model=model)
257
+
258
+ elif provider == "grok":
259
+ if model is None:
260
+ model = "grok-beta"
261
+ return GrokSender(api_key=api_key, model=model)
262
+
263
+ else:
264
+ raise ValueError(f"Provider نامعتبر: {provider}. باید 'chatgpt' یا 'grok' باشد")
265
+
266
+
267
+ # مدل‌های موجود برای هر provider
268
+ AVAILABLE_MODELS = {
269
+ "chatgpt": [
270
+ "gpt-4o-mini",
271
+ "gpt-4o",
272
+ "gpt-4-turbo",
273
+ "gpt-3.5-turbo"
274
+ ],
275
+ "grok": [
276
+ "grok-beta", # رایگان در بتا
277
+ "grok-3-mini", # ارزان‌تر
278
+ "grok-3", # flagship
279
+ "grok-2-1212" # نسخه قدیمی‌تر
280
+ ]
281
+ }
282
+
283
+
284
+ if __name__ == "__main__":
285
+ print("=" * 60)
286
+ print("🤖 LLM Sender Unified - مثال استفاده")
287
+ print("=" * 60)
288
+
289
+ # مثال 1: ChatGPT
290
+ print("\n1️⃣ ChatGPT:")
291
+ chatgpt = create_llm_sender("chatgpt")
292
+ if chatgpt.api_key:
293
+ response = chatgpt.send_simple("سلام")
294
+ print(f"✅ پاسخ: {response[:100]}...")
295
+ else:
296
+ print("⚠️ کلید OpenAI API تنظیم نشده است!")
297
+
298
+ # مثال 2: Grok
299
+ print("\n2️⃣ Grok:")
300
+ grok = create_llm_sender("grok", model="grok-beta")
301
+ if grok.api_key:
302
+ response = grok.send_simple("سلام")
303
+ print(f"✅ پاسخ: {response[:100]}...")
304
+ else:
305
+ print("⚠️ کلید xAI API تنظیم نشده است!")
306
+
307
+ # مثال 3: لیست مدل‌ها
308
+ print("\n3️⃣ مدل‌های موجود:")
309
+ for provider, models in AVAILABLE_MODELS.items():
310
+ print(f"\n{provider.upper()}:")
311
+ for model in models:
312
+ print(f" - {model}")