#!/usr/bin/env python3 import os import json import subprocess import shutil import socket import time HISTORY_PATH = "history.json" # ترتيب تفضيلي للنماذج (سنختار أول نموذج متاح منها) PREFERRED_MODELS = [ "nous-hermes2", # مفضل أولاً إذا كان منصّباً os.getenv("OLLAMA_MODEL", "mistral:instruct"), "mistral:latest", "gemma3:4b", "tinyllama:latest", ] # استيراد آمن لملف responses.py try: from responses import generate_reply as _generate_reply except Exception: def _generate_reply(*args, **kwargs): return None def ensure_ollama(): """ يتحقّق من توفّر ollama CLI (باستخدام --version) ويضمن أن السيرفر شغّال. لو السيرفر غير شغّال، يشغّله وينتظر جاهزيته. """ # استخدم المسار المباشر أولاً لتجنّب أي shadow لملف باسم 'ollama' داخل المشروع win_exe = r"C:\Users\osamawin\AppData\Local\Programs\Ollama\ollama.exe" cli = win_exe if os.path.exists(win_exe) else shutil.which("ollama") if not cli: raise RuntimeError("ollama CLI غير موجود. ثبّته أو أضِفه للـ PATH.") # تأكّد من الـ CLI try: subprocess.run([cli, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) except Exception as e: raise RuntimeError("تعذّر تشغيل 'ollama --version'. تأكّد من التثبيت.") from e # تحقّق من أن السيرفر يستمع؛ استخدم OLLAMA_HOST إن وُجد وإلا الافتراضي 127.0.0.1:11434 host = os.environ.get("OLLAMA_HOST", "127.0.0.1:11434") ip, port = host.split(":") port = int(port) def _is_up(): try: with socket.create_connection((ip, port), timeout=0.8): return True except OSError: return False if _is_up(): return # شغّل السيرفر في الخلفية cmd = [cli, "serve"] if "OLLAMA_HOST" in os.environ: cmd += ["--host", host] subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # انتظر الجاهزية (TCP) for _ in range(60): if _is_up(): return time.sleep(0.2) raise RuntimeError(f"فشل تشغيل ollama serve على {host}.") def list_installed_models(): """ يرجع قائمة أسماء النماذج المنصّبة محلياً عبر 'ollama list'. """ try: out = subprocess.run( ["ollama", "list"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ).stdout except subprocess.CalledProcessError as e: raise RuntimeError(f"خطأ عند قراءة قائمة النماذج: {e.stderr.strip() or e.stdout.strip()}") models = [] for line in out.splitlines(): line = line.strip() if not line or line.startswith("NAME") or line.startswith("-"): continue # السطر يبدأ بـ NAME ثم أعمدة أخرى، ناخذ أول عمود parts = line.split() if parts: models.append(parts[0]) return models def pick_default_model(installed): for m in PREFERRED_MODELS: if m in installed: return m if installed: return installed[0] raise RuntimeError("لا توجد نماذج منصّبة في Ollama. ثبّت نموذجاً أولاً (مثلاً: ollama pull mistral:instruct).") def ollama_generate(model, prompt, timeout=120): """ يستدعي: ollama run "" (بدون -p لأن إصدارك لا يدعمه) """ try: res = subprocess.run( ["ollama", "run", model, prompt], # ← لا تستخدم -p check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, ) out = (res.stdout or "").strip() err = (res.stderr or "").strip() if res.returncode != 0: raise RuntimeError(err or out or "خروج غير صفري من ollama") if not out: raise RuntimeError(f"{model} لم يرجّع أي مخرجات.") return out except subprocess.TimeoutExpired: raise RuntimeError(f"انتهى الوقت المحدد لطلب النموذج ({model}). جرّب لاحقاً أو خفّض طول البرومبت.") def load_history(): if os.path.exists(HISTORY_PATH): with open(HISTORY_PATH, "r", encoding="utf-8") as f: return json.load(f) return [] def save_history(history): with open(HISTORY_PATH, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) def simulate_server_scan(): print("نورا: أبحث عن خوادم...") fake_servers = ["192.168.1.5", "192.168.1.10", "192.168.1.20"] for server in fake_servers: print(f"نورا: تم العثور على خادم مفتوح في {server}") def format_chat_prompt(history, user_utterance): """ نبني برومبت بسيط يحافظ على سياق مختصر. يمكنك تطويره لاحقاً لتنسيق ChatML أو JSON حسب النموذج. """ system = "أنت المساعدة نورا. تحدثي بلغة عربية فصحى بسيطة." lines = [f"system: {system}"] for msg in history[-6:]: # آخر 6 رسائل فقط لتقليل الطول role = msg.get("role", "user") content = msg.get("content", "") lines.append(f"{role}: {content}") lines.append(f"user: {user_utterance}") lines.append("assistant:") return "\n".join(lines) def chat(): ensure_ollama() installed = list_installed_models() active_model = pick_default_model(installed) chat_history = load_history() print(f""" نظام نورا الذكي (Ollama) النموذج الحالي: {active_model} أوامر خاصة: - /models : عرض النماذج المنصّبة - /model NAME : تبديل النموذج (مثال: /model mistral:instruct) - scan : مسح الشبكة (محاكاة) - خروج | exit | quit : إنهاء المحادثة """) while True: try: user_input = input("أنت: ").strip() if not user_input: continue low = user_input.lower() if low in ["خروج", "exit", "quit"]: break if low == "scan": simulate_server_scan() continue if low == "/models": print("النماذج المتاحة محلياً:") for m in installed: print(" -", m) continue if low.startswith("/model"): # صيغة: /model NAME parts = user_input.split(maxsplit=1) if len(parts) == 1: print(f"النموذج الحالي: {active_model}") continue candidate = parts[1].strip() if candidate not in installed: print(f"⚠️ النموذج '{candidate}' غير منصّب. النماذج المتاحة: {', '.join(installed)}") continue active_model = candidate print(f"✅ تم تبديل النموذج إلى: {active_model}") continue # أولاً: ردود مخصصة من responses.py إن توفرت custom_reply = None try: r = _generate_reply(user_input, username="أسامة") # تجاهل رسالة الخطأ الجاهزة كي لا توقف تدفق الرد من النموذج if r and not r.strip().startswith("عذراً، حدث خطأ"): custom_reply = r except Exception: custom_reply = None if custom_reply is not None: print("نورا:", custom_reply) chat_history.append({"role": "user", "content": user_input}) chat_history.append({"role": "assistant", "content": custom_reply}) if len(chat_history) % 3 == 0: save_history(chat_history) continue # إذا لا يوجد رد مخصص → نستخدم النموذج النشط عبر Ollama chat_history.append({"role": "user", "content": user_input}) prompt = format_chat_prompt(chat_history, user_input) print("نورا: أفكر... (", active_model, ")") try: model_reply = ollama_generate(active_model, prompt) except RuntimeError as e: # فشل؟ جرّب بدائل بالتتابع مع طباعة سبب الفشل print(f"⚠️ فشل مع {active_model}: {e}\n🔁 أجرب بدائل...") fallback = None for m in PREFERRED_MODELS: if m in installed and m != active_model: try: print(f"→ تجربة {m} ...") model_reply = ollama_generate(m, prompt) fallback = m break except Exception as ee: print(f" × فشل {m}: {ee}") continue if fallback is None: print("نورا: حدث خطأ:", str(e)) continue else: active_model = fallback print(f"✅ تم التبديل تلقائياً إلى: {active_model}") # غالباً المخرجات ستكون مجرد نص رد assistant_response = model_reply.strip() print("نورا:", assistant_response) chat_history.append({"role": "assistant", "content": assistant_response}) # احفظ كل 3 رسائل لتقليل الكتابة if len(chat_history) % 3 == 0: save_history(chat_history) except KeyboardInterrupt: print("\nنورا: تم إنهاء المحادثة.") break except Exception as e: print(f"نورا: حدث خطأ: {str(e)}") continue # حفظ السجل النهائي عند الخروج save_history(chat_history) if __name__ == "__main__": chat()