diff --git a/packages/core/package.json b/packages/core/package.json index 5422256d750..ac368b83e21 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -522,7 +522,7 @@ "@aws-sdk/s3-request-presigner": "<3.696.0", "@aws-sdk/smithy-client": "<3.696.0", "@aws-sdk/util-arn-parser": "<3.696.0", - "@aws/mynah-ui": "^4.26.1", + "@aws/mynah-ui": "^4.27.0", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/fetch-http-handler": "^3.0.0", diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 324616a445a..fef1be3490e 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -3,7 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChatItemButton, ChatItemFormItem, ChatItemType, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { + ChatItem, + ChatItemButton, + ChatItemFormItem, + ChatItemType, + MynahUIDataModel, + QuickActionCommand, +} from '@aws/mynah-ui' import { TabType } from '../storages/tabsStorage' import { CWCChatItem } from '../connector' import { BaseConnector, BaseConnectorProps } from './baseConnector' @@ -18,12 +25,14 @@ export interface ConnectorProps extends BaseConnectorProps { title?: string, description?: string ) => void + onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void } export class Connector extends BaseConnector { private readonly onCWCContextCommandMessage private readonly onContextCommandDataReceived private readonly onShowCustomForm + private readonly onChatAnswerUpdated override getTabType(): TabType { return 'cwc' @@ -34,6 +43,7 @@ export class Connector extends BaseConnector { this.onCWCContextCommandMessage = props.onCWCContextCommandMessage this.onContextCommandDataReceived = props.onContextCommandDataReceived this.onShowCustomForm = props.onShowCustomForm + this.onChatAnswerUpdated = props.onChatAnswerUpdated } onSourceLinkClick = (tabID: string, messageId: string, link: string): void => { @@ -91,17 +101,24 @@ export class Connector extends BaseConnector { messageId: messageData.messageID ?? messageData.triggerID, body: messageData.message, followUp: followUps, - canBeVoted: true, + canBeVoted: messageData.canBeVoted ?? false, codeReference: messageData.codeReference, userIntent: messageData.userIntent, codeBlockLanguage: messageData.codeBlockLanguage, contextList: messageData.contextList, + title: messageData.title, + buttons: messageData.buttons ?? undefined, + fileList: messageData.fileList ?? undefined, + header: messageData.header ?? undefined, + padding: messageData.padding ?? false, + fullWidth: messageData.fullWidth ?? undefined, + codeBlockActions: messageData.codeBlockActions ?? undefined, } - // If it is not there we will not set it - if (messageData.messageType === 'answer-part' || messageData.messageType === 'answer') { - answer.canBeVoted = true - } + // eslint-disable-next-line aws-toolkits/no-console-log + console.log('messageData', messageData) + // eslint-disable-next-line aws-toolkits/no-console-log + console.log('answer', answer) if (messageData.relatedSuggestions !== undefined) { answer.relatedContent = { @@ -137,6 +154,8 @@ export class Connector extends BaseConnector { options: messageData.followUps, } : undefined, + buttons: messageData.buttons ?? undefined, + canBeVoted: messageData.canBeVoted ?? false, } this.onChatAnswerReceived(messageData.tabID, answer, messageData) @@ -204,7 +223,7 @@ export class Connector extends BaseConnector { } if (messageData.type === 'customFormActionMessage') { - this.onCustomFormAction(messageData.tabID, messageData.action) + this.onCustomFormAction(messageData.tabID, messageData.messageId, messageData.action) return } // For other message types, call the base class handleMessageReceive @@ -235,6 +254,7 @@ export class Connector extends BaseConnector { onCustomFormAction( tabId: string, + messageId: string, action: { id: string text?: string | undefined @@ -248,9 +268,48 @@ export class Connector extends BaseConnector { this.sendMessageToExtension({ command: 'form-action-click', action: action, + formSelectedValues: action.formItemValues, tabType: this.getTabType(), tabID: tabId, }) + + if (!this.onChatAnswerUpdated || !['accept-code-diff', 'reject-code-diff'].includes(action.id)) { + return + } + const answer: ChatItem = { + type: ChatItemType.ANSWER, + messageId: messageId, + buttons: [], + } + switch (action.id) { + case 'accept-code-diff': + answer.buttons = [ + { + keepCardAfterClick: true, + text: 'Accepted code', + id: 'accepted-code-diff', + status: 'success', + position: 'outside', + disabled: true, + }, + ] + break + case 'reject-code-diff': + answer.buttons = [ + { + keepCardAfterClick: true, + text: 'Rejected code', + id: 'rejected-code-diff', + status: 'error', + position: 'outside', + disabled: true, + }, + ] + break + default: + break + } + this.onChatAnswerUpdated(tabId, answer) } onFileClick = (tabID: string, filePath: string, messageId?: string) => { diff --git a/packages/core/src/amazonq/webview/ui/connector.ts b/packages/core/src/amazonq/webview/ui/connector.ts index 8f1cde9e565..6968761d8ac 100644 --- a/packages/core/src/amazonq/webview/ui/connector.ts +++ b/packages/core/src/amazonq/webview/ui/connector.ts @@ -63,6 +63,7 @@ export interface CWCChatItem extends ChatItem { userIntent?: UserIntent codeBlockLanguage?: string contextList?: Context[] + title?: string } export interface Context { @@ -711,7 +712,7 @@ export class Connector { tabType: 'cwc', }) } else { - this.cwChatConnector.onCustomFormAction(tabId, action) + this.cwChatConnector.onCustomFormAction(tabId, messageId ?? '', action) } break case 'agentWalkthrough': { diff --git a/packages/core/src/amazonq/webview/ui/main.ts b/packages/core/src/amazonq/webview/ui/main.ts index 5ae03840f8f..c2b5f247e75 100644 --- a/packages/core/src/amazonq/webview/ui/main.ts +++ b/packages/core/src/amazonq/webview/ui/main.ts @@ -352,6 +352,12 @@ export const createMynahUI = ( ...(item.followUp !== undefined ? { followUp: item.followUp } : {}), ...(item.fileList !== undefined ? { fileList: item.fileList } : {}), ...(item.header !== undefined ? { header: item.header } : { header: undefined }), + ...(item.buttons !== undefined ? { buttons: item.buttons } : { buttons: undefined }), + ...(item.fullWidth !== undefined ? { fullWidth: item.fullWidth } : { fullWidth: undefined }), + ...(item.padding !== undefined ? { padding: item.padding } : { padding: undefined }), + ...(item.codeBlockActions !== undefined + ? { codeBlockActions: item.codeBlockActions } + : { codeBlockActions: undefined }), }) if ( item.messageId !== undefined && @@ -374,7 +380,7 @@ export const createMynahUI = ( fileList: { fileTreeTitle: '', filePaths: item.contextList.map((file) => file.relativeFilePath), - rootFolderTitle: 'Context', + rootFolderTitle: item.title, flatList: true, collapsed: true, hideFileCount: true, diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index b2f9808a849..b23383bbaa7 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -31,8 +31,8 @@ export interface CodeWhispererConfig { } export const defaultServiceConfig: CodeWhispererConfig = { - region: 'us-east-1', - endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/', + region: 'us-west-2', + endpoint: 'https://rts.alpha-us-west-2.codewhisperer.ai.aws.dev/', } export function getCodewhispererConfig(): CodeWhispererConfig { diff --git a/packages/core/src/codewhisperer/client/user-service-2.json b/packages/core/src/codewhisperer/client/user-service-2.json index 833245ef183..986b4d465b0 100644 --- a/packages/core/src/codewhisperer/client/user-service-2.json +++ b/packages/core/src/codewhisperer/client/user-service-2.json @@ -66,6 +66,23 @@ "documentation": "

Creates a pre-signed, S3 write URL for uploading a repository zip archive.

