-
Notifications
You must be signed in to change notification settings - Fork 1
Fix/tools null properties #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -622,7 +622,9 @@ func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, in | |
| // Serialize tools and tool_choice to JSON strings | ||
| toolsJSON := "" | ||
| if len(input.Tools) > 0 { | ||
| toolsBytes, err := json.Marshal(input.Tools) | ||
| // Sanitize tools to remove null values from parameters.properties | ||
| sanitizedTools := functions.SanitizeTools(input.Tools) | ||
| toolsBytes, err := json.Marshal(sanitizedTools) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from Suggested fix toolsBytes, err := json.Marshal(sanitizedTools)
if err != nil {
log.Error("Failed to marshal sanitized tools", "error", err)
} else {
toolsJSON = string(toolsBytes)
}Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
| if err == nil { | ||
| toolsJSON = string(toolsBytes) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package functions | |
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/mudler/xlog" | ||
| ) | ||
|
|
@@ -102,3 +103,91 @@ func (f Functions) Select(name string) Functions { | |
|
|
||
| return funcs | ||
| } | ||
|
|
||
| // sanitizeValue recursively sanitizes null values in a JSON structure, converting them to empty objects. | ||
| // It handles maps, slices, and nested structures. | ||
| func sanitizeValue(value interface{}, path string) interface{} { | ||
| if value == nil { | ||
| // Convert null to empty object | ||
| xlog.Debug("SanitizeTools: found null value, converting to empty object", "path", path) | ||
| return map[string]interface{}{} | ||
| } | ||
|
|
||
| switch v := value.(type) { | ||
| case map[string]interface{}: | ||
| // Recursively sanitize map values | ||
| sanitized := make(map[string]interface{}) | ||
| for key, val := range v { | ||
| newPath := path | ||
| if newPath != "" { | ||
| newPath += "." | ||
| } | ||
| newPath += key | ||
| sanitized[key] = sanitizeValue(val, newPath) | ||
| } | ||
| return sanitized | ||
|
|
||
| case []interface{}: | ||
| // Recursively sanitize slice elements | ||
| sanitized := make([]interface{}, len(v)) | ||
| for i, val := range v { | ||
| newPath := fmt.Sprintf("%s[%d]", path, i) | ||
| sanitized[i] = sanitizeValue(val, newPath) | ||
| } | ||
| return sanitized | ||
|
|
||
| default: | ||
| // For primitive types (string, number, bool), return as-is | ||
| return value | ||
| } | ||
| } | ||
|
|
||
| // SanitizeTools removes null values from tool.parameters.properties and converts them to empty objects. | ||
| // This prevents Jinja template errors when processing tools with malformed parameter schemas. | ||
| // It works by marshaling to JSON, recursively sanitizing the JSON structure, and unmarshaling back. | ||
| func SanitizeTools(tools Tools) Tools { | ||
| if len(tools) == 0 { | ||
| return tools | ||
| } | ||
|
|
||
| xlog.Debug("SanitizeTools: processing tools", "count", len(tools)) | ||
|
|
||
| // Marshal to JSON to work with the actual JSON representation | ||
| toolsJSON, err := json.Marshal(tools) | ||
| if err != nil { | ||
| xlog.Warn("SanitizeTools: failed to marshal tools to JSON", "error", err) | ||
| return tools | ||
| } | ||
|
|
||
| // Parse JSON into a generic structure | ||
| var toolsData []map[string]interface{} | ||
| if err := json.Unmarshal(toolsJSON, &toolsData); err != nil { | ||
| xlog.Warn("SanitizeTools: failed to unmarshal tools JSON", "error", err) | ||
| return tools | ||
| } | ||
|
|
||
| // Recursively sanitize the JSON structure | ||
| for i, tool := range toolsData { | ||
| if function, ok := tool["function"].(map[string]interface{}); ok { | ||
| // Recursively sanitize the entire tool structure | ||
| tool["function"] = sanitizeValue(function, fmt.Sprintf("tools[%d].function", i)) | ||
| } | ||
| toolsData[i] = tool | ||
| } | ||
|
|
||
| // Marshal back to JSON | ||
| sanitizedJSON, err := json.Marshal(toolsData) | ||
| if err != nil { | ||
| xlog.Warn("SanitizeTools: failed to marshal sanitized tools", "error", err) | ||
| return tools | ||
| } | ||
|
|
||
| // Unmarshal back into Tools structure | ||
| var sanitized Tools | ||
| if err := json.Unmarshal(sanitizedJSON, &sanitized); err != nil { | ||
| xlog.Warn("SanitizeTools: failed to unmarshal sanitized tools", "error", err) | ||
| return tools | ||
| } | ||
|
|
||
| return sanitized | ||
| } | ||
|
Comment on lines
+109
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+145
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,4 +82,202 @@ var _ = Describe("LocalAI grammar functions", func() { | |
| Expect(functions[0].Name).To(Equal("create_event")) | ||
| }) | ||
| }) | ||
| Context("SanitizeTools()", func() { | ||
| It("returns empty slice when input is empty", func() { | ||
| tools := Tools{} | ||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(0)) | ||
| }) | ||
|
|
||
| It("converts null values in parameters.properties to empty objects", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "test_function", | ||
| Description: "A test function", | ||
| Parameters: map[string]interface{}{ | ||
| "type": "object", | ||
| "properties": map[string]interface{}{ | ||
| "valid_param": map[string]interface{}{ | ||
| "type": "string", | ||
| }, | ||
| "null_param": nil, | ||
| "another_valid": map[string]interface{}{ | ||
| "type": "integer", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(1)) | ||
| Expect(sanitized[0].Function.Name).To(Equal("test_function")) | ||
|
|
||
| properties := sanitized[0].Function.Parameters["properties"].(map[string]interface{}) | ||
| Expect(properties["valid_param"]).NotTo(BeNil()) | ||
| Expect(properties["null_param"]).NotTo(BeNil()) | ||
| Expect(properties["null_param"]).To(Equal(map[string]interface{}{})) | ||
| Expect(properties["another_valid"]).NotTo(BeNil()) | ||
| }) | ||
|
|
||
| It("preserves valid parameter structures unchanged", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "valid_function", | ||
| Description: "A function with valid parameters", | ||
| Parameters: map[string]interface{}{ | ||
| "type": "object", | ||
| "properties": map[string]interface{}{ | ||
| "param1": map[string]interface{}{ | ||
| "type": "string", | ||
| "description": "First parameter", | ||
| }, | ||
| "param2": map[string]interface{}{ | ||
| "type": "integer", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(1)) | ||
| Expect(sanitized[0].Function.Name).To(Equal("valid_function")) | ||
|
|
||
| properties := sanitized[0].Function.Parameters["properties"].(map[string]interface{}) | ||
| Expect(properties["param1"].(map[string]interface{})["type"]).To(Equal("string")) | ||
| Expect(properties["param1"].(map[string]interface{})["description"]).To(Equal("First parameter")) | ||
| Expect(properties["param2"].(map[string]interface{})["type"]).To(Equal("integer")) | ||
| }) | ||
|
|
||
| It("handles tools without parameters field", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "no_params_function", | ||
| Description: "A function without parameters", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(1)) | ||
| Expect(sanitized[0].Function.Name).To(Equal("no_params_function")) | ||
| Expect(sanitized[0].Function.Parameters).To(BeNil()) | ||
| }) | ||
|
|
||
| It("handles tools without properties field", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "no_properties_function", | ||
| Description: "A function without properties", | ||
| Parameters: map[string]interface{}{ | ||
| "type": "object", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(1)) | ||
| Expect(sanitized[0].Function.Name).To(Equal("no_properties_function")) | ||
| Expect(sanitized[0].Function.Parameters["type"]).To(Equal("object")) | ||
| }) | ||
|
|
||
| It("handles multiple tools with mixed valid and null values", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "function_with_nulls", | ||
| Parameters: map[string]interface{}{ | ||
| "properties": map[string]interface{}{ | ||
| "valid": map[string]interface{}{ | ||
| "type": "string", | ||
| }, | ||
| "null1": nil, | ||
| "null2": nil, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "function_all_valid", | ||
| Parameters: map[string]interface{}{ | ||
| "properties": map[string]interface{}{ | ||
| "param1": map[string]interface{}{ | ||
| "type": "string", | ||
| }, | ||
| "param2": map[string]interface{}{ | ||
| "type": "integer", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "function_no_params", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
| Expect(len(sanitized)).To(Equal(3)) | ||
|
|
||
| // First tool should have nulls converted to empty objects | ||
| props1 := sanitized[0].Function.Parameters["properties"].(map[string]interface{}) | ||
| Expect(props1["valid"]).NotTo(BeNil()) | ||
| Expect(props1["null1"]).To(Equal(map[string]interface{}{})) | ||
| Expect(props1["null2"]).To(Equal(map[string]interface{}{})) | ||
|
|
||
| // Second tool should remain unchanged | ||
| props2 := sanitized[1].Function.Parameters["properties"].(map[string]interface{}) | ||
| Expect(props2["param1"].(map[string]interface{})["type"]).To(Equal("string")) | ||
| Expect(props2["param2"].(map[string]interface{})["type"]).To(Equal("integer")) | ||
|
|
||
| // Third tool should remain unchanged | ||
| Expect(sanitized[2].Function.Parameters).To(BeNil()) | ||
| }) | ||
|
|
||
| It("does not modify the original tools slice", func() { | ||
| tools := Tools{ | ||
| { | ||
| Type: "function", | ||
| Function: Function{ | ||
| Name: "test_function", | ||
| Parameters: map[string]interface{}{ | ||
| "properties": map[string]interface{}{ | ||
| "null_param": nil, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| originalProperties := tools[0].Function.Parameters["properties"].(map[string]interface{}) | ||
| originalNullValue := originalProperties["null_param"] | ||
|
|
||
| sanitized := SanitizeTools(tools) | ||
|
|
||
| // Original should still have nil | ||
| Expect(originalNullValue).To(BeNil()) | ||
|
Comment on lines
+270
to
+276
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The immutability test captures Suggested fix originalProperties := tools[0].Function.Parameters["properties"].(map[string]interface{})
sanitized := SanitizeTools(tools)
// Original should still have nil (re-read after the call to detect in-place mutation)
Expect(originalProperties["null_param"]).To(BeNil())Prompt for AI assistanceCopy the prompt below and paste it into ChatGPT, Claude, or any LLM: |
||
|
|
||
| // Sanitized should have empty object | ||
| sanitizedProperties := sanitized[0].Function.Parameters["properties"].(map[string]interface{}) | ||
| Expect(sanitizedProperties["null_param"]).To(Equal(map[string]interface{}{})) | ||
| }) | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two comment lines describe sanitization logic that was never implemented in this file. They sit between the closing brace of
parse_optionsand thekv_cache_typesvector definition, unattached to any function. A future reader will look for the C++ implementation referenced by the comment and not find it, which could cause confusion about where null-sanitization actually occurs.