# noura_server.py import os, time, hmac, traceback from typing import Any, Dict from flask import Flask, request, jsonify from dotenv import load_dotenv import importlib from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError # (اختياري) لو عندك هذه الملفات فعلاً: try: import analyzer, brain, core, knowledge_search, learner, media_analyzer, memory, self_learning, self_improvement # noqa: F401 except Exception as e: print("[WARN] helper modules import issue:", repr(e)) # --- بيئة --- load_dotenv() API_KEY = (os.getenv("NOURA_API_KEY", "changeme") or "").strip() MIN_SIM_DEFAULT = float(os.getenv("MIN_SIMILARITY", "0.82")) ADAPTER_MODULE = os.getenv("SELF_LEARNING_MODULE", "self_learning_adapter") # --- الأداپتر --- sl = importlib.import_module(ADAPTER_MODULE) sl.init({"min_similarity": MIN_SIM_DEFAULT}) # (اختياري) مولّد ردود مخصص try: from telegram_listener import generate_reply as custom_generate_reply except Exception: custom_generate_reply = None # --- Flask --- app = Flask(__name__) # --- ThreadPool للمهل --- EXEC = ThreadPoolExecutor(max_workers=4) def call_with_timeout(fn, *args, timeout=1.8, default=None, label=""): fut = EXEC.submit(fn, *args) try: return fut.result(timeout=timeout) except FuturesTimeoutError: print(f"[TIMEOUT] {label or fn.__name__} > {timeout}s") return default except Exception as e: print(f"[ERR] {label or fn.__name__} crashed:", repr(e)) return default def verify_auth(req) -> bool: key = (req.headers.get("x-api-key") or "").strip() ok = hmac.compare_digest(key, API_KEY) if not ok: print(f"[AUTH FAIL] header={repr(key)} expected={repr(API_KEY)}") return ok @app.route("/health", methods=["GET"]) def health(): return jsonify({"ok": True}), 200 @app.route("/chat", methods=["POST"], endpoint="chat_api") def chat(): try: if not verify_auth(request): return jsonify({"error": "unauthorized"}), 401 t0 = time.time() data: Dict[str, Any] = request.get_json(force=True) or {} user = (data.get("user") or "unknown").strip() text = (data.get("text") or "").strip() meta = data.get("meta") or {} chat_id = str(meta.get("peer_id", "unknown")) ts = int(time.time()) reply = "" used_memory = False # 1) استرجاع من الذاكرة (بمهلة) if text: cand, score = call_with_timeout( sl.find_best, chat_id, text, timeout=1.8, default=(None, 0.0), label="find_best" ) or (None, 0.0) if cand and score >= MIN_SIM_DEFAULT: reply, used_memory = cand, True # 2) مولد مخصص (اختياري) if not reply and custom_generate_reply: r = call_with_timeout(custom_generate_reply, text, timeout=1.5, default="", label="custom_generate_reply") reply = (r or "").strip() # 3) fallback if not reply: low = text.lower() if any(w in low for w in ["سهم", "اسهم", "stocks", "stock"]): reply = "تنبيه: لستُ مستشارًا ماليًا. اكتب سؤالك بدقة وسأساعدك بشكل عام." else: reply = f"وصلتني رسالتك: «{text}». ماذا تريد بعدها؟" # 4) حفظ التعلم (لا يمنع الرد حتى لو تأخر) if text: _ = call_with_timeout(sl.add_example, chat_id, user, text, reply, meta, timeout=1.8, label="add_example") src = "memory" if used_memory else "fallback" print(f"[CHAT] chat_id={chat_id} src={src} dt={(time.time()-t0)*1000:.1f}ms text={text[:60]}") return jsonify({"reply": reply, "ts": ts, "used_memory": used_memory}) except Exception: print("[ERR] /chat top-level:\n", traceback.format_exc()) return jsonify({"error": "internal_error"}), 200 @app.route("/admin/learn", methods=["POST"]) def admin_learn_toggle(): if not verify_auth(request): return jsonify({"error":"unauthorized"}), 401 data = request.get_json(force=True) or {} chat_id = str(data.get("chat_id")) enabled = bool(data.get("enabled", True)) min_sim = data.get("min_similarity") ms = MIN_SIM_DEFAULT if min_sim is None else float(min_sim) try: sl.set_chat_config(chat_id, learn_enabled=enabled, min_similarity=ms) except Exception as e: print("[ERR] set_chat_config:\n", traceback.format_exc()) return jsonify({"ok": True, "learn_enabled": enabled}) @app.route("/admin/stats", methods=["GET"]) def admin_stats(): if not verify_auth(request): return jsonify({"error":"unauthorized"}), 401 chat_id = request.args.get("chat_id") try: c = sl.stats(chat_id) except Exception: print("[ERR] stats:\n", traceback.format_exc()) c = 0 return jsonify({"ok": True, "count": c}) if __name__ == "__main__": app.run(host="0.0.0.0", port=9531)