🔍 Code Extractor

class ConversationTimelineGenerator

Maturity: 45

A class that generates comprehensive PDF reports documenting conversation timelines, including detailed exchanges, problem-solving analysis, references, and visual summaries.

File:
/tf/active/vicechatdev/e-ink-llm/conversation_timeline.py
Lines:
25 - 422
Complexity:
complex

Purpose

ConversationTimelineGenerator creates professional PDF documentation of conversations using the ReportLab library. It produces multi-page timeline reports with title pages, executive summaries, detailed exchange histories, problem-solving analysis, and reference mappings. The class supports both comprehensive multi-page reports and quick one-page summaries, making it suitable for conversation analysis, documentation, and archival purposes.

Source Code

class ConversationTimelineGenerator:
    """Generate comprehensive conversation timeline PDFs"""
    
    def __init__(self):
        """Initialize timeline generator"""
        self.logger = logging.getLogger(__name__)
        
        # PDF styling
        self.styles = getSampleStyleSheet()
        self.setup_custom_styles()
        
        # Colors for different elements
        self.colors = {
            'header': colors.HexColor('#2E86AB'),
            'exchange': colors.HexColor('#A23B72'),
            'topic': colors.HexColor('#F18F01'),
            'reference': colors.HexColor('#C73E1D'),
            'insight': colors.HexColor('#6A994E'),
            'light_gray': colors.HexColor('#F5F5F5'),
            'medium_gray': colors.HexColor('#E0E0E0')
        }
    
    def setup_custom_styles(self):
        """Set up custom paragraph styles"""
        self.styles.add(ParagraphStyle(
            name='ConversationTitle',
            parent=self.styles['Title'],
            fontSize=18,
            spaceAfter=20,
            textColor=self.colors['header'] if hasattr(self, 'colors') else colors.blue,
            alignment=TA_CENTER
        ))
        
        self.styles.add(ParagraphStyle(
            name='ExchangeHeader',
            parent=self.styles['Heading2'],
            fontSize=14,
            spaceBefore=15,
            spaceAfter=10,
            textColor=self.colors['exchange'] if hasattr(self, 'colors') else colors.darkred,
            leftIndent=20
        ))
        
        self.styles.add(ParagraphStyle(
            name='ExchangeContent',
            parent=self.styles['Normal'],
            fontSize=11,
            spaceBefore=5,
            spaceAfter=5,
            leftIndent=30,
            rightIndent=20,
            alignment=TA_JUSTIFY
        ))
        
        self.styles.add(ParagraphStyle(
            name='TopicStyle',
            parent=self.styles['Normal'],
            fontSize=10,
            textColor=self.colors['topic'] if hasattr(self, 'colors') else colors.orange,
            leftIndent=30
        ))
        
        self.styles.add(ParagraphStyle(
            name='ReferenceStyle',
            parent=self.styles['Normal'],
            fontSize=10,
            textColor=self.colors['reference'] if hasattr(self, 'colors') else colors.red,
            leftIndent=30,
            fontName='Helvetica-Oblique'
        ))
    
    async def generate_conversation_timeline(self, 
                                           context: ConversationContext,
                                           output_path: str) -> bool:
        """
        Generate comprehensive conversation timeline PDF
        
        Args:
            context: ConversationContext with full conversation data
            output_path: Path for output PDF
            
        Returns:
            True if successful, False otherwise
        """
        self.logger.info(f"Generating conversation timeline for {context.conversation_id}")
        
        try:
            # Create PDF document
            doc = SimpleDocTemplate(
                output_path,
                pagesize=A4,
                rightMargin=72,
                leftMargin=72,
                topMargin=72,
                bottomMargin=72
            )
            
            # Build story elements
            story = []
            
            # Title page
            story.extend(self._build_title_page(context))
            story.append(PageBreak())
            
            # Executive summary
            story.extend(self._build_executive_summary(context))
            story.append(PageBreak())
            
            # Timeline visualization
            story.extend(self._build_timeline_section(context))
            story.append(PageBreak())
            
            # Detailed exchanges
            story.extend(self._build_detailed_exchanges(context))
            
            # Problem-solving analysis
            if context.problem_solving_chain:
                story.append(PageBreak())
                story.extend(self._build_problem_solving_analysis(context))
            
            # References and connections
            if context.reference_map:
                story.append(PageBreak())
                story.extend(self._build_references_section(context))
            
            # Build PDF
            doc.build(story)
            
            self.logger.info(f"Timeline PDF generated: {output_path}")
            return True
            
        except Exception as e:
            self.logger.error(f"Error generating timeline PDF: {e}")
            return False
    
    def _build_title_page(self, context: ConversationContext) -> List[Any]:
        """Build title page elements"""
        elements = []
        
        # Title
        title = f"Conversation Timeline"
        elements.append(Paragraph(title, self.styles['ConversationTitle']))
        elements.append(Spacer(1, 20))
        
        # Conversation details
        details = [
            f"<b>Conversation ID:</b> {context.conversation_id}",
            f"<b>Total Exchanges:</b> {context.total_exchanges}",
            f"<b>Generated:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            "",
            f"<b>Summary:</b> {context.conversation_summary}"
        ]
        
        for detail in details:
            elements.append(Paragraph(detail, self.styles['Normal']))
            elements.append(Spacer(1, 6))
        
        elements.append(Spacer(1, 30))
        
        # Active topics
        if context.active_topics:
            elements.append(Paragraph("<b>Active Topics:</b>", self.styles['Heading3']))
            for topic in context.active_topics:
                elements.append(Paragraph(f"• {topic.replace('_', ' ').title()}", self.styles['TopicStyle']))
            elements.append(Spacer(1, 20))
        
        # Key insights
        if context.key_insights:
            elements.append(Paragraph("<b>Key Insights:</b>", self.styles['Heading3']))
            for insight in context.key_insights:
                elements.append(Paragraph(f"• {insight}", self.styles['Normal']))
            elements.append(Spacer(1, 10))
        
        return elements
    
    def _build_executive_summary(self, context: ConversationContext) -> List[Any]:
        """Build executive summary section"""
        elements = []
        
        elements.append(Paragraph("Executive Summary", self.styles['Heading1']))
        elements.append(Spacer(1, 12))
        
        # Conversation overview
        overview = f"""
        This conversation timeline documents {context.total_exchanges} exchanges in conversation {context.conversation_id}.
        The conversation covers {len(context.active_topics)} main topic areas and demonstrates a clear progression
        through {len(context.problem_solving_chain)} problem-solving steps.
        """
        elements.append(Paragraph(overview, self.styles['Normal']))
        elements.append(Spacer(1, 15))
        
        # Statistics table
        stats_data = [
            ['Metric', 'Value'],
            ['Total Exchanges', str(context.total_exchanges)],
            ['Active Topics', str(len(context.active_topics))],
            ['Key Insights', str(len(context.key_insights))],
            ['Problem-Solving Steps', str(len(context.problem_solving_chain))],
            ['Cross-References', str(sum(len(refs) for refs in context.reference_map.values()))]
        ]
        
        stats_table = Table(stats_data, colWidths=[2*inch, 1*inch])
        stats_table.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), self.colors['header']),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('FONTSIZE', (0, 0), (-1, 0), 11),
            ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
            ('BACKGROUND', (0, 1), (-1, -1), self.colors['light_gray']),
            ('GRID', (0, 0), (-1, -1), 1, colors.black)
        ]))
        
        elements.append(stats_table)
        elements.append(Spacer(1, 20))
        
        # Problem-solving progression
        if context.problem_solving_chain:
            elements.append(Paragraph("Problem-Solving Progression", self.styles['Heading3']))
            for i, step in enumerate(context.problem_solving_chain, 1):
                step_text = f"{i}. <b>{step['step_type'].title()}</b> (Exchange {step['exchange_number']}): {step['description']}"
                elements.append(Paragraph(step_text, self.styles['Normal']))
            elements.append(Spacer(1, 15))
        
        return elements
    
    def _build_timeline_section(self, context: ConversationContext) -> List[Any]:
        """Build visual timeline section"""
        elements = []
        
        elements.append(Paragraph("Conversation Timeline", self.styles['Heading1']))
        elements.append(Spacer(1, 12))
        
        # Timeline table
        timeline_data = [['Exchange', 'Timestamp', 'Input', 'Key Topics', 'Processing Time']]
        
        for turn in context.conversation_turns:
            timeline_data.append([
                str(turn.exchange_number),
                turn.timestamp.strftime('%H:%M:%S'),
                turn.input_summary[:30] + "..." if len(turn.input_summary) > 30 else turn.input_summary,
                ", ".join(turn.topics[:2]),
                f"{turn.processing_time:.1f}s"
            ])
        
        timeline_table = Table(timeline_data, colWidths=[0.8*inch, 1*inch, 2.5*inch, 1.5*inch, 1*inch])
        timeline_table.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), self.colors['header']),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('FONTSIZE', (0, 0), (-1, 0), 10),
            ('FONTSIZE', (0, 1), (-1, -1), 9),
            ('BOTTOMPADDING', (0, 0), (-1, 0), 8),
            ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, self.colors['light_gray']]),
            ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
            ('VALIGN', (0, 0), (-1, -1), 'TOP')
        ]))
        
        elements.append(timeline_table)
        elements.append(Spacer(1, 20))
        
        return elements
    
    def _build_detailed_exchanges(self, context: ConversationContext) -> List[Any]:
        """Build detailed exchange documentation"""
        elements = []
        
        elements.append(Paragraph("Detailed Exchange History", self.styles['Heading1']))
        elements.append(Spacer(1, 12))
        
        for turn in context.conversation_turns:
            # Exchange header
            header_text = f"Exchange {turn.exchange_number} - {turn.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
            elements.append(Paragraph(header_text, self.styles['ExchangeHeader']))
            
            # Input summary
            elements.append(Paragraph(f"<b>Input:</b> {turn.input_summary}", self.styles['ExchangeContent']))
            
            # Response summary
            elements.append(Paragraph(f"<b>Response:</b> {turn.response_summary}", self.styles['ExchangeContent']))
            
            # Topics
            if turn.topics:
                topics_text = f"<b>Topics:</b> {', '.join(turn.topics)}"
                elements.append(Paragraph(topics_text, self.styles['TopicStyle']))
            
            # Key points
            if turn.key_points:
                elements.append(Paragraph("<b>Key Points:</b>", self.styles['ExchangeContent']))
                for point in turn.key_points:
                    elements.append(Paragraph(f"• {point}", self.styles['ExchangeContent']))
            
            # Processing stats
            stats_text = f"<b>Processing:</b> {turn.processing_time:.1f}s, {turn.tokens_used} tokens"
            elements.append(Paragraph(stats_text, self.styles['Normal']))
            
            elements.append(Spacer(1, 15))
        
        return elements
    
    def _build_problem_solving_analysis(self, context: ConversationContext) -> List[Any]:
        """Build problem-solving analysis section"""
        elements = []
        
        elements.append(Paragraph("Problem-Solving Analysis", self.styles['Heading1']))
        elements.append(Spacer(1, 12))
        
        # Problem-solving flow
        elements.append(Paragraph("The conversation demonstrates the following problem-solving progression:", self.styles['Normal']))
        elements.append(Spacer(1, 10))
        
        for i, step in enumerate(context.problem_solving_chain, 1):
            step_header = f"Step {i}: {step['step_type'].title()} (Exchange {step['exchange_number']})"
            elements.append(Paragraph(step_header, self.styles['ExchangeHeader']))
            
            elements.append(Paragraph(step['description'], self.styles['ExchangeContent']))
            
            if step['topics']:
                topics_text = f"Related topics: {', '.join(step['topics'])}"
                elements.append(Paragraph(topics_text, self.styles['TopicStyle']))
            
            elements.append(Spacer(1, 10))
        
        return elements
    
    def _build_references_section(self, context: ConversationContext) -> List[Any]:
        """Build references and connections section"""
        elements = []
        
        elements.append(Paragraph("References and Connections", self.styles['Heading1']))
        elements.append(Spacer(1, 12))
        
        elements.append(Paragraph("This section shows how exchanges reference and build upon previous discussions:", self.styles['Normal']))
        elements.append(Spacer(1, 10))
        
        for exchange_num, references in context.reference_map.items():
            if references:
                header = f"Exchange {exchange_num} References:"
                elements.append(Paragraph(header, self.styles['ExchangeHeader']))
                
                for ref in references:
                    ref_text = f"→ References Exchange {ref.exchange_number} ({ref.reference_type}): {ref.referenced_content}"
                    elements.append(Paragraph(ref_text, self.styles['ReferenceStyle']))
                    
                    if ref.context_snippet:
                        context_text = f"   Context: \"{ref.context_snippet}\""
                        elements.append(Paragraph(context_text, self.styles['Normal']))
                
                elements.append(Spacer(1, 10))
        
        return elements
    
    def generate_quick_summary_pdf(self, 
                                  context: ConversationContext,
                                  output_path: str) -> bool:
        """Generate a quick 1-page summary PDF"""
        try:
            c = canvas.Canvas(output_path, pagesize=letter)
            width, height = letter
            
            # Title
            c.setFont("Helvetica-Bold", 16)
            c.drawString(50, height - 50, f"Conversation Summary: {context.conversation_id}")
            
            # Basic stats
            y_pos = height - 100
            c.setFont("Helvetica", 12)
            
            stats = [
                f"Total Exchanges: {context.total_exchanges}",
                f"Active Topics: {', '.join(context.active_topics[:5])}",
                f"Key Insights: {len(context.key_insights)}",
                f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
            ]
            
            for stat in stats:
                c.drawString(50, y_pos, stat)
                y_pos -= 20
            
            # Recent exchanges
            y_pos -= 20
            c.setFont("Helvetica-Bold", 14)
            c.drawString(50, y_pos, "Recent Exchanges:")
            y_pos -= 20
            
            c.setFont("Helvetica", 10)
            for turn in context.conversation_turns[-3:]:  # Last 3 exchanges
                exchange_text = f"Ex {turn.exchange_number}: {turn.input_summary} → {turn.response_summary[:60]}..."
                c.drawString(50, y_pos, exchange_text)
                y_pos -= 15
            
            c.save()
            return True
            
        except Exception as e:
            self.logger.error(f"Error generating quick summary: {e}")
            return False

Parameters

Name Type Default Kind
bases - -

Parameter Details

No constructor parameters: The __init__ method takes no parameters. It initializes the logger, sets up PDF styling with custom paragraph styles, and defines a color scheme for different document elements.

Return Value

Instantiation returns a ConversationTimelineGenerator object. The main methods (generate_conversation_timeline and generate_quick_summary_pdf) return boolean values: True if PDF generation succeeds, False if an error occurs. The private helper methods return List[Any] containing ReportLab flowable elements for PDF construction.

Class Interface

Methods

__init__(self) -> None

Purpose: Initialize the timeline generator with logger, PDF styles, and color scheme

Returns: None - initializes instance attributes

setup_custom_styles(self) -> None

Purpose: Configure custom paragraph styles for different PDF elements (titles, headers, content, topics, references)

Returns: None - modifies self.styles by adding custom ParagraphStyle objects

async generate_conversation_timeline(self, context: ConversationContext, output_path: str) -> bool

Purpose: Generate a comprehensive multi-page PDF timeline report with title page, executive summary, timeline visualization, detailed exchanges, problem-solving analysis, and references

Parameters:

  • context: ConversationContext object containing full conversation data including turns, topics, insights, problem-solving chain, and reference map
  • output_path: String path where the output PDF file should be saved

