Phase 3: Unified Cheshire Cat integration with WebSocket-based per-user isolation
Key changes:
- CatAdapter (bot/utils/cat_client.py): WebSocket /ws/{user_id} for chat
queries instead of HTTP POST (fixes per-user memory isolation when no
API keys are configured — HTTP defaults all users to user_id='user')
- Memory management API: 8 endpoints for status, stats, facts, episodic
memories, consolidation trigger, multi-step delete with confirmation
- Web UI: Memory tab (tab9) with collection stats, fact/episodic browser,
manual consolidation trigger, and 3-step delete flow requiring exact
confirmation string
- Bot integration: Cat-first response path with query_llama fallback for
both text and embed responses, server mood detection
- Discord bridge plugin: fixed .pop() to .get() (UserMessage is a Pydantic
BaseModelDict, not a raw dict), metadata extraction via extra attributes
- Unified docker-compose: Cat + Qdrant services merged into main compose,
bot depends_on Cat healthcheck
- All plugins (discord_bridge, memory_consolidation, miku_personality)
consolidated into cat-plugins/ for volume mount
- query_llama deprecated but functional for compatibility
This commit is contained in:
128
bot/api.py
128
bot/api.py
@@ -2772,6 +2772,134 @@ def set_voice_debug_mode(enabled: bool = Form(...)):
|
||||
}
|
||||
|
||||
|
||||
# ========== Cheshire Cat Memory Management (Phase 3) ==========
|
||||
|
||||
class MemoryDeleteRequest(BaseModel):
|
||||
confirmation: str
|
||||
|
||||
@app.get("/memory/status")
|
||||
async def get_cat_memory_status():
|
||||
"""Get Cheshire Cat connection status and feature flag."""
|
||||
from utils.cat_client import cat_adapter
|
||||
is_healthy = await cat_adapter.health_check()
|
||||
return {
|
||||
"enabled": globals.USE_CHESHIRE_CAT,
|
||||
"healthy": is_healthy,
|
||||
"url": globals.CHESHIRE_CAT_URL,
|
||||
"circuit_breaker_active": cat_adapter._is_circuit_broken(),
|
||||
"consecutive_failures": cat_adapter._consecutive_failures
|
||||
}
|
||||
|
||||
@app.post("/memory/toggle")
|
||||
async def toggle_cat_integration(enabled: bool = Form(...)):
|
||||
"""Toggle Cheshire Cat integration on/off."""
|
||||
globals.USE_CHESHIRE_CAT = enabled
|
||||
logger.info(f"🐱 Cheshire Cat integration {'ENABLED' if enabled else 'DISABLED'}")
|
||||
return {
|
||||
"success": True,
|
||||
"enabled": globals.USE_CHESHIRE_CAT,
|
||||
"message": f"Cheshire Cat {'enabled' if enabled else 'disabled'}"
|
||||
}
|
||||
|
||||
@app.get("/memory/stats")
|
||||
async def get_memory_stats():
|
||||
"""Get memory collection statistics from Cheshire Cat (point counts per collection)."""
|
||||
from utils.cat_client import cat_adapter
|
||||
stats = await cat_adapter.get_memory_stats()
|
||||
if stats is None:
|
||||
return {"success": False, "error": "Could not reach Cheshire Cat"}
|
||||
return {"success": True, "collections": stats.get("collections", [])}
|
||||
|
||||
@app.get("/memory/facts")
|
||||
async def get_memory_facts():
|
||||
"""Get all declarative memory facts (learned knowledge about users)."""
|
||||
from utils.cat_client import cat_adapter
|
||||
facts = await cat_adapter.get_all_facts()
|
||||
return {"success": True, "facts": facts, "count": len(facts)}
|
||||
|
||||
@app.get("/memory/episodic")
|
||||
async def get_episodic_memories():
|
||||
"""Get all episodic memories (conversation snippets)."""
|
||||
from utils.cat_client import cat_adapter
|
||||
result = await cat_adapter.get_memory_points(collection="episodic", limit=100)
|
||||
if result is None:
|
||||
return {"success": False, "error": "Could not reach Cheshire Cat"}
|
||||
|
||||
memories = []
|
||||
for point in result.get("points", []):
|
||||
payload = point.get("payload", {})
|
||||
memories.append({
|
||||
"id": point.get("id"),
|
||||
"content": payload.get("page_content", ""),
|
||||
"metadata": payload.get("metadata", {}),
|
||||
})
|
||||
|
||||
return {"success": True, "memories": memories, "count": len(memories)}
|
||||
|
||||
@app.post("/memory/consolidate")
|
||||
async def trigger_memory_consolidation():
|
||||
"""Manually trigger memory consolidation (sleep consolidation process)."""
|
||||
from utils.cat_client import cat_adapter
|
||||
logger.info("🌙 Manual memory consolidation triggered via API")
|
||||
result = await cat_adapter.trigger_consolidation()
|
||||
if result is None:
|
||||
return {"success": False, "error": "Consolidation failed or timed out"}
|
||||
return {"success": True, "result": result}
|
||||
|
||||
@app.post("/memory/delete")
|
||||
async def delete_all_memories(request: MemoryDeleteRequest):
|
||||
"""
|
||||
Delete ALL of Miku's memories. Requires exact confirmation string.
|
||||
|
||||
The confirmation field must be exactly:
|
||||
"Yes, I am deleting Miku's memories fully."
|
||||
|
||||
This is destructive and irreversible.
|
||||
"""
|
||||
REQUIRED_CONFIRMATION = "Yes, I am deleting Miku's memories fully."
|
||||
|
||||
if request.confirmation != REQUIRED_CONFIRMATION:
|
||||
logger.warning(f"Memory deletion rejected: wrong confirmation string")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Confirmation string does not match. "
|
||||
f"Expected exactly: \"{REQUIRED_CONFIRMATION}\""
|
||||
}
|
||||
|
||||
from utils.cat_client import cat_adapter
|
||||
logger.warning("⚠️ MEMORY DELETION CONFIRMED — wiping all memories!")
|
||||
|
||||
# Wipe vector memories (episodic + declarative)
|
||||
wipe_success = await cat_adapter.wipe_all_memories()
|
||||
|
||||
# Also clear conversation history
|
||||
history_success = await cat_adapter.wipe_conversation_history()
|
||||
|
||||
if wipe_success:
|
||||
logger.warning("🗑️ All Miku memories have been deleted.")
|
||||
return {
|
||||
"success": True,
|
||||
"message": "All memories have been permanently deleted.",
|
||||
"vector_memory_wiped": wipe_success,
|
||||
"conversation_history_cleared": history_success
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Failed to wipe memory collections. Check Cat connection."
|
||||
}
|
||||
|
||||
@app.delete("/memory/point/{collection}/{point_id}")
|
||||
async def delete_single_memory_point(collection: str, point_id: str):
|
||||
"""Delete a single memory point by collection and ID."""
|
||||
from utils.cat_client import cat_adapter
|
||||
success = await cat_adapter.delete_memory_point(collection, point_id)
|
||||
if success:
|
||||
return {"success": True, "deleted": point_id}
|
||||
else:
|
||||
return {"success": False, "error": f"Failed to delete point {point_id}"}
|
||||
|
||||
|
||||
def start_api():
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=3939)
|
||||
|
||||
Reference in New Issue
Block a user