Sohan Kshirsagar commited on
Commit
83f2c01
·
unverified ·
2 Parent(s): 5ff4a891f02f42

Merge pull request #62 from sohank-17/sohank-feature-new

Browse files
multi_llm_chatbot_backend/app/api/routes.py CHANGED
@@ -19,6 +19,9 @@ 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
 
@@ -452,31 +455,67 @@ async def upload_document(file: UploadFile = File(...), request: Request = None)
452
  logger.error(f"Error processing document upload: {str(e)}")
453
  raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
454
 
455
-
456
  @router.get("/export-chat")
457
  async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")):
458
  """
459
- Export the current chat context as a downloadable file in txt, pdf, or docx format.
460
  """
461
  try:
462
  session_id = get_or_create_session_for_request(request)
463
  session = session_manager.get_session(session_id)
464
 
465
  if not session.messages:
466
- return {"error": "No messages to export in current session."}
467
 
468
- # Generate the file using utility
469
- file_stream, filename, media_type = export_chat_as_file(session.messages, format)
470
-
471
- return StreamingResponse(
472
- file_stream,
473
- media_type=media_type,
474
- headers={"Content-Disposition": f"attachment; filename={filename}"}
475
- )
476
 
477
  except Exception as e:
478
  logger.error(f"Error exporting chat: {str(e)}")
479
  return {"error": "Failed to export chat.", "detail": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
 
481
 
482
  # Add new endpoint to get document statistics
 
19
  from fastapi import Query
20
  from app.utils.file_export import export_chat_as_file
21
 
22
+ from app.utils.chat_summary import generate_summary_from_messages, parse_summary_to_blocks
23
+ from app.utils.file_export import prepare_export_response, generate_pdf_file_from_blocks
24
+
25
  import hashlib
26
  import logging
27
 
 
455
  logger.error(f"Error processing document upload: {str(e)}")
456
  raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
457
 
458
+
459
  @router.get("/export-chat")
460
  async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")):
461
  """
462
+ Export the current chat context in the requested format.
463
  """
464
  try:
465
  session_id = get_or_create_session_for_request(request)
466
  session = session_manager.get_session(session_id)
467
 
468
  if not session.messages:
469
+ return {"error": "No messages in this session."}
470
 
471
+ return prepare_export_response(session.messages, format)
 
 
 
 
 
 
 
472
 
473
  except Exception as e:
474
  logger.error(f"Error exporting chat: {str(e)}")
475
  return {"error": "Failed to export chat.", "detail": str(e)}
476
+
477
+
478
+ @router.get("/chat-summary")
479
+ async def chat_summary(
480
+ request: Request,
481
+ format: str = Query("text", regex="^(txt|pdf|docx)$")
482
+ ):
483
+ """
484
+ Generate and return a summary of the current session chat.
485
+ Can return as plain txt, PDF, or DOCX.
486
+ """
487
+ try:
488
+ session_id = get_or_create_session_for_request(request)
489
+ session = session_manager.get_session(session_id)
490
+
491
+ if not session.messages:
492
+ return {"error": "No messages in this session."}
493
+
494
+ llm = next(iter(chat_orchestrator.personas.values())).llm
495
+ summary_text = await generate_summary_from_messages(session.messages, llm)
496
+
497
+ if format == "txt":
498
+ return prepare_export_response(summary_text, "txt", filename_prefix="chat_summary")
499
+
500
+ elif format == "docx":
501
+ return prepare_export_response(summary_text, "docx", filename_prefix="chat_summary")
502
+
503
+ elif format == "pdf":
504
+ # Parse and render using block formatting
505
+ blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text)
506
+
507
+ file_stream = generate_pdf_file_from_blocks(blocks)
508
+ return StreamingResponse(
509
+ file_stream,
510
+ media_type="application/pdf",
511
+ headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"}
512
+ )
513
+
514
+ except Exception as e:
515
+ logger.error(f"Error in chat-summary endpoint: {str(e)}")
516
+ return {"error": "Summary generation failed", "detail": str(e)}
517
+
518
+
519
 
520
 
521
  # Add new endpoint to get document statistics
