- Plumbed abort signals through to tools - Updated the shell tool to properly cancel active requests by killing the entire child process tree of the underlying shell process and then report that the shell itself was canceled. Fixes https://b.corp.google.com/issues/416829935
141 lines
4 KiB
TypeScript
141 lines
4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
|
import { BaseTool, ToolResult } from './tools.js';
|
|
import { getErrorMessage } from '../utils/errors.js';
|
|
|
|
/**
|
|
* Parameters for the WebFetch tool
|
|
*/
|
|
export interface WebFetchToolParams {
|
|
/**
|
|
* The URL to fetch content from.
|
|
*/
|
|
url: string;
|
|
}
|
|
|
|
/**
|
|
* Implementation of the WebFetch tool logic
|
|
*/
|
|
export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
|
static readonly Name: string = 'web_fetch';
|
|
|
|
constructor() {
|
|
super(
|
|
WebFetchTool.Name,
|
|
'WebFetch',
|
|
'Fetches text content from a given URL. Handles potential network errors and non-success HTTP status codes.',
|
|
{
|
|
properties: {
|
|
url: {
|
|
description:
|
|
"The URL to fetch. Must be an absolute URL (e.g., 'https://example.com/file.txt').",
|
|
type: 'string',
|
|
},
|
|
},
|
|
required: ['url'],
|
|
type: 'object',
|
|
},
|
|
);
|
|
}
|
|
|
|
validateParams(params: WebFetchToolParams): string | null {
|
|
if (
|
|
this.schema.parameters &&
|
|
!SchemaValidator.validate(
|
|
this.schema.parameters as Record<string, unknown>,
|
|
params,
|
|
)
|
|
) {
|
|
return 'Parameters failed schema validation.';
|
|
}
|
|
try {
|
|
const parsedUrl = new URL(params.url);
|
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
return `Invalid URL protocol: "${parsedUrl.protocol}". Only 'http:' and 'https:' are supported.`;
|
|
}
|
|
} catch {
|
|
return `Invalid URL format: "${params.url}". Please provide a valid absolute URL (e.g., 'https://example.com').`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getDescription(params: WebFetchToolParams): string {
|
|
const displayUrl =
|
|
params.url.length > 80 ? params.url.substring(0, 77) + '...' : params.url;
|
|
return `Fetching content from ${displayUrl}`;
|
|
}
|
|
|
|
async execute(
|
|
params: WebFetchToolParams,
|
|
_signal: AbortSignal,
|
|
): Promise<ToolResult> {
|
|
const validationError = this.validateParams(params);
|
|
if (validationError) {
|
|
return {
|
|
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
|
|
returnDisplay: `Error: ${validationError}`,
|
|
};
|
|
}
|
|
|
|
const url = params.url;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'User-Agent': 'GeminiCode-ServerLogic/1.0',
|
|
},
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = `Failed to fetch data from ${url}. Status: ${response.status} ${response.statusText}`;
|
|
return {
|
|
llmContent: `Error: ${errorText}`,
|
|
returnDisplay: `Error: ${errorText}`,
|
|
};
|
|
}
|
|
|
|
// Basic check for text-based content types
|
|
const contentType = response.headers.get('content-type') || '';
|
|
if (
|
|
!contentType.includes('text/') &&
|
|
!contentType.includes('json') &&
|
|
!contentType.includes('xml')
|
|
) {
|
|
const errorText = `Unsupported content type: ${contentType} from ${url}`;
|
|
return {
|
|
llmContent: `Error: ${errorText}`,
|
|
returnDisplay: `Error: ${errorText}`,
|
|
};
|
|
}
|
|
|
|
const data = await response.text();
|
|
const MAX_LLM_CONTENT_LENGTH = 200000; // Truncate large responses
|
|
const truncatedData =
|
|
data.length > MAX_LLM_CONTENT_LENGTH
|
|
? data.substring(0, MAX_LLM_CONTENT_LENGTH) +
|
|
'\n... [Content truncated]'
|
|
: data;
|
|
|
|
const llmContent = data
|
|
? `Fetched data from ${url}:\n\n${truncatedData}`
|
|
: `No text data fetched from ${url}. Status: ${response.status}`; // Adjusted message for clarity
|
|
|
|
return {
|
|
llmContent,
|
|
returnDisplay: `Fetched content from ${url}`,
|
|
};
|
|
} catch (error: unknown) {
|
|
const errorMessage = `Failed to fetch data from ${url}. Error: ${getErrorMessage(error)}`;
|
|
return {
|
|
llmContent: `Error: ${errorMessage}`,
|
|
returnDisplay: `Error: ${errorMessage}`,
|
|
};
|
|
}
|
|
}
|
|
}
|