1use crate::contracts::runtime::tool_call::{Tool, ToolDescriptor, ToolResult};
4use crate::contracts::thread::{Message, Role, ToolCall};
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 apply_prompt_cache_hints(request: &mut ChatRequest) {
77 if let Some(pos) = request
79 .messages
80 .iter()
81 .rposition(|m| matches!(m.role, genai::chat::ChatRole::System))
82 {
83 let msg = request.messages.remove(pos);
84 request
85 .messages
86 .insert(pos, msg.with_options(genai::chat::CacheControl::Ephemeral));
87 }
88}
89
90pub fn user_message(content: impl Into<String>) -> Message {
92 Message::user(content)
93}
94
95pub fn assistant_message(content: impl Into<String>) -> Message {
97 Message::assistant(content)
98}
99
100pub fn assistant_tool_calls(content: impl Into<String>, calls: Vec<ToolCall>) -> Message {
102 Message::assistant_with_tool_calls(content, calls)
103}
104
105pub fn tool_response(call_id: impl Into<String>, result: &ToolResult) -> Message {
107 let content = serde_json::to_string(result)
108 .unwrap_or_else(|_| result.message.clone().unwrap_or_default());
109 Message::tool(call_id, content)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use serde_json::json;
116
117 struct MockTool;
119
120 #[async_trait::async_trait]
121 impl Tool for MockTool {
122 fn descriptor(&self) -> ToolDescriptor {
123 ToolDescriptor::new("mock", "Mock Tool", "A mock tool for testing").with_parameters(
124 json!({
125 "type": "object",
126 "properties": {
127 "input": { "type": "string" }
128 }
129 }),
130 )
131 }
132
133 async fn execute(
134 &self,
135 _args: serde_json::Value,
136 _ctx: &crate::contracts::ToolCallContext<'_>,
137 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
138 Ok(ToolResult::success("mock", json!({"result": "ok"})))
139 }
140 }
141
142 #[test]
143 fn test_to_genai_tool() {
144 let desc = ToolDescriptor::new("calc", "Calculator", "Calculate expressions")
145 .with_parameters(json!({"type": "object"}));
146
147 let genai_tool = to_genai_tool(&desc);
148
149 assert_eq!(genai_tool.name.to_string(), "calc");
150 assert_eq!(
151 genai_tool.description.as_deref(),
152 Some("Calculate expressions")
153 );
154 }
155
156 #[test]
157 fn test_to_chat_message_user() {
158 let msg = Message::user("Hello");
159 let chat_msg = to_chat_message(&msg);
160
161 assert!(
163 format!("{:?}", chat_msg).contains("User")
164 || format!("{:?}", chat_msg).to_lowercase().contains("user")
165 );
166 }
167
168 #[test]
169 fn test_to_chat_message_assistant() {
170 let msg = Message::assistant("Hi there");
171 let _chat_msg = to_chat_message(&msg);
172 }
174
175 #[test]
176 fn test_to_chat_message_assistant_with_tools() {
177 let calls = vec![ToolCall::new("call_1", "search", json!({"q": "rust"}))];
178 let msg = Message::assistant_with_tool_calls("Searching...", calls);
179 let _chat_msg = to_chat_message(&msg);
180 }
182
183 #[test]
184 fn test_to_chat_message_tool() {
185 let msg = Message::tool("call_1", "Result: 42");
186 let _chat_msg = to_chat_message(&msg);
187 }
189
190 #[test]
191 fn test_build_request_no_tools() {
192 let messages = vec![Message::user("Hello"), Message::assistant("Hi!")];
193
194 let request = build_request(&messages, &[]);
195
196 assert_eq!(request.messages.len(), 2);
197 assert!(request.tools.is_none());
198 }
199
200 #[test]
201 fn test_build_request_with_tools() {
202 let messages = vec![Message::user("Hello")];
203 let mock_tool = MockTool;
204 let tools: Vec<&dyn Tool> = vec![&mock_tool];
205
206 let request = build_request(&messages, &tools);
207
208 assert_eq!(request.messages.len(), 1);
209 assert!(request.tools.is_some());
210 assert_eq!(request.tools.as_ref().unwrap().len(), 1);
211 }
212
213 #[test]
214 fn test_tool_response_from_result() {
215 let result = ToolResult::success("calc", json!({"answer": 42}));
216 let msg = tool_response("call_1", &result);
217
218 assert_eq!(msg.role, Role::Tool);
219 assert_eq!(msg.tool_call_id.as_deref(), Some("call_1"));
220 assert!(msg.content.contains("42") || msg.content.contains("success"));
221 }
222
223 #[test]
226 fn test_to_chat_message_system() {
227 let msg = Message::system("You are a helpful assistant.");
228 let chat_msg = to_chat_message(&msg);
229
230 let debug_str = format!("{:?}", chat_msg);
231 assert!(debug_str.to_lowercase().contains("system") || !debug_str.is_empty());
232 }
233
234 #[test]
235 fn test_build_request_empty_messages() {
236 let messages: Vec<Message> = vec![];
237 let request = build_request(&messages, &[]);
238
239 assert!(request.messages.is_empty());
240 }
241
242 #[test]
243 fn test_build_request_multiple_tools() {
244 struct Tool1;
245 struct Tool2;
246 struct Tool3;
247
248 #[async_trait::async_trait]
249 impl Tool for Tool1 {
250 fn descriptor(&self) -> ToolDescriptor {
251 ToolDescriptor::new("tool1", "Tool 1", "First tool")
252 }
253 async fn execute(
254 &self,
255 _: serde_json::Value,
256 _: &crate::contracts::ToolCallContext<'_>,
257 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
258 Ok(ToolResult::success("tool1", json!({})))
259 }
260 }
261
262 #[async_trait::async_trait]
263 impl Tool for Tool2 {
264 fn descriptor(&self) -> ToolDescriptor {
265 ToolDescriptor::new("tool2", "Tool 2", "Second tool")
266 }
267 async fn execute(
268 &self,
269 _: serde_json::Value,
270 _: &crate::contracts::ToolCallContext<'_>,
271 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
272 Ok(ToolResult::success("tool2", json!({})))
273 }
274 }
275
276 #[async_trait::async_trait]
277 impl Tool for Tool3 {
278 fn descriptor(&self) -> ToolDescriptor {
279 ToolDescriptor::new("tool3", "Tool 3", "Third tool")
280 }
281 async fn execute(
282 &self,
283 _: serde_json::Value,
284 _: &crate::contracts::ToolCallContext<'_>,
285 ) -> Result<ToolResult, crate::contracts::runtime::tool_call::ToolError> {
286 Ok(ToolResult::success("tool3", json!({})))
287 }
288 }
289
290 let t1 = Tool1;
291 let t2 = Tool2;
292 let t3 = Tool3;
293 let tools: Vec<&dyn Tool> = vec![&t1, &t2, &t3];
294
295 let request = build_request(&[Message::user("test")], &tools);
296 assert_eq!(request.tools.as_ref().unwrap().len(), 3);
297 }
298
299 #[test]
300 fn test_to_chat_message_with_special_characters() {
301 let msg = Message::user(
302 "Hello! How are you?\n\nI have a question about \"quotes\" and 'apostrophes'.",
303 );
304 let _chat_msg = to_chat_message(&msg);
305 }
307
308 #[test]
309 fn test_to_chat_message_with_unicode() {
310 let msg = Message::user("你好世界! 🌍 Привет мир! مرحبا بالعالم");
311 let _chat_msg = to_chat_message(&msg);
312 }
314
315 #[test]
316 fn test_to_chat_message_with_empty_content() {
317 let msg = Message::user("");
318 let _chat_msg = to_chat_message(&msg);
319 }
321
322 #[test]
323 fn test_to_chat_message_with_very_long_content() {
324 let long_content = "a".repeat(100_000);
325 let msg = Message::user(&long_content);
326 let _chat_msg = to_chat_message(&msg);
327 }
329
330 #[test]
331 fn test_tool_response_from_error_result() {
332 let result = ToolResult::error("calc", "Division by zero");
333 let msg = tool_response("call_err", &result);
334
335 assert_eq!(msg.role, Role::Tool);
336 assert!(msg.content.contains("error") || msg.content.contains("Division"));
337 }
338
339 #[test]
340 fn test_tool_response_from_pending_result() {
341 let result = ToolResult::suspended("long_task", "Processing...");
342 let msg = tool_response("call_pending", &result);
343
344 assert_eq!(msg.role, Role::Tool);
345 assert!(msg.content.contains("pending") || msg.content.contains("Processing"));
346 }
347
348 #[test]
349 fn test_assistant_message_with_multiple_tool_calls() {
350 let calls = vec![
351 ToolCall::new("call_1", "search", json!({"q": "rust"})),
352 ToolCall::new("call_2", "calculate", json!({"expr": "1+1"})),
353 ToolCall::new("call_3", "format", json!({"text": "hello"})),
354 ];
355 let msg = assistant_tool_calls("I'll help you with multiple tasks.", calls);
356
357 assert_eq!(msg.role, Role::Assistant);
358 assert!(msg.tool_calls.is_some());
359 assert_eq!(msg.tool_calls.as_ref().unwrap().len(), 3);
360 }
361
362 #[test]
363 fn test_to_genai_tool_with_complex_schema() {
364 let desc =
365 ToolDescriptor::new("api", "API Call", "Make API requests").with_parameters(json!({
366 "type": "object",
367 "properties": {
368 "method": {
369 "type": "string",
370 "enum": ["GET", "POST", "PUT", "DELETE"]
371 },
372 "url": {
373 "type": "string",
374 "format": "uri"
375 },
376 "headers": {
377 "type": "object",
378 "additionalProperties": { "type": "string" }
379 },
380 "body": {
381 "type": "object"
382 }
383 },
384 "required": ["method", "url"]
385 }));
386
387 let genai_tool = to_genai_tool(&desc);
388 assert_eq!(genai_tool.name.to_string(), "api");
389 }
390
391 #[test]
392 fn test_build_request_conversation_history() {
393 let messages = vec![
395 Message::user("What is 2+2?"),
396 Message::assistant("2+2 equals 4."),
397 Message::user("And what is 4*4?"),
398 Message::assistant("4*4 equals 16."),
399 Message::user("Thanks!"),
400 Message::assistant("You're welcome!"),
401 ];
402
403 let request = build_request(&messages, &[]);
404 assert_eq!(request.messages.len(), 6);
405 }
406
407 #[test]
408 fn test_build_request_with_tool_responses() {
409 let messages = vec![
410 Message::user("Calculate 5*5"),
411 Message::assistant_with_tool_calls(
412 "I'll calculate that for you.",
413 vec![ToolCall::new("call_1", "calc", json!({"expr": "5*5"}))],
414 ),
415 Message::tool("call_1", r#"{"result": 25}"#),
416 Message::assistant("5*5 equals 25."),
417 ];
418
419 let request = build_request(&messages, &[]);
420 assert_eq!(request.messages.len(), 4);
421 }
422
423 #[test]
424 fn test_user_message_convenience() {
425 let msg = user_message("Hello");
426 assert_eq!(msg.role, Role::User);
427 assert_eq!(msg.content, "Hello");
428 }
429
430 #[test]
431 fn test_assistant_message_convenience() {
432 let msg = assistant_message("Hi there");
433 assert_eq!(msg.role, Role::Assistant);
434 assert_eq!(msg.content, "Hi there");
435 }
436
437 #[test]
438 fn apply_prompt_cache_hints_marks_last_system_message() {
439 let messages = vec![
440 Message::system("System prompt"),
441 Message::system("Session context"),
442 Message::user("Hello"),
443 Message::assistant("Hi!"),
444 ];
445 let mut request = build_request(&messages, &[]);
446 apply_prompt_cache_hints(&mut request);
447
448 let debug_0 = format!("{:?}", request.messages[0]);
451 let debug_1 = format!("{:?}", request.messages[1]);
452 assert!(
453 !debug_0.contains("Ephemeral"),
454 "first system message should not have cache hint"
455 );
456 assert!(
457 debug_1.contains("Ephemeral"),
458 "last system message should have Ephemeral cache hint"
459 );
460 assert_eq!(request.messages.len(), 4);
462 }
463
464 #[test]
465 fn apply_prompt_cache_hints_noop_without_system_messages() {
466 let messages = vec![Message::user("Hello"), Message::assistant("Hi!")];
467 let mut request = build_request(&messages, &[]);
468 let before = format!("{:?}", request.messages);
469 apply_prompt_cache_hints(&mut request);
470 let after = format!("{:?}", request.messages);
471 assert_eq!(
472 before, after,
473 "should be no-op when no system messages exist"
474 );
475 }
476
477 #[test]
478 fn apply_prompt_cache_hints_single_system_message() {
479 let messages = vec![Message::system("Only system"), Message::user("Hello")];
480 let mut request = build_request(&messages, &[]);
481 apply_prompt_cache_hints(&mut request);
482 let debug_0 = format!("{:?}", request.messages[0]);
483 assert!(
484 debug_0.contains("Ephemeral"),
485 "single system message should get cache hint"
486 );
487 }
488
489 #[test]
490 fn test_tool_response_with_complex_data() {
491 let result = ToolResult::success(
492 "api",
493 json!({
494 "status": 200,
495 "headers": {"Content-Type": "application/json"},
496 "body": {
497 "users": [
498 {"id": 1, "name": "Alice"},
499 {"id": 2, "name": "Bob"}
500 ]
501 }
502 }),
503 );
504
505 let msg = tool_response("call_api", &result);
506 assert!(msg.content.contains("users") || msg.content.contains("Alice"));
507 }
508}