""" Session export generators for ResolutionFlow. Provides markdown, plain text, HTML, and PSA/ticket note export formatters for troubleshooting sessions. """ import html from datetime import datetime, timezone from app.models.session import Session from app.schemas.session import SessionExport def _format_duration(started_at: datetime, completed_at: datetime | None) -> str: """Format duration between two datetimes as human-readable string.""" if not completed_at: return "In progress" delta = completed_at - started_at total_seconds = int(delta.total_seconds()) if total_seconds < 0: return "0 minutes" hours, remainder = divmod(total_seconds, 3600) minutes = remainder // 60 if hours > 0: return f"{hours}h {minutes}m" return f"{minutes} minutes" def generate_markdown_export(session: Session, options: SessionExport) -> str: """Generate markdown export.""" lines = [] if options.include_tree_info: tree_name = session.tree_snapshot.get("name", "Troubleshooting Session") lines.append(f"# {tree_name}") lines.append("") if session.ticket_number: lines.append(f"**Ticket:** {session.ticket_number}") if session.client_name: lines.append(f"**Client:** {session.client_name}") if options.include_timestamps: lines.append(f"**Started:** {session.started_at.strftime('%Y-%m-%d %H:%M')}") if session.completed_at: lines.append(f"**Completed:** {session.completed_at.strftime('%Y-%m-%d %H:%M')}") lines.append("") lines.append("---") lines.append("") # Scratchpad / Evidence section scratchpad = getattr(session, 'scratchpad', '') or '' if scratchpad.strip(): lines.append("## Evidence / Reference") lines.append("") lines.append(scratchpad) lines.append("") lines.append("---") lines.append("") lines.append("## Troubleshooting Steps") lines.append("") for i, decision in enumerate(session.decisions, 1): question = decision.get("question") or decision.get("action_performed", "Step") answer = decision.get("answer", "") notes = decision.get("notes", "") lines.append(f"### Step {i}: {question}") if answer: lines.append(f"**Answer:** {answer}") if notes: lines.append(f"**Notes:** {notes}") if options.include_timestamps and decision.get("timestamp"): lines.append(f"*{decision['timestamp']}*") lines.append("") return "\n".join(lines) def generate_text_export(session: Session, options: SessionExport) -> str: """Generate plain text export.""" lines = [] if options.include_tree_info: tree_name = session.tree_snapshot.get("name", "Troubleshooting Session") lines.append(tree_name) lines.append("=" * len(tree_name)) if session.ticket_number: lines.append(f"Ticket: {session.ticket_number}") if session.client_name: lines.append(f"Client: {session.client_name}") if options.include_timestamps: lines.append(f"Started: {session.started_at.strftime('%Y-%m-%d %H:%M')}") if session.completed_at: lines.append(f"Completed: {session.completed_at.strftime('%Y-%m-%d %H:%M')}") lines.append("") # Scratchpad / Evidence section scratchpad = getattr(session, 'scratchpad', '') or '' if scratchpad.strip(): lines.append("EVIDENCE / REFERENCE") lines.append("-" * 20) lines.append(scratchpad) lines.append("") lines.append("TROUBLESHOOTING STEPS") lines.append("-" * 20) for i, decision in enumerate(session.decisions, 1): question = decision.get("question") or decision.get("action_performed", "Step") answer = decision.get("answer", "") notes = decision.get("notes", "") lines.append(f"\n{i}. {question}") if answer: lines.append(f" Answer: {answer}") if notes: lines.append(f" Notes: {notes}") return "\n".join(lines) def generate_html_export(session: Session, options: SessionExport) -> str: """Generate HTML export.""" tree_name = html.escape(session.tree_snapshot.get("name", "Troubleshooting Session")) html_parts = ['', '', '', '', f'{tree_name}', '', '', ''] if options.include_tree_info: html_parts.append(f'

{tree_name}

') html_parts.append('
') if session.ticket_number: html_parts.append(f'

Ticket: {html.escape(session.ticket_number)}

') if session.client_name: html_parts.append(f'

Client: {html.escape(session.client_name)}

') if options.include_timestamps: html_parts.append(f'

Started: {session.started_at.strftime("%Y-%m-%d %H:%M")}

') if session.completed_at: html_parts.append(f'

Completed: {session.completed_at.strftime("%Y-%m-%d %H:%M")}

') html_parts.append('
') # Scratchpad / Evidence section scratchpad = getattr(session, 'scratchpad', '') or '' if scratchpad.strip(): html_parts.append('

Evidence / Reference

') html_parts.append(f'
{html.escape(scratchpad)}
') html_parts.append('

Troubleshooting Steps

') for i, decision in enumerate(session.decisions, 1): question = html.escape(decision.get("question") or decision.get("action_performed", "Step")) answer = html.escape(decision.get("answer", "")) notes = html.escape(decision.get("notes", "")) html_parts.append('
') html_parts.append(f'

Step {i}: {question}

') if answer: html_parts.append(f'

Answer: {answer}

') if notes: html_parts.append(f'

Notes: {notes}

') if options.include_timestamps and decision.get("timestamp"): html_parts.append(f'

{html.escape(str(decision["timestamp"]))}

') html_parts.append('
') html_parts.extend(['', '']) return "\n".join(html_parts) def generate_psa_export(session: Session, options: SessionExport) -> str: """Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools.""" lines = [] tree_name = session.tree_snapshot.get("name", "Troubleshooting Session") tree_description = session.tree_snapshot.get("description", "") ticket_number = session.ticket_number or "N/A" client_name = session.client_name or "N/A" date_str = session.started_at.strftime("%Y-%m-%d %H:%M") lines.append("=== TROUBLESHOOTING NOTES ===") lines.append(f"Ticket: {ticket_number} | Client: {client_name}") lines.append(f"Tree: {tree_name} | Date: {date_str}") lines.append("") # Problem section lines.append("--- PROBLEM ---") lines.append(tree_description if tree_description else "No description provided.") lines.append("") # Steps taken lines.append("--- STEPS TAKEN ---") if session.decisions: for i, decision in enumerate(session.decisions, 1): question = decision.get("question") or decision.get("action_performed", "Step") answer = decision.get("answer", "") notes = decision.get("notes", "") line = f"{i}. {question}" if answer: line += f" -> {answer}" lines.append(line) if notes: lines.append(f" Notes: {notes}") else: lines.append("No steps recorded.") lines.append("") # Resolution - last decision answer lines.append("--- RESOLUTION ---") if session.decisions: last_decision = session.decisions[-1] resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded") lines.append(resolution) else: lines.append("No resolution recorded.") lines.append("") # Time spent lines.append("--- TIME SPENT ---") duration = _format_duration(session.started_at, session.completed_at) lines.append(f"Duration: {duration}") lines.append("") # Engineer notes (scratchpad) lines.append("--- ENGINEER NOTES ---") scratchpad = getattr(session, 'scratchpad', '') or '' lines.append(scratchpad.strip() if scratchpad.strip() else "None") return "\n".join(lines)