multi_llm_chatbot_backend/app/utils/chat_summary.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from app.llm.llm_client import LLMClient
3
+ import logging
4
+ import re
5
+ from typing import List, Dict
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ async def generate_summary_from_messages(messages: List[dict], llm: LLMClient, max_tokens: int = 800) -> str:
10
+ """
11
+ Summarize the conversation using the given LLM client.
12
+ """
13
+ try:
14
+ full_text = "\n\n".join([f"{m['role']}:\n{m['content']}" for m in messages])
15
+
16
+ system_prompt = (
17
+ "You are an academic assistant. Summarize the following PhD chat conversation "
18
+ "into concise bullet points (max 10) or short paragraphs. Focus on insights, questions, and advice."
19
+ )
20
+
21
+ context = [{"role": "user", "content": f"Chat Log:\n{full_text}"}]
22
+
23
+ summary = await llm.generate(
24
+ system_prompt=system_prompt,
25
+ context=context,
26
+ temperature=0.4,
27
+ max_tokens=max_tokens
28
+ )
29
+
30
+ return summary.strip()
31
+
32
+ except Exception as e:
33
+ logger.error(f"Error generating summary: {str(e)}")
34
+ return "Summary generation failed. Please try again later."
35
+
36
+ def parse_summary_to_blocks(summary_text: str) -> List[Dict]:
37
+ #summary_text = re.sub(r'(?<!\n)([*•] )', r'\n\1', summary_text)
38
+ #summary_text = re.sub(r'(?<!\n)(\d+\.\s+)', r'\n\1', summary_text)
39
+ #summary_text = re.sub(r'(?<=[.!?])(?=\S)', ' ', summary_text)
40
+
41
+ lines = summary_text.strip().splitlines()
42
+ blocks = []
43
+ current_block = None
44
+
45
+ def flush_current_block():
46
+ if current_block:
47
+ blocks.append(current_block.copy())
48
+
49
+ for line in lines:
50
+ line = line.strip()
51
+ if not line:
52
+ continue
53
+
54
+ # Match section headings (e.g. **Title:**) as heading block
55
+ heading_match = re.match(r'^\*\*(.+?)\*\*:?$', line)
56
+ if heading_match:
57
+ flush_current_block()
58
+ current_block = {"type": "heading", "text": heading_match.group(1).strip()}
59
+ flush_current_block()
60
+ current_block = None
61
+ continue
62
+
63
+ # Match bullet list
64
+ if line.startswith("* "):
65
+ if current_block is None or current_block["type"] != "list" or current_block.get("style") != "bullet":
66
+ flush_current_block()
67
+ current_block = {"type": "list", "style": "bullet", "items": []}
68
+ current_block["items"].append(line[2:].strip())
69
+ continue
70
+
71
+ # Match numbered list
72
+ number_match = re.match(r'^\d+\.\s+(.*)', line)
73
+ if number_match:
74
+ if current_block is None or current_block["type"] != "list" or current_block.get("style") != "numbered":
75
+ flush_current_block()
76
+ current_block = {"type": "list", "style": "numbered", "items": []}
77
+ current_block["items"].append(number_match.group(1).strip())
78
+ continue
79
+
80
+ # Default: treat as paragraph
81
+ flush_current_block()
82
+ current_block = {"type": "paragraph", "text": line}
83
+ flush_current_block()
84
+ current_block = None
85
+
86
+ flush_current_block()
87
+
88
+ import pprint
89
+ print("[DEBUG] Summary Blocks:")
90
+ pprint.pprint(blocks)
91
+ return blocks
92
+
multi_llm_chatbot_backend/app/utils/file_export.py CHANGED
@@ -1,9 +1,14 @@
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
  """
@@ -33,42 +38,123 @@ def generate_docx_file(text: str) -> BytesIO:
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}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from io import BytesIO
2
+ from typing import List, Tuple, Union
3
  from docx import Document
4
  from reportlab.lib.pagesizes import letter
5
  from reportlab.pdfgen import canvas
6
+ from fastapi.responses import StreamingResponse
7
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, ListFlowable, ListItem
8
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
9
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT
10
+ from io import BytesIO
11
+ import re
12
 
13
  def format_messages_for_export(messages: List[dict]) -> str:
14
  """
 
38
 
