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",
|
"title": "Install slash commands",
|
||||||
"category": "DevChat"
|
"category": "DevChat"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "DevChat.StartLocalService",
|
||||||
|
"title": "Start local service",
|
||||||
|
"category": "DevChat"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "DevChat.Chat",
|
"command": "DevChat.Chat",
|
||||||
"title": "Chat with DevChat",
|
"title": "Chat with DevChat",
|
||||||
@ -253,6 +258,10 @@
|
|||||||
{
|
{
|
||||||
"command": "DevChat.InstallCommands",
|
"command": "DevChat.InstallCommands",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "DevChat.StartLocalService",
|
||||||
|
"when": "false"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"explorer/context": [
|
"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 { UiUtilWrapper } from "../util/uiUtil";
|
||||||
|
|
||||||
import { sendCommandListByDevChatRun } from '../handler/workflowCommandHandler';
|
import { sendCommandListByDevChatRun } from '../handler/workflowCommandHandler';
|
||||||
import DevChat from "../toolwrapper/devchat";
|
import { DevChatClient } from "../toolwrapper/devchatClient";
|
||||||
import { chatWithDevChat } from '../handler/chatHandler';
|
import { chatWithDevChat } from '../handler/chatHandler';
|
||||||
import { focusDevChatInput } from '../handler/focusHandler';
|
import { focusDevChatInput } from '../handler/focusHandler';
|
||||||
import { DevChatConfig } from '../util/config';
|
import { DevChatConfig } from '../util/config';
|
||||||
import { MessageHandler } from "../handler/messageHandler";
|
import { MessageHandler } from "../handler/messageHandler";
|
||||||
|
import { startLocalService } from '../util/localService';
|
||||||
|
import { logger } from "../util/logger";
|
||||||
|
|
||||||
const readdir = util.promisify(fs.readdir);
|
const readdir = util.promisify(fs.readdir);
|
||||||
const mkdir = util.promisify(fs.mkdir);
|
const mkdir = util.promisify(fs.mkdir);
|
||||||
@ -196,7 +198,7 @@ export function registerInstallCommandsCommand(
|
|||||||
"workflowsCommands"
|
"workflowsCommands"
|
||||||
); // Adjust this path as needed
|
); // Adjust this path as needed
|
||||||
|
|
||||||
const devchat = new DevChat();
|
const dcClient = new DevChatClient();
|
||||||
|
|
||||||
if (!fs.existsSync(sysDirPath)) {
|
if (!fs.existsSync(sysDirPath)) {
|
||||||
await copyDirectory(pluginDirPath, sysDirPath);
|
await copyDirectory(pluginDirPath, sysDirPath);
|
||||||
@ -204,15 +206,15 @@ export function registerInstallCommandsCommand(
|
|||||||
|
|
||||||
// Check if ~/.chat/scripts directory exists
|
// Check if ~/.chat/scripts directory exists
|
||||||
if (!fs.existsSync(sysDirPath)) {
|
if (!fs.existsSync(sysDirPath)) {
|
||||||
// Directory does not exist, wait for updateSysCommand to finish
|
// Directory does not exist, wait for updateWorkflows to finish
|
||||||
await devchat.updateSysCommand();
|
await dcClient.updateWorkflows();
|
||||||
sendCommandListByDevChatRun();
|
sendCommandListByDevChatRun();
|
||||||
} else {
|
} else {
|
||||||
// Directory exists, execute sendCommandListByDevChatRun immediately
|
// Directory exists, execute sendCommandListByDevChatRun immediately
|
||||||
await sendCommandListByDevChatRun();
|
await sendCommandListByDevChatRun();
|
||||||
|
|
||||||
// Then asynchronously execute updateSysCommand
|
// Then asynchronously execute updateWorkflows
|
||||||
await devchat.updateSysCommand();
|
await dcClient.updateWorkflows();
|
||||||
await sendCommandListByDevChatRun();
|
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) {
|
export function registerDevChatChatCommand(context: vscode.ExtensionContext) {
|
||||||
let disposable = vscode.commands.registerCommand(
|
let disposable = vscode.commands.registerCommand(
|
||||||
"DevChat.Chat",
|
"DevChat.Chat",
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
registerFixCommand,
|
registerFixCommand,
|
||||||
registerExplainCommand,
|
registerExplainCommand,
|
||||||
registerQuickFixCommand,
|
registerQuickFixCommand,
|
||||||
|
registerStartLocalServiceCommand
|
||||||
} from './contributes/commands';
|
} from './contributes/commands';
|
||||||
import { regLanguageContext } from './contributes/context';
|
import { regLanguageContext } from './contributes/context';
|
||||||
import { regDevChatView } from './contributes/views';
|
import { regDevChatView } from './contributes/views';
|
||||||
@ -33,6 +34,7 @@ import { DevChatConfig } from './util/config';
|
|||||||
import { InlineCompletionProvider, registerCodeCompleteCallbackCommand } from "./contributes/codecomplete/codecomplete";
|
import { InlineCompletionProvider, registerCodeCompleteCallbackCommand } from "./contributes/codecomplete/codecomplete";
|
||||||
import { indexDir } from "./contributes/codecomplete/astIndex";
|
import { indexDir } from "./contributes/codecomplete/astIndex";
|
||||||
import registerQuickFixProvider from "./contributes/quickFixProvider";
|
import registerQuickFixProvider from "./contributes/quickFixProvider";
|
||||||
|
import { stopLocalService } from './util/localService';
|
||||||
|
|
||||||
|
|
||||||
async function migrateConfig() {
|
async function migrateConfig() {
|
||||||
@ -152,6 +154,7 @@ async function activate(context: vscode.ExtensionContext) {
|
|||||||
registerStatusBarItemClickCommand(context);
|
registerStatusBarItemClickCommand(context);
|
||||||
|
|
||||||
registerInstallCommandsCommand(context);
|
registerInstallCommandsCommand(context);
|
||||||
|
registerStartLocalServiceCommand(context);
|
||||||
|
|
||||||
createStatusBarItem(context);
|
createStatusBarItem(context);
|
||||||
|
|
||||||
@ -178,6 +181,13 @@ async function activate(context: vscode.ExtensionContext) {
|
|||||||
async function deactivate() {
|
async function deactivate() {
|
||||||
// stop devchat
|
// stop devchat
|
||||||
await stopDevChatBase({});
|
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.activate = activate;
|
||||||
exports.deactivate = deactivate;
|
exports.deactivate = deactivate;
|
||||||
|
@ -3,7 +3,7 @@ import { insertCodeBlockToFile } from './codeBlockHandler';
|
|||||||
import { replaceCodeBlockToFile } from './codeBlockHandler';
|
import { replaceCodeBlockToFile } from './codeBlockHandler';
|
||||||
import { doCommit } from './commitHandler';
|
import { doCommit } from './commitHandler';
|
||||||
import { getHistoryMessages } from './historyMessagesHandler';
|
import { getHistoryMessages } from './historyMessagesHandler';
|
||||||
import { getWorkflowCommandList } from './workflowCommandHandler';
|
import { handleRegCommandList } from './workflowCommandHandler';
|
||||||
import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage';
|
import { sendMessage, stopDevChat, regeneration, deleteChatMessage, userInput } from './sendMessage';
|
||||||
import { applyCodeWithDiff } from './diffHandler';
|
import { applyCodeWithDiff } from './diffHandler';
|
||||||
import { addConext } from './contextHandler';
|
import { addConext } from './contextHandler';
|
||||||
@ -36,7 +36,7 @@ messageHandler.registerHandler('doCommit', doCommit);
|
|||||||
messageHandler.registerHandler('historyMessages', getHistoryMessages);
|
messageHandler.registerHandler('historyMessages', getHistoryMessages);
|
||||||
// Register the command list
|
// Register the command list
|
||||||
// Response: { command: 'regCommandList', result: <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
|
// Send a message, send the message entered by the user to AI
|
||||||
// Response:
|
// Response:
|
||||||
// { command: 'receiveMessagePartial', text: <response message text>, user: <user>, date: <date> }
|
// { 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 {
|
export interface LoadHistoryMessages {
|
||||||
command: string;
|
command: string;
|
||||||
@ -41,18 +54,28 @@ OPENAI_API_KEY is missing from your environment or settings. Kindly input your O
|
|||||||
} as LogEntry;
|
} 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) {
|
if (!topicId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const devChat = new DevChat();
|
const dcClient = new DevChatClient();
|
||||||
const logOptions: LogOptions = {
|
const shortLogs: ShortLog[] = await dcClient.getTopicLogs(topicId, 10000, 0);
|
||||||
skip: 0,
|
|
||||||
maxCount: 10000,
|
const logEntries: Array<LogEntry> = [];
|
||||||
topic: topicId
|
for (const shortLog of shortLogs) {
|
||||||
};
|
const logE: LogEntry = {
|
||||||
const logEntries = await devChat.log(logOptions);
|
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;
|
return logEntries;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import DevChat, { ChatOptions, ChatResponse } from '../toolwrapper/devchat';
|
|
||||||
import { logger } from '../util/logger';
|
import { logger } from '../util/logger';
|
||||||
import { assertValue } from '../util/check';
|
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 };
|
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.
|
* 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.
|
* 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);
|
const [parsedMessage, chatOptions] = await parseMessageAndSetOptions(message);
|
||||||
logger.channel()?.trace(`parent hash: ${chatOptions.parent}`);
|
logger.channel()?.trace(`parent hash: ${chatOptions.parent}`);
|
||||||
|
|
||||||
// call devchat chat
|
|
||||||
const chatResponse = await devChat.chat(
|
// send chat message to devchat service
|
||||||
parsedMessage.text,
|
const llmModelData = await ApiKeyManager.llmModel();
|
||||||
chatOptions,
|
assertValue(!llmModelData || !llmModelData.model, 'You must select a LLM model to use for conversations');
|
||||||
(partialResponse: ChatResponse) => {
|
const chatReq: ChatRequest = {
|
||||||
const partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
content: parsedMessage.text,
|
||||||
handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date });
|
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");
|
assertValue(UserStopHandler.isUserInteractionStopped(), "User Stopped");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: 'receiveMessage',
|
command: 'receiveMessage',
|
||||||
text: processChatResponse(chatResponse),
|
text: processChatResponse(finalResponse),
|
||||||
hash: chatResponse['prompt-hash'],
|
hash: finalResponse['prompt-hash'],
|
||||||
user: chatResponse.user,
|
user: finalResponse.user,
|
||||||
date: chatResponse.date,
|
date: finalResponse.date,
|
||||||
isError: chatResponse.isError
|
isError: finalResponse.isError
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.channel()?.error(`Error occurred while sending response: ${error.message}`);
|
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> {
|
export async function stopDevChatBase(message: any): Promise<void> {
|
||||||
logger.channel()?.info(`Stopping devchat`);
|
logger.channel()?.info(`Stopping devchat`);
|
||||||
UserStopHandler.stopUserInteraction();
|
UserStopHandler.stopUserInteraction();
|
||||||
devChat.stop();
|
dcClient.stopAllRequest();
|
||||||
|
dcCLI.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,9 +279,10 @@ export async function deleteChatMessageBase(message:{'hash': string}): Promise<b
|
|||||||
try {
|
try {
|
||||||
assertValue(!message.hash, 'Message hash is required');
|
assertValue(!message.hash, 'Message hash is required');
|
||||||
|
|
||||||
// delete the message by devchat
|
// delete the message by devchatClient
|
||||||
const bSuccess = await devChat.delete(message.hash);
|
const res = await dcClient.deleteLog(message.hash);
|
||||||
assertValue(!bSuccess, "Failed to delete message from devchat");
|
assertValue(!res.success, "Failed to delete message from devchat client");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.channel()?.error(`Error: ${error.message}`);
|
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.
|
* @returns {Promise<void>} - A Promise that resolves when the message has been sent.
|
||||||
*/
|
*/
|
||||||
export async function sendTextToDevChat(text: string): Promise<void> {
|
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 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 { 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) {
|
export interface TopicEntry {
|
||||||
let workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
if (!workspaceDir) {
|
root_prompt: LogEntry;
|
||||||
workspaceDir = os.homedir();
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
}
|
latest_time: number;
|
||||||
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
|
hidden: boolean;
|
||||||
|
title: string | null;
|
||||||
if (!fs.existsSync(deletedTopicsPath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedTopics = fs.readFileSync(deletedTopicsPath, 'utf-8').split('\n');
|
|
||||||
// check whether topicId in deletedTopics
|
|
||||||
return deletedTopics.includes(topicId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册获取当前topic列表的命令
|
// 注册获取当前topic列表的命令
|
||||||
regInMessage({ command: 'getTopics' });
|
regInMessage({ command: "getTopics" });
|
||||||
regOutMessage({ command: 'getTopics', topics: [] });
|
regOutMessage({ command: "getTopics", topics: [] });
|
||||||
export async function getTopics(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
|
export async function getTopics(
|
||||||
const devChat = new DevChat();
|
message: any,
|
||||||
const topicEntriesAll: TopicEntry[] = await devChat.topics();
|
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||||
|
): Promise<void> {
|
||||||
|
const topics = await dcClient.getTopics(100, 0);
|
||||||
|
const entries: TopicEntry[] = [];
|
||||||
|
|
||||||
let topicEntries: TopicEntry[] = [];
|
for (const topic of topics) {
|
||||||
for (const topicEntry of topicEntriesAll) {
|
const rootLog: LogEntry = {
|
||||||
if (isDeleteTopic(topicEntry.root_prompt.hash)) {
|
hash: topic.root_prompt_hash,
|
||||||
continue;
|
parent: topic.root_prompt_parent,
|
||||||
}
|
user: topic.root_prompt_user,
|
||||||
// append topicEntry to topicEntries
|
date: topic.root_prompt_date,
|
||||||
topicEntries.push(topicEntry);
|
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的命令
|
// 注册删除topic的命令
|
||||||
regInMessage({ command: 'deleteTopic', topicId: '' });
|
regInMessage({ command: "deleteTopic", topicId: "" });
|
||||||
export async function deleteTopic(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
|
export async function deleteTopic(
|
||||||
|
message: any,
|
||||||
|
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||||
|
): Promise<void> {
|
||||||
const topicId = message.topicId;
|
const topicId = message.topicId;
|
||||||
let workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
await dcClient.deleteTopic(topicId);
|
||||||
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'));
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,60 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import { MessageHandler } from './messageHandler';
|
import { MessageHandler } from "./messageHandler";
|
||||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
import { regInMessage, regOutMessage } from "../util/reg_messages";
|
||||||
import { ApiKeyManager } from '../util/apiKey';
|
import { DevChatClient } from "../toolwrapper/devchatClient";
|
||||||
import DevChat from '../toolwrapper/devchat';
|
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'});
|
export async function getWorkflowCommandList(
|
||||||
regOutMessage({command: 'regCommandList', result: [{name: '', pattern: '', description: ''}]});
|
message: any,
|
||||||
export async function getWorkflowCommandList(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
panel: vscode.WebviewPanel | vscode.WebviewView
|
||||||
existPannel = panel;
|
): Promise<void> {
|
||||||
|
const dcClient = new DevChatClient();
|
||||||
|
|
||||||
const commandList = await new DevChat().commands();
|
// All workflows registered in DevChat
|
||||||
MessageHandler.sendMessage(panel, { command: 'regCommandList', result: commandList });
|
const workflows = await dcClient.getWorkflowList();
|
||||||
return;
|
|
||||||
|
// 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() {
|
export async function sendCommandListByDevChatRun() {
|
||||||
if (existPannel) {
|
if (existPannel) {
|
||||||
await getWorkflowCommandList({}, existPannel!);
|
await getWorkflowCommandList({}, existPannel!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import * as vscode from "vscode";
|
|
||||||
import { applyCodeWithDiff, applyEditCodeWithDiff } from "../../handler/diffHandler";
|
import { applyCodeWithDiff, applyEditCodeWithDiff } from "../../handler/diffHandler";
|
||||||
import { getSymbolDefines } from "../../context/contextRefDefs";
|
|
||||||
|
|
||||||
|
|
||||||
export namespace UnofficialEndpoints {
|
export namespace UnofficialEndpoints {
|
||||||
@ -13,16 +11,6 @@ export namespace UnofficialEndpoints {
|
|||||||
return true;
|
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) {
|
export async function runCode(code: string) {
|
||||||
// run code
|
// run code
|
||||||
// delcare use vscode
|
// delcare use vscode
|
||||||
|
@ -81,10 +81,6 @@ const functionRegistry: any = {
|
|||||||
/**
|
/**
|
||||||
* Unofficial endpoints
|
* Unofficial endpoints
|
||||||
*/
|
*/
|
||||||
"/get_symbol_defines_in_selected_code": {
|
|
||||||
keys: [],
|
|
||||||
handler: UnofficialEndpoints.getSymbolDefinesInSelectedCode,
|
|
||||||
},
|
|
||||||
"/run_code": {
|
"/run_code": {
|
||||||
keys: ["code"],
|
keys: ["code"],
|
||||||
handler: UnofficialEndpoints.runCode,
|
handler: UnofficialEndpoints.runCode,
|
||||||
|
@ -51,6 +51,7 @@ export function createStatusBarItem(context: vscode.ExtensionContext): vscode.St
|
|||||||
// install devchat workflow commands
|
// install devchat workflow commands
|
||||||
if (!hasInstallCommands) {
|
if (!hasInstallCommands) {
|
||||||
hasInstallCommands = true;
|
hasInstallCommands = true;
|
||||||
|
await vscode.commands.executeCommand('DevChat.StartLocalService');
|
||||||
await vscode.commands.executeCommand('DevChat.InstallCommands');
|
await vscode.commands.executeCommand('DevChat.InstallCommands');
|
||||||
// vscode.commands.executeCommand('DevChat.InstallCommandPython');
|
// 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
|
// src/apiKey.ts
|
||||||
|
|
||||||
import DevChat from '@/toolwrapper/devchat';
|
|
||||||
import { UiUtilWrapper } from './uiUtil';
|
|
||||||
import { DevChatConfig } from './config';
|
import { DevChatConfig } from './config';
|
||||||
import { logger } from './logger';
|
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/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"test","gui","tools"
|
"test","gui","tools"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user