diff --git a/clients/tabby-agent/openapi/tabby.json b/clients/tabby-agent/openapi/tabby.json index bd1ba1f..29bc556 100644 --- a/clients/tabby-agent/openapi/tabby.json +++ b/clients/tabby-agent/openapi/tabby.json @@ -2,14 +2,11 @@ "openapi": "3.0.3", "info": { "title": "Tabby Server", - "description": "\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby?style=social)](https://github.com/TabbyML/tabby)\n\nOpenAPI documentation for [tabby](https://github.com/TabbyML/tabby), a self-hosted AI coding assistant.", + "description": "\n[![tabby stars](https://img.shields.io/github/stars/TabbyML/tabby)](https://github.com/TabbyML/tabby)\n[![Join Slack](https://shields.io/badge/Join-Tabby%20Slack-red?logo=slack)](https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA)\n\nInstall following IDE / Editor extensions to get started with [Tabby](https://github.com/TabbyML/tabby).\n* [VSCode Extension](https://github.com/TabbyML/tabby/tree/main/clients/vscode) – Install from the [marketplace](https://marketplace.visualstudio.com/items?itemName=TabbyML.vscode-tabby), or [open-vsx.org](https://open-vsx.org/extension/TabbyML/vscode-tabby)\n* [VIM Extension](https://github.com/TabbyML/tabby/tree/main/clients/vim)\n* [IntelliJ Platform Plugin](https://github.com/TabbyML/tabby/tree/main/clients/intellij) – Install from the [marketplace](https://plugins.jetbrains.com/plugin/22379-tabby)\n", "license": { "name": "Apache 2.0", "url": "https://github.com/TabbyML/tabby/blob/main/LICENSE" }, - "version": "0.1.0" + "version": "0.5.5" }, - "servers": [ - { "url": "https://playground.app.tabbyml.com", "description": "Playground server" }, - { "url": "http://localhost:8080", "description": "Local server" } - ], + "servers": [{ "url": "/", "description": "Server" }], "paths": { "/v1/completions": { "post": { @@ -40,7 +37,7 @@ } }, "/v1/health": { - "post": { + "get": { "tags": ["v1"], "operationId": "health", "responses": { @@ -50,6 +47,34 @@ } } } + }, + "/v1beta/search": { + "get": { + "tags": ["v1beta"], + "operationId": "search", + "parameters": [ + { "name": "q", "in": "query", "required": true, "schema": { "type": "string", "default": "get" } }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 20, "nullable": true, "minimum": 0.0 } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { "type": "integer", "default": 0, "nullable": true, "minimum": 0.0 } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } } + }, + "501": { "description": "When code search is not enabled, the endpoint will returns 501 Not Implemented" } + } + } } }, "components": { @@ -65,7 +90,6 @@ "CompletionRequest": { "type": "object", "properties": { - "prompt": { "type": "string", "example": "def fib(n):", "nullable": true }, "language": { "type": "string", "description": "Language identifier, full list is maintained at\nhttps://code.visualstudio.com/docs/languages/identifiers", @@ -73,7 +97,12 @@ "nullable": true }, "segments": { "allOf": [{ "$ref": "#/components/schemas/Segments" }], "nullable": true }, - "user": { "type": "string", "nullable": true } + "user": { + "type": "string", + "description": "A unique identifier representing your end-user, which can help Tabby to monitor & generating\nreports.", + "nullable": true + }, + "debug_options": { "allOf": [{ "$ref": "#/components/schemas/DebugOptions" }], "nullable": true } }, "example": { "language": "python", @@ -85,16 +114,41 @@ "required": ["id", "choices"], "properties": { "id": { "type": "string" }, - "choices": { "type": "array", "items": { "$ref": "#/components/schemas/Choice" } } + "choices": { "type": "array", "items": { "$ref": "#/components/schemas/Choice" } }, + "debug_data": { "allOf": [{ "$ref": "#/components/schemas/DebugData" }], "nullable": true } + }, + "example": { "choices": [{ "index": 0, "text": "string" }], "id": "string" } + }, + "DebugData": { + "type": "object", + "properties": { + "snippets": { "type": "array", "items": { "$ref": "#/components/schemas/Snippet" }, "nullable": true }, + "prompt": { "type": "string", "nullable": true } + } + }, + "DebugOptions": { + "type": "object", + "properties": { + "raw_prompt": { + "type": "string", + "description": "When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored.\n\nThis is useful for certain requests that aim to test the tabby's e2e quality.", + "nullable": true + }, + "return_snippets": { "type": "boolean", "description": "When true, returns `snippets` in `debug_data`." }, + "return_prompt": { "type": "boolean", "description": "When true, returns `prompt` in `debug_data`." }, + "disable_retrieval_augmented_code_completion": { + "type": "boolean", + "description": "When true, disable retrieval augmented code completion." + } } }, "HealthState": { "type": "object", - "required": ["model", "device", "compute_type", "arch", "cpu_info", "cpu_count", "cuda_devices", "version"], + "required": ["model", "device", "arch", "cpu_info", "cpu_count", "cuda_devices", "version"], "properties": { "model": { "type": "string" }, + "chat_model": { "type": "string", "nullable": true }, "device": { "type": "string" }, - "compute_type": { "type": "string" }, "arch": { "type": "string" }, "cpu_info": { "type": "string" }, "cpu_count": { "type": "integer", "minimum": 0.0 }, @@ -102,6 +156,27 @@ "version": { "$ref": "#/components/schemas/Version" } } }, + "Hit": { + "type": "object", + "required": ["score", "doc", "id"], + "properties": { + "score": { "type": "number", "format": "float" }, + "doc": { "$ref": "#/components/schemas/HitDocument" }, + "id": { "type": "integer", "format": "int32", "minimum": 0.0 } + } + }, + "HitDocument": { + "type": "object", + "required": ["body", "filepath", "git_url", "kind", "language", "name"], + "properties": { + "body": { "type": "string" }, + "filepath": { "type": "string" }, + "git_url": { "type": "string" }, + "kind": { "type": "string" }, + "language": { "type": "string" }, + "name": { "type": "string" } + } + }, "LogEventRequest": { "type": "object", "required": ["type", "completion_id", "choice_index"], @@ -111,6 +186,14 @@ "choice_index": { "type": "integer", "format": "int32", "minimum": 0.0 } } }, + "SearchResponse": { + "type": "object", + "required": ["num_hits", "hits"], + "properties": { + "num_hits": { "type": "integer", "minimum": 0.0 }, + "hits": { "type": "array", "items": { "$ref": "#/components/schemas/Hit" } } + } + }, "Segments": { "type": "object", "required": ["prefix"], @@ -123,6 +206,15 @@ } } }, + "Snippet": { + "type": "object", + "required": ["filepath", "body", "score"], + "properties": { + "filepath": { "type": "string" }, + "body": { "type": "string" }, + "score": { "type": "number", "format": "float" } + } + }, "Version": { "type": "object", "required": ["build_date", "build_timestamp", "git_sha", "git_describe"], diff --git a/clients/tabby-agent/src/Agent.ts b/clients/tabby-agent/src/Agent.ts index 65b0710..094c20d 100644 --- a/clients/tabby-agent/src/Agent.ts +++ b/clients/tabby-agent/src/Agent.ts @@ -30,7 +30,11 @@ export type HighCompletionTimeoutRateIssue = { name: "highCompletionTimeoutRate"; completionResponseStats: Record; }; -export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeoutRateIssue; +export type ConnectionFailedIssue = { + name: "connectionFailed"; + message: string; +}; +export type AgentIssue = SlowCompletionResponseTimeIssue | HighCompletionTimeoutRateIssue | ConnectionFailedIssue; /** * Represents the status of the agent. @@ -95,14 +99,14 @@ export interface AgentFunction { /** * @returns the current issues if any exists */ - getIssues(detail?: boolean): AgentIssue["name"][]; + getIssues(): AgentIssue["name"][]; /** * Get the detail of an issue by index or name. * @param options if `index` is provided, `name` will be ignored * @returns the issue detail if exists, otherwise null */ - getIssueDetail(options: { index?: number; name?: AgentIssue["name"] }): AgentIssue | null; + getIssueDetail(options: { index?: number; name?: T["name"] }): T | null; /** * @returns server info returned from latest server health check, returns null if not available diff --git a/clients/tabby-agent/src/AnonymousUsageLogger.ts b/clients/tabby-agent/src/AnonymousUsageLogger.ts index fae0a5c..aea9691 100644 --- a/clients/tabby-agent/src/AnonymousUsageLogger.ts +++ b/clients/tabby-agent/src/AnonymousUsageLogger.ts @@ -81,7 +81,7 @@ export class AnonymousUsageLogger { if (this.disabled) { return; } - if (unique && this.emittedUniqueEvent.indexOf(event) >= 0) { + if (unique && this.emittedUniqueEvent.includes(event)) { return; } if (unique) { diff --git a/clients/tabby-agent/src/Auth.ts b/clients/tabby-agent/src/Auth.ts index ad45eaf..69573df 100644 --- a/clients/tabby-agent/src/Auth.ts +++ b/clients/tabby-agent/src/Auth.ts @@ -169,7 +169,7 @@ export class Auth extends EventEmitter { clearInterval(timer); resolve(true); } catch (error) { - if (error instanceof HttpError && [400, 401, 403, 405].indexOf(error.status) !== -1) { + if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) { this.logger.debug({ error }, "Expected error when polling jwt"); } else { // unknown error but still keep polling @@ -205,7 +205,7 @@ export class Auth extends EventEmitter { payload: decodeJwt(refreshedJwt.data.jwt), }; } catch (error) { - if (error instanceof HttpError && [400, 401, 403, 405].indexOf(error.status) !== -1) { + if (error instanceof HttpError && [400, 401, 403, 405].includes(error.status)) { this.logger.debug({ error }, "Error when refreshing jwt"); } else { // unknown error, retry a few times diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index 98fe2f1..607e54e 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -4,8 +4,9 @@ import deepEqual from "deep-equal"; import { deepmerge } from "deepmerge-ts"; import { getProperty, setProperty, deleteProperty } from "dot-prop"; import createClient from "openapi-fetch"; -import { paths as TabbyApi } from "./types/tabbyApi"; -import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError } from "./utils"; +import type { ParseAs } from "openapi-fetch"; +import type { paths as TabbyApi } from "./types/tabbyApi"; +import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError, errorToString } from "./utils"; import type { Agent, AgentStatus, @@ -47,6 +48,7 @@ export class TabbyAgent extends EventEmitter implements Agent { private status: AgentStatus = "notInitialized"; private issues: AgentIssue["name"][] = []; private serverHealthState: ServerHealthState | null = null; + private connectionErrorMessage: string | null = null; private api: ReturnType>; private auth: Auth; private dataStore: DataStore | null = null; @@ -88,6 +90,7 @@ export class TabbyAgent extends EventEmitter implements Agent { this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig); allLoggers.forEach((logger) => (logger.level = this.config.logs.level)); this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable; + if (isBlank(this.config.server.token) && this.config.server.requestHeaders["Authorization"] === undefined) { if (this.config.server.endpoint !== this.auth?.endpoint) { this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore }); @@ -104,6 +107,8 @@ export class TabbyAgent extends EventEmitter implements Agent { this.completionProviderStats.resetWindowed(); this.popIssue("slowCompletionResponseTime"); this.popIssue("highCompletionTimeoutRate"); + this.popIssue("connectionFailed"); + this.connectionErrorMessage = null; } await this.setupApi(); @@ -167,11 +172,16 @@ export class TabbyAgent extends EventEmitter implements Agent { name: "slowCompletionResponseTime", completionResponseStats: this.completionProviderStats.windowed().stats, }; + case "connectionFailed": + return { + name: "connectionFailed", + message: this.connectionErrorMessage, + }; } } private pushIssue(issue: AgentIssue["name"]) { - if (this.issues.indexOf(issue) === -1) { + if (!this.issues.includes(issue)) { this.issues.push(issue); this.logger.debug({ issue }, "Issues Pushed"); this.emitIssueUpdated(); @@ -206,65 +216,61 @@ export class TabbyAgent extends EventEmitter implements Agent { } } - private async post[0]>( - path: T, - requestOptions: Parameters>[1], - abortOptions?: { signal?: AbortSignal; timeout?: number }, - ): Promise>>["data"]> { - const requestId = uuid(); - this.logger.debug({ requestId, path, requestOptions, abortOptions }, "API request"); - try { - const timeout = Math.min(0x7fffffff, abortOptions?.timeout || this.config.server.requestTimeout); - const signal = abortSignalFromAnyOf([AbortSignal.timeout(timeout), abortOptions?.signal]); - const response = await this.api.POST(path, { ...requestOptions, signal }); - if (response.error) { - throw new HttpError(response.response); - } - this.logger.debug({ requestId, path, response: response.data }, "API response"); - this.changeStatus("ready"); - return response.data; - } catch (error) { - if (isTimeoutError(error)) { - this.logger.debug({ requestId, path, error }, "API request timeout"); - } else if (isCanceledError(error)) { - this.logger.debug({ requestId, path, error }, "API request canceled"); - } else if ( - error instanceof HttpError && - [401, 403, 405].indexOf(error.status) !== -1 && - new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") && - isBlank(this.config.server.token) && - this.config.server.requestHeaders["Authorization"] === undefined - ) { - this.logger.debug({ requestId, path, error }, "API unauthorized"); - this.changeStatus("unauthorized"); - } else if (error instanceof HttpError) { - this.logger.error({ requestId, path, error }, "API error"); - this.changeStatus("disconnected"); - } else { - this.logger.error({ requestId, path, error }, "API request failed with unknown error"); - this.changeStatus("disconnected"); - } - throw error; - } + private createAbortSignal(options?: { signal?: AbortSignal; timeout?: number }): AbortSignal { + const timeout = Math.min(0x7fffffff, options?.timeout || this.config.server.requestTimeout); + return abortSignalFromAnyOf([AbortSignal.timeout(timeout), options?.signal]); } private async healthCheck(options?: AbortSignalOption): Promise { + const requestId = uuid(); + const requestPath = "/v1/health"; + const requestUrl = this.config.server.endpoint + requestPath; + const requestOptions = { + signal: this.createAbortSignal(options), + }; try { - const healthState = await this.post("/v1/health", {}, options); + this.logger.debug({ requestId, requestOptions, url: requestUrl }, "Health check request"); + const response = await this.api.GET(requestPath, requestOptions); + if (response.error) { + throw new HttpError(response.response); + } + this.logger.debug({ requestId, response }, "Health check response"); + this.changeStatus("ready"); + this.popIssue("connectionFailed"); + this.connectionErrorMessage = null; + const healthState = response.data; if ( typeof healthState === "object" && healthState["model"] !== undefined && healthState["device"] !== undefined ) { this.serverHealthState = healthState; - if (this.status === "ready") { - this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState); - } + this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState); } - } catch (_) { - if (this.status === "ready" || this.status === "notInitialized") { + } catch (error) { + this.serverHealthState = null; + if ( + error instanceof HttpError && + [401, 403, 405].includes(error.status) && + new URL(this.config.server.endpoint).hostname.endsWith("app.tabbyml.com") && + isBlank(this.config.server.token) && + this.config.server.requestHeaders["Authorization"] === undefined + ) { + this.logger.debug({ requestId, error }, "Health check error: unauthorized"); + this.changeStatus("unauthorized"); + } else { + if (isTimeoutError(error)) { + this.logger.debug({ requestId, error }, "Health check error: timeout"); + this.connectionErrorMessage = `GET ${requestUrl}: Timed out.`; + } else if (isCanceledError(error)) { + this.logger.debug({ requestId, error }, "Health check error: canceled"); + this.connectionErrorMessage = `GET ${requestUrl}: Canceled.`; + } else { + this.logger.error({ requestId, error }, "Health check error: unknown error"); + this.connectionErrorMessage = `GET ${requestUrl}: Request failed: \n${errorToString(error)}`; + } + this.pushIssue("connectionFailed"); this.changeStatus("disconnected"); - this.serverHealthState = null; } } } @@ -380,11 +386,11 @@ export class TabbyAgent extends EventEmitter implements Agent { return this.issues; } - public getIssueDetail(options: { index?: number; name?: AgentIssue["name"] }): AgentIssue | null { - if (options.index !== undefined) { - return this.issueFromName(this.issues[options.index]); - } else if (options.name !== undefined && this.issues.indexOf(options.name) !== -1) { - return this.issueFromName(options.name); + public getIssueDetail(options: { index?: number; name?: T["name"] }): T | null { + if (options.index !== undefined && options.index < this.issues.length) { + return this.issueFromName(this.issues[options.index]) as T; + } else if (options.name !== undefined && this.issues.includes(options.name)) { + return this.issueFromName(options.name) as T; } else { return null; } @@ -480,27 +486,36 @@ export class TabbyAgent extends EventEmitter implements Agent { ); // Send http request + const requestId = uuid(); stats.requestSent = true; requestStartedAt = performance.now(); try { - const response = await this.post( - "/v1/completions", - { - body: { - language: request.language, - segments, - user: this.auth?.user, - }, + const requestPath = "/v1/completions"; + const requestOptions = { + body: { + language: request.language, + segments, + user: this.auth?.user, }, - { + signal: this.createAbortSignal({ signal, timeout: this.config.completion.timeout, - }, + }), + }; + this.logger.debug( + { requestId, requestOptions, url: this.config.server.endpoint + requestPath }, + "Completion request", ); + const response = await this.api.POST(requestPath, requestOptions); + if (response.error) { + throw new HttpError(response.response); + } + this.logger.debug({ requestId, response }, "Completion response"); + const responseData = response.data; stats.requestLatency = performance.now() - requestStartedAt; completionResponse = { - id: response.id, - choices: response.choices.map((choice) => { + id: responseData.id, + choices: responseData.choices.map((choice) => { return { index: choice.index, text: choice.text, @@ -513,12 +528,17 @@ export class TabbyAgent extends EventEmitter implements Agent { }; } catch (error) { if (isCanceledError(error)) { + this.logger.debug({ requestId, error }, "Completion request canceled"); stats.requestCanceled = true; stats.requestLatency = performance.now() - requestStartedAt; - } - if (isTimeoutError(error)) { + } else if (isTimeoutError(error)) { + this.logger.debug({ requestId, error }, "Completion request timeout"); stats.requestTimeout = true; stats.requestLatency = NaN; + } else { + this.logger.error({ requestId, error }, "Completion request failed with unknown error"); + // schedule a health check + this.healthCheck(); } // rethrow error throw error; @@ -586,19 +606,35 @@ export class TabbyAgent extends EventEmitter implements Agent { throw new Error("Agent is not initialized"); } this.completionProviderStats.addEvent(request.type); - await this.post( - "/v1/events", - { + const requestId = uuid(); + try { + const requestPath = "/v1/events"; + const requestOptions = { body: request, params: { query: { select_kind: request.select_kind, }, }, - parseAs: "text", - }, - options, - ); - return true; + signal: this.createAbortSignal(options), + parseAs: "text" as ParseAs, + }; + this.logger.debug({ requestId, requestOptions, url: this.config.server.endpoint + requestPath }, "Event request"); + const response = await this.api.POST(requestPath, requestOptions); + if (response.error) { + throw new HttpError(response.response); + } + this.logger.debug({ requestId, response }, "Event response"); + return true; + } catch (error) { + if (isTimeoutError(error)) { + this.logger.debug({ requestId, error }, "Event request timeout"); + } else if (isCanceledError(error)) { + this.logger.debug({ requestId, error }, "Event request canceled"); + } else { + this.logger.error({ requestId, error }, "Event request failed with unknown error"); + } + return false; + } } } diff --git a/clients/tabby-agent/src/index.ts b/clients/tabby-agent/src/index.ts index 36257ab..2f83465 100644 --- a/clients/tabby-agent/src/index.ts +++ b/clients/tabby-agent/src/index.ts @@ -12,6 +12,7 @@ export { IssuesUpdatedEvent, SlowCompletionResponseTimeIssue, HighCompletionTimeoutRateIssue, + ConnectionFailedIssue, ClientProperties, AgentInitOptions, ServerHealthState, diff --git a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts b/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts index 1dffb6b..f5fd97a 100644 --- a/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts +++ b/clients/tabby-agent/src/postprocess/calculateReplaceRangeBySyntax.ts @@ -11,7 +11,7 @@ export async function calculateReplaceRangeBySyntax( context: CompletionContext, ): Promise { const { position, prefix, suffix, prefixLines, suffixLines, language } = context; - if (supportedLanguages.indexOf(language) < 0) { + if (!supportedLanguages.includes(language)) { return response; } const languageConfig = languagesConfigs[language]; diff --git a/clients/tabby-agent/src/postprocess/limitScope.ts b/clients/tabby-agent/src/postprocess/limitScope.ts index 4720382..e5e260e 100644 --- a/clients/tabby-agent/src/postprocess/limitScope.ts +++ b/clients/tabby-agent/src/postprocess/limitScope.ts @@ -15,7 +15,7 @@ export function limitScope( return limitScopeByIndentation(context, config["indentation"])(input); } : (input) => { - if (config.experimentalSyntax && supportedLanguages.indexOf(context.language) >= 0) { + if (config.experimentalSyntax && supportedLanguages.includes(context.language)) { return limitScopeBySyntax(context)(input); } else { return limitScopeByIndentation(context, config["indentation"])(input); diff --git a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts b/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts index ebb8139..d2b5d26 100644 --- a/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts +++ b/clients/tabby-agent/src/postprocess/limitScopeBySyntax.ts @@ -42,7 +42,7 @@ function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): Tre for (const types of typeList) { let scope = node; while (scope) { - if (types.indexOf(scope.type) >= 0) { + if (types.includes(scope.type)) { return scope; } scope = scope.parent; @@ -54,7 +54,7 @@ function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): Tre export function limitScopeBySyntax(context: CompletionContext): PostprocessFilter { return async (input) => { const { position, text, language, prefix, suffix } = context; - if (supportedLanguages.indexOf(language) < 0) { + if (!supportedLanguages.includes(language)) { return input; } const languageConfig = languagesConfigs[language]; diff --git a/clients/tabby-agent/src/types/tabbyApi.d.ts b/clients/tabby-agent/src/types/tabbyApi.d.ts index bd4dea0..e48c76e 100644 --- a/clients/tabby-agent/src/types/tabbyApi.d.ts +++ b/clients/tabby-agent/src/types/tabbyApi.d.ts @@ -11,7 +11,10 @@ export interface paths { post: operations["event"]; }; "/v1/health": { - post: operations["health"]; + get: operations["health"]; + }; + "/v1beta/search": { + get: operations["search"]; }; } @@ -34,8 +37,6 @@ export interface components { * } */ CompletionRequest: { - /** @example def fib(n): */ - prompt?: string | null; /** * @description Language identifier, full list is maintained at * https://code.visualstudio.com/docs/languages/identifiers @@ -43,22 +44,72 @@ export interface components { */ language?: string | null; segments?: components["schemas"]["Segments"] | null; + /** + * @description A unique identifier representing your end-user, which can help Tabby to monitor & generating + * reports. + */ user?: string | null; + debug_options?: components["schemas"]["DebugOptions"] | null; }; + /** + * @example { + * "choices": [ + * { + * "index": 0, + * "text": "string" + * } + * ], + * "id": "string" + * } + */ CompletionResponse: { id: string; choices: components["schemas"]["Choice"][]; + debug_data?: components["schemas"]["DebugData"] | null; + }; + DebugData: { + snippets?: components["schemas"]["Snippet"][] | null; + prompt?: string | null; + }; + DebugOptions: { + /** + * @description When `raw_prompt` is specified, it will be passed directly to the inference engine for completion. `segments` field in `CompletionRequest` will be ignored. + * + * This is useful for certain requests that aim to test the tabby's e2e quality. + */ + raw_prompt?: string | null; + /** @description When true, returns `snippets` in `debug_data`. */ + return_snippets?: boolean; + /** @description When true, returns `prompt` in `debug_data`. */ + return_prompt?: boolean; + /** @description When true, disable retrieval augmented code completion. */ + disable_retrieval_augmented_code_completion?: boolean; }; HealthState: { model: string; + chat_model?: string | null; device: string; - compute_type: string; arch: string; cpu_info: string; cpu_count: number; cuda_devices: string[]; version: components["schemas"]["Version"]; }; + Hit: { + /** Format: float */ + score: number; + doc: components["schemas"]["HitDocument"]; + /** Format: int32 */ + id: number; + }; + HitDocument: { + body: string; + filepath: string; + git_url: string; + kind: string; + language: string; + name: string; + }; LogEventRequest: { /** * @description Event type, should be `view` or `select`. @@ -69,12 +120,22 @@ export interface components { /** Format: int32 */ choice_index: number; }; + SearchResponse: { + num_hits: number; + hits: components["schemas"]["Hit"][]; + }; Segments: { /** @description Content that appears before the cursor in the editor window. */ prefix: string; /** @description Content that appears after the cursor in the editor window. */ suffix?: string | null; }; + Snippet: { + filepath: string; + body: string; + /** Format: float */ + score: number; + }; Version: { build_date: string; build_timestamp: string; @@ -114,6 +175,11 @@ export interface operations { }; }; event: { + parameters: { + query: { + select_kind?: string | null; + }; + }; requestBody: { content: { "application/json": components["schemas"]["LogEventRequest"]; @@ -140,4 +206,25 @@ export interface operations { }; }; }; + search: { + parameters: { + query: { + q: string; + limit?: number | null; + offset?: number | null; + }; + }; + responses: { + /** @description Success */ + 200: { + content: { + "application/json": components["schemas"]["SearchResponse"]; + }; + }; + /** @description When code search is not enabled, the endpoint will returns 501 Not Implemented */ + 501: { + content: never; + }; + }; + }; } diff --git a/clients/tabby-agent/src/utils.ts b/clients/tabby-agent/src/utils.ts index 94f0e3a..460dced 100644 --- a/clients/tabby-agent/src/utils.ts +++ b/clients/tabby-agent/src/utils.ts @@ -102,10 +102,18 @@ export class HttpError extends Error { export function isTimeoutError(error: any) { return ( (error instanceof Error && error.name === "TimeoutError") || - (error instanceof HttpError && [408, 499].indexOf(error.status) !== -1) + (error instanceof HttpError && [408, 499].includes(error.status)) ); } export function isCanceledError(error: any) { return error instanceof Error && error.name === "AbortError"; } + +export function errorToString(error: any) { + let message = error.message || error.toString(); + if (error.cause) { + message += "\nCaused by: " + errorToString(error.cause); + } + return message; +} diff --git a/clients/vscode/.eslintrc.json b/clients/vscode/.eslintrc.json deleted file mode 100644 index 5dfecab..0000000 --- a/clients/vscode/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "@typescript-eslint/naming-convention": "warn", - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" - }, - "ignorePatterns": ["out", "dist", "**/*.d.ts"] -} diff --git a/clients/vscode/src/TabbyStatusBarItem.ts b/clients/vscode/src/TabbyStatusBarItem.ts index 5e74b8c..6d1228d 100644 --- a/clients/vscode/src/TabbyStatusBarItem.ts +++ b/clients/vscode/src/TabbyStatusBarItem.ts @@ -1,5 +1,6 @@ import { StatusBarAlignment, ThemeColor, window } from "vscode"; import { createMachine, interpret } from "@xstate/fsm"; +import type { StatusChangedEvent, AuthRequiredEvent, IssuesUpdatedEvent } from "tabby-agent"; import { agent } from "./agent"; import { notifications } from "./notifications"; import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; @@ -137,12 +138,12 @@ export class TabbyStatusBarItem { this.completionProvider.on("loadingStatusUpdated", () => { this.fsmService.send(agent().getStatus()); }); - agent().on("statusChanged", (event) => { + agent().on("statusChanged", (event: StatusChangedEvent) => { console.debug("Tabby agent statusChanged", { event }); this.fsmService.send(event.status); }); - agent().on("authRequired", (event) => { + agent().on("authRequired", (event: AuthRequiredEvent) => { console.debug("Tabby agent authRequired", { event }); notifications.showInformationStartAuth({ onAuthStart: () => { @@ -154,16 +155,17 @@ export class TabbyStatusBarItem { }); }); - agent().on("issuesUpdated", (event) => { + agent().on("issuesUpdated", (event: IssuesUpdatedEvent) => { console.debug("Tabby agent issuesUpdated", { event }); this.fsmService.send(agent().getStatus()); - if (event.issues.length > 0 && !this.completionResponseWarningShown) { + if (event.issues.includes("connectionFailed")) { + notifications.showInformationWhenDisconnected(); + } else if (!this.completionResponseWarningShown && event.issues.includes("highCompletionTimeoutRate")) { this.completionResponseWarningShown = true; - if (event.issues[0] === "slowCompletionResponseTime") { - notifications.showInformationWhenSlowCompletionResponseTime(); - } else if (event.issues[0] === "highCompletionTimeoutRate") { - notifications.showInformationWhenHighCompletionTimeoutRate(); - } + notifications.showInformationWhenHighCompletionTimeoutRate(); + } else if (!this.completionResponseWarningShown && event.issues.includes("slowCompletionResponseTime")) { + this.completionResponseWarningShown = true; + notifications.showInformationWhenSlowCompletionResponseTime(); } }); } @@ -281,14 +283,16 @@ export class TabbyStatusBarItem { this.item.color = colorWarning; this.item.backgroundColor = backgroundColorWarning; this.item.text = `${iconIssueExist} ${label}`; - const issue = agent().getIssueDetail({ index: 0 }); + const issue = + agent().getIssueDetail({ name: "highCompletionTimeoutRate" }) ?? + agent().getIssueDetail({ name: "slowCompletionResponseTime" }); switch (issue?.name) { - case "slowCompletionResponseTime": - this.item.tooltip = "Completion requests appear to take too much time."; - break; case "highCompletionTimeoutRate": this.item.tooltip = "Most completion requests timed out."; break; + case "slowCompletionResponseTime": + this.item.tooltip = "Completion requests appear to take too much time."; + break; default: this.item.tooltip = ""; break; @@ -299,12 +303,12 @@ export class TabbyStatusBarItem { arguments: [ () => { switch (issue?.name) { - case "slowCompletionResponseTime": - notifications.showInformationWhenSlowCompletionResponseTime(); - break; case "highCompletionTimeoutRate": notifications.showInformationWhenHighCompletionTimeoutRate(); break; + case "slowCompletionResponseTime": + notifications.showInformationWhenSlowCompletionResponseTime(); + break; } }, ], diff --git a/clients/vscode/src/notifications.ts b/clients/vscode/src/notifications.ts index 7165cb6..3c34614 100644 --- a/clients/vscode/src/notifications.ts +++ b/clients/vscode/src/notifications.ts @@ -1,4 +1,9 @@ import { commands, window, workspace, env, ConfigurationTarget, Uri } from "vscode"; +import type { + HighCompletionTimeoutRateIssue, + SlowCompletionResponseTimeIssue, + ConnectionFailedIssue, +} from "tabby-agent"; import { agent } from "./agent"; function showInformationWhenInitializing() { @@ -84,16 +89,37 @@ function showInformationWhenInlineSuggestDisabled() { }); } -function showInformationWhenDisconnected() { - window - .showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings") - .then((selection) => { +function showInformationWhenDisconnected(modal: boolean = false) { + if (modal) { + const message = agent().getIssueDetail({ name: "connectionFailed" })?.message; + window + .showWarningMessage( + `Cannot connect to Tabby Server.`, + { + modal: true, + detail: message, + }, + "Settings", + ) + .then((selection) => { + switch (selection) { + case "Settings": + commands.executeCommand("tabby.openSettings"); + break; + } + }); + } else { + window.showWarningMessage(`Cannot connect to Tabby Server.`, "Detail", "Settings").then((selection) => { switch (selection) { + case "Detail": + showInformationWhenDisconnected(true); + break; case "Settings": commands.executeCommand("tabby.openSettings"); break; } }); + } } function showInformationStartAuth(callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) { @@ -171,7 +197,8 @@ function getHelpMessageForCompletionResponseTimeIssue() { function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { if (modal) { - const stats = agent().getIssueDetail({ name: "slowCompletionResponseTime" })?.completionResponseStats; + const stats = agent().getIssueDetail({ name: "slowCompletionResponseTime" }) + ?.completionResponseStats; let statsMessage = ""; if (stats && stats["responses"] && stats["averageResponseTime"]) { statsMessage = `The average response time of recent ${stats["responses"]} completion requests is ${Number( @@ -212,7 +239,8 @@ function showInformationWhenSlowCompletionResponseTime(modal: boolean = false) { function showInformationWhenHighCompletionTimeoutRate(modal: boolean = false) { if (modal) { - const stats = agent().getIssueDetail({ name: "highCompletionTimeoutRate" })?.completionResponseStats; + const stats = agent().getIssueDetail({ name: "highCompletionTimeoutRate" }) + ?.completionResponseStats; let statsMessage = ""; if (stats && stats["total"] && stats["timeouts"]) { statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`;