feat: Added ExcludeValues + Fixed issue with regex matching type. (#1818)

## Description

### This PR introduces a new excludedValues field for tool parameters,
enhancing validation capabilities.

This change introduces a new excludedValues field for tool parameters.
This field allows developers to specify a list of values that are not
allowed for a parameter.
The excludedValues field supports both exact value matching and regular
expression matching.

The changes include:
- Updating the tool parameter documentation to include the
excludedValues field.
- Adding the excludedValues field to the CommonParameter struct.
- Implementing the logic to check for excluded values in the Parse
method of each parameter type.
- Updating the MatchStringOrRegex function to support non-string inputs
by converting them to strings before regex matching. This makes the
allowedValues and excludedValues checks more robust.
- Adding unit tests for allowedValues to verify the MatchStringOrRegex
change on parameters.
- Adding unit tests to verify the excludedValues functionality.

## PR Checklist

- [x] Make sure you reviewed
CONTRIBUTING.md
(httpshttps://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md)
- [x] Make sure to open an issue as a
bug/issue
(https://github.com/googleapis/genai-toolbox/issues/new/choose)
before writing your code! That way we can discuss the change, evaluate
designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
- [ ] Make sure to add ! if this involve a breaking change

🛠️ Fixes #1792

Co-authored-by: Averi Kitsch <akitsch@google.com>
This commit is contained in:
Philippe Batardiere
2025-10-30 00:32:57 +01:00
committed by GitHub
parent 4797751819
commit a8e98dc99d
3 changed files with 372 additions and 37 deletions

View File

@@ -78,13 +78,14 @@ the parameter.
```
| **field** | **type** | **required** | **description** |
|---------------|:--------------:|:------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|----------------|:--------------:|:------------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean" "array" |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
| default | parameter type | false | Default value of the parameter. If provided, `required` will be `false`. |
| required | bool | false | Indicate if the parameter is required. Default to `true`. |
| allowedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| excludedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| escape | string | false | Only available for type `string`. Indicate the escaping delimiters used for the parameter. This field is intended to be used with templateParameters. Must be one of "single-quotes", "double-quotes", "backticks", "square-brackets". |
| minValue | int or float | false | Only available for type `integer` and `float`. Indicate the minimum value allowed. |
| maxValue | int or float | false | Only available for type `integer` and `float`. Indicate the maximum value allowed. |
@@ -109,13 +110,14 @@ in the list using the items field:
```
| **field** | **type** | **required** | **description** |
|---------------|:----------------:|:------------:|----------------------------------------------------------------------------|
|----------------|:----------------:|:------------:|----------------------------------------------------------------------------|
| name | string | true | Name of the parameter. |
| type | string | true | Must be "array" |
| description | string | true | Natural language description of the parameter to describe it to the agent. |
| default | parameter type | false | Default value of the parameter. If provided, `required` will be `false`. |
| required | bool | false | Indicate if the parameter is required. Default to `true`. |
| allowedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| excludedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| items | parameter object | true | Specify a Parameter object for the type of the values in the array. |
{{< notice note >}}
@@ -248,13 +250,14 @@ tools:
```
| **field** | **type** | **required** | **description** |
|---------------|:----------------:|:---------------:|-------------------------------------------------------------------------------------|
|----------------|:----------------:|:---------------:|-------------------------------------------------------------------------------------|
| name | string | true | Name of the template parameter. |
| type | string | true | Must be one of "string", "integer", "float", "boolean", "array" |
| description | string | true | Natural language description of the template parameter to describe it to the agent. |
| default | parameter type | false | Default value of the parameter. If provided, `required` will be `false`. |
| required | bool | false | Indicate if the parameter is required. Default to `true`. |
| allowedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| excludedValues | []string | false | Input value will be checked against this field. Regex is also supported. |
| items | parameter object | true (if array) | Specify a Parameter object for the type of the values in the array (string only). |
## Authorized Invocations

View File

@@ -428,6 +428,7 @@ type CommonParameter struct {
Desc string `yaml:"description" validate:"required"`
Required *bool `yaml:"required"`
AllowedValues []any `yaml:"allowedValues"`
ExcludedValues []any `yaml:"excludedValues"`
AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
}
@@ -469,6 +470,24 @@ func (p *CommonParameter) IsAllowedValues(v any) bool {
return false
}
// GetExcludedValues returns the excluded values for the Parameter.
func (p *CommonParameter) GetExcludedValues() []any {
return p.ExcludedValues
}
// IsExcludedValues checks if the value is allowed.
func (p *CommonParameter) IsExcludedValues(v any) bool {
if len(p.ExcludedValues) == 0 {
return false
}
for _, av := range p.ExcludedValues {
if MatchStringOrRegex(v, av) {
return true
}
}
return false
}
// MatchStringOrRegex checks if the input matches the target
func MatchStringOrRegex(input, target any) bool {
targetS, ok := target.(string)
@@ -480,7 +499,7 @@ func MatchStringOrRegex(input, target any) bool {
// if target is not regex, run direct comparison
return input == target
}
inputS := fmt.Sprintf("%s", input)
inputS := fmt.Sprintf("%v", input)
return re.MatchString(inputS)
}
@@ -594,6 +613,19 @@ func NewStringParameterWithAllowedValues(name string, desc string, allowedValues
}
}
// NewStringParameterWithExcludedValues is a convenience function for initializing a StringParameter with a list of excludedValues
func NewStringParameterWithExcludedValues(name string, desc string, excludedValues []any) *StringParameter {
return &StringParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Desc: desc,
ExcludedValues: excludedValues,
AuthServices: nil,
},
}
}
var _ Parameter = &StringParameter{}
// StringParameter is a parameter representing the "string" type.
@@ -612,6 +644,9 @@ func (p *StringParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(newV) {
return nil, fmt.Errorf("%s is not an allowed value", newV)
}
if p.IsExcludedValues(newV) {
return nil, fmt.Errorf("%s is an excluded value", newV)
}
if p.Escape != nil {
return applyEscape(*p.Escape, newV)
}
@@ -735,6 +770,19 @@ func NewIntParameterWithAllowedValues(name string, desc string, allowedValues []
}
}
// NewIntParameterWithExcludedValues is a convenience function for initializing a IntParameter with a list of excludedValues
func NewIntParameterWithExcludedValues(name string, desc string, excludedValues []any) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Desc: desc,
ExcludedValues: excludedValues,
AuthServices: nil,
},
}
}
var _ Parameter = &IntParameter{}
// IntParameter is a parameter representing the "int" type.
@@ -766,6 +814,9 @@ func (p *IntParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(out) {
return nil, fmt.Errorf("%d is not an allowed value", out)
}
if p.IsExcludedValues(out) {
return nil, fmt.Errorf("%d is an excluded value", out)
}
if p.MinValue != nil && out < *p.MinValue {
return nil, fmt.Errorf("%d is under the minimum value", out)
}
@@ -877,6 +928,19 @@ func NewFloatParameterWithAllowedValues(name string, desc string, allowedValues
}
}
// NewFloatParameterWithExcludedValues is a convenience function for initializing a FloatParameter with a list of excluded values.
func NewFloatParameterWithExcludedValues(name string, desc string, excludedValues []any) *FloatParameter {
return &FloatParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeFloat,
Desc: desc,
ExcludedValues: excludedValues,
AuthServices: nil,
},
}
}
var _ Parameter = &FloatParameter{}
// FloatParameter is a parameter representing the "float" type.
@@ -906,6 +970,9 @@ func (p *FloatParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(out) {
return nil, fmt.Errorf("%g is not an allowed value", out)
}
if p.IsExcludedValues(out) {
return nil, fmt.Errorf("%g is an excluded value", out)
}
if p.MinValue != nil && out < *p.MinValue {
return nil, fmt.Errorf("%g is under the minimum value", out)
}
@@ -1013,6 +1080,19 @@ func NewBooleanParameterWithAllowedValues(name string, desc string, allowedValue
}
}
// NewBooleanParameterWithExcludedValues is a convenience function for initializing a BooleanParameter with a list of excluded values.
func NewBooleanParameterWithExcludedValues(name string, desc string, excludedValues []any) *BooleanParameter {
return &BooleanParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeBool,
Desc: desc,
ExcludedValues: excludedValues,
AuthServices: nil,
},
}
}
var _ Parameter = &BooleanParameter{}
// BooleanParameter is a parameter representing the "boolean" type.
@@ -1029,6 +1109,9 @@ func (p *BooleanParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(newV) {
return nil, fmt.Errorf("%t is not an allowed value", newV)
}
if p.IsExcludedValues(newV) {
return nil, fmt.Errorf("%t is an excluded value", newV)
}
return newV, nil
}
@@ -1125,6 +1208,20 @@ func NewArrayParameterWithAllowedValues(name string, desc string, allowedValues
}
}
// NewArrayParameterWithExcludedValues is a convenience function for initializing a ArrayParameter with a list of excluded values.
func NewArrayParameterWithExcludedValues(name string, desc string, excludedValues []any, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeArray,
Desc: desc,
ExcludedValues: excludedValues,
AuthServices: nil,
},
Items: items,
}
}
var _ Parameter = &ArrayParameter{}
// ArrayParameter is a parameter representing the "array" type.
@@ -1170,6 +1267,19 @@ func (p *ArrayParameter) IsAllowedValues(v []any) bool {
return false
}
func (p *ArrayParameter) IsExcludedValues(v []any) bool {
a := p.GetExcludedValues()
if len(a) == 0 {
return false
}
for _, av := range a {
if reflect.DeepEqual(v, av) {
return true
}
}
return false
}
func (p *ArrayParameter) Parse(v any) (any, error) {
arrVal, ok := v.([]any)
if !ok {
@@ -1178,6 +1288,9 @@ func (p *ArrayParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(arrVal) {
return nil, fmt.Errorf("%s is not an allowed value", arrVal)
}
if p.IsExcludedValues(arrVal) {
return nil, fmt.Errorf("%s is an excluded value", arrVal)
}
rtn := make([]any, 0, len(arrVal))
for idx, val := range arrVal {
val, err := p.Items.Parse(val)
@@ -1310,6 +1423,19 @@ func NewMapParameterWithAllowedValues(name string, desc string, allowedValues []
}
}
// NewMapParameterWithExcludedValues is a convenience function for initializing a MapParameter with a list of excluded values.
func NewMapParameterWithExcludedValues(name string, desc string, excludedValues []any, valueType string) *MapParameter {
return &MapParameter{
CommonParameter: CommonParameter{
Name: name,
Type: "map",
Desc: desc,
ExcludedValues: excludedValues,
},
ValueType: valueType,
}
}
// UnmarshalYAML handles parsing the MapParameter from YAML input.
func (p *MapParameter) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
var rawItem struct {
@@ -1364,6 +1490,19 @@ func (p *MapParameter) IsAllowedValues(v map[string]any) bool {
return false
}
func (p *MapParameter) IsExcludedValues(v map[string]any) bool {
a := p.GetExcludedValues()
if len(a) == 0 {
return false
}
for _, av := range a {
if reflect.DeepEqual(v, av) {
return true
}
}
return false
}
// Parse validates and parses an incoming value for the map parameter.
func (p *MapParameter) Parse(v any) (any, error) {
m, ok := v.(map[string]any)
@@ -1373,6 +1512,9 @@ func (p *MapParameter) Parse(v any) (any, error) {
if !p.IsAllowedValues(m) {
return nil, fmt.Errorf("%s is not an allowed value", m)
}
if p.IsExcludedValues(m) {
return nil, fmt.Errorf("%s is an excluded value", m)
}
// for generic maps, convert json.Numbers to their corresponding types
if p.ValueType == "" {
convertedData, err := util.ConvertNumbers(m)

View File

@@ -750,6 +750,43 @@ func TestParametersParse(t *testing.T) {
"my_string": "bar",
},
},
{
name: "string not allowed regex",
params: tools.Parameters{
tools.NewStringParameterWithAllowedValues("my_string", "this param is a string", []any{"^f.*"}),
},
in: map[string]any{
"my_string": "bar",
},
},
{
name: "string excluded",
params: tools.Parameters{
tools.NewStringParameterWithExcludedValues("my_string", "this param is a string", []any{"foo"}),
},
in: map[string]any{
"my_string": "foo",
},
},
{
name: "string excluded regex",
params: tools.Parameters{
tools.NewStringParameterWithExcludedValues("my_string", "this param is a string", []any{"^f.*"}),
},
in: map[string]any{
"my_string": "foo",
},
},
{
name: "string not excluded",
params: tools.Parameters{
tools.NewStringParameterWithExcludedValues("my_string", "this param is a string", []any{"foo"}),
},
in: map[string]any{
"my_string": "bar",
},
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "bar"}},
},
{
name: "string with escape backticks",
params: tools.Parameters{
@@ -829,6 +866,16 @@ func TestParametersParse(t *testing.T) {
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 1}},
},
{
name: "int allowed regex",
params: tools.Parameters{
tools.NewIntParameterWithAllowedValues("my_int", "this param is an int", []any{"^\\d{2}$"}),
},
in: map[string]any{
"my_int": 10,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 10}},
},
{
name: "int not allowed",
params: tools.Parameters{
@@ -838,6 +885,53 @@ func TestParametersParse(t *testing.T) {
"my_int": 2,
},
},
{
name: "int not allowed regex",
params: tools.Parameters{
tools.NewIntParameterWithAllowedValues("my_int", "this param is an int", []any{"^\\d{2}$"}),
},
in: map[string]any{
"my_int": 100,
},
},
{
name: "int excluded",
params: tools.Parameters{
tools.NewIntParameterWithExcludedValues("my_int", "this param is an int", []any{1}),
},
in: map[string]any{
"my_int": 1,
},
},
{
name: "int excluded regex",
params: tools.Parameters{
tools.NewIntParameterWithExcludedValues("my_int", "this param is an int", []any{"^\\d{2}$"}),
},
in: map[string]any{
"my_int": 10,
},
},
{
name: "int not excluded",
params: tools.Parameters{
tools.NewIntParameterWithExcludedValues("my_int", "this param is an int", []any{1}),
},
in: map[string]any{
"my_int": 2,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 2}},
},
{
name: "int not excluded regex",
params: tools.Parameters{
tools.NewIntParameterWithExcludedValues("my_int", "this param is an int", []any{"^\\d{2}$"}),
},
in: map[string]any{
"my_int": 2,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 2}},
},
{
name: "int minValue",
params: tools.Parameters{
@@ -905,6 +999,16 @@ func TestParametersParse(t *testing.T) {
},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.1}},
},
{
name: "float allowed regex",
params: tools.Parameters{
tools.NewFloatParameterWithAllowedValues("my_float", "this param is a float", []any{"^0\\.\\d+$"}),
},
in: map[string]any{
"my_float": 0.99,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 0.99}},
},
{
name: "float not allowed",
params: tools.Parameters{
@@ -914,6 +1018,54 @@ func TestParametersParse(t *testing.T) {
"my_float": 1.2,
},
},
{
name: "float not allowed regex",
params: tools.Parameters{
tools.NewFloatParameterWithAllowedValues("my_float", "this param is a float", []any{"^0\\.\\d+$"}),
},
in: map[string]any{
"my_float": 1.99,
},
},
{
name: "float excluded",
params: tools.Parameters{
tools.NewFloatParameterWithExcludedValues("my_float", "this param is a float", []any{1.1}),
},
in: map[string]any{
"my_float": 1.1,
},
},
{
name: "float excluded regex",
params: tools.Parameters{
tools.NewFloatParameterWithExcludedValues("my_float", "this param is a float", []any{"^0\\.\\d+$"}),
},
in: map[string]any{
"my_float": 0.99,
},
},
{
name: "float not excluded",
params: tools.Parameters{
tools.NewFloatParameterWithExcludedValues("my_float", "this param is a float", []any{1.1}),
},
in: map[string]any{
"my_float": 1.2,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.2}},
},
{
name: "float not excluded regex",
params: tools.Parameters{
tools.NewFloatParameterWithExcludedValues("my_float", "this param is a float", []any{"^0\\.\\d+$"}),
},
in: map[string]any{
"my_float": 1.99,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.99}},
},
{
name: "float minValue",
params: tools.Parameters{
@@ -990,6 +1142,25 @@ func TestParametersParse(t *testing.T) {
"my_bool": true,
},
},
{
name: "bool excluded",
params: tools.Parameters{
tools.NewBooleanParameterWithExcludedValues("my_bool", "this param is a bool", []any{true}),
},
in: map[string]any{
"my_bool": true,
},
},
{
name: "bool not excluded",
params: tools.Parameters{
tools.NewBooleanParameterWithExcludedValues("my_bool", "this param is a bool", []any{false}),
},
in: map[string]any{
"my_bool": true,
},
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
},
{
name: "string default",
params: tools.Parameters{
@@ -1136,6 +1307,25 @@ func TestParametersParse(t *testing.T) {
"my_map": map[string]any{"key1": "val2"},
},
},
{
name: "map excluded",
params: tools.Parameters{
tools.NewMapParameterWithExcludedValues("my_map", "a map", []any{map[string]any{"key1": "val1"}}, "string"),
},
in: map[string]any{
"my_map": map[string]any{"key1": "val1"},
},
},
{
name: "map not excluded",
params: tools.Parameters{
tools.NewMapParameterWithExcludedValues("my_map", "a map", []any{map[string]any{"key1": "val1"}}, "string"),
},
in: map[string]any{
"my_map": map[string]any{"key1": "val2"},
},
want: tools.ParamValues{tools.ParamValue{Name: "my_map", Value: map[string]any{"key1": "val2"}}},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {