use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::chat::types::{ConversationItem, Lifecycle};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChatRole {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatTurn {
pub id: String,
pub role: ChatRole,
pub text: String,
pub status: Lifecycle,
pub items: Vec<ConversationItem>,
pub created_at: String,
pub from: Option<String>,
}
impl ChatTurn {
fn now_rfc3339() -> String {
OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
}
pub fn user(id: String, text: String) -> Self {
Self {
id,
role: ChatRole::User,
text,
status: Lifecycle::Completed,
items: Vec::new(),
created_at: Self::now_rfc3339(),
from: None,
}
}
pub fn absorb_item(&mut self, item: ConversationItem) {
if let ConversationItem::Message { text, .. } = &item {
self.push_text(text);
} else {
self.items.push(item);
}
}
pub fn push_text(&mut self, fragment: &str) {
if !self.text.is_empty() {
self.text.push('\n');
}
self.text.push_str(fragment);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_turn_round_trips_through_json() {
let turn = ChatTurn::user("turn-0".into(), "please fix the build".into());
let value = serde_json::to_value(&turn).expect("serialize");
let decoded: ChatTurn = serde_json::from_value(value).expect("deserialize");
assert_eq!(decoded, turn);
assert_eq!(decoded.role, ChatRole::User);
}
#[test]
fn attributed_turn_round_trips_and_absent_from_decodes_none() {
let mut turn = ChatTurn::user("turn-1".into(), "worker report".into());
turn.from = Some("worker".to_string());
let value = serde_json::to_value(&turn).expect("serialize");
assert_eq!(value["from"], "worker");
let decoded: ChatTurn = serde_json::from_value(value).expect("deserialize");
assert_eq!(decoded.from.as_deref(), Some("worker"));
let mut value =
serde_json::to_value(ChatTurn::user("turn-2".into(), "hi".into())).expect("serialize");
value.as_object_mut().expect("object").remove("from");
let decoded: ChatTurn = serde_json::from_value(value).expect("deserialize");
assert_eq!(decoded.from, None);
}
#[test]
fn absorb_item_joins_prose_and_appends_the_rest() {
let mut turn = ChatTurn::user("turn-3".into(), String::new());
turn.absorb_item(ConversationItem::Message {
id: "m-1".into(),
text: "first".into(),
phase: None,
});
turn.absorb_item(ConversationItem::Tool {
id: "t-1".into(),
name: "Bash".into(),
status: Lifecycle::Completed,
input: None,
output: None,
});
turn.absorb_item(ConversationItem::Message {
id: "m-2".into(),
text: "second".into(),
phase: None,
});
assert_eq!(turn.text, "first\nsecond");
assert_eq!(turn.items.len(), 1, "prose joins text, tools append");
}
}