Path: blob/main/src/vs/workbench/contrib/chat/common/chatArtifactExtraction.ts
13579 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { match as globMatch } from '../../../../base/common/glob.js';6import { getExtensionForMimeType } from '../../../../base/common/mime.js';7import { basename as pathBasename } from '../../../../base/common/path.js';8import { basename } from '../../../../base/common/resources.js';9import { URI } from '../../../../base/common/uri.js';10import { IChatToolInvocation, IToolResultOutputDetailsSerialized } from './chatService/chatService.js';11import { ChatResponseResource, IResponse } from './model/chatModel.js';12import { IArtifactGroupConfig, IChatArtifact } from './tools/chatArtifactsService.js';13import { isToolResultInputOutputDetails } from './tools/languageModelToolsService.js';1415const CHAT_MEMORY_FILE_SCHEME = 'chat-memory-file';16const MEMORY_TOOL_ID = 'copilot_memory';1718export namespace ChatMemoryFileResource {19export function createUri(memoryPath: string, sessionResource: URI): URI {20return URI.from({21scheme: CHAT_MEMORY_FILE_SCHEME,22path: memoryPath,23query: sessionResource.toString(),24});25}2627export function isChatMemoryFileUri(uri: URI): boolean {28return uri.scheme === CHAT_MEMORY_FILE_SCHEME;29}3031export function parse(uri: URI): { memoryPath: string; sessionResource: string } {32return {33memoryPath: uri.path,34sessionResource: uri.query,35};36}37}3839/**40* Matches a MIME type against a pattern supporting wildcards.41* E.g. `image/*` matches `image/png`, `image/jpeg`, etc.42*/43function matchMimeType(pattern: string, mimeType: string): boolean {44if (pattern === mimeType) {45return true;46}47const [patternType, patternSubtype] = pattern.split('/');48const [type] = mimeType.split('/');49return patternSubtype === '*' && patternType === type;50}5152/**53* Finds the first matching rule for a file path from byFilePath rules.54*/55function findFilePathRule(56filePath: string,57byFilePath: Record<string, IArtifactGroupConfig>58): IArtifactGroupConfig | undefined {59const fileBasename = pathBasename(filePath);60for (const [pattern, config] of Object.entries(byFilePath)) {61if (globMatch(pattern, filePath) || globMatch(pattern, fileBasename)) {62return config;63}64}65return undefined;66}6768/**69* Finds the first matching rule for a MIME type from byMimeType rules.70*/71function findMimeTypeRule(72mimeType: string,73byMimeType: Record<string, IArtifactGroupConfig>74): IArtifactGroupConfig | undefined {75for (const [pattern, config] of Object.entries(byMimeType)) {76if (matchMimeType(pattern, mimeType)) {77return config;78}79}80return undefined;81}8283function isToolResultOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized {84return typeof obj === 'object' && obj !== null85&& 'output' in obj && typeof (obj as IToolResultOutputDetailsSerialized).output === 'object'86&& (obj as IToolResultOutputDetailsSerialized).output?.type === 'data'87&& typeof (obj as IToolResultOutputDetailsSerialized).output?.mimeType === 'string';88}8990function getMemoryPathFromParams(params: unknown): string | undefined {91if (typeof params !== 'object' || params === null) {92return undefined;93}94const path = (params as Record<string, unknown>)['path'];95return typeof path === 'string' ? path : undefined;96}9798const memoryWriteCommands = new Set(['create', 'str_replace', 'insert']);99100function isMemoryWriteCommand(params: unknown): boolean {101if (typeof params !== 'object' || params === null) {102return false;103}104const command = (params as Record<string, unknown>)['command'];105return typeof command === 'string' && memoryWriteCommands.has(command);106}107108/**109* Extracts artifacts from a single response's content parts, applying the given rules.110* Pure function, no side effects.111*/112export function extractArtifactsFromResponse(113response: IResponse,114sessionResource: URI,115byMimeType: Record<string, IArtifactGroupConfig>,116byFilePath: Record<string, IArtifactGroupConfig>,117byMemoryFilePath: Record<string, IArtifactGroupConfig> = {},118): IChatArtifact[] {119const artifacts: IChatArtifact[] = [];120const seenUris = new Set<string>();121122for (const part of response.value) {123// File writes: codeblockUri124if (part.kind === 'codeblockUri') {125const uri = part.uri;126const uriStr = uri.toString();127if (seenUris.has(uriStr)) {128continue;129}130const rule = findFilePathRule(uri.path, byFilePath);131if (rule) {132seenUris.add(uriStr);133artifacts.push({134label: basename(uri),135uri: uriStr,136type: 'plan',137groupName: rule.groupName,138onlyShowGroup: rule.onlyShowGroup,139});140}141}142143// File writes: textEditGroup144if (part.kind === 'textEditGroup') {145const uri = part.uri;146const uriStr = uri.toString();147if (seenUris.has(uriStr)) {148continue;149}150const rule = findFilePathRule(uri.path, byFilePath);151if (rule) {152seenUris.add(uriStr);153artifacts.push({154label: basename(uri),155uri: uriStr,156type: 'plan',157groupName: rule.groupName,158onlyShowGroup: rule.onlyShowGroup,159});160}161}162163// File writes: workspaceEdit164if (part.kind === 'workspaceEdit') {165for (const edit of part.edits) {166const uri = edit.newResource ?? edit.oldResource;167if (!uri) {168continue;169}170const uriStr = uri.toString();171if (seenUris.has(uriStr)) {172continue;173}174const rule = findFilePathRule(uri.path, byFilePath);175if (rule) {176seenUris.add(uriStr);177artifacts.push({178label: basename(uri),179uri: uriStr,180type: 'plan',181groupName: rule.groupName,182onlyShowGroup: rule.onlyShowGroup,183});184}185}186}187188// Memory tool invocations189if ((part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.toolId === MEMORY_TOOL_ID) {190const params = IChatToolInvocation.getParameters(part);191const memoryPath = getMemoryPathFromParams(params);192if (memoryPath && isMemoryWriteCommand(params)) {193const rule = findFilePathRule(memoryPath, byMemoryFilePath);194if (rule) {195const key = `memory:${part.toolCallId}:${memoryPath}`;196if (!seenUris.has(key)) {197seenUris.add(key);198artifacts.push({199label: pathBasename(memoryPath),200uri: ChatMemoryFileResource.createUri(memoryPath, sessionResource).toString(),201type: 'plan',202groupName: rule.groupName,203onlyShowGroup: rule.onlyShowGroup,204});205}206}207}208}209210// Image results from tool invocations211if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {212const details = IChatToolInvocation.resultDetails(part);213if (!details) {214continue;215}216217// IToolResultInputOutputDetails — has output array with embedded data parts218if (isToolResultInputOutputDetails(details)) {219for (let i = 0; i < details.output.length; i++) {220const outputPart = details.output[i];221if (outputPart.type === 'embed' && !outputPart.isText && outputPart.mimeType) {222const rule = findMimeTypeRule(outputPart.mimeType, byMimeType);223if (rule) {224const key = `${part.toolCallId}:${i}`;225if (!seenUris.has(key)) {226seenUris.add(key);227const ext = getExtensionForMimeType(outputPart.mimeType);228const permalinkBasename = ext ? `file${ext}` : 'file.bin';229const artifactUri = ChatResponseResource.createUri(sessionResource, part.toolCallId, i, permalinkBasename);230artifacts.push({231label: outputPart.uri?.path.split('/').pop() ?? `${rule.groupName} ${i + 1}`,232uri: artifactUri.toString(),233toolCallId: part.toolCallId,234dataPartIndex: i,235type: 'screenshot',236groupName: rule.groupName,237onlyShowGroup: rule.onlyShowGroup,238});239}240}241}242}243}244245// IToolResultOutputDetailsSerialized — single output with mimeType + base64Data246if (isToolResultOutputDetailsSerialized(details)) {247const rule = findMimeTypeRule(details.output.mimeType, byMimeType);248if (rule) {249const key = `${part.toolCallId}:0`;250if (!seenUris.has(key)) {251seenUris.add(key);252const ext = getExtensionForMimeType(details.output.mimeType);253const permalinkBasename = ext ? `file${ext}` : 'file.bin';254const artifactUri = ChatResponseResource.createUri(sessionResource, part.toolCallId, 0, permalinkBasename);255artifacts.push({256label: `${rule.groupName}`,257uri: artifactUri.toString(),258toolCallId: part.toolCallId,259dataPartIndex: 0,260type: 'screenshot',261groupName: rule.groupName,262onlyShowGroup: rule.onlyShowGroup,263});264}265}266}267}268}269270return artifacts;271}272273274