Skip to content

Commit

Permalink
lightspeed: move playbook exp/gen away from the LSP server
Browse files Browse the repository at this point in the history
It's tricky to get the LSP to send the requests through a HTTP server and we don't
want to deal with to different network configuration/stack.

In addition, we would like to migrate away from Axios and use Electron's `fetch` which
is already configured to use the VS Code proxy configuration.
  • Loading branch information
goneri committed Aug 6, 2024
1 parent 652415b commit a010cf4
Show file tree
Hide file tree
Showing 18 changed files with 183 additions and 225 deletions.
21 changes: 0 additions & 21 deletions packages/ansible-language-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/ansible-language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"dependencies": {
"@flatten-js/interval-tree": "^1.1.3",
"antsibull-docs": "^1.0.2",
"axios": "^1.7.2",
"glob": "^10.4.5",
"ini": "^4.1.3",
"lodash": "^4.17.21",
Expand Down
97 changes: 0 additions & 97 deletions packages/ansible-language-server/src/ansibleLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,6 @@ import { doValidate } from "./providers/validationProvider";
import { ValidationManager } from "./services/validationManager";
import { WorkspaceManager } from "./services/workspaceManager";
import { getAnsibleMetaData } from "./utils/getAnsibleMetaData";
import axios from "axios";
import { AxiosError } from "axios";
import { getBaseUri } from "./utils/webUtils";
import {
ExplanationResponse,
GenerationResponse,
IError,
} from "./interfaces/lightspeedApi";
import { mapError } from "./utils/handleApiError";

/**
* Initializes the connection and registers all lifecycle event handlers.
Expand Down Expand Up @@ -360,94 +351,6 @@ export class AnsibleLanguageService {
}
},
);

this.connection.onRequest(
"playbook/explanation",
async (params): Promise<ExplanationResponse> => {
const accessToken: string = params["accessToken"];
const URL: string = params["URL"];
const content: string = params["content"];
const explanationId: string = params["explanationId"];

const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};

const axiosInstance = axios.create({
baseURL: `${getBaseUri(URL)}/api/v0`,
headers: headers,
});

const result: ExplanationResponse = await axiosInstance
.post(
"/ai/explanations/",
{
content: content,
explanationId: explanationId,
},
{ signal: AbortSignal.timeout(28000) },
)
.then((response) => {
return response.data;
})
.catch((error) => {
const err = error as AxiosError;
const mappedError: IError = mapError(err);
return mappedError;
});

console.log(result);

return result;
},
);

this.connection.onRequest(
"playbook/generation",
async (params): Promise<GenerationResponse> => {
const accessToken: string = params["accessToken"];
const URL: string = params["URL"];
const text: string = params["text"];
const createOutline: boolean = params["createOutline"];
const outline: string | undefined = params["outline"];
const generationId: string = params["generationId"];
const wizardId: string | undefined = params["wizardId"];

const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};

const axiosInstance = axios.create({
baseURL: `${getBaseUri(URL)}/api/v0`,
headers: headers,
});

const result: GenerationResponse = await axiosInstance
.post(
"/ai/generations/",
{
text,
createOutline,
outline,
generationId,
wizardId,
},
{ signal: AbortSignal.timeout(28000) },
)
.then((response) => {
return response.data;
})
.catch((error) => {
const err = error as AxiosError;
const mappedError: IError = mapError(err);
return mappedError;
});

return result;
},
);
}

private handleError(error: unknown, contextName: string) {
Expand Down
24 changes: 0 additions & 24 deletions packages/ansible-language-server/src/interfaces/lightspeedApi.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/definitions/lightspeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export namespace LightSpeedCommands {
}

export const LIGHTSPEED_API_VERSION = "v0";
export const LIGHTSPEED_PLAYBOOK_EXPLANATION_URL = `${LIGHTSPEED_API_VERSION}/ai/explanations/`;
export const LIGHTSPEED_PLAYBOOK_GENERATION_URL = `${LIGHTSPEED_API_VERSION}/ai/generations/`;
export const LIGHTSPEED_SUGGESTION_COMPLETION_URL = `${LIGHTSPEED_API_VERSION}/ai/completions/`;
export const LIGHTSPEED_SUGGESTION_FEEDBACK_URL = `${LIGHTSPEED_API_VERSION}/ai/feedback/`;
export const LIGHTSPEED_SUGGESTION_CONTENT_MATCHES_URL = `${LIGHTSPEED_API_VERSION}/ai/contentmatches/`;
Expand Down
127 changes: 106 additions & 21 deletions src/features/lightspeed/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,39 @@ import axios, { AxiosInstance, AxiosError } from "axios";

import { SettingsManager } from "../../settings";
import {
CompletionResponseParams,
CompletionRequestParams,
FeedbackRequestParams,
FeedbackResponseParams,
CompletionResponseParams,
ContentMatchesRequestParams,
ContentMatchesResponseParams,
ExplanationRequestParams,
ExplanationResponseParams,
FeedbackRequestParams,
FeedbackResponseParams,
GenerationRequestParams,
GenerationResponseParams,
} from "../../interfaces/lightspeed";
import {
LIGHTSPEED_SUGGESTION_CONTENT_MATCHES_URL,
LIGHTSPEED_PLAYBOOK_EXPLANATION_URL,
LIGHTSPEED_PLAYBOOK_GENERATION_URL,
LIGHTSPEED_SUGGESTION_COMPLETION_URL,
LIGHTSPEED_SUGGESTION_CONTENT_MATCHES_URL,
LIGHTSPEED_SUGGESTION_FEEDBACK_URL,
UserAction,
} from "../../definitions/lightspeed";
import { getBaseUri } from "./utils/webUtils";
import { ANSIBLE_LIGHTSPEED_API_TIMEOUT } from "../../definitions/constants";
import { IError } from "@ansible/ansible-language-server/src/interfaces/lightspeedApi";
import { IError } from "./utils/errors";
import { lightSpeedManager } from "../../extension";
import { LightspeedUser } from "./lightspeedUser";
import { inlineSuggestionHideHandler } from "./inlineSuggestions";
import {
getOneClickTrialProvider,
OneClickTrialProvider,
} from "./utils/oneClickTrial";
import { mapError } from "./handleApiError";

const UNKNOWN_ERROR: string = "An unknown error occurred.";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let _handleApiError: any;

export async function mapError(error: AxiosError): Promise<IError> {
if (!_handleApiError) {
try {
_handleApiError =
await require("@ansible/ansible-language-server/src/utils/handleApiError");
} catch (e) {
_handleApiError =
await require(/* webpackIgnore: true */ "../../../../server/src/utils/handleApiError");
}
}
return _handleApiError.mapError(error);
}

