Przeglądaj źródła

dashboard, bugfixes

Lukas Goldschmidt 2 dni temu
rodzic
commit
5c27ac6471
4 zmienionych plików z 753 dodań i 20 usunięć
  1. 703 0
      dashboard.html
  2. 1 0
      docker-compose.yml
  3. 7 0
      mem0server.py
  4. 42 20
      reset_memory.py

+ 703 - 0
dashboard.html

@@ -0,0 +1,703 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>mem0 dashboard</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet">
+<style>
+  :root {
+    --bg:        #0a0a0a;
+    --surface:   #111111;
+    --surface2:  #181818;
+    --border:    #242424;
+    --border2:   #2e2e2e;
+    --text:      #d4d0c8;
+    --muted:     #555550;
+    --accent:    #c8a96e;
+    --accent2:   #6e9ec8;
+    --danger:    #c86e6e;
+    --ok:        #6ec87a;
+    --mono:      'IBM Plex Mono', monospace;
+    --sans:      'IBM Plex Sans', sans-serif;
+  }
+
+  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+  body {
+    background: var(--bg);
+    color: var(--text);
+    font-family: var(--sans);
+    font-size: 14px;
+    font-weight: 300;
+    min-height: 100vh;
+    /* subtle noise texture */
+    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
+  }
+
+  /* ── HEADER ─────────────────────────────────────────────── */
+  header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 18px 32px;
+    border-bottom: 1px solid var(--border);
+    background: var(--surface);
+    position: sticky;
+    top: 0;
+    z-index: 100;
+  }
+
+  .header-left {
+    display: flex;
+    align-items: baseline;
+    gap: 14px;
+  }
+
+  .logo {
+    font-family: var(--mono);
+    font-size: 15px;
+    font-weight: 500;
+    letter-spacing: 0.08em;
+    color: var(--accent);
+  }
+
+  .logo span { color: var(--muted); font-weight: 300; }
+
+  .tagline {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    letter-spacing: 0.04em;
+  }
+
+  .header-right {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+  }
+
+  .status-pill {
+    font-family: var(--mono);
+    font-size: 11px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 4px 10px;
+    border: 1px solid var(--border2);
+    border-radius: 2px;
+    background: var(--surface2);
+  }
+
+  .status-dot {
+    width: 6px; height: 6px;
+    border-radius: 50%;
+    background: var(--muted);
+    transition: background 0.3s;
+  }
+  .status-dot.ok  { background: var(--ok);     box-shadow: 0 0 6px var(--ok); }
+  .status-dot.err { background: var(--danger);  box-shadow: 0 0 6px var(--danger); }
+
+  .btn-refresh {
+    font-family: var(--mono);
+    font-size: 11px;
+    letter-spacing: 0.06em;
+    background: none;
+    border: 1px solid var(--border2);
+    color: var(--muted);
+    padding: 5px 14px;
+    cursor: pointer;
+    border-radius: 2px;
+    transition: color 0.2s, border-color 0.2s;
+  }
+  .btn-refresh:hover { color: var(--text); border-color: var(--accent); }
+
+  /* ── LAYOUT ─────────────────────────────────────────────── */
+  main {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 0;
+    max-width: 1400px;
+    margin: 0 auto;
+  }
+
+  .panel {
+    padding: 28px 32px;
+    border-right: 1px solid var(--border);
+    animation: fadeIn 0.4s ease both;
+  }
+  .panel:last-child { border-right: none; }
+
+  @keyframes fadeIn {
+    from { opacity: 0; transform: translateY(8px); }
+    to   { opacity: 1; transform: translateY(0); }
+  }
+
+  .panel-header {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    margin-bottom: 22px;
+    padding-bottom: 14px;
+    border-bottom: 1px solid var(--border);
+  }
+
+  .panel-title {
+    font-family: var(--mono);
+    font-size: 11px;
+    font-weight: 500;
+    letter-spacing: 0.12em;
+    text-transform: uppercase;
+    color: var(--accent);
+  }
+
+  .panel-count {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+  }
+
+  /* ── BOOK GROUPS ─────────────────────────────────────────── */
+  .book-group {
+    border: 1px solid var(--border);
+    border-radius: 2px;
+    margin-bottom: 10px;
+    background: var(--surface);
+    overflow: hidden;
+    animation: fadeIn 0.3s ease both;
+  }
+
+  .book-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 11px 14px;
+    cursor: pointer;
+    user-select: none;
+    transition: background 0.15s;
+  }
+  .book-header:hover { background: var(--surface2); }
+
+  .book-header-left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    min-width: 0;
+  }
+
+  .chevron {
+    font-size: 10px;
+    color: var(--muted);
+    transition: transform 0.2s;
+    flex-shrink: 0;
+  }
+  .book-group.open .chevron { transform: rotate(90deg); }
+
+  .book-name {
+    font-family: var(--mono);
+    font-size: 12px;
+    color: var(--text);
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .book-badge {
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--muted);
+    background: var(--surface2);
+    border: 1px solid var(--border);
+    padding: 2px 7px;
+    border-radius: 2px;
+    flex-shrink: 0;
+  }
+
+  .btn-delete-book {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.05em;
+    background: none;
+    border: 1px solid transparent;
+    color: var(--muted);
+    padding: 3px 10px;
+    cursor: pointer;
+    border-radius: 2px;
+    transition: color 0.2s, border-color 0.2s;
+    flex-shrink: 0;
+  }
+  .btn-delete-book:hover {
+    color: var(--danger);
+    border-color: var(--danger);
+  }
+  .btn-delete-book.confirming {
+    color: var(--danger);
+    border-color: var(--danger);
+    background: rgba(200, 110, 110, 0.08);
+  }
+
+  .book-entries {
+    display: none;
+    border-top: 1px solid var(--border);
+  }
+  .book-group.open .book-entries { display: block; }
+
+  .entry {
+    padding: 10px 14px;
+    border-bottom: 1px solid var(--border);
+    display: grid;
+    grid-template-columns: 1fr auto;
+    gap: 8px;
+    align-items: start;
+  }
+  .entry:last-child { border-bottom: none; }
+
+  .entry-text {
+    font-size: 12px;
+    line-height: 1.5;
+    color: var(--text);
+  }
+
+  .entry-meta {
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--muted);
+    text-align: right;
+    white-space: nowrap;
+  }
+
+  /* ── CONVERSATIONAL MEMORIES ─────────────────────────────── */
+  .memory-item {
+    padding: 12px 0;
+    border-bottom: 1px solid var(--border);
+    display: grid;
+    grid-template-columns: 1fr auto;
+    gap: 12px;
+    align-items: start;
+    animation: fadeIn 0.3s ease both;
+  }
+  .memory-item:last-child { border-bottom: none; }
+
+  .memory-text {
+    font-size: 13px;
+    line-height: 1.55;
+    color: var(--text);
+  }
+
+  .memory-time {
+    font-family: var(--mono);
+    font-size: 10px;
+    color: var(--muted);
+    white-space: nowrap;
+    padding-top: 2px;
+  }
+
+  .user-section {
+    margin-bottom: 20px;
+  }
+
+  .user-label {
+    font-family: var(--mono);
+    font-size: 10px;
+    letter-spacing: 0.1em;
+    color: var(--accent2);
+    text-transform: uppercase;
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+  .user-label::after {
+    content: '';
+    flex: 1;
+    height: 1px;
+    background: var(--border);
+  }
+
+  /* ── EMPTY / LOADING STATES ──────────────────────────────── */
+  .state-msg {
+    font-family: var(--mono);
+    font-size: 11px;
+    color: var(--muted);
+    padding: 24px 0;
+    text-align: center;
+    letter-spacing: 0.04em;
+  }
+
+  .loading-bar {
+    height: 1px;
+    background: var(--border);
+    overflow: hidden;
+    margin-bottom: 20px;
+  }
+  .loading-bar::after {
+    content: '';
+    display: block;
+    height: 100%;
+    width: 40%;
+    background: var(--accent);
+    animation: slide 1.2s ease-in-out infinite;
+  }
+  @keyframes slide {
+    0%   { transform: translateX(-100%); }
+    100% { transform: translateX(350%); }
+  }
+
+  /* ── TOAST ───────────────────────────────────────────────── */
+  #toast {
+    position: fixed;
+    bottom: 24px;
+    right: 24px;
+    font-family: var(--mono);
+    font-size: 12px;
+    padding: 10px 18px;
+    border-radius: 2px;
+    border: 1px solid var(--border2);
+    background: var(--surface);
+    color: var(--text);
+    opacity: 0;
+    transform: translateY(8px);
+    transition: opacity 0.25s, transform 0.25s;
+    z-index: 999;
+    pointer-events: none;
+  }
+  #toast.show { opacity: 1; transform: translateY(0); }
+  #toast.ok   { border-color: var(--ok);     color: var(--ok); }
+  #toast.err  { border-color: var(--danger);  color: var(--danger); }
+
+  /* ── RESPONSIVE ──────────────────────────────────────────── */
+  @media (max-width: 900px) {
+    main { grid-template-columns: 1fr; }
+    .panel { border-right: none; border-bottom: 1px solid var(--border); }
+  }
+</style>
+</head>
+<body>
+
+<header>
+  <div class="header-left">
+    <div class="logo">mem0<span>/dashboard</span></div>
+    <div class="tagline">192.168.0.200:8420</div>
+  </div>
+  <div class="header-right">
+    <div class="status-pill">
+      <div class="status-dot" id="statusDot"></div>
+      <span id="statusText" style="font-family:var(--mono);font-size:11px;color:var(--muted)">connecting</span>
+    </div>
+    <button class="btn-refresh" onclick="loadAll()">⟳ refresh</button>
+  </div>
+</header>
+
+<main>
+  <!-- ── KNOWLEDGE PANEL ── -->
+  <div class="panel" style="animation-delay:0.05s">
+    <div class="panel-header">
+      <div class="panel-title">knowledge base</div>
+      <div class="panel-count" id="knowledgeCount">—</div>
+    </div>
+    <div id="knowledgeLoading" class="loading-bar"></div>
+    <div id="knowledgeContent"></div>
+  </div>
+
+  <!-- ── MEMORIES PANEL ── -->
+  <div class="panel" style="animation-delay:0.1s">
+    <div class="panel-header">
+      <div class="panel-title">conversational memory</div>
+      <div class="panel-count" id="memoriesCount">—</div>
+    </div>
+    <div id="memoriesLoading" class="loading-bar"></div>
+    <div id="memoriesContent"></div>
+  </div>
+</main>
+
+<div id="toast"></div>
+
+<script>
+const BASE = 'http://192.168.0.200:8420';
+
+// ── TOAST ──────────────────────────────────────────────────────
+function toast(msg, type = 'ok') {
+  const el = document.getElementById('toast');
+  el.textContent = msg;
+  el.className = `show ${type}`;
+  clearTimeout(el._t);
+  el._t = setTimeout(() => el.className = '', 2800);
+}
+
+// ── HEALTH ─────────────────────────────────────────────────────
+async function checkHealth() {
+  try {
+    const r = await fetch(`${BASE}/health`);
+    const dot = document.getElementById('statusDot');
+    const txt = document.getElementById('statusText');
+    if (r.ok) {
+      dot.className = 'status-dot ok';
+      txt.textContent = 'online';
+    } else {
+      throw new Error();
+    }
+  } catch {
+    document.getElementById('statusDot').className = 'status-dot err';
+    document.getElementById('statusText').textContent = 'unreachable';
+  }
+}
+
+// ── HELPERS ────────────────────────────────────────────────────
+function fmtDate(iso) {
+  if (!iso) return '—';
+  const d = new Date(iso);
+  return d.toLocaleDateString('en-GB', { day:'2-digit', month:'short' })
+       + ' ' + d.toLocaleTimeString('en-GB', { hour:'2-digit', minute:'2-digit' });
+}
+
+function stopLoading(id) {
+  const el = document.getElementById(id);
+  if (el) el.style.display = 'none';
+}
+
+// ── KNOWLEDGE ──────────────────────────────────────────────────
+
+// Phase 1: fetch just enough to build the book list (headers only)
+async function loadKnowledge() {
+  document.getElementById('knowledgeLoading').style.display = 'block';
+  document.getElementById('knowledgeContent').innerHTML = '';
+
+  try {
+    const r = await fetch(`${BASE}/knowledge/recent`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ user_id: 'knowledge_base', limit: 50 })
+    });
+    const data = await r.json();
+    const items = data.results || [];
+    stopLoading('knowledgeLoading');
+    renderKnowledge(items);
+  } catch (e) {
+    stopLoading('knowledgeLoading');
+    document.getElementById('knowledgeContent').innerHTML =
+      '<div class="state-msg">failed to load — is the server reachable?</div>';
+  }
+}
+
+function renderKnowledge(items) {
+  const container = document.getElementById('knowledgeContent');
+
+  if (!items.length) {
+    container.innerHTML = '<div class="state-msg">no knowledge entries found</div>';
+    document.getElementById('knowledgeCount').textContent = '0 entries';
+    return;
+  }
+
+  // Group by source_file to build headers
+  const groups = {};
+  for (const item of items) {
+    const src = item.metadata?.source_file || '(no source)';
+    if (!groups[src]) groups[src] = 0;
+    groups[src]++;
+  }
+
+  const sorted = Object.entries(groups).sort((a, b) => b[1] - a[1]);
+  document.getElementById('knowledgeCount').textContent =
+    `${items.length}+ entries · ${sorted.length} books`;
+
+  container.innerHTML = '';
+  for (const [src, count] of sorted) {
+    container.appendChild(buildBookGroup(src, count));
+  }
+}
+
+// Phase 2: fetch entries for a specific book on expand (lazy)
+async function loadBookEntries(src, entriesDiv) {
+  entriesDiv.innerHTML = '<div class="state-msg">loading…</div>';
+  try {
+    const r = await fetch(`${BASE}/knowledge/search`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        query: src.replace(/\.pdf$/i, '').replace(/[-_]/g, ' '),
+        user_id: 'knowledge_base',
+        limit: 200
+      })
+    });
+    const data = await r.json();
+    const entries = (data.results || []).sort(
+      (a, b) => (a.metadata?.page ?? 0) - (b.metadata?.page ?? 0)
+    );
+    entriesDiv.innerHTML = '';
+    if (!entries.length) {
+      entriesDiv.innerHTML = '<div class="state-msg">no entries found</div>';
+      return;
+    }
+    for (const entry of entries) {
+      const row = document.createElement('div');
+      row.className = 'entry';
+      const page = entry.metadata?.page    ? `p.${entry.metadata.page}`    : '';
+      const ch   = entry.metadata?.chapter ? `ch.${entry.metadata.chapter}` : '';
+      const loc  = [ch, page].filter(Boolean).join(' · ');
+      row.innerHTML = `
+        <div class="entry-text">${entry.memory || ''}</div>
+        <div class="entry-meta">${loc}<br>${fmtDate(entry.created_at)}</div>
+      `;
+      entriesDiv.appendChild(row);
+    }
+  } catch (e) {
+    entriesDiv.innerHTML = '<div class="state-msg">failed to load entries</div>';
+  }
+}
+
+function buildBookGroup(src, count) {
+  const group = document.createElement('div');
+  group.className = 'book-group';
+  group.dataset.src = src;
+
+  const header = document.createElement('div');
+  header.className = 'book-header';
+  header.innerHTML = `
+    <div class="book-header-left">
+      <span class="chevron">▶</span>
+      <span class="book-name" title="${src}">${src}</span>
+      <span class="book-badge">${count}</span>
+    </div>
+    <button class="btn-delete-book" data-src="${src}">delete</button>
+  `;
+
+  const entriesDiv = document.createElement('div');
+  entriesDiv.className = 'book-entries';
+
+  // Lazy load on first expand
+  header.querySelector('.book-header-left').addEventListener('click', () => {
+    group.classList.toggle('open');
+    if (group.classList.contains('open') && !group.dataset.loaded) {
+      group.dataset.loaded = 'true';
+      loadBookEntries(src, entriesDiv);
+    }
+  });
+
+  // Delete — confirm on first click, execute on second
+  const delBtn = header.querySelector('.btn-delete-book');
+  delBtn.addEventListener('click', (e) => {
+    e.stopPropagation();
+    if (!delBtn.classList.contains('confirming')) {
+      delBtn.classList.add('confirming');
+      delBtn.textContent = 'confirm?';
+      setTimeout(() => {
+        delBtn.classList.remove('confirming');
+        delBtn.textContent = 'delete';
+      }, 2500);
+    } else {
+      deleteBook(src, group);
+    }
+  });
+
+  group.appendChild(header);
+  group.appendChild(entriesDiv);
+  return group;
+}
+
+async function deleteBook(src, groupEl) {
+  try {
+    const r = await fetch(`${BASE}/knowledge`, {
+      method: 'DELETE',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ filter: { 'metadata.source_file': src } })
+    });
+    if (r.ok) {
+      groupEl.style.transition = 'opacity 0.3s';
+      groupEl.style.opacity = '0';
+      setTimeout(() => {
+        groupEl.remove();
+        // update count
+        const remaining = document.querySelectorAll('.book-group').length;
+        const entries = document.querySelectorAll('.entry').length;
+        document.getElementById('knowledgeCount').textContent =
+          `${entries} entries · ${remaining} books`;
+      }, 300);
+      toast(`deleted: ${src}`);
+    } else {
+      toast('delete failed', 'err');
+    }
+  } catch {
+    toast('delete failed — server unreachable', 'err');
+  }
+}
+
+// ── MEMORIES ───────────────────────────────────────────────────
+async function loadMemories() {
+  document.getElementById('memoriesLoading').style.display = 'block';
+  document.getElementById('memoriesContent').innerHTML = '';
+
+  // Fetch for known user IDs — extend this array as needed
+  const userIds = ['main', 'testuser', 'default'];
+  const allResults = {};
+
+  try {
+    await Promise.all(userIds.map(async uid => {
+      const r = await fetch(`${BASE}/memories/recent`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ user_id: uid, limit: 100 })
+      });
+      const data = await r.json();
+      const items = data.results || [];
+      if (items.length) allResults[uid] = items;
+    }));
+
+    stopLoading('memoriesLoading');
+    renderMemories(allResults);
+  } catch (e) {
+    stopLoading('memoriesLoading');
+    document.getElementById('memoriesContent').innerHTML =
+      '<div class="state-msg">failed to load</div>';
+  }
+}
+
+function renderMemories(byUser) {
+  const container = document.getElementById('memoriesContent');
+  const allEntries = Object.values(byUser).flat();
+
+  if (!allEntries.length) {
+    container.innerHTML = '<div class="state-msg">no memories found</div>';
+    document.getElementById('memoriesCount').textContent = '0 memories';
+    return;
+  }
+
+  document.getElementById('memoriesCount').textContent =
+    `${allEntries.length} memories · ${Object.keys(byUser).length} users`;
+
+  container.innerHTML = '';
+
+  for (const [uid, items] of Object.entries(byUser)) {
+    const section = document.createElement('div');
+    section.className = 'user-section';
+
+    const label = document.createElement('div');
+    label.className = 'user-label';
+    label.textContent = uid;
+    section.appendChild(label);
+
+    for (const item of items) {
+      const row = document.createElement('div');
+      row.className = 'memory-item';
+      row.innerHTML = `
+        <div class="memory-text">${item.memory || ''}</div>
+        <div class="memory-time">${fmtDate(item.created_at)}</div>
+      `;
+      section.appendChild(row);
+    }
+
+    container.appendChild(section);
+  }
+}
+
+// ── LOAD ALL ───────────────────────────────────────────────────
+async function loadAll() {
+  await checkHealth();
+  await Promise.all([loadKnowledge(), loadMemories()]);
+}
+
+// Init
+loadAll();
+</script>
+</body>
+</html>

