File size: 18,847 Bytes
ebd5147
970f0b0
 
e8f0276
ec18391
e8f0276
ebd5147
 
e8f0276
970f0b0
498a56c
970f0b0
 
ec18391
970f0b0
 
e8f0276
 
 
ec18391
970f0b0
 
e8f0276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
970f0b0
 
 
e8f0276
970f0b0
e8f0276
 
 
ec18391
e8f0276
 
 
 
 
 
 
 
970f0b0
 
e8f0276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
970f0b0
e8f0276
 
 
 
 
 
ec18391
e8f0276
 
 
 
970f0b0
e8f0276
 
 
970f0b0
 
ec18391
 
e8f0276
 
970f0b0
 
ec18391
970f0b0
e8f0276
 
ec18391
e8f0276
 
 
 
 
 
 
 
 
 
 
 
 
ec18391
 
e8f0276
 
 
 
 
 
 
ec18391
 
e8f0276
ebd5147
ec18391
 
e8f0276
ebd5147
970f0b0
 
e8f0276
 
 
 
 
 
 
 
ec18391
e8f0276
 
ec18391
e8f0276
ec18391
 
e8f0276
ec18391
 
 
 
 
 
 
 
 
 
 
e8f0276
 
 
 
 
 
 
 
 
 
360dd57
ec18391
e8f0276
ebd5147
970f0b0
e8f0276
ec18391
 
970f0b0
 
e8f0276
970f0b0
e8f0276
970f0b0
e8f0276
970f0b0
e8f0276
970f0b0
e8f0276
970f0b0
e8f0276
 
 
 
 
970f0b0
 
e8f0276
970f0b0
e8f0276
970f0b0
 
e8f0276
 
970f0b0
e8f0276
 
ec18391
e8f0276
 
 
 
 
ec18391
970f0b0
ec18391
e8f0276
970f0b0
ec18391
970f0b0
 
e8f0276
970f0b0
e8f0276
 
970f0b0
ec18391
 
e8f0276
ec18391
970f0b0
 
e8f0276
 
ec18391
e8f0276
ec18391
e8f0276
 
 
 
 
ec18391
 
e8f0276
 
ec18391
e8f0276
970f0b0
 
 
ec18391
e8f0276
 
 
 
ec18391
e8f0276
 
ec18391
 
e8f0276
ec18391
 
e8f0276
 
ec18391
e8f0276
970f0b0
ec18391
 
e8f0276
ec18391
e8f0276
 
 
 
ec18391
e8f0276
970f0b0
e8f0276
 
ec18391
e8f0276
ec18391
 
e8f0276
970f0b0
ec18391
970f0b0
e8f0276
 
 
ec18391
 
 
 
e8f0276
 
970f0b0
ec18391
e8f0276
 
ec18391
e8f0276
970f0b0
e8f0276
ec18391
e8f0276
ec18391
360dd57
970f0b0
e8f0276
 
970f0b0
e8f0276
970f0b0
e8f0276
 
ec18391
e8f0276
 
970f0b0
ec18391
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
VERITAS Integrated Memory System
==================================
CogniVault (RAG) + Aluna Memory (LTM) โ€” unified HuggingFace Space
Rob "The Sounds Guy" Barenbrug | Built by VERITAS
Stack: Python 3.11 + Gradio 5.15.0
"""

import os, json, sqlite3, hashlib, datetime, re, logging
from pathlib import Path

import gradio as gr

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("veritas")

# โ”€โ”€โ”€ STORAGE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
DB_PATH  = Path("/data/veritas.db")  if Path("/data").exists() else Path("veritas.db")
DATA_DIR = Path("/data/uploads")     if Path("/data").exists() else Path("uploads")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def init_db():
    with sqlite3.connect(str(DB_PATH)) as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS memories (
                id           TEXT PRIMARY KEY,
                content      TEXT NOT NULL,
                category     TEXT DEFAULT 'general',
                importance   INTEGER DEFAULT 5,
                source       TEXT DEFAULT 'manual',
                created_at   TEXT DEFAULT (datetime('now')),
                updated_at   TEXT DEFAULT (datetime('now')),
                access_count INTEGER DEFAULT 0
            );
            CREATE TABLE IF NOT EXISTS knowledge (
                id          TEXT PRIMARY KEY,
                title       TEXT,
                content     TEXT NOT NULL,
                source      TEXT DEFAULT 'manual',
                tags        TEXT DEFAULT '[]',
                doc_type    TEXT DEFAULT 'text',
                chunk_index INTEGER DEFAULT 0,
                parent_id   TEXT,
                created_at  TEXT DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_mem_cat  ON memories(category);
            CREATE INDEX IF NOT EXISTS idx_mem_imp  ON memories(importance DESC);
            CREATE INDEX IF NOT EXISTS idx_know_src ON knowledge(source);
        """)
