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

AAP-30427: VS Code Extension settings page to take a new custom_prompt field #1545

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,18 @@
"type": "string",
"markdownDescription": "Model ID to override your organization's default model. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.",
"order": 4
},
"ansible.lightspeed.playbookGenerationCustomPrompt": {
"scope": "resource",
"type": "string",
"markdownDescription": "Custom Prompt for Playbook generation. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.",
"order": 5
},
"ansible.lightspeed.playbookExplanationCustomPrompt": {
"scope": "resource",
"type": "string",
"markdownDescription": "Custom Prompt for Playbook explanation. This setting is only applicable to commercial users with an Ansible Lightspeed seat assignment.",
"order": 6
}
}
},
Expand Down
13 changes: 13 additions & 0 deletions src/features/lightspeed/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ export class LightSpeedAPI {
return {} as ExplanationResponseParams;
}
try {
const customPrompt =
lightSpeedManager.settingsManager.settings.lightSpeedService
.playbookExplanationCustomPrompt;
if (customPrompt && customPrompt.length > 0) {
inputData.customPrompt = customPrompt;
}
const requestData = {
...inputData,
metadata: { ansibleExtensionVersion: this._extensionVersion },
Expand Down Expand Up @@ -336,6 +342,13 @@ export class LightSpeedAPI {
return {} as GenerationResponseParams;
}
try {
const customPrompt =
lightSpeedManager.settingsManager.settings.lightSpeedService
.playbookGenerationCustomPrompt;
if (customPrompt && customPrompt.length > 0) {
inputData.customPrompt = customPrompt;
}

const requestData = {
...inputData,
metadata: { ansibleExtensionVersion: this._extensionVersion },
Expand Down
57 changes: 40 additions & 17 deletions src/features/lightspeed/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,52 @@
? responseErrorData.message
: "unknown";
}

let detail: string = "";
if (typeof responseErrorData.message == "string") {
detail = responseErrorData.message ?? "";
} else if (Array.isArray(responseErrorData.message)) {
const messages = responseErrorData.message as [];
messages.forEach((value: string, index: number) => {
detail =
detail +
"(" +
(index + 1) +
") " +
value +
(index < messages.length - 1 ? " " : "");
});
}
const items = (err?.response?.data as Record<string, unknown>) ?? {};
const detail = Object.hasOwn(items, "detail")
? items["detail"]
: undefined;

// Clone the Error to preserve the original definition
return new Error(e.code, message, detail, e.check);
return new Error(
e.code,
message,
this.prettyPrintDetail(detail),
e.check,
);
}

return undefined;
}

public prettyPrintDetail(detail: unknown): string | undefined {
let pretty: string = "";
if (detail === undefined) {
return undefined;
} else if (typeof detail == "string") {
pretty = detail ?? "";
} else if (Array.isArray(detail)) {
const items = detail as [];
items.forEach((value: string, index: number) => {
pretty =
pretty +
"(" +
(index + 1) +
") " +
value +

Check warning on line 117 in src/features/lightspeed/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/features/lightspeed/errors.ts#L113-L117

Added lines #L113 - L117 were not covered by tests
(index < items.length - 1 ? " " : "");
});

Check warning on line 119 in src/features/lightspeed/errors.ts

View check run for this annotation

Codecov / codecov/patch

src/features/lightspeed/errors.ts#L119

Added line #L119 was not covered by tests
} else if (detail instanceof Object && detail.constructor === Object) {
const items = detail as Record<string, unknown>;
const keys: string[] = Object.keys(detail);
keys.forEach((key, index) => {
pretty =
pretty +
`${key}: ${items[key]}` +
(index < keys.length - 1 ? " " : "");
});
}
return pretty;
}
}

export const ERRORS = new Errors();
Expand Down
23 changes: 14 additions & 9 deletions src/features/lightspeed/handleApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,38 @@
}