+ 1 - 0
docker-compose.yml

@@ -8,6 +8,7 @@ services:
       - "8420:8420"
     volumes:
       - ./mem0server.py:/app/mem0server.py:ro
+      - ./dashboard.html:/app/dashboard.html:ro
     env_file:
       - .env
     restart: unless-stopped

+ 7 - 0
mem0server.py

@@ -4,6 +4,7 @@ import json
 import httpx
 from fastapi import FastAPI, Request
 from fastapi.responses import JSONResponse
+from fastapi.responses import HTMLResponse
 from mem0 import Memory
 
 # =============================================================================
@@ -16,6 +17,8 @@ if not GROQ_API_KEY:
 
 RERANKER_URL = os.environ.get("RERANKER_URL", "http://192.168.0.200:5200/rerank")
 
+DASHBOARD_HTML = open("dashboard.html").read()
+
 # =============================================================================
 # SAFE JSON RESPONSE  (handles Infinity / NaN from Chroma / reranker scores)
 # =============================================================================
@@ -471,3 +474,7 @@ async def search_all(req: Request):
         f"conv={len(conv_items)} know={len(know_items)} merged={len(merged)}"
     )
     return SafeJSONResponse(content={"results": merged})
+
+@app.get("/dashboard")
+async def dashboard():
+    return HTMLResponse(content=DASHBOARD_HTML)

