173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import { SchemaValidator } from '../utils/schemaValidator.js';
|
|
import { BaseTool, ToolResult } from './tools.js';
|
|
import { ToolCallConfirmationDetails } from '../ui/types.js'; // Added for shouldConfirmExecute
|
|
|
|
/**
|
|
* Parameters for the WebFetch tool
|
|
*/
|
|
export interface WebFetchToolParams {
|
|
/**
|
|
* The URL to fetch content from.
|
|
*/
|
|
url: string;
|
|
}
|
|
|
|
/**
|
|
* Standardized result from the WebFetch tool
|
|
*/
|
|
export interface WebFetchToolResult extends ToolResult {}
|
|
|
|
/**
|
|
* Implementation of the WebFetch tool that reads content from a URL.
|
|
*/
|
|
export class WebFetchTool extends BaseTool<
|
|
WebFetchToolParams,
|
|
WebFetchToolResult
|
|
> {
|
|
static readonly Name: string = 'web_fetch';
|
|
|
|
/**
|
|
* Creates a new instance of the WebFetchTool
|
|
*/
|
|
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',
|
|
},
|
|
);
|
|
// No rootDirectory needed for web fetching
|
|
}
|
|
|
|
/**
|
|
* Validates the parameters for the WebFetch tool
|
|
* @param params Parameters to validate
|
|
* @returns An error message string if invalid, null otherwise
|
|
*/
|
|
invalidParams(params: WebFetchToolParams): string | null {
|
|
// 1. Validate against the basic schema first
|
|
if (
|
|
this.schema.parameters &&
|
|
!SchemaValidator.validate(
|
|
this.schema.parameters as Record<string, unknown>,
|
|
params,
|
|
)
|
|
) {
|
|
return 'Parameters failed schema validation.';
|
|
}
|
|
|
|
// 2. Validate the URL format and protocol
|
|
try {
|
|
const parsedUrl = new URL(params.url);
|
|
// Ensure it's an HTTP or HTTPS URL
|
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
return `Invalid URL protocol: "${parsedUrl.protocol}". Only 'http:' and 'https:' are supported.`;
|
|
}
|
|
} catch (error) {
|
|
// The URL constructor throws if the format is invalid
|
|
return `Invalid URL format: "${params.url}". Please provide a valid absolute URL (e.g., 'https://example.com').`;
|
|
}
|
|
|
|
// If all checks pass, the parameters are valid
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets a description of the web fetch operation.
|
|
* @param params Parameters for the web fetch.
|
|
* @returns A string describing the operation.
|
|
*/
|
|
getDescription(params: WebFetchToolParams): string {
|
|
// Shorten long URLs for display
|
|
const displayUrl =
|
|
params.url.length > 80 ? params.url.substring(0, 77) + '...' : params.url;
|
|
return `Fetching content from ${displayUrl}`;
|
|
}
|
|
|
|
/**
|
|
* Determines if the tool should prompt for confirmation before execution.
|
|
* Web fetches are generally safe, so default to false.
|
|
* @param params Parameters for the tool execution
|
|
* @returns Whether execute should be confirmed.
|
|
*/
|
|
async shouldConfirmExecute(
|
|
params: WebFetchToolParams,
|
|
): Promise<ToolCallConfirmationDetails | false> {
|
|
// Could add logic here to confirm based on domain, etc. if needed
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
/**
|
|
* Fetches content from the specified URL.
|
|
* @param params Parameters for the web fetch operation.
|
|
* @returns Result with the fetched content or an error message.
|
|
*/
|
|
async execute(params: WebFetchToolParams): Promise<WebFetchToolResult> {
|
|
const validationError = this.invalidParams(params);
|
|
if (validationError) {
|
|
return {
|
|
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
|
|
returnDisplay: `**Error:** Invalid parameters. ${validationError}`,
|
|
};
|
|
}
|
|
|
|
const url = params.url;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'User-Agent': 'GeminiCode-CLI/1.0',
|
|
},
|
|
signal: AbortSignal.timeout(15000), // 15 seconds timeout
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// fetch doesn't throw on bad HTTP status codes (4xx, 5xx)
|
|
const errorText = `Failed to fetch data from ${url}. Status: ${response.status} ${response.statusText}`;
|
|
return {
|
|
llmContent: `Error: ${errorText}`,
|
|
returnDisplay: `**Error:** ${errorText}`,
|
|
};
|
|
}
|
|
|
|
// Assuming the response is text. Add checks for content-type if needed.
|
|
const data = await response.text();
|
|
let llmContent = '';
|
|
// Truncate very large responses for the LLM context
|
|
const MAX_LLM_CONTENT_LENGTH = 100000;
|
|
if (data) {
|
|
llmContent = `Fetched data from ${url}:\n\n${
|
|
data.length > MAX_LLM_CONTENT_LENGTH
|
|
? data.substring(0, MAX_LLM_CONTENT_LENGTH) +
|
|
'\n... [Content truncated]'
|
|
: data
|
|
}`;
|
|
} else {
|
|
llmContent = `No data fetched from ${url}. Status: ${response.status}`;
|
|
}
|
|
return {
|
|
llmContent,
|
|
returnDisplay: `Fetched content from ${url}`, // Simple display message
|
|
};
|
|
} catch (error: any) {
|
|
// This catches network errors (DNS resolution, connection refused, etc.)
|
|
// and errors from the URL constructor if somehow bypassed validation (unlikely)
|
|
const errorMessage = `Failed to fetch data from ${url}. Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
return {
|
|
llmContent: `Error: ${errorMessage}`,
|
|
returnDisplay: `**Error:** ${errorMessage}`,
|
|
};
|
|
}
|
|
}
|
|
}
|