// If the error is unknown fallback to defaults
const detail = err.response?.data;
const items = (err?.response?.data as Record<string, unknown>) ?? {};
const detail = Object.hasOwn(items, "detail") ? items["detail"] : undefined;
const status: number | string = err?.response?.status ?? err?.code ?? 500;
if (err instanceof CanceledError) {
return ERRORS_CONNECTION_CANCELED_TIMEOUT;
}
if (status === 400) {
return ERRORS_BAD_REQUEST.withDetail(detail);
return ERRORS_BAD_REQUEST.withDetail(ERRORS.prettyPrintDetail(detail));
}
if (status === 401) {
return ERRORS_UNAUTHORIZED.withDetail(detail);
return ERRORS_UNAUTHORIZED.withDetail(ERRORS.prettyPrintDetail(detail));
}
if (status === 403) {
return ERRORS_UNAUTHORIZED.withDetail(detail);
return ERRORS_UNAUTHORIZED.withDetail(ERRORS.prettyPrintDetail(detail));
}
if (status === 404) {
return ERRORS_NOT_FOUND.withDetail(detail);
return ERRORS_NOT_FOUND.withDetail(ERRORS.prettyPrintDetail(detail));
}
if (status === 429) {
return ERRORS_TOO_MANY_REQUESTS.withDetail(detail);
return ERRORS_TOO_MANY_REQUESTS.withDetail(
ERRORS.prettyPrintDetail(detail),
);

Check warning on line 45 in src/features/lightspeed/handleApiError.ts

View check run for this annotation

Codecov / codecov/patch

src/features/lightspeed/handleApiError.ts#L44-L45

Added lines #L44 - L45 were not covered by tests
}
if (status === 500) {
return ERRORS_UNKNOWN.withDetail(detail);
return ERRORS_UNKNOWN.withDetail(ERRORS.prettyPrintDetail(detail));
}
if (status === AxiosError.ECONNABORTED) {
return ERRORS_CONNECTION_TIMEOUT.withDetail(detail);
return ERRORS_CONNECTION_TIMEOUT.withDetail(
ERRORS.prettyPrintDetail(detail),
);

Check warning on line 53 in src/features/lightspeed/handleApiError.ts

View check run for this annotation

Codecov / codecov/patch

src/features/lightspeed/handleApiError.ts#L52-L53

Added lines #L52 - L53 were not covered by tests
}

