Spaces:
Running
Running
Girish Jeswani commited on
Commit ·
5ff4a89
1
Parent(s): e285e4c
remove redundant code
Browse files- multi_llm_chatbot_backend/app/api/routes_old.py +0 -404
- multi_llm_chatbot_backend/app/core/orchestrator.py +0 -41
- multi_llm_chatbot_backend/app/llm/gemini_client.py +0 -155
- multi_llm_chatbot_backend/app/llm/mistral_client.py +0 -27
- multi_llm_chatbot_backend/app/llm/ollama_client.py +0 -34
- multi_llm_chatbot_backend/app/llm/short_ollama_client.py +0 -101
- phd-advisor-frontend/README.md +0 -70
multi_llm_chatbot_backend/app/api/routes_old.py
DELETED
|
@@ -1,404 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from fastapi import APIRouter, Body, HTTPException
|
| 3 |
-
import httpx
|
| 4 |
-
from app.llm.llm_client import LLMClient
|
| 5 |
-
from app.llm.gemini_client import GeminiClient
|
| 6 |
-
from app.llm.short_ollama_client import ShortResponseOllamaClient
|
| 7 |
-
from app.models.persona import Persona
|
| 8 |
-
from app.core.orchestrator import ChatOrchestrator
|
| 9 |
-
from app.core.seamless_orchestrator import SeamlessOrchestrator
|
| 10 |
-
from app.core.context import GlobalSessionContext
|
| 11 |
-
from app.models.default_personas import get_default_personas
|
| 12 |
-
from pydantic import BaseModel
|
| 13 |
-
from typing import Optional, List
|
| 14 |
-
from fastapi import UploadFile, File
|
| 15 |
-
from app.utils.document_extractor import extract_text_from_file
|
| 16 |
-
from app.core.orchestrator import answer_with_persona_context
|
| 17 |
-
from app.utils.chroma_client import add_persona_doc
|
| 18 |
-
import hashlib
|
| 19 |
-
from app.utils.file_limits import is_within_upload_limit
|
| 20 |
-
|
| 21 |
-
router = APIRouter()
|
| 22 |
-
|
| 23 |
-
# Provider management
|
| 24 |
-
current_provider = "gemini"
|
| 25 |
-
available_providers = ["ollama", "gemini"]
|
| 26 |
-
|
| 27 |
-
def create_llm_client(provider: str = None) -> LLMClient:
|
| 28 |
-
"""Create LLM client based on provider"""
|
| 29 |
-
if provider is None:
|
| 30 |
-
provider = current_provider
|
| 31 |
-
|
| 32 |
-
if provider == "gemini":
|
| 33 |
-
try:
|
| 34 |
-
return GeminiClient(model_name=os.getenv("GEMINI_MODEL"))
|
| 35 |
-
except ValueError as e:
|
| 36 |
-
# Fallback to Ollama if Gemini API key is not available
|
| 37 |
-
print(f"Gemini API key not found, falling back to Ollama: {e}")
|
| 38 |
-
return ShortResponseOllamaClient(model_name="llama3.2:1b")
|
| 39 |
-
elif provider == "ollama":
|
| 40 |
-
return ShortResponseOllamaClient(model_name="llama3.2:1b")
|
| 41 |
-
else:
|
| 42 |
-
raise ValueError(f"Unknown provider: {provider}")
|
| 43 |
-
|
| 44 |
-
# Initialize with default provider
|
| 45 |
-
llm = create_llm_client()
|
| 46 |
-
chat_orchestrator = ChatOrchestrator()
|
| 47 |
-
seamless_orchestrator = SeamlessOrchestrator(llm=llm)
|
| 48 |
-
|
| 49 |
-
session_context = GlobalSessionContext()
|
| 50 |
-
|
| 51 |
-
# Initialize personas
|
| 52 |
-
DEFAULT_PERSONAS = get_default_personas(llm)
|
| 53 |
-
|
| 54 |
-
for persona in DEFAULT_PERSONAS:
|
| 55 |
-
chat_orchestrator.register_persona(persona)
|
| 56 |
-
|
| 57 |
-
# Data models
|
| 58 |
-
class UserInput(BaseModel):
|
| 59 |
-
user_input: str
|
| 60 |
-
|
| 61 |
-
class PersonaInput(BaseModel):
|
| 62 |
-
id: str
|
| 63 |
-
name: str
|
| 64 |
-
system_prompt: str
|
| 65 |
-
|
| 66 |
-
class ChatMessage(BaseModel):
|
| 67 |
-
user_input: str
|
| 68 |
-
session_id: Optional[str] = None
|
| 69 |
-
response_length: Optional[str] = "medium"
|
| 70 |
-
|
| 71 |
-
class ReplyToAdvisor(BaseModel):
|
| 72 |
-
user_input: str
|
| 73 |
-
advisor_id: str
|
| 74 |
-
original_message_id: Optional[str] = None
|
| 75 |
-
|
| 76 |
-
class ProviderSwitch(BaseModel):
|
| 77 |
-
provider: str
|
| 78 |
-
|
| 79 |
-
# Helper functions for response validation
|
| 80 |
-
def _is_valid_response(response: str, persona_id: str) -> bool:
|
| 81 |
-
"""Validate response quality"""
|
| 82 |
-
if len(response) < 2 or len(response) > 5000:
|
| 83 |
-
return False
|
| 84 |
-
|
| 85 |
-
# Check for AI confusion indicators
|
| 86 |
-
confusion_indicators = [
|
| 87 |
-
f"Thank you, Dr. {persona_id.title()}",
|
| 88 |
-
"Assistant:",
|
| 89 |
-
f"Dr. {persona_id.title()} Advisor:",
|
| 90 |
-
"excellent discussion, Assistant"
|
| 91 |
-
]
|
| 92 |
-
|
| 93 |
-
return not any(indicator in response for indicator in confusion_indicators)
|
| 94 |
-
|
| 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 |
-
}
|
| 102 |
-
return fallbacks.get(persona_id, "I'd be happy to help. Could you provide more details?")
|
| 103 |
-
|
| 104 |
-
# Provider management endpoints
|
| 105 |
-
@router.get("/current-provider")
|
| 106 |
-
async def get_current_provider():
|
| 107 |
-
return {
|
| 108 |
-
"current_provider": current_provider,
|
| 109 |
-
"available_providers": available_providers,
|
| 110 |
-
"model_info": {
|
| 111 |
-
"name": llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash",
|
| 112 |
-
"provider": current_provider
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
@router.post("/switch-provider")
|
| 117 |
-
async def switch_provider(provider_data: ProviderSwitch):
|
| 118 |
-
global current_provider, llm
|
| 119 |
-
|
| 120 |
-
if provider_data.provider not in available_providers:
|
| 121 |
-
raise HTTPException(
|
| 122 |
-
status_code=400,
|
| 123 |
-
detail=f"Unknown provider: {provider_data.provider}. Available: {available_providers}"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
try:
|
| 127 |
-
# Update current provider
|
| 128 |
-
current_provider = provider_data.provider
|
| 129 |
-
|
| 130 |
-
# Create new LLM client
|
| 131 |
-
new_llm = create_llm_client(current_provider)
|
| 132 |
-
llm = new_llm
|
| 133 |
-
|
| 134 |
-
# Update all personas with new LLM
|
| 135 |
-
new_personas = get_default_personas(new_llm)
|
| 136 |
-
chat_orchestrator.personas.clear()
|
| 137 |
-
for persona in new_personas:
|
| 138 |
-
chat_orchestrator.register_persona(persona)
|
| 139 |
-
|
| 140 |
-
# Update seamless orchestrator
|
| 141 |
-
seamless_orchestrator.llm = new_llm
|
| 142 |
-
|
| 143 |
-
return {
|
| 144 |
-
"message": f"Successfully switched to {current_provider}",
|
| 145 |
-
"current_provider": current_provider,
|
| 146 |
-
"model_info": {
|
| 147 |
-
"name": new_llm.model_name if hasattr(new_llm, 'model_name') else "gemini-2.0-flash",
|
| 148 |
-
"provider": current_provider
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
except Exception as e:
|
| 153 |
-
raise HTTPException(
|
| 154 |
-
status_code=500,
|
| 155 |
-
detail=f"Failed to switch to {provider_data.provider}: {str(e)}"
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
# Sequential advisor responses endpoint
|
| 159 |
-
@router.post("/chat-sequential")
|
| 160 |
-
async def chat_sequential(message: ChatMessage):
|
| 161 |
-
"""Generate advisor responses with improved quality controls"""
|
| 162 |
-
|
| 163 |
-
try:
|
| 164 |
-
orchestrator_result = await seamless_orchestrator.process_message(message.user_input)
|
| 165 |
-
|
| 166 |
-
if orchestrator_result["status"] == "orchestrator_asking":
|
| 167 |
-
return {
|
| 168 |
-
"type": "orchestrator_question",
|
| 169 |
-
"responses": [{
|
| 170 |
-
"persona": "PhD Advisor Assistant",
|
| 171 |
-
"response": orchestrator_result["orchestrator_question"]
|
| 172 |
-
}],
|
| 173 |
-
"collected_info": orchestrator_result["collected_info"]
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
elif orchestrator_result["status"] == "ready_for_advisors":
|
| 177 |
-
enhanced_context = orchestrator_result["enhanced_context"]
|
| 178 |
-
|
| 179 |
-
# Clear previous advisor responses to avoid confusion
|
| 180 |
-
session_context.clear()
|
| 181 |
-
session_context.append("user", message.user_input)
|
| 182 |
-
session_context.append("orchestrator", enhanced_context)
|
| 183 |
-
|
| 184 |
-
advisor_order = chat_orchestrator.get_response_order()
|
| 185 |
-
print("Advisor Order:")
|
| 186 |
-
print(advisor_order)
|
| 187 |
-
responses = []
|
| 188 |
-
|
| 189 |
-
for persona_id in advisor_order:
|
| 190 |
-
try:
|
| 191 |
-
persona = chat_orchestrator.personas[persona_id]
|
| 192 |
-
reply = await persona.respond(session_context.full_log, response_length="medium")
|
| 193 |
-
print("Replies:")
|
| 194 |
-
print(reply)
|
| 195 |
-
|
| 196 |
-
# Validate response before adding
|
| 197 |
-
if _is_valid_response(reply, persona_id):
|
| 198 |
-
responses.append({
|
| 199 |
-
"persona": persona.name,
|
| 200 |
-
"persona_id": persona_id,
|
| 201 |
-
"response": reply,
|
| 202 |
-
})
|
| 203 |
-
else:
|
| 204 |
-
# Fallback response for invalid responses
|
| 205 |
-
responses.append({
|
| 206 |
-
"persona": persona.name,
|
| 207 |
-
"persona_id": persona_id,
|
| 208 |
-
"response": _get_persona_fallback(persona_id),
|
| 209 |
-
})
|
| 210 |
-
|
| 211 |
-
session_context.append(persona_id, reply)
|
| 212 |
-
|
| 213 |
-
except Exception as e:
|
| 214 |
-
print(f"Error generating response for {persona_id}: {e}")
|
| 215 |
-
responses.append({
|
| 216 |
-
"persona": chat_orchestrator.personas[persona_id].name,
|
| 217 |
-
"persona_id": persona_id,
|
| 218 |
-
"response": _get_persona_fallback(persona_id),
|
| 219 |
-
})
|
| 220 |
-
|
| 221 |
-
print("Response Block: " )
|
| 222 |
-
print(responses)
|
| 223 |
-
return {
|
| 224 |
-
"type": "sequential_responses",
|
| 225 |
-
"responses": responses,
|
| 226 |
-
"collected_info": orchestrator_result["collected_info"]
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
except Exception as e:
|
| 230 |
-
print(f"Error in chat_sequential: {e}")
|
| 231 |
-
return {
|
| 232 |
-
"type": "error",
|
| 233 |
-
"responses": [{
|
| 234 |
-
"persona": "System",
|
| 235 |
-
"response": "I'm having trouble processing your request. Could you please try again?"
|
| 236 |
-
}]
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
# Individual advisor endpoint with context
|
| 241 |
-
@router.post("/chat/{persona_id}")
|
| 242 |
-
async def chat_with_specific_advisor(persona_id: str, input: UserInput):
|
| 243 |
-
"""Chat with a specific advisor"""
|
| 244 |
-
try:
|
| 245 |
-
if persona_id not in chat_orchestrator.personas:
|
| 246 |
-
raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found")
|
| 247 |
-
|
| 248 |
-
session_context.append("user", input.user_input)
|
| 249 |
-
persona = chat_orchestrator.personas[persona_id]
|
| 250 |
-
context = session_context.full_log.copy()
|
| 251 |
-
reply = await persona.respond(context, response_length="medium")
|
| 252 |
-
session_context.append(persona_id, reply)
|
| 253 |
-
|
| 254 |
-
return {
|
| 255 |
-
"persona": persona.name,
|
| 256 |
-
"persona_id": persona_id,
|
| 257 |
-
"response": reply
|
| 258 |
-
}
|
| 259 |
-
except HTTPException:
|
| 260 |
-
raise
|
| 261 |
-
except Exception as e:
|
| 262 |
-
print(f"Error in chat_with_specific_advisor: {e}")
|
| 263 |
-
return {
|
| 264 |
-
"persona": "System",
|
| 265 |
-
"response": "I'm having trouble generating a response right now. Please try again."
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
# Reply to specific advisor endpoint
|
| 269 |
-
@router.post("/reply-to-advisor")
|
| 270 |
-
async def reply_to_advisor(reply: ReplyToAdvisor):
|
| 271 |
-
"""Reply to a specific advisor and get response only from that advisor"""
|
| 272 |
-
|
| 273 |
-
try:
|
| 274 |
-
if reply.advisor_id not in chat_orchestrator.personas:
|
| 275 |
-
raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found")
|
| 276 |
-
|
| 277 |
-
# Add user reply to context
|
| 278 |
-
session_context.append("user", reply.user_input)
|
| 279 |
-
|
| 280 |
-
# Get response from specific advisor
|
| 281 |
-
persona = chat_orchestrator.personas[reply.advisor_id]
|
| 282 |
-
|
| 283 |
-
# Generate response
|
| 284 |
-
reply_response = await persona.respond(session_context.full_log, response_length="medium")
|
| 285 |
-
session_context.append(reply.advisor_id, reply_response)
|
| 286 |
-
|
| 287 |
-
return {
|
| 288 |
-
"type": "advisor_reply",
|
| 289 |
-
"persona": persona.name,
|
| 290 |
-
"persona_id": reply.advisor_id,
|
| 291 |
-
"response": reply_response,
|
| 292 |
-
"original_message_id": reply.original_message_id
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
except HTTPException:
|
| 296 |
-
raise
|
| 297 |
-
except Exception as e:
|
| 298 |
-
print(f"Error in reply_to_advisor: {e}")
|
| 299 |
-
return {
|
| 300 |
-
"type": "error",
|
| 301 |
-
"persona": "System",
|
| 302 |
-
"response": "I'm having trouble generating a reply right now. Please try again."
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
# Reset session
|
| 306 |
-
@router.post("/reset-session")
|
| 307 |
-
async def reset_session():
|
| 308 |
-
try:
|
| 309 |
-
seamless_orchestrator.reset()
|
| 310 |
-
session_context.clear()
|
| 311 |
-
return {"status": "reset", "message": "Session reset successfully"}
|
| 312 |
-
except Exception as e:
|
| 313 |
-
print(f"Error resetting session: {e}")
|
| 314 |
-
return {"status": "error", "message": "Failed to reset session"}
|
| 315 |
-
|
| 316 |
-
# Context inspection
|
| 317 |
-
@router.get("/context")
|
| 318 |
-
def get_context():
|
| 319 |
-
return session_context.full_log
|
| 320 |
-
|
| 321 |
-
# Legacy model switching endpoint (now redirects to provider switching)
|
| 322 |
-
@router.post("/switch-model")
|
| 323 |
-
async def switch_model(model_name: str = Body(...)):
|
| 324 |
-
# For backward compatibility, try to map model names to providers
|
| 325 |
-
if "gemini" in model_name.lower():
|
| 326 |
-
return await switch_provider(ProviderSwitch(provider="gemini"))
|
| 327 |
-
else:
|
| 328 |
-
return await switch_provider(ProviderSwitch(provider="ollama"))
|
| 329 |
-
|
| 330 |
-
@router.get("/current-model")
|
| 331 |
-
async def get_current_model():
|
| 332 |
-
# For backward compatibility
|
| 333 |
-
model_name = llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash"
|
| 334 |
-
return {
|
| 335 |
-
"model": model_name,
|
| 336 |
-
"provider": current_provider
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
@router.post("/upload-document")
|
| 340 |
-
async def upload_document(file: UploadFile = File(...)):
|
| 341 |
-
# Validate file type
|
| 342 |
-
if file.content_type not in [
|
| 343 |
-
"application/pdf",
|
| 344 |
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 345 |
-
"text/plain"
|
| 346 |
-
]:
|
| 347 |
-
raise HTTPException(status_code=400, detail="Unsupported file type.")
|
| 348 |
-
|
| 349 |
-
try:
|
| 350 |
-
# Read file bytes
|
| 351 |
-
file_bytes = await file.read()
|
| 352 |
-
|
| 353 |
-
# Check file size limit
|
| 354 |
-
if not is_within_upload_limit("default", file_bytes, session_context):
|
| 355 |
-
raise HTTPException(status_code=400, detail="Upload exceeds session document size limit (10 MB).")
|
| 356 |
-
|
| 357 |
-
# Extract and validate text
|
| 358 |
-
content = extract_text_from_file(file_bytes, file.content_type)
|
| 359 |
-
if not content.strip():
|
| 360 |
-
raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
|
| 361 |
-
|
| 362 |
-
# Track file size and name
|
| 363 |
-
session_context.append("Document", f"[Uploaded Document Content]\n{content.strip()}")
|
| 364 |
-
session_context.uploaded_files.append(file.filename)
|
| 365 |
-
session_context.total_upload_size += len(file_bytes)
|
| 366 |
-
|
| 367 |
-
return {"message": "Document uploaded and added to context successfully."}
|
| 368 |
-
|
| 369 |
-
except Exception as e:
|
| 370 |
-
raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
|
| 371 |
-
|
| 372 |
-
# Debug endpoint
|
| 373 |
-
@router.get("/debug/personas")
|
| 374 |
-
async def debug_personas():
|
| 375 |
-
return {
|
| 376 |
-
"personas": {
|
| 377 |
-
pid: {
|
| 378 |
-
"name": persona.name,
|
| 379 |
-
"prompt": persona.system_prompt[:100] + "..."
|
| 380 |
-
} for pid, persona in chat_orchestrator.personas.items()
|
| 381 |
-
},
|
| 382 |
-
"context_length": len(session_context.full_log),
|
| 383 |
-
"current_provider": current_provider
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
class PersonaQuery(BaseModel):
|
| 387 |
-
question: str
|
| 388 |
-
persona: str
|
| 389 |
-
|
| 390 |
-
@router.post("/ask/")
|
| 391 |
-
async def ask_question(query: PersonaQuery):
|
| 392 |
-
response = await answer_with_persona_context(query.question, query.persona)
|
| 393 |
-
|
| 394 |
-
# Store Q&A in vector DB
|
| 395 |
-
combined_text = f"Q: {query.question}\nA: {response}"
|
| 396 |
-
doc_id = hashlib.md5(combined_text.encode()).hexdigest() # Create a unique doc ID
|
| 397 |
-
|
| 398 |
-
add_persona_doc(combined_text, query.persona, doc_id)
|
| 399 |
-
|
| 400 |
-
return {"response": response}
|
| 401 |
-
|
| 402 |
-
@router.get("/uploaded-files")
|
| 403 |
-
def get_uploaded_filenames():
|
| 404 |
-
return {"files": session_context.uploaded_files}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/core/orchestrator.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
from app.models.persona import Persona
|
| 2 |
-
from typing import List
|
| 3 |
-
from app.utils.chroma_client import query_persona_knowledge
|
| 4 |
-
from app.llm.gemini_client import GeminiClient
|
| 5 |
-
from app.llm.llm_client import LLMClient
|
| 6 |
-
|
| 7 |
-
class ChatOrchestrator:
|
| 8 |
-
def __init__(self):
|
| 9 |
-
self.personas: dict[str, Persona] = {}
|
| 10 |
-
self.active_personas: List[str] = [] # renamed for clarity
|
| 11 |
-
|
| 12 |
-
def register_persona(self, persona: Persona):
|
| 13 |
-
self.personas[persona.id] = persona
|
| 14 |
-
|
| 15 |
-
def set_active_personas(self, ids: List[str]):
|
| 16 |
-
self.active_personas = [pid for pid in ids if pid in self.personas]
|
| 17 |
-
|
| 18 |
-
def get_active_personas(self) -> List[str]:
|
| 19 |
-
return self.active_personas
|
| 20 |
-
|
| 21 |
-
def get_response_order(self) ->List[str]:
|
| 22 |
-
|
| 23 |
-
# I have created this function to be a placeholder for the actual logic of response sequencing
|
| 24 |
-
# This logic can be replaced with something smarter like a LLM deciding order based on chat context
|
| 25 |
-
|
| 26 |
-
return self.personas
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
async def answer_with_persona_context(question: str, persona: str) -> str:
|
| 31 |
-
llm: LLMClient = GeminiClient()
|
| 32 |
-
context_chunks = query_persona_knowledge(question, persona)
|
| 33 |
-
context = "\n".join(context_chunks) if context_chunks else "No relevant info found."
|
| 34 |
-
|
| 35 |
-
system_prompt = f"You are a helpful assistant responding as a {persona}."
|
| 36 |
-
message_context = [
|
| 37 |
-
{"role": "system", "content": system_prompt},
|
| 38 |
-
{"role": "user", "content": f"Context:\n{context}\n\nQuestion:\n{question}"}
|
| 39 |
-
]
|
| 40 |
-
|
| 41 |
-
return await llm.generate(system_prompt, message_context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/llm/gemini_client.py
DELETED
|
@@ -1,155 +0,0 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
import os
|
| 3 |
-
from typing import List
|
| 4 |
-
from app.llm.llm_client import LLMClient
|
| 5 |
-
|
| 6 |
-
class GeminiClient(LLMClient):
|
| 7 |
-
def __init__(self, model_name: str = None):
|
| 8 |
-
if model_name is None:
|
| 9 |
-
model_name = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp")
|
| 10 |
-
|
| 11 |
-
self.model_name = model_name
|
| 12 |
-
self.api_key = os.getenv("GEMINI_API_KEY")
|
| 13 |
-
print("DEBUG: GEMINI_API_KEY =", os.getenv("GEMINI_API_KEY"))
|
| 14 |
-
if not self.api_key:
|
| 15 |
-
raise ValueError("GEMINI_API_KEY environment variable is required")
|
| 16 |
-
|
| 17 |
-
self.base_url = "https://generativelanguage.googleapis.com/v1beta/models"
|
| 18 |
-
|
| 19 |
-
async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
|
| 20 |
-
# Build focused context - only include recent relevant messages
|
| 21 |
-
recent_context = context[-3:] if len(context) > 3 else context
|
| 22 |
-
|
| 23 |
-
# Format messages for Gemini API
|
| 24 |
-
formatted_messages = []
|
| 25 |
-
|
| 26 |
-
# Add system instruction as initial conversation
|
| 27 |
-
if system_prompt:
|
| 28 |
-
formatted_messages.append({
|
| 29 |
-
"role": "user",
|
| 30 |
-
"parts": [{"text": system_prompt}]
|
| 31 |
-
})
|
| 32 |
-
formatted_messages.append({
|
| 33 |
-
"role": "model",
|
| 34 |
-
"parts": [{"text": "I understand. I'll follow these instructions."}]
|
| 35 |
-
})
|
| 36 |
-
|
| 37 |
-
# Add only the user's current question
|
| 38 |
-
for msg in recent_context:
|
| 39 |
-
if msg['role'] == 'user':
|
| 40 |
-
formatted_messages.append({
|
| 41 |
-
"role": "user",
|
| 42 |
-
"parts": [{"text": msg['content']}]
|
| 43 |
-
})
|
| 44 |
-
break # Only use most recent user message
|
| 45 |
-
|
| 46 |
-
payload = {
|
| 47 |
-
"contents": formatted_messages,
|
| 48 |
-
"generationConfig": {
|
| 49 |
-
"temperature": temperature,
|
| 50 |
-
"topK": 40,
|
| 51 |
-
"topP": 0.9,
|
| 52 |
-
"maxOutputTokens": max_tokens, # Reduced from default to ensure concise responses
|
| 53 |
-
"stopSequences": ["Student:", "Question:", "\n\nStudent:", "\n\nQuestion:"]
|
| 54 |
-
},
|
| 55 |
-
"safetySettings": [
|
| 56 |
-
{
|
| 57 |
-
"category": "HARM_CATEGORY_HARASSMENT",
|
| 58 |
-
"threshold": "BLOCK_MEDIUM_AND_ABOVE"
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
"category": "HARM_CATEGORY_HATE_SPEECH",
|
| 62 |
-
"threshold": "BLOCK_MEDIUM_AND_ABOVE"
|
| 63 |
-
},
|
| 64 |
-
{
|
| 65 |
-
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
| 66 |
-
"threshold": "BLOCK_MEDIUM_AND_ABOVE"
|
| 67 |
-
},
|
| 68 |
-
{
|
| 69 |
-
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 70 |
-
"threshold": "BLOCK_MEDIUM_AND_ABOVE"
|
| 71 |
-
}
|
| 72 |
-
]
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
try:
|
| 76 |
-
url = f"{self.base_url}/{self.model_name}:generateContent?key={self.api_key}"
|
| 77 |
-
|
| 78 |
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 79 |
-
response = await client.post(url, json=payload)
|
| 80 |
-
response.raise_for_status()
|
| 81 |
-
|
| 82 |
-
result = response.json()
|
| 83 |
-
|
| 84 |
-
# Extract text from Gemini response
|
| 85 |
-
if "candidates" in result and len(result["candidates"]) > 0:
|
| 86 |
-
candidate = result["candidates"][0]
|
| 87 |
-
if "content" in candidate and "parts" in candidate["content"]:
|
| 88 |
-
parts = candidate["content"]["parts"]
|
| 89 |
-
if len(parts) > 0 and "text" in parts[0]:
|
| 90 |
-
text = parts[0]["text"].strip()
|
| 91 |
-
|
| 92 |
-
# Clean up response
|
| 93 |
-
text = self._clean_response(text)
|
| 94 |
-
|
| 95 |
-
# Validate response quality
|
| 96 |
-
if len(text) < 20 or self._is_poor_quality(text):
|
| 97 |
-
print("Poor Response Quality from Gemini")
|
| 98 |
-
return self._get_fallback_response()
|
| 99 |
-
|
| 100 |
-
return text
|
| 101 |
-
|
| 102 |
-
print("Candidate Issue")
|
| 103 |
-
return self._get_fallback_response()
|
| 104 |
-
|
| 105 |
-
except httpx.HTTPError as e:
|
| 106 |
-
print(f"HTTP error calling Gemini API: {e}")
|
| 107 |
-
return "I'm having trouble connecting to generate a response. Please try again."
|
| 108 |
-
except Exception as e:
|
| 109 |
-
print(f"Error calling Gemini API: {e}")
|
| 110 |
-
return "I'm having trouble generating a response right now. Please try again."
|
| 111 |
-
|
| 112 |
-
def _clean_response(self, response: str) -> str:
|
| 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 |
-
]
|
| 121 |
-
|
| 122 |
-
for prefix in prefixes_to_remove:
|
| 123 |
-
if response.startswith(prefix):
|
| 124 |
-
response = response[len(prefix):].strip()
|
| 125 |
-
|
| 126 |
-
# Remove excessive academic fluff
|
| 127 |
-
fluff_patterns = [
|
| 128 |
-
"conceptual insights:", "actionable advice:", "my inquisitive student",
|
| 129 |
-
"excellent question", "thank you for", "assistant!", "excellent discussion"
|
| 130 |
-
]
|
| 131 |
-
|
| 132 |
-
for pattern in fluff_patterns:
|
| 133 |
-
response = response.replace(pattern, "").strip()
|
| 134 |
-
|
| 135 |
-
# Remove trailing incomplete sentences
|
| 136 |
-
sentences = response.split('.')
|
| 137 |
-
if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
|
| 138 |
-
response = '.'.join(sentences[:-1]) + '.'
|
| 139 |
-
|
| 140 |
-
return response
|
| 141 |
-
|
| 142 |
-
def _is_poor_quality(self, response: str) -> bool:
|
| 143 |
-
"""Check if response quality is poor"""
|
| 144 |
-
poor_indicators = [
|
| 145 |
-
"Thank you, Dr." in response, # AI confusion about identity
|
| 146 |
-
"Assistant:" in response,
|
| 147 |
-
len(response.split()) > 700, # Too verbose
|
| 148 |
-
response.count("?") > 3, # Too many questions
|
| 149 |
-
"excellent discussion, Assistant" in response,
|
| 150 |
-
]
|
| 151 |
-
return any(poor_indicators)
|
| 152 |
-
|
| 153 |
-
def _get_fallback_response(self) -> str:
|
| 154 |
-
"""Return a simple fallback when quality is poor"""
|
| 155 |
-
return "I'd be happy to help with that. Could you provide more specific details about what you're looking for?"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/llm/mistral_client.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
from typing import List
|
| 3 |
-
from app.llm.llm_client import LLMClient
|
| 4 |
-
|
| 5 |
-
OLLAMA_URL = "http://localhost:11434/api/generate"
|
| 6 |
-
|
| 7 |
-
class MistralClient(LLMClient):
|
| 8 |
-
async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
|
| 9 |
-
# Flatten context into a string
|
| 10 |
-
formatted = "\n".join(f"{msg['role'].capitalize()}: {msg['content']}" for msg in context)
|
| 11 |
-
full_prompt = f"{system_prompt}\n\n{formatted}\n\nAssistant:"
|
| 12 |
-
|
| 13 |
-
payload = {
|
| 14 |
-
"model": "mistral",
|
| 15 |
-
"prompt": full_prompt,
|
| 16 |
-
"temperature": temperature,
|
| 17 |
-
"num_predict": max_tokens,
|
| 18 |
-
"stream": False
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
try:
|
| 22 |
-
async with httpx.AsyncClient(timeout=60.0) as client:
|
| 23 |
-
response = await client.post(OLLAMA_URL, json=payload)
|
| 24 |
-
response.raise_for_status()
|
| 25 |
-
return response.json().get("response", "[No response]")
|
| 26 |
-
except httpx.HTTPError as e:
|
| 27 |
-
return f"[Error from Mistral: {str(e)}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/llm/ollama_client.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
from typing import List
|
| 3 |
-
from app.llm.llm_client import LLMClient
|
| 4 |
-
|
| 5 |
-
OLLAMA_URL = "http://localhost:11434/api/generate"
|
| 6 |
-
|
| 7 |
-
class OllamaClient(LLMClient):
|
| 8 |
-
def __init__(self, model_name: str = "llama3.2:1b"):
|
| 9 |
-
self.model_name = model_name
|
| 10 |
-
|
| 11 |
-
async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
|
| 12 |
-
# Flatten context into a string
|
| 13 |
-
formatted = "\n".join(f"{msg['role'].capitalize()}: {msg['content']}" for msg in context)
|
| 14 |
-
full_prompt = f"{system_prompt}\n\n{formatted}\n\nAssistant:"
|
| 15 |
-
|
| 16 |
-
payload = {
|
| 17 |
-
"model": self.model_name,
|
| 18 |
-
"prompt": full_prompt,
|
| 19 |
-
"stream": False,
|
| 20 |
-
"options": {
|
| 21 |
-
"temperature": temperature,
|
| 22 |
-
"top_p": 0.9,
|
| 23 |
-
"top_k": 40,
|
| 24 |
-
"num_predict": max_tokens, # Limit response length for faster generation
|
| 25 |
-
}
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
async with httpx.AsyncClient(timeout=30.0) as client: # Reduced timeout
|
| 30 |
-
response = await client.post(OLLAMA_URL, json=payload)
|
| 31 |
-
response.raise_for_status()
|
| 32 |
-
return response.json().get("response", "[No response]")
|
| 33 |
-
except httpx.HTTPError as e:
|
| 34 |
-
return f"[Error from {self.model_name}: {str(e)}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multi_llm_chatbot_backend/app/llm/short_ollama_client.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
from typing import List
|
| 3 |
-
from app.llm.llm_client import LLMClient
|
| 4 |
-
|
| 5 |
-
OLLAMA_URL = "http://localhost:11434/api/generate"
|
| 6 |
-
|
| 7 |
-
# Improved LLM client with better short response handling for Ollama
|
| 8 |
-
class ShortResponseOllamaClient(LLMClient):
|
| 9 |
-
def __init__(self, model_name: str = "llama3.2:1b"):
|
| 10 |
-
self.model_name = model_name
|
| 11 |
-
|
| 12 |
-
async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
|
| 13 |
-
# Build cleaner context - only include recent relevant messages
|
| 14 |
-
recent_context = context[-3:] if len(context) > 3 else context
|
| 15 |
-
|
| 16 |
-
# Create a focused prompt
|
| 17 |
-
prompt_parts = [system_prompt]
|
| 18 |
-
|
| 19 |
-
# Add only the user's current question
|
| 20 |
-
for msg in recent_context:
|
| 21 |
-
if msg['role'] == 'user':
|
| 22 |
-
prompt_parts.append(f"Student Question: {msg['content']}")
|
| 23 |
-
break # Only use the most recent user message
|
| 24 |
-
|
| 25 |
-
prompt_parts.append("Your Response:")
|
| 26 |
-
prompt = "\n\n".join(prompt_parts)
|
| 27 |
-
|
| 28 |
-
payload = {
|
| 29 |
-
"model": self.model_name,
|
| 30 |
-
"prompt": prompt,
|
| 31 |
-
"stream": False,
|
| 32 |
-
"options": {
|
| 33 |
-
"temperature": temperature,
|
| 34 |
-
"top_p": 0.9,
|
| 35 |
-
"top_k": 40,
|
| 36 |
-
"num_predict": max_tokens, # Reduced from 200 to force shorter responses
|
| 37 |
-
"repeat_penalty": 1.1,
|
| 38 |
-
"stop": ["\n\n", "Student:", "Question:", "Response:"] # Stop tokens
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
try:
|
| 43 |
-
async with httpx.AsyncClient(timeout=25.0) as client:
|
| 44 |
-
response = await client.post("http://localhost:11434/api/generate", json=payload)
|
| 45 |
-
response.raise_for_status()
|
| 46 |
-
result = response.json().get("response", "[No response]").strip()
|
| 47 |
-
|
| 48 |
-
# Enhanced cleanup
|
| 49 |
-
result = self._clean_response(result)
|
| 50 |
-
|
| 51 |
-
# Validate response quality
|
| 52 |
-
if len(result) < 20 or self._is_poor_quality(result):
|
| 53 |
-
return self._get_fallback_response()
|
| 54 |
-
|
| 55 |
-
return result
|
| 56 |
-
|
| 57 |
-
except Exception as e:
|
| 58 |
-
return "I'm having trouble generating a response right now. Please try again."
|
| 59 |
-
|
| 60 |
-
def _clean_response(self, response: str) -> str:
|
| 61 |
-
"""Clean up common response issues"""
|
| 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:
|
| 70 |
-
if response.startswith(prefix):
|
| 71 |
-
response = response[len(prefix):].strip()
|
| 72 |
-
|
| 73 |
-
# Remove trailing incomplete sentences
|
| 74 |
-
sentences = response.split('.')
|
| 75 |
-
if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
|
| 76 |
-
response = '.'.join(sentences[:-1]) + '.'
|
| 77 |
-
|
| 78 |
-
# Remove excessive academic fluff
|
| 79 |
-
fluff_patterns = [
|
| 80 |
-
"conceptual insights:", "actionable advice:", "my inquisitive student",
|
| 81 |
-
"excellent question", "thank you for", "assistant!"
|
| 82 |
-
]
|
| 83 |
-
|
| 84 |
-
for pattern in fluff_patterns:
|
| 85 |
-
response = response.replace(pattern, "").strip()
|
| 86 |
-
|
| 87 |
-
return response
|
| 88 |
-
|
| 89 |
-
def _is_poor_quality(self, response: str) -> bool:
|
| 90 |
-
"""Check if response quality is poor"""
|
| 91 |
-
poor_indicators = [
|
| 92 |
-
"Thank you, Dr." in response, # AI confusion about identity
|
| 93 |
-
"Assistant:" in response,
|
| 94 |
-
len(response.split()) > 100, # Too verbose
|
| 95 |
-
response.count("?") > 3, # Too many questions
|
| 96 |
-
]
|
| 97 |
-
return any(poor_indicators)
|
| 98 |
-
|
| 99 |
-
def _get_fallback_response(self) -> str:
|
| 100 |
-
"""Return a simple fallback when quality is poor"""
|
| 101 |
-
return "I'd be happy to help with that. Could you provide more specific details about what you're looking for?"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
phd-advisor-frontend/README.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
| 1 |
-
# Getting Started with Create React App
|
| 2 |
-
|
| 3 |
-
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 4 |
-
|
| 5 |
-
## Available Scripts
|
| 6 |
-
|
| 7 |
-
In the project directory, you can run:
|
| 8 |
-
|
| 9 |
-
### `npm start`
|
| 10 |
-
|
| 11 |
-
Runs the app in the development mode.\
|
| 12 |
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
| 13 |
-
|
| 14 |
-
The page will reload when you make changes.\
|
| 15 |
-
You may also see any lint errors in the console.
|
| 16 |
-
|
| 17 |
-
### `npm test`
|
| 18 |
-
|
| 19 |
-
Launches the test runner in the interactive watch mode.\
|
| 20 |
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 21 |
-
|
| 22 |
-
### `npm run build`
|
| 23 |
-
|
| 24 |
-
Builds the app for production to the `build` folder.\
|
| 25 |
-
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 26 |
-
|
| 27 |
-
The build is minified and the filenames include the hashes.\
|
| 28 |
-
Your app is ready to be deployed!
|
| 29 |
-
|
| 30 |
-
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 31 |
-
|
| 32 |
-
### `npm run eject`
|
| 33 |
-
|
| 34 |
-
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
| 35 |
-
|
| 36 |
-
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 37 |
-
|
| 38 |
-
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
| 39 |
-
|
| 40 |
-
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
| 41 |
-
|
| 42 |
-
## Learn More
|
| 43 |
-
|
| 44 |
-
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 45 |
-
|
| 46 |
-
To learn React, check out the [React documentation](https://reactjs.org/).
|
| 47 |
-
|
| 48 |
-
### Code Splitting
|
| 49 |
-
|
| 50 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
| 51 |
-
|
| 52 |
-
### Analyzing the Bundle Size
|
| 53 |
-
|
| 54 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
| 55 |
-
|
| 56 |
-
### Making a Progressive Web App
|
| 57 |
-
|
| 58 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
| 59 |
-
|
| 60 |
-
### Advanced Configuration
|
| 61 |
-
|
| 62 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
| 63 |
-
|
| 64 |
-
### Deployment
|
| 65 |
-
|
| 66 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
| 67 |
-
|
| 68 |
-
### `npm run build` fails to minify
|
| 69 |
-
|
| 70 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|