File size: 6,013 Bytes
9ae78b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fd89cd
 
 
 
0c725f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fd89cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f5ba86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ae78b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
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 ""