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

Updated AIB contract with default portal language #959

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8ebe024
Added default language of portal support in AI Builder contract with …
ritikramuka May 20, 2024
96816a7
Merge branch 'main' into users/ramukaritik/default-language-support-t…
ritikramuka May 20, 2024
86ac69a
removed lint error
ritikramuka May 20, 2024
d706dc6
Merge branch 'users/ramukaritik/default-language-support-to-AIB-contr…
ritikramuka May 20, 2024
81073f2
feat: Add function to read website YAML file
ritikramuka May 20, 2024
f445682
refactor: Update default language code variable name in sendApiReques…
ritikramuka May 21, 2024
abff20e
refactor: Moved strings to constants, and findWebsiteYAML function to…
ritikramuka May 21, 2024
9717e15
refactor: added defaultPortalLanguageCode before crossGeoDataMovement…
ritikramuka May 21, 2024
6ea0bfa
refactor: Add ADX_LANGUAGECODE and ADX_WEBSITE_LANGUAGE constants
ritikramuka May 21, 2024
c2b8de4
refactor: Update fetchLanguageCodeFromAPI function to use getCommonHe…
ritikramuka May 21, 2024
8802e5e
refactor: Update findWebsiteYAML function to handle parent directories
ritikramuka May 21, 2024
70d0862
refactor: Update findWebsiteYAML function to handle parent directories
ritikramuka May 21, 2024
71ddc67
Merge branch 'main' into users/ramukaritik/default-language-support-t…
ritikramuka May 21, 2024
ecb9def
refactored code to language utils
ritikramuka May 21, 2024
a55bd8c
Merge branch 'users/ramukaritik/default-language-support-to-AIB-contr…
ritikramuka May 21, 2024
0928d01
refactor: Update getDefaultLanguageCode function to handle desktop an…
ritikramuka May 21, 2024
97dedd0
refactor: Update Utils.ts to include parseYAMLAndFetchKey function
ritikramuka May 21, 2024
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
3 changes: 2 additions & 1 deletion src/common/copilot/IntelligenceApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { enableCrossGeoDataFlowInGeo } from "./utils/copilotUtil";
const clientType = EXTENSION_NAME + '-' + getExtensionType();
const clientVersion = getExtensionVersion();

