use crate::agents::ActionDisplay;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
pub timestamp: chrono::DateTime<chrono::Local>,
#[serde(default)]
pub actions: Vec<ActionDisplay>,
#[serde(default)]
pub thinking: Option<String>,
#[serde(default)]
pub images: Option<Vec<String>>,
#[serde(default)]
pub tool_calls: Option<Vec<crate::models::tool_call::ToolCall>>,
#[serde(default)]
pub tool_call_id: Option<String>,
#[serde(default)]
pub tool_name: Option<String>,
}
impl ChatMessage {
pub fn user(content: impl Into<String>) -> Self {
Self::new(MessageRole::User, content.into())
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::new(MessageRole::Assistant, content.into())
}
pub fn system(content: impl Into<String>) -> Self {
Self::new(MessageRole::System, content.into())
}
pub fn tool(
tool_call_id: impl Into<String>,
tool_name: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self {
role: MessageRole::Tool,
content: content.into(),
timestamp: chrono::Local::now(),
actions: Vec::new(),
thinking: None,
images: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
tool_name: Some(tool_name.into()),
}
}
fn new(role: MessageRole, content: String) -> Self {
Self {
role,
content,
timestamp: chrono::Local::now(),
actions: Vec::new(),
thinking: None,
images: None,
tool_calls: None,
tool_call_id: None,
tool_name: None,
}
}
pub fn with_images(mut self, images: Vec<String>) -> Self {
self.images = Some(images);
self
}
pub fn with_tool_calls(mut self, tool_calls: Vec<crate::models::tool_call::ToolCall>) -> Self {
self.tool_calls = if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
};
self
}
pub fn extract_thinking(text: &str) -> (Option<String>, String) {
if !text.contains("Thinking...") {
return (None, text.to_string());
}
if let Some(thinking_start) = text.find("Thinking...")
&& let Some(thinking_end) = text.find("...done thinking.")
{
let thinking_content_start = thinking_start + "Thinking...".len();
let thinking_text = text[thinking_content_start..thinking_end]
.trim()
.to_string();
let answer_start = thinking_end + "...done thinking.".len();
let answer_text = text[answer_start..].trim().to_string();
return (Some(thinking_text), answer_text);
}
if let Some(thinking_start) = text.find("Thinking...") {
let thinking_content_start = thinking_start + "Thinking...".len();
let thinking_text = text[thinking_content_start..].trim().to_string();
return (Some(thinking_text), String::new());
}
(None, text.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MessageRole {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone)]
pub struct ModelResponse {
pub content: String,
pub usage: Option<TokenUsage>,
pub model_name: String,
pub thinking: Option<String>,
pub tool_calls: Option<Vec<crate::models::tool_call::ToolCall>>,
}
#[derive(Debug, Clone)]
pub struct TokenUsage {
pub prompt_tokens: usize,
pub completion_tokens: usize,
pub total_tokens: usize,
}
pub type StreamCallback = Arc<dyn Fn(&str) + Send + Sync>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_role_equality() {
let user1 = MessageRole::User;
let user2 = MessageRole::User;
let assistant = MessageRole::Assistant;
assert_eq!(user1, user2, "User roles should be equal");
assert_ne!(user1, assistant, "Different roles should not be equal");
}
#[test]
fn test_chat_message_constructors() {
let user = ChatMessage::user("Hello!");
assert_eq!(user.role, MessageRole::User);
assert_eq!(user.content, "Hello!");
assert!(user.tool_calls.is_none());
let assistant = ChatMessage::assistant("Hi there");
assert_eq!(assistant.role, MessageRole::Assistant);
let system = ChatMessage::system("You are helpful");
assert_eq!(system.role, MessageRole::System);
let tool = ChatMessage::tool("call_1", "read_file", "file contents");
assert_eq!(tool.role, MessageRole::Tool);
assert_eq!(tool.tool_call_id, Some("call_1".to_string()));
assert_eq!(tool.tool_name, Some("read_file".to_string()));
}
#[test]
fn test_chat_message_builders() {
let msg = ChatMessage::user("test").with_images(vec!["base64data".to_string()]);
assert_eq!(msg.images, Some(vec!["base64data".to_string()]));
}
#[test]
fn test_token_usage_structure() {
let usage = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
assert_eq!(usage.prompt_tokens, 100);
assert_eq!(usage.completion_tokens, 50);
assert_eq!(usage.total_tokens, 150);
}
#[test]
fn test_model_response_creation() {
let usage = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let response = ModelResponse {
content: "Hello, world!".to_string(),
usage: Some(usage),
model_name: "ollama/tinyllama".to_string(),
thinking: None,
tool_calls: None,
};
assert_eq!(response.content, "Hello, world!");
assert!(response.usage.is_some());
assert_eq!(response.model_name, "ollama/tinyllama");
assert_eq!(response.usage.unwrap().total_tokens, 150);
assert!(response.tool_calls.is_none());
}
}