Cyril Dupland
Enhance PDF generation and update configurations: Add PDF_LOGO_PATH to .env.example, include markdown-pdf in requirements.txt, and refactor PDF generation logic to support logo integration. Introduce tests for PDF generation from Markdown input and update summarizer agent to streamline export functionality.
3cd0aad | """Summarizer agents: split into two nodes (LLM -> Export PDF/Upload).""" | |
| from io import BytesIO | |
| from typing import Callable, Union | |
| from datetime import datetime, timezone | |
| from langchain_core.messages import SystemMessage | |
| from langchain_core.language_models.chat_models import BaseChatModel | |
| from graphs.state import AgentState | |
| from graphs.prompts import SUMMARIZER_SYSTEM_PROMPT | |
| from langchain_core.documents import Document | |
| from langchain_core.messages import BaseMessage, SystemMessage, AIMessage | |
| def summarizer_llm_node( | |
| llm: BaseChatModel, | |
| ): | |
| """Node: ask the LLM to generate a Markdown summary from the conversation/context. | |
| Produces state keys: | |
| - summary_markdown | |
| - messages (appends the AIMessage containing the summary) | |
| """ | |
| def _run(state: AgentState) -> AgentState: | |
| messages = list(state.get("messages", [])) | |
| sys = SystemMessage(content=SUMMARIZER_SYSTEM_PROMPT) | |
| response = llm.invoke([sys] + messages) | |
| summary_markdown = response.content or "" | |
| return { | |
| "summary_markdown": summary_markdown, | |
| "messages": messages + [AIMessage(content=summary_markdown)], | |
| } | |
| return _run | |
| def summarizer_export_node( | |
| markdown_to_pdf: Callable[[str], Union[bytes, BytesIO]], | |
| upload_pdf: Callable[[bytes, str], str], | |
| ): | |
| """Node: takes `summary_markdown` from state, builds PDF and uploads it. | |
| Produces state keys: | |
| - summary_pdf_path (URL or storage path) | |
| - messages (appends an AIMessage with a link/attachment metadata) | |
| """ | |
| def _run(state: AgentState) -> AgentState: | |
| import os | |
| messages = list(state.get("messages", [])) | |
| summary_markdown = state.get("summary_markdown", "") or "" | |
| # Convert Markdown to PDF | |
| pdf_result = markdown_to_pdf(summary_markdown) | |
| # Ensure pdf_bytes is of type bytes (not BytesIO) | |
| if hasattr(pdf_result, "read"): | |
| pdf_bytes = pdf_result.read() | |
| else: | |
| pdf_bytes = pdf_result | |
| now = datetime.now(timezone.utc) | |
| # Build a project-relative path: summaries/YYYY/MM/summary-<timestamp>.pdf | |
| filename = f"summary-{now.strftime('%Y%m%d-%H%M%S')}.pdf" | |
| dir_path = os.path.join("tmp_summaries") | |
| if not os.path.exists(dir_path): | |
| os.makedirs(dir_path, exist_ok=True) | |
| object_path = os.path.join(dir_path, filename) | |
| # Always write locally first so the file exists even if upload fails | |
| with open(object_path, "wb") as f: | |
| f.write(pdf_bytes) | |
| # Try remote upload, but do not fail the node if upload fails | |
| uploaded_path = None | |
| try: | |
| uploaded_path = upload_pdf(object_path, filename) | |
| except Exception as e: | |
| # Swallow upload error but keep local file | |
| import logging | |
| logging.exception("Upload of summary PDF failed") | |
| uploaded_path = None | |
| metadata = { | |
| "file_name": filename, | |
| "type": "conversation_summary_pdf", | |
| "generated_at": now.isoformat(), | |
| "link": uploaded_path if uploaded_path else "", | |
| } | |
| return { | |
| "summary_pdf_path": uploaded_path if uploaded_path else object_path, | |
| "messages": messages + [AIMessage(content=f"Synthèse prête: {uploaded_path or object_path}", metadata={"document": metadata})], | |
| } | |
| return _run | |
| def summarizer_node( | |
| llm: BaseChatModel, | |
| markdown_to_pdf: Callable[[str], Union[bytes, BytesIO]], | |
| upload_pdf: Callable[[bytes, str], str], | |
| ): | |
| """Compatibility wrapper: runs LLM summarization then export in one node.""" | |
| llm_runner = summarizer_llm_node(llm) | |
| export_runner = summarizer_export_node(markdown_to_pdf, upload_pdf) | |
| def _run(state: AgentState) -> AgentState: | |
| state_after_llm = llm_runner(state) | |
| state_after_export = export_runner(state_after_llm) | |
| return state_after_export | |
| return _run | |