1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum Role {
16 System,
18 User,
20 Assistant,
22 Tool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Message {
30 pub role: Role,
32 pub content: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub tool_call_id: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub tool_calls: Option<Vec<ToolCall>>,
43}
44
45impl Message {
46 pub fn system(content: impl Into<String>) -> Self {
48 Self {
49 role: Role::System,
50 content: content.into(),
51 name: None,
52 tool_call_id: None,
53 tool_calls: None,
54 }
55 }
56
57 pub fn user(content: impl Into<String>) -> Self {
59 Self {
60 role: Role::User,
61 content: content.into(),
62 name: None,
63 tool_call_id: None,
64 tool_calls: None,
65 }
66 }
67
68 pub fn assistant(content: impl Into<String>) -> Self {
70 Self {
71 role: Role::Assistant,
72 content: content.into(),
73 name: None,
74 tool_call_id: None,
75 tool_calls: None,
76 }
77 }
78
79 pub fn assistant_with_tool_calls(
81 content: impl Into<String>,
82 tool_calls: Vec<ToolCall>,
83 ) -> Self {
84 Self {
85 role: Role::Assistant,
86 content: content.into(),
87 name: None,
88 tool_call_id: None,
89 tool_calls: Some(tool_calls),
90 }
91 }
92
93 pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
95 Self {
96 role: Role::Tool,
97 content: content.into(),
98 name: None,
99 tool_call_id: Some(tool_call_id.into()),
100 tool_calls: None,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ToolCall {
108 pub id: String,
110 pub name: String,
112 pub arguments: Value,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ModelResponse {
120 pub id: String,
122 pub content: Option<String>,
124 pub tool_calls: Vec<ToolCall>,
126 pub finish_reason: Option<String>,
128 pub created_at: DateTime<Utc>,
130}
131
132impl ModelResponse {
133 pub fn new_message(content: impl Into<String>) -> Self {
135 Self {
136 id: Uuid::new_v4().to_string(),
137 content: Some(content.into()),
138 tool_calls: vec![],
139 finish_reason: Some("stop".to_string()),
140 created_at: Utc::now(),
141 }
142 }
143
144 pub fn new_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
146 Self {
147 id: Uuid::new_v4().to_string(),
148 content: None,
149 tool_calls,
150 finish_reason: Some("tool_calls".to_string()),
151 created_at: Utc::now(),
152 }
153 }
154
155 pub fn has_tool_calls(&self) -> bool {
157 !self.tool_calls.is_empty()
158 }
159
160 pub fn has_content(&self) -> bool {
162 self.content.is_some() && !self.content.as_ref().unwrap().is_empty()
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(tag = "type")]
172pub enum RunItem {
173 Message(MessageItem),
175 ToolCall(ToolCallItem),
177 ToolOutput(ToolOutputItem),
179 Handoff(HandoffItem),
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct MessageItem {
186 pub id: String,
187 pub role: Role,
188 pub content: String,
189 pub created_at: DateTime<Utc>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ToolCallItem {
195 pub id: String,
196 pub tool_name: String,
197 pub arguments: Value,
198 pub created_at: DateTime<Utc>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ToolOutputItem {
204 pub id: String,
205 pub tool_call_id: String,
206 pub output: Value,
207 pub error: Option<String>,
208 pub created_at: DateTime<Utc>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct HandoffItem {
214 pub id: String,
215 pub from_agent: String,
216 pub to_agent: String,
217 pub reason: Option<String>,
218 pub created_at: DateTime<Utc>,
219}
220
221pub struct ItemHelpers;
223
224impl ItemHelpers {
225 pub fn to_messages(items: &[RunItem]) -> Vec<Message> {
228 let mut messages = Vec::new();
229 let mut pending_tool_calls: Vec<ToolCall> = Vec::new();
230
231 for (i, item) in items.iter().enumerate() {
232 match item {
233 RunItem::Message(msg) => {
234 if msg.role == Role::Assistant && !pending_tool_calls.is_empty() {
237 messages.push(Message {
238 role: msg.role,
239 content: msg.content.clone(),
240 name: None,
241 tool_call_id: None,
242 tool_calls: Some(pending_tool_calls.clone()),
243 });
244 pending_tool_calls.clear();
245 } else {
246 messages.push(Message {
247 role: msg.role,
248 content: msg.content.clone(),
249 name: None,
250 tool_call_id: None,
251 tool_calls: None,
252 });
253 }
254 }
255 RunItem::ToolCall(tool_call) => {
256 pending_tool_calls.push(ToolCall {
258 id: tool_call.id.clone(),
259 name: tool_call.tool_name.clone(),
260 arguments: tool_call.arguments.clone(),
261 });
262
263 if i + 1 < items.len() {
266 if let RunItem::ToolOutput(_) = &items[i + 1] {
267 if !pending_tool_calls.is_empty() {
269 messages.push(Message {
270 role: Role::Assistant,
271 content: String::new(),
272 name: None,
273 tool_call_id: None,
274 tool_calls: Some(pending_tool_calls.clone()),
275 });
276 pending_tool_calls.clear();
277 }
278 }
279 }
280 }
281 RunItem::ToolOutput(output) => {
282 let content = if let Some(error) = &output.error {
283 format!("Error: {}", error)
284 } else {
285 output.output.to_string()
286 };
287 messages.push(Message::tool(content, &output.tool_call_id));
288 }
289 _ => {}
290 }
291 }
292
293 if !pending_tool_calls.is_empty() {
295 messages.push(Message {
296 role: Role::Assistant,
297 content: String::new(),
298 name: None,
299 tool_call_id: None,
300 tool_calls: Some(pending_tool_calls),
301 });
302 }
303
304 messages
305 }
306
307 pub fn filter_by_type<T>(
309 items: &[RunItem],
310 filter_fn: impl Fn(&RunItem) -> Option<&T>,
311 ) -> Vec<&T> {
312 items.iter().filter_map(filter_fn).collect()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use pretty_assertions::assert_eq;
320
321 #[test]
322 fn test_message_creation() {
323 let sys_msg = Message::system("You are a helpful assistant");
324 assert_eq!(sys_msg.role, Role::System);
325 assert_eq!(sys_msg.content, "You are a helpful assistant");
326 assert!(sys_msg.tool_call_id.is_none());
327
328 let user_msg = Message::user("Hello");
329 assert_eq!(user_msg.role, Role::User);
330 assert_eq!(user_msg.content, "Hello");
331
332 let tool_msg = Message::tool("Result", "call_123");
333 assert_eq!(tool_msg.role, Role::Tool);
334 assert_eq!(tool_msg.tool_call_id, Some("call_123".to_string()));
335 }
336
337 #[test]
338 fn test_model_response() {
339 let response = ModelResponse::new_message("Hello, how can I help?");
340 assert!(response.has_content());
341 assert!(!response.has_tool_calls());
342 assert_eq!(response.content, Some("Hello, how can I help?".to_string()));
343
344 let tool_call = ToolCall {
345 id: "call_1".to_string(),
346 name: "get_weather".to_string(),
347 arguments: serde_json::json!({"city": "Tokyo"}),
348 };
349
350 let tool_response = ModelResponse::new_tool_calls(vec![tool_call]);
351 assert!(!tool_response.has_content());
352 assert!(tool_response.has_tool_calls());
353 assert_eq!(tool_response.tool_calls.len(), 1);
354 }
355
356 #[test]
357 fn test_run_items() {
358 let msg_item = RunItem::Message(MessageItem {
359 id: "msg_1".to_string(),
360 role: Role::User,
361 content: "Hello".to_string(),
362 created_at: Utc::now(),
363 });
364
365 let tool_item = RunItem::ToolCall(ToolCallItem {
366 id: "call_1".to_string(),
367 tool_name: "calculator".to_string(),
368 arguments: serde_json::json!({"operation": "add", "a": 1, "b": 2}),
369 created_at: Utc::now(),
370 });
371
372 let serialized = serde_json::to_string(&msg_item).unwrap();
374 assert!(serialized.contains("\"type\":\"Message\""));
375
376 let serialized_tool = serde_json::to_string(&tool_item).unwrap();
377 assert!(serialized_tool.contains("\"type\":\"ToolCall\""));
378 }
379
380 #[test]
381 fn test_item_helpers_to_messages() {
382 let items = vec![
383 RunItem::Message(MessageItem {
384 id: "1".to_string(),
385 role: Role::User,
386 content: "What's the weather?".to_string(),
387 created_at: Utc::now(),
388 }),
389 RunItem::ToolCall(ToolCallItem {
390 id: "2".to_string(),
391 tool_name: "get_weather".to_string(),
392 arguments: serde_json::json!({"city": "Paris"}),
393 created_at: Utc::now(),
394 }),
395 RunItem::ToolOutput(ToolOutputItem {
396 id: "3".to_string(),
397 tool_call_id: "2".to_string(),
398 output: serde_json::json!({"temp": 20, "condition": "sunny"}),
399 error: None,
400 created_at: Utc::now(),
401 }),
402 ];
403
404 let messages = ItemHelpers::to_messages(&items);
405 assert_eq!(messages.len(), 3); assert_eq!(messages[0].role, Role::User);
407 assert_eq!(messages[1].role, Role::Assistant); assert!(messages[1].tool_calls.is_some());
409 assert_eq!(messages[2].role, Role::Tool);
410 }
411
412 #[test]
413 fn test_handoff_item() {
414 let handoff = HandoffItem {
415 id: "handoff_1".to_string(),
416 from_agent: "triage".to_string(),
417 to_agent: "specialist".to_string(),
418 reason: Some("User needs technical support".to_string()),
419 created_at: Utc::now(),
420 };
421
422 let item = RunItem::Handoff(handoff.clone());
423 let serialized = serde_json::to_string(&item).unwrap();
424 assert!(serialized.contains("\"type\":\"Handoff\""));
425 assert!(serialized.contains("\"from_agent\":\"triage\""));
426 }
427
428 #[test]
429 fn test_role_serialization() {
430 let role = Role::Assistant;
431 let serialized = serde_json::to_string(&role).unwrap();
432 assert_eq!(serialized, "\"assistant\"");
433
434 let deserialized: Role = serde_json::from_str("\"system\"").unwrap();
435 assert_eq!(deserialized, Role::System);
436 }
437}