init_db()

def make_id(s: str) -> str:
    return hashlib.sha256((s + datetime.datetime.utcnow().isoformat()).encode()).hexdigest()[:20]

# โ”€โ”€โ”€ MEMORY OPS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def store_memory(content, category="general", importance=5, source="manual"):
    if not str(content).strip():
        return {"error": "Content cannot be empty"}
    with sqlite3.connect(str(DB_PATH)) as conn:
        row = conn.execute("SELECT id FROM memories WHERE content=?", (content,)).fetchone()
        if row:
            conn.execute("UPDATE memories SET access_count=access_count+1, updated_at=datetime('now') WHERE id=?", (row[0],))
            return {"id": row[0], "status": "deduplicated", "message": "Already stored โ€” access count bumped"}
        mid = make_id(content)
        conn.execute("INSERT INTO memories (id, content, category, importance, source) VALUES (?,?,?,?,?)",
                     (mid, content, category, int(importance), source))
    return {"id": mid, "status": "stored", "category": category, "importance": importance}

def search_memories(query, category=None, limit=20):
    q = f"%{query.lower()}%"
    with sqlite3.connect(str(DB_PATH)) as conn:
        if category and category != "all":
            rows = conn.execute(
                "SELECT id,content,category,importance,created_at,access_count FROM memories "
                "WHERE lower(content) LIKE ? AND category=? ORDER BY importance DESC,access_count DESC LIMIT ?",
                (q, category, limit)).fetchall()
        else:
            rows = conn.execute(
                "SELECT id,content,category,importance,created_at,access_count FROM memories "
                "WHERE lower(content) LIKE ? ORDER BY importance DESC,access_count DESC LIMIT ?",
                (q, limit)).fetchall()
        ids = [r[0] for r in rows]
        if ids:
            conn.execute(f"UPDATE memories SET access_count=access_count+1 WHERE id IN ({','.join('?'*len(ids))})", ids)
    return [{"id":r[0],"content":r[1],"category":r[2],"importance":r[3],"created_at":r[4],"access_count":r[5]} for r in rows]

def get_all_memories(limit=100):
    with sqlite3.connect(str(DB_PATH)) as conn:
        rows = conn.execute(
            "SELECT id,content,category,importance,created_at,access_count FROM memories "
            "ORDER BY importance DESC,updated_at DESC LIMIT ?", (limit,)).fetchall()
    return [{"id":r[0],"content":r[1],"category":r[2],"importance":r[3],"created_at":r[4],"access_count":r[5]} for r in rows]

# โ”€โ”€โ”€ KNOWLEDGE OPS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def chunk_text(text, size=800, overlap=100):
    paras = [p.strip() for p in re.split(r'\n\n+', text) if p.strip()]
    chunks, cur = [], ""
    for p in paras:
        if len(cur) + len(p) + 2 < size:
            cur = (cur + "\n\n" + p).strip() if cur else p
        else:
            if cur: chunks.append(cur)
            cur = p
    if cur: chunks.append(cur)
    final = []
    for c in chunks:
        if len(c) > size * 1.5:
            for i in range(0, len(c), size - overlap):
                piece = c[i:i+size]
                if piece.strip(): final.append(piece)
        else:
            final.append(c)
    return final or [text]

def ingest_text(title, content, source="manual", tags_str="", doc_type="text"):
    if not str(content).strip():
        return {"error": "Content is empty"}
    tags = [t.strip() for t in str(tags_str).split(",") if t.strip()]
    chunks = chunk_text(str(content))
    parent_id = make_id(str(title) + str(content)[:80])
    with sqlite3.connect(str(DB_PATH)) as conn:
        for i, chunk in enumerate(chunks):
            kid = make_id(chunk + str(i))
            conn.execute(
                "INSERT OR REPLACE INTO knowledge (id,title,content,source,tags,doc_type,chunk_index,parent_id) VALUES (?,?,?,?,?,?,?,?)",
                (kid, str(title), chunk, source, json.dumps(tags), doc_type, i, parent_id))
    return {"parent_id": parent_id, "title": str(title), "chunks": len(chunks),
            "chars": len(str(content)), "source": source, "tags": tags, "status": "ingested"}

