mirror of
https://github.com/we-promise/sure.git
synced 2026-09-04 14:21:23 +00:00
fix(ai): drop empty data-driven enums from assistant function schemas (#3016)
* fix(ai): drop empty data-driven enums from assistant function schemas
Enum values in tool schemas are built from family data (account names,
categories, merchants, tags, tickers). A family with none of these gets
enum: [], which is invalid JSON Schema. OpenAI tolerates it, but strict
OpenAI-compatible providers reject the entire request, breaking chat for
fresh families until they create a tag or merchant.
Prune empty enums in build_schema, falling back to a plain string. One
choke point covers every function and both consumers: chat tool
definitions for all providers, and the /mcp endpoint's tools/list.
* fix(ai): address review feedback on enum pruning
Stop recursion at populated enum values: enum members are literal
values, not subschemas, so a literal like enum: [{ enum: [] }] must be
preserved verbatim rather than rewritten.
Also cover PREVIEW_FUNCTION_CLASSES in the registry regression test by
enabling the preview preference on the test user, with a guard assertion
so the test fails if preview functions ever silently drop out.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
require "test_helper"
|
||||
|
||||
class Assistant::FunctionTest < ActiveSupport::TestCase
|
||||
class EmptyEnumFunction < Assistant::Function
|
||||
class << self
|
||||
def name
|
||||
"empty_enum_function"
|
||||
end
|
||||
|
||||
def description
|
||||
"Test function with data-driven enums"
|
||||
end
|
||||
end
|
||||
|
||||
def call(params = {})
|
||||
{}
|
||||
end
|
||||
|
||||
def params_schema
|
||||
build_schema(
|
||||
required: [ "name" ],
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Property-level enum built from empty user data",
|
||||
enum: []
|
||||
},
|
||||
accounts: {
|
||||
type: "array",
|
||||
description: "Items-level enum built from empty user data",
|
||||
items: { enum: [] },
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
order: {
|
||||
enum: [ "asc", "desc" ],
|
||||
description: "Static enum that must be preserved"
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
@function = EmptyEnumFunction.new(users(:family_admin))
|
||||
end
|
||||
|
||||
test "drops empty enums so strict providers accept the schema" do
|
||||
properties = @function.to_definition[:params_schema][:properties]
|
||||
|
||||
refute properties[:name].key?(:enum)
|
||||
assert_equal "string", properties[:name][:type]
|
||||
|
||||
refute properties[:accounts][:items].key?(:enum)
|
||||
assert_equal "string", properties[:accounts][:items][:type]
|
||||
assert_equal 1, properties[:accounts][:minItems]
|
||||
end
|
||||
|
||||
test "preserves populated enums and surrounding schema" do
|
||||
schema = @function.to_definition[:params_schema]
|
||||
|
||||
assert_equal [ "asc", "desc" ], schema[:properties][:order][:enum]
|
||||
assert_equal [ "name" ], schema[:required]
|
||||
assert_equal "object", schema[:type]
|
||||
end
|
||||
|
||||
test "keeps populated enum values verbatim, including literal objects and arrays" do
|
||||
schema = {
|
||||
status: { enum: [ { enum: [] }, [ "nested" ] ] },
|
||||
order: { enum: [ "asc" ] }
|
||||
}
|
||||
|
||||
pruned = @function.send(:prune_empty_enums, schema)
|
||||
|
||||
assert_equal [ { enum: [] }, [ "nested" ] ], pruned[:status][:enum]
|
||||
assert_equal [ "asc" ], pruned[:order][:enum]
|
||||
end
|
||||
|
||||
test "no registered function emits an empty enum" do
|
||||
user = users(:family_admin)
|
||||
user.update!(preferences: (user.preferences || {}).merge("preview_features_enabled" => true))
|
||||
|
||||
function_classes = Assistant.function_classes(user)
|
||||
assert_empty Assistant::PREVIEW_FUNCTION_CLASSES - function_classes,
|
||||
"expected preview functions to be included in the assertion"
|
||||
|
||||
function_classes.each do |function_class|
|
||||
definition = function_class.new(user).to_definition
|
||||
assert_no_empty_enums definition[:params_schema], function_class.name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def assert_no_empty_enums(node, function_name, path = "params_schema")
|
||||
case node
|
||||
when Hash
|
||||
node.each do |key, value|
|
||||
refute key.to_sym == :enum && value == [],
|
||||
"#{function_name} emits an empty enum at #{path}.#{key}, which is invalid JSON Schema"
|
||||
assert_no_empty_enums(value, function_name, "#{path}.#{key}")
|
||||
end
|
||||
when Array
|
||||
node.each_with_index do |value, index|
|
||||
assert_no_empty_enums(value, function_name, "#{path}[#{index}]")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user