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