copilot spring boot agent implementation

This commit is contained in:
vudayani
2024-09-25 09:25:02 +05:30
committed by Martin Lippert
parent a63fc2a541
commit 3d18619e98
23 changed files with 693 additions and 16 deletions

View File

@@ -126,6 +126,11 @@ public class GradleProjectClasspath implements IClasspath {
public String getName() {
return project == null ? null : project.getName();
}
@Override
public String getJavaVersion() {
return JavaUtils.getJavaRuntimeMinorVersion(getJavaRuntimeVersion());
}
public String getGradleVersion() throws GradleException {
if (buildEnvironment == null) {

View File

@@ -30,19 +30,22 @@ public class ClasspathData implements IClasspath {
final public static ClasspathData EMPTY_CLASSPATH_DATA = new ClasspathData(
null,
Collections.emptySet()
Collections.emptySet(),
null
);
private String name;
private Set<CPE> classpathEntries;
private String javaVersion;
private Cache<String, Optional<CPE>> binaryLibLookupCache;
public ClasspathData() {}
public ClasspathData(String name, Collection<CPE> classpathEntries) {
public ClasspathData(String name, Collection<CPE> classpathEntries, String javaVersion) {
this.name = name;
this.classpathEntries = ImmutableSet.copyOf(classpathEntries);
this.javaVersion = javaVersion;
this.binaryLibLookupCache = CacheBuilder.newBuilder().build();
}
@@ -56,7 +59,8 @@ public class ClasspathData implements IClasspath {
}
return new ClasspathData(
d.getName(),
entries==null ? ImmutableSet.of() : entries
entries==null ? ImmutableSet.of() : entries,
d.getJavaVersion()
);
}
@@ -68,6 +72,15 @@ public class ClasspathData implements IClasspath {
public void setName(String name) {
this.name = name;
}
@Override
public String getJavaVersion() {
return javaVersion;
}
public void setJavaVersion(String javaVersion) {
this.javaVersion = javaVersion;
}
@Override
public Set<CPE> getClasspathEntries() {

View File

@@ -79,6 +79,11 @@ public class DelegatingCachedClasspath implements IClasspath {
public ImmutableList<CPE> getClasspathEntries() throws Exception {
return ImmutableList.copyOf(cachedData.get().getClasspathEntries());
}
@Override
public String getJavaVersion() {
return cachedData.get().getJavaVersion();
}
public boolean isCached() {
return fileBasedCache.isCached();

View File

@@ -31,7 +31,7 @@ public interface IClasspath {
public static final Logger log = LoggerFactory.getLogger(IClasspath.class);
String getName();
/**
* Classpath entries paths
*
@@ -58,4 +58,10 @@ public interface IClasspath {
return Optional.empty();
}
/**
* Finds Java Version by parsing the classpath entries
* @return returns java version
*/
String getJavaVersion();
}

View File

@@ -63,7 +63,7 @@ public class JandexClasspathTest {
ClasspathData getClasspath() {
return new ClasspathData(name, ImmutableList.of(
CPE.source(new File(root, "src"), outputFolder)
));
), "");
}
JandexClasspath getJandexClasspath() {

View File

@@ -28,13 +28,15 @@ public class Classpath {
public static final String ENTRY_KIND_SOURCE = "source";
public static final String ENTRY_KIND_BINARY = "binary";
public static final Classpath EMPTY = new Classpath(Collections.<CPE>emptyList());
public static final Classpath EMPTY = new Classpath(Collections.<CPE>emptyList(), "");
private List<CPE> entries;
private String javaVersion;
public Classpath(List<CPE> entries) {
public Classpath(List<CPE> entries, String javaVersion) {
super();
this.entries = entries;
this.javaVersion = javaVersion;
}
public List<CPE> getEntries() {
@@ -44,10 +46,18 @@ public class Classpath {
public void setEntries(List<CPE> entries) {
this.entries = entries;
}
public String getJavaVersion() {
return javaVersion;
}
public void setJavaVersion(String javaVersion) {
this.javaVersion = javaVersion;
}
@Override
public String toString() {
return "Classpath [entries=" + entries + "]";
return "Classpath [entries=" + entries + ", javaVersion=" + javaVersion + "]";
}
public static class CPE {

View File

@@ -226,6 +226,11 @@ public class MavenProjectClasspath implements IClasspath {
public ImmutableList<CPE> getClasspathEntries() throws Exception {
return cachedData != null ? ImmutableList.copyOf(cachedData.getClasspathEntries()) : ImmutableList.of();
}
@Override
public String getJavaVersion() {
return cachedData.getJavaVersion() != null ? cachedData.getJavaVersion() : null;
}
private Set<Artifact> projectDependencies(MavenProject project) {
return project == null ? Collections.emptySet() : project.getArtifacts();
@@ -252,8 +257,9 @@ public class MavenProjectClasspath implements IClasspath {
ImmutableList<CPE> entries = resolveClasspathEntries(project);
String name = project.getArtifact().getArtifactId();
return new ClasspathData(name, new LinkedHashSet<>(entries));
String javaVersion = maven.getJavaRuntimeVersion();
return new ClasspathData(name, new LinkedHashSet<>(entries), javaVersion);
}
@Override

View File

@@ -95,11 +95,31 @@ public class ClasspathUtil {
}
}
}
Classpath classpath = new Classpath(cpEntries);
String javaVersion = extractJavaVersion(getJreContainer(javaProject.getRawClasspath()).getPath().lastSegment());
Classpath classpath = new Classpath(cpEntries, javaVersion);
logger.debug("classpath=" + classpath.getEntries().size() + " entries");
return classpath;
}
private static String extractJavaVersion(String versionString) {
String[] parts = versionString.split("-");
if (parts.length > 1) {
return parts[1]; // for "JavaSE-17" style
} else if (versionString.contains(".")) {
parts = versionString.split("\\.");
if (parts[0] == "1") {
if (parts.length > 1) {
return parts[1];
}
} else {
String version = parts[0];
int idx = version.indexOf('+');
return idx >= 0 ? version.substring(0, idx) : version;
}
}
return versionString;
}
private static AtomicBoolean enabledDownloadSources = new AtomicBoolean(false);
public static List<CPE> createCpes(IJavaProject javaProject, IClasspathEntry entry) throws MalformedURLException, JavaModelException {

View File

@@ -137,7 +137,7 @@ public class SendClasspathNotificationsJob extends Job {
}
if (filteredCPEs.size() != classpath.getEntries().size()) {
// Only send effective classpath that has all entries physically present.
classpath = new Classpath(filteredCPEs);
classpath = new Classpath(filteredCPEs, "");
}
} catch (Exception e) {
logger.log(e);

View File

@@ -376,4 +376,4 @@
</plugins>
</build>
</project>
</project>

View File

@@ -18,6 +18,8 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
@@ -33,12 +35,18 @@ import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.BeansParams;
import com.google.gson.JsonElement;
public class WorkspaceBootExecutableProjects {
public record ExecutableProject(String name, String uri, String gav, String mainClass, Collection<String> classpath) {}
public record BootProjectInfo(String name, String uri, String mainClass, String buildTool, String springBootVersion, String javaVersion) {}
final static String CMD = "sts/spring-boot/executableBootProjects";
final static String BOOT_PROJECT_INFO_CMD = "sts/spring-boot/bootProjectInfo";
private final static Logger log = LoggerFactory.getLogger(WorkspaceBootExecutableProjects.class);
final private JavaProjectFinder projectFinder;
@@ -50,6 +58,10 @@ public class WorkspaceBootExecutableProjects {
this.projectFinder = projectFinder;
this.symbolIndex = symbolIndex;
server.onCommand(CMD, params -> findExecutableProjects());
server.onCommand(BOOT_PROJECT_INFO_CMD, (params) -> {
return getBootProjectInfo(params);
});
}
private CompletableFuture<Optional<ExecutableProject>> mapToExecProject(IJavaProject project) {
@@ -113,4 +125,36 @@ public class WorkspaceBootExecutableProjects {
});
}
}
private CompletableFuture<Optional<BootProjectInfo>> mapToBootProjectInfo(IJavaProject project) {
BeansParams params = new BeansParams();
params.setProjectName(project.getElementName());
return symbolIndex.beans(params).thenApply(beans -> {
List<Bean> bootAppBeans = beans.stream()
.filter(b -> hasAnnotation(b, Annotations.BOOT_APP))
.limit(2)
.collect(Collectors.toList());
if (bootAppBeans.size() > 0) {
try {
String appBean = bootAppBeans.get(0) != null ? bootAppBeans.get(0).getType() : null;
String springBootVersion = SpringProjectUtil.getSpringBootVersion(project).toString();
String buildTool = project.getProjectBuild().getType();
String javaVersion = project.getClasspath().getJavaVersion();
return Optional
.of(new BootProjectInfo(project.getElementName(), project.getLocationUri().toASCIIString(),
appBean, buildTool, springBootVersion, javaVersion));
} catch (Exception e) {
log.error("", e);
}
}
return Optional.empty();
});
}
private CompletableFuture<BootProjectInfo> getBootProjectInfo(ExecuteCommandParams params) {
String projectUri = ((JsonElement) params.getArguments().get(0)).getAsString();
IJavaProject project = projectFinder.find(new TextDocumentIdentifier(projectUri)).orElse(null);
return mapToBootProjectInfo(project).thenApply(opt -> opt.orElse(null));
}
}

View File

@@ -358,7 +358,7 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService, Serv
} else {
log.debug("deleted = false");
URI projectUri = new URI(uri);
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries());
ClasspathData classpath = new ClasspathData(event.name, event.classpath.getEntries(), event.classpath.getJavaVersion());
IJavaProject oldProject = table.get(uri);
if (oldProject != null && classpath.equals(oldProject.getClasspath())) {
// nothing has changed

View File

@@ -80,6 +80,7 @@ public class MockProjects {
final private File root;
final private String name;
// final private String javaVersion;
final private List<File> sourceFolders = new ArrayList<File>();
private File defaultOutputFolder;
@@ -100,12 +101,19 @@ public class MockProjects {
}
return cp;
}
@Override
public String getJavaVersion() {
// return javaVersion;
return null;
}
};
public MockProject(String name) {
synchronized (projectsByName) {
assertFalse(projectsByName.containsKey(name));
this.name = name;
// this.javaVersion = "";
this.root = Files.createTempDir();
createSourceFolder("src/main/java");
createSourceFolder("src/main/resources");

View File

@@ -6,7 +6,8 @@ import {
window,
workspace,
ExtensionContext,
Uri
Uri,
lm
} from 'vscode';
import * as commons from '@pivotal-tools/commons-vscode';
@@ -22,6 +23,11 @@ import * as setLogLevelUi from './set-log-levels-ui';
import { startTestJarSupport } from "./test-jar-launch";
import { startPropertiesConversionSupport } from "./convert-props-yaml";
import { activateCopilotFeatures } from "./copilot";
import * as springBootAgent from './copilot/springBootAgent';
import { SpringCli } from './copilot/springCli';
import { applyLspEdit } from "./copilot/guideApply";
import { isLlmApiReady } from "./copilot/util";
import CopilotRequest, { logger } from "./copilot/copilotRequest";
const PROPERTIES_LANGUAGE_ID = "spring-boot-properties";
const YAML_LANGUAGE_ID = "spring-boot-properties-yaml";
@@ -32,6 +38,7 @@ const JPA_QUERY_PROPERTIES_LANGUAGE_ID = "jpa-query-properties";
const STOP_ASKING = "Stop Asking";
export const SPRINGCLI = new SpringCli();
/** Called when extension is activated */
export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
@@ -162,9 +169,15 @@ export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
rewrite.activate(client, options, context);
setLogLevelUi.activate(client, options, context);
startPropertiesConversionSupport(context);
if(isLlmApiReady)
activateSpringBootParticipant(context);
else
window.showInformationMessage("Spring Boot chat participant is not available. Please use the vscode insiders version 1.90.0 or above and make sure all `lm` API is enabled.");
registerMiscCommands(context);
commands.registerCommand('vscode-spring-boot.agent.apply', applyLspEdit);
return new ApiManager(client).api;
});
}
@@ -192,3 +205,13 @@ function registerMiscCommands(context: ExtensionContext) {
}),
);
}
async function activateSpringBootParticipant(context: ExtensionContext) {
const model = (await lm.selectChatModels(CopilotRequest.DEFAULT_MODEL_SELECTOR))?.[0];
if (!model) {
const models = await lm.selectChatModels();
logger.error(`Not a suitable model. The available models are: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later) and all \`lm\` API is enabled.`);
return;
}
springBootAgent.activate(context);
}

View File

@@ -0,0 +1,90 @@
import { LanguageModelChatRequestOptions, LanguageModelChatSelector, CancellationToken, Disposable, LanguageModelChatMessage, window, ProgressLocation, lm, LogOutputChannel, LanguageModelChatMessageRole, LanguageModelError } from "vscode";
export const logger: LogOutputChannel = window.createOutputChannel("Spring tools agent", { log: true });
export default class CopilotRequest {
public static readonly DEFAULT_END_MARK = '<|endofresponse|>';
public static readonly DEFAULT_MAX_ROUNDS = 2;
public static readonly DEFAULT_MODEL_SELECTOR: LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-3.5-turbo' };
public static readonly DEFAULT_MODEL_OPTIONS: LanguageModelChatRequestOptions = { modelOptions: {} };
public static readonly NOT_CANCELLABLE: CancellationToken = { isCancellationRequested: false, onCancellationRequested: () => Disposable.from() };
public constructor(
private readonly systemMessagesOrPrompts: LanguageModelChatMessage[] = [],
private readonly modelSelector: LanguageModelChatSelector = CopilotRequest.DEFAULT_MODEL_SELECTOR,
private readonly modelOptions: LanguageModelChatRequestOptions = CopilotRequest.DEFAULT_MODEL_OPTIONS,
private readonly endMark: string = CopilotRequest.DEFAULT_END_MARK,
private readonly maxRounds: number = CopilotRequest.DEFAULT_MAX_ROUNDS,
) {
}
public async chatRequest(userMessage: LanguageModelChatMessage[], modelOptions: LanguageModelChatRequestOptions = CopilotRequest.DEFAULT_MODEL_OPTIONS, cancellationToken: CancellationToken = CopilotRequest.NOT_CANCELLABLE): Promise<string> {
const messages = [...this.systemMessagesOrPrompts];
let answer: string = '';
let rounds: number = 0;
return window.withProgress({
location: ProgressLocation.Window,
title: "Copilot request",
cancellable: true
}, async (progress, cancellation) => {
progress.report({ message: "processing..." });
if (cancellation.isCancellationRequested) {
logger.info("Chat request cancelled");
return 'Chat request cancelled';
}
const _send = async (message: LanguageModelChatMessage[]): Promise<boolean> => {
rounds++;
let response: string = '';
messages.push(...message);
try {
messages.forEach(m => logger.info(m.content));
response = await this.sendRequest(messages, modelOptions, cancellationToken);
answer += response;
logger.info(`Response: \n`, response);
} catch (e) {
if (e instanceof LanguageModelError) {
logger.error(e.message, e.code);
throw e;
} else {
const cause = e.cause || e;
logger.error(`Failed to chat with copilot`, e.message, e.stack);
throw cause;
}
}
messages.push(new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, response));
return answer.trim().endsWith(this.endMark);
}
let completeResponse: boolean = await _send(userMessage);
while (!completeResponse && rounds < this.maxRounds) {
completeResponse = await _send([new LanguageModelChatMessage(LanguageModelChatMessageRole.User, `continue your response from where you left off, or end your response with "//${this.endMark}" to finish the conversation.`)]);
}
logger.debug('rounds', rounds);
return answer.replace("//"+this.endMark, "");
});
}
private async selectModel() {
const model = (await lm.selectChatModels(this.modelSelector))?.[0];
if (!model) {
const models = await lm.selectChatModels();
throw new Error(`No suitable model, available models: [${models.map(m => m.name).join(', ')}]. Please make sure you have installed the latest "GitHub Copilot Chat" (v0.16.0 or later).`);
}
return model;
}
private async sendRequest(messages: LanguageModelChatMessage[], modelOptions: LanguageModelChatRequestOptions, cancellationToken: CancellationToken) {
const response = [];
const model = await this.selectModel();
const chatResponse = await model.sendRequest(messages, modelOptions ?? this.modelOptions, cancellationToken);
for await (const fragment of chatResponse.text) {
response.push(fragment);
}
return response.join('');
}
}

View File

@@ -0,0 +1,35 @@
import { Uri, workspace, window } from "vscode";
import { SPRINGCLI } from "../Main";
import { getTargetGuideMardown } from "./util";
import { createConverter } from "vscode-languageclient/lib/common/protocolConverter";
import fs from "fs";
const CONVERTER = createConverter(undefined, true, true);
const CANCELLED = "Cancelled";
export async function applyLspEdit(uri: Uri) {
try {
if (!uri) {
uri = await getTargetGuideMardown();
}
const lspEdit = await SPRINGCLI.guideLspEdit(uri);
const workspaceEdit = await CONVERTER.asWorkspaceEdit(lspEdit);
console.log(lspEdit);
await Promise.all(workspaceEdit.entries().map(async ([uri, edits]) => {
if (fs.existsSync(uri.fsPath)) {
const doc = await workspace.openTextDocument(uri.fsPath);
await window.showTextDocument(doc);
}
}));
return await workspace.applyEdit(workspaceEdit, {
isRefactoring: true
});
} catch (error) {
if (error !== CANCELLED) {
window.showErrorMessage(error);
}
}
}

View File

@@ -0,0 +1,110 @@
import CopilotRequest from "./copilotRequest";
import { CancellationToken, chat, ChatContext, ChatRequest, ChatResponseStream, ChatResult, commands, ExtensionContext, l10n, LanguageModelChatMessage, LanguageModelChatMessageRole, Uri, workspace } from "vscode";
import { systemBoot2Prompt, systemBoot3Prompt, systemPrompt } from "./system-ai-prompt";
import { userPrompt } from "./user-ai-prompt";
import { getWorkspaceRoot, writeResponseToFile } from "./util";
import { SPRINGCLI } from "../Main";
const PARTICIPANT_ID = 'springboot.agent';
const SYSTEM_PROMPT = systemPrompt;
const USER_PROMPT = userPrompt;
interface BootProjectInfo {
name: string;
uri: string;
mainClass: string;
buildTool: string;
springBootVersion: string;
javaVersion: string;
}
interface SpringBootChatAgentResult extends ChatResult {
metadata: {
command: string;
}
}
export default class SpringBootChatAgent {
copilotRequest: CopilotRequest;
constructor(copilotRequest: CopilotRequest) {
this.copilotRequest = copilotRequest;
}
public async handlePrompts(request: ChatRequest, context: ChatContext, stream: ChatResponseStream, cancellationToken: CancellationToken): Promise<SpringBootChatAgentResult> {
const selectedProject = (await getWorkspaceRoot());
if(!selectedProject) {
stream.markdown('No project selected from the workspace');
return;
}
const selectedProjectUri = Uri.file(selectedProject?.fsPath).toString();
// Fetch project related information from the Spring boot language server
const bootProjInfo = await commands.executeCommand("sts/spring-boot/bootProjectInfo", selectedProjectUri) as BootProjectInfo;
const projectContext = `
Use the following project information for the solution: Please suggest code compatible with the project version.
Main Spring project name: ${bootProjInfo.name}
Root Package name: ${bootProjInfo.mainClass.substring(0, bootProjInfo.mainClass.lastIndexOf('.'))}
Build tool: ${bootProjInfo.buildTool}
Spring boot version: ${bootProjInfo.springBootVersion}
Java version: ${bootProjInfo.javaVersion}
User prompt: ${request.prompt}
`;
// Enhance prompt with project information and user prompt. Provide spring boot 3 speicifc context when necessary
const messages = [
LanguageModelChatMessage.User(projectContext),
bootProjInfo.springBootVersion.startsWith('3') ? LanguageModelChatMessage.User(systemBoot3Prompt) : LanguageModelChatMessage.User(systemBoot2Prompt),
LanguageModelChatMessage.User('User Input: ' +request.prompt),
];
stream.progress('Generating code....This will take a few minutes');
// Chat request to copilot LLM
const response = await this.copilotRequest.chatRequest(messages, {}, cancellationToken);
// write the response to markdown file
const targetMarkdownUri = await writeResponseToFile(response, bootProjInfo.name, selectedProject.fsPath);
let documentContent;
if (!targetMarkdownUri) {
documentContent = 'Note: The code provided is just an example and may not be suitable for production use. \n ' + response;
} else {
// modify the response from copilot LLM i.e. make response Boot 3 compliant if necessary
if (bootProjInfo.springBootVersion.startsWith('3')) {
const enhancedResponse = await SPRINGCLI.enhanceResponse(targetMarkdownUri, selectedProject.fsPath);
await writeResponseToFile(enhancedResponse, bootProjInfo.name, selectedProject.fsPath);
}
documentContent = await workspace.fs.readFile(targetMarkdownUri);
}
const chatResponse = Buffer.from(documentContent).toString();
stream.markdown(chatResponse);
stream.button({
command: 'vscode-spring-boot.agent.apply',
title: l10n.t('Preview Changes')
});
return { metadata: { command: '' } };
}
}
export function activate(
context: ExtensionContext
) {
const systemPrompts: LanguageModelChatMessage[] = [
new LanguageModelChatMessage(LanguageModelChatMessageRole.User, SYSTEM_PROMPT),
// new LanguageModelChatMessage(LanguageModelChatMessageRole.User, USER_PROMPT)
];
const copilotRequest = new CopilotRequest(systemPrompts);
const springBootChatAgent = new SpringBootChatAgent(copilotRequest);
const agent = chat.createChatParticipant(PARTICIPANT_ID, async (request, context, progress, token) => {
return springBootChatAgent.handlePrompts(request, context, progress, token);
});
agent.iconPath = Uri.joinPath(context.extensionUri, 'readme-imgs', 'spring-tools-icon.png');
}

View File

@@ -0,0 +1,121 @@
import { ProgressLocation, Uri, window, workspace } from "vscode";
import cp from "child_process";
import * as vscode from 'vscode';
import { homedir } from "os";
import { getWorkspaceRoot, getWorkspaceRootPath } from "./util";
import path from "path";
import { WorkspaceEdit } from "vscode-languageclient";
export const CANCELLED = "Cancelled";
export class SpringCli {
get executable(): string {
return workspace.getConfiguration("spring-cli").get("executable") || "spring";
}
guideLspEdit(uri: Uri, cwd?: string): Promise<WorkspaceEdit> {
const args = [
"guide",
"apply",
"--lsp-edit",
"--file",
uri.fsPath
];
return this.fetchJson("Applying guide", uri.fsPath, args, cwd || path.dirname(uri.fsPath), true);
}
enhanceResponse(uri: Uri, cwd: string): Thenable<string> {
const args = [
"ai",
"enhance-response",
"--file",
uri.fsPath
];
return this.exec("Spring cli ai", "Enhance response", args, cwd);
}
private async executeCommand(args: string[], cwd?: string): Promise<string> {
const processOpts = { cwd: cwd || (await getWorkspaceRoot())?.fsPath || homedir() };
const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")}`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")}`, processOpts);
const dataChunks: string[] = [];
process.stdout.on("data", s => dataChunks.push(s));
return new Promise<string>((resolve, reject) => {
process.on("exit", (code) => {
if (code) {
reject(`Failed to execute command: ${dataChunks.join()}`);
} else {
resolve(dataChunks.join());
}
});
});
}
private async exec<T>(title: string, message: string, args: string[], cwd?: string): Promise<T> {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
cancellable: true,
title,
}, async (progress, cancellation) => {
if (message) {
progress.report({ message });
}
return new Promise<T>(async (resolve, reject) => {
if (cancellation.isCancellationRequested) {
reject("Cancelled");
}
try {
const output: string = await this.executeCommand(args, cwd);
resolve(output as T);
} catch (error) {
console.error(`Error: ${error}`);
reject(error);
}
});
});
}
private async fetchJson<T>(title: string, message: string, args: string[], cwd?: string, omitJsonParam?: boolean): Promise<T> {
return window.withProgress({
location: ProgressLocation.Window,
cancellable: true,
title
}, (progress, cancellation) => {
if (message) {
progress.report({ message });
}
return new Promise<T>(async (resolve, reject) => {
if (cancellation.isCancellationRequested) {
reject(CANCELLED);
}
const processOpts = { cwd: cwd || getWorkspaceRootPath()?.fsPath || homedir() };
const process = this.executable.endsWith(".jar") ? await cp.exec(`java -jar ${this.executable} ${args.join(" ")}`, processOpts) : await cp.exec(`${this.executable} ${args.join(" ")} ${omitJsonParam ? "" : "--json"}`, processOpts);
cancellation.onCancellationRequested(() => process.kill());
const dataChunks: string[] = [];
process.stdout.on("data", s => dataChunks.push(s));
process.on("exit", (code) => {
if (code) {
if (cancellation.isCancellationRequested) {
reject(CANCELLED);
} else {
reject(`Failed to fetch data: ${dataChunks.join()}`);
}
} else {
try {
resolve(JSON.parse(dataChunks.join()) as T);
} catch (error) {
reject(error);
}
}
});
});
});
}
}

View File

@@ -0,0 +1,46 @@
import CopilotRequest from "./copilotRequest"
export const systemPrompt = `**Your task is to create Java source code for a Spring boot application. Follow these guidelines:**
- IMPORTANT: CONCLUDE YOUR RESPONSE WITH THE MARKER \"//${CopilotRequest.DEFAULT_END_MARK}\" TO INDICATE END OF RESPONSE.
- Generate a pom.xml snippet that includes the necessary Spring Boot Starter dependencies such as "spring-boot-starter-jpa".
- Organize code into appropriate package. Use the Package name as the root package and place the files in sub-packages accordingly.
- Include import statements in all code files.
- Generate architectural layers (Controller, Service, Repository, Entity) code as appropriate.
- If no entity or domain object is specified in the description, use a Person entity with the properties name and phone number.
- Generate constructors, property getters, and setters for the entity or domain object (e.g., "getName" and "setName" for the name property).
- Add any required annotations to the main application class (class with the @SpringBootApplication annotation).
- When generating markdown for code blocks in the final response, include an appropriate value for the "info" field.
- Provide application.properties file with sample configurations.
- Include unit tests for each architectural layer (Controller, Service, Repository) as appropriate.
- Include an integration test if there are multiple architectural layers.
`
export const systemBoot3Prompt = `For Spring boot 3 and above:
- IMPORTANT: For JPA related applications, the 'javax' package has been replaced with 'jakarta' package. All JPA-related imports should use 'jakarta.persistence' instead of 'javax.persistence'.
\`\`\`
"""
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@Entity
public class Person {
}
\`\`\`
- The coordinates of the MySQL JDBC driver have changed from "mysql:mysql-connector-java" to "com.mysql:mysql-connector-j". If you are using the MySQL JDBC driver, update its coordinates accordingly.
Only when the generated code requires a MySQL JDBC driver, the MySQL JDBC driver should be "com.mysql:mysql-connector-j". This is a change from the older "mysql:mysql-connector-java".
Please ensure to use the "com.mysql:mysql-connector-j" MySQL JDBC driver in pom.xml. Here is the maven dependency to be used in this case:
\`\`\`
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
\`\`\`
`
export const systemBoot2Prompt = `
- All JPA-related imports should use 'javax.persistence'
- The MySQL JDBC driver coordinates are "mysql:mysql-connector-java"
`

View File

@@ -0,0 +1,9 @@
export const userPrompt = `Create the Spring Java application with the following project information
Ensure the solution meets the following additional criteria
1. Include import statements in all code files
2. Generate getter and setters in entity classes
3. Provide the necessary Spring Boot Starter dependencies in the pom.xml file
4. When generating markdown for the 'application.properties' file in Spring Boot, please ensure that the markdown 'info' field is set to 'properties' for that specific section.
`

View File

@@ -0,0 +1,91 @@
import path from "path";
import { Uri, WorkspaceFolder, version, window, workspace } from "vscode";
import fs from "fs";
import { SemVer } from "semver";
export function getExecutable(): string {
return workspace.getConfiguration("spring-cli").get("executable") || "spring";
}
export async function getWorkspaceRoot(): Promise<Uri | undefined> {
if (workspace.workspaceFolders && workspace.workspaceFolders.length) {
if (workspace.workspaceFolders.length === 1) {
return workspace.workspaceFolders[0].uri;
} else {
return await window.showQuickPick(
workspace.workspaceFolders.map((c: WorkspaceFolder) => ({ value: c.uri, label: getRelativePathToWorkspaceFolder(c.uri), description: getWorkspaceFolderName(c.uri) })),
{ placeHolder: "Select the target project." },
).then(res => res && res.value);
}
}
}
export function getWorkspaceRootPath(): Uri | undefined {
if (workspace.workspaceFolders && workspace.workspaceFolders.length) {
return workspace.workspaceFolders[0].uri
}
}
function getRelativePathToWorkspaceFolder(file: Uri): string {
if (file) {
const wf: WorkspaceFolder = workspace.getWorkspaceFolder(file);
if (wf) {
return path.relative(wf.uri.fsPath, file.fsPath);
}
}
return '';
}
function getWorkspaceFolderName(file: Uri): string {
if (file) {
const wf: WorkspaceFolder = workspace.getWorkspaceFolder(file);
if (wf) {
return wf.name;
}
}
return '';
}
export async function getTargetGuideMardown(): Promise<Uri> {
if (window.activeTextEditor) {
const activeUri = window.activeTextEditor.document.uri;
if (/README-\S+.md/.test(path.basename(activeUri.path).toLowerCase())) {
return activeUri;
}
}
const candidates: Uri[] = await workspace.findFiles("**/README-*.md");
if (candidates.length > 0) {
if (candidates.length === 1) {
return candidates[0];
} else {
return await window.showQuickPick(
candidates.map((c: Uri) => ({ value: c, label: getRelativePathToWorkspaceFolder(c), description: getWorkspaceFolderName(c) })),
{ placeHolder: "Select the target project." },
).then(res => res && res.value);
}
}
return undefined;
}
export async function writeResponseToFile(response: string, appName: string, selectedProject: string) {
const readmeFilePath = path.resolve(selectedProject, `README-ai-${appName}.md`);
if (fs.existsSync(readmeFilePath)) {
try {
fs.unlinkSync(readmeFilePath);
} catch (ex) {
throw new Error(`Could not delete readme file: ${readmeFilePath}, ${ex}`);
}
}
try {
fs.writeFileSync(readmeFilePath, response);
return Uri.file(readmeFilePath);
} catch (ex) {
throw new Error(`Could not write readme file: ${readmeFilePath}, ${ex}`);
}
}
export function isLlmApiReady(): boolean {
return version.includes('insider') && new SemVer(version).compare(new SemVer("1.90.0-insider")) >= 0;
}

View File

@@ -30,7 +30,24 @@
"activationEvents": [
"onCommand:vscode-spring-boot.ls.start"
],
"enabledApiProposals": [
"chatVariableResolver"
],
"contributes": {
"chatParticipants": [
{
"id": "springboot.agent",
"fullName": "Spring boot",
"name": "springboot",
"description": "Spring boot chat agent",
"commands": [
{
"name": "add",
"description": "Add a new spring module to the exisiting project"
}
]
}
],
"javaExtensions": [
"./jars/io.projectreactor.reactor-core.jar",
"./jars/org.reactivestreams.reactive-streams.jar",
@@ -248,6 +265,12 @@
"command": "vscode-spring-boot.query.explain",
"title": "Explain Spel expressions, Spring Data queries, and AOP annotations (using Copilot)",
"category": "Spring Boot"
},
{
"command": "vscode-spring-boot.agent.apply",
"title": "Apply Changes",
"category": "Spring Boot Agent",
"enablement": "false"
}
],
"configuration": [
@@ -433,6 +456,17 @@
}
}
},
{
"id": "spring-cli",
"title": "Spring CLI",
"properties": {
"spring-cli.executable": {
"type": "string",
"default": "spring",
"description": "Spring CLI executable. Either name of the executable if it's on the PATH environment variable or absolute path to the spring CLI executable (or JAR for dev purposes)"
}
}
},
{
"id": "ls",
"title": "Language Server",
@@ -1334,6 +1368,7 @@
"@pivotal-tools/commons-vscode": "file:../commons-vscode/pivotal-tools-commons-vscode-0.2.4.tgz",
"portfinder": "1.0.32",
"ps-list": "^7.2.0",
"semver": "^7.6.2",
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB