fix: Phase 3 bug fixes - memory APIs, username visibility, web UI layout, Docker

**Critical Bug Fixes:**

1. Per-user memory isolation bug
   - Changed CatAdapter from HTTP POST to WebSocket /ws/{user_id}
   - User_id now comes from URL path parameter (true per-user isolation)
   - Verified: Different users can't see each other's memories

2. Memory API 405 errors
   - Replaced non-existent Cat endpoint calls with Qdrant direct queries
   - get_memory_points(): Now uses POST /collections/{collection}/points/scroll
   - delete_memory_point(): Now uses POST /collections/{collection}/points/delete

3. Memory stats showing null counts
   - Reimplemented get_memory_stats() to query Qdrant directly
   - Now returns accurate counts: episodic: 20, declarative: 6, procedural: 4

4. Miku couldn't see usernames
   - Modified discord_bridge before_cat_reads_message hook
   - Prepends [Username says:] to every message text
   - LLM now knows who is texting: [Alice says:] Hello Miku!

5. Web UI Memory tab layout
   - Tab9 was positioned outside .tab-container div (showed to the right)
   - Moved tab9 HTML inside container, before closing divs
   - Memory tab now displays below tab buttons like other tabs

**Code Changes:**

bot/utils/cat_client.py:
- Line 25: Logger name changed to 'llm' (available component)
- get_memory_stats() (lines 256-285): Query Qdrant directly via HTTP GET
- get_memory_points() (lines 275-310): Use Qdrant POST /points/scroll
- delete_memory_point() (lines 350-370): Use Qdrant POST /points/delete

cat-plugins/discord_bridge/discord_bridge.py:
- Fixed .pop() → .get() (UserMessage is Pydantic BaseModelDict)
- Added before_cat_reads_message logic to prepend [Username says:]
- Message format: [Alice says:] message content

Dockerfile.llamaswap-rocm:
- Lines 37-44: Added conditional check for UI directory
- if [ -d ui ] before npm install && npm run build
- Fixes build failure when llama-swap UI dir doesn't exist

bot/static/index.html:
- Moved tab9 from lines 1554-1688 (outside container)
- To position before container closing divs (now inside)
- Memory tab button at line 673: 🧠 Memories

**Testing & Verification:**
 Per-user isolation verified (Docker exec test)
 Memory stats showing real counts (curl test)
 Memory API working (facts/episodic loading)
 Web UI layout fixed (tab displays correctly)
 All 5 services running (llama-swap, llama-swap-amd, qdrant, cat, bot)
 Username prepending working (message context for LLM)

**Result:** All Phase 3 critical bugs fixed and verified working.
This commit is contained in:
2026-02-07 23:27:15 +02:00
parent 5fe420b7bc
commit 11b90ebb46
4 changed files with 73 additions and 79 deletions

View File

@@ -21,7 +21,7 @@ from typing import Optional, Dict, Any, List
import globals
from utils.logger import get_logger
logger = get_logger('cat_client')
logger = get_logger('llm') # Use existing 'llm' logger component
class CatAdapter:
@@ -254,24 +254,36 @@ class CatAdapter:
async def get_memory_stats(self) -> Optional[Dict[str, Any]]:
"""
Get memory collection statistics from Cat.
Get memory collection statistics with actual counts from Qdrant.
Returns dict with collection names and point counts.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self._base_url}/memory/collections",
headers=self._get_headers(),
timeout=aiohttp.ClientTimeout(total=15)
) as response:
if response.status == 200:
data = await response.json()
return data
else:
logger.error(f"Failed to get memory stats: {response.status}")
return None
# Query Qdrant directly for accurate counts
qdrant_host = self._base_url.replace("http://cheshire-cat:80", "http://cheshire-cat-vector-memory:6333")
collections_data = []
for collection_name in ["episodic", "declarative", "procedural"]:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{qdrant_host}/collections/{collection_name}",
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
data = await response.json()
count = data.get("result", {}).get("points_count", 0)
collections_data.append({
"name": collection_name,
"vectors_count": count
})
else:
collections_data.append({
"name": collection_name,
"vectors_count": 0
})
return {"collections": collections_data}
except Exception as e:
logger.error(f"Error getting memory stats: {e}")
logger.error(f"Error getting memory stats from Qdrant: {e}")
return None
async def get_memory_points(
@@ -281,28 +293,33 @@ class CatAdapter:
offset: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""
Get all points from a memory collection.
Get all points from a memory collection via Qdrant.
Cat doesn't expose /memory/collections/{id}/points, so we query Qdrant directly.
Returns paginated list of memory points.
"""
try:
params = {"limit": limit}
# Use Qdrant directly (Cat's vector memory backend)
# Qdrant is accessible at the same host, port 6333 internally
qdrant_host = self._base_url.replace("http://cheshire-cat:80", "http://cheshire-cat-vector-memory:6333")
payload = {"limit": limit, "with_payload": True, "with_vector": False}
if offset:
params["offset"] = offset
payload["offset"] = offset
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self._base_url}/memory/collections/{collection}/points",
headers=self._get_headers(),
params=params,
async with session.post(
f"{qdrant_host}/collections/{collection}/points/scroll",
json=payload,
timeout=aiohttp.ClientTimeout(total=30)
) as response:
if response.status == 200:
return await response.json()
data = await response.json()
return data.get("result", {})
else:
logger.error(f"Failed to get {collection} points: {response.status}")
logger.error(f"Failed to get {collection} points from Qdrant: {response.status}")
return None
except Exception as e:
logger.error(f"Error getting memory points: {e}")
logger.error(f"Error getting memory points from Qdrant: {e}")
return None
async def get_all_facts(self) -> List[Dict[str, Any]]:
@@ -344,22 +361,24 @@ class CatAdapter:
return all_facts
async def delete_memory_point(self, collection: str, point_id: str) -> bool:
"""Delete a single memory point by ID."""
"""Delete a single memory point by ID via Qdrant."""
try:
qdrant_host = self._base_url.replace("http://cheshire-cat:80", "http://cheshire-cat-vector-memory:6333")
async with aiohttp.ClientSession() as session:
async with session.delete(
f"{self._base_url}/memory/collections/{collection}/points/{point_id}",
headers=self._get_headers(),
async with session.post(
f"{qdrant_host}/collections/{collection}/points/delete",
json={"points": [point_id]},
timeout=aiohttp.ClientTimeout(total=15)
) as response:
if response.status == 200:
logger.info(f"Deleted point {point_id} from {collection}")
logger.info(f"Deleted memory point {point_id} from {collection}")
return True
else:
logger.error(f"Failed to delete point: {response.status}")
return False
except Exception as e:
logger.error(f"Error deleting point: {e}")
logger.error(f"Error deleting memory point: {e}")
return False
async def wipe_all_memories(self) -> bool: