Compare commits

..

27 Commits

Author SHA1 Message Date
rahulpinto19
b49f297a93 removed test file 2026-02-17 10:42:19 +00:00
rahulpinto19
51a854209d test2 2026-02-17 10:38:31 +00:00
rahulpinto19
a1bc7daf68 test 2026-02-17 10:33:13 +00:00
rahulpinto19
07c5e91fda improved deleting 2026-02-17 10:30:14 +00:00
rahulpinto19
62b64148bb add delete comment 2026-02-17 10:20:52 +00:00
rahulpinto19
47f5683745 improved comment 2026-02-17 10:13:37 +00:00
rahulpinto19
e85b827b1e removed delete 2026-02-17 10:02:59 +00:00
rahulpinto19
d22851388b improved prepare report 2026-02-17 10:01:26 +00:00
rahulpinto19
bed9c66113 make the font small 2026-02-17 09:53:57 +00:00
rahulpinto19
6191e2c203 injected errrors 2026-02-17 09:47:15 +00:00
rahulpinto19
99e84dd393 filename with space handled 2026-02-17 09:29:19 +00:00
rahulpinto19
1b5d55b9ca handled space with file name 2026-02-17 07:59:34 +00:00
rahulpinto19
81f7e479af add file name with the spaces 2026-02-17 07:29:41 +00:00
rahulpinto19
600c8a3a8a ordered correct 2026-02-17 07:02:39 +00:00
rahulpinto19
17fe418aee improved the delete logic 2026-02-17 06:45:05 +00:00
rahulpinto19
a950b54fad delete the comment on the success 2026-02-17 06:37:49 +00:00
rahulpinto19
df6f0818f7 fix-links 2026-02-17 06:28:30 +00:00
dishaprakash
58d38a07f0 docs(Go SDK): Update documentation for the refactored Go SDK (#2479)
## Description

This PR updates the documentation after the refactor of the Go SDK into
a multi-module structure

## 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)
- [ ] 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)
- [ ] Make sure to add `!` if this involve a breaking change

🛠️ Fixes #<issue_number_goes_here>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-17 06:12:36 +00:00
rahulpinto19
636cfb7600 removed duplicacy 2026-02-16 13:28:44 +00:00
rahulpinto19
a0f511dc38 removed redirect links by scrub 2026-02-16 13:21:30 +00:00
rahulpinto19
2fa1803fde accept 200 links 2026-02-16 12:49:35 +00:00
rahulpinto19
4424086c94 add comment id 2026-02-16 12:22:26 +00:00
rahulpinto19
538287097b find comments 2026-02-16 12:16:26 +00:00
rahulpinto19
5f11440639 check rewrite 2026-02-16 12:16:26 +00:00
rahulpinto19
bb2a23ff0e test comment in conversation 2026-02-16 12:16:26 +00:00
rahulpinto19
1c3c613860 add a pr comment 2026-02-16 12:16:26 +00:00
rahulpinto19
c5bee2128e docs: optimize link checker 2026-02-16 12:16:25 +00:00
30 changed files with 157 additions and 1313 deletions

View File

@@ -18,9 +18,9 @@ releaseType: simple
versionFile: "cmd/version.txt"
extraFiles: [
"README.md",
"docs/en/getting-started/colab_quickstart.ipynb",
"docs/en/getting-started/introduction/_index.md",
"docs/en/getting-started/mcp_quickstart/_index.md",
"docs/en /getting-started/colab_quickstart.ipynb",
"docs/en/getting-started/introduction /_index.md",
"docs/en/getting-started/mcp_quickstart /_index.md",
"docs/en/getting-started/quickstart/shared/configure_toolbox.md",
"docs/en/samples/alloydb/_index.md",
"docs/en/samples/alloydb/mcp_quickstart.md",

View File

@@ -15,6 +15,10 @@ name: Link Checker
on:
pull_request:
permissions:
contents: read
pull-requests: write
issues: write
jobs:
@@ -23,8 +27,36 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Identify Changed Files
id: changed-files
shell: bash
run: |
git fetch origin main
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD)
if [ -z "$CHANGED_FILES" ]; then
echo "No markdown files changed. Skipping checks."
echo "HAS_CHANGES=false" >> $GITHUB_ENV
else
echo "--- Changed Files to Scan ---"
echo "$CHANGED_FILES"
echo "-----------------------------"
# FIX: Wrap filenames in quotes to handle spaces
FILES_QUOTED=$(echo "$CHANGED_FILES" | sed 's/^/"/;s/$/"/' | tr '\n' ' ')
# Write to env using EOF pattern
echo "CHECK_FILES<<EOF" >> $GITHUB_ENV
echo "$FILES_QUOTED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "HAS_CHANGES=true" >> $GITHUB_ENV
fi
- name: Restore lychee cache
if: env.HAS_CHANGES == 'true'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .lycheecache
@@ -33,6 +65,7 @@ jobs:
- name: Link Checker
id: lychee-check
if: env.HAS_CHANGES == 'true'
uses: lycheeverse/lychee-action@a8c4c7cb88f0c7386610c35eb25108e448569cb0 # v2
continue-on-error: true
with:
@@ -42,8 +75,7 @@ jobs:
--cache
--max-cache-age 1d
--exclude '^neo4j\+.*' --exclude '^bolt://.*'
README.md
docs/
${{ env.CHECK_FILES }}
output: lychee-report.md
format: markdown
fail: true
@@ -51,18 +83,50 @@ jobs:
debug: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find comment
uses: peter-evans/find-comment@v4
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: "## Link Resolution Note"
- name: Delete comment on success
if: steps.lychee-check.outcome == 'success' && steps.find-comment.outputs.comment-id != ''
run: |
gh api \
--method DELETE \
-H "Accept: application/vnd.github+json" \
/repos/${{ github.repository }}/issues/comments/${{ steps.find-comment.outputs.comment-id }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Report
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
run: |
echo "## Link Resolution Note" > full-report.md
echo "Local links and directory changes work differently on GitHub than on the docsite.You must ensure fixes pass the **GitHub check** and also work with **\`hugo server\`**." >> full-report.md
echo "See [Link Checking and Fixing with Lychee](https://github.com/googleapis/genai-toolbox/blob/main/DEVELOPER.md#link-checking-and-fixing-with-lychee) for more details." >> full-report.md
echo "" >> full-report.md
sed -E '/(Redirect|Redirects per input)/d' lychee-report.md >> full-report.md
- name: Create PR Comment
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: full-report.md
edit-mode: replace
- name: Display Failure Report
# Run this ONLY if the link checker failed
if: steps.lychee-check.outcome == 'failure'
run: |
echo "## Link Resolution Note" >> $GITHUB_STEP_SUMMARY
echo "Local links and directory changes work differently on GitHub than on the docsite." >> $GITHUB_STEP_SUMMARY
echo "You must ensure fixes pass the **GitHub check** and also work with **\`hugo server\`**." >> $GITHUB_STEP_SUMMARY
echo "See [Link Checking and Fixing with Lychee](https://github.com/googleapis/genai-toolbox/blob/main/DEVELOPER.md#link-checking-and-fixing-with-lychee) for more details." >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "### Broken Links Found" >> $GITHUB_STEP_SUMMARY
cat ./lychee-report.md >> $GITHUB_STEP_SUMMARY
# We can now simply output the prepared file to the job summary
cat full-report.md >> $GITHUB_STEP_SUMMARY
# Fail the job
exit 1

View File

@@ -105,9 +105,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookeradddashboardelement"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookeradddashboardfilter"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerconversationalanalytics"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectdirectory"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectfile"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectdirectory"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergenerateembedurl"
@@ -124,7 +122,6 @@ 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/lookergetprojectdirectories"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfile"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojectfiles"
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetprojects"

View File

@@ -1778,7 +1778,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", "get_dashboards", "run_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "get_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"},
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look", "make_look", "get_dashboards", "run_dashboard", "make_dashboard", "add_dashboard_element", "add_dashboard_filter", "generate_embed_url", "health_pulse", "health_analyze", "health_vacuum", "dev_mode", "get_projects", "get_project_files", "get_project_file", "create_project_file", "update_project_file", "delete_project_file", "validate_project", "get_connections", "get_connection_schemas", "get_connection_databases", "get_connection_tables", "get_connection_table_columns"},
},
},
},

View File

@@ -33,10 +33,10 @@ to expose your developer assistant tools to a Looker instance:
## Set up Looker
1. Get a Looker Client ID and Client Secret. Follow the directions
[here](https://cloud.google.com/looker/docs/api-auth#authentication_with_an_sdk).
[here](https://cloud.google.com/looker/docs/api-auth#authentication_wit_an_sdk).
1. Have the base URL of your Looker instance available. It is likely
something like `https://looker.example.com`. In some cases the API is
something like `https://loer.example.com`. In some cases the API is
listening at a different port, and you will need to use
`https://looker.example.com:19999` instead.

View File

@@ -543,9 +543,6 @@ See [Usage Examples](../reference/cli.md#examples).
* `create_project_file`: Create a new LookML file.
* `update_project_file`: Update an existing LookML file.
* `delete_project_file`: Delete a LookML file.
* `get_project_directories`: Retrieves a list of project directories for a given LookML project.
* `create_project_directory`: Creates a new directory within a specified LookML project.
* `delete_project_directory`: Deletes a directory from a specified LookML project.
* `validate_project`: Check the syntax of a LookML project.
* `get_connections`: Get the available connections in a Looker instance.
* `get_connection_schemas`: Get the available schemas in a connection.

View File

@@ -1,44 +0,0 @@
---
title: "looker-create-project-directory"
type: docs
weight: 1
description: >
A "looker-create-project-directory" tool creates a new directory in a LookML project.
aliases:
- /resources/tools/looker-create-project-directory
---
## About
A `looker-create-project-directory` tool creates a new directory within a specified LookML project.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
## Example
```yaml
kind: tools
name: looker-create-project-directory
type: looker-create-project-directory
source: looker-source
description: |
This tool creates a new directory within a specific LookML project.
It is useful for organizing project files.
Parameters:
- project_id (string): The ID of the LookML project.
- directory_path (string): The path of the directory to create.
Output:
A string confirming the creation of the directory.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|----------------------------------------------------|
| type | string | true | Must be "looker-create-project-directory". |
| source | string | true | Name of the source Looker instance. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -1,44 +0,0 @@
---
title: "looker-delete-project-directory"
type: docs
weight: 1
description: >
A "looker-delete-project-directory" tool deletes a directory from a LookML project.
aliases:
- /resources/tools/looker-delete-project-directory
---
## About
A `looker-delete-project-directory` tool deletes a directory from a specified LookML project.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
## Example
```yaml
kind: tools
name: looker-delete-project-directory
type: looker-delete-project-directory
source: looker-source
description: |
This tool deletes a directory from a specific LookML project.
It is useful for removing unnecessary or obsolete directories.
Parameters:
- project_id (string): The ID of the LookML project.
- directory_path (string): The path of the directory to delete.
Output:
A string confirming the deletion of the directory.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|----------------------------------------------------|
| type | string | true | Must be "looker-delete-project-directory". |
| source | string | true | Name of the source Looker instance. |
| description | string | true | Description of the tool that is passed to the LLM. |

View File

@@ -1,43 +0,0 @@
---
title: "looker-get-project-directories"
type: docs
weight: 1
description: >
A "looker-get-project-directories" tool returns the directories within a specific LookML project.
aliases:
- /resources/tools/looker-get-project-directories
---
## About
A `looker-get-project-directories` tool retrieves the directories within a specified LookML project.
It's compatible with the following sources:
- [looker](../../sources/looker.md)
## Example
```yaml
kind: tools
name: looker-get-project-directories
type: looker-get-project-directories
source: looker-source
description: |
This tool retrieves a list of directories within a specific LookML project.
It is useful for exploring the project structure.
Parameters:
- project_id (string): The ID of the LookML project.
Output:
A JSON array of strings, representing the directories within the project.
```
## Reference
| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|----------------------------------------------------|
| type | string | true | Must be "looker-get-project-directories". |
| source | string | true | Name of the source Looker instance. |
| description | string | true | Description of the tool that is passed to the LLM. |

2
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/httplog/v3 v3.3.0
github.com/go-chi/httplog/v2 v2.1.1
github.com/go-chi/render v1.0.3
github.com/go-goquery/goquery v1.0.1
github.com/go-playground/validator/v10 v10.28.0

4
go.sum
View File

@@ -902,8 +902,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk=
github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=

View File

@@ -59,35 +59,25 @@ func NewStdLogger(outW, errW io.Writer, logLevel string) (Logger, error) {
}
// DebugContext logs debug messages
func (sl *StdLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StdLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.outLogger.DebugContext(ctx, msg, keysAndValues...)
}
// InfoContext logs debug messages
func (sl *StdLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StdLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.outLogger.InfoContext(ctx, msg, keysAndValues...)
}
// WarnContext logs warning messages
func (sl *StdLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StdLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.errLogger.WarnContext(ctx, msg, keysAndValues...)
}
// ErrorContext logs error messages
func (sl *StdLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StdLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.errLogger.ErrorContext(ctx, msg, keysAndValues...)
}
// SlogLogger returns a single standard *slog.Logger that routes
// records to the outLogger or errLogger based on the log level.
func (sl *StdLogger) SlogLogger() *slog.Logger {
splitHandler := &SplitHandler{
OutHandler: sl.outLogger.Handler(),
ErrHandler: sl.errLogger.Handler(),
}
return slog.New(splitHandler)
}
const (
Debug = "DEBUG"
Info = "INFO"
@@ -187,64 +177,21 @@ func NewStructuredLogger(outW, errW io.Writer, logLevel string) (Logger, error)
}
// DebugContext logs debug messages
func (sl *StructuredLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StructuredLogger) DebugContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.outLogger.DebugContext(ctx, msg, keysAndValues...)
}
// InfoContext logs info messages
func (sl *StructuredLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StructuredLogger) InfoContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.outLogger.InfoContext(ctx, msg, keysAndValues...)
}
// WarnContext logs warning messages
func (sl *StructuredLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StructuredLogger) WarnContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.errLogger.WarnContext(ctx, msg, keysAndValues...)
}
// ErrorContext logs error messages
func (sl *StructuredLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...any) {
func (sl *StructuredLogger) ErrorContext(ctx context.Context, msg string, keysAndValues ...interface{}) {
sl.errLogger.ErrorContext(ctx, msg, keysAndValues...)
}
// SlogLogger returns a single standard *slog.Logger that routes
// records to the outLogger or errLogger based on the log level.
func (sl *StructuredLogger) SlogLogger() *slog.Logger {
splitHandler := &SplitHandler{
OutHandler: sl.outLogger.Handler(),
ErrHandler: sl.errLogger.Handler(),
}
return slog.New(splitHandler)
}
type SplitHandler struct {
OutHandler slog.Handler
ErrHandler slog.Handler
}
func (h *SplitHandler) Enabled(ctx context.Context, level slog.Level) bool {
if level >= slog.LevelWarn {
return h.ErrHandler.Enabled(ctx, level)
}
return h.OutHandler.Enabled(ctx, level)
}
func (h *SplitHandler) Handle(ctx context.Context, r slog.Record) error {
if r.Level >= slog.LevelWarn {
return h.ErrHandler.Handle(ctx, r)
}
return h.OutHandler.Handle(ctx, r)
}
func (h *SplitHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &SplitHandler{
OutHandler: h.OutHandler.WithAttrs(attrs),
ErrHandler: h.ErrHandler.WithAttrs(attrs),
}
}
func (h *SplitHandler) WithGroup(name string) slog.Handler {
return &SplitHandler{
OutHandler: h.OutHandler.WithGroup(name),
ErrHandler: h.ErrHandler.WithGroup(name),
}
}

View File

@@ -16,20 +16,16 @@ package log
import (
"context"
"log/slog"
)
// Logger is the interface used throughout the project for logging.
type Logger interface {
// DebugContext is for reporting additional information about internal operations.
DebugContext(ctx context.Context, format string, args ...any)
DebugContext(ctx context.Context, format string, args ...interface{})
// InfoContext is for reporting informational messages.
InfoContext(ctx context.Context, format string, args ...any)
InfoContext(ctx context.Context, format string, args ...interface{})
// WarnContext is for reporting warning messages.
WarnContext(ctx context.Context, format string, args ...any)
WarnContext(ctx context.Context, format string, args ...interface{})
// ErrorContext is for reporting errors.
ErrorContext(ctx context.Context, format string, args ...any)
// Single standard slog.Logger that routes records to the outLogger or
// errLogger based on log levels
SlogLogger() *slog.Logger
ErrorContext(ctx context.Context, format string, args ...interface{})
}

View File

@@ -959,48 +959,6 @@ tools:
Output:
A confirmation message upon successful file deletion.
get_project_directories:
kind: looker-get-project-directories
source: looker-source
description: |
This tool retrieves the list of directories within a specified LookML project.
Parameters:
- project_id (required): The unique ID of the LookML project.
Output:
A JSON array of strings, where each string is the name of a directory within the project.
create_project_directory:
kind: looker-create-project-directory
source: looker-source
description: |
This tool creates a new directory within a specified LookML project.
Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first.
Parameters:
- project_id (required): The unique ID of the LookML project.
- directory_path (required): The path to the new directory within the project.
Output:
A confirmation message upon successful directory creation.
delete_project_directory:
kind: looker-delete-project-directory
source: looker-source
description: |
This tool permanently deletes a specified directory within a LookML project.
Prerequisite: The Looker session must be in Development Mode. Use `dev_mode: true` first.
Parameters:
- project_id (required): The unique ID of the LookML project.
- directory_path (required): The path to the directory within the project.
Output:
A confirmation message upon successful directory deletion.
validate_project:
kind: looker-validate-project
source: looker-source
@@ -1129,9 +1087,6 @@ toolsets:
- 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

View File

@@ -28,7 +28,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/httplog/v3"
"github.com/go-chi/httplog/v2"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/log"
@@ -347,16 +347,31 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
if err != nil {
return nil, fmt.Errorf("unable to initialize http log: %w", err)
}
schema := *httplog.SchemaGCP
schema.Level = cfg.LogLevel.String()
schema.Concise(true)
httpOpts := &httplog.Options{
Level: logLevel,
Schema: &schema,
var httpOpts httplog.Options
switch cfg.LoggingFormat.String() {
case "json":
httpOpts = httplog.Options{
JSON: true,
LogLevel: logLevel,
Concise: true,
RequestHeaders: false,
MessageFieldName: "message",
SourceFieldName: "logging.googleapis.com/sourceLocation",
TimeFieldName: "timestamp",
LevelFieldName: "severity",
}
case "standard":
httpOpts = httplog.Options{
LogLevel: logLevel,
Concise: true,
RequestHeaders: false,
MessageFieldName: "message",
}
default:
return nil, fmt.Errorf("invalid Logging format: %q", cfg.LoggingFormat.String())
}
logger := l.SlogLogger()
r.Use(httplog.RequestLogger(logger, httpOpts))
httpLogger := httplog.NewLogger("httplog", httpOpts)
r.Use(httplog.RequestLogger(httpLogger))
sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := InitializeConfigs(ctx, cfg)
if err != nil {

View File

@@ -107,15 +107,15 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
return nil, fmt.Errorf("expected allowedDataset to have at least 2 parts (project.dataset): %s", datasetFQN)
}
datasetID := parts[1]
fmt.Fprintf(&sqlDescriptionBuilder, " The query must only access the `%s` dataset. "+
sqlDescriptionBuilder.WriteString(fmt.Sprintf(" The query must only access the `%s` dataset. "+
"To query a table within this dataset (e.g., `my_table`), "+
"qualify it with the dataset id (e.g., `%s.my_table`).", datasetFQN, datasetID)
"qualify it with the dataset id (e.g., `%s.my_table`).", datasetFQN, datasetID))
} else {
datasetIDs := []string{}
for _, ds := range allowedDatasets {
datasetIDs = append(datasetIDs, fmt.Sprintf("`%s`", ds))
}
fmt.Fprintf(&sqlDescriptionBuilder, " The query must only access datasets from the following list: %s.", strings.Join(datasetIDs, ", "))
sqlDescriptionBuilder.WriteString(fmt.Sprintf(" The query must only access datasets from the following list: %s.", strings.Join(datasetIDs, ", ")))
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2026 Google LLC
// 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.
@@ -301,32 +301,3 @@ func UpdateProjectFile(l *v4.LookerSDK, projectId string, fileContent FileConten
err := l.AuthSession.Do(nil, "PUT", "/4.0", path, nil, fileContent, options)
return err
}
func GetProjectDirectories(l *v4.LookerSDK, projectId string, options *rtl.ApiSettings) (string, error) {
var result string
path := fmt.Sprintf("/projects/%s/directories", url.PathEscape(projectId))
err := l.AuthSession.Do(&result, "GET", "/4.0", path, nil, nil, options)
return result, err
}
type Directory struct {
Path string `json:"path"`
}
func CreateProjectDirectory(l *v4.LookerSDK, projectId string, directoryPath string, options *rtl.ApiSettings) error {
d := Directory{
Path: directoryPath,
}
var result string
path := fmt.Sprintf("/projects/%s/directories", url.PathEscape(projectId))
return l.AuthSession.Do(&result, "POST", "/4.0", path, nil, d, options)
}
func DeleteProjectDirectory(l *v4.LookerSDK, projectId string, directoryPath string, options *rtl.ApiSettings) error {
var query = map[string]any{
"path": directoryPath,
}
var result string
path := fmt.Sprintf("/projects/%s/directories", url.PathEscape(projectId))
return l.AuthSession.Do(&result, "DELETE", "/4.0", path, query, nil, options)
}

View File

@@ -1,177 +0,0 @@
// 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 lookercreateprojectdirectory
import (
"context"
"fmt"
"net/http"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const resourceType string = "looker-create-project-directory"
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{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(string) (*v4.LookerSDK, error)
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigType() string {
return resourceType
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project")
directoryPathParameter := parameters.NewStringParameter("directory_path", "The path to create in the project")
params := parameters.Parameters{projectIdParameter, directoryPathParameter}
annotations := cfg.Annotations
if annotations == nil {
readOnlyHint := false
annotations = &tools.ToolAnnotations{
ReadOnlyHint: &readOnlyHint,
}
}
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
// finish tool setup
return Tool{
Config: cfg,
Parameters: params,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: params.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Config
Parameters parameters.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
}
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err)
}
mapParams := params.AsMap()
projectId, ok := mapParams["project_id"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil)
}
directoryPath, ok := mapParams["directory_path"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("'directory_path' must be a string, got %T", mapParams["directory_path"]), nil)
}
err = lookercommon.CreateProjectDirectory(sdk, projectId, directoryPath, source.LookerApiSettings())
if err != nil {
return nil, util.ProcessGeneralError(err)
}
return fmt.Sprintf("Created directory %s in project %s", directoryPath, projectId), nil
}
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return false, err
}
return source.UseClientAuthorization(), nil
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return "", err
}
return source.GetAuthTokenHeaderName(), nil
}
func (t Tool) GetParameters() parameters.Parameters {
return t.Parameters
}