Returns: Boolean - True if PDF generation succeeds, False if an error occurs

_build_title_page(self, context: ConversationContext) -> List[Any]

Purpose: Build ReportLab flowable elements for the title page including conversation details, active topics, and key insights

Parameters:

  • context: ConversationContext object with conversation metadata

Returns: List of ReportLab flowable elements (Paragraph, Spacer objects) for the title page

_build_executive_summary(self, context: ConversationContext) -> List[Any]

Purpose: Build executive summary section with conversation overview, statistics table, and problem-solving progression

Parameters:

  • context: ConversationContext object with conversation statistics and problem-solving chain

Returns: List of ReportLab flowable elements for the executive summary section

_build_timeline_section(self, context: ConversationContext) -> List[Any]

Purpose: Build visual timeline section with a table showing exchange numbers, timestamps, inputs, topics, and processing times

Parameters:

  • context: ConversationContext object with conversation_turns data

Returns: List of ReportLab flowable elements including a formatted timeline table

_build_detailed_exchanges(self, context: ConversationContext) -> List[Any]

Purpose: Build detailed documentation for each conversation exchange including input/response summaries, topics, key points, and processing statistics

Parameters:

  • context: ConversationContext object with conversation_turns containing detailed exchange information

Returns: List of ReportLab flowable elements documenting each exchange in detail

