// devchat.ts import * as dotenv from 'dotenv'; import * as path from 'path'; import * as fs from 'fs'; import { logger } from '../util/logger'; import { CommandRun } from "../util/commonUtil"; import ExtensionContextHolder from '../util/extensionContext'; import { UiUtilWrapper } from '../util/uiUtil'; import { ApiKeyManager } from '../util/apiKey'; import { exitCode } from 'process'; import * as yaml from 'yaml'; const envPath = path.join(__dirname, '..', '.env'); dotenv.config({ path: envPath }); export interface ChatOptions { parent?: string; reference?: string[]; header?: string[]; functions?: string; role?: string; function_name?: string; context?: string[]; } export interface LogOptions { skip?: number; maxCount?: number; topic?: string; } export interface LogEntry { hash: string; parent: string; user: string; date: string; request: string; response: string; context: Array<{ content: string; role: string; }>; } export interface CommandEntry { name: string; description: string; } // define TopicEntry interface /* [ { root_prompt: LogEntry, latest_time: 1689849274, hidden: false, title: null } ] */ export interface TopicEntry { root_prompt: LogEntry; latest_time: number; hidden: boolean; title: string | null; } export interface ChatResponse { "prompt-hash": string; user: string; date: string; response: string; finish_reason: string; isError: boolean; } class DevChat { private commandRun: CommandRun; constructor() { this.commandRun = new CommandRun(); } public stop() { this.commandRun.stop(); } async buildArgs(options: ChatOptions): Promise { let args = ["prompt"]; 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); } } const isEnableFunctionCalling = UiUtilWrapper.getConfiguration('DevChat', 'EnableFunctionCalling'); if (options.functions && isEnableFunctionCalling) { args.push("-f", options.functions); } if (options.function_name) { args.push("-n", options.function_name); } if (options.parent) { args.push("-p", options.parent); } const llmModelData = await ApiKeyManager.llmModel(); if (llmModelData && llmModelData.model) { args.push("-m", llmModelData.model); } return args; } private parseOutData(stdout: string, isPartial: boolean): ChatResponse { const responseLines = stdout.trim().split("\n"); if (responseLines.length < 2) { return { "prompt-hash": "", user: "", date: "", response: "", finish_reason: "", isError: isPartial ? false : true, }; } const userLine = responseLines.shift()!; const user = (userLine.match(/User: (.+)/)?.[1]) ?? ""; const dateLine = responseLines.shift()!; const date = (dateLine.match(/Date: (.+)/)?.[1]) ?? ""; let promptHashLine = ""; for (let i = responseLines.length - 1; i >= 0; i--) { if (responseLines[i].startsWith("prompt")) { promptHashLine = responseLines[i]; responseLines.splice(i, 1); break; } } let finishReasonLine = ""; for (let i = responseLines.length - 1; i >= 0; i--) { if (responseLines[i].startsWith("finish_reason:")) { finishReasonLine = responseLines[i]; responseLines.splice(i, 1); break; } } if (!promptHashLine) { return { "prompt-hash": "", user: user, date: date, response: responseLines.join("\n"), finish_reason: "", isError: isPartial ? false : true, }; } const finishReason = finishReasonLine.split(" ")[1]; const promptHash = promptHashLine.split(" ")[1]; const response = responseLines.join("\n"); return { "prompt-hash": promptHash, user, date, response, finish_reason: finishReason, isError: false, }; } async chat(content: string, options: ChatOptions = {}, onData: (data: ChatResponse) => void): Promise { const llmModelData = await ApiKeyManager.llmModel(); if (!llmModelData) { return { "prompt-hash": "", user: "", date: "", response: `Error: no valid llm model is selected!`, finish_reason: "", isError: true, }; } const args = await this.buildArgs(options); args.push("--"); args.push(content); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); let openaiApiKey = await ApiKeyManager.getApiKey(); if (!openaiApiKey) { logger.channel()?.error('The OpenAI key is invalid!'); logger.channel()?.show(); } const openaiStream = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.stream'); const openAiApiBaseObject = llmModelData.api_base? { OPENAI_API_BASE: llmModelData.api_base } : {}; const activeLlmModelKey = llmModelData.api_key; let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); if (!devChat) { devChat = 'devchat'; } const reduceModelData = Object.keys(llmModelData) .filter(key => key !== 'api_key' && key !== 'provider' && key !== 'model' && key !== 'api_base') .reduce((obj, key) => { obj[key] = llmModelData[key]; return obj; }, {}); let devchatConfig = {}; devchatConfig[llmModelData.model] = { "provider": llmModelData.provider, "stream": openaiStream, ...reduceModelData }; let devchatModels = { "default_model": llmModelData.model, "models": devchatConfig}; // write to config file const os = process.platform; const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME; const configPath = path.join(userHome!, '.chat', 'config.yml'); // write devchatConfig to configPath const yamlString = yaml.stringify(devchatModels); fs.writeFileSync(configPath, yamlString); try { let receviedStdout = ""; const onStdoutPartial = (stdout: string) => { receviedStdout += stdout; const data = this.parseOutData(receviedStdout, true); onData(data); }; const spawnAsyncOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { PYTHONUTF8:1, ...process.env, OPENAI_API_KEY: activeLlmModelKey, ...openAiApiBaseObject }, }; logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); logger.channel()?.info(`Running devchat with environment: ${JSON.stringify(openAiApiBaseObject)}`); const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined); if (stderr) { return { "prompt-hash": "", user: "", date: "", response: stderr, finish_reason: "", isError: true, }; } const response = this.parseOutData(stdout, false); return response; } catch (error: any) { return { "prompt-hash": "", user: "", date: "", response: `Error: ${error.stderr}\nExit code: ${error.code}`, finish_reason: "", isError: true, }; } } async delete(hash: string): Promise { const args = ["log", "--delete", hash]; const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const openaiApiKey = process.env.OPENAI_API_KEY; logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env, OPENAI_API_KEY: openaiApiKey, }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); return false; } if (stdout.indexOf('Failed to delete prompt') >= 0) { logger.channel()?.error(`Failed to delete prompt: ${hash}`); logger.channel()?.show(); return false; } if (code !== 0) { logger.channel()?.error(`Exit code: ${code}`); logger.channel()?.show(); return false; } return true; } async log(options: LogOptions = {}): Promise { const args = this.buildLogArgs(options); const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); const openaiApiKey = process.env.OPENAI_API_KEY; logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env, OPENAI_API_KEY: openaiApiKey, }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); return []; } const logs = JSON.parse(stdout.trim()).reverse(); for (const log of logs) { log.response = log.responses[0]; delete log.responses; } return logs; } // command devchat run --list // output: // [ // { // "name": "code", // "description": "Generate code with a general template embedded into the prompt." // }, // { // "name": "code.py", // "description": "Generate code with a Python-specific template embedded into the prompt." // }, // { // "name": "commit_message", // "description": "Generate a commit message for the given git diff." // }, // { // "name": "release_note", // "description": "Generate a release note for the given commit log." // } // ] async commands(): Promise { const args = ["run", "--list"]; const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); return []; } try { const commands = JSON.parse(stdout.trim()); return commands; } catch (error) { logger.channel()?.error(`Error parsing JSON: ${error}`); logger.channel()?.show(); return []; } } async commandPrompt(command: string): Promise { const args = ["run", command]; const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); } return stdout; } async updateSysCommand(): Promise { const args = ["run", "--update-sys"]; const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); } logger.channel()?.info(`${stdout}`); return stdout; } async topics(): Promise { const args = ["topic", "-l"]; const devChat = this.getDevChatPath(); const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath(); logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`); const spawnOptions = { maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB cwd: workspaceDir, env: { ...process.env }, }; const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined); logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`); if (stderr) { logger.channel()?.error(`Error: ${stderr}`); logger.channel()?.show(); return []; } try { const topics = JSON.parse(stdout.trim()).reverse(); // convert responses to respose, and remove responses field // responses is in TopicEntry.root_prompt.responses for (const topic of topics) { if (topic.root_prompt.responses) { topic.root_prompt.response = topic.root_prompt.responses[0]; delete topic.root_prompt.responses; } } return topics; } catch (error) { logger.channel()?.error(`Error parsing JSON: ${error}`); logger.channel()?.show(); return []; } } private buildLogArgs(options: LogOptions): string[] { let args = ["log"]; if (options.skip) { args.push('--skip', `${options.skip}`); } if (options.maxCount) { args.push('--max-count', `${options.maxCount}`); } else { const maxLogCount = UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount'); args.push('--max-count', `${maxLogCount}`); } if (options.topic) { args.push('--topic', `${options.topic}`); } return args; } private getDevChatPath(): string { let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath'); if (!devChat) { devChat = 'devchat'; } return devChat; } } export default DevChat;