View File

@@ -1,108 +0,0 @@
// 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 lookercreateprojectdirectory_test
import (
"strings"
"testing"
"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/lookercreateprojectdirectory"
)
func TestParseFromYamlLookerCreateProjectDirectory(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: tools
name: example_tool
type: looker-create-project-directory
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Type: "looker-create-project-directory",
Source: "my-instance",
Description: "some description",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
// Parse contents
_, _, _, got, _, _, err := server.UnmarshalResourceConfig(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 TestFailParseFromYamlLookerCreateProjectDirectory(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: `
kind: tools
name: example_tool
type: looker-create-project-directory
source: my-instance
method: GOT
description: some description
`,
err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-create-project-directory\": [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-create-project-directory",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
_, _, _, _, _, _, err := server.UnmarshalResourceConfig(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)
}
})
}
}

View File

@@ -1,179 +0,0 @@
// 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 lookerdeleteprojectdirectory
import (
"context"
"fmt"
"net/http"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const resourceType string = "looker-delete-project-directory"
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{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(string) (*v4.LookerSDK, error)
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigType() string {
return resourceType
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project")
directoryPathParameter := parameters.NewStringParameter("directory_path", "The path to delete in the project")
params := parameters.Parameters{projectIdParameter, directoryPathParameter}
annotations := cfg.Annotations
if annotations == nil {
readOnlyHint := false
destructiveHint := true
annotations = &tools.ToolAnnotations{
ReadOnlyHint: &readOnlyHint,
DestructiveHint: &destructiveHint,
}
}
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
// finish tool setup
return Tool{
Config: cfg,
Parameters: params,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: params.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Config
Parameters parameters.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
}
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err)
}
mapParams := params.AsMap()
projectId, ok := mapParams["project_id"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil)
}
directoryPath, ok := mapParams["directory_path"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("'directory_path' must be a string, got %T", mapParams["directory_path"]), nil)
}
err = lookercommon.DeleteProjectDirectory(sdk, projectId, directoryPath, source.LookerApiSettings())
if err != nil {
return nil, util.ProcessGeneralError(err)
}
return fmt.Sprintf("Deleted directory %s in project %s", directoryPath, projectId), nil
}
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return false, err
}
return source.UseClientAuthorization(), nil
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return "", err
}
return source.GetAuthTokenHeaderName(), nil
}
func (t Tool) GetParameters() parameters.Parameters {
return t.Parameters
}

