use serde::Deserialize;
const BASE: &str = include_str!("../../prompts/base.md");
const SUMMARY: &str = include_str!("../../prompts/summary.md");
const TASKS: &str = include_str!("../../prompts/tasks.md");
const REVIEW: &str = include_str!("../../prompts/review.md");
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "kind", content = "text", rename_all = "snake_case")]
pub enum Command {
Summary,
Tasks,
Review,
Custom(String),
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("Напишите, что сделать")]
EmptyRequest,
#[error("Заметка пустая — сначала расшифруйте аудио или напишите текст")]
EmptyNote,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prepared {
pub heading: String,
pub system: String,
pub user: String,
}
pub fn prepare(command: &Command, note: &str) -> Result<Prepared, Error> {
if note.trim().is_empty() {
return Err(Error::EmptyNote);
}
let (heading, task) = match command {
Command::Summary => ("Конспект".to_string(), SUMMARY.trim().to_string()),
Command::Tasks => ("Задачи".to_string(), TASKS.trim().to_string()),
Command::Review => ("Ревью".to_string(), REVIEW.trim().to_string()),
Command::Custom(request) => {
let request = request.trim();
if request.is_empty() {
return Err(Error::EmptyRequest);
}
(heading_for(request), format!("Просьба человека: {request}"))
}
};
Ok(Prepared {
heading,
system: format!("{}\n\n## Что сделать сейчас\n\n{task}", BASE.trim()),
user: format!("<заметка>\n{}\n</заметка>", note.trim()),
})
}
fn heading_for(request: &str) -> String {
let line = request.lines().next().unwrap_or(request).trim();
let mut chars = line.chars();
let short: String = chars.by_ref().take(60).collect();
let mut h = if chars.next().is_some() {
format!("{}…", short.trim_end())
} else {
short
};
if let Some(first) = h.chars().next() {
h = first.to_uppercase().collect::<String>() + &h[first.len_utf8()..];
}
h
}
pub fn to_block(heading: &str, answer: &str) -> String {
let body: Vec<String> = answer
.trim()
.lines()
.map(|l| {
if l.starts_with("## ") || l.starts_with("# ") {
format!("### {}", l.trim_start_matches('#').trim())
} else {
l.to_string()
}
})
.collect();
format!("## {heading}\n\n{}\n", body.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ready_commands_have_headings_and_task_text() {
let p = prepare(&Command::Tasks, "## Расшифровка\n\nтекст").unwrap();
assert_eq!(p.heading, "Задачи");
assert!(p.system.starts_with("Ты помогаешь человеку работать с заметкой"));
assert!(p.system.contains("- [ ] Что сделать"));
assert_eq!(p.user, "<заметка>\n## Расшифровка\n\nтекст\n</заметка>");
assert_eq!(prepare(&Command::Summary, "x").unwrap().heading, "Конспект");
assert_eq!(prepare(&Command::Review, "x").unwrap().heading, "Ревью");
}
#[test]
fn custom_request_becomes_heading_and_task() {
let p = prepare(&Command::Custom(" напиши письмо клиенту по итогам ".into()), "x").unwrap();
assert_eq!(p.heading, "Напиши письмо клиенту по итогам");
assert!(p
.system
.ends_with("Просьба человека: напиши письмо клиенту по итогам"));
let long = "а".repeat(100);
assert_eq!(heading_for(&long).chars().count(), 61, "60 знаков и многоточие");
}
#[test]
fn empty_input_is_refused() {
assert_eq!(
prepare(&Command::Custom(" ".into()), "x"),
Err(Error::EmptyRequest)
);
assert_eq!(prepare(&Command::Summary, " \n "), Err(Error::EmptyNote));
}
#[test]
fn answer_headings_are_demoted() {
assert_eq!(
to_block("Конспект", "# Конспект\n\nТекст.\n## Решения\n- Одно\n### Детали"),
"## Конспект\n\n### Конспект\n\nТекст.\n### Решения\n- Одно\n### Детали\n"
);
}
#[test]
fn command_comes_from_the_interface_as_json() {
let c: Command = serde_json::from_str(r#"{"kind":"custom","text":"выпиши цифры"}"#).unwrap();
assert_eq!(c, Command::Custom("выпиши цифры".into()));
let c: Command = serde_json::from_str(r#"{"kind":"summary"}"#).unwrap();
assert_eq!(c, Command::Summary);
}
}