e04ed9c211
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
197 lines
7.8 KiB
Go
197 lines
7.8 KiB
Go
// 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 vectorassistimprovequeryrecall
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
yaml "github.com/goccy/go-yaml"
|
|
"github.com/googleapis/mcp-toolbox/internal/tools"
|
|
"github.com/googleapis/mcp-toolbox/internal/util"
|
|
"github.com/googleapis/mcp-toolbox/internal/util/parameters"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
const resourceType string = "vector-assist-improve-query-recall"
|
|
|
|
// Query to check if the index exists and if it is an HNSW index.
|
|
const checkIndexQuery = `
|
|
SELECT
|
|
(COUNT(1) > 0) AS index_present,
|
|
COALESCE(BOOL_OR(indexdef ILIKE '%USING hnsw%'), false) AS is_hnsw
|
|
FROM pg_indexes
|
|
WHERE schemaname = @schema_name::TEXT
|
|
AND tablename = @table_name::TEXT
|
|
AND indexname = @index_name::TEXT
|
|
AND indexdef ILIKE '%' || @vector_column_name::TEXT || '%';
|
|
`
|
|
|
|
// Query to find the optimal index parameters
|
|
const improveRecallQuery = `
|
|
SELECT output_ef_search AS ef_search
|
|
FROM vector_assist.find_ef_search_for_target_recall(
|
|
table_name => @table_name::TEXT,
|
|
schema_name => @schema_name::TEXT,
|
|
column_name => @vector_column_name::TEXT,
|
|
top_k => @top_k::INT,
|
|
target_recall => @target_recall::FLOAT,
|
|
distance_func => @distance_func::TEXT
|
|
);
|
|
`
|
|
|
|
func init() {
|
|
if !tools.Register(resourceType, newConfig) {
|
|
panic(fmt.Sprintf("tool type %q already registered", resourceType))
|
|
}
|
|
}
|
|
|
|
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
|
|
actual := Config{ConfigBase: tools.ConfigBase{Name: name}}
|
|
if err := decoder.DecodeContext(ctx, &actual); err != nil {
|
|
return nil, err
|
|
}
|
|
return actual, nil
|
|
}
|
|
|
|
type compatibleSource interface {
|
|
PostgresPool() *pgxpool.Pool
|
|
RunSQL(context.Context, string, []any) (any, error)
|
|
}
|
|
|
|
type Config struct {
|
|
tools.ConfigBase `yaml:",inline"`
|
|
Type string `yaml:"type" validate:"required"`
|
|
Source string `yaml:"source" validate:"required"`
|
|
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
|
|
}
|
|
|
|
var _ tools.ToolConfig = Config{}
|
|
|
|
func (cfg Config) ToolConfigType() string {
|
|
return resourceType
|
|
}
|
|
|
|
func (cfg Config) Initialize(context.Context) (tools.Tool, error) {
|
|
allParameters := parameters.Parameters{
|
|
parameters.NewStringParameter("schema_name", "Optional parameter: Schema name of the table.", parameters.WithStringDefault("public")),
|
|
parameters.NewStringParameter("table_name", "Table name experiencing degraded vector search recall.", parameters.WithStringRequired(true)),
|
|
parameters.NewStringParameter("vector_column_name", "Column name containing the vector embeddings.", parameters.WithStringRequired(true)),
|
|
parameters.NewStringParameter("index_name", "Name of the vector index to tune.", parameters.WithStringRequired(true)),
|
|
parameters.NewIntParameter("top_k", "Optional parameter: Top k value for the vector search.", parameters.WithIntDefault(10)),
|
|
parameters.NewFloatParameter("target_recall", "Optional parameter: Target recall value for search results.", parameters.WithFloatDefault(0.95)),
|
|
parameters.NewStringParameter("distance_func", "Optional parameter: Distance function used for the vector search similarity.", parameters.WithStringDefault("cosine")),
|
|
}
|
|
paramManifest := allParameters.Manifest()
|
|
|
|
if cfg.Description == "" {
|
|
cfg.Description = "Use this tool to troubleshoot and optimize existing vector search workloads when a user reports irrelevant results, poor accuracy, or degraded recall. It determines the optimal tuning parameter (such as ef_search) for an active vector index to improve the search results. The tool outputs an actionable SQL query recommendation to be executed as a next action using the 'execute_sql' tool."
|
|
}
|
|
|
|
return Tool{
|
|
BaseTool: tools.NewBaseTool(
|
|
cfg,
|
|
tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations),
|
|
tools.Manifest{Description: cfg.Description, Parameters: paramManifest, AuthRequired: cfg.AuthRequired},
|
|
allParameters,
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
var _ tools.Tool = Tool{}
|
|
|
|
type Tool struct {
|
|
tools.BaseTool[Config]
|
|
}
|
|
|
|
func (t Tool) ToConfig() tools.ToolConfig {
|
|
return t.Cfg
|
|
}
|
|
|
|
func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
|
|
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Cfg.Source, t.Cfg.Name, t.Cfg.Type)
|
|
if err != nil {
|
|
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
|
|
}
|
|
paramsMap := params.AsMap()
|
|
|
|
namedArgs := pgx.NamedArgs{}
|
|
for key, value := range paramsMap {
|
|
namedArgs[key] = value
|
|
}
|
|
|
|
// Check if the index exists and if it is an HNSW index.
|
|
checkResp, err := source.RunSQL(ctx, checkIndexQuery, []any{namedArgs})
|
|
if err != nil {
|
|
return nil, util.ProcessGeneralError(err)
|
|
}
|
|
|
|
checkBytes, marshalErr := json.Marshal(checkResp)
|
|
if marshalErr != nil {
|
|
return nil, util.NewClientServerError("failed to process index check response", http.StatusInternalServerError, marshalErr)
|
|
}
|
|
|
|
var checkRows []map[string]interface{}
|
|
if unmarshalErr := json.Unmarshal(checkBytes, &checkRows); unmarshalErr != nil || len(checkRows) == 0 {
|
|
return nil, util.NewClientServerError("unexpected empty response from database", http.StatusInternalServerError, unmarshalErr)
|
|
}
|
|
|
|
row := checkRows[0]
|
|
indexPresent, ok := row["index_present"].(bool)
|
|
if !ok {
|
|
// If the key is missing or isn't a boolean, it's likely a server-side/query issue.
|
|
return nil, util.NewClientServerError("Internal error: 'index_present' is missing or has an invalid type.", http.StatusInternalServerError, nil)
|
|
}
|
|
if !indexPresent {
|
|
return nil, util.NewClientServerError("Index not found for the given table and vector column. If the table lacks an existing vector setup, use the 'define_spec' tool to configure the database.", http.StatusBadRequest, nil)
|
|
}
|
|
|
|
isHnsw, ok := row["is_hnsw"].(bool)
|
|
if !ok {
|
|
return nil, util.NewClientServerError("Internal error: 'is_hnsw' is missing or has an invalid type.", http.StatusInternalServerError, nil)
|
|
}
|
|
if !isHnsw {
|
|
return nil, util.NewClientServerError("Unsupported index type for recall optimization. Only HNSW index is supported.", http.StatusBadRequest, nil)
|
|
}
|
|
|
|
// Calculate the optimal index parameters to achieve the target recall.
|
|
tuningResp, err := source.RunSQL(ctx, improveRecallQuery, []any{namedArgs})
|
|
if err != nil {
|
|
return nil, util.ProcessGeneralError(err)
|
|
}
|
|
|
|
tuningBytes, marshalErr := json.Marshal(tuningResp)
|
|
if marshalErr != nil {
|
|
return nil, util.NewClientServerError("failed to process tuning response", http.StatusInternalServerError, marshalErr)
|
|
}
|
|
|
|
var tuningRows []map[string]interface{}
|
|
if unmarshalErr := json.Unmarshal(tuningBytes, &tuningRows); unmarshalErr != nil || len(tuningRows) == 0 {
|
|
return nil, util.NewClientServerError("unexpected empty tuning response from database", http.StatusInternalServerError, unmarshalErr)
|
|
}
|
|
|
|
// Extract ef_search (JSON decoder defaults numbers to float64)
|
|
efSearchVal, ok := tuningRows[0]["ef_search"].(float64)
|
|
if !ok {
|
|
return nil, util.NewClientServerError("Failed to calculate appropriate efSearch value", http.StatusInternalServerError, nil)
|
|
}
|
|
|
|
queryRecommendation := fmt.Sprintf("SET hnsw.ef_search = %d;", int(efSearchVal))
|
|
return queryRecommendation, nil
|
|
}
|