+ 42 - 20
reset_memory.py

@@ -1,23 +1,45 @@
-import sys, subprocess, requests, os
+import sys, subprocess, requests, time
+
+CHROMA_V2    = "http://192.168.0.200:8001/api/v2/tenants/default_tenant/databases/default_database"
+CONTAINER    = "mem0server"
+COMPOSE_FILE = "/home/lucky/mem0server/docker-compose.yml"
+MEM0_SQLITE  = "/root/.mem0/history.db"
 
-chroma_base = "http://192.168.0.200:8001/api/v1"
 name = sys.argv[1]
 
-# 1. Reset Chroma collection
-requests.delete(f"{chroma_base}/collections/{name}")
-requests.post(f"{chroma_base}/collections", json={"name": name})
-print(f"chroma collection reset: {name}")
-
-# 2. Wipe mem0's SQLite so integer IDs don't drift from UUIDs
-result = subprocess.run(
-    ["docker", "exec", "mem0server", "find", "/", "-name", "*.db"],
-    capture_output=True, text=True
-)
-for path in result.stdout.strip().splitlines():
-    if "mem0" in path.lower():
-        subprocess.run(["docker", "exec", "mem0server", "rm", "-f", path])
-        print(f"removed sqlite: {path}")
-
-# 3. Restart so mem0 reinitialises cleanly
-subprocess.run(["docker", "compose", "restart", "mem0server"])
-print("mem0server restarted")
+# 1. Wipe and recreate Chroma collection
+requests.delete(f"{CHROMA_V2}/collections/{name}")
+requests.post(f"{CHROMA_V2}/collections", json={"name": name})
+print(f"chroma reset: {name}")
+
+# 2. Verify entry count is 0
+col = requests.get(f"{CHROMA_V2}/collections/{name}").json()
+col_id = col.get("id")
+count = requests.get(f"{CHROMA_V2}/collections/{col_id}/count").json()
+print(f"chroma verified: {name} → {count} entries")
+
+# 3. Delete mem0 SQLite
+subprocess.run(["docker", "exec", CONTAINER, "rm", "-f", MEM0_SQLITE])
+print(f"removed sqlite: {MEM0_SQLITE}")
+
+# 4. Restart
+subprocess.run(["docker", "compose", "-f", COMPOSE_FILE, "restart", CONTAINER])
+print("restarting...")
+
+# 5. Wait for health
+online = False
+for i in range(15):
+    time.sleep(2)
+    try:
+        r = requests.get("http://192.168.0.200:8420/health", timeout=2)
+        if r.ok:
+            online = True
+            break
+    except:
+        pass
+    print(f"  waiting... ({(i+1)*2}s)")
+
+if online:
+    print(f"server back online ✓")
+else:
+    print("server did not come back — check: docker logs mem0server")