@@ -2,13 +2,15 @@ use std::vec;
22
33use derive_more:: derive:: Display ;
44use derive_setters:: Setters ;
5+ use forge_json_repair:: coerce_to_schema;
56use serde:: { Deserialize , Serialize } ;
7+ use strum:: IntoEnumIterator ;
68
79use super :: response:: { ExtraContent , FunctionCall , ToolCall } ;
810use super :: tool_choice:: { FunctionType , ToolChoice } ;
911use crate :: domain:: {
10- Context , ContextMessage , ModelId , ToolCallFull , ToolCallId , ToolDefinition , ToolName ,
11- ToolResult , ToolValue ,
12+ Context , ContextMessage , ModelId , ToolCallFull , ToolCallId , ToolCatalog , ToolDefinition ,
13+ ToolName , ToolResult , ToolValue ,
1214} ;
1315use crate :: dto:: openai:: ReasoningDetail ;
1416
@@ -406,17 +408,30 @@ impl From<Context> for Request {
406408 }
407409}
408410
411+ fn serialize_tool_call_arguments ( tool_call : & ToolCallFull ) -> String {
412+ let serialized_arguments = || serde_json:: to_string ( & tool_call. arguments ) . unwrap ( ) ;
413+
414+ let Ok ( parsed_arguments) = tool_call. arguments . parse ( ) else {
415+ return serialized_arguments ( ) ;
416+ } ;
417+
418+ let normalized_arguments = ToolCatalog :: iter ( )
419+ . find ( |tool| tool. definition ( ) . name == tool_call. name )
420+ . map ( |tool| coerce_to_schema ( parsed_arguments. clone ( ) , & tool. definition ( ) . input_schema ) )
421+ . unwrap_or ( parsed_arguments) ;
422+
423+ serde_json:: to_string ( & normalized_arguments) . unwrap_or_else ( |_| serialized_arguments ( ) )
424+ }
425+
409426impl From < ToolCallFull > for ToolCall {
410427 fn from ( value : ToolCallFull ) -> Self {
428+ let arguments = serialize_tool_call_arguments ( & value) ;
411429 let extra_content = value. thought_signature . map ( ExtraContent :: from) ;
412430
413431 Self {
414432 id : value. call_id ,
415433 r#type : FunctionType ,
416- function : FunctionCall {
417- arguments : serde_json:: to_string ( & value. arguments ) . unwrap ( ) ,
418- name : Some ( value. name ) ,
419- } ,
434+ function : FunctionCall { arguments, name : Some ( value. name ) } ,
420435 extra_content,
421436 }
422437 }
@@ -681,7 +696,8 @@ mod tests {
681696 }
682697
683698 use forge_domain:: {
684- ContextMessage , Role , TextMessage , ToolCallFull , ToolCallId , ToolName , ToolResult ,
699+ ContextMessage , Role , TextMessage , ToolCallFull , ToolCallId , ToolCatalog , ToolName ,
700+ ToolResult ,
685701 } ;
686702 use insta:: assert_json_snapshot;
687703
@@ -731,6 +747,43 @@ mod tests {
731747 assert_json_snapshot ! ( router_message) ;
732748 }
733749
750+ #[ test]
751+ fn test_assistant_message_with_dump_style_tool_call_arguments_conversion ( ) {
752+ let fixture = ToolCatalog :: tool_call_patch (
753+ "/tmp/file.txt" ,
754+ "new text" ,
755+ "old text" ,
756+ false ,
757+ )
758+ . arguments (
759+ serde_json:: from_str :: < forge_domain:: ToolCallArguments > (
760+ r#""{\"file_path\":\"/tmp/file.txt\",\"old_string\":\"old text\",\"new_string\":\"new text\",\"replace_all\":false}""# ,
761+ )
762+ . unwrap ( ) ,
763+ )
764+ . call_id ( ToolCallId :: new ( "123" ) ) ;
765+
766+ let assistant_message = ContextMessage :: Text (
767+ TextMessage :: new ( Role :: Assistant , "Using tool" )
768+ . tool_calls ( vec ! [ fixture] )
769+ . model ( ModelId :: new ( "gpt-3.5-turbo" ) ) ,
770+ ) ;
771+ let actual = Message :: from ( assistant_message) ;
772+ let actual =
773+ serde_json:: to_value ( actual. tool_calls . expect ( "Tool calls should exist" ) ) . unwrap ( ) ;
774+ let expected = serde_json:: json!( [
775+ {
776+ "id" : "123" ,
777+ "type" : "function" ,
778+ "function" : {
779+ "arguments" : "{\" file_path\" :\" /tmp/file.txt\" ,\" new_string\" :\" new text\" ,\" old_string\" :\" old text\" ,\" replace_all\" :false}" ,
780+ "name" : "patch"
781+ }
782+ }
783+ ] ) ;
784+ assert_eq ! ( actual, expected) ;
785+ }
786+
734787 #[ test]
735788 fn test_tool_message_conversion ( ) {
736789 let tool_result = ToolResult :: new ( ToolName :: new ( "test_tool" ) )
0 commit comments