feat(06-02): create exporter module for markdown export

- export_discussion() formats discussion with rounds and messages
- export_project() creates full project export with header
- _format_consensus() helper for consensus section formatting
- Follows SPEC.md markdown format
This commit is contained in:
Mikkel Georgsen 2026-01-17 01:48:14 +00:00
parent edb4ab5593
commit 152d6173d6

117
src/moai/core/exporter.py Normal file
View file

@ -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)