2023-05-24 01:50:57 +00:00
|
|
|
import axios from "axios";
|
2023-05-24 16:21:38 +00:00
|
|
|
import { EventEmitter } from "events";
|
2023-06-02 03:58:34 +00:00
|
|
|
import { v4 as uuid } from "uuid";
|
2023-06-06 13:29:04 +00:00
|
|
|
import deepEqual from "deep-equal";
|
|
|
|
|
import deepMerge from "deepmerge";
|
2023-06-02 03:58:34 +00:00
|
|
|
import { TabbyApi, CancelablePromise, ApiError, ChoiceEvent, CompletionEvent } from "./generated";
|
2023-06-06 13:29:04 +00:00
|
|
|
import { sleep, cancelable, splitLines, isBlank } from "./utils";
|
|
|
|
|
import { Agent, AgentEvent, AgentInitOptions, CompletionRequest, CompletionResponse } from "./Agent";
|
|
|
|
|
import { AgentConfig, defaultAgentConfig } from "./AgentConfig";
|
|
|
|
|
import { CompletionCache } from "./CompletionCache";
|
2023-05-24 01:50:57 +00:00
|
|
|
|
|
|
|
|
export class TabbyAgent extends EventEmitter implements Agent {
|
2023-06-06 13:29:04 +00:00
|
|
|
private config: AgentConfig = defaultAgentConfig;
|
2023-05-24 01:50:57 +00:00
|
|
|
private status: "connecting" | "ready" | "disconnected" = "connecting";
|
|
|
|
|
private api: TabbyApi;
|
2023-06-06 13:29:04 +00:00
|
|
|
private completionCache: CompletionCache = new CompletionCache();
|
2023-05-24 01:50:57 +00:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
2023-06-06 13:29:04 +00:00
|
|
|
this.onConfigUpdated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onConfigUpdated() {
|
|
|
|
|
this.api = new TabbyApi({ BASE: this.config.server.endpoint });
|
2023-05-24 01:50:57 +00:00
|
|
|
this.ping();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private changeStatus(status: "connecting" | "ready" | "disconnected") {
|
|
|
|
|
if (this.status != status) {
|
|
|
|
|
this.status = status;
|
|
|
|
|
const event: AgentEvent = { event: "statusChanged", status };
|
2023-05-24 16:21:38 +00:00
|
|
|
super.emit("statusChanged", event);
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ping(tries: number = 0): Promise<boolean> {
|
|
|
|
|
try {
|
2023-06-06 13:29:04 +00:00
|
|
|
await axios.get(this.config.server.endpoint);
|
2023-05-24 01:50:57 +00:00
|
|
|
this.changeStatus("ready");
|
|
|
|
|
return true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (tries > 5) {
|
|
|
|
|
this.changeStatus("disconnected");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this.changeStatus("connecting");
|
|
|
|
|
const pingRetryDelay = 1000;
|
|
|
|
|
await sleep(pingRetryDelay);
|
|
|
|
|
return this.ping(tries + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private wrapApiPromise<T>(promise: CancelablePromise<T>): CancelablePromise<T> {
|
2023-05-29 02:09:44 +00:00
|
|
|
return cancelable(
|
2023-05-24 01:50:57 +00:00
|
|
|
promise
|
2023-05-29 02:09:44 +00:00
|
|
|
.then((resolved: T) => {
|
2023-05-24 01:50:57 +00:00
|
|
|
this.changeStatus("ready");
|
2023-05-29 02:09:44 +00:00
|
|
|
return resolved;
|
2023-05-24 01:50:57 +00:00
|
|
|
})
|
|
|
|
|
.catch((err: ApiError) => {
|
|
|
|
|
this.changeStatus("disconnected");
|
2023-05-29 02:09:44 +00:00
|
|
|
throw err;
|
|
|
|
|
}),
|
|
|
|
|
() => {
|
2023-05-24 01:50:57 +00:00
|
|
|
promise.cancel();
|
2023-05-29 02:09:44 +00:00
|
|
|
}
|
|
|
|
|
);
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-02 03:58:34 +00:00
|
|
|
private createPrompt(request: CompletionRequest): string {
|
|
|
|
|
const maxLines = 20;
|
|
|
|
|
const prefix = request.text.slice(0, request.position);
|
|
|
|
|
const lines = splitLines(prefix);
|
|
|
|
|
const cutoff = Math.max(lines.length - maxLines, 0);
|
|
|
|
|
const prompt = lines.slice(cutoff).join("");
|
|
|
|
|
return prompt;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 13:29:04 +00:00
|
|
|
public initialize(params: AgentInitOptions): boolean {
|
|
|
|
|
if (params.config) {
|
|
|
|
|
this.updateConfig(params.config);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public updateConfig(config: AgentConfig): boolean {
|
|
|
|
|
if (!deepEqual(this.config, config)) {
|
|
|
|
|
this.config = deepMerge(this.config, config);
|
|
|
|
|
this.onConfigUpdated();
|
|
|
|
|
const event: AgentEvent = { event: "configUpdated", config: this.config };
|
|
|
|
|
super.emit("configUpdated", event);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-06 13:29:04 +00:00
|
|
|
public getConfig(): AgentConfig {
|
|
|
|
|
return this.config;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-05-24 16:21:38 +00:00
|
|
|
public getStatus(): "connecting" | "ready" | "disconnected" {
|
|
|
|
|
return this.status;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-24 01:50:57 +00:00
|
|
|
public getCompletions(request: CompletionRequest): CancelablePromise<CompletionResponse> {
|
2023-05-29 02:09:44 +00:00
|
|
|
if (this.completionCache.has(request)) {
|
|
|
|
|
return new CancelablePromise((resolve) => {
|
|
|
|
|
resolve(this.completionCache.get(request));
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-06-02 03:58:34 +00:00
|
|
|
const prompt = this.createPrompt(request);
|
|
|
|
|
if (isBlank(prompt)) {
|
|
|
|
|
// Create a empty completion response
|
|
|
|
|
return new CancelablePromise((resolve) => {
|
|
|
|
|
resolve({
|
|
|
|
|
id: "agent-" + uuid(),
|
|
|
|
|
created: new Date().getTime(),
|
2023-06-06 13:29:04 +00:00
|
|
|
choices: [],
|
2023-06-02 03:58:34 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-06-06 13:29:04 +00:00
|
|
|
const promise = this.wrapApiPromise(
|
|
|
|
|
this.api.default.completionsV1CompletionsPost({
|
|
|
|
|
prompt,
|
|
|
|
|
language: request.language,
|
|
|
|
|
})
|
|
|
|
|
);
|
2023-05-29 02:09:44 +00:00
|
|
|
return cancelable(
|
|
|
|
|
promise.then((response: CompletionResponse) => {
|
|
|
|
|
this.completionCache.set(request, response);
|
|
|
|
|
return response;
|
|
|
|
|
}),
|
|
|
|
|
() => {
|
|
|
|
|
promise.cancel();
|
|
|
|
|
}
|
|
|
|
|
);
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-06 13:29:04 +00:00
|
|
|
public postEvent(request: ChoiceEvent | CompletionEvent): CancelablePromise<boolean> {
|
2023-05-29 02:09:44 +00:00
|
|
|
return this.wrapApiPromise(this.api.default.eventsV1EventsPost(request));
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
}
|