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";
|
2023-08-11 11:39:17 +00:00
|
|
|
import { deepmerge } from "deepmerge-ts";
|
|
|
|
|
import { getProperty, setProperty, deleteProperty } from "dot-prop";
|
2023-09-15 03:05:46 +00:00
|
|
|
import createClient from "openapi-fetch";
|
|
|
|
|
import { paths as TabbyApi } from "./types/tabbyApi";
|
|
|
|
|
import { splitLines, isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError } from "./utils";
|
|
|
|
|
import type {
|
2023-06-15 15:53:21 +00:00
|
|
|
Agent,
|
|
|
|
|
AgentStatus,
|
2023-08-17 14:28:41 +00:00
|
|
|
AgentIssue,
|
2023-06-15 15:53:21 +00:00
|
|
|
AgentEvent,
|
|
|
|
|
AgentInitOptions,
|
2023-09-15 03:05:46 +00:00
|
|
|
AbortSignalOption,
|
2023-08-17 14:28:41 +00:00
|
|
|
ServerHealthState,
|
2023-06-15 15:53:21 +00:00
|
|
|
CompletionRequest,
|
|
|
|
|
CompletionResponse,
|
|
|
|
|
LogEventRequest,
|
|
|
|
|
} from "./Agent";
|
|
|
|
|
import { Auth } from "./Auth";
|
2023-08-11 11:39:17 +00:00
|
|
|
import { AgentConfig, PartialAgentConfig, defaultAgentConfig, userAgentConfig } from "./AgentConfig";
|
2023-06-06 13:29:04 +00:00
|
|
|
import { CompletionCache } from "./CompletionCache";
|
2023-09-01 05:30:53 +00:00
|
|
|
import { CompletionDebounce } from "./CompletionDebounce";
|
2023-06-15 15:53:21 +00:00
|
|
|
import { DataStore } from "./dataStore";
|
2023-07-13 08:31:20 +00:00
|
|
|
import { postprocess, preCacheProcess } from "./postprocess";
|
2023-06-06 14:25:31 +00:00
|
|
|
import { rootLogger, allLoggers } from "./logger";
|
2023-06-16 08:58:50 +00:00
|
|
|
import { AnonymousUsageLogger } from "./AnonymousUsageLogger";
|
2023-08-17 14:28:41 +00:00
|
|
|
import { ResponseStats, completionResponseTimeStatsStrategy } from "./ResponseStats";
|
2023-05-24 01:50:57 +00:00
|
|
|
|
2023-06-15 15:53:21 +00:00
|
|
|
/**
|
|
|
|
|
* Different from AgentInitOptions or AgentConfig, this may contain non-serializable objects,
|
|
|
|
|
* so it is not suitable for cli, but only used when imported as module by other js project.
|
|
|
|
|
*/
|
|
|
|
|
export type TabbyAgentOptions = {
|
2023-08-11 11:39:17 +00:00
|
|
|
dataStore?: DataStore;
|
2023-06-15 15:53:21 +00:00
|
|
|
};
|
|
|
|
|
|
2023-05-24 01:50:57 +00:00
|
|
|
export class TabbyAgent extends EventEmitter implements Agent {
|
2023-06-06 14:25:31 +00:00
|
|
|
private readonly logger = rootLogger.child({ component: "TabbyAgent" });
|
2023-06-16 08:58:50 +00:00
|
|
|
private anonymousUsageLogger: AnonymousUsageLogger;
|
2023-06-06 13:29:04 +00:00
|
|
|
private config: AgentConfig = defaultAgentConfig;
|
2023-09-10 02:25:23 +00:00
|
|
|
private userConfig: PartialAgentConfig = {}; // config from `~/.tabby-client/agent/config.toml`
|
2023-08-11 11:39:17 +00:00
|
|
|
private clientConfig: PartialAgentConfig = {}; // config from `initialize` and `updateConfig` method
|
2023-06-15 15:53:21 +00:00
|
|
|
private status: AgentStatus = "notInitialized";
|
2023-08-17 14:28:41 +00:00
|
|
|
private issues: AgentIssue["name"][] = [];
|
|
|
|
|
private serverHealthState: ServerHealthState | null = null;
|
2023-09-15 03:05:46 +00:00
|
|
|
private api: ReturnType<typeof createClient<TabbyApi>>;
|
2023-06-15 15:53:21 +00:00
|
|
|
private auth: Auth;
|
|
|
|
|
private dataStore: DataStore | null = null;
|
2023-06-06 13:29:04 +00:00
|
|
|
private completionCache: CompletionCache = new CompletionCache();
|
2023-09-15 03:05:46 +00:00
|
|
|
private completionDebounce: CompletionDebounce = new CompletionDebounce();
|
|
|
|
|
private nonParallelProvideCompletionAbortController: AbortController | null = null;
|
|
|
|
|
private completionResponseStats: ResponseStats = new ResponseStats(completionResponseTimeStatsStrategy);
|
2023-06-15 15:53:21 +00:00
|
|
|
static readonly tryConnectInterval = 1000 * 30; // 30s
|
|
|
|
|
private tryingConnectTimer: ReturnType<typeof setInterval> | null = null;
|
2023-05-24 01:50:57 +00:00
|
|
|
|
2023-06-15 15:53:21 +00:00
|
|
|
private constructor() {
|
2023-05-24 01:50:57 +00:00
|
|
|
super();
|
2023-06-15 15:53:21 +00:00
|
|
|
|
|
|
|
|
this.tryingConnectTimer = setInterval(async () => {
|
|
|
|
|
if (this.status === "disconnected") {
|
|
|
|
|
this.logger.debug("Trying to connect...");
|
|
|
|
|
await this.healthCheck();
|
|
|
|
|
}
|
|
|
|
|
}, TabbyAgent.tryConnectInterval);
|
2023-08-17 14:28:41 +00:00
|
|
|
|
|
|
|
|
this.completionResponseStats.on("healthy", () => {
|
|
|
|
|
this.popIssue("slowCompletionResponseTime");
|
|
|
|
|
this.popIssue("highCompletionTimeoutRate");
|
|
|
|
|
});
|
|
|
|
|
this.completionResponseStats.on("highTimeoutRate", () => {
|
|
|
|
|
if (this.status === "ready" || this.status === "issuesExist") {
|
|
|
|
|
this.popIssue("slowCompletionResponseTime");
|
|
|
|
|
this.pushIssue("highCompletionTimeoutRate");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.completionResponseStats.on("slowResponseTime", () => {
|
|
|
|
|
if (this.status === "ready" || this.status === "issuesExist") {
|
|
|
|
|
this.popIssue("highCompletionTimeoutRate");
|
|
|
|
|
this.pushIssue("slowCompletionResponseTime");
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-06-06 13:29:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-11 11:39:17 +00:00
|
|
|
static async create(options?: TabbyAgentOptions): Promise<TabbyAgent> {
|
2023-06-15 15:53:21 +00:00
|
|
|
const agent = new TabbyAgent();
|
|
|
|
|
agent.dataStore = options?.dataStore;
|
2023-06-16 08:58:50 +00:00
|
|
|
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
2023-06-15 15:53:21 +00:00
|
|
|
return agent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async applyConfig() {
|
2023-08-11 11:39:17 +00:00
|
|
|
this.config = deepmerge(defaultAgentConfig, this.userConfig, this.clientConfig);
|
2023-06-06 14:25:31 +00:00
|
|
|
allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
|
2023-06-16 08:58:50 +00:00
|
|
|
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
|
2023-08-11 11:39:17 +00:00
|
|
|
if (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 });
|
|
|
|
|
this.auth.on("updated", this.setupApi.bind(this));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-09-15 03:05:46 +00:00
|
|
|
// If `Authorization` request header is provided, use it directly.
|
2023-08-11 11:39:17 +00:00
|
|
|
this.auth = null;
|
2023-06-15 15:53:21 +00:00
|
|
|
}
|
2023-06-24 13:33:33 +00:00
|
|
|
await this.setupApi();
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-24 13:33:33 +00:00
|
|
|
private async setupApi() {
|
2023-09-15 03:05:46 +00:00
|
|
|
this.api = createClient<TabbyApi>({
|
|
|
|
|
baseUrl: this.config.server.endpoint.replace(/\/+$/, ""), // remove trailing slash
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: this.auth?.token ? `Bearer ${this.auth.token}` : undefined,
|
|
|
|
|
...this.config.server.requestHeaders,
|
|
|
|
|
},
|
2023-06-24 13:33:33 +00:00
|
|
|
});
|
2023-06-15 15:53:21 +00:00
|
|
|
await this.healthCheck();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private changeStatus(status: AgentStatus) {
|
2023-05-24 01:50:57 +00:00
|
|
|
if (this.status != status) {
|
|
|
|
|
this.status = status;
|
|
|
|
|
const event: AgentEvent = { event: "statusChanged", status };
|
2023-06-06 14:25:31 +00:00
|
|
|
this.logger.debug({ event }, "Status changed");
|
2023-05-24 16:21:38 +00:00
|
|
|
super.emit("statusChanged", event);
|
2023-08-11 11:39:17 +00:00
|
|
|
if (this.status === "unauthorized") {
|
|
|
|
|
this.emitAuthRequired();
|
|
|
|
|
}
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-17 14:28:41 +00:00
|
|
|
private issueWithDetails(issue: AgentIssue["name"]): AgentIssue {
|
|
|
|
|
switch (issue) {
|
|
|
|
|
case "highCompletionTimeoutRate":
|
|
|
|
|
return {
|
|
|
|
|
name: "highCompletionTimeoutRate",
|
|
|
|
|
completionResponseStats: this.completionResponseStats.stats(),
|
|
|
|
|
};
|
|
|
|
|
case "slowCompletionResponseTime":
|
|
|
|
|
return {
|
|
|
|
|
name: "slowCompletionResponseTime",
|
|
|
|
|
completionResponseStats: this.completionResponseStats.stats(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private pushIssue(issue: AgentIssue["name"]) {
|
|
|
|
|
if (this.issues.indexOf(issue) === -1) {
|
|
|
|
|
this.issues.push(issue);
|
|
|
|
|
this.changeStatus("issuesExist");
|
|
|
|
|
const event: AgentEvent = { event: "newIssue", issue: this.issueWithDetails(issue) };
|
|
|
|
|
this.logger.debug({ event }, "New issue");
|
|
|
|
|
super.emit("newIssue", event);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private popIssue(issue: AgentIssue["name"]) {
|
|
|
|
|
this.issues = this.issues.filter((i) => i !== issue);
|
|
|
|
|
if (this.issues.length === 0 && this.status === "issuesExist") {
|
|
|
|
|
this.changeStatus("ready");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-11 11:39:17 +00:00
|
|
|
private emitAuthRequired() {
|
|
|
|
|
const event: AgentEvent = { event: "authRequired", server: this.config.server };
|
|
|
|
|
super.emit("authRequired", event);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
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);
|
2023-08-17 14:28:41 +00:00
|
|
|
}
|
2023-09-15 03:05:46 +00:00
|
|
|
this.logger.debug({ requestId, path, response: response.data }, "API response");
|
|
|
|
|
if (this.status !== "issuesExist") {
|
|
|
|
|
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") &&
|
|
|
|
|
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;
|
|
|
|
|
}
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
private async healthCheck(options?: AbortSignalOption): Promise<any> {
|
|
|
|
|
try {
|
|
|
|
|
const healthState = await this.post("/v1/health", {}, options);
|
|
|
|
|
if (
|
|
|
|
|
typeof healthState === "object" &&
|
|
|
|
|
healthState["model"] !== undefined &&
|
|
|
|
|
healthState["device"] !== undefined
|
|
|
|
|
) {
|
|
|
|
|
this.serverHealthState = healthState;
|
|
|
|
|
if (this.status === "ready") {
|
|
|
|
|
this.anonymousUsageLogger.uniqueEvent("AgentConnected", healthState);
|
2023-09-05 07:06:35 +00:00
|
|
|
}
|
2023-09-15 03:05:46 +00:00
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
2023-06-15 15:53:21 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-07 16:11:31 +00:00
|
|
|
private createSegments(request: CompletionRequest): { prefix: string; suffix: string } {
|
2023-06-22 03:01:57 +00:00
|
|
|
// max lines in prefix and suffix configurable
|
2023-09-01 05:30:53 +00:00
|
|
|
const maxPrefixLines = this.config.completion.prompt.maxPrefixLines;
|
|
|
|
|
const maxSuffixLines = this.config.completion.prompt.maxSuffixLines;
|
2023-06-02 03:58:34 +00:00
|
|
|
const prefix = request.text.slice(0, request.position);
|
2023-06-07 16:11:31 +00:00
|
|
|
const prefixLines = splitLines(prefix);
|
|
|
|
|
const suffix = request.text.slice(request.position);
|
|
|
|
|
const suffixLines = splitLines(suffix);
|
|
|
|
|
return {
|
2023-06-22 03:01:57 +00:00
|
|
|
prefix: prefixLines.slice(Math.max(prefixLines.length - maxPrefixLines, 0)).join(""),
|
|
|
|
|
suffix: suffixLines.slice(0, maxSuffixLines).join(""),
|
2023-06-07 16:11:31 +00:00
|
|
|
};
|
2023-06-02 03:58:34 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-11 11:39:17 +00:00
|
|
|
public async initialize(options: AgentInitOptions): Promise<boolean> {
|
2023-09-12 05:25:20 +00:00
|
|
|
if (options.client || options.clientProperties) {
|
2023-06-06 14:25:31 +00:00
|
|
|
// Client info is only used in logging for now
|
|
|
|
|
// `pino.Logger.setBindings` is not present in the browser
|
2023-09-12 05:25:20 +00:00
|
|
|
allLoggers.forEach((logger) => logger.setBindings?.({ client: options.client, ...options.clientProperties }));
|
|
|
|
|
this.anonymousUsageLogger.addProperties({ client: options.client, ...options.clientProperties });
|
2023-06-24 13:33:33 +00:00
|
|
|
}
|
2023-07-03 03:19:09 +00:00
|
|
|
if (userAgentConfig) {
|
|
|
|
|
await userAgentConfig.load();
|
|
|
|
|
this.userConfig = userAgentConfig.config;
|
|
|
|
|
userAgentConfig.on("updated", async (config) => {
|
|
|
|
|
this.userConfig = config;
|
|
|
|
|
await this.applyConfig();
|
|
|
|
|
});
|
|
|
|
|
userAgentConfig.watch();
|
|
|
|
|
}
|
2023-06-24 13:33:33 +00:00
|
|
|
if (options.config) {
|
2023-08-11 11:39:17 +00:00
|
|
|
this.clientConfig = options.config;
|
2023-06-06 14:25:31 +00:00
|
|
|
}
|
2023-06-24 13:33:33 +00:00
|
|
|
await this.applyConfig();
|
2023-08-10 09:14:45 +00:00
|
|
|
await this.anonymousUsageLogger.uniqueEvent("AgentInitialized");
|
2023-06-15 15:53:21 +00:00
|
|
|
this.logger.debug({ options }, "Initialized");
|
|
|
|
|
return this.status !== "notInitialized";
|
2023-06-06 13:29:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-11 11:39:17 +00:00
|
|
|
public async updateConfig(key: string, value: any): Promise<boolean> {
|
|
|
|
|
const current = getProperty(this.clientConfig, key);
|
|
|
|
|
if (!deepEqual(current, value)) {
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
deleteProperty(this.clientConfig, key);
|
|
|
|
|
} else {
|
|
|
|
|
setProperty(this.clientConfig, key, value);
|
|
|
|
|
}
|
|
|
|
|
const prevStatus = this.status;
|
2023-06-15 15:53:21 +00:00
|
|
|
await this.applyConfig();
|
2023-08-17 14:28:41 +00:00
|
|
|
// If server config changed, clear server health state
|
|
|
|
|
if (key.startsWith("server")) {
|
|
|
|
|
this.serverHealthState = null;
|
|
|
|
|
}
|
2023-08-11 11:39:17 +00:00
|
|
|
// If status unchanged, `authRequired` will not be emitted when `applyConfig`,
|
|
|
|
|
// so we need to emit it manually.
|
|
|
|
|
if (key.startsWith("server") && prevStatus === "unauthorized" && this.status === "unauthorized") {
|
|
|
|
|
this.emitAuthRequired();
|
|
|
|
|
}
|
2023-06-06 13:29:04 +00:00
|
|
|
const event: AgentEvent = { event: "configUpdated", config: this.config };
|
2023-06-06 14:25:31 +00:00
|
|
|
this.logger.debug({ event }, "Config updated");
|
2023-06-06 13:29:04 +00:00
|
|
|
super.emit("configUpdated", event);
|
|
|
|
|
}
|
2023-06-23 06:43:55 +00:00
|
|
|
return true;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-11 11:39:17 +00:00
|
|
|
public async clearConfig(key: string): Promise<boolean> {
|
|
|
|
|
return await this.updateConfig(key, undefined);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 13:29:04 +00:00
|
|
|
public getConfig(): AgentConfig {
|
|
|
|
|
return this.config;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-15 15:53:21 +00:00
|
|
|
public getStatus(): AgentStatus {
|
2023-05-24 16:21:38 +00:00
|
|
|
return this.status;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-17 14:28:41 +00:00
|
|
|
public getIssues(): AgentIssue[] {
|
|
|
|
|
return this.issues.map((issue) => this.issueWithDetails(issue));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getServerHealthState(): ServerHealthState | null {
|
|
|
|
|
return this.serverHealthState;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
public async requestAuthUrl(options?: AbortSignalOption): Promise<{ authUrl: string; code: string } | null> {
|
2023-06-24 21:43:13 +00:00
|
|
|
if (this.status === "notInitialized") {
|
2023-09-15 03:05:46 +00:00
|
|
|
throw new Error("Agent is not initialized");
|
|
|
|
|
}
|
|
|
|
|
await this.healthCheck(options);
|
|
|
|
|
if (this.status !== "unauthorized") {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
return await this.auth.requestAuthUrl(options);
|
2023-06-24 21:43:13 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
public async waitForAuthToken(code: string, options?: AbortSignalOption): Promise<void> {
|
2023-06-23 06:43:55 +00:00
|
|
|
if (this.status === "notInitialized") {
|
2023-09-15 03:05:46 +00:00
|
|
|
throw new Error("Agent is not initialized");
|
2023-06-23 06:43:55 +00:00
|
|
|
}
|
2023-09-15 03:05:46 +00:00
|
|
|
await this.auth.pollingToken(code, options);
|
|
|
|
|
await this.setupApi();
|
2023-06-15 15:53:21 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
public async provideCompletions(
|
|
|
|
|
request: CompletionRequest,
|
|
|
|
|
options?: AbortSignalOption,
|
|
|
|
|
): Promise<CompletionResponse> {
|
2023-06-15 15:53:21 +00:00
|
|
|
if (this.status === "notInitialized") {
|
2023-09-15 03:05:46 +00:00
|
|
|
throw new Error("Agent is not initialized");
|
2023-06-15 15:53:21 +00:00
|
|
|
}
|
2023-09-15 03:05:46 +00:00
|
|
|
if (this.nonParallelProvideCompletionAbortController) {
|
|
|
|
|
this.nonParallelProvideCompletionAbortController.abort();
|
|
|
|
|
}
|
|
|
|
|
this.nonParallelProvideCompletionAbortController = new AbortController();
|
|
|
|
|
const signal = abortSignalFromAnyOf([this.nonParallelProvideCompletionAbortController.signal, options?.signal]);
|
|
|
|
|
let completionResponse: CompletionResponse | null = null;
|
|
|
|
|
if (this.completionCache.has(request)) {
|
|
|
|
|
// Hit cache
|
|
|
|
|
this.logger.debug({ request }, "Completion cache hit");
|
|
|
|
|
await this.completionDebounce.debounce(
|
|
|
|
|
{
|
|
|
|
|
request,
|
|
|
|
|
config: this.config.completion.debounce,
|
|
|
|
|
responseTime: 0,
|
|
|
|
|
},
|
|
|
|
|
{ signal },
|
|
|
|
|
);
|
|
|
|
|
completionResponse = this.completionCache.get(request);
|
|
|
|
|
} else {
|
|
|
|
|
// No cache
|
|
|
|
|
const segments = this.createSegments(request);
|
|
|
|
|
if (isBlank(segments.prefix)) {
|
|
|
|
|
// Empty prompt
|
|
|
|
|
this.logger.debug("Segment prefix is blank, returning empty completion response");
|
|
|
|
|
completionResponse = {
|
|
|
|
|
id: "agent-" + uuid(),
|
|
|
|
|
choices: [],
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// Request server
|
|
|
|
|
await this.completionDebounce.debounce(
|
|
|
|
|
{
|
2023-09-01 05:30:53 +00:00
|
|
|
request,
|
2023-09-15 03:05:46 +00:00
|
|
|
config: this.config.completion.debounce,
|
|
|
|
|
responseTime: this.completionResponseStats.stats()["averageResponseTime"],
|
|
|
|
|
},
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const requestStartedAt = performance.now();
|
|
|
|
|
const apiPath = "/v1/completions";
|
|
|
|
|
try {
|
|
|
|
|
completionResponse = await this.post(
|
|
|
|
|
apiPath,
|
2023-08-17 14:28:41 +00:00
|
|
|
{
|
2023-09-15 03:05:46 +00:00
|
|
|
body: {
|
|
|
|
|
language: request.language,
|
|
|
|
|
segments,
|
|
|
|
|
user: this.auth?.user,
|
|
|
|
|
},
|
2023-08-17 14:28:41 +00:00
|
|
|
},
|
|
|
|
|
{
|
2023-09-15 03:05:46 +00:00
|
|
|
signal,
|
2023-08-17 14:28:41 +00:00
|
|
|
timeout: request.manually ? this.config.completion.timeout.manually : this.config.completion.timeout.auto,
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-09-15 03:05:46 +00:00
|
|
|
this.completionResponseStats.push({
|
|
|
|
|
name: apiPath,
|
|
|
|
|
status: 200,
|
|
|
|
|
responseTime: performance.now() - requestStartedAt,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// record timed out request in stats, do not record canceled request
|
|
|
|
|
if (isTimeoutError(error)) {
|
|
|
|
|
this.completionResponseStats.push({
|
|
|
|
|
name: apiPath,
|
|
|
|
|
status: error.status,
|
|
|
|
|
responseTime: performance.now() - requestStartedAt,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
completionResponse = await preCacheProcess(request, completionResponse);
|
|
|
|
|
if (options?.signal?.aborted) {
|
|
|
|
|
throw options.signal.reason;
|
|
|
|
|
}
|
|
|
|
|
this.completionCache.set(request, completionResponse);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
completionResponse = await postprocess(request, completionResponse);
|
|
|
|
|
if (options?.signal?.aborted) {
|
|
|
|
|
throw options.signal.reason;
|
|
|
|
|
}
|
|
|
|
|
return completionResponse;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
public async postEvent(request: LogEventRequest, options?: AbortSignalOption): Promise<boolean> {
|
2023-06-15 15:53:21 +00:00
|
|
|
if (this.status === "notInitialized") {
|
2023-09-15 03:05:46 +00:00
|
|
|
throw new Error("Agent is not initialized");
|
2023-06-15 15:53:21 +00:00
|
|
|
}
|
2023-09-25 01:07:25 +00:00
|
|
|
await this.post(
|
|
|
|
|
"/v1/events",
|
|
|
|
|
{
|
|
|
|
|
body: request,
|
|
|
|
|
params: {
|
|
|
|
|
query: {
|
|
|
|
|
select_kind: request.select_kind,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
parseAs: "text",
|
|
|
|
|
},
|
|
|
|
|
options,
|
|
|
|
|
);
|
2023-09-15 03:05:46 +00:00
|
|
|
return true;
|
2023-05-24 01:50:57 +00:00
|
|
|
}
|
|
|
|
|
}
|