server/aws-lsp-codewhisperer/src/language-server/chat/contexts/triggerContext.ts (129 lines of code) (raw):

import { TriggerType } from '@aws/chat-client-ui-types' import { ChatTriggerType, UserIntent, Tool, ToolResult, RelevantTextDocument } from '@amzn/codewhisperer-streaming' import { BedrockTools, ChatParams, CursorState, InlineChatParams } from '@aws/language-server-runtimes/server-interface' import { Features } from '../../types' import { DocumentContext, DocumentContextExtractor } from './documentContext' import { SendMessageCommandInput } from '../../../shared/streamingClientService' import { LocalProjectContextController } from '../../../shared/localProjectContextController' import { convertChunksToRelevantTextDocuments } from '../tools/relevantTextDocuments' export interface TriggerContext extends Partial<DocumentContext> { userIntent?: UserIntent triggerType?: TriggerType useRelevantDocuments?: boolean relevantDocuments?: RelevantTextDocument[] } export class QChatTriggerContext { private static readonly DEFAULT_CURSOR_STATE: CursorState = { position: { line: 0, character: 0 } } #workspace: Features['workspace'] #documentContextExtractor: DocumentContextExtractor #logger: Features['logging'] constructor(workspace: Features['workspace'], logger: Features['logging']) { this.#workspace = workspace this.#documentContextExtractor = new DocumentContextExtractor({ logger, workspace }) this.#logger = logger } async getNewTriggerContext(params: ChatParams | InlineChatParams): Promise<TriggerContext> { const documentContext: DocumentContext | undefined = await this.extractDocumentContext(params) const useRelevantDocuments = 'context' in params ? params.context?.some(context => typeof context !== 'string' && context.command === '@workspace') : false const relevantDocuments = useRelevantDocuments ? await this.extractProjectContext(params.prompt.prompt) : [] return { ...documentContext, userIntent: this.#guessIntentFromPrompt(params.prompt.prompt), useRelevantDocuments, relevantDocuments, } } getChatParamsFromTrigger( params: ChatParams | InlineChatParams, triggerContext: TriggerContext, chatTriggerType: ChatTriggerType, customizationArn?: string, profileArn?: string, tools: BedrockTools = [] ): SendMessageCommandInput { const { prompt } = params const data: SendMessageCommandInput = { conversationState: { chatTriggerType: chatTriggerType, currentMessage: { userInputMessage: { content: prompt.escapedPrompt ?? prompt.prompt, userInputMessageContext: triggerContext.cursorState && triggerContext.relativeFilePath ? { editorState: { cursorState: triggerContext.cursorState, document: { text: triggerContext.text, programmingLanguage: triggerContext.programmingLanguage, relativeFilePath: triggerContext.relativeFilePath, }, ...(triggerContext.useRelevantDocuments && { useRelevantDocuments: triggerContext.useRelevantDocuments, relevantDocuments: triggerContext.relevantDocuments, }), }, tools, } : { tools, ...(triggerContext.useRelevantDocuments && { editorState: { useRelevantDocuments: triggerContext.useRelevantDocuments, relevantDocuments: triggerContext.relevantDocuments, }, }), }, userIntent: triggerContext.userIntent, origin: 'IDE', }, }, customizationArn, }, profileArn, } return data } // public for testing async extractDocumentContext( input: Pick<ChatParams | InlineChatParams, 'cursorState' | 'textDocument'> ): Promise<DocumentContext | undefined> { const { textDocument: textDocumentIdentifier, cursorState } = input const textDocument = textDocumentIdentifier?.uri && (await this.#workspace.getTextDocument(textDocumentIdentifier.uri)) return textDocument ? this.#documentContextExtractor.extractDocumentContext( textDocument, // we want to include a default position if a text document is found so users can still ask questions about the opened file // the range will be expanded up to the max characters downstream cursorState?.[0] ?? QChatTriggerContext.DEFAULT_CURSOR_STATE ) : undefined } async extractProjectContext(query?: string): Promise<RelevantTextDocument[]> { if (query) { try { const contextController = await LocalProjectContextController.getInstance() const resp = await contextController.queryVectorIndex({ query }) return convertChunksToRelevantTextDocuments(resp) } catch (e) { this.#logger.error(`Failed to extract project context for chat trigger: ${e}`) } } return [] } #guessIntentFromPrompt(prompt?: string): UserIntent | undefined { if (prompt === undefined) { return undefined } else if (/^explain/i.test(prompt)) { return UserIntent.EXPLAIN_CODE_SELECTION } else if (/^refactor/i.test(prompt)) { return UserIntent.SUGGEST_ALTERNATE_IMPLEMENTATION } else if (/^fix/i.test(prompt)) { return UserIntent.APPLY_COMMON_BEST_PRACTICES } else if (/^optimize/i.test(prompt)) { return UserIntent.IMPROVE_CODE } return undefined } }