Sohan Kshirsagar commited on
Commit
1f02f42
·
1 Parent(s): 5e464c3

chat summary generator and pdf formatter

Browse files
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
+