mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-29 17:28:05 -05:00
Compare commits
1 Commits
processing
...
new-image-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2959576afa |
@@ -152,6 +152,9 @@ import (
|
|||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerqueryurl"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerqueryurl"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrundashboard"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrundashboard"
|
||||||
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderdashboard"
|
||||||
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderlook"
|
||||||
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderquery"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlook"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrunlook"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerupdateprojectfile"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerupdateprojectfile"
|
||||||
_ "github.com/googleapis/genai-toolbox/internal/tools/mindsdb/mindsdbexecutesql"
|
_ "github.com/googleapis/genai-toolbox/internal/tools/mindsdb/mindsdbexecutesql"
|
||||||
|
|||||||
@@ -1613,7 +1613,7 @@ func TestPrebuiltTools(t *testing.T) {
|
|||||||
wantToolset: server.ToolsetConfigs{
|
wantToolset: server.ToolsetConfigs{
|
||||||
"looker_tools": tools.ToolsetConfig{
|
"looker_tools": tools.ToolsetConfig{
|
||||||
Name: "looker_tools",
|
Name: "looker_tools",
|
||||||
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look", "make_look", "get_dashboards", "run_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns"},
|
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look", "make_look", "get_dashboards", "run_dashboard", "render_query", "render_look", "render_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1042,6 +1042,41 @@ tools:
|
|||||||
such as `table_name`, `column_name`, `data_type`, and `is_nullable`.
|
such as `table_name`, `column_name`, `data_type`, and `is_nullable`.
|
||||||
|
|
||||||
|
|
||||||
|
render_query:
|
||||||
|
kind: looker-render-query
|
||||||
|
source: looker-source
|
||||||
|
description: |
|
||||||
|
render_query Tool
|
||||||
|
|
||||||
|
This tool runs a query and returns a jpg or png of the
|
||||||
|
visualization for that query. It accepts the query_id as a
|
||||||
|
parameter. It also accepts a height and width in pixels,
|
||||||
|
defaulting to 1200 x 1575. The format parameter can be 'jpg'
|
||||||
|
or 'png'. 'png' is the default.
|
||||||
|
|
||||||
|
render_look:
|
||||||
|
kind: looker-render-look
|
||||||
|
source: looker-source
|
||||||
|
description: |
|
||||||
|
render_look Tool
|
||||||
|
|
||||||
|
This tool runs the query associated with a look and returns
|
||||||
|
a jpg or png of the visualization for that look. It accepts
|
||||||
|
the look_id as a parameter. It also accepts a height and width
|
||||||
|
in pixels, defaulting to 1200 x 1575. The format parameter
|
||||||
|
can be 'jpg' or 'png'. 'png' is the default.
|
||||||
|
|
||||||
|
render_dashboard:
|
||||||
|
kind: looker-render-dashboard
|
||||||
|
source: looker-source
|
||||||
|
description: |
|
||||||
|
render_dashboard Tool
|
||||||
|
|
||||||
|
This tool renders a dashboard as a pdf, jpg, or png. It accepts
|
||||||
|
the dashboard_id as a parameter. It also accepts a height and width
|
||||||
|
in pixels, defaulting to 2400 x 3150. The format parameter
|
||||||
|
can be 'pdf', 'jpg', or 'png'. 'pdf' is the default.
|
||||||
|
|
||||||
toolsets:
|
toolsets:
|
||||||
looker_tools:
|
looker_tools:
|
||||||
- get_models
|
- get_models
|
||||||
@@ -1058,6 +1093,9 @@ toolsets:
|
|||||||
- make_look
|
- make_look
|
||||||
- get_dashboards
|
- get_dashboards
|
||||||
- run_dashboard
|
- run_dashboard
|
||||||
|
- render_query
|
||||||
|
- render_look
|
||||||
|
- render_dashboard
|
||||||
- make_dashboard
|
- make_dashboard
|
||||||
- add_dashboard_element
|
- add_dashboard_element
|
||||||
- add_dashboard_filter
|
- add_dashboard_filter
|
||||||
@@ -1076,4 +1114,4 @@ toolsets:
|
|||||||
- get_connection_schemas
|
- get_connection_schemas
|
||||||
- get_connection_databases
|
- get_connection_databases
|
||||||
- get_connection_tables
|
- get_connection_tables
|
||||||
- get_connection_table_columns
|
- get_connection_table_columns
|
||||||
@@ -16,6 +16,7 @@ package lookercommon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -348,3 +349,12 @@ func UpdateProjectFile(l *v4.LookerSDK, projectId string, fileContent FileConten
|
|||||||
err := l.AuthSession.Do(nil, "PUT", "/4.0", path, nil, fileContent, options)
|
err := l.AuthSession.Do(nil, "PUT", "/4.0", path, nil, fileContent, options)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReturnImage(mimeType string, imageData string) map[string]any {
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["type"] = "image"
|
||||||
|
data["mimeType"] = mimeType
|
||||||
|
data["data"] = fmt.Sprintf("data:%s;base64,%s", mimeType, string(base64.StdEncoding.EncodeToString([]byte(imageData))))
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,4 +208,4 @@ func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) bool
|
|||||||
|
|
||||||
func (t Tool) GetAuthTokenHeaderName() string {
|
func (t Tool) GetAuthTokenHeaderName() string {
|
||||||
return t.AuthTokenHeaderName
|
return t.AuthTokenHeaderName
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderdashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
|
lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
|
|
||||||
|
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||||
|
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kind string = "looker-render-dashboard"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !tools.Register(kind, newConfig) {
|
||||||
|
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||||
|
actual := Config{Name: name}
|
||||||
|
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string `yaml:"name" validate:"required"`
|
||||||
|
Kind string `yaml:"kind" validate:"required"`
|
||||||
|
Source string `yaml:"source" validate:"required"`
|
||||||
|
Description string `yaml:"description" validate:"required"`
|
||||||
|
AuthRequired []string `yaml:"authRequired"`
|
||||||
|
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.ToolConfig = Config{}
|
||||||
|
|
||||||
|
func (cfg Config) ToolConfigKind() string {
|
||||||
|
return kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||||
|
// verify source exists
|
||||||
|
rawS, ok := srcs[cfg.Source]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the source is compatible
|
||||||
|
s, ok := rawS.(*lookersrc.Source)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardidParameter := parameters.NewStringParameter("dashboard_id", "The id of the dashboard to render.")
|
||||||
|
// An 8.5 x 11 piece of paper with .25 inch margins works out to
|
||||||
|
// 2400 x 3150, assuming 300 dpi. So that is a good
|
||||||
|
// default for the size of the rendering.
|
||||||
|
// An A4 paper is close enough to accomodate this too.
|
||||||
|
widthParameter := parameters.NewIntParameterWithDefault("width", 3150, "The image width. Default 3150")
|
||||||
|
heightParameter := parameters.NewIntParameterWithDefault("height", 2400, "The image height. Default 2400")
|
||||||
|
formatParameter := parameters.NewStringParameterWithDefault("format", "pdf", "The image type: pdf, png, or jpg")
|
||||||
|
|
||||||
|
params := parameters.Parameters{
|
||||||
|
dashboardidParameter,
|
||||||
|
widthParameter,
|
||||||
|
heightParameter,
|
||||||
|
formatParameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations := cfg.Annotations
|
||||||
|
if annotations == nil {
|
||||||
|
readOnlyHint := true
|
||||||
|
annotations = &tools.ToolAnnotations{
|
||||||
|
ReadOnlyHint: &readOnlyHint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
|
||||||
|
|
||||||
|
// finish tool setup
|
||||||
|
return Tool{
|
||||||
|
Config: cfg,
|
||||||
|
Parameters: params,
|
||||||
|
UseClientOAuth: s.UseClientAuthorization(),
|
||||||
|
AuthTokenHeaderName: s.GetAuthTokenHeaderName(),
|
||||||
|
Client: s.Client,
|
||||||
|
ApiSettings: s.ApiSettings,
|
||||||
|
manifest: tools.Manifest{
|
||||||
|
Description: cfg.Description,
|
||||||
|
Parameters: params.Manifest(),
|
||||||
|
AuthRequired: cfg.AuthRequired,
|
||||||
|
},
|
||||||
|
mcpManifest: mcpManifest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.Tool = Tool{}
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
Config
|
||||||
|
UseClientOAuth bool
|
||||||
|
AuthTokenHeaderName string
|
||||||
|
Client *v4.LookerSDK
|
||||||
|
ApiSettings *rtl.ApiSettings
|
||||||
|
Parameters parameters.Parameters `yaml:"parameters"`
|
||||||
|
manifest tools.Manifest
|
||||||
|
mcpManifest tools.McpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ToConfig() tools.ToolConfig {
|
||||||
|
return t.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||||
|
logger, err := util.LoggerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||||
|
}
|
||||||
|
logger.DebugContext(ctx, "params = ", params)
|
||||||
|
paramsMap := params.AsMap()
|
||||||
|
|
||||||
|
dashboard_id := paramsMap["dashboard_id"].(string)
|
||||||
|
width := int64(paramsMap["width"].(int))
|
||||||
|
height := int64(paramsMap["height"].(int))
|
||||||
|
format := paramsMap["format"].(string)
|
||||||
|
|
||||||
|
var mimeType string
|
||||||
|
switch format {
|
||||||
|
case "pdf":
|
||||||
|
mimeType = "application/pdf"
|
||||||
|
case "jpg":
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
case "png":
|
||||||
|
mimeType = "image/png"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("format \"%s\" unsupported. must be png, pdf, or jpg", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := v4.RequestCreateDashboardRenderTask{
|
||||||
|
DashboardId: dashboard_id,
|
||||||
|
ResultFormat: format,
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := sdk.CreateDashboardRenderTask(req, t.ApiSettings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error making create_dashboard_render_task request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
switch *task.Status {
|
||||||
|
case "success":
|
||||||
|
break outer
|
||||||
|
case "failure":
|
||||||
|
return nil, fmt.Errorf("failure processing render task: %s", *task.StatusDetail)
|
||||||
|
default:
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
task, err = sdk.RenderTask(*task.Id, "", t.ApiSettings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting render_task status: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := sdk.RenderTaskResults(*task.Id, t.ApiSettings)
|
||||||
|
|
||||||
|
return lookercommon.ReturnImage(mimeType, resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) {
|
||||||
|
return parameters.ParseParams(t.Parameters, data, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Manifest() tools.Manifest {
|
||||||
|
return t.manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) McpManifest() tools.McpManifest {
|
||||||
|
return t.mcpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||||
|
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) bool {
|
||||||
|
return t.UseClientOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) GetAuthTokenHeaderName() string {
|
||||||
|
return t.AuthTokenHeaderName
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderdashboard_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/server"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||||
|
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderdashboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFromYamlLookerRenderLook(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
want server.ToolConfigs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic example",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-dashboard
|
||||||
|
source: my-instance
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
want: server.ToolConfigs{
|
||||||
|
"example_tool": lkr.Config{
|
||||||
|
Name: "example_tool",
|
||||||
|
Kind: "looker-render-dashboard",
|
||||||
|
Source: "my-instance",
|
||||||
|
Description: "some description",
|
||||||
|
AuthRequired: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to unmarshal: %s", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||||
|
t.Fatalf("incorrect parse: diff %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailParseFromYamlLookerRenderLook(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Invalid method",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-dashboard
|
||||||
|
source: my-instance
|
||||||
|
method: GOT
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
err: "unable to parse tool \"example_tool\" as kind \"looker-render-dashboard\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-render-dashboard\n> 4 | method: GOT\n ^\n 5 | source: my-instance",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expect parsing to fail")
|
||||||
|
}
|
||||||
|
errStr := err.Error()
|
||||||
|
if !strings.Contains(errStr, tc.err) {
|
||||||
|
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
207
internal/tools/looker/lookerrenderlook/lookerrenderlook.go
Normal file
207
internal/tools/looker/lookerrenderlook/lookerrenderlook.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderlook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
|
lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
|
|
||||||
|
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||||
|
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kind string = "looker-render-look"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !tools.Register(kind, newConfig) {
|
||||||
|
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||||
|
actual := Config{Name: name}
|
||||||
|
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string `yaml:"name" validate:"required"`
|
||||||
|
Kind string `yaml:"kind" validate:"required"`
|
||||||
|
Source string `yaml:"source" validate:"required"`
|
||||||
|
Description string `yaml:"description" validate:"required"`
|
||||||
|
AuthRequired []string `yaml:"authRequired"`
|
||||||
|
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.ToolConfig = Config{}
|
||||||
|
|
||||||
|
func (cfg Config) ToolConfigKind() string {
|
||||||
|
return kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||||
|
// verify source exists
|
||||||
|
rawS, ok := srcs[cfg.Source]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the source is compatible
|
||||||
|
s, ok := rawS.(*lookersrc.Source)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
lookidParameter := parameters.NewStringParameter("look_id", "The id of the look to render.")
|
||||||
|
// An 8.5 x 11 piece of paper with .25 inch margins works out to 4
|
||||||
|
// quarters each 1200 x 1575, assuming 300 dpi. So that is a good
|
||||||
|
// default for the size of the rendering.
|
||||||
|
// An A4 paper is close enough to accomodate this too.
|
||||||
|
widthParameter := parameters.NewIntParameterWithDefault("width", 1575, "The image width. Default 1575")
|
||||||
|
heightParameter := parameters.NewIntParameterWithDefault("height", 1200, "The image height. Default 1200")
|
||||||
|
formatParameter := parameters.NewStringParameterWithDefault("format", "png", "The image type: png or jpg")
|
||||||
|
|
||||||
|
params := parameters.Parameters{
|
||||||
|
lookidParameter,
|
||||||
|
widthParameter,
|
||||||
|
heightParameter,
|
||||||
|
formatParameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations := cfg.Annotations
|
||||||
|
if annotations == nil {
|
||||||
|
readOnlyHint := true
|
||||||
|
annotations = &tools.ToolAnnotations{
|
||||||
|
ReadOnlyHint: &readOnlyHint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
|
||||||
|
|
||||||
|
// finish tool setup
|
||||||
|
return Tool{
|
||||||
|
Config: cfg,
|
||||||
|
Parameters: params,
|
||||||
|
UseClientOAuth: s.UseClientAuthorization(),
|
||||||
|
AuthTokenHeaderName: s.GetAuthTokenHeaderName(),
|
||||||
|
Client: s.Client,
|
||||||
|
ApiSettings: s.ApiSettings,
|
||||||
|
manifest: tools.Manifest{
|
||||||
|
Description: cfg.Description,
|
||||||
|
Parameters: params.Manifest(),
|
||||||
|
AuthRequired: cfg.AuthRequired,
|
||||||
|
},
|
||||||
|
mcpManifest: mcpManifest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.Tool = Tool{}
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
Config
|
||||||
|
UseClientOAuth bool
|
||||||
|
AuthTokenHeaderName string
|
||||||
|
Client *v4.LookerSDK
|
||||||
|
ApiSettings *rtl.ApiSettings
|
||||||
|
Parameters parameters.Parameters `yaml:"parameters"`
|
||||||
|
manifest tools.Manifest
|
||||||
|
mcpManifest tools.McpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ToConfig() tools.ToolConfig {
|
||||||
|
return t.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||||
|
logger, err := util.LoggerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||||
|
}
|
||||||
|
logger.DebugContext(ctx, "params = ", params)
|
||||||
|
paramsMap := params.AsMap()
|
||||||
|
|
||||||
|
look_id := paramsMap["look_id"].(string)
|
||||||
|
width := int64(paramsMap["width"].(int))
|
||||||
|
height := int64(paramsMap["height"].(int))
|
||||||
|
format := paramsMap["format"].(string)
|
||||||
|
|
||||||
|
var mimeType string
|
||||||
|
switch format {
|
||||||
|
case "jpg":
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
case "png":
|
||||||
|
mimeType = "image/png"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("format \"%s\" unsupported. must be png or jpg", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyVis := true
|
||||||
|
|
||||||
|
req := v4.RequestRunLook{
|
||||||
|
LookId: look_id,
|
||||||
|
ResultFormat: format,
|
||||||
|
ApplyVis: &applyVis,
|
||||||
|
ImageWidth: &width,
|
||||||
|
ImageHeight: &height,
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := sdk.RunLook(req, t.ApiSettings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error making run_look request: %s", err)
|
||||||
|
}
|
||||||
|
logger.DebugContext(ctx, "resp = ", resp)
|
||||||
|
|
||||||
|
return lookercommon.ReturnImage(mimeType, resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) {
|
||||||
|
return parameters.ParseParams(t.Parameters, data, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Manifest() tools.Manifest {
|
||||||
|
return t.manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) McpManifest() tools.McpManifest {
|
||||||
|
return t.mcpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||||
|
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) bool {
|
||||||
|
return t.UseClientOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) GetAuthTokenHeaderName() string {
|
||||||
|
return t.AuthTokenHeaderName
|
||||||
|
}
|
||||||
116
internal/tools/looker/lookerrenderlook/lookerrenderlook_test.go
Normal file
116
internal/tools/looker/lookerrenderlook/lookerrenderlook_test.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderlook_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/server"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||||
|
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderlook"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFromYamlLookerRenderLook(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
want server.ToolConfigs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic example",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-look
|
||||||
|
source: my-instance
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
want: server.ToolConfigs{
|
||||||
|
"example_tool": lkr.Config{
|
||||||
|
Name: "example_tool",
|
||||||
|
Kind: "looker-render-look",
|
||||||
|
Source: "my-instance",
|
||||||
|
Description: "some description",
|
||||||
|
AuthRequired: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to unmarshal: %s", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||||
|
t.Fatalf("incorrect parse: diff %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailParseFromYamlLookerRenderLook(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Invalid method",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-look
|
||||||
|
source: my-instance
|
||||||
|
method: GOT
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
err: "unable to parse tool \"example_tool\" as kind \"looker-render-look\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-render-look\n> 4 | method: GOT\n ^\n 5 | source: my-instance",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expect parsing to fail")
|
||||||
|
}
|
||||||
|
errStr := err.Error()
|
||||||
|
if !strings.Contains(errStr, tc.err) {
|
||||||
|
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
206
internal/tools/looker/lookerrenderquery/lookerrenderquery.go
Normal file
206
internal/tools/looker/lookerrenderquery/lookerrenderquery.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
|
lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
|
|
||||||
|
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||||
|
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kind string = "looker-render-query"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !tools.Register(kind, newConfig) {
|
||||||
|
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||||
|
actual := Config{Name: name}
|
||||||
|
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actual, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string `yaml:"name" validate:"required"`
|
||||||
|
Kind string `yaml:"kind" validate:"required"`
|
||||||
|
Source string `yaml:"source" validate:"required"`
|
||||||
|
Description string `yaml:"description" validate:"required"`
|
||||||
|
AuthRequired []string `yaml:"authRequired"`
|
||||||
|
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.ToolConfig = Config{}
|
||||||
|
|
||||||
|
func (cfg Config) ToolConfigKind() string {
|
||||||
|
return kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||||
|
// verify source exists
|
||||||
|
rawS, ok := srcs[cfg.Source]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the source is compatible
|
||||||
|
s, ok := rawS.(*lookersrc.Source)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryidParameter := parameters.NewStringParameter("query_id", "The id of the query to render.")
|
||||||
|
// An 8.5 x 11 piece of paper with .25 inch margins works out to 4
|
||||||
|
// quarters each 1200 x 1575, assuming 300 dpi. So that is a good
|
||||||
|
// default for the size of the rendering.
|
||||||
|
// An A4 paper is close enough to accomodate this too.
|
||||||
|
widthParameter := parameters.NewIntParameterWithDefault("width", 1575, "The image width. Default 1575")
|
||||||
|
heightParameter := parameters.NewIntParameterWithDefault("height", 1200, "The image height. Default 1200")
|
||||||
|
formatParameter := parameters.NewStringParameterWithDefault("format", "png", "The image type: png or jpg")
|
||||||
|
|
||||||
|
params := parameters.Parameters{
|
||||||
|
queryidParameter,
|
||||||
|
widthParameter,
|
||||||
|
heightParameter,
|
||||||
|
formatParameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations := cfg.Annotations
|
||||||
|
if annotations == nil {
|
||||||
|
readOnlyHint := true
|
||||||
|
annotations = &tools.ToolAnnotations{
|
||||||
|
ReadOnlyHint: &readOnlyHint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
|
||||||
|
|
||||||
|
// finish tool setup
|
||||||
|
return Tool{
|
||||||
|
Config: cfg,
|
||||||
|
Parameters: params,
|
||||||
|
UseClientOAuth: s.UseClientAuthorization(),
|
||||||
|
AuthTokenHeaderName: s.GetAuthTokenHeaderName(),
|
||||||
|
Client: s.Client,
|
||||||
|
ApiSettings: s.ApiSettings,
|
||||||
|
manifest: tools.Manifest{
|
||||||
|
Description: cfg.Description,
|
||||||
|
Parameters: params.Manifest(),
|
||||||
|
AuthRequired: cfg.AuthRequired,
|
||||||
|
},
|
||||||
|
mcpManifest: mcpManifest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate interface
|
||||||
|
var _ tools.Tool = Tool{}
|
||||||
|
|
||||||
|
type Tool struct {
|
||||||
|
Config
|
||||||
|
UseClientOAuth bool
|
||||||
|
AuthTokenHeaderName string
|
||||||
|
Client *v4.LookerSDK
|
||||||
|
ApiSettings *rtl.ApiSettings
|
||||||
|
Parameters parameters.Parameters `yaml:"parameters"`
|
||||||
|
manifest tools.Manifest
|
||||||
|
mcpManifest tools.McpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ToConfig() tools.ToolConfig {
|
||||||
|
return t.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) {
|
||||||
|
logger, err := util.LoggerFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||||
|
}
|
||||||
|
logger.DebugContext(ctx, "params = ", params)
|
||||||
|
paramsMap := params.AsMap()
|
||||||
|
|
||||||
|
query_id := paramsMap["query_id"].(string)
|
||||||
|
width := int64(paramsMap["width"].(int))
|
||||||
|
height := int64(paramsMap["height"].(int))
|
||||||
|
format := paramsMap["format"].(string)
|
||||||
|
|
||||||
|
var mimeType string
|
||||||
|
switch format {
|
||||||
|
case "jpg":
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
case "png":
|
||||||
|
mimeType = "image/png"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("format \"%s\" unsupported. must be png or jpg", format)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyVis := true
|
||||||
|
|
||||||
|
req := v4.RequestRunQuery{
|
||||||
|
QueryId: query_id,
|
||||||
|
ResultFormat: format,
|
||||||
|
ApplyVis: &applyVis,
|
||||||
|
ImageWidth: &width,
|
||||||
|
ImageHeight: &height,
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk, err := lookercommon.GetLookerSDK(t.UseClientOAuth, t.ApiSettings, t.Client, accessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting sdk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := sdk.RunQuery(req, t.ApiSettings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error making run_query request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookercommon.ReturnImage(mimeType, resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) {
|
||||||
|
return parameters.ParseParams(t.Parameters, data, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Manifest() tools.Manifest {
|
||||||
|
return t.manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) McpManifest() tools.McpManifest {
|
||||||
|
return t.mcpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||||
|
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) bool {
|
||||||
|
return t.UseClientOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) GetAuthTokenHeaderName() string {
|
||||||
|
return t.AuthTokenHeaderName
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2025 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 lookerrenderquery_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
yaml "github.com/goccy/go-yaml"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/server"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||||
|
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerrenderquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFromYamlLookerRenderQuery(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
want server.ToolConfigs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "basic example",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-query
|
||||||
|
source: my-instance
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
want: server.ToolConfigs{
|
||||||
|
"example_tool": lkr.Config{
|
||||||
|
Name: "example_tool",
|
||||||
|
Kind: "looker-render-query",
|
||||||
|
Source: "my-instance",
|
||||||
|
Description: "some description",
|
||||||
|
AuthRequired: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to unmarshal: %s", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||||
|
t.Fatalf("incorrect parse: diff %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailParseFromYamlLookerRenderQuery(t *testing.T) {
|
||||||
|
ctx, err := testutils.ContextWithNewLogger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Invalid method",
|
||||||
|
in: `
|
||||||
|
tools:
|
||||||
|
example_tool:
|
||||||
|
kind: looker-render-query
|
||||||
|
source: my-instance
|
||||||
|
method: GOT
|
||||||
|
description: some description
|
||||||
|
`,
|
||||||
|
err: "unable to parse tool \"example_tool\" as kind \"looker-render-query\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-render-query\n> 4 | method: GOT\n ^\n 5 | source: my-instance",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
got := struct {
|
||||||
|
Tools server.ToolConfigs `yaml:"tools"`
|
||||||
|
}{}
|
||||||
|
// Parse contents
|
||||||
|
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expect parsing to fail")
|
||||||
|
}
|
||||||
|
errStr := err.Error()
|
||||||
|
if !strings.Contains(errStr, tc.err) {
|
||||||
|
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user