replace GTP call with devchat

This commit is contained in:
bobo.yang 2023-04-26 06:48:39 +08:00
parent 3b619f06b2
commit ce4634767c
19 changed files with 5153 additions and 291 deletions

5
jest.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["<rootDir>/src/__test__/**/*.test.ts"],
};

4926
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/jest": "^29.5.1",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "16.x", "@types/node": "16.x",
"@types/vscode": "^1.77.0", "@types/vscode": "^1.77.0",
@ -48,7 +49,9 @@
"@vscode/test-electron": "^2.3.0", "@vscode/test-electron": "^2.3.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",
"glob": "^8.1.0", "glob": "^8.1.0",
"jest": "^29.5.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"webpack": "^5.76.3", "webpack": "^5.76.3",

View File

@ -0,0 +1,38 @@
import DevChat from "../devchat";
describe("DevChat", () => {
const devChat = new DevChat();
test("some test", () => {
const xx = 20;
expect(xx)
})
test("chat should return a valid response", async () => {
const chatResponse = await devChat.chat("Help me with TypeScript");
expect(chatResponse).toHaveProperty("prompt-hash");
expect(chatResponse).toHaveProperty("user");
expect(chatResponse).toHaveProperty("date");
expect(chatResponse).toHaveProperty("response");
expect(chatResponse).toHaveProperty("isError");
expect(chatResponse.isError).toBe(false);
expect(chatResponse.response.length).toBeGreaterThan(0);
});
test("log should return an array of log entries", async () => {
const logEntries = await devChat.log({ maxCount: 5 });
expect(Array.isArray(logEntries)).toBe(true);
expect(logEntries.length).toBeLessThanOrEqual(5);
logEntries.forEach((entry) => {
expect(entry).toHaveProperty("prompt-hash");
expect(entry).toHaveProperty("user");
expect(entry).toHaveProperty("date");
expect(entry).toHaveProperty("message");
expect(entry).toHaveProperty("response");
});
});
});

View File

