Source code for klea_utils.ui.export
#!/usr/bin/env python3
"""
Session transcript export helpers.
Framework-agnostic rendering of a chat's rendered transcript (the
``klea_utils.ui.state.MessageData`` entries) to Markdown, shared by the
frontends' ``/export`` client command (ADR-0047).
File: klea_utils/ui/export.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import logging
import re
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any
logger = logging.getLogger(__name__)
#: Display labels for the non-tool transcript roles.
_ROLE_LABELS = {
"user": "User",
"agent": "Assistant",
"system": "System",
}
def _message_label(message: Mapping[str, Any]) -> str:
"""Return the display label for a transcript message.
Tool messages are prefixed with ``Tool:`` and their ``header`` (for
example ``Tool: run_command``); the remaining roles map to a fixed label.
"""
role = str(message.get("role") or "")
if role == "tool":
header = str(message.get("header") or "").strip()
return f"Tool: {header}" if header else "Tool"
return _ROLE_LABELS.get(role, role.capitalize() or "Message")
def _clean_title(title: str) -> str:
"""Collapse whitespace in *title* so it cannot break the ``#`` heading."""
return " ".join((title or "").split()) or "Klea session"
[docs]
def transcript_markdown(
messages: Sequence[Mapping[str, Any]],
*,
title: str = "Klea session",
exported_at: str = "",
) -> str:
"""Render *messages* as a Markdown transcript.
Each turn is a ``##`` heading (``## User``, ``## Assistant``,
``## Tool: <name>``) rather than bold, so the role labels stay distinct
from any ``**bold**`` in the message content.
:param messages: The chat's rendered transcript entries (each with
``role``/``text``/``stamp``, and ``header`` for tool blocks).
:param title: The document title (usually the session name); whitespace is
collapsed and an empty title falls back to ``Klea session``.
:param exported_at: An optional timestamp line (empty to omit).
:returns: The Markdown document, ending in a newline.
"""
lines = [f"# {_clean_title(title)}", ""]
if exported_at:
lines += [f"_Exported {exported_at}_", ""]
for message in messages:
stamp = str(message.get("stamp") or "")
heading = f"## {_message_label(message)}"
if stamp:
heading += f" ({stamp})"
lines += [heading, "", str(message.get("text") or "").rstrip(), ""]
return "\n".join(lines).rstrip() + "\n"
[docs]
def export_filename(name: str, *, suffix: str = "md") -> str:
"""Return a filesystem-safe export filename derived from *name*.
Replaces any run of characters outside ``[A-Za-z0-9._-]`` with a single
hyphen; falls back to ``klea-session`` when *name* has no usable
characters.
:param name: The session name (usually the chat title).
:param suffix: The file extension, without the dot (default ``md``).
:returns: A safe ``<slug>.<suffix>`` filename.
"""
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", (name or "").strip()).strip("-.")
return f"{slug or 'klea-session'}.{suffix}"
[docs]
def export_timestamp(when: datetime | None = None) -> str:
"""Return a human-readable, ASCII timestamp for the export header."""
moment = when or datetime.now().astimezone()
return moment.strftime("%Y-%m-%d %H:%M:%S %Z").strip()