use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::tools::ToolCall;
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum MessageRole {
#[serde(rename = "system")]
System,
#[serde(rename = "user")]
User,
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "tool")]
Tool,
}
#[derive(Debug, Serialize, Deserialize, Clone, TypedBuilder)]
pub struct Message {
#[builder(default = Uuid::new_v4())]
pub id: Uuid,
pub conversation_id: Uuid,
pub role: MessageRole,
pub content: String,
#[builder(default)]
pub metadata: HashMap<String, serde_json::Value>,
#[builder(default = Utc::now())]
pub timestamp: DateTime<Utc>,
#[builder(default)]
pub tool_calls: Vec<ToolCall>,
#[builder(default)]
pub tool_call_id: Option<String>,
}
impl Message {
pub fn new(conversation_id: Uuid, role: MessageRole, content: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
conversation_id,
role,
content: content.into(),
metadata: HashMap::new(),
timestamp: Utc::now(),
tool_calls: Vec::new(),
tool_call_id: None,
}
}
pub fn system(conversation_id: Uuid, content: impl Into<String>) -> Self {
Self::new(conversation_id, MessageRole::System, content)
}
pub fn user(conversation_id: Uuid, content: impl Into<String>) -> Self {
Self::new(conversation_id, MessageRole::User, content)
}
pub fn assistant(conversation_id: Uuid, content: impl Into<String>) -> Self {
Self::new(conversation_id, MessageRole::Assistant, content)
}
pub fn tool(
conversation_id: Uuid,
content: impl Into<String>,
tool_call_id: String,
) -> anyhow::Result<Self> {
if tool_call_id.is_empty() {
anyhow::bail!("Tool call ID cannot be empty");
}
let mut msg = Self::new(conversation_id, MessageRole::Tool, content);
msg.tool_call_id = Some(tool_call_id);
Ok(msg)
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn with_metadata_typed<T: serde::Serialize>(
mut self,
key: impl Into<String>,
value: T,
) -> anyhow::Result<Self> {
let json_value = serde_json::to_value(value)?;
self.metadata.insert(key.into(), json_value);
Ok(self)
}
pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> anyhow::Result<Self> {
if self.role != MessageRole::Assistant {
anyhow::bail!(
"Tool calls can only be added to assistant messages, found {:?}",
self.role
);
}
self.tool_calls = tool_calls;
Ok(self)
}
pub fn add_tool_call(&mut self, tool_call: ToolCall) -> anyhow::Result<()> {
if self.role != MessageRole::Assistant {
anyhow::bail!(
"Tool calls can only be added to assistant messages, found {:?}",
self.role
);
}
self.tool_calls.push(tool_call);
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConversationStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "paused")]
Paused,
#[serde(rename = "archived")]
Archived,
#[serde(rename = "deleted")]
Deleted,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Conversation {
pub id: Uuid,
pub title: Option<String>,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metadata: HashMap<String, serde_json::Value>,
pub status: ConversationStatus,
pub messages: Vec<Message>,
}
impl Conversation {
pub fn new() -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
title: None,
description: None,
created_at: now,
updated_at: now,
metadata: HashMap::new(),
status: ConversationStatus::Active,
messages: Vec::new(),
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn set_status(&mut self, status: ConversationStatus) {
self.status = status;
self.updated_at = Utc::now();
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn add_message(&mut self, message: Message) -> anyhow::Result<()> {
if message.conversation_id != self.id {
anyhow::bail!(
"Message conversation_id {} does not match conversation id {}",
message.conversation_id,
self.id
);
}
self.messages.push(message);
self.touch();
Ok(())
}
pub fn get_messages(&self) -> &[Message] {
&self.messages
}
}
impl Default for Conversation {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let conv_id = Uuid::new_v4();
let msg = Message::user(conv_id, "Hello, world!");
assert_eq!(msg.conversation_id, conv_id);
assert_eq!(msg.role, MessageRole::User);
assert_eq!(msg.content, "Hello, world!");
assert!(msg.tool_calls.is_empty());
}
#[test]
fn test_conversation_creation() {
let conv = Conversation::new()
.with_title("Test Conversation")
.with_description("A test conversation");
assert_eq!(conv.title, Some("Test Conversation".to_string()));
assert_eq!(conv.description, Some("A test conversation".to_string()));
assert_eq!(conv.status, ConversationStatus::Active);
}
#[test]
fn test_tool_call_creation() {
let tool_call = ToolCall::new("test_function", [r#"{"param": "value"}"#]);
assert_eq!(tool_call.function.name, "test_function");
assert_eq!(tool_call.function.arguments, vec![r#"{"param": "value"}"#]);
assert_eq!(tool_call.call_type, "function");
assert!(!tool_call.id.is_empty());
}
#[test]
fn test_message_with_tool_calls() {
let conv_id = Uuid::new_v4();
let tool_call = ToolCall::new("get_weather", [r#"{"location": "New York"}"#]);
let msg = Message::assistant(conv_id, "I'll check the weather for you.")
.with_tool_calls(vec![tool_call])
.expect("Failed to add tool calls");
assert_eq!(msg.tool_calls.len(), 1);
assert_eq!(msg.tool_calls[0].function.name, "get_weather");
assert_eq!(
msg.tool_calls[0].function.arguments,
vec![r#"{"location": "New York"}"#]
);
}
#[test]
fn test_message_tool_call_validation() {
let conv_id = Uuid::new_v4();
let tool_call = ToolCall::new("get_weather", [r#"{"location": "New York"}"#]);
let user_msg = Message::user(conv_id, "What's the weather?");
let result = user_msg.with_tool_calls(vec![tool_call.clone()]);
assert!(result.is_err());
let assistant_msg = Message::assistant(conv_id, "Let me check.");
let result = assistant_msg.with_tool_calls(vec![tool_call]);
assert!(result.is_ok());
}
#[test]
fn test_tool_message_validation() {
let conv_id = Uuid::new_v4();
let result = Message::tool(conv_id, "Result", String::new());
assert!(result.is_err());
let result = Message::tool(conv_id, "Result", "call_123".to_string());
assert!(result.is_ok());
}
#[test]
fn test_conversation_add_message() {
let mut conv = Conversation::new();
let msg = Message::user(conv.id, "Hello");
conv.add_message(msg).expect("Failed to add message");
assert_eq!(conv.messages.len(), 1);
assert_eq!(conv.messages[0].content, "Hello");
}
#[test]
fn test_conversation_add_message_wrong_id() {
let mut conv = Conversation::new();
let other_id = Uuid::new_v4();
let msg = Message::user(other_id, "Hello");
let result = conv.add_message(msg);
assert!(result.is_err());
}
#[test]
fn test_message_with_metadata_typed() {
let conv_id = Uuid::new_v4();
let msg = Message::user(conv_id, "Hello")
.with_metadata_typed("count", 42)
.expect("Failed to add metadata");
assert_eq!(msg.metadata.get("count"), Some(&serde_json::json!(42)));
}
#[test]
fn test_tool_call_with_multiple_args() {
let tool_call = ToolCall::new(
"complex_function",
vec![
"arg1".to_string(),
"arg2".to_string(),
r#"{"key": "value"}"#.to_string(),
],
);
assert_eq!(tool_call.function.name, "complex_function");
assert_eq!(tool_call.function.arguments.len(), 3);
assert_eq!(tool_call.function.arguments[0], "arg1");
assert_eq!(tool_call.function.arguments[1], "arg2");
assert_eq!(tool_call.function.arguments[2], r#"{"key": "value"}"#);
}
}