commcoach: rewrite PDF export - direct markdown-to-pdf via reportlab, remove broken RendererPdf dependency
Made-with: Cursor
This commit is contained in:
parent
210c9d44a1
commit
cdb974d658
1 changed files with 65 additions and 113 deletions
|
|
@ -140,132 +140,84 @@ def buildSessionMarkdown(session: Dict[str, Any], messages: List[Dict[str, Any]]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
class _DebugUtils:
|
|
||||||
"""Minimal stub for services.utils required by RendererPdf."""
|
|
||||||
@staticmethod
|
|
||||||
def debugLogToFile(message: str, context: str = "DEBUG"):
|
|
||||||
logger.debug(f"[{context}] {message}")
|
|
||||||
|
|
||||||
|
|
||||||
class _ServicesStub:
|
|
||||||
"""Minimal services stub providing utils for RendererPdf."""
|
|
||||||
def __init__(self):
|
|
||||||
self.utils = _DebugUtils()
|
|
||||||
|
|
||||||
|
|
||||||
async def renderDossierPdf(context: Dict[str, Any], sessions: List[Dict[str, Any]],
|
async def renderDossierPdf(context: Dict[str, Any], sessions: List[Dict[str, Any]],
|
||||||
tasks: List[Dict[str, Any]], scores: List[Dict[str, Any]],
|
tasks: List[Dict[str, Any]], scores: List[Dict[str, Any]],
|
||||||
aiService=None) -> Optional[bytes]:
|
aiService=None) -> Optional[bytes]:
|
||||||
"""Render a dossier as PDF using the existing RendererPdf."""
|
"""Render a dossier as PDF directly from markdown content via reportlab."""
|
||||||
try:
|
md = buildDossierMarkdown(context, sessions, tasks, scores)
|
||||||
from modules.services.serviceGeneration.renderers.rendererPdf import RendererPdf
|
title = context.get("title", "Coaching Dossier")
|
||||||
extractedContent = _buildPdfContent(context, sessions, tasks, scores, isDossier=True)
|
return _markdownToPdf(md, title)
|
||||||
renderer = RendererPdf(services=_ServicesStub())
|
|
||||||
docs = await renderer.render(extractedContent=extractedContent, title=context.get("title", "Dossier"), aiService=aiService)
|
|
||||||
if docs and len(docs) > 0:
|
|
||||||
return docs[0].documentData
|
|
||||||
except ImportError:
|
|
||||||
logger.warning("RendererPdf not available, falling back to markdown-based PDF")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"PDF rendering failed: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def renderSessionPdf(session: Dict[str, Any], messages: List[Dict[str, Any]],
|
async def renderSessionPdf(session: Dict[str, Any], messages: List[Dict[str, Any]],
|
||||||
tasks: List[Dict[str, Any]], scores: List[Dict[str, Any]],
|
tasks: List[Dict[str, Any]], scores: List[Dict[str, Any]],
|
||||||
aiService=None) -> Optional[bytes]:
|
aiService=None) -> Optional[bytes]:
|
||||||
"""Render a session as PDF."""
|
"""Render a session as PDF directly from markdown content via reportlab."""
|
||||||
|
md = buildSessionMarkdown(session, messages, tasks, scores)
|
||||||
|
title = f"Coaching Session — {_formatDate(session.get('startedAt'))}"
|
||||||
|
return _markdownToPdf(md, title)
|
||||||
|
|
||||||
|
|
||||||
|
def _markdownToPdf(markdownText: str, title: str) -> Optional[bytes]:
|
||||||
|
"""Convert markdown text to a styled PDF using reportlab."""
|
||||||
try:
|
try:
|
||||||
from modules.services.serviceGeneration.renderers.rendererPdf import RendererPdf
|
from reportlab.lib.pagesizes import A4
|
||||||
title = f"Session {_formatDate(session.get('startedAt'))}"
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||||
extractedContent = _buildPdfContent({"title": title}, [session], tasks, scores, isDossier=False, messages=messages)
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
renderer = RendererPdf(services=_ServicesStub())
|
from reportlab.lib.enums import TA_LEFT
|
||||||
docs = await renderer.render(extractedContent=extractedContent, title=title, aiService=aiService)
|
from reportlab.lib import colors
|
||||||
if docs and len(docs) > 0:
|
|
||||||
return docs[0].documentData
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("RendererPdf not available")
|
logger.warning("reportlab not available for PDF export")
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Session PDF rendering failed: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import io
|
||||||
|
buf = io.BytesIO()
|
||||||
|
doc = SimpleDocTemplate(buf, pagesize=A4, rightMargin=60, leftMargin=60, topMargin=50, bottomMargin=40)
|
||||||
|
|
||||||
def _buildPdfContent(context, sessions, tasks, scores, isDossier=True, messages=None) -> Dict[str, Any]:
|
baseStyles = getSampleStyleSheet()
|
||||||
"""Convert dossier/session data into the extractedContent format expected by RendererPdf."""
|
sTitle = ParagraphStyle("DocTitle", parent=baseStyles["Title"], fontSize=18, spaceAfter=20, textColor=colors.HexColor("#1e40af"))
|
||||||
title = context.get("title", "Export")
|
sH2 = ParagraphStyle("DocH2", parent=baseStyles["Heading2"], fontSize=13, spaceBefore=16, spaceAfter=6, textColor=colors.HexColor("#1f2937"))
|
||||||
sections = []
|
sH3 = ParagraphStyle("DocH3", parent=baseStyles["Heading3"], fontSize=11, spaceBefore=12, spaceAfter=4, textColor=colors.HexColor("#374151"))
|
||||||
|
sBody = ParagraphStyle("DocBody", parent=baseStyles["Normal"], fontSize=10, leading=14, spaceAfter=4, alignment=TA_LEFT)
|
||||||
|
sBullet = ParagraphStyle("DocBullet", parent=sBody, leftIndent=18, bulletIndent=6, spaceBefore=2, spaceAfter=2)
|
||||||
|
sMeta = ParagraphStyle("DocMeta", parent=sBody, fontSize=8, textColor=colors.grey)
|
||||||
|
|
||||||
sections.append({
|
story = [Paragraph(title, sTitle), Spacer(1, 10)]
|
||||||
"id": "header",
|
|
||||||
"content_type": "heading",
|
|
||||||
"elements": [{"text": title, "level": 1}],
|
|
||||||
})
|
|
||||||
|
|
||||||
if isDossier and context.get("description"):
|
for line in markdownText.split("\n"):
|
||||||
sections.append({
|
stripped = line.strip()
|
||||||
"id": "desc",
|
if not stripped:
|
||||||
"content_type": "paragraph",
|
story.append(Spacer(1, 4))
|
||||||
"elements": [{"text": context.get("description")}],
|
elif stripped.startswith("# "):
|
||||||
})
|
continue
|
||||||
|
elif stripped.startswith("### "):
|
||||||
|
story.append(Paragraph(stripped[4:], sH3))
|
||||||
|
elif stripped.startswith("## "):
|
||||||
|
story.append(Paragraph(stripped[3:], sH2))
|
||||||
|
elif stripped.startswith("---"):
|
||||||
|
story.append(Spacer(1, 8))
|
||||||
|
elif stripped.startswith("*") and stripped.endswith("*"):
|
||||||
|
story.append(Paragraph(f"<i>{stripped.strip('*')}</i>", sMeta))
|
||||||
|
elif stripped.startswith("- [x] "):
|
||||||
|
story.append(Paragraph(f"\u2713 {stripped[6:]}", sBullet))
|
||||||
|
elif stripped.startswith("- [ ] "):
|
||||||
|
story.append(Paragraph(f"\u25CB {stripped[6:]}", sBullet))
|
||||||
|
elif stripped.startswith("- "):
|
||||||
|
story.append(Paragraph(f"\u2022 {stripped[2:]}", sBullet))
|
||||||
|
else:
|
||||||
|
text = stripped.replace("**", "<b>").replace("__", "<b>")
|
||||||
|
text = text.replace("*", "<i>").replace("_", "<i>")
|
||||||
|
story.append(Paragraph(text, sBody))
|
||||||
|
|
||||||
completedSessions = [s for s in sessions if s.get("status") == "completed"] if isDossier else sessions
|
doc.build(story)
|
||||||
if completedSessions:
|
buf.seek(0)
|
||||||
sessionRows = []
|
return buf.getvalue()
|
||||||
for s in completedSessions:
|
except Exception as e:
|
||||||
sessionRows.append({
|
logger.warning(f"PDF generation failed: {e}")
|
||||||
"cells": [
|
return None
|
||||||
_formatDate(s.get("startedAt") or s.get("createdAt")),
|
|
||||||
str(s.get("competenceScore") or "--"),
|
|
||||||
s.get("summary", "")[:200] if s.get("summary") else "",
|
|
||||||
]
|
|
||||||
})
|
|
||||||
sections.append({
|
|
||||||
"id": "sessions",
|
|
||||||
"content_type": "heading",
|
|
||||||
"elements": [{"text": "Sessions", "level": 2}],
|
|
||||||
})
|
|
||||||
sections.append({
|
|
||||||
"id": "sessions_table",
|
|
||||||
"content_type": "table",
|
|
||||||
"elements": [{
|
|
||||||
"headers": ["Datum", "Score", "Zusammenfassung"],
|
|
||||||
"rows": sessionRows,
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
|
|
||||||
if messages:
|
|
||||||
chatElements = []
|
|
||||||
for msg in messages:
|
|
||||||
role = "Du" if msg.get("role") == "user" else "Coach"
|
|
||||||
chatElements.append({"text": f"{role}: {msg.get('content', '')}"})
|
|
||||||
sections.append({
|
|
||||||
"id": "chat",
|
|
||||||
"content_type": "heading",
|
|
||||||
"elements": [{"text": "Gesprächsverlauf", "level": 2}],
|
|
||||||
})
|
|
||||||
sections.append({
|
|
||||||
"id": "chat_content",
|
|
||||||
"content_type": "paragraph",
|
|
||||||
"elements": chatElements,
|
|
||||||
})
|
|
||||||
|
|
||||||
if tasks:
|
|
||||||
taskItems = [{"text": f"{'[x]' if t.get('status') == 'done' else '[ ]'} {t.get('title')}"} for t in tasks]
|
|
||||||
sections.append({
|
|
||||||
"id": "tasks",
|
|
||||||
"content_type": "heading",
|
|
||||||
"elements": [{"text": "Aufgaben", "level": 2}],
|
|
||||||
})
|
|
||||||
sections.append({
|
|
||||||
"id": "task_list",
|
|
||||||
"content_type": "bullet_list",
|
|
||||||
"elements": taskItems,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"metadata": {"title": title},
|
|
||||||
"documents": [{"id": "main", "title": title, "sections": sections}],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _formatDate(isoStr: Optional[str]) -> str:
|
def _formatDate(isoStr: Optional[str]) -> str:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue