feat(tools/looker): New Looker tools for dashboards (#1118)

* get_dashboards
* make_dashboard
* add_dashboard_element
This commit is contained in:
Dr. Strangelove
2025-08-11 21:13:08 +00:00
committed by GitHub
parent 6f55b78e96
commit 42be3f550c
16 changed files with 1718 additions and 11 deletions

View File

@@ -61,6 +61,8 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorequerycollection"
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestorevalidaterules"
_ "github.com/googleapis/genai-toolbox/internal/tools/http"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookeradddashboardelement"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetdashboards"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetdimensions"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetexplores"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetfilters"
@@ -68,6 +70,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmeasures"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmodels"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetparameters"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakedashboard"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakelook"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquery"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"

View File

@@ -1290,7 +1290,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"looker-tools": tools.ToolsetConfig{
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"},
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", "make_dashboard", "add_dashboard_element"},
},
},
},

View File

@@ -0,0 +1,63 @@
---
title: "looker-add-dashboard-element"
type: docs
weight: 1
description: >
"looker-add-dashboard-element" generates a Looker look in the users personal folder in
Looker
aliases:
- /resources/tools/looker-add-dashboard-element
---
## About
The `looker-add-dashboard-element` creates a dashboard element
in the given dashboard.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-add-dashboard-element` takes eleven parameters:
1. the `model`
2. the `explore`
3. the `fields` list
4. an optional set of `filters`
5. an optional set of `pivots`
6. an optional set of `sorts`
7. an optional `limit`
8. an optional `tz`
9. an optional `vis_config`
10. the `title`
11. the `dashboard_id`
## Example
```yaml
tools:
add_dashboard_element:
kind: looker-add-dashboard-element
source: looker-source
description: |
add_dashboard_element Tool
This tool creates a new tile in a Looker dashboard using
the query parameters and the vis_config specified.
Most of the parameters are the same as the query_url
tool. In addition, there is a title that may be provided.
The dashboard_id must be specified. That is obtained
from calling make_dashboard.
This tool can be called many times for one dashboard_id
and the resulting tiles will be added in order.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "looker-add-dashboard-element" |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,60 @@
---
title: "looker-get-dashboards"
type: docs
weight: 1
description: >
"looker-get-dashboards" searches for saved Looks in a Looker
source.
aliases:
- /resources/tools/looker-get-dashboards
---
## About
The `looker-get-dashboards` tool searches for a saved Dashboard by
name or description.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-get-dashboards` takes four parameters, the `title`, `desc`, `limit`
and `offset`.
Title and description use SQL style wildcards and are case insensitive.
Limit and offset are used to page through a larger set of matches and
default to 100 and 0.
## Example
```yaml
tools:
get_dashboards:
kind: looker-get-dashboards
source: looker-source
description: |
get_dashboards Tool
This tool is used to search for saved dashboards in a Looker instance.
String search params use case-insensitive matching. String search
params can contain % and '_' as SQL LIKE pattern match wildcard
expressions. example="dan%" will match "danger" and "Danzig" but
not "David" example="D_m%" will match "Damage" and "dump".
Most search params can accept "IS NULL" and "NOT NULL" as special
expressions to match or exclude (respectively) rows where the
column is null.
The limit and offset are used to paginate the results.
The result of the get_dashboards tool is a list of json objects.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "looker-get-dashboards" |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -0,0 +1,53 @@
---
title: "looker-make-dashboard"
type: docs
weight: 1
description: >
"looker-make-dashboard" generates a Looker dashboard in the users personal folder in
Looker
aliases:
- /resources/tools/looker-make-dashboard
---
## About
The `looker-make-dashboard` creates a dashboard in the user's
Looker personal folder.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-make-dashboard` takes one parameter:
1. the `title`
## Example
```yaml
tools:
make_dashboard:
kind: looker-make-dashboard
source: looker-source
description: |
make_dashboard Tool
This tool creates a new dashboard in Looker. The dashboard is
initially empty and the add_dashboard_element tool is used to
add content to the dashboard.
The newly created dashboard will be created in the user's
personal folder in looker. The dashboard name must be unique.
The result is a json document with a link to the newly
created dashboard and the id of the dashboard. Use the id
when calling add_dashboard_element.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
| kind | string | true | Must be "looker-make-dashboard" |
| source | string | true | Name of the source the SQL should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -18,7 +18,7 @@ It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-make-look` takes eight parameters:
`looker-make-look` takes eleven parameters:
1. the `model`
2. the `explore`

View File

@@ -17,7 +17,7 @@ It's compatible with the following sources:
- [looker](../../sources/looker.md)
`looker-query-url` takes eight parameters:
`looker-query-url` takes nine parameters:
1. the `model`
2. the `explore`
@@ -46,8 +46,121 @@ tools:
parameter.
The vis_config is optional. If provided, it will be used to
control the default visualization for the query. These are
some sample vis_config settings.
control the default visualization for the query. Here are
some notes on making visualizations.
### Cartesian Charts (Area, Bar, Column, Line, Scatter)
These chart types share a large number of configuration options.
**General**
* `type`: The type of visualization (`looker_area`, `looker_bar`, `looker_column`, `looker_line`, `looker_scatter`).
* `series_types`: Override the chart type for individual series.
* `show_view_names`: Display view names in labels and tooltips (`true`/`false`).
* `series_labels`: Provide custom names for series.
**Styling & Colors**
* `colors`: An array of color values to be used for the chart series.
* `series_colors`: A mapping of series names to specific color values.
* `color_application`: Advanced controls for color palette application (collection, palette, reverse, etc.).
* `font_size`: Font size for labels (e.g., '12px').
**Legend**
* `hide_legend`: Show or hide the chart legend (`true`/`false`).
* `legend_position`: Placement of the legend (`'center'`, `'left'`, `'right'`).
**Axes**
* `swap_axes`: Swap the X and Y axes (`true`/`false`).
* `x_axis_scale`: Scale of the x-axis (`'auto'`, `'ordinal'`, `'linear'`, `'time'`).
* `x_axis_reversed`, `y_axis_reversed`: Reverse the direction of an axis (`true`/`false`).
* `x_axis_gridlines`, `y_axis_gridlines`: Display gridlines for an axis (`true`/`false`).
* `show_x_axis_label`, `show_y_axis_label`: Show or hide the axis title (`true`/`false`).
* `show_x_axis_ticks`, `show_y_axis_ticks`: Show or hide axis tick marks (`true`/`false`).
* `x_axis_label`, `y_axis_label`: Set a custom title for an axis.
* `x_axis_datetime_label`: A format string for datetime labels on the x-axis (e.g., `'%Y-%m'`).
* `x_padding_left`, `x_padding_right`: Adjust padding on the ends of the x-axis.
* `x_axis_label_rotation`, `x_axis_label_rotation_bar`: Set rotation for x-axis labels.
* `x_axis_zoom`, `y_axis_zoom`: Enable zooming on an axis (`true`/`false`).
* `y_axes`: An array of configuration objects for multiple y-axes.
**Data & Series**
* `stacking`: How to stack series (`''` for none, `'normal'`, `'percent'`).
* `ordering`: Order of series in a stack (`'none'`, etc.).
* `limit_displayed_rows`: Enable or disable limiting the number of rows displayed (`true`/`false`).
* `limit_displayed_rows_values`: Configuration for the row limit (e.g., `{ "first_last": "first", "show_hide": "show", "num_rows": 10 }`).
* `discontinuous_nulls`: How to render null values in line charts (`true`/`false`).
* `point_style`: Style for points on line and area charts (`'none'`, `'circle'`, `'circle_outline'`).
* `series_point_styles`: Override point styles for individual series.
* `interpolation`: Line interpolation style (`'linear'`, `'monotone'`, `'step'`, etc.).
* `show_value_labels`: Display values on data points (`true`/`false`).
* `label_value_format`: A format string for value labels.
* `show_totals_labels`: Display total labels on stacked charts (`true`/`false`).
* `totals_color`: Color for total labels.
* `show_silhouette`: Display a "silhouette" of hidden series in stacked charts (`true`/`false`).
* `hidden_series`: An array of series names to hide from the visualization.
**Scatter/Bubble Specific**
* `size_by_field`: The field used to determine the size of bubbles.
* `color_by_field`: The field used to determine the color of bubbles.
* `plot_size_by_field`: Whether to display the size-by field in the legend.
* `cluster_points`: Group nearby points into clusters (`true`/`false`).
* `quadrants_enabled`: Display quadrants on the chart (`true`/`false`).
* `quadrant_properties`: Configuration for quadrant labels and colors.
* `custom_quadrant_value_x`, `custom_quadrant_value_y`: Set quadrant boundaries as a percentage.
* `custom_quadrant_point_x`, `custom_quadrant_point_y`: Set quadrant boundaries to a specific value.
**Miscellaneous**
* `reference_lines`: Configuration for displaying reference lines.
* `trend_lines`: Configuration for displaying trend lines.
* `trellis`: Configuration for creating trellis (small multiple) charts.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering interactions.
### Boxplot
* Inherits most of the Cartesian chart options.
* `type`: Must be `looker_boxplot`.
### Funnel
* `type`: Must be `looker_funnel`.
* `orientation`: How data is read (`'automatic'`, `'dataInRows'`, `'dataInColumns'`).
* `percentType`: How percentages are calculated (`'percentOfMaxValue'`, `'percentOfPriorRow'`).
* `labelPosition`, `valuePosition`, `percentPosition`: Placement of labels (`'left'`, `'right'`, `'inline'`, `'hidden'`).
* `labelColor`, `labelColorEnabled`: Set a custom color for labels.
* `labelOverlap`: Allow labels to overlap (`true`/`false`).
* `barColors`: An array of colors for the funnel steps.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
### Pie / Donut
* `type`: Must be `looker_pie`.
* `value_labels`: Where to display values (`'legend'`, `'labels'`).
* `label_type`: The format of data labels (`'labPer'`, `'labVal'`, `'lab'`, `'val'`, `'per'`).
* `start_angle`, `end_angle`: The start and end angles of the pie chart.
* `inner_radius`: The inner radius, used to create a donut chart.
* `series_colors`, `series_labels`: Override colors and labels for specific slices.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
* `advanced_vis_config`: A string containing JSON for advanced Highcharts configuration.
### Waterfall
* Inherits most of the Cartesian chart options.
* `type`: Must be `looker_waterfall`.
* `up_color`: Color for positive (increasing) values.
* `down_color`: Color for negative (decreasing) values.
* `total_color`: Color for the total bar.
### Word Cloud
* `type`: Must be `looker_wordcloud`.
* `rotation`: Enable random word rotation (`true`/`false`).
* `colors`: An array of colors for the words.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
These are some sample vis_config settings.
A bar chart -
{{
@@ -307,13 +420,29 @@ tools:
"y_axis_zoom": true
}}
A single value visualization -
A single record visualization -
{{
"defaults_version": 1,
"show_view_names": false,
"type": "looker_single_record"
}}
A single value visualization -
{{
"comparison_reverse_colors": false,
"comparison_type": "value", "conditional_formatting_include_nulls": false, "conditional_formatting_include_totals": false,
"custom_color": "#1A73E8",
"custom_color_enabled": true,
"defaults_version": 1,
"enable_conditional_formatting": false,
"series_types": {},
"show_comparison": false,
"show_comparison_label": true,
"show_single_value_title": true,
"single_value_title": "Total Clicks",
"type": "single_value"
}}
A Pie chart -
{{
"defaults_version": 1,

View File

@@ -127,8 +127,121 @@ tools:
parameter.
The vis_config is optional. If provided, it will be used to
control the default visualization for the query. These are
some sample vis_config settings.
control the default visualization for the query. Here are
some notes on making visualizations.
### Cartesian Charts (Area, Bar, Column, Line, Scatter)
These chart types share a large number of configuration options.
**General**
* `type`: The type of visualization (`looker_area`, `looker_bar`, `looker_column`, `looker_line`, `looker_scatter`).
* `series_types`: Override the chart type for individual series.
* `show_view_names`: Display view names in labels and tooltips (`true`/`false`).
* `series_labels`: Provide custom names for series.
**Styling & Colors**
* `colors`: An array of color values to be used for the chart series.
* `series_colors`: A mapping of series names to specific color values.
* `color_application`: Advanced controls for color palette application (collection, palette, reverse, etc.).
* `font_size`: Font size for labels (e.g., '12px').
**Legend**
* `hide_legend`: Show or hide the chart legend (`true`/`false`).
* `legend_position`: Placement of the legend (`'center'`, `'left'`, `'right'`).
**Axes**
* `swap_axes`: Swap the X and Y axes (`true`/`false`).
* `x_axis_scale`: Scale of the x-axis (`'auto'`, `'ordinal'`, `'linear'`, `'time'`).
* `x_axis_reversed`, `y_axis_reversed`: Reverse the direction of an axis (`true`/`false`).
* `x_axis_gridlines`, `y_axis_gridlines`: Display gridlines for an axis (`true`/`false`).
* `show_x_axis_label`, `show_y_axis_label`: Show or hide the axis title (`true`/`false`).
* `show_x_axis_ticks`, `show_y_axis_ticks`: Show or hide axis tick marks (`true`/`false`).
* `x_axis_label`, `y_axis_label`: Set a custom title for an axis.
* `x_axis_datetime_label`: A format string for datetime labels on the x-axis (e.g., `'%Y-%m'`).
* `x_padding_left`, `x_padding_right`: Adjust padding on the ends of the x-axis.
* `x_axis_label_rotation`, `x_axis_label_rotation_bar`: Set rotation for x-axis labels.
* `x_axis_zoom`, `y_axis_zoom`: Enable zooming on an axis (`true`/`false`).
* `y_axes`: An array of configuration objects for multiple y-axes.
**Data & Series**
* `stacking`: How to stack series (`''` for none, `'normal'`, `'percent'`).
* `ordering`: Order of series in a stack (`'none'`, etc.).
* `limit_displayed_rows`: Enable or disable limiting the number of rows displayed (`true`/`false`).
* `limit_displayed_rows_values`: Configuration for the row limit (e.g., `{ "first_last": "first", "show_hide": "show", "num_rows": 10 }`).
* `discontinuous_nulls`: How to render null values in line charts (`true`/`false`).
* `point_style`: Style for points on line and area charts (`'none'`, `'circle'`, `'circle_outline'`).
* `series_point_styles`: Override point styles for individual series.
* `interpolation`: Line interpolation style (`'linear'`, `'monotone'`, `'step'`, etc.).
* `show_value_labels`: Display values on data points (`true`/`false`).
* `label_value_format`: A format string for value labels.
* `show_totals_labels`: Display total labels on stacked charts (`true`/`false`).
* `totals_color`: Color for total labels.
* `show_silhouette`: Display a "silhouette" of hidden series in stacked charts (`true`/`false`).
* `hidden_series`: An array of series names to hide from the visualization.
**Scatter/Bubble Specific**
* `size_by_field`: The field used to determine the size of bubbles.
* `color_by_field`: The field used to determine the color of bubbles.
* `plot_size_by_field`: Whether to display the size-by field in the legend.
* `cluster_points`: Group nearby points into clusters (`true`/`false`).
* `quadrants_enabled`: Display quadrants on the chart (`true`/`false`).
* `quadrant_properties`: Configuration for quadrant labels and colors.
* `custom_quadrant_value_x`, `custom_quadrant_value_y`: Set quadrant boundaries as a percentage.
* `custom_quadrant_point_x`, `custom_quadrant_point_y`: Set quadrant boundaries to a specific value.
**Miscellaneous**
* `reference_lines`: Configuration for displaying reference lines.
* `trend_lines`: Configuration for displaying trend lines.
* `trellis`: Configuration for creating trellis (small multiple) charts.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering interactions.
### Boxplot
* Inherits most of the Cartesian chart options.
* `type`: Must be `looker_boxplot`.
### Funnel
* `type`: Must be `looker_funnel`.
* `orientation`: How data is read (`'automatic'`, `'dataInRows'`, `'dataInColumns'`).
* `percentType`: How percentages are calculated (`'percentOfMaxValue'`, `'percentOfPriorRow'`).
* `labelPosition`, `valuePosition`, `percentPosition`: Placement of labels (`'left'`, `'right'`, `'inline'`, `'hidden'`).
* `labelColor`, `labelColorEnabled`: Set a custom color for labels.
* `labelOverlap`: Allow labels to overlap (`true`/`false`).
* `barColors`: An array of colors for the funnel steps.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
### Pie / Donut
* `type`: Must be `looker_pie`.
* `value_labels`: Where to display values (`'legend'`, `'labels'`).
* `label_type`: The format of data labels (`'labPer'`, `'labVal'`, `'lab'`, `'val'`, `'per'`).
* `start_angle`, `end_angle`: The start and end angles of the pie chart.
* `inner_radius`: The inner radius, used to create a donut chart.
* `series_colors`, `series_labels`: Override colors and labels for specific slices.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
* `advanced_vis_config`: A string containing JSON for advanced Highcharts configuration.
### Waterfall
* Inherits most of the Cartesian chart options.
* `type`: Must be `looker_waterfall`.
* `up_color`: Color for positive (increasing) values.
* `down_color`: Color for negative (decreasing) values.
* `total_color`: Color for the total bar.
### Word Cloud
* `type`: Must be `looker_wordcloud`.
* `rotation`: Enable random word rotation (`true`/`false`).
* `colors`: An array of colors for the words.
* `color_application`: Advanced color palette controls.
* `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering.
These are some sample vis_config settings.
A bar chart -
{{
@@ -388,13 +501,29 @@ tools:
"y_axis_zoom": true
}}
A single value visualization -
A single record visualization -
{{
"defaults_version": 1,
"show_view_names": false,
"type": "looker_single_record"
}}
A single value visualization -
{{
"comparison_reverse_colors": false,
"comparison_type": "value", "conditional_formatting_include_nulls": false, "conditional_formatting_include_totals": false,
"custom_color": "#1A73E8",
"custom_color_enabled": true,
"defaults_version": 1,
"enable_conditional_formatting": false,
"series_types": {},
"show_comparison": false,
"show_comparison_label": true,
"show_single_value_title": true,
"single_value_title": "Total Clicks",
"type": "single_value"
}}
A Pie chart -
{{
"defaults_version": 1,
@@ -483,6 +612,59 @@ tools:
The result is a json document with a link to the newly
created look.
get_dashboards:
kind: looker-get-dashboards
source: looker-source
description: |
get_dashboards Tool
This tool is used to search for saved dashboards in a Looker instance.
String search params use case-insensitive matching. String search
params can contain % and '_' as SQL LIKE pattern match wildcard
expressions. example="dan%" will match "danger" and "Danzig" but
not "David" example="D_m%" will match "Damage" and "dump".
Most search params can accept "IS NULL" and "NOT NULL" as special
expressions to match or exclude (respectively) rows where the
column is null.
The limit and offset are used to paginate the results.
The result of the get_dashboards tool is a list of json objects.
make_dashboard:
kind: looker-make-dashboard
source: looker-source
description: |
make_dashboard Tool
This tool creates a new dashboard in Looker. The dashboard is
initially empty and the add_dashboard_element tool is used to
add content to the dashboard.
The newly created dashboard will be created in the user's
personal folder in looker. The dashboard name must be unique.
The result is a json document with a link to the newly
created dashboard and the id of the dashboard. Use the id
when calling add_dashboard_element.
add_dashboard_element:
kind: looker-add-dashboard-element
source: looker-source
description: |
add_dashboard_element Tool
This tool creates a new tile in a Looker dashboard using
the query parameters and the vis_config specified.
Most of the parameters are the same as the query_url
tool. In addition, there is a title that may be provided.
The dashboard_id must be specified. That is obtained
from calling make_dashboard.
This tool can be called many times for one dashboard_id
and the resulting tiles will be added in order.
toolsets:
looker-tools:
- get_models
@@ -497,3 +679,6 @@ toolsets:
- get_looks
- run_look
- make_look
- get_dashboards
- make_dashboard
- add_dashboard_element

View File

@@ -0,0 +1,200 @@
// 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 lookeradddashboardelement
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/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const kind string = "looker-add-dashboard-element"
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"`
}
// 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)
}
parameters := lookercommon.GetQueryParameters()
dashIdParameter := tools.NewStringParameter("dashboard_id", "The id of the dashboard where this tile will exist")
parameters = append(parameters, dashIdParameter)
titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the Dashboard Element")
parameters = append(parameters, titleParameter)
vizParameter := tools.NewMapParameterWithDefault("vis_config",
map[string]any{},
"The visualization config for the query",
"",
)
parameters = append(parameters, vizParameter)
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
return Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.Client,
ApiSettings: s.ApiSettings,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: parameters.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Client *v4.LookerSDK
ApiSettings *rtl.ApiSettings
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
var (
dataType string = "data"
visType string = "vis"
)
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (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)
wq, err := lookercommon.ProcessQueryArgs(ctx, params)
if err != nil {
return nil, fmt.Errorf("error building query request: %w", err)
}
paramsMap := params.AsMap()
dashboard_id := paramsMap["dashboard_id"].(string)
title := paramsMap["title"].(string)
visConfig := paramsMap["vis_config"].(map[string]any)
wq.VisConfig = &visConfig
qrespFields := "id"
qresp, err := t.Client.CreateQuery(*wq, qrespFields, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making create query request: %s", err)
}
wde := v4.WriteDashboardElement{
DashboardId: &dashboard_id,
Title: &title,
QueryId: qresp.Id,
}
switch len(visConfig) {
case 0:
wde.Type = &dataType
default:
wde.Type = &visType
}
fields := ""
req := v4.RequestCreateDashboardElement{
Body: wde,
Fields: &fields,
}
resp, err := t.Client.CreateDashboardElement(req, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making create dashboard element request: %s", err)
}
logger.DebugContext(ctx, "resp = %v", resp)
data := make(map[string]any)
data["result"] = fmt.Sprintf("Dashboard element added to dashboard %s", dashboard_id)
return data, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.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)
}

View 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 lookeradddashboardelement_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/lookeradddashboardelement"
)
func TestParseFromYamlLookerAddDashboardElement(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-add-dashboard-element
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Kind: "looker-add-dashboard-element",
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 TestFailParseFromYamlLookerAddDashboardElement(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-add-dashboard-element
source: my-instance
method: GOT
description: some description
`,
err: "unable to parse tool \"example_tool\" as kind \"looker-add-dashboard-element\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-add-dashboard-element\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)
}
})
}
}

View File

@@ -0,0 +1,188 @@
// 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 lookergetdashboards
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/util"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const kind string = "looker-get-dashboards"
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"`
}
// 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)
}
titleParameter := tools.NewStringParameterWithDefault("title", "", "The title of the dashboard.")
descParameter := tools.NewStringParameterWithDefault("desc", "", "The description of the dashboard.")
limitParameter := tools.NewIntParameterWithDefault("limit", 100, "The number of dashboards to fetch. Default 100")
offsetParameter := tools.NewIntParameterWithDefault("offset", 0, "The number of dashboards to skip before fetching. Default 0")
parameters := tools.Parameters{
titleParameter,
descParameter,
limitParameter,
offsetParameter,
}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
return Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.Client,
ApiSettings: s.ApiSettings,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: parameters.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Client *v4.LookerSDK
ApiSettings *rtl.ApiSettings
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
title_ptr := &title
if *title_ptr == "" {
title_ptr = nil
}
desc := paramsMap["desc"].(string)
desc_ptr := &desc
if *desc_ptr == "" {
desc_ptr = nil
}
limit := int64(paramsMap["limit"].(int))
offset := int64(paramsMap["offset"].(int))
req := v4.RequestSearchDashboards{
Title: title_ptr,
Description: desc_ptr,
Limit: &limit,
Offset: &offset,
}
logger.ErrorContext(ctx, "Making request %v", req)
resp, err := t.Client.SearchDashboards(req, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making get_dashboards request: %s", err)
}
logger.ErrorContext(ctx, "Got response %v", resp)
var data []any
for _, v := range resp {
logger.DebugContext(ctx, "Got response element of %v\n", v)
vMap := make(map[string]any)
if v.Id != nil {
vMap["id"] = *v.Id
}
if v.Title != nil {
vMap["title"] = *v.Title
}
if v.Description != nil {
vMap["description"] = *v.Description
}
logger.DebugContext(ctx, "Converted to %v\n", vMap)
data = append(data, vMap)
}
logger.DebugContext(ctx, "data = ", data)
return data, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.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 true
}