", "idempotent": true }, + "CreateWorkspace": { + "name": "CreateWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "CreateWorkspaceRequest" }, + "output": { "shape": "CreateWorkspaceResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "ConflictException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

Create a workspace based on a workspace root

" + }, "DeleteTaskAssistConversation": { "name": "DeleteTaskAssistConversation", "http": { @@ -83,6 +100,22 @@ ], "documentation": "

API to delete task assist conversation.

" }, + "DeleteWorkspace": { + "name": "DeleteWorkspace", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "DeleteWorkspaceRequest" }, + "output": { "shape": "DeleteWorkspaceResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

Delete a workspace based on a workspaceId

" + }, "GenerateCompletions": { "name": "GenerateCompletions", "http": { @@ -215,6 +248,21 @@ { "shape": "AccessDeniedException" } ] }, + "ListAvailableProfiles": { + "name": "ListAvailableProfiles", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "ListAvailableProfilesRequest" }, + "output": { "shape": "ListAvailableProfilesResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ] + }, "ListCodeAnalysisFindings": { "name": "ListCodeAnalysisFindings", "http": { @@ -248,6 +296,22 @@ ], "documentation": "

Return configruations for each feature that has been setup for A/B testing.

" }, + "ListWorkspaceMetadata": { + "name": "ListWorkspaceMetadata", + "http": { + "method": "POST", + "requestUri": "/" + }, + "input": { "shape": "ListWorkspaceMetadataRequest" }, + "output": { "shape": "ListWorkspaceMetadataResponse" }, + "errors": [ + { "shape": "ThrottlingException" }, + { "shape": "InternalServerException" }, + { "shape": "ValidationException" }, + { "shape": "AccessDeniedException" } + ], + "documentation": "

List workspace metadata based on a workspace root

" + }, "ResumeTransformation": { "name": "ResumeTransformation", "http": { @@ -404,6 +468,57 @@ "documentation": "

Reason for AccessDeniedException

", "enum": ["UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS"] }, + "ActiveFunctionalityList": { + "type": "list", + "member": { "shape": "FunctionalityName" }, + "max": 10, + "min": 0 + }, + "AdditionalContentEntry": { + "type": "structure", + "required": ["name", "description"], + "members": { + "name": { + "shape": "AdditionalContentEntryNameString", + "documentation": "

The name/identifier for this context entry

" + }, + "description": { + "shape": "AdditionalContentEntryDescriptionString", + "documentation": "

A description of what this context entry represents

" + }, + "innerContext": { + "shape": "AdditionalContentEntryInnerContextString", + "documentation": "

The actual contextual content

" + } + }, + "documentation": "

Structure representing a single entry of additional contextual content

" + }, + "AdditionalContentEntryDescriptionString": { + "type": "string", + "max": 1024, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryInnerContextString": { + "type": "string", + "max": 8192, + "min": 1, + "sensitive": true + }, + "AdditionalContentEntryNameString": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "[a-z]+(?:-[a-z0-9]+)*", + "sensitive": true + }, + "AdditionalContentList": { + "type": "list", + "member": { "shape": "AdditionalContentEntry" }, + "documentation": "

A list of additional content entries, limited to 20 items

", + "max": 20, + "min": 0 + }, "AppStudioState": { "type": "structure", "required": ["namespace", "propertyName", "propertyContext"], @@ -451,6 +566,20 @@ "min": 0, "sensitive": true }, + "ApplicationProperties": { + "type": "structure", + "required": ["tenantId", "applicationArn", "tenantUrl", "applicationType"], + "members": { + "tenantId": { "shape": "TenantId" }, + "applicationArn": { "shape": "ResourceArn" }, + "tenantUrl": { "shape": "Url" }, + "applicationType": { "shape": "FunctionalityName" } + } + }, + "ApplicationPropertiesList": { + "type": "list", + "member": { "shape": "ApplicationProperties" } + }, "ArtifactId": { "type": "string", "max": 126, @@ -488,13 +617,17 @@ "followupPrompt": { "shape": "FollowupPrompt", "documentation": "

Followup Prompt

" + }, + "toolUses": { + "shape": "ToolUses", + "documentation": "

ToolUse Request

" } }, "documentation": "

Markdown text message.

" }, "AssistantResponseMessageContentString": { "type": "string", - "max": 4096, + "max": 100000, "min": 0, "sensitive": true }, @@ -508,6 +641,14 @@ "type": "boolean", "box": true }, + "ByUserAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "s3Uri": { "shape": "S3Uri" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "ChatAddMessageEvent": { "type": "structure", "required": ["conversationId", "messageId"], @@ -532,7 +673,7 @@ "type": "list", "member": { "shape": "ChatMessage" }, "documentation": "

Indicates Participant in Chat conversation

", - "max": 10, + "max": 100, "min": 0 }, "ChatInteractWithMessageEvent": { @@ -596,6 +737,11 @@ "hasProjectLevelContext": { "shape": "Boolean" } } }, + "ClientId": { + "type": "string", + "max": 255, + "min": 1 + }, "CodeAnalysisFindingsSchema": { "type": "string", "enum": ["codeanalysis/findings/1.0"] @@ -868,7 +1014,9 @@ }, "CreateTaskAssistConversationRequest": { "type": "structure", - "members": {}, + "members": { + "profileArn": { "shape": "ProfileArn" } + }, "documentation": "

Structure to represent bootstrap conversation request.

" }, "CreateTaskAssistConversationResponse": { @@ -889,7 +1037,8 @@ "artifactType": { "shape": "ArtifactType" }, "uploadIntent": { "shape": "UploadIntent" }, "uploadContext": { "shape": "UploadContext" }, - "uploadId": { "shape": "UploadId" } + "uploadId": { "shape": "UploadId" }, + "profileArn": { "shape": "ProfileArn" } } }, "CreateUploadUrlRequestContentChecksumString": { @@ -919,6 +1068,26 @@ "requestHeaders": { "shape": "RequestHeaders" } } }, + "CreateWorkspaceRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { "shape": "CreateWorkspaceRequestWorkspaceRootString" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "CreateWorkspaceRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "CreateWorkspaceResponse": { + "type": "structure", + "required": ["workspace"], + "members": { + "workspace": { "shape": "WorkspaceMetadata" } + } + }, "CursorState": { "type": "structure", "members": { @@ -959,11 +1128,19 @@ "type": "list", "member": { "shape": "Customization" } }, + "DashboardAnalytics": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "DeleteTaskAssistConversationRequest": { "type": "structure", "required": ["conversationId"], "members": { - "conversationId": { "shape": "ConversationId" } + "conversationId": { "shape": "ConversationId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent bootstrap conversation request.

" }, @@ -975,6 +1152,18 @@ }, "documentation": "

Structure to represent bootstrap conversation response.

" }, + "DeleteWorkspaceRequest": { + "type": "structure", + "required": ["workspaceId"], + "members": { + "workspaceId": { "shape": "UUID" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "DeleteWorkspaceResponse": { + "type": "structure", + "members": {} + }, "Description": { "type": "string", "max": 256, @@ -1282,6 +1471,19 @@ "max": 100, "min": 0 }, + "ErrorDetails": { + "type": "string", + "max": 2048, + "min": 0 + }, + "ExternalIdentityDetails": { + "type": "structure", + "members": { + "issuerUrl": { "shape": "IssuerUrl" }, + "clientId": { "shape": "ClientId" }, + "scimEndpoint": { "shape": "String" } + } + }, "FeatureDevCodeAcceptanceEvent": { "type": "structure", "required": ["conversationId", "linesOfCodeAccepted", "charactersOfCodeAccepted"], @@ -1418,6 +1620,20 @@ "min": 0, "sensitive": true }, + "FunctionalityName": { + "type": "string", + "enum": [ + "COMPLETIONS", + "ANALYSIS", + "CONVERSATIONS", + "TASK_ASSIST", + "TRANSFORMATIONS", + "CHAT_CUSTOMIZATION", + "TRANSFORMATIONS_WEBAPP" + ], + "max": 64, + "min": 1 + }, "GenerateCompletionsRequest": { "type": "structure", "required": ["fileContext"], @@ -1430,7 +1646,8 @@ "customizationArn": { "shape": "CustomizationArn" }, "optOutPreference": { "shape": "OptOutPreference" }, "userContext": { "shape": "UserContext" }, - "profileArn": { "shape": "ProfileArn" } + "profileArn": { "shape": "ProfileArn" }, + "workspaceId": { "shape": "UUID" } } }, "GenerateCompletionsRequestMaxResultsInteger": { @@ -1457,7 +1674,8 @@ "type": "structure", "required": ["jobId"], "members": { - "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" } + "jobId": { "shape": "GetCodeAnalysisRequestJobIdString" }, + "profileArn": { "shape": "ProfileArn" } } }, "GetCodeAnalysisRequestJobIdString": { @@ -1477,7 +1695,8 @@ "type": "structure", "required": ["jobId"], "members": { - "jobId": { "shape": "GetCodeFixJobRequestJobIdString" } + "jobId": { "shape": "GetCodeFixJobRequestJobIdString" }, + "profileArn": { "shape": "ProfileArn" } } }, "GetCodeFixJobRequestJobIdString": { @@ -1498,7 +1717,8 @@ "required": ["conversationId", "codeGenerationId"], "members": { "conversationId": { "shape": "ConversationId" }, - "codeGenerationId": { "shape": "CodeGenerationId" } + "codeGenerationId": { "shape": "CodeGenerationId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Request for getting task assist code generation.

" }, @@ -1519,7 +1739,8 @@ "required": ["testGenerationJobGroupName", "testGenerationJobId"], "members": { "testGenerationJobGroupName": { "shape": "TestGenerationJobGroupName" }, - "testGenerationJobId": { "shape": "UUID" } + "testGenerationJobId": { "shape": "UUID" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get test generation request.

" }, @@ -1534,7 +1755,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get code transformation plan request.

" }, @@ -1550,7 +1772,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent get code transformation request.

" }, @@ -1589,6 +1812,47 @@ "max": 256, "min": 1 }, + "IdentityDetails": { + "type": "structure", + "members": { + "ssoIdentityDetails": { "shape": "SSOIdentityDetails" }, + "externalIdentityDetails": { "shape": "ExternalIdentityDetails" } + }, + "union": true + }, + "ImageBlock": { + "type": "structure", + "required": ["format", "source"], + "members": { + "format": { "shape": "ImageFormat" }, + "source": { "shape": "ImageSource" } + }, + "documentation": "

Represents the image source itself and the format of the image.

" + }, + "ImageBlocks": { + "type": "list", + "member": { "shape": "ImageBlock" }, + "max": 10, + "min": 0 + }, + "ImageFormat": { + "type": "string", + "enum": ["png", "jpeg", "gif", "webp"] + }, + "ImageSource": { + "type": "structure", + "members": { + "bytes": { "shape": "ImageSourceBytesBlob" } + }, + "documentation": "

Image bytes limited to ~10MB considering overhead of base64 encoding

", + "sensitive": true, + "union": true + }, + "ImageSourceBytesBlob": { + "type": "blob", + "max": 1500000, + "min": 1 + }, "Import": { "type": "structure", "members": { @@ -1656,6 +1920,11 @@ "fault": true, "retryable": { "throttling": false } }, + "IssuerUrl": { + "type": "string", + "max": 255, + "min": 1 + }, "LineRangeList": { "type": "list", "member": { "shape": "Range" } @@ -1664,7 +1933,8 @@ "type": "structure", "members": { "maxResults": { "shape": "ListAvailableCustomizationsRequestMaxResultsInteger" }, - "nextToken": { "shape": "Base64EncodedPaginationToken" } + "nextToken": { "shape": "Base64EncodedPaginationToken" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListAvailableCustomizationsRequestMaxResultsInteger": { @@ -1681,13 +1951,35 @@ "nextToken": { "shape": "Base64EncodedPaginationToken" } } }, + "ListAvailableProfilesRequest": { + "type": "structure", + "members": { + "maxResults": { "shape": "ListAvailableProfilesRequestMaxResultsInteger" }, + "nextToken": { "shape": "Base64EncodedPaginationToken" } + } + }, + "ListAvailableProfilesRequestMaxResultsInteger": { + "type": "integer", + "box": true, + "max": 10, + "min": 1 + }, + "ListAvailableProfilesResponse": { + "type": "structure", + "required": ["profiles"], + "members": { + "profiles": { "shape": "ProfileList" }, + "nextToken": { "shape": "Base64EncodedPaginationToken" } + } + }, "ListCodeAnalysisFindingsRequest": { "type": "structure", "required": ["jobId", "codeAnalysisFindingsSchema"], "members": { "jobId": { "shape": "ListCodeAnalysisFindingsRequestJobIdString" }, "nextToken": { "shape": "PaginationToken" }, - "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" } + "codeAnalysisFindingsSchema": { "shape": "CodeAnalysisFindingsSchema" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListCodeAnalysisFindingsRequestJobIdString": { @@ -1707,7 +1999,8 @@ "type": "structure", "required": ["userContext"], "members": { - "userContext": { "shape": "UserContext" } + "userContext": { "shape": "UserContext" }, + "profileArn": { "shape": "ProfileArn" } } }, "ListFeatureEvaluationsResponse": { @@ -1717,6 +2010,29 @@ "featureEvaluations": { "shape": "FeatureEvaluationsList" } } }, + "ListWorkspaceMetadataRequest": { + "type": "structure", + "required": ["workspaceRoot"], + "members": { + "workspaceRoot": { "shape": "ListWorkspaceMetadataRequestWorkspaceRootString" }, + "nextToken": { "shape": "String" }, + "maxResults": { "shape": "Integer" }, + "profileArn": { "shape": "ProfileArn" } + } + }, + "ListWorkspaceMetadataRequestWorkspaceRootString": { + "type": "string", + "max": 1024, + "min": 1 + }, + "ListWorkspaceMetadataResponse": { + "type": "structure", + "required": ["workspaces"], + "members": { + "workspaces": { "shape": "WorkspaceList" }, + "nextToken": { "shape": "String" } + } + }, "Long": { "type": "long", "box": true @@ -1750,16 +2066,65 @@ "min": 1, "pattern": "[-a-zA-Z0-9._]*" }, + "Notifications": { + "type": "list", + "member": { "shape": "NotificationsFeature" }, + "max": 10, + "min": 0 + }, + "NotificationsFeature": { + "type": "structure", + "required": ["feature", "toggle"], + "members": { + "feature": { "shape": "FeatureName" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "OperatingSystem": { "type": "string", "enum": ["MAC", "WINDOWS", "LINUX"], "max": 64, "min": 1 }, + "OptInFeatureToggle": { + "type": "string", + "enum": ["ON", "OFF"] + }, + "OptInFeatures": { + "type": "structure", + "members": { + "promptLogging": { "shape": "PromptLogging" }, + "byUserAnalytics": { "shape": "ByUserAnalytics" }, + "dashboardAnalytics": { "shape": "DashboardAnalytics" }, + "notifications": { "shape": "Notifications" }, + "workspaceContext": { "shape": "WorkspaceContext" } + } + }, "OptOutPreference": { "type": "string", "enum": ["OPTIN", "OPTOUT"] }, + "Origin": { + "type": "string", + "documentation": "

Enum to represent the origin application conversing with Sidekick.

", + "enum": [ + "CHATBOT", + "CONSOLE", + "DOCUMENTATION", + "MARKETING", + "MOBILE", + "SERVICE_INTERNAL", + "UNIFIED_SEARCH", + "UNKNOWN", + "MD", + "IDE", + "SAGE_MAKER", + "CLI", + "AI_EDITOR", + "OPENSEARCH_DASHBOARD", + "GITLAB" + ] + }, "PackageInfo": { "type": "structure", "members": { @@ -1821,12 +2186,56 @@ "sensitive": true }, "PrimitiveInteger": { "type": "integer" }, + "Profile": { + "type": "structure", + "required": ["arn", "profileName"], + "members": { + "arn": { "shape": "ProfileArn" }, + "identityDetails": { "shape": "IdentityDetails" }, + "profileName": { "shape": "ProfileName" }, + "description": { "shape": "ProfileDescription" }, + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" }, + "kmsKeyArn": { "shape": "ResourceArn" }, + "activeFunctionalities": { "shape": "ActiveFunctionalityList" }, + "status": { "shape": "ProfileStatus" }, + "errorDetails": { "shape": "ErrorDetails" }, + "resourcePolicy": { "shape": "ResourcePolicy" }, + "profileType": { "shape": "ProfileType" }, + "optInFeatures": { "shape": "OptInFeatures" }, + "permissionUpdateRequired": { "shape": "Boolean" }, + "applicationProperties": { "shape": "ApplicationPropertiesList" } + } + }, "ProfileArn": { "type": "string", "max": 950, "min": 0, "pattern": "arn:aws:codewhisperer:[-.a-z0-9]{1,63}:\\d{12}:profile/([a-zA-Z0-9]){12}" }, + "ProfileDescription": { + "type": "string", + "max": 256, + "min": 1, + "pattern": "[\\sa-zA-Z0-9_-]*" + }, + "ProfileList": { + "type": "list", + "member": { "shape": "Profile" } + }, + "ProfileName": { + "type": "string", + "max": 100, + "min": 1, + "pattern": "[a-zA-Z][a-zA-Z0-9_-]*" + }, + "ProfileStatus": { + "type": "string", + "enum": ["ACTIVE", "CREATING", "CREATE_FAILED", "UPDATING", "UPDATE_FAILED", "DELETING", "DELETE_FAILED"] + }, + "ProfileType": { + "type": "string", + "enum": ["Q_DEVELOPER", "CODEWHISPERER"] + }, "ProgrammingLanguage": { "type": "structure", "required": ["languageName"], @@ -1845,6 +2254,14 @@ "type": "list", "member": { "shape": "TransformationProgressUpdate" } }, + "PromptLogging": { + "type": "structure", + "required": ["s3Uri", "toggle"], + "members": { + "s3Uri": { "shape": "S3Uri" }, + "toggle": { "shape": "OptInFeatureToggle" } + } + }, "Range": { "type": "structure", "required": ["start", "end"], @@ -1918,7 +2335,7 @@ "RelevantDocumentList": { "type": "list", "member": { "shape": "RelevantTextDocument" }, - "max": 5, + "max": 30, "min": 0 }, "RelevantTextDocument": { @@ -1952,7 +2369,7 @@ }, "RelevantTextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40960, "min": 0, "sensitive": true }, @@ -1989,12 +2406,24 @@ "documentation": "

This exception is thrown when describing a resource that does not exist.

", "exception": true }, + "ResourcePolicy": { + "type": "structure", + "required": ["effect"], + "members": { + "effect": { "shape": "ResourcePolicyEffect" } + } + }, + "ResourcePolicyEffect": { + "type": "string", + "enum": ["ALLOW", "DENY"] + }, "ResumeTransformationRequest": { "type": "structure", "required": ["transformationJobId"], "members": { "transformationJobId": { "shape": "TransformationJobId" }, - "userActionStatus": { "shape": "TransformationUserActionStatus" } + "userActionStatus": { "shape": "TransformationUserActionStatus" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent stop code transformation request.

" }, @@ -2037,6 +2466,27 @@ "min": 0, "sensitive": true }, + "S3Uri": { + "type": "string", + "max": 1024, + "min": 1, + "pattern": "s3://((?!xn--)[a-z0-9](?![^/]*[.]{2})[a-z0-9-.]{1,61}[a-z0-9](?Represents span in a text

" + "documentation": "

Represents span in a text.

" }, "SpanEndInteger": { "type": "integer", @@ -2176,7 +2632,8 @@ "idempotencyToken": true }, "scope": { "shape": "CodeAnalysisScope" }, - "codeScanName": { "shape": "CodeScanName" } + "codeScanName": { "shape": "CodeScanName" }, + "profileArn": { "shape": "ProfileArn" } } }, "StartCodeAnalysisRequestClientTokenString": { @@ -2207,7 +2664,8 @@ "description": { "shape": "StartCodeFixJobRequestDescriptionString" }, "ruleId": { "shape": "StartCodeFixJobRequestRuleIdString" }, "codeFixName": { "shape": "CodeFixName" }, - "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" } + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" }, + "profileArn": { "shape": "ProfileArn" } } }, "StartCodeFixJobRequestDescriptionString": { @@ -2245,7 +2703,8 @@ "codeGenerationId": { "shape": "CodeGenerationId" }, "currentCodeGenerationId": { "shape": "CodeGenerationId" }, "intent": { "shape": "Intent" }, - "intentContext": { "shape": "IntentContext" } + "intentContext": { "shape": "IntentContext" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent start code generation request.

" }, @@ -2272,7 +2731,9 @@ "clientToken": { "shape": "StartTestGenerationRequestClientTokenString", "idempotencyToken": true - } + }, + "profileArn": { "shape": "ProfileArn" }, + "referenceTrackerConfiguration": { "shape": "ReferenceTrackerConfiguration" } }, "documentation": "

Structure to represent test generation request.

" }, @@ -2299,7 +2760,8 @@ "required": ["workspaceState", "transformationSpec"], "members": { "workspaceState": { "shape": "WorkspaceState" }, - "transformationSpec": { "shape": "TransformationSpec" } + "transformationSpec": { "shape": "TransformationSpec" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent code transformation request.

" }, @@ -2320,7 +2782,8 @@ "type": "structure", "required": ["transformationJobId"], "members": { - "transformationJobId": { "shape": "TransformationJobId" } + "transformationJobId": { "shape": "TransformationJobId" }, + "profileArn": { "shape": "ProfileArn" } }, "documentation": "

Structure to represent stop code transformation request.

" }, @@ -2389,15 +2852,15 @@ "members": { "url": { "shape": "SupplementaryWebLinkUrlString", - "documentation": "

URL of the web reference link

" + "documentation": "

URL of the web reference link.

" }, "title": { "shape": "SupplementaryWebLinkTitleString", - "documentation": "

Title of the web reference link

" + "documentation": "

Title of the web reference link.

" }, "snippet": { "shape": "SupplementaryWebLinkSnippetString", - "documentation": "

Relevant text snippet from the link

" + "documentation": "

Relevant text snippet from the link.

" } }, "documentation": "

Represents an additional reference link retured with the Chat message

" @@ -2584,6 +3047,11 @@ }, "union": true }, + "TenantId": { + "type": "string", + "max": 1024, + "min": 1 + }, "TerminalUserInteractionEvent": { "type": "structure", "members": { @@ -2730,7 +3198,7 @@ }, "TextDocumentTextString": { "type": "string", - "max": 10240, + "max": 40000, "min": 0, "sensitive": true }, @@ -2751,6 +3219,124 @@ "enum": ["MONTHLY_REQUEST_COUNT"] }, "Timestamp": { "type": "timestamp" }, + "Tool": { + "type": "structure", + "members": { + "toolSpecification": { "shape": "ToolSpecification" } + }, + "documentation": "

Information about a tool that can be used.

", + "union": true + }, + "ToolDescription": { + "type": "string", + "documentation": "

The description for the tool.

", + "max": 10240, + "min": 1, + "sensitive": true + }, + "ToolInputSchema": { + "type": "structure", + "members": { + "json": { "shape": "SensitiveDocument" } + }, + "documentation": "

The input schema for the tool in JSON format.

" + }, + "ToolName": { + "type": "string", + "documentation": "

The name for the tool.

", + "max": 64, + "min": 0, + "pattern": "[a-zA-Z][a-zA-Z0-9_]*", + "sensitive": true + }, + "ToolResult": { + "type": "structure", + "required": ["toolUseId", "content"], + "members": { + "toolUseId": { "shape": "ToolUseId" }, + "content": { + "shape": "ToolResultContent", + "documentation": "

Content of the tool result.

" + }, + "status": { "shape": "ToolResultStatus" } + }, + "documentation": "

A tool result that contains the results for a tool request that was previously made.

" + }, + "ToolResultContent": { + "type": "list", + "member": { "shape": "ToolResultContentBlock" } + }, + "ToolResultContentBlock": { + "type": "structure", + "members": { + "text": { + "shape": "ToolResultContentBlockTextString", + "documentation": "

A tool result that is text.

" + }, + "json": { + "shape": "SensitiveDocument", + "documentation": "

A tool result that is JSON format data.

" + } + }, + "union": true + }, + "ToolResultContentBlockTextString": { + "type": "string", + "max": 800000, + "min": 0, + "sensitive": true + }, + "ToolResultStatus": { + "type": "string", + "documentation": "

Status of the tools result.

", + "enum": ["success", "error"] + }, + "ToolResults": { + "type": "list", + "member": { "shape": "ToolResult" }, + "max": 10, + "min": 0 + }, + "ToolSpecification": { + "type": "structure", + "required": ["inputSchema", "name"], + "members": { + "inputSchema": { "shape": "ToolInputSchema" }, + "name": { "shape": "ToolName" }, + "description": { "shape": "ToolDescription" } + }, + "documentation": "

The specification for the tool.

" + }, + "ToolUse": { + "type": "structure", + "required": ["toolUseId", "name", "input"], + "members": { + "toolUseId": { "shape": "ToolUseId" }, + "name": { "shape": "ToolName" }, + "input": { + "shape": "SensitiveDocument", + "documentation": "

The input to pass to the tool.

" + } + }, + "documentation": "

Contains information about a tool that the model is requesting be run. The model uses the result from the tool to generate a response.

" + }, + "ToolUseId": { + "type": "string", + "documentation": "

The ID for the tool request.

", + "max": 64, + "min": 0, + "pattern": "[a-zA-Z0-9_-]+" + }, + "ToolUses": { + "type": "list", + "member": { "shape": "ToolUse" }, + "max": 10, + "min": 0 + }, + "Tools": { + "type": "list", + "member": { "shape": "Tool" } + }, "TransformEvent": { "type": "structure", "required": ["jobId"], @@ -2967,7 +3553,8 @@ "taskAssistPlanningUploadContext": { "shape": "TaskAssistPlanningUploadContext" }, "transformationUploadContext": { "shape": "TransformationUploadContext" }, "codeAnalysisUploadContext": { "shape": "CodeAnalysisUploadContext" }, - "codeFixUploadContext": { "shape": "CodeFixUploadContext" } + "codeFixUploadContext": { "shape": "CodeFixUploadContext" }, + "workspaceContextUploadContext": { "shape": "WorkspaceContextUploadContext" } }, "union": true }, @@ -2986,9 +3573,15 @@ "AUTOMATIC_FILE_SECURITY_SCAN", "FULL_PROJECT_SECURITY_SCAN", "UNIT_TESTS_GENERATION", - "CODE_FIX_GENERATION" + "CODE_FIX_GENERATION", + "WORKSPACE_CONTEXT" ] }, + "Url": { + "type": "string", + "max": 1024, + "min": 1 + }, "UserContext": { "type": "structure", "required": ["ideCategory", "operatingSystem", "product"], @@ -3016,18 +3609,26 @@ }, "userInputMessageContext": { "shape": "UserInputMessageContext", - "documentation": "

Chat message context associated with the Chat Message

" + "documentation": "

Chat message context associated with the Chat Message.

" }, "userIntent": { "shape": "UserIntent", - "documentation": "

User Intent

" + "documentation": "

User Intent.

" + }, + "origin": { + "shape": "Origin", + "documentation": "

User Input Origin.

" + }, + "images": { + "shape": "ImageBlocks", + "documentation": "

Images associated with the Chat Message.

" } }, - "documentation": "

Structure to represent a chat input message from User

" + "documentation": "

Structure to represent a chat input message from User.

" }, "UserInputMessageContentString": { "type": "string", - "max": 4096, + "max": 600000, "min": 0, "sensitive": true }, @@ -3065,6 +3666,18 @@ "userSettings": { "shape": "UserSettings", "documentation": "

Settings information, e.g., whether the user has enabled cross-region API calls.

" + }, + "additionalContext": { + "shape": "AdditionalContentList", + "documentation": "

List of additional contextual content entries that can be included with the message.

" + }, + "toolResults": { + "shape": "ToolResults", + "documentation": "

ToolResults for the requested ToolUses.

" + }, + "tools": { + "shape": "Tools", + "documentation": "

Tools that can be used.

" } }, "documentation": "

Additional Chat message context associated with the Chat Message

" @@ -3157,6 +3770,35 @@ "documentation": "

Reason for ValidationException

", "enum": ["INVALID_CONVERSATION_ID", "CONTENT_LENGTH_EXCEEDS_THRESHOLD", "INVALID_KMS_GRANT"] }, + "WorkspaceContext": { + "type": "structure", + "required": ["toggle"], + "members": { + "toggle": { "shape": "OptInFeatureToggle" } + } + }, + "WorkspaceContextUploadContext": { + "type": "structure", + "required": ["workspaceId", "relativePath", "programmingLanguage"], + "members": { + "workspaceId": { "shape": "UUID" }, + "relativePath": { "shape": "SensitiveString" }, + "programmingLanguage": { "shape": "ProgrammingLanguage" } + } + }, + "WorkspaceList": { + "type": "list", + "member": { "shape": "WorkspaceMetadata" } + }, + "WorkspaceMetadata": { + "type": "structure", + "required": ["workspaceId", "workspaceStatus"], + "members": { + "workspaceId": { "shape": "UUID" }, + "workspaceStatus": { "shape": "WorkspaceStatus" }, + "environmentId": { "shape": "SensitiveString" } + } + }, "WorkspaceState": { "type": "structure", "required": ["uploadId", "programmingLanguage"], @@ -3176,6 +3818,10 @@ }, "documentation": "

Represents a Workspace state uploaded to S3 for Async Code Actions

" }, + "WorkspaceStatus": { + "type": "string", + "enum": ["CREATED", "PENDING", "READY", "CONNECTED", "DELETING"] + }, "timeBetweenChunks": { "type": "list", "member": { "shape": "Double" }, diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index 3cf030b9b8e..05a23403dda 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -4,7 +4,11 @@ */ import { SendMessageCommandOutput, SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' -import { GenerateAssistantResponseCommandOutput, GenerateAssistantResponseRequest } from '@amzn/codewhisperer-streaming' +import { + GenerateAssistantResponseCommandOutput, + GenerateAssistantResponseRequest, + ToolUse, +} from '@amzn/codewhisperer-streaming' import * as vscode from 'vscode' import { ToolkitError } from '../../../../shared/errors' import { createCodeWhispererChatStreamingClient } from '../../../../shared/clients/codewhispererChatClient' @@ -13,6 +17,17 @@ import { UserWrittenCodeTracker } from '../../../../codewhisperer/tracker/userWr export class ChatSession { private sessionId?: string + /** + * _readFiles = list of files read from the project to gather context before generating response. + * _filePath = The path helps the system locate exactly where to make the necessary changes in the project structure + * _tempFilePath = Used to show the code diff view in the editor including LLM changes. + * _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user + */ + private _readFiles: string[] = [] + private _filePath: string | undefined + private _tempFilePath: string | undefined + private _toolUse: ToolUse | undefined + private _showDiffOnFileWrite: boolean = false contexts: Map = new Map() // TODO: doesn't handle the edge case when two files share the same relativePath string but from different root @@ -22,6 +37,14 @@ export class ChatSession { return this.sessionId } + public get toolUse(): ToolUse | undefined { + return this._toolUse + } + + public setToolUse(toolUse: ToolUse | undefined) { + this._toolUse = toolUse + } + public tokenSource!: vscode.CancellationTokenSource constructor() { @@ -35,6 +58,33 @@ export class ChatSession { public setSessionID(id?: string) { this.sessionId = id } + public get readFiles(): string[] { + return this._readFiles + } + public get filePath(): string | undefined { + return this._filePath + } + public get tempFilePath(): string | undefined { + return this._tempFilePath + } + public get showDiffOnFileWrite(): boolean { + return this._showDiffOnFileWrite + } + public setShowDiffOnFileWrite(value: boolean) { + this._showDiffOnFileWrite = value + } + public setFilePath(filePath: string | undefined) { + this._filePath = filePath + } + public setTempFilePath(tempFilePath: string | undefined) { + this._tempFilePath = tempFilePath + } + public addToReadFiles(filePath: string) { + this._readFiles.push(filePath) + } + public clearListOfReadFiles() { + this._readFiles = [] + } async chatIam(chatRequest: SendMessageRequest): Promise { const client = await createQDeveloperStreamingClient() @@ -60,10 +110,6 @@ export class ChatSession { async chatSso(chatRequest: GenerateAssistantResponseRequest): Promise { const client = await createCodeWhispererChatStreamingClient() - if (this.sessionId !== undefined && chatRequest.conversationState !== undefined) { - chatRequest.conversationState.conversationId = this.sessionId - } - const response = await client.generateAssistantResponse(chatRequest) if (!response.generateAssistantResponseResponse) { throw new ToolkitError( diff --git a/packages/core/src/codewhispererChat/constants.ts b/packages/core/src/codewhispererChat/constants.ts index 84dd2dae292..8c1226a0055 100644 --- a/packages/core/src/codewhispererChat/constants.ts +++ b/packages/core/src/codewhispererChat/constants.ts @@ -4,6 +4,8 @@ */ import * as path from 'path' import fs from '../shared/fs/fs' +import { Tool } from '@amzn/codewhisperer-streaming' +import toolsJson from '../codewhispererChat/tools/tool_index.json' import { ContextLengths } from './controllers/chat/model' export const promptFileExtension = '.md' @@ -22,6 +24,12 @@ export const getUserPromptsDirectory = () => { export const createSavedPromptCommandId = 'create-saved-prompt' +export const tools: Tool[] = Object.entries(toolsJson).map(([, toolSpec]) => ({ + toolSpecification: { + ...toolSpec, + inputSchema: { json: toolSpec.inputSchema }, + }, +})) export const defaultContextLengths: ContextLengths = { additionalContextLengths: { fileContextLength: 0, diff --git a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts index be286122dc6..82bb79242c3 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/chatRequest/converter.ts @@ -6,7 +6,9 @@ import { ConversationState, CursorState, DocumentSymbol, SymbolType, TextDocument } from '@amzn/codewhisperer-streaming' import { AdditionalContentEntryAddition, ChatTriggerType, RelevantTextDocumentAddition, TriggerPayload } from '../model' import { undefinedIfEmpty } from '../../../../shared/utilities/textUtilities' +import { tools } from '../../../constants' import { getLogger } from '../../../../shared/logger/logger' +import vscode from 'vscode' const fqnNameSizeDownLimit = 1 const fqnNameSizeUpLimit = 256 @@ -159,14 +161,21 @@ export function triggerPayloadToChatRequest(triggerPayload: TriggerPayload): { c cursorState, relevantDocuments: triggerPayload.relevantTextDocuments, useRelevantDocuments: triggerPayload.useRelevantDocuments, + workspaceFolders: vscode.workspace.workspaceFolders?.map(({ uri }) => uri.fsPath) ?? [], }, additionalContext: triggerPayload.additionalContents, + tools, + ...(triggerPayload.toolResults !== undefined && + triggerPayload.toolResults !== null && { toolResults: triggerPayload.toolResults }), }, userIntent: triggerPayload.userIntent, + ...(triggerPayload.origin !== undefined && + triggerPayload.origin !== null && { origin: triggerPayload.origin }), }, }, chatTriggerType, customizationArn: customizationArn, + history: triggerPayload.chatHistory, }, } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 7264bcece69..a4be6387eb7 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -45,7 +45,12 @@ import { EditorContextCommand } from '../../commands/registerCommands' import { PromptsGenerator } from './prompts/promptsGenerator' import { TriggerEventsStorage } from '../../storages/triggerEvents' import { SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' -import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' +import { + CodeWhispererStreamingServiceException, + Origin, + ToolResult, + ToolResultStatus, +} from '@amzn/codewhisperer-streaming' import { UserIntentRecognizer } from './userIntent/userIntentRecognizer' import { CWCTelemetryHelper, recordTelemetryChatRunCommand } from './telemetryHelper' import { CodeWhispererTracker } from '../../../codewhisperer/tracker/codewhispererTracker' @@ -77,10 +82,16 @@ import { createSavedPromptCommandId, aditionalContentNameLimit, additionalContentInnerContextLimit, + tools, workspaceChunkMaxSize, defaultContextLengths, } from '../../constants' import { ChatSession } from '../../clients/chat/v0/chat' +import { ChatHistoryManager } from '../../storages/chatHistory' +import { amazonQTabSuffix } from '../../../shared/constants' +import { OutputKind } from '../../tools/toolShared' +import { ToolUtils, Tool } from '../../tools/toolUtils' +import { ChatStream } from '../../tools/chatStream' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher @@ -142,6 +153,7 @@ export class ChatController { private readonly userIntentRecognizer: UserIntentRecognizer private readonly telemetryHelper: CWCTelemetryHelper private userPromptsWatcher: vscode.FileSystemWatcher | undefined + private readonly chatHistoryManager: ChatHistoryManager public constructor( private readonly chatControllerMessageListeners: ChatControllerMessageListeners, @@ -159,6 +171,7 @@ export class ChatController { this.editorContentController = new EditorContentController() this.promptGenerator = new PromptsGenerator() this.userIntentRecognizer = new UserIntentRecognizer() + this.chatHistoryManager = new ChatHistoryManager() onDidChangeAmazonQVisibility((visible) => { if (visible) { @@ -386,6 +399,31 @@ export class ChatController { }) } + private async processAcceptCodeDiff(message: CustomFormActionMessage) { + const session = this.sessionStorage.getSession(message.tabID ?? '') + const filePath = session.filePath ?? '' + const fileExists = await fs.existsFile(filePath) + const tempFilePath = session.tempFilePath + const tempFileExists = await fs.existsFile(tempFilePath ?? '') + if (fileExists && tempFileExists) { + const fileContent = await fs.readFileText(filePath) + const tempFileContent = await fs.readFileText(tempFilePath ?? '') + if (fileContent !== tempFileContent) { + await fs.writeFile(filePath, tempFileContent) + } + await fs.delete(tempFilePath ?? '') + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)) + } else if (!fileExists && tempFileExists) { + const fileContent = await fs.readFileText(tempFilePath ?? '') + await fs.writeFile(filePath, fileContent) + await fs.delete(tempFilePath ?? '') + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath)) + } + // Reset the filePaths to undefined + session.setFilePath(undefined) + session.setTempFilePath(undefined) + } + private async processCopyCodeToClipboard(message: CopyCodeToClipboard) { this.telemetryHelper.recordInteractWithMessage(message) } @@ -396,6 +434,7 @@ export class ChatController { private async processTabCloseMessage(message: TabClosedMessage) { this.sessionStorage.deleteSession(message.tabID) + this.chatHistoryManager.clear() this.triggerEventsStorage.removeTabEvents(message.tabID) // this.telemetryHelper.recordCloseChat(message.tabID) } @@ -604,6 +643,14 @@ export class ChatController { const newFileDoc = await vscode.workspace.openTextDocument(newFilePath) await vscode.window.showTextDocument(newFileDoc) telemetry.ui_click.emit({ elementId: 'amazonq_createSavedPrompt' }) + } else if (message.action.id === 'accept-code-diff') { + await this.processAcceptCodeDiff(message) + } else if (message.action.id === 'reject-code-diff') { + // Reset the filePaths to undefined + this.sessionStorage.getSession(message.tabID ?? '').setFilePath(undefined) + this.sessionStorage.getSession(message.tabID ?? '').setTempFilePath(undefined) + } else if (message.action.id === 'confirm-tool-use') { + await this.processToolUseMessage(message) } } @@ -614,55 +661,70 @@ export class ChatController { } private async processFileClickMessage(message: FileClick) { const session = this.sessionStorage.getSession(message.tabID) - const lineRanges = session.contexts.get(message.filePath) - - if (!lineRanges) { - return - } - - // Check if clicked file is in a different workspace root - const projectRoot = - session.relativePathToWorkspaceRoot.get(message.filePath) || workspace.workspaceFolders?.[0]?.uri.fsPath - if (!projectRoot) { - return - } - let absoluteFilePath = path.join(projectRoot, message.filePath) + // Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly. + if (session.showDiffOnFileWrite) { + const filePath = session.filePath ?? message.filePath + const fileExists = await fs.existsFile(filePath) + // Check if fileExists=false, If yes, return instead of showing broken diff experience. + if (!session.tempFilePath) { + void vscode.window.showInformationMessage('Generated code changes have been reviewed and processed.') + return + } + const leftUri = fileExists ? vscode.Uri.file(filePath) : vscode.Uri.from({ scheme: 'untitled' }) + const rightUri = vscode.Uri.file(session.tempFilePath ?? filePath) + const fileName = path.basename(filePath) + await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, `${fileName} ${amazonQTabSuffix}`) + } else { + const lineRanges = session.contexts.get(message.filePath) + + if (!lineRanges) { + return + } - // Handle clicking on a user prompt outside the workspace - if (message.filePath.endsWith(promptFileExtension)) { - try { - await vscode.workspace.fs.stat(vscode.Uri.file(absoluteFilePath)) - } catch { - absoluteFilePath = path.join(getUserPromptsDirectory(), message.filePath) + // Check if clicked file is in a different workspace root + const projectRoot = + session.relativePathToWorkspaceRoot.get(message.filePath) || workspace.workspaceFolders?.[0]?.uri.fsPath + if (!projectRoot) { + return + } + let absoluteFilePath = path.join(projectRoot, message.filePath) + + // Handle clicking on a user prompt outside the workspace + if (message.filePath.endsWith(promptFileExtension)) { + try { + await vscode.workspace.fs.stat(vscode.Uri.file(absoluteFilePath)) + } catch { + absoluteFilePath = path.join(getUserPromptsDirectory(), message.filePath) + } } - } - try { - // Open the file in VSCode - const document = await workspace.openTextDocument(absoluteFilePath) - const editor = await window.showTextDocument(document, ViewColumn.Active) - - // Create multiple selections based on line ranges - const selections: Selection[] = lineRanges - .filter(({ first, second }) => first !== -1 && second !== -1) - .map(({ first, second }) => { - const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based - const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) - return new Selection( - startPosition.line, - startPosition.character, - endPosition.line, - endPosition.character - ) - }) + try { + // Open the file in VSCode + const document = await workspace.openTextDocument(absoluteFilePath) + const editor = await window.showTextDocument(document, ViewColumn.Active) + + // Create multiple selections based on line ranges + const selections: Selection[] = lineRanges + .filter(({ first, second }) => first !== -1 && second !== -1) + .map(({ first, second }) => { + const startPosition = new Position(first - 1, 0) // Convert 1-based to 0-based + const endPosition = new Position(second - 1, document.lineAt(second - 1).range.end.character) + return new Selection( + startPosition.line, + startPosition.character, + endPosition.line, + endPosition.character + ) + }) - // Apply multiple selections to the editor - if (selections.length > 0) { - editor.selection = selections[0] // Set the first selection as active - editor.selections = selections // Apply multiple selections - editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenter) - } - } catch (error) {} + // Apply multiple selections to the editor + if (selections.length > 0) { + editor.selection = selections[0] // Set the first selection as active + editor.selections = selections // Apply multiple selections + editor.revealRange(selections[0], vscode.TextEditorRevealType.InCenter) + } + } catch (error) {} + } } private processException(e: any, tabID: string) { @@ -685,6 +747,7 @@ export class ChatController { getLogger().error(`error: ${errorMessage} tabID: ${tabID} requestID: ${requestID}`) this.sessionStorage.deleteSession(tabID) + this.chatHistoryManager.clear() } private async processContextMenuCommand(command: EditorContextCommand) { @@ -758,6 +821,7 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromContextMenuCommand(command), customization: getSelectedCustomization(), + chatHistory: this.chatHistoryManager.getHistory(), additionalContents: [], relevantTextDocuments: [], documentReferences: [], @@ -805,6 +869,7 @@ export class ChatController { switch (message.command) { case 'clear': this.sessionStorage.deleteSession(message.tabID) + this.chatHistoryManager.clear() this.triggerEventsStorage.removeTabEvents(message.tabID) recordTelemetryChatRunCommand('clear') return @@ -843,6 +908,7 @@ export class ChatController { codeQuery: lastTriggerEvent.context?.focusAreaContext?.names, userIntent: message.userIntent, customization: getSelectedCustomization(), + chatHistory: this.chatHistoryManager.getHistory(), contextLengths: { ...defaultContextLengths, }, @@ -859,10 +925,102 @@ export class ChatController { } } + private async processToolUseMessage(message: CustomFormActionMessage) { + const tabID = message.tabID + if (!tabID) { + return + } + this.editorContextExtractor + .extractContextForTrigger('ChatMessage') + .then(async (context) => { + const triggerID = randomUUID() + this.triggerEventsStorage.addTriggerEvent({ + id: triggerID, + tabID: message.tabID, + message: undefined, + type: 'chat_message', + context, + }) + const session = this.sessionStorage.getSession(tabID) + const toolUse = session.toolUse + if (!toolUse || !toolUse.input) { + return + } + session.setToolUse(undefined) + + const toolResults: ToolResult[] = [] + + const result = ToolUtils.tryFromToolUse(toolUse) + if ('type' in result) { + const tool: Tool = result + + try { + await ToolUtils.validate(tool) + + const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse) + const output = await ToolUtils.invoke(tool, chatStream) + + toolResults.push({ + content: [ + output.output.kind === OutputKind.Text + ? { text: output.output.content } + : { json: output.output.content }, + ], + toolUseId: toolUse.toolUseId, + status: ToolResultStatus.SUCCESS, + }) + } catch (e: any) { + toolResults.push({ + content: [{ text: e.message }], + toolUseId: toolUse.toolUseId, + status: ToolResultStatus.ERROR, + }) + } + } else { + const toolResult: ToolResult = result + toolResults.push(toolResult) + } + + await this.generateResponse( + { + message: '', + trigger: ChatTriggerType.ChatMessage, + query: undefined, + codeSelection: context?.focusAreaContext?.selectionInsideExtendedCodeBlock, + fileText: context?.focusAreaContext?.extendedCodeBlock ?? '', + fileLanguage: context?.activeFileContext?.fileLanguage, + filePath: context?.activeFileContext?.filePath, + matchPolicy: context?.activeFileContext?.matchPolicy, + codeQuery: context?.focusAreaContext?.names, + userIntent: undefined, + customization: getSelectedCustomization(), + toolResults: toolResults, + origin: Origin.IDE, + chatHistory: this.chatHistoryManager.getHistory(), + context: [], + relevantTextDocuments: [], + additionalContents: [], + documentReferences: [], + useRelevantDocuments: false, + contextLengths: { + ...defaultContextLengths, + }, + }, + triggerID + ) + }) + .catch((e) => { + this.processException(e, tabID) + }) + } + private async processPromptMessageAsNewThread(message: PromptMessage) { + const session = this.sessionStorage.getSession(message.tabID) + session.clearListOfReadFiles() + session.setShowDiffOnFileWrite(false) this.editorContextExtractor .extractContextForTrigger('ChatMessage') - .then((context) => { + .then(async (context) => { const triggerID = randomUUID() this.triggerEventsStorage.addTriggerEvent({ id: triggerID, @@ -871,7 +1029,7 @@ export class ChatController { type: 'chat_message', context, }) - return this.generateResponse( + await this.generateResponse( { message: message.message ?? '', trigger: ChatTriggerType.ChatMessage, @@ -884,6 +1042,8 @@ export class ChatController { codeQuery: context?.focusAreaContext?.names, userIntent: this.userIntentRecognizer.getFromPromptChatMessage(message), customization: getSelectedCustomization(), + chatHistory: this.chatHistoryManager.getHistory(), + origin: Origin.IDE, context: message.context ?? [], relevantTextDocuments: [], additionalContents: [], @@ -1110,6 +1270,15 @@ export class ChatController { triggerPayload.contextLengths.userInputContextLength = triggerPayload.message.length triggerPayload.contextLengths.focusFileContextLength = triggerPayload.fileText.length const request = triggerPayloadToChatRequest(triggerPayload) + if ( + this.chatHistoryManager.getConversationId() !== undefined && + this.chatHistoryManager.getConversationId() !== '' + ) { + request.conversationState.conversationId = this.chatHistoryManager.getConversationId() + } else { + this.chatHistoryManager.setConversationId(randomUUID()) + request.conversationState.conversationId = this.chatHistoryManager.getConversationId() + } triggerPayload.documentReferences = this.mergeRelevantTextDocuments(triggerPayload.relevantTextDocuments) // Update context transparency after it's truncated dynamically to show users only the context sent. @@ -1160,12 +1329,31 @@ export class ChatController { this.telemetryHelper.recordEnterFocusConversation(triggerEvent.tabID) this.telemetryHelper.recordStartConversation(triggerEvent, triggerPayload) + this.chatHistoryManager.appendUserMessage({ + userInputMessage: { + content: triggerPayload.message, + userIntent: triggerPayload.userIntent, + ...(triggerPayload.origin && { origin: triggerPayload.origin }), + userInputMessageContext: { + tools: tools, + ...(triggerPayload.toolResults && { toolResults: triggerPayload.toolResults }), + }, + }, + }) + getLogger().info( `response to tab: ${tabID} conversationID: ${session.sessionIdentifier} requestID: ${ response.$metadata.requestId } metadata: ${inspect(response.$metadata, { depth: 12 })}` ) - await this.messenger.sendAIResponse(response, session, tabID, triggerID, triggerPayload) + await this.messenger.sendAIResponse( + response, + session, + tabID, + triggerID, + triggerPayload, + this.chatHistoryManager + ) } catch (e: any) { this.telemetryHelper.recordMessageResponseError(triggerPayload, tabID, getHttpStatusCode(e) ?? 0) // clears session, record telemetry before this call diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index dd80676cf8b..02a0a8253f1 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -9,6 +9,7 @@ import { AuthNeededException, CodeReference, ContextCommandData, + CustomFormActionMessage, EditorContextCommandMessage, OpenSettingsMessage, QuickActionMessage, @@ -20,6 +21,7 @@ import { ChatResponseStream as cwChatResponseStream, CodeWhispererStreamingServiceException, SupplementaryWebLink, + ToolUse, } from '@amzn/codewhisperer-streaming' import { ChatMessage, ErrorMessage, FollowUp, Suggestion } from '../../../view/connector/connector' import { ChatSession } from '../../../clients/chat/v0/chat' @@ -37,7 +39,10 @@ import { LspController } from '../../../../amazonq/lsp/lspController' import { extractCodeBlockLanguage } from '../../../../shared/markdown' import { extractAuthFollowUp } from '../../../../amazonq/util/authUtils' import { helpMessage } from '../../../../amazonq/webview/ui/texts/constants' -import { ChatItemButton, ChatItemFormItem, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatItemButton, ChatItemFormItem, MynahIconsType, MynahUIDataModel } from '@aws/mynah-ui' +import { ChatHistoryManager } from '../../../storages/chatHistory' +import { ToolType, ToolUtils } from '../../../tools/toolUtils' +import { ChatStream } from '../../../tools/chatStream' export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help' @@ -84,6 +89,10 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: mergedRelevantDocuments, + title: 'Context', + buttons: undefined, + fileList: undefined, + canBeVoted: false, }, tabID ) @@ -121,7 +130,8 @@ export class Messenger { session: ChatSession, tabID: string, triggerID: string, - triggerPayload: TriggerPayload + triggerPayload: TriggerPayload, + chatHistoryManager: ChatHistoryManager ) { let message = '' const messageID = response.$metadata.requestId ?? '' @@ -129,6 +139,8 @@ export class Messenger { let followUps: FollowUp[] = [] let relatedSuggestions: Suggestion[] = [] let codeBlockLanguage: string = 'plaintext' + let toolUseInput = '' + const toolUse: ToolUse = { toolUseId: undefined, name: undefined, input: undefined } if (response.message === undefined) { throw new ToolkitError( @@ -156,7 +168,7 @@ export class Messenger { }) const eventCounts = new Map() - waitUntil( + await waitUntil( async () => { for await (const chatEvent of response.message!) { for (const key of keys(chatEvent)) { @@ -186,6 +198,40 @@ export class Messenger { ] } + const cwChatEvent: cwChatResponseStream = chatEvent + if ( + cwChatEvent.toolUseEvent?.input !== undefined && + cwChatEvent.toolUseEvent.input.length > 0 && + !cwChatEvent.toolUseEvent.stop + ) { + toolUseInput += cwChatEvent.toolUseEvent.input + } + + if (cwChatEvent.toolUseEvent?.stop) { + toolUse.input = JSON.parse(toolUseInput) + toolUse.toolUseId = cwChatEvent.toolUseEvent.toolUseId ?? '' + toolUse.name = cwChatEvent.toolUseEvent.name ?? '' + session.setToolUse(toolUse) + + const tool = ToolUtils.tryFromToolUse(toolUse) + if ('type' in tool) { + const requiresAcceptance = ToolUtils.requiresAcceptance(tool) + + const chatStream = new ChatStream(this, tabID, triggerID, toolUse, requiresAcceptance) + ToolUtils.queueDescription(tool, chatStream) + + if (!requiresAcceptance) { + this.dispatcher.sendCustomFormActionMessage( + new CustomFormActionMessage(tabID, { + id: 'confirm-tool-use', + }) + ) + } + } else { + // TODO: Handle the error + } + } + if ( chatEvent.assistantResponseEvent?.content !== undefined && chatEvent.assistantResponseEvent.content.length > 0 @@ -331,6 +377,17 @@ export class Messenger { ) ) + chatHistoryManager.pushAssistantMessage({ + assistantResponseMessage: { + messageId: messageID, + content: message, + references: codeReference, + ...(toolUse && + toolUse.input !== undefined && + toolUse.input !== '' && { toolUses: [{ ...toolUse }] }), + }, + }) + getLogger().info( `All events received. requestId=%s counts=%s`, response.$metadata.requestId, @@ -365,6 +422,57 @@ export class Messenger { ) } + public sendPartialToolLog( + message: string, + tabID: string, + triggerID: string, + toolUse: ToolUse | undefined, + requiresAcceptance = false + ) { + const buttons: ChatItemButton[] = [] + if (requiresAcceptance) { + buttons.push({ + icon: 'play' as MynahIconsType, + id: 'confirm-tool-use', + status: 'clear', + text: 'Run', + }) + } + + this.dispatcher.sendChatMessage( + new ChatMessage( + { + message, + messageType: 'answer-part', + followUps: undefined, + followUpsHeader: undefined, + relatedSuggestions: undefined, + triggerID, + messageID: toolUse?.toolUseId ?? `tool-output`, + userIntent: undefined, + codeBlockLanguage: undefined, + contextList: undefined, + canBeVoted: false, + buttons: toolUse?.name === ToolType.ExecuteBash ? undefined : buttons, + fullWidth: true, + header: + toolUse?.name === ToolType.ExecuteBash + ? { + icon: 'code-block' as MynahIconsType, + body: 'Terminal command', + buttons: buttons, + } + : undefined, + codeBlockActions: + // eslint-disable-next-line unicorn/no-null + toolUse?.name === ToolType.ExecuteBash ? { 'insert-to-cursor': null, copy: null } : undefined, + padding: toolUse?.name === ToolType.ExecuteBash ? true : false, + }, + tabID + ) + ) + } + private editorContextMenuCommandVerbs: Map = new Map([ ['aws.amazonq.explainCode', 'Explain'], ['aws.amazonq.explainIssue', 'Explain'], @@ -425,6 +533,7 @@ export class Messenger { userIntent: undefined, codeBlockLanguage: undefined, contextList: undefined, + title: undefined, }, tabID ) diff --git a/packages/core/src/codewhispererChat/controllers/chat/model.ts b/packages/core/src/codewhispererChat/controllers/chat/model.ts index 8e13360a486..b07cea0cc4a 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/model.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/model.ts @@ -4,7 +4,14 @@ */ import * as vscode from 'vscode' -import { AdditionalContentEntry, RelevantTextDocument, UserIntent } from '@amzn/codewhisperer-streaming' +import { + AdditionalContentEntry, + ChatMessage, + Origin, + RelevantTextDocument, + ToolResult, + UserIntent, +} from '@amzn/codewhisperer-streaming' import { MatchPolicy, CodeQuery } from '../../clients/chat/v0/model' import { Selection } from 'vscode' import { TabOpenType } from '../../../amazonq/webview/ui/storages/tabsStorage' @@ -196,6 +203,9 @@ export interface TriggerPayload { traceId?: string contextLengths: ContextLengths workspaceRulesCount?: number + chatHistory?: ChatMessage[] + toolResults?: ToolResult[] + origin?: Origin } export type ContextLengths = { diff --git a/packages/core/src/codewhispererChat/storages/chatHistory.ts b/packages/core/src/codewhispererChat/storages/chatHistory.ts new file mode 100644 index 00000000000..03ed505b2cf --- /dev/null +++ b/packages/core/src/codewhispererChat/storages/chatHistory.ts @@ -0,0 +1,214 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { + ChatMessage, + Tool, + ToolResult, + ToolResultStatus, + UserInputMessage, + UserInputMessageContext, +} from '@amzn/codewhisperer-streaming' +import { randomUUID } from '../../shared/crypto' +import { getLogger } from '../../shared/logger/logger' +import { tools } from '../constants' + +// Maximum number of messages to keep in history +const MaxConversationHistoryLength = 100 + +/** + * ChatHistoryManager handles the storage and manipulation of chat history + * for CodeWhisperer Chat sessions. + */ +export class ChatHistoryManager { + private conversationId: string + private history: ChatMessage[] = [] + private logger = getLogger() + private lastUserMessage?: ChatMessage + private tools: Tool[] = [] + + constructor() { + this.conversationId = randomUUID() + this.logger.info(`Generated new conversation id: ${this.conversationId}`) + this.tools = tools + } + + /** + * Get the conversation ID + */ + public getConversationId(): string { + return this.conversationId + } + + public setConversationId(conversationId: string) { + this.conversationId = conversationId + } + + /** + * Get the full chat history + */ + public getHistory(): ChatMessage[] { + return [...this.history] + } + + /** + * Clear the conversation history + */ + public clear(): void { + this.history = [] + this.conversationId = '' + } + + /** + * Append a new user message to be sent + */ + public appendUserMessage(newMessage: ChatMessage): void { + this.lastUserMessage = newMessage + this.fixHistory() + if (!newMessage.userInputMessage?.content || newMessage.userInputMessage?.content.trim() === '') { + this.logger.warn('input must not be empty when adding new messages') + } + this.history.push(this.formatChatHistoryMessage(this.lastUserMessage)) + } + + /** + * Push an assistant message to the history + */ + public pushAssistantMessage(newMessage: ChatMessage): void { + if (newMessage !== undefined && this.lastUserMessage !== undefined) { + this.logger.warn('last Message should not be defined when pushing an assistant message') + } + this.history.push(newMessage) + } + + /** + * Fixes the history to maintain the following invariants: + * 1. The history length is <= MAX_CONVERSATION_HISTORY_LENGTH. Oldest messages are dropped. + * 2. The first message is from the user. Oldest messages are dropped if needed. + * 3. The last message is from the assistant. The last message is dropped if it is from the user. + * 4. If the last message is from the assistant and it contains tool uses, and a next user + * message is set without tool results, then the user message will have cancelled tool results. + */ + public fixHistory(): void { + // Trim the conversation history if it exceeds the maximum length + if (this.history.length > MaxConversationHistoryLength) { + // Find the second oldest user message without tool results + let indexToTrim: number | undefined + + for (let i = 1; i < this.history.length; i++) { + const message = this.history[i] + if (message.userInputMessage) { + const userMessage = message.userInputMessage + const ctx = userMessage.userInputMessageContext + const hasNoToolResults = ctx && (!ctx.toolResults || ctx.toolResults.length === 0) + if (hasNoToolResults && userMessage.content !== '') { + indexToTrim = i + break + } + } + } + if (indexToTrim !== undefined) { + this.logger.debug(`Removing the first ${indexToTrim} elements in the history`) + this.history.splice(0, indexToTrim) + } else { + this.logger.debug('No valid starting user message found in the history, clearing') + this.history = [] + } + } + + // Ensure the last message is from the assistant + if (this.history.length > 0 && this.history[this.history.length - 1].userInputMessage !== undefined) { + this.logger.debug('Last message in history is from the user, dropping') + this.history.pop() + } + + // TODO: If the last message from the assistant contains tool uses, ensure the next user message contains tool results + + const lastHistoryMessage = this.history[this.history.length - 1] + + if ( + lastHistoryMessage && + (lastHistoryMessage.assistantResponseMessage || + lastHistoryMessage.assistantResponseMessage !== undefined) && + this.lastUserMessage + ) { + const toolUses = lastHistoryMessage.assistantResponseMessage.toolUses + + if (toolUses && toolUses.length > 0) { + if (this.lastUserMessage.userInputMessage) { + if (this.lastUserMessage.userInputMessage.userInputMessageContext) { + const ctx = this.lastUserMessage.userInputMessage.userInputMessageContext + + if (!ctx.toolResults || ctx.toolResults.length === 0) { + ctx.toolResults = toolUses.map((toolUse) => ({ + toolUseId: toolUse.toolUseId, + content: [ + { + type: 'Text', + text: 'Tool use was cancelled by the user', + }, + ], + status: ToolResultStatus.ERROR, + })) + } + } else { + const toolResults = toolUses.map((toolUse) => ({ + toolUseId: toolUse.toolUseId, + content: [ + { + type: 'Text', + text: 'Tool use was cancelled by the user', + }, + ], + status: ToolResultStatus.ERROR, + })) + + this.lastUserMessage.userInputMessage.userInputMessageContext = { + shellState: undefined, + envState: undefined, + toolResults: toolResults, + tools: this.tools.length === 0 ? undefined : [...this.tools], + } + } + } + } + } + } + + /** + * Adds tool results to the conversation. + */ + addToolResults(toolResults: ToolResult[]): void { + const userInputMessageContext: UserInputMessageContext = { + shellState: undefined, + envState: undefined, + toolResults: toolResults, + tools: this.tools.length === 0 ? undefined : [...this.tools], + } + + const msg: UserInputMessage = { + content: '', + userInputMessageContext: userInputMessageContext, + } + + if (this.lastUserMessage?.userInputMessage) { + this.lastUserMessage.userInputMessage = msg + } + } + + private formatChatHistoryMessage(message: ChatMessage): ChatMessage { + if (message.userInputMessage !== undefined) { + return { + userInputMessage: { + ...message.userInputMessage, + userInputMessageContext: { + ...message.userInputMessage.userInputMessageContext, + tools: undefined, + }, + }, + } + } + return message + } +} diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts new file mode 100644 index 00000000000..2f2984fd990 --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -0,0 +1,58 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Writable } from 'stream' +import { getLogger } from '../../shared/logger/logger' +import { Messenger } from '../controllers/chat/messenger/messenger' +import { ToolUse } from '@amzn/codewhisperer-streaming' + +/** + * A writable stream that feeds each chunk/line to the chat UI. + * Used for streaming tool output (like bash execution) to the chat interface. + */ +export class ChatStream extends Writable { + private accumulatedLogs = '' + + public constructor( + private readonly messenger: Messenger, + private readonly tabID: string, + private readonly triggerID: string, + private readonly toolUse: ToolUse | undefined, + private readonly requiresAcceptance = false, + private readonly logger = getLogger('chatStream') + ) { + super() + this.logger.debug(`ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}`) + this.messenger.sendInitalStream(tabID, triggerID, undefined) + } + + override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void { + const text = chunk.toString() + this.accumulatedLogs += text + this.logger.debug(`ChatStream received chunk: ${text}`) + this.messenger.sendPartialToolLog( + this.accumulatedLogs, + this.tabID, + this.triggerID, + this.toolUse, + this.requiresAcceptance + ) + callback() + } + + override _final(callback: (error?: Error | null) => void): void { + this.logger.info(`this.accumulatedLogs: ${this.accumulatedLogs}`) + if (this.accumulatedLogs.trim().length > 0) { + this.messenger.sendPartialToolLog( + this.accumulatedLogs, + this.tabID, + this.triggerID, + this.toolUse, + this.requiresAcceptance + ) + } + callback() + } +} diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts new file mode 100644 index 00000000000..420b5691eb4 --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -0,0 +1,211 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Writable } from 'stream' +import { getLogger } from '../../shared/logger/logger' +import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir() +import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils' +import { InvokeOutput, OutputKind, sanitizePath } from './toolShared' + +export const readOnlyCommands: string[] = ['ls', 'cat', 'echo', 'pwd', 'which', 'head', 'tail'] +export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB +export const lineCount: number = 1024 +export const dangerousPatterns: string[] = ['|', '<(', '$(', '`', '>', '&&', '||'] + +export interface ExecuteBashParams { + command: string + cwd?: string +} + +export class ExecuteBash { + private readonly command: string + private readonly workingDirectory?: string + private readonly logger = getLogger('executeBash') + + constructor(params: ExecuteBashParams) { + this.command = params.command + this.workingDirectory = params.cwd ? sanitizePath(params.cwd) : fs.getUserHomeDir() + } + + public async validate(): Promise { + if (!this.command.trim()) { + throw new Error('Bash command cannot be empty.') + } + + const args = ExecuteBash.parseCommand(this.command) + if (!args || args.length === 0) { + throw new Error('No command found.') + } + + try { + await ExecuteBash.whichCommand(args[0]) + } catch { + throw new Error(`Command "${args[0]}" not found on PATH.`) + } + } + + public requiresAcceptance(): boolean { + try { + const args = ExecuteBash.parseCommand(this.command) + if (!args || args.length === 0) { + return true + } + + if (args.some((arg) => dangerousPatterns.some((pattern) => arg.includes(pattern)))) { + return true + } + + const command = args[0] + return !readOnlyCommands.includes(command) + } catch (error) { + this.logger.warn(`Error while checking acceptance: ${(error as Error).message}`) + return true + } + } + + public async invoke(updates: Writable): Promise { + this.logger.info(`Invoking bash command: "${this.command}" in cwd: "${this.workingDirectory}"`) + + return new Promise(async (resolve, reject) => { + this.logger.debug(`Spawning process with command: bash -c "${this.command}" (cwd=${this.workingDirectory})`) + + const stdoutBuffer: string[] = [] + const stderrBuffer: string[] = [] + + let firstChunk = true + const childProcessOptions: ChildProcessOptions = { + spawnOptions: { + cwd: this.workingDirectory, + stdio: ['pipe', 'pipe', 'pipe'], + }, + collect: false, + waitForStreams: true, + onStdout: (chunk: string) => { + ExecuteBash.handleChunk(firstChunk ? '```console\n' + chunk : chunk, stdoutBuffer, updates) + firstChunk = false + }, + onStderr: (chunk: string) => { + ExecuteBash.handleChunk(chunk, stderrBuffer, updates) + }, + } + + const childProcess = new ChildProcess('bash', ['-c', this.command], childProcessOptions) + + try { + const result = await childProcess.run() + const exitStatus = result.exitCode ?? 0 + const stdout = stdoutBuffer.join('\n') + const stderr = stderrBuffer.join('\n') + const [stdoutTrunc, stdoutSuffix] = ExecuteBash.truncateSafelyWithSuffix( + stdout, + maxBashToolResponseSize / 3 + ) + const [stderrTrunc, stderrSuffix] = ExecuteBash.truncateSafelyWithSuffix( + stderr, + maxBashToolResponseSize / 3 + ) + + const outputJson = { + exitStatus: exitStatus.toString(), + stdout: stdoutTrunc + (stdoutSuffix ? ' ... truncated' : ''), + stderr: stderrTrunc + (stderrSuffix ? ' ... truncated' : ''), + } + + resolve({ + output: { + kind: OutputKind.Json, + content: outputJson, + }, + }) + } catch (err: any) { + this.logger.error(`Failed to execute bash command '${this.command}': ${err.message}`) + reject(new Error(`Failed to execute command: ${err.message}`)) + } + }) + } + + private static handleChunk(chunk: string, buffer: string[], updates: Writable) { + try { + updates.write(chunk) + const lines = chunk.split(/\r?\n/) + for (const line of lines) { + buffer.push(line) + if (buffer.length > lineCount) { + buffer.shift() + } + } + } catch (error) { + // Log the error but don't let it crash the process + throw new Error('Error handling output chunk') + } + } + + private static truncateSafelyWithSuffix(str: string, maxLength: number): [string, boolean] { + if (str.length > maxLength) { + return [str.substring(0, maxLength), true] + } + return [str, false] + } + + private static async whichCommand(cmd: string): Promise { + const cp = new ChildProcess('which', [cmd], { + collect: true, + waitForStreams: true, + }) + const result = await cp.run() + + if (result.exitCode !== 0) { + throw new Error(`Command "${cmd}" not found on PATH.`) + } + + const output = result.stdout.trim() + if (!output) { + throw new Error(`Command "${cmd}" found but 'which' returned empty output.`) + } + return output + } + + private static parseCommand(command: string): string[] | undefined { + const result: string[] = [] + let current = '' + let inQuote: string | undefined + let escaped = false + + for (const char of command) { + if (escaped) { + current += char + escaped = false + } else if (char === '\\') { + escaped = true + } else if (inQuote) { + if (char === inQuote) { + inQuote = undefined + } else { + current += char + } + } else if (char === '"' || char === "'") { + inQuote = char + } else if (char === ' ' || char === '\t') { + if (current) { + result.push(current) + current = '' + } + } else { + current += char + } + } + + if (current) { + result.push(current) + } + + return result + } + + public queueDescription(updates: Writable): void { + updates.write(`\n\`\`\`shell\n${this.command}\n\`\`\`\n`) + updates.end() + } +} diff --git a/packages/core/src/codewhispererChat/tools/fsRead.ts b/packages/core/src/codewhispererChat/tools/fsRead.ts new file mode 100644 index 00000000000..a078347dc3d --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/fsRead.ts @@ -0,0 +1,147 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as vscode from 'vscode' +import { getLogger } from '../../shared/logger/logger' +import { readDirectoryRecursively } from '../../shared/utilities/workspaceUtils' +import fs from '../../shared/fs/fs' +import { InvokeOutput, maxToolResponseSize, OutputKind, sanitizePath } from './toolShared' +import { Writable } from 'stream' +import path from 'path' + +export interface FsReadParams { + path: string + readRange?: number[] +} + +export class FsRead { + private fsPath: string + private readonly readRange?: number[] + private isFile?: boolean // true for file, false for directory + private readonly logger = getLogger('fsRead') + + constructor(params: FsReadParams) { + this.fsPath = params.path + this.readRange = params.readRange + } + + public async validate(): Promise { + this.logger.debug(`Validating fsPath: ${this.fsPath}`) + if (!this.fsPath || this.fsPath.trim().length === 0) { + throw new Error('Path cannot be empty.') + } + + const sanitized = sanitizePath(this.fsPath) + this.fsPath = sanitized + + const fileUri = vscode.Uri.file(this.fsPath) + let exists: boolean + try { + exists = await fs.exists(fileUri) + if (!exists) { + throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`) + } + } catch (err) { + throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`) + } + + this.isFile = await fs.existsFile(fileUri) + this.logger.debug(`Validation succeeded for path: ${this.fsPath}`) + } + + public queueDescription(updates: Writable): void { + const fileName = path.basename(this.fsPath) + const fileUri = vscode.Uri.file(this.fsPath) + updates.write(`Reading: [${fileName}](${fileUri})`) + updates.end() + } + + public async invoke(updates: Writable): Promise { + try { + const fileUri = vscode.Uri.file(this.fsPath) + + if (this.isFile) { + const fileContents = await this.readFile(fileUri) + this.logger.info(`Read file: ${this.fsPath}, size: ${fileContents.length}`) + return this.handleFileRange(fileContents) + } else if (!this.isFile) { + const maxDepth = this.getDirectoryDepth() ?? 0 + const listing = await readDirectoryRecursively(fileUri, maxDepth) + return this.createOutput(listing.join('\n')) + } else { + throw new Error(`"${this.fsPath}" is neither a standard file nor directory.`) + } + } catch (error: any) { + this.logger.error(`Failed to read "${this.fsPath}": ${error.message || error}`) + throw new Error(`[fs_read] Failed to read "${this.fsPath}": ${error.message || error}`) + } + } + + private async readFile(fileUri: vscode.Uri): Promise { + this.logger.info(`Reading file: ${fileUri.fsPath}`) + return await fs.readFileText(fileUri) + } + + private handleFileRange(fullText: string): InvokeOutput { + if (!this.readRange || this.readRange.length === 0) { + this.logger.info('No range provided. returning entire file.') + return this.createOutput(this.enforceMaxSize(fullText)) + } + + const lines = fullText.split('\n') + const [start, end] = this.parseLineRange(lines.length, this.readRange) + if (start > end) { + this.logger.error(`Invalid range: ${this.readRange.join('-')}`) + return this.createOutput('') + } + + this.logger.info(`Reading file: ${this.fsPath}, lines ${start + 1}-${end + 1}`) + const slice = lines.slice(start, end + 1).join('\n') + return this.createOutput(this.enforceMaxSize(slice)) + } + + private parseLineRange(lineCount: number, range: number[]): [number, number] { + const startIdx = range[0] + let endIdx = range.length >= 2 ? range[1] : undefined + + if (endIdx === undefined) { + endIdx = -1 + } + + const convert = (i: number): number => { + return i < 0 ? lineCount + i : i - 1 + } + + const finalStart = Math.max(0, Math.min(lineCount - 1, convert(startIdx))) + const finalEnd = Math.max(0, Math.min(lineCount - 1, convert(endIdx))) + return [finalStart, finalEnd] + } + + private getDirectoryDepth(): number | undefined { + if (!this.readRange || this.readRange.length === 0) { + return 0 + } + return this.readRange[0] + } + + private enforceMaxSize(content: string): string { + const byteCount = Buffer.byteLength(content, 'utf8') + if (byteCount > maxToolResponseSize) { + throw new Error( + `This tool only supports reading ${maxToolResponseSize} bytes at a time. + You tried to read ${byteCount} bytes. Try executing with fewer lines specified.` + ) + } + return content + } + + private createOutput(content: string): InvokeOutput { + return { + output: { + kind: OutputKind.Text, + content: content, + }, + } + } +} diff --git a/packages/core/src/codewhispererChat/tools/fsWrite.ts b/packages/core/src/codewhispererChat/tools/fsWrite.ts new file mode 100644 index 00000000000..c1f02309ed4 --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/fsWrite.ts @@ -0,0 +1,191 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InvokeOutput, OutputKind, sanitizePath } from './toolShared' +import { getLogger } from '../../shared/logger/logger' +import vscode from 'vscode' +import { fs } from '../../shared/fs/fs' +import { Writable } from 'stream' +import path from 'path' + +interface BaseParams { + path: string +} + +export interface CreateParams extends BaseParams { + command: 'create' + fileText?: string + newStr?: string +} + +export interface StrReplaceParams extends BaseParams { + command: 'strReplace' + oldStr: string + newStr: string +} + +export interface InsertParams extends BaseParams { + command: 'insert' + insertLine: number + newStr: string +} + +export interface AppendParams extends BaseParams { + command: 'append' + newStr: string +} + +export type FsWriteParams = CreateParams | StrReplaceParams | InsertParams | AppendParams + +export class FsWrite { + private readonly logger = getLogger('fsWrite') + + constructor(private readonly params: FsWriteParams) {} + + public async invoke(updates: Writable): Promise { + const sanitizedPath = sanitizePath(this.params.path) + + switch (this.params.command) { + case 'create': + await this.handleCreate(this.params, sanitizedPath) + break + case 'strReplace': + await this.handleStrReplace(this.params, sanitizedPath) + break + case 'insert': + await this.handleInsert(this.params, sanitizedPath) + break + case 'append': + await this.handleAppend(this.params, sanitizedPath) + break + } + + return { + output: { + kind: OutputKind.Text, + content: '', + }, + } + } + + public queueDescription(updates: Writable): void { + const fileName = path.basename(this.params.path) + const fileUri = vscode.Uri.file(this.params.path) + updates.write(`Writing to: [${fileName}](${fileUri})`) + updates.end() + } + + public async validate(): Promise { + switch (this.params.command) { + case 'create': + if (!this.params.path) { + throw new Error('Path must not be empty') + } + break + case 'strReplace': + case 'insert': { + const fileExists = await fs.existsFile(this.params.path) + if (!fileExists) { + throw new Error('The provided path must exist in order to replace or insert contents into it') + } + break + } + case 'append': + if (!this.params.path) { + throw new Error('Path must not be empty') + } + if (!this.params.newStr) { + throw new Error('Content to append must not be empty') + } + break + } + } + + private async handleCreate(params: CreateParams, sanitizedPath: string): Promise { + const content = this.getCreateCommandText(params) + + const fileExists = await fs.existsFile(sanitizedPath) + const actionType = fileExists ? 'Replacing' : 'Creating' + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `${actionType}: ${sanitizedPath}`, + cancellable: false, + }, + async () => { + await fs.writeFile(sanitizedPath, content) + } + ) + } + + private async handleStrReplace(params: StrReplaceParams, sanitizedPath: string): Promise { + const fileContent = await fs.readFileText(sanitizedPath) + + const matches = [...fileContent.matchAll(new RegExp(this.escapeRegExp(params.oldStr), 'g'))] + + if (matches.length === 0) { + throw new Error(`No occurrences of "${params.oldStr}" were found`) + } + if (matches.length > 1) { + throw new Error(`${matches.length} occurrences of oldStr were found when only 1 is expected`) + } + + const newContent = fileContent.replace(params.oldStr, params.newStr) + await fs.writeFile(sanitizedPath, newContent) + + void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`) + } + + private async handleInsert(params: InsertParams, sanitizedPath: string): Promise { + const fileContent = await fs.readFileText(sanitizedPath) + const lines = fileContent.split('\n') + + const numLines = lines.length + const insertLine = Math.max(0, Math.min(params.insertLine, numLines)) + + let newContent: string + if (insertLine === 0) { + newContent = params.newStr + '\n' + fileContent + } else { + newContent = [...lines.slice(0, insertLine), params.newStr, ...lines.slice(insertLine)].join('\n') + } + + await fs.writeFile(sanitizedPath, newContent) + + void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`) + } + + private async handleAppend(params: AppendParams, sanitizedPath: string): Promise { + const fileContent = await fs.readFileText(sanitizedPath) + const needsNewline = fileContent.length !== 0 && !fileContent.endsWith('\n') + + let contentToAppend = params.newStr + if (needsNewline) { + contentToAppend = '\n' + contentToAppend + } + + const newContent = fileContent + contentToAppend + await fs.writeFile(sanitizedPath, newContent) + + void vscode.window.showInformationMessage(`Updated: ${sanitizedPath}`) + } + + private getCreateCommandText(params: CreateParams): string { + if (params.fileText) { + return params.fileText + } + if (params.newStr) { + this.logger.warn('Required field `fileText` is missing, use the provided `newStr` instead') + return params.newStr + } + this.logger.warn('No content provided for the create command') + return '' + } + + private escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + } +} diff --git a/packages/core/src/codewhispererChat/tools/toolShared.ts b/packages/core/src/codewhispererChat/tools/toolShared.ts new file mode 100644 index 00000000000..104e883cd45 --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/toolShared.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path' +import fs from '../../shared/fs/fs' + +export const maxToolResponseSize = 30720 // 30KB + +export enum OutputKind { + Text = 'text', + Json = 'json', +} + +export interface InvokeOutput { + output: { + kind: OutputKind + content: string | any + } +} + +export function sanitizePath(inputPath: string): string { + let sanitized = inputPath.trim() + + if (sanitized.startsWith('~')) { + sanitized = path.join(fs.getUserHomeDir(), sanitized.slice(1)) + } + + if (!path.isAbsolute(sanitized)) { + sanitized = path.resolve(sanitized) + } + return sanitized +} diff --git a/packages/core/src/codewhispererChat/tools/toolUtils.ts b/packages/core/src/codewhispererChat/tools/toolUtils.ts new file mode 100644 index 00000000000..20bfa1afb0e --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/toolUtils.ts @@ -0,0 +1,126 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { Writable } from 'stream' +import { FsRead, FsReadParams } from './fsRead' +import { FsWrite, FsWriteParams } from './fsWrite' +import { ExecuteBash, ExecuteBashParams } from './executeBash' +import { ToolResult, ToolResultContentBlock, ToolResultStatus, ToolUse } from '@amzn/codewhisperer-streaming' +import { InvokeOutput } from './toolShared' + +export enum ToolType { + FsRead = 'fsRead', + FsWrite = 'fsWrite', + ExecuteBash = 'executeBash', +} + +export type Tool = + | { type: ToolType.FsRead; tool: FsRead } + | { type: ToolType.FsWrite; tool: FsWrite } + | { type: ToolType.ExecuteBash; tool: ExecuteBash } + +export class ToolUtils { + static displayName(tool: Tool): string { + switch (tool.type) { + case ToolType.FsRead: + return 'Read from filesystem' + case ToolType.FsWrite: + return 'Write to filesystem' + case ToolType.ExecuteBash: + return 'Execute shell command' + } + } + + static requiresAcceptance(tool: Tool) { + switch (tool.type) { + case ToolType.FsRead: + return false + case ToolType.FsWrite: + return true + case ToolType.ExecuteBash: + return tool.tool.requiresAcceptance() + } + } + + static async invoke(tool: Tool, updates: Writable): Promise { + switch (tool.type) { + case ToolType.FsRead: + return tool.tool.invoke(updates) + case ToolType.FsWrite: + return tool.tool.invoke(updates) + case ToolType.ExecuteBash: + return tool.tool.invoke(updates) + } + } + + static queueDescription(tool: Tool, updates: Writable): void { + switch (tool.type) { + case ToolType.FsRead: + tool.tool.queueDescription(updates) + break + case ToolType.FsWrite: + tool.tool.queueDescription(updates) + break + case ToolType.ExecuteBash: + tool.tool.queueDescription(updates) + break + } + } + + static async validate(tool: Tool): Promise { + switch (tool.type) { + case ToolType.FsRead: + return tool.tool.validate() + case ToolType.FsWrite: + return tool.tool.validate() + case ToolType.ExecuteBash: + return tool.tool.validate() + } + } + + static tryFromToolUse(value: ToolUse): Tool | ToolResult { + const mapErr = (parseError: any): ToolResult => ({ + toolUseId: value.toolUseId, + content: [ + { + type: 'text', + text: `Failed to validate tool parameters: ${parseError}. The model has either suggested tool parameters which are incompatible with the existing tools, or has suggested one or more tool that does not exist in the list of known tools.`, + } as ToolResultContentBlock, + ], + status: ToolResultStatus.ERROR, + }) + + try { + switch (value.name) { + case ToolType.FsRead: + return { + type: ToolType.FsRead, + tool: new FsRead(value.input as unknown as FsReadParams), + } + case ToolType.FsWrite: + return { + type: ToolType.FsWrite, + tool: new FsWrite(value.input as unknown as FsWriteParams), + } + case ToolType.ExecuteBash: + return { + type: ToolType.ExecuteBash, + tool: new ExecuteBash(value.input as unknown as ExecuteBashParams), + } + default: + return { + toolUseId: value.toolUseId, + content: [ + { + type: 'text', + text: `The tool, "${value.name}" is not supported by the client`, + } as ToolResultContentBlock, + ], + } + } + } catch (error) { + return mapErr(error) + } + } +} diff --git a/packages/core/src/codewhispererChat/tools/tool_index.json b/packages/core/src/codewhispererChat/tools/tool_index.json new file mode 100644 index 00000000000..58b8731e264 --- /dev/null +++ b/packages/core/src/codewhispererChat/tools/tool_index.json @@ -0,0 +1,76 @@ +{ + "fsRead": { + "name": "fsRead", + "description": "A tool for reading files (e.g. `cat -n`), or listing files/directories (e.g. `ls -la` or `find . -maxdepth 2). The behavior of this tool is determined by the `path` parameter pointing to a file or directory.\n* If `path` is a file, this tool returns the result of running `cat -n`, and the optional `readRange` determines what range of lines will be read from the specified file.\n* If `path` is a directory, this tool returns the listed files and directories of the specified path, as if running `ls -la`. If the `readRange` parameter is provided, the tool acts like the `find . -maxdepth `, where `readRange` is the number of subdirectories deep to search, e.g. [2] will run `find . -maxdepth 2`.", + "inputSchema": { + "type": "object", + "properties": { + "path": { + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", + "type": "string" + }, + "readRange": { + "description": "Optional parameter when reading either files or directories.\n* When `path` is a file, if none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[startLine, -1]` shows all lines from `startLine` to the end of the file.\n* When `path` is a directory, if none is given, the results of `ls -l` are given. If provided, the current directory and indicated number of subdirectories will be shown, e.g. [2] will show the current directory and directories two levels deep.", + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "required": ["path"] + } + }, + "fsWrite": { + "name": "fsWrite", + "description": "A tool for creating and editing files\n * The `create` command will override the file at `path` if it already exists as a file, and otherwise create a new file\n * The `append` command will add content to the end of an existing file, automatically adding a newline if the file doesn't end with one. The file must exist.\n Notes for using the `strReplace` command:\n * The `oldStr` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n * If the `oldStr` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `oldStr` to make it unique\n * The `newStr` parameter should contain the edited lines that should replace the `oldStr`. The `insert` command will insert `newStr` after `insertLine` and place it on its own line.", + "inputSchema": { + "type": "object", + "properties": { + "command": { + "type": "string", + "enum": ["create", "strReplace", "insert", "append"], + "description": "The commands to run. Allowed options are: `create`, `strReplace`, `insert`, `append`." + }, + "fileText": { + "description": "Required parameter of `create` command, with the content of the file to be created.", + "type": "string" + }, + "insertLine": { + "description": "Required parameter of `insert` command. The `newStr` will be inserted AFTER the line `insertLine` of `path`.", + "type": "integer" + }, + "newStr": { + "description": "Required parameter of `strReplace` command containing the new string. Required parameter of `insert` command containing the string to insert. Required parameter of `append` command containing the content to append to the file.", + "type": "string" + }, + "oldStr": { + "description": "Required parameter of `strReplace` command containing the string in `path` to replace.", + "type": "string" + }, + "path": { + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", + "type": "string" + } + }, + "required": ["command", "path"] + } + }, + "executeBash": { + "name": "executeBash", + "description": "Execute the specified bash command.", + "inputSchema": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Bash command to execute" + }, + "cwd": { + "type": "string", + "description": "Parameter to set the current working directory for the bash command." + } + }, + "required": ["command", "cwd"] + } + } +} diff --git a/packages/core/src/codewhispererChat/view/connector/connector.ts b/packages/core/src/codewhispererChat/view/connector/connector.ts index 0b2b29498c4..2c1902f964c 100644 --- a/packages/core/src/codewhispererChat/view/connector/connector.ts +++ b/packages/core/src/codewhispererChat/view/connector/connector.ts @@ -7,7 +7,17 @@ import { Timestamp } from 'aws-sdk/clients/apigateway' import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { EditorContextCommandType } from '../../commands/registerCommands' import { AuthFollowUpType } from '../../../amazonq/auth/model' -import { ChatItemButton, ChatItemFormItem, MynahUIDataModel, QuickActionCommand } from '@aws/mynah-ui' +import { + ChatItemButton, + ChatItemContent, + ChatItemFormItem, + CodeBlockActions, + MynahIcons, + MynahIconsType, + MynahUIDataModel, + QuickActionCommand, + Status, +} from '@aws/mynah-ui' import { DocumentReference } from '../../controllers/chat/model' class UiMessage { @@ -208,6 +218,23 @@ export interface ChatMessageProps { readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined readonly contextList: DocumentReference[] | undefined + readonly title?: string + readonly buttons?: ChatItemButton[] + readonly fileList?: ChatItemContent['fileList'] + readonly canBeVoted?: boolean + readonly codeBlockActions?: CodeBlockActions | null + readonly header?: + | (ChatItemContent & { + icon?: MynahIcons | MynahIconsType + status?: { + status?: Status + icon?: MynahIcons | MynahIconsType + text?: string + } + }) + | null + readonly fullWidth?: boolean + readonly padding?: boolean } export class ChatMessage extends UiMessage { @@ -223,7 +250,24 @@ export class ChatMessage extends UiMessage { readonly userIntent: string | undefined readonly codeBlockLanguage: string | undefined readonly contextList: DocumentReference[] | undefined + readonly title?: string + readonly buttons?: ChatItemButton[] + readonly fileList?: ChatItemContent['fileList'] + readonly canBeVoted?: boolean = false override type = 'chatMessage' + readonly codeBlockActions?: CodeBlockActions | null + readonly header?: + | (ChatItemContent & { + icon?: MynahIcons | MynahIconsType + status?: { + status?: Status + icon?: MynahIcons | MynahIconsType + text?: string + } + }) + | null + readonly fullWidth?: boolean + readonly padding?: boolean constructor(props: ChatMessageProps, tabID: string) { super(tabID) @@ -238,6 +282,14 @@ export class ChatMessage extends UiMessage { this.userIntent = props.userIntent this.codeBlockLanguage = props.codeBlockLanguage this.contextList = props.contextList + this.title = props.title + this.buttons = props.buttons + this.fileList = props.fileList + this.canBeVoted = props.canBeVoted + this.codeBlockActions = props.codeBlockActions + this.header = props.header + this.fullWidth = props.fullWidth + this.padding = props.padding } } @@ -318,4 +370,8 @@ export class AppToWebViewMessageDispatcher { public sendShowCustomFormMessage(message: ShowCustomFormMessage) { this.appsToWebViewMessagePublisher.publish(message) } + + public sendCustomFormActionMessage(message: CustomFormActionMessage) { + this.appsToWebViewMessagePublisher.publish(message) + } } diff --git a/packages/core/src/codewhispererChat/view/messages/messageListener.ts b/packages/core/src/codewhispererChat/view/messages/messageListener.ts index bb2871957c8..5937f269866 100644 --- a/packages/core/src/codewhispererChat/view/messages/messageListener.ts +++ b/packages/core/src/codewhispererChat/view/messages/messageListener.ts @@ -38,6 +38,7 @@ export class UIMessageListener { case 'clear': case 'transform': case 'chat-prompt': + // listen here this.processChatMessage(msg) break case 'new-tab-was-created': @@ -188,9 +189,8 @@ export class UIMessageListener { }) } - private processInsertCodeAtCursorPosition(msg: any) { - this.referenceLogController.addReferenceLog(msg.codeReference, (msg.code as string) ?? '') - this.chatControllerMessagePublishers.processInsertCodeAtCursorPosition.publish({ + private createCommonMessagePayload(msg: any) { + return { command: msg.command, tabID: msg.tabID, messageId: msg.messageId, @@ -202,7 +202,16 @@ export class UIMessageListener { codeBlockIndex: msg.codeBlockIndex, totalCodeBlocks: msg.totalCodeBlocks, codeBlockLanguage: msg.codeBlockLanguage, - }) + } + } + private processInsertCodeAtCursorPosition(msg: any) { + this.referenceLogController.addReferenceLog(msg.codeReference, (msg.code as string) ?? '') + this.chatControllerMessagePublishers.processInsertCodeAtCursorPosition.publish( + this.createCommonMessagePayload(msg) + ) + } + private processCodeWasCopiedToClipboard(msg: any) { + this.chatControllerMessagePublishers.processCopyCodeToClipboard.publish(this.createCommonMessagePayload(msg)) } private processAcceptDiff(msg: any) { @@ -221,22 +230,6 @@ export class UIMessageListener { }) } - private processCodeWasCopiedToClipboard(msg: any) { - this.chatControllerMessagePublishers.processCopyCodeToClipboard.publish({ - command: msg.command, - tabID: msg.tabID, - messageId: msg.messageId, - userIntent: msg.userIntent, - code: msg.code, - insertionTargetType: msg.insertionTargetType, - codeReference: msg.codeReference, - eventId: msg.eventId, - codeBlockIndex: msg.codeBlockIndex, - totalCodeBlocks: msg.totalCodeBlocks, - codeBlockLanguage: msg.codeBlockLanguage, - }) - } - private processTabWasRemoved(msg: any) { this.chatControllerMessagePublishers.processTabClosedMessage.publish({ tabID: msg.tabID, @@ -257,6 +250,7 @@ export class UIMessageListener { }) } + // na yue create someting similar -> web view to mynah private processChatMessage(msg: any) { this.chatControllerMessagePublishers.processPromptChatMessage.publish({ message: msg.chatMessage, diff --git a/packages/core/src/shared/logger/logger.ts b/packages/core/src/shared/logger/logger.ts index 3338602685a..32680cb57b6 100644 --- a/packages/core/src/shared/logger/logger.ts +++ b/packages/core/src/shared/logger/logger.ts @@ -16,6 +16,10 @@ export type LogTopic = | 'amazonqLsp' | 'chat' | 'stepfunctions' + | 'fsRead' + | 'fsWrite' + | 'executeBash' + | 'chatStream' | 'unknown' class ErrorLog { diff --git a/packages/core/src/shared/settings-amazonq.gen.ts b/packages/core/src/shared/settings-amazonq.gen.ts index 9447f43d12b..25d99235c52 100644 --- a/packages/core/src/shared/settings-amazonq.gen.ts +++ b/packages/core/src/shared/settings-amazonq.gen.ts @@ -18,9 +18,7 @@ export const amazonqSettings = { "amazonQWelcomePage": {}, "amazonQSessionConfigurationMessage": {}, "minIdeVersion": {}, - "ssoCacheError": {}, - "AmazonQLspManifestMessage": {}, - "AmazonQ-WorkspaceLspManifestMessage":{} + "ssoCacheError": {} }, "amazonQ.showCodeWithReferences": {}, "amazonQ.allowFeatureDevelopmentToRunCodeAndTests": {}, diff --git a/packages/core/src/shared/utilities/workspaceUtils.ts b/packages/core/src/shared/utilities/workspaceUtils.ts index 12cce75b3ff..e97297ad265 100644 --- a/packages/core/src/shared/utilities/workspaceUtils.ts +++ b/packages/core/src/shared/utilities/workspaceUtils.ts @@ -671,3 +671,80 @@ export async function findStringInDirectory(searchStr: string, dirPath: string) }) return spawnResult } + +/** + * Returns a one-character tag for a directory ('d'), symlink ('l'), or file ('-'). + */ +export function formatListing(name: string, fileType: vscode.FileType, fullPath: string): string { + let typeChar = '-' + if (fileType === vscode.FileType.Directory) { + typeChar = 'd' + } else if (fileType === vscode.FileType.SymbolicLink) { + typeChar = 'l' + } + return `${typeChar} ${fullPath}` +} + +/** + * Recursively lists directories using a BFS approach, returning lines like: + * d /absolute/path/to/folder + * - /absolute/path/to/file.txt + * + * You can either pass a custom callback or rely on the default `formatListing`. + * + * @param dirUri The folder to begin traversing + * @param maxDepth Maximum depth to descend (0 => just this folder) + * @param customFormatCallback Optional. If given, it will override the default line-formatting + */ +export async function readDirectoryRecursively( + dirUri: vscode.Uri, + maxDepth: number, + customFormatCallback?: (name: string, fileType: vscode.FileType, fullPath: string) => string +): Promise { + const logger = getLogger() + logger.info(`Reading directory: ${dirUri.fsPath} to max depth: ${maxDepth}`) + + const queue: Array<{ uri: vscode.Uri; depth: number }> = [{ uri: dirUri, depth: 0 }] + const results: string[] = [] + + const formatter = customFormatCallback ?? formatListing + + while (queue.length > 0) { + const { uri, depth } = queue.shift()! + if (depth > maxDepth) { + logger.info(`Skipping directory: ${uri.fsPath} (depth ${depth} > max ${maxDepth})`) + continue + } + + let entries: [string, vscode.FileType][] + try { + entries = await fs.readdir(uri) + } catch (err) { + logger.error(`Cannot read directory: ${uri.fsPath} (${err})`) + results.push(`Cannot read directory: ${uri.fsPath} (${err})`) + continue + } + + for (const [name, fileType] of entries) { + const childUri = vscode.Uri.joinPath(uri, name) + results.push(formatter(name, fileType, childUri.fsPath)) + + if (fileType === vscode.FileType.Directory && depth < maxDepth) { + queue.push({ uri: childUri, depth: depth + 1 }) + } + } + } + + return results +} + +export function getWorkspacePaths() { + const workspaceFolders = vscode.workspace.workspaceFolders + return workspaceFolders?.map((folder) => folder.uri.fsPath) ?? [] +} + +export function getWorkspaceForFile(filepath: string) { + const fileUri = vscode.Uri.file(filepath) + const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri) + return workspaceFolder?.uri.fsPath +} diff --git a/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts b/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts new file mode 100644 index 00000000000..68ec48d8851 --- /dev/null +++ b/packages/core/src/test/codewhispererChat/tools/executeBash.test.ts @@ -0,0 +1,112 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { strict as assert } from 'assert' +import sinon from 'sinon' +import { ExecuteBash } from '../../../codewhispererChat/tools/executeBash' +import { ChildProcess } from '../../../shared/utilities/processUtils' + +describe('ExecuteBash Tool', () => { + let runStub: sinon.SinonStub + let invokeStub: sinon.SinonStub + + beforeEach(() => { + runStub = sinon.stub(ChildProcess.prototype, 'run') + invokeStub = sinon.stub(ExecuteBash.prototype, 'invoke') + }) + + afterEach(() => { + sinon.restore() + }) + + it('pass validation for a safe command (read-only)', async () => { + runStub.resolves({ + exitCode: 0, + stdout: '/bin/ls', + stderr: '', + error: undefined, + signal: undefined, + }) + const execBash = new ExecuteBash({ command: 'ls' }) + await execBash.validate() + }) + + it('fail validation if the command is empty', async () => { + const execBash = new ExecuteBash({ command: ' ' }) + await assert.rejects( + execBash.validate(), + /Bash command cannot be empty/i, + 'Expected an error for empty command' + ) + }) + + it('set requiresAcceptance=true if the command has dangerous patterns', () => { + const execBash = new ExecuteBash({ command: 'ls && rm -rf /' }) + const needsAcceptance = execBash.requiresAcceptance() + assert.equal(needsAcceptance, true, 'Should require acceptance for dangerous pattern') + }) + + it('set requiresAcceptance=false if it is a read-only command', () => { + const execBash = new ExecuteBash({ command: 'cat file.txt' }) + const needsAcceptance = execBash.requiresAcceptance() + assert.equal(needsAcceptance, false, 'Read-only command should not require acceptance') + }) + + it('whichCommand cannot find the first arg', async () => { + runStub.resolves({ + exitCode: 1, + stdout: '', + stderr: '', + error: undefined, + signal: undefined, + }) + + const execBash = new ExecuteBash({ command: 'noSuchCmd' }) + await assert.rejects(execBash.validate(), /not found on PATH/i, 'Expected not found error from whichCommand') + }) + + it('whichCommand sees first arg on PATH', async () => { + runStub.resolves({ + exitCode: 0, + stdout: '/usr/bin/noSuchCmd\n', + stderr: '', + error: undefined, + signal: undefined, + }) + + const execBash = new ExecuteBash({ command: 'noSuchCmd' }) + await execBash.validate() + }) + + it('stub invoke() call', async () => { + invokeStub.resolves({ + output: { + kind: 'json', + content: { + exitStatus: '0', + stdout: 'mocked stdout lines', + stderr: '', + }, + }, + }) + + const execBash = new ExecuteBash({ command: 'ls' }) + + const dummyWritable = { write: () => {} } as any + const result = await execBash.invoke(dummyWritable) + + assert.strictEqual(result.output.kind, 'json') + const out = result.output.content as unknown as { + exitStatus: string + stdout: string + stderr: string + } + assert.strictEqual(out.exitStatus, '0') + assert.strictEqual(out.stdout, 'mocked stdout lines') + assert.strictEqual(out.stderr, '') + + assert.strictEqual(invokeStub.callCount, 1) + }) +}) diff --git a/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts new file mode 100644 index 00000000000..befe5ca624b --- /dev/null +++ b/packages/core/src/test/codewhispererChat/tools/fsRead.test.ts @@ -0,0 +1,122 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert' +import { FsRead } from '../../../codewhispererChat/tools/fsRead' +import { TestFolder } from '../../testUtil' +import path from 'path' + +describe('FsRead Tool', () => { + let testFolder: TestFolder + + before(async () => { + testFolder = await TestFolder.create() + }) + + it('throws if path is empty', async () => { + const fsRead = new FsRead({ path: '' }) + await assert.rejects(fsRead.validate(), /Path cannot be empty/i, 'Expected an error about empty path') + }) + + it('reads entire file', async () => { + const fileContent = 'Line 1\nLine 2\nLine 3' + const filePath = await testFolder.write('fullFile.txt', fileContent) + + const fsRead = new FsRead({ path: filePath }) + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + + assert.strictEqual(result.output.kind, 'text', 'Output kind should be "text"') + assert.strictEqual(result.output.content, fileContent, 'File content should match exactly') + }) + + it('reads partial lines of a file', async () => { + const fileContent = 'A\nB\nC\nD\nE\nF' + const filePath = await testFolder.write('partialFile.txt', fileContent) + + const fsRead = new FsRead({ path: filePath, readRange: [2, 4] }) + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + + assert.strictEqual(result.output.kind, 'text') + assert.strictEqual(result.output.content, 'B\nC\nD') + }) + + it('lists directory contents up to depth = 1', async () => { + await testFolder.mkdir('subfolder') + await testFolder.write('fileA.txt', 'fileA content') + await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB') + + const fsRead = new FsRead({ path: testFolder.path, readRange: [1] }) + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + + const lines = result.output.content.split('\n') + const hasFileA = lines.some((line: string | string[]) => line.includes('- ') && line.includes('fileA.txt')) + const hasSubfolder = lines.some((line: string | string[]) => line.includes('d ') && line.includes('subfolder')) + + assert.ok(hasFileA, 'Should list fileA.txt in the directory output') + assert.ok(hasSubfolder, 'Should list the subfolder in the directory output') + }) + + it('throws error if path does not exist', async () => { + const missingPath = path.join(testFolder.path, 'no_such_file.txt') + const fsRead = new FsRead({ path: missingPath }) + + await assert.rejects( + fsRead.validate(), + /does not exist or cannot be accessed/i, + 'Expected an error indicating the path does not exist' + ) + }) + + it('throws error if content exceeds 30KB', async function () { + const bigContent = 'x'.repeat(35_000) + const bigFilePath = await testFolder.write('bigFile.txt', bigContent) + + const fsRead = new FsRead({ path: bigFilePath }) + await fsRead.validate() + + await assert.rejects( + fsRead.invoke(process.stdout), + /This tool only supports reading \d+ bytes at a time/i, + 'Expected a size-limit error' + ) + }) + + it('invalid line range', async () => { + const filePath = await testFolder.write('rangeTest.txt', '1\n2\n3') + const fsRead = new FsRead({ path: filePath, readRange: [3, 2] }) + + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + assert.strictEqual(result.output.kind, 'text') + assert.strictEqual(result.output.content, '') + }) + + it('expands ~ path', async () => { + const fsRead = new FsRead({ path: '~' }) + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + + assert.strictEqual(result.output.kind, 'text') + assert.ok(result.output.content.length > 0) + }) + + it('resolves relative path', async () => { + await testFolder.mkdir('relTest') + const filePath = path.join('relTest', 'relFile.txt') + const content = 'Hello from a relative file!' + await testFolder.write(filePath, content) + + const relativePath = path.relative(process.cwd(), path.join(testFolder.path, filePath)) + + const fsRead = new FsRead({ path: relativePath }) + await fsRead.validate() + const result = await fsRead.invoke(process.stdout) + + assert.strictEqual(result.output.kind, 'text') + assert.strictEqual(result.output.content, content) + }) +}) diff --git a/packages/core/src/test/codewhispererChat/tools/fsWrite.test.ts b/packages/core/src/test/codewhispererChat/tools/fsWrite.test.ts new file mode 100644 index 00000000000..932575c97d6 --- /dev/null +++ b/packages/core/src/test/codewhispererChat/tools/fsWrite.test.ts @@ -0,0 +1,417 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { + AppendParams, + CreateParams, + FsWrite, + InsertParams, + StrReplaceParams, +} from '../../../codewhispererChat/tools/fsWrite' +import { TestFolder } from '../../testUtil' +import path from 'path' +import assert from 'assert' +import { fs } from '../../../shared/fs/fs' +import { InvokeOutput, OutputKind } from '../../../codewhispererChat/tools/toolShared' + +describe('FsWrite Tool', function () { + let testFolder: TestFolder + const expectedOutput: InvokeOutput = { + output: { + kind: OutputKind.Text, + content: '', + }, + } + + describe('handleCreate', function () { + before(async function () { + testFolder = await TestFolder.create() + }) + + it('creates a new file with fileText content', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + const fileExists = await fs.existsFile(filePath) + assert.ok(!fileExists) + + const params: CreateParams = { + command: 'create', + fileText: 'Hello World', + path: filePath, + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Hello World') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('replaces existing file with fileText content', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + const fileExists = await fs.existsFile(filePath) + assert.ok(fileExists) + + const params: CreateParams = { + command: 'create', + fileText: 'Goodbye', + path: filePath, + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Goodbye') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('uses newStr when fileText is not provided', async function () { + const filePath = path.join(testFolder.path, 'file2.txt') + + const params: CreateParams = { + command: 'create', + newStr: 'Hello World', + path: filePath, + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Hello World') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('creates an empty file when no content is provided', async function () { + const filePath = path.join(testFolder.path, 'file3.txt') + + const params: CreateParams = { + command: 'create', + path: filePath, + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, '') + + assert.deepStrictEqual(output, expectedOutput) + }) + }) + + describe('handleStrReplace', async function () { + before(async function () { + testFolder = await TestFolder.create() + }) + + it('replaces a single occurrence of a string', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + await fs.writeFile(filePath, 'Hello World') + + const params: StrReplaceParams = { + command: 'strReplace', + path: filePath, + oldStr: 'Hello', + newStr: 'Goodbye', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Goodbye World') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('throws error when no matches are found', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + + const params: StrReplaceParams = { + command: 'strReplace', + path: filePath, + oldStr: 'Invalid', + newStr: 'Goodbye', + } + + const fsWrite = new FsWrite(params) + await assert.rejects(() => fsWrite.invoke(process.stdout), /No occurrences of "Invalid" were found/) + }) + + it('throws error when multiple matches are found', async function () { + const filePath = path.join(testFolder.path, 'file2.txt') + await fs.writeFile(filePath, 'Hello Hello World') + + const params: StrReplaceParams = { + command: 'strReplace', + path: filePath, + oldStr: 'Hello', + newStr: 'Goodbye', + } + + const fsWrite = new FsWrite(params) + await assert.rejects( + () => fsWrite.invoke(process.stdout), + /2 occurrences of oldStr were found when only 1 is expected/ + ) + }) + + it('handles regular expression special characters correctly', async function () { + const filePath = path.join(testFolder.path, 'file3.txt') + await fs.writeFile(filePath, 'Text with special chars: .*+?^${}()|[]\\') + + const params: StrReplaceParams = { + command: 'strReplace', + path: filePath, + oldStr: '.*+?^${}()|[]\\', + newStr: 'REPLACED', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Text with special chars: REPLACED') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('preserves whitespace and newlines during replacement', async function () { + const filePath = path.join(testFolder.path, 'file4.txt') + await fs.writeFile(filePath, 'Line 1\n Indented line\nLine 3') + + const params: StrReplaceParams = { + command: 'strReplace', + path: filePath, + oldStr: ' Indented line\n', + newStr: ' Double indented\n', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const content = await fs.readFileText(filePath) + assert.strictEqual(content, 'Line 1\n Double indented\nLine 3') + + assert.deepStrictEqual(output, expectedOutput) + }) + }) + + describe('handleInsert', function () { + before(async function () { + testFolder = await TestFolder.create() + }) + + it('inserts text after the specified line number', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + await fs.writeFile(filePath, 'Line 1\nLine 2\nLine 3\nLine 4') + + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: 2, + newStr: 'New Line', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1\nLine 2\nNew Line\nLine 3\nLine 4') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('inserts text at the beginning when line number is 0', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: 0, + newStr: 'New First Line', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'New First Line\nLine 1\nLine 2\nNew Line\nLine 3\nLine 4') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('inserts text at the end when line number exceeds file length', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: 10, + newStr: 'New Last Line', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'New First Line\nLine 1\nLine 2\nNew Line\nLine 3\nLine 4\nNew Last Line') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('handles insertion into an empty file', async function () { + const filePath = path.join(testFolder.path, 'file2.txt') + await fs.writeFile(filePath, '') + + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: 0, + newStr: 'First Line', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'First Line\n') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('handles negative line numbers by inserting at the beginning', async function () { + const filePath = path.join(testFolder.path, 'file2.txt') + + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: -1, + newStr: 'New First Line', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'New First Line\nFirst Line\n') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('throws error when file does not exist', async function () { + const filePath = path.join(testFolder.path, 'nonexistent.txt') + + const params: InsertParams = { + command: 'insert', + path: filePath, + insertLine: 1, + newStr: 'New Line', + } + + const fsWrite = new FsWrite(params) + await assert.rejects(() => fsWrite.invoke(process.stdout), /no such file or directory/) + }) + }) + + describe('handleAppend', function () { + before(async function () { + testFolder = await TestFolder.create() + }) + + it('appends text to the end of a file', async function () { + const filePath = path.join(testFolder.path, 'file1.txt') + await fs.writeFile(filePath, 'Line 1\nLine 2\nLine 3\n') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: 'Line 4', + } + + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3\nLine 4') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('adds a newline before appending if file does not end with one', async function () { + const filePath = path.join(testFolder.path, 'file2.txt') + await fs.writeFile(filePath, 'Line 1\nLine 2\nLine 3') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: 'Line 4', + } + + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3\nLine 4') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('appends to an empty file', async function () { + const filePath = path.join(testFolder.path, 'file3.txt') + await fs.writeFile(filePath, '') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: 'Line 1', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('appends multiple lines correctly', async function () { + const filePath = path.join(testFolder.path, 'file3.txt') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: 'Line 2\nLine 3', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('handles appending empty string', async function () { + const filePath = path.join(testFolder.path, 'file3.txt') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: '', + } + const fsWrite = new FsWrite(params) + const output = await fsWrite.invoke(process.stdout) + + const newContent = await fs.readFileText(filePath) + assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3\n') + + assert.deepStrictEqual(output, expectedOutput) + }) + + it('throws error when file does not exist', async function () { + const filePath = path.join(testFolder.path, 'nonexistent.txt') + + const params: AppendParams = { + command: 'append', + path: filePath, + newStr: 'New Line', + } + + const fsWrite = new FsWrite(params) + await assert.rejects(() => fsWrite.invoke(process.stdout), /no such file or directory/) + }) + }) +}) diff --git a/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts new file mode 100644 index 00000000000..4395258cbdf --- /dev/null +++ b/packages/core/src/test/codewhispererChat/tools/toolShared.test.ts @@ -0,0 +1,285 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import { Writable } from 'stream' +import { sanitizePath, OutputKind, InvokeOutput } from '../../../codewhispererChat/tools/toolShared' +import { ToolUtils, Tool, ToolType } from '../../../codewhispererChat/tools/toolUtils' +import { FsRead } from '../../../codewhispererChat/tools/fsRead' +import { FsWrite } from '../../../codewhispererChat/tools/fsWrite' +import { ExecuteBash } from '../../../codewhispererChat/tools/executeBash' +import { ToolUse } from '@amzn/codewhisperer-streaming' +import path from 'path' +import fs from '../../../shared/fs/fs' + +describe('ToolUtils', function () { + let sandbox: sinon.SinonSandbox + let mockFsRead: sinon.SinonStubbedInstance + let mockFsWrite: sinon.SinonStubbedInstance + let mockExecuteBash: sinon.SinonStubbedInstance + let mockWritable: sinon.SinonStubbedInstance + + beforeEach(function () { + sandbox = sinon.createSandbox() + mockFsRead = sandbox.createStubInstance(FsRead) + mockFsWrite = sandbox.createStubInstance(FsWrite) + mockExecuteBash = sandbox.createStubInstance(ExecuteBash) + mockWritable = { + write: sandbox.stub(), + } as unknown as sinon.SinonStubbedInstance + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('displayName', function () { + it('returns correct display name for FsRead tool', function () { + const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } + assert.strictEqual(ToolUtils.displayName(tool), 'Read from filesystem') + }) + + it('returns correct display name for FsWrite tool', function () { + const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } + assert.strictEqual(ToolUtils.displayName(tool), 'Write to filesystem') + }) + + it('returns correct display name for ExecuteBash tool', function () { + const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } + assert.strictEqual(ToolUtils.displayName(tool), 'Execute shell command') + }) + }) + + describe('requiresAcceptance', function () { + it('returns false for FsRead tool', function () { + const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } + assert.strictEqual(ToolUtils.requiresAcceptance(tool), false) + }) + + it('returns true for FsWrite tool', function () { + const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } + assert.strictEqual(ToolUtils.requiresAcceptance(tool), true) + }) + + it('delegates to the tool for ExecuteBash', function () { + mockExecuteBash.requiresAcceptance.returns(true) + const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } + assert.strictEqual(ToolUtils.requiresAcceptance(tool), true) + + mockExecuteBash.requiresAcceptance.returns(false) + assert.strictEqual(ToolUtils.requiresAcceptance(tool), false) + + assert(mockExecuteBash.requiresAcceptance.calledTwice) + }) + }) + + describe('invoke', function () { + it('delegates to FsRead tool invoke method', async function () { + const expectedOutput: InvokeOutput = { + output: { + kind: OutputKind.Text, + content: 'test content', + }, + } + mockFsRead.invoke.resolves(expectedOutput) + + const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } + const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable) + + assert.deepStrictEqual(result, expectedOutput) + assert(mockFsRead.invoke.calledOnceWith(mockWritable)) + }) + + it('delegates to FsWrite tool invoke method', async function () { + const expectedOutput: InvokeOutput = { + output: { + kind: OutputKind.Text, + content: 'write success', + }, + } + mockFsWrite.invoke.resolves(expectedOutput) + + const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } + const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable) + + assert.deepStrictEqual(result, expectedOutput) + assert(mockFsWrite.invoke.calledOnceWith(mockWritable)) + }) + + it('delegates to ExecuteBash tool invoke method', async function () { + const expectedOutput: InvokeOutput = { + output: { + kind: OutputKind.Json, + content: '{"stdout":"command output","exit_status":"0"}', + }, + } + mockExecuteBash.invoke.resolves(expectedOutput) + + const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } + const result = await ToolUtils.invoke(tool, mockWritable as unknown as Writable) + + assert.deepStrictEqual(result, expectedOutput) + assert(mockExecuteBash.invoke.calledOnceWith(mockWritable)) + }) + }) + + describe('queueDescription', function () { + it('delegates to FsRead tool queueDescription method', function () { + const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } + ToolUtils.queueDescription(tool, mockWritable as unknown as Writable) + + assert(mockFsRead.queueDescription.calledOnceWith(mockWritable)) + }) + + it('delegates to FsWrite tool queueDescription method', function () { + const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } + ToolUtils.queueDescription(tool, mockWritable as unknown as Writable) + + assert(mockFsWrite.queueDescription.calledOnceWith(mockWritable)) + }) + + it('delegates to ExecuteBash tool queueDescription method', function () { + const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } + ToolUtils.queueDescription(tool, mockWritable as unknown as Writable) + + assert(mockExecuteBash.queueDescription.calledOnceWith(mockWritable)) + }) + }) + + describe('validate', function () { + it('delegates to FsRead tool validate method', async function () { + mockFsRead.validate.resolves() + + const tool: Tool = { type: ToolType.FsRead, tool: mockFsRead as unknown as FsRead } + await ToolUtils.validate(tool) + + assert(mockFsRead.validate.calledOnce) + }) + + it('delegates to FsWrite tool validate method', async function () { + mockFsWrite.validate.resolves() + + const tool: Tool = { type: ToolType.FsWrite, tool: mockFsWrite as unknown as FsWrite } + await ToolUtils.validate(tool) + + assert(mockFsWrite.validate.calledOnce) + }) + + it('delegates to ExecuteBash tool validate method', async function () { + mockExecuteBash.validate.resolves() + + const tool: Tool = { type: ToolType.ExecuteBash, tool: mockExecuteBash as unknown as ExecuteBash } + await ToolUtils.validate(tool) + + assert(mockExecuteBash.validate.calledOnce) + }) + }) + + describe('tryFromToolUse', function () { + it('creates FsRead tool from ToolUse', function () { + const toolUse: ToolUse = { + toolUseId: 'test-id', + name: ToolType.FsRead, + input: { path: '/test/path', mode: 'Line' }, + } + + const result = ToolUtils.tryFromToolUse(toolUse) + + assert.strictEqual('type' in result, true) + if ('type' in result) { + assert.strictEqual(result.type, ToolType.FsRead) + assert(result.tool instanceof FsRead) + } + }) + + it('creates FsWrite tool from ToolUse', function () { + const toolUse: ToolUse = { + toolUseId: 'test-id', + name: ToolType.FsWrite, + input: { command: 'create', path: '/test/path', file_text: 'content' }, + } + + const result = ToolUtils.tryFromToolUse(toolUse) + + assert.strictEqual('type' in result, true) + if ('type' in result) { + assert.strictEqual(result.type, ToolType.FsWrite) + assert(result.tool instanceof FsWrite) + } + }) + + it('creates ExecuteBash tool from ToolUse', function () { + const toolUse: ToolUse = { + toolUseId: 'test-id', + name: ToolType.ExecuteBash, + input: { command: 'ls -la' }, + } + + const result = ToolUtils.tryFromToolUse(toolUse) + + assert.strictEqual('type' in result, true) + if ('type' in result) { + assert.strictEqual(result.type, ToolType.ExecuteBash) + assert(result.tool instanceof ExecuteBash) + } + }) + + it('returns error result for unsupported tool', function () { + const toolUse: ToolUse = { + toolUseId: 'test-id', + name: 'UnsupportedTool' as any, + input: {}, + } + + const result = ToolUtils.tryFromToolUse(toolUse) + + assert.strictEqual('toolUseId' in result, true) + if ('toolUseId' in result) { + assert.strictEqual(result.toolUseId, 'test-id') + assert.strictEqual( + result.content?.[0].text ?? '', + 'The tool, "UnsupportedTool" is not supported by the client' + ) + } + }) + }) +}) + +describe('sanitizePath', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('trims whitespace from input path', function () { + const result = sanitizePath(' /test/path ') + assert.strictEqual(result, '/test/path') + }) + + it('expands tilde to user home directory', function () { + const homeDir = '/Users/testuser' + sandbox.stub(fs, 'getUserHomeDir').returns(homeDir) + + const result = sanitizePath('~/documents/file.txt') + assert.strictEqual(result, path.join(homeDir, 'documents/file.txt')) + }) + + it('converts relative paths to absolute paths', function () { + const result = sanitizePath('relative/path') + assert.strictEqual(result, path.resolve('relative/path')) + }) + + it('leaves absolute paths unchanged', function () { + const absolutePath = path.resolve('/absolute/path') + const result = sanitizePath(absolutePath) + assert.strictEqual(result, absolutePath) + }) +}) diff --git a/src.gen/@amzn/codewhisperer-streaming/README.md b/src.gen/@amzn/codewhisperer-streaming/README.md index 5f5df60669d..ea254be0911 100644 --- a/src.gen/@amzn/codewhisperer-streaming/README.md +++ b/src.gen/@amzn/codewhisperer-streaming/README.md @@ -227,13 +227,6 @@ CreateProfile
-CreateWorkspace - - -[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/CreateWorkspaceCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/CreateWorkspaceCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/CreateWorkspaceCommandOutput/) -
-
- DeleteCustomization @@ -304,13 +297,6 @@ ListTagsForResource
-ListWorkspaceMetadata - - -[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/ListWorkspaceMetadataCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/ListWorkspaceMetadataCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/ListWorkspaceMetadataCommandOutput/) -
-
- TagResource @@ -367,6 +353,13 @@ CreateUploadUrl
+CreateWorkspace + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/CreateWorkspaceCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/CreateWorkspaceCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/CreateWorkspaceCommandOutput/) +
+
+ DeleteTaskAssistConversation @@ -374,6 +367,13 @@ DeleteTaskAssistConversation
+DeleteWorkspace + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/DeleteWorkspaceCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/DeleteWorkspaceCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/DeleteWorkspaceCommandOutput/) +
+
+ GenerateCompletions @@ -451,6 +451,13 @@ ListFeatureEvaluations
+ListWorkspaceMetadata + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/ListWorkspaceMetadataCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/ListWorkspaceMetadataCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/ListWorkspaceMetadataCommandOutput/) +
+
+ ResumeTransformation @@ -570,6 +577,13 @@ DeleteAssignment
+DeleteConversation + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/DeleteConversationCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/DeleteConversationCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/DeleteConversationCommandOutput/) +
+
+ DeleteExtension @@ -759,6 +773,20 @@ UntagResource
+UpdateConversation + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/UpdateConversationCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/UpdateConversationCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/UpdateConversationCommandOutput/) +
+
+ +UpdatePlugin + + +[Command API Reference](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/codewhispererstreaming/command/UpdatePluginCommand/) / [Input](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/UpdatePluginCommandInput/) / [Output](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-codewhispererstreaming/Interface/UpdatePluginCommandOutput/) +
+
+ UpdateTroubleshootingCommandResult diff --git a/src.gen/@amzn/codewhisperer-streaming/src/commands/ExportResultArchiveCommand.ts b/src.gen/@amzn/codewhisperer-streaming/src/commands/ExportResultArchiveCommand.ts index 8b8a5016715..c2376564160 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/commands/ExportResultArchiveCommand.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/commands/ExportResultArchiveCommand.ts @@ -56,6 +56,7 @@ export interface ExportResultArchiveCommandOutput extends ExportResultArchiveRes * testGenerationJobId: "STRING_VALUE", * }, * }, + * profileArn: "STRING_VALUE", * }; * const command = new ExportResultArchiveCommand(input); * const response = await client.send(command); diff --git a/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateAssistantResponseCommand.ts b/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateAssistantResponseCommand.ts index 4bfdeb6aa74..f952da3895f 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateAssistantResponseCommand.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateAssistantResponseCommand.ts @@ -100,6 +100,9 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ // WorkspaceFolderList + * "STRING_VALUE", + * ], * }, * shellState: { // ShellState * shellName: "STRING_VALUE", // required @@ -212,6 +215,15 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ // ImageBlocks + * { // ImageBlock + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: { // ImageSource Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { // AssistantResponseMessage * messageId: "STRING_VALUE", @@ -238,6 +250,13 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ // ToolUses + * { // ToolUse + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * ], @@ -271,6 +290,9 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ + * "STRING_VALUE", + * ], * }, * shellState: { * shellName: "STRING_VALUE", // required @@ -364,6 +386,15 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ + * { + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: {// Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { * messageId: "STRING_VALUE", @@ -390,6 +421,13 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ + * { + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * chatTriggerType: "MANUAL" || "DIAGNOSTIC" || "INLINE_CHAT", // required @@ -651,6 +689,17 @@ export interface GenerateAssistantResponseCommandOutput extends GenerateAssistan * // input: "STRING_VALUE", * // stop: true || false, * // }, + * // citationEvent: { // CitationEvent + * // target: { // CitationTarget Union: only one key present + * // location: Number("int"), + * // range: { + * // start: Number("int"), + * // end: Number("int"), + * // }, + * // }, + * // citationText: "STRING_VALUE", + * // citationLink: "STRING_VALUE", // required + * // }, * // invalidStateEvent: { // InvalidStateEvent * // reason: "INVALID_TASK_ASSIST_PLAN", // required * // message: "STRING_VALUE", // required diff --git a/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateTaskAssistPlanCommand.ts b/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateTaskAssistPlanCommand.ts index ef188a0600a..edd5c5aa6ef 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateTaskAssistPlanCommand.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/commands/GenerateTaskAssistPlanCommand.ts @@ -100,6 +100,9 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ // WorkspaceFolderList + * "STRING_VALUE", + * ], * }, * shellState: { // ShellState * shellName: "STRING_VALUE", // required @@ -212,6 +215,15 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ // ImageBlocks + * { // ImageBlock + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: { // ImageSource Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { // AssistantResponseMessage * messageId: "STRING_VALUE", @@ -238,6 +250,13 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ // ToolUses + * { // ToolUse + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * ], @@ -271,6 +290,9 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ + * "STRING_VALUE", + * ], * }, * shellState: { * shellName: "STRING_VALUE", // required @@ -364,6 +386,15 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ + * { + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: {// Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { * messageId: "STRING_VALUE", @@ -390,6 +421,13 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ + * { + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * chatTriggerType: "MANUAL" || "DIAGNOSTIC" || "INLINE_CHAT", // required @@ -400,6 +438,7 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * programmingLanguage: "", // required * contextTruncationScheme: "ANALYSIS" || "GUMBY", * }, + * profileArn: "STRING_VALUE", * }; * const command = new GenerateTaskAssistPlanCommand(input); * const response = await client.send(command); @@ -654,6 +693,17 @@ export interface GenerateTaskAssistPlanCommandOutput extends GenerateTaskAssistP * // input: "STRING_VALUE", * // stop: true || false, * // }, + * // citationEvent: { // CitationEvent + * // target: { // CitationTarget Union: only one key present + * // location: Number("int"), + * // range: { + * // start: Number("int"), + * // end: Number("int"), + * // }, + * // }, + * // citationText: "STRING_VALUE", + * // citationLink: "STRING_VALUE", // required + * // }, * // invalidStateEvent: { // InvalidStateEvent * // reason: "INVALID_TASK_ASSIST_PLAN", // required * // message: "STRING_VALUE", // required diff --git a/src.gen/@amzn/codewhisperer-streaming/src/commands/SendMessageCommand.ts b/src.gen/@amzn/codewhisperer-streaming/src/commands/SendMessageCommand.ts index 8a83024b54d..d2e9ad6631a 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/commands/SendMessageCommand.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/commands/SendMessageCommand.ts @@ -101,6 +101,9 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ // WorkspaceFolderList + * "STRING_VALUE", + * ], * }, * shellState: { // ShellState * shellName: "STRING_VALUE", // required @@ -213,6 +216,15 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ // ImageBlocks + * { // ImageBlock + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: { // ImageSource Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { // AssistantResponseMessage * messageId: "STRING_VALUE", @@ -239,6 +251,13 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ // ToolUses + * { // ToolUse + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * ], @@ -272,6 +291,9 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * }, * ], * useRelevantDocuments: true || false, + * workspaceFolders: [ + * "STRING_VALUE", + * ], * }, * shellState: { * shellName: "STRING_VALUE", // required @@ -365,6 +387,15 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * ], * }, * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", + * origin: "STRING_VALUE", + * images: [ + * { + * format: "png" || "jpeg" || "gif" || "webp", // required + * source: {// Union: only one key present + * bytes: new Uint8Array(), // e.g. Buffer.from("") or new TextEncoder().encode("") + * }, + * }, + * ], * }, * assistantResponseMessage: { * messageId: "STRING_VALUE", @@ -391,6 +422,13 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * content: "STRING_VALUE", // required * userIntent: "SUGGEST_ALTERNATE_IMPLEMENTATION" || "APPLY_COMMON_BEST_PRACTICES" || "IMPROVE_CODE" || "SHOW_EXAMPLES" || "CITE_SOURCES" || "EXPLAIN_LINE_BY_LINE" || "EXPLAIN_CODE_SELECTION" || "GENERATE_CLOUDFORMATION_TEMPLATE" || "GENERATE_UNIT_TESTS" || "CODE_GENERATION", * }, + * toolUses: [ + * { + * toolUseId: "STRING_VALUE", // required + * name: "STRING_VALUE", // required + * input: "DOCUMENT_VALUE", // required + * }, + * ], * }, * }, * chatTriggerType: "MANUAL" || "DIAGNOSTIC" || "INLINE_CHAT", // required @@ -653,6 +691,17 @@ export interface SendMessageCommandOutput extends SendMessageResponse, __Metadat * // input: "STRING_VALUE", * // stop: true || false, * // }, + * // citationEvent: { // CitationEvent + * // target: { // CitationTarget Union: only one key present + * // location: Number("int"), + * // range: { + * // start: Number("int"), + * // end: Number("int"), + * // }, + * // }, + * // citationText: "STRING_VALUE", + * // citationLink: "STRING_VALUE", // required + * // }, * // invalidStateEvent: { // InvalidStateEvent * // reason: "INVALID_TASK_ASSIST_PLAN", // required * // message: "STRING_VALUE", // required diff --git a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts index 07e2794da2d..33559867823 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/models/models_0.ts @@ -361,7 +361,7 @@ export const FollowupPromptFilterSensitiveLog = (obj: FollowupPrompt): any => ({ }) /** - * Represents span in a text + * Represents span in a text. * @public */ export interface Span { @@ -405,19 +405,19 @@ export interface Reference { */ export interface SupplementaryWebLink { /** - * URL of the web reference link + * URL of the web reference link. * @public */ url: string | undefined; /** - * Title of the web reference link + * Title of the web reference link. * @public */ title: string | undefined; /** - * Relevant text snippet from the link + * Relevant text snippet from the link. * @public */ snippet?: string | undefined; @@ -439,6 +439,43 @@ export const SupplementaryWebLinkFilterSensitiveLog = (obj: SupplementaryWebLink }), }) +/** + * Contains information about a tool that the model is requesting be run. The model uses the result from the tool to generate a response. + * @public + */ +export interface ToolUse { + /** + * The ID for the tool request. + * @public + */ + toolUseId: string | undefined; + + /** + * The name for the tool. + * @public + */ + name: string | undefined; + + /** + * The input to pass to the tool. + * @public + */ + input: __DocumentType | undefined; +} + +/** + * @internal + */ +export const ToolUseFilterSensitiveLog = (obj: ToolUse): any => ({ + ...obj, + ...(obj.name && { name: + SENSITIVE_STRING + }), + ...(obj.input && { input: + SENSITIVE_STRING + }), +}) + /** * Markdown text message. * @public @@ -473,6 +510,12 @@ export interface AssistantResponseMessage { * @public */ followupPrompt?: FollowupPrompt | undefined; + + /** + * ToolUse Request + * @public + */ + toolUses?: (ToolUse)[] | undefined; } /** @@ -492,6 +535,12 @@ export const AssistantResponseMessageFilterSensitiveLog = (obj: AssistantRespons ...(obj.followupPrompt && { followupPrompt: FollowupPromptFilterSensitiveLog(obj.followupPrompt) }), + ...(obj.toolUses && { toolUses: + obj.toolUses.map( + item => + ToolUseFilterSensitiveLog(item) + ) + }), }) /** @@ -614,6 +663,165 @@ export const BinaryPayloadEventFilterSensitiveLog = (obj: BinaryPayloadEvent): a }), }) +/** + * @public + * @enum + */ +export const ImageFormat = { + GIF: "gif", + JPEG: "jpeg", + PNG: "png", + WEBP: "webp", +} as const +/** + * @public + */ +export type ImageFormat = typeof ImageFormat[keyof typeof ImageFormat] + +/** + * Image bytes limited to ~10MB considering overhead of base64 encoding + * @public + */ +export type ImageSource = + | ImageSource.BytesMember + | ImageSource.$UnknownMember + +/** + * @public + */ +export namespace ImageSource { + + export interface BytesMember { + bytes: Uint8Array; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + bytes?: never; + $unknown: [string, any]; + } + + export interface Visitor { + bytes: (value: Uint8Array) => T; + _: (name: string, value: any) => T; + } + + export const visit = ( + value: ImageSource, + visitor: Visitor + ): T => { + if (value.bytes !== undefined) return visitor.bytes(value.bytes); + return visitor._(value.$unknown[0], value.$unknown[1]); + } + +} +/** + * @internal + */ +export const ImageSourceFilterSensitiveLog = (obj: ImageSource): any => { + if (obj.bytes !== undefined) return {bytes: + obj.bytes + }; + if (obj.$unknown !== undefined) return {[obj.$unknown[0]]: 'UNKNOWN'}; +} + +/** + * Represents the image source itself and the format of the image. + * @public + */ +export interface ImageBlock { + format: ImageFormat | undefined; + /** + * Image bytes limited to ~10MB considering overhead of base64 encoding + * @public + */ + source: ImageSource | undefined; +} + +/** + * @internal + */ +export const ImageBlockFilterSensitiveLog = (obj: ImageBlock): any => ({ + ...obj, + ...(obj.source && { source: + SENSITIVE_STRING + }), +}) + +/** + * @public + * @enum + */ +export const Origin = { + /** + * Any AI Editor. + */ + AI_EDITOR: "AI_EDITOR", + /** + * AWS Chatbot + */ + CHATBOT: "CHATBOT", + /** + * Any CLI caller. + */ + CLI: "CLI", + /** + * AWS Management Console (https://.console.aws.amazon.com) + */ + CONSOLE: "CONSOLE", + /** + * AWS Documentation Website (https://docs.aws.amazon.com) + */ + DOCUMENTATION: "DOCUMENTATION", + /** + * Any caller from GitLab Q integration. + */ + GITLAB: "GITLAB", + /** + * Any IDE caller. + */ + IDE: "IDE", + /** + * AWS Marketing Website (https://aws.amazon.com) + */ + MARKETING: "MARKETING", + /** + * MD. + */ + MD: "MD", + /** + * AWS Mobile Application (ACMA) + */ + MOBILE: "MOBILE", + /** + * Amazon OpenSearch dashboard + */ + OPENSEARCH_DASHBOARD: "OPENSEARCH_DASHBOARD", + /** + * Amazon SageMaker's Rome Chat. + */ + SAGE_MAKER: "SAGE_MAKER", + /** + * Internal Service Traffic (Integ Tests, Canaries, etc.). This is the default when no Origin header present in request. + */ + SERVICE_INTERNAL: "SERVICE_INTERNAL", + /** + * Unified Search in AWS Management Console (https://.console.aws.amazon.com) + */ + UNIFIED_SEARCH: "UNIFIED_SEARCH", + /** + * Origin header is not set. + */ + UNKNOWN: "UNKNOWN", +} as const +/** + * @public + */ +export type Origin = typeof Origin[keyof typeof Origin] + /** * Information about the state of the AWS management console page from which the user is calling * @public @@ -1062,6 +1270,12 @@ export interface EditorState { * @public */ useRelevantDocuments?: boolean | undefined; + + /** + * Represents IDE provided list of workspace folders + * @public + */ + workspaceFolders?: (string)[] | undefined; } /** @@ -1081,6 +1295,9 @@ export const EditorStateFilterSensitiveLog = (obj: EditorState): any => ({ RelevantTextDocumentFilterSensitiveLog(item) ) }), + ...(obj.workspaceFolders && { workspaceFolders: + SENSITIVE_STRING + }), }) /** @@ -1563,13 +1780,13 @@ export interface UserInputMessageContext { userSettings?: UserSettings | undefined; /** - * List of additional contextual content entries that can be included with the message + * List of additional contextual content entries that can be included with the message. * @public */ additionalContext?: (AdditionalContentEntry)[] | undefined; /** - * ToolResults for the requested ToolUses + * ToolResults for the requested ToolUses. * @public */ toolResults?: (ToolResult)[] | undefined; @@ -1628,7 +1845,7 @@ export const UserInputMessageContextFilterSensitiveLog = (obj: UserInputMessageC }) /** - * Structure to represent a chat input message from User + * Structure to represent a chat input message from User. * @public */ export interface UserInputMessage { @@ -1639,16 +1856,28 @@ export interface UserInputMessage { content: string | undefined; /** - * Chat message context associated with the Chat Message + * Chat message context associated with the Chat Message. * @public */ userInputMessageContext?: UserInputMessageContext | undefined; /** - * User Intent + * User Intent. * @public */ userIntent?: UserIntent | undefined; + + /** + * User Input Origin. + * @public + */ + origin?: Origin | undefined; + + /** + * Images associated with the Chat Message. + * @public + */ + images?: (ImageBlock)[] | undefined; } /** @@ -1662,6 +1891,12 @@ export const UserInputMessageFilterSensitiveLog = (obj: UserInputMessage): any = ...(obj.userInputMessageContext && { userInputMessageContext: UserInputMessageContextFilterSensitiveLog(obj.userInputMessageContext) }), + ...(obj.images && { images: + obj.images.map( + item => + ImageBlockFilterSensitiveLog(item) + ) + }), }) /** @@ -1678,7 +1913,7 @@ export type ChatMessage = export namespace ChatMessage { /** - * Structure to represent a chat input message from User + * Structure to represent a chat input message from User. * @public */ export interface UserInputMessageMember { @@ -1735,6 +1970,106 @@ export const ChatMessageFilterSensitiveLog = (obj: ChatMessage): any => { if (obj.$unknown !== undefined) return {[obj.$unknown[0]]: 'UNKNOWN'}; } +/** + * Represents the target of a citation event + * @public + */ +export type CitationTarget = + | CitationTarget.LocationMember + | CitationTarget.RangeMember + | CitationTarget.$UnknownMember + +/** + * @public + */ +export namespace CitationTarget { + + /** + * Represents a position in the response text where a citation should be added + * @public + */ + export interface LocationMember { + location: number; + range?: never; + $unknown?: never; + } + + /** + * Represents the range in the response text to be targetted by a citation + * @public + */ + export interface RangeMember { + location?: never; + range: Span; + $unknown?: never; + } + + /** + * @public + */ + export interface $UnknownMember { + location?: never; + range?: never; + $unknown: [string, any]; + } + + export interface Visitor { + location: (value: number) => T; + range: (value: Span) => T; + _: (name: string, value: any) => T; + } + + export const visit = ( + value: CitationTarget, + visitor: Visitor + ): T => { + if (value.location !== undefined) return visitor.location(value.location); + if (value.range !== undefined) return visitor.range(value.range); + return visitor._(value.$unknown[0], value.$unknown[1]); + } + +} + +/** + * Streaming response event for citations + * @public + */ +export interface CitationEvent { + /** + * The position or the range of the response text to be cited + * @public + */ + target: CitationTarget | undefined; + + /** + * The text inside the citation '1' in [1] + * @public + */ + citationText?: string | undefined; + + /** + * The link to the document being cited + * @public + */ + citationLink: string | undefined; +} + +/** + * @internal + */ +export const CitationEventFilterSensitiveLog = (obj: CitationEvent): any => ({ + ...obj, + ...(obj.target && { target: + obj.target + }), + ...(obj.citationText && { citationText: + SENSITIVE_STRING + }), + ...(obj.citationLink && { citationLink: + SENSITIVE_STRING + }), +}) + /** * Streaming response event for generated code text. * @public @@ -3073,6 +3408,7 @@ export const ToolUseEventFilterSensitiveLog = (obj: ToolUseEvent): any => ({ */ export type ChatResponseStream = | ChatResponseStream.AssistantResponseEventMember + | ChatResponseStream.CitationEventMember | ChatResponseStream.CodeEventMember | ChatResponseStream.CodeReferenceEventMember | ChatResponseStream.DryRunSucceedEventMember @@ -3106,6 +3442,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3126,6 +3463,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3146,6 +3484,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3166,6 +3505,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3186,6 +3526,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3206,6 +3547,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3226,6 +3568,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3246,6 +3589,7 @@ export namespace ChatResponseStream { intentsEvent: IntentsEvent; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3266,6 +3610,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent: InteractionComponentsEvent; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3286,6 +3631,28 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent: ToolUseEvent; + citationEvent?: never; + invalidStateEvent?: never; + error?: never; + $unknown?: never; + } + + /** + * Citation event + * @public + */ + export interface CitationEventMember { + messageMetadataEvent?: never; + assistantResponseEvent?: never; + dryRunSucceedEvent?: never; + codeReferenceEvent?: never; + supplementaryWebLinksEvent?: never; + followupPromptEvent?: never; + codeEvent?: never; + intentsEvent?: never; + interactionComponentsEvent?: never; + toolUseEvent?: never; + citationEvent: CitationEvent; invalidStateEvent?: never; error?: never; $unknown?: never; @@ -3306,6 +3673,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent: InvalidStateEvent; error?: never; $unknown?: never; @@ -3326,6 +3694,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error: InternalServerException; $unknown?: never; @@ -3345,6 +3714,7 @@ export namespace ChatResponseStream { intentsEvent?: never; interactionComponentsEvent?: never; toolUseEvent?: never; + citationEvent?: never; invalidStateEvent?: never; error?: never; $unknown: [string, any]; @@ -3361,6 +3731,7 @@ export namespace ChatResponseStream { intentsEvent: (value: IntentsEvent) => T; interactionComponentsEvent: (value: InteractionComponentsEvent) => T; toolUseEvent: (value: ToolUseEvent) => T; + citationEvent: (value: CitationEvent) => T; invalidStateEvent: (value: InvalidStateEvent) => T; error: (value: InternalServerException) => T; _: (name: string, value: any) => T; @@ -3380,6 +3751,7 @@ export namespace ChatResponseStream { if (value.intentsEvent !== undefined) return visitor.intentsEvent(value.intentsEvent); if (value.interactionComponentsEvent !== undefined) return visitor.interactionComponentsEvent(value.interactionComponentsEvent); if (value.toolUseEvent !== undefined) return visitor.toolUseEvent(value.toolUseEvent); + if (value.citationEvent !== undefined) return visitor.citationEvent(value.citationEvent); if (value.invalidStateEvent !== undefined) return visitor.invalidStateEvent(value.invalidStateEvent); if (value.error !== undefined) return visitor.error(value.error); return visitor._(value.$unknown[0], value.$unknown[1]); @@ -3420,6 +3792,9 @@ export const ChatResponseStreamFilterSensitiveLog = (obj: ChatResponseStream): a if (obj.toolUseEvent !== undefined) return {toolUseEvent: ToolUseEventFilterSensitiveLog(obj.toolUseEvent) }; + if (obj.citationEvent !== undefined) return {citationEvent: + CitationEventFilterSensitiveLog(obj.citationEvent) + }; if (obj.invalidStateEvent !== undefined) return {invalidStateEvent: obj.invalidStateEvent }; @@ -3860,6 +4235,8 @@ export interface ExportResultArchiveRequest { * @public */ exportContext?: ExportContext | undefined; + + profileArn?: string | undefined; } /** @@ -3884,61 +4261,6 @@ export const ExportResultArchiveResponseFilterSensitiveLog = (obj: ExportResultA }), }) -/** - * @public - * @enum - */ -export const Origin = { - /** - * AWS Chatbot - */ - CHATBOT: "CHATBOT", - /** - * AWS Management Console (https://.console.aws.amazon.com) - */ - CONSOLE: "CONSOLE", - /** - * AWS Documentation Website (https://docs.aws.amazon.com) - */ - DOCUMENTATION: "DOCUMENTATION", - /** - * Any IDE caller. - */ - IDE: "IDE", - /** - * AWS Marketing Website (https://aws.amazon.com) - */ - MARKETING: "MARKETING", - /** - * MD. - */ - MD: "MD", - /** - * AWS Mobile Application (ACMA) - */ - MOBILE: "MOBILE", - /** - * Amazon SageMaker's Rome Chat. - */ - SAGE_MAKER: "SAGE_MAKER", - /** - * Internal Service Traffic (Integ Tests, Canaries, etc.). This is the default when no Origin header present in request. - */ - SERVICE_INTERNAL: "SERVICE_INTERNAL", - /** - * Unified Search in AWS Management Console (https://.console.aws.amazon.com) - */ - UNIFIED_SEARCH: "UNIFIED_SEARCH", - /** - * Origin header is not set. - */ - UNKNOWN: "UNKNOWN", -} as const -/** - * @public - */ -export type Origin = typeof Origin[keyof typeof Origin] - /** * Structure to represent a SendMessage request. * @public @@ -4008,6 +4330,8 @@ export interface GenerateTaskAssistPlanRequest { * @public */ workspaceState: WorkspaceState | undefined; + + profileArn?: string | undefined; } /** diff --git a/src.gen/@amzn/codewhisperer-streaming/src/protocols/Aws_restJson1.ts b/src.gen/@amzn/codewhisperer-streaming/src/protocols/Aws_restJson1.ts index 252aa8f3e43..e6cb747b960 100644 --- a/src.gen/@amzn/codewhisperer-streaming/src/protocols/Aws_restJson1.ts +++ b/src.gen/@amzn/codewhisperer-streaming/src/protocols/Aws_restJson1.ts @@ -26,6 +26,7 @@ import { BinaryPayloadEvent, ChatMessage, ChatResponseStream, + CitationEvent, CodeEvent, CodeReferenceEvent, ConflictException, @@ -43,6 +44,8 @@ import { FollowupPrompt, FollowupPromptEvent, GitState, + ImageBlock, + ImageSource, IntentsEvent, InteractionComponent, InteractionComponentEntry, @@ -72,6 +75,7 @@ import { ToolResult, ToolResultContentBlock, ToolSpecification, + ToolUse, ToolUseEvent, TransformationExportContext, UnitTestGenerationExportContext, @@ -126,6 +130,7 @@ export const se_ExportResultArchiveCommand = async( 'exportContext': _ => _json(_), 'exportId': [], 'exportIntent': [], + 'profileArn': [], })); b.m("POST") .h(headers) @@ -171,6 +176,7 @@ export const se_GenerateTaskAssistPlanCommand = async( let body: any; body = JSON.stringify(take(input, { 'conversationState': _ => se_ConversationState(_, context), + 'profileArn': [], 'workspaceState': _ => _json(_), })); b.m("POST") @@ -557,6 +563,11 @@ const de_CommandError = async( toolUseEvent: await de_ToolUseEvent_event(event["toolUseEvent"], context), }; } + if (event["citationEvent"] != null) { + return { + citationEvent: await de_CitationEvent_event(event["citationEvent"], context), + }; + } if (event["invalidStateEvent"] != null) { return { invalidStateEvent: await de_InvalidStateEvent_event(event["invalidStateEvent"], context), @@ -627,6 +638,15 @@ const de_CommandError = async( Object.assign(contents, de_BinaryPayloadEvent(data, context)); return contents; } + const de_CitationEvent_event = async ( + output: any, + context: __SerdeContext + ): Promise => { + const contents: CitationEvent = {} as any; + const data: any = await parseBody(output.body, context); + Object.assign(contents, _json(data)); + return contents; + } const de_CodeEvent_event = async ( output: any, context: __SerdeContext @@ -733,7 +753,22 @@ const de_CommandError = async( // se_AppStudioState omitted. - // se_AssistantResponseMessage omitted. + /** + * serializeAws_restJson1AssistantResponseMessage + */ + const se_AssistantResponseMessage = ( + input: AssistantResponseMessage, + context: __SerdeContext + ): any => { + return take(input, { + 'content': [], + 'followupPrompt': _json, + 'messageId': [], + 'references': _json, + 'supplementaryWebLinks': _json, + 'toolUses': _ => se_ToolUses(_, context), + }); + } /** * serializeAws_restJson1ChatHistory @@ -755,7 +790,7 @@ const de_CommandError = async( context: __SerdeContext ): any => { return ChatMessage.visit(input, { - assistantResponseMessage: value => ({ "assistantResponseMessage": _json(value) }), + assistantResponseMessage: value => ({ "assistantResponseMessage": se_AssistantResponseMessage(value, context) }), userInputMessage: value => ({ "userInputMessage": se_UserInputMessage(value, context) }), _: (name, value) => ({ name: value } as any) }); @@ -801,6 +836,44 @@ const de_CommandError = async( // se_GitState omitted. + /** + * serializeAws_restJson1ImageBlock + */ + const se_ImageBlock = ( + input: ImageBlock, + context: __SerdeContext + ): any => { + return take(input, { + 'format': [], + 'source': _ => se_ImageSource(_, context), + }); + } + + /** + * serializeAws_restJson1ImageBlocks + */ + const se_ImageBlocks = ( + input: (ImageBlock)[], + context: __SerdeContext + ): any => { + return input.filter((e: any) => e != null).map(entry => { + return se_ImageBlock(entry, context); + }); + } + + /** + * serializeAws_restJson1ImageSource + */ + const se_ImageSource = ( + input: ImageSource, + context: __SerdeContext + ): any => { + return ImageSource.visit(input, { + bytes: value => ({ "bytes": context.base64Encoder(value) }), + _: (name, value) => ({ name: value } as any) + }); + } + // se_Position omitted. // se_ProgrammingLanguage omitted. @@ -946,6 +1019,32 @@ const de_CommandError = async( }); } + /** + * serializeAws_restJson1ToolUse + */ + const se_ToolUse = ( + input: ToolUse, + context: __SerdeContext + ): any => { + return take(input, { + 'input': _ => se_SensitiveDocument(_, context), + 'name': [], + 'toolUseId': [], + }); + } + + /** + * serializeAws_restJson1ToolUses + */ + const se_ToolUses = ( + input: (ToolUse)[], + context: __SerdeContext + ): any => { + return input.filter((e: any) => e != null).map(entry => { + return se_ToolUse(entry, context); + }); + } + // se_TransformationExportContext omitted. // se_UnitTestGenerationExportContext omitted. @@ -959,6 +1058,8 @@ const de_CommandError = async( ): any => { return take(input, { 'content': [], + 'images': _ => se_ImageBlocks(_, context), + 'origin': [], 'userInputMessageContext': _ => se_UserInputMessageContext(_, context), 'userIntent': [], }); @@ -988,6 +1089,8 @@ const de_CommandError = async( // se_UserSettings omitted. + // se_WorkspaceFolderList omitted. + // se_WorkspaceState omitted. // de_AssistantResponseEvent omitted. @@ -1006,6 +1109,10 @@ const de_CommandError = async( }) as any; } + // de_CitationEvent omitted. + + // de_CitationTarget omitted. + // de_CodeEvent omitted. // de_CodeReferenceEvent omitted.