Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Fix findings and tests using GPT #731

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# [0.103.0](https://github.com/getappmap/vscode-appland/compare/v0.102.0...v0.103.0) (2023-10-31)


### Features

* Update @appland/components to 3.9.0 ([fa2cc6e](https://github.com/getappmap/vscode-appland/commit/fa2cc6e56f96b13225d1a865a3972a4495eecf28))

# [0.102.0](https://github.com/getappmap/vscode-appland/compare/v0.101.2...v0.102.0) (2023-10-11)


### Bug Fixes

* AppMaps displayed chronologically for Java requests ([#813](https://github.com/getappmap/vscode-appland/issues/813)) ([4f8e730](https://github.com/getappmap/vscode-appland/commit/4f8e730be089000d8c2eb3e28b1226f39efaf0e4))
* Instructions page should be reactive to the Java agent file ([3bfbcb6](https://github.com/getappmap/vscode-appland/commit/3bfbcb602e53e19285e902a56e427c7f07fe2459))


### Features

* Accurately determine if Runtime Analysis step is complete ([#829](https://github.com/getappmap/vscode-appland/issues/829)) ([0dd1fa2](https://github.com/getappmap/vscode-appland/commit/0dd1fa2d61e5b7923c67bf0d1e051b2074df3120))
* The Code Objects view also lists External Service Calls ([d9b9c32](https://github.com/getappmap/vscode-appland/commit/d9b9c325edb40791d94353ccd3541baab1d241fa))
* Update [@appland](https://github.com/appland) dependencies ([d696a53](https://github.com/getappmap/vscode-appland/commit/d696a5388d158f827c97caf5db4242340d8b44eb))
* update @appland/components to 3.8.0 ([8b66898](https://github.com/getappmap/vscode-appland/commit/8b668983fe30602f5960836fe1d1120d6b28b31f))
* **deleteAppMaps.ts:** add closeEditorByUri function after deleting appmaps ([2a54b8c](https://github.com/getappmap/vscode-appland/commit/2a54b8cde5c5b0b5894185cfe63f78c0cd539e4e))

## [0.101.2](https://github.com/getappmap/vscode-appland/compare/v0.101.1...v0.101.2) (2023-09-21)


Expand Down
1 change: 1 addition & 0 deletions extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"identifier":{"id":"undefined_publisher.undefined"},"location":{"$mid":1,"fsPath":"/Users/kgilpin/source/appland/vscode-appland/extern","path":"/Users/kgilpin/source/appland/vscode-appland/extern","scheme":"file"},"relativeLocation":"extern"},{"identifier":{"id":"appland.appmap","uuid":"41d86b02-68d3-4049-9422-95da6d11cc2e"},"version":"0.90.1","location":{"$mid":1,"fsPath":"/Users/kgilpin/source/appland/vscode-appland/out","path":"/Users/kgilpin/source/appland/vscode-appland/out","scheme":"file"},"relativeLocation":"out","metadata":{"id":"41d86b02-68d3-4049-9422-95da6d11cc2e","publisherDisplayName":"AppLand","publisherId":"f7f1004e-6038-49cd-a096-4e618fe53f77","isPreReleaseVersion":false}}]
43 changes: 40 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "appmap",
"displayName": "AppMap",
"description": "Interactive maps of runtime code behavior",
"version": "0.101.2",
"version": "0.103.0",
"repository": {
"type": "git",
"url": "https://github.com/getappmap/vscode-appland"
Expand Down Expand Up @@ -131,6 +131,22 @@
"command": "appmap.openCodeObjectInAppMap",
"title": "AppMap: Open Code Object in AppMap"
},
{
"command": "appmap.fixFinding",
"title": "AppMap: Suggest a Fix for an Analysis Finding"
},
{
"command": "appmap.fixTest",
"title": "AppMap: Suggest a Fix for a Failing Test"
},
{
"command": "appmap.ask",
"title": "AppMap: Ask a Question About the Current Code Selection"
},
{
"command": "appmap.generate",
"title": "AppMap: Generate Code Based on the Current Code Selection"
},
{
"command": "appmap.touchOutOfDateTestFiles",
"title": "AppMap: Touch Out-of-Date Test Files"
Expand All @@ -145,14 +161,24 @@
},
{
"command": "appmap.context.openAsJson",
"title": "AppMap View: Open as JSON",
"title": "Open as JSON",
"icon": "$(bracket)"
},
{
"command": "appmap.context.deleteAppMap",
"title": "AppMap View: Delete AppMap",
"title": "Delete",
"icon": "$(trash)"
},
{
"command": "appmap.context.fixTest",
"title": "Fix",
"icon": "$(wrench)"
},
{
"command": "appmap.context.fixFinding",
"title": "Fix",
"icon": "$(wrench)"
},
{
"command": "appmap.context.rename",
"title": "AppMap View: Rename AppMap"
Expand Down Expand Up @@ -376,6 +402,16 @@
"when": "view == appmap.views.appmaps && viewItem == appmap.views.appmaps.appMap",
"group": "inline"
},
{
"command": "appmap.context.fixTest",
"when": "view == appmap.views.findings && viewItem == appmap.views.analysis.failedTest",
"group": "inline"
},
{
"command": "appmap.context.fixFinding",
"when": "view == appmap.views.findings && viewItem == appmap.views.analysis.finding",
"group": "inline"
},
{
"command": "appmap.context.openInFileExplorer",
"when": "view == appmap.views.appmaps && viewItem == appmap.views.appmaps.appMap"
Expand Down Expand Up @@ -498,6 +534,7 @@
"diff": "^5.1.0",
"jquery": "^3.5.1",
"js-yaml": "^4.1.0",
"openai": "^3.3.0",
"popper.js": "^1.16.1",
"proper-lockfile": "^4.1.2",
"semver": "^7.3.5",
Expand Down
220 changes: 220 additions & 0 deletions src/commands/ask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import * as vscode from 'vscode';
import LineInfoIndex, { LineInfo } from '../services/lineInfoIndex';
import assert from 'assert';
import AppMapCollection from '../services/appmapCollection';
import { promptForAppMap } from '../lib/promptForAppMap';
import { debug } from 'console';
import { readFile, writeFile } from 'fs/promises';
import { AppMap, AppMapFilter, CodeObject, buildAppMap } from '@appland/models';
import buildOpenAIApi from '../lib/buildOpenAIApi';
import { join } from 'path';
import { FormatType, Specification, buildDiagram, format } from '@appland/sequence-diagram';
import { randomUUID } from 'crypto';
import { Completion, Question } from '../lib/ask';
import selectedCode from '../lib/ask/selectedCode';
import contextAppMap from '../lib/ask/contextAppMap';

class LineInfoQuickPickItem implements vscode.QuickPickItem {
constructor(public lineInfo: LineInfo) {}

get label(): string {
return [
`line ${this.lineInfo.line}`,
this.lineInfo.codeObjects?.map((co) => co.fqid).join(', '),
].join(': ');
}
}

export default function register(
context: vscode.ExtensionContext,
lineInfoIndex: LineInfoIndex,
appmapCollection: AppMapCollection
): void {
context.subscriptions.push(
vscode.commands.registerCommand('appmap.ask', async () => {
const { activeTextEditor } = vscode.window;
if (!activeTextEditor) return;

const selection = selectedCode(activeTextEditor);
if (!selection) return;

const documentUri = activeTextEditor.document.uri;
const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri);
if (!workspaceFolder) return;

const lineInfo = (await lineInfoIndex.lineInfo(documentUri)).filter(
(line) => line.codeObjects
);
if (lineInfo.length === 0) {
vscode.window.showErrorMessage(
`I couldn't find any AppMaps related to ${documentUri.path}`
);
return;
}

const lineCodePrompt =
selection.text.split('\n').length === 0
? selection.text
: [selection.text.split('\n')[0], '...'].join('');
const question = await vscode.window.showInputBox({
placeHolder: `Ask a question about: ${lineCodePrompt}`,
value: 'How does this code work?',
});
if (!question) return;

const distanceFromSelection = (item: LineInfoQuickPickItem): number =>
Math.min(
Math.abs(item.lineInfo.line - selection.startLine),
Math.abs(item.lineInfo.line - selection.endLine)
);

const lineInfoItems = lineInfo.map((info) => new LineInfoQuickPickItem(info));
const lineDistance = lineInfoItems.map(distanceFromSelection);
const preferredDistance = lineDistance.sort()[0];

const choiceItems: vscode.QuickPickItem[] = [
...lineInfoItems.filter(
(lineInfo) => distanceFromSelection(lineInfo) === preferredDistance
),
{ label: '---' },
...lineInfoItems.filter(
(lineInfo) => distanceFromSelection(lineInfo) !== preferredDistance
),
];

const lineAbout = await vscode.window.showQuickPick(choiceItems, {
placeHolder: `Verify the function that you want to ask about`,
});
if (!lineAbout) return;
if (!(lineAbout instanceof LineInfoQuickPickItem)) return;

const codeObjectEntry = ((lineAbout as LineInfoQuickPickItem).lineInfo.codeObjects || [])[0];
assert(codeObjectEntry);

if (codeObjectEntry.appMapFiles.length === 0) {
console.warn(`No AppMaps for ${codeObjectEntry.fqid}`);
return;
}

let appMapFileName: string;
if (codeObjectEntry.appMapFiles.length === 1) {
appMapFileName = codeObjectEntry.appMapFiles[0];
} else {
const appmapFiles = new Set(Object.keys(await codeObjectEntry.appMapMetadata()));
const appmaps = appmapCollection
.allAppMaps()
.filter((appmap) => appmapFiles.has(appmap.descriptor.resourceUri.fsPath));

const selectedAppMap = await promptForAppMap(appmaps);
if (!selectedAppMap) return;

appMapFileName = selectedAppMap.fsPath;
}

const q = new Question(selection.text, question);

const appmapUri = vscode.Uri.file(appMapFileName);
{
const data = await readFile(appmapUri.fsPath, 'utf-8');
const appmap = buildAppMap().source(data).build();

let codeObject: CodeObject | undefined;
appmap.classMap.visit((co) => {
if (co.fqid === codeObjectEntry.fqid) codeObject = co;
});
if (!codeObject) {
vscode.window.showInformationMessage(
`Could not find code object ${codeObjectEntry.fqid} in the AppMap. Maybe it's out of date?`
);
return;
}
q.codeObject = codeObject;

const { events } = appmap;
let filterAppMap: AppMap;
{
const codeObjectEvents = events.filter(
(event) => event.codeObject.fqid === codeObjectEntry.fqid
);
q.returnValues = [
...new Set(
codeObjectEvents
.map((e) => {
return e.returnValue
? [e.returnValue.class, e.returnValue.value].join(': ')
: undefined;
})
.filter(Boolean)
),
] as string[];
filterAppMap = contextAppMap(appmap, codeObjectEvents);

q.scopeCodeObjects = [...new Set(appmap.rootEvents().map((e) => e.codeObject))];
}

{
const filterAppMapData = JSON.stringify(filterAppMap, null, 2);
let appmapName = ['ask_appmap', randomUUID()].join('-'); // question.replace(/[^a-zA-Z0-9]/g, '-'); KEG: Can result in a too-long file name.
if (appmapName.endsWith('-')) appmapName = appmapName.slice(0, -1);
const filePath = join(workspaceFolder?.uri.fsPath, `${appmapName}.appmap.json`);
await writeFile(filePath, filterAppMapData);
const uri = vscode.Uri.file(filePath);
await vscode.commands.executeCommand('vscode.openWith', uri, 'appmap.views.appMapFile');
}

const specification = Specification.build(filterAppMap, { loops: true });
assert(appmapUri);
let sequenceDiagramAppMap: AppMap;
{
const sequenceDiagramFilter = new AppMapFilter();
if (appmap.metadata.language?.name !== 'java')
sequenceDiagramFilter.declutter.hideExternalPaths.on = true;
sequenceDiagramAppMap = sequenceDiagramFilter.filter(filterAppMap, []);
}
const diagram = buildDiagram(appmapUri.fsPath, sequenceDiagramAppMap, specification);
q.sequenceDiagram = format(FormatType.PlantUML, diagram, appmapUri.fsPath).diagram;
}

const openAI = await buildOpenAIApi(context);
if (!openAI) return;

let completion: Completion | undefined;
await vscode.window.withProgress(
{
title: `Thinking about your question...`,
location: vscode.ProgressLocation.Notification,
},
async () => {
try {
completion = await q.complete(openAI);
} catch (e) {
debug(e);
vscode.window.showErrorMessage(`Unable to process your question: ${e}`);
}
}
);
if (!completion) return;

const fence = '```';
const responseText = [
`## ${lineCodePrompt}`,
`### You asked`,
`${fence}${question}${fence}`,
`### AI response:`,
completion.response,
`## Prompt
<details>
<summary>Click to expand</summary>

${fence}
${completion.prompt}
${fence}
</details>`,
].join('\n\n');
const newDocument = await vscode.workspace.openTextDocument({
content: responseText,
});
await vscode.window.showTextDocument(newDocument);
})
);
}
Loading