View File

@@ -1,108 +0,0 @@
// 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 lookerdeleteprojectdirectory_test
import (
"strings"
"testing"
"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/lookerdeleteprojectdirectory"
)
func TestParseFromYamlLookerDeleteProjectDirectory(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: tools
name: example_tool
type: looker-delete-project-directory
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Type: "looker-delete-project-directory",
Source: "my-instance",
Description: "some description",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
// Parse contents
_, _, _, got, _, _, err := server.UnmarshalResourceConfig(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 TestFailParseFromYamlLookerDeleteProjectDirectory(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: `
kind: tools
name: example_tool
type: looker-delete-project-directory
source: my-instance
method: GOT
description: some description
`,
err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-delete-project-directory\": [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-delete-project-directory",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
_, _, _, _, _, _, err := server.UnmarshalResourceConfig(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)
}
})
}
}

View File

@@ -1,172 +0,0 @@
// 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 lookergetprojectdirectories
import (
"context"
"fmt"
"net/http"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
"github.com/looker-open-source/sdk-codegen/go/rtl"
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
)
const resourceType string = "looker-get-project-directories"
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{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(string) (*v4.LookerSDK, error)
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description" validate:"required"`
AuthRequired []string `yaml:"authRequired"`
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
}
// validate interface
var _ tools.ToolConfig = Config{}
func (cfg Config) ToolConfigType() string {
return resourceType
}
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
projectIdParameter := parameters.NewStringParameter("project_id", "The id of the project")
params := parameters.Parameters{projectIdParameter}
annotations := cfg.Annotations
if annotations == nil {
readOnlyHint := true
annotations = &tools.ToolAnnotations{
ReadOnlyHint: &readOnlyHint,
}
}
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, annotations)
// finish tool setup
return Tool{
Config: cfg,
Parameters: params,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: params.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}, nil
}
// validate interface
var _ tools.Tool = Tool{}
type Tool struct {
Config
Parameters parameters.Parameters `yaml:"parameters"`
manifest tools.Manifest
mcpManifest tools.McpManifest
}
func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
}
sdk, err := source.GetLookerSDK(string(accessToken))
if err != nil {
return nil, util.NewClientServerError("error getting sdk", http.StatusInternalServerError, err)
}
mapParams := params.AsMap()
projectId, ok := mapParams["project_id"].(string)
if !ok {
return nil, util.NewAgentError(fmt.Sprintf("'project_id' must be a string, got %T", mapParams["project_id"]), nil)
}
resp, err := lookercommon.GetProjectDirectories(sdk, projectId, source.LookerApiSettings())
if err != nil {
return nil, util.ProcessGeneralError(err)
}
return resp, nil
}
func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
}
func (t Tool) Manifest() tools.Manifest {
return t.manifest
}
func (t Tool) McpManifest() tools.McpManifest {
return t.mcpManifest
}
func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return false, err
}
return source.UseClientAuthorization(), nil
}
func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}
func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return "", err
}
return source.GetAuthTokenHeaderName(), nil
}
func (t Tool) GetParameters() parameters.Parameters {
return t.Parameters
}