View 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 lookergetdashboards_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/lookergetdashboards"
)
func TestParseFromYamlLookerGetDashboards(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-get-dashboards
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Kind: "looker-get-dashboards",
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 TestFailParseFromYamlLookerGetDashboards(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-get-dashboards
source: my-instance
method: GOT
description: some description
`,
err: "unable to parse tool \"example_tool\" as kind \"looker-get-dashboards\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-get-dashboards\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)
}
})
}
}

View File

@@ -0,0 +1,201 @@
// 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 lookermakedashboard
import (
"context"
"encoding/json"
"fmt"
"slices"
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/util"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const kind string = "looker-make-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"`
}
// 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)
}
parameters := tools.Parameters{}
titleParameter := tools.NewStringParameter("title", "The title of the Dashboard")
parameters = append(parameters, titleParameter)
descParameter := tools.NewStringParameterWithDefault("description", "", "The description of the Dashboard")
parameters = append(parameters, descParameter)
mcpManifest := tools.McpManifest{
Name: cfg.Name,
Description: cfg.Description,
InputSchema: parameters.McpManifest(),
}
// finish tool setup
return Tool{
Name: cfg.Name,
Kind: kind,
Parameters: parameters,
AuthRequired: cfg.AuthRequired,
Client: s.Client,
ApiSettings: s.ApiSettings,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: parameters.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Client *v4.LookerSDK
ApiSettings *rtl.ApiSettings
AuthRequired []string `yaml:"authRequired"`
Parameters tools.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (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)
mrespFields := "id,personal_folder_id"
mresp, err := t.Client.Me(mrespFields, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making me request: %s", err)
}
paramsMap := params.AsMap()
title := paramsMap["title"].(string)
description := paramsMap["description"].(string)
if mresp.PersonalFolderId == nil || *mresp.PersonalFolderId == "" {
return nil, fmt.Errorf("user does not have a personal folder. cannot continue")
}
dashs, err := t.Client.FolderDashboards(*mresp.PersonalFolderId, "title", t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error getting existing dashboards in folder: %s", err)
}
dashTitles := []string{}
for _, dash := range dashs {
dashTitles = append(dashTitles, *dash.Title)
}
if slices.Contains(dashTitles, title) {
lt, _ := json.Marshal(dashTitles)
return nil, fmt.Errorf("title %s already used in user's folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
}
wd := v4.WriteDashboard{
Title: &title,
Description: &description,
FolderId: mresp.PersonalFolderId,
}
resp, err := t.Client.CreateDashboard(wd, t.ApiSettings)
if err != nil {
return nil, fmt.Errorf("error making create dashboard request: %s", err)
}
logger.DebugContext(ctx, "resp = %v", resp)
setting, err := t.Client.GetSetting("host_url", t.ApiSettings)
if err != nil {
logger.ErrorContext(ctx, "error getting settings: %s", err)
}
data := make(map[string]any)
if resp.Id != nil {
data["id"] = *resp.Id
}
if resp.Url != nil {
if setting.HostUrl != nil {
data["url"] = *setting.HostUrl + *resp.Url
} else {
data["url"] = *resp.Url
}
}
logger.DebugContext(ctx, "data = %v", data)
return data, nil
}
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
return tools.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)
}

