From 62413397333ad456162277b3caa4a554a92b00fc Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Wed, 11 Sep 2024 02:13:22 -0500 Subject: [PATCH] feat(tabby-agent): adding providing best fit range for smart apply function add provideSmartApplyLineRange functionality to TabbyAgent and ChatEditProvider a --- clients/tabby-agent/src/AgentConfig.ts | 8 ++- clients/tabby-agent/src/TabbyAgent.ts | 67 ++++++++++++++++++- .../tabby-agent/src/lsp/ChatEditProvider.ts | 64 +++++++++++++++++- clients/tabby-agent/src/lsp/protocol.ts | 31 +++++++++ .../prompts/provide-smart-apply-line-range.md | 28 ++++++++ 5 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 clients/tabby-agent/src/prompts/provide-smart-apply-line-range.md diff --git a/clients/tabby-agent/src/AgentConfig.ts b/clients/tabby-agent/src/AgentConfig.ts index 48320756dbda..971d9df90ba1 100644 --- a/clients/tabby-agent/src/AgentConfig.ts +++ b/clients/tabby-agent/src/AgentConfig.ts @@ -3,7 +3,7 @@ import generateCommitMessagePrompt from "./prompts/generate-commit-message.md"; import generateDocsPrompt from "./prompts/generate-docs.md"; import editCommandReplacePrompt from "./prompts/edit-command-replace.md"; import editCommandInsertPrompt from "./prompts/edit-command-insert.md"; - +import provideSmartApplyLineRangePrompt from "./prompts/provide-smart-apply-line-range.md"; export type AgentConfig = { server: { endpoint: string; @@ -98,6 +98,9 @@ export type AgentConfig = { promptTemplate: string; responseMatcher: string; }; + provideSmartApplyLineRange: { + promptTemplate: string; + }; }; logs: { // Controls the level of the logger written to the `~/.tabby-client/agent/logs/` @@ -206,6 +209,9 @@ export const defaultAgentConfig: AgentConfig = { responseMatcher: /(?<=(["'`]+)?\s*)(feat|fix|docs|refactor|style|test|build|ci|chore)(\(\S+\))?:.+(?=\s*\1)/gis.toString(), }, + provideSmartApplyLineRange: { + promptTemplate: provideSmartApplyLineRangePrompt, + }, }, logs: { level: "silent", diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index 90aa0aa3c4e0..9fb848901a38 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -47,7 +47,6 @@ import { AnonymousUsageLogger } from "./AnonymousUsageLogger"; import { loadTlsCaCerts } from "./loadCaCerts"; import { createProxyForUrl, ProxyConfig } from "./http/proxy"; import { name as agentName, version as agentVersion } from "../package.json"; - export class TabbyAgent extends EventEmitter implements Agent { private readonly logger = getLogger("TabbyAgent"); private anonymousUsageLogger = new AnonymousUsageLogger(); @@ -1024,6 +1023,72 @@ export class TabbyAgent extends EventEmitter implements Agent { } } + public async provideSmartApplyLineRange(document: string, applyCode: string): Promise { + if (this.status === "notInitialized") { + throw new Error("Agent is not initialized"); + } + + const promptTemplate = this.config.chat.provideSmartApplyLineRange.promptTemplate; + + // request chat api + const requestId = uuid(); + try { + if (!this.api) { + throw new Error("http client not initialized"); + } + const requestPath = "/v1/chat/completions"; + const messages: { role: "user"; content: string }[] = [ + { + role: "user", + content: promptTemplate.replace(/{{document}}|{{applyCode}}/g, (pattern: string) => { + switch (pattern) { + case "{{document}}": + return document; + case "{{applyCode}}": + return applyCode; + default: + return ""; + } + }), + }, + ]; + const requestOptions = { + body: { + messages, + model: "", + stream: true, + }, + parseAs: "stream" as ParseAs, + }; + const requestDescription = `POST ${this.config.server.endpoint + requestPath}`; + this.logger.debug(`Chat request: ${requestDescription}. [${requestId}]`); + this.logger.trace(`Chat request body: [${requestId}]`, requestOptions.body); + const response = await this.api.POST(requestPath, requestOptions); + this.logger.debug(`Chat response status: ${response.response.status}. [${requestId}]`); + if (response.error || !response.response.ok) { + throw new HttpError(response.response); + } + if (!response.response.body) { + return null; + } + const readableStream = readChatStream(response.response.body); + return readableStream; + } catch (error) { + if (error instanceof HttpError && error.status == 404) { + return await this.provideSmartApplyLineRange(document, applyCode); + } + if (isCanceledError(error)) { + this.logger.debug(`Chat request canceled. [${requestId}]`); + } else if (isUnauthorizedError(error)) { + this.logger.debug(`Chat request failed due to unauthorized. [${requestId}]`); + } else { + this.logger.error(`Chat request failed. [${requestId}]`, error); + } + this.healthCheck(); + return null; + } + } + private clientInfoToString(session: Record | undefined): string { let envInfo = `Node.js/${process.version}`; diff --git a/clients/tabby-agent/src/lsp/ChatEditProvider.ts b/clients/tabby-agent/src/lsp/ChatEditProvider.ts index f4346f6a9ff5..e49c250f86cd 100644 --- a/clients/tabby-agent/src/lsp/ChatEditProvider.ts +++ b/clients/tabby-agent/src/lsp/ChatEditProvider.ts @@ -14,6 +14,9 @@ import { ChatEditMutexError, ApplyWorkspaceEditRequest, ApplyWorkspaceEditParams, + ChatLineRangeSmartApplyRequest, + ChatLineRangeSmartApplyParams, + ChatLineRangeSmartApplyResult, } from "./protocol"; import { TextDocuments } from "./TextDocuments"; import { TextDocument } from "vscode-languageserver-textdocument"; @@ -23,7 +26,6 @@ import * as Diff from "diff"; import { TabbyAgent } from "../TabbyAgent"; import { isEmptyRange } from "../utils/range"; import { isBlank } from "../utils"; - export type Edit = { id: ChatEditToken; location: Location; @@ -54,6 +56,10 @@ export class ChatEditProvider { this.connection.onRequest(ChatEditResolveRequest.type, async (params) => { return this.resolveEdit(params); }); + + this.connection.onRequest(ChatLineRangeSmartApplyRequest.type, async (params) => { + return this.provideSmartApplyLineRange(params); + }); } isCurrentEdit(id: ChatEditToken): boolean { @@ -428,6 +434,62 @@ export class ChatEditProvider { } } + async provideSmartApplyLineRange( + params: ChatLineRangeSmartApplyParams, + ): Promise { + const document = this.documents.get(params.uri); + if (!document) { + return undefined; + } + if (!this.agent.getServerHealthState()?.chat_model) { + throw { + name: "ChatFeatureNotAvailableError", + message: "Chat feature not available", + } as ChatFeatureNotAvailableError; + } + + //TODO(Sma1lboy): maybe visible range with huge offset, don't do whole file + const documentText = document + .getText() + .split("\n") + .map((line, idx) => `${idx + 1} | ${line}`) + .join("\n"); + + if (this.mutexAbortController) { + throw { + name: "ChatEditMutexError", + message: "Another edit is already in progress", + } as ChatEditMutexError; + } + this.mutexAbortController = new AbortController(); + + const stream = await this.agent.provideSmartApplyLineRange(documentText, params.applyCode); + if (!stream) { + return undefined; + } + let response = ""; + for await (const delta of stream) { + response += delta; + } + + const regex = /(.*?)<\/GENERATEDCODE>/s; + const match = response.match(regex); + + if (match && match[1]) { + response = match[1].trim(); + } + + const range = response.split("-"); + + if (range.length !== 2) { + return undefined; + } + const startLine = parseInt(range[0] ? range[0] : "0"); + const endLine = parseInt(range[1] ? range[1] : "0"); + this.mutexAbortController = null; + return { start: startLine, end: endLine }; + } + // header line // <<<<<<< Editing by Tabby <.#=+-> // markers: diff --git a/clients/tabby-agent/src/lsp/protocol.ts b/clients/tabby-agent/src/lsp/protocol.ts index b7c31d49f0cd..f35cf0751ba1 100644 --- a/clients/tabby-agent/src/lsp/protocol.ts +++ b/clients/tabby-agent/src/lsp/protocol.ts @@ -520,6 +520,37 @@ export type ChatEditResolveCommand = LspCommand & { arguments: [ChatEditResolveParams]; }; +/** + * [Tabby] Provide Best fit line range for smart apply request(↩️) + * + * This method is sent from the client to server to smart apply from chat panel. + * - method: `tabby/chat/smartApply` + * - params: {@link ChatLineRangeSmartApplyParams} + * - result: {@link ChatLineRangeSmartApplyResult} | null + */ +export namespace ChatLineRangeSmartApplyRequest { + export const method = "tabby/chat/smartApply"; + export const messageDirection = MessageDirection.clientToServer; + export const type = new ProtocolRequestType< + ChatLineRangeSmartApplyParams, + ChatLineRangeSmartApplyResult | null, + void, + ChatFeatureNotAvailableError, + void + >(method); +} + +export type ChatLineRangeSmartApplyParams = { + //the uri of the document + uri: string; + applyCode: string; +}; + +export type ChatLineRangeSmartApplyResult = { + start: number; + end: number; +}; + /** * [Tabby] GenerateCommitMessage Request(↩️) * diff --git a/clients/tabby-agent/src/prompts/provide-smart-apply-line-range.md b/clients/tabby-agent/src/prompts/provide-smart-apply-line-range.md new file mode 100644 index 000000000000..3fa0612f10bd --- /dev/null +++ b/clients/tabby-agent/src/prompts/provide-smart-apply-line-range.md @@ -0,0 +1,28 @@ +You are an AI assistant specialized in determining the most appropriate location to insert new code into an existing file. Your task is to analyze the given file content and the code to be inserted, then provide only the line range where the new code should be inserted. + +The file content is provided line by line, with each line in the format: +line number | code + +The new code to be inserted is provided in XML tags. + +Your task: + +1. Analyze the existing code structure and the new code to be inserted. +2. Determine the most appropriate location for insertion. +3. Provide ONLY the line range for insertion. + +You must reply with ONLY the suggested insertion range in the format startLine-endLine, enclosed in XML tags. + +Do not include any explanation, existing code, or the code to be inserted in your response. + +File content: + +{{document}} + + +Code to be inserted: + +{{applyCode}} + + +Provide only the appropriate insertion range.