use cognis_core::Message;
#[derive(Debug, Clone)]
pub struct ConversationTurn {
pub user: Option<Message>,
pub assistant: Option<Message>,
pub tool_messages: Vec<Message>,
}
impl ConversationTurn {
pub fn is_complete(&self) -> bool {
self.user.is_some() && self.assistant.is_some()
}
pub fn message_count(&self) -> usize {
self.user.as_ref().map_or(0, |_| 1)
+ self.assistant.as_ref().map_or(0, |_| 1)
+ self.tool_messages.len()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConversationSummary {
pub turn_count: usize,
pub complete_turn_count: usize,
pub message_count: usize,
pub user_message_count: usize,
pub ai_message_count: usize,
pub tool_message_count: usize,
pub system_message_count: usize,
pub first_user_input: Option<String>,
pub last_ai_output: Option<String>,
pub total_chars: usize,
}
pub fn turns_from_messages(messages: &[Message]) -> Vec<ConversationTurn> {
let mut out: Vec<ConversationTurn> = Vec::new();
let mut current: Option<ConversationTurn> = None;
for m in messages {
match m {
Message::System(_) => continue,
Message::Human(_) => {
if let Some(t) = current.take() {
out.push(t);
}
current = Some(ConversationTurn {
user: Some(m.clone()),
assistant: None,
tool_messages: Vec::new(),
});
}
Message::Ai(_) => {
let mut t = current.take().unwrap_or(ConversationTurn {
user: None,
assistant: None,
tool_messages: Vec::new(),
});
t.assistant = Some(m.clone());
out.push(t);
}
Message::Tool(_) => match current.as_mut() {
Some(t) => t.tool_messages.push(m.clone()),
None => {
current = Some(ConversationTurn {
user: None,
assistant: None,
tool_messages: vec![m.clone()],
});
}
},
}
}
if let Some(t) = current.take() {
out.push(t);
}
out
}
pub fn summarize(messages: &[Message]) -> ConversationSummary {
let mut s = ConversationSummary {
message_count: messages.len(),
..Default::default()
};
for m in messages {
s.total_chars += m.content().len();
match m {
Message::System(_) => s.system_message_count += 1,
Message::Human(_) => {
s.user_message_count += 1;
if s.first_user_input.is_none() {
s.first_user_input = Some(truncate(m.content(), 200));
}
}
Message::Ai(_) => {
s.ai_message_count += 1;
s.last_ai_output = Some(truncate(m.content(), 200));
}
Message::Tool(_) => s.tool_message_count += 1,
}
}
let turns = turns_from_messages(messages);
s.turn_count = turns.len();
s.complete_turn_count = turns.iter().filter(|t| t.is_complete()).count();
s
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
fn h(s: &str) -> Message {
Message::human(s)
}
fn a(s: &str) -> Message {
Message::ai(s)
}
fn sys(s: &str) -> Message {
Message::system(s)
}
#[test]
fn empty_messages_produce_empty_summary() {
let s = summarize(&[]);
assert_eq!(s, ConversationSummary::default());
assert!(turns_from_messages(&[]).is_empty());
}
#[test]
fn pairs_user_with_next_ai() {
let msgs = vec![h("hi"), a("hello"), h("how"), a("good")];
let turns = turns_from_messages(&msgs);
assert_eq!(turns.len(), 2);
assert!(turns.iter().all(|t| t.is_complete()));
}
#[test]
fn trailing_user_is_incomplete_turn() {
let msgs = vec![h("hi"), a("hello"), h("waiting")];
let turns = turns_from_messages(&msgs);
assert_eq!(turns.len(), 2);
assert!(turns[0].is_complete());
assert!(!turns[1].is_complete());
assert_eq!(turns[1].user.as_ref().unwrap().content(), "waiting");
assert!(turns[1].assistant.is_none());
}
#[test]
fn system_messages_dont_count_as_turns() {
let msgs = vec![sys("preamble"), h("hi"), a("hello")];
let turns = turns_from_messages(&msgs);
assert_eq!(turns.len(), 1);
let s = summarize(&msgs);
assert_eq!(s.system_message_count, 1);
assert_eq!(s.turn_count, 1);
}
#[test]
fn summary_aggregates_counts_and_previews() {
let msgs = vec![
sys("be brief"),
h("first question"),
a("first reply"),
h("second question"),
a("second reply"),
];
let s = summarize(&msgs);
assert_eq!(s.message_count, 5);
assert_eq!(s.user_message_count, 2);
assert_eq!(s.ai_message_count, 2);
assert_eq!(s.system_message_count, 1);
assert_eq!(s.turn_count, 2);
assert_eq!(s.complete_turn_count, 2);
assert_eq!(s.first_user_input.as_deref(), Some("first question"));
assert_eq!(s.last_ai_output.as_deref(), Some("second reply"));
}
#[test]
fn truncates_long_previews() {
let long = "a".repeat(500);
let msgs = vec![h(&long), a(&long)];
let s = summarize(&msgs);
let preview = s.first_user_input.unwrap();
assert!(preview.chars().count() <= 200);
assert!(preview.ends_with('…'));
}
}