@@ -25,43 +25,60 @@ package toolconverters
2525
2626import (
2727 "encoding/json"
28+ "fmt"
2829
2930 "github.com/ansys/aali-sharedtypes/pkg/logging"
3031 "github.com/ansys/aali-sharedtypes/pkg/sharedtypes"
31- "github.com/anthropics/anthropic-sdk-go"
32- "github.com/anthropics/anthropic-sdk-go/packages/param"
33- "github.com/openai/openai-go"
32+ "github.com/openai/openai-go/v2"
33+ "github.com/openai/openai-go/v2/shared"
3434)
3535
3636// ConvertMCPToOpenAIFormat converts MCP tools to OpenAI function calling format.
37+ //
38+ // Parameters:
39+ //
40+ // ctx: The logging context map.
41+ // mcpTools: Array of MCP tool definitions.
42+ //
43+ // Returns:
44+ //
45+ // []openai.ChatCompletionToolUnionParam: OpenAI formatted tools.
46+ // []error: List of errors for tools that were skipped during conversion.
3747func ConvertMCPToOpenAIFormat (
3848 ctx * logging.ContextMap ,
3949 mcpTools []interface {},
40- ) []openai.ChatCompletionToolParam {
41- var openaiTools []openai.ChatCompletionToolParam
50+ ) ([]openai.ChatCompletionToolUnionParam , []error ) {
51+ var openaiTools []openai.ChatCompletionToolUnionParam
52+ var errors []error
4253
4354 for i , mcpTool := range mcpTools {
4455 // Convert interface{} to map for field access
4556 toolMap , ok := mcpTool .(map [string ]interface {})
4657 if ! ok {
47- logging .Log .Warnf (ctx , "Skipping tool %d: not a valid object" , i )
58+ toolJSON , _ := json .Marshal (mcpTool )
59+ err := fmt .Errorf ("tool at index %d is not a valid object, got type %T, value: %s" , i , mcpTool , string (toolJSON ))
60+ errors = append (errors , err )
61+ logging .Log .Errorf (ctx , "Skipping tool %d: not a valid object (type: %T, value: %s)" , i , mcpTool , string (toolJSON ))
4862 continue
4963 }
5064
5165 // Extract required fields
5266 name , nameOk := toolMap ["name" ].(string )
5367 if ! nameOk || name == "" {
54- logging .Log .Warnf (ctx , "Skipping tool %d: missing or invalid 'name' field" , i )
68+ toolJSON , _ := json .Marshal (toolMap )
69+ err := fmt .Errorf ("tool at index %d is missing or has invalid 'name' field, tool data: %s" , i , string (toolJSON ))
70+ errors = append (errors , err )
71+ logging .Log .Errorf (ctx , "Skipping tool %d: missing or invalid 'name' field, tool data: %s" , i , string (toolJSON ))
5572 continue
5673 }
5774
58- // Extract description (optional)
75+ // Extract description (
5976 description , _ := toolMap ["description" ].(string )
6077 if description == "" {
6178 logging .Log .Warnf (ctx , "Tool '%s': missing description (recommended for better LLM understanding)" , name )
6279 }
6380
64- // Extract inputSchema (optional)
81+ // Extract inputSchema
6582 inputSchema , schemaOk := toolMap ["inputSchema" ].(map [string ]interface {})
6683 if ! schemaOk {
6784 logging .Log .Warnf (ctx , "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)" , name )
@@ -72,218 +89,70 @@ func ConvertMCPToOpenAIFormat(
7289 }
7390 }
7491
75- // Convert to OpenAI format using openai.F() wrappers
76- openaiTool := openai.ChatCompletionToolParam {
77- Type : openai .F (openai .ChatCompletionToolTypeFunction ),
78- Function : openai .F (openai.FunctionDefinitionParam {
79- Name : openai .String (name ),
80- Description : openai .String (description ),
81- Parameters : openai .F (openai .FunctionParameters (inputSchema )),
82- }),
92+ // Convert to OpenAI format
93+ functionDef := shared.FunctionDefinitionParam {
94+ Name : name ,
95+ Description : openai .String (description ),
96+ Parameters : shared .FunctionParameters (inputSchema ),
8397 }
8498
99+ openaiTool := openai .ChatCompletionFunctionTool (functionDef )
85100 openaiTools = append (openaiTools , openaiTool )
86101 logging .Log .Debugf (ctx , "Converted MCP tool '%s' to OpenAI format" , name )
87102 }
88103
89104 if len (openaiTools ) > 0 {
90105 logging .Log .Infof (ctx , "Converted %d MCP tools to OpenAI format" , len (openaiTools ))
91- } else if len (mcpTools ) > 0 {
92- logging .Log .Warnf (ctx , "No valid tools converted from %d MCP tools provided" , len (mcpTools ))
93106 }
94-
95- return openaiTools
96- }
97-
98- // ConvertMCPToMistralFormat converts MCP tools to Mistral function calling format.
99- func ConvertMCPToMistralFormat (
100- ctx * logging.ContextMap ,
101- mcpTools []interface {},
102- ) []map [string ]interface {} {
103- var mistralTools []map [string ]interface {}
104-
105- for i , mcpTool := range mcpTools {
106- // Convert interface{} to map for field access
107- toolMap , ok := mcpTool .(map [string ]interface {})
108- if ! ok {
109- logging .Log .Warnf (ctx , "Skipping tool %d: not a valid object" , i )
110- continue
111- }
112-
113- // Extract required fields
114- name , nameOk := toolMap ["name" ].(string )
115- if ! nameOk || name == "" {
116- logging .Log .Warnf (ctx , "Skipping tool %d: missing or invalid 'name' field" , i )
117- continue
118- }
119-
120- // Extract description (optional)
121- description , _ := toolMap ["description" ].(string )
122- if description == "" {
123- logging .Log .Warnf (ctx , "Tool '%s': missing description (recommended for better LLM understanding)" , name )
124- }
125-
126- // Extract inputSchema (optional)
127- inputSchema , schemaOk := toolMap ["inputSchema" ].(map [string ]interface {})
128- if ! schemaOk {
129- logging .Log .Warnf (ctx , "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)" , name )
130- // Create empty schema as fallback
131- inputSchema = map [string ]interface {}{
132- "type" : "object" ,
133- "properties" : map [string ]interface {}{},
134- }
135- }
136-
137- // Convert to Mistral format (OpenAI-compatible)
138- mistralTool := map [string ]interface {}{
139- "type" : "function" ,
140- "function" : map [string ]interface {}{
141- "name" : name ,
142- "description" : description ,
143- "parameters" : inputSchema ,
144- },
145- }
146-
147- mistralTools = append (mistralTools , mistralTool )
148- logging .Log .Debugf (ctx , "Converted MCP tool '%s' to Mistral format" , name )
149- }
150-
151- if len (mistralTools ) > 0 {
152- logging .Log .Infof (ctx , "Converted %d MCP tools to Mistral format" , len (mistralTools ))
153- } else if len (mcpTools ) > 0 {
154- logging .Log .Warnf (ctx , "No valid tools converted from %d MCP tools provided" , len (mcpTools ))
107+ if len (errors ) > 0 {
108+ logging .Log .Errorf (ctx , "Failed to convert %d out of %d MCP tools (see detailed errors above)" , len (errors ), len (mcpTools ))
155109 }
156110
157- return mistralTools
111+ return openaiTools , errors
158112}
159113
160- // ConvertMCPToAnthropicFormat converts MCP tools to Anthropic function calling format.
161- func ConvertMCPToAnthropicFormat (
162- ctx * logging.ContextMap ,
163- mcpTools []interface {},
164- ) []anthropic.ToolParam {
165- var anthropicTools []anthropic.ToolParam
166-
167- for i , mcpTool := range mcpTools {
168- // Convert interface{} to map for field access
169- toolMap , ok := mcpTool .(map [string ]interface {})
170- if ! ok {
171- logging .Log .Warnf (ctx , "Skipping tool %d: not a valid object" , i )
172- continue
173- }
174-
175- // Extract required fields
176- name , nameOk := toolMap ["name" ].(string )
177- if ! nameOk || name == "" {
178- logging .Log .Warnf (ctx , "Skipping tool %d: missing or invalid 'name' field" , i )
179- continue
180- }
181-
182- // Extract description (optional)
183- description , _ := toolMap ["description" ].(string )
184- if description == "" {
185- logging .Log .Warnf (ctx , "Tool '%s': missing description (recommended for better LLM understanding)" , name )
186- }
187-
188- // Extract inputSchema (optional)
189- inputSchema , schemaOk := toolMap ["inputSchema" ].(map [string ]interface {})
190- if ! schemaOk {
191- logging .Log .Warnf (ctx , "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)" , name )
192- // Create empty schema as fallback
193- inputSchema = map [string ]interface {}{
194- "type" : "object" ,
195- "properties" : map [string ]interface {}{},
196- }
197- }
198-
199- // Extract required fields from inputSchema
200- var required []string
201- if req , ok := inputSchema ["required" ].([]interface {}); ok {
202- for _ , r := range req {
203- if rStr , ok := r .(string ); ok {
204- required = append (required , rStr )
205- }
206- }
207- }
208-
209- // Extract properties from inputSchema
210- properties , _ := inputSchema ["properties" ]
211-
212- // Convert to Anthropic format
213- anthropicTool := anthropic.ToolParam {
214- Name : name ,
215- InputSchema : anthropic.ToolInputSchemaParam {
216- Type : "object" ,
217- Properties : properties ,
218- Required : required ,
219- },
220- }
221-
222- // Add description if present
223- if description != "" {
224- anthropicTool .Description = param .NewOpt (description )
225- }
226-
227- anthropicTools = append (anthropicTools , anthropicTool )
228- logging .Log .Debugf (ctx , "Converted MCP tool '%s' to Anthropic format" , name )
229- }
230-
231- if len (anthropicTools ) > 0 {
232- logging .Log .Infof (ctx , "Converted %d MCP tools to Anthropic format" , len (anthropicTools ))
233- } else if len (mcpTools ) > 0 {
234- logging .Log .Warnf (ctx , "No valid tools converted from %d MCP tools provided" , len (mcpTools ))
235- }
236-
237- return anthropicTools
238- }
239-
240- // ConvertAnthropicToolCallsToSharedTypes converts Anthropic ToolUseBlock responses to shared ToolCall format.
241- func ConvertAnthropicToolCallsToSharedTypes (
242- ctx * logging.ContextMap ,
243- toolUseBlocks []anthropic.ToolUseBlock ,
244- ) []sharedtypes.ToolCall {
245- var toolCalls []sharedtypes.ToolCall
246-
247- for _ , block := range toolUseBlocks {
248- // Unmarshal json.RawMessage to map[string]interface{}
249- var input map [string ]interface {}
250- if err := json .Unmarshal (block .Input , & input ); err != nil {
251- logging .Log .Warnf (ctx , "Failed to parse tool input for %s: %v" , block .Name , err )
252- input = make (map [string ]interface {})
253- }
254-
255- toolCalls = append (toolCalls , sharedtypes.ToolCall {
256- ID : block .ID ,
257- Type : "tool_use" ,
258- Name : block .Name ,
259- Input : input ,
260- })
261- }
262-
263- if len (toolCalls ) > 0 {
264- logging .Log .Infof (ctx , "Converted %d Anthropic tool calls to shared format" , len (toolCalls ))
265- }
266-
267- return toolCalls
268- }
269-
270- // ConvertOpenAIToolCallsToSharedTypes converts OpenAI ChatCompletionMessageToolCall responses to shared ToolCall format.
114+ // ConvertOpenAIToolCallsToSharedTypes converts OpenAI SDK tool calls to shared ToolCall format.
115+ //
116+ // Parameters:
117+ //
118+ // ctx: The logging context map.
119+ // openaiToolCalls: Array of OpenAI tool call responses.
120+ //
121+ // Returns:
122+ //
123+ // []sharedtypes.ToolCall: Shared format tool calls.
124+ // []error: List of errors for tool calls that were skipped during conversion.
271125func ConvertOpenAIToolCallsToSharedTypes (
272126 ctx * logging.ContextMap ,
273- openaiToolCalls []openai.ChatCompletionMessageToolCall ,
274- ) []sharedtypes.ToolCall {
127+ openaiToolCalls []openai.ChatCompletionMessageToolCallUnion ,
128+ ) ( []sharedtypes.ToolCall , [] error ) {
275129 var toolCalls []sharedtypes.ToolCall
130+ var errors []error
131+
132+ for i , tc := range openaiToolCalls {
133+ // Skip tool calls with empty arguments
134+ if tc .Function .Arguments == "" {
135+ err := fmt .Errorf ("tool call at index %d (ID: %s, Name: %s) has empty arguments" , i , tc .ID , tc .Function .Name )
136+ errors = append (errors , err )
137+ logging .Log .Errorf (ctx , "Tool call at index %d (ID: %s, Name: %s) has empty arguments, skipping" , i , tc .ID , tc .Function .Name )
138+ continue
139+ }
276140
277- for _ , tc := range openaiToolCalls {
141+ // Parse arguments
278142 var args map [string ]interface {}
279143 if err := json .Unmarshal ([]byte (tc .Function .Arguments ), & args ); err != nil {
280- logging .Log .Warnf (ctx , "Failed to parse tool call arguments for %s: %v" , tc .Function .Name , err )
281- args = make (map [string ]interface {})
144+ parseErr := fmt .Errorf ("failed to parse tool call at index %d (ID: %s, Name: %s): %w, raw arguments: %s" ,
145+ i , tc .ID , tc .Function .Name , err , tc .Function .Arguments )
146+ errors = append (errors , parseErr )
147+ logging .Log .Errorf (ctx , "Failed to parse tool call at index %d (ID: %s, Name: %s): %v, raw arguments: %s, skipping tool call" ,
148+ i , tc .ID , tc .Function .Name , err , tc .Function .Arguments )
149+ continue
282150 }
283151
152+ // Only append valid tool calls
284153 toolCalls = append (toolCalls , sharedtypes.ToolCall {
285154 ID : tc .ID ,
286- Type : "function" ,
155+ Type : string ( tc . Type ) ,
287156 Name : tc .Function .Name ,
288157 Input : args ,
289158 })
@@ -292,6 +161,9 @@ func ConvertOpenAIToolCallsToSharedTypes(
292161 if len (toolCalls ) > 0 {
293162 logging .Log .Infof (ctx , "Converted %d OpenAI tool calls to shared format" , len (toolCalls ))
294163 }
164+ if len (errors ) > 0 {
165+ logging .Log .Errorf (ctx , "Failed to convert %d out of %d tool calls (see detailed errors above)" , len (errors ), len (openaiToolCalls ))
166+ }
295167
296- return toolCalls
168+ return toolCalls , errors
297169}
0 commit comments