Init DevChatClient and DevChatCLI
This commit is contained in:
parent
1110171074
commit
d1f712ff90
294
src/toolwrapper/devchatCLI.ts
Normal file
294
src/toolwrapper/devchatCLI.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
import { logger } from "../util/logger";
|
||||
import { CommandRun } from "../util/commonUtil";
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
import { ApiKeyManager } from "../util/apiKey";
|
||||
import { assertValue } from "../util/check";
|
||||
import { getFileContent } from "../util/commonUtil";
|
||||
import { DevChatConfig } from "../util/config";
|
||||
|
||||
import { getMicromambaUrl } from "../util/python_installer/conda_url";
|
||||
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
|
||||
const envPath = path.join(__dirname, "..", ".env");
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
export interface ChatOptions {
|
||||
parent?: string;
|
||||
reference?: string[];
|
||||
header?: 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;
|
||||
}
|
||||
|
||||
export class DevChatCLI {
|
||||
private commandRun: CommandRun;
|
||||
|
||||
constructor() {
|
||||
this.commandRun = new CommandRun();
|
||||
}
|
||||
|
||||
private async buildArgs(options: ChatOptions): Promise<string[]> {
|
||||
let args = ["-m", "devchat", "route"];
|
||||
|
||||
if (options.reference) {
|
||||
for (const reference of options.reference) {
|
||||
args.push("-r", reference);
|
||||
}
|
||||
}
|
||||
if (options.header) {
|
||||
for (const header of options.header) {
|
||||
args.push("-i", header);
|
||||
}
|
||||
}
|
||||
if (options.context) {
|
||||
for (const context of options.context) {
|
||||
args.push("-c", context);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.parent) {
|
||||
args.push("-p", options.parent);
|
||||
}
|
||||
|
||||
const llmModelData = await ApiKeyManager.llmModel();
|
||||
assertValue(
|
||||
!llmModelData || !llmModelData.model,
|
||||
"You must select a LLM model to use for conversations"
|
||||
);
|
||||
args.push("-m", llmModelData.model);
|
||||
|
||||
const functionCalling = DevChatConfig.getInstance().get(
|
||||
"enable_function_calling"
|
||||
);
|
||||
if (functionCalling) {
|
||||
args.push("-a");
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private parseOutData(stdout: string, isPartial: boolean): ChatResponse {
|
||||
const responseLines = stdout.trim().split("\n");
|
||||
|
||||
if (responseLines.length < 2) {
|
||||
return this.createChatResponse("", "", "", "", !isPartial);
|
||||
}
|
||||
// logger.channel()?.info(`\n-responseLines: ${responseLines}`);
|
||||
|
||||
const [userLine, remainingLines1] = this.extractLine(
|
||||
responseLines,
|
||||
"User: "
|
||||
);
|
||||
const user = this.parseLine(userLine, /User: (.+)/);
|
||||
|
||||
const [dateLine, remainingLines2] = this.extractLine(
|
||||
remainingLines1,
|
||||
"Date: "
|
||||
);
|
||||
const date = this.parseLine(dateLine, /Date: (.+)/);
|
||||
|
||||
const [promptHashLine, remainingLines3] = this.extractLine(
|
||||
remainingLines2,
|
||||
"prompt"
|
||||
);
|
||||
const [finishReasonLine, remainingLines4] = this.extractLine(
|
||||
remainingLines3,
|
||||
"finish_reason:"
|
||||
);
|
||||
|
||||
if (!promptHashLine) {
|
||||
return this.createChatResponse(
|
||||
"",
|
||||
user,
|
||||
date,
|
||||
remainingLines4.join("\n"),
|
||||
!isPartial
|
||||
);
|
||||
}
|
||||
|
||||
const finishReason = finishReasonLine.split(" ")[1];
|
||||
const promptHash = promptHashLine.split(" ")[1];
|
||||
const response = remainingLines4.join("\n");
|
||||
|
||||
return this.createChatResponse(
|
||||
promptHash,
|
||||
user,
|
||||
date,
|
||||
response,
|
||||
false,
|
||||
finishReason
|
||||
);
|
||||
}
|
||||
|
||||
private extractLine(
|
||||
lines: string[],
|
||||
startWith: string
|
||||
): [string, string[]] {
|
||||
const index = lines.findIndex((line) => line.startsWith(startWith));
|
||||
const extractedLine = index !== -1 ? lines.splice(index, 1)[0] : "";
|
||||
return [extractedLine, lines];
|
||||
}
|
||||
|
||||
private parseLine(line: string, regex: RegExp): string {
|
||||
return line.match(regex)?.[1] ?? "";
|
||||
}
|
||||
|
||||
private createChatResponse(
|
||||
promptHash: string,
|
||||
user: string,
|
||||
date: string,
|
||||
response: string,
|
||||
isError: boolean,
|
||||
finishReason = ""
|
||||
): ChatResponse {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"prompt-hash": promptHash,
|
||||
user,
|
||||
date,
|
||||
response,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
finish_reason: finishReason,
|
||||
isError,
|
||||
};
|
||||
}
|
||||
|
||||
public input(data: string) {
|
||||
this.commandRun?.write(data + "\n");
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.commandRun.stop();
|
||||
}
|
||||
|
||||
async runWorkflow(
|
||||
content: string,
|
||||
options: ChatOptions = {},
|
||||
onData: (data: ChatResponse) => void,
|
||||
): Promise<ChatResponse> {
|
||||
// TODO: Use another cli command to run workflow instead of `devchat route`
|
||||
try {
|
||||
// build args for devchat prompt command
|
||||
const args = await this.buildArgs(options);
|
||||
args.push("--");
|
||||
args.push(content);
|
||||
|
||||
// build env variables for prompt command
|
||||
const llmModelData = await ApiKeyManager.llmModel();
|
||||
assertValue(!llmModelData, "No valid llm model selected");
|
||||
const envs = {
|
||||
...process.env,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
PYTHONUTF8: 1,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
command_python:
|
||||
DevChatConfig.getInstance().get("python_for_commands") ||
|
||||
"",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
PYTHONPATH:
|
||||
UiUtilWrapper.extensionPath() + "/tools/site-packages",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
OPENAI_API_KEY: llmModelData.api_key.trim(),
|
||||
DEVCHAT_UNIT_TESTS_USE_USER_MODEL: 1,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
...(llmModelData.api_base
|
||||
? {
|
||||
OPENAI_API_BASE: llmModelData.api_base,
|
||||
OPENAI_BASE_URL: llmModelData.api_base,
|
||||
}
|
||||
: {}),
|
||||
DEVCHAT_PROXY:
|
||||
DevChatConfig.getInstance().get("DEVCHAT_PROXY") || "",
|
||||
MAMBA_BIN_PATH: getMicromambaUrl(),
|
||||
};
|
||||
|
||||
// build process options
|
||||
const spawnAsyncOptions = {
|
||||
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
env: envs,
|
||||
};
|
||||
|
||||
logger
|
||||
.channel()
|
||||
?.info(
|
||||
`api_key: ${llmModelData.api_key.replace(
|
||||
/^(.{4})(.*)(.{4})$/,
|
||||
(_, first, middle, last) =>
|
||||
first + middle.replace(/./g, "*") + last
|
||||
)}`
|
||||
);
|
||||
logger.channel()?.info(`api_base: ${llmModelData.api_base}`);
|
||||
|
||||
// run command
|
||||
// handle stdout as steam mode
|
||||
let receviedStdout = "";
|
||||
const onStdoutPartial = (stdout: string) => {
|
||||
receviedStdout += stdout;
|
||||
const data = this.parseOutData(receviedStdout, true);
|
||||
onData(data);
|
||||
};
|
||||
// run command
|
||||
const pythonApp =
|
||||
DevChatConfig.getInstance().get("python_for_chat") || "python3";
|
||||
logger
|
||||
.channel()
|
||||
?.info(`Running devchat:${pythonApp} ${args.join(" ")}`);
|
||||
const {
|
||||
exitCode: code,
|
||||
stdout,
|
||||
stderr,
|
||||
} = await this.commandRun.spawnAsync(
|
||||
pythonApp,
|
||||
args,
|
||||
spawnAsyncOptions,
|
||||
onStdoutPartial,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
// handle result
|
||||
assertValue(code !== 0, stderr || "Command exited with error code");
|
||||
const responseData = this.parseOutData(stdout, false);
|
||||
// return result
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"prompt-hash": "",
|
||||
user: "",
|
||||
date: "",
|
||||
response: responseData.response,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
finish_reason: "",
|
||||
isError: false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"prompt-hash": "",
|
||||
user: "",
|
||||
date: "",
|
||||
response: `Error: ${error.message}`,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
finish_reason: "error",
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
256
src/toolwrapper/devchatClient.ts
Normal file
256
src/toolwrapper/devchatClient.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import axios, { AxiosResponse, CancelTokenSource } from "axios";
|
||||
|
||||
import { logger } from "../util/logger";
|
||||
import { getFileContent } from "../util/commonUtil";
|
||||
import {
|
||||
devchatSocket,
|
||||
startSocketConn,
|
||||
closeSocketConn,
|
||||
} from "./socketClient";
|
||||
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
import { workspace } from "vscode";
|
||||
import { deleteTopic } from "@/handler/topicHandler";
|
||||
|
||||
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;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export class DevChatClient {
|
||||
private baseURL: string;
|
||||
|
||||
private _cancelMessageToken: CancelTokenSource | null = null;
|
||||
|
||||
constructor() {
|
||||
// TODO: tmp dev
|
||||
this.baseURL = "http://localhost:22222";
|
||||
}
|
||||
|
||||
async _get(path: string): Promise<AxiosResponse> {
|
||||
try {
|
||||
logger.channel()?.debug(`GET request to ${this.baseURL}${path}`);
|
||||
const response = await axios.get(`${this.baseURL}${path}`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async _post(path: string, data: any = undefined): Promise<AxiosResponse> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseURL}${path}`, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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"];
|
||||
// TODO: tmp string literal 临时字面量
|
||||
if (chatRes.finish_reason === "should_run_workflow") {
|
||||
chatRes.extra = chunkData["extra"];
|
||||
logger
|
||||
.channel()
|
||||
?.debug(
|
||||
"res on data: should_run_workflow. do nothing now."
|
||||
);
|
||||
logger
|
||||
.channel()
|
||||
?.debug(
|
||||
`chatRes.extra: ${JSON.stringify(
|
||||
chatRes.extra
|
||||
)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
chatRes.isError = chunkData["isError"];
|
||||
|
||||
chatRes.response += chunkData["content"];
|
||||
logger.channel()?.debug(`${chunkData["content"]}`);
|
||||
onData(chatRes);
|
||||
});
|
||||
|
||||
response.data.on("end", () => {
|
||||
logger.channel()?.debug("\nStreaming ended");
|
||||
resolve(chatRes); // Resolve the promise with chatRes when the stream ends
|
||||
});
|
||||
|
||||
response.data.on("error", (error) => {
|
||||
logger.channel()?.error("Streaming error:", error);
|
||||
// TODO: handle error
|
||||
reject(error); // Reject the promise on error
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO: handle error
|
||||
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> {
|
||||
// TODO: 处理当jsondata太大时,写入临时文件
|
||||
const data = {
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
// workspace: undefined,
|
||||
jsondata: JSON.stringify(logData),
|
||||
};
|
||||
const response = await this._post("/logs/insert", data);
|
||||
logger
|
||||
.channel()
|
||||
?.debug(
|
||||
`insertLog response data: ${JSON.stringify(
|
||||
response.data
|
||||
)}, ${typeof response.data}}`
|
||||
);
|
||||
// TODO: handle error
|
||||
|
||||
const res: LogInsertRes = {
|
||||
hash: response.data["hash"],
|
||||
error: response.data["error"],
|
||||
};
|
||||
return res;
|
||||
}
|
||||
stopAllRequest(): void {
|
||||
this.cancelMessage();
|
||||
// add other requests here if needed
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user