-
Notifications
You must be signed in to change notification settings - Fork 747
feat(amazonq): add experiment for basic e2e flow of inline chat through Flare. #7235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
a2c78fb
a08ca02
2d4e6f6
47d98ca
9647ba8
53671c3
360de61
9e83cac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ import { | |
| CodeWhispererStreamingServiceException, | ||
| GenerateAssistantResponseCommandOutput, | ||
| } from '@amzn/codewhisperer-streaming' | ||
| import { LanguageClient } from 'vscode-languageclient' | ||
| import { inlineChatRequestType } from '@aws/language-server-runtimes/protocol' | ||
| import { AuthUtil, getSelectedCustomization } from 'aws-core-vscode/codewhisperer' | ||
| import { | ||
| ChatSessionStorage, | ||
|
|
@@ -25,6 +27,9 @@ import { codeWhispererClient } from 'aws-core-vscode/codewhisperer' | |
| import type { InlineChatEvent } from 'aws-core-vscode/codewhisperer' | ||
| import { InlineTask } from '../controller/inlineTask' | ||
| import { extractAuthFollowUp } from 'aws-core-vscode/amazonq' | ||
| import { InlineChatParams, InlineChatResult } from '@aws/language-server-runtimes-types' | ||
| import { decodeRequest, encryptRequest } from '../../lsp/encryption' | ||
| import { getCursorState } from '../../lsp/utils' | ||
|
|
||
| export class InlineChatProvider { | ||
| private readonly editorContextExtractor: EditorContextExtractor | ||
|
|
@@ -34,13 +39,52 @@ export class InlineChatProvider { | |
| private errorEmitter = new vscode.EventEmitter<void>() | ||
| public onErrorOccured = this.errorEmitter.event | ||
|
|
||
| public constructor() { | ||
| public constructor( | ||
| private readonly client: LanguageClient, | ||
| private readonly encryptionKey: Buffer | ||
| ) { | ||
| this.editorContextExtractor = new EditorContextExtractor() | ||
| this.userIntentRecognizer = new UserIntentRecognizer() | ||
| this.sessionStorage = new ChatSessionStorage() | ||
| this.triggerEventsStorage = new TriggerEventsStorage() | ||
| } | ||
|
|
||
| private getCurrentEditorParams(prompt: string): InlineChatParams { | ||
| const editor = vscode.window.activeTextEditor | ||
| if (!editor) { | ||
| throw new ToolkitError('No active editor') | ||
| } | ||
|
|
||
| const documentUri = editor.document.uri.toString() | ||
| const cursorState = getCursorState(editor.selections) | ||
| return { | ||
| prompt: { | ||
| prompt, | ||
| }, | ||
| cursorState, | ||
| textDocument: { | ||
| uri: documentUri, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| public async processPromptMessageLSP(message: PromptMessage): Promise<InlineChatResult> { | ||
| // TODO: handle partial responses. | ||
| getLogger().info('Making inline chat request with message %O', message) | ||
| const params = this.getCurrentEditorParams(message.message ?? '') | ||
| const inlineChatRequest = await encryptRequest<InlineChatParams>(params, this.encryptionKey) | ||
| const response = await this.client.sendRequest(inlineChatRequestType.method, inlineChatRequest) | ||
| const decryptedMessage = | ||
| typeof response === 'string' && this.encryptionKey | ||
| ? await decodeRequest(response, this.encryptionKey) | ||
| : response | ||
| const result: InlineChatResult = decryptedMessage as InlineChatResult | ||
| this.client.info(`Logging response for inline chat ${JSON.stringify(decryptedMessage)}`) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does the decrypted message have any secrets (may need partialClone()) ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good callout, but I think this one is safe. Here's what I see in the logs: |
||
|
|
||
| return result | ||
| } | ||
|
|
||
| // TODO: remove in favor of LSP implementation. | ||
| public async processPromptMessage(message: PromptMessage) { | ||
| return this.editorContextExtractor | ||
| .extractContextForTrigger('ChatMessage') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import * as jose from 'jose' | ||
|
|
||
| export async function encryptRequest<T>(params: T, encryptionKey: Buffer): Promise<{ message: string } | T> { | ||
| const payload = new TextEncoder().encode(JSON.stringify(params)) | ||
|
|
||
| const encryptedMessage = await new jose.CompactEncrypt(payload) | ||
| .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' }) | ||
| .encrypt(encryptionKey) | ||
|
|
||
| return { message: encryptedMessage } | ||
| } | ||
|
|
||
| export async function decodeRequest<T>(request: string, key: Buffer): Promise<T> { | ||
| const result = await jose.jwtDecrypt(request, key, { | ||
| clockTolerance: 60, // Allow up to 60 seconds to account for clock differences | ||
| contentEncryptionAlgorithms: ['A256GCM'], | ||
| keyManagementAlgorithms: ['dir'], | ||
| }) | ||
|
|
||
| if (!result.payload) { | ||
| throw new Error('JWT payload not found') | ||
| } | ||
| return result.payload as T | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import * as vscode from 'vscode' | ||
|
|
||
| export function getCursorState(selection: readonly vscode.Selection[]) { | ||
|
||
| return selection.map((s) => ({ | ||
| range: { | ||
| start: { | ||
| line: s.start.line, | ||
| character: s.start.character, | ||
| }, | ||
| end: { | ||
| line: s.end.line, | ||
| character: s.end.character, | ||
| }, | ||
| }, | ||
| })) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think its worth it to pull out decryption to its own thing? We used it a lot in messages and we might have to use it in inline suggestions as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that makes sense. I'll track that as a followup. These encrypt and decrypt functions could also benefit from some tests to enforce the invariant that decrypt and encrypt are inverses with the same key.