Skip to content

Commit 15101b1

Browse files
authored
feat(tools): Add valueFromParam support to Tool config (googleapis#2333)
This PR introduces a new configuration field valueFromParam to the tool definitions. This feature allows a parameter to automatically inherit its value from another sibling parameter, mainly to streamline the configuration of vector insertion tools. Parameters utilizing valueFromParam are excluded from the Tool and MCP manifests. This means the LLM does not see these parameters and is not required to generate them. The value is resolved internally by the Toolbox during execution.
1 parent e4f60e5 commit 15101b1

File tree

5 files changed

+212
-20
lines changed

5 files changed

+212
-20
lines changed

cmd/root_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,3 +2181,115 @@ func TestDefaultToolsFileBehavior(t *testing.T) {
21812181
})
21822182
}
21832183
}
2184+
2185+
func TestParameterReferenceValidation(t *testing.T) {
2186+
ctx, err := testutils.ContextWithNewLogger()
2187+
if err != nil {
2188+
t.Fatalf("unexpected error: %s", err)
2189+
}
2190+
2191+
// Base template
2192+
baseYaml := `
2193+
sources:
2194+
dummy-source:
2195+
kind: http
2196+
baseUrl: http://example.com
2197+
tools:
2198+
test-tool:
2199+
kind: postgres-sql
2200+
source: dummy-source
2201+
description: test tool
2202+
statement: SELECT 1;
2203+
parameters:
2204+
%s`
2205+
2206+
tcs := []struct {
2207+
desc string
2208+
params string
2209+
wantErr bool
2210+
errSubstr string
2211+
}{
2212+
{
2213+
desc: "valid backward reference",
2214+
params: `
2215+
- name: source_param
2216+
type: string
2217+
description: source
2218+
- name: copy_param
2219+
type: string
2220+
description: copy
2221+
valueFromParam: source_param`,
2222+
wantErr: false,
2223+
},
2224+
{
2225+
desc: "valid forward reference (out of order)",
2226+
params: `
2227+
- name: copy_param
2228+
type: string
2229+
description: copy
2230+
valueFromParam: source_param
2231+
- name: source_param
2232+
type: string
2233+
description: source`,
2234+
wantErr: false,
2235+
},
2236+
{
2237+
desc: "invalid missing reference",
2238+
params: `
2239+
- name: copy_param
2240+
type: string
2241+
description: copy
2242+
valueFromParam: non_existent_param`,
2243+
wantErr: true,
2244+
errSubstr: "references '\"non_existent_param\"' in the 'valueFromParam' field",
2245+
},
2246+
{
2247+
desc: "invalid self reference",
2248+
params: `
2249+
- name: myself
2250+
type: string
2251+
description: self
2252+
valueFromParam: myself`,
2253+
wantErr: true,
2254+
errSubstr: "parameter \"myself\" cannot copy value from itself",
2255+
},
2256+
{
2257+
desc: "multiple valid references",
2258+
params: `
2259+
- name: a
2260+
type: string
2261+
description: a
2262+
- name: b
2263+
type: string
2264+
description: b
2265+
valueFromParam: a
2266+
- name: c
2267+
type: string
2268+
description: c
2269+
valueFromParam: a`,
2270+
wantErr: false,
2271+
},
2272+
}
2273+
2274+
for _, tc := range tcs {
2275+
t.Run(tc.desc, func(t *testing.T) {
2276+
// Indent parameters to match YAML structure
2277+
yamlContent := fmt.Sprintf(baseYaml, tc.params)
2278+
2279+
_, err := parseToolsFile(ctx, []byte(yamlContent))
2280+
2281+
if tc.wantErr {
2282+
if err == nil {
2283+
t.Fatal("expected error, got nil")
2284+
}
2285+
if !strings.Contains(err.Error(), tc.errSubstr) {
2286+
t.Errorf("error %q does not contain expected substring %q", err.Error(), tc.errSubstr)
2287+
}
2288+
} else {
2289+
if err != nil {
2290+
t.Fatalf("unexpected error: %v", err)
2291+
}
2292+
}
2293+
})
2294+
}
2295+
}

docs/en/resources/embeddingModels/_index.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ title: "EmbeddingModels"
33
type: docs
44
weight: 2
55
description: >
6-
EmbeddingModels represent services that transform text into vector embeddings for semantic search.
6+
EmbeddingModels represent services that transform text into vector embeddings
7+
for semantic search.
78
---
89

910
EmbeddingModels represent services that generate vector representations of text
10-
data. In the MCP Toolbox, these models enable **Semantic Queries**,
11-
allowing [Tools](../tools/) to automatically convert human-readable text into
12-
numerical vectors before using them in a query.
11+
data. In the MCP Toolbox, these models enable **Semantic Queries**, allowing
12+
[Tools](../tools/) to automatically convert human-readable text into numerical
13+
vectors before using them in a query.
1314