_build_problem_solving_analysis(self, context: ConversationContext) -> List[Any]

Purpose: Build problem-solving analysis section showing the progression of problem-solving steps throughout the conversation

Parameters:

  • context: ConversationContext object with problem_solving_chain data

Returns: List of ReportLab flowable elements documenting problem-solving progression

_build_references_section(self, context: ConversationContext) -> List[Any]

Purpose: Build references and connections section showing how exchanges reference and build upon previous discussions

Parameters:

  • context: ConversationContext object with reference_map data

Returns: List of ReportLab flowable elements documenting cross-references between exchanges

generate_quick_summary_pdf(self, context: ConversationContext, output_path: str) -> bool

Purpose: Generate a quick one-page summary PDF with basic statistics and recent exchanges using ReportLab canvas

Parameters:

  • context: ConversationContext object with conversation data
  • output_path: String path where the output PDF file should be saved

Returns: Boolean - True if PDF generation succeeds, False if an error occurs

Attributes

Name Type Description Scope
logger logging.Logger Logger instance for tracking PDF generation operations and errors instance
styles reportlab.lib.styles.StyleSheet1 ReportLab stylesheet containing both default and custom paragraph styles for PDF formatting instance
colors Dict[str, colors.HexColor] Dictionary mapping element types (header, exchange, topic, reference, insight, light_gray, medium_gray) to HexColor objects for consistent PDF styling instance