export async function sendApiRequest(userPrompt: UserPrompt[], activeFileParams: IActiveFileParams, orgID: string, apiToken: string, sessionID: string, entityName: string, entityColumns: string[], telemetry: ITelemetry, aibEndpoint: string | null, geoName: string | null, crossGeoDataMovementEnabledPPACFlag = false) {
export async function sendApiRequest(userPrompt: UserPrompt[], activeFileParams: IActiveFileParams, orgID: string, apiToken: string, sessionID: string, entityName: string, entityColumns: string[], telemetry: ITelemetry, aibEndpoint: string | null, geoName: string | null, defaultPortalLanguageCode: string, crossGeoDataMovementEnabledPPACFlag = false) {

ritikramuka marked this conversation as resolved.
Show resolved Hide resolved
if (!aibEndpoint) {
return NetworkError;
Expand All @@ -40,6 +40,7 @@ export async function sendApiRequest(userPrompt: UserPrompt[], activeFileParams:
"targetColumns": entityColumns,
"clientType": clientType,
"clientVersion": clientVersion,
"defaultPortalLanguageCode": defaultPortalLanguageCode,
}
},
"crossGeoOptions": {
Expand Down
113 changes: 113 additions & 0 deletions src/common/copilot/Language/Utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import * as vscode from "vscode";
import { RequestInit } from "node-fetch";
import { findWebsiteYAML, parseYAMLAndFetchKey } from "../../utilities/Utils";
import { fetchJsonResponse, getCommonHeaders } from "../../services/AuthenticationProvider";
import { sendTelemetryEvent } from "../telemetry/copilotTelemetry";
import { CopilotGetLanguageCodeFailureEvent, CopilotGetLanguageCodeSuccessEvent } from "../telemetry/telemetryConstants";
import { ADX_LANGUAGECODE, ADX_WEBSITE_LANGUAGE } from "../constants";
import { ITelemetry } from "../../../client/telemetry/ITelemetry";
import { getDefaultLanguageCodeWeb } from "../../../web/client/utilities/fileAndEntityUtil";

declare const IS_DESKTOP: string | undefined;

export async function getDefaultLanguageCode(orgUrl:string, telemetry: ITelemetry, sessionID: string, dataverseToken: string) {
let defaultPortalLanguageCode = vscode.env.language;
if (IS_DESKTOP) {
const lcid = await fetchLanguageCodeId();
defaultPortalLanguageCode = await fetchLanguageCodeFromAPI(orgUrl, dataverseToken, telemetry, sessionID, lcid);
} else {
defaultPortalLanguageCode = getDefaultLanguageCodeWeb();
}
return defaultPortalLanguageCode;
}

export async function readWebsiteYAML(filePath: string): Promise<string | null> {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(
vscode.Uri.file(filePath)
);

if (workspaceFolder) {
const workspaceFolderPath = workspaceFolder.uri.fsPath;
return await findWebsiteYAML(filePath, workspaceFolderPath);
}

return null;
}

export async function fetchLanguageCodeId(): Promise<string> {
try {
let activeFilePath = "";
if (vscode.window.activeTextEditor) {
activeFilePath = vscode.window.activeTextEditor.document.uri.fsPath;
} else if (vscode.workspace.workspaceFolders?.length === 1) {
activeFilePath = vscode.workspace.workspaceFolders[0].uri.fsPath;
} else {
// Handle multiple workspace folders when no active text editor is present
return "";
}

const yamlContent = await readWebsiteYAML(activeFilePath);
if (yamlContent) {
const languageCodeId = parseYAMLAndFetchKey(yamlContent, ADX_WEBSITE_LANGUAGE);
return languageCodeId;
} else {
return "";
}
} catch (error) {
return "";
}
}

export async function fetchLanguageCodeFromAPI(
orgUrl: string,
apiToken: string,
telemetry: ITelemetry,
sessionID: string,
lcid: string
): Promise<string> {
try {
const dataverseApiUrl = `${
orgUrl.endsWith("/") ? orgUrl : orgUrl.concat("/")
}api/data/v9.2/adx_portallanguages`;

const requestOptions: RequestInit = {
method: "GET",
headers: getCommonHeaders(apiToken),
};

const startTime = performance.now();
const portalLanguagesResponse = await fetchJsonResponse(
dataverseApiUrl,
requestOptions
);
const endTime = performance.now();
const responseTime = endTime - startTime || 0;

const matchingLanguage = portalLanguagesResponse.value.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(language: any) => language.adx_lcid === parseInt(lcid, 10)
);

sendTelemetryEvent(telemetry, {
eventName: CopilotGetLanguageCodeSuccessEvent,
copilotSessionId: sessionID,
durationInMills: responseTime,
orgUrl: orgUrl,
});

return matchingLanguage?.[ADX_LANGUAGECODE] ?? vscode.env.language;
} catch (error) {
sendTelemetryEvent(telemetry, {
eventName: CopilotGetLanguageCodeFailureEvent,
copilotSessionId: sessionID,
error: error as Error,
orgUrl: orgUrl,
});
return vscode.env.language;
}
}
10 changes: 7 additions & 3 deletions src/common/copilot/PowerPagesCopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { orgChangeErrorEvent, orgChangeEvent } from "../OrgChangeNotifier";
import { getDisabledOrgList, getDisabledTenantList } from "./utils/copilotUtil";
import { INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID } from "../services/Constants";
import { ArtemisService } from "../services/ArtemisService";
import { getDefaultLanguageCode } from "./Language/Utils";

let intelligenceApiToken: string;
let userID: string; // Populated from PAC or intelligence API
Expand Down Expand Up @@ -361,11 +362,11 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider {
let metadataInfo = { entityName: '', formName: '' };
let componentInfo: string[] = [];

const dataverseToken = (await dataverseAuthentication(telemetry, activeOrgUrl, true)).accessToken;

if (activeFileParams.dataverseEntity == ADX_ENTITYFORM || activeFileParams.dataverseEntity == ADX_ENTITYLIST) {
metadataInfo = await getEntityName(telemetry, sessionID, activeFileParams.dataverseEntity);

const dataverseToken = (await dataverseAuthentication(telemetry, activeOrgUrl, true)).accessToken;

if (activeFileParams.dataverseEntity == ADX_ENTITYFORM) {
const formColumns = await getFormXml(metadataInfo.entityName, metadataInfo.formName, activeOrgUrl, dataverseToken, telemetry, sessionID);
componentInfo = formColumns;
Expand All @@ -375,7 +376,10 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider {
}

}
return sendApiRequest(data, activeFileParams, orgID, intelligenceApiToken, sessionID, metadataInfo.entityName, componentInfo, telemetry, this.aibEndpoint, this.geoName, this.crossGeoDataMovementEnabledPPACFlag);

const defaultPortalLanguageCode = await getDefaultLanguageCode(activeOrgUrl, this.telemetry, sessionID, dataverseToken);

return sendApiRequest(data, activeFileParams, orgID, intelligenceApiToken, sessionID, metadataInfo.entityName, componentInfo, telemetry, this.aibEndpoint, this.geoName, defaultPortalLanguageCode, this.crossGeoDataMovementEnabledPPACFlag);
})
.then(apiResponse => {
this.sendMessageToWebview({ type: 'apiResponse', value: apiResponse });
Expand Down
2 changes: 2 additions & 0 deletions src/common/copilot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const THUMBS_UP = 'thumbsUp';
export const THUMBS_DOWN = 'thumbsDown';
export const ADX_ENTITYFORM = "adx_entityform";
export const ADX_ENTITYLIST = "adx_entitylist";
export const ADX_LANGUAGECODE = "adx_languagecode";
export const ADX_WEBSITE_LANGUAGE = "adx_website_language";
export const ATTRIBUTE_DESCRIPTION = 'description';
export const ATTRIBUTE_DATAFIELD_NAME = 'datafieldname';
export const ATTRIBUTE_CLASSID = 'classid';
Expand Down
15 changes: 2 additions & 13 deletions src/common/copilot/dataverseMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import fetch, { RequestInit } from "node-fetch";
import { RequestInit } from "node-fetch";
import path from "path";
import * as vscode from "vscode";
import yaml from 'yaml';
Expand All @@ -14,6 +14,7 @@ import { getEntityMetadata } from "../../web/client/utilities/fileAndEntityUtil"
import { DOMParser } from "@xmldom/xmldom";
import { ATTRIBUTE_CLASSID, ATTRIBUTE_DATAFIELD_NAME, ATTRIBUTE_DESCRIPTION, ControlClassIdMap, SYSTEFORMS_API_PATH } from "./constants";
import { getUserAgent } from "../utilities/Utils";
import { fetchJsonResponse } from "../services/AuthenticationProvider";


declare const IS_DESKTOP: string | undefined;
Expand Down Expand Up @@ -79,17 +80,6 @@ export async function getFormXml(entityName: string, formName: string, orgUrl: s
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function fetchJsonResponse(url: string, requestInit: RequestInit): Promise<any> {
const response = await fetch(url, requestInit);

if (!response.ok) {
throw new Error(`Network request failed with status ${response.status}`);
}

return response.json();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getAttributesFromResponse(jsonResponse: any): string[] {
if (jsonResponse.Attributes && Array.isArray(jsonResponse.Attributes) && jsonResponse.Attributes.length > 0) {
Expand Down Expand Up @@ -219,4 +209,3 @@ function parseYamlContent(content: string, telemetry: ITelemetry, sessionID: str
return {};
}
}

2 changes: 2 additions & 0 deletions src/common/copilot/telemetry/telemetryConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ export const CopilotNotAvailableECSConfig = 'CopilotNotAvailableECSConfig';
export const CopilotExplainCode = 'CopilotExplainCode';
export const CopilotExplainCodeSize = 'CopilotExplainCodeSize';
export const CopilotNpsAuthenticationCompleted = "CopilotNpsAuthenticationCompleted";
export const CopilotGetLanguageCodeSuccessEvent = "CopilotGetLanguageCodeSuccessEvent";
export const CopilotGetLanguageCodeFailureEvent = "CopilotGetLanguageCodeFailureEvent";
12 changes: 12 additions & 0 deletions src/common/services/AuthenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import * as vscode from "vscode";
import fetch, { RequestInit } from "node-fetch";
import { showErrorDialog } from "../../web/client/common/errorHandler";
import { ITelemetry } from "../../client/telemetry/ITelemetry";
import { sendTelemetryEvent } from "../copilot/telemetry/copilotTelemetry";
Expand Down Expand Up @@ -53,6 +54,17 @@ export function getCommonHeaders(
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function fetchJsonResponse(url: string, requestInit: RequestInit): Promise<any> {
const response = await fetch(url, requestInit);

if (!response.ok) {
throw new Error(`Network request failed with status ${response.status}`);
}

return response.json();
}

//Get access token for Intelligence API service
export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessionID: string, orgId: string, firstTimeAuth = false): Promise<{ accessToken: string, user: string, userId: string }> {
let accessToken = '';
Expand Down
33 changes: 33 additions & 0 deletions src/common/utilities/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/

import * as vscode from "vscode";
import yaml from 'yaml';
import { EXTENSION_ID, EXTENSION_NAME, SETTINGS_EXPERIMENTAL_STORE_NAME } from "../../client/constants";
import { CUSTOM_TELEMETRY_FOR_POWER_PAGES_SETTING_NAME } from "../OneDSLoggerTelemetry/telemetryConstants";
import { PacWrapper } from "../../client/pac/PacWrapper";
import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, DataverseEntityNameMap, EntityFieldMap, FieldTypeMap, PAC_SUCCESS } from "../copilot/constants";
import { IActiveFileData, IActiveFileParams } from "../copilot/model";
import path from "path";

export function getSelectedCode(editor: vscode.TextEditor): string {
if (!editor) {
Expand Down Expand Up @@ -179,3 +181,34 @@ export function getActiveEditorContent(): IActiveFileData {

return activeFileData;
}

export async function findWebsiteYAML(
dir: string,
workspaceFolderPath: string
): Promise<string | null> {
const websiteYAMLFilePath = vscode.Uri.joinPath(vscode.Uri.file(dir), "website.yml").fsPath;

const diskRead = await import("fs");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we importing this fs here with an await?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

following the pattern used in the code. We had these multiple imports in block level scope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evaluate if this is really adding benefit in performance or is there a specific reason - if not lets keep this at start of file like other modules

try {
await diskRead.promises.access(websiteYAMLFilePath, diskRead.constants.F_OK);
} catch (error) {
const parentDir = path.dirname(dir);
if (parentDir === dir || !parentDir.startsWith(workspaceFolderPath)) {
return null;
}
return findWebsiteYAML(parentDir, workspaceFolderPath);
}

try {
const yamlContent = await diskRead.readFileSync(websiteYAMLFilePath, "utf8");
return yamlContent;
} catch (error) {
return null;
}
}

export async function parseYAMLAndFetchKey(yamlContent: string, key: string): Promise<string> {
const parsedYAML = yaml.parse(yamlContent);
return parsedYAML[key];
}
13 changes: 13 additions & 0 deletions src/web/client/utilities/fileAndEntityUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as vscode from "vscode";
import WebExtensionContext from "../WebExtensionContext";
import { IAttributePath } from "../common/interfaces";
import { SchemaEntityMetadata } from "../schema/constants";
import { queryParameters } from "../common/constants";

// File utility functions
export function fileHasDirtyChanges(fileFsPath: string) {
Expand Down Expand Up @@ -89,6 +90,18 @@ export function getFileName(fsPath: string) {
return fsPath.split(/[\\/]/).pop();
}

export function getDefaultLanguageCodeWeb() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should not be fetching languages on the fly in copilot using these functions - but as part of init call - check the way we are passing orgId, envId etc. That is the right pattern to use. Otherwise you will land in cross module dependency.

const lcid =
WebExtensionContext.websiteIdToLanguage.get(
WebExtensionContext.urlParametersMap.get(queryParameters.WEBSITE_ID) as string
) ?? "";

const defaultLanguageCode =
WebExtensionContext.languageIdCodeMap.get(lcid) ?? vscode.env.language;

return defaultLanguageCode;
}

// Entity utility functions
export function getEntityEtag(entityId: string) {
return WebExtensionContext.entityDataMap.getEntityMap.get(entityId)
Expand Down
Loading