1415
This is primarily used in two scenarios:
1516

@@ -19,14 +20,33 @@ This is primarily used in two scenarios:
1920
- **Semantic Search**: Converting a natural language query into a vector to
2021
perform similarity searches.
2122

23+
## Hidden Parameter Duplication (valueFromParam)
24+
25+
When building tools for vector ingestion, you often need the same input string
26+
twice:
27+
28+
1. To store the original text in a TEXT column.
29+
1. To generate the vector embedding for a VECTOR column.
30+
31+
Requesting an Agent (LLM) to output the exact same string twice is inefficient
32+
and error-prone. The `valueFromParam` field solves this by allowing a parameter
33+
to inherit its value from another parameter in the same tool.
34+
35+
### Key Behaviors
36+
37+
1. Hidden from Manifest: Parameters with valueFromParam set are excluded from
38+
the tool definition sent to the Agent. The Agent does not know this parameter
39+
exists.
40+
1. Auto-Filled: When the tool is executed, the Toolbox automatically copies the
41+
value from the referenced parameter before processing embeddings.
42+
2243
## Example
2344

2445
The following configuration defines an embedding model and applies it to
2546
specific tool parameters.
2647

27-
{{< notice tip >}}
28-
Use environment variable replacement with the format ${ENV_NAME}
29-
instead of hardcoding your API keys into the configuration file.
48+
{{< notice tip >}} Use environment variable replacement with the format
49+
${ENV_NAME} instead of hardcoding your API keys into the configuration file.
3050
{{< /notice >}}
3151

3252
### Step 1 - Define an Embedding Model
@@ -40,14 +60,12 @@ embeddingModels:
4060
model: gemini-embedding-001
4161
apiKey: ${GOOGLE_API_KEY}
4262
dimension: 768
43-
4463
```
4564
4665
### Step 2 - Embed Tool Parameters
4766
4867
Use the defined embedding model, embed your query parameters using the
49-
`embeddedBy` field. Only string-typed
50-
parameters can be embedded:
68+
`embeddedBy` field. Only string-typed parameters can be embedded:
5169

5270
```yaml
5371
tools:
@@ -61,10 +79,13 @@ tools:
6179
parameters:
6280
- name: content
6381
type: string
82+
description: The raw text content to be stored in the database.
6483
- name: vector_string
6584
type: string
66-
description: The text to be vectorized and stored.
67-
embeddedBy: gemini-model # refers to the name of a defined embedding model
85+
# This parameter is hidden from the LLM.
86+
# It automatically copies the value from 'content' and embeds it.
87+
valueFromParam: content
88+
embeddedBy: gemini-model
6889
6990
# Semantic search tool
7091
search_embedding:

internal/server/config.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,43 @@ func (c *ToolConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interfac
296296
return fmt.Errorf("invalid 'kind' field for tool %q (must be a string)", name)
297297
}
298298

