diff --git a/cmd/root.go b/cmd/root.go index a9b41989ac42..5b70bbd29408 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,6 +92,7 @@ import ( _ "github.com/googleapis/genai-toolbox/internal/tools/cloudhealthcare/cloudhealthcaresearchdicomstudies" _ "github.com/googleapis/genai-toolbox/internal/tools/cloudmonitoring" _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcloneinstance" + _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreatebackup" _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreatedatabase" _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreateusers" _ "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlgetinstances" diff --git a/cmd/root_test.go b/cmd/root_test.go index 590eb4bd28f3..55d28239204e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1493,7 +1493,7 @@ func TestPrebuiltTools(t *testing.T) { wantToolset: server.ToolsetConfigs{ "cloud_sql_postgres_admin_tools": tools.ToolsetConfig{ Name: "cloud_sql_postgres_admin_tools", - ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "postgres_upgrade_precheck", "clone_instance"}, + ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "postgres_upgrade_precheck", "clone_instance", "create_backup"}, }, }, }, @@ -1503,7 +1503,7 @@ func TestPrebuiltTools(t *testing.T) { wantToolset: server.ToolsetConfigs{ "cloud_sql_mysql_admin_tools": tools.ToolsetConfig{ Name: "cloud_sql_mysql_admin_tools", - ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "clone_instance"}, + ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "clone_instance", "create_backup"}, }, }, }, @@ -1513,7 +1513,7 @@ func TestPrebuiltTools(t *testing.T) { wantToolset: server.ToolsetConfigs{ "cloud_sql_mssql_admin_tools": tools.ToolsetConfig{ Name: "cloud_sql_mssql_admin_tools", - ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "clone_instance"}, + ToolNames: []string{"create_instance", "get_instance", "list_instances", "create_database", "list_databases", "create_user", "wait_for_operation", "clone_instance", "create_backup"}, }, }, }, diff --git a/docs/en/how-to/connect-ide/cloud_sql_mssql_admin_mcp.md b/docs/en/how-to/connect-ide/cloud_sql_mssql_admin_mcp.md index 03abf8422eb1..c88e6edfc089 100644 --- a/docs/en/how-to/connect-ide/cloud_sql_mssql_admin_mcp.md +++ b/docs/en/how-to/connect-ide/cloud_sql_mssql_admin_mcp.md @@ -48,6 +48,7 @@ instance, database and users: * `roles/cloudsql.editor`: Provides permissions to manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * `roles/cloudsql.admin`: Provides full control over all resources. * All `editor` and `viewer` tools * `create_instance` @@ -299,6 +300,7 @@ instances and interacting with your database: * **create_user**: Creates a new user in a Cloud SQL instance. * **wait_for_operation**: Waits for a Cloud SQL operation to complete. * **clone_instance**: Creates a clone of an existing Cloud SQL for SQL Server instance. +* **create_backup**: Creates a backup on a Cloud SQL instance. {{< notice note >}} Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs diff --git a/docs/en/how-to/connect-ide/cloud_sql_mysql_admin_mcp.md b/docs/en/how-to/connect-ide/cloud_sql_mysql_admin_mcp.md index 85eb0412134d..d68b7562cbd8 100644 --- a/docs/en/how-to/connect-ide/cloud_sql_mysql_admin_mcp.md +++ b/docs/en/how-to/connect-ide/cloud_sql_mysql_admin_mcp.md @@ -48,6 +48,7 @@ database and users: * `roles/cloudsql.editor`: Provides permissions to manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * `roles/cloudsql.admin`: Provides full control over all resources. * All `editor` and `viewer` tools * `create_instance` @@ -299,6 +300,7 @@ instances and interacting with your database: * **create_user**: Creates a new user in a Cloud SQL instance. * **wait_for_operation**: Waits for a Cloud SQL operation to complete. * **clone_instance**: Creates a clone of an existing Cloud SQL for MySQL instance. +* **create_backup**: Creates a backup on a Cloud SQL instance. {{< notice note >}} Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs diff --git a/docs/en/how-to/connect-ide/cloud_sql_pg_admin_mcp.md b/docs/en/how-to/connect-ide/cloud_sql_pg_admin_mcp.md index fabeb0d8beb3..035daa4a1aba 100644 --- a/docs/en/how-to/connect-ide/cloud_sql_pg_admin_mcp.md +++ b/docs/en/how-to/connect-ide/cloud_sql_pg_admin_mcp.md @@ -48,6 +48,7 @@ instance, database and users: * `roles/cloudsql.editor`: Provides permissions to manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * `roles/cloudsql.admin`: Provides full control over all resources. * All `editor` and `viewer` tools * `create_instance` @@ -299,6 +300,7 @@ instances and interacting with your database: * **create_user**: Creates a new user in a Cloud SQL instance. * **wait_for_operation**: Waits for a Cloud SQL operation to complete. * **clone_instance**: Creates a clone of an existing Cloud SQL for PostgreSQL instance. +* **create_backup**: Creates a backup on a Cloud SQL instance. {{< notice note >}} Prebuilt tools are pre-1.0, so expect some tool changes between versions. LLMs diff --git a/docs/en/reference/prebuilt-tools.md b/docs/en/reference/prebuilt-tools.md index 061f27b0ff14..c13ebceb3bb9 100644 --- a/docs/en/reference/prebuilt-tools.md +++ b/docs/en/reference/prebuilt-tools.md @@ -187,6 +187,7 @@ See [Usage Examples](../reference/cli.md#examples). manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * **Cloud SQL Admin** (`roles/cloudsql.admin`): Provides full control over all resources. * All `editor` and `viewer` tools @@ -203,6 +204,7 @@ See [Usage Examples](../reference/cli.md#examples). * `create_user`: Creates a new user in a Cloud SQL instance. * `wait_for_operation`: Waits for a Cloud SQL operation to complete. * `clone_instance`: Creates a clone for an existing Cloud SQL for MySQL instance. + * `create_backup`: Creates a backup on a Cloud SQL instance. ## Cloud SQL for PostgreSQL @@ -275,6 +277,7 @@ See [Usage Examples](../reference/cli.md#examples). manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * **Cloud SQL Admin** (`roles/cloudsql.admin`): Provides full control over all resources. * All `editor` and `viewer` tools @@ -290,6 +293,7 @@ See [Usage Examples](../reference/cli.md#examples). * `create_user`: Creates a new user in a Cloud SQL instance. * `wait_for_operation`: Waits for a Cloud SQL operation to complete. * `clone_instance`: Creates a clone for an existing Cloud SQL for PostgreSQL instance. + * `create_backup`: Creates a backup on a Cloud SQL instance. ## Cloud SQL for SQL Server @@ -336,6 +340,7 @@ See [Usage Examples](../reference/cli.md#examples). manage existing resources. * All `viewer` tools * `create_database` + * `create_backup` * **Cloud SQL Admin** (`roles/cloudsql.admin`): Provides full control over all resources. * All `editor` and `viewer` tools @@ -351,6 +356,7 @@ See [Usage Examples](../reference/cli.md#examples). * `create_user`: Creates a new user in a Cloud SQL instance. * `wait_for_operation`: Waits for a Cloud SQL operation to complete. * `clone_instance`: Creates a clone for an existing Cloud SQL for SQL Server instance. + * `create_backup`: Creates a backup on a Cloud SQL instance. ## Dataplex diff --git a/docs/en/resources/tools/cloudgda/cloud-gda-query.md b/docs/en/resources/tools/cloudgda/cloud-gda-query.md index faf119d6e6fa..0ec1812d62d6 100644 --- a/docs/en/resources/tools/cloudgda/cloud-gda-query.md +++ b/docs/en/resources/tools/cloudgda/cloud-gda-query.md @@ -41,13 +41,13 @@ tools: ### Usage Flow -When using this tool, a `prompt` parameter containing a natural language query is provided to the tool (typically by an agent). The tool then interacts with the Gemini Data Analytics API using the context defined in your configuration. +When using this tool, a `query` parameter containing a natural language query is provided to the tool (typically by an agent). The tool then interacts with the Gemini Data Analytics API using the context defined in your configuration. The structure of the response depends on the `generationOptions` configured in your tool definition (e.g., enabling `generateQueryResult` will include the SQL query results). See [Data Analytics API REST documentation](https://clouddocs.devsite.corp.google.com/gemini/docs/conversational-analytics-api/reference/rest/v1alpha/projects.locations/queryData?rep_location=global) for details. -**Example Input Prompt:** +**Example Input Query:** ```text How many accounts who have region in Prague are eligible for loans? A3 contains the data of region. diff --git a/docs/en/resources/tools/cloudsql/cloudsqlcreatebackup.md b/docs/en/resources/tools/cloudsql/cloudsqlcreatebackup.md new file mode 100644 index 000000000000..751534a0ba3e --- /dev/null +++ b/docs/en/resources/tools/cloudsql/cloudsqlcreatebackup.md @@ -0,0 +1,45 @@ +--- +title: cloud-sql-create-backup +type: docs +weight: 10 +description: "Creates a backup on a Cloud SQL instance." +--- + +The `cloud-sql-create-backup` tool creates an on-demand backup on a Cloud SQL instance using the Cloud SQL Admin API. + +{{< notice info dd>}} +This tool uses a `source` of kind `cloud-sql-admin`. +{{< /notice >}} + +## Examples + +Basic backup creation (current state) + +```yaml +tools: + backup-creation-basic: + kind: cloud-sql-create-backup + source: cloud-sql-admin-source + description: "Creates a backup on the given Cloud SQL instance." +``` +## Reference +### Tool Configuration +| **field** | **type** | **required** | **description** | +| -------------- | :------: | :----------: | ------------------------------------------------------------- | +| kind | string | true | Must be "cloud-sql-create-backup". | +| source | string | true | The name of the `cloud-sql-admin` source to use. | +| description | string | false | A description of the tool. | + +### Tool Inputs + +| **parameter** | **type** | **required** | **description** | +| -------------------------- | :------: | :----------: | ------------------------------------------------------------------------------- | +| project | string | true | The project ID. | +| instance | string | true | The name of the instance to take a backup on. Does not include the project ID. | +| location | string | false | (Optional) Location of the backup run. | +| backup_description | string | false | (Optional) The description of this backup run. | + +## See Also +- [Cloud SQL Admin API documentation](https://cloud.google.com/sql/docs/mysql/admin-api) +- [Toolbox Cloud SQL tools documentation](../cloudsql) +- [Cloud SQL Backup API documentation](https://cloud.google.com/sql/docs/mysql/backup-recovery/backups) \ No newline at end of file diff --git a/internal/prebuiltconfigs/tools/cloud-sql-mssql-admin.yaml b/internal/prebuiltconfigs/tools/cloud-sql-mssql-admin.yaml index 7830ea45cb19..b3b1cbb19190 100644 --- a/internal/prebuiltconfigs/tools/cloud-sql-mssql-admin.yaml +++ b/internal/prebuiltconfigs/tools/cloud-sql-mssql-admin.yaml @@ -43,6 +43,9 @@ tools: clone_instance: kind: cloud-sql-clone-instance source: cloud-sql-admin-source + create_backup: + kind: cloud-sql-create-backup + source: cloud-sql-admin-source toolsets: cloud_sql_mssql_admin_tools: @@ -54,3 +57,4 @@ toolsets: - create_user - wait_for_operation - clone_instance + - create_backup diff --git a/internal/prebuiltconfigs/tools/cloud-sql-mysql-admin.yaml b/internal/prebuiltconfigs/tools/cloud-sql-mysql-admin.yaml index 145f1cbc33aa..2b5713719625 100644 --- a/internal/prebuiltconfigs/tools/cloud-sql-mysql-admin.yaml +++ b/internal/prebuiltconfigs/tools/cloud-sql-mysql-admin.yaml @@ -43,6 +43,9 @@ tools: clone_instance: kind: cloud-sql-clone-instance source: cloud-sql-admin-source + create_backup: + kind: cloud-sql-create-backup + source: cloud-sql-admin-source toolsets: cloud_sql_mysql_admin_tools: @@ -54,3 +57,4 @@ toolsets: - create_user - wait_for_operation - clone_instance + - create_backup diff --git a/internal/prebuiltconfigs/tools/cloud-sql-postgres-admin.yaml b/internal/prebuiltconfigs/tools/cloud-sql-postgres-admin.yaml index dffac3dc1b89..72d15d69a20d 100644 --- a/internal/prebuiltconfigs/tools/cloud-sql-postgres-admin.yaml +++ b/internal/prebuiltconfigs/tools/cloud-sql-postgres-admin.yaml @@ -46,6 +46,9 @@ tools: postgres_upgrade_precheck: kind: postgres-upgrade-precheck source: cloud-sql-admin-source + create_backup: + kind: cloud-sql-create-backup + source: cloud-sql-admin-source toolsets: cloud_sql_postgres_admin_tools: @@ -58,3 +61,4 @@ toolsets: - wait_for_operation - postgres_upgrade_precheck - clone_instance + - create_backup diff --git a/internal/sources/cloudsqladmin/cloud_sql_admin.go b/internal/sources/cloudsqladmin/cloud_sql_admin.go index 7d8929b78229..5da165cbfa8b 100644 --- a/internal/sources/cloudsqladmin/cloud_sql_admin.go +++ b/internal/sources/cloudsqladmin/cloud_sql_admin.go @@ -352,6 +352,28 @@ func (s *Source) GetWaitForOperations(ctx context.Context, service *sqladmin.Ser return nil, nil } +func (s *Source) InsertBackupRun(ctx context.Context, project, instance, location, backupDescription, accessToken string) (any, error) { + backupRun := &sqladmin.BackupRun{} + if location != "" { + backupRun.Location = location + } + if backupDescription != "" { + backupRun.Description = backupDescription + } + + service, err := s.GetService(ctx, string(accessToken)) + if err != nil { + return nil, err + } + + resp, err := service.BackupRuns.Insert(project, instance, backupRun).Do() + if err != nil { + return nil, fmt.Errorf("error creating backup: %w", err) + } + + return resp, nil +} + func generateCloudSQLConnectionMessage(ctx context.Context, source *Source, logger log.Logger, opResponse map[string]any, connectionMessageTemplate string) (string, bool) { operationType, ok := opResponse["operationType"].(string) if !ok || operationType != "CREATE_DATABASE" { diff --git a/internal/tools/cloudgda/cloudgda.go b/internal/tools/cloudgda/cloudgda.go index c70d5ba3f002..6ea132318e3f 100644 --- a/internal/tools/cloudgda/cloudgda.go +++ b/internal/tools/cloudgda/cloudgda.go @@ -28,6 +28,21 @@ import ( const kind string = "cloud-gemini-data-analytics-query" +// Guidance is the tool guidance string. +const Guidance = `Tool guidance: + Inputs: + 1. query: A natural language formulation of a database query. + Outputs: (all optional) + 1. disambiguation_question: Clarification questions or comments where the tool needs the users' input. + 2. generated_query: The generated query for the user query. + 3. intent_explanation: An explanation for why the tool produced ` + "`generated_query`" + `. + 4. query_result: The result of executing ` + "`generated_query`" + `. + 5. natural_language_answer: The natural language answer that summarizes the ` + "`query`" + ` and ` + "`query_result`" + `. + +Usage guidance: + 1. If ` + "`disambiguation_question`" + ` is produced, then solicit the needed inputs from the user and try the tool with a new ` + "`query`" + ` that has the needed clarification. + 2. If ` + "`natural_language_answer`" + ` is produced, use ` + "`intent_explanation`" + ` and ` + "`generated_query`" + ` to see if you need to clarify any assumptions for the user.` + func init() { if !tools.Register(kind, newConfig) { panic(fmt.Sprintf("tool kind %q already registered", kind)) @@ -68,11 +83,18 @@ func (cfg Config) ToolConfigKind() string { func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { // Define the parameters for the Gemini Data Analytics Query API - // The prompt is the only input parameter. + // The query is the only input parameter. allParameters := parameters.Parameters{ - parameters.NewStringParameterWithRequired("prompt", "The natural language question to ask.", true), + parameters.NewStringParameterWithRequired("query", "A natural language formulation of a database query.", true), } + // The input and outputs are for tool guidance, usage guidance is for multi-turn interaction. + guidance := Guidance + if cfg.Description != "" { + cfg.Description += "\n\n" + guidance + } else { + cfg.Description = guidance + } mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) return Tool{ @@ -105,9 +127,9 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para } paramsMap := params.AsMap() - prompt, ok := paramsMap["prompt"].(string) + query, ok := paramsMap["query"].(string) if !ok { - return nil, fmt.Errorf("prompt parameter not found or not a string") + return nil, fmt.Errorf("query parameter not found or not a string") } // Parse the access token if provided @@ -125,7 +147,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para payload := &QueryDataRequest{ Parent: payloadParent, - Prompt: prompt, + Prompt: query, Context: t.Context, GenerationOptions: t.GenerationOptions, } diff --git a/internal/tools/cloudgda/cloudgda_test.go b/internal/tools/cloudgda/cloudgda_test.go index 34e8848cb8b9..2e92c56213be 100644 --- a/internal/tools/cloudgda/cloudgda_test.go +++ b/internal/tools/cloudgda/cloudgda_test.go @@ -328,9 +328,9 @@ func TestInvoke(t *testing.T) { t.Fatalf("failed to initialize tool: %v", err) } - // Prepare parameters for invocation - ONLY prompt + // Prepare parameters for invocation - ONLY query params := parameters.ParamValues{ - {Name: "prompt", Value: "How many accounts who have region in Prague are eligible for loans?"}, + {Name: "query", Value: "How many accounts who have region in Prague are eligible for loans?"}, } resourceMgr := resources.NewResourceManager(srcs, nil, nil, nil, nil, nil, nil) diff --git a/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup.go b/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup.go new file mode 100644 index 000000000000..2ad723f0d5ab --- /dev/null +++ b/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup.go @@ -0,0 +1,180 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudsqlcreatebackup + +import ( + "context" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/googleapis/genai-toolbox/internal/embeddingmodels" + "github.com/googleapis/genai-toolbox/internal/sources" + "github.com/googleapis/genai-toolbox/internal/tools" + "github.com/googleapis/genai-toolbox/internal/util/parameters" + "google.golang.org/api/sqladmin/v1" +) + +const kind string = "cloud-sql-create-backup" + +var _ tools.ToolConfig = Config{} + +type compatibleSource interface { + GetDefaultProject() string + GetService(context.Context, string) (*sqladmin.Service, error) + UseClientAuthorization() bool + InsertBackupRun(ctx context.Context, project, instance, location, backupDescription, accessToken string) (any, error) +} + +// Config defines the configuration for the create-backup tool. +type Config struct { + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Description string `yaml:"description"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired"` +} + +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 +} + +// ToolConfigKind returns the kind of the tool. +func (cfg Config) ToolConfigKind() string { + return kind +} + +// Initialize initializes the tool from the configuration. +func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) { + rawS, ok := srcs[cfg.Source] + if !ok { + return nil, fmt.Errorf("no source named %q configured", cfg.Source) + } + s, ok := rawS.(compatibleSource) + if !ok { + return nil, fmt.Errorf("invalid source for %q tool: source %q not compatible", kind, cfg.Source) + } + + project := s.GetDefaultProject() + var projectParam parameters.Parameter + if project != "" { + projectParam = parameters.NewStringParameterWithDefault("project", project, "The GCP project ID. This is pre-configured; do not ask for it unless the user explicitly provides a different one.") + } else { + projectParam = parameters.NewStringParameter("project", "The project ID") + } + + allParameters := parameters.Parameters{ + projectParam, + parameters.NewStringParameter("instance", "Cloud SQL instance ID. This does not include the project ID."), + // Location and backup_description are optional. + parameters.NewStringParameterWithRequired("location", "Location of the backup run.", false), + parameters.NewStringParameterWithRequired("backup_description", "The description of this backup run.", false), + } + paramManifest := allParameters.Manifest() + + description := cfg.Description + if description == "" { + description = "Creates a backup on a Cloud SQL instance." + } + + mcpManifest := tools.GetMcpManifest(cfg.Name, description, cfg.AuthRequired, allParameters, nil) + + return Tool{ + Config: cfg, + AllParams: allParameters, + manifest: tools.Manifest{Description: description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired}, + mcpManifest: mcpManifest, + }, nil +} + +// Tool represents the create-backup tool. +type Tool struct { + Config + AllParams parameters.Parameters `yaml:"allParams"` + manifest tools.Manifest + mcpManifest tools.McpManifest +} + +func (t Tool) ToConfig() tools.ToolConfig { + return t.Config +} + +func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Kind) + if err != nil { + return nil, err + } + paramsMap := params.AsMap() + + project, ok := paramsMap["project"].(string) + if !ok { + return nil, fmt.Errorf("error casting 'project' parameter: %v", paramsMap["project"]) + } + instance, ok := paramsMap["instance"].(string) + if !ok { + return nil, fmt.Errorf("error casting 'instance' parameter: %v", paramsMap["instance"]) + } + + location, _ := paramsMap["location"].(string) + description, _ := paramsMap["backup_description"].(string) + + return source.InsertBackupRun(ctx, project, instance, location, description, string(accessToken)) +} + +// ParseParams parses the parameters for the tool. +func (t Tool) ParseParams(data map[string]any, claims map[string]map[string]any) (parameters.ParamValues, error) { + return parameters.ParseParams(t.AllParams, data, claims) +} + +func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { + return parameters.EmbedParams(ctx, t.AllParams, paramValues, embeddingModelsMap, nil) +} + +// Manifest returns the tool's manifest. +func (t Tool) Manifest() tools.Manifest { + return t.manifest +} + +// McpManifest returns the tool's MCP manifest. +func (t Tool) McpManifest() tools.McpManifest { + return t.mcpManifest +} + +// Authorized checks if the tool is authorized. +func (t Tool) Authorized(verifiedAuthServices []string) bool { + return true +} + +func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) { + source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Kind) + if err != nil { + return false, err + } + + return source.UseClientAuthorization(), nil +} + +func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) { + return "Authorization", nil +} diff --git a/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup_test.go b/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup_test.go new file mode 100644 index 000000000000..f3f46a470df0 --- /dev/null +++ b/internal/tools/cloudsql/cloudsqlcreatebackup/cloudsqlcreatebackup_test.go @@ -0,0 +1,72 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudsqlcreatebackup_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/testutils" + "github.com/googleapis/genai-toolbox/internal/tools/cloudsql/cloudsqlcreatebackup" +) + +func TestParseFromYaml(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: + create-backup-tool: + kind: cloud-sql-create-backup + description: a test description + source: a-source + `, + want: server.ToolConfigs{ + "create-backup-tool": cloudsqlcreatebackup.Config{ + Name: "create-backup-tool", + Kind: "cloud-sql-create-backup", + Description: "a test description", + Source: "a-source", + 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) + } + }) + } +} diff --git a/tests/cloudgda/cloud_gda_integration_test.go b/tests/cloudgda/cloud_gda_integration_test.go index 3a7c8ad07fd8..343973f14729 100644 --- a/tests/cloudgda/cloud_gda_integration_test.go +++ b/tests/cloudgda/cloud_gda_integration_test.go @@ -139,12 +139,12 @@ func TestCloudGdaToolEndpoints(t *testing.T) { // 1. RunToolGetTestByName expectedManifest := map[string]any{ toolName: map[string]any{ - "description": "Test GDA Tool", + "description": "Test GDA Tool\n\n" + cloudgda.Guidance, "parameters": []any{ map[string]any{ - "name": "prompt", + "name": "query", "type": "string", - "description": "The natural language question to ask.", + "description": "A natural language formulation of a database query.", "required": true, "authSources": []any{}, }, @@ -155,7 +155,7 @@ func TestCloudGdaToolEndpoints(t *testing.T) { tests.RunToolGetTestByName(t, toolName, expectedManifest) // 2. RunToolInvokeParametersTest - params := []byte(`{"prompt": "test question"}`) + params := []byte(`{"query": "test question"}`) tests.RunToolInvokeParametersTest(t, toolName, params, "\"queryResult\":\"SELECT * FROM table;\"") // 3. Manual MCP Tool Call Test @@ -172,7 +172,7 @@ func TestCloudGdaToolEndpoints(t *testing.T) { Params: map[string]any{ "name": toolName, "arguments": map[string]any{ - "prompt": "test question", + "query": "test question", }, }, } diff --git a/tests/cloudsql/cloud_sql_create_backup_test.go b/tests/cloudsql/cloud_sql_create_backup_test.go new file mode 100644 index 000000000000..d9b7d052649c --- /dev/null +++ b/tests/cloudsql/cloud_sql_create_backup_test.go @@ -0,0 +1,232 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudsql + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "regexp" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/googleapis/genai-toolbox/internal/testutils" + "github.com/googleapis/genai-toolbox/tests" + "google.golang.org/api/sqladmin/v1" +) + +var ( + createBackupToolKind = "cloud-sql-create-backup" +) + +type createBackupTransport struct { + transport http.RoundTripper + url *url.URL +} + +func (t *createBackupTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if strings.HasPrefix(req.URL.String(), "https://sqladmin.googleapis.com") { + req.URL.Scheme = t.url.Scheme + req.URL.Host = t.url.Host + } + return t.transport.RoundTrip(req) +} + +type mastercreateBackupHandler struct { + t *testing.T +} + +func (h *mastercreateBackupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.UserAgent(), "genai-toolbox/") { + h.t.Errorf("User-Agent header not found") + } + var backupRun sqladmin.BackupRun + if err := json.NewDecoder(r.Body).Decode(&backupRun); err != nil { + h.t.Fatalf("failed to decode request body: %v", err) + } else { + h.t.Logf("Received request body: %+v", backupRun) + } + + var expectedBackupRun sqladmin.BackupRun + var response any + var statusCode int + + switch backupRun.Description { + case "": + expectedBackupRun = sqladmin.BackupRun{} + response = map[string]any{"name": "op1", "status": "PENDING"} + statusCode = http.StatusOK + case "test desc": + expectedBackupRun = sqladmin.BackupRun{Location: "us-central1", Description: "test desc"} + response = map[string]any{"name": "op1", "status": "PENDING"} + statusCode = http.StatusOK + default: + http.Error(w, fmt.Sprintf("unhandled instance name: %s", backupRun.Instance), http.StatusInternalServerError) + return + } + + if diff := cmp.Diff(expectedBackupRun, backupRun); diff != "" { + h.t.Errorf("unexpected request body (-want +got):\n%s", diff) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func TestCreateBackupToolEndpoints(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + handler := &mastercreateBackupHandler{t: t} + server := httptest.NewServer(handler) + defer server.Close() + + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("failed to parse server URL: %v", err) + } + + originalTransport := http.DefaultClient.Transport + if originalTransport == nil { + originalTransport = http.DefaultTransport + } + http.DefaultClient.Transport = &createBackupTransport{ + transport: originalTransport, + url: serverURL, + } + t.Cleanup(func() { + http.DefaultClient.Transport = originalTransport + }) + + var args []string + toolsFile := getCreateBackupToolsConfig() + 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, 30*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) + } + + tcs := []struct { + name string + toolName string + body string + want string + expectError bool + errorStatus int + }{ + { + name: "successful backup creation with no optional parameters", + toolName: "create-backup", + body: `{"project": "p1", "instance": "instance-no-optional"}`, + want: `{"name":"op1","status":"PENDING"}`, + }, + { + name: "successful backup creation with optional parameters", + toolName: "create-backup", + body: `{"project": "p1", "instance": "instance-optional", "location": "us-central1", "backup_description": "test desc"}`, + want: `{"name":"op1","status":"PENDING"}`, + }, + { + name: "missing instance name", + toolName: "create-backup", + body: `{"project": "p1", "escription": "invalid"}`, + expectError: true, + errorStatus: http.StatusBadRequest, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + api := fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/invoke", tc.toolName) + req, err := http.NewRequest(http.MethodPost, api, bytes.NewBufferString(tc.body)) + if err != nil { + t.Fatalf("unable to create request: %s", err) + } + req.Header.Add("Content-type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("unable to send request: %s", err) + } + defer resp.Body.Close() + + if tc.expectError { + if resp.StatusCode != tc.errorStatus { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("expected status %d but got %d: %s", tc.errorStatus, resp.StatusCode, string(bodyBytes)) + } + return + } + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + t.Fatalf("response status code is not 200, got %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var result struct { + Result string `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + var got, want map[string]any + if err := json.Unmarshal([]byte(result.Result), &got); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + if err := json.Unmarshal([]byte(tc.want), &want); err != nil { + t.Fatalf("failed to unmarshal want: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf("unexpected result: got %+v, want %+v", got, want) + } + }) + } +} + +func getCreateBackupToolsConfig() map[string]any { + return map[string]any{ + "sources": map[string]any{ + "my-cloud-sql-source": map[string]any{ + "kind": "cloud-sql-admin", + }, + }, + "tools": map[string]any{ + "create-backup": map[string]any{ + "kind": createBackupToolKind, + "source": "my-cloud-sql-source", + }, + }, + } +}