Merge branch 'main' into adk-python-processing

This commit is contained in:
Twisha Bansal
2026-02-19 10:08:34 +05:30
committed by GitHub
13 changed files with 328 additions and 59 deletions

View File

@@ -114,7 +114,7 @@ jobs:
- name: Create PR Comment
if: env.HAS_CHANGES == 'true' && steps.lychee-check.outcome == 'failure'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -3231,9 +3231,10 @@
}
},
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",

View File

@@ -133,3 +133,4 @@ instead of hardcoding your secrets into the configuration file.
| user | string | true | Name of the Postgres user to connect as (e.g. "my-pg-user"). |
| password | string | true | Password of the Postgres user (e.g. "my-password"). |
| queryParams | map[string]string | false | Raw query to be added to the db connection string. |
| queryExecMode | string | false | pgx query execution mode. Valid values: `cache_statement` (default), `cache_describe`, `describe_exec`, `exec`, `simple_protocol`. Useful with connection poolers that don't support prepared statement caching. |

View File

@@ -4,8 +4,8 @@ linkTitle: "Redis"
type: docs
weight: 1
description: >
Redis is a in-memory data structure store.
Redis is a in-memory data structure store.
---
## About
@@ -44,6 +44,9 @@ password: ${MY_AUTH_STRING} # Omit this field if you don't have a password.
# database: 0
# clusterEnabled: false
# useGCPIAM: false
# tls:
# enabled: false
# insecureSkipVerify: false
```
{{< notice tip >}}
@@ -61,7 +64,7 @@ Here is an example tools.yaml config with [AUTH][auth] enabled:
```yaml
kind: sources
name: my-redis-cluster-instance
type: memorystore-redis
type: redis
address:
- 127.0.0.1:6379
password: ${MY_AUTH_STRING}
@@ -78,7 +81,7 @@ using IAM authentication:
```yaml
kind: sources
name: my-redis-cluster-instance
type: memorystore-redis
type: redis
address:
- 127.0.0.1:6379
useGCPIAM: true
@@ -89,14 +92,16 @@ clusterEnabled: true
## Reference
| **field** | **type** | **required** | **description** |
|----------------|:--------:|:------------:|---------------------------------------------------------------------------------------------------------------------------------|
| type | string | true | Must be "memorystore-redis". |
| address | string | true | Primary endpoint for the Memorystore Redis instance to connect to. |
| username | string | false | If you are using a non-default user, specify the user name here. If you are using Memorystore for Redis, leave this field blank |
| password | string | false | If you have [Redis AUTH][auth] enabled, specify the AUTH string here |
| database | int | false | The Redis database to connect to. Not applicable for cluster enabled instances. The default database is `0`. |
| clusterEnabled | bool | false | Set it to `true` if using a Redis Cluster instance. Defaults to `false`. |
| useGCPIAM | string | false | Set it to `true` if you are using GCP's IAM authentication. Defaults to `false`. |
| **field** | **type** | **required** | **description** |
|------------------------|:--------:|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------|
| type | string | true | Must be "redis". |
| address | string | true | Primary endpoint for the Memorystore Redis instance to connect to. |
| username | string | false | If you are using a non-default user, specify the user name here. If you are using Memorystore for Redis, leave this field blank |
| password | string | false | If you have [Redis AUTH][auth] enabled, specify the AUTH string here |
| database | int | false | The Redis database to connect to. Not applicable for cluster enabled instances. The default database is `0`. |
| tls.enabled | bool | false | Set it to `true` to enable TLS for the Redis connection. Defaults to `false`. |
| tls.insecureSkipVerify | bool | false | Set it to `true` to skip TLS certificate verification. **Warning:** This is insecure and not recommended for production. Defaults to `false`. |
| clusterEnabled | bool | false | Set it to `true` if using a Redis Cluster instance. Defaults to `false`. |
| useGCPIAM | bool | false | Set it to `true` if you are using GCP's IAM authentication. Defaults to `false`. |
[auth]: https://cloud.google.com/memorystore/docs/redis/about-redis-auth

View File

@@ -217,13 +217,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@@ -1598,17 +1598,6 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"optional": true,
"peer": true,
"peerDependencies": {
"zod": "^3.25 || ^4"
}
}
}
}

View File

@@ -87,8 +87,29 @@ body {
display: flex;
flex-direction: column;
padding: 15px;
align-items: center;
align-items: stretch;
position: relative;
min-width: 200px;
max-width: 50vw;
overflow: visible;
box-sizing: border-box;
}
.resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: 0;
width: 4px;
cursor: ew-resize;
background-color: transparent;
z-index: 10;
transition: background-color 0.2s ease;
}
.resize-handle:hover,
.resize-handle.active {
background-color: var(--toolbox-blue);
}
.nav-logo {
@@ -626,10 +647,13 @@ body {
.search-container {
display: flex;
width: 100%;
min-width: 0;
margin-bottom: 15px;
box-sizing: border-box;
#toolset-search-input {
flex-grow: 1;
flex: 1;
min-width: 0;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 20px 0 0 20px;
@@ -637,6 +661,7 @@ body {
font-family: inherit;
font-size: 0.9em;
color: var(--text-primary-gray);
box-sizing: border-box;
&:focus {
outline: none;

View 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.
const STORAGE_KEY = 'toolbox-second-nav-width';
const DEFAULT_WIDTH = 250;
const MIN_WIDTH = 200;
const MAX_WIDTH_PERCENT = 50;
/**
* Creates and attaches a resize handle to the second navigation panel
*/
export function initializeResize() {
const secondNav = document.querySelector('.second-nav');
if (!secondNav) {
return;
}
// Create resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
resizeHandle.setAttribute('aria-label', 'Resize panel');
secondNav.appendChild(resizeHandle);
// Load saved width or use default
let initialWidth = DEFAULT_WIDTH;
try {
const savedWidth = localStorage.getItem(STORAGE_KEY);
if (savedWidth) {
const parsed = parseInt(savedWidth, 10);
if (!isNaN(parsed) && parsed >= MIN_WIDTH) {
initialWidth = parsed;
}
}
} catch (e) {
// localStorage may be unavailable in private browsing mode
console.warn('Failed to load saved panel width:', e);
}
setPanelWidth(secondNav, initialWidth);
// Setup resize functionality
let startX = 0;
let startWidth = 0;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const newWidth = startWidth + deltaX;
const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100;
const clampedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth));
setPanelWidth(secondNav, clampedWidth);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
resizeHandle.classList.remove('active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
// Save width to localStorage
try {
localStorage.setItem(STORAGE_KEY, secondNav.offsetWidth.toString());
} catch (e) {
// localStorage may be unavailable in private browsing mode
console.warn('Failed to save panel width:', e);
}
};
resizeHandle.addEventListener('mousedown', (e) => {
startX = e.clientX;
startWidth = secondNav.offsetWidth;
resizeHandle.classList.add('active');
document.body.style.cursor = 'ew-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Handle window resize to enforce max width
window.addEventListener('resize', () => {
const currentWidth = secondNav.offsetWidth;
const maxWidth = (window.innerWidth * MAX_WIDTH_PERCENT) / 100;
if (currentWidth > maxWidth) {
setPanelWidth(secondNav, maxWidth);
try {
localStorage.setItem(STORAGE_KEY, maxWidth.toString());
} catch (e) {
// localStorage may be unavailable in private browsing mode
console.warn('Failed to save panel width:', e);
}
}
});
}
/**
* Sets the width of the panel and updates flex property
*/
function setPanelWidth(panel, width) {
panel.style.flex = `0 0 ${width}px`;
}

View File

@@ -22,12 +22,16 @@
<script type="module" src="/ui/js/tools.js"></script>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
<script type="module">
document.addEventListener('DOMContentLoaded', async () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions())
renderMainContent('main-content-container', 'tool-display-area', getToolInstructions());
// Initialize resize functionality
const { initializeResize } = await import('/ui/js/resize.js');
initializeResize();
});
</script>
</body>

View File

@@ -29,12 +29,16 @@
<script type="module" src="/ui/js/toolsets.js"></script>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
<script type="module">
document.addEventListener('DOMContentLoaded', async () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', 'tool-display-area', getToolsetInstructions());
// Initialize resize functionality
const { initializeResize } = await import('/ui/js/resize.js');
initializeResize();
});
</script>
</body>

View File

@@ -24,6 +24,7 @@ import (
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/orderedmap"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"go.opentelemetry.io/otel/trace"
)
@@ -48,14 +49,15 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required"`
Database string `yaml:"database" validate:"required"`
QueryParams map[string]string `yaml:"queryParams"`
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password" validate:"required"`
Database string `yaml:"database" validate:"required"`
QueryParams map[string]string `yaml:"queryParams"`
QueryExecMode string `yaml:"queryExecMode" validate:"omitempty,oneof=cache_statement cache_describe describe_exec exec simple_protocol"`
}
func (r Config) SourceConfigType() string {
@@ -63,7 +65,7 @@ func (r Config) SourceConfigType() string {
}
func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
pool, err := initPostgresConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryParams)
pool, err := initPostgresConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.QueryParams, r.QueryExecMode)
if err != nil {
return nil, fmt.Errorf("unable to create pool: %w", err)
}
@@ -126,7 +128,7 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an
return out, nil
}
func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string) (*pgxpool.Pool, error) {
func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname string, queryParams map[string]string, queryExecMode string) (*pgxpool.Pool, error) {
//nolint:all // Reassigned ctx
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceType, name)
defer span.End()
@@ -150,7 +152,18 @@ func initPostgresConnectionPool(ctx context.Context, tracer trace.Tracer, name,
Path: dbname,
RawQuery: ConvertParamMapToRawQuery(queryParams),
}
pool, err := pgxpool.New(ctx, url.String())
config, err := pgxpool.ParseConfig(url.String())
if err != nil {
return nil, fmt.Errorf("unable to parse connection uri: %w", err)
}
execMode, err := ParseQueryExecMode(queryExecMode)
if err != nil {
return nil, err
}
config.ConnConfig.DefaultQueryExecMode = execMode
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %w", err)
}
@@ -165,3 +178,20 @@ func ConvertParamMapToRawQuery(queryParams map[string]string) string {
}
return strings.Join(queryArray, "&")
}
func ParseQueryExecMode(queryExecMode string) (pgx.QueryExecMode, error) {
switch queryExecMode {
case "", "cache_statement":
return pgx.QueryExecModeCacheStatement, nil
case "cache_describe":
return pgx.QueryExecModeCacheDescribe, nil
case "describe_exec":
return pgx.QueryExecModeDescribeExec, nil
case "exec":
return pgx.QueryExecModeExec, nil
case "simple_protocol":
return pgx.QueryExecModeSimpleProtocol, nil
default:
return 0, fmt.Errorf("invalid queryExecMode %q: must be one of %q, %q, %q, %q, or %q", queryExecMode, "cache_statement", "cache_describe", "describe_exec", "exec", "simple_protocol")
}
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/sources/postgres"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/jackc/pgx/v5"
)
func TestParseFromYamlPostgres(t *testing.T) {
@@ -88,6 +89,32 @@ func TestParseFromYamlPostgres(t *testing.T) {
},
},
},
{
desc: "example with query exec mode",
in: `
kind: sources
name: my-pg-instance
type: postgres
host: my-host
port: my-port
database: my_db
user: my_user
password: my_pass
queryExecMode: simple_protocol
`,
want: map[string]sources.SourceConfig{
"my-pg-instance": postgres.Config{
Name: "my-pg-instance",
Type: postgres.SourceType,
Host: "my-host",
Port: "my-port",
Database: "my_db",
User: "my_user",
Password: "my_pass",
QueryExecMode: "simple_protocol",
},
},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
@@ -137,6 +164,21 @@ func TestFailParseFromYaml(t *testing.T) {
`,
err: "error unmarshaling sources: unable to parse source \"my-pg-instance\" as \"postgres\": Key: 'Config.Password' Error:Field validation for 'Password' failed on the 'required' tag",
},
{
desc: "invalid query exec mode",
in: `
kind: sources
name: my-pg-instance
type: postgres
host: my-host
port: my-port
database: my_db
user: my_user
password: my_pass
queryExecMode: invalid_mode
`,
err: "error unmarshaling sources: unable to parse source \"my-pg-instance\" as \"postgres\": [6:16] Key: 'Config.QueryExecMode' Error:Field validation for 'QueryExecMode' failed on the 'oneof' tag\n 3 | name: my-pg-instance\n 4 | password: my_pass\n 5 | port: my-port\n> 6 | queryExecMode: invalid_mode\n ^\n 7 | type: postgres\n 8 | user: my_user",
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
@@ -193,3 +235,32 @@ func TestConvertParamMapToRawQuery(t *testing.T) {
})
}
}
func TestParseQueryExecMode(t *testing.T) {
tcs := []struct {
desc string
in string
want pgx.QueryExecMode
wantErr bool
}{
{desc: "empty (default)", in: "", want: pgx.QueryExecModeCacheStatement},
{desc: "cache_statement", in: "cache_statement", want: pgx.QueryExecModeCacheStatement},
{desc: "cache_describe", in: "cache_describe", want: pgx.QueryExecModeCacheDescribe},
{desc: "describe_exec", in: "describe_exec", want: pgx.QueryExecModeDescribeExec},
{desc: "exec", in: "exec", want: pgx.QueryExecModeExec},
{desc: "simple_protocol", in: "simple_protocol", want: pgx.QueryExecModeSimpleProtocol},
{desc: "invalid mode", in: "invalid_mode", wantErr: true},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
got, err := postgres.ParseQueryExecMode(tc.in)
if (err != nil) != tc.wantErr {
t.Fatalf("parseQueryExecMode() error = %v, wantErr %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Errorf("parseQueryExecMode() = %v, want %v", got, tc.want)
}
})
}
}

View File

@@ -15,6 +15,7 @@ package redis
import (
"context"
"crypto/tls"
"fmt"
"time"
@@ -44,14 +45,20 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
}
type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Address []string `yaml:"address" validate:"required"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Database int `yaml:"database"`
UseGCPIAM bool `yaml:"useGCPIAM"`
ClusterEnabled bool `yaml:"clusterEnabled"`
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Address []string `yaml:"address" validate:"required"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Database int `yaml:"database"`
UseGCPIAM bool `yaml:"useGCPIAM"`
ClusterEnabled bool `yaml:"clusterEnabled"`
TLS TLSConfig `yaml:"tls"`
}
type TLSConfig struct {
Enabled bool `yaml:"enabled"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
}
func (r Config) SourceConfigType() string {
@@ -91,6 +98,13 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) {
}
}
var tlsConfig *tls.Config
if r.TLS.Enabled {
tlsConfig = &tls.Config{
InsecureSkipVerify: r.TLS.InsecureSkipVerify,
}
}
var client RedisClient
var err error
if r.ClusterEnabled {
@@ -104,6 +118,7 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) {
CredentialsProviderContext: authFn,
Username: r.Username,
Password: r.Password,
TLSConfig: tlsConfig,
})
err = clusterClient.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {
return shard.Ping(ctx).Err()
@@ -125,6 +140,7 @@ func initRedisClient(ctx context.Context, r Config) (RedisClient, error) {
CredentialsProviderContext: authFn,
Username: r.Username,
Password: r.Password,
TLSConfig: tlsConfig,
})
_, err = standaloneClient.Ping(ctx).Result()
if err != nil {

View File

@@ -63,6 +63,9 @@ func TestParseFromYamlRedis(t *testing.T) {
database: 1
useGCPIAM: true
clusterEnabled: true
tls:
enabled: true
insecureSkipVerify: true
`,
want: map[string]sources.SourceConfig{
"my-redis-instance": redis.Config{
@@ -73,6 +76,10 @@ func TestParseFromYamlRedis(t *testing.T) {
Database: 1,
ClusterEnabled: true,
UseGCPIAM: true,
TLS: redis.TLSConfig{
Enabled: true,
InsecureSkipVerify: true,
},
},
},
},