Girish Jeswani commited on
Commit
a90ba14
·
unverified ·
2 Parent(s): acd15c1311d8ab

Merge pull request #56 from sohank-17/girish-response-fixes

Browse files
.gitignore CHANGED
@@ -4,6 +4,7 @@
4
  # backend
5
  multi_llm_chatbot_backend/.env
6
  multi_llm_chatbot_backend/chroma_db/
 
7
 
8
  # frontend
9
 
 
4
  # backend
5
  multi_llm_chatbot_backend/.env
6
  multi_llm_chatbot_backend/chroma_db/
7
+ multi_llm_chatbot_backend/chromadb_storage/
8
 
9
  # frontend
10
 
README.md CHANGED
@@ -4,7 +4,7 @@ An AI-powered academic guidance system that provides personalized advice through
4
 
5
  ## Features
6
 
7
- - **Multiple AI Advisor Personas**: Chat with specialized advisors including Methodist (methodology expert), Theorist (conceptual frameworks), and Pragmatist (action-focused guidance)
8
  - **Document Upload Support**: Upload PDFs, Word documents, and text files to provide context for your questions
9
  - **Multi-LLM Backend**: Supports both Gemini API and Ollama models with seamless provider switching
10
  - **Real-time Chat Interface**: Modern, responsive chat interface with advisor-specific styling
@@ -83,7 +83,7 @@ The application will open at `http://localhost:3000`
83
 
84
  ## Advisor Personas
85
 
86
- ### Methodist - Research Methodology Expert
87
  - **Focus**: Research design, validity, sampling, methodological rigor
88
  - **Style**: Precise, analytical, methodology-focused
89
  - **Best for**: Research design questions, data collection methods, validity concerns
 
4
 
5
  ## Features
6
 
7
+ - **Multiple AI Advisor Personas**: Chat with specialized advisors including Methodologist (methodology expert), Theorist (conceptual frameworks), and Pragmatist (action-focused guidance)
8
  - **Document Upload Support**: Upload PDFs, Word documents, and text files to provide context for your questions
9
  - **Multi-LLM Backend**: Supports both Gemini API and Ollama models with seamless provider switching
10
  - **Real-time Chat Interface**: Modern, responsive chat interface with advisor-specific styling
 
83
 
84
  ## Advisor Personas
85
 
86
+ ### Methodologist - Research Methodology Expert
87
  - **Focus**: Research design, validity, sampling, methodological rigor
88
  - **Style**: Precise, analytical, methodology-focused
89
  - **Best for**: Research design questions, data collection methods, validity concerns
multi_llm_chatbot_backend/app/api/routes.py CHANGED
@@ -133,7 +133,7 @@ def _is_valid_response(response: str, persona_id: str) -> bool:
133
  def _get_persona_fallback(persona_id: str) -> str:
134
  """Get persona-specific fallback responses"""
135
  fallbacks = {
136
- "methodist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?",
137
  "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?",
138
  "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?"
139
  }
@@ -188,42 +188,64 @@ async def switch_provider(provider_data: ProviderSwitch):
188
 
189
  # Main chat endpoint (SAME INTERFACE, improved backend)
190
  @router.post("/chat-sequential")
191
- async def chat_sequential(message: ChatMessage, request: Request):
192
- """Generate advisor responses - ENHANCED with RAG"""
 
 
193
  try:
 
194
  session_id = get_or_create_session_for_request(request, message.session_id)
195
 
196
- # Process message through improved orchestrator (now with RAG)
197
- result = await chat_orchestrator.process_message(
198
  user_input=message.user_input,
199
  session_id=session_id,
200
  response_length=message.response_length or "medium"
201
  )
202
-
203
- # Add RAG information to response
204
- if result["type"] == "persona_responses":
205
- # Count how many personas used documents
206
- personas_with_docs = sum(1 for r in result["responses"] if r.get("used_documents", False))
207
- total_chunks_used = sum(r.get("document_chunks_used", 0) for r in result["responses"])
208
 
209
- result["rag_info"] = {
210
- "personas_using_documents": personas_with_docs,
211
- "total_document_chunks_used": total_chunks_used,
212
- "rag_enabled": True
 
 
 
213
  }
214
-
215
- return result
216
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  except Exception as e:
218
- logger.error(f"Error in chat-sequential: {str(e)}")
219
- return {
220
- "type": "error",
221
- "message": "I encountered an error processing your request. Please try again.",
222
- "error": str(e),
223
- "rag_enabled": False
224
- }
225
 
226
- # Individual advisor endpoint (SAME INTERFACE)
227
  @router.post("/chat/{persona_id}")
228
  async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
229
  """Chat with a specific advisor - SAME INTERFACE"""
@@ -241,17 +263,33 @@ async def chat_with_specific_advisor(persona_id: str, input: UserInput, request:
241
  session_id=session_id
242
  )
243
 
244
- if result["type"] == "single_persona_response":
 
 
245
  persona_data = result["persona"]
246
  return {
247
  "persona": persona_data["persona_name"],
248
  "persona_id": persona_data["persona_id"],
249
  "response": persona_data["response"]
250
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  else:
 
252
  return {
253
  "persona": "System",
254
- "response": result.get("message", "I'm having trouble generating a response right now. Please try again.")
255
  }
256
 
257
  except HTTPException:
@@ -310,51 +348,37 @@ async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
310
  @router.post("/upload-document")
311
  async def upload_document(file: UploadFile = File(...), request: Request = None):
312
  """
313
- Upload document with RAG integration - ENHANCED VERSION
314
-
315
- Now documents are:
316
- 1. Chunked intelligently
317
- 2. Embedded and stored in ChromaDB
318
- 3. Available for semantic retrieval
319
- 4. NOT stored in session context (saves memory)
320
  """
321
- # Validate file type
322
- if file.content_type not in [
323
- "application/pdf",
324
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
325
- "text/plain"
326
- ]:
327
- raise HTTPException(status_code=400, detail="Unsupported file type.")
328
-
329
  try:
330
- # Get session using compatibility layer
331
  session_id = get_or_create_session_for_request(request)
332
  session = session_manager.get_session(session_id)
333
 
334
- file_bytes = await file.read()
335
-
336
- # Simple size check
337
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
338
- if len(file_bytes) > MAX_FILE_SIZE:
339
- raise HTTPException(status_code=400, detail="Upload exceeds file size limit (10 MB).")
340
 
341
- # Extract text content
 
 
342
  content = extract_text_from_file(file_bytes, file.content_type)
343
  if not content.strip():
344
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
345
 
346
- # Get RAG manager
347
  rag_manager = get_rag_manager()
348
 
349
  # Determine file type for metadata
350
  file_type_map = {
351
  "application/pdf": "pdf",
352
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
353
  "text/plain": "txt"
354
  }
355
  file_type = file_type_map.get(file.content_type, "unknown")
356
 
357
- # Add document to vector database instead of session context
358
  rag_result = rag_manager.add_document(
359
  content=content,
360
  filename=file.filename,
@@ -363,27 +387,46 @@ async def upload_document(file: UploadFile = File(...), request: Request = None)
363
  )
364
 
365
  if not rag_result["success"]:
366
- raise HTTPException(status_code=500, detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}")
 
 
 
367
 
368
- # Add just the filename to session for tracking (not the full content)
369
  session.uploaded_files.append(file.filename)
370
  session.total_upload_size += len(file_bytes)
371
 
372
- # Add a brief document reference to session messages (not full content)
373
- session.append_message("system", f"Document uploaded: {file.filename} ({rag_result['chunks_created']} chunks, {rag_result['total_tokens']} tokens)")
 
 
 
 
 
 
 
 
 
374
 
375
  return {
376
- "message": "Document uploaded and processed successfully.",
377
  "filename": file.filename,
378
- "chunks_created": rag_result["chunks_created"],
379
- "total_tokens": rag_result["total_tokens"],
380
- "processing_method": "RAG_vector_storage"
 
 
 
 
 
 
 
381
  }
382
 
383
  except HTTPException:
384
  raise
385
  except Exception as e:
386
- logger.error(f"Error uploading document: {str(e)}")
387
  raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
388
 
389
 
@@ -512,7 +555,7 @@ async def search_documents(request: Request, query: str = Body(..., embed=True),
512
 
513
  # Get persona context for search enhancement
514
  persona_contexts = {
515
- "methodist": "methodology research design analysis",
516
  "theorist": "theory theoretical framework conceptual",
517
  "pragmatist": "practical application implementation"
518
  }
@@ -663,6 +706,137 @@ async def chat_with_specific_persona(persona_id: str, message: ChatMessage, requ
663
  "message": f"Error chatting with {persona_id}: {str(e)}",
664
  "persona_id": persona_id
665
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  # Also add a debug endpoint to check RAG status:
668
 
 
133
  def _get_persona_fallback(persona_id: str) -> str:
134
  """Get persona-specific fallback responses"""
135
  fallbacks = {
136
+ "methodologist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?",
137
  "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?",
138
  "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?"
139
  }
 
188
 
189
  # Main chat endpoint (SAME INTERFACE, improved backend)
190
  @router.post("/chat-sequential")
191
+ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
192
+ """
193
+ Enhanced sequential chat with improved document awareness and response quality
194
+ """
195
  try:
196
+ # Get or create session
197
  session_id = get_or_create_session_for_request(request, message.session_id)
198
 
199
+ # Process message with enhanced orchestrator
200
+ orchestrator_result = await chat_orchestrator.process_message_with_enhanced_context(
201
  user_input=message.user_input,
202
  session_id=session_id,
203
  response_length=message.response_length or "medium"
204
  )
205
+
206
+ if orchestrator_result["status"] == "success":
207
+ responses = orchestrator_result["responses"]
 
 
 
208
 
209
+ # Enhanced RAG information for debugging and user feedback
210
+ rag_info = {
211
+ "total_personas": len(responses),
212
+ "personas_using_documents": sum(1 for r in responses if r.get("used_documents", False)),
213
+ "total_document_chunks_used": sum(r.get("document_chunks_used", 0) for r in responses),
214
+ "document_aware_query": orchestrator_result.get("document_references_detected", False),
215
+ "available_documents": orchestrator_result.get("available_documents", [])
216
  }
217
+
218
+ return {
219
+ "type": "persona_responses",
220
+ "responses": responses,
221
+ "rag_info": rag_info,
222
+ "session_id": session_id,
223
+ "query_analysis": {
224
+ "document_references_found": orchestrator_result.get("document_references_detected", False),
225
+ "personas_with_high_quality_context": [
226
+ r["persona_id"] for r in responses
227
+ if r.get("context_quality") == "high"
228
+ ]
229
+ }
230
+ }
231
+ else:
232
+ # Fallback response with helpful guidance
233
+ return {
234
+ "type": "clarification_needed",
235
+ "message": orchestrator_result.get("message", "I need more information to help you effectively."),
236
+ "suggestions": orchestrator_result.get("suggestions", [
237
+ "Could you provide more details about your specific question?",
238
+ "Consider uploading relevant documents to get more targeted advice.",
239
+ "Try asking about a specific aspect of your research."
240
+ ]),
241
+ "session_id": session_id,
242
+ "rag_info": {"total_personas": 0, "personas_using_documents": 0}
243
+ }
244
+
245
  except Exception as e:
246
+ logger.error(f"Error in enhanced sequential chat: {str(e)}")
247
+ raise HTTPException(status_code=500, detail=f"Error processing request: {str(e)}")
 
 
 
 
 
248
 
 
249
  @router.post("/chat/{persona_id}")
250
  async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
251
  """Chat with a specific advisor - SAME INTERFACE"""
 
263
  session_id=session_id
264
  )
265
 
266
+ # FIX: Handle the actual response structure from orchestrator
267
+ if result.get("type") == "single_persona_response" and "persona" in result:
268
+ # New expected structure
269
  persona_data = result["persona"]
270
  return {
271
  "persona": persona_data["persona_name"],
272
  "persona_id": persona_data["persona_id"],
273
  "response": persona_data["response"]
274
  }
275
+ elif "persona_id" in result and "response" in result:
276
+ # Current actual structure from orchestrator
277
+ return {
278
+ "persona": result["persona_name"],
279
+ "persona_id": result["persona_id"],
280
+ "response": result["response"]
281
+ }
282
+ elif result.get("type") == "error" or "error" in result:
283
+ # Error handling
284
+ return {
285
+ "persona": "System",
286
+ "response": result.get("error", "I'm having trouble generating a response right now. Please try again.")
287
+ }
288
  else:
289
+ # Fallback
290
  return {
291
  "persona": "System",
292
+ "response": "I'm having trouble generating a response right now. Please try again."
293
  }
294
 
295
  except HTTPException:
 
348
  @router.post("/upload-document")
349
  async def upload_document(file: UploadFile = File(...), request: Request = None):
350
  """
351
+ Enhanced document upload with better metadata tracking and user feedback
 
 
 
 
 
 
352
  """
 
 
 
 
 
 
 
 
353
  try:
354
+ # Get or create session
355
  session_id = get_or_create_session_for_request(request)
356
  session = session_manager.get_session(session_id)
357
 
358
+ # Validate file
 
 
359
  MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
360
+ if file.size and file.size > MAX_FILE_SIZE:
361
+ raise HTTPException(status_code=413, detail="File size exceeds 10MB limit")
362
 
363
+
364
+ # Read and validate file content
365
+ file_bytes = await file.read()
366
  content = extract_text_from_file(file_bytes, file.content_type)
367
  if not content.strip():
368
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
369
 
370
+ # Get enhanced RAG manager
371
  rag_manager = get_rag_manager()
372
 
373
  # Determine file type for metadata
374
  file_type_map = {
375
  "application/pdf": "pdf",
376
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
377
  "text/plain": "txt"
378
  }
379
  file_type = file_type_map.get(file.content_type, "unknown")
380
 
381
+ # Add document to enhanced vector database
382
  rag_result = rag_manager.add_document(
383
  content=content,
384
  filename=file.filename,
 
387
  )
388
 
389
  if not rag_result["success"]:
390
+ raise HTTPException(
391
+ status_code=500,
392
+ detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}"
393
+ )
394
 
395
+ # Update session tracking
396
  session.uploaded_files.append(file.filename)
397
  session.total_upload_size += len(file_bytes)
398
 
399
+ # Add enhanced document reference to session messages
400
+ doc_metadata = rag_result.get("document_metadata", {})
401
+ doc_title = doc_metadata.get("title", file.filename)
402
+
403
+ session.append_message(
404
+ "system",
405
+ f"Document uploaded: '{doc_title}' ({file.filename}) - "
406
+ f"{rag_result['chunks_created']} sections processed, "
407
+ f"~{rag_result['total_tokens']} tokens analyzed. "
408
+ f"You can now ask questions about this document by referencing it by name."
409
+ )
410
 
411
  return {
412
+ "message": f"Document '{file.filename}' uploaded and processed successfully.",
413
  "filename": file.filename,
414
+ "document_title": doc_title,
415
+ "chunks_created": rag_result['chunks_created'],
416
+ "total_tokens": rag_result['total_tokens'],
417
+ "file_type": file_type,
418
+ "can_reference_by_name": True,
419
+ "suggestions": [
420
+ f"Try asking: 'What methodology does my {file.filename} propose?'",
421
+ f"Or: 'What are the key findings in {doc_title}?'",
422
+ f"Or: 'Compare the approach in my document with current best practices'"
423
+ ]
424
  }
425
 
426
  except HTTPException:
427
  raise
428
  except Exception as e:
429
+ logger.error(f"Error processing document upload: {str(e)}")
430
  raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
431
 
432
 
 
555
 
556
  # Get persona context for search enhancement
557
  persona_contexts = {
558
+ "methodologist": "methodology research design analysis",
559
  "theorist": "theory theoretical framework conceptual",
560
  "pragmatist": "practical application implementation"
561
  }
 
706
  "message": f"Error chatting with {persona_id}: {str(e)}",
707
  "persona_id": persona_id
708
  }
