KashefTech commited on
Commit
b9f96bb
·
verified ·
1 Parent(s): f0fc0eb

Upload 2 files

Browse files
Files changed (2) hide show
  1. app (3).py +1174 -0
  2. llm_sender_unified (2).py +383 -0
app (3).py ADDED
@@ -0,0 +1,1174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ✅ System prompt برای DeepInfra/Qwen3 - جلوگیری از thinking mode
10
+ DEEPINFRA_SYSTEM_PROMPT = """You are a Persian text anonymizer. Your ONLY job is to replace sensitive entities with EXACT placeholders.
11
+
12
+ PLACEHOLDER FORMAT (mandatory - no other format allowed):
13
+ - Company names → company-01, company-02, company-03, ...
14
+ - Person names → person-01, person-02, person-03, ...
15
+ - Money amounts → amount-01, amount-02, amount-03, ...
16
+ - Percentages → percent-01, percent-02, percent-03, ...
17
+
18
+ STRICT RULES:
19
+ 1. Use ONLY these formats. NEVER use [Company A], [Person 1], (company), etc.
20
+ 2. Replace same entity with same placeholder every time
21
+ 3. Keep all other words exactly as-is
22
+ 4. Output ONLY the anonymized text or JSON - no explanation, no thinking
23
+ """
24
+
25
+ def strip_thinking(text: str) -> str:
26
+ """✅ حذف بلوک‌های <think>...</think> که Qwen3 تولید می‌کند"""
27
+ if not text:
28
+ return text
29
+ cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
30
+ return cleaned.strip()
31
+
32
+
33
+
34
+ # ✅ مدل‌های موجود - به‌روزرسانی نوامبر 2024
35
+ AVAILABLE_MODELS = {
36
+ "chatgpt": [
37
+ "gpt-5.1",
38
+ "gpt-5",
39
+ "gpt-4.1",
40
+ "gpt-4o",
41
+ "gpt-4o-mini",
42
+ "gpt-4-turbo",
43
+ ],
44
+ "grok": [
45
+ "grok-4-0709",
46
+ "grok-3",
47
+ "grok-3-mini",
48
+ "grok-2-1212",
49
+ ],
50
+ "deepinfra": [
51
+ "Qwen/Qwen3-14B",
52
+ "Qwen/Qwen3-32B",
53
+ "Qwen/Qwen3-30B-A3B",
54
+ "Qwen/Qwen2.5-72B-Instruct",
55
+ "Qwen/Qwen2.5-14B-Instruct",
56
+ ]
57
+ }
58
+
59
+ logging.basicConfig(level=logging.INFO)
60
+ logger = logging.getLogger(__name__)
61
+
62
+ class AnonymizerAdvanced:
63
+ """ناشناس‌ساز پیشرفته با روش‌های متعدد"""
64
+
65
+ def __init__(
66
+ self,
67
+ deepinfra_key: str = None,
68
+ llm_provider: str = "chatgpt",
69
+ llm_model: str = None,
70
+ entities_to_anonymize: List[str] = None
71
+ ):
72
+ self.deepinfra_key = deepinfra_key or os.getenv("DEEPINFRA_API_KEY")
73
+ self.llm_provider = llm_provider
74
+ self.llm_model = llm_model
75
+ self.entities_to_anonymize = entities_to_anonymize or ["person", "company", "amount", "percent"]
76
+ self.mapping_table = {}
77
+ self.reverse_mapping = {}
78
+
79
+ # ایجاد LLM sender
80
+ self._create_llm_sender()
81
+
82
+ logger.info(f"✅ Anonymizer Advanced مقداردهی شد با {llm_provider}")
83
+
84
+ def _create_llm_sender(self):
85
+ """ایجاد LLM sender مناسب"""
86
+ try:
87
+ # ✅ همیشه از Hugging Face Secrets استفاده کن
88
+ if self.llm_provider == "chatgpt":
89
+ api_key = os.getenv("OPENAI_API_KEY")
90
+ logger.info("🔑 استفاده از OPENAI_API_KEY از Secrets")
91
+ elif self.llm_provider == "grok":
92
+ api_key = os.getenv("XAI_API_KEY")
93
+ logger.info("🔑 استفاده از XAI_API_KEY از Secrets")
94
+ elif self.llm_provider == "deepinfra":
95
+ api_key = os.getenv("DEEPINFRA_API_KEY")
96
+ logger.info("🔑 استفاده از DEEPINFRA_API_KEY از Secrets")
97
+ else:
98
+ api_key = None
99
+ logger.warning("⚠️ Provider ناشناخته")
100
+
101
+ # ایجاد sender
102
+ self.llm_sender = create_llm_sender(
103
+ provider=self.llm_provider,
104
+ api_key=api_key,
105
+ model=self.llm_model
106
+ )
107
+
108
+ logger.info(f"✅ LLM Sender ایجاد شد: {self.llm_provider} - {self.llm_sender.model}")
109
+
110
+ except Exception as e:
111
+ logger.error(f"❌ خطا در ایجاد LLM Sender: {e}")
112
+ # fallback to ChatGPT
113
+ self.llm_sender = create_llm_sender("chatgpt")
114
+
115
+ def set_llm_provider(self, provider: str, model: str = None, entities: List[str] = None):
116
+ """تغییر provider و مدل LLM و موجودیت‌های ناشناس‌سازی"""
117
+ self.llm_provider = provider
118
+ self.llm_model = model
119
+ if entities is not None:
120
+ self.entities_to_anonymize = entities
121
+ self._create_llm_sender()
122
+ logger.info(f"✅ LLM تغییر یافت به: {provider} - {model}")
123
+ logger.info(f"✅ موجودیت‌های ناشناس‌سازی: {self.entities_to_anonymize}")
124
+
125
+ def anonymize_with_deepinfra(self, text: str) -> Tuple[str, Dict]:
126
+ """ناشناس‌سازی با DeepInfra (Qwen3-14B) - بر اساس موجودیت‌های انتخابی"""
127
+ logger.info("🧠 روش DeepInfra (Qwen3-14B)...")
128
+
129
+ if not self.deepinfra_key:
130
+ logger.error("❌ DeepInfra API Key موجود نیست")
131
+ raise ValueError("DeepInfra API Key مورد نیاز است")
132
+
133
+ # ✅ ساخت دستورات بر اساس موجودیت‌های انتخابی
134
+ instructions = []
135
+ instruction_number = 1
136
+
137
+ if "person" in self.entities_to_anonymize:
138
+ instructions.append(f"{instruction_number}. اسامی اشخاص → person-01, person-02, ...")
139
+ instruction_number += 1
140
+
141
+ if "company" in self.entities_to_anonymize:
142
+ instructions.append(f"{instruction_number}. نام شرکت‌ها/سازمان‌ها → company-01, company-02, ...")
143
+ instruction_number += 1
144
+
145
+ if "amount" in self.entities_to_anonymize:
146
+ instructions.append(f"{instruction_number}. اعداد و ارقام و مبالغ (مثل: 50 میلیارد، 100 هزار، 25.5 میلیون، ۳۰۰ دستگاه) → amount-01, amount-02, ...")
147
+ instruction_number += 1
148
+
149
+ if "percent" in self.entities_to_anonymize:
150
+ instructions.append(f"{instruction_number}. درصدها → percent-01, percent-02, ...")
151
+ instruction_number += 1
152
+
153
+ # اگه هیچی انتخاب نشده، متن رو همون‌طور برگردون
154
+ if not instructions:
155
+ logger.warning("⚠️ هیچ موجودیتی برای ناشناس‌سازی انتخاب نشده!")
156
+ return text, {}
157
+
158
+ instructions_text = "\n".join(instructions)
159
+ instructions_text += f"\n{instruction_number}. فقط این توکن‌ها استفاده کنید"
160
+ instructions_text += f"\n{instruction_number + 1}. شماره‌های نسخه را درست حفظ کنید"
161
+ instructions_text += f"\n{instruction_number + 2}. اگر موجودیت تکرار شود از شماره قدیمی استفاده کنید"
162
+
163
+ try:
164
+ # مرحله 1: ناشناس‌سازی متن
165
+ # ✅ ساخت مثال برای amount (اگر انتخاب شده)
166
+ example_text = ""
167
+ if "amount" in self.entities_to_anonymize:
168
+ example_text = """
169
+ مثال:
170
+ متن اصلی: "فروش 50 میلیارد ریال در سال گذشته بود."
171
+ متن ناشناس: "فروش amount-01 در سال گذشته بود."
172
+ """
173
+
174
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
175
+ {instructions_text}
176
+ {example_text}
177
+ متن:
178
+ {text}
179
+
180
+ خروجی: فقط متن ناشناس شده (بدون توضیح اضافی)"""
181
+
182
+ response1 = requests.post(
183
+ "https://api.deepinfra.com/v1/openai/chat/completions",
184
+ headers={
185
+ "Authorization": f"Bearer {self.deepinfra_key}",
186
+ "Content-Type": "application/json"
187
+ },
188
+ json={
189
+ "model": "Qwen/Qwen3-14B",
190
+ "messages": [
191
+ {"role": "system", "content": DEEPINFRA_SYSTEM_PROMPT},
192
+ {"role": "user", "content": prompt1}
193
+ ],
194
+ "max_tokens": 4096,
195
+ "temperature": 0.1
196
+ },
197
+ timeout=60
198
+ )
199
+
200
+ if response1.status_code != 200:
201
+ logger.error(f"❌ DeepInfra Error: {response1.status_code}")
202
+ raise Exception(f"DeepInfra API Error: {response1.status_code}")
203
+
204
+ # ✅ حذف بلوک‌های thinking که Qwen3 تولید می‌کند
205
+ anonymized_text = strip_thinking(response1.json()['choices'][0]['message']['content'])
206
+ logger.info("✅ DeepInfra: ناشناس‌سازی موفق")
207
+
208
+ # مرحله 2: استخراج mapping - فقط برای موجودیت‌های انتخابی
209
+ mapping_instructions = []
210
+ json_example = "{\n"
211
+
212
+ if "person" in self.entities_to_anonymize:
213
+ mapping_instructions.append('- برای person-XX: نام کامل شخص (مثلاً "علی احمدی")')
214
+ json_example += ' "person-01": "متن اصلی کامل",\n'
215
+
216
+ if "company" in self.entities_to_anonymize:
217
+ mapping_instructions.append('- برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")')
218
+ json_example += ' "company-01": "متن اصلی کامل",\n'
219
+
220
+ if "amount" in self.entities_to_anonymize:
221
+ mapping_instructions.append('- برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")')
222
+ json_example += ' "amount-01": "متن اصلی کامل با واحد",\n'
223
+
224
+ if "percent" in self.entities_to_anonymize:
225
+ mapping_instructions.append('- برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")')
226
+ json_example += ' "percent-01": "عدد + درصد",\n'
227
+
228
+ json_example += " ...\n}"
229
+ mapping_instructions_text = "\n".join(mapping_instructions)
230
+
231
+ prompt2 = f"""متن اصلی:
232
+ {text}
233
+
234
+ متن ناشناس شده:
235
+ {anonymized_text}
236
+
237
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
238
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
239
+
240
+ **مهم:**
241
+ {mapping_instructions_text}
242
+
243
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
244
+ {json_example}"""
245
+
246
+ response2 = requests.post(
247
+ "https://api.deepinfra.com/v1/openai/chat/completions",
248
+ headers={
249
+ "Authorization": f"Bearer {self.deepinfra_key}",
250
+ "Content-Type": "application/json"
251
+ },
252
+ json={
253
+ "model": "Qwen/Qwen3-14B",
254
+ "messages": [
255
+ {"role": "system", "content": DEEPINFRA_SYSTEM_PROMPT},
256
+ {"role": "user", "content": prompt2}
257
+ ],
258
+ "max_tokens": 2048,
259
+ "temperature": 0.1
260
+ },
261
+ timeout=60
262
+ )
263
+
264
+ if response2.status_code == 200:
265
+ # ✅ حذف بلوک‌های thinking + پاکسازی
266
+ mapping_text = strip_thinking(response2.json()['choices'][0]['message']['content'])
267
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
268
+
269
+ try:
270
+ self.mapping_table = json.loads(mapping_text)
271
+ self._fix_percent_mapping()
272
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
273
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
274
+ except json.JSONDecodeError:
275
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
276
+ self._extract_mapping_from_text(text, anonymized_text)
277
+ else:
278
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
279
+ self._extract_mapping_from_text(text, anonymized_text)
280
+
281
+ return anonymized_text, self.mapping_table
282
+
283
+ except Exception as e:
284
+ logger.error(f"❌ DeepInfra Exception: {e}")
285
+ raise
286
+
287
+ def _fix_percent_mapping(self):
288
+ """اصلاح mapping برای درصدها"""
289
+ for token, value in self.mapping_table.items():
290
+ value_str = str(value).strip()
291
+
292
+ if token.startswith('percent-'):
293
+ if not re.search(r'(درصد|%|درصدی)', value_str):
294
+ self.mapping_table[token] = f"{value_str} درصد"
295
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
296
+
297
+ elif token.startswith('amount-'):
298
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
299
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
300
+
301
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
302
+ """استخراج mapping از متن‌های اصلی و ناشناس شده - فقط برای موجودیت‌های انتخابی"""
303
+
304
+ # ✅ استخراج فقط توکن‌های انتخابی
305
+ all_tokens = []
306
+ for entity_type in self.entities_to_anonymize:
307
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
308
+ all_tokens.extend([(t, entity_type) for t in tokens])
309
+
310
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
311
+
312
+ # ✅ الگوهای موجودیت - فقط برای انتخابی‌ها
313
+ patterns = {}
314
+ if "person" in self.entities_to_anonymize:
315
+ patterns['person'] = r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b'
316
+ if "company" in self.entities_to_anonymize:
317
+ patterns['company'] = r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*'
318
+ if "amount" in self.entities_to_anonymize:
319
+ patterns['amount'] = r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)'
320
+ if "percent" in self.entities_to_anonymize:
321
+ patterns['percent'] = r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)'
322
+
323
+ original_entities = {}
324
+ for entity_type, pattern in patterns.items():
325
+ matches = list(re.finditer(pattern, original))
326
+ original_entities[entity_type] = [m.group().strip() for m in matches]
327
+
328
+ for token, entity_type in all_tokens:
329
+ if entity_type in original_entities and original_entities[entity_type]:
330
+ token_num = int(token.split('-')[1]) - 1
331
+
332
+ if token_num < len(original_entities[entity_type]):
333
+ original_text = original_entities[entity_type][token_num]
334
+ self.mapping_table[token] = original_text
335
+ self.reverse_mapping[original_text] = token
336
+ else:
337
+ original_text = original_entities[entity_type][-1]
338
+ if token not in self.mapping_table:
339
+ self.mapping_table[token] = original_text
340
+ self.reverse_mapping[original_text] = token
341
+
342
+ def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
343
+ """استفاده از LLM یکپارچه"""
344
+ logger.info(f"🤖 {self.llm_provider.upper()} اجرای پرامپت...")
345
+
346
+ if not analysis_prompt or not analysis_prompt.strip():
347
+ logger.info("⚠️ پرامپت خالی - بدون تحلیل")
348
+ return "⚠️ هیچ دستور تحلیل داده نشده است"
349
+
350
+ # ✅ بررسی اینکه آیا مدل GPT-4 است
351
+ is_gpt4 = self.llm_model and any(x in self.llm_model.lower() for x in ['gpt-4', 'gpt4'])
352
+
353
+ if is_gpt4:
354
+ # ✅ پرامپت ویژه GPT-4 با مثال‌های واقعی
355
+ logger.info("🎯 استفاده از پرامپت ویژه GPT-4")
356
+ return self._analyze_with_gpt4_prompt(anonymized_text, analysis_prompt)
357
+ else:
358
+ # پرامپت عادی برای GPT-5 و Grok
359
+ return self._analyze_with_standard_prompt(anonymized_text, analysis_prompt)
360
+
361
+ def _analyze_with_gpt4_prompt(self, anonymized_text: str, analysis_prompt: str) -> str:
362
+ """پرامپت ویژه GPT-4 با few-shot examples"""
363
+
364
+ # ✅ مثال‌های واقعی Few-Shot
365
+ few_shot_examples = """
366
+ EXAMPLE 1 - CORRECT:
367
+ Input: "company-01 فروش amount-01 داشت"
368
+ Your output should be EXACTLY: "company-01 فروش amount-01 داشت"
369
+ NOT: "company-01 فروش مبلغ amount-01 داشت"
370
+
371
+ EXAMPLE 2 - CORRECT:
372
+ Input: "amount-02 به amount-03 رسید"
373
+ Your output should be EXACTLY: "amount-02 به amount-03 رسید"
374
+ NOT: "مبلغ amount-02 به amount-03 رسید"
375
+
376
+ EXAMPLE 3 - CORRECT:
377
+ Input: "company-01 سود percent-01 داشت"
378
+ Your output should be EXACTLY: "company-01 سود percent-01 داشت"
379
+ NOT: "شرکت company-01 سود درصد percent-01 داشت"
380
+ """
381
+
382
+ # لیست توکن‌های انتخابی
383
+ tokens_list = []
384
+ if "person" in self.entities_to_anonymize:
385
+ tokens_list.append("person-XX")
386
+ if "company" in self.entities_to_anonymize:
387
+ tokens_list.append("company-XX")
388
+ if "amount" in self.entities_to_anonymize:
389
+ tokens_list.append("amount-XX")
390
+ if "percent" in self.entities_to_anonymize:
391
+ tokens_list.append("percent-XX")
392
+
393
+ tokens_str = ", ".join(tokens_list)
394
+
395
+ # ✅ پرامپت انگلیسی برای GPT-4 (بهتر کار می‌کند)
396
+ combined_text = f"""You are processing anonymized Persian/Farsi text containing placeholder tokens.
397
+
398
+ ANONYMIZED TEXT:
399
+ {anonymized_text}
400
+
401
+ USER REQUEST:
402
+ {analysis_prompt}
403
+
404
+ CRITICAL RULES:
405
+ 1. Use ONLY these exact tokens: {tokens_str}
406
+ 2. NEVER add words before/after tokens
407
+ 3. Keep the EXACT format: amount-01 (not "مبلغ amount-01" or "amount- 01")
408
+ 4. Do NOT create new tokens
409
+ 5. Preserve the exact structure
410
+
411
+ {few_shot_examples}
412
+
413
+ FORBIDDEN PATTERNS - NEVER USE:
414
+ ❌ "مبلغ amount-01" → ✅ Use: "amount-01"
415
+ ❌ "شرکت company-01" → ✅ Use: "company-01"
416
+ ❌ "فروش به amount-02" → ✅ Use: "فروش amount-02"
417
+ ❌ "درصد percent-01" → ✅ Use: "percent-01"
418
+ ❌ "amount- 01" (space) → ✅ Use: "amount-01"
419
+
420
+ Now process the text following these rules EXACTLY."""
421
+
422
+ try:
423
+ # ✅ temperature خیلی پایین برای GPT-4
424
+ logger.info(f"🌡️ Temperature: 0.05 (GPT-4 ویژه)")
425
+
426
+ response = self.llm_sender.send(
427
+ combined_text,
428
+ lang='en', # انگلیسی برای GPT-4
429
+ temperature=0.05, # خیلی خیلی پایین
430
+ max_tokens=2000
431
+ )
432
+
433
+ # ✅ دیباگ: نمایش خروجی خام LLM
434
+ logger.info("=" * 60)
435
+ logger.info("🔍 DEBUG - خروجی خام GPT-4:")
436
+ logger.info(response[:500] + "..." if len(response) > 500 else response)
437
+ logger.info("=" * 60)
438
+
439
+ # ✅ پاکسازی قوی‌تر
440
+ cleaned_response = self._clean_llm_response(response)
441
+
442
+ # ✅ دیباگ: نمایش خروجی بعد از clean
443
+ logger.info("=" * 60)
444
+ logger.info("🧹 DEBUG - خروجی بعد از clean:")
445
+ logger.info(cleaned_response[:500] + "..." if len(cleaned_response) > 500 else cleaned_response)
446
+ logger.info("=" * 60)
447
+
448
+ logger.info(f"✅ GPT-4: {len(cleaned_response)} کاراکتر")
449
+ return cleaned_response
450
+
451
+ except Exception as e:
452
+ logger.error(f"❌ GPT-4 Exception: {e}")
453
+ return f"❌ خطا در ارتباط با GPT-4: {str(e)}"
454
+
455
+ def _analyze_with_standard_prompt(self, anonymized_text: str, analysis_prompt: str) -> str:
456
+ """پرامپت استاندارد برای GPT-5 و Grok"""
457
+
458
+ tokens_instruction = []
459
+ examples = []
460
+
461
+ if "person" in self.entities_to_anonymize:
462
+ tokens_instruction.append("person-XX")
463
+ examples.append("✅ صحیح: person-01 در جلسه حضور داشت\n❌ غلط: آقای person-01")
464
+
465
+ if "company" in self.entities_to_anonymize:
466
+ tokens_instruction.append("company-XX")
467
+ examples.append("✅ صحیح: company-01 فعالیت کرد\n❌ غلط: شرکت company-01")
468
+
469
+ if "amount" in self.entities_to_anonymize:
470
+ tokens_instruction.append("amount-XX")
471
+ examples.append("✅ صحیح: فروش amount-01 بود\n❌ غلط: فروش مبلغ amount-01")
472
+
473
+ if "percent" in self.entities_to_anonymize:
474
+ tokens_instruction.append("percent-XX")
475
+ examples.append("✅ صحیح: رشد percent-01 داشت\n❌ غلط: رشد درصد percent-01")
476
+
477
+ tokens_str = ", ".join(tokens_instruction)
478
+ examples_str = "\n".join(examples)
479
+
480
+ combined_text = f"""متن ناشناس‌سازی شده:
481
+ {anonymized_text}
482
+
483
+ دستورات:
484
+ {analysis_prompt}
485
+
486
+ ⚠️ قوانین مهم:
487
+ 1. فقط از کدهای ناشناس موجود استفاده کن: {tokens_str}
488
+ 2. هیچ کلمه‌ای قبل یا بعد از این کدها اضافه نکن
489
+ 3. کد جدید ایجاد نکن
490
+ 4. ساختار دقیق متن را حفظ کن
491
+
492
+ مثال‌های صحیح و غلط:
493
+ {examples_str}"""
494
+
495
+ try:
496
+ temp_to_use = 0.2
497
+ logger.info(f"🌡️ Temperature: {temp_to_use}")
498
+
499
+ response = self.llm_sender.send(
500
+ combined_text,
501
+ lang='fa',
502
+ temperature=temp_to_use,
503
+ max_tokens=2000
504
+ )
505
+
506
+ response = self._clean_llm_response(response)
507
+
508
+ logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
509
+ return response
510
+
511
+ except Exception as e:
512
+ logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
513
+ return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
514
+
515
+ def _clean_llm_response(self, text: str) -> str:
516
+ """پاکسازی کلمات اضافی که LLM ممکن است قبل از موجودیت‌ها اضافه کرده باشد"""
517
+ logger.info("🧹 پاکسازی کلمات اضافی...")
518
+
519
+ cleaned = text
520
+ changes_made = 0
521
+
522
+ # الگوهای کلمات اضافی برای هر نوع موجودیت
523
+ patterns = []
524
+
525
+ if "person" in self.entities_to_anonymize:
526
+ patterns.extend([
527
+ (r'(?:آقای|خانم|شخص|فرد)\s+(person-\d+)', r'\1'),
528
+ (r'(person-\d+)\s+(?:نامدار|محترم|عزیز)', r'\1'),
529
+ ])
530
+
531
+ if "company" in self.entities_to_anonymize:
532
+ patterns.extend([
533
+ (r'(?:شرکت|سازمان|گروه|هلدینگ|بانک|موسسه)\s+(company-\d+)', r'\1'),
534
+ (r'(company-\d+)\s+(?:محترم)', r'\1'),
535
+ ])
536
+
537
+ if "amount" in self.entities_to_anonymize:
538
+ patterns.extend([
539
+ # ✅ الگوهای کامل برای amount - تمام حالات ممکن
540
+ # حالت 1: کلمات قبل از amount
541
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|مقدار)\s+(amount-\d+)', r'\1'),
542
+ (r'(?:فروش|درآمد|سود|زیان|هزینه|خرج)\s+(amount-\d+)', r'\1'),
543
+ (r'(?:دارایی|بدهی|سرمایه|پول|وام)\s+(amount-\d+)', r'\1'),
544
+
545
+ # حالت 2: حروف اضافه قبل از amount
546
+ (r'\bبه\s+(amount-\d+)', r'\1'),
547
+ (r'\bبا\s+(amount-\d+)', r'\1'),
548
+ (r'\bاز\s+(amount-\d+)', r'\1'),
549
+ (r'\bتا\s+(amount-\d+)', r'\1'),
550
+ (r'\bدر\s+(amount-\d+)', r'\1'),
551
+ (r'\bبرای\s+(amount-\d+)', r'\1'),
552
+
553
+ # حالت 3: واحدها بعد از amount (اگر نباید باشند)
554
+ (r'(amount-\d+)\s+(?:ریال|تومان|دلار|یورو)', r'\1'),
555
+ (r'(amount-\d+)\s+(?:میلیون|میلیارد|هزار|تریلیون)', r'\1'),
556
+
557
+ # حالت 4: ترکیبات
558
+ (r'(?:به\s+مبلغ)\s+(amount-\d+)', r'\1'),
559
+ (r'(?:با\s+ارزش)\s+(amount-\d+)', r'\1'),
560
+ (r'(?:در\s+حد)\s+(amount-\d+)', r'\1'),
561
+
562
+ # حالت 5: فعل + amount (بدون حرف اضافه)
563
+ (r'(?:رسید|رسیده|می\u200cرسد)\s+(amount-\d+)', r'\1'),
564
+ (r'(?:شد|شده|می\u200cشود)\s+(amount-\d+)', r'\1'),
565
+ (r'(?:بود|بوده|است)\s+(amount-\d+)', r'\1'),
566
+ ])
567
+
568
+ if "percent" in self.entities_to_anonymize:
569
+ patterns.extend([
570
+ (r'(?:درصد|%)\s+(percent-\d+)', r'\1'),
571
+ (r'(percent-\d+)\s+(?:درصد|درصدی|%)', r'\1'),
572
+ ])
573
+
574
+ # اعمال الگوها
575
+ for pattern, replacement in patterns:
576
+ new_text = re.sub(pattern, replacement, cleaned)
577
+ if new_text != cleaned:
578
+ count = len(re.findall(pattern, cleaned))
579
+ changes_made += count
580
+ cleaned = new_text
581
+ logger.info(f" ✅ حذف '{pattern}': {count} مورد")
582
+
583
+ if changes_made > 0:
584
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
585
+ else:
586
+ logger.info("✅ کلمه اضافی یافت نشد")
587
+
588
+ return cleaned
589
+
590
+ def restore_text(self, anonymized_text: str) -> str:
591
+ """بازگردانی متن با ترتیب بهینه برای amount"""
592
+ logger.info("🔄 بازگردانی متن...")
593
+
594
+ if not self.mapping_table:
595
+ logger.warning("⚠️ جدول نگاشت خالی است")
596
+ return anonymized_text
597
+
598
+ logger.info(f"📋 تعداد موجودیت‌ها در mapping: {len(self.mapping_table)}")
599
+
600
+ # ✅ STEP 1: normalize (hyphen یونیکد و جداسازی کلمات چسبیده)
601
+ restored = self._normalize_tokens(anonymized_text)
602
+
603
+ # ✅ STEP 2: restore قوی مخصوص amount با regex (قبل از clean!)
604
+ # این کلیدی است - باید قبل از clean انجام شود
605
+ logger.info("🔥 بازگردانی amount با regex...")
606
+ amount_restored_count = 0
607
+ for placeholder, original in self.mapping_table.items():
608
+ if placeholder.startswith("amount-"):
609
+ # استخراج شماره
610
+ num = placeholder.split("-")[1]
611
+ # الگوی regex: amount [فاصله اختیاری] - [فاصله اختیاری] شماره
612
+ pattern = rf'amount\s*-\s*{num}'
613
+ matches = re.findall(pattern, restored)
614
+ if matches:
615
+ restored = re.sub(pattern, original, restored)
616
+ amount_restored_count += 1
617
+ logger.info(f"✅ regex: {placeholder} → {original[:30]}...")
618
+
619
+ if amount_restored_count > 0:
620
+ logger.info(f"✅ {amount_restored_count} amount با regex بازگردانی شد")
621
+
622
+ # ✅ STEP 3: clean (حذف کلمات اضافی)
623
+ # حالا که amount ها restore شدن، می‌تونیم clean کنیم
624
+ restored = self._clean_for_restore(restored)
625
+
626
+ # ✅ STEP 4: replace ساده برای بقیه (person, company, percent)
627
+ replacements_count = 0
628
+ for placeholder, original in sorted(self.mapping_table.items(), key=lambda x: len(x[0]), reverse=True):
629
+ # amount ها رو قبلاً restore کردیم
630
+ if placeholder.startswith("amount-"):
631
+ continue
632
+
633
+ if placeholder in restored:
634
+ restored = restored.replace(placeholder, original)
635
+ replacements_count += 1
636
+ logger.info(f"✅ {placeholder} → {original[:30]}...")
637
+ else:
638
+ logger.warning(f"⚠️ {placeholder} در متن یافت نشد!")
639
+
640
+ total_restored = amount_restored_count + replacements_count
641
+ logger.info(f"✅ بازگردانی کامل - {total_restored}/{len(self.mapping_table)} جایگزین شد")
642
+
643
+ # ✅ STEP 5: fallback regex برای توکن‌های باقی‌مانده
644
+ if total_restored < len(self.mapping_table):
645
+ logger.info("🔍 تلاش برای یافتن توکن‌های گم‌شده با regex...")
646
+ restored = self._restore_with_regex(restored)
647
+
648
+ # هشدار در صورت شکست کامل
649
+ if total_restored == 0 and len(self.mapping_table) > 0:
650
+ logger.error("❌ هیچ توکنی جایگزین ��شد! متن ورودی احتمالاً متفاوت است.")
651
+
652
+ return restored
653
+
654
+ def _clean_for_restore(self, text: str) -> str:
655
+ """پاکسازی خاص برای بازگردانی (شبیه _clean_llm_response اما سبک‌تر)"""
656
+ logger.info("🧹 پاکسازی قبل از بازگردانی...")
657
+
658
+ cleaned = text
659
+ changes_made = 0
660
+
661
+ patterns = []
662
+
663
+ if "amount" in self.entities_to_anonymize:
664
+ patterns.extend([
665
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|فروش|درآمد|هزینه|سود|زیان)\s+(amount-\d+)', r'\1'),
666
+ (r'\bبه\s+(amount-\d+)', r'\1'),
667
+ (r'\bبا\s+(amount-\d+)', r'\1'),
668
+ (r'\bاز\s+(amount-\d+)', r'\1'),
669
+ (r'\bتا\s+(amount-\d+)', r'\1'),
670
+ ])
671
+
672
+ for pattern, replacement in patterns:
673
+ new_text = re.sub(pattern, replacement, cleaned)
674
+ if new_text != cleaned:
675
+ changes_made += re.subn(pattern, replacement, cleaned)[1]
676
+ cleaned = new_text
677
+
678
+ if changes_made > 0:
679
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
680
+
681
+ return cleaned
682
+
683
+ def _restore_with_regex(self, text: str) -> str:
684
+ """بازگردانی با استفاده از regex برای پیدا کردن توکن‌های دارای کلمات اضافی"""
685
+ restored = text
686
+
687
+ for placeholder, original in self.mapping_table.items():
688
+ # اگر قبلاً جایگزین شده، رد شو
689
+ if placeholder not in text:
690
+ # الگوی regex: کلمه اضافی (اختیاری) + توکن
691
+ # مثلاً: "فروش amount-01" یا "مبلغ amount-05"
692
+ entity_type = placeholder.split('-')[0]
693
+ entity_num = placeholder.split('-')[1]
694
+
695
+ # الگوهای مختلف
696
+ patterns = [
697
+ # کلمه فارسی + فاصله + توکن
698
+ rf'[ء-ي]+\s+({entity_type}-{entity_num})\b',
699
+ # توکن + فاصله + کلمه فارسی
700
+ rf'\b({entity_type}-{entity_num})\s+[ء-ي]+',
701
+ # فاصله اضافی داخل توکن
702
+ rf'\b{entity_type}\s+-\s+{entity_num}\b',
703
+ ]
704
+
705
+ for pattern in patterns:
706
+ matches = list(re.finditer(pattern, restored))
707
+ if matches:
708
+ logger.info(f"✅ پیدا شد با regex: {pattern}")
709
+ for match in matches:
710
+ # جایگزینی کل عبارت با فقط original
711
+ full_match = match.group(0)
712
+ # اگر توکن داخل match هست، فقط اون رو جایگزین کن
713
+ if placeholder in full_match:
714
+ restored = restored.replace(full_match, full_match.replace(placeholder, original))
715
+ else:
716
+ # اگر فرمت توکن متفاوت بود
717
+ restored = restored.replace(full_match, original)
718
+ logger.info(f"✅ regex: {placeholder} → {original[:30]}...")
719
+ break
720
+
721
+ return restored
722
+
723
+ def _normalize_tokens(self, text: str) -> str:
724
+ """نرمال‌سازی توکن‌ها - حذف فاصله‌های اضافی و hyphen یونیکد"""
725
+ logger.info("🧹 نرمال‌سازی توکن‌ها...")
726
+
727
+ normalized = text
728
+ changes = 0
729
+
730
+ # ✅ 1. نرمال‌سازی hyphen های یونیکد برای همه موجودیت‌ها
731
+ # این hyphen ها: ‐ ‑ ‒ – — − و hyphen معمولی -
732
+ unicode_hyphens = r'[\u2010\u2011\u2012\u2013\u2014\u2212\-]'
733
+
734
+ for entity_type in self.entities_to_anonymize:
735
+ # تبدیل همه hyphen ها به - معمولی
736
+ pattern = rf'{entity_type}{unicode_hyphens}(\d+)'
737
+ replacement = rf'{entity_type}-\1'
738
+ count = len(re.findall(pattern, normalized))
739
+ if count > 0:
740
+ normalized = re.sub(pattern, replacement, normalized)
741
+ changes += count
742
+ logger.info(f" ✅ {entity_type}: {count} hyphen یونیکد نرمال شد")
743
+
744
+ # ✅ 2. حذف فاضله‌های اضافی داخل توکن
745
+ for entity_type in self.entities_to_anonymize:
746
+ pattern = rf'{entity_type}\s+-\s+(\d+)'
747
+ replacement = rf'{entity_type}-\1'
748
+ count = len(re.findall(pattern, normalized))
749
+ if count > 0:
750
+ normalized = re.sub(pattern, replacement, normalized)
751
+ changes += count
752
+ logger.info(f" ✅ {entity_type}: {count} فاصله اضافی حذف شد")
753
+
754
+ # ✅ 3. جدا کردن توکن‌ها از کلمات فارسی چسبیده (ویژه amount)
755
+ # مثال: amount-01در → amount-01 در
756
+ if "amount" in self.entities_to_anonymize:
757
+ pattern = r'(amount-\d+)([ء-ي])'
758
+ replacement = r'\1 \2'
759
+ before = normalized
760
+ normalized = re.sub(pattern, replacement, normalized)
761
+ if normalized != before:
762
+ count = len(re.findall(pattern, before))
763
+ changes += count
764
+ logger.info(f" ✅ amount: {count} کلمه چسبیده جدا شد")
765
+
766
+ # ✅ 4. جدا کردن توکن‌ها از نشانه‌گذاری (ویژه amount)
767
+ # مثال: amount-01، → amount-01 ،
768
+ if "amount" in self.entities_to_anonymize:
769
+ pattern = r'(amount-\d+)([،؛:.!?])'
770
+ replacement = r'\1 \2'
771
+ before = normalized
772
+ normalized = re.sub(pattern, replacement, normalized)
773
+ if normalized != before:
774
+ count = len(re.findall(pattern, before))
775
+ changes += count
776
+ logger.info(f" ✅ amount: {count} نشانه‌گذاری جدا شد")
777
+
778
+ if changes > 0:
779
+ logger.info(f"✅ مجموع {changes} تغییر نرمال‌سازی")
780
+
781
+ return normalized
782
+
783
+ def get_mapping_table_md(self) -> str:
784
+ """تبدیل جدول نگاشت به Markdown"""
785
+ if not self.mapping_table:
786
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
787
+
788
+ table = "### 📋 جدول نگاشت\n\n"
789
+ table += "| شناسه | متن اصلی |\n"
790
+ table += "|-------|----------|\n"
791
+
792
+ for token, original in sorted(self.mapping_table.items()):
793
+ table += f"| **{token}** | {original} |\n"
794
+
795
+ return table
796
+
797
+ # متغیر سراسری
798
+ anonymizer = None
799
+
800
+ def process(
801
+ input_text: str,
802
+ analysis_prompt: str,
803
+ llm_provider: str,
804
+ llm_model: str,
805
+ anonymize_all: bool,
806
+ anonymize_person: bool,
807
+ anonymize_company: bool,
808
+ anonymize_amount: bool,
809
+ anonymize_percent: bool
810
+ ):
811
+ """پردازش متن - 4 مرحله"""
812
+ global anonymizer
813
+
814
+ if not input_text.strip():
815
+ return "", "", "", ""
816
+
817
+ # ✅ ساخت لیست موجودیت‌های انتخابی
818
+ if anonymize_all:
819
+ entities = ["person", "company", "amount", "percent"]
820
+ else:
821
+ entities = []
822
+ if anonymize_person:
823
+ entities.append("person")
824
+ if anonymize_company:
825
+ entities.append("company")
826
+ if anonymize_amount:
827
+ entities.append("amount")
828
+ if anonymize_percent:
829
+ entities.append("percent")
830
+
831
+ # اگه هیچی انتخاب نشده
832
+ if not entities:
833
+ return "", "❌ لطفاً حداقل یک موجودیت برای ناشناس‌سازی انتخاب کنید", "", ""
834
+
835
+ deepinfra_key = os.getenv("DEEPINFRA_API_KEY")
836
+
837
+ # ایجاد یا آپدیت anonymizer
838
+ if not anonymizer:
839
+ anonymizer = AnonymizerAdvanced(
840
+ deepinfra_key,
841
+ llm_provider=llm_provider,
842
+ llm_model=llm_model,
843
+ entities_to_anonymize=entities
844
+ )
845
+ else:
846
+ anonymizer.set_llm_provider(llm_provider, llm_model, entities)
847
+ anonymizer.mapping_table = {}
848
+ anonymizer.reverse_mapping = {}
849
+
850
+ try:
851
+ logger.info("=" * 70)
852
+ logger.info(f"🚀 شروع پردازش - LLM: {llm_provider} ({llm_model})")
853
+ logger.info(f"🎯 موجودیت‌های انتخابی: {', '.join(entities)}")
854
+ logger.info("=" * 70)
855
+
856
+ # مرحله 1: ناشناس‌سازی
857
+ logger.info("🔐 مرحله 1: ناشناس‌سازی (DeepInfra - Qwen3-14B)...")
858
+ anonymized_text, _ = anonymizer.anonymize_with_deepinfra(input_text)
859
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
860
+
861
+ # ✅ دیباگ: بررسی توکن‌های موجود در متن ناشناس
862
+ logger.info("=" * 70)
863
+ logger.info("🔍 DEBUG - توکن‌های موجود در متن ناشناس:")
864
+ for entity_type in entities:
865
+ tokens_found = re.findall(f'{entity_type}-\\d+', anonymized_text)
866
+ unique_tokens = sorted(set(tokens_found))
867
+ logger.info(f" {entity_type}: {unique_tokens}")
868
+ logger.info("=" * 70)
869
+
870
+ # مرحله 2: LLM (فقط اگر analysis_prompt داده شده باشد)
871
+ has_analysis = analysis_prompt and analysis_prompt.strip()
872
+
873
+ if has_analysis:
874
+ logger.info(f"🤖 مرحله 2: {llm_provider.upper()}...")
875
+ llm_response = anonymizer.analyze_with_llm(anonymized_text, analysis_prompt)
876
+ logger.info(f"✅ {llm_provider.upper()}: {len(llm_response)} کاراکتر")
877
+ else:
878
+ logger.info("⚠️ مرحله 2: بدون تحلیل LLM (پرامپت خالی)")
879
+ llm_response = "⚠️ هیچ دستور تحلیل داده نشده است"
880
+
881
+ # مرحله 3: بازگردانی
882
+ logger.info("🔄 مرحله 3: بازگردانی...")
883
+
884
+ # ✅ اصلاح: اگر تحلیل انجام نشده، متن ناشناس اصلی رو restore کن
885
+ if has_analysis:
886
+ # اگر LLM تحلیل کرده، خروجی LLM رو restore کن
887
+ restored_text = anonymizer.restore_text(llm_response)
888
+ else:
889
+ # اگر تحلیل نشده، متن ناشناس اصلی رو restore کن
890
+ restored_text = anonymizer.restore_text(anonymized_text)
891
+
892
+ logger.info("✅ بازگردانی کامل")
893
+
894
+ # مرحله 4: جدول نگاشت
895
+ logger.info("📋 مرحله 4: جدول نگاشت...")
896
+ mapping_str = anonymizer.get_mapping_table_md()
897
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
898
+
899
+ logger.info("=" * 70)
900
+ logger.info("✅ تمام مراحل کامل!")
901
+ logger.info("=" * 70)
902
+
903
+ return restored_text, llm_response, anonymized_text, mapping_str
904
+
905
+ except Exception as e:
906
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
907
+ return "", f"❌ خطا: {str(e)}", "", ""
908
+
909
+ def clear_all():
910
+ """پاک کردن همه"""
911
+ return "", "", "", "", "", "", True, False, False, False, False
912
+
913
+ # Gradio Interface
914
+ css_rtl = """
915
+ .input-box {
916
+ direction: rtl;
917
+ text-align: right;
918
+ }
919
+ .textbox textarea {
920
+ direction: rtl;
921
+ text-align: right;
922
+ font-family: 'Tahoma', serif;
923
+ }
924
+ .thick-divider {
925
+ border-top: 2px solid #333;
926
+ margin: 10px 0;
927
+ }
928
+ .compact-group {
929
+ margin: 0;
930
+ padding: 0;
931
+ }
932
+ .compact-checkbox label {
933
+ padding: 5px 10px !important;
934
+ margin: 3px 0 !important;
935
+ font-size: 0.95em !important;
936
+ }
937
+ """
938
+
939
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
940
+
941
+ gr.Markdown("# 🔐 پلتفرم امن چت با مدل‌های متنوع و ناشناس‌سازی داده‌ها", elem_classes="input-box")
942
+
943
+ # ردیف اول: تنظیمات مدل و انتخاب موجودیت‌ها
944
+ with gr.Row():
945
+ # سمت راست: تنظیمات مدل
946
+ with gr.Column(scale=1):
947
+ with gr.Group():
948
+ gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
949
+
950
+ llm_provider = gr.Dropdown(
951
+ choices=["chatgpt", "grok", "deepinfra"],
952
+ value="chatgpt",
953
+ label="🤖 انتخاب مدل زبانی",
954
+ interactive=True
955
+ )
956
+
957
+ llm_model = gr.Dropdown(
958
+ choices=AVAILABLE_MODELS["chatgpt"],
959
+ value="gpt-4o-mini",
960
+ label="📦 انتخاب نسخه مدل",
961
+ interactive=True
962
+ )
963
+
964
+ # سمت چپ: انتخاب موجودیت‌ها
965
+ with gr.Column(scale=1):
966
+ with gr.Group():
967
+ gr.Markdown("### 🎯 انتخاب موجودیت‌ها", elem_classes="input-box")
968
+
969
+ anonymize_all = gr.Checkbox(
970
+ label="✅ همه موجودیت‌ها",
971
+ value=True,
972
+ elem_classes="input-box compact-checkbox"
973
+ )
974
+
975
+ anonymize_person = gr.Checkbox(
976
+ label="👤 اسامی اشخاص",
977
+ value=False,
978
+ elem_classes="input-box compact-checkbox"
979
+ )
980
+
981
+ anonymize_company = gr.Checkbox(
982
+ label="🏢 نام شرکت‌ها",
983
+ value=False,
984
+ elem_classes="input-box compact-checkbox"
985
+ )
986
+
987
+ anonymize_amount = gr.Checkbox(
988
+ label="💰 ارقام مالی",
989
+ value=False,
990
+ elem_classes="input-box compact-checkbox"
991
+ )
992
+
993
+ anonymize_percent = gr.Checkbox(
994
+ label="📊 درصدها",
995
+ value=False,
996
+ elem_classes="input-box compact-checkbox"
997
+ )
998
+
999
+ # خط جداکننده پررنگ
1000
+ gr.Markdown("---", elem_classes="thick-divider")
1001
+
1002
+ # ردیف دوم: دستورات پردازش و متن ورودی
1003
+ with gr.Row():
1004
+ # سمت راست: دستورات پردازش
1005
+ with gr.Column(scale=1):
1006
+ gr.Markdown("### 📋 دستورات پردازش", elem_classes="input-box")
1007
+
1008
+ analysis_prompt = gr.Textbox(
1009
+ lines=22,
1010
+ placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
1011
+ label="📋 دستورات LLM (اختیاری)",
1012
+ elem_classes="textbox"
1013
+ )
1014
+
1015
+ # سمت چپ: متن ورودی
1016
+ with gr.Column(scale=1):
1017
+ gr.Markdown("### 📝 متن ورودی", elem_classes="input-box")
1018
+
1019
+ input_text = gr.Textbox(
1020
+ lines=22,
1021
+ placeholder="متن مالی/خبری را وارد کنید...",
1022
+ label="",
1023
+ elem_classes="textbox"
1024
+ )
1025
+
1026
+ # دکمه‌های پردازش و پاک کردن
1027
+ with gr.Row():
1028
+ process_btn = gr.Button(
1029
+ "▶️ پردازش",
1030
+ variant="primary",
1031
+ size="lg",
1032
+ scale=2
1033
+ )
1034
+
1035
+ clear_btn = gr.Button(
1036
+ "🗑️ پاک کردن",
1037
+ variant="stop",
1038
+ size="lg",
1039
+ scale=1
1040
+ )
1041
+
1042
+ # نتایج
1043
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
1044
+
1045
+ with gr.Row():
1046
+ with gr.Column(scale=1):
1047
+ restored_text = gr.Textbox(
1048
+ lines=12,
1049
+ label="✅ متن بازگردانی شده",
1050
+ interactive=False,
1051
+ elem_classes="textbox"
1052
+ )
1053
+
1054
+ with gr.Column(scale=1):
1055
+ llm_analysis = gr.Textbox(
1056
+ lines=12,
1057
+ label="🤖 تحلیل LLM",
1058
+ interactive=False,
1059
+ elem_classes="textbox"
1060
+ )
1061
+
1062
+ with gr.Column(scale=1):
1063
+ anonymized_text = gr.Textbox(
1064
+ lines=12,
1065
+ label="🔒 متن ناشناس‌شده",
1066
+ interactive=False,
1067
+ elem_classes="textbox"
1068
+ )
1069
+
1070
+ mapping_table = gr.Markdown(
1071
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
1072
+ label="📋 جدول نگاشت",
1073
+ elem_classes="input-box"
1074
+ )
1075
+
1076
+
1077
+ # Event Handler برای تغییر provider
1078
+ def handle_provider_change(provider):
1079
+ models = AVAILABLE_MODELS.get(provider, [])
1080
+ default_model = models[0] if models else None
1081
+ return gr.update(choices=models, value=default_model)
1082
+
1083
+ llm_provider.change(
1084
+ fn=handle_provider_change,
1085
+ inputs=[llm_provider],
1086
+ outputs=[llm_model]
1087
+ )
1088
+
1089
+ def handle_select_all(select_all):
1090
+ if select_all:
1091
+ return (
1092
+ gr.update(value=False, interactive=False),
1093
+ gr.update(value=False, interactive=False),
1094
+ gr.update(value=False, interactive=False),
1095
+ gr.update(value=False, interactive=False)
1096
+ )
1097
+ else:
1098
+ return (
1099
+ gr.update(value=False, interactive=True),
1100
+ gr.update(value=False, interactive=True),
1101
+ gr.update(value=False, interactive=True),
1102
+ gr.update(value=False, interactive=True)
1103
+ )
1104
+
1105
+ anonymize_all.change(
1106
+ fn=handle_select_all,
1107
+ inputs=[anonymize_all],
1108
+ outputs=[anonymize_person, anonymize_company, anonymize_amount, anonymize_percent]
1109
+ )
1110
+
1111
+ # پردازش
1112
+ process_btn.click(
1113
+ fn=process,
1114
+ inputs=[
1115
+ input_text,
1116
+ analysis_prompt,
1117
+ llm_provider,
1118
+ llm_model,
1119
+ anonymize_all,
1120
+ anonymize_person,
1121
+ anonymize_company,
1122
+ anonymize_amount,
1123
+ anonymize_percent
1124
+ ],
1125
+ outputs=[restored_text, llm_analysis, anonymized_text, mapping_table]
1126
+ )
1127
+
1128
+ # پاک کردن
1129
+ clear_btn.click(
1130
+ fn=clear_all,
1131
+ outputs=[
1132
+ input_text,
1133
+ analysis_prompt,
1134
+ restored_text,
1135
+ llm_analysis,
1136
+ anonymized_text,
1137
+ mapping_table,
1138
+ anonymize_all,
1139
+ anonymize_person,
1140
+ anonymize_company,
1141
+ anonymize_amount,
1142
+ anonymize_percent
1143
+ ]
1144
+ )
1145
+
1146
+ if __name__ == "__main__":
1147
+ print("=" * 70)
1148
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
1149
+ print("=" * 70)
1150
+ print("\n📋 نحوه استفاده:\n")
1151
+ print("1. API Keyها را در Hugging Face Secrets تنظیم کنید:")
1152
+ print(" - DEEPINFRA_API_KEY (ضروری برای ناشناس‌سازی - Qwen3-14B)")
1153
+ print(" - OPENAI_API_KEY (برای ChatGPT)")
1154
+ print(" - XAI_API_KEY (برای Grok)")
1155
+ print(" - DEEPINFRA_API_KEY (برای DeepInfra به عنوان LLM تحلیل)")
1156
+ print("2. http://localhost:7860 را باز کنید")
1157
+ print("3. مدل زبانی (ChatGPT/Grok) و نسخه مدل را انتخاب کنید")
1158
+ print("4. موجودیت‌های مورد نظر برای ناشناس‌سازی را انتخاب کنید")
1159
+ print("5. متن و دستورات پردازش را وارد کنید")
1160
+ print("6. 'پردازش' را کلیک کنید\n")
1161
+ print("🔐 تمام API Keyها از Hugging Face Secrets خوانده می‌شوند")
1162
+ print("📦 مدل‌های پشتیبانی شده:")
1163
+ print(" • ناشناس‌سازی: DeepInfra Qwen/Qwen3-14B")
1164
+ print(" • ChatGPT: gpt-5.1, gpt-5, gpt-4.1, gpt-4o, gpt-4o-mini")
1165
+ print(" • Grok: grok-4-0709, grok-3, grok-3-mini")
1166
+ print(" • DeepInfra: Qwen/Qwen3-14B, Qwen/Qwen3-32B, Qwen/Qwen2.5-72B-Instruct")
1167
+ print("=" * 70 + "\n")
1168
+
1169
+ app.launch(
1170
+ server_name="0.0.0.0",
1171
+ server_port=7860,
1172
+ share=False,
1173
+ show_error=True
1174
+ )
llm_sender_unified (2).py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # ✅ حذف بلوک‌های thinking برای DeepInfra/Qwen3
154
+ if isinstance(self, DeepInfraSender):
155
+ llm_response = DeepInfraSender.strip_thinking(llm_response)
156
+ logger.info("✅ پاسخ دریافت شد")
157
+ return llm_response
158
+
159
+ # پردازش خطاهای مختلف
160
+ elif response.status_code == 429: # Rate limiting
161
+ wait_time = 5 * (attempt + 1)
162
+ logger.warning(f"⚠️ Rate limit | صبر: {wait_time} ثانیه")
163
+ if attempt < retry_count - 1:
164
+ time.sleep(wait_time)
165
+ continue
166
+ else:
167
+ return (
168
+ "❌ سهمیه API تمام شده است. لطفاً بعداً تلاش کنید."
169
+ if lang == 'fa'
170
+ else "❌ API quota exceeded. Please try later."
171
+ )
172
+
173
+ elif response.status_code == 401:
174
+ return (
175
+ "❌ کلید API نامعتبر است!"
176
+ if lang == 'fa'
177
+ else "❌ Invalid API key!"
178
+ )
179
+
180
+ elif response.status_code in [502, 503, 504]: # Server errors
181
+ wait_time = 2 * (attempt + 1)
182
+ logger.warning(f"⚠️ Server error {response.status_code} | صبر: {wait_time} ثانیه")
183
+ if attempt < retry_count - 1:
184
+ time.sleep(wait_time)
185
+ continue
186
+ else:
187
+ return (
188
+ f"❌ خطای سرور: {response.status_code}"
189
+ if lang == 'fa'
190
+ else f"❌ Server error: {response.status_code}"
191
+ )
192
+
193
+ else:
194
+ # خطای دیگر
195
+ try:
196
+ error_data = response.json() if response.content else {}
197
+ if isinstance(error_data, dict):
198
+ error_msg = error_data.get('error', {}).get('message', response.text)
199
+ else:
200
+ error_msg = str(error_data)
201
+ except:
202
+ error_msg = response.text[:200]
203
+
204
+ logger.error(f"❌ API Error: {error_msg}")
205
+ return f"❌ API Error: {error_msg}"
206
+
207
+ except requests.exceptions.Timeout:
208
+ logger.warning("⚠️ Timeout | صبر: 3 ثانیه و تلاش مجدد")
209
+ if attempt < retry_count - 1:
210
+ time.sleep(3)
211
+ continue
212
+ else:
213
+ return (
214
+ "❌ خطای اتصال: timeout"
215
+ if lang == 'fa'
216
+ else "❌ Connection error: timeout"
217
+ )
218
+
219
+ except requests.exceptions.ConnectionError as e:
220
+ logger.warning("⚠️ Connection error | صبر: 2 ثانیه و تلاش مجدد")
221
+ if attempt < retry_count - 1:
222
+ time.sleep(2)
223
+ continue
224
+ else:
225
+ return (
226
+ f"❌ خطای اتصال: {str(e)}"
227
+ if lang == 'fa'
228
+ else f"❌ Connection error: {str(e)}"
229
+ )
230
+
231
+ except Exception as e:
232
+ logger.error(f"❌ خطای غیرمنتظره: {str(e)}")
233
+ return (
234
+ f"❌ خطا در ارتباط با LLM: {str(e)}"
235
+ if lang == 'fa'
236
+ else f"❌ Error connecting to LLM: {str(e)}"
237
+ )
238
+
239
+
240
+ class ChatGPTSender(LLMSender):
241
+ """کلاس برای ارسال به ChatGPT"""
242
+
243
+ def __init__(self, api_key: Optional[str] = None, model: str = "gpt-4o-mini"):
244
+ raw_key = api_key or os.getenv("OPENAI_API_KEY", "")
245
+ cleaned_key = raw_key.strip() if raw_key else ""
246
+
247
+ super().__init__(cleaned_key, model)
248
+ self.base_url = self.get_base_url()
249
+
250
+ if not self.api_key:
251
+ logger.warning("⚠️ کلید OpenAI API تنظیم نشده است!")
252
+
253
+ def get_default_model(self) -> str:
254
+ return "gpt-4o-mini"
255
+
256
+ def get_base_url(self) -> str:
257
+ return "https://api.openai.com/v1/chat/completions"
258
+
259
+
260
+ class GrokSender(LLMSender):
261
+ """کلاس برای ارسال به Grok (xAI)"""
262
+
263
+ def __init__(self, api_key: Optional[str] = None, model: str = "grok-beta"):
264
+ raw_key = api_key or os.getenv("XAI_API_KEY", "")
265
+ cleaned_key = raw_key.strip() if raw_key else ""
266
+
267
+ super().__init__(cleaned_key, model)
268
+ self.base_url = self.get_base_url()
269
+
270
+ if not self.api_key:
271
+ logger.warning("⚠️ کلید xAI API تنظیم نشده است!")
272
+
273
+ def get_default_model(self) -> str:
274
+ return "grok-beta"
275
+
276
+ def get_base_url(self) -> str:
277
+ return "https://api.x.ai/v1/chat/completions"
278
+
279
+
280
+ class DeepInfraSender(LLMSender):
281
+ """کلاس برای ارسال به DeepInfra"""
282
+
283
+ def __init__(self, api_key: Optional[str] = None, model: str = "Qwen/Qwen3-14B"):
284
+ raw_key = api_key or os.getenv("DEEPINFRA_API_KEY", "")
285
+ cleaned_key = raw_key.strip() if raw_key else ""
286
+
287
+ super().__init__(cleaned_key, model)
288
+ self.base_url = self.get_base_url()
289
+
290
+ if not self.api_key:
291
+ logger.warning("⚠️ کلید DeepInfra API تنظیم نشده است!")
292
+
293
+ def get_default_model(self) -> str:
294
+ return "Qwen/Qwen3-14B"
295
+
296
+ def get_base_url(self) -> str:
297
+ return "https://api.deepinfra.com/v1/openai/chat/completions"
298
+
299
+ def _uses_max_completion_tokens(self) -> bool:
300
+ return False
301
+
302
+ def _requires_default_temperature(self) -> bool:
303
+ return False
304
+
305
+ @staticmethod
306
+ def strip_thinking(text: str) -> str:
307
+ """✅ حذف بلوک‌های <think>...</think> که Qwen3 تولید می‌کند"""
308
+ if not text:
309
+ return text
310
+ cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
311
+ return cleaned.strip()
312
+
313
+
314
+ def create_llm_sender(
315
+ provider: str = "chatgpt",
316
+ api_key: Optional[str] = None,
317
+ model: Optional[str] = None
318
+ ) -> LLMSender:
319
+ """ایجاد LLM sender بر اساس provider"""
320
+ provider = provider.lower()
321
+
322
+ if provider == "chatgpt":
323
+ if model is None:
324
+ model = "gpt-4o-mini"
325
+ return ChatGPTSender(api_key=api_key, model=model)
326
+
327
+ elif provider == "grok":
328
+ if model is None:
329
+ model = "grok-beta"
330
+ return GrokSender(api_key=api_key, model=model)
331
+
332
+ elif provider == "deepinfra":
333
+ if model is None:
334
+ model = "Qwen/Qwen3-14B"
335
+ return DeepInfraSender(api_key=api_key, model=model)
336
+
337
+ else:
338
+ raise ValueError(f"Provider نامعتبر: {provider}")
339
+
340
+
341
+ # ✅ مدل‌های موجود (به‌روزرسانی شده با DeepInfra)
342
+ AVAILABLE_MODELS = {
343
+ "chatgpt": [
344
+ "gpt-5.1",
345
+ "gpt-5",
346
+ "gpt-4.1",
347
+ "gpt-4o-mini",
348
+ "gpt-4o",
349
+ "gpt-4-turbo",
350
+ "gpt-3.5-turbo"
351
+ ],
352
+ "grok": [
353
+ "grok-3-mini",
354
+ "grok-3",
355
+ "grok-2-1212"
356
+ ],
357
+ "deepinfra": [
358
+ "Qwen/Qwen3-14B",
359
+ "Qwen/Qwen3-32B",
360
+ "Qwen/Qwen3-30B-A3B",
361
+ "Qwen/Qwen2.5-72B-Instruct",
362
+ "Qwen/Qwen2.5-14B-Instruct",
363
+ ]
364
+ }
365
+
366
+
367
+ if __name__ == "__main__":
368
+ print("=" * 60)
369
+ print("🤖 LLM Sender - نسخه اصلاح شده")
370
+ print("✨ رفع مشکل temperature برای GPT-5")
371
+ print("=" * 60)
372
+
373
+ # تست
374
+ print("\n🧪 تست مدل‌ها:")
375
+ test_models = ['gpt-5', 'gpt-5.1', 'gpt-4o']
376
+ for model in test_models:
377
+ sender = create_llm_sender("chatgpt", model=model)
378
+ uses_completion = sender._uses_max_completion_tokens()
379
+ requires_default_temp = sender._requires_default_temperature()
380
+
381
+ print(f"\n مدل: {model}")
382
+ print(f" • max_tokens: {'max_completion_tokens' if uses_completion else 'max_tokens'}")
383
+ print(f" • temperature: {'1.0 (default only)' if requires_default_temp else '0.7 (custom)'}")