1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::id::ToolCallId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11#[non_exhaustive]
12pub enum Role {
13 System,
15 #[default]
17 User,
18 Assistant,
20 Tool,
22 Developer,
24}
25
26impl Role {
27 #[must_use]
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::System => "system",
32 Self::User => "user",
33 Self::Assistant => "assistant",
34 Self::Tool => "tool",
35 Self::Developer => "developer",
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43#[non_exhaustive]
44pub enum ImageMime {
45 #[default]
47 Jpeg,
48 Png,
50 Gif,
52 WebP,
54}
55
56impl ImageMime {
57 #[must_use]
59 pub const fn as_str(self) -> &'static str {
60 match self {
61 Self::Jpeg => "image/jpeg",
62 Self::Png => "image/png",
63 Self::Gif => "image/gif",
64 Self::WebP => "image/webp",
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(tag = "type", rename_all = "snake_case")]
72#[non_exhaustive]
73pub enum ContentPart {
74 Text {
76 text: String,
78 },
79 Image {
81 mime: ImageMime,
83 url: String,
85 },
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[allow(
91 clippy::derive_partial_eq_without_eq,
92 reason = "serde_json::Value is not Eq"
93)]
94pub struct ToolCall {
95 pub id: ToolCallId,
97 pub name: String,
99 pub arguments: Value,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[allow(
106 clippy::derive_partial_eq_without_eq,
107 reason = "contains ToolCall with JSON Value"
108)]
109pub struct Message {
110 pub role: Role,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub content: Option<String>,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub parts: Vec<ContentPart>,
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub tool_calls: Vec<ToolCall>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub tool_call_id: Option<ToolCallId>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub name: Option<String>,
127}
128
129impl Message {
130 #[must_use]
132 pub fn system(content: impl Into<String>) -> Self {
133 Self {
134 role: Role::System,
135 content: Some(content.into()),
136 parts: Vec::new(),
137 tool_calls: Vec::new(),
138 tool_call_id: None,
139 name: None,
140 }
141 }
142
143 #[must_use]
145 pub fn user(content: impl Into<String>) -> Self {
146 Self {
147 role: Role::User,
148 content: Some(content.into()),
149 parts: Vec::new(),
150 tool_calls: Vec::new(),
151 tool_call_id: None,
152 name: None,
153 }
154 }
155
156 #[must_use]
158 pub fn assistant(content: impl Into<String>) -> Self {
159 Self {
160 role: Role::Assistant,
161 content: Some(content.into()),
162 parts: Vec::new(),
163 tool_calls: Vec::new(),
164 tool_call_id: None,
165 name: None,
166 }
167 }
168
169 #[must_use]
171 pub const fn assistant_tools(tool_calls: Vec<ToolCall>) -> Self {
172 Self {
173 role: Role::Assistant,
174 content: None,
175 parts: Vec::new(),
176 tool_calls,
177 tool_call_id: None,
178 name: None,
179 }
180 }
181
182 #[must_use]
184 pub fn tool_result(
185 tool_call_id: ToolCallId,
186 name: impl Into<String>,
187 content: impl Into<String>,
188 ) -> Self {
189 Self {
190 role: Role::Tool,
191 content: Some(content.into()),
192 parts: Vec::new(),
193 tool_calls: Vec::new(),
194 tool_call_id: Some(tool_call_id),
195 name: Some(name.into()),
196 }
197 }
198
199 #[must_use]
201 pub fn text(&self) -> String {
202 if let Some(content) = &self.content {
203 return content.clone();
204 }
205 self.parts
206 .iter()
207 .filter_map(|p| match p {
208 ContentPart::Text { text } => Some(text.as_str()),
209 ContentPart::Image { .. } => None,
210 })
211 .collect::<Vec<_>>()
212 .join("")
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn round_trip_user() {
222 let m = Message::user("hello");
223 let json = serde_json::to_string(&m).expect("ser");
224 let back: Message = serde_json::from_str(&json).expect("de");
225 assert_eq!(back.role, Role::User);
226 assert_eq!(back.text(), "hello");
227 }
228
229 #[test]
230 fn tool_call_message() {
231 let id = ToolCallId::new("call_1").expect("id");
232 let m = Message::assistant_tools(vec![ToolCall {
233 id: id.clone(),
234 name: "add".into(),
235 arguments: serde_json::json!({"a":1,"b":2}),
236 }]);
237 assert_eq!(m.tool_calls.len(), 1);
238 assert_eq!(m.tool_calls.first().map(|c| &c.id), Some(&id));
239 }
240}