NeonCharlie-24 commited on
Commit
e19e41d
·
unverified ·
1 Parent(s): d16b388

Fix/dup first message (#44)

Browse files

* fixed duplicate user message in chat history on first message sent.

* removed unused chat-sequential endpoint and updated references in docs to chat-stream.

* moved user message db storage from frontend to backend.

* small frontend update to refresh sidebar after message exchange.

README.md CHANGED
@@ -310,7 +310,7 @@ curl -X POST "http://localhost:8000/switch-provider" \
310
  - `GET /auth/me` - Get current user profile
311
 
312
  ### Chat Endpoints
313
- - `POST /chat-sequential` - Get responses from all advisors
314
  - `POST /chat/{persona_id}` - Chat with specific advisor
315
  - `POST /reply-to-advisor` - Reply to specific advisor message
316
 
 
310
  - `GET /auth/me` - Get current user profile
311
 
312
  ### Chat Endpoints
313
+ - `POST /chat-stream` - Get streaming responses from all advisors (NDJSON)
314
  - `POST /chat/{persona_id}` - Chat with specific advisor
315
  - `POST /reply-to-advisor` - Reply to specific advisor message
316
 
multi_llm_chatbot_backend/README.md CHANGED
@@ -15,7 +15,7 @@ A modular, extensible FastAPI backend for building an AI-powered research adviso
15
  ```text
16
  User Input
17
 
18
- /chat-sequential → Orchestrator
19
  ↓ ↙ ↘
20
  SessionManager ContextManager RAGManager
21
  ↓ ↓ ↓
 
15
  ```text
16
  User Input
17
 
18
+ /chat-stream → Orchestrator
19
  ↓ ↙ ↘
20
  SessionManager ContextManager RAGManager
21
  ↓ ↓ ↓
multi_llm_chatbot_backend/app/api/README.md CHANGED
@@ -39,7 +39,7 @@ Uses JWT-based Bearer token auth via FastAPI dependencies.
39
 
40
  | Endpoint | Method | Description |
41
  |----------|--------|-------------|
42
- | `/chat-sequential` | `POST` | Run a full advisor loop and return all persona responses |
43
  | `/reply-to-advisor` | `POST` | Ask a question to a specific advisor/persona |
44
 
45
  These routes handle:
@@ -166,7 +166,7 @@ JWT tokens are passed via the `Authorization: Bearer ...` header.
166
  ## High-Level Flow
167
 
168
  ```text
169
- Frontend → /chat-sequential → orchestrator → personas → RAG + LLM → response[]
170
  ↘ /upload-document → extractor → RAG chunks → indexed
171
  ↘ /context or /reset-session → session_manager
172
  ↘ /export-chat or /chat-summary → utils + formatter
 
39
 
40
  | Endpoint | Method | Description |
41
  |----------|--------|-------------|
42
+ | `/chat-stream` | `POST` | Stream advisor responses as newline-delimited JSON |
43
  | `/reply-to-advisor` | `POST` | Ask a question to a specific advisor/persona |
44
 
45
  These routes handle:
 
166
  ## High-Level Flow
167
 
168
  ```text
169
+ Frontend → /chat-stream → orchestrator → personas → RAG + LLM → response[]
170
  ↘ /upload-document → extractor → RAG chunks → indexed
171
  ↘ /context or /reset-session → session_manager
172
  ↘ /export-chat or /chat-summary → utils + formatter
multi_llm_chatbot_backend/app/api/routes/chat.py CHANGED
@@ -9,6 +9,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
9
  from fastapi.responses import StreamingResponse
10
  from pydantic import BaseModel, Field
11
 
 
12
  from app.api.utils import get_or_create_session_for_request_async
13
  from app.core.auth import get_current_active_user
14
  from app.core.bootstrap import chat_orchestrator
@@ -68,7 +69,7 @@ async def chat_stream(
68
  current_user: User = Depends(get_current_active_user),
69
  ) -> StreamingResponse:
70
  """
71
- Streaming variant of chat-sequential (newline-delimited JSON).
72
  @param message: ChatMessage containing user input and optional session/chat IDs
73
  @param request: FastAPI Request object for session management
74
  @param current_user: Authenticated user from dependency injection
@@ -77,6 +78,7 @@ async def chat_stream(
77
 
78
  async def _event_generator():
79
  try:
 
80
  if message.chat_session_id:
81
  sid = f"chat_{message.chat_session_id}"
82
  if sid not in session_manager.sessions:
@@ -90,15 +92,14 @@ async def chat_stream(
90
 
91
  session = session_manager.get_session(sid)
92
 
93
- if (
94
- len(session.messages) > 0 and
95
- session.messages[-1].get("role") == "user" and
96
- session.messages[-1].get("content") == message.user_input
97
- ):
98
- # TODO: This should be handled in the front-end input
99
- logger.warning(f"Repeated user input: {message.user_input}")
100
-
101
  session.append_message("user", message.user_input)
 
 
 
 
 
 
102
 
103
  if chat_orchestrator.needs_clarification(session, message.user_input):
104
  clar = await chat_orchestrator.generate_contextual_clarification(message.user_input)
@@ -313,199 +314,6 @@ async def create_new_chat(
313
  logger.error(f"Error creating new chat: {e}")
314
  raise HTTPException(status_code=500, detail="Failed to create new chat")
315
 
316
- @router.post("/chat-sequential")
317
- async def chat_sequential_enhanced(
318
- message: ChatMessage,
319
- request: Request,
320
- current_user: User = Depends(get_current_active_user)
321
- ) -> Dict[str, Any]:
322
- """
323
- Enhanced sequential chat with proper session management, document access, and intelligent persona ordering
324
- @param message: ChatMessage containing user input and optional session/chat IDs
325
- @param request: FastAPI Request object for session management
326
- @param current_user: Authenticated user from dependency injection
327
- @return: Dict response with LLM responses if successful, else error details
328
- """
329
- try:
330
- # Ensure consistent session ID for document retrieval
331
- if message.chat_session_id:
332
- # Use the memory session format that matches document storage
333
- session_id = f"chat_{message.chat_session_id}"
334
- logger.info(f"Using chat session: {session_id}")
335
-
336
- # FIXED: Ensure session exists in memory (load if needed)
337
- if session_id not in session_manager.sessions:
338
- logger.warning(f"Chat session {message.chat_session_id} not in memory, loading now")
339
-
340
- # FIXED: Pass the user_id parameter to properly load existing session
341
- loaded_session_id = await get_or_create_session_for_request_async(
342
- request,
343
- chat_session_id=message.chat_session_id,
344
- user_id=str(current_user.id)
345
- )
346
-
347
- # Use the loaded session ID
348
- session_id = loaded_session_id
349
- logger.info(f"Loaded session from database: {session_id}")
350
- else:
351
- # No specific chat session, create/use ephemeral session
352
- session_id = await get_or_create_session_for_request_async(request)
353
- logger.info(f"Using ephemeral session: {session_id}")
354
-
355
- # Get session from memory
356
- session = session_manager.get_session(session_id)
357
-
358
- # Log session debugging info
359
- rag_stats = session.get_rag_stats()
360
- logger.info(f"Session {session_id} has {rag_stats.get('total_documents', 0)} documents available")
361
-
362
- # Warn if a repeated input message is received
363
- if (
364
- session.messages and
365
- session.messages[-1].get('role') == 'user' and
366
- session.messages[-1].get('content') == message.user_input
367
- ):
368
- # TODO: This should be handled in the front-end input
369
- logger.warning(f"Repeated user input: {message.user_input}")
370
- session.append_message("user", message.user_input)
371
-
372
- # Check if the user's message is vague and needs clarification
373
- if chat_orchestrator.needs_clarification(session, message.user_input):
374
- clarification = await chat_orchestrator.generate_contextual_clarification(
375
- message.user_input
376
- )
377
- logger.info(f"Clarification triggered for input: {message.user_input!r}")
378
- return {
379
- "status": "clarification_needed",
380
- "message": clarification["question"],
381
- "suggestions": clarification["suggestions"],
382
- "session_debug": {
383
- "session_id": session_id,
384
- "trigger": "vague_input"
385
- }
386
- }
387
-
388
- # If an enabled tool can handle this query, return its response
389
- # directly and skip persona generation.
390
- tool_result = await chat_orchestrator.get_tool_response(message.user_input)
391
- if tool_result.used_tool:
392
- session.append_message("orchestrator", tool_result.text)
393
- return {
394
- "responses": [{
395
- "persona_id": "orchestrator",
396
- "persona_name": "Orchestrator",
397
- "content": tool_result.text,
398
- "used_documents": False,
399
- "document_chunks_used": 0,
400
- }],
401
- "session_debug": {
402
- "session_id": session_id,
403
- "tool_used": True,
404
- }
405
- }
406
-
407
- # RESTORED: Get intelligently ordered personas based on context
408
- top_personas = await chat_orchestrator.get_top_personas(
409
- session_id=session_id,
410
- k=3 # Limit to top 3 most relevant personas
411
- )
412
-
413
- logger.info(f"Intelligent persona order for session {session_id}: {top_personas}")
414
-
415
- # Generate responses from ONLY the top personas
416
- responses = []
417
-
418
- for persona_id in top_personas:
419
- try:
420
- logger.info(f"Generating response for {persona_id} with session {session_id}")
421
-
422
- # Generate response from this specific persona
423
- persona_result = await chat_orchestrator.chat_with_persona(
424
- user_input=message.user_input,
425
- persona_id=persona_id,
426
- session_id=session_id, # This ensures document access
427
- response_length=message.response_length or "medium"
428
- )
429
-
430
- # FIXED: Safe response processing with proper error handling
431
- if isinstance(persona_result, dict):
432
- # Handle different response formats
433
- if "persona_name" in persona_result and "response" in persona_result:
434
- responses.append({
435
- "persona_id": persona_result["persona_id"],
436
- "persona_name": persona_result["persona_name"],
437
- "content": persona_result["response"],
438
- "used_documents": persona_result.get("used_documents", False),
439
- "document_chunks_used": persona_result.get("document_chunks_used", 0)
440
- })
441
- elif persona_result.get("type") == "single_persona_response" and "persona" in persona_result:
442
- persona_data = persona_result["persona"]
443
- responses.append({
444
- "persona_id": persona_data["persona_id"],
445
- "persona_name": persona_data["persona_name"],
446
- "content": persona_data["response"],
447
- "used_documents": persona_data.get("used_documents", False),
448
- "document_chunks_used": persona_data.get("document_chunks_used", 0)
449
- })
450
- elif "error" in persona_result:
451
- # Handle error responses
452
- responses.append({
453
- "persona_id": persona_id,
454
- "persona_name": chat_orchestrator.personas[persona_id].name,
455
- "content": persona_result["response"],
456
- "used_documents": False,
457
- "document_chunks_used": 0
458
- })
459
- else:
460
- # Generic dict response
461
- content = persona_result.get("response") or persona_result.get("content", "")
462
- if content.strip():
463
- responses.append({
464
- "persona_id": persona_id,
465
- "persona_name": chat_orchestrator.personas[persona_id].name,
466
- "content": content,
467
- "used_documents": persona_result.get("used_documents", False),
468
- "document_chunks_used": persona_result.get("document_chunks_used", 0)
469
- })
470
- else:
471
- # Fallback for non-dict responses
472
- responses.append({
473
- "persona_id": persona_id,
474
- "persona_name": chat_orchestrator.personas[persona_id].name,
475
- "content": "I'm having trouble processing your question right now. Please try again.",
476
- "used_documents": False,
477
- "document_chunks_used": 0
478
- })
479
-
480
- except Exception as e:
481
- logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
482
- responses.append({
483
- "persona_id": persona_id,
484
- "persona_name": chat_orchestrator.personas[persona_id].name,
485
- "content": "I encountered an error while processing your question. Please try again.",
486
- "used_documents": False,
487
- "document_chunks_used": 0
488
- })
489
-
490
- return {
491
- "responses": responses,
492
- "session_debug": {
493
- "session_id": session_id,
494
- "documents_available": rag_stats.get('total_documents', 0),
495
- "chunks_available": rag_stats.get('total_chunks', 0),
496
- "valid_responses": len(responses),
497
- "selected_personas": top_personas,
498
- "total_personas_available": len(chat_orchestrator.personas)
499
- }
500
- }
501
-
502
- except Exception as e:
503
- logger.error(f"Error in chat_sequential_enhanced: {e}")
504
- import traceback
505
- logger.error(f"Full traceback: {traceback.format_exc()}")
506
- raise HTTPException(status_code=500, detail=f"Chat processing failed: {str(e)}")
507
-
508
-
509
  @router.post("/chat/{persona_id}")
510
  async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
511
  """Chat with a specific advisor - UPDATED"""
 
9
  from fastapi.responses import StreamingResponse
10
  from pydantic import BaseModel, Field
11
 
12
+ from app.api.routes.chat_sessions import persist_message
13
  from app.api.utils import get_or_create_session_for_request_async
14
  from app.core.auth import get_current_active_user
15
  from app.core.bootstrap import chat_orchestrator
 
69
  current_user: User = Depends(get_current_active_user),
70
  ) -> StreamingResponse:
71
  """
72
+ Streaming chat endpoint (newline-delimited JSON).
73
  @param message: ChatMessage containing user input and optional session/chat IDs
74
  @param request: FastAPI Request object for session management
75
  @param current_user: Authenticated user from dependency injection
 
78
 
79
  async def _event_generator():
80
  try:
81
+ # Load or create the in-memory session
82
  if message.chat_session_id:
83
  sid = f"chat_{message.chat_session_id}"
84
  if sid not in session_manager.sessions:
 
92
 
93
  session = session_manager.get_session(sid)
94
 
95
+ # Append user message to in-memory session and persist to MongoDB
 
 
 
 
 
 
 
96
  session.append_message("user", message.user_input)
97
+ if message.chat_session_id:
98
+ await persist_message(message.chat_session_id, {
99
+ "id": str(ObjectId()),
100
+ "type": "user",
101
+ "content": message.user_input,
102
+ })
103
 
104
  if chat_orchestrator.needs_clarification(session, message.user_input):
105
  clar = await chat_orchestrator.generate_contextual_clarification(message.user_input)
 
314
  logger.error(f"Error creating new chat: {e}")
315
  raise HTTPException(status_code=500, detail="Failed to create new chat")
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  @router.post("/chat/{persona_id}")
318
  async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
319
  """Chat with a specific advisor - UPDATED"""
multi_llm_chatbot_backend/app/api/routes/chat_sessions.py CHANGED
@@ -23,6 +23,21 @@ class SaveMessageRequest(BaseModel):
23
  session_id: str
24
  message: dict
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @router.post("/chat-sessions", response_model=dict)
27
  async def create_chat_session(
28
  request: CreateChatSessionRequest,
@@ -58,6 +73,7 @@ async def create_chat_session(
58
  detail="Could not create chat session"
59
  )
60
 
 
61
  @router.get("/chat-sessions", response_model=List[ChatSessionResponse])
62
  async def get_user_chat_sessions(
63
  current_user: User = Depends(get_current_active_user),
@@ -227,18 +243,7 @@ async def save_message_to_session(
227
  detail="Chat session not found"
228
  )
229
 
230
- # Add timestamp to message if not present
231
- message = request.message.copy()
232
- if "timestamp" not in message:
233
- message["timestamp"] = datetime.utcnow().isoformat()
234
-
235
- await db.chat_sessions.update_one(
236
- {"_id": ObjectId(session_id)},
237
- {
238
- "$push": {"messages": message},
239
- "$set": {"updated_at": datetime.utcnow()}
240
- }
241
- )
242
 
243
  return {"message": "Message saved successfully"}
244
 
 
23
  session_id: str
24
  message: dict
25
 
26
+ async def persist_message(session_id: str, message: dict):
27
+ """Write a single message to a MongoDB chat session."""
28
+ db = get_database()
29
+ msg = message.copy()
30
+ if "timestamp" not in msg:
31
+ msg["timestamp"] = datetime.utcnow().isoformat()
32
+ await db.chat_sessions.update_one(
33
+ {"_id": ObjectId(session_id)},
34
+ {
35
+ "$push": {"messages": msg},
36
+ "$set": {"updated_at": datetime.utcnow()}
37
+ }
38
+ )
39
+
40
+
41
  @router.post("/chat-sessions", response_model=dict)
42
  async def create_chat_session(
43
  request: CreateChatSessionRequest,
 
73
  detail="Could not create chat session"
74
  )
75
 
76
+
77
  @router.get("/chat-sessions", response_model=List[ChatSessionResponse])
78
  async def get_user_chat_sessions(
79
  current_user: User = Depends(get_current_active_user),
 
243
  detail="Chat session not found"
244
  )
245
 
246
+ await persist_message(session_id, request.message)
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  return {"message": "Message saved successfully"}
249
 
multi_llm_chatbot_backend/app/core/README.md CHANGED
@@ -129,7 +129,7 @@ This is the main **message routing engine**.
129
  - Persona-specific fallback logic
130
  - Session reset/deletion
131
 
132
- Used by `/chat-sequential`, `/reply-to-advisor`, etc.
133
 
134
  ---
135
 
 
129
  - Persona-specific fallback logic
130
  - Session reset/deletion
131
 
132
+ Used by `/chat-stream`, `/reply-to-advisor`, etc.
133
 
134
  ---
135
 
phd-advisor-frontend/src/components/Sidebar.js CHANGED
@@ -26,7 +26,8 @@ const Sidebar = ({
26
  onSidebarToggle,
27
  isMobileOpen = false,
28
  onMobileToggle,
29
- onNavigateToCanvas
 
30
  }) => {
31
  const { config } = useAppConfig();
32
  const canvasLabel = config?.app?.title ? `${config.app.title} Canvas` : 'Canvas';
@@ -75,6 +76,13 @@ const Sidebar = ({
75
  }
76
  }, [currentSessionId, authToken]);
77
 
 
 
 
 
 
 
 
78
 
79
  const fetchChatSessions = async () => {
80
  try {
 
26
  onSidebarToggle,
27
  isMobileOpen = false,
28
  onMobileToggle,
29
+ onNavigateToCanvas,
30
+ refreshTrigger
31
  }) => {
32
  const { config } = useAppConfig();
33
  const canvasLabel = config?.app?.title ? `${config.app.title} Canvas` : 'Canvas';
 
76
  }
77
  }, [currentSessionId, authToken]);
78
 
79
+ // Refresh session list when parent signals a message exchange completed
80
+ useEffect(() => {
81
+ if (refreshTrigger > 0 && authToken) {
82
+ fetchChatSessions();
83
+ }
84
+ }, [refreshTrigger]);
85
+
86
 
87
  const fetchChatSessions = async () => {
88
  try {
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -33,6 +33,7 @@ const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSig
33
  const [isSavingSession, setIsSavingSession] = useState(false);
34
  const [isLoadingSession, setIsLoadingSession] = useState(false);
35
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
 
36
 
37
 
38
 
@@ -380,9 +381,6 @@ const handleNewChat = async (sessionId = null) => {
380
  }
381
  }
382
 
383
- // Save user message to database
384
- await saveMessageToSession(userMessage);
385
-
386
  // Update session title if this is the first message and title is generic
387
  if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
388
  const newTitle = inputMessage.length > 30
@@ -490,6 +488,7 @@ const handleNewChat = async (sessionId = null) => {
490
  } finally {
491
  setIsLoading(false);
492
  setThinkingAdvisors([]);
 
493
  }
494
  };
495
 
@@ -753,6 +752,7 @@ const handleNewChat = async (sessionId = null) => {
753
  isMobileOpen={isMobileMenuOpen}
754
  onMobileToggle={setIsMobileMenuOpen}
755
  onNavigateToCanvas={onNavigateToCanvas}
 
756
  />
757
 
758
  <div className={`main-chat-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
 
33
  const [isSavingSession, setIsSavingSession] = useState(false);
34
  const [isLoadingSession, setIsLoadingSession] = useState(false);
35
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
36
+ const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
37
 
38
 
39
 
 
381
  }
382
  }
383
 
 
 
 
384
  // Update session title if this is the first message and title is generic
385
  if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
386
  const newTitle = inputMessage.length > 30
 
488
  } finally {
489
  setIsLoading(false);
490
  setThinkingAdvisors([]);
491
+ setSidebarRefreshTrigger(prev => prev + 1);
492
  }
493
  };
494
 
 
752
  isMobileOpen={isMobileMenuOpen}
753
  onMobileToggle={setIsMobileMenuOpen}
754
  onNavigateToCanvas={onNavigateToCanvas}
755
+ refreshTrigger={sidebarRefreshTrigger}
756
  />
757
 
758
  <div className={`main-chat-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>