Girish Jeswani commited on
Commit
311d8ab
·
2 Parent(s): de20511acd15c1

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