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