KashefTech commited on
Commit
3ea8250
·
verified ·
1 Parent(s): 1ec9d9e

Upload 2 files

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