def ingest_file(file_obj, source="upload"):
    if file_obj is None:
        return {"error": "No file provided"}
    # Gradio 5 returns filepath string or dict
    if isinstance(file_obj, dict):
        filepath = file_obj.get("name", file_obj.get("path", ""))
        filename = file_obj.get("orig_name", Path(filepath).name)
    else:
        filepath = str(file_obj)
        filename = Path(filepath).name
    ext = Path(filename).suffix.lower()
    if ext not in [".txt", ".md"]:
        return {"error": f"Unsupported: {ext}. Use .txt or .md"}
    try:
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            content = f.read()
        return ingest_text(filename, content, source=source, tags_str=f"{ext.strip('.')},{source}", doc_type=ext.strip("."))
    except Exception as e:
        return {"error": str(e)}

def search_knowledge(query, limit=20):
    q = f"%{query.lower()}%"
    with sqlite3.connect(str(DB_PATH)) as conn:
        rows = conn.execute(
            "SELECT id,title,content,source,tags,doc_type,created_at FROM knowledge "
            "WHERE lower(content) LIKE ? OR lower(title) LIKE ? ORDER BY created_at DESC LIMIT ?",
            (q, q, limit)).fetchall()
    return [{"id":r[0],"title":r[1],"content":r[2],"source":r[3],"tags":json.loads(r[4]),"doc_type":r[5],"created_at":r[6]} for r in rows]

def rag_query(question):
    if not str(question).strip():
        return "Enter a question above."
    know = search_knowledge(question, 6)
    mems = search_memories(question, limit=4)
    if not know and not mems:
        return "โŒ No context found. Add documents via ๐Ÿ“š Knowledge or memories via ๐Ÿง  Memory tab."
    parts = []
    if know:
        parts.append("## Knowledge Base\n")
        for r in know[:5]:
            parts.append(f"**[{r['title']}]** (source: {r['source']})\n{r['content'][:500]}\n---")
    if mems:
        parts.append("\n## Memory Context\n")
        for m in mems[:3]:
            parts.append(f"**[{m['category']} | importance {m['importance']}]**\n{m['content'][:300]}\n---")
    return "\n".join(parts)

def get_stats():
    with sqlite3.connect(str(DB_PATH)) as conn:
        mc = conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
        kc = conn.execute("SELECT COUNT(*) FROM knowledge").fetchone()[0]
        cats = dict(conn.execute("SELECT category,COUNT(*) FROM memories GROUP BY category").fetchall())
        srcs = dict(conn.execute("SELECT source,COUNT(*) FROM knowledge GROUP BY source").fetchall())
        top  = conn.execute("SELECT content,importance,access_count FROM memories ORDER BY importance DESC,access_count DESC LIMIT 5").fetchall()
    return {"total_memories": mc, "total_knowledge_chunks": kc, "categories": cats,
            "knowledge_sources": srcs,
            "top_memories": [{"content": r[0][:120], "importance": r[1], "access_count": r[2]} for r in top]}

