mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
fix: add patch for sync upstream
This commit is contained in:
@@ -314,6 +314,11 @@ describe('MemoryTool', () => {
|
||||
memoryTool = new MemoryTool();
|
||||
// Mock fs.readFile to return empty string (file doesn't exist)
|
||||
vi.mocked(fs.readFile).mockResolvedValue('');
|
||||
|
||||
// Clear allowlist before each test to ensure clean state
|
||||
const invocation = memoryTool.build({ fact: 'test', scope: 'global' });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(invocation.constructor as any).allowlist.clear();
|
||||
});
|
||||
|
||||
it('should return confirmation details when memory file is not allowlisted for global scope', async () => {
|
||||
|
||||
@@ -201,7 +201,7 @@ class MemoryToolInvocation extends BaseToolInvocation<
|
||||
getDescription(): string {
|
||||
const scope = this.params.scope || 'global';
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
return `in ${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||
return `${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(
|
||||
|
||||
@@ -366,6 +366,253 @@ describe('ShellTool', () => {
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCoAuthorToGitCommit', () => {
|
||||
it('should add co-author to git commit with double quotes', async () => {
|
||||
const command = 'git commit -m "Initial commit"';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
// Mock the shell execution to return success
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
// Verify that the command was executed with co-author added
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>',
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should add co-author to git commit with single quotes', async () => {
|
||||
const command = "git commit -m 'Fix bug'";
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>',
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle git commit with additional flags', async () => {
|
||||
const command = 'git commit -a -m "Add feature"';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>',
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify non-git commands', async () => {
|
||||
const command = 'npm install';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
// On Linux, commands are wrapped with pgrep functionality
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining('npm install'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify git commands without -m flag', async () => {
|
||||
const command = 'git commit';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
// On Linux, commands are wrapped with pgrep functionality
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining('git commit'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle git commit with escaped quotes in message', async () => {
|
||||
const command = 'git commit -m "Fix \\"quoted\\" text"';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>',
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add co-author when disabled in config', async () => {
|
||||
// Mock config with disabled co-author
|
||||
(mockConfig.getGitCoAuthor as Mock).mockReturnValue({
|
||||
enabled: false,
|
||||
name: 'Qwen-Coder',
|
||||
email: 'qwen-coder@alibabacloud.com',
|
||||
});
|
||||
|
||||
const command = 'git commit -m "Initial commit"';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
// On Linux, commands are wrapped with pgrep functionality
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining('git commit -m "Initial commit"'),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom name and email from config', async () => {
|
||||
// Mock config with custom co-author details
|
||||
(mockConfig.getGitCoAuthor as Mock).mockReturnValue({
|
||||
enabled: true,
|
||||
name: 'Custom Bot',
|
||||
email: 'custom@example.com',
|
||||
});
|
||||
|
||||
const command = 'git commit -m "Test commit"';
|
||||
const invocation = shellTool.build({ command });
|
||||
const promise = invocation.execute(mockAbortSignal);
|
||||
|
||||
resolveExecutionPromise({
|
||||
rawOutput: Buffer.from(''),
|
||||
output: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
aborted: false,
|
||||
pid: 12345,
|
||||
});
|
||||
|
||||
await promise;
|
||||
|
||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Co-authored-by: Custom Bot <custom@example.com>',
|
||||
),
|
||||
expect.any(String),
|
||||
expect.any(Function),
|
||||
mockAbortSignal,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldConfirmExecute', () => {
|
||||
@@ -396,123 +643,6 @@ describe('ShellTool', () => {
|
||||
expect(() => shellTool.build({ command: '' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCoAuthorToGitCommit', () => {
|
||||
it('should add co-author to git commit with double quotes', () => {
|
||||
const command = 'git commit -m "Initial commit"';
|
||||
// Use public test method
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe(
|
||||
`git commit -m "Initial commit
|
||||
|
||||
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should add co-author to git commit with single quotes', () => {
|
||||
const command = "git commit -m 'Fix bug'";
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe(
|
||||
`git commit -m 'Fix bug
|
||||
|
||||
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle git commit with additional flags', () => {
|
||||
const command = 'git commit -a -m "Add feature"';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe(
|
||||
`git commit -a -m "Add feature
|
||||
|
||||
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify non-git commands', () => {
|
||||
const command = 'npm install';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe('npm install');
|
||||
});
|
||||
|
||||
it('should not modify git commands without -m flag', () => {
|
||||
const command = 'git commit';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe('git commit');
|
||||
});
|
||||
|
||||
it('should handle git commit with escaped quotes in message', () => {
|
||||
const command = 'git commit -m "Fix \\"quoted\\" text"';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe(
|
||||
`git commit -m "Fix \\"quoted\\" text
|
||||
|
||||
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add co-author when disabled in config', () => {
|
||||
// Mock config with disabled co-author
|
||||
(mockConfig.getGitCoAuthor as Mock).mockReturnValue({
|
||||
enabled: false,
|
||||
name: 'Qwen-Coder',
|
||||
email: 'qwen-coder@alibabacloud.com',
|
||||
});
|
||||
|
||||
const command = 'git commit -m "Initial commit"';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe('git commit -m "Initial commit"');
|
||||
});
|
||||
|
||||
it('should use custom name and email from config', () => {
|
||||
// Mock config with custom co-author details
|
||||
(mockConfig.getGitCoAuthor as Mock).mockReturnValue({
|
||||
enabled: true,
|
||||
name: 'Custom Bot',
|
||||
email: 'custom@example.com',
|
||||
});
|
||||
|
||||
const command = 'git commit -m "Test commit"';
|
||||
const result = (
|
||||
shellTool as unknown as {
|
||||
addCoAuthorToGitCommit: (command: string) => string;
|
||||
}
|
||||
).addCoAuthorToGitCommit(command);
|
||||
expect(result).toBe(
|
||||
`git commit -m "Test commit
|
||||
|
||||
Co-authored-by: Custom Bot <custom@example.com>"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateToolParams', () => {
|
||||
|
||||
@@ -382,9 +382,7 @@ export class ShellTool extends BaseDeclarativeTool<
|
||||
);
|
||||
}
|
||||
|
||||
protected override validateToolParams(
|
||||
params: ShellToolParams,
|
||||
): string | null {
|
||||
override validateToolParams(params: ShellToolParams): string | null {
|
||||
const commandCheck = isCommandAllowed(params.command, this.config);
|
||||
if (!commandCheck.allowed) {
|
||||
if (!commandCheck.reason) {
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
|
||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||
import {
|
||||
BaseTool,
|
||||
ToolResult,
|
||||
BaseDeclarativeTool,
|
||||
BaseToolInvocation,
|
||||
Kind,
|
||||
ToolCallConfirmationDetails,
|
||||
ToolConfirmationOutcome,
|
||||
Icon,
|
||||
ToolInvocation,
|
||||
ToolResult,
|
||||
} from './tools.js';
|
||||
|
||||
import { Config, ApprovalMode } from '../config/config.js';
|
||||
import { getResponseText } from '../utils/generateContentResponseUtilities.js';
|
||||
import { fetchWithTimeout } from '../utils/fetch.js';
|
||||
import { fetchWithTimeout, isPrivateIp } from '../utils/fetch.js';
|
||||
import { convert } from 'html-to-text';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
@@ -35,18 +38,158 @@ export interface WebFetchToolParams {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the WebFetch tool invocation logic
|
||||
*/
|
||||
class WebFetchToolInvocation extends BaseToolInvocation<
|
||||
WebFetchToolParams,
|
||||
ToolResult
|
||||
> {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
params: WebFetchToolParams,
|
||||
) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
private async executeDirectFetch(signal: AbortSignal): Promise<ToolResult> {
|
||||
let url = this.params.url;
|
||||
|
||||
// Convert GitHub blob URL to raw URL
|
||||
if (url.includes('github.com') && url.includes('/blob/')) {
|
||||
url = url
|
||||
.replace('github.com', 'raw.githubusercontent.com')
|
||||
.replace('/blob/', '/');
|
||||
console.debug(
|
||||
`[WebFetchTool] Converted GitHub blob URL to raw URL: ${url}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug(`[WebFetchTool] Fetching content from: ${url}`);
|
||||
const response = await fetchWithTimeout(url, URL_FETCH_TIMEOUT_MS);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = `Request failed with status code ${response.status} ${response.statusText}`;
|
||||
console.error(`[WebFetchTool] ${errorMessage}`);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
console.debug(`[WebFetchTool] Successfully fetched content from ${url}`);
|
||||
const html = await response.text();
|
||||
const textContent = convert(html, {
|
||||
wordwrap: false,
|
||||
selectors: [
|
||||
{ selector: 'a', options: { ignoreHref: true } },
|
||||
{ selector: 'img', format: 'skip' },
|
||||
],
|
||||
}).substring(0, MAX_CONTENT_LENGTH);
|
||||
|
||||
console.debug(
|
||||
`[WebFetchTool] Converted HTML to text (${textContent.length} characters)`,
|
||||
);
|
||||
|
||||
const geminiClient = this.config.getGeminiClient();
|
||||
const fallbackPrompt = `The user requested the following: "${this.params.prompt}".
|
||||
|
||||
I have fetched the content from ${this.params.url}. Please use the following content to answer the user's request.
|
||||
|
||||
---
|
||||
${textContent}
|
||||
---`;
|
||||
|
||||
console.debug(
|
||||
`[WebFetchTool] Processing content with prompt: "${this.params.prompt}"`,
|
||||
);
|
||||
|
||||
const result = await geminiClient.generateContent(
|
||||
[{ role: 'user', parts: [{ text: fallbackPrompt }] }],
|
||||
{},
|
||||
signal,
|
||||
);
|
||||
const resultText = getResponseText(result) || '';
|
||||
|
||||
console.debug(
|
||||
`[WebFetchTool] Successfully processed content from ${this.params.url}`,
|
||||
);
|
||||
|
||||
return {
|
||||
llmContent: resultText,
|
||||
returnDisplay: `Content from ${this.params.url} processed successfully.`,
|
||||
};
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
const errorMessage = `Error during fetch for ${url}: ${error.message}`;
|
||||
console.error(`[WebFetchTool] ${errorMessage}`, error);
|
||||
return {
|
||||
llmContent: `Error: ${errorMessage}`,
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
override getDescription(): string {
|
||||
const displayPrompt =
|
||||
this.params.prompt.length > 100
|
||||
? this.params.prompt.substring(0, 97) + '...'
|
||||
: this.params.prompt;
|
||||
return `Fetching content from ${this.params.url} and processing with prompt: "${displayPrompt}"`;
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(): Promise<
|
||||
ToolCallConfirmationDetails | false
|
||||
> {
|
||||
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: `Confirm Web Fetch`,
|
||||
prompt: `Fetch content from ${this.params.url} and process with: ${this.params.prompt}`,
|
||||
urls: [this.params.url],
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
||||
}
|
||||
},
|
||||
};
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
// Check if URL is private/localhost
|
||||
const isPrivate = isPrivateIp(this.params.url);
|
||||
|
||||
if (isPrivate) {
|
||||
console.debug(
|
||||
`[WebFetchTool] Private IP detected for ${this.params.url}, using direct fetch`,
|
||||
);
|
||||
} else {
|
||||
console.debug(
|
||||
`[WebFetchTool] Public URL detected for ${this.params.url}, using direct fetch`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.executeDirectFetch(signal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the WebFetch tool logic
|
||||
*/
|
||||
export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||
export class WebFetchTool extends BaseDeclarativeTool<
|
||||
WebFetchToolParams,
|
||||
ToolResult
|
||||
> {
|
||||
static readonly Name: string = 'web_fetch';
|
||||
|
||||
constructor(private readonly config: Config) {
|
||||
super(
|
||||
WebFetchTool.Name,
|
||||
'WebFetch',
|
||||
'Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model\'s response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".\n - The URL must be a fully-formed valid URL\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large',
|
||||
Icon.Globe,
|
||||
'Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model\'s response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".\n - The URL must be a fully-formed valid URL\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Supports both public and private/localhost URLs using direct fetch',
|
||||
Kind.Fetch,
|
||||
{
|
||||
properties: {
|
||||
url: {
|
||||
@@ -68,64 +211,9 @@ export class WebFetchTool extends BaseTool<WebFetchToolParams, ToolResult> {
|
||||
}
|
||||
}
|
||||
|
||||
private async executeFetch(
|
||||
protected override validateToolParams(
|
||||
params: WebFetchToolParams,
|
||||
signal: AbortSignal,
|
||||
): Promise<ToolResult> {
|
||||
let url = params.url;
|
||||
|
||||
// Convert GitHub blob URL to raw URL
|
||||
if (url.includes('github.com') && url.includes('/blob/')) {
|
||||
url = url
|
||||
.replace('github.com', 'raw.githubusercontent.com')
|
||||
.replace('/blob/', '/');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, URL_FETCH_TIMEOUT_MS);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Request failed with status code ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const html = await response.text();
|
||||
const textContent = convert(html, {
|
||||
wordwrap: false,
|
||||
selectors: [
|
||||
{ selector: 'a', options: { ignoreHref: true } },
|
||||
{ selector: 'img', format: 'skip' },
|
||||
],
|
||||
}).substring(0, MAX_CONTENT_LENGTH);
|
||||
|
||||
const geminiClient = this.config.getGeminiClient();
|
||||
const fallbackPrompt = `The user requested the following: "${params.prompt}".
|
||||
|
||||
I have fetched the content from ${params.url}. Please use the following content to answer the user's request.
|
||||
|
||||
---
|
||||
${textContent}
|
||||
---`;
|
||||
const result = await geminiClient.generateContent(
|
||||
[{ role: 'user', parts: [{ text: fallbackPrompt }] }],
|
||||
{},
|
||||
signal,
|
||||
);
|
||||
const resultText = getResponseText(result) || '';
|
||||
return {
|
||||
llmContent: resultText,
|
||||
returnDisplay: `Content from ${params.url} processed successfully.`,
|
||||
};
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
const errorMessage = `Error during fetch for ${url}: ${error.message}`;
|
||||
return {
|
||||
llmContent: `Error: ${errorMessage}`,
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
validateParams(params: WebFetchToolParams): string | null {
|
||||
): string | null {
|
||||
const errors = SchemaValidator.validate(
|
||||
this.schema.parametersJsonSchema,
|
||||
params,
|
||||
@@ -148,52 +236,9 @@ ${textContent}
|
||||
return null;
|
||||
}
|
||||
|
||||
getDescription(params: WebFetchToolParams): string {
|
||||
const displayPrompt =
|
||||
params.prompt.length > 100
|
||||
? params.prompt.substring(0, 97) + '...'
|
||||
: params.prompt;
|
||||
return `Fetching content from ${params.url} and processing with prompt: "${displayPrompt}"`;
|
||||
}
|
||||
|
||||
async shouldConfirmExecute(
|
||||
protected createInvocation(
|
||||
params: WebFetchToolParams,
|
||||
): Promise<ToolCallConfirmationDetails | false> {
|
||||
if (this.config.getApprovalMode() === ApprovalMode.AUTO_EDIT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const validationError = this.validateParams(params);
|
||||
if (validationError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'info',
|
||||
title: `Confirm Web Fetch`,
|
||||
prompt: `Fetch content from ${params.url} and process with: ${params.prompt}`,
|
||||
urls: [params.url],
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
|
||||
this.config.setApprovalMode(ApprovalMode.AUTO_EDIT);
|
||||
}
|
||||
},
|
||||
};
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
async execute(
|
||||
params: WebFetchToolParams,
|
||||
signal: AbortSignal,
|
||||
): Promise<ToolResult> {
|
||||
const validationError = this.validateParams(params);
|
||||
if (validationError) {
|
||||
return {
|
||||
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
|
||||
returnDisplay: validationError,
|
||||
};
|
||||
}
|
||||
|
||||
return this.executeFetch(params, signal);
|
||||
): ToolInvocation<WebFetchToolParams, ToolResult> {
|
||||
return new WebFetchToolInvocation(this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user