View File

@@ -1,108 +0,0 @@
// 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 lookergetprojectdirectories_test
import (
"strings"
"testing"
"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/lookergetprojectdirectories"
)
func TestParseFromYamlLookerGetProjectDirectories(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: tools
name: example_tool
type: looker-get-project-directories
source: my-instance
description: some description
`,
want: server.ToolConfigs{
"example_tool": lkr.Config{
Name: "example_tool",
Type: "looker-get-project-directories",
Source: "my-instance",
Description: "some description",
AuthRequired: []string{},
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
// Parse contents
_, _, _, got, _, _, err := server.UnmarshalResourceConfig(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 TestFailParseFromYamlLookerGetProjectDirectories(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: `
kind: tools
name: example_tool
type: looker-get-project-directories
source: my-instance
method: GOT
description: some description
`,
err: "error unmarshaling tools: unable to parse tool \"example_tool\" as type \"looker-get-project-directories\": [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-project-directories",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
_, _, _, _, _, _, err := server.UnmarshalResourceConfig(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)
}
})
}
}

View File

@@ -129,18 +129,13 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
t.Fatalf("unable to create AlloyDB connection pool: %s", err)
}
// Generate a unique ID
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
// cleanup test environment
tests.CleanupPostgresTables(t, ctx, pool)
// This will execute after all tool tests complete (success, fail, or t.Fatal)
t.Cleanup(func() {
tests.CleanupPostgresTables(t, context.Background(), pool, uniqueID)
})
//Create table names using the UUID
tableNameParam := "param_table_" + uniqueID
tableNameAuth := "auth_table_" + uniqueID
tableNameTemplateParam := "template_param_table_" + uniqueID
// create table name with UUID
tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)

View File

@@ -114,18 +114,13 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
}
// Generate a unique ID
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
// cleanup test environment
tests.CleanupPostgresTables(t, ctx, pool)
// This will execute after all tool tests complete (success, fail, or t.Fatal)
t.Cleanup(func() {
tests.CleanupPostgresTables(t, context.Background(), pool, uniqueID)
})
//Create table names using the UUID
tableNameParam := "param_table_" + uniqueID
tableNameAuth := "auth_table_" + uniqueID
tableNameTemplateParam := "template_param_table_" + uniqueID
// create table name with UUID
tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)

View File

@@ -109,18 +109,13 @@ func TestCockroachDB(t *testing.T) {
}
t.Logf("✅ Connected to: %s", version)
// Generate a unique ID
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
// cleanup test environment
tests.CleanupPostgresTables(t, ctx, pool)
// This will execute after all tool tests complete (success, fail, or t.Fatal)
t.Cleanup(func() {
tests.CleanupPostgresTables(t, context.Background(), pool, uniqueID)
})
//Create table names using the UUID
tableNameParam := "param_table_" + uniqueID
tableNameAuth := "auth_table_" + uniqueID
tableNameTemplateParam := "template_param_table_" + uniqueID
// create table names with UUID suffix
tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool (using CockroachDB explicit INT primary keys)
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetCockroachDBParamToolInfo(tableNameParam)

View File

@@ -972,15 +972,14 @@ func TestCloudSQLMySQL_IPTypeParsingFromYAML(t *testing.T) {
}
// Finds and drops all tables in a postgres database.
func CleanupPostgresTables(t *testing.T, ctx context.Context, pool *pgxpool.Pool, uniqueID string) {
func CleanupPostgresTables(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
query := `
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name LIKE $1;`
rows, err := pool.Query(ctx, query, "%\\_"+uniqueID)
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';`
rows, err := pool.Query(ctx, query)
if err != nil {
t.Fatalf("Failed to query for tables for uniqueID %s in 'public' schema: %v", uniqueID, err)
t.Fatalf("Failed to query for all tables in 'public' schema: %v", err)
}
defer rows.Close()
@@ -995,18 +994,13 @@ func CleanupPostgresTables(t *testing.T, ctx context.Context, pool *pgxpool.Pool
}
if len(tablesToDrop) == 0 {
t.Logf("INTEGRATION CLEANUP: No tables found for uniqueID: %s", uniqueID)
return
}
t.Logf("INTEGRATION CLEANUP: Dropping %d isolated tables: %v", len(tablesToDrop), tablesToDrop)
dropQuery := fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE;", strings.Join(tablesToDrop, ", "))
if _, err := pool.Exec(ctx, dropQuery); err != nil {
t.Fatalf("Failed to drop tables for uniqueID %s in 'public' schema: %v", uniqueID, err)
} else {
t.Logf("INTEGRATION CLEANUP SUCCESS: Wiped all tables for uniqueID: %s", uniqueID)
t.Fatalf("Failed to drop all tables in 'public' schema: %v", err)
}
}

