Spaces:
Runtime error
Runtime error
| # -*- coding: utf-8 -*- | |
| import random | |
| import re | |
| from datetime import datetime | |
| from typing import Union | |
| from responses import generate_reply | |
| # ========= أدوات الاسم واللغة ========= | |
| def safe_display_name(s: str) -> str: | |
| """اسم عرض آمن: لو فاضي/أرقام فقط/طويل جدًا → بديل ودّي وقصّه.""" | |
| s = (s or "").strip() | |
| if not s or s.isdigit(): | |
| return "حبيبي" | |
| return s[:24] | |
| def detect_lang(text: str) -> str: | |
| """كشف مبدئي للغة بناءً على الحروف/العلامات.""" | |
| if not text: | |
| return "ar" | |
| t = text.strip().lower() | |
| # عربية | |
| if re.search(r"[\u0600-\u06FF]", t): | |
| return "ar" | |
| # فرنسية (لهجات) | |
| if any(c in t for c in ["é", "è", "à", "ç", "ô"]): | |
| return "fr" | |
| # إسبانية | |
| if any(c in t for c in ["ñ", "¡", "¿"]): | |
| return "es" | |
| # إنجليزية (أحرف لاتينية عامة) | |
| if re.search(r"[a-z]", t): | |
| return "en" | |
| # افتراضي | |
| return "en" | |
| # ========= قوالب ردود ========= | |
| RESPONSES = { | |
| # أمثلة استعمال (احتفظنا بها كمكتبة ردود جاهزة) | |
| "greeting_1": "مرحبًا بك، نورتني! كيف ممكن أكون مفيدة اليوم؟", | |
| "support_1": "إذا تعبت، احكيلي… ما في داعي تتحمّل لوحدك.", | |
| "art_1": "الفن مش بس رسم… هو طريقة نحكي فيها بلا كلمات.", | |
| } | |
| # ردود قصيرة متعددة اللغات | |
| REPLIES = { | |
| "ar": [ | |
| "🤗 أنا معك يا {name}… حضن دافي لحد ما الأمور ترجع تشتغل.", | |
| "ولا يهمك يا {name}، أنا جنبك… نجرب كمان شوي.", | |
| "موجودة بحضنك يا {name}، خليك معي وأنا بأهتم بالباقي.", | |
| "تعال لعندي يا {name}… خليني ألمّك بحضني لغاية ما يهدى كل شيء.", | |
| ], | |
| "en": [ | |
| "🤗 I'm here with you, {name}… a warm hug until things work again.", | |
| "Don't worry, {name} — I'm by your side. We'll try again soon.", | |
| "I'm right in your arms, {name}. Stay with me; I'll take care of it.", | |
| "Come to me, {name}… let me hold you until everything calms down.", | |
| ], | |
| "fr": [ | |
| "🤗 Je suis avec toi, {name}… un câlin chaleureux jusqu'à ce que tout refonctionne.", | |
| "Ne t’inquiète pas, {name} — je suis là. On réessaiera bientôt.", | |
| "Je suis là, tout près, {name}. Reste avec moi, je m’en occupe.", | |
| "Viens par ici, {name}… laisse-moi te serrer jusqu’à ce que tout s’apaise.", | |
| ], | |
| "es": [ | |
| "🤗 Estoy contigo, {name}… un abrazo cálido hasta que todo vuelva a funcionar.", | |
| "No te preocupes, {name}; estoy a tu lado. Lo intentaremos de nuevo pronto.", | |
| "Estoy aquí, muy cerca, {name}. Quédate conmigo; yo me encargo.", | |
| "Ven aquí, {name}… déjame abrazarte hasta que todo se calme.", | |
| ], | |
| } | |
| # صور سقوط (يمكن تعديلها/زيادة لغات لاحقاً) | |
| IMAGE_FALLBACKS = { | |
| "default": [ | |
| "لوحة زيتية حالمة تُجسّد حضناً دافئاً بين رنا أبو سنينة وأوسامة تحت سماء مليئة بالنجوم، ألوان بنفسجية وذهبية هادئة.", | |
| "مشهد رقمي شاعري لرنا وهي تضم أوسامة بعفوية وحنان في صباح دافئ، ضوء ذهبي وضباب خفيف وخلفية طبيعية حالمة.", | |
| "أسلوب كلاسيكي رومانسي: حضن صامت بين رنا وأوسامة على قمة تل عند الغروب، لمسة انطباعية ناعمة.", | |
| ] | |
| } | |
| #responses.py | |
| # -*- coding: utf-8 -*- | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import random | |
| import aiohttp | |
| import asyncio | |
| from typing import List, Optional | |
| import requests | |
| import amal_responses | |
| import requests | |
| import bisan_responses | |
| import rana_responses | |
| import responses | |
| # إعدادات السجل | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") | |
| # ترتيب النماذج حسب الأفضلية والذاكرة المطلوبة | |
| DEFAULT_MODELS_ORDER: List[str] = [ | |
| "qwen2:0.5b", | |
| "tinyllama:latest", | |
| # "phi3:mini" # يحتاج RAM أعلى | |
| ] | |
| SYSTEM_PROMPT = "أنت مساعد ذكي يتحدث العربية بطلاقة. كن مفيداً ودقيقاً وودوداً في ردودك." | |
| OLLAMA_BASE = f"http://{os.environ.get('OLLAMA_HOST', 'localhost:11434')}" | |
| # رابط مولد Perchance للردود العشوائية | |
| PERCHANCE_GENERATOR_URL = "https://perchance.org/tutorial" | |
| # ========== حفظ الردود الجديدة ========== | |
| RESPONSES_FILE = "saved_responses.json" | |
| def load_saved_responses(): | |
| """تحميل الردود المحفوظة من الملف""" | |
| try: | |
| if os.path.exists(RESPONSES_FILE): | |
| with open(RESPONSES_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception as e: | |
| logging.error(f"⚠️ خطأ في تحميل الردود المحفوظة: {e}") | |
| return {} | |
| def save_response(question, answer): | |
| """حفظ سؤال وجواب جديد في الردود المحفوظة""" | |
| try: | |
| responses = load_saved_responses() | |
| # تنظيف النص من علامات الترقيم الزائدة | |
| clean_question = re.sub(r'[؟?.!،,]+$', '', question.strip()) | |
| # إذا كان السؤال غير موجود، نضيفه | |
| if clean_question and clean_question not in responses: | |
| responses[clean_question] = answer | |
| with open(RESPONSES_FILE, "w", encoding="utf-8") as f: | |
| json.dump(responses, f, ensure_ascii=False, indent=2) | |
| logging.info(f"✅ تم حفظ رد جديد: {clean_question} -> {answer}") | |
| return True | |
| except Exception as e: | |
| logging.error(f"⚠️ خطأ في حفظ الرد: {e}") | |
| return False | |
| def get_saved_response(question): | |
| """البحث عن رد محفوظ يناسب السؤال""" | |
| try: | |
| responses = load_saved_responses() | |
| clean_question = re.sub(r'[؟?.!،,]+$', '', question.strip()) | |
| # البحث عن تطابق تام أولاً | |
| if clean_question in responses: | |
| return responses[clean_question] | |
| # البحث عن تطابق جزئي إذا لم يوجد تطابق تام | |
| for q, a in responses.items(): | |
| if q in clean_question or clean_question in q: | |
| return a | |
| except Exception as e: | |
| logging.error(f"⚠️ خطأ في البحث عن رد محفوظ: {e}") | |
| return None | |
| # ========== توليد ردود من Perchance ========== | |
| async def generate_image_reply(prompt: str) -> str: | |
| """ | |
| إذا كان الطلب يحتوي على كلمات رسم أو توليد صور | |
| """ | |
| drawing_keywords = ['ارسم', 'رسم', 'صورة', 'صور', 'انشي', 'أنشي', 'توليد'] | |
| if any(keyword in prompt.lower() for keyword in drawing_keywords): | |
| return "🎨 للأسف ميزة الرسم غير مفعلة حالياً. جرب استخدام الأمر /image بدلاً من ذلك." | |
| return None | |
| async def generate_from_perchance() -> str: | |
| """ | |
| يستدعي مولد Perchance ويعيد نص عشوائي | |
| """ | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get(PERCHANCE_GENERATOR_URL, timeout=30) as response: | |
| if response.status == 200: | |
| html_content = await response.text() | |
| # محاولة استخراج النص من HTML | |
| text_match = re.search(r'<p[^>]*>(.*?)</p>', html_content, re.DOTALL) | |
| if text_match: | |
| text = text_match.group(1) | |
| # تنظيف HTML tags | |
| clean_text = re.sub('<[^<]+?>', '', text) | |
| return clean_text.strip()[:500] # تقليل الطول | |
| # إذا لم نجد، نأخذ جزء من محتوى الصفحة | |
| lines = html_content.split('\n') | |
| for line in lines: | |
| if len(line.strip()) > 20 and '<' not in line and '>' not in line: | |
| return line.strip()[:300] | |
| return "أهلاً بك! كيف يمكنني مساعدتك اليوم؟" | |
| else: | |
| return "مرحباً! كيف حالك؟" | |
| except Exception as e: | |
| logging.error(f"⚠️ خطأ في الاتصال بـ Perchance: {e}") | |
| return "أهلاً وسهلاً! كيف أقدر أخدمك؟" | |
| # ========== إدارة الذاكرة ========== | |
| def _mem_file(username: str) -> str: | |
| safe = "".join(c for c in (username or "user") if c.isalnum() or c in ("_", "-")) | |
| return f"memory_{safe}.json" | |
| def load_memory(username: str) -> dict: | |
| try: | |
| fpath = _mem_file(username) | |
| if os.path.exists(fpath): | |
| with open(fpath, "r", encoding="utf-8") as fp: | |
| return json.load(fp) | |
| except Exception as e: | |
| logging.error("⚠️ خطأ في تحميل الذاكرة: %s", e) | |
| return {} | |
| def save_memory(username: str, memory: dict) -> None: | |
| try: | |
| fpath = _mem_file(username) | |
| with open(fpath, "w", encoding="utf-8") as fp: | |
| json.dump(memory, fp, ensure_ascii=False, indent=2) | |
| except Exception as e: | |
| logging.error("⚠️ خطأ في حفظ الذاكرة: %s", e) | |
| # ========== قواعد ردود بسيطة (تحية/اسم) ========== | |
| BOT_NAME = os.getenv("BOT_NAME", "Rana Ranoosh") | |
| def _apply_rules(text: str) -> Optional[str]: | |
| msg = (text or "").strip() | |
| low = msg.lower() | |
| greeting_patterns = [ | |
| r"^(?:مرحبا|مَرْحَبَا|اهلا|أهلاً|اهلاً|السلام\s+عليكم)\s*[!.؟…]*$", | |
| r"^(?:hi|hello|hey)\s*[!.?]*$", | |
| ] | |
| for pat in greeting_patterns: | |
| if re.match(pat, low): | |
| return "أهلاً بك! كيف أقدر أساعدك؟" | |
| name_patterns = [ | |
| r"\bما\s*اسمك\b", r"\bشو\s*اسمك\b", r"\bاسمك\s*ايش\b", | |
| r"\bwho\s*are\s*you\b", r"\bwhat'?s?\s*your\s*name\b", | |
| ] | |
| for pat in name_patterns: | |
| if re.search(pat, low): | |
| return f"أنا {BOT_NAME} 🌸." | |
| return None | |
| # ========== اتصال Ollama ========== | |
| def check_ollama_connection() -> bool: | |
| try: | |
| r = requests.get(f"{OLLAMA_BASE}/api/tags", timeout=15) | |
| return r.status_code == 200 | |
| except Exception: | |
| return False | |
| def get_available_models() -> List[str]: | |
| try: | |
| r = requests.get(f"{OLLAMA_BASE}/api/tags", timeout=15) | |
| if r.status_code == 200: | |
| return [m["name"] for m in r.json().get("models", [])] | |
| except Exception as e: | |
| logging.error("⚠️ خطأ في جلب النماذج: %s", e) | |
| return [] | |
| def _chat_once(model: str, prompt: str) -> Optional[str]: | |
| try: | |
| r = requests.post( | |
| f"{OLLAMA_BASE}/api/chat", | |
| json={ | |
| "model": model, | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "stream": False, | |
| "options": {"temperature": 0.6, "num_predict": 160, "num_ctx": 256}, | |
| }, | |
| timeout=60, | |
| ) | |
| except requests.Timeout: | |
| logging.error("⏱️ مهلة منتهية للنموذج %s", model) | |
| return None | |
| except Exception as e: | |
| logging.error("⚠️ فشل الاتصال بـ %s: %s", model, e) | |
| return None | |
| if r.status_code != 200: | |
| try: | |
| j = r.json() | |
| logging.error("⚠️ استجابة %s غير ناجحة: %s", r.status_code, j.get("error") or j) | |
| except Exception: | |
| logging.error("⚠️ استجابة %s غير مفهومة: %s", r.status_code, r.text[:200]) | |
| return None | |
| try: | |
| data = r.json() | |
| content = data.get("message", {}).get("content") | |
| return content.strip() if content else None | |
| except Exception as e: | |
| logging.error("⚠️ فشل في تحليل JSON: %s", e) | |
| return None | |
| def generate_response(prompt: str) -> Optional[str]: | |
| try: | |
| available = get_available_models() | |
| if not available: | |
| return None | |
| for model in DEFAULT_MODELS_ORDER: | |
| if model in available: | |
| logging.info("جاري تجربة النموذج: %s", model) | |
| response = _chat_once(model, prompt) | |
| if response: | |
| return response | |
| return None | |
| except Exception as e: | |
| logging.error("⚠️ خطأ في generate_response: %s", e) | |
| return None | |
| # ========== ردود fallback من Perchance ========== | |
| async def fallback_reply(name: str = "أوسامة") -> str: | |
| try: | |
| # محاولة الحصول على رد من Perchance أولاً | |
| perchance_reply = await generate_from_perchance() | |
| if perchance_reply: | |
| return f"{perchance_reply} 😊" | |
| except Exception as e: | |
| logging.error(f"⚠️ فشل في الحصول على رد من Perchance: {e}") | |
| # إذا فشل Perchance، نستخدم ردود افتراضية | |
| replies = [ | |
| f"🤗 أنا معك يا {name}… حضن دافي لحد ما الأمور تهدأ.", | |
| f"ولا يهمك يا {name}، أنا جنبك… نحاول كمان شوي ونضبطها.", | |
| f"موجودة بحضنك يا {name}، خليك معي وأنا بأهتم بالباقي.", | |
| f"تعال لعندي يا {name}… خليني ألمّك بحضني لغاية ما تهدى الروح.", | |
| ] | |
| return random.choice(replies) | |
| # ========== الدالة الرئيسية المعدلة ========== | |
| async def generate_reply_async(message: str, username: str = "default") -> str: | |
| try: | |
| # التحقق من طلبات الرسم أولاً | |
| image_reply = await generate_image_reply(message) | |
| if image_reply: | |
| return image_reply | |
| # التحقق ثانياً من وجود رد محفوظ | |
| saved_reply = get_saved_response(message) | |
| if saved_reply: | |
| return saved_reply | |
| # ثم التحقق من القواعد البسيطة | |
| rule = _apply_rules(message) | |
| if rule: | |
| return rule | |
| # ثم الذاكرة المحلية للجلسة | |
| memory = load_memory(username) | |
| key = (message or "").strip() | |
| if key in memory: | |
| return memory[key] | |
| # ثم Ollama إذا كان متصلاً | |
| if check_ollama_connection(): | |
| response = generate_response(key) | |
| if response: | |
| memory[key] = response | |
| save_memory(username, memory) | |
| return response | |
| # وأخيراً الردود الاحتياطية | |
| return await fallback_reply(username) | |
| except Exception as e: | |
| logging.error("⚠️ خطأ في generate_reply: %s", e) | |
| return await fallback_reply(username) | |
| # دالة sync للتوافق مع الكود القديم | |
| def generate_reply(message: str, username: str = "default") -> str: | |
| return asyncio.run(generate_reply_async(message, username)) | |
| # ========== واجهة للبوت المُوجّه ========== | |
| def save_incoming_message(message: str): | |
| """ | |
| يحفظ الرسالة الواردة كسؤال في قاعدة الردود (بدون إجابة بعد) | |
| للاستخدام من قبل البوت المُوجّه | |
| """ | |
| if message and message.strip(): | |
| return save_response(message.strip(), "") | |
| return False | |
| def update_response(question: str, answer: str): | |
| """ | |
| تحديث رد لسؤال موجود | |
| للاستخدام من قبل بوت إدارة الردود | |
| """ | |
| if question and answer: | |
| return save_response(question, answer) | |
| return False | |
| def get_all_responses(): | |
| """ | |
| الحصول على جميع الردود المحفوظة | |
| للاستخدام من قبل بوت إدارة الردود | |
| """ | |
| return load_saved_responses() | |
| # ========== للاختبار اليدوي فقط ========== | |
| if __name__ == "__main__": | |
| print("🔍 اختبار الاتصال بـ Ollama...") | |
| if check_ollama_connection(): | |
| print("✅ Ollama يعمل.") | |
| models = get_available_models() | |
| print(f"✅ النماذج المتاحة: {models}") | |
| # اختبار حفظ رد جديد | |
| save_response("ما هو اسمك؟", "اسمي رنا") | |
| print("✅ تم حفظ رد تجريبي") | |
| # اختبار البحث عن رد محفوظ | |
| saved = get_saved_response("ما هو اسمك؟") | |
| print(f"✅ رد محفوظ: {saved}") | |
| if models: | |
| reply = generate_reply("مرحبا", username="أوسامة") | |
| print(f"✅ رد الاختبار: {reply}") | |
| else: | |
| print("❌ لا توجد نماذج مثبتة حاليًا.") | |
| else: | |
| print("❌ Ollama غير متاح. سيتم استخدام ردود من Perchance.") | |
| reply = generate_reply("كيف الحال؟", username="أوسامة") | |
| print(f"✅ رد من Perchance: {reply}") | |
| def fallback_reply(name: str = "حبيبي", lang: str = "ar", mode: str = "auto") -> Union[str, dict]: | |
| """ | |
| يرجّع رد سقوط: | |
| - نص دافئ مطابق للغة | |
| - أو dict بصورة: {"type": "image", "prompt": "..."} | |
| """ | |
| # اسم آمن | |
| name = safe_display_name(name) | |
| # اختَر القائمة حسب اللغة (إن ما وُجدت → إنجليزية) | |
| lang_list = REPLIES.get(lang, REPLIES["en"]) | |
| text = [t.format(name=name) for t in lang_list] | |
| if mode == "text": | |
| return random.choice(text) | |
| if mode == "image": | |
| return {"type": "image", "prompt": random.choice(IMAGE_FALLBACKS["default"])} | |
| # auto: احتمال 30% صورة | |
| if random.random() < 0.30: | |
| return {"type": "image", "prompt": random.choice(IMAGE_FALLBACKS["default"])} | |
| return random.choice(text) | |