@ -1,106 +1,93 @@
// chatPanel.ts
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { v4 as uuidv4 } from 'uuid'; import DevChat from './devchat';
import { chatWithGPT } from './openaiClient'; import handleMessage from './messageHandler';
export default class ChatPanel { export default class ChatPanel {
public static currentPanel: ChatPanel | undefined; private static _instance: ChatPanel | undefined;
private readonly _panel: vscode.WebviewPanel; private readonly _panel: vscode.WebviewPanel;
private _session_id: string;
private _sessionName: string;
private _messageHistory: Array<{ role: string; content: string }>; private _messageHistory: Array<{ role: string; content: string }>;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private static _sessions: { [sessionName: string]: ChatPanel } = {};
// Create or reveal the chat panel
public static createOrShow(extensionUri: vscode.Uri, sessionName: string) { public static createOrShow(extensionUri: vscode.Uri) {
const session = ChatPanel._sessions[sessionName]; if (ChatPanel._instance) {
ChatPanel._instance._panel.reveal();
if (session) { } else {
// If a session with the given name exists, reveal the existing panel const panel = ChatPanel.createWebviewPanel(extensionUri);
session._panel.reveal(); ChatPanel._instance = new ChatPanel(panel, extensionUri);
return
} }
}
// Create a new webview panel
private static createWebviewPanel(extensionUri: vscode.Uri): vscode.WebviewPanel {
const column = vscode.window.activeTextEditor const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn ? vscode.window.activeTextEditor.viewColumn
: undefined; : undefined;
// Create a new webview panel return vscode.window.createWebviewPanel(
const panel = vscode.window.createWebviewPanel(
'chatPanel', 'chatPanel',
sessionName, 'Chat',
column || vscode.ViewColumn.One, column || vscode.ViewColumn.One,
{ {
enableScripts: true, enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')] localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
} }
); );
// Set the webview's initial HTML content
const chatPanel = new ChatPanel(panel, extensionUri, uuidv4(), sessionName);
ChatPanel._sessions[sessionName] = chatPanel;
} }
public static sessions() { private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
return ChatPanel._sessions;
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, session_id: string, sessionName: string) {
// ... initialize the chat panel ...
this._panel = panel; this._panel = panel;
this._sessionName = sessionName;
this._session_id = session_id;
this._messageHistory = []; this._messageHistory = [];
// Set the webview options this.setWebviewOptions(extensionUri);
this.setWebviewContent(extensionUri);
this.registerEventListeners();
}
// Set webview options
private setWebviewOptions(extensionUri: vscode.Uri) {
this._panel.webview.options = { this._panel.webview.options = {
enableScripts: true, enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')] localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
}; };
}
// Set the webview content // Set webview content
private setWebviewContent(extensionUri: vscode.Uri) {
this._panel.webview.html = this._getHtmlContent(extensionUri); this._panel.webview.html = this._getHtmlContent(extensionUri);
}
// Handle webview events and dispose of the panel when closed // Register event listeners for the panel and webview
private registerEventListeners() {
this._panel.onDidDispose(() => this.dispose(), null, this._disposables); this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
this._panel.webview.onDidReceiveMessage( this._panel.webview.onDidReceiveMessage(
async (message) => { async (message) => {
switch (message.command) { handleMessage(message, this._panel);
case 'sendMessage':
const [status, response] = await chatWithGPT(message.text, this._session_id, this._messageHistory);
if (status == 0) {
this._messageHistory.push({ role: 'user', content: message.text });
this._messageHistory.push({ role: 'assistant', content: response });
}
this._panel.webview.postMessage({ command: 'receiveMessage', text: response });
return;
}
}, },
null, null,
this._disposables this._disposables
); );
} }
// Get the HTML content for the panel
private _getHtmlContent(extensionUri: vscode.Uri): string { private _getHtmlContent(extensionUri: vscode.Uri): string {
const htmlPath = vscode.Uri.joinPath(extensionUri, 'media', 'chatPanel.html'); const htmlPath = vscode.Uri.joinPath(extensionUri, 'media', 'chatPanel.html');
const htmlContent = fs.readFileSync(htmlPath.fsPath, 'utf8'); const htmlContent = fs.readFileSync(htmlPath.fsPath, 'utf8');
// Replace the resource placeholder with the correct resource URI
return htmlContent.replace(/<vscode-resource:(\/.+?)>/g, (_, resourcePath) => { return htmlContent.replace(/<vscode-resource:(\/.+?)>/g, (_, resourcePath) => {
const resourceUri = vscode.Uri.joinPath(extensionUri, 'media', resourcePath); const resourceUri = vscode.Uri.joinPath(extensionUri, 'media', resourcePath);
return this._panel.webview.asWebviewUri(resourceUri).toString(); return this._panel.webview.asWebviewUri(resourceUri).toString();
}); });
} }
// Dispose the panel and clean up resources
public dispose() { public dispose() {
// ... dispose the panel and clean up resources ... ChatPanel._instance = undefined;
// Remove the ChatPanel instance from the _sessions object
delete ChatPanel._sessions[this._sessionName];
// Dispose of the WebviewPanel and other resources
this._panel.dispose(); this._panel.dispose();
while (this._disposables.length) { while (this._disposables.length) {
const disposable = this._disposables.pop(); const disposable = this._disposables.pop();
@ -109,6 +96,4 @@ export default class ChatPanel {
} }
} }
} }
// ... other helper methods ...
} }

114
src/devchat.ts Normal file
View File

