Skip to content

Commit 70ed8a0

Browse files
feat(tool/looker): git_branch tools for looker. (#2718)
## Description This tool is used to fetch and manipulate the git branch of a LookML project. ## PR Checklist > Thank you for opening a Pull Request! Before submitting your PR, there are a > few things you can do to make sure it goes smoothly: - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #2678
1 parent e350fc7 commit 70ed8a0

8 files changed

Lines changed: 491 additions & 2 deletions

File tree

cmd/internal/imports.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import (
134134
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfile"
135135
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfiles"
136136
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojects"
137+
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergitbranch"
137138
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthanalyze"
138139
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthpulse"
139140
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerhealthvacuum"

cmd/internal/tools_file_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1926,7 +1926,7 @@ func TestPrebuiltTools(t *testing.T) {
19261926
wantToolset: server.ToolsetConfigs{
19271927
"looker_dev_tools": tools.ToolsetConfig{
19281928
Name: "looker_dev_tools",
1929-
ToolNames: []string{"health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns", "get_lookml_tests", "run_lookml_tests", "create_view_from_table"},
1929+
ToolNames: []string{"health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_project_directories", "create_project_directory", "delete_project_directory", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns", "get_lookml_tests", "run_lookml_tests", "create_view_from_table", "project_git_branch"},
19301930
},
19311931
},
19321932
},

docs/en/reference/prebuilt-tools.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ See [Usage Examples](../reference/cli.md#examples).
587587
* `get_lookml_tests`: Retrieves a list of available LookML tests for a project.
588588
* `run_lookml_tests`: Executes specific LookML tests within a project.
589589
* `create_view_from_table`: Generates boilerplate LookML views directly from the database schema.
590+
* `project_git_branch`: Fetch and manipulate the git branch of a LookML project.
590591

591592
## Looker Conversational Analytics
592593

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
title: "looker-git-branch"
3+
type: docs
4+
weight: 1
5+
description: >
6+
A "looker-git-branch" tool is used to retrieve and manipulate the git branch
7+
of a LookML project.
8+
aliases:
9+
- /resources/tools/looker-git-branch
10+
---
11+
12+
## About
13+
14+
A `looker-git-branch` tool is used to retrieve and manipulate the git branch
15+
of a LookML project.
16+
17+
It's compatible with the following sources:
18+
19+
- [looker](../../sources/looker.md)
20+
21+
`looker-git-branch` requires two parameters, the LookML `project_id` and the
22+
`operation`. The operation must be one of `list`, `get`, `create`, `switch`,
23+
or `delete`.
24+
25+
The `list` operation retrieves the list of available branches. The `get`
26+
operation retrieves the current branch.
27+
28+
`create`, `switch` and `delete` all require an additional parameter, the
29+
`branch` name. The `create` operation creates a new branch. The `switch`
30+
operation switches to the specified branch. The `delete` operation deletes
31+
the specified branch.
32+
33+
`create` and `switch` can both use an additional `ref` parameter, which is
34+
the git ref that the branch should be at. If it isn't specified, `create`
35+
will start with the ref of the current branch. `switch` will start with the
36+
HEAD of that branch. Specifying `ref` will do the equivalent of `reset --hard`
37+
on the branch.
38+
39+
## Example
40+
```yaml
41+
kind: tools
42+
name: project_git_branch
43+
type: looker-git-branch
44+
source: looker-source
45+
description: |
46+
This tool is used to retrieve and manipulate the git branch of a LookML
47+
project.
48+
49+
An operation id must be provided which is one of the following:
50+
* `list` - List all the available branch names.
51+
* `get` - Get the current branch name.
52+
* `create` - Create a new branch. The branch is initial set to the current
53+
ref, unless ref is specified. This only works in dev mode.
54+
* `switch` - Change the branch to the given branch, and update to the given
55+
ref if specified. This only works in dev mode.
56+
* `delete` - Delete a branch. This only works in dev mode.
57+
58+
Parameters:
59+
- project_id (required): The unique ID of the LookML project.
60+
- operation (required): One of `list`, `get`, `create`, `switch`, or `delete`.
61+
- branch (optional): The branch to create, switch to, or delete.
62+
- ref (optional): The ref to start a newly created branch, or change a branch
63+
with `reset --hard` on a switch operation.
64+
```
65+
66+
## Reference
67+
68+
| **field** | **type** | **required** | **description** |
69+
|-------------|:--------:|:------------:|----------------------------------------------------|
70+
| type | string | true | Must be "looker-git-branch". |
71+
| source | string | true | Name of the source the SQL should execute on. |
72+
| description | string | true | Description of the tool that is passed to the LLM. |

internal/prebuiltconfigs/tools/looker-dev.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,25 @@ tools:
388388
Output:
389389
A confirmation message upon successful view generation, or an error message if the operation fails.
390390
391+
project_git_branch:
392+
kind: looker-git-branch
393+
source: looker-source
394+
description: |
395+
This tool is used to retrieve and manipulate the git branch of a LookML project.
396+
397+
An operation id must be provided which is one of the following:
398+
* `list` - List all the available branch names.
399+
* `get` - Get the current branch name.
400+
* `create` - Create a new branch. The branch is initial set to the current ref, unless ref is specified. This only works in dev mode.
401+
* `switch` - Change the branch to the given branch, and update to the given ref if specified. This only works in dev mode.
402+
* `delete` - Delete a branch. This only works in dev mode.
403+
404+
Parameters:
405+
- project_id (required): The unique ID of the LookML project.
406+
- operation (required): One of `list`, `get`, `create`, `switch`, or `delete`.
407+
- branch (optional): The branch to create, switch to, or delete.
408+
- ref (optional): The ref to start a newly created branch, or change a branch with `reset --hard` on a switch operation.
409+
391410
toolsets:
392411
looker_dev_tools:
393412
- health_pulse
@@ -412,3 +431,4 @@ toolsets:
412431
- get_lookml_tests
413432
- run_lookml_tests
414433
- create_view_from_table
434+
- project_git_branch
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package lookergitbranch
15+
16+
import (
17+
"context"
18+
"fmt"
19+
"net/http"
20+
21+
yaml "github.com/goccy/go-yaml"
22+
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
23+
"github.com/googleapis/genai-toolbox/internal/sources"
24+
"github.com/googleapis/genai-toolbox/internal/tools"
25+
"github.com/googleapis/genai-toolbox/internal/util"
26+
"github.com/googleapis/genai-toolbox/internal/util/parameters"
27+
28+
"github.com/looker-open-source/sdk-codegen/go/rtl"
29+
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
30+
)
31+
32+
const resourceType string = "looker-git-branch"
33+
34+
func init() {
35+
if !tools.Register(resourceType, newConfig) {
36+
panic(fmt.Sprintf("tool type %q already registered", resourceType))
37+
}
38+
}
39+
40+
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
41+
actual := Config{Name: name}
42+
if err := decoder.DecodeContext(ctx, &actual); err != nil {
43+
return nil, err
44+
}
45+
return actual, nil
46+
}
47+
48+
type compatibleSource interface {
49+
UseClientAuthorization() bool
50+
GetAuthTokenHeaderName() string
51+
LookerApiSettings() *rtl.ApiSettings
52+
GetLookerSDK(string) (*v4.LookerSDK, error)
53+
}
54+
55+
type Config struct {
56+
Name string `yaml:"name" validate:"required"`
57+
Type string `yaml:"type" validate:"required"`
58+
Source string `yaml:"source" validate:"required"`
59+
Description string `yaml:"description" validate:"required"`
60+
AuthRequired []string `yaml:"authRequired"`
61+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
62+
}
63+
64+
// validate interface
65+
var _ tools.ToolConfig = Config{}
66+
67+
func (cfg Config) ToolConfigType() string {
68+
return resourceType
69+
}
70+
71+
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
72+
projectIdParameter := parameters.NewStringParameter("project_id", "The project_id")
73+
operationParameter := parameters.NewStringParameter("operation", "The operation, one of `list`, `get`, `create`, `switch`, or `delete`")
74+
branchParameter := parameters.NewStringParameterWithDefault("branch", "", "The git branch on which to operate. Not required for `list` or `get` operations.")
75+
refParameter := parameters.NewStringParameterWithDefault("ref", "", "The ref to use as the start of a new branch. If not specified for a `create` operation it will default to HEAD of current branch. If supplied with a `switch` operation will `reset --hard` the branch.")
76+
params := parameters.Parameters{projectIdParameter, operationParameter, branchParameter, refParameter}
77+
78+
annotations := cfg.Annotations
79+
80+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
81+
82+
// finish tool setup
83+
return Tool{
84+
Config: cfg,
85+
Parameters: params,
86+
manifest: tools.Manifest{
87+
Description: cfg.Description,
88+
Parameters: params.Manifest(),
89+
AuthRequired: cfg.AuthRequired,
90+
},
91+
mcpManifest: mcpManifest,
92+
}, nil
93+
}
94+
95+
// validate interface
96+
var _ tools.Tool = Tool{}
97+
98+
type Tool struct {
99+
Config
100+
Parameters parameters.Parameters `yaml:"parameters"`
101+
manifest tools.Manifest
102+
mcpManifest tools.McpManifest
103+
}
104+
105+
func (t Tool) ToConfig() tools.ToolConfig {
106+
return t.Config
107+
}
108+
109+
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
110+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
111+
if err != nil {
112+
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
113+
}
114+
115+
logger, err := util.LoggerFromContext(ctx)
116+
if err != nil {
117+
return nil, util.NewClientServerError("unable to get logger from ctx", http.StatusInternalServerError, err)
118+
}
119+
120+
sdk, err := source.GetLookerSDK(string(accessToken))
121+
if err != nil {
122+
return nil, util.NewClientServerError(fmt.Sprintf("error getting sdk: %v", err), http.StatusInternalServerError, err)
123+
}
124+
125+
mapParams := params.AsMap()
126+
logger.DebugContext(ctx, "looker_git_branch params = ", mapParams)
127+
projectId := mapParams["project_id"].(string)
128+
operation := mapParams["operation"].(string)
129+
branch := mapParams["branch"].(string)
130+
ref := mapParams["ref"].(string)
131+
132+
switch operation {
133+
case "list":
134+
resp, err := sdk.AllGitBranches(projectId, source.LookerApiSettings())
135+
if err != nil {
136+
return nil, util.NewClientServerError(fmt.Sprintf("error making list_git_branches request: %s", err), http.StatusInternalServerError, err)
137+
}
138+
return resp, nil
139+
case "get":
140+
resp, err := sdk.GitBranch(projectId, source.LookerApiSettings())
141+
if err != nil {
142+
return nil, util.NewClientServerError(fmt.Sprintf("error making get_git_branch request: %s", err), http.StatusInternalServerError, err)
143+
}
144+
return resp, nil
145+
case "create":
146+
if branch == "" {
147+
return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil)
148+
}
149+
body := v4.WriteGitBranch{
150+
Name: &branch,
151+
}
152+
if ref != "" {
153+
body.Ref = &ref
154+
}
155+
resp, err := sdk.CreateGitBranch(projectId, body, source.LookerApiSettings())
156+
if err != nil {
157+
return nil, util.NewClientServerError(fmt.Sprintf("error making create_git_branch request: %s", err), http.StatusInternalServerError, err)
158+
}
159+
return resp, nil
160+
case "switch":
161+
if branch == "" {
162+
return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil)
163+
}
164+
body := v4.WriteGitBranch{
165+
Name: &branch,
166+
}
167+
if ref != "" {
168+
body.Ref = &ref
169+
}
170+
resp, err := sdk.UpdateGitBranch(projectId, body, source.LookerApiSettings())
171+
if err != nil {
172+
return nil, util.NewClientServerError(fmt.Sprintf("error making update_git_branch request: %s", err), http.StatusInternalServerError, err)
173+
}
174+
return resp, nil
175+
case "delete":
176+
if branch == "" {
177+
return nil, util.NewClientServerError(fmt.Sprintf("%s operation: branch must be specified", operation), http.StatusInternalServerError, nil)
178+
}
179+
_, err := sdk.DeleteGitBranch(projectId, branch, source.LookerApiSettings())
180+
if err != nil {
181+
return nil, util.NewClientServerError(fmt.Sprintf("error making delete_git_branch request: %s", err), http.StatusInternalServerError, err)
182+
}
183+
return fmt.Sprintf("Deleted branch %s", branch), nil
184+
default:
185+
return nil, util.NewClientServerError(fmt.Sprintf("unknown operation: %s. Must be one of `list`, `get`, `create`, `switch`, or `delete`", operation), http.StatusInternalServerError, nil)
186+
}
187+
}
188+
189+
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
190+
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
191+
}
192+
193+
func (t Tool) Manifest() tools.Manifest {
194+
return t.manifest
195+
}
196+
197+
func (t Tool) McpManifest() tools.McpManifest {
198+
return t.mcpManifest
199+
}
200+
201+
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
202+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
203+
if err != nil {
204+
return false, err
205+
}
206+
return source.UseClientAuthorization(), nil
207+
}
208+
209+
func (t Tool) Authorized(verifiedAuthServices []string) bool {
210+
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
211+
}
212+
213+
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
214+
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
215+
if err != nil {
216+
return "", err
217+
}
218+
return source.GetAuthTokenHeaderName(), nil
219+
}
220+
221+
func (t Tool) GetParameters() parameters.Parameters {
222+
return t.Parameters
223+
}

0 commit comments

Comments
 (0)