Girish Jeswani commited on
Commit
55471ff
·
2 Parent(s): 6fd5d0dbebb492

Merge pull request #53 from sohank-17/girish-rag-feature

Browse files
.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
- # Use the new orchestrator with session management
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
- # Convert new format back to old format for backward compatibility
201
- if result["type"] == "clarification":
202
- return {
203
- "type": "orchestrator_question",
204
- "responses": [{
205
- "persona": "PhD Advisor Assistant",
206
- "response": result["message"]
207
- }],
208
- "collected_info": {}
 
209
  }
210
 
211
- elif result["type"] == "persona_responses":
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 chat_sequential: {e}")
237
  return {
238
  "type": "error",
239
- "responses": [{
240
- "persona": "System",
241
- "response": "I'm having trouble processing your request. Could you please try again?"
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
- """Upload document - SAME INTERFACE"""
 
 
 
 
 
 
 
 
 
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 session document size limit (10 MB).")
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
- # Add to session context using new system
357
- session.add_uploaded_file(file.filename, content, len(file_bytes))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
- return {"message": "Document uploaded and added to context successfully."}
 
 
 
 
 
 
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 - SAME INTERFACE"""
383
  try:
384
  session_id = get_or_create_session_for_request(request)
385
  session = session_manager.get_session(session_id)
386
- return session.messages # Return messages in same format as before
 
 
 
 
 
 
 
 
 
 
 
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 - SAME INTERFACE"""
395
  try:
396
  session_id = get_or_create_session_for_request(request)
397
- success = chat_orchestrator.reset_session(session_id)
 
 
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
- # Debug endpoint (SAME INTERFACE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  @router.get("/debug/personas")
427
  async def debug_personas(request: Request):
428
- """Debug personas - SAME INTERFACE"""
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
- "context_length": len(session.messages),
441
- "current_provider": current_provider
 
 
 
 
 
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: ConversationContext, user_input: str) -> bool:
127
  """
128
- Determine if user input needs clarification
129
  """
130
- # Don't ask for clarification if this is a follow-up message
131
- user_messages = session.get_messages_by_role("user")
132
- if len(user_messages) > 1:
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
- # Check if input is too short (likely vague)
154
- if len(user_input.split()) < 8:
155
- return True
156
-
157
- return False
 
 
 
 
 
 
 
 
 
 
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
- try:
212
- response_data = await self._generate_single_persona_response(
213
- session, persona, response_length
214
- )
215
- responses.append(response_data)
216
-
217
- # Add persona response to session
218
- session.append_message(persona_id, response_data['response'])
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
- context_messages = session.get_recent_messages(limit=20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- # Generate response using persona
244
- response = await persona.respond(context_messages, response_length)
 
 
 
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
- """Individual conversation context for a session"""
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 all messages but preserve metadata"""
38
- self.messages = []
39
  self.last_accessed = datetime.now()
 
 
 
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def add_uploaded_file(self, filename: str, content: str, file_size: int):
42
- """Add uploaded file content to context"""
 
 
 
 
 
43
  self.uploaded_files.append(filename)
44
  self.total_upload_size += file_size
45
- self.append_message("document", f"[Uploaded: {filename}]\n{content}")
46
-
 
 
 
 
 
 
 
 
 
 
47
  def get_context_size(self) -> int:
48
- """Calculate total context size in characters"""
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
- 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
- setCollectedInfo(data.collected_info || {});
174
- setThinkingAdvisors([]);
175
 
176
- if (data.type === 'orchestrator_question') {
177
- const orchestratorMessage = {
178
- id: generateMessageId(),
179
- type: 'orchestrator',
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: 'error',
215
- content: data.responses[0].response,
216
  timestamp: new Date()
217
  };
218
- setMessages(prev => [...prev, errorMessage]);
219
  }
220
 
221
- } catch (error) {
222
- console.error('Error sending message:', error);
223
  const errorMessage = {
224
  id: generateMessageId(),
225
  type: 'error',
226
- content: 'Sorry, I encountered an error. Please try again.',
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
- if (data.type === 'advisor_reply') {
270
- const replyMessage = {
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: data.response,
284
  timestamp: new Date()
285
- };
286
- setMessages(prev => [...prev, errorMessage]);
287
- }
288
 
289
- } catch (error) {
290
- console.error('Error sending reply:', error);
291
- const errorMessage = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  id: generateMessageId(),
293
- type: 'error',
294
- content: 'Sorry, I encountered an error. Please try again.',
295
- timestamp: new Date()
 
 
 
 
 
 
 
 
 
296
  };
297
- setMessages(prev => [...prev, errorMessage]);
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
- const handleExpandMessage = async (messageId, advisorId) => {
326
- const advisor = advisors[advisorId];
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
- // Use advisor-specific prompt or general expansion prompt
348
- const expandPrompt = advisorSpecificPrompts[advisorId] ||
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
- // Use the existing /reply-to-advisor endpoint
352
- const response = await fetch('http://localhost:8000/reply-to-advisor', {
353
- method: 'POST',
354
- headers: {
355
- 'Content-Type': 'application/json',
356
- },
357
- body: JSON.stringify({
358
- advisor_id: advisorId,
359
- user_input: expandPrompt,
360
- original_message_id: messageId
361
- }),
362
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- if (!response.ok) {
365
- throw new Error(`HTTP error! status: ${response.status}`);
366
- }
367
 
368
- const data = await response.json();
369
- setThinkingAdvisors([]);
370
 
371
- if (data.type === 'advisor_reply') {
372
- const expandedMessage = {
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: 'error',
397
- content: 'Sorry, I encountered an error while expanding the response. Please try again.',
398
- timestamp: new Date()
 
 
 
 
 
 
 
 
 
 
399
  };
400
- setMessages(prev => [...prev, errorMessage]);
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
  }