1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(rename_all = "lowercase")]
5pub enum Role {
6 System,
7 User,
8 Assistant,
9 Tool,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ToolCall {
14 pub id: String,
15 pub name: String,
16 pub arguments: serde_json::Value,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolResult {
22 pub tool_call_id: String,
23 pub name: String,
24 pub content: String,
25 #[serde(default)]
26 pub is_error: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Message {
36 pub role: Role,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub content: Option<String>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub tool_calls: Vec<ToolCall>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub tool_call_id: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub name: Option<String>,
45}
46
47impl Message {
48 pub fn system(text: impl Into<String>) -> Self {
49 Self {
50 role: Role::System,
51 content: Some(text.into()),
52 tool_calls: Vec::new(),
53 tool_call_id: None,
54 name: None,
55 }
56 }
57
58 pub fn user(text: impl Into<String>) -> Self {
59 Self {
60 role: Role::User,
61 content: Some(text.into()),
62 tool_calls: Vec::new(),
63 tool_call_id: None,
64 name: None,
65 }
66 }
67
68 pub fn assistant_text(text: impl Into<String>) -> Self {
69 Self {
70 role: Role::Assistant,
71 content: Some(text.into()),
72 tool_calls: Vec::new(),
73 tool_call_id: None,
74 name: None,
75 }
76 }
77
78 pub fn assistant_tool_calls(calls: Vec<ToolCall>) -> Self {
79 Self {
80 role: Role::Assistant,
81 content: None,
82 tool_calls: calls,
83 tool_call_id: None,
84 name: None,
85 }
86 }
87
88 pub fn tool_response(result: ToolResult) -> Self {
89 Self {
90 role: Role::Tool,
91 content: Some(result.content),
92 tool_calls: Vec::new(),
93 tool_call_id: Some(result.tool_call_id),
94 name: Some(result.name),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn assistant_with_tool_calls_serializes_without_content() {
105 let m = Message::assistant_tool_calls(vec![ToolCall {
106 id: "call_1".into(),
107 name: "bash".into(),
108 arguments: serde_json::json!({"cmd": "ls"}),
109 }]);
110 let v = serde_json::to_value(&m).unwrap();
111 assert_eq!(v["role"], "assistant");
112 assert!(v.get("content").is_none(), "content should be omitted");
113 assert_eq!(v["tool_calls"][0]["name"], "bash");
114 }
115}