Spaces:
Running
Running
Merge pull request #56 from sohank-17/girish-response-fixes
Browse files- .gitignore +1 -0
- README.md +2 -2
- multi_llm_chatbot_backend/app/api/routes.py +237 -63
- multi_llm_chatbot_backend/app/api/routes_old.py +1 -1
- multi_llm_chatbot_backend/app/core/context_manager.py +1 -1
- multi_llm_chatbot_backend/app/core/improved_orchestrator.py +387 -197
- multi_llm_chatbot_backend/app/core/rag_manager.py +492 -8
- multi_llm_chatbot_backend/app/llm/gemini_client.py +2 -2
- multi_llm_chatbot_backend/app/llm/improved_ollama_client.py +2 -2
- multi_llm_chatbot_backend/app/llm/short_ollama_client.py +2 -2
- multi_llm_chatbot_backend/app/models/default_personas.py +107 -10
- multi_llm_chatbot_backend/app/tests/debug_rag.py +2 -2
- multi_llm_chatbot_backend/app/tests/test_rag_system.py +1 -1
- phd-advisor-frontend/src/data/advisors.js +197 -14
.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
|
| 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 |
-
###
|
| 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 |
-
"
|
| 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
|
| 192 |
-
"""
|
|
|
|
|
|
|
| 193 |
try:
|
|
|
|
| 194 |
session_id = get_or_create_session_for_request(request, message.session_id)
|
| 195 |
|
| 196 |
-
# Process message
|
| 197 |
-
|
| 198 |
user_input=message.user_input,
|
| 199 |
session_id=session_id,
|
| 200 |
response_length=message.response_length or "medium"
|
| 201 |
)
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 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 |
-
|
| 210 |
-
|
| 211 |
-
"
|
| 212 |
-
"
|
|
|
|
|
|
|
|
|
|
| 213 |
}
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
except Exception as e:
|
| 218 |
-
logger.error(f"Error in
|
| 219 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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":
|
| 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 |
-
|
| 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
|
| 331 |
session_id = get_or_create_session_for_request(request)
|
| 332 |
session = session_manager.get_session(session_id)
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
# Simple size check
|
| 337 |
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
| 338 |
-
if
|
| 339 |
-
raise HTTPException(status_code=
|
| 340 |
|
| 341 |
-
|
|
|
|
|
|
|
| 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
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
-
#
|
| 369 |
session.uploaded_files.append(file.filename)
|
| 370 |
session.total_upload_size += len(file_bytes)
|
| 371 |
|
| 372 |
-
# Add
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
return {
|
| 376 |
-
"message": "Document uploaded and processed successfully.",
|
| 377 |
"filename": file.filename,
|
| 378 |
-
"
|
| 379 |
-
"
|
| 380 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
|
| 383 |
except HTTPException:
|
| 384 |
raise
|
| 385 |
except Exception as e:
|
| 386 |
-
logger.error(f"Error
|
| 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 |
-
"
|
| 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 |
-
"
|
| 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', '
|
| 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 |
-
|
| 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("
|
| 59 |
|
| 60 |
return {
|
| 61 |
-
"
|
| 62 |
-
"session_id": session.session_id,
|
| 63 |
"message": clarification,
|
| 64 |
-
"
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
# Generate responses from all personas
|
| 68 |
responses = await self._generate_persona_responses(session, response_length)
|
| 69 |
|
| 70 |
return {
|
| 71 |
-
"
|
| 72 |
-
"session_id": session.session_id,
|
| 73 |
"responses": responses,
|
| 74 |
-
"
|
| 75 |
}
|
| 76 |
|
| 77 |
except Exception as e:
|
| 78 |
logger.error(f"Error in process_message: {str(e)}")
|
| 79 |
return {
|
| 80 |
-
"
|
| 81 |
-
"
|
| 82 |
-
"message": "I encountered an error processing your request. Please try again.",
|
| 83 |
"error": str(e)
|
| 84 |
}
|
| 85 |
-
|
| 86 |
-
async def
|
| 87 |
-
user_input: str,
|
| 88 |
-
persona_id: str,
|
| 89 |
-
session_id: Optional[str] = None,
|
| 90 |
-
response_length: str = "medium") -> Dict[str, Any]:
|
| 91 |
"""
|
| 92 |
-
|
| 93 |
"""
|
| 94 |
try:
|
| 95 |
-
|
| 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 |
-
#
|
| 108 |
-
|
| 109 |
-
response = await self._generate_single_persona_response(session, persona, response_length)
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
return {
|
| 115 |
-
"
|
| 116 |
-
"
|
| 117 |
-
"
|
| 118 |
-
"
|
| 119 |
-
"
|
| 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
|
| 127 |
return {
|
| 128 |
-
"
|
| 129 |
-
"
|
| 130 |
-
"
|
| 131 |
-
"error": str(e)
|
| 132 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
def _needs_clarification(self, session, user_input: str) -> bool:
|
| 135 |
"""
|
| 136 |
-
Determine if
|
| 137 |
"""
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 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 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
async def _generate_clarification_question(self, session: ConversationContext) -> str:
|
| 160 |
"""
|
| 161 |
-
Generate a clarification question based on context
|
| 162 |
"""
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 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
|
| 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(
|
| 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 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
return {
|
| 271 |
"persona_id": persona.id,
|
| 272 |
"persona_name": persona.name,
|
| 273 |
"response": response,
|
| 274 |
-
"
|
| 275 |
-
"
|
| 276 |
-
"
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
except Exception as e:
|
| 280 |
-
logger.error(f"Error
|
| 281 |
return {
|
| 282 |
"persona_id": persona.id,
|
| 283 |
"persona_name": persona.name,
|
| 284 |
-
"response": self.
|
| 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
|
| 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
|
| 309 |
-
"""Get persona-specific fallback
|
| 310 |
fallbacks = {
|
| 311 |
-
"
|
| 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 |
-
|
| 346 |
-
|
| 347 |
-
This ensures each advisor gets the most relevant document chunks
|
| 348 |
-
based on their specialization
|
| 349 |
"""
|
| 350 |
-
|
| 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
|
| 358 |
"""
|
| 359 |
-
|
| 360 |
"""
|
| 361 |
try:
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 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 |
-
|
|
|
|
| 376 |
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
return ""
|
| 380 |
|
| 381 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
global _rag_manager
|
| 462 |
if _rag_manager is None:
|
| 463 |
-
|
| 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.
|
| 117 |
-
"
|
| 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.
|
| 75 |
-
"
|
| 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.
|
| 66 |
-
"
|
| 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 |
-
"
|
| 6 |
-
"name": "
|
| 7 |
-
"system_prompt": "You are a
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
},
|
| 10 |
"theorist": {
|
| 11 |
-
"name": "Theorist
|
| 12 |
-
"system_prompt": "You are a
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
},
|
| 15 |
"pragmatist": {
|
| 16 |
-
"name": "Pragmatist
|
| 17 |
-
"system_prompt": "You are
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 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 = ["
|
| 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": "
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
| 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
|
| 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: '
|
| 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 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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 |
};
|