Skip to content

Commit

Permalink
[@powerpages] GitHub copilot auth handling for AIB and PAC (#947)
Browse files Browse the repository at this point in the history
* chore: Update package.json with extension dependencies and enabled API proposals for chat participants

* feat: Add PowerPagesChatParticipant for chat functionality

* feat: Add logic to handle chat requests in PowerPagesChatParticipant

* feat: Removed logs

* TODO

* feat: Add logic to handle chat requests in PowerPagesChatParticipant(correct response format)

* chore: Remove unused code and update PowerPagesChatParticipant initialization

* chore: Update PowerPagesChatParticipant initialization and pac integration

* feat: Initialize organization details in PowerPagesChatParticipant

* chore: Remove unnecessary code and update OrgChangeNotifier initialization

* Fix lint warnings

* feat: Refactor PowerPagesChatParticipant class

The code changes refactor the PowerPagesChatParticipant class in the `PowerPagesChatParticipant.ts` file. The changes include:
- Fixing a typo in the `instance` property declaration
- Updating the constructor parameters to have consistent spacing
- Adding a comment to handle chat requests
- Removing a console.log statement
- Updating the `intializeOrgDetails` method to have consistent spacing
- Updating the `intializeOrgDetails` method to use destructuring assignment
- Updating the `intializeOrgDetails` method to update the `orgDetails` in the global state
- Removing unused code

Co-authored-by: amitjoshi <[email protected]>

* Added response for error scenarios

* Added & removed comments

* refactor: Update PowerPagesCopilot name in package.json

The code changes in the package.json file update the "name" property for the "powerpages" module to "Power Pages Copilot". This change reflects the updated name for the module.

Co-authored-by: amitjoshi <[email protected]>

* Code refactoring for utils and constants

* Enhanced pac auth handling

* refactor: Update PowerPagesChatParticipantConstants

The code changes in the `PowerPagesChatParticipantConstants.ts` file refactor the constants used in the PowerPages chat participant. The changes include:
- Cleaning up the code formatting

Co-authored-by: amitjoshi <[email protected]>

---------

Co-authored-by: amitjoshi <[email protected]>
Co-authored-by: tyaginidhi <[email protected]>
Co-authored-by: Nidhi Tyagi 🌟🐇🌴❄️ <[email protected]>
  • Loading branch information
4 people authored May 17, 2024
1 parent 5945ef7 commit 67dc648
Show file tree
Hide file tree
Showing 12 changed files with 952 additions and 748 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
{
"id": "powerpages",
"name": "powerpages",
"fullName": "Power Pages Copilot",
"description": "Power Pages Copilot",
"isSticky": true
}
Expand Down
6 changes: 1 addition & 5 deletions src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import { oneDSLoggerWrapper } from "../common/OneDSLoggerTelemetry/oneDSLoggerWr
import { OrgChangeNotifier, orgChangeEvent } from "../common/OrgChangeNotifier";
import { ActiveOrgOutput } from "./pac/PacTypes";
import { telemetryEventNames } from "./telemetry/TelemetryEventNames";
import { PowerPagesChatParticipant } from "../common/chat-participants/powerpages/PowerPagesChatParticipant";

let client: LanguageClient;
let _context: vscode.ExtensionContext;
Expand Down Expand Up @@ -178,9 +177,6 @@ export async function activate(
// Add CRUD related callback subscription here
await handleFileSystemCallbacks(_context, _telemetry);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const PowerPagesChatParticipantInstance = new PowerPagesChatParticipant(_context, _telemetry);

const cliContext = new CliAcquisitionContext(_context, _telemetry);
const cli = new CliAcquisition(cliContext);
const cliPath = await cli.ensureInstalled();
Expand Down Expand Up @@ -219,7 +215,7 @@ export async function activate(
oneDSLoggerWrapper.getLogger().traceError(exceptionError.name, exceptionError.message, exceptionError, { eventName: 'VscodeDesktopUsage' });
}
// Init OrgChangeNotifier instance
OrgChangeNotifier.createOrgChangeNotifierInstance(pacTerminal.getWrapper());
OrgChangeNotifier.createOrgChangeNotifierInstance(pacTerminal.getWrapper(), _context);

_telemetry.sendTelemetryEvent("PowerPagesWebsiteYmlExists"); // Capture's PowerPages Users
oneDSLoggerWrapper.getLogger().traceInfo("PowerPagesWebsiteYmlExists");
Expand Down
5 changes: 4 additions & 1 deletion src/client/lib/PacActivityBarUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AuthTreeView } from './AuthPanelView';
import { EnvAndSolutionTreeView } from './EnvAndSolutionTreeView';
import { PowerPagesCopilot } from '../../common/copilot/PowerPagesCopilot';
import { ITelemetry } from '../telemetry/ITelemetry';
import { PowerPagesChatParticipant } from '../../common/chat-participants/powerpages/PowerPagesChatParticipant';

export function RegisterPanels(pacWrapper: PacWrapper, context: vscode.ExtensionContext, telemetry: ITelemetry): vscode.Disposable[] {
const authPanel = new AuthTreeView(() => pacWrapper.authList(), pacWrapper);
Expand All @@ -20,11 +21,13 @@ export function RegisterPanels(pacWrapper: PacWrapper, context: vscode.Extension

const copilotPanel = new PowerPagesCopilot(context.extensionUri, context, telemetry, pacWrapper);

const powerPagesChatParticipant = PowerPagesChatParticipant.getInstance(context, telemetry, pacWrapper);

vscode.window.registerWebviewViewProvider('powerpages.copilot', copilotPanel, {
webviewOptions: {
retainContextWhenHidden: true,
},
});

return [authPanel, envAndSolutionPanel, copilotPanel];
return [authPanel, envAndSolutionPanel, copilotPanel, powerPagesChatParticipant];
}
11 changes: 7 additions & 4 deletions src/common/OrgChangeNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ export class OrgChangeNotifier {
private _pacWrapper: PacWrapper | undefined;
private _orgDetails: ActiveOrgOutput | undefined;
private static _orgChangeNotifierObj: OrgChangeNotifier | undefined;
private extensionContext: vscode.ExtensionContext;

private constructor(pacWrapper: PacWrapper) {
private constructor(pacWrapper: PacWrapper, extensionContext: vscode.ExtensionContext) {
this._pacWrapper = pacWrapper;
this.activeOrgDetails();
if (this._pacWrapper) {
this.setupFileWatcher();
}

this.extensionContext = extensionContext;
}

public static createOrgChangeNotifierInstance(pacWrapper: PacWrapper) {
public static createOrgChangeNotifierInstance(pacWrapper: PacWrapper, extensionContext: vscode.ExtensionContext) {
if (!OrgChangeNotifier._orgChangeNotifierObj) {
OrgChangeNotifier._orgChangeNotifierObj = new OrgChangeNotifier(pacWrapper);
OrgChangeNotifier._orgChangeNotifierObj = new OrgChangeNotifier(pacWrapper, extensionContext);
}
return OrgChangeNotifier._orgChangeNotifierObj;
}
Expand All @@ -53,4 +56,4 @@ export class OrgChangeNotifier {
orgChangeErrorEventEmitter.fire();
}
}
}
}
59 changes: 59 additions & 0 deletions src/common/OrgHandlerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { ExtensionContext } from 'vscode';
import { ActiveOrgOutput } from '../client/pac/PacTypes';
import { PacWrapper } from '../client/pac/PacWrapper';
import { OrgDetails } from './chat-participants/powerpages/PowerPagesChatParticipantTypes';
import { PAC_SUCCESS } from './copilot/constants';
import { createAuthProfileExp } from './Utils';

export const ORG_DETAILS_KEY = 'orgDetails';

export function handleOrgChangeSuccess(
orgDetails: ActiveOrgOutput,
extensionContext: ExtensionContext
): { orgID: string, orgUrl: string } {
const orgID = orgDetails.OrgId;
const orgUrl = orgDetails.OrgUrl;

extensionContext.globalState.update(ORG_DETAILS_KEY, { orgID, orgUrl });

//TODO: Handle AIB GEOs

return { orgID, orgUrl };
}

export async function initializeOrgDetails(
isOrgDetailsInitialized: boolean,
extensionContext: ExtensionContext,
pacWrapper?: PacWrapper
): Promise<{ orgID?: string, orgUrl?: string }> {
if (isOrgDetailsInitialized) {
return {};
}

const orgDetails: OrgDetails | undefined = extensionContext.globalState.get(ORG_DETAILS_KEY);
let orgID: string | undefined;
let orgUrl: string | undefined;

if (orgDetails && orgDetails.orgID && orgDetails.orgUrl) {
orgID = orgDetails.orgID;
orgUrl = orgDetails.orgUrl;
} else {
if (pacWrapper) {
const pacActiveOrg = await pacWrapper.activeOrg();
if (pacActiveOrg && pacActiveOrg.Status === PAC_SUCCESS) {
const orgDetails = handleOrgChangeSuccess(pacActiveOrg.Results, extensionContext);
orgID = orgDetails.orgID;
orgUrl = orgDetails.orgUrl;
} else {
await createAuthProfileExp(pacWrapper);
}
}
}

return { orgID, orgUrl };
}
4 changes: 3 additions & 1 deletion src/common/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export function showConnectedOrgMessage(environmentName: string, orgUrl: string)
export async function showInputBoxAndGetOrgUrl() {
return vscode.window.showInputBox({
placeHolder: vscode.l10n.t("Enter the environment URL"),
prompt: vscode.l10n.t("Active auth profile is not found or has expired. To create a new auth profile, enter the environment URL.")
prompt: vscode.l10n.t("Active auth profile is not found or has expired. To create a new auth profile, enter the environment URL."),
ignoreFocusOut: true, //Input box should not close on focus out

});
}

Expand Down
124 changes: 115 additions & 9 deletions src/common/chat-participants/powerpages/PowerPagesChatParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,130 @@ import { createChatParticipant } from '../ChatParticipantUtils';
import { IPowerPagesChatResult } from './PowerPagesChatParticipantTypes';
import { ITelemetry } from '../../../client/telemetry/ITelemetry';
import TelemetryReporter from '@vscode/extension-telemetry';

import { sendApiRequest } from '../../copilot/IntelligenceApiService';
import { PacWrapper } from '../../../client/pac/PacWrapper';
import { intelligenceAPIAuthentication } from '../../AuthenticationProvider';
import { ActiveOrgOutput } from '../../../client/pac/PacTypes';
import { orgChangeErrorEvent, orgChangeEvent } from '../../OrgChangeNotifier';
import { ORG_DETAILS_KEY, handleOrgChangeSuccess, initializeOrgDetails } from '../../OrgHandlerUtils';
import { getEndpoint } from './PowerPagesChatParticipantUtils';
import { AUTHENTICATION_FAILED_MSG, COPILOT_NOT_AVAILABLE_MSG, PAC_AUTH_NOT_FOUND, POWERPAGES_CHAT_PARTICIPANT_ID, RESPONSE_AWAITED_MSG } from './PowerPagesChatParticipantConstants';

export class PowerPagesChatParticipant {
private static instance: PowerPagesChatParticipant | null = null;
private chatParticipant: vscode.ChatParticipant;
private telemetry: ITelemetry;
private extensionContext: vscode.ExtensionContext;
private readonly _pacWrapper?: PacWrapper;
private isOrgDetailsInitialized = false;
private readonly _disposables: vscode.Disposable[] = [];
private cachedEndpoint: { intelligenceEndpoint: string, geoName: string } | null = null;

private orgID: string | undefined;
private orgUrl: string | undefined;

constructor(context: vscode.ExtensionContext, telemetry: ITelemetry | TelemetryReporter,) {
private constructor(context: vscode.ExtensionContext, telemetry: ITelemetry | TelemetryReporter, pacWrapper?: PacWrapper) {

this.chatParticipant = createChatParticipant('powerpages', this.handler);
this.chatParticipant = createChatParticipant(POWERPAGES_CHAT_PARTICIPANT_ID, this.handler);

//TODO: Check the icon image
this.chatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'src', 'common', 'chat-participants', 'powerpages', 'assets', 'copilot.png');

this.telemetry = telemetry;

this.extensionContext = context;

this._pacWrapper = pacWrapper;

this._disposables.push(orgChangeEvent(async (orgDetails: ActiveOrgOutput) => {
await this.handleOrgChangeSuccess(orgDetails);
}));

this._disposables.push(orgChangeErrorEvent(async () => {
this.extensionContext.globalState.update(ORG_DETAILS_KEY, { orgID: undefined, orgUrl: undefined});
}));

}

public static getInstance(context: vscode.ExtensionContext, telemetry: ITelemetry | TelemetryReporter, pacWrapper?: PacWrapper) {
if (!PowerPagesChatParticipant.instance) {
PowerPagesChatParticipant.instance = new PowerPagesChatParticipant(context, telemetry, pacWrapper);
}

return PowerPagesChatParticipant.instance;
}

public dispose() {
this.chatParticipant.dispose();
}

private handler: vscode.ChatRequestHandler = async (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_request: vscode.ChatRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: vscode.ChatRequest,
_context: vscode.ChatContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_stream: vscode.ChatResponseStream,
stream: vscode.ChatResponseStream,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_token: vscode.CancellationToken
): Promise<IPowerPagesChatResult> => {
// Handle chat requests here

// TODO: Handle authentication and org change
stream.progress(RESPONSE_AWAITED_MSG)

await this.initializeOrgDetails();

if (!this.orgID) {
stream.markdown(PAC_AUTH_NOT_FOUND);
return {
metadata: {
command: ''
}
};
}

const intelligenceApiAuthResponse = await intelligenceAPIAuthentication(this.telemetry, '', this.orgID, true);

if (!intelligenceApiAuthResponse) {
stream.markdown(AUTHENTICATION_FAILED_MSG);

return {
metadata: {
command: '',
}
};
}

const intelligenceApiToken = intelligenceApiAuthResponse.accessToken;

const { intelligenceEndpoint, geoName } = await getEndpoint(this.orgID, this.telemetry, this.cachedEndpoint);

if (!intelligenceEndpoint || !geoName) {
stream.markdown(COPILOT_NOT_AVAILABLE_MSG)

return {
metadata: {
command: ''
}
};
}

const userPrompt = request.prompt;

if (!userPrompt) {

//TODO: Show start message

return {
metadata: {
command: ''
}
};
}

//TODO: Handle form and list scenarios
const llmResponse = await sendApiRequest([{ displayText: userPrompt, code: '' }], { dataverseEntity: '', entityField: '', fieldType: '' }, this.orgID, intelligenceApiToken, '', '', [], this.telemetry, intelligenceEndpoint, geoName);

stream.markdown(llmResponse[0].displayText);

stream.markdown('\n```typescript\n' + llmResponse[0].code + '\n```');

return {
metadata: {
Expand All @@ -46,4 +141,15 @@ export class PowerPagesChatParticipant {

};

private async initializeOrgDetails(): Promise<void> {
const { orgID, orgUrl } = await initializeOrgDetails(this.isOrgDetailsInitialized, this.extensionContext, this._pacWrapper);
this.orgID = orgID;
this.orgUrl = orgUrl;
}

private async handleOrgChangeSuccess(orgDetails: ActiveOrgOutput): Promise<void> {
const { orgID, orgUrl } = handleOrgChangeSuccess(orgDetails, this.extensionContext);
this.orgID = orgID;
this.orgUrl = orgUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

export const POWERPAGES_CHAT_PARTICIPANT_ID = 'powerpages';
export const RESPONSE_AWAITED_MSG = 'Working on it...'
export const AUTHENTICATION_FAILED_MSG = 'Authentication failed. Please try again.';
export const COPILOT_NOT_AVAILABLE_MSG = 'Copilot is not available. Please contact your administrator.';
export const PAC_AUTH_NOT_FOUND = 'Active auth profile is not found or has expired. Please try again.';
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export interface IPowerPagesChatResult extends vscode.ChatResult {
command: string;
}
}
export interface OrgDetails {
orgID: string;
orgUrl: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { ITelemetry } from "../../../client/telemetry/ITelemetry";
import { getIntelligenceEndpoint } from "../../ArtemisService";

export async function getEndpoint(
orgID: string,
telemetry: ITelemetry,
cachedEndpoint: { intelligenceEndpoint: string; geoName: string } | null
): Promise<{ intelligenceEndpoint: string; geoName: string }> {
if (!cachedEndpoint) {
cachedEndpoint = await getIntelligenceEndpoint(orgID, telemetry, '') as { intelligenceEndpoint: string; geoName: string };
}
return cachedEndpoint;
}
Loading

0 comments on commit 67dc648

Please sign in to comment.