View File

@@ -222,21 +222,6 @@ func TestLooker(t *testing.T) {
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"get_project_directories": map[string]any{
"type": "looker-get-project-directories",
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"create_project_directory": map[string]any{
"type": "looker-create-project-directory",
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"delete_project_directory": map[string]any{
"type": "looker-delete-project-directory",
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
},
"validate_project": map[string]any{
"type": "looker-validate-project",
"source": "my-instance",
@@ -1466,71 +1451,6 @@ func TestLooker(t *testing.T) {
},
},
)
tests.RunToolGetTestByName(t, "get_project_directories",
map[string]any{
"get_project_directories": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The id of the project",
"name": "project_id",
"required": true,
"type": "string",
},
},
},
},
)
tests.RunToolGetTestByName(t, "create_project_directory",
map[string]any{
"create_project_directory": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The id of the project",
"name": "project_id",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The path to create in the project",
"name": "directory_path",
"required": true,
"type": "string",
},
},
},
},
)
tests.RunToolGetTestByName(t, "delete_project_directory",
map[string]any{
"delete_project_directory": map[string]any{
"description": "Simple tool to test end to end functionality.",
"authRequired": []any{},
"parameters": []any{
map[string]any{
"authSources": []any{},
"description": "The id of the project",
"name": "project_id",
"required": true,
"type": "string",
},
map[string]any{
"authSources": []any{},
"description": "The path to delete in the project",
"name": "directory_path",
"required": true,
"type": "string",
},
},
},
},
)
tests.RunToolGetTestByName(t, "validate_project",
map[string]any{
"validate_project": map[string]any{
@@ -1767,15 +1687,6 @@ func TestLooker(t *testing.T) {
wantResult = "deleted"
tests.RunToolInvokeParametersTest(t, "delete_project_file", []byte(`{"project_id": "the_look", "file_path": "foo.view.lkml"}`), wantResult)
wantResult = "Created"
tests.RunToolInvokeParametersTest(t, "create_project_directory", []byte(`{"project_id": "the_look", "directory_path": "foo_dir"}`), wantResult)
wantResult = "foo_dir"
tests.RunToolInvokeParametersTest(t, "get_project_directories", []byte(`{"project_id": "the_look"}`), wantResult)
wantResult = "Deleted"
tests.RunToolInvokeParametersTest(t, "delete_project_directory", []byte(`{"project_id": "the_look", "directory_path": "foo_dir"}`), wantResult)
wantResult = "\"errors\":[]"
tests.RunToolInvokeParametersTest(t, "validate_project", []byte(`{"project_id": "the_look"}`), wantResult)

View File

@@ -93,18 +93,13 @@ func TestPostgres(t *testing.T) {
t.Fatalf("unable to create postgres connection pool: %s", err)
}
// Generate a unique ID
uniqueID := strings.ReplaceAll(uuid.New().String(), "-", "")
// cleanup test environment
tests.CleanupPostgresTables(t, ctx, pool)
// This will execute after all tool tests complete (success, fail, or t.Fatal)
t.Cleanup(func() {
tests.CleanupPostgresTables(t, context.Background(), pool, uniqueID)
})
//Create table names using the UUID
tableNameParam := "param_table_" + uniqueID
tableNameAuth := "auth_table_" + uniqueID
tableNameTemplateParam := "template_param_table_" + uniqueID
// create table name with UUID
tableNameParam := "param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameAuth := "auth_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, nameParamToolStmt, arrayToolStmt, paramTestParams := tests.GetPostgresSQLParamToolInfo(tableNameParam)

View File

@@ -3201,7 +3201,7 @@ func RunMySQLListTablesMissingUniqueIndexes(t *testing.T, ctx context.Context, p
createTableHelper := func(t *testing.T, tableName, databaseName string, primaryKey, uniqueKey, nonUniqueKey bool, ctx context.Context, pool *sql.DB) func() {
var stmt strings.Builder
fmt.Fprintf(&stmt, "CREATE TABLE %s (", tableName)
stmt.WriteString(fmt.Sprintf("CREATE TABLE %s (", tableName))
stmt.WriteString("c1 INT")
if primaryKey {
stmt.WriteString(" PRIMARY KEY")