View 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 lookermakedashboard_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/lookermakedashboard"
)
func TestParseFromYamlLookerMakeDashboard(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-make-dashboard
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Kind: "looker-make-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 TestFailParseFromYamlMakeDashboard(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-make-dashboard
source: my-instance
method: GOT
description: some description
`,
err: "unable to parse tool \"example_tool\" as kind \"looker-make-dashboard\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-make-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)
}
})
}
}

View File

@@ -25,7 +25,7 @@ import (
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakelook"
)
func TestParseFromYamlLookerQuery(t *testing.T) {
func TestParseFromYamlLookerMakeLook(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -73,7 +73,7 @@ func TestParseFromYamlLookerQuery(t *testing.T) {
}
func TestFailParseFromYamlLookerQuery(t *testing.T) {
func TestFailParseFromYamlLookerMakeLook(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)

View File

@@ -120,6 +120,16 @@ func TestLooker(t *testing.T) {
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"get_looks": map[string]any{
"kind": "looker-get-looks",
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"get_dashboards": map[string]any{
"kind": "looker-get-dashboards",
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
},
}
@@ -347,6 +357,266 @@ func TestLooker(t *testing.T) {
},
},
)
tests.RunToolGetTestByName(t, "query_sql",
map[string]any{
"query_sql": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The model containing the explore.",
"name": "model",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The explore to be queried.",
"name": "explore",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The fields to be retrieved.",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be returned in the query",
"name": "field",
"required": true,
"type": "string",
},
"name": "fields",
"required": true,
"type": "array",
},
map[string]any{
"AdditionalProperties": true,
"authSources": []any{},
"description": "The filters for the query",
"name": "filters",
"required": false,
"type": "object",
},
map[string]any{
"authSources": []any{},
"description": "The query pivots (must be included in fields as well).",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be used as a pivot in the query",
"name": "pivot_field",
"required": false,
"type": "string",
},
"name": "pivots",
"required": false,
"type": "array",
},
map[string]any{
"authSources": []any{},
"description": "The sorts like \"field.id desc 0\".",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be used as a sort in the query",
"name": "sort_field",
"required": false,
"type": "string",
},
"name": "sorts",
"required": false,
"type": "array",
},
map[string]any{
"authSources": []any{},
"description": "The row limit.",
"name": "limit",
"required": false,
"type": "integer",
},
map[string]any{
"authSources": []any{},
"description": "The query timezone.",
"name": "tz",
"required": false,
"type": "string",
},
},
},
},
)
tests.RunToolGetTestByName(t, "query_url",
map[string]any{
"query_url": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The model containing the explore.",
"name": "model",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The explore to be queried.",
"name": "explore",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The fields to be retrieved.",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be returned in the query",
"name": "field",
"required": true,
"type": "string",
},
"name": "fields",
"required": true,
"type": "array",
},
map[string]any{
"AdditionalProperties": true,
"authSources": []any{},
"description": "The filters for the query",
"name": "filters",
"required": false,
"type": "object",
},
map[string]any{
"authSources": []any{},
"description": "The query pivots (must be included in fields as well).",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be used as a pivot in the query",
"name": "pivot_field",
"required": false,
"type": "string",
},
"name": "pivots",
"required": false,
"type": "array",
},
map[string]any{
"authSources": []any{},
"description": "The sorts like \"field.id desc 0\".",
"items": map[string]any{
"authSources": []any{},
"description": "A field to be used as a sort in the query",
"name": "sort_field",
"required": false,
"type": "string",
},
"name": "sorts",
"required": false,
"type": "array",
},
map[string]any{
"authSources": []any{},
"description": "The row limit.",
"name": "limit",
"required": false,
"type": "integer",
},
map[string]any{
"authSources": []any{},
"description": "The query timezone.",
"name": "tz",
"required": false,
"type": "string",
},
map[string]any{
"AdditionalProperties": true,
"authSources": []any{},
"description": "The visualization config for the query",
"name": "vis_config",
"required": false,
"type": "object",
},
},
},
},
)
tests.RunToolGetTestByName(t, "get_looks",
map[string]any{
"get_looks": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The title of the look.",
"name": "title",
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The description of the look.",
"name": "desc",
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The number of looks to fetch. Default 100",
"name": "limit",
"required": false,
"type": "integer",
},
map[string]any{
"authSources": []any{},
"description": "The number of looks to skip before fetching. Default 0",
"name": "offset",
"required": false,
"type": "integer",
},
},
},
},
)
tests.RunToolGetTestByName(t, "get_dashboards",
map[string]any{
"get_dashboards": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The title of the dashboard.",
"name": "title",
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The description of the dashboard.",
"name": "desc",
"required": false,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The number of dashboards to fetch. Default 100",
"name": "limit",
"required": false,
"type": "integer",
},
map[string]any{
"authSources": []any{},
"description": "The number of dashboards to skip before fetching. Default 0",
"name": "offset",
"required": false,
"type": "integer",
},
},
},
},
)
wantResult := "{\"label\":\"System Activity\",\"name\":\"system__activity\",\"project_name\":\"system__activity\"}"
tests.RunToolInvokeSimpleTest(t, "get_models", wantResult)
@@ -374,4 +644,11 @@ func TestLooker(t *testing.T) {
wantResult = "system__activity"
tests.RunToolInvokeParametersTest(t, "query_url", []byte(`{"model": "system__activity", "explore": "look", "fields": ["look.count"]}`), wantResult)
// A system that is just being used for testing has no looks or dashboards
wantResult = "null"
tests.RunToolInvokeParametersTest(t, "get_looks", []byte(`{"title": "FOO", "desc": "BAR"}`), wantResult)
wantResult = "null"
tests.RunToolInvokeParametersTest(t, "get_dashboards", []byte(`{"title": "FOO", "desc": "BAR"}`), wantResult)
}