Spaces:
Sleeping
Sleeping
merge into main
Browse files
multi_llm_chatbot_backend/app/api/routes.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
from fastapi import APIRouter, Body, HTTPException, Header, UploadFile, File, Request
|
|
|
|
| 3 |
from typing import Optional, List
|
| 4 |
import httpx
|
| 5 |
from app.llm.llm_client import LLMClient
|
|
@@ -13,6 +14,11 @@ from app.models.default_personas import get_default_personas
|
|
| 13 |
from app.utils.document_extractor import extract_text_from_file
|
| 14 |
from app.utils.file_limits import is_within_upload_limit
|
| 15 |
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
import hashlib
|
| 17 |
import logging
|
| 18 |
|
|
@@ -423,6 +429,33 @@ async def upload_document(file: UploadFile = File(...), request: Request = None)
|
|
| 423 |
logger.error(f"Error processing document upload: {str(e)}")
|
| 424 |
raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
# Add new endpoint to get document statistics
|
| 427 |
@router.get("/document-stats")
|
| 428 |
async def get_document_stats(request: Request):
|
|
@@ -596,6 +629,32 @@ async def debug_personas(request: Request):
|
|
| 596 |
"error": str(e)
|
| 597 |
}
|
| 598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
@router.post("/chat/{persona_id}")
|
| 600 |
async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request):
|
| 601 |
"""
|
|
|
|
| 1 |
import os
|
| 2 |
from fastapi import APIRouter, Body, HTTPException, Header, UploadFile, File, Request
|
| 3 |
+
from fastapi import Query
|
| 4 |
from typing import Optional, List
|
| 5 |
import httpx
|
| 6 |
from app.llm.llm_client import LLMClient
|
|
|
|
| 14 |
from app.utils.document_extractor import extract_text_from_file
|
| 15 |
from app.utils.file_limits import is_within_upload_limit
|
| 16 |
from pydantic import BaseModel
|
| 17 |
+
|
| 18 |
+
from fastapi.responses import StreamingResponse
|
| 19 |
+
from fastapi import Query
|
| 20 |
+
from app.utils.file_export import export_chat_as_file
|
| 21 |
+
|
| 22 |
import hashlib
|
| 23 |
import logging
|
| 24 |
|
|
|
|
| 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 |
+
|
| 433 |
+
@router.get("/export-chat")
|
| 434 |
+
async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")):
|
| 435 |
+
"""
|
| 436 |
+
Export the current chat context as a downloadable file in txt, pdf, or docx format.
|
| 437 |
+
"""
|
| 438 |
+
try:
|
| 439 |
+
session_id = get_or_create_session_for_request(request)
|
| 440 |
+
session = session_manager.get_session(session_id)
|
| 441 |
+
|
| 442 |
+
if not session.messages:
|
| 443 |
+
return {"error": "No messages to export in current session."}
|
| 444 |
+
|
| 445 |
+
# Generate the file using utility
|
| 446 |
+
file_stream, filename, media_type = export_chat_as_file(session.messages, format)
|
| 447 |
+
|
| 448 |
+
return StreamingResponse(
|
| 449 |
+
file_stream,
|
| 450 |
+
media_type=media_type,
|
| 451 |
+
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
except Exception as e:
|
| 455 |
+
logger.error(f"Error exporting chat: {str(e)}")
|
| 456 |
+
return {"error": "Failed to export chat.", "detail": str(e)}
|
| 457 |
+
|
| 458 |
+
|
| 459 |
# Add new endpoint to get document statistics
|
| 460 |
@router.get("/document-stats")
|
| 461 |
async def get_document_stats(request: Request):
|
|
|
|
| 629 |
"error": str(e)
|
| 630 |
}
|
| 631 |
|
| 632 |
+
@router.get("/debug/ranked-personas")
|
| 633 |
+
async def get_ranked_personas(request: Request, k: int = Query(3, ge=1, le=10)):
|
| 634 |
+
"""
|
| 635 |
+
Debug endpoint: Get top-k ranked personas based on current session context.
|
| 636 |
+
Uses LLM to rank based on latest conversation messages.
|
| 637 |
+
"""
|
| 638 |
+
try:
|
| 639 |
+
session_id = get_or_create_session_for_request(request)
|
| 640 |
+
|
| 641 |
+
# Call the ranking method
|
| 642 |
+
top_personas = await chat_orchestrator.get_top_personas(session_id=session_id, k=k)
|
| 643 |
+
|
| 644 |
+
# Include some metadata for debug purposes
|
| 645 |
+
return {
|
| 646 |
+
"ranked_personas": top_personas,
|
| 647 |
+
"available_personas": list(chat_orchestrator.personas.keys()),
|
| 648 |
+
"session_id": session_id
|
| 649 |
+
}
|
| 650 |
+
except Exception as e:
|
| 651 |
+
logger.error(f"Error in /debug/ranked-personas: {e}")
|
| 652 |
+
return {
|
| 653 |
+
"ranked_personas": [],
|
| 654 |
+
"error": str(e)
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
|
| 658 |
@router.post("/chat/{persona_id}")
|
| 659 |
async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request):
|
| 660 |
"""
|
multi_llm_chatbot_backend/app/core/improved_orchestrator.py
CHANGED
|
@@ -4,6 +4,9 @@ from app.core.session_manager import ConversationContext, get_session_manager
|
|
| 4 |
from app.core.context_manager import get_context_manager
|
| 5 |
from app.core.rag_manager import get_rag_manager
|
| 6 |
from app.llm.llm_client import LLMClient
|
|
|
|
|
|
|
|
|
|
| 7 |
import logging
|
| 8 |
import re
|
| 9 |
|
|
@@ -594,4 +597,72 @@ When analyzing the document context:
|
|
| 594 |
return {
|
| 595 |
"error": f"Error processing request: {str(e)}",
|
| 596 |
"persona_id": persona_id
|
| 597 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from app.core.context_manager import get_context_manager
|
| 5 |
from app.core.rag_manager import get_rag_manager
|
| 6 |
from app.llm.llm_client import LLMClient
|
| 7 |
+
from app.models.default_personas import is_valid_persona_id
|
| 8 |
+
|
| 9 |
+
import json
|
| 10 |
import logging
|
| 11 |
import re
|
| 12 |
|
|
|
|
| 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]:
|
| 604 |
+
"""
|
| 605 |
+
Use the LLM to rank personas based on current session context.
|
| 606 |
+
Falls back to default persona order if LLM fails or returns invalid data.
|
| 607 |
+
"""
|
| 608 |
+
try:
|
| 609 |
+
session = self.session_manager.get_session(session_id)
|
| 610 |
+
|
| 611 |
+
if not self.personas:
|
| 612 |
+
logger.warning("No personas registered.")
|
| 613 |
+
return []
|
| 614 |
+
|
| 615 |
+
# Use the LLM from one of the existing persona objects
|
| 616 |
+
llm = next(iter(self.personas.values())).llm
|
| 617 |
+
|
| 618 |
+
# Use recent conversation context (last 5 messages)
|
| 619 |
+
recent_context = "\n".join(
|
| 620 |
+
msg['content'] for msg in session.get_recent_messages(5)
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
# Format available persona descriptions
|
| 624 |
+
persona_descriptions = "\n".join([
|
| 625 |
+
f"- ID: {p.id}\n Name: {p.name}\n Prompt: {p.system_prompt.strip()}"
|
| 626 |
+
for p in self.personas.values()
|
| 627 |
+
])
|
| 628 |
+
|
| 629 |
+
prompt = f"""
|
| 630 |
+
The user is seeking PhD advice. Based on the conversation below, choose the top {k} most relevant advisors.
|
| 631 |
+
|
| 632 |
+
Respond ONLY with a JSON list of exactly {k} advisor IDs in order of relevance.
|
| 633 |
+
Example response: ["methodist", "pragmatist", "theorist"]
|
| 634 |
+
|
| 635 |
+
--- Conversation ---
|
| 636 |
+
{recent_context}
|
| 637 |
+
|
| 638 |
+
--- Available Advisors ---
|
| 639 |
+
{persona_descriptions}
|
| 640 |
+
""".strip()
|
| 641 |
+
|
| 642 |
+
llm_response = await llm.generate(
|
| 643 |
+
system_prompt="You are an assistant that selects the best advisors for a PhD student.",
|
| 644 |
+
context=[{"role": "user", "content": prompt}],
|
| 645 |
+
temperature=0.4,
|
| 646 |
+
max_tokens=150
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
# Step 1: Try direct JSON load
|
| 650 |
+
try:
|
| 651 |
+
top_ids = json.loads(llm_response.strip())
|
| 652 |
+
except json.JSONDecodeError:
|
| 653 |
+
# Step 2: Fallback: try extracting list of quoted strings
|
| 654 |
+
top_ids = re.findall(r'"(.*?)"', llm_response)
|
| 655 |
+
logger.warning(f"Fallback JSON extraction used: {top_ids}")
|
| 656 |
+
|
| 657 |
+
# Step 3: Filter valid persona IDs
|
| 658 |
+
valid_ids = [pid for pid in top_ids if pid in self.personas]
|
| 659 |
+
|
| 660 |
+
if len(valid_ids) < k:
|
| 661 |
+
logger.warning(f"LLM returned insufficient or invalid IDs. Got: {valid_ids}")
|
| 662 |
+
return list(self.personas.keys())[:k]
|
| 663 |
+
|
| 664 |
+
return valid_ids[:k]
|
| 665 |
+
|
| 666 |
+
except Exception as e:
|
| 667 |
+
logger.error(f"Error selecting top personas: {e}")
|
| 668 |
+
return list(self.personas.keys())[:k]
|
multi_llm_chatbot_backend/app/utils/file_export.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from io import BytesIO
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
from docx import Document
|
| 4 |
+
from reportlab.lib.pagesizes import letter
|
| 5 |
+
from reportlab.pdfgen import canvas
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def format_messages_for_export(messages: List[dict]) -> str:
|
| 9 |
+
"""
|
| 10 |
+
Convert chat messages into a structured exportable string.
|
| 11 |
+
"""
|
| 12 |
+
return "\n\n".join([f"{m['role']}:\n{m['content'].strip()}" for m in messages])
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def generate_txt_file(text: str) -> BytesIO:
|
| 16 |
+
buffer = BytesIO()
|
| 17 |
+
buffer.write(text.encode("utf-8"))
|
| 18 |
+
buffer.seek(0)
|
| 19 |
+
return buffer
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate_docx_file(text: str) -> BytesIO:
|
| 23 |
+
doc = Document()
|
| 24 |
+
for block in text.split("\n\n"):
|
| 25 |
+
doc.add_paragraph(block)
|
| 26 |
+
doc.add_paragraph("") # spacing
|
| 27 |
+
|
| 28 |
+
buffer = BytesIO()
|
| 29 |
+
doc.save(buffer)
|
| 30 |
+
buffer.seek(0)
|
| 31 |
+
return buffer
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def generate_pdf_file(text: str) -> BytesIO:
|
| 35 |
+
buffer = BytesIO()
|
| 36 |
+
c = canvas.Canvas(buffer, pagesize=letter)
|
| 37 |
+
|
| 38 |
+
width, height = letter
|
| 39 |
+
y = height - 40
|
| 40 |
+
line_height = 14
|
| 41 |
+
|
| 42 |
+
for line in text.splitlines():
|
| 43 |
+
if y < 40:
|
| 44 |
+
c.showPage()
|
| 45 |
+
y = height - 40
|
| 46 |
+
c.drawString(40, y, line)
|
| 47 |
+
y -= line_height
|
| 48 |
+
|
| 49 |
+
c.save()
|
| 50 |
+
buffer.seek(0)
|
| 51 |
+
return buffer
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def export_chat_as_file(messages: List[dict], format: str) -> Tuple[BytesIO, str, str]:
|
| 55 |
+
"""
|
| 56 |
+
Export chat messages to the requested file format.
|
| 57 |
+
Returns: (file_stream, filename, media_type)
|
| 58 |
+
"""
|
| 59 |
+
text = format_messages_for_export(messages)
|
| 60 |
+
|
| 61 |
+
if format == "txt":
|
| 62 |
+
stream = generate_txt_file(text)
|
| 63 |
+
return stream, "chat_export.txt", "text/plain"
|
| 64 |
+
|
| 65 |
+
elif format == "docx":
|
| 66 |
+
stream = generate_docx_file(text)
|
| 67 |
+
return stream, "chat_export.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 68 |
+
|
| 69 |
+
elif format == "pdf":
|
| 70 |
+
stream = generate_pdf_file(text)
|
| 71 |
+
return stream, "chat_export.pdf", "application/pdf"
|
| 72 |
+
|
| 73 |
+
else:
|
| 74 |
+
raise ValueError(f"Unsupported export format: {format}")
|
multi_llm_chatbot_backend/requirements.txt
CHANGED
|
@@ -7,4 +7,6 @@ python-dotenv
|
|
| 7 |
chromadb
|
| 8 |
sentence-transformers
|
| 9 |
nltk
|
| 10 |
-
tiktoken
|
|
|
|
|
|
|
|
|
| 7 |
chromadb
|
| 8 |
sentence-transformers
|
| 9 |
nltk
|
| 10 |
+
tiktoken
|
| 11 |
+
reportlab
|
| 12 |
+
docx
|