import gradio as gr import re import os import requests import logging from typing import Dict, List, Tuple, Set import json logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class AnonymizerCerebrasEnhanced: def __init__(self, api_key: str = None): self.api_key = api_key or os.getenv("CEREBRAS_API_KEY") self.mapping_table = {} self.counters = { 'company': 0, 'person': 0, 'amount': 0, 'phone': 0, 'email': 0, 'id_number': 0, 'date': 0, 'location': 0, 'percent': 0 } self.seen_entities = {} # برای ثبات نگاشت if not self.api_key: raise ValueError("❌ کلید API Cerebras یافت نشد!") logger.info("✅ Anonymizer Enhanced مقداردهی شد") def get_system_prompt(self) -> str: """ایجاد دستورالعمل سیستمی پیشرفته برای Groq""" return """شما یک «ناشناس‌ساز متون مالی/خبری فارسی» هستید. وظیفه‌تان جایگزینی اسامی خاص و مقادیر عددی با شناسه‌های بی‌معناست. ## **قوانین اندیس‌گذاری - CRITICAL** ### **1. ترتیب شماره‌گذاری الزامی:** - شرکت‌ها: company-01, company-02, company-03, company-04, ... (پیوسته و بدون گپ) - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ) - اعداد: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ) - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ) - تاریخها: date-01, date-02, date-03, ... (پیوسته و بدون گپ) ### **2. ثبات شناسه‌ها در متن - MUST MAINTAIN:** - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد - اگر "مهدی احمدی" اول‌بار person-01 شد، در تمام متن همان باشد - **CRITICAL: اگر "سروش خسروی" = person-01، تو "خسروی" تنهایی هم = person-01 باشد** ### **3. تشخیص صحیح انواع:** **شرکت/سازمان:** همراه اول، بانک ملی، ایران‌خودرو، سایپا، بانک مرکزی، سامانه کدال، وزارت نفت **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین، ابوالفضل نجارزاده، سروش خسروی **عدد:** 37، 70، 677، 73.7، 178 (هر عددی) **درصد:** 37 درصدی، 15 درصدی، 53 درصد، 43% **تاریخ:** 1403، 1404، اردیبهشت، فروردین، 30 آذر 1403 ## **انواع موجودیت‌ها:** **company-XX:** نام شرکت‌ها، سازمان‌ها، بانک‌ها، هلدینگ‌ها، گروه‌های مالی **person-XX:** نام و نام خانوادگی اشخاص - شامل نام کامل، نام کوچک تنهایی، نام خانوادگی تنهایی **amount-XX:** مبالغ مالی شامل ریال، تومان، همت، دلار، تن، دستگاه و واحدهای اندازه‌گیری **percent-XX:** درصدها و نسبت‌ها **date-XX:** تمام تاریخ‌ها شامل سال، ماہ، روز و ترکیب آنها ## **قوانین کلیدی:** 1. **ترتیب شماره‌گذاری:** اولین باری که موجودیت ظاهر می‌شود، شماره می‌گیرد (01، 02، 03، ...) 2. **حفظ هویت یکسان:** اگر همان موجودیت دوباره آمد، از همان شماره استفاده کن. 3. **CRITICAL - Entity Linking برای اشخاص:** - اگر "سروش خسروی" = person-01 شد، تو "خسروی" تنهایی = person-01 - اگر "محمدرضا فرزین" = person-01 شد، تو "فرزین" یا "محمدرضا" = person-01 - اگر "علی احمدی" = person-01 شد، تو "احمدی"، "علی"، "آن شخص" همه = person-01 - **MUST TRACK: نام کامل → نام کوچک → نام خانوادگی → ضمیرها** - **نام خانوادگی تنهایی را هرگز بدون linking رها نکن** 4. **تشخیص نام‌های مختلف:** "فولاد مبارکه اصفهان" و "فولاد مبارکه" و "این شرکت" همه company-01 هستند. 5. **CRITICAL - تمام تاریخ‌ها باید Anonymize شوند:** - سال ONLY: "سال 1403" → "سال date-01" - ماہ ONLY: "اردیبهشت" → "date-02" - سال + ماہ: "اردیبهشت 1404" → "date-03 date-04" - تاریخ مکمل: "1403/04/12" → "date-05/date-06/date-07" - **NO EXCEPTION: تمام اعداد تاریخ باید anonymize شوند** - **یکسانی برقرار کن: اگر "1403" یک جا date-01 شد، همه جا date-01 باشد** 6. **مبالغ و درصدهای مختلف:** هر عدد جدید، شماره جدید می‌گیرد 7. **حفظ ساختار:** ساختار جمله را حفظ کن، کلمات توصیفی مثل "شرکت"، "بانک"، "گروه" را قبل از برچسب حفظ کن 8. **هیچ توضیح اضافه‌ای نده:** فقط متن ناشناس‌شده را برگردان ## **موارد حفظ شده:** - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل، سرپرست - واحدها: میلیارد تومان، همت، ریال، ماه، سال - مکان‌ها: تهران، اصفهان، ایران - کلمات توضیحی: "شرکت"، "بانک"، "گروه" ## **ممنوع:** - کلمات انگلیسی اضافی - تغییر ساختار جمله - حذف یا اضافه کردن کلمات - **نام خانوادگی یا نام کوچک تنهایی را بدون linking رها کردن** ## **نمونه‌های آموزشی:** **نمونه ۱ - Entity Linking برای نام‌ها (CRITICAL):** ورودی: سروش خسروی، سرپرست هیأت‌مدیره. خسروی اعلام کرد که سود خالص 216 میلیارد تومان بود. خسروی همچنین به چالش‌ها اشاره کرد. خروجی: person-01، سرپرست هیأت‌مدیره. person-01 اعلام کرد که سود خالص amount-01 بود. person-01 همچنین به چالش‌ها اشاره کرد. **نمونه ۲ - تمام تاریخها Anonymize شوند (CRITICAL):** ورودی: سال 1403 یکی از سخت‌ترین سال‌ها برای صنعت پتروشیمی بود. در اردیبهشت 1404 شرکت گزارش منتشر کرد و کاهش سود خالص در سال 1403 را اعلام کرد. خروجی: سال date-01 یکی از سخت‌ترین سال‌ها برای صنعت پتروشیمی بود. در date-02 date-03 شرکت گزارش منتشر کرد و کاهش سود خالص در سال date-01 را اعلام کرد. **نمونه ۳:** ورودی: مهدی اخوان بهابادی، مدیرعامل همراه اول، اعلام کرد درآمد عملیاتی شرکت با رشد 37 درصدی به 70 هزار و 677 میلیارد تومان رسیده است. سود خالص 7101 میلیارد تومان و تلفیقی گروه همراه اول 8003 میلیارد تومان شد. خروجی: person-01، مدیرعامل company-01، اعلام کرد درآمد عملیاتی شرکت با رشد percent-01 به amount-01 رسیده است. سود خالص amount-02 و تلفیقی گروه company-01 amount-03 شد. **نمونه ۴:** ورودی: بانک مرکزی و بانک ملی با همکاری محمدرضا فرزین، 60 درصد سپرده‌ها را مدیریت کردند. خروجی: company-01 و company-02 با همکاری person-01، percent-01 سپرده‌ها را مدیریت کردند. **نمونه ۵:** ورودی: سایپا و ایران‌خودرو مجموع زیان 620 همت داشتند و سایپا 269 هزار میلیارد زیان اعلام کرد. خروجی: company-01 و company-02 مجموع زیان amount-01 داشتند و company-01 amount-02 زیان اعلام کرد. **نمونه ۶ - تاریخ مکمل:** ورودی: مجمع عمومی مورخ 1403/04/12 برگزار شد و گزارش مالی منتهی به 30 آذر 1403 تصویب رسید. خروجی: مجمع عمومی مورخ date-01/date-02/date-03 برگزار شد و گزارش مالی منتهی به date-04 date-05 date-06 تصویب رسید. **نمونه ۷:** ورودی: بانک پاسارگاد با شناسایی سود خالص 155 هزار میلیارد ریالی در رده دوم سودآورترین بانک‌های کشور قرار گرفت. در مقابل، بانک سرمایه با مدیرعاملی فرج‌اله قدمی وضعیت بحرانی دارد. خروجی: company-01 با شناسایی سود خالص amount-01 در رده دوم سودآورترین بانک‌های کشور قرار گرفت. در مقابل، company-02 با مدیرعاملی person-01 وضعیت بحرانی دارد. **نمونه ۸:** ورودی: بانک سرمایه با مدیرعاملی فرج‌اله قدمی زیان خالص 2700 میلیارد تومانی در سه‌ماهه نخست 1404 گزارش کرد. نسبت کفایت سرمایه به منفی 345 درصد رسیده و زیان انباشته نزدیک به 67 هزار میلیارد تومان است. خروجی: company-01 با مدیرعاملی person-01 زیان خالص amount-01 در سه‌ماهه نخست date-01 گزارش کرد. نسبت کفایت سرمایه به منفی percent-01 رسیده و زیان انباشته نزدیک به amount-02 است. **نمونه ۹:** ورودی: دو بانک ملت و پاسارگاد به ترتیب با شناسایی سود خالص 157 و 155 هزار میلیارد ریالی رقابت تنگاتنگی داشته و در رده‌های اول و دوم جای دارند. خروجی: دو بانک company-01 و company-02 به ترتیب با شناسایی سود خالص amount-01 و amount-02 رقابت تنگاتنگی داشته و در رده‌های اول و دوم جای دارند. **نمونه ۱۰:** ورودی: مرور صورت‌های مالی بانک‌ها نشان می‌دهد سهم سودهای ارزی به‌راحتی به 40–60٪ رسیده است و این مسئله نشان‌دهنده وضعیت غیرعادی بازار است. خروجی: مرور صورت‌های مالی بانک‌ها نشان می‌دهد سهم سودهای ارزی به‌راحتی به percent-01 رسیده است و این مسئله نشان‌دهنده وضعیت غیرعادی بازار است. **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست.""" def get_user_prompt(self, text: str) -> str: """تشکیل پرامپت کاربر""" return f"""متن مالی فارسی زیر را تجزیه و تحلیل کنید. تمام موجودیت‌های حساس را شناسایی کنید و یک JSON Array برگردانید. متن: {text} **مهم**: - اگر چند بار یک نام تکرار شود، یک id بدهید - کلمات عمومی را حفظ کنید - واحدها را حفظ کنید - فقط JSON برگردانید! یک JSON Array برگردانید. هر عنصر دارای: - "text": متن دقیق استخراج شده - "type": نوع (company, person, amount, percent, phone, email, date, location, id_number) - "original": توضیح اضافی اگر نام مستعار باشد""" def call_cerebras(self, text: str) -> List[Dict]: """فراخوانی Cerebras API با پرامپت بهبود شده""" logger.info("🔄 فراخوانی Cerebras API با دستورالعمل قوی...") system_prompt = self.get_system_prompt() user_prompt = self.get_user_prompt(text) try: response = requests.post( "https://api.cerebras.ai/v1/chat/completions", headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" }, json={ "model": "llama-3.3-70b", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], "max_tokens": 4000, "temperature": 0.1 }, timeout=30 ) if response.status_code != 200: logger.error(f"❌ خطای API Cerebras: {response.text}") return [] result = response.json() content = result['choices'][0]['message']['content'] try: # تمیز کردن محتوا از markdown اگر وجود داشته باشد content = content.replace("```json", "").replace("```", "").strip() entities = json.loads(content) if not isinstance(entities, list): entities = [] logger.info(f"✅ {len(entities)} موجودیت استخراج شد") return entities except json.JSONDecodeError: logger.error(f"❌ خطا در JSON parsing: {content[:200]}") return [] except Exception as e: logger.error(f"❌ خطا Cerebras: {e}") return [] def get_placeholder(self, entity_type: str) -> str: """تولید placeholder با format جدید""" type_lower = entity_type.lower() if type_lower not in self.counters: type_lower = 'amount' self.counters[type_lower] += 1 return f"{type_lower}-{self.counters[type_lower]:02d}" def anonymize(self, text: str) -> Tuple[str, List]: """ناشناس‌سازی متن با قوانین ثبات""" logger.info("🚀 شروع ناشناس‌سازی متن...") # تنظیف self.mapping_table = {} self.seen_entities = {} for key in self.counters: self.counters[key] = 0 # دریافت موجودیت‌ها entities = self.call_cerebras(text) if not entities: logger.warning("⚠️ موجودیتی شناسایی نشد") return text, [] logger.info("🔄 Processing entities...") # جایگزینی با قانون ثبات anonymized = text replacements = [] for entity in entities: entity_type = entity.get('type', 'amount').lower() entity_text = entity.get('text', '').strip() original_info = entity.get('original', '') if not entity_text: continue # بررسی اگر این موجودیت قبلاً دیده شده است entity_key = (entity_type, entity_text.lower()) if entity_key in self.seen_entities: token = self.seen_entities[entity_key] logger.info(f"🔄 موجودیت تکراری: {entity_text} → {token}") else: token = self.get_placeholder(entity_type) self.seen_entities[entity_key] = token self.mapping_table[token] = { 'original': entity_text, 'type': entity_type, 'note': original_info } logger.info(f"✅ جایگزینی: {entity_text} → {token}") # جایگزینی دقیق (case-sensitive اول، سپس case-insensitive) idx = anonymized.find(entity_text) if idx != -1: anonymized = anonymized[:idx] + token + anonymized[idx + len(entity_text):] replacements.append({ 'original': entity_text, 'placeholder': token, 'type': entity_type }) logger.info(f"✅ ناشناس‌سازی کامل - {len(self.mapping_table)} نگاشت") return anonymized, entities def get_mapping_table_str(self) -> str: """جدول نگاشت جزئی""" if not self.mapping_table: return "❌ موجودیتی شناسایی نشد" result = "## 📊 جدول نگاشت\n\n" result += "| توکن | اطلاعات اصلی | نوع |\n" result += "|------|--------|------|\n" for token, info in sorted(self.mapping_table.items()): entity_type = info.get('type', 'unknown') original = info.get('original', '') note = info.get('note', '') note_str = f" ({note})" if note else "" result += f"| `{token}` | {original}{note_str} | {entity_type} |\n" return result def restore(self, text: str) -> str: """بازگردانی اطلاعات اصلی""" logger.info("🔄 بازگردانی اطلاعات...") restored = text for token, info in self.mapping_table.items(): original = info.get('original', '') restored = restored.replace(token, original) logger.info("✅ بازگردانی کامل") return restored # متغیرهای global anonymizer = None def process(input_text: str) -> Tuple[str, str, str, str, str]: """ روند کامل: 1. ناشناس‌سازی با Cerebras (llama-3.3-70b) + پرامپت قوی 2. ارسال به ChatGPT (حتما!) 3. بازگردانی پاسخ ChatGPT """ global anonymizer try: if not input_text.strip(): return "", "", "", "", "" # دریافت API Keys api_key_cerebras = os.getenv("CEREBRAS_API_KEY") api_key_gpt = os.getenv("OPENAI_API_KEY") if not api_key_gpt: logger.error("❌ OPENAI_API_KEY یافت نشد") return "", "", "", "", "" if not api_key_cerebras: logger.error("❌ CEREBRAS_API_KEY یافت نشد") return "", "", "", "", "" # ============================================ # مرحله 1: مقداردهی # ============================================ if not anonymizer: logger.info("Initializing anonymizer...") anonymizer = AnonymizerCerebrasEnhanced() # ============================================ # مرحله 2: ناشناس‌سازی با پرامپت قوی # ============================================ logger.info("Step 1: Anonymizing text with Cerebras...") anonymized_text, entities = anonymizer.anonymize(input_text) if not entities: logger.warning("⚠️ موجودیتی شناسایی نشد - متن ناشناس نشد") return input_text, "", "", "", "" # ============================================ # مرحله 3: جدول نگاشت # ============================================ logger.info("Step 2: Creating mapping table") mapping = anonymizer.get_mapping_table_str() logger.info(f"📋 {len(anonymizer.mapping_table)} نگاشت ایجاد شد") # ============================================ # مرحله 4: ارسال به ChatGPT (حتما!) # ============================================ logger.info("Step 3: Sending to ChatGPT...") prompt = f"""متن ناشناس‌شده زیر (متن مالی) را تحلیل و خلاصه کنید. متن: {anonymized_text} لطفاً: 1. خلاصه‌ای مختصر و معنادار ارائه دهید 2. نکات اصلی را مشخص کنید 3. تمام توکن‌های ناشناس (مثل company-01، amount-02) را حفظ کنید 4. تنها اطلاعات موجود در متن را بیان کنید""" logger.info(f"📤 ارسال به ChatGPT (gpt-4o-mini)...") try: gpt_response_obj = requests.post( "https://api.openai.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key_gpt}"}, json={ "model": "gpt-4o-mini", "messages": [ { "role": "system", "content": "شما دستیار تحلیل متون مالی فارسی هستید. متن‌های ناشناس‌شده را دقیق تحلیل کنید. تمام توکن‌های ناشناس را حفظ کنید." }, {"role": "user", "content": prompt} ], "max_tokens": 1500, "temperature": 0.7 }, timeout=30 ) if gpt_response_obj.status_code == 200: gpt_response = gpt_response_obj.json()['choices'][0]['message']['content'] logger.info("✅ پاسخ دریافت شد") else: error_text = gpt_response_obj.json().get('error', {}).get('message', gpt_response_obj.text) logger.error(f"❌ خطای ChatGPT: {error_text}") return input_text, anonymized_text, "", "", mapping except Exception as e: logger.error(f"❌ خطا در ارسال به ChatGPT: {e}") return input_text, anonymized_text, "", "", mapping # ============================================ # مرحله 5: بازگردانی پاسخ ChatGPT # ============================================ logger.info("Step 4: Restoring original text...") restored_text = anonymizer.restore(gpt_response) logger.info(f"✅ بازگردانی کامل") logger.info(f"Done. Input: {len(input_text)} | Anonymized: {len(anonymized_text)} | Entities: {len(entities)}") return input_text, anonymized_text, gpt_response, restored_text, mapping except Exception as e: logger.error(f"❌ خطا عمومی: {e}", exc_info=True) return "", "", "", "", "" def clear(): """پاک کردن""" empty_mapping = "### 📋 جدول نگاشت\nدر انتظار پردازش..." return "", "", "", "", empty_mapping # رابط Gradio - کاملاً فارسی‌زبان و RTL css_rtl = """ #input_text textarea { direction: rtl; text-align: right; } #anonymized_text textarea { direction: rtl; text-align: right; } #gpt_response textarea { direction: rtl; text-align: right; } #restored_text textarea { direction: rtl; text-align: right; } """ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app: gr.Markdown("# 🔐 سیستم ناشناس‌سازی متون مالی فارسی") gr.Markdown("#### استخراج موجودیت‌های حساس و ناشناس‌سازی آنها") with gr.Row(): # بلوک 1: متن ورودی (سمت راست) with gr.Column(scale=2): input_text = gr.Textbox( lines=12, placeholder="متن مالی/خبری را وارد کنید...", label="📝 متن ورودی", elem_id="input_text" ) # دکمه‌های کنترل with gr.Column(scale=1): gr.HTML("
") process_btn = gr.Button("🔄 پردازش", variant="primary", size="lg") clear_btn = gr.Button("🗑️ پاک کردن", variant="stop", size="lg") # بلوک 2: متن ناشناس‌سازی شده with gr.Row(): with gr.Column(scale=1): anonymized_text = gr.Textbox( lines=10, label="🔒 متن ناشناس‌شده", interactive=False, elem_id="anonymized_text" ) # بلوک 3: پاسخ ChatGPT with gr.Column(scale=1): gpt_response = gr.Textbox( lines=10, label="🤖 تحلیل ChatGPT", interactive=False, elem_id="gpt_response" ) # بلوک 4: متن بازگردانی شده (سمت چپ) with gr.Column(scale=1): restored_text = gr.Textbox( lines=10, label="✅ متن بازگردانی شده", interactive=False, elem_id="restored_text" ) # بلوک 5: جدول نگاشت به صورت مارکداون with gr.Row(): with gr.Column(): mapping = gr.Markdown( value="### 📋 جدول نگاشت\nدر انتظار پردازش...", label="📋 جدول نگاشت" ) # Event handlers process_btn.click( fn=process, inputs=[input_text], outputs=[input_text, anonymized_text, gpt_response, restored_text, mapping] ) clear_btn.click( fn=clear, outputs=[input_text, anonymized_text, gpt_response, restored_text, mapping] ) if __name__ == "__main__": print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...") app.launch( server_name="0.0.0.0", server_port=7860, share=False, show_error=True )