39
  def generate_pdf_file(text: str) -> BytesIO:
40
  buffer = BytesIO()
41
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
42
 
43
+ styles = getSampleStyleSheet()
44
+ story = []
 
45
 
46
+ for block in text.split("\n\n"):
47
+ story.append(Paragraph(block.strip(), styles["Normal"]))
48
+ story.append(Spacer(1, 12))
 
 
 
49
 
50
+ doc.build(story)
51
  buffer.seek(0)
52
  return buffer
53
 
54
+ def export_chat_as_file(content: Union[str, List[dict]], format: str) -> Tuple[BytesIO, str, str]:
 
55
  """
56
+ Export either a list of chat messages or a summary string to the specified format.
 
57
  """
58
+ if isinstance(content, list):
59
+ text = format_messages_for_export(content)
60
+ elif isinstance(content, str):
61
+ text = content.strip()
62
+ else:
63
+ raise ValueError("Unsupported content type")
64
 
65
  if format == "txt":
66
+ return generate_txt_file(text), "chat_export.txt", "text/plain"
 
67
 
68
  elif format == "docx":
69
+ return generate_docx_file(text), "chat_export.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
 
70
 
71
  elif format == "pdf":
72
+ return generate_pdf_file(text), "chat_export.pdf", "application/pdf"
 
73
 
74
  else:
75
  raise ValueError(f"Unsupported export format: {format}")
76
+
77
+ def prepare_export_response(
78
+ content: Union[str, List[dict]],
79
+ format: str,
80
+ filename_prefix: str = "chat_export"
81
+ ) -> StreamingResponse:
82
+ """
83
+ Prepare a StreamingResponse for export, using the given filename prefix.
84
+ """
85
+ stream, filename, media_type = export_chat_as_file(content, format)
86
+
87
+ # Replace "chat_export" with custom prefix if needed
88
+ final_filename = filename.replace("chat_export", filename_prefix)
89
+
90
+ return StreamingResponse(
91
+ stream,
92
+ media_type=media_type,
93
+ headers={"Content-Disposition": f"attachment; filename={final_filename}"}
94
+ )
95
+
96
+
97
+ def _render_rich_text(text: str) -> str:
98
+ """
99
+ Convert markdown-style **bold** to reportlab <b>bold</b> tags.
100
+ """
101
+ return re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
102
+
103
+
104
+ def generate_pdf_file_from_blocks(blocks: List[dict]) -> BytesIO:
105
+ buffer = BytesIO()
106
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
107
+
108
+ styles = getSampleStyleSheet()
109
+
110
+ heading_style = ParagraphStyle(
111
+ name="Heading",
112
+ parent=styles["Heading1"],
113
+ alignment=TA_CENTER,
114
+ spaceAfter=20
115
+ )
116
+
117
+ paragraph_style = ParagraphStyle(
118
+ name="Paragraph",
119
+ parent=styles["Normal"],
120
+ alignment=TA_LEFT,
121
+ leading=14,
122
+ spaceAfter=8
123
+ )
124
+
125
+ list_style = ParagraphStyle(
126
+ name="List",
127
+ parent=styles["Normal"],
128
+ leftIndent=20,
129
+ leading=14
130
+ )
131
+
132
+ story = []
133
+
134
+ for block in blocks:
135
+ if block["type"] == "heading":
136
+ story.append(Paragraph(_render_rich_text(block["text"]), heading_style))
137
+ story.append(Spacer(1, 12))
138
+
139
+ elif block["type"] == "paragraph":
140
+ story.append(Paragraph(_render_rich_text(block["text"]), paragraph_style))
141
+
142
+ elif block["type"] == "list":
143
+ list_items = [
144
+ ListItem(Paragraph(_render_rich_text(item), list_style))
145
+ for item in block["items"]
146
+ ]
147
+ story.append(
148
+ ListFlowable(
149
+ list_items,
150
+ bulletType="1" if block.get("style") == "numbered" else "bullet",
151
+ start="1" if block.get("style") == "numbered" else None,
152
+ leftIndent=20
153
+ )
154
+ )
155
+ story.append(Spacer(1, 8))
156
+
157
+ doc.build(story)
158
+ buffer.seek(0)
159
+ return buffer
160
+