KashefTech commited on
Commit
b407d97
·
verified ·
1 Parent(s): ec7e65e

Upload 2 files

Browse files
Files changed (2) hide show
  1. app_fixed_v3.py +960 -0
  2. llm_sender_unified_fixed (3).py +334 -0
app_fixed_v3.py ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
9
+
10
+ # ✅ مدل‌های موجود - به‌روزرسانی نوامبر 2024
11
+ AVAILABLE_MODELS = {
12
+ "chatgpt": [
13
+ # GPT-5 Series (جدیدترین)
14
+ "gpt-5.1", # بهترین برای کدنویسی و وظایف agentic
15
+ "gpt-5", # مدل reasoning قبلی
16
+ # GPT-4 Series
17
+ "gpt-4.1", # هوشمندترین non-reasoning
18
+ "gpt-4o", # قدرتمند
19
+ "gpt-4o-mini", # سریع و ارزان
20
+ "gpt-4-turbo", # سریع‌تر از GPT-4
21
+ ],
22
+ "grok": [
23
+ # Grok-4 Series (جدیدترین)
24
+ "grok-4-fast-reasoning", # سریع با reasoning
25
+ "grok-4-fast-non-reasoning", # سریع بدون reasoning
26
+ "grok-4-0709", # نسخه پایدار
27
+ # Grok-3 Series
28
+ "grok-3", # قدرتمند
29
+ "grok-3-mini", # سبک
30
+ # Grok-2 Series
31
+ "grok-2-vision-1212", # با قابلیت بینایی
32
+ "grok-2-1212", # نسخه پایدار
33
+ "grok-2" # نسخه قدیمی
34
+ ]
35
+ }
36
+
37
+ logging.basicConfig(level=logging.INFO)
38
+ logger = logging.getLogger(__name__)
39
+
40
+ class AnonymizerAdvanced:
41
+ """ناشناس‌ساز پیشرفته با روش‌های متعدد"""
42
+
43
+ def __init__(
44
+ self,
45
+ cerebras_key: str = None,
46
+ llm_provider: str = "chatgpt",
47
+ llm_model: str = None,
48
+ entities_to_anonymize: List[str] = None
49
+ ):
50
+ self.cerebras_key = cerebras_key or os.getenv("CEREBRAS_API_KEY")
51
+ self.llm_provider = llm_provider
52
+ self.llm_model = llm_model
53
+ self.entities_to_anonymize = entities_to_anonymize or ["person", "company", "amount", "percent"]
54
+ self.mapping_table = {}
55
+ self.reverse_mapping = {}
56
+
57
+ # ایجاد LLM sender
58
+ self._create_llm_sender()
59
+
60
+ logger.info(f"✅ Anonymizer Advanced مقداردهی شد با {llm_provider}")
61
+
62
+ def _create_llm_sender(self):
63
+ """ایجاد LLM sender مناسب"""
64
+ try:
65
+ # ✅ همیشه از Hugging Face Secrets استفاده کن
66
+ if self.llm_provider == "chatgpt":
67
+ api_key = os.getenv("OPENAI_API_KEY")
68
+ logger.info("🔑 استفاده از OPENAI_API_KEY از Secrets")
69
+ elif self.llm_provider == "grok":
70
+ api_key = os.getenv("XAI_API_KEY")
71
+ logger.info("🔑 استفاده از XAI_API_KEY از Secrets")
72
+ else:
73
+ api_key = None
74
+ logger.warning("⚠️ Provider ناشناخته")
75
+
76
+ # ایجاد sender
77
+ self.llm_sender = create_llm_sender(
78
+ provider=self.llm_provider,
79
+ api_key=api_key,
80
+ model=self.llm_model
81
+ )
82
+
83
+ logger.info(f"✅ LLM Sender ایجاد شد: {self.llm_provider} - {self.llm_sender.model}")
84
+
85
+ except Exception as e:
86
+ logger.error(f"❌ خطا در ایجاد LLM Sender: {e}")
87
+ # fallback to ChatGPT
88
+ self.llm_sender = create_llm_sender("chatgpt")
89
+
90
+ def set_llm_provider(self, provider: str, model: str = None, entities: List[str] = None):
91
+ """تغییر provider و مدل LLM و موجودیت‌های ناشناس‌سازی"""
92
+ self.llm_provider = provider
93
+ self.llm_model = model
94
+ if entities is not None:
95
+ self.entities_to_anonymize = entities
96
+ self._create_llm_sender()
97
+ logger.info(f"✅ LLM تغییر یافت به: {provider} - {model}")
98
+ logger.info(f"✅ موجودیت‌های ناشناس‌سازی: {self.entities_to_anonymize}")
99
+
100
+ def anonymize_with_cerebras(self, text: str) -> Tuple[str, Dict]:
101
+ """ناشناس‌سازی با Cerebras - بر اساس موجودیت‌های انتخابی"""
102
+ logger.info("🧠 روش Cerebras...")
103
+
104
+ if not self.cerebras_key:
105
+ logger.error("❌ Cerebras API Key موجود نیست")
106
+ raise ValueError("Cerebras API Key مورد نیاز است")
107
+
108
+ # ✅ ساخت دستورات بر اساس موجودیت‌های انتخابی
109
+ instructions = []
110
+ instruction_number = 1
111
+
112
+ if "person" in self.entities_to_anonymize:
113
+ instructions.append(f"{instruction_number}. اسامی اشخاص → person-01, person-02, ...")
114
+ instruction_number += 1
115
+
116
+ if "company" in self.entities_to_anonymize:
117
+ instructions.append(f"{instruction_number}. نام شرکت‌ها/سازمان‌ها → company-01, company-02, ...")
118
+ instruction_number += 1
119
+
120
+ if "amount" in self.entities_to_anonymize:
121
+ instructions.append(f"{instruction_number}. مقادیر پولی → amount-01, amount-02, ...")
122
+ instruction_number += 1
123
+
124
+ if "percent" in self.entities_to_anonymize:
125
+ instructions.append(f"{instruction_number}. درصدها → percent-01, percent-02, ...")
126
+ instruction_number += 1
127
+
128
+ # اگه هیچی انتخاب نشده، متن رو همون‌طور برگردون
129
+ if not instructions:
130
+ logger.warning("⚠️ هیچ موجودیتی برای ناشناس‌سازی انتخاب نشده!")
131
+ return text, {}
132
+
133
+ instructions_text = "\n".join(instructions)
134
+ instructions_text += f"\n{instruction_number}. فقط این توکن‌ها استفاده کنید"
135
+ instructions_text += f"\n{instruction_number + 1}. شماره‌های نسخه را درست حفظ کنید"
136
+ instructions_text += f"\n{instruction_number + 2}. اگر موجودیت تکرار شود از شماره قدیمی استفاده کنید"
137
+
138
+ try:
139
+ # مرحله 1: ناشناس‌سازی متن
140
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
141
+ {instructions_text}
142
+
143
+ متن:
144
+ {text}
145
+
146
+ خروجی: فقط متن ناشناس شده"""
147
+
148
+ response1 = requests.post(
149
+ "https://api.cerebras.ai/v1/chat/completions",
150
+ headers={
151
+ "Authorization": f"Bearer {self.cerebras_key}",
152
+ "Content-Type": "application/json"
153
+ },
154
+ json={
155
+ "model": "llama-3.3-70b",
156
+ "messages": [{"role": "user", "content": prompt1}],
157
+ "max_tokens": 4096,
158
+ "temperature": 0.1
159
+ },
160
+ timeout=60
161
+ )
162
+
163
+ if response1.status_code != 200:
164
+ logger.error(f"❌ Cerebras Error: {response1.status_code}")
165
+ raise Exception(f"Cerebras API Error: {response1.status_code}")
166
+
167
+ anonymized_text = response1.json()['choices'][0]['message']['content'].strip()
168
+ logger.info("✅ Cerebras: ناشناس‌سازی موفق")
169
+
170
+ # مرحله 2: استخراج mapping - فقط برای موجودیت‌های انتخابی
171
+ mapping_instructions = []
172
+ json_example = "{\n"
173
+
174
+ if "person" in self.entities_to_anonymize:
175
+ mapping_instructions.append('- برای person-XX: نام کامل شخص (مثلاً "علی احمدی")')
176
+ json_example += ' "person-01": "متن اصلی کامل",\n'
177
+
178
+ if "company" in self.entities_to_anonymize:
179
+ mapping_instructions.append('- برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")')
180
+ json_example += ' "company-01": "متن اصلی کامل",\n'
181
+
182
+ if "amount" in self.entities_to_anonymize:
183
+ mapping_instructions.append('- برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")')
184
+ json_example += ' "amount-01": "متن اصلی کامل با واحد",\n'
185
+
186
+ if "percent" in self.entities_to_anonymize:
187
+ mapping_instructions.append('- برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")')
188
+ json_example += ' "percent-01": "عدد + درصد",\n'
189
+
190
+ json_example += " ...\n}"
191
+ mapping_instructions_text = "\n".join(mapping_instructions)
192
+
193
+ prompt2 = f"""متن اصلی:
194
+ {text}
195
+
196
+ متن ناشناس شده:
197
+ {anonymized_text}
198
+
199
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
200
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
201
+
202
+ **مهم:**
203
+ {mapping_instructions_text}
204
+
205
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
206
+ {json_example}"""
207
+
208
+ response2 = requests.post(
209
+ "https://api.cerebras.ai/v1/chat/completions",
210
+ headers={
211
+ "Authorization": f"Bearer {self.cerebras_key}",
212
+ "Content-Type": "application/json"
213
+ },
214
+ json={
215
+ "model": "llama-3.3-70b",
216
+ "messages": [{"role": "user", "content": prompt2}],
217
+ "max_tokens": 2048,
218
+ "temperature": 0.1
219
+ },
220
+ timeout=60
221
+ )
222
+
223
+ if response2.status_code == 200:
224
+ mapping_text = response2.json()['choices'][0]['message']['content'].strip()
225
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
226
+
227
+ try:
228
+ self.mapping_table = json.loads(mapping_text)
229
+ self._fix_percent_mapping()
230
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
231
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
232
+ except json.JSONDecodeError:
233
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
234
+ self._extract_mapping_from_text(text, anonymized_text)
235
+ else:
236
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
237
+ self._extract_mapping_from_text(text, anonymized_text)
238
+
239
+ return anonymized_text, self.mapping_table
240
+
241
+ except Exception as e:
242
+ logger.error(f"❌ Cerebras Exception: {e}")
243
+ raise
244
+
245
+ def _fix_percent_mapping(self):
246
+ """اصلاح mapping برای درصدها"""
247
+ for token, value in self.mapping_table.items():
248
+ value_str = str(value).strip()
249
+
250
+ if token.startswith('percent-'):
251
+ if not re.search(r'(درصد|%|درصدی)', value_str):
252
+ self.mapping_table[token] = f"{value_str} درصد"
253
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
254
+
255
+ elif token.startswith('amount-'):
256
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
257
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
258
+
259
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
260
+ """استخراج mapping از متن‌های اصلی و ناشناس شده - فقط برای موجودیت‌های انتخابی"""
261
+
262
+ # ✅ استخراج فقط توکن‌های انتخابی
263
+ all_tokens = []
264
+ for entity_type in self.entities_to_anonymize:
265
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
266
+ all_tokens.extend([(t, entity_type) for t in tokens])
267
+
268
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
269
+
270
+ # ✅ الگوهای موجودیت - فقط برای انتخابی‌ها
271
+ patterns = {}
272
+ if "person" in self.entities_to_anonymize:
273
+ patterns['person'] = r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b'
274
+ if "company" in self.entities_to_anonymize:
275
+ patterns['company'] = r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*'
276
+ if "amount" in self.entities_to_anonymize:
277
+ patterns['amount'] = r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)'
278
+ if "percent" in self.entities_to_anonymize:
279
+ patterns['percent'] = r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)'
280
+
281
+ original_entities = {}
282
+ for entity_type, pattern in patterns.items():
283
+ matches = list(re.finditer(pattern, original))
284
+ original_entities[entity_type] = [m.group().strip() for m in matches]
285
+
286
+ for token, entity_type in all_tokens:
287
+ if entity_type in original_entities and original_entities[entity_type]:
288
+ token_num = int(token.split('-')[1]) - 1
289
+
290
+ if token_num < len(original_entities[entity_type]):
291
+ original_text = original_entities[entity_type][token_num]
292
+ self.mapping_table[token] = original_text
293
+ self.reverse_mapping[original_text] = token
294
+ else:
295
+ original_text = original_entities[entity_type][-1]
296
+ if token not in self.mapping_table:
297
+ self.mapping_table[token] = original_text
298
+ self.reverse_mapping[original_text] = token
299
+
300
+ def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
301
+ """استفاده از LLM یکپارچه"""
302
+ logger.info(f"🤖 {self.llm_provider.upper()} اجرای پرامپت...")
303
+
304
+ if not analysis_prompt or not analysis_prompt.strip():
305
+ logger.info("⚠️ پرامپت خالی - بدون تحلیل")
306
+ return "⚠️ هیچ دستور تحلیل داده نشده است"
307
+
308
+ # ✅ ساخت پیام توجه بر اساس موجودیت‌های انتخاب‌شده
309
+ tokens_instruction = []
310
+ examples = []
311
+
312
+ if "person" in self.entities_to_anonymize:
313
+ tokens_instruction.append("person-XX")
314
+ examples.append("✅ صحیح: person-01 در جلسه حضور داشت\n❌ غلط: آقای person-01 یا شخص person-01")
315
+
316
+ if "company" in self.entities_to_anonymize:
317
+ tokens_instruction.append("company-XX")
318
+ examples.append("✅ صحیح: company-01 فعالیت کرد\n❌ غلط: شرکت company-01 یا سازمان company-01")
319
+
320
+ if "amount" in self.entities_to_anonymize:
321
+ tokens_instruction.append("amount-XX")
322
+ examples.append("✅ صحیح: فروش amount-01 بود\n❌ غلط: فروش مبلغ amount-01 یا فروش با amount-01")
323
+ examples.append("⚠️ بسیار مهم: فقط amount-01 بنویس، نه 'مبلغ amount-01' یا 'با amount-01' یا 'به amount-01'")
324
+
325
+ if "percent" in self.entities_to_anonymize:
326
+ tokens_instruction.append("percent-XX")
327
+ examples.append("✅ صحیح: رشد percent-01 داشت\n❌ غلط: رشد درصد percent-01")
328
+
329
+ tokens_str = ", ".join(tokens_instruction)
330
+ examples_str = "\n".join(examples)
331
+
332
+ # ✅ اضافه کردن هشدار ویژه برای amount
333
+ special_warning = ""
334
+ if "amount" in self.entities_to_anonymize:
335
+ special_warning = """
336
+ 🚨 هشدار ویژه برای amount-XX:
337
+ - NEVER write: "مبلغ amount-01" → ONLY write: "amount-01"
338
+ - NEVER write: "فروش به amount-01" → ONLY write: "فروش amount-01"
339
+ - NEVER write: "با amount-02" → ONLY write: "amount-02"
340
+ - NEVER add ANY word before or after amount tokens!
341
+ """
342
+
343
+ combined_text = f"""متن ناشناس‌سازی شده:
344
+ {anonymized_text}
345
+
346
+ دستورات:
347
+ {analysis_prompt}
348
+
349
+ ⚠️ قوانین مهم:
350
+ 1. فقط از کدهای ناشناس موجود استفاده کن: {tokens_str}
351
+ 2. هیچ کلمه‌ای قبل یا بعد از این کدها اضافه نکن
352
+ 3. کد جدید ایجاد نکن
353
+ 4. ساختار دقیق متن را حفظ کن
354
+ 5. فرمت دقیق را حفظ کن: amount-01 نه amount- 01 یا amount -01
355
+
356
+ {special_warning}
357
+
358
+ مثال‌های صحیح و غلط:
359
+ {examples_str}
360
+
361
+ هشدار: اگر کلمه‌ای مثل "شرکت"، "آقای"، "مبلغ"، "با"، "به" قبل از کدها بگذاری، پاسخ غلط است!"""
362
+
363
+ try:
364
+ # ✅ برای GPT-4 از temperature پایین‌تر استفاده کن
365
+ temp_to_use = 0.1 if self.llm_model and 'gpt-4' in self.llm_model.lower() else 0.2
366
+ logger.info(f"🌡️ Temperature: {temp_to_use} (مدل: {self.llm_model})")
367
+
368
+ # استفاده از متد send با temperature سفارشی
369
+ response = self.llm_sender.send(
370
+ combined_text,
371
+ lang='fa',
372
+ temperature=temp_to_use,
373
+ max_tokens=2000
374
+ )
375
+
376
+ # ✅ پاکسازی کلمات اضافی (لایه امنیتی دوم)
377
+ response = self._clean_llm_response(response)
378
+
379
+ logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
380
+ return response
381
+ except Exception as e:
382
+ logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
383
+ return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
384
+
385
+ def _clean_llm_response(self, text: str) -> str:
386
+ """پاکسازی کلمات اضافی که LLM ممکن است قبل از موجودیت‌ها اضافه کرده باشد"""
387
+ logger.info("🧹 پاکسازی کلمات اضافی...")
388
+
389
+ cleaned = text
390
+ changes_made = 0
391
+
392
+ # الگوهای کلمات اضافی برای هر نوع موجودیت
393
+ patterns = []
394
+
395
+ if "person" in self.entities_to_anonymize:
396
+ patterns.extend([
397
+ (r'(?:آقای|خانم|شخص)\s+(person-\d+)', r'\1'),
398
+ (r'(person-\d+)\s+(?:نامدار|محترم)', r'\1'),
399
+ ])
400
+
401
+ if "company" in self.entities_to_anonymize:
402
+ patterns.extend([
403
+ (r'(?:شرکت|سازمان|گروه|هلدینگ|بانک)\s+(company-\d+)', r'\1'),
404
+ (r'(company-\d+)\s+(?:محترم)', r'\1'),
405
+ ])
406
+
407
+ if "amount" in self.entities_to_anonymize:
408
+ patterns.extend([
409
+ # ✅ الگوهای بیشتر برای amount
410
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|فروش|درآمد|هزینه|سود|زیان)\s+(amount-\d+)', r'\1'),
411
+ (r'(amount-\d+)\s+(?:ریال|تومان|دلار|یورو|میلیون|میلیارد|هزار)', r'\1'),
412
+ # ✅ حذف "به" قبل از amount
413
+ (r'\bبه\s+(amount-\d+)', r'\1'),
414
+ # ✅ حذف "با" قبل از amount
415
+ (r'\bبا\s+(amount-\d+)', r'\1'),
416
+ # ✅ حذف "از" قبل از amount
417
+ (r'\bاز\s+(amount-\d+)', r'\1'),
418
+ # ✅ حذف "تا" قبل از amount
419
+ (r'\bتا\s+(amount-\d+)', r'\1'),
420
+ ])
421
+
422
+ if "percent" in self.entities_to_anonymize:
423
+ patterns.extend([
424
+ (r'(?:درصد|%)\s+(percent-\d+)', r'\1'),
425
+ (r'(percent-\d+)\s+(?:درصد|درصدی|%)', r'\1'),
426
+ ])
427
+
428
+ # اعمال الگوها
429
+ for pattern, replacement in patterns:
430
+ new_text = re.sub(pattern, replacement, cleaned)
431
+ if new_text != cleaned:
432
+ changes_made += re.subn(pattern, replacement, cleaned)[1]
433
+ cleaned = new_text
434
+
435
+ if changes_made > 0:
436
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
437
+ else:
438
+ logger.info("✅ کلمه اضافی یافت نشد")
439
+
440
+ return cleaned
441
+
442
+ def restore_text(self, anonymized_text: str) -> str:
443
+ """بازگردانی متن"""
444
+ logger.info("🔄 بازگردانی متن...")
445
+
446
+ if not self.mapping_table:
447
+ logger.warning("⚠️ جدول نگاشت خالی است")
448
+ return anonymized_text
449
+
450
+ logger.info(f"📋 تعداد موجودیت‌ها در mapping: {len(self.mapping_table)}")
451
+
452
+ # ✅ STEP 1: ابتدا normalize کن (فاصله‌ها را درست کن)
453
+ restored = self._normalize_tokens(anonymized_text)
454
+
455
+ # ✅ STEP 2: بعد clean کن (کلمات اضافی را حذف کن)
456
+ # این ترتیب مهمه چون بعد از normalize، clean بهتر کار می‌کنه
457
+ restored = self._clean_for_restore(restored)
458
+
459
+ replacements_count = 0
460
+
461
+ # جایگزینی با ترتیب از طولانی‌ترین به کوتاه‌ترین برای جلوگیری از جایگزینی اشتباه
462
+ for placeholder, original in sorted(self.mapping_table.items(), key=lambda x: len(x[0]), reverse=True):
463
+ if placeholder in restored:
464
+ restored = restored.replace(placeholder, original)
465
+ replacements_count += 1
466
+ logger.info(f"✅ {placeholder} → {original[:30]}...")
467
+ else:
468
+ logger.warning(f"⚠️ {placeholder} در متن یافت نشد!")
469
+
470
+ logger.info(f"✅ بازگردانی کامل - {replacements_count}/{len(self.mapping_table)} جایگزین شد")
471
+
472
+ # ✅ اگر amount ها جایگزین نشدن، سعی کن با regex پیداشون کنی
473
+ if replacements_count < len(self.mapping_table):
474
+ logger.info("🔍 تلاش برای یافتن توکن‌های گم‌شده با regex...")
475
+ restored = self._restore_with_regex(restored)
476
+
477
+ # اگر هیچ جایگزینی انجام نشد، احتمالاً مشکل وجود دارد
478
+ if replacements_count == 0 and len(self.mapping_table) > 0:
479
+ logger.error("❌ هیچ توکنی جایگزین نشد! متن ورودی احتمالاً متفاوت است.")
480
+
481
+ return restored
482
+
483
+ def _clean_for_restore(self, text: str) -> str:
484
+ """پاکسازی خاص برای بازگردانی (شبیه _clean_llm_response اما سبک‌تر)"""
485
+ logger.info("🧹 پاکسازی قبل از بازگردانی...")
486
+
487
+ cleaned = text
488
+ changes_made = 0
489
+
490
+ patterns = []
491
+
492
+ if "amount" in self.entities_to_anonymize:
493
+ patterns.extend([
494
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|فروش|درآمد|هزینه|سود|زیان)\s+(amount-\d+)', r'\1'),
495
+ (r'\bبه\s+(amount-\d+)', r'\1'),
496
+ (r'\bبا\s+(amount-\d+)', r'\1'),
497
+ (r'\bاز\s+(amount-\d+)', r'\1'),
498
+ (r'\bتا\s+(amount-\d+)', r'\1'),
499
+ ])
500
+
501
+ for pattern, replacement in patterns:
502
+ new_text = re.sub(pattern, replacement, cleaned)
503
+ if new_text != cleaned:
504
+ changes_made += re.subn(pattern, replacement, cleaned)[1]
505
+ cleaned = new_text
506
+
507
+ if changes_made > 0:
508
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
509
+
510
+ return cleaned
511
+
512
+ def _restore_with_regex(self, text: str) -> str:
513
+ """بازگردانی با استفاده از regex برای پیدا کردن توکن‌های دارای کلمات اضافی"""
514
+ restored = text
515
+
516
+ for placeholder, original in self.mapping_table.items():
517
+ # اگر قبلاً جایگزین شده، رد شو
518
+ if placeholder not in text:
519
+ # الگوی regex: کلمه اضافی (اختیاری) + توکن
520
+ # مثلاً: "فروش amount-01" یا "مبلغ amount-05"
521
+ entity_type = placeholder.split('-')[0]
522
+ entity_num = placeholder.split('-')[1]
523
+
524
+ # الگوهای مختلف
525
+ patterns = [
526
+ # کلمه فارسی + فاصله + توکن
527
+ rf'[ء-ي]+\s+({entity_type}-{entity_num})\b',
528
+ # توکن + فاصله + کلمه فارسی
529
+ rf'\b({entity_type}-{entity_num})\s+[ء-ي]+',
530
+ # فاصله اضافی داخل توکن
531
+ rf'\b{entity_type}\s+-\s+{entity_num}\b',
532
+ ]
533
+
534
+ for pattern in patterns:
535
+ matches = list(re.finditer(pattern, restored))
536
+ if matches:
537
+ logger.info(f"✅ پیدا شد با regex: {pattern}")
538
+ for match in matches:
539
+ # جایگزینی کل عبارت با فقط original
540
+ full_match = match.group(0)
541
+ # اگر توکن داخل match هست، فقط اون رو جایگزین کن
542
+ if placeholder in full_match:
543
+ restored = restored.replace(full_match, full_match.replace(placeholder, original))
544
+ else:
545
+ # اگر فرمت توکن متفاوت بود
546
+ restored = restored.replace(full_match, original)
547
+ logger.info(f"✅ regex: {placeholder} → {original[:30]}...")
548
+ break
549
+
550
+ return restored
551
+
552
+ def _normalize_tokens(self, text: str) -> str:
553
+ """نرمال‌سازی توکن‌ها - حذف فاصله‌های اضافی"""
554
+ logger.info("🧹 نرمال‌سازی توکن‌ها...")
555
+
556
+ normalized = text
557
+ changes = 0
558
+
559
+ # الگوهای فاصله اضافی در توکن‌ها
560
+ patterns = [
561
+ # حذف فاصله‌های اضافی داخل توکن
562
+ (r'(person|company|amount|percent)\s*-\s*(\d+)', r'\1-\2'),
563
+ # حذف فاصله‌های اضافی بعد از توکن
564
+ (r'(person|company|amount|percent)-(\d+)\s+(?=[^\d])', r'\1-\2 '),
565
+ ]
566
+
567
+ for pattern, replacement in patterns:
568
+ new_text = re.sub(pattern, replacement, normalized)
569
+ if new_text != normalized:
570
+ changes += re.subn(pattern, replacement, normalized)[1]
571
+ normalized = new_text
572
+
573
+ if changes > 0:
574
+ logger.info(f"✅ {changes} فاصله اضافی حذف شد")
575
+
576
+ return normalized
577
+
578
+ def get_mapping_table_md(self) -> str:
579
+ """تبدیل جدول نگاشت به Markdown"""
580
+ if not self.mapping_table:
581
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
582
+
583
+ table = "### 📋 جدول نگاشت\n\n"
584
+ table += "| شناسه | متن اصلی |\n"
585
+ table += "|-------|----------|\n"
586
+
587
+ for token, original in sorted(self.mapping_table.items()):
588
+ table += f"| **{token}** | {original} |\n"
589
+
590
+ return table
591
+
592
+ # متغیر سراسری
593
+ anonymizer = None
594
+
595
+ def process(
596
+ input_text: str,
597
+ analysis_prompt: str,
598
+ llm_provider: str,
599
+ llm_model: str,
600
+ anonymize_all: bool,
601
+ anonymize_person: bool,
602
+ anonymize_company: bool,
603
+ anonymize_amount: bool,
604
+ anonymize_percent: bool
605
+ ):
606
+ """پردازش متن - 4 مرحله"""
607
+ global anonymizer
608
+
609
+ if not input_text.strip():
610
+ return "", "", "", ""
611
+
612
+ # ✅ ساخت لیست موجودیت‌های انتخابی
613
+ if anonymize_all:
614
+ entities = ["person", "company", "amount", "percent"]
615
+ else:
616
+ entities = []
617
+ if anonymize_person:
618
+ entities.append("person")
619
+ if anonymize_company:
620
+ entities.append("company")
621
+ if anonymize_amount:
622
+ entities.append("amount")
623
+ if anonymize_percent:
624
+ entities.append("percent")
625
+
626
+ # اگه هیچی انتخاب نشده
627
+ if not entities:
628
+ return "", "❌ لطفاً حداقل یک موجودیت برای ناشناس‌سازی انتخاب کنید", "", ""
629
+
630
+ cerebras_key = os.getenv("CEREBRAS_API_KEY")
631
+
632
+ # ایجاد یا آپدیت anonymizer
633
+ if not anonymizer:
634
+ anonymizer = AnonymizerAdvanced(
635
+ cerebras_key,
636
+ llm_provider=llm_provider,
637
+ llm_model=llm_model,
638
+ entities_to_anonymize=entities
639
+ )
640
+ else:
641
+ anonymizer.set_llm_provider(llm_provider, llm_model, entities)
642
+ anonymizer.mapping_table = {}
643
+ anonymizer.reverse_mapping = {}
644
+
645
+ try:
646
+ logger.info("=" * 70)
647
+ logger.info(f"🚀 شروع پردازش - LLM: {llm_provider} ({llm_model})")
648
+ logger.info(f"🎯 موجودیت‌های انتخابی: {', '.join(entities)}")
649
+ logger.info("=" * 70)
650
+
651
+ # مرحله 1: ناشناس‌سازی
652
+ logger.info("🔐 مرحله 1: ناشناس‌سازی...")
653
+ anonymized_text, _ = anonymizer.anonymize_with_cerebras(input_text)
654
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
655
+
656
+ # مرحله 2: LLM (فقط اگر analysis_prompt داده شده باشد)
657
+ has_analysis = analysis_prompt and analysis_prompt.strip()
658
+
659
+ if has_analysis:
660
+ logger.info(f"🤖 مرحله 2: {llm_provider.upper()}...")
661
+ llm_response = anonymizer.analyze_with_llm(anonymized_text, analysis_prompt)
662
+ logger.info(f"✅ {llm_provider.upper()}: {len(llm_response)} کاراکتر")
663
+ else:
664
+ logger.info("⚠️ مرحله 2: بدون تحلیل LLM (پرامپت خالی)")
665
+ llm_response = "⚠️ هیچ دستور تحلیل داده نشده است"
666
+
667
+ # مرحله 3: بازگردانی
668
+ logger.info("🔄 مرحله 3: بازگردانی...")
669
+
670
+ # ✅ اصلاح: اگر تحلیل انجام نشده، متن ناشناس اصلی رو restore کن
671
+ if has_analysis:
672
+ # اگر LLM تحلیل کرده، خروجی LLM رو restore کن
673
+ restored_text = anonymizer.restore_text(llm_response)
674
+ else:
675
+ # اگر تحلیل نشده، متن ناشناس اصلی رو restore کن
676
+ restored_text = anonymizer.restore_text(anonymized_text)
677
+
678
+ logger.info("✅ بازگردانی کامل")
679
+
680
+ # مرحله 4: جدول نگاشت
681
+ logger.info("📋 مرحله 4: جدول نگاشت...")
682
+ mapping_str = anonymizer.get_mapping_table_md()
683
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
684
+
685
+ logger.info("=" * 70)
686
+ logger.info("✅ تمام مراحل کامل!")
687
+ logger.info("=" * 70)
688
+
689
+ return restored_text, llm_response, anonymized_text, mapping_str
690
+
691
+ except Exception as e:
692
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
693
+ return "", f"❌ خطا: {str(e)}", "", ""
694
+
695
+ def clear_all():
696
+ """پاک کردن همه"""
697
+ return "", "", "", "", "", "", True, False, False, False, False
698
+
699
+ # Gradio Interface
700
+ css_rtl = """
701
+ .input-box {
702
+ direction: rtl;
703
+ text-align: right;
704
+ }
705
+ .textbox textarea {
706
+ direction: rtl;
707
+ text-align: right;
708
+ font-family: 'Tahoma', serif;
709
+ }
710
+ .thick-divider {
711
+ border-top: 2px solid #333;
712
+ margin: 10px 0;
713
+ }
714
+ .compact-group {
715
+ margin: 0;
716
+ padding: 0;
717
+ }
718
+ .compact-checkbox label {
719
+ padding: 5px 10px !important;
720
+ margin: 3px 0 !important;
721
+ font-size: 0.95em !important;
722
+ }
723
+ """
724
+
725
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
726
+
727
+ gr.Markdown("# 🔐 پلتفرم امن چت با مدل‌های متنوع و ناشناس‌سازی داده‌ها", elem_classes="input-box")
728
+
729
+ # ردیف اول: تنظیمات مدل و انتخاب موجودیت‌ها
730
+ with gr.Row():
731
+ # سمت راست: تنظیمات مدل
732
+ with gr.Column(scale=1):
733
+ with gr.Group():
734
+ gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
735
+
736
+ llm_provider = gr.Dropdown(
737
+ choices=["chatgpt", "grok"],
738
+ value="chatgpt",
739
+ label="🤖 انتخاب مدل زبانی",
740
+ interactive=True
741
+ )
742
+
743
+ llm_model = gr.Dropdown(
744
+ choices=AVAILABLE_MODELS["chatgpt"],
745
+ value="gpt-4o-mini",
746
+ label="📦 انتخاب نسخه مدل",
747
+ interactive=True
748
+ )
749
+
750
+ # سمت چپ: انتخاب موجودیت‌ها
751
+ with gr.Column(scale=1):
752
+ with gr.Group():
753
+ gr.Markdown("### 🎯 انتخاب موجودیت‌ها", elem_classes="input-box")
754
+
755
+ anonymize_all = gr.Checkbox(
756
+ label="✅ همه موجودیت‌ها",
757
+ value=True,
758
+ elem_classes="input-box compact-checkbox"
759
+ )
760
+
761
+ anonymize_person = gr.Checkbox(
762
+ label="👤 اسامی اشخاص",
763
+ value=False,
764
+ elem_classes="input-box compact-checkbox"
765
+ )
766
+
767
+ anonymize_company = gr.Checkbox(
768
+ label="🏢 نام شرکت‌ها",
769
+ value=False,
770
+ elem_classes="input-box compact-checkbox"
771
+ )
772
+
773
+ anonymize_amount = gr.Checkbox(
774
+ label="💰 ارقام مالی",
775
+ value=False,
776
+ elem_classes="input-box compact-checkbox"
777
+ )
778
+
779
+ anonymize_percent = gr.Checkbox(
780
+ label="📊 درصدها",
781
+ value=False,
782
+ elem_classes="input-box compact-checkbox"
783
+ )
784
+
785
+ # خط جداکننده پررنگ
786
+ gr.Markdown("---", elem_classes="thick-divider")
787
+
788
+ # ردیف دوم: دستورات پردازش و متن ورودی
789
+ with gr.Row():
790
+ # سمت راست: دستورات پردازش
791
+ with gr.Column(scale=1):
792
+ gr.Markdown("### 📋 دستورات پردازش", elem_classes="input-box")
793
+
794
+ analysis_prompt = gr.Textbox(
795
+ lines=22,
796
+ placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
797
+ label="📋 دستورات LLM (اختیاری)",
798
+ elem_classes="textbox"
799
+ )
800
+
801
+ # سمت چپ: متن ورودی
802
+ with gr.Column(scale=1):
803
+ gr.Markdown("### 📝 متن ورودی", elem_classes="input-box")
804
+
805
+ input_text = gr.Textbox(
806
+ lines=22,
807
+ placeholder="متن مالی/خبری را وارد کنید...",
808
+ label="",
809
+ elem_classes="textbox"
810
+ )
811
+
812
+ # دکمه‌های پردازش و پاک کردن
813
+ with gr.Row():
814
+ process_btn = gr.Button(
815
+ "▶️ پردازش",
816
+ variant="primary",
817
+ size="lg",
818
+ scale=2
819
+ )
820
+
821
+ clear_btn = gr.Button(
822
+ "🗑️ پاک کردن",
823
+ variant="stop",
824
+ size="lg",
825
+ scale=1
826
+ )
827
+
828
+ # نتایج
829
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
830
+
831
+ with gr.Row():
832
+ with gr.Column(scale=1):
833
+ restored_text = gr.Textbox(
834
+ lines=12,
835
+ label="✅ متن بازگردانی شده",
836
+ interactive=False,
837
+ elem_classes="textbox"
838
+ )
839
+
840
+ with gr.Column(scale=1):
841
+ llm_analysis = gr.Textbox(
842
+ lines=12,
843
+ label="🤖 تحلیل LLM",
844
+ interactive=False,
845
+ elem_classes="textbox"
846
+ )
847
+
848
+ with gr.Column(scale=1):
849
+ anonymized_text = gr.Textbox(
850
+ lines=12,
851
+ label="🔒 متن ناشناس‌شده",
852
+ interactive=False,
853
+ elem_classes="textbox"
854
+ )
855
+
856
+ mapping_table = gr.Markdown(
857
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
858
+ label="📋 جدول نگاشت",
859
+ elem_classes="input-box"
860
+ )
861
+
862
+
863
+ # Event Handler برای تغییر provider
864
+ def handle_provider_change(provider):
865
+ models = AVAILABLE_MODELS.get(provider, [])
866
+ default_model = models[0] if models else None
867
+ return gr.update(choices=models, value=default_model)
868
+
869
+ llm_provider.change(
870
+ fn=handle_provider_change,
871
+ inputs=[llm_provider],
872
+ outputs=[llm_model]
873
+ )
874
+
875
+ def handle_select_all(select_all):
876
+ if select_all:
877
+ return (
878
+ gr.update(value=False, interactive=False),
879
+ gr.update(value=False, interactive=False),
880
+ gr.update(value=False, interactive=False),
881
+ gr.update(value=False, interactive=False)
882
+ )
883
+ else:
884
+ return (
885
+ gr.update(value=False, interactive=True),
886
+ gr.update(value=False, interactive=True),
887
+ gr.update(value=False, interactive=True),
888
+ gr.update(value=False, interactive=True)
889
+ )
890
+
891
+ anonymize_all.change(
892
+ fn=handle_select_all,
893
+ inputs=[anonymize_all],
894
+ outputs=[anonymize_person, anonymize_company, anonymize_amount, anonymize_percent]
895
+ )
896
+
897
+ # پردازش
898
+ process_btn.click(
899
+ fn=process,
900
+ inputs=[
901
+ input_text,
902
+ analysis_prompt,
903
+ llm_provider,
904
+ llm_model,
905
+ anonymize_all,
906
+ anonymize_person,
907
+ anonymize_company,
908
+ anonymize_amount,
909
+ anonymize_percent
910
+ ],
911
+ outputs=[restored_text, llm_analysis, anonymized_text, mapping_table]
912
+ )
913
+
914
+ # پاک کردن
915
+ clear_btn.click(
916
+ fn=clear_all,
917
+ outputs=[
918
+ input_text,
919
+ analysis_prompt,
920
+ restored_text,
921
+ llm_analysis,
922
+ anonymized_text,
923
+ mapping_table,
924
+ anonymize_all,
925
+ anonymize_person,
926
+ anonymize_company,
927
+ anonymize_amount,
928
+ anonymize_percent
929
+ ]
930
+ )
931
+
932
+ if __name__ == "__main__":
933
+ print("=" * 70)
934
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
935
+ print("=" * 70)
936
+ print("\n📋 نحوه استفاده:\n")
937
+ print("1. API Keyها را در Hugging Face Secrets تنظیم کنید:")
938
+ print(" - CEREBRAS_API_KEY (ضروری برای ناشناس‌سازی)")
939
+ print(" - OPENAI_API_KEY (برای ChatGPT)")
940
+ print(" - XAI_API_KEY (برای Grok)")
941
+ print("2. http://localhost:7860 را باز کنید")
942
+ print("3. مدل زبانی (ChatGPT/Grok) و نسخه مدل را انتخاب کنید")
943
+ print("4. موجودیت‌های مورد نظر برای ناشناس‌سازی را انتخاب کنید")
944
+ print("5. متن و دستورات پردازش را وارد کنید")
945
+ print("6. 'پردازش' را کلیک کنید\n")
946
+ print("🔐 تمام API Keyها از Hugging Face Secrets خوانده می‌شوند")
947
+ print("📦 مدل‌های پشتیبانی شده:")
948
+ print(" • ChatGPT GPT-5: gpt-5.1, gpt-5")
949
+ print(" • ChatGPT GPT-4: gpt-4.1, gpt-4o, gpt-4o-mini, gpt-4-turbo")
950
+ print(" • Grok-4: grok-4-fast-reasoning, grok-4-fast-non-reasoning, grok-4-0709")
951
+ print(" • Grok-3: grok-3, grok-3-mini")
952
+ print(" • Grok-2: grok-2-vision-1212, grok-2-1212, grok-2")
953
+ print("=" * 70 + "\n")
954
+
955
+ app.launch(
956
+ server_name="0.0.0.0",
957
+ server_port=7860,
958
+ share=False,
959
+ show_error=True
960
+ )
llm_sender_unified_fixed (3).py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🤖 LLM Sender Unified Module
3
+ ماژول یکپارچه برای ارسال به ChatGPT و Grok
4
+ ✨ با پشتیبانی از GPT-5 models و رفع مشکل temperature
5
+ """
6
+
7
+ import requests
8
+ import os
9
+ import logging
10
+ from typing import Optional
11
+ import time
12
+ from abc import ABC, abstractmethod
13
+
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LLMSender(ABC):
19
+ """کلاس پایه برای ارسال به مدل‌های مختلف LLM"""
20
+
21
+ def __init__(self, api_key: Optional[str] = None, model: str = None):
22
+ self.api_key = api_key
23
+ self.model = model
24
+ self.base_url = ""
25
+
26
+ @abstractmethod
27
+ def get_default_model(self) -> str:
28
+ """مدل پیش‌فرض"""
29
+ pass
30
+
31
+ @abstractmethod
32
+ def get_base_url(self) -> str:
33
+ """URL پایه API"""
34
+ pass
35
+
36
+ def set_api_key(self, api_key: str):
37
+ """تنظیم کلید API"""
38
+ self.api_key = api_key
39
+ logger.info("✅ کلید API تنظیم شد")
40
+
41
+ def set_model(self, model: str):
42
+ """تغییر مدل"""
43
+ self.model = model
44
+ 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-5
50
+ 'gpt-5.1' # GPT-5.1
51
+ ]
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-5
58
+ 'o1' # تمام مدل‌های O1
59
+ ]
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 = 3
83
+ ) -> 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
+ # تهیه headers
108
+ 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.0
116
+ if temperature != 1.0:
117
+ logger.info(f"⚠️ مدل {self.model} فقط temperature=1 را قبول می‌کند")
118
+ else:
119
+ actual_temperature = temperature
120
+
121
+ # ساخت request body
122
+ data = {
123
+ "model": self.model,
124
+ "messages": [
125
+ {"role": "system", "content": system_msg},
126
+ {"role": "user", "content": text}
127
+ ],
128
+ "temperature": actual_temperature
129
+ }
130
+
131
+ # ✨ انتخاب پارامتر مناسب برای max tokens
132
+ if self._uses_max_completion_tokens():
133
+ data["max_completion_tokens"] = max_tokens
134
+ else:
135
+ data["max_tokens"] = max_tokens
136
+
137
+ # ارسال با retry mechanism
138
+ 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=timeout
147
+ )
148
+
149
+ # پردازش پاسخ موفق
150
+ if response.status_code == 200:
151
+ result = response.json()
152
+ llm_response = result['choices'][0]['message']['content']
153
+ logger.info("✅ پاسخ دریافت شد")
154
+ return llm_response
155
+
156
+ # پردازش خطاهای مختلف
157
+ elif response.status_code == 429: # Rate limiting
158
+ wait_time = 5 * (attempt + 1)
159
+ logger.warning(f"⚠️ Rate limit | صبر: {wait_time} ثانیه")
160
+ if attempt < retry_count - 1:
161
+ time.sleep(wait_time)
162
+ continue
163
+ else:
164
+ return (
165
+ "❌ سهمیه API تمام شده است. لطفاً بعداً تلاش کنید."
166
+ if lang == 'fa'
167
+ else "❌ API quota exceeded. Please try later."
168
+ )
169
+
170
+ elif response.status_code == 401:
171
+ return (
172
+ "❌ کلید API نامعتبر است!"
173
+ if lang == 'fa'
174
+ else "❌ Invalid API key!"
175
+ )
176
+
177
+ elif response.status_code in [502, 503, 504]: # Server errors
178
+ wait_time = 2 * (attempt + 1)
179
+ logger.warning(f"⚠️ Server error {response.status_code} | صبر: {wait_time} ثانیه")
180
+ if attempt < retry_count - 1:
181
+ time.sleep(wait_time)
182
+ continue
183
+ else:
184
+ return (
185
+ f"❌ خطای سرور: {response.status_code}"
186
+ if lang == 'fa'
187
+ else f"❌ Server error: {response.status_code}"
188
+ )
189
+
190
+ else:
191
+ # خطای دیگر
192
+ try:
193
+ error_data = response.json() if response.content else {}
194
+ if isinstance(error_data, dict):
195
+ error_msg = error_data.get('error', {}).get('message', response.text)
196
+ else:
197
+ error_msg = str(error_data)
198
+ except:
199
+ error_msg = response.text[:200]
200
+
201
+ logger.error(f"❌ API Error: {error_msg}")
202
+ return f"❌ API Error: {error_msg}"
203
+
204
+ except requests.exceptions.Timeout:
205
+ logger.warning("⚠️ Timeout | صبر: 3 ثانیه و تلاش مجدد")
206
+ if attempt < retry_count - 1:
207
+ time.sleep(3)
208
+ continue
209
+ else:
210
+ return (
211
+ "❌ خطای اتصال: timeout"
212
+ if lang == 'fa'
213
+ else "❌ Connection error: timeout"
214
+ )
215
+
216
+ except requests.exceptions.ConnectionError as e:
217
+ logger.warning("⚠️ Connection error | صبر: 2 ثانیه و تلاش مجدد")
218
+ if attempt < retry_count - 1:
219
+ time.sleep(2)
220
+ continue
221
+ else:
222
+ return (
223
+ f"❌ خطای اتصال: {str(e)}"
224
+ if lang == 'fa'
225
+ else f"❌ Connection error: {str(e)}"
226
+ )
227
+
228
+ except Exception as e:
229
+ logger.error(f"❌ خطای غیرمنتظره: {str(e)}")
230
+ return (
231
+ f"❌ خطا در ارتباط با LLM: {str(e)}"
232
+ if lang == 'fa'
233
+ else f"❌ Error connecting to LLM: {str(e)}"
234
+ )
235
+
236
+
237
+ class ChatGPTSender(LLMSender):
238
+ """کلاس برای ارسال به ChatGPT"""
239
+
240
+ def __init__(self, api_key: Optional[str] = None, model: str = "gpt-4o-mini"):
241
+ raw_key = api_key or os.getenv("OPENAI_API_KEY", "")
242
+ cleaned_key = raw_key.strip() if raw_key else ""
243
+
244
+ super().__init__(cleaned_key, model)
245
+ self.base_url = self.get_base_url()
246
+
247
+ if not self.api_key:
248
+ logger.warning("⚠️ کلید OpenAI API تنظیم نشده است!")
249
+
250
+ def get_default_model(self) -> str:
251
+ return "gpt-4o-mini"
252
+
253
+ def get_base_url(self) -> str:
254
+ return "https://api.openai.com/v1/chat/completions"
255
+
256
+
257
+ class GrokSender(LLMSender):
258
+ """کلاس برای ارسال به Grok (xAI)"""
259
+
260
+ def __init__(self, api_key: Optional[str] = None, model: str = "grok-beta"):
261
+ raw_key = api_key or os.getenv("XAI_API_KEY", "")
262
+ cleaned_key = raw_key.strip() if raw_key else ""
263
+
264
+ super().__init__(cleaned_key, model)
265
+ self.base_url = self.get_base_url()
266
+
267
+ if not self.api_key:
268
+ logger.warning("⚠️ کلید xAI API تنظیم نشده است!")
269
+
270
+ def get_default_model(self) -> str:
271
+ return "grok-beta"
272
+
273
+ def get_base_url(self) -> str:
274
+ return "https://api.x.ai/v1/chat/completions"
275
+
276
+
277
+ def create_llm_sender(
278
+ provider: str = "chatgpt",
279
+ api_key: Optional[str] = None,
280
+ model: Optional[str] = None
281
+ ) -> LLMSender:
282
+ """ایجاد LLM sender بر اساس provider"""
283
+ provider = provider.lower()
284
+
285
+ if provider == "chatgpt":
286
+ if model is None:
287
+ model = "gpt-4o-mini"
288
+ return ChatGPTSender(api_key=api_key, model=model)
289
+
290
+ elif provider == "grok":
291
+ if model is None:
292
+ model = "grok-beta"
293
+ return GrokSender(api_key=api_key, model=model)
294
+
295
+ else:
296
+ raise ValueError(f"Provider نامعتبر: {provider}")
297
+
298
+
299
+ # ✅ مدل‌های موجود (اصلاح شده)
300
+ AVAILABLE_MODELS = {
301
+ "chatgpt": [
302
+ "gpt-5.1", # ✅ بهترین GPT-5
303
+ "gpt-5", # ✅ GPT-5 پایه
304
+ "gpt-4.1",
305
+ "gpt-4o-mini",
306
+ "gpt-4o",
307
+ "gpt-4-turbo",
308
+ "gpt-3.5-turbo"
309
+ ],
310
+ "grok": [
311
+ "grok-3-mini",
312
+ "grok-3",
313
+ "grok-2-1212"
314
+ ]
315
+ }
316
+
317
+
318
+ if __name__ == "__main__":
319
+ print("=" * 60)
320
+ print("🤖 LLM Sender - نسخه اصلاح شده")
321
+ print("✨ رفع مشکل temperature برای GPT-5")
322
+ print("=" * 60)
323
+
324
+ # تست
325
+ print("\n🧪 تست مدل‌ها:")
326
+ test_models = ['gpt-5', 'gpt-5.1', 'gpt-4o']
327
+ for model in test_models:
328
+ sender = create_llm_sender("chatgpt", model=model)
329
+ uses_completion = sender._uses_max_completion_tokens()
330
+ requires_default_temp = sender._requires_default_temperature()
331
+
332
+ print(f"\n مدل: {model}")
333
+ print(f" • max_tokens: {'max_completion_tokens' if uses_completion else 'max_tokens'}")
334
+ print(f" • temperature: {'1.0 (default only)' if requires_default_temp else '0.7 (custom)'}")