mirror of
https://github.com/mudler/LocalAI.git
synced 2026-02-07 21:22:58 -05:00
Compare commits
3 Commits
copilot/fi
...
fix/tools-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93d3e4257a | ||
|
|
e0e904ff98 | ||
|
|
a95422f4d1 |
@@ -293,6 +293,8 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize tools JSON to remove null values from tool.parameters.properties
|
||||||
|
// This prevents Jinja template errors when processing tools with malformed parameter schemas
|
||||||
|
|
||||||
const std::vector<ggml_type> kv_cache_types = {
|
const std::vector<ggml_type> kv_cache_types = {
|
||||||
GGML_TYPE_F32,
|
GGML_TYPE_F32,
|
||||||
|
|||||||
@@ -622,7 +622,9 @@ func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, in
|
|||||||
// Serialize tools and tool_choice to JSON strings
|
// Serialize tools and tool_choice to JSON strings
|
||||||
toolsJSON := ""
|
toolsJSON := ""
|
||||||
if len(input.Tools) > 0 {
|
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)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
toolsJSON = string(toolsBytes)
|
toolsJSON = string(toolsBytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/schema"
|
"github.com/mudler/LocalAI/core/schema"
|
||||||
|
"github.com/mudler/LocalAI/pkg/functions"
|
||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,7 +43,9 @@ func ComputeChoices(
|
|||||||
// Serialize tools and tool_choice to JSON strings
|
// Serialize tools and tool_choice to JSON strings
|
||||||
toolsJSON := ""
|
toolsJSON := ""
|
||||||
if len(req.Tools) > 0 {
|
if len(req.Tools) > 0 {
|
||||||
toolsBytes, err := json.Marshal(req.Tools)
|
// Sanitize tools to remove null values from parameters.properties
|
||||||
|
sanitizedTools := functions.SanitizeTools(req.Tools)
|
||||||
|
toolsBytes, err := json.Marshal(sanitizedTools)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
toolsJSON = string(toolsBytes)
|
toolsJSON = string(toolsBytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package functions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
)
|
)
|
||||||
@@ -102,3 +103,91 @@ func (f Functions) Select(name string) Functions {
|
|||||||
|
|
||||||
return funcs
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,4 +82,202 @@ var _ = Describe("LocalAI grammar functions", func() {
|
|||||||
Expect(functions[0].Name).To(Equal("create_event"))
|
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())
|
||||||
|
|
||||||
|
// Sanitized should have empty object
|
||||||
|
sanitizedProperties := sanitized[0].Function.Parameters["properties"].(map[string]interface{})
|
||||||
|
Expect(sanitizedProperties["null_param"]).To(Equal(map[string]interface{}{}))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user