Ajout des fonctionnalités d'export Word et PDF pour le chat RAG
- Ajout de python-docx et reportlab aux dépendances - Création du module utils/word_exporter.py pour l'export Word - Création du module utils/pdf_exporter.py pour l'export PDF - Ajout des routes /chat/export-word et /chat/export-pdf dans flask_app.py - Ajout des boutons d'export (Word et PDF) dans chat.html - Les boutons apparaissent après chaque réponse de l'assistant - Support des questions reformulées avec question originale 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
204
generations/library_rag/utils/pdf_exporter.py
Normal file
204
generations/library_rag/utils/pdf_exporter.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Generate PDF documents from chat exchanges.
|
||||
|
||||
This module provides functionality to export chat conversations between users
|
||||
and the RAG assistant into formatted PDF documents.
|
||||
|
||||
Example:
|
||||
Export a simple chat exchange::
|
||||
|
||||
from pathlib import Path
|
||||
from utils.pdf_exporter import create_chat_export_pdf
|
||||
|
||||
filepath = create_chat_export_pdf(
|
||||
user_question="What is phenomenology?",
|
||||
assistant_response="Phenomenology is a philosophical movement...",
|
||||
output_dir=Path("output")
|
||||
)
|
||||
|
||||
Export with reformulated question::
|
||||
|
||||
filepath = create_chat_export_pdf(
|
||||
user_question="What does Husserl mean by phenomenology?",
|
||||
assistant_response="Husserl defines phenomenology as...",
|
||||
is_reformulated=True,
|
||||
original_question="What is phenomenology?",
|
||||
output_dir=Path("output")
|
||||
)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"reportlab is required for PDF export. "
|
||||
"Install with: pip install reportlab"
|
||||
)
|
||||
|
||||
|
||||
def create_chat_export_pdf(
|
||||
user_question: str,
|
||||
assistant_response: str,
|
||||
is_reformulated: bool = False,
|
||||
original_question: Optional[str] = None,
|
||||
output_dir: Path = Path("output"),
|
||||
) -> Path:
|
||||
"""Create a PDF document from a chat exchange.
|
||||
|
||||
Args:
|
||||
user_question: The user's question (or reformulated version).
|
||||
assistant_response: The assistant's complete response text.
|
||||
is_reformulated: Whether the question was reformulated by the system.
|
||||
If True, both original and reformulated questions are included.
|
||||
original_question: The original user question before reformulation.
|
||||
Only used when is_reformulated is True.
|
||||
output_dir: Directory where the PDF file will be saved.
|
||||
Created if it doesn't exist.
|
||||
|
||||
Returns:
|
||||
Path to the generated PDF file.
|
||||
|
||||
Raises:
|
||||
OSError: If the output directory cannot be created or file cannot be saved.
|
||||
|
||||
Example:
|
||||
>>> from pathlib import Path
|
||||
>>> filepath = create_chat_export_pdf(
|
||||
... user_question="Qu'est-ce que la phénoménologie ?",
|
||||
... assistant_response="La phénoménologie est...",
|
||||
... output_dir=Path("output/test_exports")
|
||||
... )
|
||||
>>> print(filepath)
|
||||
output/test_exports/chat_export_20250101_123045.pdf
|
||||
"""
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"chat_export_{timestamp}.pdf"
|
||||
filepath = output_dir / filename
|
||||
|
||||
# Create directory if needed
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create PDF document
|
||||
doc = SimpleDocTemplate(
|
||||
str(filepath),
|
||||
pagesize=A4,
|
||||
rightMargin=2 * cm,
|
||||
leftMargin=2 * cm,
|
||||
topMargin=2.5 * cm,
|
||||
bottomMargin=2.5 * cm,
|
||||
)
|
||||
|
||||
# Get default styles
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Custom styles
|
||||
title_style = ParagraphStyle(
|
||||
"CustomTitle",
|
||||
parent=styles["Heading1"],
|
||||
fontSize=18,
|
||||
textColor=HexColor("#2B2B2B"),
|
||||
spaceAfter=6,
|
||||
alignment=TA_CENTER,
|
||||
fontName="Helvetica-Bold",
|
||||
)
|
||||
|
||||
date_style = ParagraphStyle(
|
||||
"DateStyle",
|
||||
parent=styles["Normal"],
|
||||
fontSize=9,
|
||||
textColor=HexColor("#808080"),
|
||||
alignment=TA_CENTER,
|
||||
spaceAfter=20,
|
||||
)
|
||||
|
||||
heading_style = ParagraphStyle(
|
||||
"CustomHeading",
|
||||
parent=styles["Heading2"],
|
||||
fontSize=12,
|
||||
textColor=HexColor("#7D6E58"),
|
||||
spaceAfter=10,
|
||||
spaceBefore=15,
|
||||
fontName="Helvetica-Bold",
|
||||
)
|
||||
|
||||
question_style = ParagraphStyle(
|
||||
"QuestionStyle",
|
||||
parent=styles["Normal"],
|
||||
fontSize=11,
|
||||
textColor=HexColor("#2B2B2B"),
|
||||
spaceAfter=10,
|
||||
leftIndent=1 * cm,
|
||||
rightIndent=1 * cm,
|
||||
backColor=HexColor("#F8F4EE"),
|
||||
borderPadding=10,
|
||||
)
|
||||
|
||||
body_style = ParagraphStyle(
|
||||
"BodyStyle",
|
||||
parent=styles["Normal"],
|
||||
fontSize=11,
|
||||
textColor=HexColor("#2B2B2B"),
|
||||
spaceAfter=10,
|
||||
alignment=TA_JUSTIFY,
|
||||
leading=16,
|
||||
)
|
||||
|
||||
footer_style = ParagraphStyle(
|
||||
"FooterStyle",
|
||||
parent=styles["Normal"],
|
||||
fontSize=8,
|
||||
textColor=HexColor("#969696"),
|
||||
alignment=TA_CENTER,
|
||||
fontName="Helvetica-Oblique",
|
||||
)
|
||||
|
||||
# Build document content
|
||||
story = []
|
||||
|
||||
# Title
|
||||
story.append(Paragraph("Conversation RAG - Export", title_style))
|
||||
|
||||
# Date
|
||||
date_text = f"Exporté le {datetime.now().strftime('%d/%m/%Y à %H:%M')}"
|
||||
story.append(Paragraph(date_text, date_style))
|
||||
|
||||
# Original question (if reformulated)
|
||||
if is_reformulated and original_question:
|
||||
story.append(Paragraph("Question Originale", heading_style))
|
||||
story.append(Paragraph(original_question, question_style))
|
||||
story.append(Spacer(1, 0.5 * cm))
|
||||
|
||||
# User question
|
||||
question_label = "Question Reformulée" if is_reformulated else "Question"
|
||||
story.append(Paragraph(question_label, heading_style))
|
||||
story.append(Paragraph(user_question, question_style))
|
||||
story.append(Spacer(1, 0.5 * cm))
|
||||
|
||||
# Assistant response
|
||||
story.append(Paragraph("Réponse de l'Assistant", heading_style))
|
||||
|
||||
# Split response into paragraphs
|
||||
response_paragraphs = assistant_response.split("\n\n")
|
||||
for para in response_paragraphs:
|
||||
if para.strip():
|
||||
story.append(Paragraph(para.strip(), body_style))
|
||||
|
||||
# Footer
|
||||
story.append(Spacer(1, 1 * cm))
|
||||
story.append(
|
||||
Paragraph("Généré par Library RAG - Recherche Philosophique", footer_style)
|
||||
)
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
|
||||
return filepath
|
||||
144
generations/library_rag/utils/word_exporter.py
Normal file
144
generations/library_rag/utils/word_exporter.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Generate Word documents from chat exchanges.
|
||||
|
||||
This module provides functionality to export chat conversations between users
|
||||
and the RAG assistant into formatted Microsoft Word documents (.docx).
|
||||
|
||||
Example:
|
||||
Export a simple chat exchange::
|
||||
|
||||
from pathlib import Path
|
||||
from utils.word_exporter import create_chat_export
|
||||
|
||||
filepath = create_chat_export(
|
||||
user_question="What is phenomenology?",
|
||||
assistant_response="Phenomenology is a philosophical movement...",
|
||||
output_dir=Path("output")
|
||||
)
|
||||
|
||||
Export with reformulated question::
|
||||
|
||||
filepath = create_chat_export(
|
||||
user_question="What does Husserl mean by phenomenology?",
|
||||
assistant_response="Husserl defines phenomenology as...",
|
||||
is_reformulated=True,
|
||||
original_question="What is phenomenology?",
|
||||
output_dir=Path("output")
|
||||
)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"python-docx is required for Word export. "
|
||||
"Install with: pip install python-docx"
|
||||
)
|
||||
|
||||
|
||||
def create_chat_export(
|
||||
user_question: str,
|
||||
assistant_response: str,
|
||||
is_reformulated: bool = False,
|
||||
original_question: Optional[str] = None,
|
||||
output_dir: Path = Path("output"),
|
||||
) -> Path:
|
||||
"""Create a Word document from a chat exchange.
|
||||
|
||||
Args:
|
||||
user_question: The user's question (or reformulated version).
|
||||
assistant_response: The assistant's complete response text.
|
||||
is_reformulated: Whether the question was reformulated by the system.
|
||||
If True, both original and reformulated questions are included.
|
||||
original_question: The original user question before reformulation.
|
||||
Only used when is_reformulated is True.
|
||||
output_dir: Directory where the .docx file will be saved.
|
||||
Created if it doesn't exist.
|
||||
|
||||
Returns:
|
||||
Path to the generated .docx file.
|
||||
|
||||
Raises:
|
||||
OSError: If the output directory cannot be created or file cannot be saved.
|
||||
|
||||
Example:
|
||||
>>> from pathlib import Path
|
||||
>>> filepath = create_chat_export(
|
||||
... user_question="Qu'est-ce que la phénoménologie ?",
|
||||
... assistant_response="La phénoménologie est...",
|
||||
... output_dir=Path("output")
|
||||
... )
|
||||
>>> print(filepath)
|
||||
output/chat_export_20250101_123045.docx
|
||||
"""
|
||||
# Create document
|
||||
doc = Document()
|
||||
|
||||
# Set margins to 1 inch on all sides
|
||||
sections = doc.sections
|
||||
for section in sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
|
||||
# Title
|
||||
title = doc.add_heading("Conversation RAG - Export", level=1)
|
||||
title.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
|
||||
# Date
|
||||
date_p = doc.add_paragraph()
|
||||
date_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
date_run = date_p.add_run(
|
||||
f"Exporté le {datetime.now().strftime('%d/%m/%Y à %H:%M')}"
|
||||
)
|
||||
date_run.font.size = Pt(10)
|
||||
date_run.font.color.rgb = RGBColor(128, 128, 128)
|
||||
|
||||
doc.add_paragraph() # Spacer
|
||||
|
||||
# Original question (if reformulated)
|
||||
if is_reformulated and original_question:
|
||||
doc.add_heading("Question Originale", level=2)
|
||||
original_p = doc.add_paragraph(original_question)
|
||||
original_p.style = "Intense Quote"
|
||||
doc.add_paragraph()
|
||||
|
||||
# User question
|
||||
question_label = "Question Reformulée" if is_reformulated else "Question"
|
||||
doc.add_heading(question_label, level=2)
|
||||
question_p = doc.add_paragraph(user_question)
|
||||
question_p.style = "Intense Quote"
|
||||
|
||||
doc.add_paragraph() # Spacer
|
||||
|
||||
# Assistant response
|
||||
doc.add_heading("Réponse de l'Assistant", level=2)
|
||||
response_p = doc.add_paragraph(assistant_response)
|
||||
|
||||
# Footer
|
||||
doc.add_paragraph()
|
||||
footer_p = doc.add_paragraph()
|
||||
footer_p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
||||
footer_run = footer_p.add_run(
|
||||
"Généré par Library RAG - Recherche Philosophique"
|
||||
)
|
||||
footer_run.font.size = Pt(9)
|
||||
footer_run.font.color.rgb = RGBColor(150, 150, 150)
|
||||
footer_run.italic = True
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"chat_export_{timestamp}.docx"
|
||||
filepath = output_dir / filename
|
||||
|
||||
# Save document (create directory if needed)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(str(filepath))
|
||||
|
||||
return filepath
|
||||
Reference in New Issue
Block a user