export class LightSpeedAPI {
private axiosInstance: AxiosInstance | undefined;
private settingsManager: SettingsManager;
Expand Down Expand Up @@ -296,4 +287,98 @@ export class LightSpeedAPI {
return mappedError;
}
}

public async explanationRequest(
inputData: ExplanationRequestParams,
): Promise<ExplanationResponseParams | IError> {
// return early if the user is not authenticated
if (!(await this.lightspeedAuthenticatedUser.isAuthenticated())) {
vscode.window.showErrorMessage(
"User not authenticated to use Ansible Lightspeed.",
);
return {} as ExplanationResponseParams;
}

const axiosInstance = await this.getApiInstance();
if (axiosInstance === undefined) {
console.error("Ansible Lightspeed instance is not initialized.");
return {} as ExplanationResponseParams;
}
try {
const requestData = {
...inputData,
metadata: { ansibleExtensionVersion: this._extensionVersion },
};
console.log(
`[ansible-lightspeed] Explanation request sent to lightspeed: ${JSON.stringify(
requestData,
)}`,
);
const response = await axiosInstance.post(
LIGHTSPEED_PLAYBOOK_EXPLANATION_URL,
//LIGHTSPEED_SUGGESTION_CONTENT_MATCHES_URL,
requestData,
{
timeout: ANSIBLE_LIGHTSPEED_API_TIMEOUT,
// This is coming from our former LSP implementation, it may be a good
// idea to generalize the use of a <28s timeout to be below CloudFront's 30s
signal: AbortSignal.timeout(28000),
},
);
return response.data;
} catch (error) {
const err = error as AxiosError;
const mappedError: IError = await mapError(err);
// Do not show trial popup for errors on content matches because either
// completions or generations API should have been called already.
return mappedError;
}
}

public async generationRequest(
inputData: GenerationRequestParams,
): Promise<GenerationResponseParams | IError> {
// return early if the user is not authenticated
if (!(await this.lightspeedAuthenticatedUser.isAuthenticated())) {
vscode.window.showErrorMessage(
"User not authenticated to use Ansible Lightspeed.",
);
return {} as GenerationResponseParams;
}

const axiosInstance = await this.getApiInstance();
if (axiosInstance === undefined) {
console.error("Ansible Lightspeed instance is not initialized.");
return {} as GenerationResponseParams;
}
try {
const requestData = {
...inputData,
metadata: { ansibleExtensionVersion: this._extensionVersion },
};
console.log(
`[ansible-lightspeed] Explanation request sent to lightspeed: ${JSON.stringify(
requestData,
)}`,
);
const response = await axiosInstance.post(
LIGHTSPEED_PLAYBOOK_GENERATION_URL,
//LIGHTSPEED_SUGGESTION_CONTENT_MATCHES_URL,
requestData,
{
timeout: ANSIBLE_LIGHTSPEED_API_TIMEOUT,
// This is coming from our former LSP implementation, it may be a good
// idea to generalize the use of a <28s timeout to be below CloudFront's 30s
signal: AbortSignal.timeout(28000),
},
);
return response.data;
} catch (error) {
const err = error as AxiosError;
const mappedError: IError = await mapError(err);
// Do not show trial popup for errors on content matches because either
// completions or generations API should have been called already.
return mappedError;
}
}
}
2 changes: 1 addition & 1 deletion src/features/lightspeed/contentMatchesWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { getCurrentUTCDateTime } from "../utils/dateTime";
import * as yaml from "yaml";
import { LightspeedUser } from "./lightspeedUser";
import { parsePlays } from "./utils/parsePlays";
import { IError } from "@ansible/ansible-language-server/src/interfaces/lightspeedApi";
import { IError } from "./utils/errors";

export class ContentMatchesWebview implements vscode.WebviewViewProvider {
public static readonly viewType = "ansible.lightspeed.trainingMatchPanel";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AxiosError } from "axios";
import { IError } from "./interfaces/lightspeedApi";
import { IError } from "./utils/errors";

class Error implements IError {
readonly code: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
ERRORS_CONNECTION_CANCELED_TIMEOUT,
ERRORS_CONNECTION_TIMEOUT,
ERRORS_NOT_FOUND,
} from "../errors";
import { IError } from "../interfaces/lightspeedApi";
} from "./errors";
import { IError } from "./utils/errors";

export function mapError(err: AxiosError): IError {
// Lookup _known_ errors
Expand Down
Loading

0 comments on commit a010cf4

Please sign in to comment.