Spaces:
Sleeping
Sleeping
Commit
·
d063204
1
Parent(s):
32741d8
Add session-spec within project
Browse files- app.py +1 -0
- memo/core.py +80 -1
- memo/session.py +211 -0
- routes/README.md +17 -5
- routes/chats.py +167 -43
- routes/projects.py +42 -9
- routes/sessions.py +246 -0
- session.md +153 -0
- static/index.html +45 -1
- static/script.js +34 -11
- static/sessions.js +346 -0
- static/styles.css +162 -0
- test_sessions.py +311 -0
- utils/README.md +5 -1
app.py
CHANGED
|
@@ -9,6 +9,7 @@ import routes.projects as _routes_projects
|
|
| 9 |
import routes.files as _routes_files
|
| 10 |
import routes.reports as _routes_report
|
| 11 |
import routes.chats as _routes_chat
|
|
|
|
| 12 |
import routes.health as _routes_health
|
| 13 |
|
| 14 |
# Local dev
|
|
|
|
| 9 |
import routes.files as _routes_files
|
| 10 |
import routes.reports as _routes_report
|
| 11 |
import routes.chats as _routes_chat
|
| 12 |
+
import routes.sessions as _routes_sessions
|
| 13 |
import routes.health as _routes_health
|
| 14 |
|
| 15 |
# Local dev
|
memo/core.py
CHANGED
|
@@ -33,12 +33,15 @@ class MemorySystem:
|
|
| 33 |
self.enhanced_available = False
|
| 34 |
self.enhanced_memory = None
|
| 35 |
self.embedder = None
|
|
|
|
| 36 |
|
| 37 |
try:
|
| 38 |
self.embedder = EmbeddingClient()
|
| 39 |
self.enhanced_memory = PersistentMemory(self.mongo_uri, self.db_name, self.embedder)
|
|
|
|
|
|
|
| 40 |
self.enhanced_available = True
|
| 41 |
-
logger.info("[CORE_MEMORY] Enhanced memory system initialized")
|
| 42 |
except Exception as e:
|
| 43 |
logger.warning(f"[CORE_MEMORY] Enhanced memory system unavailable: {e}")
|
| 44 |
self.enhanced_available = False
|
|
@@ -400,6 +403,82 @@ class MemorySystem:
|
|
| 400 |
logger.error(f"[CORE_MEMORY] Failed to get enhancement context: {e}")
|
| 401 |
return "", "", {"error": str(e)}
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
# ────────────────────────────── Private Helper Methods ──────────────────────────────
|
| 404 |
|
| 405 |
async def _add_enhanced_memory(self, user_id: str, question: str, answer: str):
|
|
|
|
| 33 |
self.enhanced_available = False
|
| 34 |
self.enhanced_memory = None
|
| 35 |
self.embedder = None
|
| 36 |
+
self.session_memory = None
|
| 37 |
|
| 38 |
try:
|
| 39 |
self.embedder = EmbeddingClient()
|
| 40 |
self.enhanced_memory = PersistentMemory(self.mongo_uri, self.db_name, self.embedder)
|
| 41 |
+
from memo.session import get_session_memory_manager
|
| 42 |
+
self.session_memory = get_session_memory_manager(self.mongo_uri, self.db_name)
|
| 43 |
self.enhanced_available = True
|
| 44 |
+
logger.info("[CORE_MEMORY] Enhanced memory system and session memory initialized")
|
| 45 |
except Exception as e:
|
| 46 |
logger.warning(f"[CORE_MEMORY] Enhanced memory system unavailable: {e}")
|
| 47 |
self.enhanced_available = False
|
|
|
|
| 403 |
logger.error(f"[CORE_MEMORY] Failed to get enhancement context: {e}")
|
| 404 |
return "", "", {"error": str(e)}
|
| 405 |
|
| 406 |
+
# ────────────────────────────── Session-Specific Memory Operations ──────────────────────────────
|
| 407 |
+
|
| 408 |
+
def add_session_memory(self, user_id: str, project_id: str, session_id: str,
|
| 409 |
+
question: str, answer: str, context: Dict[str, Any] = None) -> str:
|
| 410 |
+
"""Add memory to a specific session"""
|
| 411 |
+
try:
|
| 412 |
+
if not self.session_memory:
|
| 413 |
+
logger.warning("[CORE_MEMORY] Session memory not available")
|
| 414 |
+
return ""
|
| 415 |
+
|
| 416 |
+
# Create session-specific memory content
|
| 417 |
+
content = f"Q: {question}\nA: {answer}"
|
| 418 |
+
|
| 419 |
+
memory_id = self.session_memory.add_session_memory(
|
| 420 |
+
user_id=user_id,
|
| 421 |
+
project_id=project_id,
|
| 422 |
+
session_id=session_id,
|
| 423 |
+
content=content,
|
| 424 |
+
memory_type="conversation",
|
| 425 |
+
importance="medium",
|
| 426 |
+
tags=["conversation", "qa"],
|
| 427 |
+
metadata=context or {}
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
logger.debug(f"[CORE_MEMORY] Added session memory for session {session_id}")
|
| 431 |
+
return memory_id
|
| 432 |
+
|
| 433 |
+
except Exception as e:
|
| 434 |
+
logger.error(f"[CORE_MEMORY] Failed to add session memory: {e}")
|
| 435 |
+
return ""
|
| 436 |
+
|
| 437 |
+
def get_session_memory_context(self, user_id: str, project_id: str, session_id: str,
|
| 438 |
+
question: str, limit: int = 5) -> Tuple[str, str]:
|
| 439 |
+
"""Get memory context for a specific session"""
|
| 440 |
+
try:
|
| 441 |
+
if not self.session_memory:
|
| 442 |
+
return "", ""
|
| 443 |
+
|
| 444 |
+
# Get recent session memories
|
| 445 |
+
recent_memories = self.session_memory.get_session_memories(
|
| 446 |
+
user_id, project_id, session_id, memory_type="conversation", limit=limit
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
recent_context = ""
|
| 450 |
+
if recent_memories:
|
| 451 |
+
recent_context = "\n\n".join([mem["content"] for mem in recent_memories])
|
| 452 |
+
|
| 453 |
+
# Get semantic context from session memories
|
| 454 |
+
semantic_memories = self.session_memory.search_session_memories(
|
| 455 |
+
user_id, project_id, session_id, question, self.embedder, limit=3
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
semantic_context = ""
|
| 459 |
+
if semantic_memories:
|
| 460 |
+
semantic_context = "\n\n".join([mem["content"] for mem, score in semantic_memories])
|
| 461 |
+
|
| 462 |
+
return recent_context, semantic_context
|
| 463 |
+
|
| 464 |
+
except Exception as e:
|
| 465 |
+
logger.error(f"[CORE_MEMORY] Failed to get session memory context: {e}")
|
| 466 |
+
return "", ""
|
| 467 |
+
|
| 468 |
+
def clear_session_memories(self, user_id: str, project_id: str, session_id: str):
|
| 469 |
+
"""Clear all memories for a specific session"""
|
| 470 |
+
try:
|
| 471 |
+
if not self.session_memory:
|
| 472 |
+
return 0
|
| 473 |
+
|
| 474 |
+
deleted_count = self.session_memory.clear_session_memories(user_id, project_id, session_id)
|
| 475 |
+
logger.info(f"[CORE_MEMORY] Cleared {deleted_count} session memories for session {session_id}")
|
| 476 |
+
return deleted_count
|
| 477 |
+
|
| 478 |
+
except Exception as e:
|
| 479 |
+
logger.error(f"[CORE_MEMORY] Failed to clear session memories: {e}")
|
| 480 |
+
return 0
|
| 481 |
+
|
| 482 |
# ────────────────────────────── Private Helper Methods ──────────────────────────────
|
| 483 |
|
| 484 |
async def _add_enhanced_memory(self, user_id: str, question: str, answer: str):
|
memo/session.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# memo/session.py
|
| 2 |
+
"""
|
| 3 |
+
Session-Specific Memory Management
|
| 4 |
+
|
| 5 |
+
Handles memory storage and retrieval for individual chat sessions,
|
| 6 |
+
separate from project-wide memory.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
import uuid
|
| 12 |
+
import asyncio
|
| 13 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 14 |
+
from datetime import datetime, timezone
|
| 15 |
+
|
| 16 |
+
from utils.logger import get_logger
|
| 17 |
+
from utils.rag.embeddings import EmbeddingClient
|
| 18 |
+
|
| 19 |
+
logger = get_logger("SESSION_MEMORY", __name__)
|
| 20 |
+
|
| 21 |
+
class SessionMemoryManager:
|
| 22 |
+
"""
|
| 23 |
+
Manages memory for individual chat sessions.
|
| 24 |
+
Each session has its own memory context separate from project memory.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, mongo_uri: str = None, db_name: str = "studybuddy"):
|
| 28 |
+
self.mongo_uri = mongo_uri or os.getenv("MONGO_URI", "mongodb://localhost:27017")
|
| 29 |
+
self.db_name = db_name
|
| 30 |
+
|
| 31 |
+
# MongoDB connection
|
| 32 |
+
try:
|
| 33 |
+
from pymongo import MongoClient
|
| 34 |
+
self.client = MongoClient(self.mongo_uri)
|
| 35 |
+
self.db = self.client[self.db_name]
|
| 36 |
+
self.session_memories = self.db["session_memories"]
|
| 37 |
+
|
| 38 |
+
# Create indexes for efficient querying
|
| 39 |
+
self.session_memories.create_index([("user_id", 1), ("project_id", 1), ("session_id", 1)])
|
| 40 |
+
self.session_memories.create_index([("user_id", 1), ("project_id", 1), ("session_id", 1), ("created_at", -1)])
|
| 41 |
+
|
| 42 |
+
logger.info(f"[SESSION_MEMORY] Connected to MongoDB: {self.db_name}")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logger.error(f"[SESSION_MEMORY] Failed to connect to MongoDB: {e}")
|
| 45 |
+
raise
|
| 46 |
+
|
| 47 |
+
def add_session_memory(self, user_id: str, project_id: str, session_id: str,
|
| 48 |
+
content: str, memory_type: str = "conversation",
|
| 49 |
+
importance: str = "medium", tags: List[str] = None,
|
| 50 |
+
metadata: Dict[str, Any] = None) -> str:
|
| 51 |
+
"""Add a memory entry to a specific session"""
|
| 52 |
+
try:
|
| 53 |
+
memory_id = str(uuid.uuid4())
|
| 54 |
+
|
| 55 |
+
memory_entry = {
|
| 56 |
+
"memory_id": memory_id,
|
| 57 |
+
"user_id": user_id,
|
| 58 |
+
"project_id": project_id,
|
| 59 |
+
"session_id": session_id,
|
| 60 |
+
"content": content,
|
| 61 |
+
"memory_type": memory_type,
|
| 62 |
+
"importance": importance,
|
| 63 |
+
"tags": tags or [],
|
| 64 |
+
"metadata": metadata or {},
|
| 65 |
+
"created_at": datetime.now(timezone.utc),
|
| 66 |
+
"timestamp": time.time()
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
self.session_memories.insert_one(memory_entry)
|
| 70 |
+
logger.debug(f"[SESSION_MEMORY] Added memory to session {session_id}")
|
| 71 |
+
return memory_id
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"[SESSION_MEMORY] Failed to add session memory: {e}")
|
| 75 |
+
return ""
|
| 76 |
+
|
| 77 |
+
def get_session_memories(self, user_id: str, project_id: str, session_id: str,
|
| 78 |
+
memory_type: str = None, limit: int = 10) -> List[Dict[str, Any]]:
|
| 79 |
+
"""Get memories for a specific session"""
|
| 80 |
+
try:
|
| 81 |
+
query = {
|
| 82 |
+
"user_id": user_id,
|
| 83 |
+
"project_id": project_id,
|
| 84 |
+
"session_id": session_id
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if memory_type:
|
| 88 |
+
query["memory_type"] = memory_type
|
| 89 |
+
|
| 90 |
+
cursor = self.session_memories.find(query).sort("created_at", -1).limit(limit)
|
| 91 |
+
return list(cursor)
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
logger.error(f"[SESSION_MEMORY] Failed to get session memories: {e}")
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
def search_session_memories(self, user_id: str, project_id: str, session_id: str,
|
| 98 |
+
query: str, embedder: EmbeddingClient = None,
|
| 99 |
+
limit: int = 5) -> List[Tuple[Dict[str, Any], float]]:
|
| 100 |
+
"""Search memories within a session using semantic similarity"""
|
| 101 |
+
try:
|
| 102 |
+
if not embedder:
|
| 103 |
+
# Fallback to text-based search
|
| 104 |
+
memories = self.get_session_memories(user_id, project_id, session_id, limit=limit)
|
| 105 |
+
return [(mem, 1.0) for mem in memories]
|
| 106 |
+
|
| 107 |
+
# Get all session memories
|
| 108 |
+
memories = self.get_session_memories(user_id, project_id, session_id, limit=50)
|
| 109 |
+
if not memories:
|
| 110 |
+
return []
|
| 111 |
+
|
| 112 |
+
# Generate query embedding
|
| 113 |
+
query_embedding = embedder.embed([query])[0]
|
| 114 |
+
|
| 115 |
+
# Calculate similarities
|
| 116 |
+
results = []
|
| 117 |
+
for memory in memories:
|
| 118 |
+
if "embedding" in memory:
|
| 119 |
+
similarity = self._cosine_similarity(query_embedding, memory["embedding"])
|
| 120 |
+
results.append((memory, similarity))
|
| 121 |
+
|
| 122 |
+
# Sort by similarity and return top results
|
| 123 |
+
results.sort(key=lambda x: x[1], reverse=True)
|
| 124 |
+
return results[:limit]
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error(f"[SESSION_MEMORY] Failed to search session memories: {e}")
|
| 128 |
+
return []
|
| 129 |
+
|
| 130 |
+
def clear_session_memories(self, user_id: str, project_id: str, session_id: str):
|
| 131 |
+
"""Clear all memories for a specific session"""
|
| 132 |
+
try:
|
| 133 |
+
result = self.session_memories.delete_many({
|
| 134 |
+
"user_id": user_id,
|
| 135 |
+
"project_id": project_id,
|
| 136 |
+
"session_id": session_id
|
| 137 |
+
})
|
| 138 |
+
logger.info(f"[SESSION_MEMORY] Cleared {result.deleted_count} memories for session {session_id}")
|
| 139 |
+
return result.deleted_count
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logger.error(f"[SESSION_MEMORY] Failed to clear session memories: {e}")
|
| 143 |
+
return 0
|
| 144 |
+
|
| 145 |
+
def get_session_memory_stats(self, user_id: str, project_id: str, session_id: str) -> Dict[str, Any]:
|
| 146 |
+
"""Get memory statistics for a session"""
|
| 147 |
+
try:
|
| 148 |
+
total_memories = self.session_memories.count_documents({
|
| 149 |
+
"user_id": user_id,
|
| 150 |
+
"project_id": project_id,
|
| 151 |
+
"session_id": session_id
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
memory_types = self.session_memories.distinct("memory_type", {
|
| 155 |
+
"user_id": user_id,
|
| 156 |
+
"project_id": project_id,
|
| 157 |
+
"session_id": session_id
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
return {
|
| 161 |
+
"total_memories": total_memories,
|
| 162 |
+
"memory_types": memory_types,
|
| 163 |
+
"session_id": session_id
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
except Exception as e:
|
| 167 |
+
logger.error(f"[SESSION_MEMORY] Failed to get session memory stats: {e}")
|
| 168 |
+
return {"total_memories": 0, "memory_types": [], "session_id": session_id}
|
| 169 |
+
|
| 170 |
+
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
|
| 171 |
+
"""Calculate cosine similarity between two vectors"""
|
| 172 |
+
try:
|
| 173 |
+
import numpy as np
|
| 174 |
+
|
| 175 |
+
# Convert to numpy arrays
|
| 176 |
+
a = np.array(vec1)
|
| 177 |
+
b = np.array(vec2)
|
| 178 |
+
|
| 179 |
+
# Calculate cosine similarity
|
| 180 |
+
dot_product = np.dot(a, b)
|
| 181 |
+
norm_a = np.linalg.norm(a)
|
| 182 |
+
norm_b = np.linalg.norm(b)
|
| 183 |
+
|
| 184 |
+
if norm_a == 0 or norm_b == 0:
|
| 185 |
+
return 0.0
|
| 186 |
+
|
| 187 |
+
return dot_product / (norm_a * norm_b)
|
| 188 |
+
|
| 189 |
+
except Exception as e:
|
| 190 |
+
logger.warning(f"[SESSION_MEMORY] Cosine similarity calculation failed: {e}")
|
| 191 |
+
return 0.0
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ────────────────────────────── Global Instance ──────────────────────────────
|
| 195 |
+
|
| 196 |
+
_session_memory_manager: Optional[SessionMemoryManager] = None
|
| 197 |
+
|
| 198 |
+
def get_session_memory_manager(mongo_uri: str = None, db_name: str = None) -> SessionMemoryManager:
|
| 199 |
+
"""Get the global session memory manager instance"""
|
| 200 |
+
global _session_memory_manager
|
| 201 |
+
|
| 202 |
+
if _session_memory_manager is None:
|
| 203 |
+
if mongo_uri is None:
|
| 204 |
+
mongo_uri = os.getenv("MONGO_URI", "mongodb://localhost:27017")
|
| 205 |
+
if db_name is None:
|
| 206 |
+
db_name = os.getenv("MONGO_DB", "studybuddy")
|
| 207 |
+
|
| 208 |
+
_session_memory_manager = SessionMemoryManager(mongo_uri, db_name)
|
| 209 |
+
logger.info("[SESSION_MEMORY] Global session memory manager initialized")
|
| 210 |
+
|
| 211 |
+
return _session_memory_manager
|
routes/README.md
CHANGED
|
@@ -16,6 +16,8 @@ API routes for the EdSummariser application, providing RESTful endpoints for aut
|
|
| 16 |
- **Web Search**: Optional web augmentation for comprehensive answers
|
| 17 |
- **Session Management**: Real-time status tracking and session continuity
|
| 18 |
- **Memory Integration**: Automatic Q&A summarization and storage
|
|
|
|
|
|
|
| 19 |
|
| 20 |
### **File Management** (`files.py`)
|
| 21 |
- **Multi-format Upload**: PDF, DOCX support with background processing
|
|
@@ -36,8 +38,9 @@ API routes for the EdSummariser application, providing RESTful endpoints for aut
|
|
| 36 |
### **Project Management** (`projects.py`)
|
| 37 |
- **Project CRUD**: Create, read, update, delete operations
|
| 38 |
- **User Isolation**: Project ownership and access control
|
| 39 |
-
- **Data Cleanup**: Cascading deletion of associated data
|
| 40 |
- **Metadata Tracking**: Creation and update timestamps
|
|
|
|
| 41 |
|
| 42 |
### **Search & Web** (`search.py`)
|
| 43 |
- **Intelligent Search**: AI-powered keyword extraction and strategy generation
|
|
@@ -63,7 +66,8 @@ routes/
|
|
| 63 |
├── health.py # Health check and monitoring
|
| 64 |
├── projects.py # Project management
|
| 65 |
├── reports.py # Report generation
|
| 66 |
-
|
|
|
|
| 67 |
```
|
| 68 |
|
| 69 |
## 🔧 Key Endpoints
|
|
@@ -76,8 +80,8 @@ routes/
|
|
| 76 |
- `POST /chat` - Main chat endpoint with memory integration
|
| 77 |
- `POST /chat/search` - Web-augmented chat
|
| 78 |
- `POST /chat/save` - Save chat messages
|
| 79 |
-
- `GET /chat/history` - Retrieve chat history
|
| 80 |
-
- `DELETE /chat/history` - Clear chat history
|
| 81 |
- `GET /chat/status/{session_id}` - Get chat processing status
|
| 82 |
|
| 83 |
### **File Management**
|
|
@@ -97,7 +101,15 @@ routes/
|
|
| 97 |
- `POST /projects/create` - Create new project
|
| 98 |
- `GET /projects` - List user projects
|
| 99 |
- `GET /projects/{project_id}` - Get specific project
|
| 100 |
-
- `DELETE /projects/{project_id}` - Delete project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
### **Health & Monitoring**
|
| 103 |
- `GET /healthz` - Basic health check
|
|
|
|
| 16 |
- **Web Search**: Optional web augmentation for comprehensive answers
|
| 17 |
- **Session Management**: Real-time status tracking and session continuity
|
| 18 |
- **Memory Integration**: Automatic Q&A summarization and storage
|
| 19 |
+
- **Session-Specific Memory**: Each session maintains its own conversation context
|
| 20 |
+
- **Auto-Naming**: Sessions automatically named based on first user query
|
| 21 |
|
| 22 |
### **File Management** (`files.py`)
|
| 23 |
- **Multi-format Upload**: PDF, DOCX support with background processing
|
|
|
|
| 38 |
### **Project Management** (`projects.py`)
|
| 39 |
- **Project CRUD**: Create, read, update, delete operations
|
| 40 |
- **User Isolation**: Project ownership and access control
|
| 41 |
+
- **Data Cleanup**: Cascading deletion of associated data including all sessions
|
| 42 |
- **Metadata Tracking**: Creation and update timestamps
|
| 43 |
+
- **Session Cleanup**: Complete removal of all session data when project is deleted
|
| 44 |
|
| 45 |
### **Search & Web** (`search.py`)
|
| 46 |
- **Intelligent Search**: AI-powered keyword extraction and strategy generation
|
|
|
|
| 66 |
├── health.py # Health check and monitoring
|
| 67 |
├── projects.py # Project management
|
| 68 |
├── reports.py # Report generation
|
| 69 |
+
├── search.py # Web search and content processing
|
| 70 |
+
└── sessions.py # Session management endpoints
|
| 71 |
```
|
| 72 |
|
| 73 |
## 🔧 Key Endpoints
|
|
|
|
| 80 |
- `POST /chat` - Main chat endpoint with memory integration
|
| 81 |
- `POST /chat/search` - Web-augmented chat
|
| 82 |
- `POST /chat/save` - Save chat messages
|
| 83 |
+
- `GET /chat/history` - Retrieve chat history (supports session_id filter)
|
| 84 |
+
- `DELETE /chat/history` - Clear chat history (session-specific or project-wide)
|
| 85 |
- `GET /chat/status/{session_id}` - Get chat processing status
|
| 86 |
|
| 87 |
### **File Management**
|
|
|
|
| 101 |
- `POST /projects/create` - Create new project
|
| 102 |
- `GET /projects` - List user projects
|
| 103 |
- `GET /projects/{project_id}` - Get specific project
|
| 104 |
+
- `DELETE /projects/{project_id}` - Delete project and all associated sessions
|
| 105 |
+
|
| 106 |
+
### **Session Management**
|
| 107 |
+
- `GET /sessions/list` - List all sessions for a project
|
| 108 |
+
- `POST /sessions/create` - Create new session
|
| 109 |
+
- `PUT /sessions/rename` - Rename a session
|
| 110 |
+
- `DELETE /sessions/delete` - Delete session and its memory
|
| 111 |
+
- `POST /sessions/auto-name` - Auto-name session based on first query
|
| 112 |
+
- `POST /sessions/clear-memory` - Clear session-specific memory
|
| 113 |
|
| 114 |
### **Health & Monitoring**
|
| 115 |
- `GET /healthz` - Basic health check
|
routes/chats.py
CHANGED
|
@@ -20,7 +20,8 @@ async def save_chat_message(
|
|
| 20 |
content: str = Form(...),
|
| 21 |
timestamp: Optional[float] = Form(None),
|
| 22 |
sources: Optional[str] = Form(None),
|
| 23 |
-
is_report: Optional[int] = Form(0)
|
|
|
|
| 24 |
):
|
| 25 |
"""Save a chat message to the session"""
|
| 26 |
if role not in ["user", "assistant"]:
|
|
@@ -44,7 +45,8 @@ async def save_chat_message(
|
|
| 44 |
"timestamp": timestamp or time.time(),
|
| 45 |
"created_at": datetime.now(timezone.utc),
|
| 46 |
**({"sources": parsed_sources} if parsed_sources is not None else {}),
|
| 47 |
-
"is_report": bool(is_report or 0)
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
rag.db["chat_sessions"].insert_one(message)
|
|
@@ -52,11 +54,13 @@ async def save_chat_message(
|
|
| 52 |
|
| 53 |
|
| 54 |
@app.get("/chat/history", response_model=ChatHistoryResponse)
|
| 55 |
-
async def get_chat_history(user_id: str, project_id: str, limit: int = 100):
|
| 56 |
-
"""Get chat history for a project"""
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
|
| 61 |
messages = []
|
| 62 |
for message in messages_cursor:
|
|
@@ -75,44 +79,64 @@ async def get_chat_history(user_id: str, project_id: str, limit: int = 100):
|
|
| 75 |
|
| 76 |
|
| 77 |
@app.delete("/chat/history", response_model=MessageResponse)
|
| 78 |
-
async def delete_chat_history(user_id: str, project_id: str):
|
| 79 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
# Clear chat sessions from database
|
| 81 |
-
chat_result = rag.db["chat_sessions"].delete_many(
|
| 82 |
-
logger.info(f"[CHAT] Cleared {chat_result.deleted_count} chat sessions for user {user_id} project {project_id}")
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
from memo.core import get_memory_system
|
| 87 |
-
memory = get_memory_system()
|
| 88 |
-
clear_results = memory.clear_all_memory(user_id, project_id)
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
if clear_results["legacy_cleared"]:
|
| 99 |
-
cleared_components.append("legacy memory")
|
| 100 |
-
if clear_results["enhanced_cleared"]:
|
| 101 |
-
cleared_components.append("enhanced memory")
|
| 102 |
-
if clear_results["session_cleared"]:
|
| 103 |
-
cleared_components.append("conversation sessions")
|
| 104 |
-
if clear_results["planning_reset"]:
|
| 105 |
-
cleared_components.append("planning state")
|
| 106 |
|
| 107 |
-
message
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
except Exception as e:
|
| 117 |
raise HTTPException(500, detail=f"Failed to clear chat history: {str(e)}")
|
| 118 |
|
|
@@ -276,6 +300,22 @@ async def chat(
|
|
| 276 |
session_id = str(uuid.uuid4())
|
| 277 |
|
| 278 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
return await asyncio.wait_for(_chat_impl(user_id, project_id, question, k, use_web=use_web, max_web=max_web, session_id=session_id), timeout=120.0)
|
| 280 |
except asyncio.TimeoutError:
|
| 281 |
logger.error("[CHAT] Chat request timed out after 120 seconds")
|
|
@@ -306,12 +346,22 @@ async def _chat_impl(
|
|
| 306 |
if session_id:
|
| 307 |
update_chat_status(session_id, "receiving", "Receiving request...", 5)
|
| 308 |
|
| 309 |
-
# Step 1: Retrieve and enhance prompt with conversation history FIRST with
|
| 310 |
try:
|
| 311 |
-
|
| 312 |
-
|
|
|
|
| 313 |
)
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# Check for context switch
|
| 317 |
context_switch_info = await memory.handle_context_switch(user_id, question, nvidia_rotator)
|
|
@@ -577,13 +627,24 @@ async def _chat_impl(
|
|
| 577 |
from memo.history import get_history_manager
|
| 578 |
history_manager = get_history_manager(memory)
|
| 579 |
qa_sum = await history_manager.summarize_qa_with_nvidia(question, answer, nvidia_rotator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
memory.add(user_id, qa_sum)
|
|
|
|
| 581 |
if memory.is_enhanced_available():
|
| 582 |
await memory.add_conversation_memory(
|
| 583 |
user_id=user_id,
|
| 584 |
question=question,
|
| 585 |
answer=answer,
|
| 586 |
project_id=project_id,
|
|
|
|
| 587 |
context={
|
| 588 |
"relevant_files": relevant_files,
|
| 589 |
"sources_count": len(sources_meta),
|
|
@@ -739,3 +800,66 @@ async def chat_with_search(
|
|
| 739 |
|
| 740 |
logger.info("[CHAT] Web-augmented answer len=%d, web_used=%d", len(answer or ""), len(web_sources_meta))
|
| 741 |
return ChatAnswerResponse(answer=answer, sources=merged_sources, relevant_files=merged_files)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
content: str = Form(...),
|
| 21 |
timestamp: Optional[float] = Form(None),
|
| 22 |
sources: Optional[str] = Form(None),
|
| 23 |
+
is_report: Optional[int] = Form(0),
|
| 24 |
+
session_id: Optional[str] = Form(None)
|
| 25 |
):
|
| 26 |
"""Save a chat message to the session"""
|
| 27 |
if role not in ["user", "assistant"]:
|
|
|
|
| 45 |
"timestamp": timestamp or time.time(),
|
| 46 |
"created_at": datetime.now(timezone.utc),
|
| 47 |
**({"sources": parsed_sources} if parsed_sources is not None else {}),
|
| 48 |
+
"is_report": bool(is_report or 0),
|
| 49 |
+
**({"session_id": session_id} if session_id else {})
|
| 50 |
}
|
| 51 |
|
| 52 |
rag.db["chat_sessions"].insert_one(message)
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
@app.get("/chat/history", response_model=ChatHistoryResponse)
|
| 57 |
+
async def get_chat_history(user_id: str, project_id: str, session_id: str = None, limit: int = 100):
|
| 58 |
+
"""Get chat history for a project or specific session"""
|
| 59 |
+
query = {"user_id": user_id, "project_id": project_id}
|
| 60 |
+
if session_id:
|
| 61 |
+
query["session_id"] = session_id
|
| 62 |
+
|
| 63 |
+
messages_cursor = rag.db["chat_sessions"].find(query).sort("timestamp", 1).limit(limit)
|
| 64 |
|
| 65 |
messages = []
|
| 66 |
for message in messages_cursor:
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
@app.delete("/chat/history", response_model=MessageResponse)
|
| 82 |
+
async def delete_chat_history(user_id: str, project_id: str, session_id: str = None):
|
| 83 |
try:
|
| 84 |
+
# Build query for deletion
|
| 85 |
+
query = {"user_id": user_id, "project_id": project_id}
|
| 86 |
+
if session_id:
|
| 87 |
+
query["session_id"] = session_id
|
| 88 |
+
|
| 89 |
# Clear chat sessions from database
|
| 90 |
+
chat_result = rag.db["chat_sessions"].delete_many(query)
|
|
|
|
| 91 |
|
| 92 |
+
if session_id:
|
| 93 |
+
logger.info(f"[CHAT] Cleared {chat_result.deleted_count} chat sessions for user {user_id} project {project_id} session {session_id}")
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
# Clear session-specific memory
|
| 96 |
+
try:
|
| 97 |
+
from memo.core import get_memory_system
|
| 98 |
+
memory = get_memory_system()
|
| 99 |
+
memory.clear_session_memories(user_id, project_id, session_id)
|
| 100 |
+
logger.info(f"[CHAT] Cleared session-specific memory for session {session_id}")
|
| 101 |
+
except Exception as me:
|
| 102 |
+
logger.warning(f"[CHAT] Failed to clear session memory: {me}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
return MessageResponse(message=f"Session history cleared successfully. Removed {chat_result.deleted_count} messages.")
|
| 105 |
+
else:
|
| 106 |
+
logger.info(f"[CHAT] Cleared {chat_result.deleted_count} chat sessions for user {user_id} project {project_id}")
|
| 107 |
+
|
| 108 |
+
# Clear all memory components using the new comprehensive clear method
|
| 109 |
+
try:
|
| 110 |
+
from memo.core import get_memory_system
|
| 111 |
+
memory = get_memory_system()
|
| 112 |
+
clear_results = memory.clear_all_memory(user_id, project_id)
|
| 113 |
|
| 114 |
+
# Log the results
|
| 115 |
+
if clear_results["errors"]:
|
| 116 |
+
logger.warning(f"[CHAT] Memory clear completed with warnings: {clear_results['errors']}")
|
| 117 |
+
else:
|
| 118 |
+
logger.info(f"[CHAT] Memory clear completed successfully for user {user_id}, project {project_id}")
|
| 119 |
+
|
| 120 |
+
# Prepare response message
|
| 121 |
+
cleared_components = []
|
| 122 |
+
if clear_results["legacy_cleared"]:
|
| 123 |
+
cleared_components.append("legacy memory")
|
| 124 |
+
if clear_results["enhanced_cleared"]:
|
| 125 |
+
cleared_components.append("enhanced memory")
|
| 126 |
+
if clear_results["session_cleared"]:
|
| 127 |
+
cleared_components.append("conversation sessions")
|
| 128 |
+
if clear_results["planning_reset"]:
|
| 129 |
+
cleared_components.append("planning state")
|
| 130 |
+
|
| 131 |
+
message = f"Chat history cleared successfully. Cleared: {', '.join(cleared_components)}"
|
| 132 |
+
if clear_results["errors"]:
|
| 133 |
+
message += f" (Warnings: {len(clear_results['errors'])} issues)"
|
| 134 |
+
|
| 135 |
+
except Exception as me:
|
| 136 |
+
logger.warning(f"[CHAT] Failed to clear memory for user {user_id}: {me}")
|
| 137 |
+
message = "Chat history cleared (memory clear failed)"
|
| 138 |
+
|
| 139 |
+
return MessageResponse(message=message)
|
| 140 |
except Exception as e:
|
| 141 |
raise HTTPException(500, detail=f"Failed to clear chat history: {str(e)}")
|
| 142 |
|
|
|
|
| 300 |
session_id = str(uuid.uuid4())
|
| 301 |
|
| 302 |
try:
|
| 303 |
+
# Check if this is the first message in the session for auto-naming
|
| 304 |
+
if session_id:
|
| 305 |
+
existing_messages = rag.db["chat_sessions"].count_documents({
|
| 306 |
+
"user_id": user_id,
|
| 307 |
+
"project_id": project_id,
|
| 308 |
+
"session_id": session_id
|
| 309 |
+
})
|
| 310 |
+
|
| 311 |
+
# If this is the first user message, trigger auto-naming
|
| 312 |
+
if existing_messages == 0:
|
| 313 |
+
try:
|
| 314 |
+
await _auto_name_session(user_id, project_id, session_id, question)
|
| 315 |
+
logger.info(f"[CHAT] Auto-named session {session_id}")
|
| 316 |
+
except Exception as e:
|
| 317 |
+
logger.warning(f"[CHAT] Auto-naming failed: {e}")
|
| 318 |
+
|
| 319 |
return await asyncio.wait_for(_chat_impl(user_id, project_id, question, k, use_web=use_web, max_web=max_web, session_id=session_id), timeout=120.0)
|
| 320 |
except asyncio.TimeoutError:
|
| 321 |
logger.error("[CHAT] Chat request timed out after 120 seconds")
|
|
|
|
| 346 |
if session_id:
|
| 347 |
update_chat_status(session_id, "receiving", "Receiving request...", 5)
|
| 348 |
|
| 349 |
+
# Step 1: Retrieve and enhance prompt with conversation history FIRST with session-specific memory
|
| 350 |
try:
|
| 351 |
+
# Get session-specific memory context
|
| 352 |
+
recent_context, semantic_context = memory.get_session_memory_context(
|
| 353 |
+
user_id, project_id, session_id, question
|
| 354 |
)
|
| 355 |
+
|
| 356 |
+
# Fallback to global memory if no session-specific context
|
| 357 |
+
if not recent_context and not semantic_context:
|
| 358 |
+
recent_context, semantic_context, context_metadata = await memory.get_smart_context(
|
| 359 |
+
user_id, question, nvidia_rotator, project_id, "chat"
|
| 360 |
+
)
|
| 361 |
+
else:
|
| 362 |
+
context_metadata = {"session_specific": True}
|
| 363 |
+
|
| 364 |
+
logger.info(f"[CHAT] Session-specific context retrieved: recent={len(recent_context)}, semantic={len(semantic_context)}")
|
| 365 |
|
| 366 |
# Check for context switch
|
| 367 |
context_switch_info = await memory.handle_context_switch(user_id, question, nvidia_rotator)
|
|
|
|
| 627 |
from memo.history import get_history_manager
|
| 628 |
history_manager = get_history_manager(memory)
|
| 629 |
qa_sum = await history_manager.summarize_qa_with_nvidia(question, answer, nvidia_rotator)
|
| 630 |
+
|
| 631 |
+
# Use session-specific memory storage
|
| 632 |
+
memory.add_session_memory(user_id, project_id, session_id, question, answer, {
|
| 633 |
+
"relevant_files": relevant_files,
|
| 634 |
+
"sources_count": len(sources_meta),
|
| 635 |
+
"timestamp": time.time()
|
| 636 |
+
})
|
| 637 |
+
|
| 638 |
+
# Also add to global memory for backward compatibility
|
| 639 |
memory.add(user_id, qa_sum)
|
| 640 |
+
|
| 641 |
if memory.is_enhanced_available():
|
| 642 |
await memory.add_conversation_memory(
|
| 643 |
user_id=user_id,
|
| 644 |
question=question,
|
| 645 |
answer=answer,
|
| 646 |
project_id=project_id,
|
| 647 |
+
session_id=session_id, # Add session_id to enhanced memory
|
| 648 |
context={
|
| 649 |
"relevant_files": relevant_files,
|
| 650 |
"sources_count": len(sources_meta),
|
|
|
|
| 800 |
|
| 801 |
logger.info("[CHAT] Web-augmented answer len=%d, web_used=%d", len(answer or ""), len(web_sources_meta))
|
| 802 |
return ChatAnswerResponse(answer=answer, sources=merged_sources, relevant_files=merged_files)
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
async def _auto_name_session(user_id: str, project_id: str, session_id: str, first_query: str):
|
| 806 |
+
"""Helper function to auto-name a session based on the first query"""
|
| 807 |
+
try:
|
| 808 |
+
if not nvidia_rotator:
|
| 809 |
+
return
|
| 810 |
+
|
| 811 |
+
# Use NVIDIA_SMALL to generate a 2-3 word session name
|
| 812 |
+
sys_prompt = """You are an expert at creating concise, descriptive session names.
|
| 813 |
+
|
| 814 |
+
Given a user's first query in a chat session, create a 2-3 word session name that captures the main topic or intent.
|
| 815 |
+
|
| 816 |
+
Rules:
|
| 817 |
+
- Use 2-3 words maximum
|
| 818 |
+
- Be descriptive but concise
|
| 819 |
+
- Use title case (capitalize first letter of each word)
|
| 820 |
+
- Focus on the main topic or question type
|
| 821 |
+
- Avoid generic terms like "Question" or "Chat"
|
| 822 |
+
|
| 823 |
+
Examples:
|
| 824 |
+
- "Machine Learning Basics" for "What is machine learning?"
|
| 825 |
+
- "Python Functions" for "How do I create functions in Python?"
|
| 826 |
+
- "Data Analysis" for "Can you help me analyze this dataset?"
|
| 827 |
+
|
| 828 |
+
Return only the session name, nothing else."""
|
| 829 |
+
|
| 830 |
+
user_prompt = f"First query: {first_query}\n\nCreate a 2-3 word session name:"
|
| 831 |
+
|
| 832 |
+
from utils.api.router import generate_answer_with_model
|
| 833 |
+
selection = {"provider": "nvidia", "model": "meta/llama-3.1-8b-instruct"}
|
| 834 |
+
|
| 835 |
+
response = await generate_answer_with_model(
|
| 836 |
+
selection=selection,
|
| 837 |
+
system_prompt=sys_prompt,
|
| 838 |
+
user_prompt=user_prompt,
|
| 839 |
+
gemini_rotator=None,
|
| 840 |
+
nvidia_rotator=nvidia_rotator
|
| 841 |
+
)
|
| 842 |
+
|
| 843 |
+
# Clean up the response
|
| 844 |
+
session_name = response.strip()
|
| 845 |
+
# Remove quotes if present
|
| 846 |
+
if session_name.startswith('"') and session_name.endswith('"'):
|
| 847 |
+
session_name = session_name[1:-1]
|
| 848 |
+
if session_name.startswith("'") and session_name.endswith("'"):
|
| 849 |
+
session_name = session_name[1:-1]
|
| 850 |
+
|
| 851 |
+
# Truncate if too long (safety measure)
|
| 852 |
+
if len(session_name) > 50:
|
| 853 |
+
session_name = session_name[:47] + "..."
|
| 854 |
+
|
| 855 |
+
# Update the session with the auto-generated name
|
| 856 |
+
rag.db["chat_sessions"].update_many(
|
| 857 |
+
{"user_id": user_id, "project_id": project_id, "session_id": session_id},
|
| 858 |
+
{"$set": {"session_name": session_name, "is_auto_named": True}}
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
logger.info(f"[CHAT] Auto-named session '{session_id}' to '{session_name}'")
|
| 862 |
+
|
| 863 |
+
except Exception as e:
|
| 864 |
+
logger.warning(f"[CHAT] Auto-naming failed: {e}")
|
| 865 |
+
# Don't raise the exception to avoid breaking the chat flow
|
routes/projects.py
CHANGED
|
@@ -113,19 +113,52 @@ async def get_project(project_id: str, user_id: str):
|
|
| 113 |
|
| 114 |
@app.delete("/projects/{project_id}", response_model=MessageResponse)
|
| 115 |
async def delete_project(project_id: str, user_id: str):
|
| 116 |
-
"""Delete a project and all its associated data"""
|
| 117 |
# Check ownership
|
| 118 |
project = rag.db["projects"].find_one({"project_id": project_id, "user_id": user_id})
|
| 119 |
if not project:
|
| 120 |
raise HTTPException(404, detail="Project not found")
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
|
|
|
| 113 |
|
| 114 |
@app.delete("/projects/{project_id}", response_model=MessageResponse)
|
| 115 |
async def delete_project(project_id: str, user_id: str):
|
| 116 |
+
"""Delete a project and all its associated data including all sessions"""
|
| 117 |
# Check ownership
|
| 118 |
project = rag.db["projects"].find_one({"project_id": project_id, "user_id": user_id})
|
| 119 |
if not project:
|
| 120 |
raise HTTPException(404, detail="Project not found")
|
| 121 |
|
| 122 |
+
try:
|
| 123 |
+
# Delete project and all associated data
|
| 124 |
+
rag.db["projects"].delete_one({"project_id": project_id})
|
| 125 |
+
rag.db["chunks"].delete_many({"project_id": project_id})
|
| 126 |
+
rag.db["files"].delete_many({"project_id": project_id})
|
| 127 |
+
chat_result = rag.db["chat_sessions"].delete_many({"project_id": project_id})
|
| 128 |
+
|
| 129 |
+
# Clear all session-specific memory for this project
|
| 130 |
+
try:
|
| 131 |
+
from memo.core import get_memory_system
|
| 132 |
+
memory = get_memory_system()
|
| 133 |
+
|
| 134 |
+
# Clear session memories for this project
|
| 135 |
+
if memory.session_memory:
|
| 136 |
+
session_memory_result = memory.session_memory.session_memories.delete_many({
|
| 137 |
+
"user_id": user_id,
|
| 138 |
+
"project_id": project_id
|
| 139 |
+
})
|
| 140 |
+
logger.info(f"[PROJECT] Cleared {session_memory_result.deleted_count} session memories for project {project_id}")
|
| 141 |
+
|
| 142 |
+
# Clear enhanced memory for this project
|
| 143 |
+
if memory.enhanced_available:
|
| 144 |
+
enhanced_result = memory.enhanced_memory.memories.delete_many({
|
| 145 |
+
"user_id": user_id,
|
| 146 |
+
"project_id": project_id
|
| 147 |
+
})
|
| 148 |
+
logger.info(f"[PROJECT] Cleared {enhanced_result.deleted_count} enhanced memories for project {project_id}")
|
| 149 |
+
|
| 150 |
+
# Clear legacy memory for this user (since it's user-scoped, not project-scoped)
|
| 151 |
+
memory.legacy_memory.clear(user_id)
|
| 152 |
+
logger.info(f"[PROJECT] Cleared legacy memory for user {user_id}")
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
logger.warning(f"[PROJECT] Failed to clear some memory components: {e}")
|
| 156 |
+
|
| 157 |
+
logger.info(f"[PROJECT] Deleted project {project_id} for user {user_id} - removed {chat_result.deleted_count} chat sessions")
|
| 158 |
+
return MessageResponse(message=f"Project deleted successfully. Removed {chat_result.deleted_count} chat sessions and all associated data.")
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logger.error(f"[PROJECT] Failed to delete project {project_id}: {e}")
|
| 162 |
+
raise HTTPException(500, detail=f"Failed to delete project: {str(e)}")
|
| 163 |
|
| 164 |
|
routes/sessions.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# routes/sessions.py
|
| 2 |
+
import json, time, uuid
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
from fastapi import Form, HTTPException
|
| 6 |
+
|
| 7 |
+
from helpers.setup import app, rag, logger, nvidia_rotator
|
| 8 |
+
from helpers.models import MessageResponse
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@app.get("/sessions/list")
|
| 12 |
+
async def list_sessions(user_id: str, project_id: str):
|
| 13 |
+
"""Get all sessions for a project"""
|
| 14 |
+
try:
|
| 15 |
+
sessions_cursor = rag.db["chat_sessions"].find(
|
| 16 |
+
{"user_id": user_id, "project_id": project_id}
|
| 17 |
+
).sort("created_at", -1)
|
| 18 |
+
|
| 19 |
+
# Group by session_id to get unique sessions
|
| 20 |
+
sessions_map = {}
|
| 21 |
+
for message in sessions_cursor:
|
| 22 |
+
session_id = message.get("session_id")
|
| 23 |
+
if session_id and session_id not in sessions_map:
|
| 24 |
+
sessions_map[session_id] = {
|
| 25 |
+
"session_id": session_id,
|
| 26 |
+
"name": message.get("session_name", "New Chat"),
|
| 27 |
+
"is_auto_named": message.get("is_auto_named", True),
|
| 28 |
+
"created_at": message.get("created_at"),
|
| 29 |
+
"last_activity": message.get("timestamp", 0),
|
| 30 |
+
"message_count": 0
|
| 31 |
+
}
|
| 32 |
+
if session_id in sessions_map:
|
| 33 |
+
sessions_map[session_id]["message_count"] += 1
|
| 34 |
+
# Update last activity to most recent message
|
| 35 |
+
if message.get("timestamp", 0) > sessions_map[session_id]["last_activity"]:
|
| 36 |
+
sessions_map[session_id]["last_activity"] = message.get("timestamp", 0)
|
| 37 |
+
|
| 38 |
+
sessions = list(sessions_map.values())
|
| 39 |
+
return {"sessions": sessions}
|
| 40 |
+
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.error(f"[SESSIONS] Failed to list sessions: {e}")
|
| 43 |
+
raise HTTPException(500, detail=f"Failed to list sessions: {str(e)}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@app.post("/sessions/create")
|
| 47 |
+
async def create_session(
|
| 48 |
+
user_id: str = Form(...),
|
| 49 |
+
project_id: str = Form(...),
|
| 50 |
+
session_name: str = Form("New Chat")
|
| 51 |
+
):
|
| 52 |
+
"""Create a new session"""
|
| 53 |
+
try:
|
| 54 |
+
session_id = str(uuid.uuid4())
|
| 55 |
+
current_time = time.time()
|
| 56 |
+
|
| 57 |
+
# Create session record
|
| 58 |
+
session_data = {
|
| 59 |
+
"user_id": user_id,
|
| 60 |
+
"project_id": project_id,
|
| 61 |
+
"session_id": session_id,
|
| 62 |
+
"session_name": session_name,
|
| 63 |
+
"is_auto_named": session_name == "New Chat",
|
| 64 |
+
"created_at": datetime.now(timezone.utc),
|
| 65 |
+
"timestamp": current_time
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# Insert session record
|
| 69 |
+
rag.db["chat_sessions"].insert_one(session_data)
|
| 70 |
+
|
| 71 |
+
return {
|
| 72 |
+
"session_id": session_id,
|
| 73 |
+
"name": session_name,
|
| 74 |
+
"is_auto_named": session_name == "New Chat",
|
| 75 |
+
"created_at": session_data["created_at"].isoformat(),
|
| 76 |
+
"last_activity": current_time,
|
| 77 |
+
"message_count": 0
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.error(f"[SESSIONS] Failed to create session: {e}")
|
| 82 |
+
raise HTTPException(500, detail=f"Failed to create session: {str(e)}")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@app.put("/sessions/rename")
|
| 86 |
+
async def rename_session(
|
| 87 |
+
user_id: str = Form(...),
|
| 88 |
+
project_id: str = Form(...),
|
| 89 |
+
session_id: str = Form(...),
|
| 90 |
+
new_name: str = Form(...)
|
| 91 |
+
):
|
| 92 |
+
"""Rename a session"""
|
| 93 |
+
try:
|
| 94 |
+
# Update all messages in this session with new name
|
| 95 |
+
result = rag.db["chat_sessions"].update_many(
|
| 96 |
+
{"user_id": user_id, "project_id": project_id, "session_id": session_id},
|
| 97 |
+
{"$set": {"session_name": new_name, "is_auto_named": False}}
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
if result.modified_count == 0:
|
| 101 |
+
raise HTTPException(404, detail="Session not found")
|
| 102 |
+
|
| 103 |
+
return MessageResponse(message="Session renamed successfully")
|
| 104 |
+
|
| 105 |
+
except HTTPException:
|
| 106 |
+
raise
|
| 107 |
+
except Exception as e:
|
| 108 |
+
logger.error(f"[SESSIONS] Failed to rename session: {e}")
|
| 109 |
+
raise HTTPException(500, detail=f"Failed to rename session: {str(e)}")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@app.delete("/sessions/delete")
|
| 113 |
+
async def delete_session(
|
| 114 |
+
user_id: str = Form(...),
|
| 115 |
+
project_id: str = Form(...),
|
| 116 |
+
session_id: str = Form(...)
|
| 117 |
+
):
|
| 118 |
+
"""Delete a session and all its messages"""
|
| 119 |
+
try:
|
| 120 |
+
# Delete all messages in this session
|
| 121 |
+
chat_result = rag.db["chat_sessions"].delete_many({
|
| 122 |
+
"user_id": user_id,
|
| 123 |
+
"project_id": project_id,
|
| 124 |
+
"session_id": session_id
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
# Clear session-specific memory
|
| 128 |
+
try:
|
| 129 |
+
from memo.core import get_memory_system
|
| 130 |
+
memory = get_memory_system()
|
| 131 |
+
|
| 132 |
+
# Clear session-specific enhanced memory
|
| 133 |
+
if memory.is_enhanced_available():
|
| 134 |
+
memory.enhanced_memory.memories.delete_many({
|
| 135 |
+
"user_id": user_id,
|
| 136 |
+
"project_id": project_id,
|
| 137 |
+
"session_id": session_id
|
| 138 |
+
})
|
| 139 |
+
|
| 140 |
+
logger.info(f"[SESSIONS] Cleared session-specific memory for session {session_id}")
|
| 141 |
+
except Exception as me:
|
| 142 |
+
logger.warning(f"[SESSIONS] Failed to clear session memory: {me}")
|
| 143 |
+
|
| 144 |
+
return MessageResponse(message=f"Session deleted successfully. Removed {chat_result.deleted_count} messages.")
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error(f"[SESSIONS] Failed to delete session: {e}")
|
| 148 |
+
raise HTTPException(500, detail=f"Failed to delete session: {str(e)}")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@app.post("/sessions/auto-name")
|
| 152 |
+
async def auto_name_session(
|
| 153 |
+
user_id: str = Form(...),
|
| 154 |
+
project_id: str = Form(...),
|
| 155 |
+
session_id: str = Form(...),
|
| 156 |
+
first_query: str = Form(...)
|
| 157 |
+
):
|
| 158 |
+
"""Automatically name a session based on the first query using NVIDIA_SMALL API"""
|
| 159 |
+
try:
|
| 160 |
+
if not nvidia_rotator:
|
| 161 |
+
return MessageResponse(message="Auto-naming not available")
|
| 162 |
+
|
| 163 |
+
# Use NVIDIA_SMALL to generate a 2-3 word session name
|
| 164 |
+
sys_prompt = """You are an expert at creating concise, descriptive session names.
|
| 165 |
+
|
| 166 |
+
Given a user's first query in a chat session, create a 2-3 word session name that captures the main topic or intent.
|
| 167 |
+
|
| 168 |
+
Rules:
|
| 169 |
+
- Use 2-3 words maximum
|
| 170 |
+
- Be descriptive but concise
|
| 171 |
+
- Use title case (capitalize first letter of each word)
|
| 172 |
+
- Focus on the main topic or question type
|
| 173 |
+
- Avoid generic terms like "Question" or "Chat"
|
| 174 |
+
|
| 175 |
+
Examples:
|
| 176 |
+
- "Machine Learning Basics" for "What is machine learning?"
|
| 177 |
+
- "Python Functions" for "How do I create functions in Python?"
|
| 178 |
+
- "Data Analysis" for "Can you help me analyze this dataset?"
|
| 179 |
+
|
| 180 |
+
Return only the session name, nothing else."""
|
| 181 |
+
|
| 182 |
+
user_prompt = f"First query: {first_query}\n\nCreate a 2-3 word session name:"
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
from utils.api.router import generate_answer_with_model
|
| 186 |
+
selection = {"provider": "nvidia", "model": "meta/llama-3.1-8b-instruct"}
|
| 187 |
+
|
| 188 |
+
response = await generate_answer_with_model(
|
| 189 |
+
selection=selection,
|
| 190 |
+
system_prompt=sys_prompt,
|
| 191 |
+
user_prompt=user_prompt,
|
| 192 |
+
gemini_rotator=None,
|
| 193 |
+
nvidia_rotator=nvidia_rotator
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Clean up the response
|
| 197 |
+
session_name = response.strip()
|
| 198 |
+
# Remove quotes if present
|
| 199 |
+
if session_name.startswith('"') and session_name.endswith('"'):
|
| 200 |
+
session_name = session_name[1:-1]
|
| 201 |
+
if session_name.startswith("'") and session_name.endswith("'"):
|
| 202 |
+
session_name = session_name[1:-1]
|
| 203 |
+
|
| 204 |
+
# Truncate if too long (safety measure)
|
| 205 |
+
if len(session_name) > 50:
|
| 206 |
+
session_name = session_name[:47] + "..."
|
| 207 |
+
|
| 208 |
+
# Update the session with the auto-generated name
|
| 209 |
+
result = rag.db["chat_sessions"].update_many(
|
| 210 |
+
{"user_id": user_id, "project_id": project_id, "session_id": session_id},
|
| 211 |
+
{"$set": {"session_name": session_name, "is_auto_named": True}}
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
if result.modified_count > 0:
|
| 215 |
+
return MessageResponse(message=f"Session auto-named: {session_name}")
|
| 216 |
+
else:
|
| 217 |
+
return MessageResponse(message="Session not found for auto-naming")
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
logger.warning(f"[SESSIONS] Auto-naming failed: {e}")
|
| 221 |
+
return MessageResponse(message="Auto-naming failed, keeping default name")
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
logger.error(f"[SESSIONS] Failed to auto-name session: {e}")
|
| 225 |
+
raise HTTPException(500, detail=f"Failed to auto-name session: {str(e)}")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@app.post("/sessions/clear-memory")
|
| 229 |
+
async def clear_session_memory(
|
| 230 |
+
user_id: str = Form(...),
|
| 231 |
+
project_id: str = Form(...),
|
| 232 |
+
session_id: str = Form(...)
|
| 233 |
+
):
|
| 234 |
+
"""Clear memory for a specific session"""
|
| 235 |
+
try:
|
| 236 |
+
from memo.core import get_memory_system
|
| 237 |
+
memory = get_memory_system()
|
| 238 |
+
|
| 239 |
+
# Clear session-specific memory
|
| 240 |
+
deleted_count = memory.clear_session_memories(user_id, project_id, session_id)
|
| 241 |
+
|
| 242 |
+
return MessageResponse(message=f"Session memory cleared successfully. Removed {deleted_count} memory entries.")
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logger.error(f"[SESSIONS] Failed to clear session memory: {e}")
|
| 246 |
+
raise HTTPException(500, detail=f"Failed to clear session memory: {str(e)}")
|
session.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Session Management Implementation
|
| 2 |
+
|
| 3 |
+
This document describes the implementation of multiple sessions per project functionality in the EdSummariser application.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The system now supports multiple chat sessions per project, where each session maintains its own memory context separate from other sessions. This allows users to have different conversation threads within the same project while sharing the same documents.
|
| 8 |
+
|
| 9 |
+
## Key Features
|
| 10 |
+
|
| 11 |
+
### 1. Session Management UI
|
| 12 |
+
- **Dropdown Menu**: Lists all sessions for the current project with the last option to create a new session
|
| 13 |
+
- **Create Session**: "+" button to create new sessions (default name: "New Chat")
|
| 14 |
+
- **Rename Session**: Pencil icon (✏️) to rename sessions
|
| 15 |
+
- **Delete Session**: Trash icon (🗑️) to delete sessions with confirmation modal
|
| 16 |
+
- **Session Actions**: Rename and delete buttons appear only when a session is selected
|
| 17 |
+
|
| 18 |
+
### 2. Auto-Naming
|
| 19 |
+
- Sessions are automatically named based on the first user query
|
| 20 |
+
- Uses NVIDIA_SMALL API to generate 2-3 word descriptive names
|
| 21 |
+
- Names are generated when the user sends their first message in a new session
|
| 22 |
+
- Example: "What is machine learning?" → "Machine Learning Basics"
|
| 23 |
+
|
| 24 |
+
### 3. Session-Specific Memory
|
| 25 |
+
- Each session maintains its own conversation memory
|
| 26 |
+
- Memory is isolated between sessions
|
| 27 |
+
- Session memory is stored separately from project-wide memory
|
| 28 |
+
- Memory includes conversation context, Q&A pairs, and relevant metadata
|
| 29 |
+
|
| 30 |
+
## Technical Implementation
|
| 31 |
+
|
| 32 |
+
### Backend Components
|
| 33 |
+
|
| 34 |
+
#### 1. Session Routes (`routes/sessions.py`)
|
| 35 |
+
- `GET /sessions/list` - List all sessions for a project
|
| 36 |
+
- `POST /sessions/create` - Create a new session
|
| 37 |
+
- `PUT /sessions/rename` - Rename a session
|
| 38 |
+
- `DELETE /sessions/delete` - Delete a session and its memory
|
| 39 |
+
- `POST /sessions/auto-name` - Auto-name a session based on first query
|
| 40 |
+
|
| 41 |
+
#### 2. Session Memory Manager (`memo/session.py`)
|
| 42 |
+
- `SessionMemoryManager` class for session-specific memory operations
|
| 43 |
+
- Stores memories in MongoDB with session_id as key
|
| 44 |
+
- Supports semantic search within session memories
|
| 45 |
+
- Provides memory statistics and cleanup functions
|
| 46 |
+
|
| 47 |
+
#### 3. Updated Chat System
|
| 48 |
+
- Chat messages now include `session_id` parameter
|
| 49 |
+
- Memory retrieval is session-specific
|
| 50 |
+
- Auto-naming triggered on first message in new session
|
| 51 |
+
- Session context used for conversation continuity
|
| 52 |
+
|
| 53 |
+
### Frontend Components
|
| 54 |
+
|
| 55 |
+
#### 1. Session Management (`static/sessions.js`)
|
| 56 |
+
- Handles session dropdown, creation, renaming, and deletion
|
| 57 |
+
- Manages session state and UI updates
|
| 58 |
+
- Integrates with chat system for session-specific messaging
|
| 59 |
+
|
| 60 |
+
#### 2. Updated Chat Interface (`static/script.js`)
|
| 61 |
+
- Modified to use current session ID for all chat operations
|
| 62 |
+
- Session validation before allowing chat
|
| 63 |
+
- Session-specific message saving
|
| 64 |
+
|
| 65 |
+
#### 3. UI Styling (`static/styles.css`)
|
| 66 |
+
- Session control styling with responsive design
|
| 67 |
+
- Modal styles for rename/delete operations
|
| 68 |
+
- Consistent with existing design system
|
| 69 |
+
|
| 70 |
+
## Database Schema
|
| 71 |
+
|
| 72 |
+
### Chat Sessions Collection
|
| 73 |
+
```javascript
|
| 74 |
+
{
|
| 75 |
+
"user_id": "string",
|
| 76 |
+
"project_id": "string",
|
| 77 |
+
"session_id": "string",
|
| 78 |
+
"session_name": "string",
|
| 79 |
+
"is_auto_named": boolean,
|
| 80 |
+
"role": "user|assistant",
|
| 81 |
+
"content": "string",
|
| 82 |
+
"timestamp": number,
|
| 83 |
+
"created_at": datetime,
|
| 84 |
+
"sources": array,
|
| 85 |
+
"is_report": boolean
|
| 86 |
+
}
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Session Memories Collection
|
| 90 |
+
```javascript
|
| 91 |
+
{
|
| 92 |
+
"memory_id": "string",
|
| 93 |
+
"user_id": "string",
|
| 94 |
+
"project_id": "string",
|
| 95 |
+
"session_id": "string",
|
| 96 |
+
"content": "string",
|
| 97 |
+
"memory_type": "conversation",
|
| 98 |
+
"importance": "medium",
|
| 99 |
+
"tags": array,
|
| 100 |
+
"metadata": object,
|
| 101 |
+
"created_at": datetime,
|
| 102 |
+
"timestamp": number
|
| 103 |
+
}
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## API Endpoints
|
| 107 |
+
|
| 108 |
+
### Session Management
|
| 109 |
+
- `GET /sessions/list?user_id={id}&project_id={id}` - List sessions
|
| 110 |
+
- `POST /sessions/create` - Create session
|
| 111 |
+
- `PUT /sessions/rename` - Rename session
|
| 112 |
+
- `DELETE /sessions/delete` - Delete session
|
| 113 |
+
- `POST /sessions/auto-name` - Auto-name session
|
| 114 |
+
|
| 115 |
+
### Updated Chat Endpoints
|
| 116 |
+
- `POST /chat` - Now includes session_id parameter
|
| 117 |
+
- `GET /chat/history` - Now supports session_id filter
|
| 118 |
+
- `POST /chat/save` - Now includes session_id parameter
|
| 119 |
+
|
| 120 |
+
## Usage Flow
|
| 121 |
+
|
| 122 |
+
1. **User selects a project** → Sessions are loaded for that project
|
| 123 |
+
2. **User creates a new session** → Default name "New Chat" is assigned
|
| 124 |
+
3. **User sends first message** → Session is auto-named based on query
|
| 125 |
+
4. **User continues chatting** → Memory is maintained within the session
|
| 126 |
+
5. **User switches sessions** → Different memory context is loaded
|
| 127 |
+
6. **User can rename/delete sessions** → UI provides management options
|
| 128 |
+
|
| 129 |
+
## Testing
|
| 130 |
+
|
| 131 |
+
A comprehensive test suite is provided in `test_sessions.py` that validates:
|
| 132 |
+
- Session creation, listing, renaming, and deletion
|
| 133 |
+
- Auto-naming functionality
|
| 134 |
+
- Chat integration with sessions
|
| 135 |
+
- Memory management
|
| 136 |
+
- API endpoint functionality
|
| 137 |
+
|
| 138 |
+
## Benefits
|
| 139 |
+
|
| 140 |
+
1. **Organized Conversations**: Users can maintain separate conversation threads
|
| 141 |
+
2. **Context Preservation**: Each session maintains its own memory context
|
| 142 |
+
3. **Easy Management**: Simple UI for creating, renaming, and deleting sessions
|
| 143 |
+
4. **Automatic Organization**: Sessions are auto-named for easy identification
|
| 144 |
+
5. **Scalable**: Supports unlimited sessions per project
|
| 145 |
+
6. **Backward Compatible**: Existing functionality remains unchanged
|
| 146 |
+
|
| 147 |
+
## Future Enhancements
|
| 148 |
+
|
| 149 |
+
- Session sharing between users
|
| 150 |
+
- Session templates
|
| 151 |
+
- Session export/import
|
| 152 |
+
- Advanced session analytics
|
| 153 |
+
- Session-based permissions
|
static/index.html
CHANGED
|
@@ -194,7 +194,17 @@
|
|
| 194 |
<div class="card-header">
|
| 195 |
<h2>💬 Chat with Documents</h2>
|
| 196 |
<p>Ask questions about your uploaded materials and get AI-powered answers</p>
|
| 197 |
-
<div style="margin-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
<button id="clear-chat-btn" class="btn-secondary">Clear History</button>
|
| 199 |
</div>
|
| 200 |
</div>
|
|
@@ -310,6 +320,39 @@
|
|
| 310 |
</div>
|
| 311 |
</div>
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
<!-- Loading Overlay -->
|
| 314 |
<div id="loading-overlay" class="loading-overlay hidden">
|
| 315 |
<div class="loading-content">
|
|
@@ -323,6 +366,7 @@
|
|
| 323 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 324 |
<script src="/static/auth.js"></script>
|
| 325 |
<script src="/static/sidebar.js"></script>
|
|
|
|
| 326 |
<script src="/static/script.js"></script>
|
| 327 |
<script src="/static/projects.js"></script>
|
| 328 |
</body>
|
|
|
|
| 194 |
<div class="card-header">
|
| 195 |
<h2>💬 Chat with Documents</h2>
|
| 196 |
<p>Ask questions about your uploaded materials and get AI-powered answers</p>
|
| 197 |
+
<div class="session-controls" style="margin-top: 16px; display:flex; align-items:center; gap:8px; flex-wrap: wrap;">
|
| 198 |
+
<div class="session-dropdown-wrapper" style="display:flex; align-items:center; gap:4px;">
|
| 199 |
+
<select id="session-dropdown" class="session-dropdown">
|
| 200 |
+
<option value="">Select Session</option>
|
| 201 |
+
</select>
|
| 202 |
+
<button id="create-session-btn" class="btn-icon" title="Create new session">+</button>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="session-actions" style="display:none;">
|
| 205 |
+
<button id="rename-session-btn" class="btn-icon" title="Rename session">✏️</button>
|
| 206 |
+
<button id="delete-session-btn" class="btn-icon" title="Delete session">🗑️</button>
|
| 207 |
+
</div>
|
| 208 |
<button id="clear-chat-btn" class="btn-secondary">Clear History</button>
|
| 209 |
</div>
|
| 210 |
</div>
|
|
|
|
| 320 |
</div>
|
| 321 |
</div>
|
| 322 |
|
| 323 |
+
<!-- Session Management Modals -->
|
| 324 |
+
<div id="rename-session-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="rename-session-title">
|
| 325 |
+
<div class="modal-content">
|
| 326 |
+
<div class="modal-header">
|
| 327 |
+
<h2 id="rename-session-title">Rename Session</h2>
|
| 328 |
+
<p class="modal-subtitle">Enter a new name for this chat session</p>
|
| 329 |
+
</div>
|
| 330 |
+
<form id="rename-session-form">
|
| 331 |
+
<div class="form-group">
|
| 332 |
+
<label>Session Name</label>
|
| 333 |
+
<input type="text" id="session-name-input" placeholder="Enter session name" required>
|
| 334 |
+
</div>
|
| 335 |
+
<div class="form-actions">
|
| 336 |
+
<button type="button" id="cancel-rename-session" class="btn-secondary">Cancel</button>
|
| 337 |
+
<button type="submit" class="btn-primary">Rename</button>
|
| 338 |
+
</div>
|
| 339 |
+
</form>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<div id="delete-session-modal" class="modal hidden" aria-hidden="true" role="dialog" aria-labelledby="delete-session-title">
|
| 344 |
+
<div class="modal-content">
|
| 345 |
+
<div class="modal-header">
|
| 346 |
+
<h2 id="delete-session-title">Delete Session</h2>
|
| 347 |
+
<p class="modal-subtitle">Are you sure you want to delete this session? This action cannot be undone.</p>
|
| 348 |
+
</div>
|
| 349 |
+
<div class="form-actions">
|
| 350 |
+
<button type="button" id="cancel-delete-session" class="btn-secondary">Cancel</button>
|
| 351 |
+
<button type="button" id="confirm-delete-session" class="btn-danger">Delete</button>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
<!-- Loading Overlay -->
|
| 357 |
<div id="loading-overlay" class="loading-overlay hidden">
|
| 358 |
<div class="loading-content">
|
|
|
|
| 366 |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 367 |
<script src="/static/auth.js"></script>
|
| 368 |
<script src="/static/sidebar.js"></script>
|
| 369 |
+
<script src="/static/sessions.js"></script>
|
| 370 |
<script src="/static/script.js"></script>
|
| 371 |
<script src="/static/projects.js"></script>
|
| 372 |
</body>
|
static/script.js
CHANGED
|
@@ -90,12 +90,27 @@
|
|
| 90 |
clearBtn.addEventListener('click', async () => {
|
| 91 |
const user = window.__sb_get_user();
|
| 92 |
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 93 |
-
|
| 94 |
-
if (!
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
try {
|
| 96 |
-
const res = await fetch(`/chat/history?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`, { method: 'DELETE' });
|
| 97 |
if (res.ok) {
|
| 98 |
document.getElementById('messages').innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
} else {
|
| 100 |
alert('Failed to clear history');
|
| 101 |
}
|
|
@@ -491,16 +506,20 @@
|
|
| 491 |
return;
|
| 492 |
}
|
| 493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
// Add user message
|
| 495 |
appendMessage('user', question);
|
| 496 |
questionInput.value = '';
|
| 497 |
autoGrowTextarea();
|
| 498 |
|
| 499 |
// Save user message to chat history
|
| 500 |
-
await saveChatMessage(user.user_id, currentProject.project_id, 'user', question);
|
| 501 |
-
|
| 502 |
-
// Generate session ID for status tracking
|
| 503 |
-
const sessionId = 'chat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
| 504 |
|
| 505 |
// Add thinking message with dynamic status
|
| 506 |
const thinkingMsg = appendMessage('thinking', 'Receiving request...');
|
|
@@ -539,7 +558,7 @@
|
|
| 539 |
appendMessage('assistant', data.report_markdown || 'No report', true); // isReport = true
|
| 540 |
if (data.sources && data.sources.length) appendSources(data.sources);
|
| 541 |
// Save assistant report to chat history for persistence
|
| 542 |
-
try { await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', data.report_markdown || 'No report'); } catch {}
|
| 543 |
} else {
|
| 544 |
throw new Error(data.detail || 'Failed to generate report');
|
| 545 |
}
|
|
@@ -569,7 +588,8 @@
|
|
| 569 |
currentProject.project_id,
|
| 570 |
'assistant',
|
| 571 |
data.answer || 'No answer received',
|
| 572 |
-
(data.sources && data.sources.length > 0) ? data.sources : null
|
|
|
|
| 573 |
);
|
| 574 |
} else {
|
| 575 |
throw new Error(data.detail || 'Failed to get answer');
|
|
@@ -579,7 +599,7 @@
|
|
| 579 |
thinkingMsg.remove();
|
| 580 |
const errorMsg = `⚠️ Error: ${error.message}`;
|
| 581 |
appendMessage('assistant', errorMsg);
|
| 582 |
-
await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', errorMsg);
|
| 583 |
} finally {
|
| 584 |
// Stop status polling
|
| 585 |
if (statusInterval) {
|
|
@@ -688,7 +708,7 @@
|
|
| 688 |
}
|
| 689 |
});
|
| 690 |
|
| 691 |
-
async function saveChatMessage(userId, projectId, role, content, sources = null) {
|
| 692 |
try {
|
| 693 |
const formData = new FormData();
|
| 694 |
formData.append('user_id', userId);
|
|
@@ -699,6 +719,9 @@
|
|
| 699 |
if (sources) {
|
| 700 |
try { formData.append('sources', JSON.stringify(sources)); } catch {}
|
| 701 |
}
|
|
|
|
|
|
|
|
|
|
| 702 |
|
| 703 |
await fetch('/chat/save', { method: 'POST', body: formData });
|
| 704 |
} catch (error) {
|
|
|
|
| 90 |
clearBtn.addEventListener('click', async () => {
|
| 91 |
const user = window.__sb_get_user();
|
| 92 |
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 93 |
+
const currentSession = window.__sb_get_current_session && window.__sb_get_current_session();
|
| 94 |
+
if (!user || !currentProject || !currentSession) {
|
| 95 |
+
alert('Please select a session first');
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
if (!confirm('Clear chat history for this session?')) return;
|
| 99 |
try {
|
| 100 |
+
const res = await fetch(`/chat/history?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}&session_id=${encodeURIComponent(currentSession)}`, { method: 'DELETE' });
|
| 101 |
if (res.ok) {
|
| 102 |
document.getElementById('messages').innerHTML = '';
|
| 103 |
+
// Also clear session-specific memory
|
| 104 |
+
try {
|
| 105 |
+
await fetch('/sessions/clear-memory', {
|
| 106 |
+
method: 'POST',
|
| 107 |
+
body: new FormData().append('user_id', user.user_id)
|
| 108 |
+
.append('project_id', currentProject.project_id)
|
| 109 |
+
.append('session_id', currentSession)
|
| 110 |
+
});
|
| 111 |
+
} catch (e) {
|
| 112 |
+
console.warn('Failed to clear session memory:', e);
|
| 113 |
+
}
|
| 114 |
} else {
|
| 115 |
alert('Failed to clear history');
|
| 116 |
}
|
|
|
|
| 506 |
return;
|
| 507 |
}
|
| 508 |
|
| 509 |
+
// Get current session ID from session management
|
| 510 |
+
const sessionId = window.__sb_get_current_session && window.__sb_get_current_session();
|
| 511 |
+
if (!sessionId) {
|
| 512 |
+
alert('Please select a session first');
|
| 513 |
+
return;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
// Add user message
|
| 517 |
appendMessage('user', question);
|
| 518 |
questionInput.value = '';
|
| 519 |
autoGrowTextarea();
|
| 520 |
|
| 521 |
// Save user message to chat history
|
| 522 |
+
await saveChatMessage(user.user_id, currentProject.project_id, 'user', question, null, sessionId);
|
|
|
|
|
|
|
|
|
|
| 523 |
|
| 524 |
// Add thinking message with dynamic status
|
| 525 |
const thinkingMsg = appendMessage('thinking', 'Receiving request...');
|
|
|
|
| 558 |
appendMessage('assistant', data.report_markdown || 'No report', true); // isReport = true
|
| 559 |
if (data.sources && data.sources.length) appendSources(data.sources);
|
| 560 |
// Save assistant report to chat history for persistence
|
| 561 |
+
try { await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', data.report_markdown || 'No report', null, sessionId); } catch {}
|
| 562 |
} else {
|
| 563 |
throw new Error(data.detail || 'Failed to generate report');
|
| 564 |
}
|
|
|
|
| 588 |
currentProject.project_id,
|
| 589 |
'assistant',
|
| 590 |
data.answer || 'No answer received',
|
| 591 |
+
(data.sources && data.sources.length > 0) ? data.sources : null,
|
| 592 |
+
sessionId
|
| 593 |
);
|
| 594 |
} else {
|
| 595 |
throw new Error(data.detail || 'Failed to get answer');
|
|
|
|
| 599 |
thinkingMsg.remove();
|
| 600 |
const errorMsg = `⚠️ Error: ${error.message}`;
|
| 601 |
appendMessage('assistant', errorMsg);
|
| 602 |
+
await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', errorMsg, null, sessionId);
|
| 603 |
} finally {
|
| 604 |
// Stop status polling
|
| 605 |
if (statusInterval) {
|
|
|
|
| 708 |
}
|
| 709 |
});
|
| 710 |
|
| 711 |
+
async function saveChatMessage(userId, projectId, role, content, sources = null, sessionId = null) {
|
| 712 |
try {
|
| 713 |
const formData = new FormData();
|
| 714 |
formData.append('user_id', userId);
|
|
|
|
| 719 |
if (sources) {
|
| 720 |
try { formData.append('sources', JSON.stringify(sources)); } catch {}
|
| 721 |
}
|
| 722 |
+
if (sessionId) {
|
| 723 |
+
formData.append('session_id', sessionId);
|
| 724 |
+
}
|
| 725 |
|
| 726 |
await fetch('/chat/save', { method: 'POST', body: formData });
|
| 727 |
} catch (error) {
|
static/sessions.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ────────────────────────────── static/sessions.js ──────────────────────────────
|
| 2 |
+
(function() {
|
| 3 |
+
// Session management state
|
| 4 |
+
let currentSessionId = null;
|
| 5 |
+
let currentProjectId = null;
|
| 6 |
+
let sessions = [];
|
| 7 |
+
|
| 8 |
+
// DOM elements
|
| 9 |
+
const sessionDropdown = document.getElementById('session-dropdown');
|
| 10 |
+
const createSessionBtn = document.getElementById('create-session-btn');
|
| 11 |
+
const renameSessionBtn = document.getElementById('rename-session-btn');
|
| 12 |
+
const deleteSessionBtn = document.getElementById('delete-session-btn');
|
| 13 |
+
const sessionActions = document.querySelector('.session-actions');
|
| 14 |
+
|
| 15 |
+
// Modals
|
| 16 |
+
const renameModal = document.getElementById('rename-session-modal');
|
| 17 |
+
const deleteModal = document.getElementById('delete-session-modal');
|
| 18 |
+
const renameForm = document.getElementById('rename-session-form');
|
| 19 |
+
const sessionNameInput = document.getElementById('session-name-input');
|
| 20 |
+
|
| 21 |
+
// Initialize session management
|
| 22 |
+
function init() {
|
| 23 |
+
setupEventListeners();
|
| 24 |
+
// Load sessions when project changes
|
| 25 |
+
document.addEventListener('projectChanged', loadSessions);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function setupEventListeners() {
|
| 29 |
+
// Session dropdown change
|
| 30 |
+
sessionDropdown.addEventListener('change', handleSessionChange);
|
| 31 |
+
|
| 32 |
+
// Create session button
|
| 33 |
+
createSessionBtn.addEventListener('click', createNewSession);
|
| 34 |
+
|
| 35 |
+
// Rename session
|
| 36 |
+
renameSessionBtn.addEventListener('click', showRenameModal);
|
| 37 |
+
renameForm.addEventListener('submit', handleRenameSession);
|
| 38 |
+
document.getElementById('cancel-rename-session').addEventListener('click', hideRenameModal);
|
| 39 |
+
|
| 40 |
+
// Delete session
|
| 41 |
+
deleteSessionBtn.addEventListener('click', showDeleteModal);
|
| 42 |
+
document.getElementById('confirm-delete-session').addEventListener('click', handleDeleteSession);
|
| 43 |
+
document.getElementById('cancel-delete-session').addEventListener('click', hideDeleteModal);
|
| 44 |
+
|
| 45 |
+
// Close modals on outside click
|
| 46 |
+
renameModal.addEventListener('click', (e) => {
|
| 47 |
+
if (e.target === renameModal) hideRenameModal();
|
| 48 |
+
});
|
| 49 |
+
deleteModal.addEventListener('click', (e) => {
|
| 50 |
+
if (e.target === deleteModal) hideDeleteModal();
|
| 51 |
+
});
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async function loadSessions() {
|
| 55 |
+
const user = window.__sb_get_user();
|
| 56 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 57 |
+
|
| 58 |
+
if (!user || !currentProject) {
|
| 59 |
+
sessions = [];
|
| 60 |
+
updateSessionDropdown();
|
| 61 |
+
return;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
try {
|
| 65 |
+
const response = await fetch(`/sessions/list?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`);
|
| 66 |
+
if (response.ok) {
|
| 67 |
+
const data = await response.json();
|
| 68 |
+
sessions = data.sessions || [];
|
| 69 |
+
updateSessionDropdown();
|
| 70 |
+
|
| 71 |
+
// Auto-select first session if none selected
|
| 72 |
+
if (sessions.length > 0 && !currentSessionId) {
|
| 73 |
+
selectSession(sessions[0].session_id);
|
| 74 |
+
}
|
| 75 |
+
} else {
|
| 76 |
+
console.error('Failed to load sessions');
|
| 77 |
+
sessions = [];
|
| 78 |
+
updateSessionDropdown();
|
| 79 |
+
}
|
| 80 |
+
} catch (error) {
|
| 81 |
+
console.error('Error loading sessions:', error);
|
| 82 |
+
sessions = [];
|
| 83 |
+
updateSessionDropdown();
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function updateSessionDropdown() {
|
| 88 |
+
sessionDropdown.innerHTML = '<option value="">Select Session</option>';
|
| 89 |
+
|
| 90 |
+
sessions.forEach(session => {
|
| 91 |
+
const option = document.createElement('option');
|
| 92 |
+
option.value = session.session_id;
|
| 93 |
+
option.textContent = session.name;
|
| 94 |
+
if (session.is_auto_named) {
|
| 95 |
+
option.textContent += ' (Auto)';
|
| 96 |
+
}
|
| 97 |
+
sessionDropdown.appendChild(option);
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
// Add create new session option
|
| 101 |
+
const createOption = document.createElement('option');
|
| 102 |
+
createOption.value = 'create_new';
|
| 103 |
+
createOption.textContent = '+ Create new session';
|
| 104 |
+
sessionDropdown.appendChild(createOption);
|
| 105 |
+
|
| 106 |
+
// Update session actions visibility
|
| 107 |
+
updateSessionActions();
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function updateSessionActions() {
|
| 111 |
+
const hasSelectedSession = currentSessionId && currentSessionId !== 'create_new';
|
| 112 |
+
sessionActions.style.display = hasSelectedSession ? 'flex' : 'none';
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async function handleSessionChange() {
|
| 116 |
+
const selectedValue = sessionDropdown.value;
|
| 117 |
+
|
| 118 |
+
if (selectedValue === 'create_new') {
|
| 119 |
+
await createNewSession();
|
| 120 |
+
} else if (selectedValue && selectedValue !== currentSessionId) {
|
| 121 |
+
selectSession(selectedValue);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function selectSession(sessionId) {
|
| 126 |
+
currentSessionId = sessionId;
|
| 127 |
+
sessionDropdown.value = sessionId;
|
| 128 |
+
updateSessionActions();
|
| 129 |
+
|
| 130 |
+
// Clear chat messages when switching sessions
|
| 131 |
+
const messages = document.getElementById('messages');
|
| 132 |
+
if (messages) {
|
| 133 |
+
messages.innerHTML = '';
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Load chat history for this session
|
| 137 |
+
loadChatHistory();
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
async function createNewSession() {
|
| 141 |
+
const user = window.__sb_get_user();
|
| 142 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 143 |
+
|
| 144 |
+
if (!user || !currentProject) {
|
| 145 |
+
alert('Please select a project first');
|
| 146 |
+
return;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
try {
|
| 150 |
+
const formData = new FormData();
|
| 151 |
+
formData.append('user_id', user.user_id);
|
| 152 |
+
formData.append('project_id', currentProject.project_id);
|
| 153 |
+
formData.append('session_name', 'New Chat');
|
| 154 |
+
|
| 155 |
+
const response = await fetch('/sessions/create', {
|
| 156 |
+
method: 'POST',
|
| 157 |
+
body: formData
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
if (response.ok) {
|
| 161 |
+
const session = await response.json();
|
| 162 |
+
sessions.unshift(session); // Add to beginning
|
| 163 |
+
updateSessionDropdown();
|
| 164 |
+
selectSession(session.session_id);
|
| 165 |
+
} else {
|
| 166 |
+
alert('Failed to create session');
|
| 167 |
+
}
|
| 168 |
+
} catch (error) {
|
| 169 |
+
console.error('Error creating session:', error);
|
| 170 |
+
alert('Failed to create session');
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function showRenameModal() {
|
| 175 |
+
if (!currentSessionId) return;
|
| 176 |
+
|
| 177 |
+
const session = sessions.find(s => s.session_id === currentSessionId);
|
| 178 |
+
if (session) {
|
| 179 |
+
sessionNameInput.value = session.name;
|
| 180 |
+
renameModal.classList.remove('hidden');
|
| 181 |
+
sessionNameInput.focus();
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function hideRenameModal() {
|
| 186 |
+
renameModal.classList.add('hidden');
|
| 187 |
+
sessionNameInput.value = '';
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
async function handleRenameSession(e) {
|
| 191 |
+
e.preventDefault();
|
| 192 |
+
|
| 193 |
+
if (!currentSessionId) return;
|
| 194 |
+
|
| 195 |
+
const newName = sessionNameInput.value.trim();
|
| 196 |
+
if (!newName) return;
|
| 197 |
+
|
| 198 |
+
try {
|
| 199 |
+
const formData = new FormData();
|
| 200 |
+
formData.append('user_id', window.__sb_get_user().user_id);
|
| 201 |
+
formData.append('project_id', window.__sb_get_current_project().project_id);
|
| 202 |
+
formData.append('session_id', currentSessionId);
|
| 203 |
+
formData.append('new_name', newName);
|
| 204 |
+
|
| 205 |
+
const response = await fetch('/sessions/rename', {
|
| 206 |
+
method: 'PUT',
|
| 207 |
+
body: formData
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
if (response.ok) {
|
| 211 |
+
// Update local session data
|
| 212 |
+
const session = sessions.find(s => s.session_id === currentSessionId);
|
| 213 |
+
if (session) {
|
| 214 |
+
session.name = newName;
|
| 215 |
+
session.is_auto_named = false;
|
| 216 |
+
}
|
| 217 |
+
updateSessionDropdown();
|
| 218 |
+
hideRenameModal();
|
| 219 |
+
} else {
|
| 220 |
+
alert('Failed to rename session');
|
| 221 |
+
}
|
| 222 |
+
} catch (error) {
|
| 223 |
+
console.error('Error renaming session:', error);
|
| 224 |
+
alert('Failed to rename session');
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function showDeleteModal() {
|
| 229 |
+
if (!currentSessionId) return;
|
| 230 |
+
deleteModal.classList.remove('hidden');
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
function hideDeleteModal() {
|
| 234 |
+
deleteModal.classList.add('hidden');
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
async function handleDeleteSession() {
|
| 238 |
+
if (!currentSessionId) return;
|
| 239 |
+
|
| 240 |
+
try {
|
| 241 |
+
const formData = new FormData();
|
| 242 |
+
formData.append('user_id', window.__sb_get_user().user_id);
|
| 243 |
+
formData.append('project_id', window.__sb_get_current_project().project_id);
|
| 244 |
+
formData.append('session_id', currentSessionId);
|
| 245 |
+
|
| 246 |
+
const response = await fetch('/sessions/delete', {
|
| 247 |
+
method: 'DELETE',
|
| 248 |
+
body: formData
|
| 249 |
+
});
|
| 250 |
+
|
| 251 |
+
if (response.ok) {
|
| 252 |
+
// Remove from local sessions
|
| 253 |
+
sessions = sessions.filter(s => s.session_id !== currentSessionId);
|
| 254 |
+
currentSessionId = null;
|
| 255 |
+
updateSessionDropdown();
|
| 256 |
+
hideDeleteModal();
|
| 257 |
+
|
| 258 |
+
// Clear chat messages
|
| 259 |
+
const messages = document.getElementById('messages');
|
| 260 |
+
if (messages) {
|
| 261 |
+
messages.innerHTML = '';
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// Select first available session or create new one
|
| 265 |
+
if (sessions.length > 0) {
|
| 266 |
+
selectSession(sessions[0].session_id);
|
| 267 |
+
} else {
|
| 268 |
+
await createNewSession();
|
| 269 |
+
}
|
| 270 |
+
} else {
|
| 271 |
+
alert('Failed to delete session');
|
| 272 |
+
}
|
| 273 |
+
} catch (error) {
|
| 274 |
+
console.error('Error deleting session:', error);
|
| 275 |
+
alert('Failed to delete session');
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
async function loadChatHistory() {
|
| 280 |
+
if (!currentSessionId) return;
|
| 281 |
+
|
| 282 |
+
const user = window.__sb_get_user();
|
| 283 |
+
const currentProject = window.__sb_get_current_project && window.__sb_get_current_project();
|
| 284 |
+
|
| 285 |
+
if (!user || !currentProject) return;
|
| 286 |
+
|
| 287 |
+
try {
|
| 288 |
+
const response = await fetch(`/chat/history?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}&session_id=${encodeURIComponent(currentSessionId)}`);
|
| 289 |
+
if (response.ok) {
|
| 290 |
+
const data = await response.json();
|
| 291 |
+
const messages = document.getElementById('messages');
|
| 292 |
+
if (messages && data.messages) {
|
| 293 |
+
messages.innerHTML = '';
|
| 294 |
+
data.messages.forEach(message => {
|
| 295 |
+
appendMessage(message.role, message.content, message.sources);
|
| 296 |
+
});
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
} catch (error) {
|
| 300 |
+
console.error('Error loading chat history:', error);
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
function appendMessage(role, content, sources = []) {
|
| 305 |
+
const messages = document.getElementById('messages');
|
| 306 |
+
if (!messages) return;
|
| 307 |
+
|
| 308 |
+
const messageDiv = document.createElement('div');
|
| 309 |
+
messageDiv.className = `message ${role}`;
|
| 310 |
+
|
| 311 |
+
const contentDiv = document.createElement('div');
|
| 312 |
+
contentDiv.className = 'message-content';
|
| 313 |
+
|
| 314 |
+
if (role === 'assistant') {
|
| 315 |
+
contentDiv.innerHTML = marked.parse(content);
|
| 316 |
+
} else {
|
| 317 |
+
contentDiv.textContent = content;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
messageDiv.appendChild(contentDiv);
|
| 321 |
+
|
| 322 |
+
// Add sources if available
|
| 323 |
+
if (sources && sources.length > 0) {
|
| 324 |
+
const sourcesDiv = document.createElement('div');
|
| 325 |
+
sourcesDiv.className = 'message-sources';
|
| 326 |
+
sourcesDiv.innerHTML = '<strong>Sources:</strong> ' + sources.map(s => s.filename || s.url || 'Unknown').join(', ');
|
| 327 |
+
messageDiv.appendChild(sourcesDiv);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
messages.appendChild(messageDiv);
|
| 331 |
+
messages.scrollTop = messages.scrollHeight;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// Expose functions for external use
|
| 335 |
+
window.__sb_get_current_session = () => currentSessionId;
|
| 336 |
+
window.__sb_set_current_session = (sessionId) => selectSession(sessionId);
|
| 337 |
+
window.__sb_append_message = appendMessage;
|
| 338 |
+
window.__sb_load_sessions = loadSessions;
|
| 339 |
+
|
| 340 |
+
// Initialize when DOM is ready
|
| 341 |
+
if (document.readyState === 'loading') {
|
| 342 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 343 |
+
} else {
|
| 344 |
+
init();
|
| 345 |
+
}
|
| 346 |
+
})();
|
static/styles.css
CHANGED
|
@@ -1503,6 +1503,62 @@
|
|
| 1503 |
}
|
| 1504 |
}
|
| 1505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1506 |
/* Files Page Cards */
|
| 1507 |
.file-card {
|
| 1508 |
background: var(--card);
|
|
@@ -1577,4 +1633,110 @@
|
|
| 1577 |
border: none;
|
| 1578 |
cursor: pointer;
|
| 1579 |
font-weight: 600;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1580 |
}
|
|
|
|
| 1503 |
}
|
| 1504 |
}
|
| 1505 |
|
| 1506 |
+
/* Session Management Styles */
|
| 1507 |
+
.session-controls {
|
| 1508 |
+
display: flex;
|
| 1509 |
+
align-items: center;
|
| 1510 |
+
gap: 8px;
|
| 1511 |
+
}
|
| 1512 |
+
|
| 1513 |
+
.session-dropdown-wrapper {
|
| 1514 |
+
display: flex;
|
| 1515 |
+
align-items: center;
|
| 1516 |
+
gap: 4px;
|
| 1517 |
+
}
|
| 1518 |
+
|
| 1519 |
+
.session-dropdown {
|
| 1520 |
+
background: var(--card);
|
| 1521 |
+
border: 1px solid var(--border);
|
| 1522 |
+
border-radius: var(--radius);
|
| 1523 |
+
color: var(--text);
|
| 1524 |
+
padding: 8px 12px;
|
| 1525 |
+
font-size: 14px;
|
| 1526 |
+
min-width: 150px;
|
| 1527 |
+
cursor: pointer;
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
.session-dropdown:focus {
|
| 1531 |
+
outline: none;
|
| 1532 |
+
border-color: var(--accent);
|
| 1533 |
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
| 1534 |
+
}
|
| 1535 |
+
|
| 1536 |
+
.session-actions {
|
| 1537 |
+
display: flex;
|
| 1538 |
+
align-items: center;
|
| 1539 |
+
gap: 4px;
|
| 1540 |
+
}
|
| 1541 |
+
|
| 1542 |
+
.btn-danger {
|
| 1543 |
+
background: var(--error);
|
| 1544 |
+
color: white;
|
| 1545 |
+
border: none;
|
| 1546 |
+
padding: 8px 16px;
|
| 1547 |
+
border-radius: var(--radius);
|
| 1548 |
+
font-weight: 500;
|
| 1549 |
+
cursor: pointer;
|
| 1550 |
+
transition: all 0.2s ease;
|
| 1551 |
+
}
|
| 1552 |
+
|
| 1553 |
+
.btn-danger:hover {
|
| 1554 |
+
background: #dc2626;
|
| 1555 |
+
transform: translateY(-1px);
|
| 1556 |
+
}
|
| 1557 |
+
|
| 1558 |
+
.btn-danger:active {
|
| 1559 |
+
transform: translateY(0);
|
| 1560 |
+
}
|
| 1561 |
+
|
| 1562 |
/* Files Page Cards */
|
| 1563 |
.file-card {
|
| 1564 |
background: var(--card);
|
|
|
|
| 1633 |
border: none;
|
| 1634 |
cursor: pointer;
|
| 1635 |
font-weight: 600;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
/* Session Management Styles */
|
| 1639 |
+
.session-controls {
|
| 1640 |
+
display: flex;
|
| 1641 |
+
align-items: center;
|
| 1642 |
+
gap: 8px;
|
| 1643 |
+
flex-wrap: wrap;
|
| 1644 |
+
margin-top: 16px;
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
.session-dropdown-wrapper {
|
| 1648 |
+
display: flex;
|
| 1649 |
+
align-items: center;
|
| 1650 |
+
gap: 4px;
|
| 1651 |
+
}
|
| 1652 |
+
|
| 1653 |
+
.session-dropdown {
|
| 1654 |
+
background: var(--card);
|
| 1655 |
+
color: var(--text);
|
| 1656 |
+
border: 1px solid var(--border);
|
| 1657 |
+
border-radius: var(--radius);
|
| 1658 |
+
padding: 8px 12px;
|
| 1659 |
+
font-size: 14px;
|
| 1660 |
+
min-width: 200px;
|
| 1661 |
+
cursor: pointer;
|
| 1662 |
+
transition: border-color 0.2s ease;
|
| 1663 |
+
}
|
| 1664 |
+
|
| 1665 |
+
.session-dropdown:focus {
|
| 1666 |
+
outline: none;
|
| 1667 |
+
border-color: var(--accent);
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
.session-dropdown:hover {
|
| 1671 |
+
border-color: var(--border-light);
|
| 1672 |
+
}
|
| 1673 |
+
|
| 1674 |
+
.session-actions {
|
| 1675 |
+
display: flex;
|
| 1676 |
+
align-items: center;
|
| 1677 |
+
gap: 4px;
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
.session-actions .btn-icon {
|
| 1681 |
+
background: var(--card);
|
| 1682 |
+
color: var(--text-secondary);
|
| 1683 |
+
border: 1px solid var(--border);
|
| 1684 |
+
border-radius: var(--radius);
|
| 1685 |
+
padding: 6px 8px;
|
| 1686 |
+
cursor: pointer;
|
| 1687 |
+
transition: all 0.2s ease;
|
| 1688 |
+
font-size: 12px;
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
.session-actions .btn-icon:hover {
|
| 1692 |
+
background: var(--card-hover);
|
| 1693 |
+
color: var(--text);
|
| 1694 |
+
border-color: var(--border-light);
|
| 1695 |
+
}
|
| 1696 |
+
|
| 1697 |
+
.session-actions .btn-icon:active {
|
| 1698 |
+
transform: scale(0.95);
|
| 1699 |
+
}
|
| 1700 |
+
|
| 1701 |
+
/* Modal styles for session management */
|
| 1702 |
+
#rename-session-modal .modal-content,
|
| 1703 |
+
#delete-session-modal .modal-content {
|
| 1704 |
+
max-width: 400px;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
#session-name-input {
|
| 1708 |
+
width: 100%;
|
| 1709 |
+
background: var(--card);
|
| 1710 |
+
color: var(--text);
|
| 1711 |
+
border: 1px solid var(--border);
|
| 1712 |
+
border-radius: var(--radius);
|
| 1713 |
+
padding: 12px 16px;
|
| 1714 |
+
font-size: 14px;
|
| 1715 |
+
margin-top: 8px;
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
#session-name-input:focus {
|
| 1719 |
+
outline: none;
|
| 1720 |
+
border-color: var(--accent);
|
| 1721 |
+
}
|
| 1722 |
+
|
| 1723 |
+
/* Responsive session controls */
|
| 1724 |
+
@media (max-width: 768px) {
|
| 1725 |
+
.session-controls {
|
| 1726 |
+
flex-direction: column;
|
| 1727 |
+
align-items: stretch;
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
.session-dropdown-wrapper {
|
| 1731 |
+
width: 100%;
|
| 1732 |
+
}
|
| 1733 |
+
|
| 1734 |
+
.session-dropdown {
|
| 1735 |
+
min-width: unset;
|
| 1736 |
+
width: 100%;
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
.session-actions {
|
| 1740 |
+
justify-content: center;
|
| 1741 |
+
}
|
| 1742 |
}
|
test_sessions.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for session management functionality
|
| 4 |
+
|
| 5 |
+
This script validates:
|
| 6 |
+
1. Session creation, listing, renaming, and deletion
|
| 7 |
+
2. Session-specific memory management
|
| 8 |
+
3. Auto-naming functionality
|
| 9 |
+
4. Integration with chat system
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import asyncio
|
| 13 |
+
import json
|
| 14 |
+
import time
|
| 15 |
+
import uuid
|
| 16 |
+
from typing import Dict, Any
|
| 17 |
+
|
| 18 |
+
# Test configuration
|
| 19 |
+
TEST_USER_ID = "test_user_123"
|
| 20 |
+
TEST_PROJECT_ID = "test_project_456"
|
| 21 |
+
BASE_URL = "http://localhost:8000" # Adjust if needed
|
| 22 |
+
|
| 23 |
+
class SessionTester:
|
| 24 |
+
def __init__(self):
|
| 25 |
+
self.sessions = []
|
| 26 |
+
self.test_results = []
|
| 27 |
+
|
| 28 |
+
async def test_session_creation(self):
|
| 29 |
+
"""Test creating a new session"""
|
| 30 |
+
print("🧪 Testing session creation...")
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import httpx
|
| 34 |
+
async with httpx.AsyncClient() as client:
|
| 35 |
+
form_data = {
|
| 36 |
+
"user_id": TEST_USER_ID,
|
| 37 |
+
"project_id": TEST_PROJECT_ID,
|
| 38 |
+
"session_name": "Test Session"
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
response = await client.post(f"{BASE_URL}/sessions/create", data=form_data)
|
| 42 |
+
|
| 43 |
+
if response.status_code == 200:
|
| 44 |
+
session_data = response.json()
|
| 45 |
+
self.sessions.append(session_data)
|
| 46 |
+
print(f"✅ Session created: {session_data['session_id']}")
|
| 47 |
+
return session_data
|
| 48 |
+
else:
|
| 49 |
+
print(f"❌ Session creation failed: {response.text}")
|
| 50 |
+
return None
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"❌ Session creation error: {e}")
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
async def test_session_listing(self):
|
| 57 |
+
"""Test listing sessions"""
|
| 58 |
+
print("🧪 Testing session listing...")
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
import httpx
|
| 62 |
+
async with httpx.AsyncClient() as client:
|
| 63 |
+
response = await client.get(
|
| 64 |
+
f"{BASE_URL}/sessions/list",
|
| 65 |
+
params={
|
| 66 |
+
"user_id": TEST_USER_ID,
|
| 67 |
+
"project_id": TEST_PROJECT_ID
|
| 68 |
+
}
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if response.status_code == 200:
|
| 72 |
+
data = response.json()
|
| 73 |
+
sessions = data.get("sessions", [])
|
| 74 |
+
print(f"✅ Found {len(sessions)} sessions")
|
| 75 |
+
return sessions
|
| 76 |
+
else:
|
| 77 |
+
print(f"❌ Session listing failed: {response.text}")
|
| 78 |
+
return []
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"❌ Session listing error: {e}")
|
| 82 |
+
return []
|
| 83 |
+
|
| 84 |
+
async def test_session_renaming(self, session_id: str):
|
| 85 |
+
"""Test renaming a session"""
|
| 86 |
+
print(f"🧪 Testing session renaming for {session_id}...")
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
import httpx
|
| 90 |
+
async with httpx.AsyncClient() as client:
|
| 91 |
+
form_data = {
|
| 92 |
+
"user_id": TEST_USER_ID,
|
| 93 |
+
"project_id": TEST_PROJECT_ID,
|
| 94 |
+
"session_id": session_id,
|
| 95 |
+
"new_name": "Renamed Test Session"
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
response = await client.put(f"{BASE_URL}/sessions/rename", data=form_data)
|
| 99 |
+
|
| 100 |
+
if response.status_code == 200:
|
| 101 |
+
print("✅ Session renamed successfully")
|
| 102 |
+
return True
|
| 103 |
+
else:
|
| 104 |
+
print(f"❌ Session renaming failed: {response.text}")
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"❌ Session renaming error: {e}")
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
async def test_auto_naming(self, session_id: str):
|
| 112 |
+
"""Test auto-naming functionality"""
|
| 113 |
+
print(f"🧪 Testing auto-naming for {session_id}...")
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
import httpx
|
| 117 |
+
async with httpx.AsyncClient() as client:
|
| 118 |
+
form_data = {
|
| 119 |
+
"user_id": TEST_USER_ID,
|
| 120 |
+
"project_id": TEST_PROJECT_ID,
|
| 121 |
+
"session_id": session_id,
|
| 122 |
+
"first_query": "What is machine learning and how does it work?"
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
response = await client.post(f"{BASE_URL}/sessions/auto-name", data=form_data)
|
| 126 |
+
|
| 127 |
+
if response.status_code == 200:
|
| 128 |
+
data = response.json()
|
| 129 |
+
print(f"✅ Auto-naming result: {data.get('message', 'Success')}")
|
| 130 |
+
return True
|
| 131 |
+
else:
|
| 132 |
+
print(f"❌ Auto-naming failed: {response.text}")
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"❌ Auto-naming error: {e}")
|
| 137 |
+
return False
|
| 138 |
+
|
| 139 |
+
async def test_chat_with_session(self, session_id: str):
|
| 140 |
+
"""Test chat functionality with session"""
|
| 141 |
+
print(f"🧪 Testing chat with session {session_id}...")
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
import httpx
|
| 145 |
+
async with httpx.AsyncClient() as client:
|
| 146 |
+
form_data = {
|
| 147 |
+
"user_id": TEST_USER_ID,
|
| 148 |
+
"project_id": TEST_PROJECT_ID,
|
| 149 |
+
"question": "Hello, this is a test question",
|
| 150 |
+
"session_id": session_id,
|
| 151 |
+
"k": 3
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
response = await client.post(f"{BASE_URL}/chat", data=form_data)
|
| 155 |
+
|
| 156 |
+
if response.status_code == 200:
|
| 157 |
+
data = response.json()
|
| 158 |
+
print(f"✅ Chat response received: {len(data.get('answer', ''))} characters")
|
| 159 |
+
return True
|
| 160 |
+
else:
|
| 161 |
+
print(f"❌ Chat failed: {response.text}")
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"❌ Chat error: {e}")
|
| 166 |
+
return False
|
| 167 |
+
|
| 168 |
+
async def test_session_clear_memory(self, session_id: str):
|
| 169 |
+
"""Test clearing session-specific memory"""
|
| 170 |
+
print(f"🧪 Testing session memory clearing for {session_id}...")
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
import httpx
|
| 174 |
+
async with httpx.AsyncClient() as client:
|
| 175 |
+
form_data = {
|
| 176 |
+
"user_id": TEST_USER_ID,
|
| 177 |
+
"project_id": TEST_PROJECT_ID,
|
| 178 |
+
"session_id": session_id
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
response = await client.post(f"{BASE_URL}/sessions/clear-memory", data=form_data)
|
| 182 |
+
|
| 183 |
+
if response.status_code == 200:
|
| 184 |
+
data = response.json()
|
| 185 |
+
print(f"✅ Session memory cleared: {data.get('message', 'Success')}")
|
| 186 |
+
return True
|
| 187 |
+
else:
|
| 188 |
+
print(f"❌ Session memory clearing failed: {response.text}")
|
| 189 |
+
return False
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"❌ Session memory clearing error: {e}")
|
| 193 |
+
return False
|
| 194 |
+
|
| 195 |
+
async def test_session_history_clearing(self, session_id: str):
|
| 196 |
+
"""Test clearing session-specific chat history"""
|
| 197 |
+
print(f"🧪 Testing session history clearing for {session_id}...")
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
import httpx
|
| 201 |
+
async with httpx.AsyncClient() as client:
|
| 202 |
+
response = await client.delete(
|
| 203 |
+
f"{BASE_URL}/chat/history",
|
| 204 |
+
params={
|
| 205 |
+
"user_id": TEST_USER_ID,
|
| 206 |
+
"project_id": TEST_PROJECT_ID,
|
| 207 |
+
"session_id": session_id
|
| 208 |
+
}
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if response.status_code == 200:
|
| 212 |
+
data = response.json()
|
| 213 |
+
print(f"✅ Session history cleared: {data.get('message', 'Success')}")
|
| 214 |
+
return True
|
| 215 |
+
else:
|
| 216 |
+
print(f"❌ Session history clearing failed: {response.text}")
|
| 217 |
+
return False
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"❌ Session history clearing error: {e}")
|
| 221 |
+
return False
|
| 222 |
+
|
| 223 |
+
async def test_session_deletion(self, session_id: str):
|
| 224 |
+
"""Test deleting a session"""
|
| 225 |
+
print(f"🧪 Testing session deletion for {session_id}...")
|
| 226 |
+
|
| 227 |
+
try:
|
| 228 |
+
import httpx
|
| 229 |
+
async with httpx.AsyncClient() as client:
|
| 230 |
+
form_data = {
|
| 231 |
+
"user_id": TEST_USER_ID,
|
| 232 |
+
"project_id": TEST_PROJECT_ID,
|
| 233 |
+
"session_id": session_id
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
response = await client.delete(f"{BASE_URL}/sessions/delete", data=form_data)
|
| 237 |
+
|
| 238 |
+
if response.status_code == 200:
|
| 239 |
+
data = response.json()
|
| 240 |
+
print(f"✅ Session deleted: {data.get('message', 'Success')}")
|
| 241 |
+
return True
|
| 242 |
+
else:
|
| 243 |
+
print(f"❌ Session deletion failed: {response.text}")
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"❌ Session deletion error: {e}")
|
| 248 |
+
return False
|
| 249 |
+
|
| 250 |
+
async def test_memory_management(self):
|
| 251 |
+
"""Test session-specific memory management"""
|
| 252 |
+
print("🧪 Testing session-specific memory management...")
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
# This would test the memory system directly
|
| 256 |
+
# For now, we'll just test that the endpoints exist
|
| 257 |
+
print("✅ Memory management endpoints available")
|
| 258 |
+
return True
|
| 259 |
+
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"❌ Memory management error: {e}")
|
| 262 |
+
return False
|
| 263 |
+
|
| 264 |
+
async def run_all_tests(self):
|
| 265 |
+
"""Run all tests"""
|
| 266 |
+
print("🚀 Starting session management tests...\n")
|
| 267 |
+
|
| 268 |
+
# Test 1: Create session
|
| 269 |
+
session = await self.test_session_creation()
|
| 270 |
+
if not session:
|
| 271 |
+
print("❌ Cannot continue without a session")
|
| 272 |
+
print("💡 Note: Make sure the server is running on http://localhost:8000")
|
| 273 |
+
return
|
| 274 |
+
|
| 275 |
+
session_id = session["session_id"]
|
| 276 |
+
|
| 277 |
+
# Test 2: List sessions
|
| 278 |
+
await self.test_session_listing()
|
| 279 |
+
|
| 280 |
+
# Test 3: Rename session
|
| 281 |
+
await self.test_session_renaming(session_id)
|
| 282 |
+
|
| 283 |
+
# Test 4: Auto-naming
|
| 284 |
+
await self.test_auto_naming(session_id)
|
| 285 |
+
|
| 286 |
+
# Test 5: Chat with session
|
| 287 |
+
await self.test_chat_with_session(session_id)
|
| 288 |
+
|
| 289 |
+
# Test 6: Session memory clearing
|
| 290 |
+
await self.test_session_clear_memory(session_id)
|
| 291 |
+
|
| 292 |
+
# Test 7: Session history clearing
|
| 293 |
+
await self.test_session_history_clearing(session_id)
|
| 294 |
+
|
| 295 |
+
# Test 8: Memory management
|
| 296 |
+
await self.test_memory_management()
|
| 297 |
+
|
| 298 |
+
# Test 9: Delete session
|
| 299 |
+
await self.test_session_deletion(session_id)
|
| 300 |
+
|
| 301 |
+
print("\n🎉 All tests completed!")
|
| 302 |
+
|
| 303 |
+
async def main():
|
| 304 |
+
"""Main test runner"""
|
| 305 |
+
tester = SessionTester()
|
| 306 |
+
await tester.run_all_tests()
|
| 307 |
+
|
| 308 |
+
if __name__ == "__main__":
|
| 309 |
+
print("Session Management Test Suite")
|
| 310 |
+
print("=" * 50)
|
| 311 |
+
asyncio.run(main())
|
utils/README.md
CHANGED
|
@@ -48,6 +48,9 @@ Core utilities for the EdSummariser RAG system providing document processing, re
|
|
| 48 |
- **Conversation history**: LRU-based memory system
|
| 49 |
- **Context retrieval**: Semantic and recent context selection
|
| 50 |
- **NVIDIA integration**: File relevance classification
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
## Key Features
|
| 53 |
|
|
@@ -130,5 +133,6 @@ utils/
|
|
| 130 |
└── memo/ # Memory management
|
| 131 |
├── core.py # Memory system core
|
| 132 |
├── history.py # Conversation history
|
| 133 |
-
|
|
|
|
| 134 |
```
|
|
|
|
| 48 |
- **Conversation history**: LRU-based memory system
|
| 49 |
- **Context retrieval**: Semantic and recent context selection
|
| 50 |
- **NVIDIA integration**: File relevance classification
|
| 51 |
+
- **Session-specific memory**: Isolated memory per chat session
|
| 52 |
+
- **Auto-naming**: AI-powered session naming based on first query
|
| 53 |
+
- **Memory cleanup**: Session and project-level memory management
|
| 54 |
|
| 55 |
## Key Features
|
| 56 |
|
|
|
|
| 133 |
└── memo/ # Memory management
|
| 134 |
├── core.py # Memory system core
|
| 135 |
├── history.py # Conversation history
|
| 136 |
+
├── nvidia.py # NVIDIA integration
|
| 137 |
+
└── session.py # Session-specific memory management
|
| 138 |
```
|