# โ”€โ”€โ”€ MCP JSON API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def mcp_api(request_json):
    try:
        req = json.loads(request_json)
    except:
        return json.dumps({"error": "Invalid JSON"}, indent=2)
    tool   = req.get("tool", "")
    params = req.get("params", {})
    if tool == "store_memory":
        result = store_memory(params.get("content",""), params.get("category","general"), int(params.get("importance",5)), params.get("source","claude"))
    elif tool == "search_memories":
        result = search_memories(params.get("query",""), params.get("category"), int(params.get("limit",20)))
    elif tool == "store_knowledge":
        result = ingest_text(params.get("title","Untitled"), params.get("content",""), params.get("source","claude"), params.get("tags",""), params.get("doc_type","text"))
    elif tool == "search_knowledge":
        result = search_knowledge(params.get("query",""), int(params.get("limit",20)))
    elif tool == "rag_query":
        result = {"context": rag_query(params.get("question",""))}
    elif tool == "get_session_context":
        result = {"user":"Rob 'The Sounds Guy' Barenbrug","location":"Durban, South Africa",
                  "device":"Huawei Pura 80 Pro (Termux)","vps":"veritas.alunaafrica.cloud",
                  "constraint":"CANNOT manually code โ€” click-and-run only",
                  "philosophy":"Live in truth, never in comfort",
                  "stats": get_stats(), "recent_memories": get_all_memories(10)}
    elif tool == "status":
        result = get_stats()
        result.update({"system":"VERITAS Integrated Memory","version":"1.0.0","status":"OPERATIONAL","stack":"Python 3.11 + Gradio 5.15.0"})
    else:
        result = {"error": f"Unknown tool: '{tool}'", "available": ["store_memory","search_memories","store_knowledge","search_knowledge","rag_query","get_session_context","status"]}
    return json.dumps(result, indent=2, default=str)

# โ”€โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
CATS = ["general","project","technical","personal","rib-rage","bytebot","cognivault","aluna","android","audio","business","mene-portal"]

