Skip to content

Commit a88e683

Browse files
committed
Add initial implementation of auto-modeling
1 parent b8557d3 commit a88e683

9 files changed

Lines changed: 535 additions & 1 deletion

File tree

extensions/ql-vscode/src/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,10 @@ const QUERIES_PANEL = new Setting("queriesPanel", ROOT_SETTING);
711711
export function showQueriesPanel(): boolean {
712712
return !!QUERIES_PANEL.getValue<boolean>();
713713
}
714+
715+
const DATA_EXTENSIONS = new Setting("dataExtensions", ROOT_SETTING);
716+
const LLM_GENERATION = new Setting("llmGeneration", DATA_EXTENSIONS);
717+
718+
export function showLlmGeneration(): boolean {
719+
return !!LLM_GENERATION.getValue<boolean>();
720+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Credentials } from "../common/authentication";
2+
import { OctokitResponse } from "@octokit/types";
3+
4+
export enum ClassificationType {
5+
Unknown = "CLASSIFICATION_TYPE_UNKNOWN",
6+
Neutral = "CLASSIFICATION_TYPE_NEUTRAL",
7+
Source = "CLASSIFICATION_TYPE_SOURCE",
8+
Sink = "CLASSIFICATION_TYPE_SINK",
9+
Summary = "CLASSIFICATION_TYPE_SUMMARY",
10+
}
11+
12+
export interface Classification {
13+
type: ClassificationType;
14+
kind: string;
15+
explanation: string;
16+
}
17+
18+
export interface Method {
19+
package: string;
20+
type: string;
21+
name: string;
22+
signature: string;
23+
usages: string[];
24+
classification?: Classification;
25+
input?: string;
26+
output?: string;
27+
}
28+
29+
export interface ModelRequest {
30+
language: string;
31+
candidates: Method[];
32+
samples: Method[];
33+
}
34+
35+
export interface ModelResponse {
36+
language: string;
37+
predicted: Method[];
38+
}
39+
40+
export async function autoModel(
41+
credentials: Credentials,
42+
request: ModelRequest,
43+
): Promise<ModelResponse> {
44+
const octokit = await credentials.getOctokit();
45+
46+
const response: OctokitResponse<ModelResponse> = await octokit.request(
47+
"POST /repos/github/codeql/code-scanning/codeql/auto-model",
48+
{
49+
data: request,
50+
},
51+
);
52+
53+
return response.data;
54+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { ExternalApiUsage } from "./external-api-usage";
2+
import { ModeledMethod, ModeledMethodType } from "./modeled-method";
3+
import {
4+
Classification,
5+
ClassificationType,
6+
Method,
7+
ModelRequest,
8+
} from "./auto-model-api";
9+
10+
export function createAutoModelRequest(
11+
language: string,
12+
externalApiUsages: ExternalApiUsage[],
13+
modeledMethods: Record<string, ModeledMethod>,
14+
): ModelRequest {
15+
const request: ModelRequest = {
16+
language,
17+
samples: [],
18+
candidates: [],
19+
};
20+
21+
// Sort by number of usages so we always send the most used methods first
22+
externalApiUsages = [...externalApiUsages];
23+
externalApiUsages.sort((a, b) => b.usages.length - a.usages.length);
24+
25+
for (const externalApiUsage of externalApiUsages) {
26+
const modeledMethod: ModeledMethod = modeledMethods[
27+
externalApiUsage.signature
28+
] ?? {
29+
type: "none",
30+
};
31+
32+
const numberOfArguments =
33+
externalApiUsage.methodParameters === "()"
34+
? 0
35+
: externalApiUsage.methodParameters.split(",").length;
36+
37+
for (
38+
let argumentIndex = 0;
39+
argumentIndex < numberOfArguments;
40+
argumentIndex++
41+
) {
42+
const method: Method = {
43+
package: externalApiUsage.packageName,
44+
type: externalApiUsage.typeName,
45+
name: externalApiUsage.methodName,
46+
signature: externalApiUsage.methodParameters,
47+
classification:
48+
modeledMethod.type === "none"
49+
? undefined
50+
: toMethodClassification(modeledMethod),
51+
usages: externalApiUsage.usages.map((usage) => usage.label),
52+
input: `Argument[${argumentIndex}]`,
53+
};
54+
55+
if (method.usages.length > 10) {
56+
method.usages = method.usages.slice(0, 10);
57+
}
58+
59+
if (modeledMethod.type === "none") {
60+
request.candidates.push(method);
61+
} else {
62+
request.samples.push(method);
63+
}
64+
}
65+
}
66+
67+
if (request.candidates.length > 100) {
68+
request.candidates = request.candidates.slice(0, 100);
69+
}
70+
if (request.samples.length > 20) {
71+
request.samples = request.samples.slice(0, 20);
72+
}
73+
74+
return request;
75+
}
76+
77+
function toMethodClassificationType(
78+
type: ModeledMethodType,
79+
): ClassificationType {
80+
switch (type) {
81+
case "source":
82+
return ClassificationType.Source;
83+
case "sink":
84+
return ClassificationType.Sink;
85+
case "summary":
86+
return ClassificationType.Summary;
87+
case "neutral":
88+
return ClassificationType.Neutral;
89+
default:
90+
return ClassificationType.Unknown;
91+
}
92+
}
93+
94+
function toMethodClassification(modeledMethod: ModeledMethod): Classification {
95+
return {
96+
type: toMethodClassificationType(modeledMethod.type),
97+
kind: modeledMethod.kind,
98+
explanation: "",
99+
};
100+
}
101+
102+
export function classificationTypeToModeledMethodType(
103+
type: ClassificationType,
104+
): ModeledMethodType {
105+
switch (type) {
106+
case ClassificationType.Source:
107+
return "source";
108+
case ClassificationType.Sink:
109+
return "sink";
110+
case ClassificationType.Summary:
111+
return "summary";
112+
case ClassificationType.Neutral:
113+
return "neutral";
114+
default:
115+
return "none";
116+
}
117+
}

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ import { createDataExtensionYaml, loadDataExtensionYaml } from "./yaml";
3939
import { ExternalApiUsage } from "./external-api-usage";
4040
import { ModeledMethod } from "./modeled-method";
4141
import { ExtensionPackModelFile } from "./shared/extension-pack";
42+
import { autoModel } from "./auto-model-api";
43+
import {
44+
classificationTypeToModeledMethodType,
45+
createAutoModelRequest,
46+
} from "./auto-model";
47+
import { showLlmGeneration } from "../config";
4248

4349
function getQlSubmoduleFolder(): WorkspaceFolder | undefined {
4450
const workspaceFolder = workspace.workspaceFolders?.find(
@@ -127,6 +133,13 @@ export class DataExtensionsEditorView extends AbstractWebview<
127133
case "generateExternalApi":
128134
await this.generateModeledMethods();
129135

136+
break;
137+
case "generateExternalApiFromLlm":
138+
await this.generateModeledMethodsFromLlm(
139+
msg.externalApiUsages,
140+
msg.modeledMethods,
141+
);
142+
130143
break;
131144
default:
132145
assertNever(msg);
@@ -149,6 +162,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
149162
viewState: {
150163
extensionPackModelFile: this.modelFile,
151164
modelFileExists: await pathExists(this.modelFile.filename),
165+
showLlmButton: showLlmGeneration(),
152166
},
153167
});
154168
}
@@ -367,6 +381,40 @@ export class DataExtensionsEditorView extends AbstractWebview<
367381
await this.clearProgress();
368382
}
369383

