mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-19 03:14:29 -05:00
Add Spanner source and tool.
Spanner source is initialize with the following config:
```
sources:
my-spanner-source:
kind: spanner
project: my-project-name
instance: my-instance-name
database: my_db
# dialect: postgresql # The default dialect is google_standard_sql.
```
Spanner tool (with gsql dialect) is initialize with the following
config.
```
tools:
get_flight_by_id:
kind: spanner
source: my-cloud-sql-source
description: >
Use this tool to list all airports matching search criteria. Takes
at least one of country, city, name, or all and returns all matching
airports. The agent can decide to return the results directly to
the user.
statement: "SELECT * FROM flights WHERE id = @id"
parameters:
- name: id
type: int
description: 'id' represents the unique ID for each flight.
```
Spanner tool (with postgresql dialect) is initialize with the following
config.
```
tools:
get_flight_by_id:
kind: spanner
source: my-cloud-sql-source
description: >
Use this tool to list all airports matching search criteria. Takes
at least one of country, city, name, or all and returns all matching
airports. The agent can decide to return the results directly to
the user.
statement: "SELECT * FROM flights WHERE id = $1"
parameters:
- name: id
type: int
description: 'id' represents the unique ID for each flight.
```
Note: the only difference in config for both dialects is the sql
statement.
---------
Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com>
300 lines
7.2 KiB
Go
300 lines
7.2 KiB
Go
// Copyright 2024 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package tools_test
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestParametersMarhsall(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
in []map[string]any
|
|
want tools.Parameters
|
|
}{
|
|
{
|
|
name: "string",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_string",
|
|
"type": "string",
|
|
"description": "this param is a string",
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewStringParameter("my_string", "this param is a string"),
|
|
},
|
|
},
|
|
{
|
|
name: "int",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_integer",
|
|
"type": "integer",
|
|
"description": "this param is an int",
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewIntParameter("my_integer", "this param is an int"),
|
|
},
|
|
},
|
|
{
|
|
name: "float",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_float",
|
|
"type": "float",
|
|
"description": "my param is a float",
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewFloatParameter("my_float", "my param is a float"),
|
|
},
|
|
},
|
|
{
|
|
name: "bool",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_bool",
|
|
"type": "boolean",
|
|
"description": "this param is a boolean",
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewBooleanParameter("my_bool", "this param is a boolean"),
|
|
},
|
|
},
|
|
{
|
|
name: "string array",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_array",
|
|
"type": "array",
|
|
"description": "this param is an array of strings",
|
|
"items": map[string]string{
|
|
"type": "string",
|
|
},
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("", "")),
|
|
},
|
|
},
|
|
{
|
|
name: "float array",
|
|
in: []map[string]any{
|
|
{
|
|
"name": "my_array",
|
|
"type": "array",
|
|
"description": "this param is an array of floats",
|
|
"items": map[string]string{
|
|
"type": "float",
|
|
},
|
|
},
|
|
},
|
|
want: tools.Parameters{
|
|
tools.NewArrayParameter("my_array", "this param is an array of floats", tools.NewFloatParameter("", "")),
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var got tools.Parameters
|
|
// parse map to bytes
|
|
data, err := yaml.Marshal(tc.in)
|
|
if err != nil {
|
|
t.Fatalf("unable to marshal input to yaml: %s", err)
|
|
}
|
|
// parse bytes to object
|
|
err = yaml.Unmarshal(data, &got)
|
|
if err != nil {
|
|
t.Fatalf("unable to unmarshal: %s", err)
|
|
}
|
|
if diff := cmp.Diff(tc.want, got); diff != "" {
|
|
t.Fatalf("incorrect parse: diff %v", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParametersParse(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
params tools.Parameters
|
|
in map[string]any
|
|
want tools.ParamValues
|
|
}{
|
|
{
|
|
name: "string",
|
|
params: tools.Parameters{
|
|
tools.NewStringParameter("my_string", "this param is a string"),
|
|
},
|
|
in: map[string]any{
|
|
"my_string": "hello world",
|
|
},
|
|
want: tools.ParamValues{tools.ParamValue{Name: "my_string", Value: "hello world"}},
|
|
},
|
|
{
|
|
name: "not string",
|
|
params: tools.Parameters{
|
|
tools.NewStringParameter("my_string", "this param is a string"),
|
|
},
|
|
in: map[string]any{
|
|
"my_string": 4,
|
|
},
|
|
},
|
|
{
|
|
name: "int",
|
|
params: tools.Parameters{
|
|
tools.NewIntParameter("my_int", "this param is an int"),
|
|
},
|
|
in: map[string]any{
|
|
"my_int": 100,
|
|
},
|
|
want: tools.ParamValues{tools.ParamValue{Name: "my_int", Value: 100}},
|
|
},
|
|
{
|
|
name: "not int",
|
|
params: tools.Parameters{
|
|
tools.NewIntParameter("my_int", "this param is an int"),
|
|
},
|
|
in: map[string]any{
|
|
"my_int": 14.5,
|
|
},
|
|
},
|
|
{
|
|
name: "float",
|
|
params: tools.Parameters{
|
|
tools.NewFloatParameter("my_float", "this param is a float"),
|
|
},
|
|
in: map[string]any{
|
|
"my_float": 1.5,
|
|
},
|
|
want: tools.ParamValues{tools.ParamValue{Name: "my_float", Value: 1.5}},
|
|
},
|
|
{
|
|
name: "not float",
|
|
params: tools.Parameters{
|
|
tools.NewFloatParameter("my_float", "this param is a float"),
|
|
},
|
|
in: map[string]any{
|
|
"my_float": true,
|
|
},
|
|
},
|
|
{
|
|
name: "bool",
|
|
params: tools.Parameters{
|
|
tools.NewBooleanParameter("my_bool", "this param is a bool"),
|
|
},
|
|
in: map[string]any{
|
|
"my_bool": true,
|
|
},
|
|
want: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}},
|
|
},
|
|
{
|
|
name: "not bool",
|
|
params: tools.Parameters{
|
|
tools.NewBooleanParameter("my_bool", "this param is a bool"),
|
|
},
|
|
in: map[string]any{
|
|
"my_bool": 1.5,
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// parse map to bytes
|
|
data, err := yaml.Marshal(tc.in)
|
|
if err != nil {
|
|
t.Fatalf("unable to marshal input to yaml: %s", err)
|
|
}
|
|
// parse bytes to object
|
|
var m map[string]any
|
|
err = yaml.Unmarshal(data, &m)
|
|
if err != nil {
|
|
t.Fatalf("unable to unmarshal: %s", err)
|
|
}
|
|
|
|
gotAll, err := tools.ParseParams(tc.params, m)
|
|
if err != nil {
|
|
if len(tc.want) == 0 {
|
|
// error is expected if no items in want
|
|
return
|
|
}
|
|
t.Fatalf("unexpected error from ParseParams: %s", err)
|
|
}
|
|
for i, got := range gotAll {
|
|
want := tc.want[i]
|
|
if got != want {
|
|
t.Fatalf("unexpected value: got %q, want %q", got, want)
|
|
}
|
|
gotType, wantType := reflect.TypeOf(got), reflect.TypeOf(want)
|
|
if gotType != wantType {
|
|
t.Fatalf("unexpected value: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParamValues(t *testing.T) {
|
|
tcs := []struct {
|
|
name string
|
|
in tools.ParamValues
|
|
wantSlice []any
|
|
wantMap map[string]interface{}
|
|
wantMapOrdered map[string]interface{}
|
|
}{
|
|
{
|
|
name: "string",
|
|
in: tools.ParamValues{tools.ParamValue{Name: "my_bool", Value: true}, tools.ParamValue{Name: "my_string", Value: "hello world"}},
|
|
wantSlice: []any{true, "hello world"},
|
|
wantMap: map[string]interface{}{"my_bool": true, "my_string": "hello world"},
|
|
wantMapOrdered: map[string]interface{}{"p1": true, "p2": "hello world"},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotSlice := tc.in.AsSlice()
|
|
gotMap := tc.in.AsMap()
|
|
gotMapOrdered := tc.in.AsMapByOrderedKeys()
|
|
|
|
for i, got := range gotSlice {
|
|
want := tc.wantSlice[i]
|
|
if got != want {
|
|
t.Fatalf("unexpected value: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
for i, got := range gotMap {
|
|
want := tc.wantMap[i]
|
|
if got != want {
|
|
t.Fatalf("unexpected value: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
for i, got := range gotMapOrdered {
|
|
want := tc.wantMapOrdered[i]
|
|
if got != want {
|
|
t.Fatalf("unexpected value: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|