709
+
710
+ @router.get("/debug/enhanced-personas")
711
+ async def debug_enhanced_personas(request: Request):
712
+ """
713
+ Enhanced debug endpoint with document context information
714
+ """
715
+ try:
716
+ session_id = get_or_create_session_for_request(request)
717
+ session = session_manager.get_session(session_id)
718
+
719
+ # Get enhanced RAG statistics
720
+ rag_manager = get_rag_manager()
721
+ rag_stats = rag_manager.get_document_stats(session_id)
722
+
723
+ # Analyze document awareness capabilities
724
+ document_analysis = {}
725
+ if rag_stats.get("documents"):
726
+ for doc in rag_stats["documents"]:
727
+ document_analysis[doc["filename"]] = {
728
+ "chunks_available": doc["chunks"],
729
+ "estimated_tokens": doc["estimated_tokens"],
730
+ "sections_identified": doc["sections"],
731
+ "content_types_detected": {
732
+ "has_methodology": doc.get("has_methodology", False),
733
+ "has_theory": doc.get("has_theory", False),
734
+ "has_references": doc.get("has_references", False)
735
+ }
736
+ }
737
+
738
+ return {
739
+ "personas": {
740
+ pid: {
741
+ "name": persona.name,
742
+ "expertise_area": persona.name.split(" - ")[1] if " - " in persona.name else "General",
743
+ "prompt_quality": "enhanced" if len(persona.system_prompt) > 500 else "basic",
744
+ "document_handling_enabled": "document awareness" in persona.system_prompt.lower(),
745
+ "retrieval_keywords": chat_orchestrator._get_enhanced_persona_context_keywords(pid)[:100] + "...",
746
+ "temperature": getattr(persona, 'temperature', 5)
747
+ } for pid, persona in chat_orchestrator.personas.items()
748
+ },
749
+ "session_info": {
750
+ "context_length": len(session.messages),
751
+ "uploaded_files": session.uploaded_files,
752
+ "rag_stats": rag_stats,
753
+ "document_analysis": document_analysis
754
+ },
755
+ "system_capabilities": {
756
+ "document_name_recognition": True,
757
+ "cross_document_analysis": True,
758
+ "persona_specialized_retrieval": True,
759
+ "enhanced_attribution": True,
760
+ "query_document_detection": True
761
+ },
762
+ "current_provider": current_provider,
763
+ "rag_enabled": True,
764
+ "enhancement_level": "advanced"
765
+ }
766
+ except Exception as e:
767
+ logger.error(f"Error in enhanced debug endpoint: {str(e)}")
768
+ return {
769
+ "error": str(e),
770
+ "enhancement_level": "error",
771
+ "rag_enabled": False
772
+ }
773
+
774
+ @router.get("/document-insights/{filename}")
775
+ async def get_document_insights(filename: str, request: Request):
776
+ """
777
+ NEW ENDPOINT: Get insights about a specific uploaded document
778
+ """
779
+ try:
780
+ session_id = get_or_create_session_for_request(request)
781
+ rag_manager = get_rag_manager()
782
+
783
+ # Get document statistics
784
+ stats = rag_manager.get_document_stats(session_id)
785
+
786
+ # Find the specific document
787
+ document_info = None
788
+ for doc in stats.get("documents", []):
789
+ if doc["filename"] == filename:
790
+ document_info = doc
791
+ break
792
+
793
+ if not document_info:
794
+ raise HTTPException(status_code=404, detail=f"Document {filename} not found")
795
+
796
+ # Get a sample of content from this document
797
+ results = rag_manager.collection.get(
798
+ where={"session_id": session_id, "filename": filename},
799
+ limit=3,
800
+ include=["documents", "metadatas"]
801
+ )
802
+
803
+ sample_sections = []
804
+ if results["documents"]:
805
+ for doc, metadata in zip(results["documents"], results["metadatas"]):
806
+ sample_sections.append({
807
+ "section": metadata.get("document_section", "unknown"),
808
+ "content_preview": doc[:200] + "..." if len(doc) > 200 else doc,
809
+ "keywords": metadata.get("keywords", "")
810
+ })
811
+
812
+ return {
813
+ "filename": filename,
814
+ "document_title": document_info.get("title", filename),
815
+ "file_type": document_info.get("file_type", "unknown"),
816
+ "statistics": {
817
+ "total_chunks": document_info["chunks"],
818
+ "estimated_tokens": document_info["estimated_tokens"],
819
+ "sections_identified": document_info["sections"]
820
+ },
821
+ "content_analysis": {
822
+ "has_methodology": document_info.get("has_methodology", False),
823
+ "has_theory": document_info.get("has_theory", False),
824
+ "has_references": document_info.get("has_references", False)
825
+ },
826
+ "sample_sections": sample_sections,
827
+ "suggested_queries": [
828
+ f"What methodology does my {filename} propose?",
829
+ f"What are the key theoretical concepts in {filename}?",
830
+ f"What are the main findings in my {document_info.get('title', filename)}?",
831
+ f"How can I improve the approach described in {filename}?"
832
+ ]
833
+ }
834
+
835
+ except HTTPException:
836
+ raise
837
+ except Exception as e:
838
+ logger.error(f"Error getting document insights: {str(e)}")
839
+ raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}")
840
 
841
  # Also add a debug endpoint to check RAG status:
842
 
multi_llm_chatbot_backend/app/api/routes_old.py CHANGED
@@ -95,7 +95,7 @@ def _is_valid_response(response: str, persona_id: str) -> bool:
95
  def _get_persona_fallback(persona_id: str) -> str:
96
  """Get persona-specific fallback responses"""
97
  fallbacks = {
98
- "methodist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?",
99
  "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?",
100
  "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?"
101
  }
 
95
  def _get_persona_fallback(persona_id: str) -> str:
96
  """Get persona-specific fallback responses"""
97
  fallbacks = {
98
+ "methodologist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?",
99
  "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?",
100
  "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?"
101
  }
multi_llm_chatbot_backend/app/core/context_manager.py CHANGED
@@ -167,7 +167,7 @@ class ContextManager:
167
  "role": "user",
168
  "parts": [{"text": content}]
169
  })
170
- elif role in ['assistant', 'methodist', 'theorist', 'pragmatist']:
171
  formatted.append({
172
  "role": "model",
173
  "parts": [{"text": content}]
 
167
  "role": "user",
168
  "parts": [{"text": content}]
169
  })
