Merge pull request #577 from devchat-ai/local-svc-client
Devchat local service client
This commit is contained in:
commit
19b7d14be9
@ -130,6 +130,11 @@
|
||||
"title": "Install slash commands",
|
||||
"category": "DevChat"
|
||||
},
|
||||
{
|
||||
"command": "DevChat.StartLocalService",
|
||||
"title": "Start local service",
|
||||
"category": "DevChat"
|
||||
},
|
||||
{
|
||||
"command": "DevChat.Chat",
|
||||
"title": "Chat with DevChat",
|
||||
@ -253,6 +258,10 @@
|
||||
{
|
||||
"command": "DevChat.InstallCommands",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "DevChat.StartLocalService",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
|
@ -1,340 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ChatContext } from './contextManager';
|
||||
|
||||
import { logger } from '../util/logger';
|
||||
import { handleCodeSelected } from './contextCodeSelected';
|
||||
import DevChat, { ChatOptions } from '../toolwrapper/devchat';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
async function getCurrentSelectText(activeEditor: vscode.TextEditor): Promise<string> {
|
||||
if (!activeEditor) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const document = activeEditor.document;
|
||||
const selection = activeEditor.selection;
|
||||
const selectedText = document.getText(selection);
|
||||
|
||||
return selectedText;
|
||||
}
|
||||
|
||||
// get full text in activeEditor
|
||||
async function getFullText(activeEditor: vscode.TextEditor): Promise<string> {
|
||||
if (!activeEditor) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const document = activeEditor.document;
|
||||
return document.getText();
|
||||
}
|
||||
|
||||
async function getUndefinedSymbols(content: string): Promise<string[] | undefined> {
|
||||
// run devchat prompt command
|
||||
const devChat = new DevChat();
|
||||
const chatOptions: ChatOptions = {};
|
||||
|
||||
const onData = (partialResponse) => { };
|
||||
const newContent = `
|
||||
As a software developer skilled in code analysis, your goal is to examine and understand a provided code snippet.
|
||||
The code may include various symbols, encompassing both variables and functions.
|
||||
However, the snippet doesn't include the definitions of some of these symbols.
|
||||
Now your specific task is to identify and list all such symbols whose definitions are missing but are essential for comprehending the entire code.
|
||||
This will help in fully grasping the behavior and purpose of the code. Note that the code snippet in question could range from a few lines to a singular symbol.
|
||||
|
||||
Response is json string, don't include any other output except the json object. The json object should be an array of strings, each string is a symbol name:
|
||||
\`\`\`json
|
||||
["f1", "f2"]
|
||||
\`\`\`
|
||||
|
||||
During this process, you cannot invoke the GPT function. The code snippet is as follows: \n\`\`\`` + content + '```'; ;
|
||||
|
||||
const chatResponse = await devChat.chat(newContent, chatOptions, onData, false);
|
||||
if (chatResponse && chatResponse.response) {
|
||||
logger.channel()?.info(chatResponse.response);
|
||||
}
|
||||
|
||||
// parse data in chatResponse.response
|
||||
// data format as:
|
||||
// ```json {data} ```
|
||||
// or [data]
|
||||
// or plain text
|
||||
// so, parse data between ```json and ``` or directly parse the array, or return an empty array
|
||||
if (!chatResponse || !chatResponse.response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let responseText = chatResponse.response.trim();
|
||||
let symbols: string[];
|
||||
|
||||
const indexBlock = responseText.indexOf('```');
|
||||
if (indexBlock !== -1) {
|
||||
const indexJsonEnd = responseText.indexOf('```', indexBlock+3);
|
||||
if (indexJsonEnd !== -1) {
|
||||
responseText = responseText.substring(indexBlock, indexJsonEnd + 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseText.startsWith("```") && responseText.endsWith("```")) {
|
||||
const index = responseText.indexOf('[');
|
||||
responseText = responseText.substring(index, responseText.length - 3);
|
||||
try {
|
||||
symbols = JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
symbols = JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
logger.channel()?.info(`getUndefinedSymbols: ${chatResponse.response}`);
|
||||
return symbols;
|
||||
}
|
||||
|
||||
function matchSymbolInline(line: string, symbol: string): number[] {
|
||||
const escapedSymbol = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Create a RegExp with the escaped symbol and word boundaries
|
||||
const regex = new RegExp(`\\b${escapedSymbol}\\b`, 'gu');
|
||||
|
||||
// Find the match in the selected text
|
||||
const matches = [...line.matchAll(regex)];
|
||||
|
||||
// If the symbol is found
|
||||
if (matches.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return matches.map((match) => match.index!);
|
||||
}
|
||||
|
||||
function getMatchedSymbolPositions(selectText: string, symbol: string): object[] {
|
||||
const lines = selectText.split('\n');
|
||||
const positions: object[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const matchedPositions = matchSymbolInline(line, symbol);
|
||||
for (const pos of matchedPositions) {
|
||||
positions.push({
|
||||
line: i,
|
||||
character: pos
|
||||
});
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
function isLocationInstance(loc: vscode.Location | vscode.LocationLink): boolean {
|
||||
try {
|
||||
return (loc as vscode.Location).uri !== undefined;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCodeSelectedNoFile(fileSelected: string, codeSelected: string, startLine: number) {
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const relativePath = path.relative(workspaceDir!, fileSelected);
|
||||
|
||||
const data = {
|
||||
path: relativePath,
|
||||
startLine: startLine,
|
||||
content: codeSelected
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getSymbolDefine(symbolList: string[], activeEditor: vscode.TextEditor, toFile: boolean = true): Promise<string[]> {
|
||||
const document = activeEditor!.document;
|
||||
let selection = activeEditor!.selection;
|
||||
let selectedText = document!.getText(selection);
|
||||
if (selectedText === "" && !toFile) {
|
||||
selectedText = await getFullText(activeEditor);
|
||||
selection = new vscode.Selection(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
let contextList: any[] = [];
|
||||
if (toFile) {
|
||||
contextList = [await handleCodeSelectedNoFile(document.uri.fsPath, selectedText, selection.start.line)];
|
||||
}
|
||||
let hasVisitedSymbols: Set<string> = new Set();
|
||||
let hasPushedSymbols: Set<string> = new Set();
|
||||
|
||||
// visit each symbol in symbolList, and get it's define
|
||||
for (const symbol of symbolList) {
|
||||
logger.channel()?.info(`handle symble: ${symbol} ...`);
|
||||
// get symbol position in selectedText
|
||||
// if selectedText is "abc2+abc", symbol is "abc", then symbolPosition is 5 not 0
|
||||
// because abc2 is not a symbol
|
||||
const positions: any[] = getMatchedSymbolPositions(selectedText, symbol);
|
||||
|
||||
for (const pos of positions) {
|
||||
const symbolPosition = pos.character;
|
||||
|
||||
// if symbol is like a.b.c, then split it to a, b, c
|
||||
const symbolSplit = symbol.split(".");
|
||||
let curPosition = 0;
|
||||
for (const symbolSplitItem of symbolSplit) {
|
||||
const symbolPositionNew = symbol.indexOf(symbolSplitItem, curPosition) + symbolPosition;
|
||||
curPosition = symbolPositionNew - symbolPosition + symbolSplitItem.length;
|
||||
const newPos = new vscode.Position(pos.line + selection.start.line, (pos.line > 0 ? 0 : selection.start.character) + symbolPositionNew);
|
||||
logger.channel()?.info(`handle sub symble: ${symbolSplitItem} at ${newPos.line}:${newPos.character}`);
|
||||
|
||||
try{
|
||||
// call vscode.executeDefinitionProvider
|
||||
const refLocations = await vscode.commands.executeCommand<vscode.Location[] | vscode.LocationLink[]>(
|
||||
'vscode.executeDefinitionProvider',
|
||||
document.uri,
|
||||
newPos
|
||||
);
|
||||
if (!refLocations) {
|
||||
logger.channel()?.info(`no def location for ${symbolSplitItem} at ${newPos.line}:${newPos.character}`);
|
||||
}
|
||||
|
||||
// visit each refLocation, and get it's define
|
||||
for (const refLocation of refLocations) {
|
||||
let targetUri: vscode.Uri | undefined = undefined;
|
||||
let targetRange: vscode.Range | undefined = undefined;
|
||||
|
||||
if (isLocationInstance(refLocation)) {
|
||||
const locLocation = refLocation as vscode.Location;
|
||||
targetUri = locLocation.uri;
|
||||
targetRange = locLocation.range;
|
||||
} else {
|
||||
const locLocationLink = refLocation as vscode.LocationLink;
|
||||
targetUri = locLocationLink.targetUri;
|
||||
targetRange = locLocationLink.targetRange;
|
||||
}
|
||||
if (!targetUri ||!targetRange) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.channel()?.info(`def location: ${targetUri.fsPath} ${targetRange.start.line}:${targetRange.start.character}-${targetRange.end.line}:${targetRange.end.character}`);
|
||||
const refLocationString = targetUri.fsPath + "-" + targetRange.start.line + ":" + targetRange.start.character + "-" + targetRange.end.line + ":" + targetRange.end.character;
|
||||
if (hasVisitedSymbols.has(refLocationString)) {
|
||||
continue;
|
||||
}
|
||||
hasVisitedSymbols.add(refLocationString);
|
||||
|
||||
// get defines in refLocation file
|
||||
const symbolsT: vscode.DocumentSymbol[] = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
targetUri
|
||||
);
|
||||
|
||||
let targetSymbol: any = undefined;
|
||||
const visitFun = (symbol: vscode.DocumentSymbol) => {
|
||||
if (targetRange!.start.isAfterOrEqual(symbol.range.start) && targetRange!.end.isBeforeOrEqual(symbol.range.end)) {
|
||||
targetSymbol = symbol;
|
||||
}
|
||||
|
||||
if (targetRange!.start.isAfter(symbol.range.end) || targetRange!.end.isBefore(symbol.range.start)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of symbol.children) {
|
||||
visitFun(child);
|
||||
}
|
||||
};
|
||||
for (const symbol of symbolsT) {
|
||||
visitFun(symbol);
|
||||
}
|
||||
|
||||
if (targetSymbol !== undefined) {
|
||||
logger.channel()?.info(`symbol define information: ${targetSymbol.name} at ${targetSymbol.location.uri.fsPath} ${targetSymbol.location.range.start.line}:${targetSymbol.location.range.start.character}-${targetSymbol.location.range.end.line}:${targetSymbol.location.range.end.character}`);
|
||||
const defLocationString = targetSymbol.location.uri.fsPath + "-" + targetSymbol.location.range.start.line + ":" + targetSymbol.location.range.start.character + "-" + targetSymbol.location.range.end.line + ":" + targetSymbol.location.range.end.character;
|
||||
if (hasPushedSymbols.has(defLocationString)) {
|
||||
continue;
|
||||
}
|
||||
hasPushedSymbols.add(defLocationString);
|
||||
|
||||
const documentNew = await vscode.workspace.openTextDocument(targetUri);
|
||||
|
||||
if (targetSymbol.kind === vscode.SymbolKind.Variable) {
|
||||
const renageNew = new vscode.Range(targetSymbol.range.start.line, 0, targetSymbol.range.end.line, 10000);
|
||||
if (toFile) {
|
||||
contextList.push(await handleCodeSelected(targetUri.fsPath, documentNew.getText(renageNew), targetSymbol.range.start.line));
|
||||
} else {
|
||||
contextList.push(await handleCodeSelectedNoFile(targetUri.fsPath, documentNew.getText(renageNew), targetSymbol.range.start.line));
|
||||
}
|
||||
} else {
|
||||
if (toFile) {
|
||||
contextList.push(await handleCodeSelected(targetUri.fsPath, documentNew.getText(targetSymbol.range), targetSymbol.range.start.line));
|
||||
} else {
|
||||
contextList.push(await handleCodeSelectedNoFile(targetUri.fsPath, documentNew.getText(targetSymbol.range), targetSymbol.range.start.line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`getSymbolDefine error: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return contextList;
|
||||
}
|
||||
|
||||
export async function getSymbolDefines() {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!activeEditor) {
|
||||
logger.channel()?.error('No code selected!');
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
|
||||
let selectedText = await getCurrentSelectText(activeEditor);
|
||||
if (selectedText === "") {
|
||||
selectedText = await getFullText(activeEditor);
|
||||
}
|
||||
if (selectedText === "") {
|
||||
logger.channel()?.error(`No code selected! Current selected editor is: ${activeEditor.document.uri.fsPath}}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
const symbolList = await getUndefinedSymbols(selectedText);
|
||||
if (symbolList === undefined) {
|
||||
logger.channel()?.error('Failed to get symbol list!');
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
const contextList = await getSymbolDefine(symbolList, activeEditor, false);
|
||||
return contextList;
|
||||
}
|
||||
|
||||
export const refDefsContext: ChatContext = {
|
||||
name: 'symbol definitions',
|
||||
description: 'find related definitions of classes, functions, etc. in selected code',
|
||||
handler: async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!activeEditor) {
|
||||
logger.channel()?.error('No code selected!');
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedText = await getCurrentSelectText(activeEditor);
|
||||
if (selectedText === "") {
|
||||
logger.channel()?.error(`No code selected! Current selected editor is: ${activeEditor.document.uri.fsPath}}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
const symbolList = await getUndefinedSymbols(selectedText);
|
||||
if (symbolList === undefined) {
|
||||
logger.channel()?.error('Failed to get symbol list!');
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
const contextList = await getSymbolDefine(symbolList, activeEditor);
|
||||
|
||||
return contextList;
|
||||
},
|
||||
};
|
@ -8,11 +8,13 @@ import { FilePairManager } from "../util/diffFilePairs";
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
|
||||
import { sendCommandListByDevChatRun } from '../handler/workflowCommandHandler';
|
||||
import DevChat from "../toolwrapper/devchat";
|
||||
import { DevChatClient } from "../toolwrapper/devchatClient";
|
||||
import { chatWithDevChat } from '../handler/chatHandler';
|
||||
import { focusDevChatInput } from '../handler/focusHandler';
|
||||
import { DevChatConfig } from '../util/config';
|
||||
import { MessageHandler } from "../handler/messageHandler";
|
||||
import { startLocalService } from '../util/localService';
|
||||
import { logger } from "../util/logger";
|
||||
|
||||
const readdir = util.promisify(fs.readdir);
|
||||
const mkdir = util.promisify(fs.mkdir);
|
||||
@ -196,7 +198,7 @@ export function registerInstallCommandsCommand(
|
||||
"workflowsCommands"
|
||||
); // Adjust this path as needed
|
||||
|
||||
const devchat = new DevChat();
|
||||
const dcClient = new DevChatClient();
|
||||
|
||||
if (!fs.existsSync(sysDirPath)) {
|
||||
await copyDirectory(pluginDirPath, sysDirPath);
|
||||
@ -204,15 +206,15 @@ export function registerInstallCommandsCommand(
|
||||
|
||||
// Check if ~/.chat/scripts directory exists
|
||||
if (!fs.existsSync(sysDirPath)) {
|
||||
// Directory does not exist, wait for updateSysCommand to finish
|
||||
await devchat.updateSysCommand();
|
||||
// Directory does not exist, wait for updateWorkflows to finish
|
||||
await dcClient.updateWorkflows();
|
||||
sendCommandListByDevChatRun();
|
||||
} else {
|
||||
// Directory exists, execute sendCommandListByDevChatRun immediately
|
||||
await sendCommandListByDevChatRun();
|
||||
|
||||
// Then asynchronously execute updateSysCommand
|
||||
await devchat.updateSysCommand();
|
||||
// Then asynchronously execute updateWorkflows
|
||||
await dcClient.updateWorkflows();
|
||||
await sendCommandListByDevChatRun();
|
||||
}
|
||||
}
|
||||
@ -222,6 +224,28 @@ export function registerInstallCommandsCommand(
|
||||
}
|
||||
|
||||
|
||||
export function registerStartLocalServiceCommand(
|
||||
context: vscode.ExtensionContext
|
||||
) {
|
||||
let disposable = vscode.commands.registerCommand(
|
||||
"DevChat.StartLocalService",
|
||||
async () => {
|
||||
try {
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() ?? '';
|
||||
logger.channel()?.debug(`extensionPath: ${context.extensionPath}`);
|
||||
logger.channel()?.debug(`workspacePath: ${workspaceDir}`);
|
||||
const port = await startLocalService(context.extensionPath, workspaceDir);
|
||||
logger.channel()?.debug(`Local service started on port ${port}`);
|
||||
} catch (error) {
|
||||
logger.channel()?.error('Failed to start local service:', error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
|
||||
export function registerDevChatChatCommand(context: vscode.ExtensionContext) {
|
||||
let disposable = vscode.commands.registerCommand(
|
||||
"DevChat.Chat",
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
registerFixCommand,
|
||||
registerExplainCommand,
|
||||
registerQuickFixCommand,
|
||||
registerStartLocalServiceCommand
|
||||
} from './contributes/commands';
|
||||
import { regLanguageContext } from './contributes/context';
|
||||
import { regDevChatView } from './contributes/views';
|
||||
@ -33,6 +34,7 @@ import { DevChatConfig } from './util/config';
|
||||
import { InlineCompletionProvider, registerCodeCompleteCallbackCommand } from "./contributes/codecomplete/codecomplete";
|
||||
import { indexDir } from "./contributes/codecomplete/astIndex";
|
||||
import registerQuickFixProvider from "./contributes/quickFixProvider";
|
||||
import { stopLocalService } from './util/localService';
|
||||
|
||||
|
||||
async function migrateConfig() {
|
||||
@ -152,6 +154,7 @@ async function activate(context: vscode.ExtensionContext) {
|
||||
registerStatusBarItemClickCommand(context);
|
||||
|
||||
registerInstallCommandsCommand(context);
|
||||
registerStartLocalServiceCommand(context);
|
||||
|
||||
createStatusBarItem(context);
|
||||
|
||||
@ -178,6 +181,13 @@ async function activate(context: vscode.ExtensionContext) {
|
||||
async function deactivate() {
|
||||
// stop devchat
|
||||
await stopDevChatBase({});
|
||||
|
||||
try {
|
||||
await stopLocalService();
|
||||
logger.channel()?.info('Local service stopped successfully');
|
||||
} catch (error) {
|
||||
logger.channel()?.error('Error stopping local service:', error);
|
||||
}
|
||||
}
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
|
@ -3,7 +3,7 @@ import { insertCodeBlockToFile } from './codeBlockHandler';
|
||||
import { replaceCodeBlockToFile } from './codeBlockHandler';
|
||||
import { doCommit } from './commitHandler';
|
||||
import { getHistoryMessages } from './historyMessagesHandler';
|
||||
import { getWorkflowCommandList } from './workflowCommandHandler';
|
||||
import { handleRegCommandList } from './workflowCommandHandler';
|
||||
import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage';
|
||||
import { applyCodeWithDiff } from './diffHandler';
|
||||
import { addConext } from './contextHandler';
|
||||
@ -36,7 +36,7 @@ messageHandler.registerHandler('doCommit', doCommit);
|
||||
messageHandler.registerHandler('historyMessages', getHistoryMessages);
|
||||
// Register the command list
|
||||
// Response: { command: 'regCommandList', result: <command list> }
|
||||
messageHandler.registerHandler('regCommandList', getWorkflowCommandList);
|
||||
messageHandler.registerHandler('regCommandList', handleRegCommandList);
|
||||
// Send a message, send the message entered by the user to AI
|
||||
// Response:
|
||||
// { command: 'receiveMessagePartial', text: <response message text>, user: <user>, date: <date> }
|
||||
|
@ -1,4 +1,17 @@
|
||||
import DevChat, { LogEntry, LogOptions } from '../toolwrapper/devchat';
|
||||
import { DevChatClient, ShortLog } from '../toolwrapper/devchatClient';
|
||||
|
||||
export interface LogEntry {
|
||||
hash: string;
|
||||
parent: string | null;
|
||||
user: string;
|
||||
date: string;
|
||||
request: string;
|
||||
response: string;
|
||||
context: Array<{
|
||||
content: string;
|
||||
role: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface LoadHistoryMessages {
|
||||
command: string;
|
||||
@ -41,18 +54,28 @@ OPENAI_API_KEY is missing from your environment or settings. Kindly input your O
|
||||
} as LogEntry;
|
||||
}
|
||||
|
||||
export async function loadTopicHistoryLogs(topicId: string | undefined): Promise<Array<LogEntry> | undefined> {
|
||||
async function loadTopicHistoryLogs(topicId: string | undefined): Promise<Array<LogEntry> | undefined> {
|
||||
if (!topicId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const devChat = new DevChat();
|
||||
const logOptions: LogOptions = {
|
||||
skip: 0,
|
||||
maxCount: 10000,
|
||||
topic: topicId
|
||||
};
|
||||
const logEntries = await devChat.log(logOptions);
|
||||
|
||||
const dcClient = new DevChatClient();
|
||||
const shortLogs: ShortLog[] = await dcClient.getTopicLogs(topicId, 10000, 0);
|
||||
|
||||
const logEntries: Array<LogEntry> = [];
|
||||
for (const shortLog of shortLogs) {
|
||||
const logE: LogEntry = {
|
||||
hash: shortLog.hash,
|
||||
parent: shortLog.parent,
|
||||
user: shortLog.user,
|
||||
date: shortLog.date,
|
||||
request: shortLog.request,
|
||||
response: shortLog.responses[0],
|
||||
context: shortLog.context,
|
||||
};
|
||||
|
||||
logEntries.push(logE);
|
||||
}
|
||||
|
||||
return logEntries;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import DevChat, { ChatOptions, ChatResponse } from '../toolwrapper/devchat';
|
||||
import { logger } from '../util/logger';
|
||||
import { assertValue } from '../util/check';
|
||||
import { DevChatClient, ChatRequest, ChatResponse, buildRoleContextsFromFiles, LogData } from '../toolwrapper/devchatClient';
|
||||
import { DevChatCLI } from '../toolwrapper/devchatCLI';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
|
||||
|
||||
/**
|
||||
@ -99,6 +101,15 @@ export function parseMessage(message: string): { context: string[]; instruction:
|
||||
return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text };
|
||||
}
|
||||
|
||||
|
||||
// TODO: to be removed later
|
||||
interface ChatOptions {
|
||||
parent?: string;
|
||||
reference?: string[];
|
||||
header?: string[];
|
||||
context?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a message and sets the chat options based on the parsed message.
|
||||
*
|
||||
@ -130,7 +141,9 @@ export function processChatResponse(chatResponse: ChatResponse) : string {
|
||||
}
|
||||
|
||||
|
||||
const devChat = new DevChat();
|
||||
// const devChat = new DevChat();
|
||||
const dcClient = new DevChatClient();
|
||||
const dcCLI = new DevChatCLI();
|
||||
|
||||
/**
|
||||
* Sends a message to the DevChat and handles the response.
|
||||
@ -148,24 +161,89 @@ export async function sendMessageBase(message: any, handlePartialData: (data: {
|
||||
const [parsedMessage, chatOptions] = await parseMessageAndSetOptions(message);
|
||||
logger.channel()?.trace(`parent hash: ${chatOptions.parent}`);
|
||||
|
||||
// call devchat chat
|
||||
const chatResponse = await devChat.chat(
|
||||
parsedMessage.text,
|
||||
chatOptions,
|
||||
(partialResponse: ChatResponse) => {
|
||||
const partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
||||
handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date });
|
||||
});
|
||||
|
||||
// send chat message to devchat service
|
||||
const llmModelData = await ApiKeyManager.llmModel();
|
||||
assertValue(!llmModelData || !llmModelData.model, 'You must select a LLM model to use for conversations');
|
||||
const chatReq: ChatRequest = {
|
||||
content: parsedMessage.text,
|
||||
model_name: llmModelData.model,
|
||||
api_key: llmModelData.api_key,
|
||||
api_base: llmModelData.api_base,
|
||||
parent: chatOptions.parent,
|
||||
context: chatOptions.context,
|
||||
};
|
||||
let chatResponse = await dcClient.message(
|
||||
chatReq,
|
||||
(partialRes: ChatResponse) => {
|
||||
const text = partialRes.response;
|
||||
handlePartialData({
|
||||
command: "receiveMessagePartial",
|
||||
text: text!,
|
||||
user: partialRes.user,
|
||||
date: partialRes.date,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
let workflowRes: ChatResponse | undefined = undefined;
|
||||
if (chatResponse.finish_reason === "should_run_workflow") {
|
||||
// invoke workflow via cli
|
||||
workflowRes = await dcCLI.runWorkflow(
|
||||
parsedMessage.text,
|
||||
chatOptions,
|
||||
(partialResponse: ChatResponse) => {
|
||||
const partialDataText = partialResponse.response;
|
||||
handlePartialData({
|
||||
command: "receiveMessagePartial",
|
||||
text: partialDataText!,
|
||||
user: partialResponse.user,
|
||||
date: partialResponse.date,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const finalResponse = workflowRes || chatResponse;
|
||||
|
||||
// insert log
|
||||
const roleContexts = await buildRoleContextsFromFiles(chatOptions.context);
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: chatReq.content,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: finalResponse.response,
|
||||
},
|
||||
...roleContexts
|
||||
];
|
||||
|
||||
const logData: LogData = {
|
||||
model: llmModelData.model,
|
||||
messages: messages,
|
||||
parent: chatOptions.parent,
|
||||
timestamp: Math.floor(Date.now()/1000),
|
||||
// TODO: 1 or real value?
|
||||
request_tokens: 1,
|
||||
response_tokens: 1,
|
||||
};
|
||||
const logRes = await dcClient.insertLog(logData);
|
||||
|
||||
if (logRes.hash) {
|
||||
finalResponse["prompt-hash"] = logRes.hash;
|
||||
}
|
||||
|
||||
assertValue(UserStopHandler.isUserInteractionStopped(), "User Stopped");
|
||||
|
||||
return {
|
||||
command: 'receiveMessage',
|
||||
text: processChatResponse(chatResponse),
|
||||
hash: chatResponse['prompt-hash'],
|
||||
user: chatResponse.user,
|
||||
date: chatResponse.date,
|
||||
isError: chatResponse.isError
|
||||
text: processChatResponse(finalResponse),
|
||||
hash: finalResponse['prompt-hash'],
|
||||
user: finalResponse.user,
|
||||
date: finalResponse.date,
|
||||
isError: finalResponse.isError
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Error occurred while sending response: ${error.message}`);
|
||||
@ -184,7 +262,8 @@ export async function sendMessageBase(message: any, handlePartialData: (data: {
|
||||
export async function stopDevChatBase(message: any): Promise<void> {
|
||||
logger.channel()?.info(`Stopping devchat`);
|
||||
UserStopHandler.stopUserInteraction();
|
||||
devChat.stop();
|
||||
dcClient.stopAllRequest();
|
||||
dcCLI.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,9 +279,10 @@ export async function deleteChatMessageBase(message:{'hash': string}): Promise<b
|
||||
try {
|
||||
assertValue(!message.hash, 'Message hash is required');
|
||||
|
||||
// delete the message by devchat
|
||||
const bSuccess = await devChat.delete(message.hash);
|
||||
assertValue(!bSuccess, "Failed to delete message from devchat");
|
||||
// delete the message by devchatClient
|
||||
const res = await dcClient.deleteLog(message.hash);
|
||||
assertValue(!res.success, "Failed to delete message from devchat client");
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
@ -219,5 +299,6 @@ export async function deleteChatMessageBase(message:{'hash': string}): Promise<b
|
||||
* @returns {Promise<void>} - A Promise that resolves when the message has been sent.
|
||||
*/
|
||||
export async function sendTextToDevChat(text: string): Promise<void> {
|
||||
return devChat.input(text);
|
||||
dcCLI.input(text);
|
||||
return;
|
||||
}
|
@ -1,67 +1,62 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import DevChat, { TopicEntry } from '../toolwrapper/devchat';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
import * as vscode from "vscode";
|
||||
|
||||
import { regInMessage, regOutMessage } from "../util/reg_messages";
|
||||
import { MessageHandler } from "./messageHandler";
|
||||
import { DevChatClient } from "../toolwrapper/devchatClient";
|
||||
import { LogEntry } from "./historyMessagesBase";
|
||||
|
||||
const dcClient = new DevChatClient();
|
||||
|
||||
function isDeleteTopic(topicId: string) {
|
||||
let workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (!workspaceDir) {
|
||||
workspaceDir = os.homedir();
|
||||
}
|
||||
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
|
||||
|
||||
if (!fs.existsSync(deletedTopicsPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deletedTopics = fs.readFileSync(deletedTopicsPath, 'utf-8').split('\n');
|
||||
// check whether topicId in deletedTopics
|
||||
return deletedTopics.includes(topicId);
|
||||
export interface TopicEntry {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
root_prompt: LogEntry;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
latest_time: number;
|
||||
hidden: boolean;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
// 注册获取当前topic列表的命令
|
||||
regInMessage({ command: 'getTopics' });
|
||||
regOutMessage({ command: 'getTopics', topics: [] });
|
||||
export async function getTopics(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
|
||||
const devChat = new DevChat();
|
||||
const topicEntriesAll: TopicEntry[] = await devChat.topics();
|
||||
regInMessage({ command: "getTopics" });
|
||||
regOutMessage({ command: "getTopics", topics: [] });
|
||||
export async function getTopics(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||
): Promise<void> {
|
||||
const topics = await dcClient.getTopics(100, 0);
|
||||
const entries: TopicEntry[] = [];
|
||||
|
||||
let topicEntries: TopicEntry[] = [];
|
||||
for (const topicEntry of topicEntriesAll) {
|
||||
if (isDeleteTopic(topicEntry.root_prompt.hash)) {
|
||||
continue;
|
||||
}
|
||||
// append topicEntry to topicEntries
|
||||
topicEntries.push(topicEntry);
|
||||
for (const topic of topics) {
|
||||
const rootLog: LogEntry = {
|
||||
hash: topic.root_prompt_hash,
|
||||
parent: topic.root_prompt_parent,
|
||||
user: topic.root_prompt_user,
|
||||
date: topic.root_prompt_date,
|
||||
request: topic.root_prompt_request,
|
||||
response: topic.root_prompt_response,
|
||||
context: [],
|
||||
};
|
||||
const e: TopicEntry = {
|
||||
root_prompt: rootLog,
|
||||
latest_time: topic.latest_time,
|
||||
hidden: topic.hidden,
|
||||
title: topic.title,
|
||||
};
|
||||
entries.push(e);
|
||||
}
|
||||
|
||||
MessageHandler.sendMessage(panel, { command: 'getTopics', topicEntries });
|
||||
MessageHandler.sendMessage(panel, {
|
||||
command: "getTopics",
|
||||
topicEntries: entries,
|
||||
});
|
||||
}
|
||||
|
||||
// 注册删除topic的命令
|
||||
regInMessage({ command: 'deleteTopic', topicId: '' });
|
||||
export async function deleteTopic(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
|
||||
regInMessage({ command: "deleteTopic", topicId: "" });
|
||||
export async function deleteTopic(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||
): Promise<void> {
|
||||
const topicId = message.topicId;
|
||||
let workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (!workspaceDir) {
|
||||
workspaceDir = os.homedir();
|
||||
}
|
||||
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
|
||||
|
||||
// read ${WORKSPACE_ROOT}/.chat/.deletedTopics as String[]
|
||||
// add topicId to String[]
|
||||
// write String[] to ${WORKSPACE_ROOT}/.chat/.deletedTopics
|
||||
let deletedTopics: string[] = [];
|
||||
|
||||
if (fs.existsSync(deletedTopicsPath)) {
|
||||
deletedTopics = fs.readFileSync(deletedTopicsPath, 'utf-8').split('\n');
|
||||
}
|
||||
deletedTopics.push(topicId);
|
||||
fs.writeFileSync(deletedTopicsPath, deletedTopics.join('\n'));
|
||||
await dcClient.deleteTopic(topicId);
|
||||
}
|
||||
|
@ -1,24 +1,60 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
import DevChat from '../toolwrapper/devchat';
|
||||
import * as vscode from "vscode";
|
||||
import { MessageHandler } from "./messageHandler";
|
||||
import { regInMessage, regOutMessage } from "../util/reg_messages";
|
||||
import { DevChatClient } from "../toolwrapper/devchatClient";
|
||||
import { logger } from "../util/logger";
|
||||
|
||||
let existPannel: vscode.WebviewPanel | vscode.WebviewView | undefined =
|
||||
undefined;
|
||||
|
||||
let existPannel: vscode.WebviewPanel|vscode.WebviewView|undefined = undefined;
|
||||
regInMessage({ command: "regCommandList" });
|
||||
regOutMessage({
|
||||
command: "regCommandList",
|
||||
result: [{ name: "", pattern: "", description: "" }],
|
||||
});
|
||||
export async function handleRegCommandList(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||
): Promise<void> {
|
||||
existPannel = panel;
|
||||
if (process.env.DC_LOCALSERVICE_PORT) {
|
||||
await getWorkflowCommandList(message, existPannel!);
|
||||
}
|
||||
}
|
||||
|
||||
regInMessage({command: 'regCommandList'});
|
||||
regOutMessage({command: 'regCommandList', result: [{name: '', pattern: '', description: ''}]});
|
||||
export async function getWorkflowCommandList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
existPannel = panel;
|
||||
export async function getWorkflowCommandList(
|
||||
message: any,
|
||||
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||
): Promise<void> {
|
||||
const dcClient = new DevChatClient();
|
||||
|
||||
const commandList = await new DevChat().commands();
|
||||
MessageHandler.sendMessage(panel, { command: 'regCommandList', result: commandList });
|
||||
return;
|
||||
// All workflows registered in DevChat
|
||||
const workflows = await dcClient.getWorkflowList();
|
||||
|
||||
// Get recommends from config
|
||||
const workflowsConfig = await dcClient.getWorkflowConfig();
|
||||
const recommends = workflowsConfig.recommend?.workflows || [];
|
||||
|
||||
// Filter active workflows and add recommend info
|
||||
const commandList = workflows
|
||||
.filter((workflow) => workflow.active)
|
||||
.map((workflow: any) => ({
|
||||
...workflow,
|
||||
recommend: recommends.indexOf(workflow.name),
|
||||
}));
|
||||
|
||||
if (commandList.length > 0) {
|
||||
MessageHandler.sendMessage(panel, {
|
||||
command: "regCommandList",
|
||||
result: commandList,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export async function sendCommandListByDevChatRun() {
|
||||
if (existPannel) {
|
||||
await getWorkflowCommandList({}, existPannel!);
|
||||
}
|
||||
if (existPannel) {
|
||||
await getWorkflowCommandList({}, existPannel!);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import * as vscode from "vscode";
|
||||
import { applyCodeWithDiff, applyEditCodeWithDiff } from "../../handler/diffHandler";
|
||||
import { getSymbolDefines } from "../../context/contextRefDefs";
|
||||
|
||||
|
||||
export namespace UnofficialEndpoints {
|
||||
@ -13,16 +11,6 @@ export namespace UnofficialEndpoints {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getSymbolDefinesInSelectedCode() {
|
||||
// find needed symbol defines in current editor document
|
||||
// return value is a list of symbol defines
|
||||
// each define has three fileds:
|
||||
// path: file path contain that symbol define
|
||||
// startLine: start line in that file
|
||||
// content: source code for symbol define
|
||||
return getSymbolDefines();
|
||||
}
|
||||
|
||||
export async function runCode(code: string) {
|
||||
// run code
|
||||
// delcare use vscode
|
||||
|
@ -81,10 +81,6 @@ const functionRegistry: any = {
|
||||
/**
|
||||
* Unofficial endpoints
|
||||
*/
|
||||
"/get_symbol_defines_in_selected_code": {
|
||||
keys: [],
|
||||
handler: UnofficialEndpoints.getSymbolDefinesInSelectedCode,
|
||||
},
|
||||
"/run_code": {
|
||||
keys: ["code"],
|
||||
handler: UnofficialEndpoints.runCode,
|
||||
|
@ -51,6 +51,7 @@ export function createStatusBarItem(context: vscode.ExtensionContext): vscode.St
|
||||
// install devchat workflow commands
|
||||
if (!hasInstallCommands) {
|
||||
hasInstallCommands = true;
|
||||
await vscode.commands.executeCommand('DevChat.StartLocalService');
|
||||
await vscode.commands.executeCommand('DevChat.InstallCommands');
|
||||
// vscode.commands.executeCommand('DevChat.InstallCommandPython');
|
||||
}
|
||||
|
@ -1,545 +0,0 @@
|
||||
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 * as toml from '@iarna/toml';
|
||||
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 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;
|
||||
path: string;
|
||||
// default value is -1, which means not recommended
|
||||
recommend: number;
|
||||
}
|
||||
|
||||
export interface TopicEntry {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
root_prompt: LogEntry;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
latest_time: number;
|
||||
hidden: boolean;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
class DevChat {
|
||||
private commandRun: CommandRun;
|
||||
|
||||
constructor() {
|
||||
this.commandRun = new CommandRun();
|
||||
}
|
||||
|
||||
private async loadContextsFromFiles(contexts: string[] | undefined): Promise<string[]> {
|
||||
if (!contexts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loadedContexts: string[] = [];
|
||||
for (const context of contexts) {
|
||||
const contextContent = await getFileContent(context);
|
||||
if (!contextContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
loadedContexts.push(contextContent);
|
||||
}
|
||||
return loadedContexts;
|
||||
}
|
||||
|
||||
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 buildLogArgs(options: LogOptions): string[] {
|
||||
let args = ["-m", "devchat", "log"];
|
||||
|
||||
if (options.skip) {
|
||||
args.push('--skip', `${options.skip}`);
|
||||
}
|
||||
if (options.maxCount) {
|
||||
args.push('--max-count', `${options.maxCount}`);
|
||||
} else {
|
||||
const maxLogCount = DevChatConfig.getInstance().get('max_log_count');
|
||||
args.push('--max-count', `${maxLogCount}`);
|
||||
}
|
||||
|
||||
if (options.topic) {
|
||||
args.push('--topic', `${options.topic}`);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private parseOutData(stdout: string, isPartial: boolean): ChatResponse {
|
||||
const responseLines = stdout.trim().split("\n");
|
||||
|
||||
if (responseLines.length < 2) {
|
||||
return this.createChatResponse("", "", "", "", !isPartial);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
private async runCommand(args: string[]): Promise<{code: number | null, stdout: string, stderr: string}> {
|
||||
// build env variables for command
|
||||
const envs = {
|
||||
...process.env,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"PYTHONUTF8":1,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"PYTHONPATH": UiUtilWrapper.extensionPath() + "/tools/site-packages",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"DEVCHAT_PROXY": DevChatConfig.getInstance().get('DEVCHAT_PROXY') || "",
|
||||
"MAMBA_BIN_PATH": getMicromambaUrl(),
|
||||
};
|
||||
|
||||
const pythonApp = DevChatConfig.getInstance().get('python_for_chat') || "python3";
|
||||
|
||||
// run command
|
||||
const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(
|
||||
pythonApp,
|
||||
args,
|
||||
{
|
||||
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
env: envs
|
||||
},
|
||||
undefined, undefined, undefined, undefined
|
||||
);
|
||||
|
||||
return {code, stdout, stderr};
|
||||
}
|
||||
|
||||
public input(data: string) {
|
||||
this.commandRun?.write(data + "\n");
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.commandRun.stop();
|
||||
}
|
||||
|
||||
async chat(content: string, options: ChatOptions = {}, onData: (data: ChatResponse) => void, saveToLog: boolean = true): Promise<ChatResponse> {
|
||||
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);
|
||||
let promptHash = "";
|
||||
if (saveToLog) {
|
||||
const logs = await this.logInsert(options.context, content, responseData.response, options.parent);
|
||||
assertValue(!logs || !logs.length, "Failed to insert devchat log");
|
||||
promptHash = logs[0]["hash"];
|
||||
}
|
||||
// return result
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"prompt-hash": promptHash,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async logInsert(contexts: string[] | undefined, request: string, response: string, parent: string | undefined): Promise<LogEntry[]> {
|
||||
try {
|
||||
// build log data
|
||||
const llmModelData = await ApiKeyManager.llmModel();
|
||||
const contextContentList = await this.loadContextsFromFiles(contexts);
|
||||
const contextWithRoleList = contextContentList.map(content => {
|
||||
return {
|
||||
"role": "system",
|
||||
"content": `<context>${content}</context>`
|
||||
};
|
||||
});
|
||||
|
||||
let logData = {
|
||||
"model": llmModelData?.model || "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": request
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": response
|
||||
},
|
||||
...contextWithRoleList
|
||||
],
|
||||
"timestamp": Math.floor(Date.now()/1000),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"request_tokens": 1,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"response_tokens": 1,
|
||||
...parent? {"parent": parent} : {}
|
||||
};
|
||||
const insertValue = JSON.stringify(logData);
|
||||
let insertValueOrFile = insertValue;
|
||||
if (insertValue.length > 4 * 1024) {
|
||||
const tempDir = os.tmpdir();
|
||||
const tempFile = path.join(tempDir, 'devchat_log_insert.json');
|
||||
await fs.promises.writeFile(tempFile, insertValue);
|
||||
insertValueOrFile = tempFile;
|
||||
}
|
||||
|
||||
// build args for log insert
|
||||
const args = ["-m", "devchat", "log", "--insert", insertValueOrFile];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
assertValue(stdout.indexOf('Failed to insert log') >= 0, stdout);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
const logs = JSON.parse(stdout.trim()).reverse();
|
||||
for (const log of logs) {
|
||||
log.response = log.responses[0];
|
||||
delete log.responses;
|
||||
}
|
||||
return logs;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Failed to insert log: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async delete(hash: string): Promise<boolean> {
|
||||
try {
|
||||
// build args for log delete
|
||||
const args = ["-m", "devchat", "log", "--delete", hash];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
assertValue(stdout.indexOf('Failed to delete prompt') >= 0, stdout);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Failed to delete log: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async log(options: LogOptions = {}): Promise<LogEntry[]> {
|
||||
try {
|
||||
const args = this.buildLogArgs(options);
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
const logs = JSON.parse(stdout.trim()).reverse();
|
||||
for (const log of logs) {
|
||||
log.response = log.responses[0];
|
||||
delete log.responses;
|
||||
}
|
||||
return logs;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Failed to get logs: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async loadRecommendCommands(): Promise<string[]> {
|
||||
try {
|
||||
const args = ["-m", "devchat", "workflow", "config", "--json"];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
let workflowConfig;
|
||||
try {
|
||||
workflowConfig = JSON.parse(stdout.trim());
|
||||
} catch (error) {
|
||||
logger.channel()?.error('Failed to parse commands JSON:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return workflowConfig.recommend?.workflows || [];
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async commands(): Promise<any[]> {
|
||||
try {
|
||||
const args = ["-m", "devchat", "workflow", "list", "--json"];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
let commands;
|
||||
try {
|
||||
commands = JSON.parse(stdout.trim());
|
||||
} catch (error) {
|
||||
logger.channel()?.error('Failed to parse commands JSON:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 确保每个CommandEntry对象的recommend字段默认为-1
|
||||
const recommendCommands = await this.loadRecommendCommands();
|
||||
commands = commands.map((cmd: any) => ({
|
||||
...cmd,
|
||||
recommend: recommendCommands.indexOf(cmd.name),
|
||||
}));
|
||||
|
||||
return commands;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async updateSysCommand(): Promise<string> {
|
||||
try {
|
||||
const args = ["-m", "devchat", "workflow", "update"];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
logger.channel()?.trace(`${stdout}`);
|
||||
return stdout;
|
||||
} catch (error: any) {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async topics(): Promise<TopicEntry[]> {
|
||||
try {
|
||||
const args = ["-m", "devchat", "topic", "-l"];
|
||||
|
||||
const {code, stdout, stderr} = await this.runCommand(args);
|
||||
|
||||
assertValue(code !== 0, stderr || `Command exited with ${code}`);
|
||||
if (stderr.trim() !== "") {
|
||||
logger.channel()?.warn(`${stderr}`);
|
||||
}
|
||||
|
||||
const topics = JSON.parse(stdout.trim()).reverse();
|
||||
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: any) {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DevChat;
|
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
475
src/toolwrapper/devchatClient.ts
Normal file
475
src/toolwrapper/devchatClient.ts
Normal file
@ -0,0 +1,475 @@
|
||||
import axios, { AxiosResponse, CancelTokenSource } from "axios";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
import { logger } from "../util/logger";
|
||||
import { getFileContent } from "../util/commonUtil";
|
||||
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
|
||||
|
||||
class DCLocalServicePortNotSetError extends Error {
|
||||
constructor() {
|
||||
super("DC_LOCALSERVICE_PORT is not set");
|
||||
this.name = "DCLocalServicePortNotSetError";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function catchAndReturn(defaultReturn: any) {
|
||||
return function (
|
||||
target: Object,
|
||||
propertyKey: string,
|
||||
descriptor: TypedPropertyDescriptor<any>
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
try {
|
||||
return await originalMethod.apply(this, args);
|
||||
} catch (error) {
|
||||
if (error instanceof DCLocalServicePortNotSetError) {
|
||||
logger.channel()?.warn(`DC_LOCALSERVICE_PORT is not set in [${propertyKey}]`);
|
||||
return defaultReturn;
|
||||
}
|
||||
|
||||
logger.channel()?.error(`Error in [${propertyKey}]: ${error}`);
|
||||
return defaultReturn;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}>;
|
||||
}
|
||||
|
||||
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 | undefined;
|
||||
|
||||
private _cancelMessageToken: CancelTokenSource | null = null;
|
||||
|
||||
static readonly logRawDataSizeLimit = 4 * 1024;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async _get(path: string, config?: any): Promise<AxiosResponse> {
|
||||
if (!this.baseURL) {
|
||||
if (!process.env.DC_LOCALSERVICE_PORT) {
|
||||
logger.channel()?.info("No local service port found.");
|
||||
throw new DCLocalServicePortNotSetError();
|
||||
}
|
||||
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
|
||||
this.baseURL = `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.channel()?.debug(`GET request to ${this.baseURL}${path}`);
|
||||
const response = await axios.get(`${this.baseURL}${path}`, config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async _post(path: string, data: any = undefined): Promise<AxiosResponse> {
|
||||
if (!this.baseURL) {
|
||||
if (!process.env.DC_LOCALSERVICE_PORT) {
|
||||
logger.channel()?.info("No local service port found.");
|
||||
throw new DCLocalServicePortNotSetError();
|
||||
}
|
||||
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
|
||||
this.baseURL = `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.channel()?.debug(`POST request to ${this.baseURL}${path}`);
|
||||
const response = await axios.post(`${this.baseURL}${path}`, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn([])
|
||||
async getWorkflowList(): Promise<any[]> {
|
||||
const response = await this._get("/workflows/list");
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`getWorkflowList response data: \n${JSON.stringify(
|
||||
response.data
|
||||
)}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn({})
|
||||
async getWorkflowConfig(): Promise<any> {
|
||||
const response = await this._get("/workflows/config");
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`getWorkflowConfig response data: \n${JSON.stringify(
|
||||
response.data
|
||||
)}`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn(undefined)
|
||||
async updateWorkflows(): Promise<void> {
|
||||
const response = await this._post("/workflows/update");
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`updateWorkflows response data: \n${JSON.stringify(
|
||||
response.data
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
@timeThis
|
||||
async message(
|
||||
message: ChatRequest,
|
||||
onData: (data: ChatResponse) => void
|
||||
): Promise<ChatResponse> {
|
||||
if (!this.baseURL) {
|
||||
if (!process.env.DC_LOCALSERVICE_PORT) {
|
||||
logger.channel()?.info("No local service port found.");
|
||||
}
|
||||
const port: number = parseInt(process.env.DC_LOCALSERVICE_PORT || '8008', 10);
|
||||
this.baseURL = `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
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": "", // prompt-hash is not in chatting response, it is created in the later insertLog()
|
||||
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()?.debug("should run workflow via cli.");
|
||||
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);
|
||||
chatRes.isError = true;
|
||||
chatRes.response += `\n${error}`;
|
||||
resolve(chatRes); // handle error by resolving the promise
|
||||
});
|
||||
} catch (error) {
|
||||
const errorRes: ChatResponse = {
|
||||
"prompt-hash": "",
|
||||
user: "",
|
||||
date: "",
|
||||
response: `${error}`,
|
||||
finish_reason: "",
|
||||
isError: true,
|
||||
};
|
||||
resolve(errorRes); // handle error by resolving the promise using an errorRes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@catchAndReturn({ hash: "" })
|
||||
async insertLog(logData: LogData): Promise<LogInsertRes> {
|
||||
let body = {
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
};
|
||||
|
||||
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);
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`insertLog response data: ${JSON.stringify(
|
||||
response.data
|
||||
)}, ${typeof response.data}}`
|
||||
);
|
||||
|
||||
// Clean up temp file
|
||||
if (filepath) {
|
||||
try {
|
||||
await fs.promises.unlink(filepath);
|
||||
} catch (error) {
|
||||
logger
|
||||
.channel()
|
||||
?.error(`Failed to delete temp file ${filepath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const res: LogInsertRes = {
|
||||
hash: response.data["hash"],
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn({ success: false })
|
||||
async deleteLog(logHash: string): Promise<LogDeleteRes> {
|
||||
const data = {
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
hash: logHash,
|
||||
};
|
||||
const response = await this._post("/logs/delete", data);
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`deleteLog response data: ${JSON.stringify(
|
||||
response.data
|
||||
)}, ${typeof response.data}}`
|
||||
);
|
||||
|
||||
const res: LogDeleteRes = {
|
||||
success: response.data["success"],
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn([])
|
||||
async getTopicLogs(
|
||||
topicRootHash: string,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<ShortLog[]> {
|
||||
const data = {
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
};
|
||||
const response = await this._get(`/topics/${topicRootHash}/logs`, {
|
||||
params: data,
|
||||
});
|
||||
|
||||
const logs: ShortLog[] = response.data;
|
||||
logs.reverse();
|
||||
|
||||
logger
|
||||
.channel()
|
||||
?.trace(`getTopicLogs response data: ${JSON.stringify(logs)}`);
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn([])
|
||||
async getTopics(limit: number, offset: number): Promise<any[]> {
|
||||
const data = {
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
};
|
||||
const response = await this._get(`/topics`, {
|
||||
params: data,
|
||||
});
|
||||
|
||||
const topics: any[] = response.data;
|
||||
topics.reverse();
|
||||
|
||||
logger
|
||||
.channel()
|
||||
?.trace(`getTopics response data: ${JSON.stringify(topics)}`);
|
||||
|
||||
return topics;
|
||||
}
|
||||
|
||||
@timeThis
|
||||
@catchAndReturn(undefined)
|
||||
async deleteTopic(topicRootHash: string): Promise<void> {
|
||||
const data = {
|
||||
topic_hash: topicRootHash,
|
||||
workspace: UiUtilWrapper.workspaceFoldersFirstPath(),
|
||||
};
|
||||
|
||||
const response = await this._post("/topics/delete", data);
|
||||
|
||||
logger
|
||||
.channel()
|
||||
?.trace(
|
||||
`deleteTopic response data: ${JSON.stringify(response.data)}`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
stopAllRequest(): void {
|
||||
this.cancelMessage();
|
||||
// add other requests here if needed
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
// src/apiKey.ts
|
||||
|
||||
import DevChat from '@/toolwrapper/devchat';
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
import { DevChatConfig } from './config';
|
||||
import { logger } from './logger';
|
||||
|
||||
|
26
src/util/findServicePort.ts
Normal file
26
src/util/findServicePort.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import net from 'net';
|
||||
|
||||
export async function findAvailablePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer().listen();
|
||||
|
||||
server.on('listening', () => {
|
||||
const address = server.address();
|
||||
if (typeof address !== 'object' || !address?.port) {
|
||||
server.close();
|
||||
reject(new Error('Failed to get port from server'));
|
||||
return;
|
||||
}
|
||||
server.close(() => resolve(address.port));
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
const errWithCode = err as NodeJS.ErrnoException;
|
||||
if (errWithCode.code === 'EADDRINUSE') {
|
||||
reject(new Error('Port already in use'));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
105
src/util/localService.ts
Normal file
105
src/util/localService.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { findAvailablePort } from './findServicePort';
|
||||
import * as http from 'http';
|
||||
import { logger } from './logger';
|
||||
import { DevChatConfig } from './config';
|
||||
|
||||
let serviceProcess: ChildProcess | null = null;
|
||||
|
||||
export async function startLocalService(extensionPath: string, workspacePath: string): Promise<number> {
|
||||
if (serviceProcess) {
|
||||
throw new Error('Local service is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 获取可用端口号
|
||||
const port = await findAvailablePort();
|
||||
|
||||
// 2. 设置环境变量 DC_SVC_PORT
|
||||
process.env.DC_SVC_PORT = port.toString();
|
||||
|
||||
// 3. 设置 DC_SVC_WORKSPACE 环境变量
|
||||
process.env.DC_SVC_WORKSPACE = workspacePath;
|
||||
|
||||
// 新增:设置 PYTHONPATH 环境变量
|
||||
process.env.PYTHONPATH = `${extensionPath}/tools/site-packages`;
|
||||
|
||||
// 4. 启动进程 python main.py
|
||||
const mainPyPath = extensionPath + "/tools/site-packages/devchat/_service/main.py";
|
||||
const pythonApp =
|
||||
DevChatConfig.getInstance().get("python_for_chat") || "python3";
|
||||
serviceProcess = spawn(pythonApp, [mainPyPath], {
|
||||
env: { ...process.env },
|
||||
stdio: 'inherit',
|
||||
windowsHide: true, // hide the console window on Windows
|
||||
});
|
||||
|
||||
serviceProcess.on('error', (err) => {
|
||||
logger.channel()?.error('Failed to start local service:', err);
|
||||
serviceProcess = null;
|
||||
});
|
||||
|
||||
serviceProcess.on('exit', (code) => {
|
||||
logger.channel()?.info(`Local service exited with code ${code}`);
|
||||
serviceProcess = null;
|
||||
});
|
||||
|
||||
// 5. 等待服务启动并验证
|
||||
await waitForServiceToStart(port);
|
||||
|
||||
// 6. 服务启动成功后,记录启动的端口号到环境变量
|
||||
process.env.DC_LOCALSERVICE_PORT = port.toString();
|
||||
logger.channel()?.info(`Local service port recorded: ${port}`);
|
||||
|
||||
return port;
|
||||
} catch (error) {
|
||||
logger.channel()?.error('Error starting local service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForServiceToStart(port: number): Promise<void> {
|
||||
const maxRetries = 30;
|
||||
const retryInterval = 1000; // 1 second
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await new Promise<string>((resolve, reject) => {
|
||||
http.get(`http://localhost:${port}/ping`, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
|
||||
if (response === '{"message":"pong"}') {
|
||||
logger.channel()?.info('Local service started successfully');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors and continue retrying
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
||||
}
|
||||
|
||||
throw new Error('Failed to start local service: timeout');
|
||||
}
|
||||
|
||||
export async function stopLocalService(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (!serviceProcess) {
|
||||
logger.channel()?.warn('No local service is running');
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
serviceProcess.on('exit', () => {
|
||||
serviceProcess = null;
|
||||
logger.channel()?.info('Local service stopped');
|
||||
resolve();
|
||||
});
|
||||
|
||||
serviceProcess.kill();
|
||||
});
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
// import { describe, it } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import * as path from 'path';
|
||||
import { parseMessage, parseMessageAndSetOptions, processChatResponse, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase';
|
||||
import { ChatResponse } from '../../src/toolwrapper/devchat';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
const envPath = path.join(__dirname, '../../', '.env');
|
||||
dotenv.config({ path: envPath });
|
||||
|
||||
describe('sendMessageBase', () => {
|
||||
let workspaceFoldersFirstPathStub: sinon.SinonStub;
|
||||
let getConfigurationStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
|
||||
getConfigurationStub = sinon.stub(UiUtilWrapper, 'getConfiguration');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
workspaceFoldersFirstPathStub.restore();
|
||||
getConfigurationStub.restore();
|
||||
});
|
||||
|
||||
describe('parseMessage', () => {
|
||||
it('should parse message correctly', () => {
|
||||
const message = '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!';
|
||||
const result = parseMessage(message);
|
||||
|
||||
expect(result.context).to.deep.equal(['path/to/context']);
|
||||
expect(result.instruction).to.deep.equal(['path/to/instruction']);
|
||||
expect(result.reference).to.deep.equal(['path/to/reference']);
|
||||
expect(result.text).to.equal('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMessageAndSetOptions', () => {
|
||||
it('should parse message and set options correctly', async () => {
|
||||
const message = {
|
||||
text: '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!'
|
||||
};
|
||||
|
||||
const [result, chatOptions] = await parseMessageAndSetOptions(message);
|
||||
|
||||
expect(result.context).to.deep.equal(['path/to/context']);
|
||||
expect(result.instruction).to.deep.equal(['path/to/instruction']);
|
||||
expect(result.reference).to.deep.equal(['path/to/reference']);
|
||||
expect(result.text).to.equal('Hello, world!');
|
||||
expect(chatOptions.context).to.deep.equal(['path/to/context']);
|
||||
expect(chatOptions.header).to.deep.equal([]);
|
||||
expect(chatOptions.reference).to.deep.equal(['path/to/reference']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processChatResponse', () => {
|
||||
it('should handle response text correctly when isError is false', async () => {
|
||||
const partialDataText = 'Partial data';
|
||||
const chatResponse: ChatResponse = {
|
||||
"finish_reason": "",
|
||||
response: 'Hello, user!',
|
||||
isError: false,
|
||||
user: 'user',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
'prompt-hash': 'responsehash'
|
||||
};
|
||||
|
||||
const result = await processChatResponse(chatResponse);
|
||||
expect(result).to.equal('Hello, user!');
|
||||
});
|
||||
|
||||
it('should handle response text correctly when isError is true', async () => {
|
||||
const partialDataText = 'Partial data';
|
||||
const chatResponse: ChatResponse = {
|
||||
"finish_reason": "",
|
||||
response: 'Error occurred!',
|
||||
isError: true,
|
||||
user: 'user',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
'prompt-hash': 'responsehash'
|
||||
};
|
||||
|
||||
const result = await processChatResponse(chatResponse);
|
||||
expect(result).to.equal('Error occurred!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendMessageBase', () => {
|
||||
it('should send message correct with DevChat access key', async function() {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'Access_Key_DevChat').returns(process.env.TEST_DEVCHAT_KEY);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
|
||||
const result = await sendMessageBase(message, handlePartialData);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result!.command).to.equal('receiveMessage');
|
||||
expect(result!.text).to.be.a('string');
|
||||
expect(result!.hash).to.be.a('string');
|
||||
expect(result!.user).to.be.a('string');
|
||||
expect(result!.date).to.be.a('string');
|
||||
// TODO fix
|
||||
// Need to mock more config setting
|
||||
// expect(result!.isError).to.be.false;
|
||||
});
|
||||
|
||||
it('should send message error with invalid api key', async function() {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'Access_Key_DevChat').returns('sk-KvH7ZCtHmFDCBTqH0jUv');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns('0');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
|
||||
const result = await sendMessageBase(message, handlePartialData);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result!.command).to.equal('receiveMessage');
|
||||
expect(result!.text).to.be.a('string');
|
||||
expect(result!.hash).to.be.a('string');
|
||||
expect(result!.user).to.be.a('string');
|
||||
expect(result!.date).to.be.a('string');
|
||||
expect(result!.isError).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopDevChatBase', () => {
|
||||
it('should stop sendMessageBase correctly', async () => {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'Access_Key_DevChat').returns(process.env.TEST_DEVCHAT_KEY);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
|
||||
|
||||
// Start sendMessageBase in a separate Promise
|
||||
const sendMessagePromise = sendMessageBase(message, handlePartialData);
|
||||
|
||||
// Wait for a short period to ensure sendMessageBase has started
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Call stopDevChatBase
|
||||
const stopMessage = {
|
||||
text: 'stop'
|
||||
};
|
||||
await stopDevChatBase(stopMessage);
|
||||
|
||||
// Check if sendMessageBase has been stopped and returns an error
|
||||
// TODO fix
|
||||
// Need to mock more config setting
|
||||
// try {
|
||||
// const result = await sendMessagePromise;
|
||||
// expect(result).to.undefined;
|
||||
// } catch (error) {
|
||||
// expect(error).to.be.an('error');
|
||||
// }
|
||||
});
|
||||
});
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
// import { describe, it } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import DevChat, { ChatOptions } from '../../src/toolwrapper/devchat';
|
||||
import { CommandRun } from '../../src/util/commonUtil';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
import { ApiKeyManager } from '../../src/util/apiKey';
|
||||
|
||||
|
||||
describe('DevChat', () => {
|
||||
let devChat: DevChat;
|
||||
let spawnAsyncStub: sinon.SinonStub;
|
||||
let workspaceFoldersFirstPathStub: sinon.SinonStub;
|
||||
let apiKeyManagerStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
devChat = new DevChat();
|
||||
spawnAsyncStub = sinon.stub(CommandRun.prototype, 'spawnAsync');
|
||||
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
|
||||
apiKeyManagerStub = sinon.stub(ApiKeyManager, 'llmModel');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
spawnAsyncStub.restore();
|
||||
workspaceFoldersFirstPathStub.restore();
|
||||
apiKeyManagerStub.restore();
|
||||
});
|
||||
|
||||
|
||||
describe('chat', () => {
|
||||
it('should return a ChatResponse object with isError false when the chat is successful', async () => {
|
||||
const content = 'Test chat content';
|
||||
const options: ChatOptions = {
|
||||
// Provide mock values for the options
|
||||
parent: 'parent_value',
|
||||
reference: ['ref1', 'ref2'],
|
||||
header: ['header1', 'header2'],
|
||||
context: ['context1', 'context2'],
|
||||
};
|
||||
const mockResponse = {
|
||||
exitCode: 0,
|
||||
stdout: 'User: Test user\nDate: 2022-01-01\nTest chat response\nprompt-hash: 12345',
|
||||
stderr: '',
|
||||
};
|
||||
const mockWorkspacePath = './';
|
||||
const llmModelResponse = {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"api_key": "DC.1234567890"
|
||||
}
|
||||
|
||||
spawnAsyncStub.resolves(mockResponse);
|
||||
workspaceFoldersFirstPathStub.returns(mockWorkspacePath);
|
||||
apiKeyManagerStub.resolves(llmModelResponse);
|
||||
|
||||
const response = await devChat.chat(content, options, (data)=>{}, false);
|
||||
|
||||
expect(response).to.have.property('prompt-hash', '');
|
||||
expect(response).to.have.property('user', '');
|
||||
expect(response).to.have.property('date', '');
|
||||
expect(response).to.have.property('response', 'Test chat response');
|
||||
expect(response).to.have.property('isError', false);
|
||||
expect(spawnAsyncStub.calledOnce).to.be.true;
|
||||
expect(workspaceFoldersFirstPathStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
// Add more test cases for the chat method here
|
||||
});
|
||||
|
||||
// ... other test cases
|
||||
});
|
29
test/util/findServicePort.test.ts
Normal file
29
test/util/findServicePort.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// test/util/findServicePort.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import net from 'net';
|
||||
import { findAvailablePort } from '../../src/util/findServicePort';
|
||||
|
||||
describe('findAvailablePort', () => {
|
||||
it('should return an available port when successful', async () => {
|
||||
// Arrange
|
||||
const expectedPort = await findAvailablePort();
|
||||
|
||||
// Act
|
||||
const server = net.createServer();
|
||||
const isAvailable = await new Promise<boolean>((resolve) => {
|
||||
server.listen(expectedPort, () => {
|
||||
server.close();
|
||||
resolve(true);
|
||||
});
|
||||
server.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(isAvailable).to.be.true;
|
||||
expect(expectedPort).to.be.a('number');
|
||||
expect(expectedPort).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
72
test/util/localService.test.ts
Normal file
72
test/util/localService.test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
// test/util/localService.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { startLocalService, stopLocalService } from '../../src/util/localService';
|
||||
import * as http from 'http';
|
||||
|
||||
describe('localService', () => {
|
||||
let port: number;
|
||||
|
||||
describe('startLocalService', () => {
|
||||
it('should start the local service successfully', async () => {
|
||||
const extensionPath = '.';
|
||||
const workspacePath = '.';
|
||||
|
||||
port = await startLocalService(extensionPath, workspacePath);
|
||||
|
||||
expect(port).to.be.a('number');
|
||||
expect(process.env.DC_SVC_PORT).to.equal(port.toString());
|
||||
expect(process.env.DC_SVC_WORKSPACE).to.equal(workspacePath);
|
||||
expect(process.env.DC_LOCALSERVICE_PORT).to.equal(port.toString());
|
||||
|
||||
// Verify that the service is running by sending a ping request
|
||||
const response = await sendPingRequest(port);
|
||||
expect(response).to.equal('{"message":"pong"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopLocalService', () => {
|
||||
it('should stop the local service', async () => {
|
||||
await stopLocalService();
|
||||
|
||||
// Wait a bit to ensure the service has fully stopped
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Verify that the service is no longer running
|
||||
try {
|
||||
await sendPingRequest(port);
|
||||
throw new Error('Service is still running');
|
||||
} catch (error) {
|
||||
console.log('Error type:', typeof error);
|
||||
console.log('Error:', error);
|
||||
|
||||
if (error instanceof Error) {
|
||||
expect(error.message).to.include('connect ECONNREFUSED');
|
||||
} else if (typeof error === 'object' && error !== null) {
|
||||
// Check if the error object has a 'code' property
|
||||
if ('code' in error) {
|
||||
expect(error.code).to.equal('ECONNREFUSED');
|
||||
} else if ('errors' in error && Array.isArray(error.errors)) {
|
||||
// Check if it's an AggregateError-like object
|
||||
const hasConnectionRefused = error.errors.some((e: any) => e.code === 'ECONNREFUSED');
|
||||
expect(hasConnectionRefused).to.be.true;
|
||||
} else {
|
||||
throw new Error(`Unexpected error structure: ${JSON.stringify(error)}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unexpected error type: ${typeof error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function sendPingRequest(port: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`http://localhost:${port}/ping`, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
2
tools
2
tools
@ -1 +1 @@
|
||||
Subproject commit 0557f55a8d5dadabcf225a2b7a24db9db120f895
|
||||
Subproject commit c53f73df6322df029075593792bd5ec111bfd9df
|
@ -18,7 +18,8 @@
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"exclude": [
|
||||
"test","gui","tools"
|
||||
|
Loading…
x
Reference in New Issue
Block a user