Spaces:
Running
Running
Girish Jeswani commited on
Commit ·
1719e8e
1
Parent(s): dd3fd69
add chat specific context
Browse files- multi_llm_chatbot_backend/app/api/routes/chat.py +150 -95
- multi_llm_chatbot_backend/app/api/routes/sessions.py +186 -18
- multi_llm_chatbot_backend/app/api/utils.py +141 -9
- phd-advisor-frontend/src/components/Sidebar.js +41 -19
- phd-advisor-frontend/src/pages/ChatPage.js +195 -111
- phd-advisor-frontend/src/styles/Sidebar.css +50 -0
multi_llm_chatbot_backend/app/api/routes/chat.py
CHANGED
|
@@ -1,46 +1,165 @@
|
|
| 1 |
-
from fastapi import APIRouter, Request, HTTPException, Body
|
| 2 |
from app.models.persona import Persona
|
| 3 |
from app.core.session_manager import get_session_manager
|
| 4 |
-
from app.api.utils import
|
| 5 |
from app.core.bootstrap import chat_orchestrator
|
|
|
|
|
|
|
| 6 |
from pydantic import BaseModel
|
|
|
|
| 7 |
import logging
|
|
|
|
|
|
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
| 11 |
router = APIRouter()
|
| 12 |
-
|
| 13 |
session_manager = get_session_manager()
|
| 14 |
|
| 15 |
-
#
|
| 16 |
class UserInput(BaseModel):
|
| 17 |
user_input: str
|
| 18 |
|
| 19 |
class ChatMessage(BaseModel):
|
| 20 |
user_input: str
|
| 21 |
-
session_id: str = None
|
|
|
|
| 22 |
response_length: str = "medium"
|
| 23 |
|
| 24 |
class ReplyToAdvisor(BaseModel):
|
| 25 |
user_input: str
|
| 26 |
advisor_id: str
|
| 27 |
original_message_id: str = None
|
|
|
|
| 28 |
|
| 29 |
class PersonaQuery(BaseModel):
|
| 30 |
question: str
|
| 31 |
persona: str
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
@router.post("/chat-sequential")
|
| 35 |
async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
| 36 |
"""
|
| 37 |
-
Enhanced sequential chat with
|
| 38 |
"""
|
| 39 |
try:
|
| 40 |
-
#
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
#
|
| 44 |
session = session_manager.get_session(session_id)
|
| 45 |
|
| 46 |
# Debug logging
|
|
@@ -69,10 +188,7 @@ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
|
| 69 |
"session_id": session_id
|
| 70 |
}
|
| 71 |
|
| 72 |
-
#
|
| 73 |
-
# (user message already added to session above)
|
| 74 |
-
|
| 75 |
-
# Get intelligently ordered personas based on context (TOP 3 ONLY)
|
| 76 |
top_personas = await chat_orchestrator.get_top_personas(
|
| 77 |
session_id=session_id,
|
| 78 |
k=3 # Limit to top 3 most relevant personas
|
|
@@ -100,7 +216,6 @@ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
|
| 100 |
"response": persona_result["response"]
|
| 101 |
})
|
| 102 |
else:
|
| 103 |
-
# Fallback response
|
| 104 |
responses.append({
|
| 105 |
"persona": chat_orchestrator.personas[persona_id].name,
|
| 106 |
"persona_id": persona_id,
|
|
@@ -109,7 +224,6 @@ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
|
| 109 |
|
| 110 |
except Exception as e:
|
| 111 |
logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
|
| 112 |
-
# Error fallback
|
| 113 |
responses.append({
|
| 114 |
"persona": chat_orchestrator.personas[persona_id].name,
|
| 115 |
"persona_id": persona_id,
|
|
@@ -118,7 +232,8 @@ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
|
| 118 |
|
| 119 |
return {
|
| 120 |
"type": "sequential_responses",
|
| 121 |
-
"responses": responses
|
|
|
|
| 122 |
}
|
| 123 |
|
| 124 |
except Exception as e:
|
|
@@ -128,29 +243,30 @@ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
|
| 128 |
"responses": [{
|
| 129 |
"persona": "System",
|
| 130 |
"response": "I'm having trouble processing your request. Could you please try again?"
|
| 131 |
-
}]
|
|
|
|
| 132 |
}
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
@router.post("/chat/{persona_id}")
|
| 135 |
async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
|
| 136 |
-
"""Chat with a specific advisor -
|
| 137 |
try:
|
| 138 |
if persona_id not in chat_orchestrator.personas:
|
| 139 |
raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found")
|
| 140 |
|
| 141 |
-
#
|
| 142 |
-
session_id =
|
| 143 |
|
| 144 |
-
# Use new orchestrator
|
| 145 |
result = await chat_orchestrator.chat_with_persona(
|
| 146 |
user_input=input.user_input,
|
| 147 |
persona_id=persona_id,
|
| 148 |
session_id=session_id
|
| 149 |
)
|
| 150 |
|
| 151 |
-
#
|
| 152 |
if result.get("type") == "single_persona_response" and "persona" in result:
|
| 153 |
-
# New expected structure
|
| 154 |
persona_data = result["persona"]
|
| 155 |
return {
|
| 156 |
"persona": persona_data["persona_name"],
|
|
@@ -158,20 +274,12 @@ async def chat_with_specific_advisor(persona_id: str, input: UserInput, request:
|
|
| 158 |
"response": persona_data["response"]
|
| 159 |
}
|
| 160 |
elif "persona_id" in result and "response" in result:
|
| 161 |
-
# Current actual structure from orchestrator
|
| 162 |
return {
|
| 163 |
"persona": result["persona_name"],
|
| 164 |
"persona_id": result["persona_id"],
|
| 165 |
"response": result["response"]
|
| 166 |
}
|
| 167 |
-
elif result.get("type") == "error" or "error" in result:
|
| 168 |
-
# Error handling
|
| 169 |
-
return {
|
| 170 |
-
"persona": "System",
|
| 171 |
-
"response": result.get("error", "I'm having trouble generating a response right now. Please try again.")
|
| 172 |
-
}
|
| 173 |
else:
|
| 174 |
-
# Fallback
|
| 175 |
return {
|
| 176 |
"persona": "System",
|
| 177 |
"response": "I'm having trouble generating a response right now. Please try again."
|
|
@@ -188,32 +296,32 @@ async def chat_with_specific_advisor(persona_id: str, input: UserInput, request:
|
|
| 188 |
|
| 189 |
@router.post("/reply-to-advisor")
|
| 190 |
async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
|
| 191 |
-
"""Reply to a specific advisor with proper context"""
|
| 192 |
try:
|
| 193 |
if reply.advisor_id not in chat_orchestrator.personas:
|
| 194 |
raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found")
|
| 195 |
|
| 196 |
-
#
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
# Get the session to access conversation history
|
| 200 |
session = session_manager.get_session(session_id)
|
| 201 |
|
| 202 |
# Find the original message being replied to for context
|
| 203 |
original_message = None
|
| 204 |
if reply.original_message_id:
|
| 205 |
-
# Look through session history to find the original message
|
| 206 |
for msg in session.messages:
|
| 207 |
if getattr(msg, 'id', None) == reply.original_message_id:
|
| 208 |
original_message = msg.content
|
| 209 |
break
|
| 210 |
|
| 211 |
-
# Create context-aware input
|
| 212 |
contextual_input = reply.user_input
|
| 213 |
if original_message:
|
| 214 |
contextual_input = f"[Replying to your previous message: '{original_message[:100]}...'] {reply.user_input}"
|
| 215 |
|
| 216 |
-
# Use orchestrator with context
|
| 217 |
result = await chat_orchestrator.chat_with_persona(
|
| 218 |
user_input=contextual_input,
|
| 219 |
persona_id=reply.advisor_id,
|
|
@@ -254,66 +362,13 @@ async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
|
|
| 254 |
"persona": "System",
|
| 255 |
"response": "I'm having trouble generating a reply right now. Please try again."
|
| 256 |
}
|
| 257 |
-
|
| 258 |
-
@router.post("/chat/{persona_id}")
|
| 259 |
-
async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request):
|
| 260 |
-
"""
|
| 261 |
-
Chat with a specific persona - Enhanced with RAG debugging
|
| 262 |
-
|
| 263 |
-
This endpoint helps debug RAG integration by testing individual personas
|
| 264 |
-
"""
|
| 265 |
-
try:
|
| 266 |
-
session_id = get_or_create_session_for_request(request, message.session_id)
|
| 267 |
-
|
| 268 |
-
# Validate persona exists
|
| 269 |
-
if persona_id not in chat_orchestrator.personas:
|
| 270 |
-
available_personas = list(chat_orchestrator.personas.keys())
|
| 271 |
-
raise HTTPException(
|
| 272 |
-
status_code=400,
|
| 273 |
-
detail=f"Persona '{persona_id}' not found. Available: {available_personas}"
|
| 274 |
-
)
|
| 275 |
-
|
| 276 |
-
# Use the enhanced orchestrator method
|
| 277 |
-
result = await chat_orchestrator.chat_with_persona(
|
| 278 |
-
user_input=message.user_input,
|
| 279 |
-
persona_id=persona_id,
|
| 280 |
-
session_id=session_id,
|
| 281 |
-
response_length=message.response_length or "medium"
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
# Fix: Handle the response structure properly
|
| 285 |
-
if result.get("type") == "single_persona_response" and "persona" in result:
|
| 286 |
-
persona_data = result["persona"]
|
| 287 |
-
|
| 288 |
-
# Add debugging information
|
| 289 |
-
result["debug_info"] = {
|
| 290 |
-
"persona_id": persona_id,
|
| 291 |
-
"session_id": session_id,
|
| 292 |
-
"query_length": len(message.user_input),
|
| 293 |
-
"rag_manager_available": True,
|
| 294 |
-
"used_documents": persona_data.get("used_documents", False),
|
| 295 |
-
"chunks_used": persona_data.get("document_chunks_used", 0)
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
return result
|
| 299 |
-
|
| 300 |
-
except HTTPException:
|
| 301 |
-
raise
|
| 302 |
-
except Exception as e:
|
| 303 |
-
logger.error(f"Error in individual persona chat: {str(e)}")
|
| 304 |
-
return {
|
| 305 |
-
"type": "error",
|
| 306 |
-
"message": f"Error chatting with {persona_id}: {str(e)}",
|
| 307 |
-
"persona_id": persona_id
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
@router.post("/ask/")
|
| 311 |
async def ask_question(query: PersonaQuery, request: Request):
|
| 312 |
-
"""Ask question -
|
| 313 |
try:
|
| 314 |
-
session_id =
|
| 315 |
|
| 316 |
-
# Use the new orchestrator
|
| 317 |
result = await chat_orchestrator.chat_with_persona(
|
| 318 |
user_input=query.question,
|
| 319 |
persona_id=query.persona,
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, HTTPException, Body, Depends
|
| 2 |
from app.models.persona import Persona
|
| 3 |
from app.core.session_manager import get_session_manager
|
| 4 |
+
from app.api.utils import get_or_create_session_for_request_async
|
| 5 |
from app.core.bootstrap import chat_orchestrator
|
| 6 |
+
from app.core.auth import get_current_active_user
|
| 7 |
+
from app.models.user import User
|
| 8 |
from pydantic import BaseModel
|
| 9 |
+
from typing import Optional
|
| 10 |
import logging
|
| 11 |
+
from app.core.database import get_database
|
| 12 |
+
from bson import ObjectId
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
| 16 |
router = APIRouter()
|
|
|
|
| 17 |
session_manager = get_session_manager()
|
| 18 |
|
| 19 |
+
# Enhanced data models
|
| 20 |
class UserInput(BaseModel):
|
| 21 |
user_input: str
|
| 22 |
|
| 23 |
class ChatMessage(BaseModel):
|
| 24 |
user_input: str
|
| 25 |
+
session_id: Optional[str] = None
|
| 26 |
+
chat_session_id: Optional[str] = None # MongoDB chat session ID
|
| 27 |
response_length: str = "medium"
|
| 28 |
|
| 29 |
class ReplyToAdvisor(BaseModel):
|
| 30 |
user_input: str
|
| 31 |
advisor_id: str
|
| 32 |
original_message_id: str = None
|
| 33 |
+
chat_session_id: Optional[str] = None
|
| 34 |
|
| 35 |
class PersonaQuery(BaseModel):
|
| 36 |
question: str
|
| 37 |
persona: str
|
| 38 |
|
| 39 |
+
class SwitchChatRequest(BaseModel):
|
| 40 |
+
chat_session_id: str
|
| 41 |
+
|
| 42 |
+
class NewChatRequest(BaseModel):
|
| 43 |
+
title: Optional[str] = "New Chat"
|
| 44 |
+
|
| 45 |
+
@router.post("/switch-chat")
|
| 46 |
+
async def switch_to_chat(
|
| 47 |
+
request: SwitchChatRequest,
|
| 48 |
+
req: Request,
|
| 49 |
+
current_user: User = Depends(get_current_active_user)
|
| 50 |
+
):
|
| 51 |
+
"""
|
| 52 |
+
Switch to an existing chat session and load its context
|
| 53 |
+
FIXED VERSION - Returns messages in correct frontend format
|
| 54 |
+
"""
|
| 55 |
+
try:
|
| 56 |
+
# Load the chat session into memory context
|
| 57 |
+
memory_session_id = await get_or_create_session_for_request_async(
|
| 58 |
+
req,
|
| 59 |
+
chat_session_id=request.chat_session_id,
|
| 60 |
+
user_id=str(current_user.id)
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
if not memory_session_id:
|
| 64 |
+
raise HTTPException(status_code=404, detail="Chat session not found")
|
| 65 |
+
|
| 66 |
+
# Get the loaded session
|
| 67 |
+
session = session_manager.get_session(memory_session_id)
|
| 68 |
+
|
| 69 |
+
# Get the original MongoDB chat session to retrieve messages in proper format
|
| 70 |
+
db = get_database()
|
| 71 |
+
chat_session = await db.chat_sessions.find_one({
|
| 72 |
+
"_id": ObjectId(request.chat_session_id),
|
| 73 |
+
"user_id": current_user.id,
|
| 74 |
+
"is_active": True
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
if not chat_session:
|
| 78 |
+
raise HTTPException(status_code=404, detail="Chat session not found in database")
|
| 79 |
+
|
| 80 |
+
# Return the messages in the original frontend format from MongoDB
|
| 81 |
+
original_messages = chat_session.get("messages", [])
|
| 82 |
+
|
| 83 |
+
logger.info(f"Switching to chat {request.chat_session_id} with {len(original_messages)} messages")
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
"status": "success",
|
| 87 |
+
"memory_session_id": memory_session_id,
|
| 88 |
+
"chat_session_id": request.chat_session_id,
|
| 89 |
+
"message_count": len(original_messages),
|
| 90 |
+
"context": {
|
| 91 |
+
"messages": original_messages, # Return original format messages
|
| 92 |
+
"rag_info": session.get_rag_stats()
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
except HTTPException:
|
| 97 |
+
raise
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Error switching to chat: {e}")
|
| 100 |
+
raise HTTPException(status_code=500, detail="Failed to switch to chat")
|
| 101 |
+
|
| 102 |
+
@router.post("/new-chat")
|
| 103 |
+
async def create_new_chat(
|
| 104 |
+
request: NewChatRequest,
|
| 105 |
+
req: Request,
|
| 106 |
+
current_user: User = Depends(get_current_active_user)
|
| 107 |
+
):
|
| 108 |
+
"""
|
| 109 |
+
Create a new chat with fresh context
|
| 110 |
+
"""
|
| 111 |
+
try:
|
| 112 |
+
# Create a completely new session (no chat_session_id means fresh context)
|
| 113 |
+
memory_session_id = await get_or_create_session_for_request_async(req)
|
| 114 |
+
|
| 115 |
+
# Ensure the session is completely clean
|
| 116 |
+
session = session_manager.get_session(memory_session_id)
|
| 117 |
+
session.clear_all_data() # This clears both messages and documents
|
| 118 |
+
|
| 119 |
+
return {
|
| 120 |
+
"status": "success",
|
| 121 |
+
"memory_session_id": memory_session_id,
|
| 122 |
+
"message": "New chat created with fresh context",
|
| 123 |
+
"context": {
|
| 124 |
+
"messages": [],
|
| 125 |
+
"rag_info": {"total_documents": 0, "total_chunks": 0}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.error(f"Error creating new chat: {e}")
|
| 131 |
+
raise HTTPException(status_code=500, detail="Failed to create new chat")
|
| 132 |
|
| 133 |
@router.post("/chat-sequential")
|
| 134 |
async def chat_sequential_enhanced(message: ChatMessage, request: Request):
|
| 135 |
"""
|
| 136 |
+
Enhanced sequential chat with proper session management
|
| 137 |
"""
|
| 138 |
try:
|
| 139 |
+
# Determine session ID based on whether this is an existing chat or new
|
| 140 |
+
if message.chat_session_id:
|
| 141 |
+
# This is an existing chat - we should have already loaded it via switch-chat
|
| 142 |
+
# Use the memory session ID format
|
| 143 |
+
session_id = f"chat_{message.chat_session_id}"
|
| 144 |
+
|
| 145 |
+
# Verify the session exists in memory
|
| 146 |
+
if session_id not in session_manager.sessions:
|
| 147 |
+
# If not in memory, this means switch-chat wasn't called
|
| 148 |
+
# We should load it now (but log a warning)
|
| 149 |
+
logger.warning(f"Chat session {message.chat_session_id} not in memory, loading now")
|
| 150 |
+
session_id = await get_or_create_session_for_request_async(
|
| 151 |
+
request,
|
| 152 |
+
chat_session_id=message.chat_session_id,
|
| 153 |
+
user_id="anonymous" # We don't have user context here
|
| 154 |
+
)
|
| 155 |
+
else:
|
| 156 |
+
# This is a new chat or no specific chat session
|
| 157 |
+
session_id = await get_or_create_session_for_request_async(
|
| 158 |
+
request,
|
| 159 |
+
session_id_override=message.session_id
|
| 160 |
+
)
|
| 161 |
|
| 162 |
+
# Get the session
|
| 163 |
session = session_manager.get_session(session_id)
|
| 164 |
|
| 165 |
# Debug logging
|
|
|
|
| 188 |
"session_id": session_id
|
| 189 |
}
|
| 190 |
|
| 191 |
+
# No clarification needed - proceed with intelligent persona ordering
|
|
|
|
|
|
|
|
|
|
| 192 |
top_personas = await chat_orchestrator.get_top_personas(
|
| 193 |
session_id=session_id,
|
| 194 |
k=3 # Limit to top 3 most relevant personas
|
|
|
|
| 216 |
"response": persona_result["response"]
|
| 217 |
})
|
| 218 |
else:
|
|
|
|
| 219 |
responses.append({
|
| 220 |
"persona": chat_orchestrator.personas[persona_id].name,
|
| 221 |
"persona_id": persona_id,
|
|
|
|
| 224 |
|
| 225 |
except Exception as e:
|
| 226 |
logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
|
|
|
|
| 227 |
responses.append({
|
| 228 |
"persona": chat_orchestrator.personas[persona_id].name,
|
| 229 |
"persona_id": persona_id,
|
|
|
|
| 232 |
|
| 233 |
return {
|
| 234 |
"type": "sequential_responses",
|
| 235 |
+
"responses": responses,
|
| 236 |
+
"session_id": session_id # Include session ID in response
|
| 237 |
}
|
| 238 |
|
| 239 |
except Exception as e:
|
|
|
|
| 243 |
"responses": [{
|
| 244 |
"persona": "System",
|
| 245 |
"response": "I'm having trouble processing your request. Could you please try again?"
|
| 246 |
+
}],
|
| 247 |
+
"session_id": session_id if 'session_id' in locals() else None
|
| 248 |
}
|
| 249 |
+
|
| 250 |
+
# Keep existing endpoints but update them to use async session management
|
| 251 |
+
|
| 252 |
@router.post("/chat/{persona_id}")
|
| 253 |
async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
|
| 254 |
+
"""Chat with a specific advisor - UPDATED"""
|
| 255 |
try:
|
| 256 |
if persona_id not in chat_orchestrator.personas:
|
| 257 |
raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found")
|
| 258 |
|
| 259 |
+
# Use async session management
|
| 260 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 261 |
|
|
|
|
| 262 |
result = await chat_orchestrator.chat_with_persona(
|
| 263 |
user_input=input.user_input,
|
| 264 |
persona_id=persona_id,
|
| 265 |
session_id=session_id
|
| 266 |
)
|
| 267 |
|
| 268 |
+
# Handle response structure
|
| 269 |
if result.get("type") == "single_persona_response" and "persona" in result:
|
|
|
|
| 270 |
persona_data = result["persona"]
|
| 271 |
return {
|
| 272 |
"persona": persona_data["persona_name"],
|
|
|
|
| 274 |
"response": persona_data["response"]
|
| 275 |
}
|
| 276 |
elif "persona_id" in result and "response" in result:
|
|
|
|
| 277 |
return {
|
| 278 |
"persona": result["persona_name"],
|
| 279 |
"persona_id": result["persona_id"],
|
| 280 |
"response": result["response"]
|
| 281 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
else:
|
|
|
|
| 283 |
return {
|
| 284 |
"persona": "System",
|
| 285 |
"response": "I'm having trouble generating a response right now. Please try again."
|
|
|
|
| 296 |
|
| 297 |
@router.post("/reply-to-advisor")
|
| 298 |
async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
|
| 299 |
+
"""Reply to a specific advisor with proper context - UPDATED"""
|
| 300 |
try:
|
| 301 |
if reply.advisor_id not in chat_orchestrator.personas:
|
| 302 |
raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found")
|
| 303 |
|
| 304 |
+
# Handle session management for existing chats
|
| 305 |
+
if reply.chat_session_id:
|
| 306 |
+
session_id = f"chat_{reply.chat_session_id}"
|
| 307 |
+
else:
|
| 308 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 309 |
|
|
|
|
| 310 |
session = session_manager.get_session(session_id)
|
| 311 |
|
| 312 |
# Find the original message being replied to for context
|
| 313 |
original_message = None
|
| 314 |
if reply.original_message_id:
|
|
|
|
| 315 |
for msg in session.messages:
|
| 316 |
if getattr(msg, 'id', None) == reply.original_message_id:
|
| 317 |
original_message = msg.content
|
| 318 |
break
|
| 319 |
|
| 320 |
+
# Create context-aware input
|
| 321 |
contextual_input = reply.user_input
|
| 322 |
if original_message:
|
| 323 |
contextual_input = f"[Replying to your previous message: '{original_message[:100]}...'] {reply.user_input}"
|
| 324 |
|
|
|
|
| 325 |
result = await chat_orchestrator.chat_with_persona(
|
| 326 |
user_input=contextual_input,
|
| 327 |
persona_id=reply.advisor_id,
|
|
|
|
| 362 |
"persona": "System",
|
| 363 |
"response": "I'm having trouble generating a reply right now. Please try again."
|
| 364 |
}
|
| 365 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
@router.post("/ask/")
|
| 367 |
async def ask_question(query: PersonaQuery, request: Request):
|
| 368 |
+
"""Ask question - UPDATED"""
|
| 369 |
try:
|
| 370 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 371 |
|
|
|
|
| 372 |
result = await chat_orchestrator.chat_with_persona(
|
| 373 |
user_input=query.question,
|
| 374 |
persona_id=query.persona,
|
multi_llm_chatbot_backend/app/api/routes/sessions.py
CHANGED
|
@@ -1,54 +1,222 @@
|
|
| 1 |
-
from fastapi import APIRouter, Request, HTTPException
|
| 2 |
from app.core.session_manager import get_session_manager
|
| 3 |
-
from app.api.utils import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import logging
|
| 5 |
|
| 6 |
logger = logging.getLogger(__name__)
|
| 7 |
|
| 8 |
router = APIRouter()
|
| 9 |
-
|
| 10 |
session_manager = get_session_manager()
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
@router.get("/context")
|
| 13 |
-
async def get_context(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
try:
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
session = session_manager.get_session(session_id)
|
| 17 |
rag_stats = session.get_rag_stats()
|
|
|
|
|
|
|
|
|
|
| 18 |
return {
|
|
|
|
|
|
|
| 19 |
"messages": session.messages,
|
| 20 |
"rag_info": {
|
| 21 |
"total_documents": rag_stats.get("total_documents", 0),
|
| 22 |
"total_chunks": rag_stats.get("total_chunks", 0),
|
| 23 |
"documents": rag_stats.get("documents", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
}
|
|
|
|
| 26 |
except Exception as e:
|
| 27 |
logger.error(f"Error getting context: {str(e)}")
|
| 28 |
-
return {
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
@router.post("/reset-session")
|
| 32 |
-
async def reset_session(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
try:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
else:
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
except Exception as e:
|
| 42 |
logger.error(f"Error resetting session: {e}")
|
| 43 |
-
return {"status": "error", "message": "Failed to reset session"}
|
| 44 |
-
|
| 45 |
|
| 46 |
@router.get("/session-stats")
|
| 47 |
-
async def get_session_stats(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
stats = session_manager.get_session_stats(session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
return stats
|
|
|
|
| 52 |
except Exception as e:
|
| 53 |
logger.error(f"Error getting session stats: {str(e)}")
|
| 54 |
return {"error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, HTTPException, Depends
|
| 2 |
from app.core.session_manager import get_session_manager
|
| 3 |
+
from app.api.utils import get_or_create_session_for_request_async
|
| 4 |
+
from app.core.auth import get_current_active_user
|
| 5 |
+
from app.models.user import User
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from typing import Optional
|
| 8 |
import logging
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
| 12 |
router = APIRouter()
|
|
|
|
| 13 |
session_manager = get_session_manager()
|
| 14 |
|
| 15 |
+
class ResetSessionRequest(BaseModel):
|
| 16 |
+
chat_session_id: Optional[str] = None
|
| 17 |
+
force_new: bool = False
|
| 18 |
+
|
| 19 |
@router.get("/context")
|
| 20 |
+
async def get_context(
|
| 21 |
+
request: Request,
|
| 22 |
+
chat_session_id: Optional[str] = None,
|
| 23 |
+
current_user: User = Depends(get_current_active_user)
|
| 24 |
+
):
|
| 25 |
+
"""
|
| 26 |
+
Get context for current session - ENHANCED
|
| 27 |
+
Now properly handles different chat sessions
|
| 28 |
+
"""
|
| 29 |
try:
|
| 30 |
+
# Determine which session to get context for
|
| 31 |
+
if chat_session_id:
|
| 32 |
+
# Getting context for a specific chat session
|
| 33 |
+
session_id = await get_or_create_session_for_request_async(
|
| 34 |
+
request,
|
| 35 |
+
chat_session_id=chat_session_id,
|
| 36 |
+
user_id=str(current_user.id)
|
| 37 |
+
)
|
| 38 |
+
else:
|
| 39 |
+
# Getting context for current session
|
| 40 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 41 |
+
|
| 42 |
session = session_manager.get_session(session_id)
|
| 43 |
rag_stats = session.get_rag_stats()
|
| 44 |
+
|
| 45 |
+
logger.info(f"Retrieved context for session {session_id}: {len(session.messages)} messages")
|
| 46 |
+
|
| 47 |
return {
|
| 48 |
+
"session_id": session_id,
|
| 49 |
+
"chat_session_id": chat_session_id,
|
| 50 |
"messages": session.messages,
|
| 51 |
"rag_info": {
|
| 52 |
"total_documents": rag_stats.get("total_documents", 0),
|
| 53 |
"total_chunks": rag_stats.get("total_chunks", 0),
|
| 54 |
"documents": rag_stats.get("documents", [])
|
| 55 |
+
},
|
| 56 |
+
"context_stats": {
|
| 57 |
+
"message_count": len(session.messages),
|
| 58 |
+
"user_messages": len([m for m in session.messages if m.get('role') == 'user']),
|
| 59 |
+
"uploaded_files": session.uploaded_files,
|
| 60 |
+
"total_upload_size": session.total_upload_size,
|
| 61 |
+
"created_at": session.created_at.isoformat(),
|
| 62 |
+
"last_accessed": session.last_accessed.isoformat()
|
| 63 |
}
|
| 64 |
}
|
| 65 |
+
|
| 66 |
except Exception as e:
|
| 67 |
logger.error(f"Error getting context: {str(e)}")
|
| 68 |
+
return {
|
| 69 |
+
"session_id": None,
|
| 70 |
+
"messages": [],
|
| 71 |
+
"rag_info": {"total_documents": 0, "total_chunks": 0},
|
| 72 |
+
"error": str(e)
|
| 73 |
+
}
|
| 74 |
|
| 75 |
@router.post("/reset-session")
|
| 76 |
+
async def reset_session(
|
| 77 |
+
reset_request: ResetSessionRequest,
|
| 78 |
+
request: Request,
|
| 79 |
+
current_user: User = Depends(get_current_active_user)
|
| 80 |
+
):
|
| 81 |
+
"""
|
| 82 |
+
Reset session - ENHANCED
|
| 83 |
+
Now properly handles different reset scenarios
|
| 84 |
+
"""
|
| 85 |
try:
|
| 86 |
+
if reset_request.force_new:
|
| 87 |
+
# Force create a completely new session
|
| 88 |
+
session_id = session_manager.create_session()
|
| 89 |
+
session = session_manager.get_session(session_id)
|
| 90 |
+
session.clear_all_data()
|
| 91 |
+
|
| 92 |
+
logger.info(f"Force created new session: {session_id}")
|
| 93 |
+
|
| 94 |
+
return {
|
| 95 |
+
"status": "reset",
|
| 96 |
+
"message": "New session created with fresh context",
|
| 97 |
+
"session_id": session_id,
|
| 98 |
+
"chat_session_id": None
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
elif reset_request.chat_session_id:
|
| 102 |
+
# Reset a specific chat session context
|
| 103 |
+
session_id = f"chat_{reset_request.chat_session_id}"
|
| 104 |
+
|
| 105 |
+
if session_id in session_manager.sessions:
|
| 106 |
+
success = session_manager.reset_session_completely(session_id)
|
| 107 |
+
message = "Chat session context reset successfully" if success else "Failed to reset chat session context"
|
| 108 |
+
else:
|
| 109 |
+
# Create fresh context for this chat
|
| 110 |
+
session = session_manager.get_session(session_id)
|
| 111 |
+
session.clear_all_data()
|
| 112 |
+
success = True
|
| 113 |
+
message = "Fresh context created for chat session"
|
| 114 |
+
|
| 115 |
+
logger.info(f"Reset chat session {reset_request.chat_session_id}, memory session: {session_id}")
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
"status": "reset" if success else "error",
|
| 119 |
+
"message": message,
|
| 120 |
+
"session_id": session_id,
|
| 121 |
+
"chat_session_id": reset_request.chat_session_id
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
else:
|
| 125 |
+
# Reset current session
|
| 126 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 127 |
+
success = session_manager.reset_session_completely(session_id)
|
| 128 |
+
|
| 129 |
+
logger.info(f"Reset current session: {session_id}")
|
| 130 |
+
|
| 131 |
+
return {
|
| 132 |
+
"status": "reset" if success else "error",
|
| 133 |
+
"message": "Current session reset successfully" if success else "Failed to reset current session",
|
| 134 |
+
"session_id": session_id
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
except Exception as e:
|
| 138 |
logger.error(f"Error resetting session: {e}")
|
| 139 |
+
return {"status": "error", "message": f"Failed to reset session: {str(e)}"}
|
|
|
|
| 140 |
|
| 141 |
@router.get("/session-stats")
|
| 142 |
+
async def get_session_stats(
|
| 143 |
+
request: Request,
|
| 144 |
+
chat_session_id: Optional[str] = None,
|
| 145 |
+
current_user: User = Depends(get_current_active_user)
|
| 146 |
+
):
|
| 147 |
+
"""
|
| 148 |
+
Get session statistics - ENHANCED
|
| 149 |
+
Now provides detailed stats for different session types
|
| 150 |
+
"""
|
| 151 |
try:
|
| 152 |
+
if chat_session_id:
|
| 153 |
+
# Stats for specific chat session
|
| 154 |
+
session_id = f"chat_{chat_session_id}"
|
| 155 |
+
else:
|
| 156 |
+
# Stats for current session
|
| 157 |
+
session_id = await get_or_create_session_for_request_async(request)
|
| 158 |
+
|
| 159 |
stats = session_manager.get_session_stats(session_id)
|
| 160 |
+
|
| 161 |
+
# Add additional context
|
| 162 |
+
stats["session_type"] = "chat_session" if chat_session_id else "current_session"
|
| 163 |
+
stats["chat_session_id"] = chat_session_id
|
| 164 |
+
|
| 165 |
return stats
|
| 166 |
+
|
| 167 |
except Exception as e:
|
| 168 |
logger.error(f"Error getting session stats: {str(e)}")
|
| 169 |
return {"error": str(e)}
|
| 170 |
+
|
| 171 |
+
@router.get("/active-sessions")
|
| 172 |
+
async def get_active_sessions(current_user: User = Depends(get_current_active_user)):
|
| 173 |
+
"""
|
| 174 |
+
Get all active sessions for debugging
|
| 175 |
+
"""
|
| 176 |
+
try:
|
| 177 |
+
active_count = session_manager.get_active_session_count()
|
| 178 |
+
|
| 179 |
+
# Get overview of sessions (don't return full content for privacy)
|
| 180 |
+
session_overview = {}
|
| 181 |
+
for session_id, session in session_manager.sessions.items():
|
| 182 |
+
session_overview[session_id] = {
|
| 183 |
+
"message_count": len(session.messages),
|
| 184 |
+
"uploaded_files": len(session.uploaded_files),
|
| 185 |
+
"created_at": session.created_at.isoformat(),
|
| 186 |
+
"last_accessed": session.last_accessed.isoformat(),
|
| 187 |
+
"is_chat_session": session_id.startswith("chat_")
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
return {
|
| 191 |
+
"active_session_count": active_count,
|
| 192 |
+
"sessions": session_overview
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
logger.error(f"Error getting active sessions: {str(e)}")
|
| 197 |
+
return {"error": str(e)}
|
| 198 |
+
|
| 199 |
+
@router.post("/cleanup-sessions")
|
| 200 |
+
async def cleanup_expired_sessions(current_user: User = Depends(get_current_active_user)):
|
| 201 |
+
"""
|
| 202 |
+
Manually trigger session cleanup
|
| 203 |
+
"""
|
| 204 |
+
try:
|
| 205 |
+
initial_count = session_manager.get_active_session_count()
|
| 206 |
+
|
| 207 |
+
# Force cleanup
|
| 208 |
+
session_manager._cleanup_expired_sessions()
|
| 209 |
+
|
| 210 |
+
final_count = session_manager.get_active_session_count()
|
| 211 |
+
cleaned_count = initial_count - final_count
|
| 212 |
+
|
| 213 |
+
return {
|
| 214 |
+
"status": "success",
|
| 215 |
+
"message": f"Cleaned up {cleaned_count} expired sessions",
|
| 216 |
+
"sessions_before": initial_count,
|
| 217 |
+
"sessions_after": final_count
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger.error(f"Error during session cleanup: {str(e)}")
|
| 222 |
+
return {"status": "error", "message": str(e)}
|
multi_llm_chatbot_backend/app/api/utils.py
CHANGED
|
@@ -1,24 +1,156 @@
|
|
| 1 |
from typing import Optional
|
| 2 |
from fastapi import Request
|
| 3 |
from app.core.session_manager import get_session_manager
|
|
|
|
|
|
|
|
|
|
| 4 |
|
|
|
|
| 5 |
session_manager = get_session_manager()
|
| 6 |
|
| 7 |
-
def
|
| 8 |
"""
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
2. X-Session-ID header
|
| 12 |
-
3. Client IP fallback
|
| 13 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
if session_id_override:
|
| 15 |
return session_id_override
|
| 16 |
|
|
|
|
| 17 |
session_header = request.headers.get("X-Session-ID")
|
| 18 |
if session_header:
|
| 19 |
return session_header
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
return
|
|
|
|
| 1 |
from typing import Optional
|
| 2 |
from fastapi import Request
|
| 3 |
from app.core.session_manager import get_session_manager
|
| 4 |
+
from app.core.database import get_database
|
| 5 |
+
from bson import ObjectId
|
| 6 |
+
import logging
|
| 7 |
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
session_manager = get_session_manager()
|
| 10 |
|
| 11 |
+
async def load_chat_session_into_context(chat_session_id: str, user_id: str) -> Optional[str]:
|
| 12 |
"""
|
| 13 |
+
Load a MongoDB chat session into in-memory context
|
| 14 |
+
Returns the in-memory session_id to use
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
+
try:
|
| 17 |
+
db = get_database()
|
| 18 |
+
|
| 19 |
+
# Get the chat session from MongoDB
|
| 20 |
+
chat_session = await db.chat_sessions.find_one({
|
| 21 |
+
"_id": ObjectId(chat_session_id),
|
| 22 |
+
"user_id": user_id,
|
| 23 |
+
"is_active": True
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
if not chat_session:
|
| 27 |
+
logger.warning(f"Chat session {chat_session_id} not found for user {user_id}")
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
# Create or get in-memory session with a unique ID based on chat session
|
| 31 |
+
memory_session_id = f"chat_{chat_session_id}"
|
| 32 |
+
memory_session = session_manager.get_session(memory_session_id)
|
| 33 |
+
|
| 34 |
+
# Clear existing messages and load from MongoDB
|
| 35 |
+
memory_session.clear_messages()
|
| 36 |
+
|
| 37 |
+
# Load all messages from the chat session with proper format conversion
|
| 38 |
+
messages = chat_session.get("messages", [])
|
| 39 |
+
for message in messages:
|
| 40 |
+
# Convert MongoDB message format to session manager format
|
| 41 |
+
# MongoDB format: {type: 'user'/'advisor', content: '', advisorId: '', ...}
|
| 42 |
+
# Session format: {role: '', content: '', timestamp: ''}
|
| 43 |
+
|
| 44 |
+
msg_type = message.get("type", "user")
|
| 45 |
+
content = message.get("content", "")
|
| 46 |
+
timestamp = message.get("timestamp", "")
|
| 47 |
+
|
| 48 |
+
# Map message types to roles for session manager
|
| 49 |
+
if msg_type == "user":
|
| 50 |
+
role = "user"
|
| 51 |
+
elif msg_type == "advisor":
|
| 52 |
+
role = "assistant"
|
| 53 |
+
# For advisor messages, include advisor info in content if needed
|
| 54 |
+
advisor_name = message.get("advisorName", "")
|
| 55 |
+
if advisor_name:
|
| 56 |
+
# Store advisor info as metadata that can be retrieved
|
| 57 |
+
role = f"advisor_{message.get('advisorId', 'unknown')}"
|
| 58 |
+
elif msg_type == "system":
|
| 59 |
+
role = "system"
|
| 60 |
+
elif msg_type == "error":
|
| 61 |
+
role = "system"
|
| 62 |
+
elif msg_type == "document_upload":
|
| 63 |
+
role = "system"
|
| 64 |
+
elif msg_type == "clarification":
|
| 65 |
+
role = "system"
|
| 66 |
+
else:
|
| 67 |
+
role = "user" # Default fallback
|
| 68 |
+
|
| 69 |
+
# Add the message to session with original structure preserved
|
| 70 |
+
memory_session.append_message(role, content)
|
| 71 |
+
|
| 72 |
+
# Store original message metadata for frontend reconstruction
|
| 73 |
+
if not hasattr(memory_session, 'original_messages'):
|
| 74 |
+
memory_session.original_messages = []
|
| 75 |
+
|
| 76 |
+
memory_session.original_messages.append(message)
|
| 77 |
+
|
| 78 |
+
logger.info(f"Loaded {len(messages)} messages into session {memory_session_id}")
|
| 79 |
+
return memory_session_id
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"Error loading chat session into context: {e}")
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def get_or_create_session_for_request(
|
| 86 |
+
request: Request,
|
| 87 |
+
session_id_override: Optional[str] = None,
|
| 88 |
+
chat_session_id: Optional[str] = None,
|
| 89 |
+
user_id: Optional[str] = None
|
| 90 |
+
) -> str:
|
| 91 |
+
"""
|
| 92 |
+
Enhanced session management that properly handles chat switching
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
request: FastAPI request object
|
| 96 |
+
session_id_override: Explicit session ID to use
|
| 97 |
+
chat_session_id: MongoDB chat session ID (for existing chats)
|
| 98 |
+
user_id: User ID (for loading existing chats)
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
# Case 1: Loading an existing chat session
|
| 102 |
+
if chat_session_id and user_id:
|
| 103 |
+
try:
|
| 104 |
+
# We need to handle this in the calling function since this isn't async
|
| 105 |
+
# Return a special identifier that the calling function can handle
|
| 106 |
+
return f"load_chat_{chat_session_id}"
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"Error handling chat session load: {e}")
|
| 110 |
+
# Fall through to create new session
|
| 111 |
+
|
| 112 |
+
# Case 2: Explicit session ID provided
|
| 113 |
+
if session_id_override:
|
| 114 |
+
return session_id_override
|
| 115 |
+
|
| 116 |
+
# Case 3: Check for session header
|
| 117 |
+
session_header = request.headers.get("X-Session-ID")
|
| 118 |
+
if session_header:
|
| 119 |
+
return session_header
|
| 120 |
+
|
| 121 |
+
# Case 4: Create a truly new session (don't fall back to IP)
|
| 122 |
+
# This ensures each new chat gets its own context
|
| 123 |
+
new_session_id = session_manager.create_session()
|
| 124 |
+
logger.info(f"Created new session: {new_session_id}")
|
| 125 |
+
return new_session_id
|
| 126 |
+
|
| 127 |
+
async def get_or_create_session_for_request_async(
|
| 128 |
+
request: Request,
|
| 129 |
+
session_id_override: Optional[str] = None,
|
| 130 |
+
chat_session_id: Optional[str] = None,
|
| 131 |
+
user_id: Optional[str] = None
|
| 132 |
+
) -> str:
|
| 133 |
+
"""
|
| 134 |
+
Async version that properly handles chat session loading
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
# Case 1: Loading an existing chat session
|
| 138 |
+
if chat_session_id and user_id:
|
| 139 |
+
memory_session_id = await load_chat_session_into_context(chat_session_id, user_id)
|
| 140 |
+
if memory_session_id:
|
| 141 |
+
return memory_session_id
|
| 142 |
+
# If loading failed, create new session
|
| 143 |
+
|
| 144 |
+
# Case 2: Explicit session ID provided
|
| 145 |
if session_id_override:
|
| 146 |
return session_id_override
|
| 147 |
|
| 148 |
+
# Case 3: Check for session header
|
| 149 |
session_header = request.headers.get("X-Session-ID")
|
| 150 |
if session_header:
|
| 151 |
return session_header
|
| 152 |
|
| 153 |
+
# Case 4: Create a truly new session
|
| 154 |
+
new_session_id = session_manager.create_session()
|
| 155 |
+
logger.info(f"Created new session: {new_session_id}")
|
| 156 |
+
return new_session_id
|
phd-advisor-frontend/src/components/Sidebar.js
CHANGED
|
@@ -28,6 +28,7 @@ const Sidebar = ({
|
|
| 28 |
const [isLoading, setIsLoading] = useState(true);
|
| 29 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 30 |
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
|
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
if (authToken) {
|
|
@@ -42,6 +43,17 @@ const Sidebar = ({
|
|
| 42 |
}
|
| 43 |
}, [isCollapsed, onSidebarToggle]);
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
const fetchChatSessions = async () => {
|
| 46 |
try {
|
| 47 |
const response = await fetch('http://localhost:8000/api/chat-sessions', {
|
|
@@ -65,25 +77,21 @@ const Sidebar = ({
|
|
| 65 |
};
|
| 66 |
|
| 67 |
const handleNewChat = async () => {
|
|
|
|
|
|
|
| 68 |
try {
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
title: `Chat ${new Date().toLocaleDateString()}`
|
| 77 |
-
})
|
| 78 |
-
});
|
| 79 |
-
|
| 80 |
-
if (response.ok) {
|
| 81 |
-
const newSession = await response.json();
|
| 82 |
-
setChatSessions(prev => [newSession, ...prev]);
|
| 83 |
-
onNewChat(newSession.id);
|
| 84 |
-
}
|
| 85 |
} catch (error) {
|
| 86 |
console.error('Error creating new chat:', error);
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
};
|
| 89 |
|
|
@@ -187,9 +195,13 @@ const Sidebar = ({
|
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
<Plus size={16} />
|
| 192 |
-
<span>New Chat</span>
|
| 193 |
</button>
|
| 194 |
</>
|
| 195 |
)}
|
|
@@ -204,7 +216,12 @@ const Sidebar = ({
|
|
| 204 |
>
|
| 205 |
<ChevronRight size={20} />
|
| 206 |
</button>
|
| 207 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
<Plus size={20} />
|
| 209 |
</button>
|
| 210 |
</div>
|
|
@@ -234,6 +251,11 @@ const Sidebar = ({
|
|
| 234 |
<div className="loading-spinner"></div>
|
| 235 |
{!isCollapsed && <span>Loading chats...</span>}
|
| 236 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
) : filteredSessions.length === 0 ? (
|
| 238 |
<div className="no-sessions">
|
| 239 |
{!isCollapsed && (searchTerm ? 'No chats found' : 'No chats yet')}
|
|
|
|
| 28 |
const [isLoading, setIsLoading] = useState(true);
|
| 29 |
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 30 |
const [isCollapsed, setIsCollapsed] = useState(false);
|
| 31 |
+
const [isCreatingNewChat, setIsCreatingNewChat] = useState(false); // Add loading state for new chat creation
|
| 32 |
|
| 33 |
useEffect(() => {
|
| 34 |
if (authToken) {
|
|
|
|
| 43 |
}
|
| 44 |
}, [isCollapsed, onSidebarToggle]);
|
| 45 |
|
| 46 |
+
// Add effect to refresh when currentSessionId changes (new session created)
|
| 47 |
+
useEffect(() => {
|
| 48 |
+
if (currentSessionId && authToken) {
|
| 49 |
+
// Small delay to ensure the session is saved to database
|
| 50 |
+
const timer = setTimeout(() => {
|
| 51 |
+
fetchChatSessions();
|
| 52 |
+
}, 200);
|
| 53 |
+
return () => clearTimeout(timer);
|
| 54 |
+
}
|
| 55 |
+
}, [currentSessionId, authToken]);
|
| 56 |
+
|
| 57 |
const fetchChatSessions = async () => {
|
| 58 |
try {
|
| 59 |
const response = await fetch('http://localhost:8000/api/chat-sessions', {
|
|
|
|
| 77 |
};
|
| 78 |
|
| 79 |
const handleNewChat = async () => {
|
| 80 |
+
setIsCreatingNewChat(true);
|
| 81 |
+
|
| 82 |
try {
|
| 83 |
+
// Call the parent's new chat handler and wait for it to complete
|
| 84 |
+
await onNewChat();
|
| 85 |
+
|
| 86 |
+
// Refresh the sessions list immediately after new chat is created
|
| 87 |
+
// The parent should have updated currentSessionId by now
|
| 88 |
+
await fetchChatSessions();
|
| 89 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
} catch (error) {
|
| 91 |
console.error('Error creating new chat:', error);
|
| 92 |
+
// Optionally show an error message to the user
|
| 93 |
+
} finally {
|
| 94 |
+
setIsCreatingNewChat(false);
|
| 95 |
}
|
| 96 |
};
|
| 97 |
|
|
|
|
| 195 |
</div>
|
| 196 |
</div>
|
| 197 |
|
| 198 |
+
<button
|
| 199 |
+
className="new-chat-button"
|
| 200 |
+
onClick={handleNewChat}
|
| 201 |
+
disabled={isCreatingNewChat}
|
| 202 |
+
>
|
| 203 |
<Plus size={16} />
|
| 204 |
+
<span>{isCreatingNewChat ? 'Creating...' : 'New Chat'}</span>
|
| 205 |
</button>
|
| 206 |
</>
|
| 207 |
)}
|
|
|
|
| 216 |
>
|
| 217 |
<ChevronRight size={20} />
|
| 218 |
</button>
|
| 219 |
+
<button
|
| 220 |
+
className="collapsed-new-chat"
|
| 221 |
+
onClick={handleNewChat}
|
| 222 |
+
title="New Chat"
|
| 223 |
+
disabled={isCreatingNewChat}
|
| 224 |
+
>
|
| 225 |
<Plus size={20} />
|
| 226 |
</button>
|
| 227 |
</div>
|
|
|
|
| 251 |
<div className="loading-spinner"></div>
|
| 252 |
{!isCollapsed && <span>Loading chats...</span>}
|
| 253 |
</div>
|
| 254 |
+
) : isCreatingNewChat ? (
|
| 255 |
+
<div className="loading-sessions">
|
| 256 |
+
<div className="loading-spinner"></div>
|
| 257 |
+
{!isCollapsed && <span>Creating new chat...</span>}
|
| 258 |
+
</div>
|
| 259 |
) : filteredSessions.length === 0 ? (
|
| 260 |
<div className="no-sessions">
|
| 261 |
{!isCollapsed && (searchTerm ? 'No chats found' : 'No chats yet')}
|
phd-advisor-frontend/src/pages/ChatPage.js
CHANGED
|
@@ -116,64 +116,85 @@ const ChatPage = ({ user, authToken, onNavigateToHome, onSignOut }) => {
|
|
| 116 |
};
|
| 117 |
|
| 118 |
const createNewSession = async (firstMessage = null) => {
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
return null;
|
| 141 |
}
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
return null;
|
| 145 |
-
}
|
| 146 |
-
};
|
| 147 |
|
| 148 |
// Load an existing chat session
|
| 149 |
const loadChatSession = async (sessionId) => {
|
| 150 |
if (!sessionId || isLoadingSession) return;
|
| 151 |
-
|
| 152 |
setIsLoadingSession(true);
|
| 153 |
try {
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
headers: {
|
| 156 |
'Authorization': `Bearer ${authToken}`,
|
| 157 |
'Content-Type': 'application/json'
|
| 158 |
-
}
|
|
|
|
|
|
|
|
|
|
| 159 |
});
|
| 160 |
|
| 161 |
if (response.ok) {
|
| 162 |
-
const
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
...msg
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
} catch (error) {
|
| 179 |
console.error('Error loading session:', error);
|
|
@@ -234,16 +255,67 @@ const handleSelectSession = async (sessionId) => {
|
|
| 234 |
// Handle creating new chat from sidebar
|
| 235 |
const handleNewChat = async (sessionId = null) => {
|
| 236 |
if (sessionId) {
|
| 237 |
-
//
|
| 238 |
await loadChatSession(sessionId);
|
|
|
|
| 239 |
} else {
|
| 240 |
-
//
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
}
|
| 248 |
};
|
| 249 |
|
|
@@ -313,7 +385,8 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 313 |
},
|
| 314 |
body: JSON.stringify({
|
| 315 |
user_input: inputMessage,
|
| 316 |
-
response_length: 'medium'
|
|
|
|
| 317 |
}),
|
| 318 |
});
|
| 319 |
|
|
@@ -383,78 +456,89 @@ const handleNewChat = async (sessionId = null) => {
|
|
| 383 |
};
|
| 384 |
|
| 385 |
const handleReplyToAdvisor = async (inputMessage, replyContext) => {
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
timestamp: new Date()
|
| 396 |
-
};
|
| 397 |
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
user_input: inputMessage,
|
| 414 |
-
advisor_id: replyContext.advisorId,
|
| 415 |
-
original_message_id: replyContext.messageId
|
| 416 |
-
}),
|
| 417 |
-
});
|
| 418 |
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
-
|
|
|
|
|
|
|
| 424 |
|
| 425 |
-
|
| 426 |
-
const replyResponseMessage = {
|
| 427 |
-
id: generateMessageId(),
|
| 428 |
-
type: 'advisor',
|
| 429 |
-
advisorId: data.persona_id,
|
| 430 |
-
advisorName: data.persona,
|
| 431 |
-
content: data.response,
|
| 432 |
-
isReply: true,
|
| 433 |
-
timestamp: new Date()
|
| 434 |
-
};
|
| 435 |
-
setMessages(prev => [...prev, replyResponseMessage]);
|
| 436 |
-
|
| 437 |
-
// Save advisor reply to database
|
| 438 |
-
await saveMessageToSession(replyResponseMessage);
|
| 439 |
-
}
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
const errorMessage = {
|
| 444 |
id: generateMessageId(),
|
| 445 |
-
type: '
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
| 447 |
timestamp: new Date()
|
| 448 |
};
|
| 449 |
-
setMessages(prev => [...prev,
|
| 450 |
|
| 451 |
-
// Save
|
| 452 |
-
await saveMessageToSession(
|
| 453 |
}
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
const handleCopyMessage = (messageId, content) => {
|
| 460 |
// Optional: Show a toast notification or add to message history
|
|
|
|
| 116 |
};
|
| 117 |
|
| 118 |
const createNewSession = async (firstMessage = null) => {
|
| 119 |
+
try {
|
| 120 |
+
const title = firstMessage
|
| 121 |
+
? `${firstMessage.substring(0, 30)}...`
|
| 122 |
+
: `Chat ${new Date().toLocaleDateString()}`;
|
| 123 |
|
| 124 |
+
const response = await fetch('http://localhost:8000/api/chat-sessions', {
|
| 125 |
+
method: 'POST',
|
| 126 |
+
headers: {
|
| 127 |
+
'Authorization': `Bearer ${authToken}`,
|
| 128 |
+
'Content-Type': 'application/json'
|
| 129 |
+
},
|
| 130 |
+
body: JSON.stringify({ title })
|
| 131 |
+
});
|
| 132 |
|
| 133 |
+
if (response.ok) {
|
| 134 |
+
const newSession = await response.json();
|
| 135 |
+
|
| 136 |
+
// Update state immediately
|
| 137 |
+
setCurrentSessionId(newSession.id);
|
| 138 |
+
setCurrentSessionTitle(newSession.title);
|
| 139 |
+
|
| 140 |
+
console.log('MongoDB session created:', newSession.id);
|
| 141 |
+
return newSession.id;
|
| 142 |
+
} else {
|
| 143 |
+
console.error('Failed to create new session');
|
| 144 |
+
return null;
|
| 145 |
+
}
|
| 146 |
+
} catch (error) {
|
| 147 |
+
console.error('Error creating new session:', error);
|
| 148 |
return null;
|
| 149 |
}
|
| 150 |
+
};
|
| 151 |
+
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
// Load an existing chat session
|
| 154 |
const loadChatSession = async (sessionId) => {
|
| 155 |
if (!sessionId || isLoadingSession) return;
|
|
|
|
| 156 |
setIsLoadingSession(true);
|
| 157 |
try {
|
| 158 |
+
// Use the new switch-chat endpoint that syncs context
|
| 159 |
+
const response = await fetch('http://localhost:8000/switch-chat', {
|
| 160 |
+
method: 'POST',
|
| 161 |
headers: {
|
| 162 |
'Authorization': `Bearer ${authToken}`,
|
| 163 |
'Content-Type': 'application/json'
|
| 164 |
+
},
|
| 165 |
+
body: JSON.stringify({
|
| 166 |
+
chat_session_id: sessionId
|
| 167 |
+
})
|
| 168 |
});
|
| 169 |
|
| 170 |
if (response.ok) {
|
| 171 |
+
const result = await response.json();
|
| 172 |
+
if (result.status === 'success') {
|
| 173 |
+
setCurrentSessionId(sessionId);
|
| 174 |
+
setCurrentSessionTitle(''); // Will be set from MongoDB data
|
| 175 |
+
|
| 176 |
+
// Load the messages from the synced context
|
| 177 |
+
const formattedMessages = result.context.messages.map(msg => ({
|
| 178 |
+
...msg,
|
| 179 |
+
timestamp: new Date(msg.timestamp)
|
| 180 |
+
}));
|
| 181 |
+
|
| 182 |
+
setMessages(formattedMessages);
|
| 183 |
+
setReplyingTo(null);
|
| 184 |
+
setThinkingAdvisors([]);
|
| 185 |
+
|
| 186 |
+
// Also get the session title from MongoDB
|
| 187 |
+
const sessionResponse = await fetch(`http://localhost:8000/api/chat-sessions/${sessionId}`, {
|
| 188 |
+
headers: {
|
| 189 |
+
'Authorization': `Bearer ${authToken}`,
|
| 190 |
+
'Content-Type': 'application/json'
|
| 191 |
+
}
|
| 192 |
+
});
|
| 193 |
+
if (sessionResponse.ok) {
|
| 194 |
+
const sessionData = await sessionResponse.json();
|
| 195 |
+
setCurrentSessionTitle(sessionData.title);
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
}
|
| 199 |
} catch (error) {
|
| 200 |
console.error('Error loading session:', error);
|
|
|
|
| 255 |
// Handle creating new chat from sidebar
|
| 256 |
const handleNewChat = async (sessionId = null) => {
|
| 257 |
if (sessionId) {
|
| 258 |
+
// Loading existing session
|
| 259 |
await loadChatSession(sessionId);
|
| 260 |
+
return; // Return early for existing session loading
|
| 261 |
} else {
|
| 262 |
+
// Creating completely new chat with fresh context
|
| 263 |
+
try {
|
| 264 |
+
// Step 1: Reset memory session
|
| 265 |
+
const response = await fetch('http://localhost:8000/new-chat', {
|
| 266 |
+
method: 'POST',
|
| 267 |
+
headers: {
|
| 268 |
+
'Authorization': `Bearer ${authToken}`,
|
| 269 |
+
'Content-Type': 'application/json'
|
| 270 |
+
},
|
| 271 |
+
body: JSON.stringify({
|
| 272 |
+
title: `Chat ${new Date().toLocaleDateString()}`
|
| 273 |
+
})
|
| 274 |
+
});
|
| 275 |
+
|
| 276 |
+
if (response.ok) {
|
| 277 |
+
const result = await response.json();
|
| 278 |
+
if (result.status === 'success') {
|
| 279 |
+
// Step 2: Immediately create MongoDB session
|
| 280 |
+
const newSessionId = await createNewSession(`Chat ${new Date().toLocaleDateString()}`);
|
| 281 |
+
|
| 282 |
+
if (newSessionId) {
|
| 283 |
+
// Reset all state to fresh with the new session
|
| 284 |
+
setMessages([]);
|
| 285 |
+
setCurrentSessionId(newSessionId); // Set the new session ID immediately
|
| 286 |
+
setCurrentSessionTitle(`Chat ${new Date().toLocaleDateString()}`);
|
| 287 |
+
setReplyingTo(null);
|
| 288 |
+
setThinkingAdvisors([]);
|
| 289 |
+
setUploadedDocuments([]);
|
| 290 |
+
|
| 291 |
+
console.log('New chat created with MongoDB session:', newSessionId);
|
| 292 |
+
|
| 293 |
+
// Wait a bit to ensure state has updated
|
| 294 |
+
await new Promise(resolve => setTimeout(resolve, 100));
|
| 295 |
+
return newSessionId; // Return the session ID for the sidebar
|
| 296 |
+
} else {
|
| 297 |
+
throw new Error('Failed to create MongoDB session');
|
| 298 |
+
}
|
| 299 |
+
} else {
|
| 300 |
+
throw new Error('Failed to create memory session');
|
| 301 |
+
}
|
| 302 |
+
} else {
|
| 303 |
+
throw new Error(`HTTP error: ${response.status}`);
|
| 304 |
+
}
|
| 305 |
+
} catch (error) {
|
| 306 |
+
console.error('Error creating new chat:', error);
|
| 307 |
+
|
| 308 |
+
// Fallback to local reset
|
| 309 |
+
setMessages([]);
|
| 310 |
+
setCurrentSessionId(null);
|
| 311 |
+
setCurrentSessionTitle('');
|
| 312 |
+
setReplyingTo(null);
|
| 313 |
+
setThinkingAdvisors([]);
|
| 314 |
+
setUploadedDocuments([]);
|
| 315 |
+
|
| 316 |
+
// Re-throw the error so the sidebar knows something went wrong
|
| 317 |
+
throw error;
|
| 318 |
+
}
|
| 319 |
}
|
| 320 |
};
|
| 321 |
|
|
|
|
| 385 |
},
|
| 386 |
body: JSON.stringify({
|
| 387 |
user_input: inputMessage,
|
| 388 |
+
response_length: 'medium',
|
| 389 |
+
chat_session_id: currentSessionId // Include current session ID
|
| 390 |
}),
|
| 391 |
});
|
| 392 |
|
|
|
|
| 456 |
};
|
| 457 |
|
| 458 |
const handleReplyToAdvisor = async (inputMessage, replyContext) => {
|
| 459 |
+
// Ensure we have a session before proceeding
|
| 460 |
+
let sessionId = currentSessionId;
|
| 461 |
+
if (!sessionId) {
|
| 462 |
+
sessionId = await createNewSession(inputMessage);
|
| 463 |
+
if (!sessionId) {
|
| 464 |
+
console.error('Failed to create session for reply');
|
| 465 |
+
return;
|
| 466 |
+
}
|
| 467 |
+
}
|
|
|
|
|
|
|
| 468 |
|
| 469 |
+
const replyMessage = {
|
| 470 |
+
id: generateMessageId(),
|
| 471 |
+
type: 'user',
|
| 472 |
+
content: inputMessage,
|
| 473 |
+
replyTo: {
|
| 474 |
+
advisorId: replyContext.advisorId,
|
| 475 |
+
advisorName: replyContext.advisorName,
|
| 476 |
+
messageId: replyContext.messageId
|
| 477 |
+
},
|
| 478 |
+
timestamp: new Date()
|
| 479 |
+
};
|
| 480 |
|
| 481 |
+
setMessages(prev => [...prev, replyMessage]);
|
| 482 |
+
|
| 483 |
+
// Save reply message to database with explicit session ID
|
| 484 |
+
await saveMessageToSession(replyMessage, sessionId);
|
| 485 |
+
|
| 486 |
+
setIsLoading(true);
|
| 487 |
+
setThinkingAdvisors([replyContext.advisorId]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
|
| 489 |
+
try {
|
| 490 |
+
const response = await fetch('http://localhost:8000/reply-to-advisor', {
|
| 491 |
+
method: 'POST',
|
| 492 |
+
headers: {
|
| 493 |
+
'Content-Type': 'application/json',
|
| 494 |
+
},
|
| 495 |
+
body: JSON.stringify({
|
| 496 |
+
user_input: inputMessage,
|
| 497 |
+
advisor_id: replyContext.advisorId,
|
| 498 |
+
original_message_id: replyContext.messageId,
|
| 499 |
+
chat_session_id: sessionId // Use confirmed session ID
|
| 500 |
+
}),
|
| 501 |
+
});
|
| 502 |
|
| 503 |
+
if (!response.ok) {
|
| 504 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 505 |
+
}
|
| 506 |
|
| 507 |
+
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
+
if (data.type === 'advisor_reply') {
|
| 510 |
+
const replyResponseMessage = {
|
|
|
|
| 511 |
id: generateMessageId(),
|
| 512 |
+
type: 'advisor',
|
| 513 |
+
advisorId: data.persona_id,
|
| 514 |
+
advisorName: data.persona,
|
| 515 |
+
content: data.response,
|
| 516 |
+
isReply: true,
|
| 517 |
timestamp: new Date()
|
| 518 |
};
|
| 519 |
+
setMessages(prev => [...prev, replyResponseMessage]);
|
| 520 |
|
| 521 |
+
// Save advisor reply to database
|
| 522 |
+
await saveMessageToSession(replyResponseMessage, sessionId);
|
| 523 |
}
|
| 524 |
|
| 525 |
+
} catch (error) {
|
| 526 |
+
console.error('Error replying to advisor:', error);
|
| 527 |
+
const errorMessage = {
|
| 528 |
+
id: generateMessageId(),
|
| 529 |
+
type: 'error',
|
| 530 |
+
content: 'Sorry, I encountered an error with your reply. Please try again.',
|
| 531 |
+
timestamp: new Date()
|
| 532 |
+
};
|
| 533 |
+
setMessages(prev => [...prev, errorMessage]);
|
| 534 |
+
|
| 535 |
+
// Save error message to database
|
| 536 |
+
await saveMessageToSession(errorMessage, sessionId);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
setIsLoading(false);
|
| 540 |
+
setThinkingAdvisors([]);
|
| 541 |
+
};
|
| 542 |
|
| 543 |
const handleCopyMessage = (messageId, content) => {
|
| 544 |
// Optional: Show a toast notification or add to message history
|
phd-advisor-frontend/src/styles/Sidebar.css
CHANGED
|
@@ -513,6 +513,56 @@
|
|
| 513 |
margin-left: 8px;
|
| 514 |
}
|
| 515 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
.session-menu-button {
|
| 517 |
opacity: 0;
|
| 518 |
padding: 6px;
|
|
|
|
| 513 |
margin-left: 8px;
|
| 514 |
}
|
| 515 |
|
| 516 |
+
.new-chat-button:disabled {
|
| 517 |
+
opacity: 0.6;
|
| 518 |
+
cursor: not-allowed;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.collapsed-new-chat:disabled {
|
| 522 |
+
opacity: 0.6;
|
| 523 |
+
cursor: not-allowed;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.loading-sessions {
|
| 527 |
+
display: flex;
|
| 528 |
+
align-items: center;
|
| 529 |
+
justify-content: center;
|
| 530 |
+
gap: 8px;
|
| 531 |
+
padding: 16px;
|
| 532 |
+
color: var(--text-secondary);
|
| 533 |
+
font-size: 14px;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.loading-spinner {
|
| 537 |
+
width: 16px;
|
| 538 |
+
height: 16px;
|
| 539 |
+
border: 2px solid var(--border-color);
|
| 540 |
+
border-top: 2px solid var(--primary-color);
|
| 541 |
+
border-radius: 50%;
|
| 542 |
+
animation: spin 1s linear infinite;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
@keyframes spin {
|
| 546 |
+
0% { transform: rotate(0deg); }
|
| 547 |
+
100% { transform: rotate(360deg); }
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
/* Style for creating new chat state */
|
| 551 |
+
.new-chat-button span {
|
| 552 |
+
transition: all 0.2s ease;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.new-chat-button:disabled span {
|
| 556 |
+
opacity: 0.7;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/* Ensure active session remains highlighted during creation */
|
| 560 |
+
.session-item.active {
|
| 561 |
+
background-color: var(--primary-color);
|
| 562 |
+
color: white;
|
| 563 |
+
border-radius: 8px;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
.session-menu-button {
|
| 567 |
opacity: 0;
|
| 568 |
padding: 6px;
|