384+
private async generateModeledMethodsFromLlm(
385+
externalApiUsages: ExternalApiUsage[],
386+
modeledMethods: Record<string, ModeledMethod>,
387+
): Promise<void> {
388+
const request = createAutoModelRequest(
389+
this.databaseItem.language,
390+
externalApiUsages,
391+
modeledMethods,
392+
);
393+
394+
const response = await autoModel(this.app.credentials, request);
395+
396+
const modeledMethodsByName: Record<string, ModeledMethod> = {};
397+
398+
for (const method of response.predicted) {
399+
if (method.classification === undefined) {
400+
continue;
401+
}
402+
403+
modeledMethodsByName[method.signature] = {
404+
type: classificationTypeToModeledMethodType(method.classification.type),
405+
kind: method.classification.kind,
406+
input: method.input ?? "",
407+
output: method.output ?? "",
408+
};
409+
}
410+
411+
await this.postMessage({
412+
t: "addModeledMethods",
413+
modeledMethods: modeledMethodsByName,
414+
overrideNone: true,
415+
});
416+
}
417+
370418
/*
371419
* Progress in this class is a bit weird. Most of the progress is based on running the query.
372420
* Query progress is always between 0 and 1000. However, we still have some steps that need

extensions/ql-vscode/src/data-extensions-editor/shared/view-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import { ExtensionPackModelFile } from "./extension-pack";
33
export interface DataExtensionEditorViewState {
44
extensionPackModelFile: ExtensionPackModelFile;
55
modelFileExists: boolean;
6+
showLlmButton: boolean;
67
}

extensions/ql-vscode/src/pure/interface-types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,12 @@ export interface GenerateExternalApiMessage {
544544
t: "generateExternalApi";
545545
}
546546

547+
export interface GenerateExternalApiFromLlmMessage {
548+
t: "generateExternalApiFromLlm";
549+
externalApiUsages: ExternalApiUsage[];
550+
modeledMethods: Record<string, ModeledMethod>;
551+
}
552+
547553
export type ToDataExtensionsEditorMessage =
548554
| SetExtensionPackStateMessage
549555
| SetExternalApiUsagesMessage
@@ -556,4 +562,5 @@ export type FromDataExtensionsEditorMessage =
556562
| OpenExtensionPackMessage
557563
| JumpToUsageMessage
558564
| SaveModeledMethods
559-
| GenerateExternalApiMessage;
565+
| GenerateExternalApiMessage
566+
| GenerateExternalApiFromLlmMessage;

extensions/ql-vscode/src/stories/data-extensions-editor/DataExtensionsEditor.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ DataExtensionsEditor.args = {
3030
"/home/user/vscode-codeql-starter/codeql-custom-queries-java/sql2o/models/sql2o.yml",
3131
},
3232
modelFileExists: true,
33+
showLlmButton: true,
3334
},
3435
initialExternalApiUsages: [
3536
{

extensions/ql-vscode/src/view/data-extensions-editor/DataExtensionsEditor.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ export function DataExtensionsEditor({
157157
});
158158
}, []);
159159

160+
const onGenerateFromLlmClick = useCallback(() => {
161+
vscode.postMessage({
162+
t: "generateExternalApiFromLlm",
163+
externalApiUsages,
164+
modeledMethods,
165+
});
166+
}, [externalApiUsages, modeledMethods]);
167+
160168
const onOpenExtensionPackClick = useCallback(() => {
161169
vscode.postMessage({
162170
t: "openExtensionPack",
@@ -214,6 +222,14 @@ export function DataExtensionsEditor({
214222
<VSCodeButton onClick={onGenerateClick}>
215223
Download and generate
216224
</VSCodeButton>
225+
{viewState?.showLlmButton && (
226+
<>
227+
&nbsp;
228+
<VSCodeButton onClick={onGenerateFromLlmClick}>
229+
Generate using LLM
230+
</VSCodeButton>
231+
</>
232+
)}
217233
<br />
218234
<br />
219235
<VSCodeDataGrid>

0 commit comments

Comments
 (0)