Spaces:
Running
Running
NeonCharlie-24 commited on
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 +1 -1
- multi_llm_chatbot_backend/README.md +1 -1
- multi_llm_chatbot_backend/app/api/README.md +2 -2
- multi_llm_chatbot_backend/app/api/routes/chat.py +10 -202
- multi_llm_chatbot_backend/app/api/routes/chat_sessions.py +17 -12
- multi_llm_chatbot_backend/app/core/README.md +1 -1
- phd-advisor-frontend/src/components/Sidebar.js +9 -1
- phd-advisor-frontend/src/pages/ChatPage.js +3 -3
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-
|
| 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-
|
| 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-
|
| 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-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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-
|
| 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' : ''}`}>
|