241 lines
7.6 KiB
TypeScript
241 lines
7.6 KiB
TypeScript
import type { Method, MethodSignature } from "./method";
|
|
import type { ModeledMethod } from "./modeled-method";
|
|
import { load as loadYaml } from "js-yaml";
|
|
import type { ProgressCallback } from "../common/vscode/progress";
|
|
import { withProgress } from "../common/vscode/progress";
|
|
import { createAutoModelRequest, getCandidates } from "./auto-model";
|
|
import { runAutoModelQueries } from "./auto-model-codeml-queries";
|
|
import { loadDataExtensionYaml } from "./yaml";
|
|
import type { ModelRequest, ModelResponse } from "./auto-model-api";
|
|
import { autoModel } from "./auto-model-api";
|
|
import { RequestError } from "@octokit/request-error";
|
|
import { showAndLogExceptionWithTelemetry } from "../common/logging";
|
|
import { redactableError } from "../common/errors";
|
|
import type { App } from "../common/app";
|
|
import type { CodeQLCliServer } from "../codeql-cli/cli";
|
|
import type { QueryRunner } from "../query-server";
|
|
import type { DatabaseItem } from "../databases/local-databases";
|
|
import type { Mode } from "./shared/mode";
|
|
import { CancellationTokenSource } from "vscode";
|
|
import type { ModelingStore } from "./modeling-store";
|
|
import type { ModelConfigListener } from "../config";
|
|
import type { QueryLanguage } from "../common/query-language";
|
|
|
|
/**
|
|
* The auto-modeler holds state around auto-modeling jobs and allows
|
|
* starting and stopping them.
|
|
*/
|
|
export class AutoModeler {
|
|
// Keep track of auto-modeling jobs that are in progress
|
|
// so that we can stop them.
|
|
private readonly jobs: Map<string, CancellationTokenSource>;
|
|
|
|
constructor(
|
|
private readonly app: App,
|
|
private readonly cliServer: CodeQLCliServer,
|
|
private readonly queryRunner: QueryRunner,
|
|
private readonly modelConfig: ModelConfigListener,
|
|
private readonly modelingStore: ModelingStore,
|
|
private readonly queryStorageDir: string,
|
|
private readonly databaseItem: DatabaseItem,
|
|
private readonly language: QueryLanguage,
|
|
private readonly addModeledMethods: (
|
|
modeledMethods: Record<string, ModeledMethod[]>,
|
|
) => Promise<void>,
|
|
) {
|
|
this.jobs = new Map<string, CancellationTokenSource>();
|
|
}
|
|
|
|
/**
|
|
* Models the given package's external API usages, except
|
|
* the ones that are already modeled.
|
|
* @param packageName The name of the package to model.
|
|
* @param methods The methods.
|
|
* @param modeledMethods The currently modeled methods.
|
|
* @param mode The mode we are modeling in.
|
|
*/
|
|
public async startModeling(
|
|
packageName: string,
|
|
methods: readonly Method[],
|
|
modeledMethods: Record<string, readonly ModeledMethod[]>,
|
|
mode: Mode,
|
|
): Promise<void> {
|
|
if (this.jobs.has(packageName)) {
|
|
return;
|
|
}
|
|
|
|
const cancellationTokenSource = new CancellationTokenSource();
|
|
this.jobs.set(packageName, cancellationTokenSource);
|
|
|
|
try {
|
|
await this.modelPackage(
|
|
packageName,
|
|
methods,
|
|
modeledMethods,
|
|
mode,
|
|
cancellationTokenSource,
|
|
);
|
|
} finally {
|
|
this.jobs.delete(packageName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops modeling the given package.
|
|
* @param packageName The name of the package to stop modeling.
|
|
*/
|
|
public async stopModeling(packageName: string): Promise<void> {
|
|
void this.app.logger.log(`Stopping modeling for package ${packageName}`);
|
|
const cancellationTokenSource = this.jobs.get(packageName);
|
|
if (cancellationTokenSource) {
|
|
cancellationTokenSource.cancel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops all in-progress modeling jobs.
|
|
*/
|
|
public async stopAllModeling(): Promise<void> {
|
|
for (const cancellationTokenSource of this.jobs.values()) {
|
|
cancellationTokenSource.cancel();
|
|
}
|
|
}
|
|
|
|
private async modelPackage(
|
|
packageName: string,
|
|
methods: readonly Method[],
|
|
modeledMethods: Record<string, readonly ModeledMethod[]>,
|
|
mode: Mode,
|
|
cancellationTokenSource: CancellationTokenSource,
|
|
): Promise<void> {
|
|
void this.app.logger.log(`Modeling package ${packageName}`);
|
|
|
|
const candidateBatchSize = this.modelConfig.llmGenerationBatchSize;
|
|
|
|
await withProgress(async (progress) => {
|
|
// Fetch the candidates to send to the model
|
|
const allCandidateMethods = getCandidates(mode, methods, modeledMethods);
|
|
|
|
// If there are no candidates, there is nothing to model and we just return
|
|
if (allCandidateMethods.length === 0) {
|
|
void this.app.logger.log("No candidates to model. Stopping.");
|
|
return;
|
|
}
|
|
|
|
// Find number of slices to make
|
|
const batchNumber = Math.ceil(
|
|
allCandidateMethods.length / candidateBatchSize,
|
|
);
|
|
try {
|
|
for (let i = 0; i < batchNumber; i++) {
|
|
// Check if we should stop
|
|
if (cancellationTokenSource.token.isCancellationRequested) {
|
|
break;
|
|
}
|
|
|
|
const start = i * candidateBatchSize;
|
|
const end = start + candidateBatchSize;
|
|
const candidatesToProcess = allCandidateMethods.slice(start, end);
|
|
const candidateSignatures = candidatesToProcess.map(
|
|
(c) => c.signature,
|
|
);
|
|
|
|
// Let the UI know which candidates we are modeling
|
|
this.modelingStore.addInProgressMethods(
|
|
this.databaseItem,
|
|
candidateSignatures,
|
|
);
|
|
|
|
// Kick off the process to model the slice of candidates
|
|
await this.modelCandidates(
|
|
candidatesToProcess,
|
|
mode,
|
|
progress,
|
|
cancellationTokenSource,
|
|
);
|
|
|
|
// Let the UI know which candidates we are done modeling
|
|
this.modelingStore.removeInProgressMethods(
|
|
this.databaseItem,
|
|
candidateSignatures,
|
|
);
|
|
|
|
// Let the UI know which methods have been sent to the LLM
|
|
this.modelingStore.addProcessedByAutoModelMethods(
|
|
this.databaseItem,
|
|
candidateSignatures,
|
|
);
|
|
}
|
|
} finally {
|
|
// Clear out in progress methods in case anything went wrong
|
|
this.modelingStore.removeInProgressMethods(
|
|
this.databaseItem,
|
|
allCandidateMethods.map((c) => c.signature),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async modelCandidates(
|
|
candidateMethods: MethodSignature[],
|
|
mode: Mode,
|
|
progress: ProgressCallback,
|
|
cancellationTokenSource: CancellationTokenSource,
|
|
): Promise<void> {
|
|
void this.app.logger.log("Executing auto-model queries");
|
|
|
|
const usages = await runAutoModelQueries({
|
|
mode,
|
|
candidateMethods,
|
|
cliServer: this.cliServer,
|
|
queryRunner: this.queryRunner,
|
|
queryStorageDir: this.queryStorageDir,
|
|
databaseItem: this.databaseItem,
|
|
progress: (update) => progress({ ...update }),
|
|
cancellationTokenSource,
|
|
});
|
|
if (!usages) {
|
|
return;
|
|
}
|
|
|
|
const request = await createAutoModelRequest(mode, usages);
|
|
|
|
void this.app.logger.log("Calling auto-model API");
|
|
|
|
const response = await this.callAutoModelApi(request);
|
|
if (!response) {
|
|
return;
|
|
}
|
|
|
|
const models = loadYaml(response.models, {
|
|
filename: "auto-model.yml",
|
|
});
|
|
|
|
const loadedMethods = loadDataExtensionYaml(models, this.language);
|
|
if (!loadedMethods) {
|
|
return;
|
|
}
|
|
|
|
await this.addModeledMethods(loadedMethods);
|
|
}
|
|
|
|
private async callAutoModelApi(
|
|
request: ModelRequest,
|
|
): Promise<ModelResponse | null> {
|
|
try {
|
|
return await autoModel(this.app.credentials, request, this.modelConfig);
|
|
} catch (e) {
|
|
if (e instanceof RequestError && e.status === 429) {
|
|
void showAndLogExceptionWithTelemetry(
|
|
this.app.logger,
|
|
this.app.telemetry,
|
|
redactableError`Rate limit hit, please try again soon.`,
|
|
);
|
|
return null;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|