//! Pure Conversation entity — in-memory message history plus system prompt //! and LLM generation parameters. //! //! # Architecture //! This is a pure data structure with **no I/O logic**. Load/save //! responsibilities live in [`ConversationRepository`](super::repository::ConversationRepository). #![allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap )] use serde::{Deserialize, Serialize}; /// A single message role / content pair. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Role { #[serde(rename = "user")] User, #[serde(rename = "assistant")] Assistant, #[serde(rename = "system")] System, #[serde(rename = "tool")] Tool, } /// A single message in a conversation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatMessage { pub role: Role, pub content: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, } impl ChatMessage { /// Build a user-role message with the given text content. pub fn user(content: impl Into) -> Self { Self { role: Role::User, content: Some(content.into()), tool_calls: None, tool_call_id: None, name: None, } } /// Build an assistant-role message with an optional text response. pub fn assistant(content: Option) -> Self { Self { role: Role::Assistant, content, tool_calls: None, tool_call_id: None, name: None, } } /// Build a system-role message with the given instruction text. pub fn system(content: impl Into) -> Self { Self { role: Role::System, content: Some(content.into()), tool_calls: None, tool_call_id: None, name: None, } } /// Build a tool-role result message referencing a prior tool call. pub fn tool(tool_call_id: String, content: String) -> Self { Self { role: Role::Tool, content: Some(content), tool_calls: None, tool_call_id: Some(tool_call_id), name: None, } } } /// A single conversation's message history and generation settings. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conversation { pub messages: Vec, pub system_prompt: String, pub session_id: String, pub model: String, pub max_tokens: Option, pub temperature: Option, } impl Conversation { /// Create an empty conversation with the given system prompt and /// session id, using default model / token / temperature settings. pub fn new(system_prompt: String, session_id: String) -> Self { Self { messages: Vec::new(), system_prompt, session_id, model: "anthropic/claude-opus-4-8".to_string(), max_tokens: None, temperature: None, } } /// Append a message to the conversation history. pub fn push(&mut self, msg: ChatMessage) { self.messages.push(msg); } /// Replace the system prompt and strip any prior `System`-role messages /// from history. pub fn rebuild_system(&mut self, new_prompt: String) { self.system_prompt = new_prompt; self.messages.retain(|m| !matches!(m.role, Role::System)); } /// Build the message list to send to the LLM API, with the system /// prompt prepended as the first message. pub fn to_api_messages(&self) -> Vec { let mut msgs = Vec::with_capacity(self.messages.len() + 1); msgs.push(ChatMessage::system(&self.system_prompt)); msgs.extend(self.messages.iter().cloned()); msgs } /// Number of messages in the conversation history (excluding the /// synthesized system message). pub fn len(&self) -> usize { self.messages.len() } /// Returns `true` if the conversation has no messages. pub fn is_empty(&self) -> bool { self.messages.is_empty() } }