1use crate::contracts::thread::{Message, Role, ToolCall};
4use crate::contracts::runtime::tool_call::{Tool, ToolDescriptor, ToolResult};
5use genai::chat::{ChatMessage, ChatRequest, MessageContent, ToolResponse};
6
7pub fn to_genai_tool(desc: &ToolDescriptor) -> genai::chat::Tool {
9 genai::chat::Tool::new(&desc.id)
10 .with_description(&desc.description)
11 .with_schema(desc.parameters.clone())
12}
13
14pub fn to_chat_message(msg: &Message) -> ChatMessage {
16 match msg.role {
17 Role::System => ChatMessage::system(&msg.content),
18 Role::User => ChatMessage::user(&msg.content),
19 Role::Assistant => {
20 if let Some(ref calls) = msg.tool_calls {
21 let genai_calls: Vec<genai::chat::ToolCall> = calls
23 .iter()
24 .map(|c| genai::chat::ToolCall {
25 call_id: c.id.clone(),
26 fn_name: c.name.clone(),
27 fn_arguments: c.arguments.clone(),
28 thought_signatures: None,
29 })
30 .collect();
31
32 let mut content = MessageContent::from(msg.content.as_str());
34 for call in genai_calls {
35 content.push(genai::chat::ContentPart::ToolCall(call));
36 }
37 ChatMessage::assistant(content)
38 } else {
39 ChatMessage::assistant(&msg.content)
40 }
41 }
42 Role::Tool => {
43 let call_id = msg.tool_call_id.as_deref().unwrap_or("");
44 let response = ToolResponse {
45 call_id: call_id.to_string(),
46 content: msg.content.clone(),
47 };
48 ChatMessage::from(response)
49 }
50 }
51}
52
53pub fn build_request(messages: &[Message], tools: &[&dyn Tool]) -> ChatRequest {
55 let chat_messages: Vec<ChatMessage> = messages.iter().map(to_chat_message).collect();
56
57 let genai_tools: Vec<genai::chat::Tool> = tools
58 .iter()
59 .map(|t| to_genai_tool(&t.descriptor()))
60 .collect();
61
62 let mut request = ChatRequest::new(chat_messages);
63
64 if !genai_tools.is_empty() {
65 request = request.with_tools(genai_tools);
66 }
67
68 request
69}
70
71pub fn user_message(content: impl Into<String>) -> Message {
73 Message::user(content)
74}
75
76pub fn assistant_message(content: impl Into<String>) -> Message {
78 Message::assistant(content)
79}
80
81pub fn assistant_tool_calls(content: impl Into<String>, calls: Vec<ToolCall>) -> Message {
83 Message::assistant_with_tool_calls(content, calls)
84}
85
86pub fn tool_response(call_id: impl Into<String>, result: &ToolResult) -> Message {
88 let content = serde_json::to_string(result)
89 .unwrap_or_else(|_| result.message.clone().unwrap_or_default());
90 Message::tool(call_id, content)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use serde_json::json;
97
98 struct MockTool;
100
101 #[async_trait::async_trait]
102 impl Tool for MockTool {
103 fn descriptor(&self) -> ToolDescriptor {
104 ToolDescriptor::new("mock", "Mock Tool", "A mock tool for testing").with_parameters(
105 json!({
106 "type": "object",
107 "properties": {
108 "input": { "type": "string" }
109 }
110 }),
111 )
112 }
113
114 async fn execute(
115 &self,
116 _args: serde_json::Value,
117 _ctx: &crate::contracts::ToolCallContext<'_>,
118 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
119 Ok(ToolResult::success("mock", json!({"result": "ok"})))
120 }
121 }
122
123 #[test]
124 fn test_to_genai_tool() {
125 let desc = ToolDescriptor::new("calc", "Calculator", "Calculate expressions")
126 .with_parameters(json!({"type": "object"}));
127
128 let genai_tool = to_genai_tool(&desc);
129
130 assert_eq!(genai_tool.name, "calc");
131 assert_eq!(
132 genai_tool.description.as_deref(),
133 Some("Calculate expressions")
134 );
135 }
136
137 #[test]
138 fn test_to_chat_message_user() {
139 let msg = Message::user("Hello");
140 let chat_msg = to_chat_message(&msg);
141
142 assert!(
144 format!("{:?}", chat_msg).contains("User")
145 || format!("{:?}", chat_msg).to_lowercase().contains("user")
146 );
147 }
148
149 #[test]
150 fn test_to_chat_message_assistant() {
151 let msg = Message::assistant("Hi there");
152 let _chat_msg = to_chat_message(&msg);
153 }
155
156 #[test]
157 fn test_to_chat_message_assistant_with_tools() {
158 let calls = vec![ToolCall::new("call_1", "search", json!({"q": "rust"}))];
159 let msg = Message::assistant_with_tool_calls("Searching...", calls);
160 let _chat_msg = to_chat_message(&msg);
161 }
163
164 #[test]
165 fn test_to_chat_message_tool() {
166 let msg = Message::tool("call_1", "Result: 42");
167 let _chat_msg = to_chat_message(&msg);
168 }
170
171 #[test]
172 fn test_build_request_no_tools() {
173 let messages = vec![Message::user("Hello"), Message::assistant("Hi!")];
174
175 let request = build_request(&messages, &[]);
176
177 assert_eq!(request.messages.len(), 2);
178 assert!(request.tools.is_none());
179 }
180
181 #[test]
182 fn test_build_request_with_tools() {
183 let messages = vec![Message::user("Hello")];
184 let mock_tool = MockTool;
185 let tools: Vec<&dyn Tool> = vec![&mock_tool];
186
187 let request = build_request(&messages, &tools);
188
189 assert_eq!(request.messages.len(), 1);
190 assert!(request.tools.is_some());
191 assert_eq!(request.tools.as_ref().unwrap().len(), 1);
192 }
193
194 #[test]
195 fn test_tool_response_from_result() {
196 let result = ToolResult::success("calc", json!({"answer": 42}));
197 let msg = tool_response("call_1", &result);
198
199 assert_eq!(msg.role, Role::Tool);
200 assert_eq!(msg.tool_call_id.as_deref(), Some("call_1"));
201 assert!(msg.content.contains("42") || msg.content.contains("success"));
202 }
203
204 #[test]
207 fn test_to_chat_message_system() {
208 let msg = Message::system("You are a helpful assistant.");
209 let chat_msg = to_chat_message(&msg);
210
211 let debug_str = format!("{:?}", chat_msg);
212 assert!(debug_str.to_lowercase().contains("system") || !debug_str.is_empty());
213 }
214
215 #[test]
216 fn test_build_request_empty_messages() {
217 let messages: Vec<Message> = vec![];
218 let request = build_request(&messages, &[]);
219
220 assert!(request.messages.is_empty());
221 }
222
223 #[test]
224 fn test_build_request_multiple_tools() {
225 struct Tool1;
226 struct Tool2;
227 struct Tool3;
228
229 #[async_trait::async_trait]
230 impl Tool for Tool1 {
231 fn descriptor(&self) -> ToolDescriptor {
232 ToolDescriptor::new("tool1", "Tool 1", "First tool")
233 }
234 async fn execute(
235 &self,
236 _: serde_json::Value,
237 _: &crate::contracts::ToolCallContext<'_>,
238 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
239 Ok(ToolResult::success("tool1", json!({})))
240 }
241 }
242
243 #[async_trait::async_trait]
244 impl Tool for Tool2 {
245 fn descriptor(&self) -> ToolDescriptor {
246 ToolDescriptor::new("tool2", "Tool 2", "Second tool")
247 }
248 async fn execute(
249 &self,
250 _: serde_json::Value,
251 _: &crate::contracts::ToolCallContext<'_>,
252 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
253 Ok(ToolResult::success("tool2", json!({})))
254 }
255 }
256
257 #[async_trait::async_trait]
258 impl Tool for Tool3 {
259 fn descriptor(&self) -> ToolDescriptor {
260 ToolDescriptor::new("tool3", "Tool 3", "Third tool")
261 }
262 async fn execute(
263 &self,
264 _: serde_json::Value,
265 _: &crate::contracts::ToolCallContext<'_>,
266 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
267 Ok(ToolResult::success("tool3", json!({})))
268 }
269 }
270
271 let t1 = Tool1;
272 let t2 = Tool2;
273 let t3 = Tool3;
274 let tools: Vec<&dyn Tool> = vec![&t1, &t2, &t3];
275
276 let request = build_request(&[Message::user("test")], &tools);
277 assert_eq!(request.tools.as_ref().unwrap().len(), 3);
278 }
279
280 #[test]
281 fn test_to_chat_message_with_special_characters() {
282 let msg = Message::user(
283 "Hello! How are you?\n\nI have a question about \"quotes\" and 'apostrophes'.",
284 );
285 let _chat_msg = to_chat_message(&msg);
286 }
288
289 #[test]
290 fn test_to_chat_message_with_unicode() {
291 let msg = Message::user("你好世界! 🌍 Привет мир! مرحبا بالعالم");
292 let _chat_msg = to_chat_message(&msg);
293 }
295
296 #[test]
297 fn test_to_chat_message_with_empty_content() {
298 let msg = Message::user("");
299 let _chat_msg = to_chat_message(&msg);
300 }
302
303 #[test]
304 fn test_to_chat_message_with_very_long_content() {
305 let long_content = "a".repeat(100_000);
306 let msg = Message::user(&long_content);
307 let _chat_msg = to_chat_message(&msg);
308 }
310
311 #[test]
312 fn test_tool_response_from_error_result() {
313 let result = ToolResult::error("calc", "Division by zero");
314 let msg = tool_response("call_err", &result);
315
316 assert_eq!(msg.role, Role::Tool);
317 assert!(msg.content.contains("error") || msg.content.contains("Division"));
318 }
319
320 #[test]
321 fn test_tool_response_from_pending_result() {
322 let result = ToolResult::suspended("long_task", "Processing...");
323 let msg = tool_response("call_pending", &result);
324
325 assert_eq!(msg.role, Role::Tool);
326 assert!(msg.content.contains("pending") || msg.content.contains("Processing"));
327 }
328
329 #[test]
330 fn test_assistant_message_with_multiple_tool_calls() {
331 let calls = vec![
332 ToolCall::new("call_1", "search", json!({"q": "rust"})),
333 ToolCall::new("call_2", "calculate", json!({"expr": "1+1"})),
334 ToolCall::new("call_3", "format", json!({"text": "hello"})),
335 ];
336 let msg = assistant_tool_calls("I'll help you with multiple tasks.", calls);
337
338 assert_eq!(msg.role, Role::Assistant);
339 assert!(msg.tool_calls.is_some());
340 assert_eq!(msg.tool_calls.as_ref().unwrap().len(), 3);
341 }
342
343 #[test]
344 fn test_to_genai_tool_with_complex_schema() {
345 let desc =
346 ToolDescriptor::new("api", "API Call", "Make API requests").with_parameters(json!({
347 "type": "object",
348 "properties": {
349 "method": {
350 "type": "string",
351 "enum": ["GET", "POST", "PUT", "DELETE"]
352 },
353 "url": {
354 "type": "string",
355 "format": "uri"
356 },
357 "headers": {
358 "type": "object",
359 "additionalProperties": { "type": "string" }
360 },
361 "body": {
362 "type": "object"
363 }
364 },
365 "required": ["method", "url"]
366 }));
367
368 let genai_tool = to_genai_tool(&desc);
369 assert_eq!(genai_tool.name, "api");
370 }
371
372 #[test]
373 fn test_build_request_conversation_history() {
374 let messages = vec![
376 Message::user("What is 2+2?"),
377 Message::assistant("2+2 equals 4."),
378 Message::user("And what is 4*4?"),
379 Message::assistant("4*4 equals 16."),
380 Message::user("Thanks!"),
381 Message::assistant("You're welcome!"),
382 ];
383
384 let request = build_request(&messages, &[]);
385 assert_eq!(request.messages.len(), 6);
386 }
387
388 #[test]
389 fn test_build_request_with_tool_responses() {
390 let messages = vec![
391 Message::user("Calculate 5*5"),
392 Message::assistant_with_tool_calls(
393 "I'll calculate that for you.",
394 vec![ToolCall::new("call_1", "calc", json!({"expr": "5*5"}))],
395 ),
396 Message::tool("call_1", r#"{"result": 25}"#),
397 Message::assistant("5*5 equals 25."),
398 ];
399
400 let request = build_request(&messages, &[]);
401 assert_eq!(request.messages.len(), 4);
402 }
403
404 #[test]
405 fn test_user_message_convenience() {
406 let msg = user_message("Hello");
407 assert_eq!(msg.role, Role::User);
408 assert_eq!(msg.content, "Hello");
409 }
410
411 #[test]
412 fn test_assistant_message_convenience() {
413 let msg = assistant_message("Hi there");
414 assert_eq!(msg.role, Role::Assistant);
415 assert_eq!(msg.content, "Hi there");
416 }
417
418 #[test]
419 fn test_tool_response_with_complex_data() {
420 let result = ToolResult::success(
421 "api",
422 json!({
423 "status": 200,
424 "headers": {"Content-Type": "application/json"},
425 "body": {
426 "users": [
427 {"id": 1, "name": "Alice"},
428 {"id": 2, "name": "Bob"}
429 ]
430 }
431 }),
432 );
433
434 let msg = tool_response("call_api", &result);
435 assert!(msg.content.contains("users") || msg.content.contains("Alice"));
436 }
437}