Dependencies

  • asyncio
  • logging
  • pathlib
  • typing
  • datetime
  • json
  • reportlab

Required Imports

import asyncio
import logging
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
import json
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from conversation_context import ConversationContext, ConversationTurn, ConversationReference

Usage Example

import asyncio
from conversation_timeline_generator import ConversationTimelineGenerator
from conversation_context import ConversationContext

# Create generator instance
generator = ConversationTimelineGenerator()

# Assume we have a ConversationContext object with conversation data
context = ConversationContext(conversation_id="conv_123")
# ... populate context with conversation turns, topics, insights, etc.

# Generate comprehensive timeline PDF
async def generate_report():
    success = await generator.generate_conversation_timeline(
        context=context,
        output_path="/path/to/timeline_report.pdf"
    )
    if success:
        print("Timeline PDF generated successfully")
    else:
        print("Failed to generate timeline PDF")

# Generate quick summary PDF
success = generator.generate_quick_summary_pdf(
    context=context,
    output_path="/path/to/quick_summary.pdf"
)

# Run async generation
asyncio.run(generate_report())

Best Practices

  • Always instantiate the class before calling any methods - the constructor sets up essential styling and color schemes
  • Ensure the ConversationContext object is fully populated with all required data (conversation_turns, active_topics, key_insights, etc.) before generating PDFs
  • Use await when calling generate_conversation_timeline() as it is an async method
  • Check the boolean return value to verify successful PDF generation before assuming the file exists
  • Provide absolute paths for output_path to avoid file location ambiguity
  • The class maintains state through instance attributes (logger, styles, colors), so reuse the same instance for multiple PDF generations
  • Handle exceptions in calling code as the methods catch and log errors but return False rather than raising exceptions
  • For large conversations, be aware that comprehensive timeline generation may take significant time and memory
  • The quick summary PDF is synchronous and faster, suitable for real-time previews
  • Custom styles are set up during initialization and cannot be modified after instantiation without directly accessing self.styles

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class AuditPageGenerator 60.0% similar

    A class that generates comprehensive PDF audit trail pages for documents, including document information, reviews, approvals, revision history, and event history with electronic signatures.

    From: /tf/active/vicechatdev/document_auditor/src/audit_page_generator.py
  • class PDFGenerator 58.9% similar

    A class that generates PDF documents optimized for e-ink displays, converting LLM responses and images into formatted, high-contrast PDFs with custom styling.

    From: /tf/active/vicechatdev/e-ink-llm/pdf_generator.py
  • function export_to_pdf_v1 58.5% similar

    Flask route handler that exports a chat conversation to a PDF file with formatted messages, roles, and references using the reportlab library.

    From: /tf/active/vicechatdev/docchat/app.py
  • class ConversationContext 57.4% similar

    A dataclass that stores comprehensive conversation context including timeline, turns, topics, insights, and references for managing rich conversational state.

    From: /tf/active/vicechatdev/e-ink-llm/conversation_context.py
  • class SessionDocTemplate 55.9% similar

    A custom ReportLab document template that extends BaseDocTemplate to add session information (conversation ID and exchange number) in the footer of each page.

    From: /tf/active/vicechatdev/e-ink-llm/pdf_generator.py
← Back to Browse