use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub name: String,
pub content: String,
#[serde(default)]
pub is_error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Message {
pub fn system(text: impl Into<String>) -> Self {
Self {
role: Role::System,
content: Some(text.into()),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Some(text.into()),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
pub fn assistant_text(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Some(text.into()),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
pub fn assistant_tool_calls(calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: None,
tool_calls: calls,
tool_call_id: None,
name: None,
}
}
pub fn tool_response(result: ToolResult) -> Self {
Self {
role: Role::Tool,
content: Some(result.content),
tool_calls: Vec::new(),
tool_call_id: Some(result.tool_call_id),
name: Some(result.name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assistant_with_tool_calls_serializes_without_content() {
let m = Message::assistant_tool_calls(vec![ToolCall {
id: "call_1".into(),
name: "bash".into(),
arguments: serde_json::json!({"cmd": "ls"}),
}]);
let v = serde_json::to_value(&m).unwrap();
assert_eq!(v["role"], "assistant");
assert!(v.get("content").is_none(), "content should be omitted");
assert_eq!(v["tool_calls"][0]["name"], "bash");
}
}