@ -0,0 +1,114 @@
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
interface ChatOptions {
parent?: string;
reference?: string[];
header?: string[];
context?: string[];
}
interface LogOptions {
skip?: number;
maxCount?: number;
}
interface LogEntry {
"prompt-hash": string;
user: string;
date: string;
message: string;
response: string;
}
interface ChatResponse {
"prompt-hash": string;
user: string;
date: string;
response: string;
isError: boolean;
}
class DevChat {
async chat(content: string, options: ChatOptions = {}): Promise<ChatResponse> {
let args = "";
if (options.parent) {
args += ` -p ${options.parent}`;
}
if (options.reference) {
args += ` -r ${options.reference.join(",")}`;
}
if (options.header) {
args += ` -h ${options.header.join(",")}`;
}
if (options.context) {
args += ` -c ${options.context.join(",")}`;
}
const { stdout } = await execAsync(`devchat ${args} "${content}"`, {
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
});
const responseLines = stdout.trim().split("\n");
console.log(responseLines)
if (responseLines.length === 0) {
return {
"prompt-hash": "",
user: "",
date: "",
response: "",
isError: true,
};
}
if (responseLines[0].startsWith("error")) {
return {
"prompt-hash": "",
user: "",
date: "",
response: responseLines.join("\n"),
isError: true,
};
}
const promptHashLine = responseLines.shift()!;
const promptHash = promptHashLine.split(" ")[1];
const userLine = responseLines.shift()!;
const user = (userLine.match(/User: (.+)/)?.[1]) ?? "";
const dateLine = responseLines.shift()!;
const date = (dateLine.match(/Date: (.+)/)?.[1]) ?? "";
const response = responseLines.join("\n");
return {
"prompt-hash": promptHash,
user,
date,
response,
isError: false,
};
}
async log(options: LogOptions = {}): Promise<LogEntry[]> {
let args = "log";
if (options.skip) {
args += ` --skip ${options.skip}`;
}
if (options.maxCount) {
args += ` --max-count ${options.maxCount}`;
}
const { stdout } = await execAsync(`devchat ${args}`, {
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
});
return JSON.parse(stdout.trim());
}
}
export default DevChat;

View File

@ -1,39 +1,15 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode'); const vscode = require('vscode');
const ChatPanel = require('./chatPanel').default; const ChatPanel = require('./chatPanel').default;
function activate(context: { extensionUri: any; subscriptions: any[]; }) { function activate(context: { extensionUri: any; subscriptions: any[]; }) {
let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => { let disposable = vscode.commands.registerCommand('devchat.openChatPanel', () => {
const sessionNames = Object.keys(ChatPanel.sessions()); if (vscode.workspace.workspaceFolders) {
ChatPanel.createOrShow(context.extensionUri);
} else {
vscode.window.showErrorMessage('Please open a directory before using the chat panel.');
}
});
const createNewSessionOption = 'Create new session'; context.subscriptions.push(disposable);
const options = [...sessionNames, createNewSessionOption];
const selectedOption = await vscode.window.showQuickPick(options, {
placeHolder: 'Select a session or create a new one',
});
if (!selectedOption) {
return;
}
let sessionName = selectedOption;
if (selectedOption === createNewSessionOption) {
sessionName = await vscode.window.showInputBox({
prompt: 'Enter a new session name',
placeHolder: 'Session Name',
});
if (!sessionName) {
return;
}
}
ChatPanel.createOrShow(context.extensionUri, sessionName);
});
context.subscriptions.push(disposable);
} }
exports.activate = activate; exports.activate = activate;

20
src/messageHandler.ts Normal file
View File

@ -0,0 +1,20 @@
// messageHandler.ts
import * as vscode from 'vscode';
import DevChat from './devchat';
async function handleMessage(
message: any,
panel: vscode.WebviewPanel
): Promise<void> {
switch (message.command) {
case 'sendMessage':
const devChat = new DevChat();
const chatResponse = await devChat.chat(message.text);
const response = chatResponse.response;
panel.webview.postMessage({ command: 'receiveMessage', text: response });
return;
}
}
export default handleMessage;

View File

@ -1,50 +0,0 @@
const { Configuration, OpenAIApi } = require("openai");
import * as dotenv from 'dotenv';
import * as path from 'path';
const extensionDir = path.resolve(__dirname, '..');
const envFilePath = path.join(extensionDir, '.env');
const dotenvOutput = dotenv.config({ path: envFilePath });
console.log('dotenv output:', dotenvOutput);
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
// Set up proxy settings
const openai = new OpenAIApi(configuration);
const HttpsProxyAgent = require('https-proxy-agent')
const HttpProxyAgent = require('http-proxy-agent');
export async function chatWithGPT(prompt: string, session_id: string, messageList: Array<{ role: string; content: string }>): Promise<any[]> {
const fullConversation = [
...messageList,
{
role: 'user',
content: prompt,
},
];
try {
let completion = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: fullConversation,
temperature: 0.9,
max_tokens: 2048,
top_p: 1,
frequency_penalty: 0.0,
presence_penalty: 0.6,
stop: null,
},
{
}
);
return [0, completion.data.choices[0].message.content]
} catch (e) {
console.log("Error:", e.message)
return [-1, "Error while connecting to the GPT model."];
}
}

View File

@ -1,22 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const test_electron_1 = require("@vscode/test-electron");
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await (0, test_electron_1.runTests)({ extensionDevelopmentPath, extensionTestsPath });
}
catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();
//# sourceMappingURL=runTest.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"runTest.js","sourceRoot":"","sources":["runTest.ts"],"names":[],"mappings":";;AAAA,6BAA6B;AAE7B,yDAAiD;AAEjD,KAAK,UAAU,IAAI;IAClB,IAAI;QACH,4DAA4D;QAC5D,yCAAyC;QACzC,MAAM,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEnE,0BAA0B;QAC1B,iCAAiC;QACjC,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAEpE,0DAA0D;QAC1D,MAAM,IAAA,wBAAQ,EAAC,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,CAAC,CAAC;KACjE;IAAC,OAAO,GAAG,EAAE;QACb,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KAChB;AACF,CAAC;AAED,IAAI,EAAE,CAAC"}

View File

@ -1,23 +0,0 @@
import * as path from 'path';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();

View File

@ -1,15 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
const vscode = require("vscode");
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});
//# sourceMappingURL=extension.test.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"extension.test.js","sourceRoot":"","sources":["extension.test.ts"],"names":[],"mappings":";;AAAA,iCAAiC;AAEjC,0DAA0D;AAC1D,8CAA8C;AAC9C,iCAAiC;AACjC,kDAAkD;AAElD,KAAK,CAAC,sBAAsB,EAAE,GAAG,EAAE;IAClC,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAEzD,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE;QACxB,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}

View File

@ -1,15 +0,0 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

View File

@ -1,40 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const path = require("path");
const Mocha = require("mocha");
const glob = require("glob");
function run() {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
}
else {
c();
}
});
}
catch (err) {
console.error(err);
e(err);
}
});
});
}
exports.run = run;
//# sourceMappingURL=index.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,6BAA6B;AAC7B,+BAA+B;AAC/B,6BAA6B;AAE7B,SAAgB,GAAG;IAClB,wBAAwB;IACxB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACvB,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,IAAI;KACX,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAEhD,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3B,IAAI,CAAC,eAAe,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YACxD,IAAI,GAAG,EAAE;gBACR,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;aACd;YAED,8BAA8B;YAC9B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAE9D,IAAI;gBACH,qBAAqB;gBACrB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;oBACpB,IAAI,QAAQ,GAAG,CAAC,EAAE;wBACjB,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,QAAQ,gBAAgB,CAAC,CAAC,CAAC;qBAC1C;yBAAM;wBACN,CAAC,EAAE,CAAC;qBACJ;gBACF,CAAC,CAAC,CAAC;aACH;YAAC,OAAO,GAAG,EAAE;gBACb,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACnB,CAAC,CAAC,GAAG,CAAC,CAAC;aACP;QACF,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAjCD,kBAiCC"}

View File

@ -1,38 +0,0 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -15,3 +15,4 @@
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
} }
} }