Make web_fetch return content to LLM with configurable llmMaxChars

This commit is contained in:
root 2026-02-25 13:02:38 +08:00
parent e38b3cc175
commit b1b38e37d2
2 changed files with 70 additions and 13 deletions

View file

@ -630,6 +630,11 @@ func (t *WebFetchTool) Parameters() map[string]any {
"description": "Maximum characters to extract", "description": "Maximum characters to extract",
"minimum": 100.0, "minimum": 100.0,
}, },
"llmMaxChars": map[string]any{
"type": "integer",
"description": "Maximum characters included in LLM-facing content (defaults to maxChars)",
"minimum": 100.0,
},
}, },
"required": []string{"url"}, "required": []string{"url"},
} }
@ -655,9 +660,15 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
} }
maxChars := t.maxChars maxChars := t.maxChars
if mc, ok := args["maxChars"].(float64); ok { if raw, ok := args["maxChars"]; ok {
if int(mc) > 100 { if mc, err := toInt(raw); err == nil && mc > 100 {
maxChars = int(mc) maxChars = mc
}
}
llmMaxChars := maxChars
if raw, ok := args["llmMaxChars"]; ok {
if mc, err := toInt(raw); err == nil && mc > 100 {
llmMaxChars = mc
} }
} }
@ -719,6 +730,12 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
if truncated { if truncated {
text = text[:maxChars] text = text[:maxChars]
} }
llmText := text
llmTruncated := false
if len(llmText) > llmMaxChars {
llmText = llmText[:llmMaxChars]
llmTruncated = true
}
result := map[string]any{ result := map[string]any{
"url": urlStr, "url": urlStr,
@ -730,15 +747,18 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolRe
} }
resultJSON, _ := json.MarshalIndent(result, "", " ") resultJSON, _ := json.MarshalIndent(result, "", " ")
llmPayload := map[string]any{
"url": urlStr,
"status": resp.StatusCode,
"extractor": extractor,
"sourceLength": len(text),
"truncated": truncated || llmTruncated,
"text": llmText,
}
llmJSON, _ := json.MarshalIndent(llmPayload, "", " ")
return &ToolResult{ return &ToolResult{
ForLLM: fmt.Sprintf( ForLLM: string(llmJSON),
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
len(text),
urlStr,
extractor,
truncated,
),
ForUser: string(resultJSON), ForUser: string(resultJSON),
} }
} }

View file

@ -37,9 +37,9 @@ func TestWebTool_WebFetch_Success(t *testing.T) {
t.Errorf("Expected ForUser to contain 'Test Page', got: %s", result.ForUser) t.Errorf("Expected ForUser to contain 'Test Page', got: %s", result.ForUser)
} }
// ForLLM should contain summary // ForLLM should contain extracted content for model-side reasoning
if !strings.Contains(result.ForLLM, "bytes") && !strings.Contains(result.ForLLM, "extractor") { if !strings.Contains(result.ForLLM, "Test Page") {
t.Errorf("Expected ForLLM to contain summary, got: %s", result.ForLLM) t.Errorf("Expected ForLLM to contain fetched content, got: %s", result.ForLLM)
} }
} }
@ -174,6 +174,43 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
} }
} }
func TestWebTool_WebFetch_LLMTruncation(t *testing.T) {
longContent := strings.Repeat("abc", 2000)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(longContent))
}))
defer server.Close()
tool := NewWebFetchTool(12000)
ctx := context.Background()
args := map[string]any{
"url": server.URL,
"maxChars": 6000,
"llmMaxChars": 500,
}
result := tool.Execute(ctx, args)
if result.IsError {
t.Fatalf("Expected success, got IsError=true: %s", result.ForLLM)
}
var llmResult map[string]any
if err := json.Unmarshal([]byte(result.ForLLM), &llmResult); err != nil {
t.Fatalf("failed to decode ForLLM JSON: %v", err)
}
text, ok := llmResult["text"].(string)
if !ok {
t.Fatalf("expected text field in ForLLM payload, got: %v", llmResult)
}
if len(text) > 600 {
t.Fatalf("expected LLM text to be truncated to ~500 chars, got len=%d", len(text))
}
}
// TestWebTool_WebSearch_NoApiKey verifies that no tool is created when API key is missing // TestWebTool_WebSearch_NoApiKey verifies that no tool is created when API key is missing
func TestWebTool_WebSearch_NoApiKey(t *testing.T) { func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: ""}) tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: ""})