Girish Jeswani commited on
Commit
b4a645a
·
1 Parent(s): f7cedcf

add rag feature

Browse files
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/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()