299+
// validify parameter references
300+
if rawParams, ok := v["parameters"]; ok {
301+
if paramsList, ok := rawParams.([]any); ok {
302+
// Turn params into a map
303+
validParamNames := make(map[string]bool)
304+
for _, rawP := range paramsList {
305+
if pMap, ok := rawP.(map[string]any); ok {
306+
if pName, ok := pMap["name"].(string); ok && pName != "" {
307+
validParamNames[pName] = true
308+
}
309+
}
310+
}
311+
312+
// Validate references
313+
for i, rawP := range paramsList {
314+
pMap, ok := rawP.(map[string]any)
315+
if !ok {
316+
continue
317+
}
318+
319+
pName, _ := pMap["name"].(string)
320+
refName, _ := pMap["valueFromParam"].(string)
321+
322+
if refName != "" {
323+
// Check if the referenced parameter exists
324+
if !validParamNames[refName] {
325+
return fmt.Errorf("tool %q config error: parameter %q (index %d) references '%q' in the 'valueFromParam' field, which is not a defined parameter", name, pName, i, refName)
326+
}
327+
328+
// Check for self-reference
329+
if refName == pName {
330+
return fmt.Errorf("tool %q config error: parameter %q cannot copy value from itself", name, pName)
331+
}
332+
}
333+
}
334+
}
335+
}
299336
yamlDecoder, err := util.NewStrictDecoder(v)
300337
if err != nil {
301338
return fmt.Errorf("error creating YAML decoder for tool %q: %w", name, err)

internal/util/parameters/parameters.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,12 @@ func ParseParams(ps Parameters, data map[string]any, claimsMap map[string]map[st
134134
var err error
135135
paramAuthServices := p.GetAuthServices()
136136
name := p.GetName()
137-
if len(paramAuthServices) == 0 {
137+
138+
sourceParamName := p.GetValueFromParam()
139+
if sourceParamName != "" {
140+
v = data[sourceParamName]
141+
142+
} else if len(paramAuthServices) == 0 {
138143
// parse non auth-required parameter
139144
var ok bool
140145
v, ok = data[name]
@@ -318,6 +323,7 @@ type Parameter interface {
318323
GetRequired() bool
319324
GetAuthServices() []ParamAuthService
320325
GetEmbeddedBy() string
326+
GetValueFromParam() string
321327
Parse(any) (any, error)
322328
Manifest() ParameterManifest
323329
McpManifest() (ParameterMcpManifest, []string)
@@ -465,6 +471,9 @@ func ParseParameter(ctx context.Context, p map[string]any, paramType string) (Pa
465471
func (ps Parameters) Manifest() []ParameterManifest {
466472
rtn := make([]ParameterManifest, 0, len(ps))
467473
for _, p := range ps {
474+
if p.GetValueFromParam() != "" {
475+
continue
476+
}
468477
rtn = append(rtn, p.Manifest())
469478
}
470479
return rtn
@@ -476,6 +485,11 @@ func (ps Parameters) McpManifest() (McpToolsSchema, map[string][]string) {
476485
authParam := make(map[string][]string)
477486

478487
for _, p := range ps {
488+
// If the parameter is sourced from another param, skip it in the MCP manifest
489+
if p.GetValueFromParam() != "" {
490+
continue
491+
}
492+
479493
name := p.GetName()
480494
paramManifest, authParamList := p.McpManifest()
481495
defaultV := p.GetDefault()
@@ -509,6 +523,7 @@ type ParameterManifest struct {
509523
Default any `json:"default,omitempty"`
510524
AdditionalProperties any `json:"additionalProperties,omitempty"`
511525
EmbeddedBy string `json:"embeddedBy,omitempty"`
526+
ValueFromParam string `json:"valueFromParam,omitempty"`
512527
}
513528

514529
// ParameterMcpManifest represents properties when served as part of a ToolMcpManifest.
@@ -531,6 +546,7 @@ type CommonParameter struct {
531546
AuthServices []ParamAuthService `yaml:"authServices"`
532547
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
533548
EmbeddedBy string `yaml:"embeddedBy"`
549+
ValueFromParam string `yaml:"valueFromParam"`
534550
}
535551

536552
// GetName returns the name specified for the Parameter.
@@ -588,10 +604,16 @@ func (p *CommonParameter) IsExcludedValues(v any) bool {
588604
return false
589605
}
590606

607+
// GetEmbeddedBy returns the embedding model name for the Parameter.
591608
func (p *CommonParameter) GetEmbeddedBy() string {
592609
return p.EmbeddedBy
593610
}
594611

612+
// GetValueFromParam returns the param value to copy from.
613+
func (p *CommonParameter) GetValueFromParam() string {
614+
return p.ValueFromParam
615+
}
616+
595617
// MatchStringOrRegex checks if the input matches the target
596618
func MatchStringOrRegex(input, target any) bool {
597619
targetS, ok := target.(string)

tests/embedding.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ func AddSemanticSearchConfig(t *testing.T, config map[string]any, toolKind, inse
6464
"description": "The text content associated with the vector.",
6565
},
6666
map[string]any{
67-
"name": "text_to_embed",
68-
"type": "string",
69-
"description": "The text content used to generate the vector.",
70-
"embeddedBy": "gemini_model",
67+
"name": "text_to_embed",
68+
"type": "string",
69+
"description": "The text content used to generate the vector.",
70+
"embeddedBy": "gemini_model",
71+
"valueFromParam": "content",
7172
},
7273
},
7374
}
@@ -108,7 +109,7 @@ func RunSemanticSearchToolInvokeTest(t *testing.T, insertWant, mcpInsertWant, se
108109
name: "HTTP invoke insert_docs",
109110
api: "http://127.0.0.1:5000/api/tool/insert_docs/invoke",
110111
isMcp: false,
111-
requestBody: `{"content": "The quick brown fox jumps over the lazy dog", "text_to_embed": "The quick brown fox jumps over the lazy dog"}`,
112+
requestBody: `{"content": "The quick brown fox jumps over the lazy dog"}`,
112113
want: insertWant,
113114
},
114115
{
@@ -131,8 +132,7 @@ func RunSemanticSearchToolInvokeTest(t *testing.T, insertWant, mcpInsertWant, se
131132
Params: map[string]any{
132133
"name": "insert_docs",
133134
"arguments": map[string]any{
134-
"content": "The quick brown fox jumps over the lazy dog",
135-
"text_to_embed": "The quick brown fox jumps over the lazy dog",
135+
"content": "The quick brown fox jumps over the lazy dog",
136136
},
137137
},
138138
},

0 commit comments

Comments
 (0)