170
+ elif role in ['assistant', 'methodologist', 'theorist', 'pragmatist']:
171
  formatted.append({
172
  "role": "model",
173
  "parts": [{"text": content}]
multi_llm_chatbot_backend/app/core/improved_orchestrator.py CHANGED
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
14
 
15
  class ImprovedChatOrchestrator:
16
  """
17
- Improved orchestrator with proper session management and context handling
18
  """
19
 
20
  def __init__(self):
@@ -55,159 +55,155 @@ class ImprovedChatOrchestrator:
55
  if needs_clarification:
56
  # Generate clarification question
57
  clarification = await self._generate_clarification_question(session)
58
- session.append_message("orchestrator", clarification)
59
 
60
  return {
61
- "type": "clarification",
62
- "session_id": session.session_id,
63
  "message": clarification,
64
- "context_summary": self.context_manager.get_context_summary(session.messages)
 
65
  }
66
 
67
  # Generate responses from all personas
68
  responses = await self._generate_persona_responses(session, response_length)
69
 
70
  return {
71
- "type": "persona_responses",
72
- "session_id": session.session_id,
73
  "responses": responses,
74
- "context_summary": self.context_manager.get_context_summary(session.messages)
75
  }
76
 
77
  except Exception as e:
78
  logger.error(f"Error in process_message: {str(e)}")
79
  return {
80
- "type": "error",
81
- "session_id": session_id,
82
- "message": "I encountered an error processing your request. Please try again.",
83
  "error": str(e)
84
  }
85
-
86
- async def chat_with_persona(self,
87
- user_input: str,
88
- persona_id: str,
89
- session_id: Optional[str] = None,
90
- response_length: str = "medium") -> Dict[str, Any]:
91
  """
92
- Chat with a specific persona - ENHANCED WITH RAG
93
  """
94
  try:
95
- if persona_id not in self.personas:
96
- return {
97
- "type": "error",
98
- "message": f"Persona '{persona_id}' not found"
99
- }
100
-
101
- # Get or create session
102
  session = self.session_manager.get_session(session_id)
103
 
104
- # Add user message
105
  session.append_message("user", user_input)
106
 
107
- # Generate response from specific persona with RAG
108
- persona = self.personas[persona_id]
109
- response = await self._generate_single_persona_response(session, persona, response_length)
110
 
111
- # Add persona response to session
112
- session.append_message(persona_id, response['response'])
 
 
 
 
 
113
 
114
  return {
115
- "type": "single_persona_response",
116
- "session_id": session.session_id,
117
- "persona": response,
118
- "context_summary": self.context_manager.get_context_summary(session.messages),
119
- "rag_info": {
120
- "used_documents": response.get("used_documents", False),
121
- "document_chunks_used": response.get("document_chunks_used", 0)
122
- }
123
  }
124
 
125
  except Exception as e:
126
- logger.error(f"Error in chat_with_persona: {str(e)}")
127
  return {
128
- "type": "error",
129
- "session_id": session_id,
130
- "message": "I encountered an error generating a response. Please try again.",
131
- "error": str(e)
132
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- def _needs_clarification(self, session, user_input: str) -> bool:
135
  """
136
- Determine if we need clarification - SIMPLIFIED VERSION
137
  """
138
- try:
139
- # Simple heuristics for clarification
140
- if len(user_input.strip()) < 10:
141
- return True
142
-
143
- # Check if it's a greeting or very general question
144
- greeting_words = ['hi', 'hello', 'hey', 'good morning', 'good afternoon']
145
- if any(word in user_input.lower() for word in greeting_words):
146
- return True
147
-
148
- # Very vague questions
149
- vague_words = ['help', 'advice', 'what should i do', 'i need help']
150
- if len([word for word in vague_words if word in user_input.lower()]) > 0 and len(user_input.split()) < 8:
151
- return True
152
-
153
  return False
154
-
155
- except Exception as e:
156
- logger.error(f"Error in clarification check: {str(e)}")
157
- return False # Default to no clarification needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  async def _generate_clarification_question(self, session: ConversationContext) -> str:
160
  """
161
- Generate a clarification question based on context
162
  """
163
- user_messages = session.get_messages_by_role("user")
164
- if not user_messages:
165
- return "What specific aspect of your PhD journey would you like guidance on?"
166
-
167
- latest_message = user_messages[-1]['content']
168
-
169
- # Extract what information we might be missing
170
- missing_info = []
171
-
172
- # Check for research area
173
- research_keywords = ["computer science", "biology", "psychology", "physics", "chemistry",
174
- "engineering", "literature", "history", "mathematics", "sociology"]
175
- if not any(keyword in latest_message.lower() for keyword in research_keywords):
176
- missing_info.append("research area")
177
-
178
- # Check for specific question type
179
- question_keywords = ["methodology", "theory", "writing", "analysis", "data", "literature review"]
180
- if not any(keyword in latest_message.lower() for keyword in question_keywords):
181
- missing_info.append("specific aspect")
182
-
183
- # Check for academic stage
184
- stage_keywords = ["first year", "second year", "third year", "qualifying", "dissertation",
185
- "defense", "proposal", "coursework"]
186
- if not any(keyword in latest_message.lower() for keyword in stage_keywords):
187
- missing_info.append("PhD stage")
188
-
189
- # Generate appropriate clarification question
190
- if "research area" in missing_info:
191
- return "What field or discipline are you studying in?"
192
- elif "specific aspect" in missing_info:
193
- return "What specific aspect of your research would you like guidance on?"
194
- elif "PhD stage" in missing_info:
195
- return "What stage of your PhD program are you currently in?"
196
- else:
197
- return "Could you provide more details about your specific situation or question?"
198
 
199
- async def _generate_persona_responses(self, session, response_length: str = "medium"):
200
  """
201
- Generate responses from all personas with RAG integration
202
-
203
- Each persona gets persona-specific document retrieval for more targeted responses
204
  """
205
  responses = []
206
 
207
  for persona_id, persona in self.personas.items():
208
- logger.info(f"Generating response for {persona_id} with RAG")
209
 
210
- # Generate persona response with RAG
211
  response_data = await self._generate_single_persona_response(session, persona, response_length)
212
 
213
  # Add persona response to session context
@@ -219,25 +215,21 @@ class ImprovedChatOrchestrator:
219
 
220
  async def _generate_single_persona_response(self, session, persona, response_length: str = "medium"):
221
  """
222
- Generate response from a single persona with RAG-enhanced context - FIXED VERSION
223
  """
224
  try:
225
- # Get conversation context (recent messages only for efficiency)
226
- recent_messages = session.messages[-5:] if len(session.messages) > 5 else session.messages
227
-
228
  # Get the user's latest message for document retrieval
229
  user_message = ""
230
  try:
231
- # Use the new method to get latest user message
232
  user_message = session.get_latest_user_message() or ""
233
  except AttributeError:
234
  # Fallback: manually find latest user message
235
- for msg in reversed(recent_messages):
236
  if msg.get('role') == 'user':
237
  user_message = msg.get('content', '')
238
  break
239
 
240
- # Retrieve relevant document context using RAG
241
  document_context = ""
242
  if user_message:
243
  document_context = await self._retrieve_relevant_documents(
@@ -247,68 +239,298 @@ class ImprovedChatOrchestrator:
247
  )
248
 
249
  # Build enhanced context for the LLM
250
- enhanced_context = []
251
-
252
- # Add document context at the beginning if available
253
- if document_context:
254
- enhanced_context.append({
255
- "role": "system",
256
- "content": f"{document_context}Based on the above document context and conversation history, please respond as {persona.name}."
257
- })
258
-
259
- # Add recent conversation messages
260
- enhanced_context.extend(recent_messages)
261
 
262
  # Generate response with enhanced context
263
  response = await persona.respond(enhanced_context, response_length)
264
 
265
- # Validate response
266
  if not self._is_valid_response(response, persona.id):
267
  logger.warning(f"Invalid response from {persona.id}, using fallback")
268
- response = self._get_fallback_response(persona.id)
 
 
 
 
269
 
270
  return {
271
  "persona_id": persona.id,
272
  "persona_name": persona.name,
273
  "response": response,
274
- "error": False,
275
- "used_documents": bool(document_context),
276
- "document_chunks_used": document_context.count("[Document") if document_context else 0
 
277
  }
278
 
279
  except Exception as e:
280
- logger.error(f"Error in _generate_single_persona_response for {persona.id}: {str(e)}")
281
  return {
282
  "persona_id": persona.id,
283
  "persona_name": persona.name,
284
- "response": self._get_fallback_response(persona.id),
285
- "error": True,
286
  "used_documents": False,
287
- "document_chunks_used": 0
 
 
288
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  def _is_valid_response(self, response: str, persona_id: str) -> bool:
291
  """Validate response quality"""
292
- if not response or len(response.strip()) < 10:
293
- return False
294
-
295
- if len(response) > 2000: # Too long
296
  return False
297
 
298
  # Check for AI confusion indicators
299
  confusion_indicators = [
300
  f"Thank you, Dr. {persona_id.title()}",
301
  "Assistant:",
302
- f"Dr. {persona_id.title()}:",
303
  "excellent discussion, Assistant"
304
  ]
305
 
306
  return not any(indicator in response for indicator in confusion_indicators)
307
 
308
- def _get_fallback_response(self, persona_id: str) -> str:
309
- """Get persona-specific fallback response"""
310
  fallbacks = {
311
- "methodist": "Let's focus on your research methodology. What specific methodological approach are you considering?",
312
  "theorist": "I'd like to explore the theoretical foundation of your work. What conceptual framework guides your research?",
313
  "pragmatist": "Let's take a practical approach. What's the most pressing decision you need to make about your research right now?"
314
  }
@@ -340,74 +562,42 @@ class ImprovedChatOrchestrator:
340
  """Delete a session completely"""
341
  return self.session_manager.delete_session(session_id)
342
 
 
343
  def _get_persona_context_keywords(self, persona_id: str) -> str:
344
  """
345
- Get persona-specific keywords for enhanced document retrieval
346
-
347
- This ensures each advisor gets the most relevant document chunks
348
- based on their specialization
349
  """
350
- persona_keywords = {
351
- "methodist": "methodology research design analysis methods data collection sampling validity reliability statistical approach quantitative qualitative",
352
- "theorist": "theory theoretical framework conceptual model literature review philosophy epistemology ontology paradigm abstract concepts",
353
- "pragmatist": "practical application implementation action steps next steps recommendation solution strategy timeline concrete advice"
354
- }
355
- return persona_keywords.get(persona_id, "")
356
 
357
- async def _retrieve_relevant_documents(self, user_input: str, session_id: str, persona_id: str = "") -> str:
358
  """
359
- Retrieve relevant document chunks using RAG - FIXED VERSION
360
  """
361
  try:
362
- rag_manager = get_rag_manager()
363
-
364
- # Get persona-specific context for better retrieval
365
- persona_context = self._get_persona_context_keywords(persona_id)
 
 
366
 
367
- # Search for relevant chunks
368
- relevant_chunks = rag_manager.search_documents(
369
- query=user_input,
370
- session_id=session_id,
371
- persona_context=persona_context,
372
- n_results=3 # Limit to top 3 most relevant chunks
373
- )
374
 
375
- logger.info(f"Retrieved {len(relevant_chunks)} total chunks for {persona_id}")
 
376
 
377
- if not relevant_chunks:
378
- logger.info(f"No relevant documents found for query: {user_input[:50]}...")
379
- return ""
380
 
381
- # Format retrieved content - LOWERED THRESHOLD
382
- context_parts = []
383
- for i, chunk in enumerate(relevant_chunks, 1):
384
- filename = chunk["metadata"].get("filename", "Unknown")
385
- relevance = chunk["relevance_score"]
386
- distance = chunk.get("distance", "unknown")
387
- text = chunk["text"]
388
-
389
- logger.info(f"Chunk {i} for {persona_id}: relevance={relevance:.3f}, distance={distance}")
390
-
391
- # LOWERED threshold from 0.3 to 0.1 to allow more chunks through
392
- if relevance > 0.1:
393
- context_parts.append(
394
- f"[Document {i}: {filename} (relevance: {relevance:.3f})]\n{text[:500]}..." # Limit text length
395
- )
396
- logger.info(f"Including chunk {i} for {persona_id} (relevance: {relevance:.3f})")
397
- else:
398
- logger.info(f"Excluding chunk {i} for {persona_id} (relevance too low: {relevance:.3f})")
399
-
400
- if context_parts:
401
- retrieved_context = "\n\n".join(context_parts)
402
- logger.info(f"Using {len(context_parts)} relevant chunks for {persona_id}")
403
- return f"RELEVANT DOCUMENT CONTEXT:\n{retrieved_context}\n\n"
404
-
405
- logger.info(f"No chunks met relevance threshold for {persona_id}")
406
- return ""
407
 
408
  except Exception as e:
409
- logger.error(f"Error retrieving documents for {persona_id}: {str(e)}")
410
- return ""
 
 
 
411
 
412
 
413
  async def get_top_personas(self, session_id: str, k: int = 3) -> List[str]:
 
14
 
15
  class ImprovedChatOrchestrator:
16
  """
17
+ Enhanced orchestrator with document awareness and improved context handling
18
  """
19
 
20
  def __init__(self):
 
55
  if needs_clarification:
56
  # Generate clarification question
57
  clarification = await self._generate_clarification_question(session)
58
+ session.append_message("system", f"Clarification request: {clarification}")
59
 
60
  return {
61
+ "status": "clarification_needed",
 
62
  "message": clarification,
63
+ "suggestions": self._get_clarification_suggestions(),
64
+ "session_id": session.session_id
65
  }
66
 
67
  # Generate responses from all personas
68
  responses = await self._generate_persona_responses(session, response_length)
69
 
70
  return {
71
+ "status": "success",
 
72
  "responses": responses,
73
+ "session_id": session.session_id
74
  }
75
 
76
  except Exception as e:
77
  logger.error(f"Error in process_message: {str(e)}")
78
  return {
79
+ "status": "error",
80
+ "message": "I'm having technical difficulties. Please try again.",
 
81
  "error": str(e)
82
  }
83
+
84
+ async def process_message_with_enhanced_context(self, user_input: str, session_id: str, response_length: str = "medium"):
 
 
 
 
85
  """
86
+ Enhanced message processing with document awareness and better context management
87
  """
88
  try:
89
+ # Get session
 
 
 
 
 
 
90
  session = self.session_manager.get_session(session_id)
91
 
92
+ # Add user message to session
93
  session.append_message("user", user_input)
94
 
95
+ # Detect document references in the query
96
+ document_references = self._extract_document_references_from_query(user_input)
 
97
 
98
+ # Get available documents for this session
99
+ rag_manager = get_rag_manager()
100
+ doc_stats = rag_manager.get_document_stats(session_id)
101
+ available_documents = [doc["filename"] for doc in doc_stats.get("documents", [])]
102
+
103
+ # Generate enhanced persona responses
104
+ responses = await self._generate_persona_responses(session, response_length)
105
 
106
  return {
107
+ "status": "success",
108
+ "responses": responses,
109
+ "document_references_detected": bool(document_references),
110
+ "available_documents": available_documents,
111
+ "session_id": session_id
 
 
 
112
  }
113
 
114
  except Exception as e:
115
+ logger.error(f"Error in enhanced message processing: {str(e)}")
116
  return {
117
+ "status": "error",
118
+ "message": "I'm having technical difficulties processing your request.",
119
+ "suggestions": ["Please try rephrasing your question.", "Check if your documents uploaded successfully."]
 
120
  }
121
+
122
+ def _extract_document_references_from_query(self, query: str) -> List[str]:
123
+ """Extract document references from user query"""
124
+ query_lower = query.lower()
125
+ references = []
126
+
127
+ # Common document reference patterns
128
+ patterns = [
129
+ r"(?:my|the|in)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt))", # specific files
130
+ r"(?:my|the)\s+(dissertation|thesis|proposal|chapter|manuscript)", # document types
131
+ r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:chapter|section))", # sections
132
+ ]
133
+
134
+ for pattern in patterns:
135
+ matches = re.findall(pattern, query_lower)
136
+ references.extend(matches)
137
+
138
+ return references[:3] # Limit to first 3 references
139
 
140
+ def _needs_clarification(self, session: ConversationContext, user_input: str) -> bool:
141
  """
142
+ Determine if the user input needs clarification
143
  """
144
+ # If this is not the first message, probably don't need clarification
145
+ user_messages = [msg for msg in session.messages if msg.get('role') == 'user']
146
+ if len(user_messages) > 1:
 
 
 
 
 
 
 
 
 
 
 
 
147
  return False
148
+
149
+ # Check for vague patterns
150
+ vague_patterns = [
151
+ r"^(help|advice|guidance|assistance)$",
152
+ r"i'?m (stuck|lost|confused|not sure)",
153
+ r"(what should i|how do i|where do i start)",
154
+ r"i need (help|advice|guidance)",
155
+ r"(any|some) (advice|suggestions|ideas)"
156
+ ]
157
+
158
+ user_lower = user_input.lower().strip()
159
+
160
+ for pattern in vague_patterns:
161
+ if re.search(pattern, user_lower):
162
+ return True
163
+
164
+ # Check if input is too short and vague
165
+ if len(user_input.split()) < 6 and not any(
166
+ keyword in user_lower for keyword in
167
+ ['methodology', 'theory', 'data', 'analysis', 'research', 'thesis', 'dissertation']
168
+ ):
169
+ return True
170
+
171
+ return False
172
 
173
  async def _generate_clarification_question(self, session: ConversationContext) -> str:
174
  """
175
+ Generate a clarification question based on the conversation context
176
  """
177
+ # Simple clarification questions based on common PhD needs
178
+ clarification_options = [
179
+ "What specific aspect of your PhD research would you like guidance on?",
180
+ "Are you looking for help with methodology, theory, writing, or something else?",
181
+ "What stage of your PhD program are you currently in?",
182
+ "What's the main challenge you're facing with your research right now?"
183
+ ]
184
+
185
+ # Return the first option for now (could be made smarter with AI)
186
+ return clarification_options[0]
187
+
188
+ def _get_clarification_suggestions(self) -> List[str]:
189
+ """Get suggestions for clarification"""
190
+ return [
191
+ "Ask about research methodology or design",
192
+ "Get help with theoretical frameworks",
193
+ "Request guidance on practical next steps",
194
+ "Upload a document for specific feedback"
195
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ async def _generate_persona_responses(self, session: ConversationContext, response_length: str = "medium"):
198
  """
199
+ Generate responses from all personas with enhanced RAG integration
 
 
200
  """
201
  responses = []
202
 
203
  for persona_id, persona in self.personas.items():
204
+ logger.info(f"Generating response for {persona_id} with enhanced RAG")
205
 
206
+ # Generate persona response with enhanced RAG
207
  response_data = await self._generate_single_persona_response(session, persona, response_length)
208
 
209
  # Add persona response to session context
 
215
 
216
  async def _generate_single_persona_response(self, session, persona, response_length: str = "medium"):
217
  """
218
+ Enhanced version - Generate response from a single persona with enhanced RAG integration
219
  """
220
  try:
 
 
 
221
  # Get the user's latest message for document retrieval
222
  user_message = ""
223
  try:
 
224
  user_message = session.get_latest_user_message() or ""
225
  except AttributeError:
226
  # Fallback: manually find latest user message
227
+ for msg in reversed(session.messages):
228
  if msg.get('role') == 'user':
229
  user_message = msg.get('content', '')
230
  break
231
 
232
+ # Retrieve relevant document context using enhanced RAG
233
  document_context = ""
234
  if user_message:
235
  document_context = await self._retrieve_relevant_documents(
 
239
  )
240
 
241
  # Build enhanced context for the LLM
242
+ enhanced_context = await self._build_enhanced_context_for_persona(
243
+ session, persona, user_message, document_context
244
+ )
 
 
 
 
 
 
 
 
245
 
246
  # Generate response with enhanced context
247
  response = await persona.respond(enhanced_context, response_length)
248
 
249
+ # Validate and improve response quality
250
  if not self._is_valid_response(response, persona.id):
251
  logger.warning(f"Invalid response from {persona.id}, using fallback")
252
+ response = self._get_persona_fallback(persona.id)
253
+
254
+ # Track document usage for debugging
255
+ used_documents = bool(document_context and len(document_context.strip()) > 100)
256
+ document_chunks_used = document_context.count("[Source:") if document_context else 0
257
 
258
  return {
259
  "persona_id": persona.id,
260
  "persona_name": persona.name,
261
  "response": response,
262
+ "used_documents": used_documents,
263
+ "document_chunks_used": document_chunks_used,
264
+ "response_length": response_length,
265
+ "context_quality": "high" if document_context else "conversation_only"
266
  }
267
 
268
  except Exception as e:
269
+ logger.error(f"Error generating response for {persona.id}: {str(e)}")
270
  return {
271
  "persona_id": persona.id,
272
  "persona_name": persona.name,
273
+ "response": f"I apologize, but I'm having technical difficulties. {self._get_persona_fallback(persona.id)}",
 
274
  "used_documents": False,
275
+ "document_chunks_used": 0,
276
+ "response_length": response_length,
277
+ "context_quality": "error"
278
  }
279
+
280
+ async def _retrieve_relevant_documents(self, user_input: str, session_id: str, persona_id: str = "") -> str:
281
+ """
282
+ Enhanced document retrieval with document awareness and better attribution
283
+ """
284
+ try:
285
+ rag_manager = get_rag_manager()
286
+
287
+ # Extract document hints from user query
288
+ document_hint = self._extract_document_hint_from_query(user_input)
289
+
290
+ # Get persona-specific context for better retrieval
291
+ persona_context = self._get_enhanced_persona_context_keywords(persona_id)
292
+
293
+ # Search for relevant chunks with document awareness
294
+ relevant_chunks = rag_manager.search_documents_with_context(
295
+ query=user_input,
296
+ session_id=session_id,
297
+ persona_context=persona_context,
298
+ n_results=6, # Increased for better context
299
+ document_hint=document_hint
300
+ )
301
+
302
+ logger.info(f"Retrieved {len(relevant_chunks)} chunks for {persona_id}")
303
+
304
+ if not relevant_chunks:
305
+ logger.info(f"No relevant documents found for query: {user_input[:50]}...")
306
+ return ""
307
+
308
+ # Format retrieved content with enhanced attribution
309
+ return self._format_document_context_with_attribution(relevant_chunks, persona_id)
310
+
311
+ except Exception as e:
312
+ logger.error(f"Error retrieving documents for {persona_id}: {str(e)}")
313
+ return ""
314
+
315
+ def _extract_document_hint_from_query(self, query: str) -> Optional[str]:
316
+ """
317
+ Extract document name hints from user queries
318
+ """
319
+ query_lower = query.lower()
320
+
321
+ # Common patterns for document references
322
+ document_indicators = [
323
+ r"(?:my|the|in|from)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt|doc))", # specific files
324
+ r"(?:my|the)\s+(dissertation|thesis|proposal|chapter|manuscript|paper)", # document types
325
+ r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:chapter|section|proposal))", # sections
326
+ r"(?:the|my)\s+([a-zA-Z_\-\s]+(?:document|file))", # generic documents
327
+ ]
328
+
329
+ for pattern in document_indicators:
330
+ matches = re.findall(pattern, query_lower)
331
+ if matches:
332
+ return matches[0].strip().replace(" ", "_")
333
+
334
+ return None
335
+
336
+ def _get_enhanced_persona_context_keywords(self, persona_id: str) -> str:
337
+ """
338
+ Enhanced persona-specific keywords for better document retrieval
339
+ """
340
+ enhanced_keywords = {
341
+ "methodologist": "methodology research design experimental approach data collection sampling validity reliability statistical analysis quantitative qualitative mixed-methods procedures protocol IRB ethics",
342
+ "theorist": "theory theoretical framework conceptual model literature review philosophy epistemology ontology paradigm abstract concepts hypothesis proposition postulate axiom",
343
+ "pragmatist": "practical application implementation action steps next steps recommendation solution strategy timeline concrete advice roadmap execution deliverables milestones"
344
+ }
345
+ return enhanced_keywords.get(persona_id, "")
346
+
347
+ def _format_document_context_with_attribution(self, chunks: List[Dict], persona_id: str) -> str:
348
+ """
349
+ Format document context with clear attribution and source information
350
+ """
351
+ if not chunks:
352
+ return ""
353
+
354
+ # Filter chunks by relevance (increased threshold for quality)
355
+ high_quality_chunks = [
356
+ chunk for chunk in chunks
357
+ if chunk.get("relevance_score", 0) > 0.4 # Increased from 0.3
358
+ ]
359
+
360
+ if not high_quality_chunks:
361
+ # If no high-quality chunks, take top 2 anyway but with lower confidence
362
+ high_quality_chunks = chunks[:2]
363
+
364
+ formatted_sections = []
365
+
366
+ # Group chunks by document for better organization
367
+ documents = {}
368
+ for chunk in high_quality_chunks:
369
+ doc_source = chunk.get("document_source", {})
370
+ filename = doc_source.get("filename", "unknown")
371
+
372
+ if filename not in documents:
373
+ documents[filename] = {
374
+ "title": doc_source.get("document_title", filename),
375
+ "chunks": []
376
+ }
377
+ documents[filename]["chunks"].append(chunk)
378
+
379
+ # Format each document's content
380
+ for filename, doc_data in documents.items():
381
+ doc_title = doc_data["title"]
382
+ doc_chunks = doc_data["chunks"]
383
+
384
+ formatted_sections.append(f"=== FROM DOCUMENT: {doc_title} ===")
385
+
386
+ for i, chunk in enumerate(doc_chunks):
387
+ doc_source = chunk.get("document_source", {})
388
+ section = doc_source.get("section", "unknown section")
389
+ position = doc_source.get("chunk_position", "unknown position")
390
+ relevance = chunk.get("relevance_score", 0)
391
+
392
+ chunk_intro = f"[Source: {section}, Part {position}, Relevance: {relevance:.2f}]"
393
+ formatted_sections.append(f"{chunk_intro}\n{chunk['text']}\n")
394
+
395
+ # Add context summary
396
+ total_docs = len(documents)
397
+ total_chunks = len(high_quality_chunks)
398
+
399
+ context_header = f"""
400
+ DOCUMENT CONTEXT FOR {persona_id.upper()} ANALYSIS:
401
+ Found {total_chunks} relevant passages from {total_docs} document(s).
402
+ Use this context to inform your response, and cite specific documents when referencing information.
403
+
404
+ """
405
+
406
+ formatted_context = context_header + "\n".join(formatted_sections)
407
+
408
+ # Add instructions specific to persona
409
+ persona_instructions = self._get_persona_document_instructions(persona_id)
410
+ formatted_context += f"\n\nSPECIAL INSTRUCTIONS FOR {persona_id.upper()}:\n{persona_instructions}"
411
+
412
+ return formatted_context
413
+
414
+ def _get_persona_document_instructions(self, persona_id: str) -> str:
415
+ """
416
+ Get persona-specific instructions for handling document context
417
+ """
418
+ instructions = {
419
+ "methodologist": """
420
+ When analyzing the document context:
421
+ - Focus on methodological rigor and research design elements
422
+ - Identify potential validity threats or methodological gaps
423
+ - Suggest specific improvements to research procedures
424
+ - Reference exact methodological frameworks mentioned in their documents
425
+ - Connect their approach to established research standards""",
426
+
427
+ "theorist": """
428
+ When analyzing the document context:
429
+ - Examine theoretical positioning and conceptual clarity
430
+ - Identify theoretical gaps or inconsistencies
431
+ - Suggest theoretical frameworks that align with their work
432
+ - Evaluate the coherence between theory and research questions
433
+ - Reference specific theoretical concepts mentioned in their documents""",
434
+
435
+ "pragmatist": """
436
+ When analyzing the document context:
437
+ - Extract actionable next steps from their current progress
438
+ - Identify immediate bottlenecks or decision points
439
+ - Prioritize tasks based on their timeline and constraints
440
+ - Translate theoretical concepts into practical implementation steps
441
+ - Reference specific deadlines or milestones mentioned in their documents"""
442
+ }
443
+
444
+ return instructions.get(persona_id, "Provide helpful guidance based on the document context.")
445
+
446
+ async def _build_enhanced_context_for_persona(self, session, persona, user_message: str, document_context: str) -> List[Dict[str, str]]:
447
+ """
448
+ Build enhanced context that properly integrates document information with conversation history
449
+ FIXED VERSION - No document hallucination when no documents exist
450
+ """
451
+ enhanced_context = []
452
+
453
+ # Get recent conversation history (last 6 messages for efficiency)
454
+ recent_messages = session.messages[-6:] if len(session.messages) > 6 else session.messages
455
+
456
+ # Check if we actually have meaningful document content
457
+ has_documents = bool(document_context and document_context.strip() and len(document_context.strip()) > 50)
458
+
459
+ # Build the system message with proper document awareness
460
+ if has_documents:
461
+ # Get list of uploaded documents
462
+ uploaded_docs = session.uploaded_files if hasattr(session, 'uploaded_files') else []
463
+ doc_list = ", ".join(uploaded_docs) if uploaded_docs else "uploaded documents"
464
+
465
+ system_message = f"""{persona.system_prompt}
466
+
467
+ CURRENT SESSION CONTEXT:
468
+ The student has uploaded the following documents: {doc_list}
469
+
470
+ {document_context}
471
+
472
+ IMPORTANT: When the student refers to "my document," "my dissertation," "my proposal," etc., they are referring to one of their uploaded documents. Use the document context above to understand which specific document they mean and reference it by name in your response.
473
+
474
+ Always cite your sources when referencing information from their documents using the format: "According to your [document_name]..." or "In your [section_name] from [document_name]..."
475
+ """
476
+
477
+ enhanced_context.append({
478
+ "role": "system",
479
+ "content": system_message
480
+ })
481
+ else:
482
+ # NO DOCUMENTS - Explicitly tell persona not to reference documents
483
+ system_message = f"""{persona.system_prompt}
484
+
485
+ IMPORTANT: The student has NOT uploaded any documents yet. Do not reference any specific documents, files, or assume you have access to their research materials.
486
+
487
+ If they mention "my document," "my dissertation," "my proposal," etc., you should:
488
+ 1. Acknowledge that you don't have access to their specific documents
489
+ 2. Ask them to upload the relevant files for more targeted advice
490
+ 3. Provide general guidance based on best practices in your area of expertise
491
+
492
+ Do NOT make up document names or pretend to have access to files that don't exist."""
493
+
494
+ enhanced_context.append({
495
+ "role": "system",
496
+ "content": system_message
497
+ })
498
+
499
+ # Add recent conversation history
500
+ for message in recent_messages:
501
+ if message.get('role') in ['user', 'assistant']:
502
+ enhanced_context.append({
503
+ "role": message['role'],
504
+ "content": message['content']
505
+ })
506
+
507
+ # Add current user message
508
+ enhanced_context.append({
509
+ "role": "user",
510
+ "content": user_message
511
+ })
512
+
513
+ return enhanced_context
514
 
515
  def _is_valid_response(self, response: str, persona_id: str) -> bool:
516
  """Validate response quality"""
517
+ if len(response) < 10 or len(response) > 5000:
 
 
 
518
  return False
519
 
520
  # Check for AI confusion indicators
521
  confusion_indicators = [
522
  f"Thank you, Dr. {persona_id.title()}",
523
  "Assistant:",
524
+ f"Dr. {persona_id.title()} Advisor:",
525
  "excellent discussion, Assistant"
526
  ]
527
 
528
  return not any(indicator in response for indicator in confusion_indicators)
529
 
530
+ def _get_persona_fallback(self, persona_id: str) -> str:
531
+ """Get persona-specific fallback responses"""
532
  fallbacks = {
533
+ "methodologist": "I'd be happy to help with your research methodology. What specific methodological approach are you considering?",
534
  "theorist": "I'd like to explore the theoretical foundation of your work. What conceptual framework guides your research?",
535
  "pragmatist": "Let's take a practical approach. What's the most pressing decision you need to make about your research right now?"
536
  }
 
562
  """Delete a session completely"""
563
  return self.session_manager.delete_session(session_id)
564
 
565
+ # Legacy method for backward compatibility
566
  def _get_persona_context_keywords(self, persona_id: str) -> str:
567
  """
568
+ Legacy method - use _get_enhanced_persona_context_keywords instead
 
 
 
569
  """
570
+ return self._get_enhanced_persona_context_keywords(persona_id)
 
 
 
 
 
571
 
572
+ async def chat_with_persona(self, persona_id: str, user_input: str, session_id: str, response_length: str = "medium") -> Dict[str, Any]:
573
  """
574
+ Chat with a specific persona directly
575
  """
576
  try:
577
+ persona = self.get_persona(persona_id)
578
+ if not persona:
579
+ return {
580
+ "error": f"Persona {persona_id} not found",
581
+ "available_personas": list(self.personas.keys())
582
+ }
583
 
584
+ session = self.session_manager.get_session(session_id)
585
+ session.append_message("user", user_input)
 
 
 
 
 
586
 
587
+ # Generate response from single persona
588
+ response_data = await self._generate_single_persona_response(session, persona, response_length)
589
 
590
+ # Add response to session
591
+ session.append_message(persona_id, response_data["response"])
 
592
 
593
+ return response_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
  except Exception as e:
596
+ logger.error(f"Error in chat_with_persona: {str(e)}")
597
+ return {
598
+ "error": f"Error processing request: {str(e)}",
599
+ "persona_id": persona_id
600
+ }
601
 
602
 
603
  async def get_top_personas(self, session_id: str, k: int = 3) -> List[str]:
multi_llm_chatbot_backend/app/core/rag_manager.py CHANGED
@@ -453,17 +453,501 @@ class RAGManager:
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
 
453
  except Exception as e:
454
  return {"status": "error", "message": str(e)}
455
 
456
+
457
+ class EnhancedRAGManager:
458
+ def __init__(self, persist_directory: str = "./chromadb_storage"):
459
+ """Initialize enhanced RAG manager with improved document handling"""
460
+ self.persist_directory = persist_directory
461
+ Path(persist_directory).mkdir(exist_ok=True)
462
+
463
+ # Initialize ChromaDB client
464
+ self.client = chromadb.PersistentClient(
465
+ path=persist_directory,
466
+ settings=Settings(anonymized_telemetry=False)
467
+ )
468
+
469
+ # Create or get collection
470
+ self.collection = self.client.get_or_create_collection(
471
+ name="phd_advisor_documents",
472
+ metadata={"hnsw:space": "cosine"}
473
+ )
474
+
475
+ logger.info(f"Enhanced RAG Manager initialized with collection: {self.collection.name}")
476
+
477
+ def add_document(self, content: str, filename: str, session_id: str,
478
+ file_type: str = "unknown") -> Dict[str, Any]:
479
+ """
480
+ Enhanced document addition with better metadata and document awareness
481
+ """
482
+ try:
483
+ # Preprocess the content
484
+ cleaned_content = self._preprocess_content(content)
485
+ if not cleaned_content.strip():
486
+ return {
487
+ "success": False,
488
+ "error": "Document content is empty after preprocessing",
489
+ "filename": filename
490
+ }
491
+
492
+ # Extract document metadata
493
+ doc_metadata = self._extract_document_metadata(cleaned_content, filename, file_type)
494
+
495
+ # Create intelligent chunks with overlap and context preservation
496
+ chunks = self._create_enhanced_chunks(cleaned_content, filename, doc_metadata)
497
+
498
+ # Prepare data for ChromaDB
499
+ chunk_texts = []
500
+ chunk_metadatas = []
501
+ chunk_ids = []
502
+
503
+ for i, chunk_data in enumerate(chunks):
504
+ chunk_id = f"{session_id}_{filename}_{i}_{uuid.uuid4().hex[:8]}"
505
+
506
+ # Enhanced metadata with document awareness
507
+ metadata = {
508
+ "session_id": session_id,
509
+ "filename": filename,
510
+ "file_type": file_type,
511
+ "chunk_index": i,
512
+ "total_chunks": len(chunks),
513
+ "document_section": chunk_data.get("section", "unknown"),
514
+ "keywords": chunk_data.get("keywords", ""),
515
+ "chunk_type": chunk_data.get("type", "content"),
516
+ "document_title": doc_metadata.get("title", filename),
517
+ "estimated_tokens": len(chunk_data["text"].split()) * 1.3,
518
+ "has_references": "references" in chunk_data["text"].lower(),
519
+ "has_methodology": "method" in chunk_data["text"].lower(),
520
+ "has_theory": any(word in chunk_data["text"].lower()
521
+ for word in ["theory", "theoretical", "framework", "concept"])
522
+ }
523
+
524
+ chunk_texts.append(chunk_data["text"])
525
+ chunk_metadatas.append(metadata)
526
+ chunk_ids.append(chunk_id)
527
+
528
+ # Add to ChromaDB
529
+ self.collection.add(
530
+ documents=chunk_texts,
531
+ metadatas=chunk_metadatas,
532
+ ids=chunk_ids
533
+ )
534
+
535
+ total_tokens = sum(metadata["estimated_tokens"] for metadata in chunk_metadatas)
536
+
537
+ logger.info(f"Successfully added document {filename}: {len(chunks)} chunks, ~{total_tokens:.0f} tokens")
538
+
539
+ return {
540
+ "success": True,
541
+ "filename": filename,
542
+ "chunks_created": len(chunks),
543
+ "total_tokens": int(total_tokens),
544
+ "document_metadata": doc_metadata
545
+ }
546
+
547
+ except Exception as e:
548
+ logger.error(f"Error adding document {filename}: {str(e)}")
549
+ return {
550
+ "success": False,
551
+ "filename": filename,
552
+ "error": str(e)
553
+ }
554
+
555
+ def search_documents_with_context(self, query: str, session_id: str,
556
+ persona_context: str = "", n_results: int = 5,
557
+ document_hint: str = None) -> List[Dict[str, Any]]:
558
+ """
559
+ Enhanced search with document awareness and context
560
+ """
561
+ try:
562
+ # Extract potential document references from query
563
+ document_references = self._extract_document_references(query)
564
+
565
+ # Build enhanced query
566
+ enhanced_query = self._build_enhanced_query(query, persona_context, document_references)
567
+
568
+ # Base search filters
569
+ search_filters = {"session_id": session_id}
570
+
571
+ # If specific document mentioned, prioritize it
572
+ if document_hint or document_references:
573
+ priority_filename = document_hint or document_references[0] if document_references else None
574
+ if priority_filename:
575
+ # First search: prioritize specific document
576
+ priority_results = self._search_with_filters(
577
+ enhanced_query,
578
+ {**search_filters, "filename": {"$contains": priority_filename}},
579
+ n_results=min(3, n_results)
580
+ )
581
+
582
+ # Second search: get additional context from other documents
583
+ remaining_results = max(0, n_results - len(priority_results))
584
+ if remaining_results > 0:
585
+ general_results = self._search_with_filters(
586
+ enhanced_query,
587
+ search_filters,
588
+ n_results=remaining_results + 2 # Get extras to filter out duplicates
589
+ )
590
+
591
+ # Combine results, avoiding duplicates
592
+ all_results = priority_results + [
593
+ r for r in general_results
594
+ if r["metadata"]["filename"] != priority_filename
595
+ ][:remaining_results]
596
+ else:
597
+ all_results = priority_results
598
+ else:
599
+ all_results = self._search_with_filters(enhanced_query, search_filters, n_results)
600
+ else:
601
+ all_results = self._search_with_filters(enhanced_query, search_filters, n_results)
602
+
603
+ # Enhance results with context and attribution
604
+ enhanced_results = self._enhance_search_results(all_results, query)
605
+
606
+ logger.info(f"Enhanced search returned {len(enhanced_results)} results for query: {query[:50]}...")
607
+ return enhanced_results
608
+
609
+ except Exception as e:
610
+ logger.error(f"Error in enhanced document search: {str(e)}")
611
+ return []
612
+
613
+ def _search_with_filters(self, query: str, filters: Dict, n_results: int) -> List[Dict[str, Any]]:
614
+ """Helper method for filtered search"""
615
+ results = self.collection.query(
616
+ query_texts=[query],
617
+ n_results=n_results,
618
+ where=filters
619
+ )
620
+
621
+ formatted_results = []
622
+ if results['documents'] and results['documents'][0]:
623
+ for i, (doc, metadata, distance) in enumerate(zip(
624
+ results['documents'][0],
625
+ results['metadatas'][0],
626
+ results['distances'][0]
627
+ )):
628
+ similarity_score = 1 / (1 + abs(distance)) if distance is not None else 0.5
629
+
630
+ formatted_results.append({
631
+ "text": doc,
632
+ "metadata": metadata,
633
+ "relevance_score": similarity_score,
634
+ "distance": distance,
635
+ "rank": i + 1
636
+ })
637
+
638
+ return formatted_results
639
+
640
+ def _extract_document_references(self, query: str) -> List[str]:
641
+ """Extract potential document name references from user query"""
642
+ # Common patterns for document references
643
+ patterns = [
644
+ r"(?:my|the|in)\s+([a-zA-Z_\-]+\.(?:pdf|docx|txt|doc))", # my document.pdf
645
+ r"(?:my|the)\s+(dissertation|thesis|proposal|chapter|paper|manuscript)", # my dissertation
646
+ r"(?:in|from)\s+(?:my\s+)?([a-zA-Z_\-\s]+(?:chapter|section|proposal))", # in my methodology chapter
647
+ r"(?:the|my)\s+([a-zA-Z_\-\s]+(?:document|file))", # the research document
648
+ ]
649
+
650
+ references = []
651
+ query_lower = query.lower()
652
+
653
+ for pattern in patterns:
654
+ matches = re.findall(pattern, query_lower)
655
+ references.extend(matches)
656
+
657
+ # Clean up references
658
+ cleaned_references = []
659
+ for ref in references:
660
+ cleaned = ref.strip().replace(" ", "_")
661
+ if len(cleaned) > 2: # Avoid single characters
662
+ cleaned_references.append(cleaned)
663
+
664
+ return cleaned_references[:3] # Limit to first 3 references
665
+
666
+ def _build_enhanced_query(self, original_query: str, persona_context: str,
667
+ document_refs: List[str]) -> str:
668
+ """Build enhanced query with context and document awareness"""
669
+ query_parts = [original_query]
670
+
671
+ if persona_context:
672
+ query_parts.append(persona_context)
673
+
674
+ if document_refs:
675
+ query_parts.extend(document_refs)
676
+
677
+ return " ".join(query_parts)
678
+
679
+ def _enhance_search_results(self, results: List[Dict], original_query: str) -> List[Dict[str, Any]]:
680
+ """Enhance search results with better attribution and context"""
681
+ enhanced = []
682
+
683
+ for result in results:
684
+ metadata = result["metadata"]
685
+
686
+ # Create enhanced result with clear attribution
687
+ enhanced_result = {
688
+ **result,
689
+ "document_source": {
690
+ "filename": metadata.get("filename", "unknown"),
691
+ "document_title": metadata.get("document_title", metadata.get("filename", "unknown")),
692
+ "section": metadata.get("document_section", "unknown"),
693
+ "chunk_position": f"{metadata.get('chunk_index', 0) + 1} of {metadata.get('total_chunks', 1)}"
694
+ },
695
+ "content_type": metadata.get("chunk_type", "content"),
696
+ "context_indicators": {
697
+ "has_methodology": metadata.get("has_methodology", False),
698
+ "has_theory": metadata.get("has_theory", False),
699
+ "has_references": metadata.get("has_references", False)
700
+ }
701
+ }
702
+
703
+ enhanced.append(enhanced_result)
704
+
705
+ # Sort by relevance score, but boost results from explicitly mentioned documents
706
+ enhanced.sort(key=lambda x: (
707
+ 1.0 if any(ref in x["document_source"]["filename"].lower()
708
+ for ref in self._extract_document_references(original_query)) else 0.0,
709
+ x["relevance_score"]
710
+ ), reverse=True)
711
+
712
+ return enhanced
713
+
714
+ def _extract_document_metadata(self, content: str, filename: str, file_type: str) -> Dict[str, Any]:
715
+ """Extract metadata from document content"""
716
+ lines = content.split('\n')
717
+
718
+ # Try to find title (usually first significant line)
719
+ title = filename
720
+ for line in lines[:10]:
721
+ if line.strip() and len(line.strip()) > 10 and len(line.strip()) < 100:
722
+ if not line.strip().startswith(('Abstract', 'Introduction', '1.', 'Chapter')):
723
+ title = line.strip()
724
+ break
725
+
726
+ # Extract other metadata
727
+ word_count = len(content.split())
728
+ has_sections = bool(re.search(r'(?:Chapter|Section|\d+\.)', content))
729
+
730
+ return {
731
+ "title": title,
732
+ "word_count": word_count,
733
+ "has_sections": has_sections,
734
+ "file_type": file_type,
735
+ "estimated_pages": word_count // 250 # Rough estimate
736
+ }
737
+
738
+ def _create_enhanced_chunks(self, content: str, filename: str, doc_metadata: Dict) -> List[Dict[str, Any]]:
739
+ """Create intelligent chunks with context preservation"""
740
+ # Split into logical sections first
741
+ sections = self._split_into_sections(content)
742
+
743
+ chunks = []
744
+ for section_data in sections:
745
+ section_text = section_data["text"]
746
+ section_type = section_data["type"]
747
+
748
+ # Create overlapping chunks within each section
749
+ if len(section_text.split()) > 300: # Large section, needs chunking
750
+ section_chunks = self._create_overlapping_chunks(section_text, chunk_size=250, overlap=50)
751
+ for chunk_text in section_chunks:
752
+ chunks.append({
753
+ "text": chunk_text,
754
+ "section": section_type,
755
+ "type": "content",
756
+ "keywords": self._extract_keywords(chunk_text)
757
+ })
758
+ else:
759
+ # Small section, keep as single chunk
760
+ chunks.append({
761
+ "text": section_text,
762
+ "section": section_type,
763
+ "type": section_type,
764
+ "keywords": self._extract_keywords(section_text)
765
+ })
766
+
767
+ return chunks
768
+
769
+ def _split_into_sections(self, content: str) -> List[Dict[str, Any]]:
770
+ """Split content into logical sections"""
771
+ lines = content.split('\n')
772
+ sections = []
773
+ current_section = []
774
+ current_type = "introduction"
775
+
776
+ for line in lines:
777
+ line = line.strip()
778
+ if not line:
779
+ continue
780
+
781
+ # Check if this line starts a new section
782
+ section_match = re.match(r'(?:Chapter|Section|\d+\.?\s+)(.+)', line, re.IGNORECASE)
783
+ if section_match:
784
+ # Save previous section
785
+ if current_section:
786
+ sections.append({
787
+ "text": '\n'.join(current_section),
788
+ "type": current_type
789
+ })
790
+
791
+ # Start new section
792
+ current_section = [line]
793
+ section_title = section_match.group(1).lower()
794
+ current_type = self._classify_section_type(section_title)
795
+ else:
796
+ current_section.append(line)
797
+
798
+ # Add final section
799
+ if current_section:
800
+ sections.append({
801
+ "text": '\n'.join(current_section),
802
+ "type": current_type
803
+ })
804
+
805
+ return sections if sections else [{"text": content, "type": "content"}]
806
+
807
+ def _classify_section_type(self, section_title: str) -> str:
808
+ """Classify section type based on title"""
809
+ title_lower = section_title.lower()
810
+
811
+ if any(word in title_lower for word in ['method', 'approach', 'design', 'procedure']):
812
+ return "methodology"
813
+ elif any(word in title_lower for word in ['theory', 'framework', 'literature', 'review']):
814
+ return "theory"
815
+ elif any(word in title_lower for word in ['result', 'finding', 'analysis', 'data']):
816
+ return "results"
817
+ elif any(word in title_lower for word in ['conclusion', 'discussion', 'implication']):
818
+ return "conclusion"
819
+ elif any(word in title_lower for word in ['introduction', 'background', 'abstract']):
820
+ return "introduction"
821
+ else:
822
+ return "content"
823
+
824
+ def _create_overlapping_chunks(self, text: str, chunk_size: int = 250, overlap: int = 50) -> List[str]:
825
+ """Create overlapping chunks from text"""
826
+ words = text.split()
827
+ chunks = []
828
+
829
+ start = 0
830
+ while start < len(words):
831
+ end = min(start + chunk_size, len(words))
832
+ chunk = ' '.join(words[start:end])
833
+ chunks.append(chunk)
834
+
835
+ if end >= len(words):
836
+ break
837
+
838
+ start += chunk_size - overlap
839
+
840
+ return chunks
841
+
842
+ def _extract_keywords(self, text: str) -> str:
843
+ """Extract key terms from text chunk"""
844
+ # Simple keyword extraction - could be enhanced with NLP
845
+ words = text.lower().split()
846
+
847
+ # Academic keywords to prioritize
848
+ academic_terms = set([
849
+ 'methodology', 'theory', 'analysis', 'research', 'study', 'data',
850
+ 'framework', 'approach', 'method', 'findings', 'results', 'literature',
851
+ 'hypothesis', 'experiment', 'survey', 'interview', 'observation',
852
+ 'qualitative', 'quantitative', 'mixed-methods', 'case study'
853
+ ])
854
+
855
+ found_keywords = [word for word in words if word in academic_terms]
856
+ return ' '.join(found_keywords[:5]) # Top 5 keywords
857
+
858
+ def _preprocess_content(self, content: str) -> str:
859
+ """Clean and preprocess document content"""
860
+ # Remove excessive whitespace
861
+ content = re.sub(r'\s+', ' ', content)
862
+
863
+ # Remove page numbers and headers/footers
864
+ content = re.sub(r'\n\s*\d+\s*\n', '\n', content)
865
+
866
+ # Clean up encoding issues
867
+ content = content.replace('\ufffd', ' ')
868
+
869
+ return content.strip()
870
+
871
+ def get_document_stats(self, session_id: str) -> Dict[str, Any]:
872
+ """Get enhanced statistics about documents in a session"""
873
+ try:
874
+ # Get all chunks for this session
875
+ results = self.collection.get(
876
+ where={"session_id": session_id},
877
+ include=["metadatas"]
878
+ )
879
+
880
+ if not results['metadatas']:
881
+ return {"total_chunks": 0, "total_documents": 0, "documents": []}
882
+
883
+ # Analyze documents
884
+ documents = {}
885
+ total_tokens = 0
886
+
887
+ for metadata in results['metadatas']:
888
+ filename = metadata.get('filename', 'unknown')
889
+
890
+ if filename not in documents:
891
+ documents[filename] = {
892
+ "filename": filename,
893
+ "title": metadata.get('document_title', filename),
894
+ "file_type": metadata.get('file_type', 'unknown'),
895
+ "chunks": 0,
896
+ "estimated_tokens": 0,
897
+ "sections": set(),
898
+ "has_methodology": False,
899
+ "has_theory": False,
900
+ "has_references": False
901
+ }
902
+
903
+ doc_info = documents[filename]
904
+ doc_info["chunks"] += 1
905
+ doc_info["estimated_tokens"] += metadata.get('estimated_tokens', 0)
906
+ doc_info["sections"].add(metadata.get('document_section', 'unknown'))
907
+
908
+ if metadata.get('has_methodology'):
909
+ doc_info["has_methodology"] = True
910
+ if metadata.get('has_theory'):
911
+ doc_info["has_theory"] = True
912
+ if metadata.get('has_references'):
913
+ doc_info["has_references"] = True
914
+
915
+ total_tokens += metadata.get('estimated_tokens', 0)
916
+
917
+ # Convert sets to lists for JSON serialization
918
+ for doc_info in documents.values():
919
+ doc_info["sections"] = list(doc_info["sections"])
920
+
921
+ return {
922
+ "total_chunks": len(results['metadatas']),
923
+ "total_documents": len(documents),
924
+ "total_estimated_tokens": int(total_tokens),
925
+ "documents": list(documents.values())
926
+ }
927
+
928
+ except Exception as e:
929
+ logger.error(f"Error getting document stats: {str(e)}")
930
+ return {"error": str(e), "total_chunks": 0, "total_documents": 0}
931
+
932
+
933
  # Global RAG manager instance
934
  _rag_manager = None
935
 
936
+ # def get_rag_manager() -> RAGManager:
937
+ # """Get the global RAG manager instance"""
938
+ # global _rag_manager
939
+ # if _rag_manager is None:
940
+ # try:
941
+ # _rag_manager = RAGManager()
942
+ # logger.info("RAG Manager initialized successfully")
943
+ # except Exception as e:
944
+ # logger.error(f"Failed to initialize RAG Manager: {e}")
945
+ # raise
946
+ # return _rag_manager
947
+
948
+ def get_rag_manager() -> EnhancedRAGManager:
949
+ """Get or create the global RAG manager instance"""
950
  global _rag_manager
951
  if _rag_manager is None:
952
+ _rag_manager = EnhancedRAGManager()
 
 
 
 
 
953
  return _rag_manager
multi_llm_chatbot_backend/app/llm/gemini_client.py CHANGED
@@ -113,8 +113,8 @@ class GeminiClient(LLMClient):
113
  """Clean up Gemini responses"""
114
  # Remove common prefixes that indicate AI confusion
115
  prefixes_to_remove = [
116
- "Dr. Methodist:", "Dr. Theorist:", "Dr. Pragmatist:",
117
- "Methodist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
118
  "As Dr.", "Here's my response:", "Response:", "Assistant:",
119
  "Here are 2-3 sentence", "Here's an expansion of the advice:",
120
  ]
 
113
  """Clean up Gemini responses"""
114
  # Remove common prefixes that indicate AI confusion
115
  prefixes_to_remove = [
116
+ "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:",
117
+ "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
118
  "As Dr.", "Here's my response:", "Response:", "Assistant:",
119
  "Here are 2-3 sentence", "Here's an expansion of the advice:",
120
  ]
multi_llm_chatbot_backend/app/llm/improved_ollama_client.py CHANGED
@@ -71,8 +71,8 @@ class ImprovedOllamaClient(LLMClient):
71
  # Remove common prefixes that indicate AI confusion
72
  prefixes_to_remove = [
73
  "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
74
- "Dr. Methodist:", "Dr. Theorist:", "Dr. Pragmatist:",
75
- "Methodist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
76
  ]
77
 
78
  for prefix in prefixes_to_remove:
 
71
  # Remove common prefixes that indicate AI confusion
72
  prefixes_to_remove = [
73
  "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
74
+ "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:",
75
+ "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
76
  ]
77
 
78
  for prefix in prefixes_to_remove:
multi_llm_chatbot_backend/app/llm/short_ollama_client.py CHANGED
@@ -62,8 +62,8 @@ class ShortResponseOllamaClient(LLMClient):
62
  # Remove common prefixes
63
  prefixes_to_remove = [
64
  "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
65
- "Dr. Methodist:", "Dr. Theorist:", "Dr. Pragmatist:",
66
- "Methodist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
67
  ]
68
 
69
  for prefix in prefixes_to_remove:
 
62
  # Remove common prefixes
63
  prefixes_to_remove = [
64
  "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
65
+ "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:",
66
+ "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
67
  ]
68
 
69
  for prefix in prefixes_to_remove:
multi_llm_chatbot_backend/app/models/default_personas.py CHANGED
@@ -2,20 +2,117 @@ from app.models.persona import Persona
2
 
3
  # Registry of default personas
4
  DEFAULT_PERSONAS = {
5
- "methodist": {
6
- "name": "Methodical Advisor",
7
- "system_prompt": "You are a highly methodical PhD advisor. Provide organized, step-by-step guidance with strong structure and clarity.",
8
- "default_temperature": 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  },
10
  "theorist": {
11
- "name": "Theorist Advisor",
12
- "system_prompt": "You are a philosophical PhD advisor who focuses on abstract theories and big-picture thinking.",
13
- "default_temperature": 8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  },
15
  "pragmatist": {
16
- "name": "Pragmatist Advisor",
17
- "system_prompt": "You are a practical PhD advisor who gives real-world, actionable advice focused on utility and outcomes.",
18
- "default_temperature": 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
  }
21
 
 
2
 
3
  # Registry of default personas
4
  DEFAULT_PERSONAS = {
5
+ "methodologist": {
6
+ "name": "Methodologist",
7
+ "system_prompt": """You are a distinguished PhD advisor and Research Methodology Expert with 15+ years of experience guiding doctoral students across multiple disciplines. You hold a PhD in Research Methods and Statistics from Stanford University.
8
+
9
+ **YOUR EXPERTISE:**
10
+ - Quantitative and qualitative research design
11
+ - Mixed-methods approaches and triangulation
12
+ - Statistical analysis and data validation
13
+ - Research ethics and IRB protocols
14
+ - Sampling strategies and validity frameworks
15
+ - Systematic reviews and meta-analyses
16
+
17
+ **YOUR RESPONSE STYLE:**
18
+ - Be precise and analytical, with clear methodological reasoning
19
+ - Always ground advice in established research principles
20
+ - Provide step-by-step guidance for complex methodological decisions
21
+ - Include specific examples and cite relevant methodological frameworks
22
+ - Ask clarifying questions about research design when needed
23
+
24
+ **DOCUMENT HANDLING (when documents are available):**
25
+ - Reference uploaded documents by name when discussing their work
26
+ - Extract and analyze methodological approaches from their documents
27
+ - Compare their current methodology against best practices
28
+ - Identify gaps or weaknesses in their research design
29
+ - Provide clear citations: "Based on your [document_name], I notice..."
30
+
31
+ **INTERACTION GUIDELINES:**
32
+ - Address methodological rigor without being overwhelming
33
+ - Balance theoretical frameworks with practical implementation
34
+ - Help them understand WHY certain methods are appropriate
35
+ - Connect methodology to their specific research questions and field
36
+ - Emphasize validity, reliability, and ethical considerations""",
37
+ "default_temperature": 4
38
  },
39
  "theorist": {
40
+ "name": "Theorist - Theoretical Frameworks Specialist",
41
+ "system_prompt": """You are a renowned PhD advisor and Theoretical Frameworks Specialist with deep expertise in epistemology, conceptual development, and philosophical foundations of research. You hold a PhD in Philosophy of Science from Oxford University.
42
+
43
+ **YOUR EXPERTISE:**
44
+ - Epistemological and ontological foundations
45
+ - Theoretical framework development and selection
46
+ - Literature synthesis and conceptual mapping
47
+ - Paradigmatic positioning (positivist, interpretivist, critical, pragmatic)
48
+ - Theory building and model development
49
+ - Philosophical underpinnings of research approaches
50
+ - Conceptual clarity and definitional precision
51
+
52
+ **YOUR RESPONSE STYLE:**
53
+ - Engage with deep intellectual rigor and philosophical depth
54
+ - Help students think critically about underlying assumptions
55
+ - Guide theoretical exploration without being overly abstract
56
+ - Connect theoretical concepts to practical research implications
57
+ - Encourage reflection on epistemological positioning
58
+ - Build conceptual bridges between different theoretical traditions
59
+
60
+ **DOCUMENT HANDLING (when documents are available):**
61
+ - Analyze theoretical positioning in their literature reviews
62
+ - Identify conceptual gaps and theoretical contributions
63
+ - Evaluate philosophical consistency across their work
64
+ - Suggest theoretical frameworks that align with their research questions
65
+ - Reference their work: "Your theoretical framework in [document_name] draws from..."
66
+
67
+ **INTERACTION GUIDELINES:**
68
+ - Foster deep thinking about theoretical foundations
69
+ - Help students articulate their epistemological stance
70
+ - Guide them through complex theoretical landscapes
71
+ - Encourage synthesis of multiple theoretical perspectives
72
+ - Emphasize the importance of theoretical coherence
73
+ - Make abstract concepts accessible and actionable
74
+ - Challenge assumptions constructively""",
75
+ "default_temperature": 7
76
  },
77
  "pragmatist": {
78
+ "name": "Pragmatist - Action-Focused Research Coach",
79
+ "system_prompt": """You are an energetic and results-oriented PhD advisor specializing in turning research plans into actionable progress. With a PhD in Applied Psychology from UC Berkeley and 12+ years of mentoring experience, you're known for helping students overcome analysis paralysis and make consistent progress.
80
+
81
+ **YOUR EXPERTISE:**
82
+ - Project management and timeline development
83
+ - Breaking complex research into manageable tasks
84
+ - Overcoming research roadblocks and motivation challenges
85
+ - Practical implementation of research plans
86
+ - Resource management and efficiency optimization
87
+ - Writing strategies and productivity systems
88
+ - Career development and professional networking
89
+
90
+ **YOUR RESPONSE STYLE:**
91
+ - Warm, encouraging, and motivational tone
92
+ - Focus on practical, immediately implementable advice
93
+ - Break down overwhelming tasks into smaller, manageable steps
94
+ - Emphasize progress over perfection
95
+ - Provide specific deadlines and accountability markers
96
+ - Celebrate small wins and maintain momentum
97
+ - Ask about practical constraints and real-world limitations
98
+
99
+ **DOCUMENT HANDLING (when documents are available):**
100
+ - Transform document analysis into actionable next steps
101
+ - Create concrete timelines based on their current progress
102
+ - Find immediate action items in their research materials
103
+ - Convert theoretical frameworks into practical research steps
104
+ - Reference their work: "Looking at your [document_name], I suggest..."
105
+
106
+ **INTERACTION GUIDELINES:**
107
+ - Always end with specific, actionable next steps
108
+ - Help them prioritize when facing multiple options
109
+ - Address emotional and motivational aspects of research
110
+ - Provide realistic timelines and expectations
111
+ - Focus on sustainable progress strategies
112
+ - Encourage them to start with what they can control
113
+ - Offer practical solutions to common PhD challenges
114
+ - Maintain optimism while being realistic about challenges""",
115
+ "default_temperature": 5
116
  }
117
  }
118
 
multi_llm_chatbot_backend/app/tests/debug_rag.py CHANGED
@@ -16,7 +16,7 @@ def test_direct_search():
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
  ]
@@ -55,7 +55,7 @@ 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:
 
16
  print("🔍 Testing direct document search...")
17
 
18
  test_queries = [
19
+ {"query": "research methodology approach", "persona": "methodologist"},
20
  {"query": "theoretical framework theory", "persona": "theorist"},
21
  {"query": "practical steps implementation", "persona": "pragmatist"}
22
  ]
 
55
  """Test individual persona chat to isolate issues"""
56
  print("\n💬 Testing individual persona chat...")
57
 
58
+ personas = ["methodologist", "theorist", "pragmatist"]
59
  query = "What research methodology should I use based on the uploaded document?"
60
 
61
  for persona in personas:
multi_llm_chatbot_backend/app/tests/test_rag_system.py CHANGED
@@ -65,7 +65,7 @@ def test_rag_system():
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
  ]
 
