feat(vscode): add notification when failed to connect to server. (#808)
parent
d0c9b56467
commit
adb4bcd13f
|
|
@ -2,14 +2,11 @@
|
|||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Tabby Server",
|
||||
"description": "\n[](https://github.com/TabbyML/tabby)\n\nOpenAPI documentation for [tabby](https://github.com/TabbyML/tabby), a self-hosted AI coding assistant.",
|
||||
"description": "\n[](https://github.com/TabbyML/tabby)\n[](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"],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ export type HighCompletionTimeoutRateIssue = {
|
|||
name: "highCompletionTimeoutRate";
|
||||
completionResponseStats: Record<string, number>;
|
||||
};
|
||||
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<T extends AgentIssue>(options: { index?: number; name?: T["name"] }): T | null;
|
||||
|
||||
/**
|
||||
* @returns server info returned from latest server health check, returns null if not available
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<typeof createClient<TabbyApi>>;
|
||||
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<T extends Parameters<typeof this.api.POST>[0]>(
|
||||
path: T,
|
||||
requestOptions: Parameters<typeof this.api.POST<T>>[1],
|
||||
abortOptions?: { signal?: AbortSignal; timeout?: number },
|
||||
): Promise<Awaited<ReturnType<typeof this.api.POST<T>>>["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<any> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
if (this.status === "ready" || this.status === "notInitialized") {
|
||||
this.changeStatus("disconnected");
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T extends AgentIssue>(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",
|
||||
{
|
||||
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,
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export {
|
|||
IssuesUpdatedEvent,
|
||||
SlowCompletionResponseTimeIssue,
|
||||
HighCompletionTimeoutRateIssue,
|
||||
ConnectionFailedIssue,
|
||||
ClientProperties,
|
||||
AgentInitOptions,
|
||||
ServerHealthState,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export async function calculateReplaceRangeBySyntax(
|
|||
context: CompletionContext,
|
||||
): Promise<CompletionResponse> {
|
||||
const { position, prefix, suffix, prefixLines, suffixLines, language } = context;
|
||||
if (supportedLanguages.indexOf(language) < 0) {
|
||||
if (!supportedLanguages.includes(language)) {
|
||||
return response;
|
||||
}
|
||||
const languageConfig = languagesConfigs[language];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,9 +89,18 @@ function showInformationWhenInlineSuggestDisabled() {
|
|||
});
|
||||
}
|
||||
|
||||
function showInformationWhenDisconnected() {
|
||||
function showInformationWhenDisconnected(modal: boolean = false) {
|
||||
if (modal) {
|
||||
const message = agent().getIssueDetail<ConnectionFailedIssue>({ name: "connectionFailed" })?.message;
|
||||
window
|
||||
.showInformationMessage("Cannot connect to Tabby Server. Please check settings.", "Settings")
|
||||
.showWarningMessage(
|
||||
`Cannot connect to Tabby Server.`,
|
||||
{
|
||||
modal: true,
|
||||
detail: message,
|
||||
},
|
||||
"Settings",
|
||||
)
|
||||
.then((selection) => {
|
||||
switch (selection) {
|
||||
case "Settings":
|
||||
|
|
@ -94,6 +108,18 @@ function showInformationWhenDisconnected() {
|
|||
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<SlowCompletionResponseTimeIssue>({ 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<HighCompletionTimeoutRateIssue>({ name: "highCompletionTimeoutRate" })
|
||||
?.completionResponseStats;
|
||||
let statsMessage = "";
|
||||
if (stats && stats["total"] && stats["timeouts"]) {
|
||||
statsMessage = `${stats["timeouts"]} of ${stats["total"]} completion requests timed out.\n\n`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue