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 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 get_or_create_session_for_request
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
- # Keep all the same data models as before
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 orchestrator clarification for vague queries.
38
  """
39
  try:
40
- # Get or create session
41
- session_id = get_or_create_session_for_request(request, message.session_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- # STEP 1: Check if clarification is needed using orchestrator's logic
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
- # STEP 2: No clarification needed - proceed with intelligent persona ordering
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 - SAME INTERFACE"""
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
- # Get session using compatibility layer
142
- session_id = get_or_create_session_for_request(request)
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
- # FIX: Handle the actual response structure from orchestrator
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
- # Get session using compatibility layer
197
- session_id = get_or_create_session_for_request(request)
 
 
 
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 that includes the reply context
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 - SAME INTERFACE"""
313
  try:
314
- session_id = get_or_create_session_for_request(request)
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 get_or_create_session_for_request
 
 
 
 
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(request: Request):
 
 
 
 
 
 
 
 
14
  try:
15
- session_id = get_or_create_session_for_request(request)
 
 
 
 
 
 
 
 
 
 
 
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 {"messages": [], "rag_info": {"total_documents": 0, "total_chunks": 0}}
29
-
 
 
 
 
30
 
31
  @router.post("/reset-session")
32
- async def reset_session(request: Request):
 
 
 
 
 
 
 
 
33
  try:
34
- session_id = get_or_create_session_for_request(request)
35
- success = session_manager.reset_session_completely(session_id)
36
-
37
- if success:
38
- return {"status": "reset", "message": "Session and all documents reset successfully"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  else:
40
- return {"status": "error", "message": "Failed to reset session"}
 
 
 
 
 
 
 
 
 
 
 
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(request: Request):
 
 
 
 
 
 
 
 
48
  try:
49
- session_id = get_or_create_session_for_request(request)
 
 
 
 
 
 
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 get_or_create_session_for_request(request: Request, session_id_override: Optional[str] = None) -> str:
8
  """
9
- Get or create session for request using multiple strategies:
10
- 1. Explicit session ID
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
- client_ip = request.client.host if request.client else "unknown"
22
- ip_session_id = f"ip_{client_ip}"
23
- session = session_manager.get_session(ip_session_id)
24
- return session.session_id
 
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
- const response = await fetch('http://localhost:8000/api/chat-sessions', {
70
- method: 'POST',
71
- headers: {
72
- 'Authorization': `Bearer ${authToken}`,
73
- 'Content-Type': 'application/json'
74
- },
75
- body: JSON.stringify({
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 className="new-chat-button" onClick={handleNewChat}>
 
 
 
 
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 className="collapsed-new-chat" onClick={handleNewChat} title="New Chat">
 
 
 
 
 
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
- 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
- setCurrentSessionId(newSession.id);
136
- setCurrentSessionTitle(newSession.title);
137
- return newSession.id;
138
- } else {
139
- console.error('Failed to create new session');
 
 
 
 
 
 
 
 
140
  return null;
141
  }
142
- } catch (error) {
143
- console.error('Error creating new session:', error);
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
- const response = await fetch(`http://localhost:8000/api/chat-sessions/${sessionId}`, {
 
 
155
  headers: {
156
  'Authorization': `Bearer ${authToken}`,
157
  'Content-Type': 'application/json'
158
- }
 
 
 
159
  });
160
 
161
  if (response.ok) {
162
- const session = await response.json();
163
- setCurrentSessionId(session.id);
164
- setCurrentSessionTitle(session.title);
165
-
166
- // Convert stored messages back to proper format
167
- const formattedMessages = session.messages.map(msg => ({
168
- ...msg,
169
- timestamp: new Date(msg.timestamp)
170
- }));
171
-
172
- setMessages(formattedMessages);
173
- setReplyingTo(null);
174
- setThinkingAdvisors([]);
175
- } else {
176
- console.error('Failed to load session');
 
 
 
 
 
 
 
 
 
 
 
 
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
- // If specific session ID provided, load it
238
  await loadChatSession(sessionId);
 
239
  } else {
240
- // Create a completely new session
241
- setMessages([]);
242
- setCurrentSessionId(null);
243
- setCurrentSessionTitle('');
244
- setReplyingTo(null);
245
- setThinkingAdvisors([]);
246
- setUploadedDocuments([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- const replyMessage = {
387
- id: generateMessageId(),
388
- type: 'user',
389
- content: inputMessage,
390
- replyTo: {
391
- advisorId: replyContext.advisorId,
392
- advisorName: replyContext.advisorName,
393
- messageId: replyContext.messageId
394
- },
395
- timestamp: new Date()
396
- };
397
 
398
- setMessages(prev => [...prev, replyMessage]);
399
-
400
- // Save reply message to database
401
- await saveMessageToSession(replyMessage);
402
-
403
- setIsLoading(true);
404
- setThinkingAdvisors([replyContext.advisorId]);
 
 
 
 
405
 
406
- try {
407
- const response = await fetch('http://localhost:8000/reply-to-advisor', {
408
- method: 'POST',
409
- headers: {
410
- 'Content-Type': 'application/json',
411
- },
412
- body: JSON.stringify({
413
- user_input: inputMessage,
414
- advisor_id: replyContext.advisorId,
415
- original_message_id: replyContext.messageId
416
- }),
417
- });
418
 
419
- if (!response.ok) {
420
- throw new Error(`HTTP error! Status: ${response.status}`);
421
- }
 
 
 
 
 
 
 
 
 
 
422
 
423
- const data = await response.json();
 
 
424
 
425
- if (data.type === 'advisor_reply') {
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
- } catch (error) {
442
- console.error('Error replying to advisor:', error);
443
- const errorMessage = {
444
  id: generateMessageId(),
445
- type: 'error',
446
- content: 'Sorry, I encountered an error with your reply. Please try again.',
 
 
 
447
  timestamp: new Date()
448
  };
449
- setMessages(prev => [...prev, errorMessage]);
450
 
451
- // Save error message to database
452
- await saveMessageToSession(errorMessage);
453
  }
454
 
455
- setIsLoading(false);
456
- setThinkingAdvisors([]);
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;