2024-07-08 18:35:34 +08:00
|
|
|
|
import axios, { AxiosResponse, CancelTokenSource } from "axios";
|
2024-07-08 22:18:00 +08:00
|
|
|
|
import * as path from "path";
|
|
|
|
|
import * as fs from "fs";
|
|
|
|
|
import * as os from "os";
|
2024-07-08 18:35:34 +08:00
|
|
|
|
|
|
|
|
|
import { logger } from "../util/logger";
|
|
|
|
|
import { getFileContent } from "../util/commonUtil";
|
|
|
|
|
|
|
|
|
|
import { UiUtilWrapper } from "../util/uiUtil";
|
|
|
|
|
|
|
|
|
|
function timeThis(
|
|
|
|
|
target: Object,
|
|
|
|
|
propertyKey: string,
|
|
|
|
|
descriptor: TypedPropertyDescriptor<any>
|
|
|
|
|
) {
|
|
|
|
|
const originalMethod = descriptor.value;
|
|
|
|
|
|
|
|
|
|
descriptor.value = async function (...args: any[]) {
|
|
|
|
|
const start = process.hrtime.bigint();
|
|
|
|
|
const result = await originalMethod.apply(this, args);
|
|
|
|
|
const end = process.hrtime.bigint();
|
|
|
|
|
const nanoseconds = end - start;
|
|
|
|
|
const seconds = Number(nanoseconds) / 1e9;
|
|
|
|
|
|
|
|
|
|
const className = target.constructor.name;
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(`Exec time [${className}.${propertyKey}]: ${seconds} s`);
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return descriptor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ChatRequest {
|
|
|
|
|
content: string;
|
|
|
|
|
model_name: string;
|
|
|
|
|
api_key: string;
|
|
|
|
|
api_base: string;
|
|
|
|
|
parent?: string;
|
|
|
|
|
context?: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ChatResponse {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
|
"prompt-hash": string;
|
|
|
|
|
user: string;
|
|
|
|
|
date: string;
|
|
|
|
|
response: string;
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
|
finish_reason: string;
|
|
|
|
|
isError: boolean;
|
|
|
|
|
extra?: object;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LogData {
|
|
|
|
|
model: string;
|
|
|
|
|
messages: object[];
|
|
|
|
|
parent?: string;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
request_tokens: number;
|
|
|
|
|
response_tokens: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LogInsertRes {
|
|
|
|
|
hash?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 18:57:15 +08:00
|
|
|
|
export interface LogDeleteRes {
|
|
|
|
|
success?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ShortLog {
|
|
|
|
|
hash: string;
|
|
|
|
|
parent: string | null;
|
|
|
|
|
user: string;
|
|
|
|
|
date: string;
|
|
|
|
|
request: string;
|
|
|
|
|
responses: string[];
|
|
|
|
|
context: Array<{
|
|
|
|
|
content: string;
|
|
|
|
|
role: string;
|
|
|
|
|
}>;
|
|
|
|
|
}
|
2024-07-08 18:35:34 +08:00
|
|
|
|
|
|
|
|
|
export async function buildRoleContextsFromFiles(
|
|
|
|
|
files: string[] | undefined
|
|
|
|
|
): Promise<object[]> {
|
|
|
|
|
const contexts: object[] = [];
|
|
|
|
|
if (!files) {
|
|
|
|
|
return contexts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const content = await getFileContent(file);
|
|
|
|
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
contexts.push({
|
|
|
|
|
role: "system",
|
|
|
|
|
content: `<context>${content}</context>`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return contexts;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 22:20:37 +08:00
|
|
|
|
// TODO: 在插件启动为每个vscode窗口启动一个devchat local service
|
|
|
|
|
// 1. 分配单独的端口号,该窗口的所有请求都通过该端口号发送 (22222仅为作为开发默认端口号,不应用于生产)
|
|
|
|
|
// 2. 启动local service时要配置多个worker,以便处理并发请求
|
|
|
|
|
// TODO: 在插件关闭时,关闭其对应的devchat local service
|
|
|
|
|
|
2024-07-08 18:35:34 +08:00
|
|
|
|
export class DevChatClient {
|
|
|
|
|
private baseURL: string;
|
|
|
|
|
|
|
|
|
|
private _cancelMessageToken: CancelTokenSource | null = null;
|
|
|
|
|
|
2024-07-09 10:42:43 +08:00
|
|
|
|
static readonly logRawDataSizeLimit = 4 * 1024;
|
2024-07-08 22:18:00 +08:00
|
|
|
|
|
2024-07-08 22:20:37 +08:00
|
|
|
|
// TODO: init devchat client with a port number
|
|
|
|
|
// TODO: the default 22222 is for dev only, should not be used in production
|
|
|
|
|
constructor(port: number = 22222) {
|
|
|
|
|
this.baseURL = `http://localhost:${port}`;
|
2024-07-08 18:35:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 10:42:43 +08:00
|
|
|
|
async _get(path: string, config?: any): Promise<AxiosResponse> {
|
2024-07-08 18:35:34 +08:00
|
|
|
|
try {
|
|
|
|
|
logger.channel()?.debug(`GET request to ${this.baseURL}${path}`);
|
2024-07-09 10:42:43 +08:00
|
|
|
|
const response = await axios.get(`${this.baseURL}${path}`, config);
|
2024-07-08 18:35:34 +08:00
|
|
|
|
return response;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async _post(path: string, data: any = undefined): Promise<AxiosResponse> {
|
|
|
|
|
try {
|
2024-07-09 10:42:43 +08:00
|
|
|
|
logger.channel()?.debug(`POST request to ${this.baseURL}${path}`);
|
2024-07-08 18:35:34 +08:00
|
|
|
|
const response = await axios.post(`${this.baseURL}${path}`, data);
|
|
|
|
|
return response;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 21:18:55 +08:00
|
|
|
|
@timeThis
|
|
|
|
|
async getWorkflowList(): Promise<any> {
|
2024-07-09 18:00:11 +08:00
|
|
|
|
const response = await this._get("/workflows/list");
|
2024-07-08 21:18:55 +08:00
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(
|
|
|
|
|
`getWorkflowList response data: \n${JSON.stringify(
|
|
|
|
|
response.data
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@timeThis
|
|
|
|
|
async getWorkflowConfig(): Promise<any> {
|
2024-07-09 18:00:11 +08:00
|
|
|
|
const response = await this._get("/workflows/config");
|
2024-07-08 21:18:55 +08:00
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(
|
|
|
|
|
`getWorkflowConfig response data: \n${JSON.stringify(
|
|
|
|
|
response.data
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
2024-07-08 18:35:34 +08:00
|
|
|
|
|
2024-07-08 19:33:08 +08:00
|
|
|
|
@timeThis
|
|
|
|
|
async updateWorkflows(): Promise<void> {
|
2024-07-09 18:00:11 +08:00
|
|
|
|
const response = await this._post("/workflows/update");
|
2024-07-08 19:33:08 +08:00
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(
|
|
|
|
|
`updateWorkflows response data: \n${JSON.stringify(
|
|
|
|
|
response.data
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 18:35:34 +08:00
|
|
|
|
@timeThis
|
|
|
|
|
async message(
|
|
|
|
|
message: ChatRequest,
|
|
|
|
|
onData: (data: ChatResponse) => void
|
|
|
|
|
): Promise<ChatResponse> {
|
|
|
|
|
this._cancelMessageToken = axios.CancelToken.source();
|
|
|
|
|
const workspace = UiUtilWrapper.workspaceFoldersFirstPath();
|
|
|
|
|
// const workspace = undefined;
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
...message,
|
|
|
|
|
workspace: workspace,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new Promise<ChatResponse>(async (resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.post(
|
|
|
|
|
`${this.baseURL}/message/msg`,
|
|
|
|
|
data,
|
|
|
|
|
{
|
|
|
|
|
responseType: "stream",
|
|
|
|
|
cancelToken: this._cancelMessageToken!.token,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
const chatRes: ChatResponse = {
|
|
|
|
|
"prompt-hash": "", // TODO: prompt-hash is not in chatting response
|
|
|
|
|
user: "",
|
|
|
|
|
date: "",
|
|
|
|
|
response: "",
|
|
|
|
|
finish_reason: "",
|
|
|
|
|
isError: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
response.data.on("data", (chunk) => {
|
|
|
|
|
const chunkData = JSON.parse(chunk.toString());
|
|
|
|
|
|
|
|
|
|
if (chatRes.user === "") {
|
|
|
|
|
chatRes.user = chunkData["user"];
|
|
|
|
|
}
|
|
|
|
|
if (chatRes.date === "") {
|
|
|
|
|
chatRes.date = chunkData["date"];
|
|
|
|
|
}
|
|
|
|
|
chatRes.finish_reason = chunkData["finish_reason"];
|
|
|
|
|
if (chatRes.finish_reason === "should_run_workflow") {
|
|
|
|
|
chatRes.extra = chunkData["extra"];
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
2024-07-08 22:20:37 +08:00
|
|
|
|
?.debug("should run workflow via cli.");
|
2024-07-08 18:35:34 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chatRes.isError = chunkData["isError"];
|
|
|
|
|
|
|
|
|
|
chatRes.response += chunkData["content"];
|
|
|
|
|
onData(chatRes);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
response.data.on("end", () => {
|
|
|
|
|
resolve(chatRes); // Resolve the promise with chatRes when the stream ends
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
response.data.on("error", (error) => {
|
|
|
|
|
logger.channel()?.error("Streaming error:", error);
|
2024-07-09 10:42:43 +08:00
|
|
|
|
// TODO: handle error?
|
2024-07-08 18:35:34 +08:00
|
|
|
|
reject(error); // Reject the promise on error
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2024-07-09 10:42:43 +08:00
|
|
|
|
// TODO: handle error?
|
2024-07-08 18:35:34 +08:00
|
|
|
|
reject(error); // Reject the promise if the request fails
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancelMessage(): void {
|
|
|
|
|
if (this._cancelMessageToken) {
|
|
|
|
|
this._cancelMessageToken.cancel(
|
|
|
|
|
"Message request cancelled by user"
|
|
|
|
|
);
|
|
|
|
|
this._cancelMessageToken = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert a message log.
|
|
|
|
|
*
|
|
|
|
|
* @param logData - The log data to be inserted.
|
|
|
|
|
* @returns A tuple of inserted hash and error message.
|
|
|
|
|
*/
|
|
|
|
|
@timeThis
|
|
|
|
|
async insertLog(logData: LogData): Promise<LogInsertRes> {
|
2024-07-08 22:18:00 +08:00
|
|
|
|
let body = {
|
2024-07-08 18:35:34 +08:00
|
|
|
|
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
|
|
|
|
};
|
2024-07-08 22:18:00 +08:00
|
|
|
|
|
|
|
|
|
const jsondata = JSON.stringify(logData);
|
|
|
|
|
let filepath = "";
|
|
|
|
|
|
|
|
|
|
if (jsondata.length <= DevChatClient.logRawDataSizeLimit) {
|
|
|
|
|
// Use json data directly
|
|
|
|
|
body["jsondata"] = jsondata;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// Write json data to a temp file
|
|
|
|
|
const tempDir = os.tmpdir();
|
|
|
|
|
const tempFile = path.join(tempDir, "devchat_log_insert.json");
|
|
|
|
|
await fs.promises.writeFile(tempFile, jsondata);
|
|
|
|
|
filepath = tempFile;
|
|
|
|
|
body["filepath"] = filepath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await this._post("/logs/insert", body);
|
2024-07-08 18:35:34 +08:00
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(
|
|
|
|
|
`insertLog response data: ${JSON.stringify(
|
|
|
|
|
response.data
|
|
|
|
|
)}, ${typeof response.data}}`
|
|
|
|
|
);
|
2024-07-08 22:18:00 +08:00
|
|
|
|
|
|
|
|
|
// Clean up temp file
|
|
|
|
|
if (filepath) {
|
|
|
|
|
try {
|
|
|
|
|
await fs.promises.unlink(filepath);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.channel()?.error(`Failed to delete temp file ${filepath}: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-08 18:35:34 +08:00
|
|
|
|
|
|
|
|
|
const res: LogInsertRes = {
|
|
|
|
|
hash: response.data["hash"],
|
|
|
|
|
};
|
|
|
|
|
return res;
|
|
|
|
|
}
|
2024-07-08 18:57:15 +08:00
|
|
|
|
|
|
|
|
|
@timeThis
|
|
|
|
|
async deleteLog(logHash: string): Promise<LogDeleteRes> {
|
|
|
|
|
const data = {
|
|
|
|
|
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
|
|
|
|
hash: logHash,
|
|
|
|
|
};
|
|
|
|
|
const response = await this._post("/logs/delete", data);
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(
|
|
|
|
|
`deleteLog response data: ${JSON.stringify(
|
|
|
|
|
response.data
|
|
|
|
|
)}, ${typeof response.data}}`
|
|
|
|
|
);
|
2024-07-11 10:32:42 +08:00
|
|
|
|
|
2024-07-08 18:57:15 +08:00
|
|
|
|
const res: LogDeleteRes = {
|
|
|
|
|
success: response.data["success"],
|
|
|
|
|
};
|
|
|
|
|
return res;
|
2024-07-11 10:32:42 +08:00
|
|
|
|
}
|
2024-07-08 18:57:15 +08:00
|
|
|
|
|
2024-07-08 19:04:16 +08:00
|
|
|
|
@timeThis
|
|
|
|
|
async getTopicLogs(
|
|
|
|
|
topicRootHash: string,
|
|
|
|
|
limit: number,
|
|
|
|
|
offset: number
|
|
|
|
|
): Promise<ShortLog[]> {
|
|
|
|
|
const data = {
|
|
|
|
|
limit: limit,
|
|
|
|
|
offset: offset,
|
|
|
|
|
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
|
|
|
|
};
|
2024-07-09 10:42:43 +08:00
|
|
|
|
const response = await this._get(
|
|
|
|
|
`/topics/${topicRootHash}/logs`,
|
2024-07-08 19:04:16 +08:00
|
|
|
|
{
|
|
|
|
|
params: data,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const logs: ShortLog[] = response.data;
|
|
|
|
|
logs.reverse();
|
|
|
|
|
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(`getTopicLogs response data: ${JSON.stringify(logs)}`);
|
|
|
|
|
|
|
|
|
|
return logs;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-08 19:12:06 +08:00
|
|
|
|
@timeThis
|
2024-07-08 22:20:37 +08:00
|
|
|
|
async getTopics(limit: number, offset: number): Promise<any[]> {
|
2024-07-08 19:12:06 +08:00
|
|
|
|
const data = {
|
|
|
|
|
limit: limit,
|
|
|
|
|
offset: offset,
|
|
|
|
|
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
|
|
|
|
};
|
2024-07-09 10:42:43 +08:00
|
|
|
|
const response = await this._get(`/topics`, {
|
2024-07-08 19:12:06 +08:00
|
|
|
|
params: data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const topics: any[] = response.data;
|
|
|
|
|
topics.reverse();
|
|
|
|
|
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
|
|
|
|
?.debug(`getTopics response data: ${JSON.stringify(topics)}`);
|
|
|
|
|
|
|
|
|
|
return topics;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@timeThis
|
2024-07-08 22:20:37 +08:00
|
|
|
|
async deleteTopic(topicRootHash: string): Promise<void> {
|
2024-07-08 19:12:06 +08:00
|
|
|
|
const data = {
|
|
|
|
|
topic_hash: topicRootHash,
|
|
|
|
|
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
|
|
|
|
};
|
2024-07-08 22:20:37 +08:00
|
|
|
|
|
2024-07-08 19:12:06 +08:00
|
|
|
|
const response = await this._post("/topics/delete", data);
|
|
|
|
|
|
|
|
|
|
logger
|
|
|
|
|
.channel()
|
2024-07-08 22:20:37 +08:00
|
|
|
|
?.debug(
|
|
|
|
|
`deleteTopic response data: ${JSON.stringify(response.data)}`
|
|
|
|
|
);
|
2024-07-08 19:12:06 +08:00
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-07-08 19:04:16 +08:00
|
|
|
|
|
2024-07-08 18:35:34 +08:00
|
|
|
|
stopAllRequest(): void {
|
|
|
|
|
this.cancelMessage();
|
|
|
|
|
// add other requests here if needed
|
|
|
|
|
}
|
2024-07-08 22:20:37 +08:00
|
|
|
|
}
|