Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,35 @@ func (t Tool) ToConfig() tools.ToolConfig {
return t.Cfg
}

// parseExploreReferences converts the raw explore_references parameter into typed
// references. The parameter is declared as an array of free-form maps, so the values
// the model supplies are not schema-validated; guard every field access instead of
// letting an unexpected shape (a missing key, a non-string value, or a non-object
// element) panic the tool. Mirrors the validation in datalineagesearchlineage.
func parseExploreReferences(raw []any, lookerInstanceURI string) ([]LookerExploreReference, util.ToolboxError) {
refs := make([]LookerExploreReference, 0, len(raw))
for i, er := range raw {
m, ok := er.(map[string]any)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("invalid explore reference at index %d in 'explore_references': expected object, got %T", i, er), nil)
}
model, ok := m["model"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("missing or invalid 'model' (expected string) in 'explore_references' at index %d", i), nil)
}
explore, ok := m["explore"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("missing or invalid 'explore' (expected string) in 'explore_references' at index %d", i), nil)
}
refs = append(refs, LookerExploreReference{
LookerInstanceUri: lookerInstanceURI,
LookmlModel: model,
Explore: explore,
})
}
return refs, nil
}

func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Cfg.Source, t.Cfg.Name, t.Cfg.Type)
if err != nil {
Expand Down Expand Up @@ -206,13 +235,9 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para
userQuery, _ := mapParams["user_query_with_context"].(string)
exploreReferences, _ := mapParams["explore_references"].([]any)

ler := make([]LookerExploreReference, 0)
for _, er := range exploreReferences {
ler = append(ler, LookerExploreReference{
LookerInstanceUri: source.LookerApiSettings().BaseUrl,
LookmlModel: er.(map[string]any)["model"].(string),
Explore: er.(map[string]any)["explore"].(string),
})
ler, lerErr := parseExploreReferences(exploreReferences, source.LookerApiSettings().BaseUrl)
if lerErr != nil {
return nil, lerErr
}
oauth_creds := OAuthCredentials{}
if source.UseClientAuthorization() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 lookerconversationalanalytics

import "testing"

func TestParseExploreReferences(t *testing.T) {
const baseURL = "https://looker.example.com"

t.Run("valid references are parsed", func(t *testing.T) {
raw := []any{
map[string]any{"model": "m1", "explore": "e1"},
map[string]any{"model": "m2", "explore": "e2"},
}
got, err := parseExploreReferences(raw, baseURL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != 2 {
t.Fatalf("expected 2 references, got %d", len(got))
}
if got[0].LookmlModel != "m1" || got[0].Explore != "e1" || got[0].LookerInstanceUri != baseURL {
t.Fatalf("unexpected first reference: %+v", got[0])
}
})

t.Run("nil input yields an empty slice without error", func(t *testing.T) {
got, err := parseExploreReferences(nil, baseURL)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != 0 {
t.Fatalf("expected no references, got %d", len(got))
}
})

// A model that hallucinates the explore_references shape (a non-object element,
// a missing field, or a non-string value) must surface a clean agent error
// rather than panic the tool on an unchecked type assertion.
errCases := []struct {
desc string
raw []any
}{
{"element is not an object", []any{"not-a-map"}},
{"missing model", []any{map[string]any{"explore": "e1"}}},
{"model is not a string", []any{map[string]any{"model": 123, "explore": "e1"}}},
{"missing explore", []any{map[string]any{"model": "m1"}}},
{"explore is not a string", []any{map[string]any{"model": "m1", "explore": 7}}},
}
for _, tc := range errCases {
t.Run(tc.desc, func(t *testing.T) {
if _, err := parseExploreReferences(tc.raw, baseURL); err == nil {
t.Fatalf("expected an error for %q, got nil", tc.desc)
}
})
}
}
Loading