65
  print("🔍 Test 3: Testing direct document search...")
66
 
67
  search_queries = [
68
+ {"query": "What methodology should I use?", "persona": "methodologist"},
69
  {"query": "What is the theoretical framework?", "persona": "theorist"},
70
  {"query": "What are the next steps?", "persona": "pragmatist"}
71
  ]
phd-advisor-frontend/src/data/advisors.js CHANGED
@@ -1,9 +1,10 @@
1
  import { BookOpen, Target, Brain } from 'lucide-react';
2
 
3
  export const advisors = {
4
- methodist: {
5
- name: 'Methodist',
6
- role: 'Methodical Advisor',
 
7
  // Light theme colors
8
  color: '#3B82F6',
9
  bgColor: '#EFF6FF',
@@ -12,11 +13,58 @@ export const advisors = {
12
  darkBgColor: '#1E3A8A',
13
  // Icon and description
14
  icon: BookOpen,
15
- description: 'Structured & Planning-focused'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  },
 
17
  theorist: {
 
18
  name: 'Theorist',
19
- role: 'Theoretical Advisor',
20
  // Light theme colors
21
  color: '#8B5CF6',
22
  bgColor: '#F3E8FF',
@@ -25,11 +73,58 @@ export const advisors = {
25
  darkBgColor: '#581C87',
26
  // Icon and description
27
  icon: Brain,
28
- description: 'Abstract & Conceptual'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  },
 
30
  pragmatist: {
 
31
  name: 'Pragmatist',
32
- role: 'Practical Advisor',
33
  // Light theme colors
34
  color: '#10B981',
35
  bgColor: '#ECFDF5',
@@ -38,11 +133,56 @@ export const advisors = {
38
  darkBgColor: '#065F46',
39
  // Icon and description
40
  icon: Target,
41
- description: 'Real-world & Outcome-focused'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
  };
44
 
45
- // Helper function to get theme-appropriate colors
46
  export const getAdvisorColors = (advisorId, isDark = false) => {
47
  const advisor = advisors[advisorId];
48
  if (!advisor) return { color: '#6B7280', bgColor: '#F3F4F6' };
@@ -51,10 +191,53 @@ export const getAdvisorColors = (advisorId, isDark = false) => {
51
  color: isDark ? advisor.darkColor : advisor.color,
52
  bgColor: isDark ? advisor.darkBgColor : advisor.bgColor,
53
  // For text that needs to be readable on colored backgrounds
54
- textColor: isDark ? '#F9FAFB' :
55
- advisor.color === '#10B981' ? '#047857' : // Darker green for pragmatist
56
- advisor.color === '#3B82F6' ? '#1D4ED8' : // Darker blue for methodist
57
- advisor.color === '#8B5CF6' ? '#7C3AED' : // Darker purple for theorist
58
- '#374151' // fallback
59
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  };
 
1
  import { BookOpen, Target, Brain } from 'lucide-react';
2
 
3
  export const advisors = {
4
+ methodologist: {
5
+ // Original structure preserved
6
+ name: 'Methodologist',
7
+ role: 'Research Methodology Expert',
8
  // Light theme colors
9
  color: '#3B82F6',
10
  bgColor: '#EFF6FF',
 
13
  darkBgColor: '#1E3A8A',
14
  // Icon and description
15
  icon: BookOpen,
16
+ description: 'Structured & Planning-focused',
17
+
18
+ // Enhanced information added
19
+ fullTitle: 'Methodologist - Research Methodology Expert',
20
+ credentials: 'PhD in Research Methods and Statistics from Stanford University',
21
+ experience: '15+ years of experience guiding doctoral students',
22
+
23
+ specialties: [
24
+ 'Quantitative & Qualitative Research Design',
25
+ 'Statistical Analysis & Data Validation',
26
+ 'Research Ethics & IRB Protocols',
27
+ 'Mixed-Methods Approaches',
28
+ 'Systematic Reviews & Meta-Analyses'
29
+ ],
30
+
31
+ expertise: {
32
+ primary: 'Research Methodology',
33
+ secondary: ['Statistical Analysis', 'Research Design', 'Data Collection'],
34
+ documentTypes: ['methodology chapters', 'research proposals', 'data analysis plans'],
35
+ strengths: [
36
+ 'Methodological rigor assessment',
37
+ 'Validity and reliability guidance',
38
+ 'Statistical approach optimization',
39
+ 'Research ethics consultation'
40
+ ]
41
+ },
42
+
43
+ personality: {
44
+ tone: 'Precise and analytical',
45
+ approach: 'Systematic, evidence-based guidance',
46
+ communicationStyle: 'Clear, structured, methodically reasoned',
47
+ documentHandling: 'Analyzes methodological approaches and identifies design improvements'
48
+ },
49
+
50
+ sampleQuestions: [
51
+ "What methodology does my research proposal suggest?",
52
+ "How can I improve the validity of my study design?",
53
+ "What sampling strategy would work best for my research?",
54
+ "Are there any methodological gaps in my approach?"
55
+ ],
56
+
57
+ responseStyle: {
58
+ length: 'Detailed with step-by-step guidance',
59
+ structure: 'Organized with clear methodological reasoning',
60
+ citations: 'References established research principles and frameworks'
61
+ }
62
  },
63
+
64
  theorist: {
65
+ // Original structure preserved
66
  name: 'Theorist',
67
+ role: 'Theoretical Frameworks Specialist',
68
  // Light theme colors
69
  color: '#8B5CF6',
70
  bgColor: '#F3E8FF',
 
73
  darkBgColor: '#581C87',
74
  // Icon and description
75
  icon: Brain,
76
+ description: 'Abstract & Conceptual',
77
+
78
+ // Enhanced information added
79
+ fullTitle: 'Theorist - Theoretical Frameworks Specialist',
80
+ credentials: 'PhD in Philosophy of Science from Oxford University',
81
+ experience: 'Deep expertise in epistemology and conceptual development',
82
+
83
+ specialties: [
84
+ 'Epistemological & Ontological Foundations',
85
+ 'Theoretical Framework Development',
86
+ 'Literature Synthesis & Conceptual Mapping',
87
+ 'Philosophy of Science',
88
+ 'Theory Building & Model Development'
89
+ ],
90
+
91
+ expertise: {
92
+ primary: 'Theoretical Frameworks',
93
+ secondary: ['Conceptual Development', 'Literature Synthesis', 'Philosophical Foundations'],
94
+ documentTypes: ['literature reviews', 'theoretical chapters', 'conceptual frameworks'],
95
+ strengths: [
96
+ 'Theoretical positioning guidance',
97
+ 'Conceptual clarity development',
98
+ 'Epistemological stance articulation',
99
+ 'Literature synthesis strategy'
100
+ ]
101
+ },
102
+
103
+ personality: {
104
+ tone: 'Intellectually rigorous and philosophically deep',
105
+ approach: 'Explores theoretical foundations and conceptual relationships',
106
+ communicationStyle: 'Thoughtful, reflective, theoretically grounded',
107
+ documentHandling: 'Analyzes theoretical positioning and identifies conceptual gaps'
108
+ },
109
+
110
+ sampleQuestions: [
111
+ "What theoretical framework best fits my research?",
112
+ "How do I position my work within existing literature?",
113
+ "What are the philosophical assumptions in my approach?",
114
+ "How can I strengthen my conceptual foundation?"
115
+ ],
116
+
117
+ responseStyle: {
118
+ length: 'Comprehensive with deep theoretical exploration',
119
+ structure: 'Builds from foundational concepts to specific applications',
120
+ citations: 'References major theoretical traditions and philosophers'
121
+ }
122
  },
123
+
124
  pragmatist: {
125
+ // Original structure preserved
126
  name: 'Pragmatist',
127
+ role: 'Action-Focused Research Coach',
128
  // Light theme colors
129
  color: '#10B981',
130
  bgColor: '#ECFDF5',
 
133
  darkBgColor: '#065F46',
134
  // Icon and description
135
  icon: Target,
136
+ description: 'Real-world & Outcome-focused',
137
+
138
+ // Enhanced information added
139
+ fullTitle: 'Pragmatist - Action-Focused Research Coach',
140
+ credentials: 'PhD in Applied Psychology from UC Berkeley',
141
+ experience: '12+ years of mentoring experience specializing in practical progress',
142
+
143
+ specialties: [
144
+ 'Project Management & Timeline Development',
145
+ 'Research Implementation Strategies',
146
+ 'Productivity & Workflow Optimization',
147
+ 'Academic Career Development',
148
+ 'Overcoming Research Roadblocks'
149
+ ],
150
+
151
+ expertise: {
152
+ primary: 'Practical Implementation',
153
+ secondary: ['Project Management', 'Productivity Systems', 'Career Development'],
154
+ documentTypes: ['research timelines', 'progress reports', 'implementation plans'],
155
+ strengths: [
156
+ 'Task prioritization and planning',
157
+ 'Productivity system design',
158
+ 'Motivation and momentum building',
159
+ 'Real-world constraint navigation'
160
+ ]
161
+ },
162
+
163
+ personality: {
164
+ tone: 'Warm, encouraging, and motivational',
165
+ approach: 'Focuses on practical, immediately implementable advice',
166
+ communicationStyle: 'Energetic, supportive, action-oriented',
167
+ documentHandling: 'Transforms analysis into concrete next steps and timelines'
168
+ },
169
+
170
+ sampleQuestions: [
171
+ "What should be my next immediate steps?",
172
+ "How can I prioritize my research tasks?",
173
+ "What's a realistic timeline for my project?",
174
+ "How do I overcome analysis paralysis?"
175
+ ],
176
+
177
+ responseStyle: {
178
+ length: 'Concise with clear action items',
179
+ structure: 'Always ends with specific, actionable next steps',
180
+ citations: 'References practical examples and success strategies'
181
+ }
182
  }
183
  };
184
 
185
+ // Helper function to get theme-appropriate colors (preserved exactly)
186
  export const getAdvisorColors = (advisorId, isDark = false) => {
187
  const advisor = advisors[advisorId];
188
  if (!advisor) return { color: '#6B7280', bgColor: '#F3F4F6' };
 
191
  color: isDark ? advisor.darkColor : advisor.color,
192
  bgColor: isDark ? advisor.darkBgColor : advisor.bgColor,
193
  // For text that needs to be readable on colored backgrounds
194
+ textColor: isDark ? '#F9FAFB' :
195
+ advisor.color === '#10B981' ? '#047857' : // Darker green for pragmatist
196
+ advisor.color === '#3B82F6' ? '#1D4ED8' : // Darker blue for methodologist
197
+ advisor.color === '#8B5CF6' ? '#7C3AED' : // Darker purple for theorist
198
+ '#374151' // fallback
199
  };
200
+ };
201
+
202
+ // Additional helper functions for enhanced functionality
203
+ export const getAdvisorById = (id) => {
204
+ return advisors[id];
205
+ };
206
+
207
+ export const getAdvisorSpecialties = (id) => {
208
+ const advisor = getAdvisorById(id);
209
+ return advisor ? advisor.specialties : [];
210
+ };
211
+
212
+ export const getAdvisorExpertise = (id) => {
213
+ const advisor = getAdvisorById(id);
214
+ return advisor ? advisor.expertise : {};
215
+ };
216
+
217
+ export const getDocumentHandlingInfo = (id) => {
218
+ const advisor = getAdvisorById(id);
219
+ return advisor?.personality?.documentHandling || 'Provides general guidance based on uploaded documents';
220
+ };
221
+
222
+ export const getSampleQuestionsForDocuments = (id) => {
223
+ const advisor = getAdvisorById(id);
224
+ return advisor?.sampleQuestions || [];
225
+ };
226
+
227
+ export const getAdvisorDocumentTypes = (id) => {
228
+ const advisor = getAdvisorById(id);
229
+ return advisor?.expertise?.documentTypes || [];
230
+ };
231
+
232
+ // Backward compatibility: get advisor list as array (if needed elsewhere)
233
+ export const getAdvisorsList = () => {
234
+ return Object.entries(advisors).map(([id, advisor]) => ({
235
+ id,
236
+ ...advisor
237
+ }));
238
+ };
239
+
240
+ // Get advisor IDs for iteration
241
+ export const getAdvisorIds = () => {
242
+ return Object.keys(advisors);
243
  };