feat(tool/looker): add get_field_value_suggestions tool (#3696)
## Description
This PR implements a new Looker MCP tool `get_field_value_suggestions`
to expose the Looker suggestions API (`GET
/models/{model}/views/{explore}/fields/{field}/suggestions`).
### Tool calls and frequency for a sample prompt
**Prompt**: *"Get the query count from system activity history for
completed runs where the source is 'scheduled'"*
**Before (without suggestions tool)**:
* `mcp_mylocalLookerWithOauth_get_models`: 1 time
* `mcp_mylocalLookerWithOauth_get_explores`: 1 time
* `mcp_mylocalLookerWithOauth_get_dimensions`: 1 time
* `mcp_mylocalLookerWithOauth_get_measures`: 1 time
* `mcp_mylocalLookerWithOauth_query`: 5 times (explored source values,
checked status strings, corrected bad filters, executed query, and
verified output)
**After (with suggestions tool)**:
* `mcp_looker-local_get_models`: 1 time
* `mcp_looker-local_get_explores`: 1 time
* `mcp_looker-local_get_dimensions`: 1 time
* `mcp_looker-local_get_measures`: 1 time
* `mcp_looker-local_get_field_value_suggestions`: 2 times (retrieved
"scheduled_task" and "complete" values)
* `mcp_looker-local_query`: 1 time (executed final query with correct
filters directly)
### Summary of the Solution
- Created the `get_field_value_suggestions` tool wrapping the Looker
SDK.
- Exposed the `suggestable` boolean property in the `get_dimensions`
tool metadata output, allowing LLM planning engines to identify which
dimensions support suggestions lookup.
- Wrapped the returned suggestions list in a JSON object
(`{"suggestions": [...]}`) instead of a raw JSON array to satisfy strict
client schema validation requirements and prevent `structuredContent`
parsing errors.
- Tuned parameter descriptions and prompt suggestions inside the `query`
tool and documentation to guide the LLM planning engine to use this tool
when unsure of valid values.
- Added comprehensive integration tests, unit tests, and validation test
cases.
---
🛠️ Fixes #3695 🦕
---------
Co-authored-by: Mike DeAngelo <drstrangelove@google.com>
This commit is contained in:
@@ -2024,7 +2024,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantGroups: server.GroupConfigs{
|
||||
"looker_tools": group.GroupConfig{
|
||||
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"},
|
||||
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "get_field_value_suggestions", "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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -227,6 +227,7 @@ import (
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetdashboards"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetdimensions"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetexplores"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetfieldvaluesuggestions"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetfilters"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetgitbranch"
|
||||
_ "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetlookmltests"
|
||||
|
||||
@@ -26,6 +26,7 @@ description: "Details of the Looker prebuilt configuration."
|
||||
* `get_measures`: Retrieves the list of measures in an explore.
|
||||
* `get_filters`: Retrieves the list of filters in an explore.
|
||||
* `get_parameters`: Retrieves the list of parameters in an explore.
|
||||
* `get_field_value_suggestions`: Retrieves distinct value suggestions for a field.
|
||||
* `query`: Runs a query against the LookML model.
|
||||
* `query_sql`: Generates the SQL for a query.
|
||||
* `query_url`: Generates a URL for a query in Looker.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "looker-get-field-value-suggestions"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "looker-get-field-value-suggestions" tool retrieves distinct value suggestions
|
||||
for a given field in an explore.
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `looker-get-field-value-suggestions` tool retrieves distinct value suggestions for a given field in an explore.
|
||||
|
||||
## Compatible Sources
|
||||
|
||||
{{< compatible-sources >}}
|
||||
|
||||
## Parameters
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| --------- | :------: | :----------: | --------------------------------------------------- |
|
||||
| model | string | true | The name of the LookML model. |
|
||||
| explore | string | true | The name of the explore containing the field. |
|
||||
| field | string | true | The name of the field to get suggestions for. |
|
||||
| term | string | false | Optional search term pattern to filter suggestions. |
|
||||
| filters | object | false | Optional filters to enable conditional suggestions. |
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
kind: tool
|
||||
name: get_field_value_suggestions
|
||||
type: looker-get-field-value-suggestions
|
||||
source: looker-source
|
||||
description: |
|
||||
This tool retrieves distinct value suggestions for a field, facilitating accurate filtering in downstream queries.
|
||||
|
||||
Required Parameters:
|
||||
- model: The name of the LookML model, obtained from `get_models`.
|
||||
- explore: The name of the explore containing the field, obtained from `get_explores`.
|
||||
- field: The name of the field to get suggestions for, obtained from `get_dimensions`.
|
||||
|
||||
Optional Parameters:
|
||||
- term: Optional search term pattern to filter suggestions. Evaluated as `%term%`.
|
||||
- filters: Optional filters to enable conditional suggestions (restricting suggestions based on other field values), represented as a map of field names to filter expressions, e.g., `{"users.state": "CA", "users.age": ">=60"}`.
|
||||
|
||||
Output:
|
||||
- A JSON object with a "suggestions" key containing an array of string values representing suggestions.
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
The output is a JSON object with a "suggestions" key containing an array of string values.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"suggestions": ["CA", "NY", "TX", "WA"]
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| ----------- | :------: | :----------: | -------------------------------------------------- |
|
||||
| type | string | true | Must be "looker-get-field-value-suggestions". |
|
||||
| source | string | true | Name of the source Looker instance. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
@@ -69,6 +69,7 @@ description: |
|
||||
that can be used directly as filters for that dimension.
|
||||
- If a `suggest_explore` and `suggest_dimension` are provided, you can query
|
||||
that specified explore and dimension to retrieve a list of valid filter values.
|
||||
- If a dimension includes `"suggestable": true`, you can retrieve its valid value suggestions by calling the 'get_field_value_suggestions' tool, passing the dimension's 'name' as the 'field' parameter, alongside the explore and model names.
|
||||
---
|
||||
kind: tool
|
||||
name: get_measures
|
||||
@@ -123,6 +124,25 @@ description: |
|
||||
- explore_name (required): The name of the explore within the model, obtained from `get_explores`.
|
||||
---
|
||||
kind: tool
|
||||
name: get_field_value_suggestions
|
||||
type: looker-get-field-value-suggestions
|
||||
source: looker-source
|
||||
description: |
|
||||
This tool retrieves distinct value suggestions for a field, facilitating accurate filtering in downstream queries.
|
||||
|
||||
Required Parameters:
|
||||
- model: The name of the LookML model, obtained from `get_models`.
|
||||
- explore: The name of the explore containing the field, obtained from `get_explores`.
|
||||
- field: The name of the field to get suggestions for (obtained from a dimension where 'suggestable' is true in the 'get_dimensions' output).
|
||||
|
||||
Optional Parameters:
|
||||
- term: Optional search term pattern to filter suggestions. Evaluated as `%term%`.
|
||||
- filters: Optional filters to enable conditional suggestions, represented as a map of field names to filter expressions, e.g., `{"users.state": "CA"}`.
|
||||
|
||||
Output:
|
||||
- A JSON object with a "suggestions" key containing an array of string values representing suggestions.
|
||||
---
|
||||
kind: tool
|
||||
name: query
|
||||
type: looker-query
|
||||
source: looker-source
|
||||
@@ -140,6 +160,7 @@ description: |
|
||||
- Do not quote field names.
|
||||
- Use `not null` instead of `-NULL`.
|
||||
- If a value contains a comma, enclose it in single quotes (e.g., "'New York, NY'").
|
||||
- To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.
|
||||
- filter_expression: A Looker expression filter string (custom filter). This allows complex logic and comparing fields.
|
||||
- Reference fields using `${view.field_name}` syntax.
|
||||
- Supports logical operators (`AND`, `OR`, `NOT`) and comparison operators.
|
||||
@@ -840,6 +861,7 @@ tools:
|
||||
- get_measures
|
||||
- get_filters
|
||||
- get_parameters
|
||||
- get_field_value_suggestions
|
||||
- query
|
||||
- query_sql
|
||||
- query_url
|
||||
|
||||
@@ -80,6 +80,9 @@ func ExtractLookerFieldProperties(ctx context.Context, fields *[]v4.LookmlModelE
|
||||
if v.Synonyms != nil && len(*v.Synonyms) > 0 {
|
||||
vMap["synonyms"] = *v.Synonyms
|
||||
}
|
||||
if v.Suggestable != nil {
|
||||
vMap["suggestable"] = *v.Suggestable
|
||||
}
|
||||
if v.Suggestable != nil && *v.Suggestable {
|
||||
if v.Suggestions != nil && len(*v.Suggestions) > 0 {
|
||||
vMap["suggestions"] = *v.Suggestions
|
||||
@@ -123,7 +126,9 @@ func GetQueryParameters() parameters.Parameters {
|
||||
"(e.g. \"view.field\") and values are filter expressions or "+
|
||||
"parameter values. Pass values bare — do not wrap them in extra "+
|
||||
"quote characters. For LookML `parameter` fields, use the raw "+
|
||||
"allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"allowed_value (e.g. `first_touch`), not `\"first_touch\"`."+
|
||||
" To retrieve valid filter values for a suggestible field, "+
|
||||
"use the 'get_field_value_suggestions' tool.",
|
||||
"",
|
||||
parameters.WithMapDefault(map[string]any{}),
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ func TestExtractLookerFieldProperties(t *testing.T) {
|
||||
"label": "Dimension Label",
|
||||
"label_short": "Dim Label",
|
||||
"description": "This is a dimension description",
|
||||
"suggestable": true,
|
||||
"suggest_explore": "explore",
|
||||
"suggest_dimension": "dimension",
|
||||
"suggestions": []string{"foo", "bar", "baz"},
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// Copyright 2026 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 lookergetfieldvaluesuggestions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/mcp-toolbox/internal/tools"
|
||||
"github.com/googleapis/mcp-toolbox/internal/tools/looker/lookercommon"
|
||||
"github.com/googleapis/mcp-toolbox/internal/util"
|
||||
"github.com/googleapis/mcp-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 resourceType string = "looker-get-field-value-suggestions"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(resourceType, newConfig) {
|
||||
panic(fmt.Sprintf("tool type %q already registered", resourceType))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{ConfigBase: tools.ConfigBase{Name: name}}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type compatibleSource interface {
|
||||
UseClientAuthorization() bool
|
||||
GetAuthTokenHeaderName() string
|
||||
LookerApiSettings() *rtl.ApiSettings
|
||||
GetLookerSDK(context.Context, string) (*v4.LookerSDK, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
tools.ConfigBase `yaml:",inline"`
|
||||
Type string `yaml:"type" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigType() string {
|
||||
return resourceType
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(context.Context) (tools.Tool, error) {
|
||||
if cfg.Description == "" {
|
||||
return nil, fmt.Errorf("description is required for tool %q", cfg.Name)
|
||||
}
|
||||
|
||||
params := lookercommon.GetFieldParameters()
|
||||
|
||||
// Add field value suggestion specific parameters
|
||||
fieldParam := parameters.NewStringParameter("field", "The name of the field to get suggestions for.")
|
||||
termParam := parameters.NewStringParameter("term", "Optional search term pattern.", parameters.WithStringRequired(false))
|
||||
filtersParam := parameters.NewMapParameter("filters", "Optional filters to enable conditional suggestions (restricting suggestions based on other field values).", "", parameters.WithMapDefault(map[string]any{}))
|
||||
|
||||
params = append(params, fieldParam, termParam, filtersParam)
|
||||
|
||||
return Tool{
|
||||
BaseTool: tools.NewBaseTool(
|
||||
cfg,
|
||||
tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations),
|
||||
tools.Manifest{Description: cfg.Description, Parameters: params.Manifest(), AuthRequired: cfg.AuthRequired},
|
||||
params,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
tools.BaseTool[Config]
|
||||
}
|
||||
|
||||
func (t Tool) ToConfig() tools.ToolConfig {
|
||||
return t.Cfg
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, primitiveMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
|
||||
source, err := tools.GetCompatibleSource[compatibleSource](primitiveMgr, t.Cfg.Source, t.Cfg.Name, t.Cfg.Type)
|
||||
if err != nil {
|
||||
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
mapParams := params.AsMap()
|
||||
|
||||
model, ok := mapParams["model"].(string)
|
||||
if !ok {
|
||||
return nil, util.NewAgentError("model is required and must be a string", nil)
|
||||
}
|
||||
|
||||
explore, ok := mapParams["explore"].(string)
|
||||
if !ok {
|
||||
return nil, util.NewAgentError("explore is required and must be a string", nil)
|
||||
}
|
||||
|
||||
field, ok := mapParams["field"].(string)
|
||||
if !ok {
|
||||
return nil, util.NewAgentError("field is required and must be a string", nil)
|
||||
}
|
||||
|
||||
var termPtr *string
|
||||
if val, ok := mapParams["term"].(string); ok && val != "" {
|
||||
termPtr = &val
|
||||
}
|
||||
|
||||
filters, _ := mapParams["filters"].(map[string]any)
|
||||
|
||||
sdk, err := source.GetLookerSDK(ctx, string(accessToken))
|
||||
if err != nil {
|
||||
return nil, util.NewClientServerError("error getting Looker SDK", http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
req := v4.RequestModelFieldnameSuggestions{
|
||||
ModelName: model,
|
||||
ViewName: explore, // Map 'explore' back to 'ViewName'
|
||||
FieldName: field,
|
||||
Term: termPtr,
|
||||
}
|
||||
if len(filters) > 0 {
|
||||
var f interface{} = filters
|
||||
req.Filters = &f
|
||||
}
|
||||
|
||||
resp, err := sdk.ModelFieldnameSuggestions(req, source.LookerApiSettings())
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "status=401") {
|
||||
return nil, util.NewClientServerError("unauthorized error", http.StatusUnauthorized, err)
|
||||
}
|
||||
return nil, util.ProcessGeneralError(err)
|
||||
}
|
||||
|
||||
if resp.Error != nil && *resp.Error != "" {
|
||||
return nil, util.NewAgentError(fmt.Sprintf("Looker API error: %s", *resp.Error), nil)
|
||||
}
|
||||
|
||||
logger.DebugContext(ctx, "suggestions = ", resp.Suggestions)
|
||||
|
||||
if resp.Suggestions == nil {
|
||||
return map[string]any{"suggestions": []string{}}, nil
|
||||
}
|
||||
|
||||
return map[string]any{"suggestions": *resp.Suggestions}, nil
|
||||
}
|
||||
|
||||
func (t Tool) RequiresClientAuthorization(primitiveMgr tools.SourceProvider) (bool, error) {
|
||||
source, err := tools.GetCompatibleSource[compatibleSource](primitiveMgr, t.Cfg.Source, t.Cfg.Name, t.Cfg.Type)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return source.UseClientAuthorization(), nil
|
||||
}
|
||||
|
||||
func (t Tool) GetAuthTokenHeaderName(primitiveMgr tools.SourceProvider) (string, error) {
|
||||
source, err := tools.GetCompatibleSource[compatibleSource](primitiveMgr, t.Cfg.Source, t.Cfg.Name, t.Cfg.Type)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return source.GetAuthTokenHeaderName(), nil
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright 2026 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 lookergetfieldvaluesuggestions_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/mcp-toolbox/internal/server"
|
||||
"github.com/googleapis/mcp-toolbox/internal/testutils"
|
||||
"github.com/googleapis/mcp-toolbox/internal/tools"
|
||||
lkr "github.com/googleapis/mcp-toolbox/internal/tools/looker/lookergetfieldvaluesuggestions"
|
||||
)
|
||||
|
||||
func TestParseFromYamlLookerGetFieldValueSuggestions(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: `
|
||||
kind: tool
|
||||
name: example_tool
|
||||
type: looker-get-field-value-suggestions
|
||||
source: my-instance
|
||||
description: some description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": lkr.Config{
|
||||
ConfigBase: tools.ConfigBase{
|
||||
Name: "example_tool",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
Type: "looker-get-field-value-suggestions",
|
||||
Source: "my-instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Parse contents
|
||||
_, _, _, got, _, _, err := server.UnmarshalPrimitiveConfig(ctx, testutils.FormatYaml(tc.in))
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailParseFromYamlLookerGetFieldValueSuggestions(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 field",
|
||||
in: `
|
||||
kind: tool
|
||||
name: example_tool
|
||||
type: looker-get-field-value-suggestions
|
||||
source: my-instance
|
||||
method: GOT
|
||||
description: some description
|
||||
`,
|
||||
err: "error unmarshaling tool: unable to parse tool \"example_tool\" as type \"looker-get-field-value-suggestions\": [3:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n> 3 | method: GOT\n ^\n 4 | name: example_tool\n 5 | source: my-instance\n 6 | type: looker-get-field-value-suggestions",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Parse contents
|
||||
_, _, _, _, _, _, err := server.UnmarshalPrimitiveConfig(ctx, testutils.FormatYaml(tc.in))
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -108,6 +108,11 @@ func TestLooker(t *testing.T) {
|
||||
"source": "my-instance",
|
||||
"description": "Simple tool to test end to end functionality.",
|
||||
},
|
||||
"get_field_value_suggestions": map[string]any{
|
||||
"type": "looker-get-field-value-suggestions",
|
||||
"source": "my-instance",
|
||||
"description": "Simple tool to test end to end functionality.",
|
||||
},
|
||||
"get_measures": map[string]any{
|
||||
"type": "looker-get-measures",
|
||||
"source": "my-instance",
|
||||
@@ -410,6 +415,53 @@ func TestLooker(t *testing.T) {
|
||||
},
|
||||
},
|
||||
)
|
||||
tests.RunToolGetTestByName(t, "get_field_value_suggestions",
|
||||
map[string]any{
|
||||
"get_field_value_suggestions": map[string]any{
|
||||
"description": "Simple tool to test end to end functionality.",
|
||||
"authRequired": []any{},
|
||||
"parameters": []any{
|
||||
map[string]any{
|
||||
"authServices": []any{},
|
||||
"description": "The model containing the explore.",
|
||||
"name": "model",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
map[string]any{
|
||||
"authServices": []any{},
|
||||
"description": "The explore containing the fields.",
|
||||
"name": "explore",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
map[string]any{
|
||||
"authServices": []any{},
|
||||
"description": "The name of the field to get suggestions for.",
|
||||
"name": "field",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
},
|
||||
map[string]any{
|
||||
"authServices": []any{},
|
||||
"description": "Optional search term pattern.",
|
||||
"name": "term",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
},
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"default": map[string]any{},
|
||||
"description": "Optional filters to enable conditional suggestions (restricting suggestions based on other field values).",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
tests.RunToolGetTestByName(t, "get_measures",
|
||||
map[string]any{
|
||||
"get_measures": map[string]any{
|
||||
@@ -519,7 +571,7 @@ func TestLooker(t *testing.T) {
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`. To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"default": map[string]any{},
|
||||
@@ -634,7 +686,7 @@ func TestLooker(t *testing.T) {
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`. To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"default": map[string]any{},
|
||||
@@ -749,7 +801,7 @@ func TestLooker(t *testing.T) {
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`. To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"default": map[string]any{},
|
||||
@@ -1024,7 +1076,7 @@ func TestLooker(t *testing.T) {
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`. To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"default": map[string]any{},
|
||||
@@ -1346,7 +1398,7 @@ func TestLooker(t *testing.T) {
|
||||
map[string]any{
|
||||
"additionalProperties": true,
|
||||
"authServices": []any{},
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`.",
|
||||
"description": "The filters for the query. Keys are fully-qualified field names (e.g. \"view.field\") and values are filter expressions or parameter values. Pass values bare — do not wrap them in extra quote characters. For LookML `parameter` fields, use the raw allowed_value (e.g. `first_touch`), not `\"first_touch\"`. To retrieve valid filter values for a suggestible field, use the 'get_field_value_suggestions' tool.",
|
||||
"name": "filters",
|
||||
"required": false,
|
||||
"default": map[string]any{},
|
||||
@@ -2031,10 +2083,26 @@ func TestLooker(t *testing.T) {
|
||||
wantResult = "{\"description\":\"Data about Look and dashboard usage, including frequency of views, favoriting, scheduling, embedding, and access via the API. Also includes details about individual Looks and dashboards.\",\"group_label\":\"System Activity\",\"label\":\"Content Usage\",\"name\":\"content_usage\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_explores", []byte(`{"model": "system__activity"}`), wantResult)
|
||||
|
||||
wantResult = "{\"description\":\"Number of times this content has been viewed via the Looker API\",\"label\":\"Content Usage API Count\",\"label_short\":\"API Count\",\"name\":\"content_usage.api_count\",\"type\":\"number\"}"
|
||||
wantResult = "{\"description\":\"\",\"label\":\" Dashboard Linked Looks ID\",\"label_short\":\" ID\",\"name\":\"_dashboard_linked_looks._id\",\"suggest_dimension\":\"_dashboard_linked_looks._id\",\"suggest_explore\":\"content_usage\",\"suggestable\":true,\"type\":\"string\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_dimensions", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult)
|
||||
|
||||
wantResult = "{\"description\":\"The total number of views via the Looker API\",\"label\":\"Content Usage API Total\",\"label_short\":\"API Total\",\"name\":\"content_usage.api_total\",\"type\":\"sum\"}"
|
||||
// Verify the suggestions output structure is wrapped in a JSON Object
|
||||
wantResult = `{"suggestions":`
|
||||
tests.RunToolInvokeParametersTest(t, "get_field_value_suggestions", []byte(`{"model": "system__activity", "explore": "history", "field": "history.source"}`), wantResult)
|
||||
|
||||
// Verify that the suggestions list contains the expected value
|
||||
wantResult = "{\"suggestions\":[\"api4\",\"dashboard\",\"explore\",\"merge_query\",\"regenerator\",\"sqlrunner\",\"suggest\"]}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_field_value_suggestions", []byte(`{"model": "system__activity", "explore": "history", "field": "history.source"}`), wantResult)
|
||||
|
||||
// Verify that search term filtering works
|
||||
wantResult = "{\"suggestions\":[\"api4\"]}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_field_value_suggestions", []byte(`{"model": "system__activity", "explore": "history", "field": "history.source", "term": "ap"}`), wantResult)
|
||||
|
||||
// Verify that conditional filtering based on other fields works
|
||||
wantResult = "{\"suggestions\":[\"api4\",\"dashboard\",\"explore\",\"merge_query\",\"regenerator\",\"sqlrunner\",\"suggest\"]}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_field_value_suggestions", []byte(`{"model": "system__activity", "explore": "history", "field": "history.source", "filters": {"history.status": "complete"}}`), wantResult)
|
||||
|
||||
wantResult = "{\"description\":\"\",\"label\":\"API Usage\",\"label_short\":\"API Usage\",\"name\":\"turtle::api_usage\",\"suggestable\":false,\"type\":\"turtle_look\"}"
|
||||
tests.RunToolInvokeParametersTest(t, "get_measures", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult)
|
||||
|
||||
wantResult = "[]"
|
||||
|
||||
Reference in New Issue
Block a user