use crate::cm_types::Message;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportMdLocale {
ZhHans,
En,
}
pub fn export_md_title_full(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "# CrabMate 聊天记录\n\n",
ExportMdLocale::En => "# CrabMate chat export\n\n",
}
}
pub fn export_md_title_selection(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "# CrabMate 聊天记录(已选消息)\n\n",
ExportMdLocale::En => "# CrabMate chat export (selected messages)\n\n",
}
}
pub fn export_md_heading_user(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "## 用户",
ExportMdLocale::En => "## User",
}
}
pub fn export_md_heading_assistant(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "## 助手",
ExportMdLocale::En => "## Assistant",
}
}
pub fn export_md_heading_tool(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "## 工具",
ExportMdLocale::En => "## Tool",
}
}
pub fn export_md_heading_other(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "## 其它",
ExportMdLocale::En => "## Other",
}
}
pub fn export_md_heading_timeline(l: ExportMdLocale) -> &'static str {
match l {
ExportMdLocale::ZhHans => "## 时间线",
ExportMdLocale::En => "## Timeline",
}
}
fn heading_for_role(role: &str, loc: ExportMdLocale) -> &'static str {
match role {
"user" => export_md_heading_user(loc),
"assistant" => export_md_heading_assistant(loc),
"tool" => export_md_heading_tool(loc),
"system" => export_md_heading_timeline(loc),
_ => export_md_heading_other(loc),
}
}
pub fn append_markdown_role_section(md: &mut String, role: &str, body: &str, loc: ExportMdLocale) {
if role == "system" {
return;
}
md.push_str(heading_for_role(role, loc));
md.push_str("\n\n");
md.push_str(body);
md.push_str("\n\n");
}
pub fn markdown_from_role_bodies<'a, I>(title: &str, items: I, loc: ExportMdLocale) -> String
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let mut md = String::from(title);
for (role, body) in items {
append_markdown_role_section(&mut md, role, body, loc);
}
md
}
pub fn reorder_messages_for_conversation_flow(messages: Vec<Message>) -> Vec<Message> {
let mut result: Vec<Message> = Vec::with_capacity(messages.len());
let mut tool_calls_pending: Vec<Message> = Vec::new();
for m in messages {
match m.role.as_str() {
"assistant" => {
result.append(&mut tool_calls_pending);
result.push(m);
}
"tool" => {
tool_calls_pending.push(m);
}
"user" => {
result.append(&mut tool_calls_pending);
result.push(m);
}
_ => {
result.append(&mut tool_calls_pending);
result.push(m);
}
}
}
result.append(&mut tool_calls_pending);
result
}
pub fn messages_to_markdown_with_body<F>(
messages: &[Message],
loc: ExportMdLocale,
mut body_for: F,
) -> String
where
F: FnMut(&Message) -> String,
{
let reordered = reorder_messages_for_conversation_flow(messages.to_vec());
let mut md = String::from(export_md_title_full(loc));
for m in &reordered {
if m.role == "system" {
continue;
}
let body = body_for(m);
append_markdown_role_section(&mut md, m.role.as_str(), &body, loc);
}
md
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cm_types::Message;
fn msg(role: &str, content: &str) -> Message {
Message {
role: role.to_string(),
content: Some(content.into()),
reasoning_content: None,
reasoning_details: None,
tool_calls: None,
name: None,
tool_call_id: None,
}
}
fn plain_body(m: &Message) -> String {
crate::cm_types::message_content_as_str(&m.content)
.unwrap_or("")
.to_string()
}
#[test]
fn markdown_skips_system_and_labels_roles() {
let md = messages_to_markdown_with_body(
&[
msg("system", "sys"),
msg("user", "hi"),
msg("assistant", "hey"),
msg("tool", "out"),
],
ExportMdLocale::ZhHans,
plain_body,
);
assert!(!md.contains("sys"));
assert!(md.contains("## 用户"));
assert!(md.contains("hi"));
assert!(md.contains("## 助手"));
assert!(md.contains("## 工具"));
assert!(md.contains("out"));
}
#[test]
fn markdown_reorders_tool_after_assistant() {
let messages = vec![
msg("assistant", "开始执行"),
msg("tool", "解压缩结果"),
msg("tool", "list_tree 结果"),
msg("assistant", "已解压。看看目录结构..."),
];
let md = messages_to_markdown_with_body(&messages, ExportMdLocale::ZhHans, plain_body);
let assistant_pos = md.find("## 助手").unwrap();
let tool_pos = md.find("## 工具").unwrap();
let second_assistant_pos =
md[assistant_pos + 10..].find("## 助手").unwrap() + assistant_pos + 10;
assert!(
assistant_pos < tool_pos && tool_pos < second_assistant_pos,
"工具消息应该在两个助手消息之间"
);
}
#[test]
fn en_headings() {
let md = markdown_from_role_bodies(
export_md_title_full(ExportMdLocale::En),
[("user", "hi"), ("assistant", "yo")],
ExportMdLocale::En,
);
assert!(md.contains("# CrabMate chat export"));
assert!(md.contains("## User"));
assert!(md.contains("## Assistant"));
}
}