with gr.Blocks(title="VERITAS Memory", theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
               css="footer{display:none!important}.header{text-align:center;padding:16px 0 6px}") as demo:

    gr.HTML("""<div class="header">
        <h1 style="color:#60a5fa;font-size:1.9em;margin:0">๐Ÿ”ท VERITAS Memory System</h1>
        <p style="color:#94a3b8;margin:4px 0">CogniVault RAG + Aluna Memory LTM โ€” Unified</p>
        <p style="color:#475569;font-size:0.85em">Rob "The Sounds Guy" Barenbrug &nbsp;|&nbsp; <em>Live in truth, never in comfort</em></p>
    </div>""")

    with gr.Tabs():

        # TAB 1 โ€” MEMORY
        with gr.Tab("๐Ÿง  Memory"):
            gr.Markdown("### Store long-term memories")
            with gr.Row():
                with gr.Column(scale=3):
                    t_content = gr.Textbox(label="Memory Content", placeholder="What should be rememberedโ€ฆ", lines=3)
                with gr.Column(scale=1):
                    t_cat = gr.Dropdown(CATS, value="general", label="Category")
                    t_imp = gr.Slider(1, 10, value=5, step=1, label="Importance")
            with gr.Row():
                t_store_btn = gr.Button("๐Ÿ’พ Store Memory", variant="primary")
                t_clear_btn = gr.Button("Clear")
            t_store_out = gr.JSON(label="Result")

            gr.Markdown("---\n### Search Memories")
            with gr.Row():
                t_sq   = gr.Textbox(label="Search", placeholder="Search memoriesโ€ฆ", scale=3)
                t_scat = gr.Dropdown(["all"] + CATS, value="all", label="Filter", scale=1)
                t_sbtn = gr.Button("๐Ÿ” Search", variant="primary", scale=1)
            t_sout = gr.Dataframe(headers=["content","category","importance","access_count","created_at"], label="Results", wrap=True)

            def fn_store(c, cat, imp):
                return store_memory(c, cat, imp), ""
            def fn_search_m(q, cat):
                r = search_memories(q, None if cat=="all" else cat)
                return [[x["content"][:200],x["category"],x["importance"],x["access_count"],x["created_at"]] for x in r] if r else []

            t_store_btn.click(fn_store, [t_content, t_cat, t_imp], [t_store_out, t_content])
            t_clear_btn.click(lambda: ("", None), outputs=[t_content, t_store_out])
            t_sbtn.click(fn_search_m, [t_sq, t_scat], t_sout)

        # TAB 2 โ€” KNOWLEDGE
        with gr.Tab("๐Ÿ“š Knowledge"):
            gr.Markdown("### Ingest documents into CogniVault RAG")
            with gr.Tabs():
                with gr.Tab("๐Ÿ“ Paste Text"):
                    k_title   = gr.Textbox(label="Title", placeholder="Bytebot Architecture Notes")
                    k_content = gr.Textbox(label="Content", lines=7, placeholder="Paste text, notes, WhatsApp exportsโ€ฆ")
                    k_source  = gr.Textbox(label="Source", value="manual")
                    k_tags    = gr.Textbox(label="Tags (comma-separated)", placeholder="bytebot, deployment")
                    k_btn     = gr.Button("๐Ÿ“ฅ Ingest", variant="primary")
                    k_out     = gr.JSON(label="Result")
                    k_btn.click(lambda ti,co,so,ta: ingest_text(ti,co,so,ta), [k_title,k_content,k_source,k_tags], k_out)

                with gr.Tab("๐Ÿ“ Upload File"):
                    f_file   = gr.File(label="Upload .txt or .md", file_types=[".txt",".md"])
                    f_source = gr.Textbox(label="Source", value="upload")
                    f_btn    = gr.Button("๐Ÿ“ฅ Ingest File", variant="primary")
                    f_out    = gr.JSON(label="Result")
                    f_btn.click(ingest_file, [f_file, f_source], f_out)

            gr.Markdown("---\n### Search Knowledge")
            with gr.Row():
                ks_q   = gr.Textbox(label="Search Knowledge", placeholder="Search docsโ€ฆ", scale=3)
                ks_btn = gr.Button("๐Ÿ” Search", variant="primary", scale=1)
            ks_out = gr.Dataframe(headers=["title","content","source","doc_type","created_at"], label="Knowledge Results", wrap=True)

            def fn_sk(q):
                r = search_knowledge(q)
                return [[x["title"],x["content"][:250],x["source"],x["doc_type"],x["created_at"]] for x in r] if r else []
            ks_btn.click(fn_sk, ks_q, ks_out)

        # TAB 3 โ€” RAG
        with gr.Tab("๐Ÿ”ฎ RAG Query"):
            gr.Markdown("### Ask a question โ€” get context to paste into Claude")
            r_q   = gr.Textbox(label="Question", placeholder="What is the Bytebot deployment process?", lines=2)
            r_btn = gr.Button("๐Ÿ”ฎ Retrieve Context", variant="primary")
            r_out = gr.Textbox(label="Retrieved Context โ€” copy โ†’ paste into Claude", lines=16, show_copy_button=True)
            r_btn.click(rag_query, r_q, r_out)

        # TAB 4 โ€” DASHBOARD
        with gr.Tab("๐Ÿ“Š Dashboard"):
            d_btn = gr.Button("๐Ÿ”„ Refresh", variant="secondary")
            with gr.Row():
                d_mc = gr.Number(label="Total Memories",    interactive=False)
                d_kc = gr.Number(label="Knowledge Chunks",  interactive=False)
            d_top  = gr.Dataframe(headers=["content","importance","access_count"], label="Top Memories", wrap=True)
            d_cats = gr.JSON(label="Memory Categories")
            d_src  = gr.JSON(label="Knowledge Sources")

            def fn_dash():
                s = get_stats()
                top = [[r["content"],r["importance"],r["access_count"]] for r in s["top_memories"]]
                return s["total_memories"], s["total_knowledge_chunks"], top, s["categories"], s["knowledge_sources"]

            d_btn.click(fn_dash, outputs=[d_mc,d_kc,d_top,d_cats,d_src])
            demo.load(fn_dash, outputs=[d_mc,d_kc,d_top,d_cats,d_src])

        # TAB 5 โ€” MCP API
        with gr.Tab("๐Ÿ”Œ MCP API"):
            gr.Markdown("""### JSON API โ€” Claude Integration

Tools: `store_memory` ยท `search_memories` ยท `store_knowledge` ยท `search_knowledge` ยท `rag_query` ยท `get_session_context` ยท `status`

```json
{"tool": "store_memory", "params": {"content": "Bytebot uses NestJS + PostgreSQL", "category": "bytebot", "importance": 8}}
```""")
            api_in  = gr.Textbox(label="JSON Request", lines=5, value='{"tool": "status", "params": {}}')
            api_btn = gr.Button("๐Ÿš€ Execute", variant="primary")
            api_out = gr.Textbox(label="Response", lines=14, show_copy_button=True)
            with gr.Row():
                q_st = gr.Button("๐Ÿ“Š Status")
                q_cx = gr.Button("๐ŸŽฏ Session Context")
            api_btn.click(mcp_api, api_in, api_out)
            q_st.click(lambda: mcp_api('{"tool":"status","params":{}}'), outputs=api_out)
            q_cx.click(lambda: mcp_api('{"tool":"get_session_context","params":{}}'), outputs=api_out)

demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True)