""" memory_cloud.py — conversation long-term memory for Jarvis. Backend: Firebase Firestore (cloud only). No local fallback — if Firestore is unavailable, remember() reports failure and recall() returns nothing. """ import os import time _firestore = None _firestore_failed = False def _credentials_path(): p = os.environ.get("FIREBASE_CREDENTIALS_PATH", "").strip() if p: return p here = os.path.dirname(os.path.abspath(__file__)) return os.path.join(here, "firebase_service_account.json") def _init_firestore(): """Lazily init Firestore once. Returns client or None.""" global _firestore, _firestore_failed if _firestore is not None: return _firestore if _firestore_failed: return None try: import firebase_admin from firebase_admin import credentials, firestore import json as _json js = (os.environ.get("FIREBASE_CREDENTIALS_JSON") or "").strip() if js: info = _json.loads(js) app = firebase_admin.get_app() if firebase_admin._apps else firebase_admin.initialize_app( credentials.Certificate(info) ) else: path = _credentials_path() if not os.path.exists(path): print("[warn] No firebase_service_account.json - memory disabled (no local fallback).") _firestore_failed = True return None app = firebase_admin.get_app() if firebase_admin._apps else firebase_admin.initialize_app( credentials.Certificate(path) ) _firestore = firestore.client(app) print("[ok] Firebase Firestore connected for memory.") return _firestore except Exception as exc: print(f"[warn] Firestore unavailable ({exc}) - memory disabled.") _firestore_failed = True return None def remember(content: str) -> bool: """Save a fact/interaction to Firestore. False if Firestore unavailable.""" if not content: return False db = _init_firestore() if db is None: return False try: db.collection("jarvis_memory").add({"content": content, "ts": time.time()}) return True except Exception as exc: print(f"[warn] Firestore write failed ({exc}).") return False CHAT_DOC = "shared" CHAT_CAP = 12 def set_pref(key: str, value: str) -> bool: """Cross-channel preference (one doc, never raises).""" try: db = _init_firestore() if db is None: return False import time as _t ref = db.collection("jarvis_prefs").document("user") doc = ref.get() prefs = ((doc.to_dict() or {}).get("prefs") or {}) if doc.exists else {} prefs[key] = (value or "")[:300] ref.set({"prefs": prefs, "updated": _t.time()}) return True except Exception as exc: print(f"[warn] pref save failed ({exc}).") return False def get_prefs() -> dict: try: db = _init_firestore() if db is None: return {} doc = db.collection("jarvis_prefs").document("user").get() if doc.exists: return (doc.to_dict() or {}).get("prefs") or {} except Exception as exc: print(f"[warn] pref load failed ({exc}).") return {} def chat_load(limit: int = 8) -> list: """Shared BAT<->Telegram conversation (list of {r,t,ts,src}). Never raises.""" try: db = _init_firestore() if db is None: return [] doc = db.collection("jarvis_chat").document(CHAT_DOC).get() if doc.exists: msgs = (doc.to_dict() or {}).get("msgs") or [] msgs = sorted(msgs, key=lambda m: m.get("ts", 0)) return msgs[-limit:] except Exception as exc: print(f"[warn] chat load failed ({exc}).") return [] def chat_save(role: str, text: str, src: str = "?") -> bool: """Append one exchange line to the shared conversation. Never raises.""" try: db = _init_firestore() if db is None or not text: return False ref = db.collection("jarvis_chat").document(CHAT_DOC) doc = ref.get() msgs = ((doc.to_dict() or {}).get("msgs") or []) if doc.exists else [] msgs.append({"r": role, "t": text[:500], "ts": time.time(), "src": src}) msgs = sorted(msgs, key=lambda m: m.get("ts", 0))[-CHAT_CAP:] ref.set({"msgs": msgs, "updated": time.time()}) return True except Exception as exc: print(f"[warn] chat save failed ({exc}).") return False def stats() -> dict: """Count memory entries: history (jarvis_memory docs), contacts, chat turns. Never raises — returns zeros when offline.""" out = {"history": 0, "contacts": 0, "turns": 0} try: db = _init_firestore() if db is None: return out try: out["history"] = len(db.collection("jarvis_memory").get()) except Exception: pass try: out["contacts"] = len(db.collection("jarvis_contacts").get()) except Exception: pass try: doc = db.collection("jarvis_chat").document(CHAT_DOC).get() if doc.exists: out["turns"] = len((doc.to_dict() or {}).get("msgs") or []) except Exception: pass except Exception: pass return out def recall(limit: int = 10) -> str: """Return the most recent remembered items as a spoken-friendly string.""" db = _init_firestore() if db is None: return "" try: snaps = ( db.collection("jarvis_memory") .order_by("ts", direction="DESCENDING") .limit(limit) .get() ) parts = [s.to_dict().get("content", "") for s in snaps] return "; ".join(p for p in parts if p) except Exception as exc: print(f"[warn] Firestore read failed ({exc}).") return ""