diff --git a/src/moai/core/exporter.py b/src/moai/core/exporter.py new file mode 100644 index 0000000..efdce39 --- /dev/null +++ b/src/moai/core/exporter.py @@ -0,0 +1,117 @@ +"""Markdown export functionality for MoAI discussions and projects. + +Exports discussions and projects to shareable markdown documents following +the SPEC format with sections for initial responses, rounds, and consensus. +""" + +from datetime import datetime + +from moai.core.models import Consensus, Discussion, Project, RoundType + + +def _format_consensus(consensus: Consensus | None) -> str: + """Format consensus section as markdown. + + Args: + consensus: The Consensus object to format, or None. + + Returns: + Formatted markdown string for consensus, or empty string if none. + """ + if consensus is None: + return "" + + lines = ["### Consensus"] + + if consensus.agreements: + lines.append("**Agreements:**") + for agreement in consensus.agreements: + lines.append(f"- {agreement}") + lines.append("") + + if consensus.disagreements: + lines.append("**Disagreements:**") + for disagreement in consensus.disagreements: + topic = disagreement.get("topic", "Unknown topic") + positions = disagreement.get("positions", {}) + lines.append(f"- **{topic}:**") + for model, position in positions.items(): + lines.append(f" - {model}: {position}") + lines.append("") + + return "\n".join(lines) + + +def export_discussion(discussion: Discussion) -> str: + """Export a discussion as markdown. + + Args: + discussion: The Discussion object to export (with rounds/messages loaded). + + Returns: + Markdown string formatted according to SPEC. + """ + lines = [f"## Discussion: {discussion.question}", ""] + + # Group messages by round + sorted_rounds = sorted(discussion.rounds, key=lambda r: r.round_number) + + for round_ in sorted_rounds: + # Label round type appropriately + if round_.type == RoundType.PARALLEL: + lines.append("### Initial Responses (Open)") + else: + lines.append(f"### Round {round_.round_number}") + lines.append("") + + # Format each message + sorted_messages = sorted(round_.messages, key=lambda m: m.timestamp) + for message in sorted_messages: + model_name = message.model.capitalize() + lines.append(f"**{model_name}:**") + # Quote the response content + for content_line in message.content.split("\n"): + lines.append(f"> {content_line}") + lines.append("") + + # Add consensus section if exists + consensus_md = _format_consensus(discussion.consensus) + if consensus_md: + lines.append(consensus_md) + + return "\n".join(lines) + + +def export_project(project: Project, discussions: list[Discussion]) -> str: + """Export a project and its discussions as markdown. + + Args: + project: The Project object to export. + discussions: List of Discussion objects (with rounds/messages loaded). + + Returns: + Full markdown string for the project export. + """ + # Header section + date_str = datetime.now().strftime("%Y-%m-%d") + models_str = ", ".join(m.capitalize() for m in project.models) if project.models else "None" + + lines = [ + f"# {project.name}", + "", + f"**Date:** {date_str}", + f"**Models:** {models_str}", + f"**Discussions:** {len(discussions)}", + "", + "---", + "", + ] + + # Add each discussion + for i, discussion in enumerate(discussions): + if i > 0: + lines.append("---") + lines.append("") + lines.append(export_discussion(discussion)) + + return "\n".join(lines)