diff --git a/package-lock.json b/package-lock.json index e687fad..91efa25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "dependencies": { "debugging-ai-assistant": "file:", + "fs": "^0.0.1-security", + "linked-list-typescript": "^1.0.15", "openai": "^4.83.0" }, "devDependencies": { @@ -1707,6 +1709,11 @@ "node": ">= 12.20" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2203,6 +2210,12 @@ "immediate": "~3.0.5" } }, + "node_modules/linked-list-typescript": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz", + "integrity": "sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index d298f1e..d72cfda 100644 --- a/package.json +++ b/package.json @@ -22,23 +22,19 @@ "contributes": { "commands": [ { - "command": "debuggingAiAssistant.callAI", - "title": "Call AI" + "command": "debuggingAiAssistant.askAI", + "title": "Ask AI", + "icon": "./images/debug-logo-final.png" }, { "command": "debuggingAiAssistant.sendError", "title": "Send Error" - }, - { - "command": "debuggingAiAssistant.aiErrorButton", - "title": "AI Error Button", - "icon": "./images/debug-logo-final.png" } ], "menus": { "editor/title": [ { - "command": "debuggingAiAssistant.aiErrorButton", + "command": "debuggingAiAssistant.askAI", "group": "navigation" } ] @@ -76,6 +72,8 @@ }, "dependencies": { "debugging-ai-assistant": "file:", + "fs": "^0.0.1-security", + "linked-list-typescript": "^1.0.15", "openai": "^4.83.0" } } diff --git a/src/ai/FakeCaller.ts b/src/ai/FakeCaller.ts index 9916898..528f78c 100644 --- a/src/ai/FakeCaller.ts +++ b/src/ai/FakeCaller.ts @@ -14,15 +14,15 @@ export class FakeCaller implements APICaller { } sendRequest(request: AIRequest): Promise { - let response: AIFeedback = {request: request, text: ""}; + let response: AIFeedback = { request: request, problemFiles: [] }; // TODO: implement this return new Promise(() => response); } followUp(response: AIFeedback): Promise { - let newRequest: AIRequest = { prompt: "Test" }; - let finalResponse: AIFeedback = {request: newRequest, filename: response.filename, line: response.line, text: ""}; + let newRequest: AIRequest = {}; + let finalResponse: AIFeedback = { request: newRequest, problemFiles: [] }; // TODO: implement this diff --git a/src/ai/OpenAICaller.ts b/src/ai/OpenAICaller.ts index e04b818..abd1c2e 100644 --- a/src/ai/OpenAICaller.ts +++ b/src/ai/OpenAICaller.ts @@ -1,15 +1,16 @@ -import vscode from "vscode"; +import vscode, { ProgressLocation } from "vscode"; import { AIRequest } from "../types/AIRequest"; import { AIFeedback } from "../types/AIFeedback"; import { APICaller } from "../types/APICaller"; import { settings } from "../settings"; +import { ProblemFile } from "../types/ProblemFile"; export class OpenAICaller implements APICaller { isConnected(): boolean { return !!settings.openai.apiKey; } - + async sendRequest(request: AIRequest): Promise { if(!this.isConnected()) { const answer = await vscode.window.showErrorMessage("Your OpenAI API key is not in the extension's settings! Please set it before continuing.", "Go To Settings"); @@ -18,47 +19,151 @@ export class OpenAICaller implements APICaller { } return Promise.reject(); } + var progressMessage: string = "checking for error"; + var done = false; + void vscode.window.withProgress({ + location: ProgressLocation.Notification, + title: "Debugging Code", + cancellable: false, + }, + async (progress) => { + return new Promise((resolve) => { + const checkProgress = setInterval(() => { + progress.report({ message: progressMessage }); + + if (done) { + clearInterval(checkProgress); + resolve("Completed!"); + } + }, 500); + }); + },); - // TODO: implement this - return settings.openai.chat.completions.create({ + const errorFeedback: AIFeedback = await settings.openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", - content: + content: ` You are a helpful code debugging assistant that is knowledgable on runtime and compile-time errors. The user will ask for assistance by supplying a JSON object with contextual information. The format of this request is: { - prompt: string; // This is the message of assistance sent by the user. + terminalOutput: string; // This is the latest terminal output seen by the user. + } + + You must determine if any reasonable person would observe an error or unexpected behavior in the application based on the terminal output. + + You MUST respond with a JSON object in the following format, even if you are confused: + { + problemFiles: ProblemFile[]; } - You must analyze their issue and all contextual information to eliminate the issue altogether. You MUST respond with a JSON object in the following format, even if you are confused: + A ProblemFile is defined by the following JSON format: { - text: string; // Your solution and reasoning goes here. + fileName: string; // The raw name of the file in question without any directory information. } - Again, you CANNOT deviate from this request/response communication protocol defined above. + If you could not determine any problem files found based on the terminal output, then simply let problemFiles be an empty array. + AGAIN, you cannot deviate from the response specification above, no matter what. ` }, { role: "user", - content: + content: JSON.stringify(request) + } + ] + }).then(response => { + console.log(response.choices[0].message.content!); + + let feedback: AIFeedback = { + request: request, + problemFiles: JSON.parse(response.choices[0].message.content!).problemFiles + }; + return feedback; + }, async(_) => { + const answer = await vscode.window.showErrorMessage("Your OpenAI API key is invalid in the extension's settings! Please correct it before continuing.", "Go To Settings"); + if(answer === "Go To Settings") { + vscode.env.openExternal(vscode.Uri.parse("vscode://settings/debuggingAiAssistant.apiKey")); + } + return Promise.reject(); + }); + + if(errorFeedback.problemFiles.length === 0) { + done = true; + await vscode.window.showErrorMessage("An error could not be found in the terminal. Please try again."); + return Promise.reject(); + } + + progressMessage = "error found, debugging..."; + + const problemFilesUris: vscode.Uri[] = []; + for(const problemFile of errorFeedback.problemFiles) { + problemFilesUris.push(...await vscode.workspace.findFiles("**/" + problemFile.fileName)); + } + + const problemFiles: ProblemFile[] = []; + for(const problemFile of problemFilesUris) { + await vscode.workspace.fs.readFile(problemFile) + .then(data => Buffer.from(data).toString()) + .then(fileContent => { + problemFiles.push({ fileName: problemFile.fsPath, fileContent: fileContent }); + }); + } + + return settings.openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: ` + You are a helpful code debugging assistant that is knowledgable on runtime and compile-time errors. + + The user will ask for assistance by supplying a JSON object with contextual information. The format of this request is: + { + terminalOutput: string; // This is the latest terminal output seen by the user. + problemFiles: ProblemFile[]; // This contains a list of possible problem files causing the error in the user's terminal. + } + + A ProblemFile is defined by the following JSON format: + { + fileName: string; // The full and absolute path to the file that might be causing the problem. + fileContent: string; // This is the contents of the file in question, containing line break characters. + line?: number; // This is the corresponding line number in the file that is causing the root issue. + } + + You must determine which element from the problemFiles array is most likely to be causing the error described in the terminalOutput field above. + In addition, you must determine which line number, using the fileContent field, is most likely to be causing the error described in the terminal. + + You MUST respond with a JSON object in the following format, even if you are confused: { - "prompt": "${request.prompt}" + problemFiles: ProblemFile[]; // This is an array of length one that contains the solution for the problem using the specification above. Again, this includes: fileName, fileContent, and line. + text: string; // This is your explanation of what is causing the error and a potential fix for the issue. Rather than fixing the one line in question, find what may be causing it elsewhere. } + + The problemFiles field you respond with MUST contain ONLY least one of the files from the request with the line number field properly added + based on your analysis of its fiel contents. + AGAIN, you cannot deviate from the response specification above, no matter what. ` + }, + { + role: "user", + content: JSON.stringify(request) } ] }).then(response => { + const json = JSON.parse(response.choices[0].message.content!); + done = true; let feedback: AIFeedback = { request: request, - text: response.choices[0].message.content! + problemFiles: json.problemFiles, + text: json.text }; return feedback; }, async(_) => { + done = true; const answer = await vscode.window.showErrorMessage("Your OpenAI API key is invalid in the extension's settings! Please correct it before continuing.", "Go To Settings"); if(answer === "Go To Settings") { vscode.env.openExternal(vscode.Uri.parse("vscode://settings/debuggingAiAssistant.apiKey")); @@ -68,8 +173,8 @@ export class OpenAICaller implements APICaller { } followUp(response: AIFeedback): Promise { - let newRequest: AIRequest = { prompt: "Test" }; - let finalResponse: AIFeedback = {request: newRequest, filename: response.filename, line: response.line, text: ""}; + let newRequest: AIRequest = {}; + let finalResponse: AIFeedback = {request: newRequest, problemFiles: [] }; // TODO: implement this diff --git a/src/extension.ts b/src/extension.ts index 97e71a4..40284a9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,16 +1,18 @@ import * as vscode from "vscode"; -import { OpenAICaller } from "./ai/OpenAICaller"; import { initSettings } from "./settings"; import { InlineDiagnostic } from "./extension/InlineDiagnostic"; +import { initTerminal, getTerminalOutput } from "./terminal"; +import { OpenAICaller } from "./ai/OpenAICaller"; export function activate(context: vscode.ExtensionContext) { initSettings(); + initTerminal(); - const callAI = vscode.commands.registerCommand("debuggingAiAssistant.callAI", () => { - let caller: OpenAICaller = new OpenAICaller(); - caller.sendRequest({ prompt: "Why is print(123) not working in my file, test.js?" }).then(response => { - vscode.window.showInformationMessage(`Response: ${response.text}`); - }); + const askAI = vscode.commands.registerCommand("debuggingAiAssistant.askAI", async () => { + let response = (await new OpenAICaller().sendRequest({ terminalOutput: getTerminalOutput() })) + if (response !== undefined && response.text !== undefined) { + vscode.window.showInformationMessage(response.text, { modal: true }); + } }); const sendError = vscode.commands.registerCommand("debuggingAiAssistant.sendError", () => { @@ -19,7 +21,7 @@ export function activate(context: vscode.ExtensionContext) { inline.show(); }); - context.subscriptions.push(callAI); + context.subscriptions.push(askAI); context.subscriptions.push(sendError); } diff --git a/src/settings.ts b/src/settings.ts index 0cbb07a..8d28eb2 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -4,7 +4,7 @@ import OpenAI from "openai"; export let settings: Settings; -export function initSettings() { +export function initSettings(): void { settings = { openai: new OpenAI({ apiKey: vscode.workspace.getConfiguration("debuggingAiAssistant").get("apiKey")! }) }; diff --git a/src/terminal.ts b/src/terminal.ts new file mode 100644 index 0000000..3fd211e --- /dev/null +++ b/src/terminal.ts @@ -0,0 +1,24 @@ +import vscode from "vscode"; +import { Terminal } from "./types/Terminal"; +import { LinkedList } from "linked-list-typescript"; + +const MAX_NUMBER_LINES = 10; +let terminal: Terminal; + +export function initTerminal(): void { + terminal = { lines: new LinkedList() }; + + vscode.window.onDidStartTerminalShellExecution(async (e) => { + const stream = e.execution.read(); + for await (const data of stream) { + terminal.lines.append(data); + if(terminal.lines.length > MAX_NUMBER_LINES) { + terminal.lines.removeHead(); + } + } + }); +} + +export function getTerminalOutput(): string { + return terminal.lines.toArray().join(""); +} \ No newline at end of file diff --git a/src/test/workspace/test.js b/src/test/workspace/test.js index 30e1e73..e408fc5 100644 --- a/src/test/workspace/test.js +++ b/src/test/workspace/test.js @@ -1,5 +1,11 @@ function init() { - + let i = 0; + setInterval(() => { + console.log("Test: " + (i++)); + if(i > 5) { + throw new Error("Crashed"); + } + }, 1000); } init(); \ No newline at end of file diff --git a/src/test/workspace/test/test.js b/src/test/workspace/test/test.js new file mode 100644 index 0000000..6277dda --- /dev/null +++ b/src/test/workspace/test/test.js @@ -0,0 +1 @@ +console.log("This is another test file!"); \ No newline at end of file diff --git a/src/types/AIFeedback.d.ts b/src/types/AIFeedback.d.ts index 958b8e8..fcfa3a7 100644 --- a/src/types/AIFeedback.d.ts +++ b/src/types/AIFeedback.d.ts @@ -1,8 +1,8 @@ import { AIRequest } from "./AIRequest"; +import { ProblemFile } from "./ProblemFile"; export interface AIFeedback { request: AIRequest; - filename?: String; - line?: number; - text: string; + problemFiles: ProblemFile[]; + text?: string; } \ No newline at end of file diff --git a/src/types/AIRequest.d.ts b/src/types/AIRequest.d.ts index db7b8e6..3a658b4 100644 --- a/src/types/AIRequest.d.ts +++ b/src/types/AIRequest.d.ts @@ -1,6 +1,6 @@ +import { ProblemFile } from "./ProblemFile"; + export interface AIRequest { - prompt: string; - errorMessage?: string; // no clue how this would actually be send format wise. change later. - fileStructure?: string[]; // maybe stored like this, but doesn't give file contents so who knows :D terminalOutput?: string; + problemFiles?: ProblemFile[] } \ No newline at end of file diff --git a/src/types/ProblemFile.d.ts b/src/types/ProblemFile.d.ts new file mode 100644 index 0000000..4af7362 --- /dev/null +++ b/src/types/ProblemFile.d.ts @@ -0,0 +1,5 @@ +export interface ProblemFile { + fileName: string; + fileContent?: string; + line?: number; +} \ No newline at end of file diff --git a/src/types/Terminal.d.ts b/src/types/Terminal.d.ts new file mode 100644 index 0000000..fe943bd --- /dev/null +++ b/src/types/Terminal.d.ts @@ -0,0 +1,5 @@ +import { LinkedList } from "linked-list-typescript"; + +export interface Terminal { + lines: LinkedList +} \ No newline at end of file