console.log(`Lightspeed request failed with unknown error ${err}`);
return ERRORS_UNKNOWN.withDetail(detail);
return ERRORS_UNKNOWN.withDetail(ERRORS.prettyPrintDetail(detail));
}
18 changes: 14 additions & 4 deletions src/features/lightspeed/playbookExplanation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,25 @@ export const playbookExplanation = async (extensionUri: vscode.Uri) => {
console.log(response);
if (isError(response)) {
const oneClickTrialProvider = getOneClickTrialProvider();
const my_error = response as IError;
if (!(await oneClickTrialProvider.showPopup(my_error))) {
vscode.window.showErrorMessage(my_error.message ?? UNKNOWN_ERROR);
if (!(await oneClickTrialProvider.showPopup(response))) {
const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`;
vscode.window.showErrorMessage(errorMessage);
currentPanel.setContent(
`<p><span class="codicon codicon-error"></span>The operation has failed:<p>${my_error.message}</p></p>`,
`<p><span class="codicon codicon-error"></span>The operation has failed:<p>${errorMessage}</p></p>`,
);
}
} else {
markdown = response.content;
if (markdown.length === 0) {
markdown = "### No explanation provided.";
const customPrompt =
lightSpeedManager.settingsManager.settings.lightSpeedService
.playbookExplanationCustomPrompt ?? "";
if (customPrompt.length > 0) {
markdown +=
"\n\nYou may want to consider amending your custom prompt.";
}
}
const html_snippet = marked.parse(markdown) as string;
currentPanel.setContent(html_snippet, true);
}
Expand Down
8 changes: 4 additions & 4 deletions src/features/lightspeed/playbookGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,8 @@ export async function showPlaybookGenerationPage(
if (isError(response)) {
const oneClickTrialProvider = getOneClickTrialProvider();
if (!(await oneClickTrialProvider.showPopup(response))) {
vscode.window.showErrorMessage(
response.message ?? UNKNOWN_ERROR,
);
const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`;
vscode.window.showErrorMessage(errorMessage);
}
} else {
panel.webview.postMessage({
Expand Down Expand Up @@ -198,7 +197,8 @@ export async function showPlaybookGenerationPage(
panel,
);
if (isError(response)) {
vscode.window.showErrorMessage(response.message ?? UNKNOWN_ERROR);
const errorMessage: string = `${response.message ?? UNKNOWN_ERROR} ${response.detail ?? ""}`;
vscode.window.showErrorMessage(errorMessage);
break;
}
playbook = response.playbook;
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/extensionSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ export interface LightSpeedServiceSettings {
URL: string;
suggestions: { enabled: boolean; waitWindow: number };
model: string | undefined;
playbookGenerationCustomPrompt: string | undefined;
playbookExplanationCustomPrompt: string | undefined;
}
2 changes: 2 additions & 0 deletions src/interfaces/lightspeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export interface ISuggestionDetails {

export interface GenerationRequestParams {
text: string;
customPrompt?: string;
outline?: string;
generationId: string;
createOutline: boolean;
Expand All @@ -156,6 +157,7 @@ export interface GenerationResponseParams {

export interface ExplanationRequestParams {
content: string;
customPrompt?: string;
explanationId: string;
}

Expand Down
8 changes: 8 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export class SettingsManager {
waitWindow: lightSpeedSettings.get("suggestions.waitWindow", 0),
},
model: lightSpeedSettings.get("modelIdOverride", undefined),
playbookGenerationCustomPrompt: lightSpeedSettings.get(
"playbookGenerationCustomPrompt",
undefined,
),
playbookExplanationCustomPrompt: lightSpeedSettings.get(
"playbookExplanationCustomPrompt",
undefined,
),
},
playbook: {
arguments: playbookSettings.get("arguments", ""),
Expand Down
1 change: 1 addition & 0 deletions src/webview/apps/lightspeed/playbookGeneration/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ window.addEventListener("message", async (event) => {
}
case "outline": {
setupPage(2);

outline.update(message.outline.outline);
savedPlaybook = message.outline.playbook;
generationId = message.outline.generationId;
Expand Down
27 changes: 26 additions & 1 deletion test/mockLightspeedServer/explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function explanations(
res: any,
) {
const playbook = req.body.content;
const customPrompt = req.body.customPrompt;
const explanationId = req.body.explanationId
? req.body.explanationId
: uuidv4();
Expand All @@ -28,8 +29,28 @@ export function explanations(
});
}

// Special case to replicate explanation being unavailable
if (playbook !== undefined && playbook.includes("No explanation available")) {
logger.info("Returning empty content. Explanation is not available");
return res.send({
content: "",
format,
explanationId,
});
}

// Special case to replicate a broken custom prompt
if (customPrompt && customPrompt === "custom prompt broken") {
logger.info("Returning 400. Custom prompt is invalid");
return res.status(400).send({
code: "validation",
message: "invalid",
detail: { customPrompt: "custom prompt is invalid" },
});
}

// cSpell: disable
const content = `
let content = `
## Playbook Overview and Structure

This playbook creates an Azure Virtual Network (VNET) with the name "VNET_1"
Expand Down Expand Up @@ -73,6 +94,10 @@ the following parameters:
`;
// cSpell: enable

if (customPrompt) {
content += "\nCustom prompt explanation.";
}

return res.send({
content,
format,
Expand Down
15 changes: 15 additions & 0 deletions test/mockLightspeedServer/generations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function generations(
res: any,
) {
const text = req.body.text;
const customPrompt = req.body.customPrompt;
const createOutline = req.body.createOutline;
const generationId = req.body.generationId ? req.body.generationId : uuidv4();
const wizardId = req.body.wizardId;
Expand Down Expand Up @@ -46,6 +47,16 @@ export function generations(
});
}

// Special case to replicate a broken custom prompt
if (customPrompt && customPrompt === "custom prompt broken") {
logger.info("Returning 400. Custom prompt is invalid");
return res.status(400).send({
code: "validation",
message: "invalid",
detail: { customPrompt: "custom prompt is invalid" },
});
}

// cSpell: disable
let outline: string | undefined = `1. Create VNET named VNET_1
2. Create VNET named VNET_2
Expand Down Expand Up @@ -98,6 +109,10 @@ export function generations(
outline += "\n4. Some extra step.";
}

if (customPrompt) {
outline += "\n5. Custom prompt step.";
}

return res.send({
playbook,
outline,
Expand Down
5 changes: 5 additions & 0 deletions test/testFixtures/lightspeed/playbook_explanation_none.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
- name:
hosts: all
tasks:
- name: No explanation available...
Loading