mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-11 08:28:11 -05:00
Compare commits
31 Commits
fix-ci
...
inspect-au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7071ebfd | ||
|
|
750c64bc03 | ||
|
|
63ed2ea4af | ||
|
|
f78956a306 | ||
|
|
baa0fe9260 | ||
|
|
37ae55648d | ||
|
|
1c59459c94 | ||
|
|
b9de4f7f41 | ||
|
|
2dff31ad8b | ||
|
|
61d9489344 | ||
|
|
f6b15de8cd | ||
|
|
5bf275846a | ||
|
|
4fd19bf9af | ||
|
|
99978bc697 | ||
|
|
5fda4d4ed1 | ||
|
|
6455ba964b | ||
|
|
9c045253bd | ||
|
|
3b1cce72e7 | ||
|
|
1cac9b5b37 | ||
|
|
d91bdfcbdc | ||
|
|
c60a9601b4 | ||
|
|
bff528093d | ||
|
|
34f78bd89d | ||
|
|
000d6ada38 | ||
|
|
a09f628b52 | ||
|
|
80a8ebfa0b | ||
|
|
0588e178d6 | ||
|
|
f80f18aaf6 | ||
|
|
f79cdd6144 | ||
|
|
c3a58e1d16 | ||
|
|
c7b443d94a |
@@ -62,7 +62,6 @@ steps:
|
||||
postgressql \
|
||||
postgresexecutesql
|
||||
|
||||
|
||||
- id: "alloydb-pg"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -121,8 +120,7 @@ steps:
|
||||
- "BIGTABLE_PROJECT=$PROJECT_ID"
|
||||
- "BIGTABLE_INSTANCE=$_BIGTABLE_INSTANCE"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv:
|
||||
["CLIENT_ID"]
|
||||
secretEnv: ["CLIENT_ID"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
@@ -153,7 +151,7 @@ steps:
|
||||
"BigQuery" \
|
||||
bigquery \
|
||||
bigquery
|
||||
|
||||
|
||||
- id: "dataplex"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -274,8 +272,7 @@ steps:
|
||||
- "CLOUD_SQL_MYSQL_DATABASE=$_DATABASE_NAME"
|
||||
- "CLOUD_SQL_MYSQL_REGION=$_REGION"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv:
|
||||
["CLOUD_SQL_MYSQL_USER", "CLOUD_SQL_MYSQL_PASS", "CLIENT_ID"]
|
||||
secretEnv: ["CLOUD_SQL_MYSQL_USER", "CLOUD_SQL_MYSQL_PASS", "CLIENT_ID"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
@@ -387,7 +384,7 @@ steps:
|
||||
sqlite
|
||||
|
||||
- id: "couchbase"
|
||||
name : golang:1
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
@@ -395,7 +392,8 @@ steps:
|
||||
- "COUCHBASE_SCOPE=$_COUCHBASE_SCOPE"
|
||||
- "COUCHBASE_BUCKET=$_COUCHBASE_BUCKET"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
secretEnv: ["COUCHBASE_CONNECTION", "COUCHBASE_USER", "COUCHBASE_PASS", "CLIENT_ID"]
|
||||
secretEnv:
|
||||
["COUCHBASE_CONNECTION", "COUCHBASE_USER", "COUCHBASE_PASS", "CLIENT_ID"]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
@@ -408,7 +406,7 @@ steps:
|
||||
couchbase
|
||||
|
||||
- id: "redis"
|
||||
name : golang:1
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
@@ -425,9 +423,9 @@ steps:
|
||||
"Redis" \
|
||||
redis \
|
||||
redis
|
||||
|
||||
|
||||
- id: "valkey"
|
||||
name : golang:1
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
@@ -475,7 +473,13 @@ steps:
|
||||
- "FIRESTORE_PROJECT=$PROJECT_ID"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
- "LOOKER_VERIFY_SSL=$_LOOKER_VERIFY_SSL"
|
||||
secretEnv: ["CLIENT_ID", "LOOKER_BASE_URL", "LOOKER_CLIENT_ID", "LOOKER_CLIENT_SECRET"]
|
||||
secretEnv:
|
||||
[
|
||||
"CLIENT_ID",
|
||||
"LOOKER_BASE_URL",
|
||||
"LOOKER_CLIENT_ID",
|
||||
"LOOKER_CLIENT_SECRET",
|
||||
]
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
@@ -487,25 +491,6 @@ steps:
|
||||
looker \
|
||||
looker
|
||||
|
||||
- id: "duckdb"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
secretEnv: ["CLIENT_ID"]
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_with_coverage.sh \
|
||||
"DuckDB" \
|
||||
duckdb \
|
||||
duckdb
|
||||
|
||||
- id: "alloydbwaitforoperation"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
@@ -545,8 +530,8 @@ steps:
|
||||
.ci/test_with_coverage.sh \
|
||||
"TiDB" \
|
||||
tidb \
|
||||
tidbsql tidbexecutesql
|
||||
|
||||
tidbsql tidbexecutesql
|
||||
|
||||
availableSecrets:
|
||||
secretManager:
|
||||
- versionName: projects/$PROJECT_ID/secrets/cloud_sql_pg_user/versions/latest
|
||||
|
||||
3
.github/release-please.yml
vendored
3
.github/release-please.yml
vendored
@@ -24,6 +24,7 @@ extraFiles: [
|
||||
"docs/en/getting-started/local_quickstart_js.md",
|
||||
"docs/en/getting-started/local_quickstart_go.md",
|
||||
"docs/en/getting-started/mcp_quickstart/_index.md",
|
||||
"docs/en/samples/alloydb/_index.md",
|
||||
"docs/en/samples/bigquery/local_quickstart.md",
|
||||
"docs/en/samples/bigquery/mcp_quickstart/_index.md",
|
||||
"docs/en/samples/bigquery/colab_quickstart_bigquery.ipynb",
|
||||
@@ -37,6 +38,8 @@ extraFiles: [
|
||||
"docs/en/how-to/connect-ide/cloud_sql_mysql_mcp.md",
|
||||
"docs/en/how-to/connect-ide/firestore_mcp.md",
|
||||
"docs/en/how-to/connect-ide/looker_mcp.md",
|
||||
"docs/en/how-to/connect-ide/mysql_mcp.md",
|
||||
"docs/en/how-to/connect-ide/mssql_mcp.md",
|
||||
"docs/en/how-to/connect-ide/postgres_mcp.md",
|
||||
"docs/en/how-to/connect-ide/spanner_mcp.md",
|
||||
]
|
||||
|
||||
2
.github/workflows/docs_deploy.yaml
vendored
2
.github/workflows/docs_deploy.yaml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
node-version: "22"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
2
.github/workflows/docs_preview_deploy.yaml
vendored
2
.github/workflows/docs_preview_deploy.yaml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
node-version: "22"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
2
.github/workflows/schedule_reporter.yml
vendored
2
.github/workflows/schedule_reporter.yml
vendored
@@ -26,4 +26,4 @@ jobs:
|
||||
contents: 'read'
|
||||
uses: ./.github/workflows/cloud_build_failure_reporter.yml
|
||||
with:
|
||||
trigger_names: "toolbox-test-nightly,toolbox-test-on-merge"
|
||||
trigger_names: "toolbox-test-nightly,toolbox-test-on-merge,toolbox-continuous-release"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [0.11.0](https://github.com/googleapis/genai-toolbox/compare/v0.10.0...v0.11.0) (2025-08-04)
|
||||
## [0.11.0](https://github.com/googleapis/genai-toolbox/compare/v0.11.0...v0.11.0) (2025-08-05)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
@@ -10,12 +10,16 @@
|
||||
|
||||
### Features
|
||||
|
||||
* Add DuckDB source and tool ([#879](https://github.com/googleapis/genai-toolbox/pull/879)) ([fd14933](https://github.com/googleapis/genai-toolbox/commit/fd149337e9fa8e912e8699962a7104d51cdffc5d))
|
||||
* Add TiDB source and tool ([#829](https://github.com/googleapis/genai-toolbox/issues/829)) ([6eaf36a](https://github.com/googleapis/genai-toolbox/commit/6eaf36ac8505d523fa4f5a4ac3c97209fd688cef))
|
||||
* Interactive web UI for Toolbox ([#1065](https://github.com/googleapis/genai-toolbox/issues/1065)) ([8749b03](https://github.com/googleapis/genai-toolbox/commit/8749b030035e65361047c4ead13dfacb8e9a9b59))
|
||||
* **prebuiltconfigs/cloud-sql-postgres:** Introduce additional parameter to limit context in list tables ([#1062](https://github.com/googleapis/genai-toolbox/issues/1062)) ([c3a58e1](https://github.com/googleapis/genai-toolbox/commit/c3a58e1d1678dc14d8de5006511df597fd75faa3))
|
||||
* **tools/looker-query-url:** Add support for `looker-query-url` tool ([#1015](https://github.com/googleapis/genai-toolbox/issues/1015)) ([327ddf0](https://github.com/googleapis/genai-toolbox/commit/327ddf0439058aa5ecd2c7ae8251fcde6aeff18c))
|
||||
* **tools/dataplex-lookup-entry:** Add support for `dataplex-lookup-entry` tool ([#1009](https://github.com/googleapis/genai-toolbox/issues/1009)) ([5fa1660](https://github.com/googleapis/genai-toolbox/commit/5fa1660fc8631989b4d13abea205b6426bb506a5))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tools/bigquery,mssql,mysql,postgres,spanner,tidb:** Add query logging to execute-sql tools ([#1069](https://github.com/googleapis/genai-toolbox/issues/1069)) ([0527532]([https://github.com/googleapis/genai-toolbox/commit/0527532bd7085ef9eb8f9c30f430a2f2f35cef32))
|
||||
|
||||
## [0.10.0](https://github.com/googleapis/genai-toolbox/compare/v0.9.0...v0.10.0) (2025-07-25)
|
||||
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ To install Toolbox as a binary:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.10.0
|
||||
export VERSION=0.11.0
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
@@ -127,7 +127,7 @@ You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.10.0
|
||||
export VERSION=0.11.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -151,7 +151,7 @@ To install from source, ensure you have the latest version of
|
||||
[Go installed](https://go.dev/doc/install), and then run the following command:
|
||||
|
||||
```sh
|
||||
go install github.com/googleapis/genai-toolbox@v0.10.0
|
||||
go install github.com/googleapis/genai-toolbox@v0.11.0
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexlookupentry"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchentries"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/dgraph"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/duckdbsql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoredeletedocuments"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetdocuments"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/firestore/firestoregetrules"
|
||||
@@ -69,6 +68,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmeasures"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetmodels"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetparameters"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakelook"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquery"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerquerysql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerqueryurl"
|
||||
@@ -112,7 +112,6 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/couchbase"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/dataplex"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/dgraph"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/duckdb"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/firestore"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/http"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||
|
||||
@@ -1290,7 +1290,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"looker-tools": tools.ToolsetConfig{
|
||||
Name: "looker-tools",
|
||||
ToolNames: []string{"get_models", "get_explores", "get_dimensions", "get_measures", "get_filters", "get_parameters", "query", "query_sql", "query_url", "get_looks", "run_look"},
|
||||
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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -86,7 +86,7 @@ To install Toolbox as a binary:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.10.0
|
||||
export VERSION=0.11.0
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
@@ -97,7 +97,7 @@ You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.10.0
|
||||
export VERSION=0.11.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -115,7 +115,7 @@ To install from source, ensure you have the latest version of
|
||||
[Go installed](https://go.dev/doc/install), and then run the following command:
|
||||
|
||||
```sh
|
||||
go install github.com/googleapis/genai-toolbox@v0.10.0
|
||||
go install github.com/googleapis/genai-toolbox@v0.11.0
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
@@ -171,7 +171,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -294,7 +294,7 @@ from Toolbox.
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="LangChain" lang="bash" >}}
|
||||
npm install langchain @langchain/google-vertexai
|
||||
npm install langchain @langchain/google-genai
|
||||
{{< /tab >}}
|
||||
{{< tab header="GenkitJS" lang="bash" >}}
|
||||
npm install genkit @genkit-ai/vertexai
|
||||
@@ -309,7 +309,7 @@ npm install llamaindex @llamaindex/google @llamaindex/workflow
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="LangChain" lang="js" >}}
|
||||
|
||||
import { ChatVertexAI } from "@langchain/google-vertexai";
|
||||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
||||
import { ToolboxClient } from "@toolbox-sdk/core";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
@@ -336,7 +336,7 @@ const queries = [
|
||||
];
|
||||
|
||||
async function runApplication() {
|
||||
const model = new ChatVertexAI({
|
||||
const model = new ChatGoogleGenerativeAI({
|
||||
model: "gemini-2.0-flash",
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -62,19 +62,19 @@ to expose your developer assistant tools to a Firestore instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/windows/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/windows/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -46,19 +46,19 @@ to expose your developer assistant tools to a Looker instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
@@ -263,11 +263,12 @@ The following tools are available to the LLM:
|
||||
1. **get_measures**: list the measures in a given explore
|
||||
1. **get_filters**: list the filters in a given explore
|
||||
1. **get_parameters**: list the parameters in a given explore
|
||||
1. **query**: Run a query
|
||||
1. **query**: Run a query and return the data
|
||||
1. **query_sql**: Return the SQL generated by Looker for a query
|
||||
1. **query_url**: Return a link to the query in Looker for further exploration
|
||||
1. **get_looks**: Return the saved Looks that match a title or description
|
||||
1. **run_look**: Run a saved Look and return the data
|
||||
1. **make_look**: Create a saved Look in Looker and return the URL
|
||||
|
||||
{{< notice note >}}
|
||||
Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs
|
||||
|
||||
289
docs/en/how-to/connect-ide/mssql_mcp.md
Normal file
289
docs/en/how-to/connect-ide/mssql_mcp.md
Normal file
@@ -0,0 +1,289 @@
|
||||
---
|
||||
title: SQL Server using MCP
|
||||
type: docs
|
||||
weight: 2
|
||||
description: "Connect your IDE to SQL Server using Toolbox."
|
||||
---
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol for connecting Large Language Models (LLMs) to data sources like SQL Server. This guide covers how to use [MCP Toolbox for Databases][toolbox] to expose your developer assistant tools to a SQL Server instance:
|
||||
|
||||
* [Cursor][cursor]
|
||||
* [Windsurf][windsurf] (Codium)
|
||||
* [Visual Studio Code][vscode] (Copilot)
|
||||
* [Cline][cline] (VS Code extension)
|
||||
* [Claude desktop][claudedesktop]
|
||||
* [Claude code][claudecode]
|
||||
* [Gemini CLI][geminicli]
|
||||
* [Gemini Code Assist][geminicodeassist]
|
||||
|
||||
[toolbox]: https://github.com/googleapis/genai-toolbox
|
||||
[cursor]: #configure-your-mcp-client
|
||||
[windsurf]: #configure-your-mcp-client
|
||||
[vscode]: #configure-your-mcp-client
|
||||
[cline]: #configure-your-mcp-client
|
||||
[claudedesktop]: #configure-your-mcp-client
|
||||
[claudecode]: #configure-your-mcp-client
|
||||
[geminicli]: #configure-your-mcp-client
|
||||
[geminicodeassist]: #configure-your-mcp-client
|
||||
|
||||
## Set up the database
|
||||
|
||||
1. [Create or select a SQL Server instance.](https://www.microsoft.com/en-us/sql-server/sql-server-downloads)
|
||||
|
||||
## Install MCP Toolbox
|
||||
|
||||
1. Download the latest version of Toolbox as a binary. Select the [correct binary](https://github.com/googleapis/genai-toolbox/releases) corresponding to your OS and CPU architecture. You are required to use Toolbox version V0.10.0+:
|
||||
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Verify the installation:
|
||||
|
||||
```bash
|
||||
./toolbox --version
|
||||
```
|
||||
|
||||
## Configure your MCP Client
|
||||
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Claude code" lang="en" %}}
|
||||
|
||||
1. Install [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview).
|
||||
1. Create a `.mcp.json` file in your project root if it doesn't exist.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude code to apply the new configuration.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Claude desktop" lang="en" %}}
|
||||
|
||||
1. Open [Claude desktop](https://claude.ai/download) and navigate to Settings.
|
||||
1. Under the Developer tab, tap Edit Config to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude desktop.
|
||||
1. From the new chat screen, you should see a hammer (MCP) icon appear with the new MCP server available.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Cline" lang="en" %}}
|
||||
|
||||
1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and tap the **MCP Servers** icon.
|
||||
1. Tap Configure MCP Servers to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Cursor" lang="en" %}}
|
||||
|
||||
1. Create a `.cursor` directory in your project root if it doesn't exist.
|
||||
1. Create a `.cursor/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Open [Cursor](https://www.cursor.com/) and navigate to **Settings > Cursor Settings > MCP**. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Visual Studio Code (Copilot)" lang="en" %}}
|
||||
|
||||
1. Open [VS Code](https://code.visualstudio.com/docs/copilot/overview) and create a `.vscode` directory in your project root if it doesn't exist.
|
||||
1. Create a `.vscode/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp" : {
|
||||
"servers": {
|
||||
"cloud-sql-sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","cloud-sql-mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Windsurf" lang="en" %}}
|
||||
|
||||
1. Open [Windsurf](https://docs.codeium.com/windsurf) and navigate to the Cascade assistant.
|
||||
1. Tap on the hammer (MCP) icon, then Configure to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini CLI" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini CLI](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart).
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini Code Assist" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini Code Assist](https://marketplace.visualstudio.com/items?itemName=Google.geminicodeassist) extension in Visual Studio Code.
|
||||
1. Enable Agent Mode in Gemini Code Assist chat.
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sqlserver": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mssql","--stdio"],
|
||||
"env": {
|
||||
"MSSQL_HOST": "",
|
||||
"MSSQL_PORT": "",
|
||||
"MSSQL_DATABASE": "",
|
||||
"MSSQL_USER": "",
|
||||
"MSSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
## Use Tools
|
||||
|
||||
Your AI tool is now connected to SQL Server using MCP. Try asking your AI assistant to list tables, create a table, or define and execute other SQL statements.
|
||||
|
||||
The following tools are available to the LLM:
|
||||
|
||||
1. **list_tables**: lists tables and descriptions
|
||||
1. **execute_sql**: execute any SQL statement
|
||||
|
||||
{{< notice note >}}
|
||||
Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs will adapt to the tools available, so this shouldn't affect most users.
|
||||
{{< /notice >}}
|
||||
287
docs/en/how-to/connect-ide/mysql_mcp.md
Normal file
287
docs/en/how-to/connect-ide/mysql_mcp.md
Normal file
@@ -0,0 +1,287 @@
|
||||
---
|
||||
title: MySQL using MCP
|
||||
type: docs
|
||||
weight: 2
|
||||
description: "Connect your IDE to MySQL using Toolbox."
|
||||
---
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol for connecting Large Language Models (LLMs) to data sources like MySQL. This guide covers how to use [MCP Toolbox for Databases][toolbox] to expose your developer assistant tools to a MySQL instance:
|
||||
|
||||
* [Cursor][cursor]
|
||||
* [Windsurf][windsurf] (Codium)
|
||||
* [Visual Studio Code][vscode] (Copilot)
|
||||
* [Cline][cline] (VS Code extension)
|
||||
* [Claude desktop][claudedesktop]
|
||||
* [Claude code][claudecode]
|
||||
* [Gemini CLI][geminicli]
|
||||
* [Gemini Code Assist][geminicodeassist]
|
||||
|
||||
[toolbox]: https://github.com/googleapis/genai-toolbox
|
||||
[cursor]: #configure-your-mcp-client
|
||||
[windsurf]: #configure-your-mcp-client
|
||||
[vscode]: #configure-your-mcp-client
|
||||
[cline]: #configure-your-mcp-client
|
||||
[claudedesktop]: #configure-your-mcp-client
|
||||
[claudecode]: #configure-your-mcp-client
|
||||
[geminicli]: #configure-your-mcp-client
|
||||
[geminicodeassist]: #configure-your-mcp-client
|
||||
|
||||
## Set up the database
|
||||
|
||||
1. [Create or select a MySQL instance.](https://dev.mysql.com/downloads/installer/)
|
||||
|
||||
## Install MCP Toolbox
|
||||
|
||||
1. Download the latest version of Toolbox as a binary. Select the [correct binary](https://github.com/googleapis/genai-toolbox/releases) corresponding to your OS and CPU architecture. You are required to use Toolbox version V0.10.0+:
|
||||
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
1. Make the binary executable:
|
||||
|
||||
```bash
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
1. Verify the installation:
|
||||
|
||||
```bash
|
||||
./toolbox --version
|
||||
```
|
||||
|
||||
## Configure your MCP Client
|
||||
|
||||
{{< tabpane text=true >}}
|
||||
{{% tab header="Claude code" lang="en" %}}
|
||||
|
||||
1. Install [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview).
|
||||
1. Create a `.mcp.json` file in your project root if it doesn't exist.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "mysql", "--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude code to apply the new configuration.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Claude desktop" lang="en" %}}
|
||||
|
||||
1. Open [Claude desktop](https://claude.ai/download) and navigate to Settings.
|
||||
1. Under the Developer tab, tap Edit Config to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "mysql", "--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Restart Claude desktop.
|
||||
1. From the new chat screen, you should see a hammer (MCP) icon appear with the new MCP server available.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Cline" lang="en" %}}
|
||||
|
||||
1. Open the [Cline](https://github.com/cline/cline) extension in VS Code and tap the **MCP Servers** icon.
|
||||
1. Tap Configure MCP Servers to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "mysql", "--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Cursor" lang="en" %}}
|
||||
|
||||
1. Create a `.cursor` directory in your project root if it doesn't exist.
|
||||
1. Create a `.cursor/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt", "mysql", "--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Open [Cursor](https://www.cursor.com/) and navigate to **Settings > Cursor Settings > MCP**. You should see a green active status after the server is successfully connected.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Visual Studio Code (Copilot)" lang="en" %}}
|
||||
|
||||
1. Open [VS Code](https://code.visualstudio.com/docs/copilot/overview) and create a `.vscode` directory in your project root if it doesn't exist.
|
||||
1. Create a `.vscode/mcp.json` file if it doesn't exist and open it.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mysql","--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Windsurf" lang="en" %}}
|
||||
|
||||
1. Open [Windsurf](https://docs.codeium.com/windsurf) and navigate to the Cascade assistant.
|
||||
1. Tap on the hammer (MCP) icon, then Configure to open the configuration file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mysql","--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini CLI" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini CLI](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#quickstart).
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mysql","--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{% tab header="Gemini Code Assist" lang="en" %}}
|
||||
|
||||
1. Install the [Gemini Code Assist](https://marketplace.visualstudio.com/items?itemName=Google.geminicodeassist) extension in Visual Studio Code.
|
||||
1. Enable Agent Mode in Gemini Code Assist chat.
|
||||
1. In your working directory, create a folder named `.gemini`. Within it, create a `settings.json` file.
|
||||
1. Add the following configuration, replace the environment variables with your values, and then save:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mysql": {
|
||||
"command": "./PATH/TO/toolbox",
|
||||
"args": ["--prebuilt","mysql","--stdio"],
|
||||
"env": {
|
||||
"MYSQL_HOST": "",
|
||||
"MYSQL_PORT": "",
|
||||
"MYSQL_DATABASE": "",
|
||||
"MYSQL_USER": "",
|
||||
"MYSQL_PASSWORD": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
|
||||
## Use Tools
|
||||
|
||||
Your AI tool is now connected to MySQL using MCP. Try asking your AI assistant to list tables, create a table, or define and execute other SQL statements.
|
||||
|
||||
The following tools are available to the LLM:
|
||||
|
||||
1. **list_tables**: lists tables and descriptions
|
||||
1. **execute_sql**: execute any SQL statement
|
||||
|
||||
{{< notice note >}}
|
||||
Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs will adapt to the tools available, so this shouldn't affect most users.
|
||||
{{< /notice >}}
|
||||
@@ -52,19 +52,19 @@ Omni](https://cloud.google.com/alloydb/omni/current/docs/overview).
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -33,6 +33,14 @@ cluster][alloydb-free-trial].
|
||||
- [`postgres-execute-sql`](../tools/postgres/postgres-execute-sql.md)
|
||||
Run parameterized SQL statements in AlloyDB Postgres.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)
|
||||
Connect your IDE to AlloyDB using Toolbox.
|
||||
|
||||
- [AlloyDB Admin API using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_admin_mcp/)
|
||||
Create your AlloyDB database with MCP Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -54,6 +54,11 @@ avoiding full table scans or complex filters.
|
||||
- [`bigquery-list-table-ids`](../tools/bigquery/bigquery-list-table-ids.md)
|
||||
List tables in a given dataset.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [BigQuery using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/bigquery_mcp/)
|
||||
Connect your IDE to BigQuery using Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -27,6 +27,11 @@ to a database by following these instructions][csql-mssql-connect].
|
||||
- [`mssql-execute-sql`](../tools/mssql/mssql-execute-sql.md)
|
||||
Run parameterized SQL Server queries in Cloud SQL for SQL Server.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for SQL Server using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_mssql_mcp/)
|
||||
Connect your IDE to Cloud SQL for SQL Server using Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -28,6 +28,11 @@ to a database by following these instructions][csql-mysql-quickstart].
|
||||
- [`mysql-execute-sql`](../tools/mysql/mysql-execute-sql.md)
|
||||
Run parameterized SQL queries in Cloud SQL for MySQL.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for MySQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_mysql_mcp/)
|
||||
Connect your IDE to Cloud SQL for MySQL using Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -28,6 +28,12 @@ to a database by following these instructions][csql-pg-quickstart].
|
||||
- [`postgres-execute-sql`](../tools/postgres/postgres-execute-sql.md)
|
||||
Run parameterized SQL statements in PostgreSQL.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for Postgres using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/cloud_sql_pg_mcp/)
|
||||
Connect your IDE to Cloud SQL for Postgres using Toolbox.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: DuckDB
|
||||
linkTitle: DuckDB
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
DuckDB is an in-process SQL OLAP database management system designed for analytical query processing.
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
[DuckDB](https://duckdb.org/) is an embedded analytical database management system that runs in-process with the client application. It is optimized for analytical workloads, providing high performance for complex queries with minimal setup.
|
||||
|
||||
DuckDB has the following notable characteristics:
|
||||
|
||||
- In-process, serverless database engine
|
||||
- Supports complex SQL queries for analytical processing
|
||||
- Can operate on in-memory or persistent storage
|
||||
- Zero-configuration - no external dependencies or server setup required
|
||||
- Highly optimized for columnar data storage and query execution
|
||||
|
||||
For more details, refer to the [DuckDB Documentation](https://duckdb.org/).
|
||||
|
||||
## Available Tools
|
||||
- [`duckdb-sql`](../tools/duckdb/duckdb-sql.md)
|
||||
Execute pre-defined prepared SQL queries in DuckDB.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Database File
|
||||
|
||||
To use DuckDB, you can either:
|
||||
|
||||
- Specify a file path for a persistent database stored on the filesystem
|
||||
- Omit the file path to use an in-memory database
|
||||
|
||||
## Example
|
||||
|
||||
For a persistent DuckDB database:
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
my-duckdb:
|
||||
kind: "duckdb"
|
||||
dbFilePath: "/path/to/database.db"
|
||||
configuration:
|
||||
memory_limit: "2GB"
|
||||
threads: "4"
|
||||
```
|
||||
|
||||
For an in-memory DuckDB database:
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
my-duckdb-memory:
|
||||
name: "my-duckdb-memory"
|
||||
kind: "duckdb"
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------------|:-----------------:|:------------:|---------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "duckdb". |
|
||||
| dbFilePath | string | false | Path to the DuckDB database file. Omit for an in-memory database. |
|
||||
| configuration | map[string]string | false | Additional DuckDB configuration options (e.g., `memory_limit`, `threads`). |
|
||||
|
||||
For a complete list of available configuration options, refer to the [DuckDB Configuration Documentation](https://duckdb.org/docs/stable/configuration/overview.html#local-configuration-options).
|
||||
|
||||
|
||||
For more details on the Go implementation, see the [go-duckdb package documentation](https://pkg.go.dev/github.com/scottlepp/go-duckdb#section-readme).
|
||||
@@ -23,6 +23,11 @@ reputation for reliability, feature robustness, and performance.
|
||||
- [`postgres-execute-sql`](../tools/postgres/postgres-execute-sql.md)
|
||||
Run parameterized SQL statements in PostgreSQL.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)
|
||||
Connect your IDE to PostgreSQL using Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Database User
|
||||
|
||||
@@ -31,6 +31,11 @@ the Google Cloud console][spanner-quickstart].
|
||||
- [`spanner-execute-sql`](../tools/spanner/spanner-execute-sql.md)
|
||||
Run structured and parameterized queries on Spanner.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Spanner using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/spanner_mcp/)
|
||||
Connect your IDE to Spanner using Toolbox.
|
||||
|
||||
## Requirements
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
@@ -75,6 +75,12 @@ visible to the LLM.
|
||||
|
||||
[alloydb-psv]: https://cloud.google.com/alloydb/docs/parameterized-secure-views-overview
|
||||
|
||||
{{< notice tip >}} Make sure to enable the `parameterized_views` extension before running this tool. You can do so by running this command in the AlloyDB studio:
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS parameterized_views;
|
||||
```
|
||||
{{< /notice >}}
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
@@ -95,7 +101,6 @@ tools:
|
||||
- name: my_google_service
|
||||
field: email
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|
||||
@@ -15,8 +15,9 @@ It's compatible with the following sources:
|
||||
|
||||
- [bigquery](../../sources/bigquery.md)
|
||||
|
||||
`bigquery-execute-sql` takes one input parameter `sql` and runs the sql
|
||||
statement against the `source`.
|
||||
`bigquery-execute-sql` takes a required `sql` input parameter and runs the SQL
|
||||
statement against the configured `source`. It also supports an optional `dry_run`
|
||||
parameter to validate a query without executing it.
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "DuckDB"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Tools that work with DuckDB Sources.
|
||||
---
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: "duckdb-sql"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Execute SQL statements against a DuckDB database using the DuckDB SQL tools configuration.
|
||||
aliases:
|
||||
- /resources/tools/duckdb-sql
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
A `duckdb-sql` tool executes a pre-defined SQL statement against a [DuckDB](https://duckdb.org/) database. It is compatible with any DuckDB source configuration as defined in the [DuckDB source documentation](../../sources/duckdb.md).
|
||||
|
||||
The specified SQL statement is executed as a prepared statement, and parameters are inserted according to their position: e.g., `$1` is the first parameter, `$2` is the second, and so on. If template parameters are included, they are resolved before execution of the prepared statement.
|
||||
|
||||
DuckDB's SQL dialect closely follows the conventions of the PostgreSQL dialect, with a few exceptions listed in the [DuckDB PostgreSQL Compatibility documentation](https://duckdb.org/docs/stable/sql/dialect/postgresql_compatibility.html). For an introduction to DuckDB's SQL dialect, refer to the [DuckDB SQL Introduction](https://duckdb.org/docs/stable/sql/introduction).
|
||||
|
||||
### Concepts
|
||||
|
||||
DuckDB is a relational database management system (RDBMS). Data is stored in relations (tables), where each table is a named collection of rows. Each row in a table has the same set of named columns, each with a specific data type. Tables are stored within schemas, and a collection of schemas constitutes the entire database.
|
||||
|
||||
For more details, see the [DuckDB SQL Introduction](https://duckdb.org/docs/stable/sql/introduction).
|
||||
|
||||
## Example
|
||||
|
||||
> **Note:** This tool uses parameterized queries to prevent SQL injections. Query parameters can be used as substitutes for arbitrary expressions but cannot be used for identifiers, column names, table names, or other parts of the query.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
search-users:
|
||||
kind: duckdb-sql
|
||||
source: my-duckdb
|
||||
description: Search users by name and age
|
||||
statement: SELECT * FROM users WHERE name LIKE $1 AND age >= $2
|
||||
parameters:
|
||||
- name: name
|
||||
type: string
|
||||
description: The name to search for
|
||||
- name: min_age
|
||||
type: integer
|
||||
description: Minimum age
|
||||
```
|
||||
|
||||
## Example with Template Parameters
|
||||
|
||||
> **Note:** Template parameters allow direct modifications to the SQL statement, including identifiers, column names, and table names, which makes them more vulnerable to SQL injections. Using basic parameters (see above) is recommended for performance and safety. For more details, see the [templateParameters](../#template-parameters) section.
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_table:
|
||||
kind: duckdb-sql
|
||||
source: my-duckdb
|
||||
statement: |
|
||||
SELECT * FROM {{.tableName}};
|
||||
description: |
|
||||
Use this tool to list all information from a specific table.
|
||||
Example:
|
||||
{{
|
||||
"tableName": "flights",
|
||||
}}
|
||||
templateParameters:
|
||||
- name: tableName
|
||||
type: string
|
||||
description: Table to select from
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Configuration Fields
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|--------------------|:-------------------------------:|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "duckdb-sql". |
|
||||
| source | string | true | Name of the DuckDB source configuration (see [DuckDB source documentation](../../sources/duckdb.md)). |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
| statement | string | true | The SQL statement to execute. |
|
||||
| authRequired | []string | false | List of authentication requirements for the tool (if any). |
|
||||
| parameters | [parameters](../#specifying-parameters) | false | List of parameters that will be inserted into the SQL statement |
|
||||
| templateParameters | [templateParameters](../#template-parameters) | false | List of template parameters that will be inserted into the SQL statement before executing the prepared statement. |
|
||||
@@ -1,23 +1,24 @@
|
||||
---
|
||||
title: "looker-query-url"
|
||||
title: "looker-make-look"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
"looker-query-url" generates a url link to a Looker explore.
|
||||
"looker-make-look" generates a Looker look in the users personal folder in
|
||||
Looker
|
||||
aliases:
|
||||
- /resources/tools/looker-query-url
|
||||
- /resources/tools/looker-make-look
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `looker-query-url` generates a url link to an explore in
|
||||
Looker so the query can be investigated further.
|
||||
The `looker-make-look` creates a saved Look in the user's
|
||||
Looker personal folder.
|
||||
|
||||
It's compatible with the following sources:
|
||||
|
||||
- [looker](../../sources/looker.md)
|
||||
|
||||
`looker-query-url` takes eight parameters:
|
||||
`looker-make-look` takes eight parameters:
|
||||
|
||||
1. the `model`
|
||||
2. the `explore`
|
||||
@@ -27,27 +28,38 @@ It's compatible with the following sources:
|
||||
6. an optional set of `sorts`
|
||||
7. an optional `limit`
|
||||
8. an optional `tz`
|
||||
9. an optional `vis_config`
|
||||
10. the `title`
|
||||
11. an optional `description`
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
query_url:
|
||||
kind: looker-query-url
|
||||
make_look:
|
||||
kind: looker-make-look
|
||||
source: looker-source
|
||||
description: |
|
||||
Query URL Tool
|
||||
make_look Tool
|
||||
|
||||
This tool is used to generate the URL of a query in Looker.
|
||||
The user can then explore the query further inside Looker.
|
||||
The tool also returns the query_id and slug. The parameters
|
||||
are the same as the `looker-query` tool.
|
||||
This tool creates a new look in Looker, using the query
|
||||
parameters and the vis_config specified.
|
||||
|
||||
Most of the parameters are the same as the query_url
|
||||
tool. In addition, there is a title and a description
|
||||
that must be provided.
|
||||
|
||||
The newly created look will be created in the user's
|
||||
personal folder in looker. The look name must be unique.
|
||||
|
||||
The result is a json document with a link to the newly
|
||||
created look.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "looker-query-url" |
|
||||
| kind | string | true | Must be "looker-make-look" |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
363
docs/en/resources/tools/looker/looker-query-url.md
Normal file
363
docs/en/resources/tools/looker/looker-query-url.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
title: "looker-query-url"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
"looker-query-url" generates a url link to a Looker explore.
|
||||
aliases:
|
||||
- /resources/tools/looker-query-url
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `looker-query-url` generates a url link to an explore in
|
||||
Looker so the query can be investigated further.
|
||||
|
||||
It's compatible with the following sources:
|
||||
|
||||
- [looker](../../sources/looker.md)
|
||||
|
||||
`looker-query-url` takes eight parameters:
|
||||
|
||||
1. the `model`
|
||||
2. the `explore`
|
||||
3. the `fields` list
|
||||
4. an optional set of `filters`
|
||||
5. an optional set of `pivots`
|
||||
6. an optional set of `sorts`
|
||||
7. an optional `limit`
|
||||
8. an optional `tz`
|
||||
9. an optional `vis_config`
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
query_url:
|
||||
kind: looker-query-url
|
||||
source: looker-source
|
||||
description: |
|
||||
Query URL Tool
|
||||
|
||||
This tool is used to generate the URL of a query in Looker.
|
||||
The user can then explore the query further inside Looker.
|
||||
The tool also returns the query_id and slug. The parameters
|
||||
are the same as the query tool with an additional vis_config
|
||||
parameter.
|
||||
|
||||
The vis_config is optional. If provided, it will be used to
|
||||
control the default visualization for the query. These are
|
||||
some sample vis_config settings.
|
||||
|
||||
A bar chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_bar",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A column chart with an option advanced_vis_config -
|
||||
{{
|
||||
"advanced_vis_config": "{ chart: { type: 'pie', spacingBottom: 50, spacingLeft: 50, spacingRight: 50, spacingTop: 50, }, legend: { enabled: false, }, plotOptions: { pie: { dataLabels: { enabled: true, format: '\u003cb\u003e{key}\u003c/b\u003e\u003cspan style=\"font-weight: normal\"\u003e - {percentage:.2f}%\u003c/span\u003e', }, showInLegend: false, }, }, series: [], }",
|
||||
"colors": [
|
||||
"grey"
|
||||
],
|
||||
"defaults_version": 1,
|
||||
"hidden_fields": [],
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"note_display": "below",
|
||||
"note_state": "collapsed",
|
||||
"note_text": "Unsold inventory only",
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_colors": {},
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": true,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_column",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axes": [],
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A line chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"hidden_pivots": {},
|
||||
"hidden_series": [],
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_points": true,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "",
|
||||
"trellis": "",
|
||||
"type": "looker_line",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5
|
||||
}}
|
||||
|
||||
An area chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_points": true,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_area",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A scatter plot -
|
||||
{{
|
||||
"cluster_points": false,
|
||||
"custom_quadrant_point_x": 5,
|
||||
"custom_quadrant_point_y": 5,
|
||||
"custom_value_label_column": "",
|
||||
"custom_x_column": "",
|
||||
"custom_y_column": "",
|
||||
"defaults_version": 1,
|
||||
"hidden_fields": [],
|
||||
"hidden_pivots": {},
|
||||
"hidden_points_if_no": [],
|
||||
"hidden_series": [],
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"limit_displayed_rows_values": {
|
||||
"first_last": "first",
|
||||
"num_rows": 0,
|
||||
"show_hide": "hide"
|
||||
},
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "circle",
|
||||
"quadrant_properties": {
|
||||
"0": {
|
||||
"color": "",
|
||||
"label": "Quadrant 1"
|
||||
},
|
||||
"1": {
|
||||
"color": "",
|
||||
"label": "Quadrant 2"
|
||||
},
|
||||
"2": {
|
||||
"color": "",
|
||||
"label": "Quadrant 3"
|
||||
},
|
||||
"3": {
|
||||
"color": "",
|
||||
"label": "Quadrant 4"
|
||||
}
|
||||
},
|
||||
"quadrants_enabled": false,
|
||||
"series_labels": {},
|
||||
"series_types": {},
|
||||
"show_null_points": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": true,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"size_by_field": "roi",
|
||||
"stacking": "normal",
|
||||
"swap_axes": true,
|
||||
"trellis": "",
|
||||
"type": "looker_scatter",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axes": [
|
||||
{
|
||||
"label": "",
|
||||
"orientation": "bottom",
|
||||
"series": [
|
||||
{
|
||||
"axisId": "Channel_0 - average_of_roi_first",
|
||||
"id": "Channel_0 - average_of_roi_first",
|
||||
"name": "Channel_0"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_1 - average_of_roi_first",
|
||||
"id": "Channel_1 - average_of_roi_first",
|
||||
"name": "Channel_1"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_2 - average_of_roi_first",
|
||||
"id": "Channel_2 - average_of_roi_first",
|
||||
"name": "Channel_2"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_3 - average_of_roi_first",
|
||||
"id": "Channel_3 - average_of_roi_first",
|
||||
"name": "Channel_3"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_4 - average_of_roi_first",
|
||||
"id": "Channel_4 - average_of_roi_first",
|
||||
"name": "Channel_4"
|
||||
}
|
||||
],
|
||||
"showLabels": true,
|
||||
"showValues": true,
|
||||
"tickDensity": "custom",
|
||||
"tickDensityCustom": 100,
|
||||
"type": "linear",
|
||||
"unpinAxis": false
|
||||
}
|
||||
],
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A single value visualization -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"show_view_names": false,
|
||||
"type": "looker_single_record"
|
||||
}}
|
||||
|
||||
A Pie chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"label_density": 25,
|
||||
"label_type": "labPer",
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_pie",
|
||||
"value_labels": "legend",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5
|
||||
}}
|
||||
|
||||
The result is a JSON object with the id, slug, the url, and
|
||||
the long_url.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:------------------------------------------:|:------------:|--------------------------------------------------------------------------------------------------|
|
||||
| kind | string | true | Must be "looker-query-url" |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
@@ -114,7 +114,7 @@ In this section, we will download and install the Toolbox binary.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
export VERSION="0.10.0"
|
||||
export VERSION="0.11.0"
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -179,7 +179,7 @@ to use BigQuery, and then run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.10.0/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.11.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
26
go.mod
26
go.mod
@@ -2,7 +2,7 @@ module github.com/googleapis/genai-toolbox
|
||||
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.5
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
cloud.google.com/go/alloydbconn v1.15.4
|
||||
@@ -11,7 +11,7 @@ require (
|
||||
cloud.google.com/go/cloudsqlconn v1.17.3
|
||||
cloud.google.com/go/dataplex v1.26.0
|
||||
cloud.google.com/go/firestore v1.18.0
|
||||
cloud.google.com/go/spanner v1.83.0
|
||||
cloud.google.com/go/spanner v1.84.1
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0
|
||||
github.com/couchbase/gocb/v2 v2.10.1
|
||||
@@ -31,10 +31,10 @@ require (
|
||||
github.com/looker-open-source/sdk-codegen/go v0.25.10
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.1
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/redis/go-redis/v9 v9.12.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/thlib/go-timezone-local v0.0.7
|
||||
github.com/valkey-io/valkey-go v1.0.63
|
||||
github.com/valkey-io/valkey-go v1.0.64
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
@@ -45,26 +45,19 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
google.golang.org/api v0.244.0
|
||||
google.golang.org/api v0.246.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings v0.1.17 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-amd64 v0.1.12 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-arm64 v0.1.12 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/linux-amd64 v0.1.12 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/linux-arm64 v0.1.12 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/windows-amd64 v0.1.12 // indirect
|
||||
github.com/marcboeker/go-duckdb/arrowmapping v0.0.10 // indirect
|
||||
github.com/marcboeker/go-duckdb/mapping v0.0.11 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.2 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/alloydb v1.18.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
@@ -79,7 +72,6 @@ require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.4.0 // indirect
|
||||
github.com/apache/arrow/go/v15 v15.0.2 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -100,7 +92,6 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
@@ -120,7 +111,6 @@ require (
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/marcboeker/go-duckdb/v2 v2.3.4
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -160,7 +150,7 @@ require (
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
|
||||
57
go.sum
57
go.sum
@@ -38,8 +38,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
|
||||
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg=
|
||||
cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
|
||||
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
|
||||
cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E=
|
||||
@@ -544,8 +544,8 @@ cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+
|
||||
cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos=
|
||||
cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk=
|
||||
cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M=
|
||||
cloud.google.com/go/spanner v1.83.0 h1:AH3QIoSIa01l3WbeTppkwCEYFNK1AER6drcYhPmwhxY=
|
||||
cloud.google.com/go/spanner v1.83.0/go.mod h1:QSWcjxszT0WRHNd8zyGI0WctrYA1N7j0yTFsWyol9Yw=
|
||||
cloud.google.com/go/spanner v1.84.1 h1:ShH4Y3YeDtmHa55dFiSS3YtQ0dmCuP0okfAoHp/d68w=
|
||||
cloud.google.com/go/spanner v1.84.1/go.mod h1:3GMEIjOcXINJSvb42H3M6TdlGCDzaCFpiiNQpjHPlCM=
|
||||
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
|
||||
cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
|
||||
cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0=
|
||||
@@ -563,8 +563,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
|
||||
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
|
||||
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
|
||||
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
|
||||
@@ -674,20 +674,14 @@ github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/arrow-go/v18 v18.4.0 h1:/RvkGqH517iY8bZKc4FD5/kkdwXJGjxf28JIXbJ/oB0=
|
||||
github.com/apache/arrow-go/v18 v18.4.0/go.mod h1:Aawvwhj8x2jURIzD9Moy72cF0FyJXOpkYpdmGRHcw14=
|
||||
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
|
||||
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
|
||||
github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE=
|
||||
github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
|
||||
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -749,18 +743,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/duckdb/duckdb-go-bindings v0.1.17 h1:SjpRwrJ7v0vqnIvLeVFHlhuS72+Lp8xxQ5jIER2LZP4=
|
||||
github.com/duckdb/duckdb-go-bindings v0.1.17/go.mod h1:pBnfviMzANT/9hi4bg+zW4ykRZZPCXlVuvBWEcZofkc=
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-amd64 v0.1.12 h1:8CLBnsq9YDhi2Gmt3sjSUeXxMzyMQAKefjqUy9zVPFk=
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-amd64 v0.1.12/go.mod h1:Ezo7IbAfB8NP7CqPIN8XEHKUg5xdRRQhcPPlCXImXYA=
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-arm64 v0.1.12 h1:wjO4I0GhMh2xIpiUgRpzuyOT4KxXLoUS/rjU7UUVvCE=
|
||||
github.com/duckdb/duckdb-go-bindings/darwin-arm64 v0.1.12/go.mod h1:eS7m/mLnPQgVF4za1+xTyorKRBuK0/BA44Oy6DgrGXI=
|
||||
github.com/duckdb/duckdb-go-bindings/linux-amd64 v0.1.12 h1:HzKQi2C+1jzmwANsPuYH6x9Sfw62SQTjNAEq3OySKFI=
|
||||
github.com/duckdb/duckdb-go-bindings/linux-amd64 v0.1.12/go.mod h1:1GOuk1PixiESxLaCGFhag+oFi7aP+9W8byymRAvunBk=
|
||||
github.com/duckdb/duckdb-go-bindings/linux-arm64 v0.1.12 h1:YGSR7AFLw2gJ7IbgLE6DkKYmgKv1LaRSd/ZKF1yh2oE=
|
||||
github.com/duckdb/duckdb-go-bindings/linux-arm64 v0.1.12/go.mod h1:o7crKMpT2eOIi5/FY6HPqaXcvieeLSqdXXaXbruGX7w=
|
||||
github.com/duckdb/duckdb-go-bindings/windows-amd64 v0.1.12 h1:2aduW6fnFnT2Q45PlIgHbatsPOxV9WSZ5B2HzFfxaxA=
|
||||
github.com/duckdb/duckdb-go-bindings/windows-amd64 v0.1.12/go.mod h1:IlOhJdVKUJCAPj3QsDszUo8DVdvp1nBFp4TUJVdw99s=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -836,8 +818,6 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
@@ -1021,7 +1001,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
@@ -1046,12 +1025,6 @@ github.com/looker-open-source/sdk-codegen/go v0.25.10/go.mod h1:YM/IYSsTPk7I54j4
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/marcboeker/go-duckdb/arrowmapping v0.0.10 h1:G1W+GVnUefR8uy7jHdNO+CRMsmFG5mFPIHVAespfFCA=
|
||||
github.com/marcboeker/go-duckdb/arrowmapping v0.0.10/go.mod h1:jccUb8TYD0p5TsEEeN4SXuslNJHo23QaKOqKD+U6uFU=
|
||||
github.com/marcboeker/go-duckdb/mapping v0.0.11 h1:fusN1b1l7Myxafifp596I6dNLNhN5Uv/rw31qAqBwqw=
|
||||
github.com/marcboeker/go-duckdb/mapping v0.0.11/go.mod h1:aYBjFLgfKO0aJIbDtXPiaL5/avRQISveX/j9tMf9JhU=
|
||||
github.com/marcboeker/go-duckdb/v2 v2.3.4 h1:o98wrefPbH0IdJRix4pF0+jZiXoFQ+FSR8InMsCUZD0=
|
||||
github.com/marcboeker/go-duckdb/v2 v2.3.4/go.mod h1:8adNrftF4Ye29XMrpIl5NYNosTVsZu1mz3C82WdVvrk=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -1059,9 +1032,7 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
|
||||
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
@@ -1098,8 +1069,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU=
|
||||
github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@@ -1141,8 +1112,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII=
|
||||
github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/valkey-io/valkey-go v1.0.63 h1:LNlDTcUxy9jxrmGHSvd0s/NsgEmQbvREYvvBAHCIir0=
|
||||
github.com/valkey-io/valkey-go v1.0.63/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/valkey-io/valkey-go v1.0.64 h1:3u4+b6D6zs9JQs254TLy4LqitCMHHr9XorP9GGk7XY4=
|
||||
github.com/valkey-io/valkey-go v1.0.64/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
@@ -1696,8 +1667,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/api v0.244.0 h1:lpkP8wVibSKr++NCD36XzTk/IzeKJ3klj7vbj+XU5pE=
|
||||
google.golang.org/api v0.244.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
|
||||
google.golang.org/api v0.246.0 h1:H0ODDs5PnMZVZAEtdLMn2Ul2eQi7QNjqM2DIFp8TlTM=
|
||||
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1840,8 +1811,8 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
||||
@@ -72,26 +72,36 @@ tools:
|
||||
SELECT
|
||||
ti.schema_name,
|
||||
ti.table_name AS object_name,
|
||||
json_build_object(
|
||||
'schema_name', ti.schema_name,
|
||||
'object_name', ti.table_name,
|
||||
'object_type', CASE ti.object_kind
|
||||
WHEN 'r' THEN 'TABLE'
|
||||
WHEN 'p' THEN 'PARTITIONED TABLE'
|
||||
ELSE ti.object_kind::text -- Should not happen due to WHERE clause
|
||||
END,
|
||||
'owner', ti.table_owner,
|
||||
'comment', ti.table_comment,
|
||||
'columns', COALESCE((SELECT json_agg(json_build_object('column_name',ci.column_name,'data_type',ci.data_type,'ordinal_position',ci.column_ordinal_position,'is_not_nullable',ci.is_not_nullable,'column_default',ci.column_default,'column_comment',ci.column_comment) ORDER BY ci.column_ordinal_position) FROM columns_info ci WHERE ci.table_oid = ti.table_oid), '[]'::json),
|
||||
'constraints', COALESCE((SELECT json_agg(json_build_object('constraint_name',cons.constraint_name,'constraint_type',cons.constraint_type,'constraint_definition',cons.constraint_definition,'constraint_columns',cons.constraint_columns,'foreign_key_referenced_table',cons.foreign_key_referenced_table,'foreign_key_referenced_columns',cons.foreign_key_referenced_columns)) FROM constraints_info cons WHERE cons.table_oid = ti.table_oid), '[]'::json),
|
||||
'indexes', COALESCE((SELECT json_agg(json_build_object('index_name',ii.index_name,'index_definition',ii.index_definition,'is_unique',ii.is_unique,'is_primary',ii.is_primary,'index_method',ii.index_method,'index_columns',ii.index_columns)) FROM indexes_info ii WHERE ii.table_oid = ti.table_oid), '[]'::json),
|
||||
'triggers', COALESCE((SELECT json_agg(json_build_object('trigger_name',tri.trigger_name,'trigger_definition',tri.trigger_definition,'trigger_enabled_state',tri.trigger_enabled_state)) FROM triggers_info tri WHERE tri.table_oid = ti.table_oid), '[]'::json)
|
||||
) AS object_details
|
||||
CASE
|
||||
WHEN $2 = 'simple' THEN
|
||||
-- IF format is 'simple', return basic JSON
|
||||
json_build_object('name', ti.table_name)
|
||||
ELSE
|
||||
json_build_object(
|
||||
'schema_name', ti.schema_name,
|
||||
'object_name', ti.table_name,
|
||||
'object_type', CASE ti.object_kind
|
||||
WHEN 'r' THEN 'TABLE'
|
||||
WHEN 'p' THEN 'PARTITIONED TABLE'
|
||||
ELSE ti.object_kind::text -- Should not happen due to WHERE clause
|
||||
END,
|
||||
'owner', ti.table_owner,
|
||||
'comment', ti.table_comment,
|
||||
'columns', COALESCE((SELECT json_agg(json_build_object('column_name',ci.column_name,'data_type',ci.data_type,'ordinal_position',ci.column_ordinal_position,'is_not_nullable',ci.is_not_nullable,'column_default',ci.column_default,'column_comment',ci.column_comment) ORDER BY ci.column_ordinal_position) FROM columns_info ci WHERE ci.table_oid = ti.table_oid), '[]'::json),
|
||||
'constraints', COALESCE((SELECT json_agg(json_build_object('constraint_name',cons.constraint_name,'constraint_type',cons.constraint_type,'constraint_definition',cons.constraint_definition,'constraint_columns',cons.constraint_columns,'foreign_key_referenced_table',cons.foreign_key_referenced_table,'foreign_key_referenced_columns',cons.foreign_key_referenced_columns)) FROM constraints_info cons WHERE cons.table_oid = ti.table_oid), '[]'::json),
|
||||
'indexes', COALESCE((SELECT json_agg(json_build_object('index_name',ii.index_name,'index_definition',ii.index_definition,'is_unique',ii.is_unique,'is_primary',ii.is_primary,'index_method',ii.index_method,'index_columns',ii.index_columns)) FROM indexes_info ii WHERE ii.table_oid = ti.table_oid), '[]'::json),
|
||||
'triggers', COALESCE((SELECT json_agg(json_build_object('trigger_name',tri.trigger_name,'trigger_definition',tri.trigger_definition,'trigger_enabled_state',tri.trigger_enabled_state)) FROM triggers_info tri WHERE tri.table_oid = ti.table_oid), '[]'::json)
|
||||
)
|
||||
END AS object_details
|
||||
FROM table_info ti ORDER BY ti.schema_name, ti.table_name;
|
||||
parameters:
|
||||
- name: table_names
|
||||
type: string
|
||||
description: "Optional: A comma-separated list of table names. If empty, details for all tables in user-accessible schemas will be listed."
|
||||
- name: output_format
|
||||
type: string
|
||||
description: "Optional: Use 'simple' to return table names only or use 'detailed' to return the full information schema."
|
||||
default: "detailed"
|
||||
|
||||
toolsets:
|
||||
cloud-sql-postgres-database-tools:
|
||||
|
||||
@@ -123,7 +123,313 @@ tools:
|
||||
This tool is used to generate the URL of a query in Looker.
|
||||
The user can then explore the query further inside Looker.
|
||||
The tool also returns the query_id and slug. The parameters
|
||||
are the same as the query tool.
|
||||
are the same as the query tool with an additional vis_config
|
||||
parameter.
|
||||
|
||||
The vis_config is optional. If provided, it will be used to
|
||||
control the default visualization for the query. These are
|
||||
some sample vis_config settings.
|
||||
|
||||
A bar chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_bar",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A column chart with an option advanced_vis_config -
|
||||
{{
|
||||
"advanced_vis_config": "{ chart: { type: 'pie', spacingBottom: 50, spacingLeft: 50, spacingRight: 50, spacingTop: 50, }, legend: { enabled: false, }, plotOptions: { pie: { dataLabels: { enabled: true, format: '\u003cb\u003e{key}\u003c/b\u003e\u003cspan style=\"font-weight: normal\"\u003e - {percentage:.2f}%\u003c/span\u003e', }, showInLegend: false, }, }, series: [], }",
|
||||
"colors": [
|
||||
"grey"
|
||||
],
|
||||
"defaults_version": 1,
|
||||
"hidden_fields": [],
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"note_display": "below",
|
||||
"note_state": "collapsed",
|
||||
"note_text": "Unsold inventory only",
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_colors": {},
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": true,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_column",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axes": [],
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A line chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"hidden_pivots": {},
|
||||
"hidden_series": [],
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_points": true,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "",
|
||||
"trellis": "",
|
||||
"type": "looker_line",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5
|
||||
}}
|
||||
|
||||
An area chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_points": true,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "normal",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_area",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A scatter plot -
|
||||
{{
|
||||
"cluster_points": false,
|
||||
"custom_quadrant_point_x": 5,
|
||||
"custom_quadrant_point_y": 5,
|
||||
"custom_value_label_column": "",
|
||||
"custom_x_column": "",
|
||||
"custom_y_column": "",
|
||||
"defaults_version": 1,
|
||||
"hidden_fields": [],
|
||||
"hidden_pivots": {},
|
||||
"hidden_points_if_no": [],
|
||||
"hidden_series": [],
|
||||
"interpolation": "linear",
|
||||
"label_density": 25,
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"limit_displayed_rows_values": {
|
||||
"first_last": "first",
|
||||
"num_rows": 0,
|
||||
"show_hide": "hide"
|
||||
},
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "circle",
|
||||
"quadrant_properties": {
|
||||
"0": {
|
||||
"color": "",
|
||||
"label": "Quadrant 1"
|
||||
},
|
||||
"1": {
|
||||
"color": "",
|
||||
"label": "Quadrant 2"
|
||||
},
|
||||
"2": {
|
||||
"color": "",
|
||||
"label": "Quadrant 3"
|
||||
},
|
||||
"3": {
|
||||
"color": "",
|
||||
"label": "Quadrant 4"
|
||||
}
|
||||
},
|
||||
"quadrants_enabled": false,
|
||||
"series_labels": {},
|
||||
"series_types": {},
|
||||
"show_null_points": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": true,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"size_by_field": "roi",
|
||||
"stacking": "normal",
|
||||
"swap_axes": true,
|
||||
"trellis": "",
|
||||
"type": "looker_scatter",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"x_axis_zoom": true,
|
||||
"y_axes": [
|
||||
{
|
||||
"label": "",
|
||||
"orientation": "bottom",
|
||||
"series": [
|
||||
{
|
||||
"axisId": "Channel_0 - average_of_roi_first",
|
||||
"id": "Channel_0 - average_of_roi_first",
|
||||
"name": "Channel_0"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_1 - average_of_roi_first",
|
||||
"id": "Channel_1 - average_of_roi_first",
|
||||
"name": "Channel_1"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_2 - average_of_roi_first",
|
||||
"id": "Channel_2 - average_of_roi_first",
|
||||
"name": "Channel_2"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_3 - average_of_roi_first",
|
||||
"id": "Channel_3 - average_of_roi_first",
|
||||
"name": "Channel_3"
|
||||
},
|
||||
{
|
||||
"axisId": "Channel_4 - average_of_roi_first",
|
||||
"id": "Channel_4 - average_of_roi_first",
|
||||
"name": "Channel_4"
|
||||
}
|
||||
],
|
||||
"showLabels": true,
|
||||
"showValues": true,
|
||||
"tickDensity": "custom",
|
||||
"tickDensityCustom": 100,
|
||||
"type": "linear",
|
||||
"unpinAxis": false
|
||||
}
|
||||
],
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5,
|
||||
"y_axis_zoom": true
|
||||
}}
|
||||
|
||||
A single value visualization -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"show_view_names": false,
|
||||
"type": "looker_single_record"
|
||||
}}
|
||||
|
||||
A Pie chart -
|
||||
{{
|
||||
"defaults_version": 1,
|
||||
"label_density": 25,
|
||||
"label_type": "labPer",
|
||||
"legend_position": "center",
|
||||
"limit_displayed_rows": false,
|
||||
"ordering": "none",
|
||||
"plot_size_by_field": false,
|
||||
"point_style": "none",
|
||||
"series_types": {},
|
||||
"show_null_labels": false,
|
||||
"show_silhouette": false,
|
||||
"show_totals_labels": false,
|
||||
"show_value_labels": false,
|
||||
"show_view_names": false,
|
||||
"show_x_axis_label": true,
|
||||
"show_x_axis_ticks": true,
|
||||
"show_y_axis_labels": true,
|
||||
"show_y_axis_ticks": true,
|
||||
"stacking": "",
|
||||
"totals_color": "#808080",
|
||||
"trellis": "",
|
||||
"type": "looker_pie",
|
||||
"value_labels": "legend",
|
||||
"x_axis_gridlines": false,
|
||||
"x_axis_reversed": false,
|
||||
"x_axis_scale": "auto",
|
||||
"y_axis_combined": true,
|
||||
"y_axis_gridlines": true,
|
||||
"y_axis_reversed": false,
|
||||
"y_axis_scale_mode": "linear",
|
||||
"y_axis_tick_density": "default",
|
||||
"y_axis_tick_density_custom": 5
|
||||
}}
|
||||
|
||||
The result is a JSON object with the id, slug, the url, and
|
||||
the long_url.
|
||||
@@ -158,6 +464,25 @@ tools:
|
||||
the data in a JSON structure. It accepts the look_id as the
|
||||
parameter.
|
||||
|
||||
make_look:
|
||||
kind: looker-make-look
|
||||
source: looker-source
|
||||
description: |
|
||||
make_look Tool
|
||||
|
||||
This tool creates a new look in Looker, using the query
|
||||
parameters and the vis_config specified.
|
||||
|
||||
Most of the parameters are the same as the query_url
|
||||
tool. In addition, there is a title and a description
|
||||
that must be provided.
|
||||
|
||||
The newly created look will be created in the user's
|
||||
personal folder in looker. The look name must be unique.
|
||||
|
||||
The result is a json document with a link to the newly
|
||||
created look.
|
||||
|
||||
toolsets:
|
||||
looker-tools:
|
||||
- get_models
|
||||
@@ -171,3 +496,4 @@ toolsets:
|
||||
- query_url
|
||||
- get_looks
|
||||
- run_look
|
||||
- make_look
|
||||
|
||||
33
internal/server/static/authServices.html
Normal file
33
internal/server/static/authServices.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AuthServices View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/authServices"></div>
|
||||
|
||||
<aside class="second-nav">
|
||||
<h4>My Auth Services</h4>
|
||||
<div id="secondary-panel-content">
|
||||
<p>Fetching Auth Services...</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="main-content-container"></div>
|
||||
|
||||
<script type="module" src="/ui/js/authServices.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'auth-service-info-area')
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,7 @@
|
||||
--text-secondary-gray: #6e6e6e;
|
||||
--button-primary: var(--toolbox-blue);
|
||||
--button-secondary: #616161;
|
||||
--section-border: #e0e0e0;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -161,7 +162,19 @@ body {
|
||||
background-color: var(--button-secondary)
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
.btn--setup-gis {
|
||||
background-color: white;
|
||||
color: var(--text-primary-gray);
|
||||
border: 2px solid var(--text-primary-gray);
|
||||
}
|
||||
|
||||
.btn--externalDocs {
|
||||
background-color: var(--button-secondary);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.second-nav-choice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
@@ -379,7 +392,7 @@ body {
|
||||
}
|
||||
|
||||
.auth-param-input {
|
||||
background-color: #e0e0e0;
|
||||
background-color: var(--section-border);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -513,6 +526,89 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.auth-method-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.auth-method-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary-gray);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.auth-helper-section {
|
||||
border: 1px solid var(--section-border);
|
||||
background-color: transparent;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.auth-method-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.auth-method-details {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--section-border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #fff;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.auth-input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
& label {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary-gray);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
padding: 8px 8px;
|
||||
border: 1px solid #bdc1c6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--toolbox-blue);
|
||||
box-shadow: 0 0 0 1px #1a73e8;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-method-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.auth-instructions {
|
||||
font-size: 0.8em;
|
||||
margin-top: 5px;
|
||||
color: var(--text-secondary-gray);
|
||||
}
|
||||
|
||||
.tool-response {
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
@@ -577,4 +673,119 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-details-tab {
|
||||
background-color: transparent;
|
||||
color: var(--toolbox-blue);
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-instructions {
|
||||
font-family: inherit;
|
||||
color: var(--text-primary-gray);
|
||||
padding: 24px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
|
||||
margin: 16px 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-secondary-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
color: var(--toolbox-blue);
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.resource-subtitle {
|
||||
color: var(--text-primary-gray);
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.resource-intro,
|
||||
.resource-description {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary-gray);
|
||||
margin-bottom: 16px;
|
||||
|
||||
code {
|
||||
background-color: #e9e9e9;
|
||||
color: #c0392b;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 90%;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details-box {
|
||||
margin-top: 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
h5 {
|
||||
color: var(--toolbox-blue);
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.item-detail-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, auto) 1fr;
|
||||
gap: 6px;
|
||||
padding: 4px 4px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: start;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-detail-key {
|
||||
font-weight: bolder;
|
||||
color: var(--toolbox-blue);
|
||||
padding: 4px 6px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.item-detail-value {
|
||||
color: var(--text-primary-gray);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.item-details-box.error p {
|
||||
color: #d93025;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', '')
|
||||
renderMainContent('main-content-container', 'homepage-info', getHomepageInstructions())
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
173
internal/server/static/js/auth.js
Normal file
173
internal/server/static/js/auth.js
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* Renders the Google Sign-In button using the GIS library.
|
||||
* @param {string} toolId The ID of the tool.
|
||||
* @param {string} clientId The Google OAuth Client ID.
|
||||
* @param {string} authProfileName The name of the auth service in tools file.
|
||||
*/
|
||||
function renderGoogleSignInButton(toolId, clientId, authProfileName) {
|
||||
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
||||
const GIS_CONTAINER_ID = `gisContainer-${UNIQUE_ID_BASE}`;
|
||||
const gisContainer = document.getElementById(GIS_CONTAINER_ID);
|
||||
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .btn--setup-gis`);
|
||||
|
||||
if (!gisContainer) {
|
||||
console.error('GIS container not found:', GIS_CONTAINER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
alert('Please enter an OAuth Client ID first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// hide the setup button and show the container for the GIS button
|
||||
if (setupGisBtn) setupGisBtn.style.display = 'none';
|
||||
gisContainer.innerHTML = '';
|
||||
gisContainer.style.display = 'flex';
|
||||
if (window.google && window.google.accounts && window.google.accounts.id) {
|
||||
try {
|
||||
const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: clientId,
|
||||
callback: handleResponse,
|
||||
auto_select: false
|
||||
});
|
||||
window.google.accounts.id.renderButton(
|
||||
gisContainer,
|
||||
{ theme: "outline", size: "large", text: "signin_with" }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error initializing Google Sign-In:", error);
|
||||
alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
|
||||
gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
} else {
|
||||
console.error("GIS library not fully loaded yet.");
|
||||
alert("Google Identity Services library not ready. Please try again in a moment.");
|
||||
gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the credential response from the Google Sign-In library.
|
||||
* @param {!CredentialResponse} response The credential response object from GIS.
|
||||
* @param {string} toolId The ID of the tool.
|
||||
* @param {string} authProfileName The name of the auth service in tools file.
|
||||
*/
|
||||
function handleCredentialResponse(response, toolId, authProfileName) {
|
||||
console.debug("handleCredentialResponse called with:", { response, toolId, authProfileName });
|
||||
const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
|
||||
if (!headersTextarea) {
|
||||
console.error('Headers textarea not found for toolId:', toolId);
|
||||
return;
|
||||
}
|
||||
|
||||
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
||||
const setupGisBtn = document.querySelector(`#google-auth-details-${UNIQUE_ID_BASE} .setup-gis-btn`);
|
||||
const gisContainer = document.getElementById(`gisContainer-${UNIQUE_ID_BASE}`);
|
||||
|
||||
if (response.credential) {
|
||||
const idToken = response.credential;
|
||||
|
||||
try {
|
||||
let currentHeaders = {};
|
||||
if (headersTextarea.value) {
|
||||
currentHeaders = JSON.parse(headersTextarea.value);
|
||||
}
|
||||
const HEADER_KEY = `${authProfileName}_token`;
|
||||
currentHeaders[HEADER_KEY] = `${idToken}`;
|
||||
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
||||
|
||||
if (gisContainer) gisContainer.style.display = 'none';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
|
||||
} catch (e) {
|
||||
alert('Headers are not valid JSON. Please correct and try again.');
|
||||
console.error("Header JSON parse error:", e);
|
||||
}
|
||||
} else {
|
||||
console.error("Error: No credential in response", response);
|
||||
alert('Error: No ID Token received. Check console for details.');
|
||||
|
||||
if (gisContainer) gisContainer.style.display = 'none';
|
||||
if (setupGisBtn) setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
// creates the Google Auth method dropdown
|
||||
export function createGoogleAuthMethodItem(toolId, authProfileName) {
|
||||
const UNIQUE_ID_BASE = `${toolId}-${authProfileName}`;
|
||||
const item = document.createElement('div');
|
||||
|
||||
item.className = 'auth-method-item';
|
||||
item.innerHTML = `
|
||||
<div class="auth-method-header">
|
||||
<span class="auth-method-label">Google ID Token (${authProfileName})</span>
|
||||
<button class="toggle-details-tab">Auto Setup</button>
|
||||
</div>
|
||||
<div class="auth-method-details" id="google-auth-details-${UNIQUE_ID_BASE}" style="display: none;">
|
||||
<div class="auth-controls">
|
||||
<div class="auth-input-row">
|
||||
<label for="clientIdInput-${UNIQUE_ID_BASE}">OAuth Client ID:</label>
|
||||
<input type="text" id="clientIdInput-${UNIQUE_ID_BASE}" placeholder="Enter Client ID" class="auth-input">
|
||||
</div>
|
||||
<div class="auth-instructions">
|
||||
<strong>Action Required:</strong> Add this URL (e.g., http://localhost:PORT) to the Client ID's <strong>Authorized JavaScript origins</strong> to avoid a 401 error. If using Google OAuth,
|
||||
navigate to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a> for this setting.
|
||||
</div>
|
||||
<div class="auth-method-actions">
|
||||
<button class="btn btn--setup-gis">Continue</button>
|
||||
<div id="gisContainer-${UNIQUE_ID_BASE}" class="auth-interactive-element gis-container" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toggleBtn = item.querySelector('.toggle-details-tab');
|
||||
const detailsDiv = item.querySelector(`#google-auth-details-${UNIQUE_ID_BASE}`);
|
||||
const setupGisBtn = item.querySelector('.btn--setup-gis');
|
||||
const clientIdInput = item.querySelector(`#clientIdInput-${UNIQUE_ID_BASE}`);
|
||||
const gisContainer = item.querySelector(`#gisContainer-${UNIQUE_ID_BASE}`);
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const isVisible = detailsDiv.style.display === 'flex';
|
||||
detailsDiv.style.display = isVisible ? 'none' : 'flex';
|
||||
toggleBtn.textContent = isVisible ? 'Auto Setup' : 'Close';
|
||||
if (!isVisible) {
|
||||
if (gisContainer) {
|
||||
gisContainer.innerHTML = '';
|
||||
gisContainer.style.display = 'none';
|
||||
}
|
||||
if (setupGisBtn) {
|
||||
setupGisBtn.style.display = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setupGisBtn.addEventListener('click', () => {
|
||||
const clientId = clientIdInput.value;
|
||||
if (!clientId) {
|
||||
alert('Please enter an OAuth Client ID first.');
|
||||
return;
|
||||
}
|
||||
renderGoogleSignInButton(toolId, clientId, authProfileName);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
27
internal/server/static/js/authServices.js
Normal file
27
internal/server/static/js/authServices.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { loadItems } from './resourceDisplay.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const authServiceListArea = document.getElementById('secondary-panel-content');
|
||||
const authServiceInfoArea = document.getElementById('auth-service-info-area');
|
||||
|
||||
if (!authServiceListArea || !authServiceInfoArea) {
|
||||
console.error('Required DOM elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
loadItems('authservices', authServiceListArea, authServiceInfoArea);
|
||||
});
|
||||
@@ -67,7 +67,7 @@ function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = toolName;
|
||||
button.dataset.toolname = toolName;
|
||||
button.classList.add('tool-button');
|
||||
button.classList.add('second-nav-choice');
|
||||
button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
|
||||
li.appendChild(button);
|
||||
ul.appendChild(li);
|
||||
@@ -84,7 +84,7 @@ function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
|
||||
function handleToolClick(event, secondNavContent, toolDisplayArea) {
|
||||
const toolName = event.target.dataset.toolname;
|
||||
if (toolName) {
|
||||
const currentActive = secondNavContent.querySelector('.tool-button.active');
|
||||
const currentActive = secondNavContent.querySelector('.second-nav-choice.active');
|
||||
if (currentActive) {
|
||||
currentActive.classList.remove('active');
|
||||
}
|
||||
@@ -128,6 +128,7 @@ async function fetchToolDetails(toolName, toolDisplayArea) {
|
||||
id: toolName,
|
||||
name: toolName,
|
||||
description: toolObject.description || "No description provided.",
|
||||
authRequired: toolObject.authRequired || [],
|
||||
parameters: (toolObject.parameters || []).map(param => {
|
||||
let inputType = 'text';
|
||||
const apiType = param.type ? param.type.toLowerCase() : 'string';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* @param {string} containerId The ID of the DOM element to inject the content into.
|
||||
* @param {string} idString The id of the item inside the main content area.
|
||||
*/
|
||||
function renderMainContent(containerId, idString) {
|
||||
function renderMainContent(containerId, idString, instructionContent) {
|
||||
const mainContentContainer = document.getElementById(containerId);
|
||||
if (!mainContentContainer) {
|
||||
console.error(`Content container with ID "${containerId}" not found.`);
|
||||
@@ -30,11 +30,51 @@ function renderMainContent(containerId, idString) {
|
||||
<div class="top-bar">
|
||||
</div>
|
||||
<main class="content" ${idAttribute}">
|
||||
<h1>Welcome to MCP Toolbox UI</h1>
|
||||
<p>This is the main content area. Click a tab on the left to navigate.</p>
|
||||
${instructionContent}
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mainContentContainer.innerHTML = contentHTML;
|
||||
}
|
||||
|
||||
function getHomepageInstructions() {
|
||||
return `
|
||||
<div class="resource-instructions">
|
||||
<h1 class="resource-title">Welcome to Toolbox UI</h1>
|
||||
<p class="resource-intro">Toolbox UI is a built-in web interface that allows users to visually inspect and test out configured resources such as tools and toolsets. To get started, select a resource from the navigation tab to the left.</p>
|
||||
<a href="https://googleapis.github.io/genai-toolbox/how-to/use-toolbox-ui/" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Toolbox UI Documentation</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getToolInstructions() {
|
||||
return `
|
||||
<div class="resource-instructions">
|
||||
<h1 class="resource-title">Tools</h1>
|
||||
<p class="resource-intro">To inspect and test a tool, please click on one of your tools to the left.</p>
|
||||
<h2 class="resource-subtitle">What are Tools?</h2>
|
||||
<p class="resource-description">
|
||||
Tools define actions an agent can take, such as running a SQL statement or interacting with a source.
|
||||
You can define Tools as a map in the <code>tools</code> section of your <code>tools.yaml</code> file. <br><br>
|
||||
Some tools also use <strong>parameters</strong>. Parameters for each Tool will define what inputs the agent will need to provide to invoke them.
|
||||
</p>
|
||||
<a href="https://googleapis.github.io/genai-toolbox/resources/tools/" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Tools Documentation</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getToolsetInstructions() {
|
||||
return `
|
||||
<div class="resource-instructions">
|
||||
<h1 class="resource-title">Toolsets</h1>
|
||||
<p class="resource-intro">To inspect a specific toolset, please enter the name of a toolset and press search.</p>
|
||||
<h2 class="resource-subtitle">What are Toolsets?</h2>
|
||||
<p class="resource-description">
|
||||
Toolsets define groups of tools an agent can access. You can define Toolsets as a map in the <code>toolsets</code> section of your <code>tools.yaml</code> file. Toolsets may
|
||||
only include valid tools that are also defined in your <code>tools.yaml</code> file.
|
||||
</p>
|
||||
<a href="https://googleapis.github.io/genai-toolbox/getting-started/configure/#toolsets" class="btn btn--externalDocs" target="_blank" rel="noopener noreferrer">Toolsets Documentation</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -30,8 +30,8 @@ function renderNavbar(containerId, activePath) {
|
||||
<img src="/ui/assets/mcptoolboxlogo.png" alt="App Logo">
|
||||
</div>
|
||||
<ul>
|
||||
<!--<li><a href="/ui/sources">Sources</a></li>-->
|
||||
<!--<li><a href="/ui/authservices">Auth Services</a></li>-->
|
||||
<li><a href="/ui/sources">Sources</a></li>
|
||||
<li><a href="/ui/authservices">Auth Services</a></li>
|
||||
<li><a href="/ui/tools">Tools</a></li>
|
||||
<li><a href="/ui/toolsets">Toolsets</a></li>
|
||||
</ul>
|
||||
|
||||
271
internal/server/static/js/resourceDisplay.js
Normal file
271
internal/server/static/js/resourceDisplay.js
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const MOCK_SOURCE_MANIFEST = {
|
||||
"sources": {
|
||||
"my-alloydb-pg-source": {
|
||||
kind: "alloydb-postgres",
|
||||
project: "my-project-id",
|
||||
region: "us-central1",
|
||||
cluster: "my-cluster",
|
||||
instance: "my-instance",
|
||||
database: "my_db",
|
||||
ipType: "public"
|
||||
},
|
||||
"my-bigtable-source": {
|
||||
kind: "bigtable",
|
||||
project: "my-project-id",
|
||||
instance: "test-instance"
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
async function getMockSourceListData() {
|
||||
console.warn(`[MOCK] Returning mock data for all sources`);
|
||||
return MOCK_SOURCE_MANIFEST;
|
||||
}
|
||||
|
||||
async function getMockSourceDetailsData(sourceName) {
|
||||
console.warn(`[MOCK] Returning mock data for source details: ${sourceName}`);
|
||||
const SOURCE_DATA = MOCK_SOURCE_MANIFEST.sources[sourceName];
|
||||
if (SOURCE_DATA) {
|
||||
return { "sources": { [sourceName]: SOURCE_DATA } };
|
||||
} else {
|
||||
throw new Error(`Mock Source "${sourceName}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const MOCK_AUTH_SERVICE_MANIFEST = {
|
||||
"authservices": {
|
||||
"my-auth-service-1": {
|
||||
kind: "google oauth2",
|
||||
},
|
||||
"another-auth": {
|
||||
kind: "other",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function getMockAuthServiceListData() {
|
||||
console.warn(`[MOCK] Returning mock data for all authservices`);
|
||||
return MOCK_AUTH_SERVICE_MANIFEST;
|
||||
}
|
||||
|
||||
async function getMockAuthServiceDetailsData(authServiceName) {
|
||||
console.warn(`[MOCK] Returning mock data for auth service details: ${authServiceName}`);
|
||||
const SERVICE_DATA = MOCK_AUTH_SERVICE_MANIFEST.authservices[authServiceName];
|
||||
if (SERVICE_DATA) {
|
||||
return { "authservices": { [authServiceName]: SERVICE_DATA } };
|
||||
} else {
|
||||
throw new Error(`Mock Auth Service "${authServiceName}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
const itemConfigs = {
|
||||
sources: {
|
||||
getListData: getMockSourceListData, // replace with real API call to GET all sources
|
||||
getDetailsData: getMockSourceDetailsData, // replace with real API call to GET specific source
|
||||
listKey: 'sources',
|
||||
nameAttribute: 'data-itemname',
|
||||
displayName: 'Source',
|
||||
singularName: 'source',
|
||||
},
|
||||
authservices: {
|
||||
getListData: getMockAuthServiceListData, // replace with real API call to GET all auth services
|
||||
getDetailsData: getMockAuthServiceDetailsData, //replace with real API call to get specific auth service
|
||||
listKey: 'authservices',
|
||||
nameAttribute: 'data-itemname',
|
||||
displayName: 'Auth Service',
|
||||
singularName: 'authservice',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the details of an item.
|
||||
* @param {string} itemName - The name of the item.
|
||||
* @param {!Object<string, *>} itemDetails - The object containing the item's attributes.
|
||||
* @param {!HTMLElement} displayArea - The HTML element to render the details into.
|
||||
* @param {string} displayName - The display name of the item type.
|
||||
*/
|
||||
function displayItemDetails(itemName, itemDetails, displayArea) {
|
||||
displayArea.innerHTML = '';
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tool-box item-details-box';
|
||||
|
||||
const dl = document.createElement('dl');
|
||||
dl.classList.add('item-details-list');
|
||||
|
||||
// add the name (key in the original json response) as the first key-value pair
|
||||
const nameEntryDiv = document.createElement('div');
|
||||
nameEntryDiv.classList.add('item-detail-entry');
|
||||
|
||||
const nameDt = document.createElement('dt');
|
||||
nameDt.classList.add('item-detail-key');
|
||||
nameDt.textContent = 'name';
|
||||
nameEntryDiv.appendChild(nameDt);
|
||||
|
||||
const nameDd = document.createElement('dd');
|
||||
nameDd.classList.add('item-detail-value');
|
||||
nameDd.textContent = itemName;
|
||||
nameEntryDiv.appendChild(nameDd);
|
||||
|
||||
dl.appendChild(nameEntryDiv);
|
||||
|
||||
const entries = Object.entries(itemDetails);
|
||||
if (entries.length === 0 && !itemName) {
|
||||
const para = document.createElement('p');
|
||||
para.textContent = 'No details available.';
|
||||
container.appendChild(para);
|
||||
displayArea.appendChild(container);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.classList.add('item-detail-entry');
|
||||
|
||||
const dt = document.createElement('dt');
|
||||
dt.classList.add('item-detail-key');
|
||||
dt.textContent = key;
|
||||
entryDiv.appendChild(dt);
|
||||
|
||||
const dd = document.createElement('dd');
|
||||
dd.classList.add('item-detail-value');
|
||||
dd.textContent = String(value);
|
||||
entryDiv.appendChild(dd);
|
||||
|
||||
dl.appendChild(entryDiv);
|
||||
}
|
||||
container.appendChild(dl);
|
||||
displayArea.appendChild(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and displays details for a specific item.
|
||||
* @param {string} itemType - 'sources' or 'authservices'.
|
||||
* @param {string} itemName - The name of the item.
|
||||
* @param {!HTMLElement} displayArea - The element to render details into.
|
||||
* @param {!Object} config - The configuration object for the item type.
|
||||
*/
|
||||
async function fetchItemDetails(itemType, itemName, displayArea, config) {
|
||||
displayArea.innerHTML = `<p>Loading details for ${itemName}...</p>`;
|
||||
try {
|
||||
const apiResponse = await config.getDetailsData(itemName);
|
||||
const itemDetails = apiResponse && apiResponse[config.listKey] ? apiResponse[config.listKey][itemName] : null;
|
||||
|
||||
if (!itemDetails) {
|
||||
throw new Error(`${config.displayName} "${itemName}" data not found in API response.`);
|
||||
}
|
||||
|
||||
displayItemDetails(itemName, itemDetails, displayArea);
|
||||
console.log(`${config.displayName} details:`, itemDetails);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to load details for ${config.singularName} "${itemName}":`, error);
|
||||
displayArea.innerHTML = `<p class="error">Failed to load details for ${itemName}: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on an item button.
|
||||
* @param {!Event} event - The click event object.
|
||||
* @param {string} itemType - 'sources' or 'authservices'.
|
||||
* @param {!HTMLElement} listContainer - The parent element containing the item buttons.
|
||||
* @param {!HTMLElement} displayArea - The HTML element where item details will be shown.
|
||||
* @param {!Object} config - The configuration object for the item type.
|
||||
*/
|
||||
function handleItemClick(event, itemType, listContainer, displayArea, config) {
|
||||
const itemName = event.target.getAttribute(config.nameAttribute);
|
||||
if (itemName) {
|
||||
const currentActive = listContainer.querySelector('.second-nav-choice.active');
|
||||
if (currentActive) currentActive.classList.remove('active');
|
||||
event.target.classList.add('active');
|
||||
fetchItemDetails(itemType, itemName, displayArea, config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list of items as buttons.
|
||||
* @param {string} itemType - 'sources' or 'authservices'.
|
||||
* @param {?Object} apiResponse - The API response object.
|
||||
* @param {!HTMLElement} listContainer - The element to render the list into.
|
||||
* @param {!HTMLElement} displayArea - The element for displaying item details.
|
||||
* @param {!Object} config - The configuration object for the item type.
|
||||
*/
|
||||
function renderList(itemType, apiResponse, listContainer, displayArea, config) {
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
const itemsObject = apiResponse ? apiResponse[config.listKey] : null;
|
||||
|
||||
if (!itemsObject || typeof itemsObject !== 'object' || itemsObject === null) {
|
||||
console.error(`Error: Expected an object with a "${config.listKey}" property, but received:`, apiResponse);
|
||||
listContainer.textContent = `Error: Invalid response format from ${config.displayName} API.`;
|
||||
return;
|
||||
}
|
||||
|
||||
const itemNames = Object.keys(itemsObject);
|
||||
|
||||
if (itemNames.length === 0) {
|
||||
listContainer.textContent = `No ${config.displayName}s found.`;
|
||||
return;
|
||||
}
|
||||
|
||||
const ul = document.createElement('ul');
|
||||
itemNames.forEach(itemName => {
|
||||
const li = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.textContent = itemName;
|
||||
button.setAttribute(config.nameAttribute, itemName);
|
||||
button.classList.add('second-nav-choice');
|
||||
button.addEventListener('click', (event) => handleItemClick(event, itemType, listContainer, displayArea, config));
|
||||
li.appendChild(button);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
listContainer.appendChild(ul);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and renders a list of items (sources or authservices).
|
||||
* @param {string} itemType - 'sources' or 'authservices'.
|
||||
* @param {!HTMLElement} listContainer - Element to render the list into.
|
||||
* @param {!HTMLElement} displayArea - Element to display item details.
|
||||
* @returns {!Promise<void>}
|
||||
*/
|
||||
async function loadItems(itemType, listContainer, displayArea) {
|
||||
const config = itemConfigs[itemType];
|
||||
if (!config) {
|
||||
console.error(`Unknown item type: ${itemType}`);
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = `<p class="error">Configuration error.</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!listContainer || !displayArea) {
|
||||
console.error("List container or display area not provided to loadItems");
|
||||
return;
|
||||
}
|
||||
|
||||
listContainer.innerHTML = `<p>Fetching ${config.displayName}s...</p>`;
|
||||
try {
|
||||
const apiResponse = await config.getListData();
|
||||
renderList(itemType, apiResponse, listContainer, displayArea, config);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load ${config.displayName}s:`, error);
|
||||
listContainer.innerHTML = `<p class="error">Failed to load ${config.displayName}s: <pre><code>${error}</code></pre></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { loadItems };
|
||||
27
internal/server/static/js/sources.js
Normal file
27
internal/server/static/js/sources.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { loadItems } from './resourceDisplay.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sourceListArea = document.getElementById('secondary-panel-content');
|
||||
const sourceInfoArea = document.getElementById('source-info-area');
|
||||
|
||||
if (!sourceListArea || !sourceInfoArea) {
|
||||
console.error('Required DOM elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
loadItems('sources', sourceListArea, sourceInfoArea);
|
||||
});
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { handleRunTool, displayResults } from './runTool.js';
|
||||
import { createGoogleAuthMethodItem } from './auth.js'
|
||||
|
||||
/**
|
||||
* Helper function to create form inputs for parameters.
|
||||
@@ -151,7 +152,7 @@ function createParamInput(param, toolId) {
|
||||
* parsed. The function receives the updated headers object as its argument.
|
||||
* @return {!HTMLDivElement} The outermost div element of the created modal.
|
||||
*/
|
||||
function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
|
||||
function createHeaderEditorModal(toolId, currentHeaders, toolParameters, authRequired, saveCallback) {
|
||||
const MODAL_ID = `header-modal-${toolId}`;
|
||||
let modal = document.getElementById(MODAL_ID);
|
||||
|
||||
@@ -174,9 +175,37 @@ function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
|
||||
headersTextarea.rows = 10;
|
||||
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
|
||||
|
||||
// handle authenticated params
|
||||
const authProfileNames = new Set();
|
||||
toolParameters.forEach(param => {
|
||||
const isAuthParam = param.authServices && param.authServices.length > 0;
|
||||
if (isAuthParam && param.authServices) {
|
||||
param.authServices.forEach(name => authProfileNames.add(name));
|
||||
}
|
||||
});
|
||||
|
||||
// handle authorized invocations
|
||||
if (authRequired && authRequired.length > 0) {
|
||||
authRequired.forEach(name => authProfileNames.add(name));
|
||||
}
|
||||
|
||||
modalContent.appendChild(modalHeader);
|
||||
modalContent.appendChild(headersTextarea);
|
||||
|
||||
if (authProfileNames.size > 0 || authRequired.length > 0) {
|
||||
const authHelperSection = document.createElement('div');
|
||||
authHelperSection.className = 'auth-helper-section';
|
||||
const authList = document.createElement('div');
|
||||
authList.className = 'auth-method-list';
|
||||
|
||||
authProfileNames.forEach(profileName => {
|
||||
const authItem = createGoogleAuthMethodItem(toolId, profileName);
|
||||
authList.appendChild(authItem);
|
||||
});
|
||||
authHelperSection.appendChild(authList);
|
||||
modalContent.appendChild(authHelperSection);
|
||||
}
|
||||
|
||||
const modalActions = document.createElement('div');
|
||||
const closeButton = document.createElement('button');
|
||||
const saveButton = document.createElement('button');
|
||||
@@ -205,13 +234,6 @@ function createHeaderEditorModal(toolId, currentHeaders, saveCallback) {
|
||||
modalContent.appendChild(authTokenDropdown);
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
// Close modal if clicked outside
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target === modal) {
|
||||
closeHeaderEditor(toolId);
|
||||
}
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
@@ -246,7 +268,7 @@ function createAuthTokenInfoDropdown() {
|
||||
|
||||
details.className = 'auth-token-details';
|
||||
details.appendChild(summary);
|
||||
summary.textContent = 'How to extract Google OAuth ID Token';
|
||||
summary.textContent = 'How to extract Google OAuth ID Token manually';
|
||||
content.className = 'auth-token-content';
|
||||
|
||||
// auth instruction dropdown
|
||||
@@ -321,10 +343,9 @@ export function renderToolInterface(tool, containerElement) {
|
||||
const updateLastResults = (newResults) => {
|
||||
lastResults = newResults;
|
||||
};
|
||||
|
||||
const updateCurrentHeaders = (newHeaders) => {
|
||||
currentHeaders = newHeaders;
|
||||
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, updateCurrentHeaders);
|
||||
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
|
||||
containerElement.appendChild(newModal);
|
||||
};
|
||||
|
||||
@@ -428,7 +449,7 @@ export function renderToolInterface(tool, containerElement) {
|
||||
containerElement.appendChild(responseContainer);
|
||||
|
||||
// create and append the header editor modal
|
||||
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, updateCurrentHeaders);
|
||||
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, tool.authRequired, updateCurrentHeaders);
|
||||
containerElement.appendChild(headerModal);
|
||||
|
||||
prettifyCheckbox.addEventListener('change', () => {
|
||||
|
||||
33
internal/server/static/sources.html
Normal file
33
internal/server/static/sources.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sources View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/sources"></div>
|
||||
|
||||
<aside class="second-nav">
|
||||
<h4>My Sources</h4>
|
||||
<div id="secondary-panel-content">
|
||||
<p>Fetching sources...</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="main-content-container"></div>
|
||||
|
||||
<script type="module" src="/ui/js/sources.js"></script>
|
||||
<script src="/ui/js/navbar.js"></script>
|
||||
<script src="/ui/js/mainContent.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'source-info-area')
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tools View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/tools"></div>
|
||||
@@ -26,7 +27,7 @@
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area')
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions())
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Toolsets View</title>
|
||||
<link rel="stylesheet" href="/ui/css/style.css">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container" data-active-nav="/ui/toolsets"></div>
|
||||
@@ -34,7 +34,7 @@
|
||||
const navbarContainer = document.getElementById('navbar-container');
|
||||
const activeNav = navbarContainer.getAttribute('data-active-nav');
|
||||
renderNavbar('navbar-container', activeNav);
|
||||
renderMainContent('main-content-container', 'tool-display-area');
|
||||
renderMainContent('main-content-container', 'tool-display-area', getToolsetInstructions());
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -22,6 +22,8 @@ func webRouter() (chi.Router, error) {
|
||||
|
||||
// direct routes for html pages to provide clean URLs
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/index.html") })
|
||||
r.Get("/sources", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/sources.html") })
|
||||
r.Get("/authservices", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/authServices.html") })
|
||||
r.Get("/tools", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/tools.html") })
|
||||
r.Get("/toolsets", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/toolsets.html") })
|
||||
|
||||
|
||||
@@ -73,6 +73,34 @@ func TestWebEndpoint(t *testing.T) {
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Toolsets View",
|
||||
},
|
||||
{
|
||||
name: "web sources page",
|
||||
path: "/ui/sources",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Sources View",
|
||||
},
|
||||
{
|
||||
name: "web sources page with trailing slash",
|
||||
path: "/ui/sources/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "Sources View",
|
||||
},
|
||||
{
|
||||
name: "web auth services page",
|
||||
path: "/ui/authservices",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "AuthServices View",
|
||||
},
|
||||
{
|
||||
name: "web auth services page with trailing slash",
|
||||
path: "/ui/authservices/",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "text/html",
|
||||
wantPageTitle: "AuthServices View",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package duckdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
_ "github.com/marcboeker/go-duckdb/v2"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const SourceKind string = "duckdb"
|
||||
|
||||
func init() {
|
||||
if !sources.Register(SourceKind, newConfig) {
|
||||
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Db *sql.DB
|
||||
}
|
||||
|
||||
// SourceKind implements sources.Source.
|
||||
func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) DuckDb() *sql.DB {
|
||||
return s.Db
|
||||
}
|
||||
|
||||
// validate Source
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
DatabaseFile string `yaml:"dbFilePath,omitempty"`
|
||||
Configuration map[string]string `yaml:"configuration,omitempty"`
|
||||
}
|
||||
|
||||
func (r Config) SourceConfigKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
|
||||
db, err := initDuckDbConnection(ctx, tracer, r.Name, r.DatabaseFile, r.Configuration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create db connection: %w", err)
|
||||
}
|
||||
|
||||
err = db.PingContext(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to connect sucessfully: %w", err)
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: r.Kind,
|
||||
Db: db,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ sources.SourceConfig = Config{}
|
||||
|
||||
func initDuckDbConnection(ctx context.Context, tracer trace.Tracer, name string, dbFilePath string, duckdbConfiguration map[string]string) (*sql.DB, error) {
|
||||
//nolint:all // Reassigned ctx
|
||||
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
|
||||
defer span.End()
|
||||
|
||||
var configStr = getDuckDbConfiguration(dbFilePath, duckdbConfiguration)
|
||||
|
||||
//Open database connection
|
||||
db, err := sql.Open("duckdb", configStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open duckdb connection: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func getDuckDbConfiguration(dbFilePath string, duckdbConfiguration map[string]string) string {
|
||||
if len(duckdbConfiguration) == 0 {
|
||||
return dbFilePath
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
for key, value := range duckdbConfiguration {
|
||||
params.Set(key, value)
|
||||
}
|
||||
|
||||
var configStr strings.Builder
|
||||
configStr.WriteString(dbFilePath)
|
||||
configStr.WriteString("?")
|
||||
configStr.WriteString(params.Encode())
|
||||
|
||||
return configStr.String()
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package duckdb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/duckdb"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
)
|
||||
|
||||
func TestParserFromYamlDuckDb(t *testing.T) {
|
||||
config := make(map[string]string)
|
||||
config["access_mode"] = "READ_ONLY"
|
||||
config["threads"] = "4"
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.SourceConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
sources:
|
||||
my-duckdb:
|
||||
kind: duckdb
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-duckdb": duckdb.Config{
|
||||
Name: "my-duckdb",
|
||||
Kind: duckdb.SourceKind,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with custom configuration",
|
||||
in: `
|
||||
sources:
|
||||
my-duckdb:
|
||||
kind: duckdb
|
||||
configuration:
|
||||
access_mode: READ_ONLY
|
||||
threads: 4
|
||||
`,
|
||||
want: server.SourceConfigs{
|
||||
"my-duckdb": duckdb.Config{
|
||||
Name: "my-duckdb",
|
||||
Kind: duckdb.SourceKind,
|
||||
Configuration: config,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Sources server.SourceConfigs `yaml:"sources"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.Unmarshal(testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if !cmp.Equal(tc.want, got.Sources) {
|
||||
t.Fatalf("incorrect parse: want %v, got %v", tc.want, got.Sources)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package bigqueryexecutesql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
bigqueryapi "cloud.google.com/go/bigquery"
|
||||
@@ -83,7 +84,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
}
|
||||
|
||||
sqlParameter := tools.NewStringParameter("sql", "The sql to execute.")
|
||||
parameters := tools.Parameters{sqlParameter}
|
||||
dryRunParameter := tools.NewBooleanParameterWithDefault(
|
||||
"dry_run",
|
||||
false,
|
||||
"If set to true, the query will be validated and information about the execution "+
|
||||
"will be returned without running the query. Defaults to false.",
|
||||
)
|
||||
parameters := tools.Parameters{sqlParameter, dryRunParameter}
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
@@ -120,10 +127,14 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to cast sql parameter %s", paramsMap["sql"])
|
||||
}
|
||||
dryRun, ok := paramsMap["dry_run"].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to cast dry_run parameter %s", paramsMap["dry_run"])
|
||||
}
|
||||
|
||||
dryRunJob, err := dryRunQuery(ctx, t.RestService, t.Client.Project(), t.Client.Location, sql)
|
||||
@@ -131,6 +142,18 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
return nil, fmt.Errorf("query validation failed during dry run: %w", err)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
if dryRunJob != nil {
|
||||
jobJSON, err := json.MarshalIndent(dryRunJob, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal dry run job to JSON: %w", err)
|
||||
}
|
||||
return string(jobJSON), nil
|
||||
}
|
||||
// This case should not be reached, but as a fallback, we return a message.
|
||||
return "Dry run was requested, but no job information was returned.", nil
|
||||
}
|
||||
|
||||
statementType := dryRunJob.Statistics.Query.StatementType
|
||||
// JobStatistics.QueryStatistics.StatementType
|
||||
query := t.Client.Query(sql)
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package duckdbsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/duckdb"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const kind string = "duckdb-sql"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type compatibleSource interface {
|
||||
DuckDb() *sql.DB
|
||||
}
|
||||
|
||||
// validate compatible sources are still compatible
|
||||
var _ compatibleSource = &duckdb.Source{}
|
||||
var compatibleSources = [...]string{duckdb.SourceKind}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
Statement string `yaml:"statement" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
}
|
||||
|
||||
// Initialize implements tools.ToolConfig.
|
||||
func (c Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
// verify source exists
|
||||
rawS, ok := srcs[c.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", c.Source)
|
||||
}
|
||||
|
||||
// verify the source is compatible
|
||||
s, ok := rawS.(compatibleSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
|
||||
}
|
||||
|
||||
allParameters, paramManifest, paramMcpManifest := tools.ProcessParameters(c.TemplateParameters, c.Parameters)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
InputSchema: paramMcpManifest,
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
t := Tool{
|
||||
Name: c.Name,
|
||||
Kind: kind,
|
||||
Parameters: c.Parameters,
|
||||
TemplateParameters: c.TemplateParameters,
|
||||
AllParams: allParameters,
|
||||
Statement: c.Statement,
|
||||
AuthRequired: c.AuthRequired,
|
||||
Db: s.DuckDb(),
|
||||
manifest: tools.Manifest{Description: c.Description, Parameters: paramManifest, AuthRequired: c.AuthRequired},
|
||||
mcpManifest: mcpManifest,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ToolConfigKind implements tools.ToolConfig.
|
||||
func (c Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
TemplateParameters tools.Parameters `yaml:"templateParameters"`
|
||||
AllParams tools.Parameters `yaml:"allParams"`
|
||||
|
||||
Db *sql.DB
|
||||
Statement string `yaml:"statement"`
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
// Authorized implements tools.Tool.
|
||||
func (t Tool) Authorized(verifiedAuthSources []string) bool {
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthSources)
|
||||
}
|
||||
|
||||
// Invoke implements tools.Tool.
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
paramsMap := params.AsMap()
|
||||
newStatement, err := tools.ResolveTemplateParams(t.TemplateParameters, t.Statement, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract template params %w", err)
|
||||
}
|
||||
|
||||
newParams, err := tools.GetParams(t.Parameters, paramsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to extract standard params %w", err)
|
||||
}
|
||||
|
||||
sliceParams := newParams.AsSlice()
|
||||
// Execute the SQL query with parameters
|
||||
rows, err := t.Db.QueryContext(ctx, newStatement, sliceParams...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Get column names
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get column names: %w", err)
|
||||
}
|
||||
|
||||
values := make([]any, len(cols))
|
||||
valuePtrs := make([]any, len(cols))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
// Prepare the result slice
|
||||
var result []any
|
||||
// Iterate through the rows
|
||||
for rows.Next() {
|
||||
// Scan the row into the value pointers
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, fmt.Errorf("unable to scan row: %w", err)
|
||||
}
|
||||
|
||||
// Create a map for this row
|
||||
rowMap := make(map[string]interface{})
|
||||
for i, col := range cols {
|
||||
val := values[i]
|
||||
// Handle nil values
|
||||
if val == nil {
|
||||
rowMap[col] = nil
|
||||
continue
|
||||
}
|
||||
// Store the value in the map
|
||||
rowMap[col] = val
|
||||
}
|
||||
result = append(result, rowMap)
|
||||
}
|
||||
|
||||
if err = rows.Close(); err != nil {
|
||||
return nil, fmt.Errorf("unable to close rows: %w", err)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating rows: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Manifest implements tools.Tool.
|
||||
func (t Tool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
// McpManifest implements tools.Tool.
|
||||
func (t Tool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
// ParseParams implements tools.Tool.
|
||||
func (t Tool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.AllParams, data, claimsMap)
|
||||
}
|
||||
|
||||
var _ tools.Tool = Tool{}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package duckdbsql_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/duckdbsql"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
func TestParseFromYamlDuckDb(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: duckdb-sql
|
||||
source: my-duckdb-instance
|
||||
description: some description
|
||||
statement: |
|
||||
select * from hotel WHERE name = $hotel;
|
||||
parameters:
|
||||
- name: hotel
|
||||
type: string
|
||||
description: hotel parameter description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": duckdbsql.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "duckdb-sql",
|
||||
Source: "my-duckdb-instance",
|
||||
Description: "some description",
|
||||
Statement: "select * from hotel WHERE name = $hotel;\n",
|
||||
AuthRequired: []string{},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("hotel", "hotel parameter description"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
|
||||
// Create a context with a logger
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create context with logger: %s", err)
|
||||
}
|
||||
|
||||
// Parse contents with context
|
||||
err = yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -161,7 +158,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -44,13 +44,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -164,7 +161,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -161,7 +158,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -44,13 +44,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -187,7 +184,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -161,7 +158,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -44,13 +44,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -161,7 +158,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -161,7 +158,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
219
internal/tools/looker/lookermakelook/lookermakelook.go
Normal file
219
internal/tools/looker/lookermakelook/lookermakelook.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package lookermakelook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
lookersrc "github.com/googleapis/genai-toolbox/internal/sources/looker"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/looker/lookercommon"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
"github.com/looker-open-source/sdk-codegen/go/rtl"
|
||||
v4 "github.com/looker-open-source/sdk-codegen/go/sdk/v4"
|
||||
)
|
||||
|
||||
const kind string = "looker-make-look"
|
||||
|
||||
func init() {
|
||||
if !tools.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("tool kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
||||
actual := Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.ToolConfig = Config{}
|
||||
|
||||
func (cfg Config) ToolConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
|
||||
// verify source exists
|
||||
rawS, ok := srcs[cfg.Source]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no source named %q configured", cfg.Source)
|
||||
}
|
||||
|
||||
// verify the source is compatible
|
||||
s, ok := rawS.(*lookersrc.Source)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid source for %q tool: source kind must be `looker`", kind)
|
||||
}
|
||||
|
||||
parameters := lookercommon.GetQueryParameters()
|
||||
|
||||
titleParameter := tools.NewStringParameter("title", "The title of the Look")
|
||||
parameters = append(parameters, titleParameter)
|
||||
descParameter := tools.NewStringParameterWithDefault("description", "", "The description of the Look")
|
||||
parameters = append(parameters, descParameter)
|
||||
vizParameter := tools.NewMapParameterWithDefault("vis_config",
|
||||
map[string]any{},
|
||||
"The visualization config for the query",
|
||||
"",
|
||||
)
|
||||
parameters = append(parameters, vizParameter)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
InputSchema: parameters.McpManifest(),
|
||||
}
|
||||
|
||||
// finish tool setup
|
||||
return Tool{
|
||||
Name: cfg.Name,
|
||||
Kind: kind,
|
||||
Parameters: parameters,
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
Client: s.Client,
|
||||
ApiSettings: s.ApiSettings,
|
||||
manifest: tools.Manifest{
|
||||
Description: cfg.Description,
|
||||
Parameters: parameters.Manifest(),
|
||||
AuthRequired: cfg.AuthRequired,
|
||||
},
|
||||
mcpManifest: mcpManifest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validate interface
|
||||
var _ tools.Tool = Tool{}
|
||||
|
||||
type Tool struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Client *v4.LookerSDK
|
||||
ApiSettings *rtl.ApiSettings
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
Parameters tools.Parameters `yaml:"parameters"`
|
||||
manifest tools.Manifest
|
||||
mcpManifest tools.McpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get logger from ctx: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "params = ", params)
|
||||
wq, err := lookercommon.ProcessQueryArgs(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building query request: %w", err)
|
||||
}
|
||||
|
||||
mrespFields := "id,personal_folder_id"
|
||||
mresp, err := t.Client.Me(mrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making me request: %s", err)
|
||||
}
|
||||
|
||||
paramsMap := params.AsMap()
|
||||
title := paramsMap["title"].(string)
|
||||
description := paramsMap["description"].(string)
|
||||
|
||||
looks, err := t.Client.FolderLooks(*mresp.PersonalFolderId, "title", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting existing looks in folder: %s", err)
|
||||
}
|
||||
|
||||
lookTitles := []string{}
|
||||
for _, look := range looks {
|
||||
lookTitles = append(lookTitles, *look.Title)
|
||||
}
|
||||
if slices.Contains(lookTitles, title) {
|
||||
lt, _ := json.Marshal(lookTitles)
|
||||
return nil, fmt.Errorf("title %s already used in user's folder. Currently used titles are %v. Make the call again with a unique title", title, string(lt))
|
||||
}
|
||||
|
||||
visConfig := paramsMap["vis_config"].(map[string]any)
|
||||
wq.VisConfig = &visConfig
|
||||
|
||||
qrespFields := "id"
|
||||
qresp, err := t.Client.CreateQuery(*wq, qrespFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create query request: %s", err)
|
||||
}
|
||||
|
||||
wlwq := v4.WriteLookWithQuery{
|
||||
Title: &title,
|
||||
UserId: mresp.Id,
|
||||
Description: &description,
|
||||
QueryId: qresp.Id,
|
||||
FolderId: mresp.PersonalFolderId,
|
||||
}
|
||||
resp, err := t.Client.CreateLook(wlwq, "", t.ApiSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making create look request: %s", err)
|
||||
}
|
||||
logger.DebugContext(ctx, "resp = %v", resp)
|
||||
|
||||
setting, err := t.Client.GetSetting("host_url", t.ApiSettings)
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "error getting settings: %s", err)
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
if resp.Id != nil {
|
||||
data["id"] = *resp.Id
|
||||
}
|
||||
if resp.ShortUrl != nil {
|
||||
if setting.HostUrl != nil {
|
||||
data["short_url"] = *setting.HostUrl + *resp.ShortUrl
|
||||
} else {
|
||||
data["short_url"] = *resp.ShortUrl
|
||||
}
|
||||
}
|
||||
logger.DebugContext(ctx, "data = %v", data)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Parameters, data, claims)
|
||||
}
|
||||
|
||||
func (t Tool) Manifest() tools.Manifest {
|
||||
return t.manifest
|
||||
}
|
||||
|
||||
func (t Tool) McpManifest() tools.McpManifest {
|
||||
return t.mcpManifest
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
116
internal/tools/looker/lookermakelook/lookermakelook_test.go
Normal file
116
internal/tools/looker/lookermakelook/lookermakelook_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package lookermakelook_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
lkr "github.com/googleapis/genai-toolbox/internal/tools/looker/lookermakelook"
|
||||
)
|
||||
|
||||
func TestParseFromYamlLookerQuery(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
want server.ToolConfigs
|
||||
}{
|
||||
{
|
||||
desc: "basic example",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: looker-make-look
|
||||
source: my-instance
|
||||
description: some description
|
||||
`,
|
||||
want: server.ToolConfigs{
|
||||
"example_tool": lkr.Config{
|
||||
Name: "example_tool",
|
||||
Kind: "looker-make-look",
|
||||
Source: "my-instance",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unmarshal: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got.Tools); diff != "" {
|
||||
t.Fatalf("incorrect parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFailParseFromYamlLookerQuery(t *testing.T) {
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
in string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
desc: "Invalid method",
|
||||
in: `
|
||||
tools:
|
||||
example_tool:
|
||||
kind: looker-make-look
|
||||
source: my-instance
|
||||
method: GOT
|
||||
description: some description
|
||||
`,
|
||||
err: "unable to parse tool \"example_tool\" as kind \"looker-make-look\": [4:1] unknown field \"method\"\n 1 | authRequired: []\n 2 | description: some description\n 3 | kind: looker-make-look\n> 4 | method: GOT\n ^\n 5 | source: my-instance",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := struct {
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
}{}
|
||||
// Parse contents
|
||||
err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(tc.in), &got)
|
||||
if err == nil {
|
||||
t.Fatalf("expect parsing to fail")
|
||||
}
|
||||
errStr := err.Error()
|
||||
if !strings.Contains(errStr, tc.err) {
|
||||
t.Fatalf("unexpected error string: got %q, want substring %q", errStr, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,13 +46,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -158,7 +155,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -149,7 +146,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -77,6 +74,13 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
|
||||
|
||||
parameters := lookercommon.GetQueryParameters()
|
||||
|
||||
vizParameter := tools.NewMapParameterWithDefault("vis_config",
|
||||
map[string]any{},
|
||||
"The visualization config for the query",
|
||||
"",
|
||||
)
|
||||
parameters = append(parameters, vizParameter)
|
||||
|
||||
mcpManifest := tools.McpManifest{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
@@ -124,6 +128,11 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building query request: %w", err)
|
||||
}
|
||||
|
||||
paramsMap := params.AsMap()
|
||||
visConfig := paramsMap["vis_config"].(map[string]any)
|
||||
wq.VisConfig = &visConfig
|
||||
|
||||
respFields := "id,slug,share_url,expanded_share_url"
|
||||
resp, err := t.Client.CreateQuery(*wq, respFields, t.ApiSettings)
|
||||
if err != nil {
|
||||
@@ -162,7 +171,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -45,13 +45,10 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
// AuthRequired specifies the authentication services required for this tool.
|
||||
// Currently, this field is not actively used for authorization checks within the tool itself,
|
||||
// as the Authorized method always returns true. It is included for potential future extensibility.
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
Source string `yaml:"source" validate:"required"`
|
||||
Description string `yaml:"description" validate:"required"`
|
||||
AuthRequired []string `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
@@ -166,7 +163,5 @@ func (t Tool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
|
||||
func (t Tool) Authorized(verifiedAuthServices []string) bool {
|
||||
// Currently, all Looker tools are considered authorized if the source is correctly configured.
|
||||
// The AuthRequired field in the Config struct is reserved for future, more granular authorization.
|
||||
return true
|
||||
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
|
||||
}
|
||||
|
||||
@@ -118,10 +118,10 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
|
||||
@@ -118,10 +118,10 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
|
||||
@@ -124,10 +124,10 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
cypherStr, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
cypherStr, ok := paramsMap["cypher"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["cypher"])
|
||||
}
|
||||
|
||||
if cypherStr == "" {
|
||||
@@ -145,7 +145,7 @@ func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error)
|
||||
}
|
||||
|
||||
config := neo4j.ExecuteQueryWithDatabase(t.Database)
|
||||
results, err := neo4j.ExecuteQuery[*neo4j.EagerResult](ctx, t.Driver, cypherStr, nil,
|
||||
results, err := neo4j.ExecuteQuery(ctx, t.Driver, cypherStr, nil,
|
||||
neo4j.EagerResultTransformer, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to execute query: %w", err)
|
||||
|
||||
@@ -120,10 +120,10 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
// Log the query executed for debugging.
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
|
||||
@@ -146,10 +146,10 @@ func processRows(iter *spanner.RowIterator) ([]any, error) {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
|
||||
@@ -116,10 +116,10 @@ type Tool struct {
|
||||
}
|
||||
|
||||
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) (any, error) {
|
||||
sliceParams := params.AsSlice()
|
||||
sql, ok := sliceParams[0].(string)
|
||||
paramsMap := params.AsMap()
|
||||
sql, ok := paramsMap["sql"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
|
||||
return nil, fmt.Errorf("unable to get cast %s", paramsMap["sql"])
|
||||
}
|
||||
|
||||
// Log the query executed for debugging.
|
||||
|
||||
@@ -162,6 +162,7 @@ func TestBigQueryToolEndpoints(t *testing.T) {
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, templateParamTestConfig)
|
||||
|
||||
runBigQueryExecuteSqlToolInvokeTest(t, select1Want, invokeParamWant, tableNameParam, ddlWant)
|
||||
runBigQueryExecuteSqlToolInvokeDryRunTest(t, datasetName)
|
||||
runBigQueryDataTypeTests(t)
|
||||
runBigQueryListDatasetToolInvokeTest(t, datasetName)
|
||||
runBigQueryGetDatasetInfoToolInvokeTest(t, datasetName, datasetInfoWant)
|
||||
@@ -556,6 +557,115 @@ func runBigQueryExecuteSqlToolInvokeTest(t *testing.T, select1Want, invokeParamW
|
||||
}
|
||||
}
|
||||
|
||||
func runBigQueryExecuteSqlToolInvokeDryRunTest(t *testing.T, datasetName string) {
|
||||
// Get ID token
|
||||
idToken, err := tests.GetGoogleIdToken(tests.ClientId)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting Google ID token: %s", err)
|
||||
}
|
||||
|
||||
newTableName := fmt.Sprintf("%s.new_dry_run_table_%s", datasetName, strings.ReplaceAll(uuid.New().String(), "-", ""))
|
||||
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
name string
|
||||
api string
|
||||
requestHeader map[string]string
|
||||
requestBody io.Reader
|
||||
want string
|
||||
isErr bool
|
||||
}{
|
||||
{
|
||||
name: "invoke my-exec-sql-tool with dryRun",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1", "dry_run": true}`)),
|
||||
want: `\"statementType\": \"SELECT\"`,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-exec-sql-tool with dryRun create table",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql":"CREATE TABLE %s (id INT64, name STRING)", "dry_run": true}`, newTableName))),
|
||||
want: `\"statementType\": \"CREATE_TABLE\"`,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "invoke my-exec-sql-tool with dryRun execute immediate",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf(`{"sql":"EXECUTE IMMEDIATE \"CREATE TABLE %s (id INT64, name STRING)\"", "dry_run": true}`, newTableName))),
|
||||
want: `\"statementType\": \"SCRIPT\"`,
|
||||
isErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invoke my-auth-exec-sql-tool with dryRun and auth token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": idToken},
|
||||
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1", "dry_run": true}`)),
|
||||
isErr: false,
|
||||
want: `\"statementType\": \"SELECT\"`,
|
||||
},
|
||||
{
|
||||
name: "Invoke my-auth-exec-sql-tool with dryRun and invalid auth token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{"my-google-auth_token": "INVALID_TOKEN"},
|
||||
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1","dry_run": true}`)),
|
||||
isErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invoke my-auth-exec-sql-tool with dryRun and without auth token",
|
||||
api: "http://127.0.0.1:5000/api/tool/my-auth-exec-sql-tool/invoke",
|
||||
requestHeader: map[string]string{},
|
||||
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1", "dry_run": true}`)),
|
||||
isErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range invokeTcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Send Tool invocation request
|
||||
req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create request: %s", err)
|
||||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
for k, v := range tc.requestHeader {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to send request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if tc.isErr {
|
||||
return
|
||||
}
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
// Check response body
|
||||
var body map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing response body")
|
||||
}
|
||||
|
||||
got, ok := body["result"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("unable to find result in response body")
|
||||
}
|
||||
|
||||
if !strings.Contains(got, tc.want) {
|
||||
t.Fatalf("expected %q to contain %q, but it did not", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runBigQueryDataTypeTests(t *testing.T) {
|
||||
// Test tool invoke endpoint
|
||||
invokeTcs := []struct {
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright 2025 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package duckdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/tests"
|
||||
)
|
||||
|
||||
var (
|
||||
DuckDbKind = "duckdb-sql"
|
||||
dbPath = "/tmp/users.db"
|
||||
)
|
||||
|
||||
func getDuckDbVars() map[string]any {
|
||||
return map[string]any{
|
||||
"kind": "duckdb",
|
||||
"dbFilePath": dbPath,
|
||||
"configuration": map[string]any{
|
||||
"access_mode": "READ_WRITE",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setupDuckDb(t *testing.T, createParamStmt, insertParamStmt, createAuthStmt, insertAuthStmt string, params []any, authparams []any) {
|
||||
// Remove any existing database file to ensure a clean state
|
||||
os.Remove(dbPath)
|
||||
|
||||
// Open a connection to DuckDB
|
||||
db, err := sql.Open("duckdb", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open DuckDB connection: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(createParamStmt, params...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
_, err = db.Exec(createAuthStmt, authparams...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(insertParamStmt, params...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert initial data: %v", err)
|
||||
}
|
||||
_, err = db.Exec(insertAuthStmt, authparams...)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create table: %v", err)
|
||||
}
|
||||
}
|
||||
func TestDuckDb(t *testing.T) {
|
||||
sourceConfig := getDuckDbVars()
|
||||
var args []string
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
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(), "-", "")
|
||||
|
||||
createParamTableStmt, insertParamTableStmt, paramToolStmt, idParamToolStmt, paramToolStmt2, arrayToolStmt, paramTestParams := GetDuckDbParamToolInfo(tableNameParam)
|
||||
createAuthTableStmt, insertAuthTableStmt, authToolStmt, authTestParams := GetDuckDbAuthToolInfo(tableNameAuth)
|
||||
setupDuckDb(t, createParamTableStmt, insertParamTableStmt, createAuthTableStmt, insertAuthTableStmt, paramTestParams, authTestParams)
|
||||
|
||||
toolsFile := tests.GetToolsConfig(sourceConfig, DuckDbKind, paramToolStmt, idParamToolStmt, paramToolStmt2, arrayToolStmt, authToolStmt)
|
||||
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
|
||||
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, DuckDbKind, tmplSelectCombined, tmplSelectFilterCombined, "")
|
||||
defer os.Remove(dbPath)
|
||||
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
|
||||
if err != nil {
|
||||
t.Fatalf("command initialization returned an error: %s", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
|
||||
if err != nil {
|
||||
t.Logf("toolbox command logs: \n%s", out)
|
||||
t.Fatalf("toolbox didn't start successfully: %s", err)
|
||||
}
|
||||
|
||||
tests.RunToolGetTest(t)
|
||||
|
||||
select1Want, failInvocationWant, _ := GetDuckDbWants()
|
||||
|
||||
_, invokeParamWantNull, nullWant, _ := tests.GetNonSpannerInvokeParamWant()
|
||||
invokeParamWant := "[{\"name\":\"Alice\"},{\"name\":\"Sid\"}]"
|
||||
mcpInvokeParamWant := "{\"jsonrpc\":\"2.0\",\"id\":\"my-tool\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"{\\\"name\\\":\\\"Alice\\\"}\"},{\"type\":\"text\",\"text\":\"{\\\"name\\\":\\\"Sid\\\"}\"}]}}"
|
||||
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull, nullWant, true, true)
|
||||
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
|
||||
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
|
||||
tests.WithSelectAllWant("[{\"age\":21,\"id\":1,\"name\":\"Alex\"},{\"age\":100,\"id\":2,\"name\":\"Alice\"}]"),
|
||||
tests.WithSelect1Want("[{\"age\":21,\"id\":1,\"name\":\"Alex\"}]"),
|
||||
tests.WithReplaceNameFieldArray(`["name"]`),
|
||||
tests.WithReplaceNameColFilter("name"),
|
||||
tests.WithCreateColArray(`["id INT","name VARCHAR(20)","age INT"]`),
|
||||
tests.WithInsert1Want("[{\"Count\":1}]"),
|
||||
)
|
||||
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, templateParamTestConfig)
|
||||
}
|
||||
|
||||
func GetDuckDbParamToolInfo(tableName string) (string, string, string, string, string, string, []any) {
|
||||
createStatement := fmt.Sprintf("CREATE TABLE %s (id INTEGER PRIMARY KEY, name TEXT);", tableName)
|
||||
insertStatement := fmt.Sprintf("INSERT INTO %s (id, name) VALUES (1, $1), (2, $2), (3, $3), (4, $4);", tableName)
|
||||
toolStatement := fmt.Sprintf("SELECT * EXCLUDE (id) FROM %s WHERE id = $1 OR name = $2 order by id;", tableName)
|
||||
idParamStatement := fmt.Sprintf("SELECT * FROM %s WHERE id IN (SELECT unnest(list_value($1)) AS id);", tableName)
|
||||
toolStatement2 := fmt.Sprintf("SELECT name FROM %s WHERE id = list_extract(list_value($1), 1);", tableName)
|
||||
arrayToolStatement := fmt.Sprintf("SELECT name FROM %s WHERE id = ANY($1) AND name = ANY($2) order by name;", tableName)
|
||||
params := []any{"Alice", "Jane", "Sid", nil}
|
||||
return createStatement, insertStatement, toolStatement, idParamStatement, toolStatement2, arrayToolStatement, params
|
||||
}
|
||||
|
||||
// GetDuckDbAuthToolInfo returns statements and param of my-auth-tool for duckdb-sql kind
|
||||
func GetDuckDbAuthToolInfo(tableName string) (string, string, string, []any) {
|
||||
createStatement := fmt.Sprintf("CREATE TABLE %s (id INTEGER PRIMARY KEY, name TEXT, email TEXT);", tableName)
|
||||
insertStatement := fmt.Sprintf("INSERT INTO %s (id, name, email) VALUES (1, $1, $2), (2, $3, $4)", tableName)
|
||||
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = $1;", tableName)
|
||||
params := []any{"Alice", tests.ServiceAccountEmail, "Jane", "janedoe@gmail.com"}
|
||||
return createStatement, insertStatement, toolStatement, params
|
||||
}
|
||||
|
||||
func GetDuckDbWants() (string, string, string) {
|
||||
select1Want := "[{\"1\":1}]"
|
||||
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Parser Error: syntax error at or near \"SELEC\""}],"isError":true}}`
|
||||
createTableStatement := `"CREATE TABLE t (id SERIAL PRIMARY KEY, name TEXT)"`
|
||||
return select1Want, failInvocationWant, createTableStatement
|
||||
}
|
||||
Reference in New Issue
Block a user