Skip to content

Commit 0c93fec

Browse files
Allow the internal fetch tool to return IToolResultDataPart for images (microsoft#252880)
Needs microsoft/vscode-copilot-chat#27 to be live for like a day just in case... so I'll merge it tomorrow.
1 parent 2ba4fa4 commit 0c93fec

File tree

2 files changed

+526
-25
lines changed

2 files changed

+526
-25
lines changed

src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
import { CancellationToken } from '../../../../../base/common/cancellation.js';
77
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
88
import { ResourceSet } from '../../../../../base/common/map.js';
9+
import { extname } from '../../../../../base/common/path.js';
910
import { URI } from '../../../../../base/common/uri.js';
1011
import { localize } from '../../../../../nls.js';
1112
import { IFileService } from '../../../../../platform/files/common/files.js';
1213
import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
13-
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js';
14-
import { InternalFetchWebPageToolId } from '../../common/tools/tools.js';
1514
import { detectEncodingFromBuffer } from '../../../../services/textfile/common/encoding.js';
15+
import { ChatImageMimeType } from '../../common/languageModels.js';
16+
import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultDataPart, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js';
17+
import { InternalFetchWebPageToolId } from '../../common/tools/tools.js';
1618

1719
export const FetchWebPageToolData: IToolData = {
1820
id: InternalFetchWebPageToolId,
@@ -64,22 +66,35 @@ export class FetchWebPageTool implements IToolImpl {
6466
const webContents = webUris.size > 0 ? await this._readerModeService.extract([...webUris.values()]) : [];
6567

6668
// Get contents from file URIs
67-
const fileContents: (string | undefined)[] = [];
69+
const fileContents: (string | IToolResultDataPart | undefined)[] = [];
6870
const successfulFileUris: URI[] = [];
6971
for (const uri of fileUris.values()) {
7072
try {
7173
const fileContent = await this._fileService.readFile(uri, undefined, token);
7274

73-
// Check if the content is binary
74-
const detected = detectEncodingFromBuffer({ buffer: fileContent.value, bytesRead: fileContent.value.byteLength });
75-
76-
if (detected.seemsBinary) {
77-
// For binary files, return a message indicating they're not supported
78-
// We do this for now until the tools that leverage this internal tool can support binary content
79-
fileContents.push(localize('fetchWebPage.binaryNotSupported', 'Binary files are not supported at the moment.'));
75+
// Check if this is a supported image type first
76+
const imageMimeType = this._getSupportedImageMimeType(uri);
77+
if (imageMimeType) {
78+
// For supported image files, return as IToolResultDataPart
79+
fileContents.push({
80+
kind: 'data',
81+
value: {
82+
mimeType: imageMimeType,
83+
data: fileContent.value
84+
}
85+
});
8086
} else {
81-
// For text files, convert to string
82-
fileContents.push(fileContent.value.toString());
87+
// Check if the content is binary
88+
const detected = detectEncodingFromBuffer({ buffer: fileContent.value, bytesRead: fileContent.value.byteLength });
89+
90+
if (detected.seemsBinary) {
91+
// For binary files, return a message indicating they're not supported
92+
// We do this for now until the tools that leverage this internal tool can support binary content
93+
fileContents.push(localize('fetchWebPage.binaryNotSupported', 'Binary files are not supported at the moment.'));
94+
} else {
95+
// For text files, convert to string
96+
fileContents.push(fileContent.value.toString());
97+
}
8398
}
8499

85100
successfulFileUris.push(uri);
@@ -90,7 +105,7 @@ export class FetchWebPageTool implements IToolImpl {
90105
}
91106

92107
// Build results array in original order
93-
const results: (string | undefined)[] = [];
108+
const results: (string | IToolResultDataPart | undefined)[] = [];
94109
let webIndex = 0;
95110
let fileIndex = 0;
96111
for (const url of urls) {
@@ -112,8 +127,7 @@ export class FetchWebPageTool implements IToolImpl {
112127

113128
return {
114129
content: this._getPromptPartsForResults(results),
115-
// Have multiple results show in the dropdown
116-
toolResultDetails: actuallyValidUris.length > 1 ? actuallyValidUris : undefined
130+
toolResultDetails: actuallyValidUris
117131
};
118132
}
119133

@@ -160,8 +174,8 @@ export class FetchWebPageTool implements IToolImpl {
160174
invocationMessage.appendMarkdown(localize('fetchWebPage.invocationMessage.plural', 'Fetching {0} resources', valid.length));
161175
} else if (valid.length === 1) {
162176
const url = valid[0].toString();
163-
// If the URL is too long, show it as a link... otherwise, show it as plain text
164-
if (url.length > 400) {
177+
// If the URL is too long or it's a file url, show it as a link... otherwise, show it as plain text
178+
if (url.length > 400 || validFileUris.length === 1) {
165179
pastTenseMessage.appendMarkdown(localize({
166180
key: 'fetchWebPage.pastTenseMessageResult.singularAsLink',
167181
comment: [
@@ -228,10 +242,41 @@ export class FetchWebPageTool implements IToolImpl {
228242
return { webUris, fileUris, invalidUris };
229243
}
230244

231-
private _getPromptPartsForResults(results: (string | undefined)[]): IToolResultTextPart[] {
232-
return results.map(value => ({
233-
kind: 'text',
234-
value: value || localize('fetchWebPage.invalidUrl', 'Invalid URL')
235-
}));
245+
private _getPromptPartsForResults(results: (string | IToolResultDataPart | undefined)[]): (IToolResultTextPart | IToolResultDataPart)[] {
246+
return results.map(value => {
247+
if (!value) {
248+
return {
249+
kind: 'text',
250+
value: localize('fetchWebPage.invalidUrl', 'Invalid URL')
251+
};
252+
} else if (typeof value === 'string') {
253+
return {
254+
kind: 'text',
255+
value: value
256+
};
257+
} else {
258+
// This is an IToolResultDataPart
259+
return value;
260+
}
261+
});
262+
}
263+
264+
private _getSupportedImageMimeType(uri: URI): ChatImageMimeType | undefined {
265+
const ext = extname(uri.path).toLowerCase();
266+
switch (ext) {
267+
case '.png':
268+
return ChatImageMimeType.PNG;
269+
case '.jpg':
270+
case '.jpeg':
271+
return ChatImageMimeType.JPEG;
272+
case '.gif':
273+
return ChatImageMimeType.GIF;
274+
case '.webp':
275+
return ChatImageMimeType.WEBP;
276+
case '.bmp':
277+
return ChatImageMimeType.BMP;
278+
default:
279+
return undefined;
280+
}
236281
}
237282
}

0 commit comments

Comments
 (0)