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

[@powerpages] GitHub copilot auth handling for AIB and PAC #947

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7944c2d
chore: Update package.json with extension dependencies and enabled AP…
May 1, 2024
b49bccc
feat: Add PowerPagesChatParticipant for chat functionality
May 1, 2024
989334b
feat: Add logic to handle chat requests in PowerPagesChatParticipant
May 2, 2024
ff8886c
feat: Removed logs
May 3, 2024
945d5c1
TODO
May 3, 2024
5357ac8
feat: Add logic to handle chat requests in PowerPagesChatParticipant(…
May 3, 2024
663df06
chore: Remove unused code and update PowerPagesChatParticipant initia…
May 6, 2024
26f570c
Merge branch 'main' of https://github.com/microsoft/powerplatform-vsc…
May 6, 2024
844da4f
chore: Update PowerPagesChatParticipant initialization and pac integr…
May 6, 2024
280c6ef
feat: Initialize organization details in PowerPagesChatParticipant
May 7, 2024
830a69e
chore: Remove unnecessary code and update OrgChangeNotifier initializ…
May 7, 2024
c983503
Fix lint warnings
May 10, 2024
2e2320d
Merge branch 'features/PowerPagesAgent' into user/amitjoshi/githubCop…
tyaginidhi May 10, 2024
18a2d03
Merge branch 'features/PowerPagesAgent' into user/amitjoshi/githubCop…
tyaginidhi May 13, 2024
eebcaa6
Merge branch 'features/PowerPagesAgent' into user/amitjoshi/githubCop…
May 14, 2024
258faa0
feat: Refactor PowerPagesChatParticipant class
May 14, 2024
e005e50
Merge branch 'user/amitjoshi/githubCopilotAuthHandling' of https://gi…
May 14, 2024
491cee2
Added response for error scenarios
May 14, 2024
6fd829c
Added & removed comments
May 14, 2024
3d04fd4
refactor: Update PowerPagesCopilot name in package.json
May 15, 2024
b653dd4
Code refactoring for utils and constants
May 15, 2024
04f855e
Enhanced pac auth handling
May 16, 2024
a9a22f4
refactor: Update PowerPagesChatParticipantConstants
May 17, 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
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;
}
amitjoshi438 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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';

amitjoshi438 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading