Merge pull request #322 from devchat-ai/workflow-message-step
Workflow message step
This commit is contained in:
commit
31d9dd1a9a
1590
package-lock.json
generated
1590
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,9 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.75.0"
|
"vscode": "^1.75.0"
|
||||||
},
|
},
|
||||||
|
"extensionDependencies": [
|
||||||
|
"merico.lang-bridge-vsc"
|
||||||
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/devchat-ai/devchat-vscode.git"
|
"url": "https://github.com/devchat-ai/devchat-vscode.git"
|
||||||
@ -412,7 +415,6 @@
|
|||||||
"order": 7,
|
"order": 7,
|
||||||
"markdownDescription": "Specify the default llm model for DevChat. [Price of each model](https://devchat.ai/pricing)"
|
"markdownDescription": "Specify the default llm model for DevChat. [Price of each model](https://devchat.ai/pricing)"
|
||||||
},
|
},
|
||||||
|
|
||||||
"DevChat.OpenAI.stream": {
|
"DevChat.OpenAI.stream": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
@ -794,6 +796,9 @@
|
|||||||
"axios": "^1.3.6",
|
"axios": "^1.3.6",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"mdast": "^3.0.0",
|
||||||
|
"mdast-util-from-markdown": "^2.0.0",
|
||||||
|
"mdast-util-to-markdown": "^2.1.0",
|
||||||
"mobx": "^6.10.0",
|
"mobx": "^6.10.0",
|
||||||
"mobx-react": "^9.0.0",
|
"mobx-react": "^9.0.0",
|
||||||
"mobx-state-tree": "^5.1.8",
|
"mobx-state-tree": "^5.1.8",
|
||||||
@ -807,6 +812,8 @@
|
|||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"shell-escape": "^0.2.0",
|
"shell-escape": "^0.2.0",
|
||||||
"string-argv": "^0.3.2",
|
"string-argv": "^0.3.2",
|
||||||
|
"unified": "^11.0.3",
|
||||||
|
"unist-util-visit": "^5.0.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yaml": "^2.3.2"
|
"yaml": "^2.3.2"
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class CommandManager {
|
|||||||
CommandManager.instance.registerCommand({
|
CommandManager.instance.registerCommand({
|
||||||
name: 'ask-code',
|
name: 'ask-code',
|
||||||
pattern: 'ask-code',
|
pattern: 'ask-code',
|
||||||
description: 'ask code',
|
description: 'Ask anything about your codebase and get answers from our AI agent',
|
||||||
args: 0,
|
args: 0,
|
||||||
handler: async (commandName: string, userInput: string) => {
|
handler: async (commandName: string, userInput: string) => {
|
||||||
return '';
|
return '';
|
||||||
|
@ -284,9 +284,6 @@ export function registerAskCodeIndexStartCommand(context: vscode.ExtensionContex
|
|||||||
if (!pythonVirtualEnv) {
|
if (!pythonVirtualEnv) {
|
||||||
progressBar.update("Installing devchat-ask. See OUTPUT for progress...", 0);
|
progressBar.update("Installing devchat-ask. See OUTPUT for progress...", 0);
|
||||||
await installAskCode(supportedFileTypes, progressBar, indexCode);
|
await installAskCode(supportedFileTypes, progressBar, indexCode);
|
||||||
} else {
|
|
||||||
progressBar.update("Index source files. See OUTPUT for progress...", 0);
|
|
||||||
await indexCode(pythonVirtualEnv, supportedFileTypes, progressBar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateIndexingStatus("stopped");
|
updateIndexingStatus("stopped");
|
||||||
@ -310,7 +307,7 @@ async function installAskCode(supportedFileTypes, progressBar: any, callback: Fu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UiUtilWrapper.updateConfiguration("DevChat", "PythonVirtualEnv", pythonEnvPath.trim());
|
await UiUtilWrapper.updateConfiguration("DevChat", "PythonVirtualEnv", pythonEnvPath.trim());
|
||||||
logger.channel()?.info(`Installation finished.`);
|
logger.channel()?.info(`Installation finished.`);
|
||||||
|
|
||||||
// Execute the callback function after the installation is finished
|
// Execute the callback function after the installation is finished
|
||||||
@ -318,68 +315,6 @@ async function installAskCode(supportedFileTypes, progressBar: any, callback: Fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function indexCode(pythonVirtualEnv, supportedFileTypes, progressBar: any) {
|
async function indexCode(pythonVirtualEnv, supportedFileTypes, progressBar: any) {
|
||||||
let envs = {};
|
|
||||||
|
|
||||||
const llmModelData = await ApiKeyManager.llmModel();
|
|
||||||
if (!llmModelData) {
|
|
||||||
logger.channel()?.error('No valid llm model is selected!');
|
|
||||||
logger.channel()?.show();
|
|
||||||
|
|
||||||
progressBar.endWithError("No valid llm model is selected!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let openaiApiKey = llmModelData.api_key;
|
|
||||||
if (!openaiApiKey) {
|
|
||||||
logger.channel()?.error('The OpenAI key is invalid!');
|
|
||||||
logger.channel()?.show();
|
|
||||||
|
|
||||||
progressBar.endWithError("The OpenAI key is invalid!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
envs['OPENAI_API_KEY'] = openaiApiKey;
|
|
||||||
|
|
||||||
const openAiApiBase = llmModelData.api_base;
|
|
||||||
if (openAiApiBase) {
|
|
||||||
envs['OPENAI_API_BASE'] = openAiApiBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
|
||||||
|
|
||||||
const command = pythonVirtualEnv.trim();
|
|
||||||
const args = [UiUtilWrapper.extensionPath() + "/tools/askcode_index_query.py", "index", ".", supportedFileTypes];
|
|
||||||
const options = { env: envs, cwd: workspaceDir };
|
|
||||||
|
|
||||||
indexProcess = new CommandRun();
|
|
||||||
const result = await indexProcess.spawnAsync(command, args, options, (data) => {
|
|
||||||
if (data.includes('Skip file:')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.channel()?.info(`${data}`);
|
|
||||||
}, (data) => {
|
|
||||||
if (data.includes('Skip file:')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.channel()?.info(`${data}`);
|
|
||||||
}, undefined, undefined);
|
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
|
||||||
if (result.exitCode === null) {
|
|
||||||
logger.channel()?.info(`Indexing stopped!`);
|
|
||||||
progressBar.endWithError(`Indexing stopped!`);
|
|
||||||
} else {
|
|
||||||
logger.channel()?.error(`Indexing failed: ${result.stderr}`);
|
|
||||||
logger.channel()?.show();
|
|
||||||
progressBar.endWithError(`Indexing failed: ${result.stderr}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLastModifyTime();
|
|
||||||
logger.channel()?.info(`index finished.`);
|
|
||||||
|
|
||||||
progressBar.update("Indexing finished.");
|
|
||||||
progressBar.end();
|
progressBar.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export function checkDevChatDependency(showError: boolean = true): boolean {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if DevChat is installed
|
// Check if DevChat is installed
|
||||||
const expectVersion = 'DevChat 0.2.8';
|
const expectVersion = 'DevChat 0.2.9';
|
||||||
const devchatVersion = runCommand(`"${devChat}" --version`).toString().trim();
|
const devchatVersion = runCommand(`"${devChat}" --version`).toString().trim();
|
||||||
if (devchatVersion < expectVersion) {
|
if (devchatVersion < expectVersion) {
|
||||||
logger.channel()?.info(`devchat version: ${devchatVersion}, but expect version: ${expectVersion}`);
|
logger.channel()?.info(`devchat version: ${devchatVersion}, but expect version: ${expectVersion}`);
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
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 { stopDevChatBase, sendMessageBase, deleteChatMessageBase } from './sendMessageBase';
|
import { stopDevChatBase, sendMessageBase, deleteChatMessageBase, insertDevChatLog, handleTopic } from './sendMessageBase';
|
||||||
import { UiUtilWrapper } from '../util/uiUtil';
|
import { UiUtilWrapper } from '../util/uiUtil';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@ -15,6 +15,9 @@ import { CommandRun, createTempSubdirectory } from '../util/commonUtil';
|
|||||||
|
|
||||||
const exec = promisify(execCb);
|
const exec = promisify(execCb);
|
||||||
|
|
||||||
|
let askcode_stop = true;
|
||||||
|
let askcode_runner : CommandRun | null = null;
|
||||||
|
|
||||||
let _lastMessage: any = undefined;
|
let _lastMessage: any = undefined;
|
||||||
|
|
||||||
export function createTempFile(content: string): string {
|
export function createTempFile(content: string): string {
|
||||||
@ -35,9 +38,15 @@ export function deleteTempFiles(fileName: string): void {
|
|||||||
regInMessage({command: 'askCode', text: '', parent_hash: undefined});
|
regInMessage({command: 'askCode', text: '', parent_hash: undefined});
|
||||||
regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'});
|
regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'});
|
||||||
export async function askCode(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
export async function askCode(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||||
|
try {
|
||||||
|
askcode_stop = false;
|
||||||
|
askcode_runner = null;
|
||||||
|
|
||||||
_lastMessage = [message];
|
_lastMessage = [message];
|
||||||
_lastMessage[0]['askCode'] = true;
|
_lastMessage[0]['askCode'] = true;
|
||||||
|
|
||||||
|
const port = await UiUtilWrapper.getLSPBrigePort();
|
||||||
|
|
||||||
let pythonVirtualEnv: string|undefined = vscode.workspace.getConfiguration('DevChat').get('PythonVirtualEnv');
|
let pythonVirtualEnv: string|undefined = vscode.workspace.getConfiguration('DevChat').get('PythonVirtualEnv');
|
||||||
if (!pythonVirtualEnv) {
|
if (!pythonVirtualEnv) {
|
||||||
try {
|
try {
|
||||||
@ -78,36 +87,50 @@ export async function askCode(message: any, panel: vscode.WebviewPanel|vscode.We
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||||
|
if (askcode_stop) {
|
||||||
try {
|
return;
|
||||||
// create temp directory and file
|
|
||||||
const tempDir = await createTempSubdirectory('devchat/context');
|
|
||||||
const tempFile = path.join(tempDir, "doc_context.txt");
|
|
||||||
|
|
||||||
// If tempFile already exists, delete it
|
|
||||||
if (fs.existsSync(tempFile)) {
|
|
||||||
fs.unlinkSync(tempFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandRun = new CommandRun();
|
try {
|
||||||
|
let outputResult = "";
|
||||||
|
askcode_runner = new CommandRun();
|
||||||
const command = pythonVirtualEnv.trim();
|
const command = pythonVirtualEnv.trim();
|
||||||
const args = [UiUtilWrapper.extensionPath() + "/tools/askcode_index_query.py", "query", message.text, tempFile];
|
const args = [UiUtilWrapper.extensionPath() + "/tools/askcode_index_query.py", "query", message.text, `${port}`];
|
||||||
const result = await commandRun.spawnAsync(command, args, { env: envs, cwd: workspaceDir }, (data) => {
|
const result = await askcode_runner.spawnAsync(command, args, { env: envs, cwd: workspaceDir }, (data) => {
|
||||||
|
outputResult += data;
|
||||||
|
MessageHandler.sendMessage(panel, { command: 'receiveMessagePartial', text: outputResult, hash:"", user:"", isError: false });
|
||||||
logger.channel()?.info(data);
|
logger.channel()?.info(data);
|
||||||
}, (data) => {
|
}, (data) => {
|
||||||
logger.channel()?.error(data);
|
logger.channel()?.error(data);
|
||||||
}, undefined, undefined);
|
}, undefined, undefined);
|
||||||
|
|
||||||
// Check if tempFile has been written to
|
if (result.exitCode === 0) {
|
||||||
if (!fs.existsSync(tempFile) || fs.readFileSync(tempFile, 'utf8') === '') {
|
// save askcode result to devchat
|
||||||
logger.channel()?.error(`Did not get relevant context from AskCode.`);
|
const stepIndex = result.stdout.lastIndexOf("```Step");
|
||||||
|
const stepEndIndex = result.stdout.lastIndexOf("```");
|
||||||
|
let resultOut = result.stdout;
|
||||||
|
if (stepIndex > 0 && stepEndIndex > 0) {
|
||||||
|
resultOut = result.stdout.substring(stepEndIndex+3, result.stdout.length);
|
||||||
|
}
|
||||||
|
let logHash = await insertDevChatLog(message, "/ask-code " + message.text, resultOut);
|
||||||
|
if (!logHash) {
|
||||||
|
logHash = "";
|
||||||
|
logger.channel()?.error(`Failed to insert devchat log.`);
|
||||||
logger.channel()?.show();
|
logger.channel()?.show();
|
||||||
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: "Did not get relevant context from AskCode.", hash: "", user: "", date: 0, isError: true });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message
|
MessageHandler.sendMessage(panel, { command: 'receiveMessagePartial', text: result.stdout, hash:logHash, user:"", isError: false });
|
||||||
await sendMessage({command: "sendMessage", contextInfo: [{file: tempFile, context: ""}], text: message.text, parent_hash: message.hash}, panel);
|
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: result.stdout, hash:logHash, user:"", date:0, isError: false });
|
||||||
|
|
||||||
|
const dateStr = Math.floor(Date.now()/1000).toString();
|
||||||
|
await handleTopic(
|
||||||
|
message.parent_hash,
|
||||||
|
{"text": "/ask-code " + message.text},
|
||||||
|
{ response: result.stdout, "prompt-hash": logHash, user: "", "date": dateStr, finish_reason: "", isError: false });
|
||||||
|
} else {
|
||||||
|
logger.channel()?.info(`${result.stdout}`);
|
||||||
|
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: result.stderr, hash: "", user: "", date: 0, isError: true });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
logger.channel()?.error(`error: ${error.message}`);
|
logger.channel()?.error(`error: ${error.message}`);
|
||||||
@ -117,6 +140,10 @@ export async function askCode(message: any, panel: vscode.WebviewPanel|vscode.We
|
|||||||
logger.channel()?.show();
|
logger.channel()?.show();
|
||||||
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: "Did not get relevant context from AskCode.", hash: "", user: "", date: 0, isError: true });
|
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: "Did not get relevant context from AskCode.", hash: "", user: "", date: 0, isError: true });
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
askcode_stop = true;
|
||||||
|
askcode_runner = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -199,6 +226,15 @@ export async function regeneration(message: any, panel: vscode.WebviewPanel|vsco
|
|||||||
regInMessage({command: 'stopDevChat'});
|
regInMessage({command: 'stopDevChat'});
|
||||||
export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||||
stopDevChatBase(message);
|
stopDevChatBase(message);
|
||||||
|
|
||||||
|
if (askcode_stop === false) {
|
||||||
|
askcode_stop = true;
|
||||||
|
if (askcode_runner) {
|
||||||
|
askcode_runner.stop();
|
||||||
|
askcode_runner = null;
|
||||||
|
}
|
||||||
|
await vscode.commands.executeCommand('DevChat.AskCodeIndexStop');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
regInMessage({command: 'deleteChatMessage', hash: 'xxx'});
|
regInMessage({command: 'deleteChatMessage', hash: 'xxx'});
|
||||||
|
@ -178,6 +178,17 @@ export async function stopDevChatBase(message: any): Promise<void> {
|
|||||||
devChat.stop();
|
devChat.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function insertDevChatLog(message: any, request: string, response: string): Promise<string | undefined> {
|
||||||
|
logger.channel()?.info(`Inserting devchat log`);
|
||||||
|
await devChat.logInsert(request, response, message.parent_hash);
|
||||||
|
const logs = await devChat.log({"maxCount": 1});
|
||||||
|
if (logs && logs.length > 0) {
|
||||||
|
return logs[0]['hash'];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// delete a chat message
|
// delete a chat message
|
||||||
// each message is identified by hash
|
// each message is identified by hash
|
||||||
export async function deleteChatMessageBase(message:{'hash': string}): Promise<boolean> {
|
export async function deleteChatMessageBase(message:{'hash': string}): Promise<boolean> {
|
||||||
|
@ -301,6 +301,64 @@ class DevChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async logInsert(request: string, response: string, parent: string | undefined) {
|
||||||
|
let log_data = {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": request
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": Math.floor(Date.now()/1000),
|
||||||
|
"request_tokens": 1,
|
||||||
|
"response_tokens": 1
|
||||||
|
};
|
||||||
|
if (parent) {
|
||||||
|
log_data["parent"] = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const args = ["log", "--insert", JSON.stringify(log_data)];
|
||||||
|
const devChat = this.getDevChatPath();
|
||||||
|
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||||
|
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||||
|
|
||||||
|
logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`);
|
||||||
|
const spawnOptions = {
|
||||||
|
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
|
||||||
|
cwd: workspaceDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
OPENAI_API_KEY: openaiApiKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined);
|
||||||
|
|
||||||
|
logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`);
|
||||||
|
if (stderr) {
|
||||||
|
logger.channel()?.error(`Error: ${stderr}`);
|
||||||
|
logger.channel()?.show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stdout.indexOf('Failed to insert log') >= 0) {
|
||||||
|
logger.channel()?.error(`Failed to insert log: ${log_data}`);
|
||||||
|
logger.channel()?.show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
logger.channel()?.error(`Exit code: ${code}`);
|
||||||
|
logger.channel()?.show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async delete(hash: string): Promise<boolean> {
|
async delete(hash: string): Promise<boolean> {
|
||||||
const args = ["log", "--delete", hash];
|
const args = ["log", "--delete", hash];
|
||||||
const devChat = this.getDevChatPath();
|
const devChat = this.getDevChatPath();
|
||||||
|
@ -17,6 +17,7 @@ export interface UiUtil {
|
|||||||
// current selected text
|
// current selected text
|
||||||
selectText(): string | undefined;
|
selectText(): string | undefined;
|
||||||
showErrorMessage(message: string): void;
|
showErrorMessage(message: string): void;
|
||||||
|
getLSPBrigePort(): Promise<number | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -72,5 +73,9 @@ export class UiUtilWrapper {
|
|||||||
public static showErrorMessage(message: string): void {
|
public static showErrorMessage(message: string): void {
|
||||||
this._uiUtil?.showErrorMessage(message);
|
this._uiUtil?.showErrorMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getLSPBrigePort(): Promise<number | undefined> {
|
||||||
|
return await this._uiUtil?.getLSPBrigePort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,4 +122,9 @@ export class UiUtilVscode implements UiUtil {
|
|||||||
public showErrorMessage(message: string): void {
|
public showErrorMessage(message: string): void {
|
||||||
vscode.window.showErrorMessage(message);
|
vscode.window.showErrorMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getLSPBrigePort(): Promise<number | undefined> {
|
||||||
|
const port = await vscode.commands.executeCommand('LangBrige.getAddress') as number | undefined;;
|
||||||
|
return port;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { keyframes } from "@emotion/react";
|
import { keyframes } from "@emotion/react";
|
||||||
import { Box, Container, Text } from "@mantine/core";
|
import { Box, Container, Text } from "@mantine/core";
|
||||||
import MessageBody from "@/views/components/MessageBody";
|
import MessageBody from "@/views/components/MessageBody";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMst } from "@/views/stores/RootStore";
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
import { Message } from "@/views/stores/ChatStore";
|
import { Message } from "@/views/stores/ChatStore";
|
||||||
|
import {fromMarkdown} from 'mdast-util-from-markdown';
|
||||||
|
import {toMarkdown} from 'mdast-util-to-markdown';
|
||||||
|
|
||||||
const MessageBlink = observer(() => {
|
const MessageBlink = observer(() => {
|
||||||
const { chat } = useMst();
|
const { chat } = useMst();
|
||||||
@ -18,7 +19,7 @@ const MessageBlink = observer(() => {
|
|||||||
return <Text sx={{
|
return <Text sx={{
|
||||||
animation: `${blink} 0.5s infinite;`,
|
animation: `${blink} 0.5s infinite;`,
|
||||||
width: 5,
|
width: 5,
|
||||||
marginTop: chat.responsed ? 0 : '1em',
|
marginTop: '1em',
|
||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
display: 'block'
|
display: 'block'
|
||||||
}}>|</Text>;
|
}}>|</Text>;
|
||||||
@ -49,25 +50,19 @@ const CurrentMessage = observer((props: any) => {
|
|||||||
const { width } = props;
|
const { width } = props;
|
||||||
const { chat } = useMst();
|
const { chat } = useMst();
|
||||||
const { messages, currentMessage, generating, responsed, hasDone } = chat;
|
const { messages, currentMessage, generating, responsed, hasDone } = chat;
|
||||||
|
|
||||||
// split blocks
|
// split blocks
|
||||||
const messageBlocks = getBlocks(currentMessage);
|
const messageBlocks = fromMarkdown(currentMessage);
|
||||||
const lastMessageBlocks = getBlocks(messages[messages.length - 1]?.message);
|
const lastMessageBlocks = fromMarkdown(messages[messages.length - 1]?.message);
|
||||||
const fixedCount = lastMessageBlocks.length;
|
const fixedCount = lastMessageBlocks.children.length;
|
||||||
const receivedCount = messageBlocks.length;
|
const receivedCount = messageBlocks.children.length;
|
||||||
const renderBlocks = messageBlocks.splice(-1);
|
const renderBlocks = messageBlocks.children.splice(-1);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (generating) {
|
|
||||||
// new a bot message
|
|
||||||
const messageItem = Message.create({ type: 'bot', message: currentMessage });
|
|
||||||
chat.newMessage(messageItem);
|
|
||||||
}
|
|
||||||
}, [generating]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (generating && (receivedCount - fixedCount >= 1 || !responsed)) {
|
if (generating && (receivedCount - fixedCount >= 1 || !responsed)) {
|
||||||
chat.updateLastMessage(currentMessage);
|
chat.updateLastMessage(toMarkdown({
|
||||||
|
type: 'root',
|
||||||
|
children: messageBlocks.children
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}, [currentMessage, responsed, generating]);
|
}, [currentMessage, responsed, generating]);
|
||||||
|
|
||||||
@ -80,13 +75,18 @@ const CurrentMessage = observer((props: any) => {
|
|||||||
return generating
|
return generating
|
||||||
? <Box
|
? <Box
|
||||||
sx={{
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
marginTop: -5,
|
||||||
marginBottom: 50,
|
marginBottom: 50,
|
||||||
width: width,
|
width: width,
|
||||||
pre: {
|
pre: {
|
||||||
|
margin: 0,
|
||||||
whiteSpace: 'break-spaces'
|
whiteSpace: 'break-spaces'
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<MessageBody messageText={renderBlocks.join('\n\n')} messageType="bot" />
|
<MessageBody messageType="bot" temp={true} >
|
||||||
|
{renderBlocks.length>0?toMarkdown(renderBlocks[0]):''}
|
||||||
|
</MessageBody>
|
||||||
<MessageBlink />
|
<MessageBlink />
|
||||||
</Box>
|
</Box>
|
||||||
: <></>;
|
: <></>;
|
||||||
|
@ -60,27 +60,19 @@ const InputMessage = observer((props: any) => {
|
|||||||
if (inputValue) {
|
if (inputValue) {
|
||||||
if (inputValue.trim() === '/help') {
|
if (inputValue.trim() === '/help') {
|
||||||
chat.helpMessage();
|
chat.helpMessage();
|
||||||
input.setValue('');
|
|
||||||
event.preventDefault();
|
|
||||||
} else {
|
} else {
|
||||||
const text = inputValue;
|
const text = inputValue;
|
||||||
// Add the user's message to the chat UI
|
|
||||||
const chatContexts = contexts ? [...contexts].map((item) => ({ ...item })) : undefined;
|
const chatContexts = contexts ? [...contexts].map((item) => ({ ...item })) : undefined;
|
||||||
const newMessage = Message.create({
|
if (inputValue.trim().startsWith('/ask-code')) {
|
||||||
type: 'user',
|
chat.devchatAsk(text, chatContexts);
|
||||||
message: inputValue,
|
} else{
|
||||||
contexts: chatContexts
|
chat.commonMessage(text, chatContexts);
|
||||||
});
|
}
|
||||||
chat.newMessage(newMessage);
|
}
|
||||||
// start generating
|
|
||||||
chat.startGenerating(text, chatContexts);
|
|
||||||
// Clear the input field
|
// Clear the input field
|
||||||
input.setValue('');
|
input.setValue('');
|
||||||
input.clearContexts();
|
input.clearContexts();
|
||||||
setTimeout(() => {
|
event.preventDefault();
|
||||||
chat.goScrollBottom();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import MessageMarkdown from "@/views/components/MessageMarkdown";
|
|||||||
import { useMst } from "@/views/stores/RootStore";
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
messageText: string,
|
messageType: string,
|
||||||
messageType: string
|
children: string,
|
||||||
|
temp?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -17,14 +18,16 @@ const useStyles = createStyles((theme, options:any) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const MessageBody = observer((props: IProps) => {
|
const MessageBody = observer((props: IProps) => {
|
||||||
const { messageText, messageType } = props;
|
const { children, messageType, temp=false } = props;
|
||||||
const { chat } = useMst();
|
const { chat } = useMst();
|
||||||
const {classes} = useStyles({
|
const {classes} = useStyles({
|
||||||
chatPanelWidth:chat.chatPanelWidth
|
chatPanelWidth:chat.chatPanelWidth
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
messageType === 'bot'
|
messageType === 'bot'
|
||||||
? <MessageMarkdown className={classes.bodyWidth}>{messageText}</MessageMarkdown>
|
? <MessageMarkdown className={classes.bodyWidth} temp={temp}>
|
||||||
|
{children}
|
||||||
|
</MessageMarkdown>
|
||||||
: <Container
|
: <Container
|
||||||
sx={{
|
sx={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@ -35,7 +38,7 @@ const MessageBody = observer((props: IProps) => {
|
|||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<pre>{messageText}</pre>
|
<pre>{children}</pre>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { Stack, Container, Divider, Box } from "@mantine/core";
|
import { Stack, Container, Divider, Box, Group,Text, Button, createStyles } from "@mantine/core";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import MessageBody from "@/views/components/MessageBody";
|
import MessageBody from "@/views/components/MessageBody";
|
||||||
import MessageAvatar from "@/views/components/MessageAvatar";
|
import MessageAvatar from "@/views/components/MessageAvatar";
|
||||||
@ -8,14 +8,44 @@ import { useMst } from "@/views/stores/RootStore";
|
|||||||
import { Message } from "@/views/stores/ChatStore";
|
import { Message } from "@/views/stores/ChatStore";
|
||||||
import MessageContext from "@/views/components/MessageContext";
|
import MessageContext from "@/views/components/MessageContext";
|
||||||
import CurrentMessage from "@/views/components/CurrentMessage";
|
import CurrentMessage from "@/views/components/CurrentMessage";
|
||||||
|
import { Card } from '@mantine/core';
|
||||||
|
import { IconInfoSquareRounded } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
card:{
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
fontFamily: 'var(--vscode-editor-font-familyy)',
|
||||||
|
fontSize: 'var(--vscode-editor-font-size)',
|
||||||
|
color: 'var(--vscode-menu-foreground)',
|
||||||
|
borderColor: 'var(--vscode-menu-border)',
|
||||||
|
},
|
||||||
|
cardDescription:{
|
||||||
|
marginTop: 10,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
button:{
|
||||||
|
backgroundColor:"#ED6A45",
|
||||||
|
fontFamily: 'var(--vscode-editor-font-familyy)',
|
||||||
|
fontSize: 'var(--vscode-editor-font-size)',
|
||||||
|
color:"#fff",
|
||||||
|
"&:hover":{
|
||||||
|
backgroundColor:"#ED6A45",
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
"&:focus":{
|
||||||
|
backgroundColor:"#ED6A45",
|
||||||
|
opacity: 0.8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const MessageList = observer((props: any) => {
|
const MessageList = observer((props: any) => {
|
||||||
const { chat } = useMst();
|
const { chat } = useMst();
|
||||||
|
const {classes} = useStyles();
|
||||||
|
|
||||||
return (<Stack spacing={0} sx={{margin:'0 10px 10px 10px'}}>
|
return (<Stack spacing={0} sx={{margin:'0 10px 10px 10px'}}>
|
||||||
{chat.messages.map((item, index: number) => {
|
{chat.messages.map((item, index: number) => {
|
||||||
const { message: messageText, type: messageType, hash: messageHash, contexts } = item;
|
const { message: messageText, type: messageType, hash: messageHash, contexts, confirm } = item;
|
||||||
// setMessage(messageText);
|
// setMessage(messageText);
|
||||||
return <Stack
|
return <Stack
|
||||||
spacing={0}
|
spacing={0}
|
||||||
@ -40,8 +70,28 @@ const MessageList = observer((props: any) => {
|
|||||||
whiteSpace: 'break-spaces'
|
whiteSpace: 'break-spaces'
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
|
{ messageType === 'bot' && confirm && <Card shadow="sm" padding="xs" radius="md" withBorder className={classes.card}>
|
||||||
|
<Card.Section withBorder inheritPadding py="xs">
|
||||||
|
<Group position="left">
|
||||||
|
<IconInfoSquareRounded size={20} />
|
||||||
|
<Text fw={500}>Explore with /ask-code!</Text>
|
||||||
|
</Group>
|
||||||
|
</Card.Section>
|
||||||
|
<Text className={classes.cardDescription}>
|
||||||
|
/ask-code, your AI agent, navigates through your codebase to answer questions using GPT-4, analyzing up to 10 source files for approximately $0.4 USD per question. We're evolving — soon, we'll implement the more affordable LLama2-70b model.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Would you like to proceed?
|
||||||
|
</Text>
|
||||||
|
<Group position="right" >
|
||||||
|
<Button size="compact-xs" className={classes.button} onClick={()=> chat.sendLastUserMessage() }>Yes</Button>
|
||||||
|
<Button size="compact-xs" className={classes.button} onClick={()=> chat.cancelDevchatAsk()}>No</Button>
|
||||||
|
</Group>
|
||||||
|
</Card>}
|
||||||
<MessageContext key={`message-context-${index}`} contexts={contexts} />
|
<MessageContext key={`message-context-${index}`} contexts={contexts} />
|
||||||
<MessageBody key={`message-codeblock-${index}`} messageType={messageType} messageText={messageText} />
|
<MessageBody key={`message-codeblock-${index}`} messageType={messageType} >
|
||||||
|
{messageText}
|
||||||
|
</MessageBody>
|
||||||
</Box >
|
</Box >
|
||||||
{index !== chat.messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
|
{index !== chat.messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
|
||||||
</Stack >;
|
</Stack >;
|
||||||
|
24
src/views/components/MessageMarkdown/LanguageCorner.tsx
Normal file
24
src/views/components/MessageMarkdown/LanguageCorner.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface LanguageCornerProps {
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
const LanguageCorner: React.FC<LanguageCornerProps> = ({ language }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||||
|
{language && (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#333',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '0.2rem 0.5rem',
|
||||||
|
borderRadius: '0.2rem',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
}}>
|
||||||
|
{language}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageCorner;
|
106
src/views/components/MessageMarkdown/Step.tsx
Normal file
106
src/views/components/MessageMarkdown/Step.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Accordion, Box, Button, Collapse, Group,Loader,Text } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import React from "react";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import LanguageCorner from "./LanguageCorner";
|
||||||
|
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
|
import { IconCheck, IconChevronDown, IconFileDiff, IconLoader } from "@tabler/icons-react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
|
import { keyframes,css } from "@emotion/react";
|
||||||
|
|
||||||
|
interface StepProps {
|
||||||
|
language: string;
|
||||||
|
children: string;
|
||||||
|
done:boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step = observer((props:StepProps) => {
|
||||||
|
const { chat } = useMst();
|
||||||
|
const {language,children,done} = props;
|
||||||
|
const [opened, { toggle }] = useDisclosure(false);
|
||||||
|
|
||||||
|
// extract first line with # as button label
|
||||||
|
const lines = children.split('\n');
|
||||||
|
const title = lines.length>0&&lines[0].indexOf('#')>=0?lines[0].split('#')[1]:'';
|
||||||
|
const contents = lines.slice(1,lines.length-1);
|
||||||
|
|
||||||
|
const spin = keyframes`
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
return <Accordion
|
||||||
|
variant="contained"
|
||||||
|
chevronPosition="right"
|
||||||
|
sx={{
|
||||||
|
marginTop: 5,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
item: {
|
||||||
|
borderColor: 'var(--vscode-menu-border)',
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
'&[data-active]': {
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
'&[aria-expanded="true"]': {
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
},
|
||||||
|
paddingLeft: '0.5rem',
|
||||||
|
paddingRight: '0.5rem',
|
||||||
|
fontFamily: 'var(--vscode-editor-font-familyy)',
|
||||||
|
fontSize: 'var(--vscode-editor-font-size)',
|
||||||
|
},
|
||||||
|
chevron: {
|
||||||
|
color: 'var(--vscode-menu-foreground)',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: 'var(--vscode-menu-foreground)',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'var(--vscode-menu-foreground)',
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
color: 'var(--vscode-menu-foreground)',
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: 'var(--vscode-menu-background)',
|
||||||
|
padding:'0.5rem'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accordion.Item value={title} mah='200'>
|
||||||
|
<Accordion.Control icon={
|
||||||
|
done
|
||||||
|
?<IconCheck size="1.125rem"/>
|
||||||
|
:<Loader size="xs" color="#ED6A45" speed={1} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text truncate='end' w={chat.chatPanelWidth-100}>{title}</Text>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<SyntaxHighlighter {...props}
|
||||||
|
language="markdown"
|
||||||
|
style={okaidia}
|
||||||
|
PreTag="div">
|
||||||
|
{children}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Step;
|
@ -1,42 +1,40 @@
|
|||||||
import { Button, Anchor } from "@mantine/core";
|
import { Button, Anchor, Stack, Group, Box } from "@mantine/core";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
|
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
import CodeButtons from "./CodeButtons";
|
import CodeButtons from "./CodeButtons";
|
||||||
|
import Step from "./Step";
|
||||||
|
import LanguageCorner from "./LanguageCorner";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMst } from "@/views/stores/RootStore";
|
import { useMst } from "@/views/stores/RootStore";
|
||||||
import { Message } from "@/views/stores/ChatStore";
|
import { Message } from "@/views/stores/ChatStore";
|
||||||
import messageUtil from '@/util/MessageUtil';
|
import messageUtil from '@/util/MessageUtil';
|
||||||
|
import {fromMarkdown} from 'mdast-util-from-markdown';
|
||||||
|
import {visit} from 'unist-util-visit';
|
||||||
|
|
||||||
interface MessageMarkdownProps extends React.ComponentProps<typeof ReactMarkdown> {
|
interface MessageMarkdownProps extends React.ComponentProps<typeof ReactMarkdown> {
|
||||||
children: string,
|
children: string,
|
||||||
className: string
|
className: string,
|
||||||
|
temp?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageMarkdown = observer((props: MessageMarkdownProps) => {
|
type Step = {
|
||||||
const { children } = props;
|
index: number,
|
||||||
const { chat } = useMst();
|
content: string;
|
||||||
|
endsWithTripleBacktick: boolean;
|
||||||
const LanguageCorner = (props: any) => {
|
|
||||||
const { language } = props;
|
|
||||||
|
|
||||||
return (<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
|
||||||
{language && (
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#333',
|
|
||||||
color: '#fff',
|
|
||||||
padding: '0.2rem 0.5rem',
|
|
||||||
borderRadius: '0.2rem',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
}}>
|
|
||||||
{language}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MessageMarkdown = observer((props: MessageMarkdownProps) => {
|
||||||
|
const { children,temp=false } = props;
|
||||||
|
const { chat } = useMst();
|
||||||
|
const [steps, setSteps] = useState<Step[]>([]);
|
||||||
|
const tree = fromMarkdown(children);
|
||||||
|
const codes = tree.children.filter(node => node.type === 'code');
|
||||||
|
const lastNode = tree.children[tree.children.length-1];
|
||||||
|
let index = 1;
|
||||||
|
|
||||||
const handleExplain = (value: string | undefined) => {
|
const handleExplain = (value: string | undefined) => {
|
||||||
console.log(value);
|
console.log(value);
|
||||||
switch (value) {
|
switch (value) {
|
||||||
@ -48,27 +46,9 @@ const MessageMarkdown = observer((props: MessageMarkdownProps) => {
|
|||||||
}),
|
}),
|
||||||
Message.create({
|
Message.create({
|
||||||
type: 'bot',
|
type: 'bot',
|
||||||
message: `***/ask_code***
|
message: `***/ask-code***
|
||||||
|
|
||||||
If you would like to ask questions related to your own codebase, you can enable and use the /ask_code feature of DevChat.
|
Your AI agent, navigates through your codebase to answer questions using GPT-4, analyzing up to 10 source files for approximately $0.4 USD per question. We're evolving — soon, we'll implement the more affordable LLama2-70b model.
|
||||||
|
|
||||||
While /ask_code is being enabled, DevChat will need to index your codebase before you can use this feature. Indexing usually takes a while, depending on the size of your codebase, your computing power and the network. Once it’s done, you can ask questions about your codebase by typing the “/ask_code” command, followed by your question.
|
|
||||||
|
|
||||||
Example questions:
|
|
||||||
(Here we only show example questions from a few popular open-source projects’ codebases.)
|
|
||||||
|
|
||||||
How do I access POST form fields in Express?
|
|
||||||
How do I pass command line arguments to a Node.js program?
|
|
||||||
How do I print the value of a tensor object in TensorFlow?
|
|
||||||
How do I force Kubernetes to re-pull an image in Kubernetes?
|
|
||||||
How do I set focus on an input field after rendering in React?
|
|
||||||
|
|
||||||
\`Please check DevChat.ask_code settings\` before enabling the feature, because once indexing has been started, changing the settings will not affect the process anymore, unless if you terminate it and re-index.
|
|
||||||
|
|
||||||
To enable, you can enter \`DevChat:Start AskCode Index\` in the Command Palette or click on the button to start indexing now.
|
|
||||||
|
|
||||||
<button value="settings">Settings</button>
|
|
||||||
<button value="start_indexing">Start Indexing</button>
|
|
||||||
`
|
`
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@ -135,9 +115,20 @@ Generate a professionally written and formatted release note in markdown with th
|
|||||||
|
|
||||||
return <ReactMarkdown
|
return <ReactMarkdown
|
||||||
{...props}
|
{...props}
|
||||||
|
remarkPlugins={[()=> (tree) =>{
|
||||||
|
visit(tree, function (node) {
|
||||||
|
if (node.type === 'code' && (node.lang ==='step' || node.lang ==='Step')) {
|
||||||
|
node.data = {
|
||||||
|
hProperties:{
|
||||||
|
index: index++
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]}
|
||||||
rehypePlugins={[rehypeRaw]}
|
rehypePlugins={[rehypeRaw]}
|
||||||
components={{
|
components={{
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, index, ...props }) {
|
||||||
|
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
const value = String(children).replace(/\n$/, '');
|
const value = String(children).replace(/\n$/, '');
|
||||||
@ -150,6 +141,12 @@ Generate a professionally written and formatted release note in markdown with th
|
|||||||
if (lanugage === 'markdown' || lanugage === 'text') {
|
if (lanugage === 'markdown' || lanugage === 'text') {
|
||||||
wrapLongLines = true;
|
wrapLongLines = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lanugage === 'step' || lanugage === 'Step') {
|
||||||
|
let done = Number(index) < codes.length? true : lastNode.type !== 'code';
|
||||||
|
return <Step language={lanugage} done={temp?done:true}>{value}</Step>;
|
||||||
|
}
|
||||||
|
|
||||||
return !inline && lanugage ? (
|
return !inline && lanugage ? (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<LanguageCorner language={lanugage} />
|
<LanguageCorner language={lanugage} />
|
||||||
|
@ -61,6 +61,7 @@ export const Message = types.model({
|
|||||||
type: types.enumeration(['user', 'bot', 'system']),
|
type: types.enumeration(['user', 'bot', 'system']),
|
||||||
message: types.string,
|
message: types.string,
|
||||||
contexts: types.maybe(types.array(ChatContext)),
|
contexts: types.maybe(types.array(ChatContext)),
|
||||||
|
confirm: types.maybe(types.boolean)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChatStore = types.model('Chat', {
|
export const ChatStore = types.model('Chat', {
|
||||||
@ -82,6 +83,34 @@ export const ChatStore = types.model('Chat', {
|
|||||||
})
|
})
|
||||||
.actions(self => {
|
.actions(self => {
|
||||||
|
|
||||||
|
const goScrollBottom = () => {
|
||||||
|
self.scrollBottom++;
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastNonEmptyHash = () => {
|
||||||
|
let lastNonEmptyHash;
|
||||||
|
for (let i = self.messages.length - 1; i >= 0; i--) {
|
||||||
|
if (self.messages[i].hash) {
|
||||||
|
lastNonEmptyHash = self.messages[i].hash;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastNonEmptyHash === 'message' ? null : lastNonEmptyHash;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process and send the message to the extension
|
||||||
|
const contextInfo = chatContexts => chatContexts.map((item, index: number) => {
|
||||||
|
const { file, path, content, command } = item;
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
context: {
|
||||||
|
path: path,
|
||||||
|
command: command,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const helpMessage = (originalMessage = false) => {
|
const helpMessage = (originalMessage = false) => {
|
||||||
|
|
||||||
let helps = `
|
let helps = `
|
||||||
@ -97,7 +126,7 @@ To get started, here are some of the things that I can do for you:
|
|||||||
|
|
||||||
[/release_note: draft a release note based on your latest commits](#release_note)
|
[/release_note: draft a release note based on your latest commits](#release_note)
|
||||||
|
|
||||||
${self.features['ask-code'] ? '[/ask-code: ask me questions about your codebase](#ask_code)' : ''}
|
${self.features['ask-code'] ? '[/ask-code: ask anything about your codebase and get answers from our AI agent](#ask_code)' : ''}
|
||||||
|
|
||||||
You can configure DevChat from [Settings](#settings).`;
|
You can configure DevChat from [Settings](#settings).`;
|
||||||
|
|
||||||
@ -111,10 +140,83 @@ You can configure DevChat from [Settings](#settings).`;
|
|||||||
type: 'bot',
|
type: 'bot',
|
||||||
message: helps
|
message: helps
|
||||||
}));
|
}));
|
||||||
|
// goto bottom
|
||||||
|
goScrollBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const devchatAsk = (userMessage, chatContexts) => {
|
||||||
|
self.messages.push(
|
||||||
|
Message.create({
|
||||||
|
type: 'user',
|
||||||
|
contexts: chatContexts,
|
||||||
|
message: userMessage
|
||||||
|
}));
|
||||||
|
self.messages.push(
|
||||||
|
Message.create({
|
||||||
|
type: 'bot',
|
||||||
|
message: '',
|
||||||
|
confirm: true
|
||||||
|
}));
|
||||||
|
// goto bottom
|
||||||
|
goScrollBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startGenerating = (text: string, chatContexts) => {
|
||||||
|
self.generating = true;
|
||||||
|
self.responsed = false;
|
||||||
|
self.hasDone = false;
|
||||||
|
self.errorMessage = '';
|
||||||
|
self.currentMessage = '';
|
||||||
|
messageUtil.sendMessage({
|
||||||
|
command: 'sendMessage',
|
||||||
|
text: text,
|
||||||
|
contextInfo: contextInfo(chatContexts),
|
||||||
|
parent_hash: lastNonEmptyHash()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendLastUserMessage = () => {
|
||||||
|
const lastUserMessage = self.messages[self.messages.length - 2];
|
||||||
|
const lastBotMessage = self.messages[self.messages.length - 1];
|
||||||
|
if (lastUserMessage && lastUserMessage.type === 'user') {
|
||||||
|
lastBotMessage.confirm = false;
|
||||||
|
startGenerating(lastUserMessage.message, lastUserMessage.contexts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDevchatAsk = () => {
|
||||||
|
const lastBotMessage = self.messages[self.messages.length - 1];
|
||||||
|
if (lastBotMessage && lastBotMessage.type === 'bot') {
|
||||||
|
lastBotMessage.confirm = false;
|
||||||
|
lastBotMessage.message = 'You\'ve cancelled the question. Please let me know if you have any other questions or if there\'s anything else I can assist with.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonMessage = (text: string, chatContexts) => {
|
||||||
|
self.messages.push({
|
||||||
|
type: 'user',
|
||||||
|
message: text,
|
||||||
|
contexts: chatContexts
|
||||||
|
});
|
||||||
|
self.messages.push({
|
||||||
|
type: 'bot',
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
// start generating
|
||||||
|
startGenerating(text, chatContexts);
|
||||||
|
// goto bottom
|
||||||
|
goScrollBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
helpMessage,
|
helpMessage,
|
||||||
|
devchatAsk,
|
||||||
|
sendLastUserMessage,
|
||||||
|
cancelDevchatAsk,
|
||||||
|
goScrollBottom,
|
||||||
|
startGenerating,
|
||||||
|
commonMessage,
|
||||||
updateChatPanelWidth: (width: number) => {
|
updateChatPanelWidth: (width: number) => {
|
||||||
self.chatPanelWidth = width;
|
self.chatPanelWidth = width;
|
||||||
},
|
},
|
||||||
@ -124,38 +226,6 @@ You can configure DevChat from [Settings](#settings).`;
|
|||||||
updateFeatures: (features: any) => {
|
updateFeatures: (features: any) => {
|
||||||
self.features = features;
|
self.features = features;
|
||||||
},
|
},
|
||||||
startGenerating: (text: string, chatContexts) => {
|
|
||||||
self.generating = true;
|
|
||||||
self.responsed = false;
|
|
||||||
self.hasDone = false;
|
|
||||||
self.errorMessage = '';
|
|
||||||
self.currentMessage = '';
|
|
||||||
let lastNonEmptyHash;
|
|
||||||
for (let i = self.messages.length - 1; i >= 0; i--) {
|
|
||||||
if (self.messages[i].hash) {
|
|
||||||
lastNonEmptyHash = self.messages[i].hash;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Process and send the message to the extension
|
|
||||||
const contextInfo = chatContexts.map((item, index: number) => {
|
|
||||||
const { file, path, content, command } = item;
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
context: {
|
|
||||||
path: path,
|
|
||||||
command: command,
|
|
||||||
content: content,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
messageUtil.sendMessage({
|
|
||||||
command: 'sendMessage',
|
|
||||||
text: text,
|
|
||||||
contextInfo: contextInfo,
|
|
||||||
parent_hash: lastNonEmptyHash === 'message' ? null : lastNonEmptyHash
|
|
||||||
});
|
|
||||||
},
|
|
||||||
startSystemMessage: () => {
|
startSystemMessage: () => {
|
||||||
self.generating = true;
|
self.generating = true;
|
||||||
self.responsed = false;
|
self.responsed = false;
|
||||||
@ -229,9 +299,6 @@ You can configure DevChat from [Settings](#settings).`;
|
|||||||
self.isTop = false;
|
self.isTop = false;
|
||||||
self.isBottom = false;
|
self.isBottom = false;
|
||||||
},
|
},
|
||||||
goScrollBottom: () => {
|
|
||||||
self.scrollBottom++;
|
|
||||||
},
|
|
||||||
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
|
fetchHistoryMessages: flow(function* (params: { pageIndex: number }) {
|
||||||
const { pageIndex, entries } = yield fetchHistoryMessages(params);
|
const { pageIndex, entries } = yield fetchHistoryMessages(params);
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
|
@ -1,210 +1,35 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
from chat.ask_codebase.chains.smart_qa import SmartQA
|
||||||
import tempfile
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
from chat.ask_codebase.store.qdrant import QdrantWrapper as Q, get_client
|
def query(question, lsp_brige_port):
|
||||||
from chat.ask_codebase.indexing.embedding import EmbeddingWrapper as E
|
root_path = os.getcwd()
|
||||||
|
|
||||||
from langchain.embeddings import HuggingFaceEmbeddings
|
# Create an instance of SmartQA
|
||||||
from chat.ask_codebase.indexing.loader.file import (
|
smart_qa = SmartQA(root_path)
|
||||||
FileLoader,
|
|
||||||
FileSource,
|
|
||||||
gen_local_reference_maker,
|
|
||||||
)
|
|
||||||
from chat.util.misc import is_source_code
|
|
||||||
from chat.ask_codebase.chains.simple_qa import SimpleQA
|
|
||||||
from chat.ask_codebase.chains.stuff_dc_qa import StuffDocumentCodeQa
|
|
||||||
|
|
||||||
|
# Use SmartQA to get the answer
|
||||||
|
answer = smart_qa.run(question=question, verbose=False, dfs_depth=3, dfs_max_visit=10, bridge_url=f'http://localhost:{lsp_brige_port}' )
|
||||||
|
|
||||||
def get_app_data_dir(app_name):
|
# Print the answer
|
||||||
home = os.path.expanduser("~")
|
print(answer[0])
|
||||||
if os.name == "nt": # For Windows
|
|
||||||
appPath = os.path.join(home, "AppData", "Roaming", app_name)
|
|
||||||
else: # For Unix and Linux
|
|
||||||
appPath = os.path.join(home, ".local", "share", app_name)
|
|
||||||
|
|
||||||
if not os.path.exists(appPath):
|
|
||||||
os.makedirs(appPath)
|
|
||||||
return appPath
|
|
||||||
|
|
||||||
supportedFileTypes = []
|
|
||||||
|
|
||||||
STORAGE_FILE = os.path.join(get_app_data_dir("devchat"), "qdrant_storage2")
|
|
||||||
SOURCE_NAME = ""
|
|
||||||
|
|
||||||
# 为已经分析的文件记录最后修改时间
|
|
||||||
g_file_last_modified_saved = {}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_file_last_modified(filePath: str):
|
|
||||||
# filePath表示存储了文件最后修改时间的文件名,内容实用JSON存储
|
|
||||||
# 如果文件不存在,表示尚未进行分析,结束函数执行
|
|
||||||
if not os.path.exists(filePath):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 如果文件存在,读取文件内容,解析文件中记录的每一个文件的最后修改时间
|
|
||||||
with open(filePath, 'r', encoding="utf-8") as f:
|
|
||||||
fileLastModified = json.load(f)
|
|
||||||
|
|
||||||
return fileLastModified
|
|
||||||
|
|
||||||
|
|
||||||
def save_file_last_modified(filePath: str, fileLastModified: dict):
|
|
||||||
# filePath表示存储了文件最后修改时间的文件名,内容实用JSON存储
|
|
||||||
with open(filePath, 'w+', encoding="utf-8") as f:
|
|
||||||
json.dump(fileLastModified, f)
|
|
||||||
|
|
||||||
return fileLastModified
|
|
||||||
|
|
||||||
def is_source_code_new(filePath: str):
|
|
||||||
# 使用正则表达式来判断一个文件是否是源码文件
|
|
||||||
for pattern in supportedFileTypes:
|
|
||||||
if re.match(pattern.strip(), filePath):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_file_modified(filePath: str) -> bool:
|
|
||||||
if not is_source_code_new(filePath):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 获取当前路径
|
|
||||||
currentPath = os.getcwd()
|
|
||||||
# 将filePath转换为相对路径
|
|
||||||
relativePath = os.path.relpath(filePath, currentPath)
|
|
||||||
|
|
||||||
# 检查文件路径中是否包含'.xxx'形式的目录
|
|
||||||
for part in relativePath.split(os.sep):
|
|
||||||
if part.startswith('.') or part in ["node_modules", "__pycache__"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 获取文件上次分析时记录的最后修改时间
|
|
||||||
fileLastModified = g_file_last_modified_saved.get(relativePath, 0)
|
|
||||||
# 获取文件当前的最后修改时间
|
|
||||||
fileCurrentModified = os.path.getmtime(filePath)
|
|
||||||
|
|
||||||
# 如果最后修改时间不同,那么更新记录的最后修改时间,并返回True
|
|
||||||
if fileLastModified != fileCurrentModified:
|
|
||||||
g_file_last_modified_saved[relativePath] = fileCurrentModified
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def index(repo_path: str):
|
|
||||||
try:
|
|
||||||
client = get_client(STORAGE_FILE)
|
|
||||||
source = FileSource(
|
|
||||||
path=repo_path,
|
|
||||||
rel_root=repo_path,
|
|
||||||
ref_maker=gen_local_reference_maker(repo_path),
|
|
||||||
file_filter=is_file_modified,
|
|
||||||
)
|
|
||||||
loader = FileLoader(sources=[source])
|
|
||||||
documents = loader.load()
|
|
||||||
|
|
||||||
e = E(embedding=HuggingFaceEmbeddings())
|
|
||||||
data = e.embed(documents)
|
|
||||||
q = Q.create(
|
|
||||||
source_name=SOURCE_NAME,
|
|
||||||
embedding_cls=HuggingFaceEmbeddings,
|
|
||||||
client=client,
|
|
||||||
)
|
|
||||||
|
|
||||||
q.insert(data)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
def query(question: str, doc_context: str):
|
|
||||||
try:
|
|
||||||
client = get_client(mode=STORAGE_FILE)
|
|
||||||
q = Q.reuse(
|
|
||||||
source_name=SOURCE_NAME,
|
|
||||||
embedding_cls=HuggingFaceEmbeddings,
|
|
||||||
client=client,
|
|
||||||
)
|
|
||||||
|
|
||||||
chain = StuffDocumentCodeQa(q)
|
|
||||||
|
|
||||||
ans, docs = chain.run(question)
|
|
||||||
|
|
||||||
print(f"\n# Question: \n{question}")
|
|
||||||
print(f"\n# Answer: \n{ans}")
|
|
||||||
print(f"\n# Relevant Documents: \n")
|
|
||||||
doc_dict = {"path": "AskCode Context","content": json.dumps([{"filepath": d.metadata.get('filepath'), "content": d.page_content} for d in docs])}
|
|
||||||
with open(doc_context, 'w') as f:
|
|
||||||
json.dump(doc_dict, f)
|
|
||||||
for d in docs:
|
|
||||||
print(f"- filepath: {d.metadata.get('filepath')}")
|
|
||||||
print(f" location: {d.metadata.get('reference')}\n")
|
|
||||||
|
|
||||||
print(f"Save doc context to {doc_context}")
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
global supportedFileTypes
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python index_and_query.py [command] [args]")
|
|
||||||
print("Available commands: index, query")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
command = sys.argv[1]
|
|
||||||
|
|
||||||
if command == "index":
|
|
||||||
if len(sys.argv) < 4:
|
if len(sys.argv) < 4:
|
||||||
print("Usage: python index_and_query.py index [repo_path] [supportedFileTypes]")
|
print("Usage: python index_and_query.py query [question] [port]")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
repo_path = sys.argv[2]
|
|
||||||
# 获取supportedFileTypes的值
|
|
||||||
supportedFileTypes = sys.argv[3].split(',')
|
|
||||||
index(repo_path)
|
|
||||||
|
|
||||||
elif command == "query":
|
|
||||||
if len(sys.argv) < 4:
|
|
||||||
print("Usage: python index_and_query.py query [question] [doc_context]")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
question = sys.argv[2]
|
question = sys.argv[2]
|
||||||
doc_context = sys.argv[3]
|
port = sys.argv[3]
|
||||||
query(question, doc_context)
|
query(question, port)
|
||||||
|
sys.exit(0)
|
||||||
else:
|
|
||||||
print("Invalid command. Available commands: index, query")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
|
||||||
currentPath = os.getcwd()
|
|
||||||
g_file_last_modified_saved = load_file_last_modified('./.chat/.index_modified.json')
|
|
||||||
if os.path.exists(".chat/askcode.json"):
|
|
||||||
with open(".chat/askcode.json", "r") as f:
|
|
||||||
askcode_data = json.load(f)
|
|
||||||
SOURCE_NAME = askcode_data.get("SOURCE_NAME", str(uuid.uuid4()))
|
|
||||||
else:
|
|
||||||
SOURCE_NAME = str(uuid.uuid4())
|
|
||||||
currentPath = os.getcwd()
|
|
||||||
with open(".chat/askcode.json", "w+") as f:
|
|
||||||
json.dump({"SOURCE_NAME": SOURCE_NAME}, f)
|
|
||||||
main()
|
main()
|
||||||
save_file_last_modified('./.chat/.index_modified.json', g_file_last_modified_saved)
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
sys.exit(1)
|
|
Loading…
x
Reference in New Issue
Block a user