Spaces:
Running
Running
Merge pull request #53 from sohank-17/girish-rag-feature
Browse files- .gitignore +6 -1
- multi_llm_chatbot_backend/app/api/routes.py +276 -65
- multi_llm_chatbot_backend/app/core/improved_orchestrator.py +159 -69
- multi_llm_chatbot_backend/app/core/rag_manager.py +469 -0
- multi_llm_chatbot_backend/app/core/session_manager.py +117 -24
- multi_llm_chatbot_backend/app/tests/debug_rag.py +208 -0
- multi_llm_chatbot_backend/app/tests/test_rag_system.py +155 -0
- multi_llm_chatbot_backend/requirements.txt +8 -1
- phd-advisor-frontend/src/components/MessageBubble.js +131 -3
- phd-advisor-frontend/src/pages/ChatPage.js +198 -223
- phd-advisor-frontend/src/styles/components.css +243 -0
.gitignore
CHANGED
|
@@ -3,5 +3,10 @@
|
|
| 3 |
|
| 4 |
# backend
|
| 5 |
multi_llm_chatbot_backend/.env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
# frontend
|
|
|
|
| 3 |
|
| 4 |
# backend
|
| 5 |
multi_llm_chatbot_backend/.env
|
| 6 |
+
multi_llm_chatbot_backend/chroma_db/
|
| 7 |
+
|
| 8 |
+
# frontend
|
| 9 |
+
|
| 10 |
+
# Ignore all __pycache__ folders
|
| 11 |
+
**/__pycache__/
|
| 12 |
|
|
|
multi_llm_chatbot_backend/app/api/routes.py
CHANGED
|
@@ -8,6 +8,7 @@ from app.llm.improved_ollama_client import ImprovedOllamaClient
|
|
| 8 |
from app.models.persona import Persona
|
| 9 |
from app.core.improved_orchestrator import ImprovedChatOrchestrator
|
| 10 |
from app.core.session_manager import get_session_manager
|
|
|
|
| 11 |
from app.models.default_personas import get_default_personas
|
| 12 |
from app.utils.document_extractor import extract_text_from_file
|
| 13 |
from app.utils.file_limits import is_within_upload_limit
|
|
@@ -182,64 +183,38 @@ async def switch_provider(provider_data: ProviderSwitch):
|
|
| 182 |
# Main chat endpoint (SAME INTERFACE, improved backend)
|
| 183 |
@router.post("/chat-sequential")
|
| 184 |
async def chat_sequential(message: ChatMessage, request: Request):
|
| 185 |
-
"""
|
| 186 |
-
SAME INTERFACE AS BEFORE - Generate advisor responses
|
| 187 |
-
Now with improved session management behind the scenes
|
| 188 |
-
"""
|
| 189 |
try:
|
| 190 |
-
# Get session using compatibility layer
|
| 191 |
session_id = get_or_create_session_for_request(request, message.session_id)
|
| 192 |
|
| 193 |
-
#
|
| 194 |
result = await chat_orchestrator.process_message(
|
| 195 |
user_input=message.user_input,
|
| 196 |
session_id=session_id,
|
| 197 |
-
response_length=message.response_length
|
| 198 |
)
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
if result["type"] == "
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
"
|
|
|
|
| 209 |
}
|
| 210 |
|
| 211 |
-
|
| 212 |
-
# Convert new response format to old format
|
| 213 |
-
return {
|
| 214 |
-
"type": "sequential_responses",
|
| 215 |
-
"responses": [
|
| 216 |
-
{
|
| 217 |
-
"persona": resp["persona_name"],
|
| 218 |
-
"persona_id": resp["persona_id"],
|
| 219 |
-
"response": resp["response"]
|
| 220 |
-
}
|
| 221 |
-
for resp in result["responses"]
|
| 222 |
-
],
|
| 223 |
-
"collected_info": {}
|
| 224 |
-
}
|
| 225 |
|
| 226 |
-
else:
|
| 227 |
-
return {
|
| 228 |
-
"type": "error",
|
| 229 |
-
"responses": [{
|
| 230 |
-
"persona": "System",
|
| 231 |
-
"response": result.get("message", "Please try again.")
|
| 232 |
-
}]
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
except Exception as e:
|
| 236 |
-
logger.error(f"Error in
|
| 237 |
return {
|
| 238 |
"type": "error",
|
| 239 |
-
"
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
}]
|
| 243 |
}
|
| 244 |
|
| 245 |
# Individual advisor endpoint (SAME INTERFACE)
|
|
@@ -326,13 +301,21 @@ async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
|
|
| 326 |
"response": "I'm having trouble generating a reply right now. Please try again."
|
| 327 |
}
|
| 328 |
|
| 329 |
-
# Document upload (SAME INTERFACE)
|
| 330 |
@router.post("/upload-document")
|
| 331 |
async def upload_document(file: UploadFile = File(...), request: Request = None):
|
| 332 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
if file.content_type not in [
|
| 334 |
"application/pdf",
|
| 335 |
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 336 |
"text/plain"
|
| 337 |
]:
|
| 338 |
raise HTTPException(status_code=400, detail="Unsupported file type.")
|
|
@@ -347,16 +330,49 @@ async def upload_document(file: UploadFile = File(...), request: Request = None)
|
|
| 347 |
# Simple size check
|
| 348 |
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
| 349 |
if len(file_bytes) > MAX_FILE_SIZE:
|
| 350 |
-
raise HTTPException(status_code=400, detail="Upload exceeds
|
| 351 |
|
|
|
|
| 352 |
content = extract_text_from_file(file_bytes, file.content_type)
|
| 353 |
if not content.strip():
|
| 354 |
raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
|
| 355 |
|
| 356 |
-
#
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
except HTTPException:
|
| 362 |
raise
|
|
@@ -364,6 +380,21 @@ async def upload_document(file: UploadFile = File(...), request: Request = None)
|
|
| 364 |
logger.error(f"Error uploading document: {str(e)}")
|
| 365 |
raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
# Get uploaded files (SAME INTERFACE)
|
| 368 |
@router.get("/uploaded-files")
|
| 369 |
async def get_uploaded_filenames(request: Request):
|
|
@@ -379,31 +410,44 @@ async def get_uploaded_filenames(request: Request):
|
|
| 379 |
# Context endpoint (SAME INTERFACE)
|
| 380 |
@router.get("/context")
|
| 381 |
async def get_context(request: Request):
|
| 382 |
-
"""Get context -
|
| 383 |
try:
|
| 384 |
session_id = get_or_create_session_for_request(request)
|
| 385 |
session = session_manager.get_session(session_id)
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
except Exception as e:
|
| 388 |
logger.error(f"Error getting context: {str(e)}")
|
| 389 |
-
return []
|
| 390 |
|
| 391 |
-
# Reset session (SAME INTERFACE)
|
| 392 |
@router.post("/reset-session")
|
| 393 |
async def reset_session(request: Request):
|
| 394 |
-
"""Reset session -
|
| 395 |
try:
|
| 396 |
session_id = get_or_create_session_for_request(request)
|
| 397 |
-
|
|
|
|
|
|
|
| 398 |
|
| 399 |
if success:
|
| 400 |
-
return {"status": "reset", "message": "Session reset successfully"}
|
| 401 |
else:
|
| 402 |
return {"status": "error", "message": "Failed to reset session"}
|
| 403 |
except Exception as e:
|
| 404 |
logger.error(f"Error resetting session: {e}")
|
| 405 |
return {"status": "error", "message": "Failed to reset session"}
|
| 406 |
|
|
|
|
| 407 |
# Legacy model endpoints (SAME INTERFACE)
|
| 408 |
@router.post("/switch-model")
|
| 409 |
async def switch_model(model_name: str = Body(...)):
|
|
@@ -422,30 +466,195 @@ async def get_current_model():
|
|
| 422 |
"provider": current_provider
|
| 423 |
}
|
| 424 |
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
@router.get("/debug/personas")
|
| 427 |
async def debug_personas(request: Request):
|
| 428 |
-
"""Debug personas -
|
| 429 |
try:
|
| 430 |
session_id = get_or_create_session_for_request(request)
|
| 431 |
session = session_manager.get_session(session_id)
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
return {
|
| 434 |
"personas": {
|
| 435 |
pid: {
|
| 436 |
"name": persona.name,
|
| 437 |
-
"prompt": persona.system_prompt[:100] + "..."
|
|
|
|
| 438 |
} for pid, persona in chat_orchestrator.personas.items()
|
| 439 |
},
|
| 440 |
-
"
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
}
|
| 443 |
except Exception as e:
|
| 444 |
logger.error(f"Error in debug endpoint: {str(e)}")
|
| 445 |
return {
|
| 446 |
"personas": {},
|
| 447 |
-
"context_length": 0,
|
| 448 |
-
"current_provider": current_provider
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
}
|
| 450 |
|
| 451 |
# Ask endpoint (SAME INTERFACE)
|
|
@@ -476,6 +685,8 @@ async def ask_question(query: PersonaQuery, request: Request):
|
|
| 476 |
except Exception as e:
|
| 477 |
logger.error(f"Error in ask endpoint: {str(e)}")
|
| 478 |
return {"response": "I encountered an error. Please try again."}
|
|
|
|
|
|
|
| 479 |
|
| 480 |
# Root endpoint (SAME INTERFACE)
|
| 481 |
@router.get("/")
|
|
|
|
| 8 |
from app.models.persona import Persona
|
| 9 |
from app.core.improved_orchestrator import ImprovedChatOrchestrator
|
| 10 |
from app.core.session_manager import get_session_manager
|
| 11 |
+
from app.core.rag_manager import get_rag_manager
|
| 12 |
from app.models.default_personas import get_default_personas
|
| 13 |
from app.utils.document_extractor import extract_text_from_file
|
| 14 |
from app.utils.file_limits import is_within_upload_limit
|
|
|
|
| 183 |
# Main chat endpoint (SAME INTERFACE, improved backend)
|
| 184 |
@router.post("/chat-sequential")
|
| 185 |
async def chat_sequential(message: ChatMessage, request: Request):
|
| 186 |
+
"""Generate advisor responses - ENHANCED with RAG"""
|
|
|
|
|
|
|
|
|
|
| 187 |
try:
|
|
|
|
| 188 |
session_id = get_or_create_session_for_request(request, message.session_id)
|
| 189 |
|
| 190 |
+
# Process message through improved orchestrator (now with RAG)
|
| 191 |
result = await chat_orchestrator.process_message(
|
| 192 |
user_input=message.user_input,
|
| 193 |
session_id=session_id,
|
| 194 |
+
response_length=message.response_length or "medium"
|
| 195 |
)
|
| 196 |
|
| 197 |
+
# Add RAG information to response
|
| 198 |
+
if result["type"] == "persona_responses":
|
| 199 |
+
# Count how many personas used documents
|
| 200 |
+
personas_with_docs = sum(1 for r in result["responses"] if r.get("used_documents", False))
|
| 201 |
+
total_chunks_used = sum(r.get("document_chunks_used", 0) for r in result["responses"])
|
| 202 |
+
|
| 203 |
+
result["rag_info"] = {
|
| 204 |
+
"personas_using_documents": personas_with_docs,
|
| 205 |
+
"total_document_chunks_used": total_chunks_used,
|
| 206 |
+
"rag_enabled": True
|
| 207 |
}
|
| 208 |
|
| 209 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
except Exception as e:
|
| 212 |
+
logger.error(f"Error in chat-sequential: {str(e)}")
|
| 213 |
return {
|
| 214 |
"type": "error",
|
| 215 |
+
"message": "I encountered an error processing your request. Please try again.",
|
| 216 |
+
"error": str(e),
|
| 217 |
+
"rag_enabled": False
|
|
|
|
| 218 |
}
|
| 219 |
|
| 220 |
# Individual advisor endpoint (SAME INTERFACE)
|
|
|
|
| 301 |
"response": "I'm having trouble generating a reply right now. Please try again."
|
| 302 |
}
|
| 303 |
|
|
|
|
| 304 |
@router.post("/upload-document")
|
| 305 |
async def upload_document(file: UploadFile = File(...), request: Request = None):
|
| 306 |
+
"""
|
| 307 |
+
Upload document with RAG integration - ENHANCED VERSION
|
| 308 |
+
|
| 309 |
+
Now documents are:
|
| 310 |
+
1. Chunked intelligently
|
| 311 |
+
2. Embedded and stored in ChromaDB
|
| 312 |
+
3. Available for semantic retrieval
|
| 313 |
+
4. NOT stored in session context (saves memory)
|
| 314 |
+
"""
|
| 315 |
+
# Validate file type
|
| 316 |
if file.content_type not in [
|
| 317 |
"application/pdf",
|
| 318 |
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 319 |
"text/plain"
|
| 320 |
]:
|
| 321 |
raise HTTPException(status_code=400, detail="Unsupported file type.")
|
|
|
|
| 330 |
# Simple size check
|
| 331 |
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
| 332 |
if len(file_bytes) > MAX_FILE_SIZE:
|
| 333 |
+
raise HTTPException(status_code=400, detail="Upload exceeds file size limit (10 MB).")
|
| 334 |
|
| 335 |
+
# Extract text content
|
| 336 |
content = extract_text_from_file(file_bytes, file.content_type)
|
| 337 |
if not content.strip():
|
| 338 |
raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
|
| 339 |
|
| 340 |
+
# Get RAG manager
|
| 341 |
+
rag_manager = get_rag_manager()
|
| 342 |
+
|
| 343 |
+
# Determine file type for metadata
|
| 344 |
+
file_type_map = {
|
| 345 |
+
"application/pdf": "pdf",
|
| 346 |
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
| 347 |
+
"text/plain": "txt"
|
| 348 |
+
}
|
| 349 |
+
file_type = file_type_map.get(file.content_type, "unknown")
|
| 350 |
+
|
| 351 |
+
# Add document to vector database instead of session context
|
| 352 |
+
rag_result = rag_manager.add_document(
|
| 353 |
+
content=content,
|
| 354 |
+
filename=file.filename,
|
| 355 |
+
session_id=session_id,
|
| 356 |
+
file_type=file_type
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
if not rag_result["success"]:
|
| 360 |
+
raise HTTPException(status_code=500, detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}")
|
| 361 |
+
|
| 362 |
+
# Add just the filename to session for tracking (not the full content)
|
| 363 |
+
session.uploaded_files.append(file.filename)
|
| 364 |
+
session.total_upload_size += len(file_bytes)
|
| 365 |
+
|
| 366 |
+
# Add a brief document reference to session messages (not full content)
|
| 367 |
+
session.append_message("system", f"Document uploaded: {file.filename} ({rag_result['chunks_created']} chunks, {rag_result['total_tokens']} tokens)")
|
| 368 |
|
| 369 |
+
return {
|
| 370 |
+
"message": "Document uploaded and processed successfully.",
|
| 371 |
+
"filename": file.filename,
|
| 372 |
+
"chunks_created": rag_result["chunks_created"],
|
| 373 |
+
"total_tokens": rag_result["total_tokens"],
|
| 374 |
+
"processing_method": "RAG_vector_storage"
|
| 375 |
+
}
|
| 376 |
|
| 377 |
except HTTPException:
|
| 378 |
raise
|
|
|
|
| 380 |
logger.error(f"Error uploading document: {str(e)}")
|
| 381 |
raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
|
| 382 |
|
| 383 |
+
# Add new endpoint to get document statistics
|
| 384 |
+
@router.get("/document-stats")
|
| 385 |
+
async def get_document_stats(request: Request):
|
| 386 |
+
"""Get statistics about uploaded documents in vector database"""
|
| 387 |
+
try:
|
| 388 |
+
session_id = get_or_create_session_for_request(request)
|
| 389 |
+
rag_manager = get_rag_manager()
|
| 390 |
+
|
| 391 |
+
stats = rag_manager.get_document_stats(session_id)
|
| 392 |
+
return stats
|
| 393 |
+
|
| 394 |
+
except Exception as e:
|
| 395 |
+
logger.error(f"Error getting document stats: {str(e)}")
|
| 396 |
+
return {"total_chunks": 0, "total_documents": 0, "documents": []}
|
| 397 |
+
|
| 398 |
# Get uploaded files (SAME INTERFACE)
|
| 399 |
@router.get("/uploaded-files")
|
| 400 |
async def get_uploaded_filenames(request: Request):
|
|
|
|
| 410 |
# Context endpoint (SAME INTERFACE)
|
| 411 |
@router.get("/context")
|
| 412 |
async def get_context(request: Request):
|
| 413 |
+
"""Get context - ENHANCED with RAG information"""
|
| 414 |
try:
|
| 415 |
session_id = get_or_create_session_for_request(request)
|
| 416 |
session = session_manager.get_session(session_id)
|
| 417 |
+
|
| 418 |
+
# Get RAG statistics
|
| 419 |
+
rag_stats = session.get_rag_stats()
|
| 420 |
+
|
| 421 |
+
return {
|
| 422 |
+
"messages": session.messages,
|
| 423 |
+
"rag_info": {
|
| 424 |
+
"total_documents": rag_stats.get("total_documents", 0),
|
| 425 |
+
"total_chunks": rag_stats.get("total_chunks", 0),
|
| 426 |
+
"documents": rag_stats.get("documents", [])
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
except Exception as e:
|
| 430 |
logger.error(f"Error getting context: {str(e)}")
|
| 431 |
+
return {"messages": [], "rag_info": {"total_documents": 0, "total_chunks": 0}}
|
| 432 |
|
|
|
|
| 433 |
@router.post("/reset-session")
|
| 434 |
async def reset_session(request: Request):
|
| 435 |
+
"""Reset session - ENHANCED with RAG cleanup"""
|
| 436 |
try:
|
| 437 |
session_id = get_or_create_session_for_request(request)
|
| 438 |
+
|
| 439 |
+
# Use the enhanced reset that clears both conversation and vector DB
|
| 440 |
+
success = session_manager.reset_session_completely(session_id)
|
| 441 |
|
| 442 |
if success:
|
| 443 |
+
return {"status": "reset", "message": "Session and all documents reset successfully"}
|
| 444 |
else:
|
| 445 |
return {"status": "error", "message": "Failed to reset session"}
|
| 446 |
except Exception as e:
|
| 447 |
logger.error(f"Error resetting session: {e}")
|
| 448 |
return {"status": "error", "message": "Failed to reset session"}
|
| 449 |
|
| 450 |
+
|
| 451 |
# Legacy model endpoints (SAME INTERFACE)
|
| 452 |
@router.post("/switch-model")
|
| 453 |
async def switch_model(model_name: str = Body(...)):
|
|
|
|
| 466 |
"provider": current_provider
|
| 467 |
}
|
| 468 |
|
| 469 |
+
@router.post("/search-documents")
|
| 470 |
+
async def search_documents(request: Request, query: str = Body(..., embed=True), persona: str = Body("", embed=True)):
|
| 471 |
+
"""
|
| 472 |
+
Search uploaded documents using RAG
|
| 473 |
+
|
| 474 |
+
This endpoint allows direct document search for debugging/testing
|
| 475 |
+
"""
|
| 476 |
+
try:
|
| 477 |
+
session_id = get_or_create_session_for_request(request)
|
| 478 |
+
rag_manager = get_rag_manager()
|
| 479 |
+
|
| 480 |
+
# Get persona context for search enhancement
|
| 481 |
+
persona_contexts = {
|
| 482 |
+
"methodist": "methodology research design analysis",
|
| 483 |
+
"theorist": "theory theoretical framework conceptual",
|
| 484 |
+
"pragmatist": "practical application implementation"
|
| 485 |
+
}
|
| 486 |
+
persona_context = persona_contexts.get(persona, "")
|
| 487 |
+
|
| 488 |
+
# Search documents
|
| 489 |
+
results = rag_manager.search_documents(
|
| 490 |
+
query=query,
|
| 491 |
+
session_id=session_id,
|
| 492 |
+
persona_context=persona_context,
|
| 493 |
+
n_results=5
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
return {
|
| 497 |
+
"query": query,
|
| 498 |
+
"persona_filter": persona,
|
| 499 |
+
"results_count": len(results),
|
| 500 |
+
"results": results
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
except Exception as e:
|
| 504 |
+
logger.error(f"Error searching documents: {str(e)}")
|
| 505 |
+
return {"query": query, "results_count": 0, "results": [], "error": str(e)}
|
| 506 |
+
|
| 507 |
+
@router.get("/session-stats")
|
| 508 |
+
async def get_session_stats(request: Request):
|
| 509 |
+
"""Get comprehensive session statistics including RAG data"""
|
| 510 |
+
try:
|
| 511 |
+
session_id = get_or_create_session_for_request(request)
|
| 512 |
+
stats = session_manager.get_session_stats(session_id)
|
| 513 |
+
return stats
|
| 514 |
+
except Exception as e:
|
| 515 |
+
logger.error(f"Error getting session stats: {str(e)}")
|
| 516 |
+
return {"error": str(e)}
|
| 517 |
+
|
| 518 |
+
|
| 519 |
@router.get("/debug/personas")
|
| 520 |
async def debug_personas(request: Request):
|
| 521 |
+
"""Debug personas - ENHANCED with RAG information"""
|
| 522 |
try:
|
| 523 |
session_id = get_or_create_session_for_request(request)
|
| 524 |
session = session_manager.get_session(session_id)
|
| 525 |
|
| 526 |
+
# Get RAG statistics
|
| 527 |
+
rag_manager = get_rag_manager()
|
| 528 |
+
rag_stats = rag_manager.get_document_stats(session_id)
|
| 529 |
+
|
| 530 |
return {
|
| 531 |
"personas": {
|
| 532 |
pid: {
|
| 533 |
"name": persona.name,
|
| 534 |
+
"prompt": persona.system_prompt[:100] + "...",
|
| 535 |
+
"retrieval_keywords": chat_orchestrator._get_persona_context_keywords(pid)
|
| 536 |
} for pid, persona in chat_orchestrator.personas.items()
|
| 537 |
},
|
| 538 |
+
"session_info": {
|
| 539 |
+
"context_length": len(session.messages),
|
| 540 |
+
"uploaded_files": session.uploaded_files,
|
| 541 |
+
"rag_stats": rag_stats
|
| 542 |
+
},
|
| 543 |
+
"current_provider": current_provider,
|
| 544 |
+
"rag_enabled": True
|
| 545 |
}
|
| 546 |
except Exception as e:
|
| 547 |
logger.error(f"Error in debug endpoint: {str(e)}")
|
| 548 |
return {
|
| 549 |
"personas": {},
|
| 550 |
+
"session_info": {"context_length": 0},
|
| 551 |
+
"current_provider": current_provider,
|
| 552 |
+
"rag_enabled": False,
|
| 553 |
+
"error": str(e)
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
@router.post("/chat/{persona_id}")
|
| 557 |
+
async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request):
|
| 558 |
+
"""
|
| 559 |
+
Chat with a specific persona - Enhanced with RAG debugging
|
| 560 |
+
|
| 561 |
+
This endpoint helps debug RAG integration by testing individual personas
|
| 562 |
+
"""
|
| 563 |
+
try:
|
| 564 |
+
session_id = get_or_create_session_for_request(request, message.session_id)
|
| 565 |
+
|
| 566 |
+
# Validate persona exists
|
| 567 |
+
if persona_id not in chat_orchestrator.personas:
|
| 568 |
+
available_personas = list(chat_orchestrator.personas.keys())
|
| 569 |
+
raise HTTPException(
|
| 570 |
+
status_code=400,
|
| 571 |
+
detail=f"Persona '{persona_id}' not found. Available: {available_personas}"
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# Use the enhanced orchestrator method
|
| 575 |
+
result = await chat_orchestrator.chat_with_persona(
|
| 576 |
+
user_input=message.user_input,
|
| 577 |
+
persona_id=persona_id,
|
| 578 |
+
session_id=session_id,
|
| 579 |
+
response_length=message.response_length or "medium"
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
# Fix: Handle the response structure properly
|
| 583 |
+
if result.get("type") == "single_persona_response" and "persona" in result:
|
| 584 |
+
persona_data = result["persona"]
|
| 585 |
+
|
| 586 |
+
# Add debugging information
|
| 587 |
+
result["debug_info"] = {
|
| 588 |
+
"persona_id": persona_id,
|
| 589 |
+
"session_id": session_id,
|
| 590 |
+
"query_length": len(message.user_input),
|
| 591 |
+
"rag_manager_available": True,
|
| 592 |
+
"used_documents": persona_data.get("used_documents", False),
|
| 593 |
+
"chunks_used": persona_data.get("document_chunks_used", 0)
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
return result
|
| 597 |
+
|
| 598 |
+
except HTTPException:
|
| 599 |
+
raise
|
| 600 |
+
except Exception as e:
|
| 601 |
+
logger.error(f"Error in individual persona chat: {str(e)}")
|
| 602 |
+
return {
|
| 603 |
+
"type": "error",
|
| 604 |
+
"message": f"Error chatting with {persona_id}: {str(e)}",
|
| 605 |
+
"persona_id": persona_id
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
# Also add a debug endpoint to check RAG status:
|
| 609 |
+
|
| 610 |
+
@router.get("/debug/rag-status")
|
| 611 |
+
async def debug_rag_status(request: Request):
|
| 612 |
+
"""
|
| 613 |
+
Debug endpoint to check RAG system status
|
| 614 |
+
"""
|
| 615 |
+
try:
|
| 616 |
+
session_id = get_or_create_session_for_request(request)
|
| 617 |
+
|
| 618 |
+
# Get RAG manager
|
| 619 |
+
rag_manager = get_rag_manager()
|
| 620 |
+
|
| 621 |
+
# Get session stats
|
| 622 |
+
session_stats = session_manager.get_session_stats(session_id)
|
| 623 |
+
|
| 624 |
+
# Test a simple search
|
| 625 |
+
test_search = rag_manager.search_documents(
|
| 626 |
+
query="test methodology research",
|
| 627 |
+
session_id=session_id,
|
| 628 |
+
persona_context="",
|
| 629 |
+
n_results=3
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
return {
|
| 633 |
+
"rag_manager_healthy": True,
|
| 634 |
+
"session_id": session_id,
|
| 635 |
+
"session_stats": session_stats.get("rag_stats", {}),
|
| 636 |
+
"test_search_results": len(test_search),
|
| 637 |
+
"test_search_details": [
|
| 638 |
+
{
|
| 639 |
+
"relevance": chunk.get("relevance_score", 0),
|
| 640 |
+
"distance": chunk.get("distance", "unknown"),
|
| 641 |
+
"text_length": len(chunk.get("text", "")),
|
| 642 |
+
"filename": chunk.get("metadata", {}).get("filename", "unknown")
|
| 643 |
+
}
|
| 644 |
+
for chunk in test_search[:3]
|
| 645 |
+
],
|
| 646 |
+
"persona_keywords": {
|
| 647 |
+
pid: chat_orchestrator._get_persona_context_keywords(pid)
|
| 648 |
+
for pid in chat_orchestrator.personas.keys()
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
except Exception as e:
|
| 653 |
+
logger.error(f"Error in RAG debug: {str(e)}")
|
| 654 |
+
return {
|
| 655 |
+
"rag_manager_healthy": False,
|
| 656 |
+
"error": str(e),
|
| 657 |
+
"session_id": session_id if 'session_id' in locals() else "unknown"
|
| 658 |
}
|
| 659 |
|
| 660 |
# Ask endpoint (SAME INTERFACE)
|
|
|
|
| 685 |
except Exception as e:
|
| 686 |
logger.error(f"Error in ask endpoint: {str(e)}")
|
| 687 |
return {"response": "I encountered an error. Please try again."}
|
| 688 |
+
|
| 689 |
+
|
| 690 |
|
| 691 |
# Root endpoint (SAME INTERFACE)
|
| 692 |
@router.get("/")
|
multi_llm_chatbot_backend/app/core/improved_orchestrator.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Dict, List, Optional, Any
|
|
| 2 |
from app.models.persona import Persona
|
| 3 |
from app.core.session_manager import ConversationContext, get_session_manager
|
| 4 |
from app.core.context_manager import get_context_manager
|
|
|
|
| 5 |
from app.llm.llm_client import LLMClient
|
| 6 |
import logging
|
| 7 |
import re
|
|
@@ -85,7 +86,7 @@ class ImprovedChatOrchestrator:
|
|
| 85 |
session_id: Optional[str] = None,
|
| 86 |
response_length: str = "medium") -> Dict[str, Any]:
|
| 87 |
"""
|
| 88 |
-
Chat with a specific persona
|
| 89 |
"""
|
| 90 |
try:
|
| 91 |
if persona_id not in self.personas:
|
|
@@ -100,7 +101,7 @@ class ImprovedChatOrchestrator:
|
|
| 100 |
# Add user message
|
| 101 |
session.append_message("user", user_input)
|
| 102 |
|
| 103 |
-
# Generate response from specific persona
|
| 104 |
persona = self.personas[persona_id]
|
| 105 |
response = await self._generate_single_persona_response(session, persona, response_length)
|
| 106 |
|
|
@@ -111,7 +112,11 @@ class ImprovedChatOrchestrator:
|
|
| 111 |
"type": "single_persona_response",
|
| 112 |
"session_id": session.session_id,
|
| 113 |
"persona": response,
|
| 114 |
-
"context_summary": self.context_manager.get_context_summary(session.messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
except Exception as e:
|
|
@@ -123,38 +128,30 @@ class ImprovedChatOrchestrator:
|
|
| 123 |
"error": str(e)
|
| 124 |
}
|
| 125 |
|
| 126 |
-
def _needs_clarification(self, session
|
| 127 |
"""
|
| 128 |
-
Determine if
|
| 129 |
"""
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
return False
|
| 134 |
-
|
| 135 |
-
# Check if input is vague
|
| 136 |
-
vague_patterns = [
|
| 137 |
-
r"i'm (not sure|unsure|confused|lost)",
|
| 138 |
-
r"i (don't know|dunno) (what|how|where)",
|
| 139 |
-
r"help me with (my|the|a) (thesis|research|phd)",
|
| 140 |
-
r"(what should i|how do i|where do i start)",
|
| 141 |
-
r"i need (help|advice|guidance)$",
|
| 142 |
-
r"(stuck|struggling) with",
|
| 143 |
-
r"(any|some) (advice|suggestions|ideas)$",
|
| 144 |
-
r"^(help|advice|guidance)$",
|
| 145 |
-
]
|
| 146 |
-
|
| 147 |
-
user_lower = user_input.lower().strip()
|
| 148 |
-
|
| 149 |
-
for pattern in vague_patterns:
|
| 150 |
-
if re.search(pattern, user_lower):
|
| 151 |
return True
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
async def _generate_clarification_question(self, session: ConversationContext) -> str:
|
| 160 |
"""
|
|
@@ -196,62 +193,84 @@ class ImprovedChatOrchestrator:
|
|
| 196 |
else:
|
| 197 |
return "Could you provide more details about your specific situation or question?"
|
| 198 |
|
| 199 |
-
async def _generate_persona_responses(self,
|
| 200 |
-
session: ConversationContext,
|
| 201 |
-
response_length: str) -> List[Dict[str, Any]]:
|
| 202 |
"""
|
| 203 |
-
Generate responses from all personas
|
|
|
|
|
|
|
| 204 |
"""
|
| 205 |
responses = []
|
| 206 |
|
| 207 |
-
# Get the conversation context for personas
|
| 208 |
-
context_messages = session.get_recent_messages(limit=20)
|
| 209 |
-
|
| 210 |
for persona_id, persona in self.personas.items():
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
except Exception as e:
|
| 221 |
-
logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
|
| 222 |
-
# Add fallback response
|
| 223 |
-
responses.append({
|
| 224 |
-
"persona_id": persona_id,
|
| 225 |
-
"persona_name": persona.name,
|
| 226 |
-
"response": self._get_fallback_response(persona_id),
|
| 227 |
-
"error": True
|
| 228 |
-
})
|
| 229 |
|
| 230 |
return responses
|
| 231 |
|
| 232 |
-
async def _generate_single_persona_response(self,
|
| 233 |
-
session: ConversationContext,
|
| 234 |
-
persona: Persona,
|
| 235 |
-
response_length: str) -> Dict[str, Any]:
|
| 236 |
"""
|
| 237 |
-
Generate response from a single persona
|
| 238 |
"""
|
| 239 |
try:
|
| 240 |
-
# Get conversation context
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
#
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
# Validate response
|
| 247 |
if not self._is_valid_response(response, persona.id):
|
|
|
|
| 248 |
response = self._get_fallback_response(persona.id)
|
| 249 |
|
| 250 |
return {
|
| 251 |
"persona_id": persona.id,
|
| 252 |
"persona_name": persona.name,
|
| 253 |
"response": response,
|
| 254 |
-
"error": False
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
except Exception as e:
|
|
@@ -260,7 +279,9 @@ class ImprovedChatOrchestrator:
|
|
| 260 |
"persona_id": persona.id,
|
| 261 |
"persona_name": persona.name,
|
| 262 |
"response": self._get_fallback_response(persona.id),
|
| 263 |
-
"error": True
|
|
|
|
|
|
|
| 264 |
}
|
| 265 |
|
| 266 |
def _is_valid_response(self, response: str, persona_id: str) -> bool:
|
|
@@ -314,4 +335,73 @@ class ImprovedChatOrchestrator:
|
|
| 314 |
|
| 315 |
def delete_session(self, session_id: str) -> bool:
|
| 316 |
"""Delete a session completely"""
|
| 317 |
-
return self.session_manager.delete_session(session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from app.models.persona import Persona
|
| 3 |
from app.core.session_manager import ConversationContext, get_session_manager
|
| 4 |
from app.core.context_manager import get_context_manager
|
| 5 |
+
from app.core.rag_manager import get_rag_manager
|
| 6 |
from app.llm.llm_client import LLMClient
|
| 7 |
import logging
|
| 8 |
import re
|
|
|
|
| 86 |
session_id: Optional[str] = None,
|
| 87 |
response_length: str = "medium") -> Dict[str, Any]:
|
| 88 |
"""
|
| 89 |
+
Chat with a specific persona - ENHANCED WITH RAG
|
| 90 |
"""
|
| 91 |
try:
|
| 92 |
if persona_id not in self.personas:
|
|
|
|
| 101 |
# Add user message
|
| 102 |
session.append_message("user", user_input)
|
| 103 |
|
| 104 |
+
# Generate response from specific persona with RAG
|
| 105 |
persona = self.personas[persona_id]
|
| 106 |
response = await self._generate_single_persona_response(session, persona, response_length)
|
| 107 |
|
|
|
|
| 112 |
"type": "single_persona_response",
|
| 113 |
"session_id": session.session_id,
|
| 114 |
"persona": response,
|
| 115 |
+
"context_summary": self.context_manager.get_context_summary(session.messages),
|
| 116 |
+
"rag_info": {
|
| 117 |
+
"used_documents": response.get("used_documents", False),
|
| 118 |
+
"document_chunks_used": response.get("document_chunks_used", 0)
|
| 119 |
+
}
|
| 120 |
}
|
| 121 |
|
| 122 |
except Exception as e:
|
|
|
|
| 128 |
"error": str(e)
|
| 129 |
}
|
| 130 |
|
| 131 |
+
def _needs_clarification(self, session, user_input: str) -> bool:
|
| 132 |
"""
|
| 133 |
+
Determine if we need clarification - SIMPLIFIED VERSION
|
| 134 |
"""
|
| 135 |
+
try:
|
| 136 |
+
# Simple heuristics for clarification
|
| 137 |
+
if len(user_input.strip()) < 10:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
return True
|
| 139 |
+
|
| 140 |
+
# Check if it's a greeting or very general question
|
| 141 |
+
greeting_words = ['hi', 'hello', 'hey', 'good morning', 'good afternoon']
|
| 142 |
+
if any(word in user_input.lower() for word in greeting_words):
|
| 143 |
+
return True
|
| 144 |
+
|
| 145 |
+
# Very vague questions
|
| 146 |
+
vague_words = ['help', 'advice', 'what should i do', 'i need help']
|
| 147 |
+
if len([word for word in vague_words if word in user_input.lower()]) > 0 and len(user_input.split()) < 8:
|
| 148 |
+
return True
|
| 149 |
+
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Error in clarification check: {str(e)}")
|
| 154 |
+
return False # Default to no clarification needed
|
| 155 |
|
| 156 |
async def _generate_clarification_question(self, session: ConversationContext) -> str:
|
| 157 |
"""
|
|
|
|
| 193 |
else:
|
| 194 |
return "Could you provide more details about your specific situation or question?"
|
| 195 |
|
| 196 |
+
async def _generate_persona_responses(self, session, response_length: str = "medium"):
|
|
|
|
|
|
|
| 197 |
"""
|
| 198 |
+
Generate responses from all personas with RAG integration
|
| 199 |
+
|
| 200 |
+
Each persona gets persona-specific document retrieval for more targeted responses
|
| 201 |
"""
|
| 202 |
responses = []
|
| 203 |
|
|
|
|
|
|
|
|
|
|
| 204 |
for persona_id, persona in self.personas.items():
|
| 205 |
+
logger.info(f"Generating response for {persona_id} with RAG")
|
| 206 |
+
|
| 207 |
+
# Generate persona response with RAG
|
| 208 |
+
response_data = await self._generate_single_persona_response(session, persona, response_length)
|
| 209 |
+
|
| 210 |
+
# Add persona response to session context
|
| 211 |
+
session.append_message(persona_id, response_data["response"])
|
| 212 |
+
|
| 213 |
+
responses.append(response_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
return responses
|
| 216 |
|
| 217 |
+
async def _generate_single_persona_response(self, session, persona, response_length: str = "medium"):
|
|
|
|
|
|
|
|
|
|
| 218 |
"""
|
| 219 |
+
Generate response from a single persona with RAG-enhanced context - FIXED VERSION
|
| 220 |
"""
|
| 221 |
try:
|
| 222 |
+
# Get conversation context (recent messages only for efficiency)
|
| 223 |
+
recent_messages = session.messages[-5:] if len(session.messages) > 5 else session.messages
|
| 224 |
+
|
| 225 |
+
# Get the user's latest message for document retrieval
|
| 226 |
+
user_message = ""
|
| 227 |
+
try:
|
| 228 |
+
# Use the new method to get latest user message
|
| 229 |
+
user_message = session.get_latest_user_message() or ""
|
| 230 |
+
except AttributeError:
|
| 231 |
+
# Fallback: manually find latest user message
|
| 232 |
+
for msg in reversed(recent_messages):
|
| 233 |
+
if msg.get('role') == 'user':
|
| 234 |
+
user_message = msg.get('content', '')
|
| 235 |
+
break
|
| 236 |
+
|
| 237 |
+
# Retrieve relevant document context using RAG
|
| 238 |
+
document_context = ""
|
| 239 |
+
if user_message:
|
| 240 |
+
document_context = await self._retrieve_relevant_documents(
|
| 241 |
+
user_input=user_message,
|
| 242 |
+
session_id=session.session_id,
|
| 243 |
+
persona_id=persona.id
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Build enhanced context for the LLM
|
| 247 |
+
enhanced_context = []
|
| 248 |
+
|
| 249 |
+
# Add document context at the beginning if available
|
| 250 |
+
if document_context:
|
| 251 |
+
enhanced_context.append({
|
| 252 |
+
"role": "system",
|
| 253 |
+
"content": f"{document_context}Based on the above document context and conversation history, please respond as {persona.name}."
|
| 254 |
+
})
|
| 255 |
|
| 256 |
+
# Add recent conversation messages
|
| 257 |
+
enhanced_context.extend(recent_messages)
|
| 258 |
+
|
| 259 |
+
# Generate response with enhanced context
|
| 260 |
+
response = await persona.respond(enhanced_context, response_length)
|
| 261 |
|
| 262 |
# Validate response
|
| 263 |
if not self._is_valid_response(response, persona.id):
|
| 264 |
+
logger.warning(f"Invalid response from {persona.id}, using fallback")
|
| 265 |
response = self._get_fallback_response(persona.id)
|
| 266 |
|
| 267 |
return {
|
| 268 |
"persona_id": persona.id,
|
| 269 |
"persona_name": persona.name,
|
| 270 |
"response": response,
|
| 271 |
+
"error": False,
|
| 272 |
+
"used_documents": bool(document_context),
|
| 273 |
+
"document_chunks_used": document_context.count("[Document") if document_context else 0
|
| 274 |
}
|
| 275 |
|
| 276 |
except Exception as e:
|
|
|
|
| 279 |
"persona_id": persona.id,
|
| 280 |
"persona_name": persona.name,
|
| 281 |
"response": self._get_fallback_response(persona.id),
|
| 282 |
+
"error": True,
|
| 283 |
+
"used_documents": False,
|
| 284 |
+
"document_chunks_used": 0
|
| 285 |
}
|
| 286 |
|
| 287 |
def _is_valid_response(self, response: str, persona_id: str) -> bool:
|
|
|
|
| 335 |
|
| 336 |
def delete_session(self, session_id: str) -> bool:
|
| 337 |
"""Delete a session completely"""
|
| 338 |
+
return self.session_manager.delete_session(session_id)
|
| 339 |
+
|
| 340 |
+
def _get_persona_context_keywords(self, persona_id: str) -> str:
|
| 341 |
+
"""
|
| 342 |
+
Get persona-specific keywords for enhanced document retrieval
|
| 343 |
+
|
| 344 |
+
This ensures each advisor gets the most relevant document chunks
|
| 345 |
+
based on their specialization
|
| 346 |
+
"""
|
| 347 |
+
persona_keywords = {
|
| 348 |
+
"methodist": "methodology research design analysis methods data collection sampling validity reliability statistical approach quantitative qualitative",
|
| 349 |
+
"theorist": "theory theoretical framework conceptual model literature review philosophy epistemology ontology paradigm abstract concepts",
|
| 350 |
+
"pragmatist": "practical application implementation action steps next steps recommendation solution strategy timeline concrete advice"
|
| 351 |
+
}
|
| 352 |
+
return persona_keywords.get(persona_id, "")
|
| 353 |
+
|
| 354 |
+
async def _retrieve_relevant_documents(self, user_input: str, session_id: str, persona_id: str = "") -> str:
|
| 355 |
+
"""
|
| 356 |
+
Retrieve relevant document chunks using RAG - FIXED VERSION
|
| 357 |
+
"""
|
| 358 |
+
try:
|
| 359 |
+
rag_manager = get_rag_manager()
|
| 360 |
+
|
| 361 |
+
# Get persona-specific context for better retrieval
|
| 362 |
+
persona_context = self._get_persona_context_keywords(persona_id)
|
| 363 |
+
|
| 364 |
+
# Search for relevant chunks
|
| 365 |
+
relevant_chunks = rag_manager.search_documents(
|
| 366 |
+
query=user_input,
|
| 367 |
+
session_id=session_id,
|
| 368 |
+
persona_context=persona_context,
|
| 369 |
+
n_results=3 # Limit to top 3 most relevant chunks
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
logger.info(f"Retrieved {len(relevant_chunks)} total chunks for {persona_id}")
|
| 373 |
+
|
| 374 |
+
if not relevant_chunks:
|
| 375 |
+
logger.info(f"No relevant documents found for query: {user_input[:50]}...")
|
| 376 |
+
return ""
|
| 377 |
+
|
| 378 |
+
# Format retrieved content - LOWERED THRESHOLD
|
| 379 |
+
context_parts = []
|
| 380 |
+
for i, chunk in enumerate(relevant_chunks, 1):
|
| 381 |
+
filename = chunk["metadata"].get("filename", "Unknown")
|
| 382 |
+
relevance = chunk["relevance_score"]
|
| 383 |
+
distance = chunk.get("distance", "unknown")
|
| 384 |
+
text = chunk["text"]
|
| 385 |
+
|
| 386 |
+
logger.info(f"Chunk {i} for {persona_id}: relevance={relevance:.3f}, distance={distance}")
|
| 387 |
+
|
| 388 |
+
# LOWERED threshold from 0.3 to 0.1 to allow more chunks through
|
| 389 |
+
if relevance > 0.1:
|
| 390 |
+
context_parts.append(
|
| 391 |
+
f"[Document {i}: {filename} (relevance: {relevance:.3f})]\n{text[:500]}..." # Limit text length
|
| 392 |
+
)
|
| 393 |
+
logger.info(f"Including chunk {i} for {persona_id} (relevance: {relevance:.3f})")
|
| 394 |
+
else:
|
| 395 |
+
logger.info(f"Excluding chunk {i} for {persona_id} (relevance too low: {relevance:.3f})")
|
| 396 |
+
|
| 397 |
+
if context_parts:
|
| 398 |
+
retrieved_context = "\n\n".join(context_parts)
|
| 399 |
+
logger.info(f"Using {len(context_parts)} relevant chunks for {persona_id}")
|
| 400 |
+
return f"RELEVANT DOCUMENT CONTEXT:\n{retrieved_context}\n\n"
|
| 401 |
+
|
| 402 |
+
logger.info(f"No chunks met relevance threshold for {persona_id}")
|
| 403 |
+
return ""
|
| 404 |
+
|
| 405 |
+
except Exception as e:
|
| 406 |
+
logger.error(f"Error retrieving documents for {persona_id}: {str(e)}")
|
| 407 |
+
return ""
|
multi_llm_chatbot_backend/app/core/rag_manager.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chromadb
|
| 2 |
+
from chromadb.config import Settings
|
| 3 |
+
from sentence_transformers import SentenceTransformer
|
| 4 |
+
import nltk
|
| 5 |
+
import tiktoken
|
| 6 |
+
from typing import List, Dict, Any, Optional
|
| 7 |
+
import uuid
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Download required NLTK data
|
| 16 |
+
try:
|
| 17 |
+
nltk.data.find('tokenizers/punkt')
|
| 18 |
+
except LookupError:
|
| 19 |
+
try:
|
| 20 |
+
nltk.download('punkt')
|
| 21 |
+
except Exception as e:
|
| 22 |
+
logger.warning(f"Could not download NLTK punkt: {e}")
|
| 23 |
+
|
| 24 |
+
class DocumentChunker:
|
| 25 |
+
"""Intelligent document chunking with overlaps and semantic boundaries"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, chunk_size: int = 500, overlap: int = 50):
|
| 28 |
+
self.chunk_size = chunk_size
|
| 29 |
+
self.overlap = overlap
|
| 30 |
+
try:
|
| 31 |
+
self.encoding = tiktoken.get_encoding("cl100k_base") # GPT-4 encoding
|
| 32 |
+
except Exception as e:
|
| 33 |
+
logger.warning(f"Could not load tiktoken encoding: {e}")
|
| 34 |
+
self.encoding = None
|
| 35 |
+
|
| 36 |
+
def _count_tokens(self, text: str) -> int:
|
| 37 |
+
"""Count tokens in text"""
|
| 38 |
+
if self.encoding:
|
| 39 |
+
return len(self.encoding.encode(text))
|
| 40 |
+
else:
|
| 41 |
+
# Fallback: approximate 4 chars per token
|
| 42 |
+
return len(text) // 4
|
| 43 |
+
|
| 44 |
+
def _encode_text(self, text: str) -> List[int]:
|
| 45 |
+
"""Encode text to tokens"""
|
| 46 |
+
if self.encoding:
|
| 47 |
+
return self.encoding.encode(text)
|
| 48 |
+
else:
|
| 49 |
+
# Fallback: return character indices
|
| 50 |
+
return list(range(len(text)))
|
| 51 |
+
|
| 52 |
+
def _decode_tokens(self, tokens: List[int]) -> str:
|
| 53 |
+
"""Decode tokens back to text"""
|
| 54 |
+
if self.encoding:
|
| 55 |
+
return self.encoding.decode(tokens)
|
| 56 |
+
else:
|
| 57 |
+
# Fallback: can't properly decode without tiktoken
|
| 58 |
+
return ""
|
| 59 |
+
|
| 60 |
+
def chunk_text(self, text: str, metadata: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 61 |
+
"""
|
| 62 |
+
Chunk text intelligently with semantic boundaries
|
| 63 |
+
"""
|
| 64 |
+
# Clean and normalize text
|
| 65 |
+
text = self._clean_text(text)
|
| 66 |
+
|
| 67 |
+
# Simple sentence splitting fallback if NLTK isn't available
|
| 68 |
+
try:
|
| 69 |
+
sentences = nltk.sent_tokenize(text)
|
| 70 |
+
except Exception:
|
| 71 |
+
# Fallback sentence splitting
|
| 72 |
+
sentences = re.split(r'[.!?]+', text)
|
| 73 |
+
sentences = [s.strip() for s in sentences if s.strip()]
|
| 74 |
+
|
| 75 |
+
chunks = []
|
| 76 |
+
current_chunk = ""
|
| 77 |
+
current_tokens = 0
|
| 78 |
+
chunk_index = 0
|
| 79 |
+
|
| 80 |
+
for sentence in sentences:
|
| 81 |
+
sentence = sentence.strip()
|
| 82 |
+
if not sentence:
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
+
sentence_tokens = self._count_tokens(sentence)
|
| 86 |
+
|
| 87 |
+
# If adding this sentence would exceed chunk size, save current chunk
|
| 88 |
+
if current_tokens + sentence_tokens > self.chunk_size and current_chunk:
|
| 89 |
+
chunk_data = self._create_chunk_metadata(
|
| 90 |
+
current_chunk.strip(),
|
| 91 |
+
metadata,
|
| 92 |
+
chunk_index
|
| 93 |
+
)
|
| 94 |
+
chunks.append(chunk_data)
|
| 95 |
+
|
| 96 |
+
# Start new chunk with overlap
|
| 97 |
+
overlap_text = self._get_overlap_text(current_chunk)
|
| 98 |
+
current_chunk = overlap_text + " " + sentence
|
| 99 |
+
current_tokens = self._count_tokens(current_chunk)
|
| 100 |
+
chunk_index += 1
|
| 101 |
+
else:
|
| 102 |
+
current_chunk += " " + sentence
|
| 103 |
+
current_tokens += sentence_tokens
|
| 104 |
+
|
| 105 |
+
# Add final chunk if it has content
|
| 106 |
+
if current_chunk.strip():
|
| 107 |
+
chunk_data = self._create_chunk_metadata(
|
| 108 |
+
current_chunk.strip(),
|
| 109 |
+
metadata,
|
| 110 |
+
chunk_index
|
| 111 |
+
)
|
| 112 |
+
chunks.append(chunk_data)
|
| 113 |
+
|
| 114 |
+
logger.info(f"Created {len(chunks)} chunks from document: {metadata.get('filename', 'unknown')}")
|
| 115 |
+
return chunks
|
| 116 |
+
|
| 117 |
+
def _clean_text(self, text: str) -> str:
|
| 118 |
+
"""Clean and normalize text"""
|
| 119 |
+
# Remove excessive whitespace and newlines
|
| 120 |
+
text = re.sub(r'\n+', '\n', text)
|
| 121 |
+
text = re.sub(r'\s+', ' ', text)
|
| 122 |
+
return text.strip()
|
| 123 |
+
|
| 124 |
+
def _get_overlap_text(self, text: str) -> str:
|
| 125 |
+
"""Get the last N tokens for overlap"""
|
| 126 |
+
if self.encoding:
|
| 127 |
+
tokens = self._encode_text(text)
|
| 128 |
+
if len(tokens) <= self.overlap:
|
| 129 |
+
return text
|
| 130 |
+
|
| 131 |
+
overlap_tokens = tokens[-self.overlap:]
|
| 132 |
+
return self._decode_tokens(overlap_tokens)
|
| 133 |
+
else:
|
| 134 |
+
# Fallback: use character-based overlap
|
| 135 |
+
words = text.split()
|
| 136 |
+
overlap_words = words[-self.overlap:] if len(words) > self.overlap else words
|
| 137 |
+
return " ".join(overlap_words)
|
| 138 |
+
|
| 139 |
+
def _create_chunk_metadata(self, chunk_text: str, base_metadata: Dict[str, Any], chunk_index: int) -> Dict[str, Any]:
|
| 140 |
+
"""Create comprehensive metadata for a chunk"""
|
| 141 |
+
return {
|
| 142 |
+
"text": chunk_text,
|
| 143 |
+
"chunk_id": str(uuid.uuid4()),
|
| 144 |
+
"chunk_index": chunk_index,
|
| 145 |
+
"token_count": self._count_tokens(chunk_text),
|
| 146 |
+
"filename": base_metadata.get("filename", "unknown"),
|
| 147 |
+
"file_type": base_metadata.get("file_type", "unknown"),
|
| 148 |
+
"session_id": base_metadata.get("session_id"),
|
| 149 |
+
"upload_timestamp": base_metadata.get("upload_timestamp"),
|
| 150 |
+
"file_size": base_metadata.get("file_size", 0)
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
class SimpleEmbeddingFunction:
|
| 154 |
+
"""Simple embedding function wrapper for ChromaDB - FIXED for new interface"""
|
| 155 |
+
|
| 156 |
+
def __init__(self, model):
|
| 157 |
+
self.model = model
|
| 158 |
+
|
| 159 |
+
def __call__(self, input):
|
| 160 |
+
"""Generate embeddings for input texts - Updated signature for ChromaDB 0.4.16+"""
|
| 161 |
+
try:
|
| 162 |
+
# Handle both single string and list of strings
|
| 163 |
+
if isinstance(input, str):
|
| 164 |
+
input = [input]
|
| 165 |
+
|
| 166 |
+
embeddings = self.model.encode(input, convert_to_tensor=False)
|
| 167 |
+
return embeddings.tolist() if hasattr(embeddings, 'tolist') else embeddings
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.error(f"Error generating embeddings: {e}")
|
| 170 |
+
# Return dummy embeddings as fallback
|
| 171 |
+
return [[0.0] * 384 for _ in input] # 384 is dimension for all-MiniLM-L6-v2
|
| 172 |
+
|
| 173 |
+
class RAGManager:
|
| 174 |
+
"""
|
| 175 |
+
Retrieval-Augmented Generation Manager - FIXED VERSION
|
| 176 |
+
|
| 177 |
+
Handles document storage, embedding, and retrieval using ChromaDB
|
| 178 |
+
"""
|
| 179 |
+
|
| 180 |
+
def __init__(self, embedding_model: str = "all-MiniLM-L6-v2", persist_directory: str = "./chroma_db"):
|
| 181 |
+
self.embedding_model_name = embedding_model
|
| 182 |
+
self.persist_directory = Path(persist_directory)
|
| 183 |
+
self.persist_directory.mkdir(exist_ok=True)
|
| 184 |
+
|
| 185 |
+
# Initialize embedding model
|
| 186 |
+
logger.info(f"Loading embedding model: {embedding_model}")
|
| 187 |
+
try:
|
| 188 |
+
self.embedding_model = SentenceTransformer(embedding_model)
|
| 189 |
+
logger.info("Embedding model loaded successfully")
|
| 190 |
+
except Exception as e:
|
| 191 |
+
logger.error(f"Failed to load embedding model: {e}")
|
| 192 |
+
raise
|
| 193 |
+
|
| 194 |
+
# Initialize ChromaDB client
|
| 195 |
+
try:
|
| 196 |
+
self.client = chromadb.PersistentClient(
|
| 197 |
+
path=str(self.persist_directory),
|
| 198 |
+
settings=Settings(
|
| 199 |
+
anonymized_telemetry=False,
|
| 200 |
+
allow_reset=True
|
| 201 |
+
)
|
| 202 |
+
)
|
| 203 |
+
logger.info("ChromaDB client initialized")
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"Failed to initialize ChromaDB client: {e}")
|
| 206 |
+
raise
|
| 207 |
+
|
| 208 |
+
# Initialize collection
|
| 209 |
+
self.collection_name = "phd_advisor_documents"
|
| 210 |
+
self.collection = self._get_or_create_collection()
|
| 211 |
+
|
| 212 |
+
# Initialize chunker
|
| 213 |
+
self.chunker = DocumentChunker()
|
| 214 |
+
|
| 215 |
+
def _get_or_create_collection(self):
|
| 216 |
+
"""Get or create the ChromaDB collection"""
|
| 217 |
+
try:
|
| 218 |
+
# First, try to get existing collection
|
| 219 |
+
try:
|
| 220 |
+
collection = self.client.get_collection(
|
| 221 |
+
name=self.collection_name
|
| 222 |
+
)
|
| 223 |
+
logger.info(f"Found existing collection: {self.collection_name}")
|
| 224 |
+
return collection
|
| 225 |
+
except ValueError:
|
| 226 |
+
# Collection doesn't exist, create it
|
| 227 |
+
logger.info(f"Creating new collection: {self.collection_name}")
|
| 228 |
+
collection = self.client.create_collection(
|
| 229 |
+
name=self.collection_name,
|
| 230 |
+
embedding_function=SimpleEmbeddingFunction(self.embedding_model),
|
| 231 |
+
metadata={"description": "PhD Advisor document storage"}
|
| 232 |
+
)
|
| 233 |
+
logger.info(f"Created collection: {self.collection_name}")
|
| 234 |
+
return collection
|
| 235 |
+
|
| 236 |
+
except Exception as e:
|
| 237 |
+
logger.error(f"Error with collection management: {e}")
|
| 238 |
+
# Try to reset and recreate
|
| 239 |
+
try:
|
| 240 |
+
logger.info("Attempting to reset and recreate collection...")
|
| 241 |
+
self.client.reset()
|
| 242 |
+
collection = self.client.create_collection(
|
| 243 |
+
name=self.collection_name,
|
| 244 |
+
embedding_function=SimpleEmbeddingFunction(self.embedding_model),
|
| 245 |
+
metadata={"description": "PhD Advisor document storage"}
|
| 246 |
+
)
|
| 247 |
+
logger.info("Successfully recreated collection")
|
| 248 |
+
return collection
|
| 249 |
+
except Exception as e2:
|
| 250 |
+
logger.error(f"Failed to recreate collection: {e2}")
|
| 251 |
+
raise
|
| 252 |
+
|
| 253 |
+
def add_document(self, content: str, filename: str, session_id: str, file_type: str = "unknown") -> Dict[str, Any]:
|
| 254 |
+
"""
|
| 255 |
+
Add a document to the vector database
|
| 256 |
+
"""
|
| 257 |
+
try:
|
| 258 |
+
# Create base metadata
|
| 259 |
+
base_metadata = {
|
| 260 |
+
"filename": filename,
|
| 261 |
+
"file_type": file_type,
|
| 262 |
+
"session_id": session_id,
|
| 263 |
+
"upload_timestamp": str(uuid.uuid4()), # Using UUID as timestamp for simplicity
|
| 264 |
+
"file_size": len(content)
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
# Chunk the document
|
| 268 |
+
chunks = self.chunker.chunk_text(content, base_metadata)
|
| 269 |
+
|
| 270 |
+
if not chunks:
|
| 271 |
+
raise ValueError("No chunks created from document")
|
| 272 |
+
|
| 273 |
+
# Prepare data for ChromaDB
|
| 274 |
+
chunk_ids = [chunk["chunk_id"] for chunk in chunks]
|
| 275 |
+
chunk_texts = [chunk["text"] for chunk in chunks]
|
| 276 |
+
chunk_metadatas = [
|
| 277 |
+
{k: v for k, v in chunk.items() if k != "text" and k != "chunk_id"}
|
| 278 |
+
for chunk in chunks
|
| 279 |
+
]
|
| 280 |
+
|
| 281 |
+
# Add to ChromaDB with error handling
|
| 282 |
+
try:
|
| 283 |
+
self.collection.add(
|
| 284 |
+
ids=chunk_ids,
|
| 285 |
+
documents=chunk_texts,
|
| 286 |
+
metadatas=chunk_metadatas
|
| 287 |
+
)
|
| 288 |
+
logger.info(f"Successfully added {len(chunks)} chunks for document: {filename}")
|
| 289 |
+
except Exception as e:
|
| 290 |
+
logger.error(f"Error adding chunks to ChromaDB: {e}")
|
| 291 |
+
# Try to recreate collection and try again
|
| 292 |
+
self.collection = self._get_or_create_collection()
|
| 293 |
+
self.collection.add(
|
| 294 |
+
ids=chunk_ids,
|
| 295 |
+
documents=chunk_texts,
|
| 296 |
+
metadatas=chunk_metadatas
|
| 297 |
+
)
|
| 298 |
+
logger.info(f"Successfully added {len(chunks)} chunks after collection reset")
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
"success": True,
|
| 302 |
+
"filename": filename,
|
| 303 |
+
"chunks_created": len(chunks),
|
| 304 |
+
"total_tokens": sum(chunk["token_count"] for chunk in chunks),
|
| 305 |
+
"chunk_ids": chunk_ids
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
except Exception as e:
|
| 309 |
+
logger.error(f"Error adding document {filename}: {str(e)}")
|
| 310 |
+
return {
|
| 311 |
+
"success": False,
|
| 312 |
+
"filename": filename,
|
| 313 |
+
"error": str(e)
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
def search_documents(self, query: str, session_id: str, persona_context: str = "", n_results: int = 5) -> List[Dict[str, Any]]:
|
| 317 |
+
"""
|
| 318 |
+
Search for relevant document chunks
|
| 319 |
+
"""
|
| 320 |
+
try:
|
| 321 |
+
# Enhance query with persona context for better retrieval
|
| 322 |
+
enhanced_query = f"{query} {persona_context}".strip()
|
| 323 |
+
|
| 324 |
+
# Search the collection
|
| 325 |
+
results = self.collection.query(
|
| 326 |
+
query_texts=[enhanced_query],
|
| 327 |
+
n_results=n_results,
|
| 328 |
+
where={"session_id": session_id} # Filter by session
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
# Format results
|
| 332 |
+
retrieved_chunks = []
|
| 333 |
+
if results['documents'] and results['documents'][0]:
|
| 334 |
+
for i, (doc, metadata, distance) in enumerate(zip(
|
| 335 |
+
results['documents'][0],
|
| 336 |
+
results['metadatas'][0],
|
| 337 |
+
results['distances'][0]
|
| 338 |
+
)):
|
| 339 |
+
# ChromaDB returns squared euclidean distance, convert to similarity
|
| 340 |
+
# For better similarity scores, we'll use: similarity = 1 / (1 + distance)
|
| 341 |
+
similarity_score = 1 / (1 + abs(distance)) if distance is not None else 0.5
|
| 342 |
+
|
| 343 |
+
chunk_data = {
|
| 344 |
+
"text": doc,
|
| 345 |
+
"metadata": metadata,
|
| 346 |
+
"relevance_score": similarity_score,
|
| 347 |
+
"distance": distance, # Keep original for debugging
|
| 348 |
+
"rank": i + 1
|
| 349 |
+
}
|
| 350 |
+
retrieved_chunks.append(chunk_data)
|
| 351 |
+
|
| 352 |
+
logger.info(f"Retrieved {len(retrieved_chunks)} chunks for query: {query[:50]}...")
|
| 353 |
+
return retrieved_chunks
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
logger.error(f"Error searching documents: {str(e)}")
|
| 357 |
+
return []
|
| 358 |
+
|
| 359 |
+
def get_document_stats(self, session_id: str) -> Dict[str, Any]:
|
| 360 |
+
"""Get statistics about documents in a session"""
|
| 361 |
+
try:
|
| 362 |
+
# Get all chunks for this session
|
| 363 |
+
results = self.collection.get(
|
| 364 |
+
where={"session_id": session_id}
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
if not results or not results.get('metadatas'):
|
| 368 |
+
return {
|
| 369 |
+
"total_chunks": 0,
|
| 370 |
+
"total_documents": 0,
|
| 371 |
+
"documents": []
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
# Analyze metadata
|
| 375 |
+
metadatas = results['metadatas']
|
| 376 |
+
documents = {}
|
| 377 |
+
|
| 378 |
+
for metadata in metadatas:
|
| 379 |
+
filename = metadata.get('filename', 'unknown')
|
| 380 |
+
if filename not in documents:
|
| 381 |
+
documents[filename] = {
|
| 382 |
+
"filename": filename,
|
| 383 |
+
"file_type": metadata.get('file_type', 'unknown'),
|
| 384 |
+
"chunk_count": 0,
|
| 385 |
+
"total_tokens": 0
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
documents[filename]["chunk_count"] += 1
|
| 389 |
+
documents[filename]["total_tokens"] += metadata.get('token_count', 0)
|
| 390 |
+
|
| 391 |
+
return {
|
| 392 |
+
"total_chunks": len(metadatas),
|
| 393 |
+
"total_documents": len(documents),
|
| 394 |
+
"documents": list(documents.values())
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
except Exception as e:
|
| 398 |
+
logger.error(f"Error getting document stats: {str(e)}")
|
| 399 |
+
return {"total_chunks": 0, "total_documents": 0, "documents": []}
|
| 400 |
+
|
| 401 |
+
def delete_session_documents(self, session_id: str) -> bool:
|
| 402 |
+
"""Delete all documents for a session"""
|
| 403 |
+
try:
|
| 404 |
+
# Get all document IDs for this session
|
| 405 |
+
results = self.collection.get(
|
| 406 |
+
where={"session_id": session_id}
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
if results and results.get('ids'):
|
| 410 |
+
chunk_ids = results['ids']
|
| 411 |
+
self.collection.delete(ids=chunk_ids)
|
| 412 |
+
logger.info(f"Deleted {len(chunk_ids)} chunks for session: {session_id}")
|
| 413 |
+
return True
|
| 414 |
+
|
| 415 |
+
return True # No documents to delete is also success
|
| 416 |
+
|
| 417 |
+
except Exception as e:
|
| 418 |
+
logger.error(f"Error deleting session documents: {str(e)}")
|
| 419 |
+
return False
|
| 420 |
+
|
| 421 |
+
def health_check(self) -> Dict[str, Any]:
|
| 422 |
+
"""Check if RAG system is working properly"""
|
| 423 |
+
try:
|
| 424 |
+
# Test basic operations
|
| 425 |
+
test_doc = "This is a test document for health checking."
|
| 426 |
+
test_session = "health_check_session"
|
| 427 |
+
|
| 428 |
+
# Try adding a test document
|
| 429 |
+
result = self.add_document(test_doc, "test.txt", test_session, "txt")
|
| 430 |
+
if not result["success"]:
|
| 431 |
+
return {"status": "error", "message": "Failed to add test document"}
|
| 432 |
+
|
| 433 |
+
# Try searching
|
| 434 |
+
search_results = self.search_documents("test", test_session)
|
| 435 |
+
|
| 436 |
+
# Try getting stats
|
| 437 |
+
stats = self.get_document_stats(test_session)
|
| 438 |
+
|
| 439 |
+
# Cleanup
|
| 440 |
+
self.delete_session_documents(test_session)
|
| 441 |
+
|
| 442 |
+
return {
|
| 443 |
+
"status": "healthy",
|
| 444 |
+
"embedding_model": self.embedding_model_name,
|
| 445 |
+
"collection_name": self.collection_name,
|
| 446 |
+
"test_results": {
|
| 447 |
+
"add_document": result["success"],
|
| 448 |
+
"search_documents": len(search_results) > 0,
|
| 449 |
+
"get_stats": stats["total_chunks"] > 0
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
except Exception as e:
|
| 454 |
+
return {"status": "error", "message": str(e)}
|
| 455 |
+
|
| 456 |
+
# Global RAG manager instance
|
| 457 |
+
_rag_manager = None
|
| 458 |
+
|
| 459 |
+
def get_rag_manager() -> RAGManager:
|
| 460 |
+
"""Get the global RAG manager instance"""
|
| 461 |
+
global _rag_manager
|
| 462 |
+
if _rag_manager is None:
|
| 463 |
+
try:
|
| 464 |
+
_rag_manager = RAGManager()
|
| 465 |
+
logger.info("RAG Manager initialized successfully")
|
| 466 |
+
except Exception as e:
|
| 467 |
+
logger.error(f"Failed to initialize RAG Manager: {e}")
|
| 468 |
+
raise
|
| 469 |
+
return _rag_manager
|
multi_llm_chatbot_backend/app/core/session_manager.py
CHANGED
|
@@ -4,49 +4,106 @@ import uuid
|
|
| 4 |
from dataclasses import dataclass, field
|
| 5 |
import asyncio
|
| 6 |
from threading import Lock
|
|
|
|
| 7 |
|
| 8 |
@dataclass
|
| 9 |
class ConversationContext:
|
| 10 |
-
"""
|
| 11 |
-
session_id: str
|
| 12 |
-
messages: List[dict] = field(default_factory=list)
|
| 13 |
-
uploaded_files: List[str] = field(default_factory=list)
|
| 14 |
-
total_upload_size: int = 0
|
| 15 |
-
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 16 |
-
created_at: datetime = field(default_factory=datetime.now)
|
| 17 |
-
last_accessed: datetime = field(default_factory=datetime.now)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
def append_message(self, role: str, content: str):
|
| 20 |
-
"""Add a message to the conversation"""
|
| 21 |
self.messages.append({
|
| 22 |
"role": role,
|
| 23 |
"content": content,
|
| 24 |
"timestamp": datetime.now().isoformat()
|
| 25 |
})
|
| 26 |
self.last_accessed = datetime.now()
|
| 27 |
-
|
| 28 |
-
def get_recent_messages(self, limit: int = 20) -> List[dict]:
|
| 29 |
-
"""Get recent messages with limit"""
|
| 30 |
-
return self.messages[-limit:] if len(self.messages) > limit else self.messages
|
| 31 |
-
|
| 32 |
-
def get_messages_by_role(self, role: str) -> List[dict]:
|
| 33 |
-
"""Get all messages from a specific role"""
|
| 34 |
-
return [msg for msg in self.messages if msg['role'] == role]
|
| 35 |
-
|
| 36 |
def clear_messages(self):
|
| 37 |
-
"""Clear
|
| 38 |
-
self.messages
|
| 39 |
self.last_accessed = datetime.now()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def add_uploaded_file(self, filename: str, content: str, file_size: int):
|
| 42 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
self.uploaded_files.append(filename)
|
| 44 |
self.total_upload_size += file_size
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def get_context_size(self) -> int:
|
| 48 |
-
"""Calculate
|
| 49 |
return sum(len(msg['content']) for msg in self.messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
class SessionManager:
|
| 52 |
"""Thread-safe session manager for handling multiple user conversations"""
|
|
@@ -115,6 +172,42 @@ class SessionManager:
|
|
| 115 |
|
| 116 |
if expired_sessions:
|
| 117 |
print(f"Cleaned up {len(expired_sessions)} expired sessions")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
# Global session manager instance
|
| 120 |
session_manager = SessionManager()
|
|
|
|
| 4 |
from dataclasses import dataclass, field
|
| 5 |
import asyncio
|
| 6 |
from threading import Lock
|
| 7 |
+
from app.core.rag_manager import get_rag_manager
|
| 8 |
|
| 9 |
@dataclass
|
| 10 |
class ConversationContext:
|
| 11 |
+
"""Enhanced conversation context for RAG integration"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
def __init__(self, session_id: str = None):
|
| 14 |
+
self.session_id = session_id or str(uuid.uuid4())
|
| 15 |
+
self.messages: List[Dict[str, str]] = []
|
| 16 |
+
self.uploaded_files: List[str] = [] # Now just stores filenames, not content
|
| 17 |
+
self.total_upload_size: int = 0 # For tracking purposes only
|
| 18 |
+
self.created_at = datetime.now()
|
| 19 |
+
self.last_accessed = datetime.now()
|
| 20 |
+
|
| 21 |
+
# New RAG-related attributes
|
| 22 |
+
self.document_chunks_count: int = 0 # Track total chunks in vector DB
|
| 23 |
+
self.last_retrieval_stats: Dict[str, Any] = {} # Last RAG retrieval info
|
| 24 |
+
|
| 25 |
def append_message(self, role: str, content: str):
|
| 26 |
+
"""Add a message to the conversation history"""
|
| 27 |
self.messages.append({
|
| 28 |
"role": role,
|
| 29 |
"content": content,
|
| 30 |
"timestamp": datetime.now().isoformat()
|
| 31 |
})
|
| 32 |
self.last_accessed = datetime.now()
|
| 33 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def clear_messages(self):
|
| 35 |
+
"""Clear conversation messages but keep document references"""
|
| 36 |
+
self.messages.clear()
|
| 37 |
self.last_accessed = datetime.now()
|
| 38 |
+
|
| 39 |
+
def get_messages_by_role(self, role: str) -> List[Dict[str, str]]:
|
| 40 |
+
"""Get all messages by a specific role - ADDED METHOD"""
|
| 41 |
+
return [msg for msg in self.messages if msg.get('role') == role]
|
| 42 |
|
| 43 |
+
def get_recent_messages(self, count: int = 10) -> List[Dict[str, str]]:
|
| 44 |
+
"""Get the most recent N messages"""
|
| 45 |
+
return self.messages[-count:] if len(self.messages) > count else self.messages
|
| 46 |
+
|
| 47 |
+
def get_user_messages(self) -> List[Dict[str, str]]:
|
| 48 |
+
"""Get all user messages"""
|
| 49 |
+
return self.get_messages_by_role('user')
|
| 50 |
+
|
| 51 |
+
def get_latest_user_message(self) -> Optional[str]:
|
| 52 |
+
"""Get the content of the most recent user message"""
|
| 53 |
+
user_messages = self.get_user_messages()
|
| 54 |
+
return user_messages[-1]['content'] if user_messages else None
|
| 55 |
+
|
| 56 |
def add_uploaded_file(self, filename: str, content: str, file_size: int):
|
| 57 |
+
"""
|
| 58 |
+
Add uploaded file - MODIFIED FOR RAG
|
| 59 |
+
|
| 60 |
+
Now we only track the filename and size in session context.
|
| 61 |
+
The actual content goes to the vector database via RAG manager.
|
| 62 |
+
"""
|
| 63 |
self.uploaded_files.append(filename)
|
| 64 |
self.total_upload_size += file_size
|
| 65 |
+
|
| 66 |
+
# Add a system message noting the upload (not the full content)
|
| 67 |
+
self.append_message("system", f"Document '{filename}' uploaded and processed into vector database")
|
| 68 |
+
|
| 69 |
+
# Update document chunk count from RAG manager
|
| 70 |
+
try:
|
| 71 |
+
rag_manager = get_rag_manager()
|
| 72 |
+
stats = rag_manager.get_document_stats(self.session_id)
|
| 73 |
+
self.document_chunks_count = stats.get("total_chunks", 0)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"Warning: Could not update chunk count: {e}")
|
| 76 |
+
|
| 77 |
def get_context_size(self) -> int:
|
| 78 |
+
"""Calculate conversation context size in characters (excluding vector DB documents)"""
|
| 79 |
return sum(len(msg['content']) for msg in self.messages)
|
| 80 |
+
|
| 81 |
+
def get_rag_stats(self) -> Dict[str, Any]:
|
| 82 |
+
"""Get statistics about documents in vector database for this session"""
|
| 83 |
+
try:
|
| 84 |
+
rag_manager = get_rag_manager()
|
| 85 |
+
return rag_manager.get_document_stats(self.session_id)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
return {"error": str(e), "total_chunks": 0, "total_documents": 0}
|
| 88 |
+
|
| 89 |
+
def clear_all_data(self):
|
| 90 |
+
"""Clear both conversation and vector database documents"""
|
| 91 |
+
# Clear conversation messages
|
| 92 |
+
self.clear_messages()
|
| 93 |
+
|
| 94 |
+
# Clear vector database documents
|
| 95 |
+
try:
|
| 96 |
+
rag_manager = get_rag_manager()
|
| 97 |
+
success = rag_manager.delete_session_documents(self.session_id)
|
| 98 |
+
if success:
|
| 99 |
+
self.uploaded_files.clear()
|
| 100 |
+
self.total_upload_size = 0
|
| 101 |
+
self.document_chunks_count = 0
|
| 102 |
+
self.append_message("system", "All conversation history and documents cleared")
|
| 103 |
+
else:
|
| 104 |
+
self.append_message("system", "Conversation cleared, but some documents may remain")
|
| 105 |
+
except Exception as e:
|
| 106 |
+
self.append_message("system", f"Conversation cleared, document cleanup failed: {str(e)}")
|
| 107 |
|
| 108 |
class SessionManager:
|
| 109 |
"""Thread-safe session manager for handling multiple user conversations"""
|
|
|
|
| 172 |
|
| 173 |
if expired_sessions:
|
| 174 |
print(f"Cleaned up {len(expired_sessions)} expired sessions")
|
| 175 |
+
|
| 176 |
+
def reset_session_completely(self, session_id: str) -> bool:
|
| 177 |
+
"""
|
| 178 |
+
Completely reset a session - both conversation and vector database documents
|
| 179 |
+
"""
|
| 180 |
+
with self.lock:
|
| 181 |
+
if session_id in self.sessions:
|
| 182 |
+
session = self.sessions[session_id]
|
| 183 |
+
session.clear_all_data()
|
| 184 |
+
return True
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
def get_session_stats(self, session_id: str) -> Dict[str, Any]:
|
| 188 |
+
"""Get comprehensive session statistics including RAG data"""
|
| 189 |
+
with self.lock:
|
| 190 |
+
if session_id not in self.sessions:
|
| 191 |
+
return {"error": "Session not found"}
|
| 192 |
+
|
| 193 |
+
session = self.sessions[session_id]
|
| 194 |
+
|
| 195 |
+
# Get basic session stats
|
| 196 |
+
basic_stats = {
|
| 197 |
+
"session_id": session_id,
|
| 198 |
+
"message_count": len(session.messages),
|
| 199 |
+
"uploaded_files": session.uploaded_files,
|
| 200 |
+
"total_upload_size": session.total_upload_size,
|
| 201 |
+
"context_size_chars": session.get_context_size(),
|
| 202 |
+
"created_at": session.created_at.isoformat(),
|
| 203 |
+
"last_accessed": session.last_accessed.isoformat()
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
# Get RAG stats
|
| 207 |
+
rag_stats = session.get_rag_stats()
|
| 208 |
+
|
| 209 |
+
# Combine stats
|
| 210 |
+
return {**basic_stats, "rag_stats": rag_stats}
|
| 211 |
|
| 212 |
# Global session manager instance
|
| 213 |
session_manager = SessionManager()
|
multi_llm_chatbot_backend/app/tests/debug_rag.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Debug RAG Chat Integration
|
| 4 |
+
|
| 5 |
+
This script helps debug why RAG isn't working in chat responses.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
BASE_URL = "http://localhost:8000"
|
| 13 |
+
|
| 14 |
+
def test_direct_search():
|
| 15 |
+
"""Test direct document search with detailed output"""
|
| 16 |
+
print("🔍 Testing direct document search...")
|
| 17 |
+
|
| 18 |
+
test_queries = [
|
| 19 |
+
{"query": "research methodology approach", "persona": "methodist"},
|
| 20 |
+
{"query": "theoretical framework theory", "persona": "theorist"},
|
| 21 |
+
{"query": "practical steps implementation", "persona": "pragmatist"}
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
for test in test_queries:
|
| 25 |
+
print(f"\nTesting: '{test['query']}' for {test['persona']}")
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
response = requests.post(
|
| 29 |
+
f"{BASE_URL}/search-documents",
|
| 30 |
+
json=test,
|
| 31 |
+
timeout=10
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
if response.status_code == 200:
|
| 35 |
+
result = response.json()
|
| 36 |
+
print(f"✅ Found {result['results_count']} results")
|
| 37 |
+
|
| 38 |
+
for i, chunk in enumerate(result['results'], 1):
|
| 39 |
+
score = chunk['relevance_score']
|
| 40 |
+
distance = chunk.get('distance', 'unknown')
|
| 41 |
+
text_preview = chunk['text'][:100]
|
| 42 |
+
|
| 43 |
+
print(f" Result {i}:")
|
| 44 |
+
print(f" Relevance: {score:.4f}")
|
| 45 |
+
print(f" Distance: {distance}")
|
| 46 |
+
print(f" Text: {text_preview}...")
|
| 47 |
+
print(f" Meets 0.1 threshold: {'✅' if score > 0.1 else '❌'}")
|
| 48 |
+
else:
|
| 49 |
+
print(f"❌ Search failed: {response.status_code}")
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"❌ Search error: {e}")
|
| 53 |
+
|
| 54 |
+
def test_single_persona_chat():
|
| 55 |
+
"""Test individual persona chat to isolate issues"""
|
| 56 |
+
print("\n💬 Testing individual persona chat...")
|
| 57 |
+
|
| 58 |
+
personas = ["methodist", "theorist", "pragmatist"]
|
| 59 |
+
query = "What research methodology should I use based on the uploaded document?"
|
| 60 |
+
|
| 61 |
+
for persona in personas:
|
| 62 |
+
print(f"\nTesting {persona}...")
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
# Note: This endpoint might not exist yet, but we can try
|
| 66 |
+
response = requests.post(
|
| 67 |
+
f"{BASE_URL}/chat/{persona}",
|
| 68 |
+
json={"user_input": query, "response_length": "short"},
|
| 69 |
+
timeout=15
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if response.status_code == 200:
|
| 73 |
+
result = response.json()
|
| 74 |
+
print(f"✅ {persona} response received")
|
| 75 |
+
|
| 76 |
+
if 'rag_info' in result:
|
| 77 |
+
rag_info = result['rag_info']
|
| 78 |
+
print(f" Used documents: {rag_info.get('used_documents', False)}")
|
| 79 |
+
print(f" Chunks used: {rag_info.get('document_chunks_used', 0)}")
|
| 80 |
+
|
| 81 |
+
if 'persona' in result:
|
| 82 |
+
response_text = result['persona'].get('response', '')[:150]
|
| 83 |
+
print(f" Response: {response_text}...")
|
| 84 |
+
else:
|
| 85 |
+
print(f"❌ {persona} chat failed: {response.status_code}")
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"❌ {persona} chat error: {e}")
|
| 89 |
+
|
| 90 |
+
def test_sequential_chat_debug():
|
| 91 |
+
"""Test sequential chat with more detailed debugging"""
|
| 92 |
+
print("\n🔄 Testing sequential chat with debug info...")
|
| 93 |
+
|
| 94 |
+
query = "Based on the methodology document I uploaded, what approach should I take?"
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
response = requests.post(
|
| 98 |
+
f"{BASE_URL}/chat-sequential",
|
| 99 |
+
json={"user_input": query, "response_length": "short"},
|
| 100 |
+
timeout=30
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
if response.status_code == 200:
|
| 104 |
+
result = response.json()
|
| 105 |
+
print(f"✅ Sequential chat response received")
|
| 106 |
+
print(f" Response type: {result.get('type')}")
|
| 107 |
+
|
| 108 |
+
if 'rag_info' in result:
|
| 109 |
+
rag_info = result['rag_info']
|
| 110 |
+
print(f" RAG enabled: {rag_info.get('rag_enabled', False)}")
|
| 111 |
+
print(f" Personas using documents: {rag_info.get('personas_using_documents', 0)}")
|
| 112 |
+
print(f" Total chunks used: {rag_info.get('total_document_chunks_used', 0)}")
|
| 113 |
+
|
| 114 |
+
if 'responses' in result:
|
| 115 |
+
print(f"\n Individual persona results:")
|
| 116 |
+
for resp in result['responses']:
|
| 117 |
+
persona_name = resp.get('persona_name', 'Unknown')
|
| 118 |
+
used_docs = resp.get('used_documents', False)
|
| 119 |
+
chunks_used = resp.get('document_chunks_used', 0)
|
| 120 |
+
error = resp.get('error', False)
|
| 121 |
+
|
| 122 |
+
print(f" {persona_name}:")
|
| 123 |
+
print(f" Used documents: {used_docs}")
|
| 124 |
+
print(f" Chunks used: {chunks_used}")
|
| 125 |
+
print(f" Error: {error}")
|
| 126 |
+
|
| 127 |
+
if resp.get('response'):
|
| 128 |
+
response_preview = resp['response'][:100]
|
| 129 |
+
print(f" Response: {response_preview}...")
|
| 130 |
+
else:
|
| 131 |
+
print(f"❌ Sequential chat failed: {response.status_code}")
|
| 132 |
+
print(f" Response: {response.text}")
|
| 133 |
+
|
| 134 |
+
except Exception as e:
|
| 135 |
+
print(f"❌ Sequential chat error: {e}")
|
| 136 |
+
|
| 137 |
+
def upload_better_test_document():
|
| 138 |
+
"""Upload a more targeted test document"""
|
| 139 |
+
print("\n📄 Uploading better test document...")
|
| 140 |
+
|
| 141 |
+
better_doc = """
|
| 142 |
+
PhD Research Methodology Guide for Machine Learning
|
| 143 |
+
|
| 144 |
+
METHODOLOGY SECTION:
|
| 145 |
+
For machine learning research, you should use a mixed-methods approach combining quantitative experiments with qualitative analysis. Start with baseline models and incrementally add complexity. Use proper train/validation/test splits with stratified sampling. Statistical significance testing is crucial for methodology validation.
|
| 146 |
+
|
| 147 |
+
THEORETICAL FRAMEWORK SECTION:
|
| 148 |
+
Ground your work in established learning theory and computational complexity theory. Consider information theory principles and statistical learning theory. The epistemological assumptions should assume that knowledge can be extracted from data through systematic analysis. Review relevant literature and position your work within existing theoretical frameworks.
|
| 149 |
+
|
| 150 |
+
PRACTICAL IMPLEMENTATION SECTION:
|
| 151 |
+
Your next steps should include: 1) Define clear research questions, 2) Design data collection methods, 3) Implement baseline models, 4) Conduct hyperparameter tuning, 5) Perform comprehensive evaluation, 6) Document all results. Create a timeline with specific milestones and success metrics for each phase.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
with open("better_test_doc.txt", "w") as f:
|
| 156 |
+
f.write(better_doc)
|
| 157 |
+
|
| 158 |
+
with open("better_test_doc.txt", "rb") as f:
|
| 159 |
+
files = {"file": ("ml_methodology_guide.txt", f, "text/plain")}
|
| 160 |
+
response = requests.post(f"{BASE_URL}/upload-document", files=files)
|
| 161 |
+
|
| 162 |
+
import os
|
| 163 |
+
os.remove("better_test_doc.txt")
|
| 164 |
+
|
| 165 |
+
if response.status_code == 200:
|
| 166 |
+
result = response.json()
|
| 167 |
+
print(f"✅ Better document uploaded: {result['chunks_created']} chunks")
|
| 168 |
+
return True
|
| 169 |
+
else:
|
| 170 |
+
print(f"❌ Upload failed: {response.status_code}")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"❌ Upload error: {e}")
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
def main():
|
| 178 |
+
"""Run debug sequence"""
|
| 179 |
+
print("🔧 RAG Chat Debug Tool")
|
| 180 |
+
print("=" * 40)
|
| 181 |
+
|
| 182 |
+
# Test 1: Upload better document
|
| 183 |
+
if not upload_better_test_document():
|
| 184 |
+
print("❌ Document upload failed, continuing with existing document...")
|
| 185 |
+
|
| 186 |
+
# Test 2: Direct search
|
| 187 |
+
test_direct_search()
|
| 188 |
+
|
| 189 |
+
# Test 3: Individual persona chat (if endpoint exists)
|
| 190 |
+
test_single_persona_chat()
|
| 191 |
+
|
| 192 |
+
# Test 4: Sequential chat with debug
|
| 193 |
+
test_sequential_chat_debug()
|
| 194 |
+
|
| 195 |
+
print("\n" + "=" * 40)
|
| 196 |
+
print("🔍 Debug Analysis:")
|
| 197 |
+
print("1. Check if direct search shows relevance scores > 0.1")
|
| 198 |
+
print("2. Check if individual personas work (may not be implemented)")
|
| 199 |
+
print("3. Check if sequential chat shows 'used_documents: true' for any persona")
|
| 200 |
+
print("4. Look for server logs showing document retrieval attempts")
|
| 201 |
+
|
| 202 |
+
print("\nIf RAG still isn't working:")
|
| 203 |
+
print("- Check server logs for 'Retrieved X chunks for persona_name'")
|
| 204 |
+
print("- Verify relevance scores are above 0.1 threshold")
|
| 205 |
+
print("- Ensure document context is being passed to personas")
|
| 206 |
+
|
| 207 |
+
if __name__ == "__main__":
|
| 208 |
+
main()
|
multi_llm_chatbot_backend/app/tests/test_rag_system.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
BASE_URL = "http://localhost:8000"
|
| 6 |
+
|
| 7 |
+
def test_rag_system():
|
| 8 |
+
"""
|
| 9 |
+
Comprehensive test of the RAG system
|
| 10 |
+
"""
|
| 11 |
+
print("🚀 Testing RAG System Integration\n")
|
| 12 |
+
|
| 13 |
+
# Test 1: Upload a document
|
| 14 |
+
print("📄 Test 1: Uploading a document...")
|
| 15 |
+
|
| 16 |
+
# Create a sample research document
|
| 17 |
+
sample_document = """
|
| 18 |
+
Research Methodology for Machine Learning PhD
|
| 19 |
+
|
| 20 |
+
Abstract: This document outlines the methodology for conducting research in machine learning,
|
| 21 |
+
focusing on experimental design, data collection, and validation techniques.
|
| 22 |
+
|
| 23 |
+
1. Introduction
|
| 24 |
+
Machine learning research requires rigorous methodology to ensure reproducible results.
|
| 25 |
+
The theoretical framework must be grounded in statistical learning theory.
|
| 26 |
+
|
| 27 |
+
2. Methodology
|
| 28 |
+
Our research design follows a mixed-methods approach combining quantitative experiments
|
| 29 |
+
with qualitative analysis. Data collection involves sampling from multiple datasets
|
| 30 |
+
to ensure statistical validity and external validity.
|
| 31 |
+
|
| 32 |
+
3. Practical Implementation
|
| 33 |
+
The implementation strategy focuses on actionable steps: first, establish baseline models,
|
| 34 |
+
then implement incremental improvements, and finally conduct comprehensive evaluation.
|
| 35 |
+
Each step should have clear success metrics and timelines.
|
| 36 |
+
|
| 37 |
+
4. Theoretical Framework
|
| 38 |
+
The conceptual foundation draws from information theory, statistical learning theory,
|
| 39 |
+
and computational complexity theory. The epistemological assumptions underlying
|
| 40 |
+
our approach assume that knowledge can be extracted from data through systematic analysis.
|
| 41 |
+
|
| 42 |
+
5. Next Steps
|
| 43 |
+
Immediate action items include: data preprocessing, model selection, hyperparameter tuning,
|
| 44 |
+
and result validation. The timeline should prioritize high-impact activities first.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
# Save as temporary file
|
| 48 |
+
with open("temp_research_doc.txt", "w") as f:
|
| 49 |
+
f.write(sample_document)
|
| 50 |
+
|
| 51 |
+
# Upload the document
|
| 52 |
+
with open("temp_research_doc.txt", "rb") as f:
|
| 53 |
+
files = {"file": ("research_methodology.txt", f, "text/plain")}
|
| 54 |
+
upload_response = requests.post(f"{BASE_URL}/upload-document", files=files)
|
| 55 |
+
|
| 56 |
+
print(f"Upload Status: {upload_response.status_code}")
|
| 57 |
+
print(f"Response: {json.dumps(upload_response.json(), indent=2)}\n")
|
| 58 |
+
|
| 59 |
+
# Test 2: Get document statistics
|
| 60 |
+
print("📊 Test 2: Getting document statistics...")
|
| 61 |
+
stats_response = requests.get(f"{BASE_URL}/document-stats")
|
| 62 |
+
print(f"Stats: {json.dumps(stats_response.json(), indent=2)}\n")
|
| 63 |
+
|
| 64 |
+
# Test 3: Test direct document search
|
| 65 |
+
print("🔍 Test 3: Testing direct document search...")
|
| 66 |
+
|
| 67 |
+
search_queries = [
|
| 68 |
+
{"query": "What methodology should I use?", "persona": "methodist"},
|
| 69 |
+
{"query": "What is the theoretical framework?", "persona": "theorist"},
|
| 70 |
+
{"query": "What are the next steps?", "persona": "pragmatist"}
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
for search in search_queries:
|
| 74 |
+
print(f"\nSearching: '{search['query']}' for {search['persona']}")
|
| 75 |
+
search_response = requests.post(
|
| 76 |
+
f"{BASE_URL}/search-documents",
|
| 77 |
+
json=search
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if search_response.status_code == 200:
|
| 81 |
+
results = search_response.json()
|
| 82 |
+
print(f"Found {results['results_count']} results")
|
| 83 |
+
for i, result in enumerate(results['results'][:2], 1): # Show top 2
|
| 84 |
+
print(f" Result {i}: {result['text'][:100]}... (score: {result['relevance_score']:.3f})")
|
| 85 |
+
else:
|
| 86 |
+
print(f"Search failed: {search_response.status_code}")
|
| 87 |
+
|
| 88 |
+
print("\n" + "="*60)
|
| 89 |
+
|
| 90 |
+
# Test 4: Test RAG-enhanced chat
|
| 91 |
+
print("💬 Test 4: Testing RAG-enhanced chat responses...\n")
|
| 92 |
+
|
| 93 |
+
chat_queries = [
|
| 94 |
+
"I need help with my research methodology. What approach should I take?",
|
| 95 |
+
"Can you explain the theoretical framework I should consider?",
|
| 96 |
+
"What are the practical next steps I should focus on?"
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
for query in chat_queries:
|
| 100 |
+
print(f"Query: {query}")
|
| 101 |
+
chat_response = requests.post(
|
| 102 |
+
f"{BASE_URL}/chat-sequential",
|
| 103 |
+
json={"user_input": query, "response_length": "short"}
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
if chat_response.status_code == 200:
|
| 107 |
+
result = chat_response.json()
|
| 108 |
+
print(f"Response type: {result.get('type')}")
|
| 109 |
+
|
| 110 |
+
if 'rag_info' in result:
|
| 111 |
+
rag_info = result['rag_info']
|
| 112 |
+
print(f"RAG Info: {rag_info['personas_using_documents']}/{len(result.get('responses', []))} personas used documents")
|
| 113 |
+
print(f"Total chunks used: {rag_info['total_document_chunks_used']}")
|
| 114 |
+
|
| 115 |
+
# Show one advisor response
|
| 116 |
+
if 'responses' in result and result['responses']:
|
| 117 |
+
advisor = result['responses'][0]
|
| 118 |
+
print(f"\n{advisor['persona_name']}: {advisor['response'][:200]}...")
|
| 119 |
+
if advisor.get('used_documents'):
|
| 120 |
+
print(f"✅ Used {advisor.get('document_chunks_used', 0)} document chunks")
|
| 121 |
+
else:
|
| 122 |
+
print("❌ No documents used")
|
| 123 |
+
else:
|
| 124 |
+
print(f"Chat failed: {chat_response.status_code}")
|
| 125 |
+
|
| 126 |
+
print("\n" + "-"*40 + "\n")
|
| 127 |
+
|
| 128 |
+
# Test 5: Session statistics
|
| 129 |
+
print("📈 Test 5: Getting comprehensive session statistics...")
|
| 130 |
+
session_stats = requests.get(f"{BASE_URL}/session-stats")
|
| 131 |
+
if session_stats.status_code == 200:
|
| 132 |
+
stats = session_stats.json()
|
| 133 |
+
print(f"Session Stats:")
|
| 134 |
+
print(f" Messages: {stats.get('message_count', 0)}")
|
| 135 |
+
print(f" Uploaded files: {len(stats.get('uploaded_files', []))}")
|
| 136 |
+
print(f" RAG documents: {stats.get('rag_stats', {}).get('total_documents', 0)}")
|
| 137 |
+
print(f" RAG chunks: {stats.get('rag_stats', {}).get('total_chunks', 0)}")
|
| 138 |
+
|
| 139 |
+
# Cleanup
|
| 140 |
+
import os
|
| 141 |
+
try:
|
| 142 |
+
os.remove("temp_research_doc.txt")
|
| 143 |
+
except:
|
| 144 |
+
pass
|
| 145 |
+
|
| 146 |
+
print("\n🎉 RAG System Testing Complete!")
|
| 147 |
+
print("\nKey Improvements:")
|
| 148 |
+
print("✅ Documents are now chunked and stored in vector database")
|
| 149 |
+
print("✅ Each advisor gets persona-specific document chunks")
|
| 150 |
+
print("✅ Only relevant content is retrieved for each query")
|
| 151 |
+
print("✅ Session context is much more efficient")
|
| 152 |
+
print("✅ Supports semantic search across all uploaded documents")
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
test_rag_system()
|
multi_llm_chatbot_backend/requirements.txt
CHANGED
|
@@ -1,3 +1,10 @@
|
|
| 1 |
fastapi
|
| 2 |
uvicorn
|
| 3 |
-
httpx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
fastapi
|
| 2 |
uvicorn
|
| 3 |
+
httpx
|
| 4 |
+
PyPDF2
|
| 5 |
+
docx2txt
|
| 6 |
+
python-dotenv
|
| 7 |
+
chromadb
|
| 8 |
+
sentence-transformers
|
| 9 |
+
nltk
|
| 10 |
+
tiktoken
|
phd-advisor-frontend/src/components/MessageBubble.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
import React, { useState } from 'react';
|
| 2 |
-
import { Reply, Copy, Check, Maximize2 } from 'lucide-react';
|
| 3 |
import { advisors, getAdvisorColors } from '../data/advisors';
|
| 4 |
import { useTheme } from '../contexts/ThemeContext';
|
| 5 |
|
|
@@ -13,6 +13,8 @@ const MessageBubble = ({
|
|
| 13 |
const { isDark } = useTheme();
|
| 14 |
const [showTooltip, setShowTooltip] = useState(null);
|
| 15 |
const [copiedStates, setCopiedStates] = useState({});
|
|
|
|
|
|
|
| 16 |
|
| 17 |
const handleCopy = async (messageId, content) => {
|
| 18 |
try {
|
|
@@ -33,6 +35,10 @@ const MessageBubble = ({
|
|
| 33 |
if (onExpand) onExpand(messageId, advisorId);
|
| 34 |
};
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
const showTooltipWithDelay = (tooltipType) => {
|
| 37 |
setTimeout(() => setShowTooltip(tooltipType), 500);
|
| 38 |
};
|
|
@@ -41,6 +47,99 @@ const MessageBubble = ({
|
|
| 41 |
setShowTooltip(null);
|
| 42 |
};
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
if (message.type === 'user') {
|
| 45 |
return (
|
| 46 |
<div className="user-message-container">
|
|
@@ -75,7 +174,8 @@ const MessageBubble = ({
|
|
| 75 |
className="advisor-message-bubble"
|
| 76 |
style={{
|
| 77 |
backgroundColor: colors.bgColor,
|
| 78 |
-
borderColor: colors.color + '40'
|
|
|
|
| 79 |
}}
|
| 80 |
>
|
| 81 |
<div className="advisor-message-header">
|
|
@@ -169,9 +269,37 @@ const MessageBubble = ({
|
|
| 169 |
<div className="tooltip">Expand on this response</div>
|
| 170 |
)}
|
| 171 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
</div>
|
| 173 |
</div>
|
| 174 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
</div>
|
| 176 |
</div>
|
| 177 |
);
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { Reply, Copy, Check, Maximize2, Info, FileText, Hash, Target } from 'lucide-react';
|
| 3 |
import { advisors, getAdvisorColors } from '../data/advisors';
|
| 4 |
import { useTheme } from '../contexts/ThemeContext';
|
| 5 |
|
|
|
|
| 13 |
const { isDark } = useTheme();
|
| 14 |
const [showTooltip, setShowTooltip] = useState(null);
|
| 15 |
const [copiedStates, setCopiedStates] = useState({});
|
| 16 |
+
const [showInfoOverlay, setShowInfoOverlay] = useState(false);
|
| 17 |
+
const overlayRef = useRef(null);
|
| 18 |
|
| 19 |
const handleCopy = async (messageId, content) => {
|
| 20 |
try {
|
|
|
|
| 35 |
if (onExpand) onExpand(messageId, advisorId);
|
| 36 |
};
|
| 37 |
|
| 38 |
+
const handleInfoToggle = () => {
|
| 39 |
+
setShowInfoOverlay(!showInfoOverlay);
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
const showTooltipWithDelay = (tooltipType) => {
|
| 43 |
setTimeout(() => setShowTooltip(tooltipType), 500);
|
| 44 |
};
|
|
|
|
| 47 |
setShowTooltip(null);
|
| 48 |
};
|
| 49 |
|
| 50 |
+
// Close overlay when clicking outside
|
| 51 |
+
useEffect(() => {
|
| 52 |
+
const handleClickOutside = (event) => {
|
| 53 |
+
if (overlayRef.current && !overlayRef.current.contains(event.target)) {
|
| 54 |
+
setShowInfoOverlay(false);
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
if (showInfoOverlay) {
|
| 59 |
+
document.addEventListener('mousedown', handleClickOutside);
|
| 60 |
+
return () => {
|
| 61 |
+
document.removeEventListener('mousedown', handleClickOutside);
|
| 62 |
+
};
|
| 63 |
+
}
|
| 64 |
+
}, [showInfoOverlay]);
|
| 65 |
+
|
| 66 |
+
// RAG Metadata Component
|
| 67 |
+
const RagInfoOverlay = ({ ragMetadata, colors }) => {
|
| 68 |
+
const hasDocuments = ragMetadata?.usedDocuments || false;
|
| 69 |
+
const chunksUsed = ragMetadata?.chunksUsed || 0;
|
| 70 |
+
const documentChunks = ragMetadata?.documentChunks || [];
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<div
|
| 74 |
+
ref={overlayRef}
|
| 75 |
+
className="rag-info-overlay"
|
| 76 |
+
style={{
|
| 77 |
+
borderColor: colors.color + '40',
|
| 78 |
+
backgroundColor: isDark ? '#1f2937' : '#ffffff'
|
| 79 |
+
}}
|
| 80 |
+
>
|
| 81 |
+
<div className="rag-overlay-header" style={{ color: colors.color }}>
|
| 82 |
+
<Info size={14} />
|
| 83 |
+
<span>RAG Information</span>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div className="rag-overlay-content">
|
| 87 |
+
{/* Basic Stats */}
|
| 88 |
+
<div className="rag-stat-row">
|
| 89 |
+
<div className="rag-stat-label">Used Documents:</div>
|
| 90 |
+
<div className={`rag-stat-value ${hasDocuments ? 'positive' : 'negative'}`}>
|
| 91 |
+
{hasDocuments ? 'Yes' : 'No'}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div className="rag-stat-row">
|
| 96 |
+
<div className="rag-stat-label">Document Chunks:</div>
|
| 97 |
+
<div className="rag-stat-value">{chunksUsed}</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
{/* Document Details */}
|
| 101 |
+
{hasDocuments && documentChunks.length > 0 && (
|
| 102 |
+
<div className="rag-documents-section">
|
| 103 |
+
<div className="rag-section-title">
|
| 104 |
+
<FileText size={12} />
|
| 105 |
+
Referenced Sources
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
{documentChunks.map((chunk, index) => (
|
| 109 |
+
<div key={index} className="rag-document-item">
|
| 110 |
+
<div className="rag-document-header">
|
| 111 |
+
<span className="rag-filename">
|
| 112 |
+
{chunk.metadata?.filename || 'Unknown file'}
|
| 113 |
+
</span>
|
| 114 |
+
<span className="rag-relevance">
|
| 115 |
+
<Target size={10} />
|
| 116 |
+
{Math.round((chunk.relevance_score || 0) * 100)}%
|
| 117 |
+
</span>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
{chunk.text && (
|
| 121 |
+
<div className="rag-chunk-preview">
|
| 122 |
+
{chunk.text.substring(0, 120)}
|
| 123 |
+
{chunk.text.length > 120 && '...'}
|
| 124 |
+
</div>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
))}
|
| 128 |
+
</div>
|
| 129 |
+
)}
|
| 130 |
+
|
| 131 |
+
{/* No Documents Message */}
|
| 132 |
+
{!hasDocuments && (
|
| 133 |
+
<div className="rag-no-documents">
|
| 134 |
+
<Hash size={12} />
|
| 135 |
+
<span>This response was generated without referencing uploaded documents.</span>
|
| 136 |
+
</div>
|
| 137 |
+
)}
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
);
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
if (message.type === 'user') {
|
| 144 |
return (
|
| 145 |
<div className="user-message-container">
|
|
|
|
| 174 |
className="advisor-message-bubble"
|
| 175 |
style={{
|
| 176 |
backgroundColor: colors.bgColor,
|
| 177 |
+
borderColor: colors.color + '40',
|
| 178 |
+
position: 'relative' // For overlay positioning
|
| 179 |
}}
|
| 180 |
>
|
| 181 |
<div className="advisor-message-header">
|
|
|
|
| 269 |
<div className="tooltip">Expand on this response</div>
|
| 270 |
)}
|
| 271 |
</div>
|
| 272 |
+
|
| 273 |
+
{/* NEW: Info Button */}
|
| 274 |
+
<div className="tooltip-container">
|
| 275 |
+
<button
|
| 276 |
+
className="action-button"
|
| 277 |
+
onClick={handleInfoToggle}
|
| 278 |
+
onMouseEnter={() => showTooltipWithDelay('info')}
|
| 279 |
+
onMouseLeave={hideTooltip}
|
| 280 |
+
style={{
|
| 281 |
+
color: showInfoOverlay ? colors.color : colors.color,
|
| 282 |
+
borderColor: showInfoOverlay ? colors.color : colors.color + '40',
|
| 283 |
+
backgroundColor: showInfoOverlay ? colors.color + '20' : 'transparent'
|
| 284 |
+
}}
|
| 285 |
+
>
|
| 286 |
+
<Info size={14} />
|
| 287 |
+
</button>
|
| 288 |
+
{showTooltip === 'info' && (
|
| 289 |
+
<div className="tooltip">RAG Information</div>
|
| 290 |
+
)}
|
| 291 |
+
</div>
|
| 292 |
</div>
|
| 293 |
</div>
|
| 294 |
)}
|
| 295 |
+
|
| 296 |
+
{/* RAG Info Overlay */}
|
| 297 |
+
{showInfoOverlay && (
|
| 298 |
+
<RagInfoOverlay
|
| 299 |
+
ragMetadata={message.ragMetadata}
|
| 300 |
+
colors={colors}
|
| 301 |
+
/>
|
| 302 |
+
)}
|
| 303 |
</div>
|
| 304 |
</div>
|
| 305 |
);
|
phd-advisor-frontend/src/pages/ChatPage.js
CHANGED
|
@@ -138,270 +138,245 @@ const ChatPage = ({ onNavigateToHome }) => {
|
|
| 138 |
};
|
| 139 |
|
| 140 |
const handleSendMessage = async (inputMessage) => {
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
content: inputMessage,
|
| 150 |
-
timestamp: new Date()
|
| 151 |
-
};
|
| 152 |
-
setMessages(prev => [...prev, userMessage]);
|
| 153 |
-
|
| 154 |
-
setIsLoading(true);
|
| 155 |
-
setThinkingAdvisors(['system']);
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
method: 'POST',
|
| 160 |
-
headers: {
|
| 161 |
-
'Content-Type': 'application/json',
|
| 162 |
-
},
|
| 163 |
-
body: JSON.stringify({
|
| 164 |
-
user_input: inputMessage
|
| 165 |
-
}),
|
| 166 |
-
});
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
|
| 173 |
-
setCollectedInfo(data.collected_info || {});
|
| 174 |
-
setThinkingAdvisors([]);
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
content: data.responses[0].response,
|
| 181 |
-
timestamp: new Date()
|
| 182 |
-
};
|
| 183 |
-
setMessages(prev => [...prev, orchestratorMessage]);
|
| 184 |
-
} else if (data.type === 'sequential_responses') {
|
| 185 |
-
const advisorIds = ['methodist', 'theorist', 'pragmatist'];
|
| 186 |
-
|
| 187 |
-
for (let i = 0; i < advisorIds.length; i++) {
|
| 188 |
-
const advisorId = advisorIds[i];
|
| 189 |
-
const response = data.responses.find(r => r.persona_id === advisorId);
|
| 190 |
-
|
| 191 |
-
if (response) {
|
| 192 |
-
setThinkingAdvisors([advisorId]);
|
| 193 |
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 194 |
-
|
| 195 |
-
const advisorMessage = {
|
| 196 |
-
id: generateMessageId(),
|
| 197 |
-
type: 'advisor',
|
| 198 |
-
advisorId: advisorId,
|
| 199 |
-
content: response.response,
|
| 200 |
-
timestamp: new Date()
|
| 201 |
-
};
|
| 202 |
-
|
| 203 |
-
setMessages(prev => [...prev, advisorMessage]);
|
| 204 |
-
setThinkingAdvisors([]);
|
| 205 |
-
|
| 206 |
-
if (i < advisorIds.length - 1) {
|
| 207 |
-
await new Promise(resolve => setTimeout(resolve, 500));
|
| 208 |
-
}
|
| 209 |
-
}
|
| 210 |
-
}
|
| 211 |
-
} else if (data.type === 'error') {
|
| 212 |
-
const errorMessage = {
|
| 213 |
id: generateMessageId(),
|
| 214 |
-
type: '
|
| 215 |
-
content: data.responses
|
| 216 |
timestamp: new Date()
|
| 217 |
};
|
| 218 |
-
setMessages(prev => [...prev,
|
| 219 |
}
|
| 220 |
|
| 221 |
-
}
|
| 222 |
-
console.error('Error sending message:', error);
|
| 223 |
const errorMessage = {
|
| 224 |
id: generateMessageId(),
|
| 225 |
type: 'error',
|
| 226 |
-
content:
|
| 227 |
timestamp: new Date()
|
| 228 |
};
|
| 229 |
setMessages(prev => [...prev, errorMessage]);
|
| 230 |
-
} finally {
|
| 231 |
-
setIsLoading(false);
|
| 232 |
-
setThinkingAdvisors([]);
|
| 233 |
}
|
| 234 |
-
};
|
| 235 |
-
|
| 236 |
-
const handleReplyToAdvisor = async (inputMessage, replyInfo) => {
|
| 237 |
-
const userMessage = {
|
| 238 |
-
id: generateMessageId(),
|
| 239 |
-
type: 'user',
|
| 240 |
-
content: inputMessage,
|
| 241 |
-
timestamp: new Date(),
|
| 242 |
-
replyingTo: replyInfo
|
| 243 |
-
};
|
| 244 |
-
setMessages(prev => [...prev, userMessage]);
|
| 245 |
-
|
| 246 |
-
setIsLoading(true);
|
| 247 |
-
setThinkingAdvisors([replyInfo.advisorId]);
|
| 248 |
-
|
| 249 |
-
try {
|
| 250 |
-
const response = await fetch('http://localhost:8000/reply-to-advisor', {
|
| 251 |
-
method: 'POST',
|
| 252 |
-
headers: {
|
| 253 |
-
'Content-Type': 'application/json',
|
| 254 |
-
},
|
| 255 |
-
body: JSON.stringify({
|
| 256 |
-
user_input: inputMessage,
|
| 257 |
-
advisor_id: replyInfo.advisorId,
|
| 258 |
-
original_message_id: replyInfo.messageId
|
| 259 |
-
}),
|
| 260 |
-
});
|
| 261 |
-
|
| 262 |
-
if (!response.ok) {
|
| 263 |
-
throw new Error(`HTTP error! status: ${response.status}`);
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
const data = await response.json();
|
| 267 |
-
setThinkingAdvisors([]);
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
id: generateMessageId(),
|
| 272 |
-
type: 'advisor',
|
| 273 |
-
advisorId: replyInfo.advisorId,
|
| 274 |
-
content: data.response,
|
| 275 |
-
timestamp: new Date(),
|
| 276 |
-
isReply: true
|
| 277 |
-
};
|
| 278 |
-
setMessages(prev => [...prev, replyMessage]);
|
| 279 |
-
} else if (data.type === 'error') {
|
| 280 |
const errorMessage = {
|
| 281 |
id: generateMessageId(),
|
| 282 |
type: 'error',
|
| 283 |
-
content:
|
| 284 |
timestamp: new Date()
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
id: generateMessageId(),
|
| 293 |
-
type: '
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
};
|
| 297 |
-
setMessages(prev => [...prev,
|
| 298 |
-
} finally {
|
| 299 |
-
setIsLoading(false);
|
| 300 |
-
setThinkingAdvisors([]);
|
| 301 |
-
setReplyingTo(null);
|
| 302 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
};
|
| 304 |
|
| 305 |
const handleCopyMessage = (messageId, content) => {
|
| 306 |
// Optional: Show a toast notification or add to message history
|
| 307 |
console.log(`Copied message ${messageId}: ${content.substring(0, 50)}...`);
|
| 308 |
-
|
| 309 |
-
// could add a temporary notification here if desired
|
| 310 |
-
// const notificationMessage = {
|
| 311 |
-
// id: generateMessageId(),
|
| 312 |
-
// type: 'system',
|
| 313 |
-
// content: '✅ Response copied to clipboard',
|
| 314 |
-
// timestamp: new Date()
|
| 315 |
-
// };
|
| 316 |
-
|
| 317 |
-
// setMessages(prev => [...prev, notificationMessage]);
|
| 318 |
-
|
| 319 |
-
// // Remove the notification after 3 seconds
|
| 320 |
-
// setTimeout(() => {
|
| 321 |
-
// setMessages(prev => prev.filter(msg => msg.id !== notificationMessage.id));
|
| 322 |
-
// }, 3000);
|
| 323 |
};
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
try {
|
| 329 |
-
setIsLoading(true);
|
| 330 |
-
setThinkingAdvisors([advisorId]);
|
| 331 |
-
|
| 332 |
-
// Find the original message to expand on
|
| 333 |
-
const originalMessage = messages.find(msg => msg.id === messageId);
|
| 334 |
-
|
| 335 |
-
if (!originalMessage) {
|
| 336 |
-
console.error('Original message not found');
|
| 337 |
-
return;
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
// Create advisor-specific expansion prompts
|
| 341 |
-
const advisorSpecificPrompts = {
|
| 342 |
-
'methodologist': `Please expand on your previous response with more methodological detail. Include specific research methods, data collection techniques, analytical approaches, and methodological considerations that would be relevant. Provide practical examples and step-by-step guidance where applicable.`,
|
| 343 |
-
'theorist': `Please elaborate on your previous response by exploring the theoretical frameworks and conceptual foundations in greater depth. Include additional theoretical perspectives, scholarly context, and conceptual analysis that would enrich understanding of the topic.`,
|
| 344 |
-
'pragmatist': `Please expand on your previous response with more practical, actionable details. Include specific examples, concrete next steps, real-world applications, and practical strategies that can be immediately implemented.`
|
| 345 |
-
};
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
`Please expand and elaborate on your previous response in more detail. Provide additional insights, practical examples, and deeper analysis that would be helpful for understanding and implementing your advice.`;
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
|
| 368 |
-
|
| 369 |
-
setThinkingAdvisors([]);
|
| 370 |
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
id: generateMessageId(),
|
| 374 |
-
type: 'advisor',
|
| 375 |
-
advisorId: advisorId,
|
| 376 |
-
content: data.response,
|
| 377 |
-
timestamp: new Date(),
|
| 378 |
-
isExpansion: true, // Mark this as an expansion
|
| 379 |
-
expandedFrom: messageId // Reference to original message
|
| 380 |
-
};
|
| 381 |
-
setMessages(prev => [...prev, expandedMessage]);
|
| 382 |
-
} else if (data.type === 'error') {
|
| 383 |
-
const errorMessage = {
|
| 384 |
-
id: generateMessageId(),
|
| 385 |
-
type: 'error',
|
| 386 |
-
content: data.response,
|
| 387 |
-
timestamp: new Date()
|
| 388 |
-
};
|
| 389 |
-
setMessages(prev => [...prev, errorMessage]);
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
} catch (error) {
|
| 393 |
-
console.error('Error expanding message:', error);
|
| 394 |
-
const errorMessage = {
|
| 395 |
id: generateMessageId(),
|
| 396 |
-
type: '
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
};
|
| 400 |
-
setMessages(prev => [...prev,
|
| 401 |
-
} finally {
|
| 402 |
-
setIsLoading(false);
|
| 403 |
-
setThinkingAdvisors([]);
|
| 404 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
};
|
| 406 |
|
| 407 |
const handleReplyToMessage = (message) => {
|
|
|
|
| 138 |
};
|
| 139 |
|
| 140 |
const handleSendMessage = async (inputMessage) => {
|
| 141 |
+
if (replyingTo) {
|
| 142 |
+
await handleReplyToAdvisor(inputMessage, replyingTo);
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
const userMessage = {
|
| 147 |
+
id: generateMessageId(),
|
| 148 |
+
type: 'user',
|
| 149 |
+
content: inputMessage,
|
| 150 |
+
timestamp: new Date()
|
| 151 |
+
};
|
| 152 |
+
setMessages(prev => [...prev, userMessage]);
|
| 153 |
+
|
| 154 |
+
setIsLoading(true);
|
| 155 |
+
setThinkingAdvisors(['system']);
|
| 156 |
+
|
| 157 |
+
try {
|
| 158 |
+
const response = await fetch('http://localhost:8000/chat-sequential', {
|
| 159 |
+
method: 'POST',
|
| 160 |
+
headers: {
|
| 161 |
+
'Content-Type': 'application/json',
|
| 162 |
+
},
|
| 163 |
+
body: JSON.stringify({
|
| 164 |
+
user_input: inputMessage
|
| 165 |
+
}),
|
| 166 |
+
});
|
| 167 |
|
| 168 |
+
if (!response.ok) {
|
| 169 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 170 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
const data = await response.json();
|
| 173 |
+
console.log('Backend response:', data); // Debug log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
+
if (data.type === 'persona_responses' && data.responses) {
|
| 176 |
+
// Map each advisor response with RAG metadata
|
| 177 |
+
const advisorMessages = data.responses.map((advisor, index) => ({
|
| 178 |
+
id: generateMessageId(),
|
| 179 |
+
type: 'advisor',
|
| 180 |
+
advisorId: advisor.persona_id,
|
| 181 |
+
advisorName: advisor.persona_name,
|
| 182 |
+
content: advisor.response,
|
| 183 |
+
timestamp: new Date(),
|
| 184 |
+
// NEW: Map RAG metadata to the structure MessageBubble expects
|
| 185 |
+
ragMetadata: {
|
| 186 |
+
usedDocuments: advisor.used_documents || false,
|
| 187 |
+
chunksUsed: advisor.document_chunks_used || 0,
|
| 188 |
+
documentChunks: advisor.retrieved_chunks || []
|
| 189 |
+
}
|
| 190 |
+
}));
|
| 191 |
|
| 192 |
+
setMessages(prev => [...prev, ...advisorMessages]);
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
// Optional: Add RAG summary message if documents were used
|
| 195 |
+
const ragInfo = data.rag_info || {};
|
| 196 |
+
if (ragInfo.personas_using_documents > 0) {
|
| 197 |
+
const ragSummaryMessage = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
id: generateMessageId(),
|
| 199 |
+
type: 'system',
|
| 200 |
+
content: `📚 ${ragInfo.personas_using_documents}/${data.responses.length} advisors referenced your uploaded documents (${ragInfo.total_document_chunks_used} chunks used)`,
|
| 201 |
timestamp: new Date()
|
| 202 |
};
|
| 203 |
+
setMessages(prev => [...prev, ragSummaryMessage]);
|
| 204 |
}
|
| 205 |
|
| 206 |
+
} else if (data.type === 'error') {
|
|
|
|
| 207 |
const errorMessage = {
|
| 208 |
id: generateMessageId(),
|
| 209 |
type: 'error',
|
| 210 |
+
content: data.message || 'An error occurred. Please try again.',
|
| 211 |
timestamp: new Date()
|
| 212 |
};
|
| 213 |
setMessages(prev => [...prev, errorMessage]);
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
+
} catch (error) {
|
| 217 |
+
console.error('Error sending message:', error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
const errorMessage = {
|
| 219 |
id: generateMessageId(),
|
| 220 |
type: 'error',
|
| 221 |
+
content: 'Sorry, I encountered an error. Please try again.',
|
| 222 |
timestamp: new Date()
|
| 223 |
+
};
|
| 224 |
+
setMessages(prev => [...prev, errorMessage]);
|
| 225 |
+
}
|
| 226 |
|
| 227 |
+
setIsLoading(false);
|
| 228 |
+
setThinkingAdvisors([]);
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
const handleReplyToAdvisor = async (inputMessage, replyContext) => {
|
| 232 |
+
const replyMessage = {
|
| 233 |
+
id: generateMessageId(),
|
| 234 |
+
type: 'user',
|
| 235 |
+
content: inputMessage,
|
| 236 |
+
replyTo: {
|
| 237 |
+
advisorId: replyContext.advisorId,
|
| 238 |
+
advisorName: replyContext.advisorName,
|
| 239 |
+
messageId: replyContext.messageId
|
| 240 |
+
},
|
| 241 |
+
timestamp: new Date()
|
| 242 |
+
};
|
| 243 |
+
setMessages(prev => [...prev, replyMessage]);
|
| 244 |
+
|
| 245 |
+
setIsLoading(true);
|
| 246 |
+
setThinkingAdvisors([replyContext.advisorId]);
|
| 247 |
+
|
| 248 |
+
try {
|
| 249 |
+
const response = await fetch('http://localhost:8000/reply-to-advisor', {
|
| 250 |
+
method: 'POST',
|
| 251 |
+
headers: {
|
| 252 |
+
'Content-Type': 'application/json',
|
| 253 |
+
},
|
| 254 |
+
body: JSON.stringify({
|
| 255 |
+
user_input: inputMessage,
|
| 256 |
+
advisor_id: replyContext.advisorId,
|
| 257 |
+
original_message_id: replyContext.messageId
|
| 258 |
+
}),
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
if (!response.ok) {
|
| 262 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
const data = await response.json();
|
| 266 |
+
|
| 267 |
+
if (data.type === 'advisor_reply') {
|
| 268 |
+
const replyResponseMessage = {
|
| 269 |
id: generateMessageId(),
|
| 270 |
+
type: 'advisor',
|
| 271 |
+
advisorId: data.persona_id,
|
| 272 |
+
advisorName: data.persona,
|
| 273 |
+
content: data.response,
|
| 274 |
+
isReply: true,
|
| 275 |
+
timestamp: new Date(),
|
| 276 |
+
// NEW: Map RAG metadata for reply responses too
|
| 277 |
+
ragMetadata: {
|
| 278 |
+
usedDocuments: data.used_documents || false,
|
| 279 |
+
chunksUsed: data.document_chunks_used || 0,
|
| 280 |
+
documentChunks: data.retrieved_chunks || []
|
| 281 |
+
}
|
| 282 |
};
|
| 283 |
+
setMessages(prev => [...prev, replyResponseMessage]);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
+
|
| 286 |
+
} catch (error) {
|
| 287 |
+
console.error('Error replying to advisor:', error);
|
| 288 |
+
const errorMessage = {
|
| 289 |
+
id: generateMessageId(),
|
| 290 |
+
type: 'error',
|
| 291 |
+
content: 'Sorry, I encountered an error with your reply. Please try again.',
|
| 292 |
+
timestamp: new Date()
|
| 293 |
+
};
|
| 294 |
+
setMessages(prev => [...prev, errorMessage]);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
setIsLoading(false);
|
| 298 |
+
setThinkingAdvisors([]);
|
| 299 |
+
setReplyingTo(null);
|
| 300 |
};
|
| 301 |
|
| 302 |
const handleCopyMessage = (messageId, content) => {
|
| 303 |
// Optional: Show a toast notification or add to message history
|
| 304 |
console.log(`Copied message ${messageId}: ${content.substring(0, 50)}...`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
};
|
| 306 |
|
| 307 |
+
const handleExpandMessage = async (messageId, advisorId) => {
|
| 308 |
+
const advisor = advisors[advisorId];
|
| 309 |
+
if (!advisor) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
+
const originalMessage = messages.find(msg => msg.id === messageId);
|
| 312 |
+
if (!originalMessage) return;
|
|
|
|
| 313 |
|
| 314 |
+
const expandPrompt = `Please expand on your previous response: "${originalMessage.content.substring(0, 100)}..." Provide more detail and depth.`;
|
| 315 |
+
|
| 316 |
+
const expandMessage = {
|
| 317 |
+
id: generateMessageId(),
|
| 318 |
+
type: 'user',
|
| 319 |
+
content: expandPrompt,
|
| 320 |
+
timestamp: new Date(),
|
| 321 |
+
isExpandRequest: true,
|
| 322 |
+
expandsMessageId: messageId
|
| 323 |
+
};
|
| 324 |
+
setMessages(prev => [...prev, expandMessage]);
|
| 325 |
+
|
| 326 |
+
setIsLoading(true);
|
| 327 |
+
setThinkingAdvisors([advisorId]);
|
| 328 |
+
|
| 329 |
+
try {
|
| 330 |
+
const response = await fetch(`http://localhost:8000/chat/${advisorId}`, {
|
| 331 |
+
method: 'POST',
|
| 332 |
+
headers: {
|
| 333 |
+
'Content-Type': 'application/json',
|
| 334 |
+
},
|
| 335 |
+
body: JSON.stringify({
|
| 336 |
+
user_input: expandPrompt,
|
| 337 |
+
response_length: 'long'
|
| 338 |
+
}),
|
| 339 |
+
});
|
| 340 |
|
| 341 |
+
if (!response.ok) {
|
| 342 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 343 |
+
}
|
| 344 |
|
| 345 |
+
const data = await response.json();
|
|
|
|
| 346 |
|
| 347 |
+
if (data.type === 'single_persona_response' && data.persona) {
|
| 348 |
+
const expandedMessage = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
id: generateMessageId(),
|
| 350 |
+
type: 'advisor',
|
| 351 |
+
advisorId: advisorId,
|
| 352 |
+
advisorName: advisor.name,
|
| 353 |
+
content: data.persona.response,
|
| 354 |
+
isExpansion: true,
|
| 355 |
+
expandsMessageId: messageId,
|
| 356 |
+
timestamp: new Date(),
|
| 357 |
+
// NEW: Map RAG metadata for expanded responses
|
| 358 |
+
ragMetadata: {
|
| 359 |
+
usedDocuments: data.persona.used_documents || false,
|
| 360 |
+
chunksUsed: data.persona.document_chunks_used || 0,
|
| 361 |
+
documentChunks: data.persona.retrieved_chunks || []
|
| 362 |
+
}
|
| 363 |
};
|
| 364 |
+
setMessages(prev => [...prev, expandedMessage]);
|
|
|
|
|
|
|
|
|
|
| 365 |
}
|
| 366 |
+
|
| 367 |
+
} catch (error) {
|
| 368 |
+
console.error('Error expanding message:', error);
|
| 369 |
+
const errorMessage = {
|
| 370 |
+
id: generateMessageId(),
|
| 371 |
+
type: 'error',
|
| 372 |
+
content: 'Sorry, I encountered an error expanding the response. Please try again.',
|
| 373 |
+
timestamp: new Date()
|
| 374 |
+
};
|
| 375 |
+
setMessages(prev => [...prev, errorMessage]);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
setIsLoading(false);
|
| 379 |
+
setThinkingAdvisors([]);
|
| 380 |
};
|
| 381 |
|
| 382 |
const handleReplyToMessage = (message) => {
|
phd-advisor-frontend/src/styles/components.css
CHANGED
|
@@ -830,6 +830,230 @@
|
|
| 830 |
display: none;
|
| 831 |
}
|
| 832 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
/* Animations */
|
| 834 |
@keyframes bounce {
|
| 835 |
0%, 80%, 100% {
|
|
@@ -911,4 +1135,23 @@
|
|
| 911 |
padding: 4px 8px;
|
| 912 |
}
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
}
|
|
|
|
| 830 |
display: none;
|
| 831 |
}
|
| 832 |
|
| 833 |
+
.rag-info-overlay {
|
| 834 |
+
position: absolute;
|
| 835 |
+
top: 100%;
|
| 836 |
+
right: 0;
|
| 837 |
+
z-index: 1000;
|
| 838 |
+
width: 320px;
|
| 839 |
+
max-width: 90vw;
|
| 840 |
+
border: 1px solid;
|
| 841 |
+
border-radius: 8px;
|
| 842 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
| 843 |
+
margin-top: 8px;
|
| 844 |
+
backdrop-filter: blur(10px);
|
| 845 |
+
animation: ragOverlaySlideIn 0.2s ease-out;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
@keyframes ragOverlaySlideIn {
|
| 849 |
+
from {
|
| 850 |
+
opacity: 0;
|
| 851 |
+
transform: translateY(-10px) scale(0.95);
|
| 852 |
+
}
|
| 853 |
+
to {
|
| 854 |
+
opacity: 1;
|
| 855 |
+
transform: translateY(0) scale(1);
|
| 856 |
+
}
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
.rag-overlay-header {
|
| 860 |
+
display: flex;
|
| 861 |
+
align-items: center;
|
| 862 |
+
gap: 8px;
|
| 863 |
+
padding: 12px 16px;
|
| 864 |
+
font-weight: 600;
|
| 865 |
+
font-size: 14px;
|
| 866 |
+
border-bottom: 1px solid var(--border-color);
|
| 867 |
+
background: var(--bg-secondary);
|
| 868 |
+
border-radius: 8px 8px 0 0;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
.rag-overlay-content {
|
| 872 |
+
padding: 16px;
|
| 873 |
+
max-height: 400px;
|
| 874 |
+
overflow-y: auto;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.rag-stat-row {
|
| 878 |
+
display: flex;
|
| 879 |
+
justify-content: space-between;
|
| 880 |
+
align-items: center;
|
| 881 |
+
margin-bottom: 12px;
|
| 882 |
+
padding: 8px 0;
|
| 883 |
+
border-bottom: 1px solid var(--border-secondary);
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.rag-stat-row:last-child {
|
| 887 |
+
border-bottom: none;
|
| 888 |
+
margin-bottom: 0;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
.rag-stat-label {
|
| 892 |
+
font-size: 13px;
|
| 893 |
+
color: var(--text-secondary);
|
| 894 |
+
font-weight: 500;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
.rag-stat-value {
|
| 898 |
+
font-size: 13px;
|
| 899 |
+
font-weight: 600;
|
| 900 |
+
color: var(--text-primary);
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
.rag-stat-value.positive {
|
| 904 |
+
color: #10B981;
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
.rag-stat-value.negative {
|
| 908 |
+
color: #6B7280;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
.rag-documents-section {
|
| 912 |
+
margin-top: 16px;
|
| 913 |
+
padding-top: 16px;
|
| 914 |
+
border-top: 1px solid var(--border-secondary);
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
.rag-section-title {
|
| 918 |
+
display: flex;
|
| 919 |
+
align-items: center;
|
| 920 |
+
gap: 6px;
|
| 921 |
+
font-size: 12px;
|
| 922 |
+
font-weight: 600;
|
| 923 |
+
color: var(--text-secondary);
|
| 924 |
+
margin-bottom: 12px;
|
| 925 |
+
text-transform: uppercase;
|
| 926 |
+
letter-spacing: 0.5px;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.rag-document-item {
|
| 930 |
+
background: var(--bg-tertiary);
|
| 931 |
+
border: 1px solid var(--border-secondary);
|
| 932 |
+
border-radius: 6px;
|
| 933 |
+
padding: 10px;
|
| 934 |
+
margin-bottom: 8px;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
.rag-document-item:last-child {
|
| 938 |
+
margin-bottom: 0;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.rag-document-header {
|
| 942 |
+
display: flex;
|
| 943 |
+
justify-content: space-between;
|
| 944 |
+
align-items: center;
|
| 945 |
+
margin-bottom: 6px;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.rag-filename {
|
| 949 |
+
font-size: 12px;
|
| 950 |
+
font-weight: 600;
|
| 951 |
+
color: var(--text-primary);
|
| 952 |
+
flex: 1;
|
| 953 |
+
margin-right: 8px;
|
| 954 |
+
white-space: nowrap;
|
| 955 |
+
overflow: hidden;
|
| 956 |
+
text-overflow: ellipsis;
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
.rag-relevance {
|
| 960 |
+
display: flex;
|
| 961 |
+
align-items: center;
|
| 962 |
+
gap: 3px;
|
| 963 |
+
font-size: 11px;
|
| 964 |
+
font-weight: 600;
|
| 965 |
+
color: var(--accent-color);
|
| 966 |
+
background: var(--accent-color)15;
|
| 967 |
+
padding: 2px 6px;
|
| 968 |
+
border-radius: 10px;
|
| 969 |
+
white-space: nowrap;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.rag-chunk-preview {
|
| 973 |
+
font-size: 11px;
|
| 974 |
+
line-height: 1.4;
|
| 975 |
+
color: var(--text-tertiary);
|
| 976 |
+
background: var(--bg-quaternary);
|
| 977 |
+
padding: 6px 8px;
|
| 978 |
+
border-radius: 4px;
|
| 979 |
+
border-left: 2px solid var(--accent-color);
|
| 980 |
+
margin-top: 6px;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.rag-no-documents {
|
| 984 |
+
display: flex;
|
| 985 |
+
align-items: center;
|
| 986 |
+
gap: 8px;
|
| 987 |
+
padding: 12px;
|
| 988 |
+
background: var(--bg-secondary);
|
| 989 |
+
border: 1px dashed var(--border-secondary);
|
| 990 |
+
border-radius: 6px;
|
| 991 |
+
color: var(--text-secondary);
|
| 992 |
+
font-size: 12px;
|
| 993 |
+
font-style: italic;
|
| 994 |
+
margin-top: 8px;
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
/* Accessibility improvements */
|
| 998 |
+
.rag-info-overlay:focus-within {
|
| 999 |
+
outline: 2px solid var(--accent-color);
|
| 1000 |
+
outline-offset: 2px;
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
/* Smooth transitions */
|
| 1004 |
+
.action-button {
|
| 1005 |
+
transition: all 0.2s ease;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
.action-button:hover {
|
| 1009 |
+
transform: translateY(-1px);
|
| 1010 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
/* Custom scrollbar for overlay content */
|
| 1014 |
+
.rag-overlay-content::-webkit-scrollbar {
|
| 1015 |
+
width: 4px;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
.rag-overlay-content::-webkit-scrollbar-track {
|
| 1019 |
+
background: var(--bg-secondary);
|
| 1020 |
+
border-radius: 2px;
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
.rag-overlay-content::-webkit-scrollbar-thumb {
|
| 1024 |
+
background: var(--border-color);
|
| 1025 |
+
border-radius: 2px;
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
.rag-overlay-content::-webkit-scrollbar-thumb:hover {
|
| 1029 |
+
background: var(--text-secondary);
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
/* Dark theme adjustments */
|
| 1033 |
+
[data-theme="dark"] .rag-info-overlay {
|
| 1034 |
+
background: #1f2937;
|
| 1035 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
[data-theme="dark"] .rag-overlay-header {
|
| 1039 |
+
background: #374151;
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
[data-theme="dark"] .rag-document-item {
|
| 1043 |
+
background: #374151;
|
| 1044 |
+
border-color: #4B5563;
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
[data-theme="dark"] .rag-chunk-preview {
|
| 1048 |
+
background: #4B5563;
|
| 1049 |
+
color: #D1D5DB;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
[data-theme="dark"] .rag-no-documents {
|
| 1053 |
+
background: #374151;
|
| 1054 |
+
border-color: #4B5563;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
/* Animations */
|
| 1058 |
@keyframes bounce {
|
| 1059 |
0%, 80%, 100% {
|
|
|
|
| 1135 |
padding: 4px 8px;
|
| 1136 |
}
|
| 1137 |
|
| 1138 |
+
.rag-info-overlay {
|
| 1139 |
+
width: 280px;
|
| 1140 |
+
left: 0;
|
| 1141 |
+
right: auto;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.rag-document-header {
|
| 1145 |
+
flex-direction: column;
|
| 1146 |
+
align-items: flex-start;
|
| 1147 |
+
gap: 4px;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
.rag-filename {
|
| 1151 |
+
margin-right: 0;
|
| 1152 |
+
white-space: normal;
|
| 1153 |
+
overflow: visible;
|
| 1154 |
+
text-overflow: initial;
|
| 1155 |
+
}
|
| 1156 |
+
|
| 1157 |
}
|