Skip to content

Commit c433add

Browse files
Starter integration with Azure agent extension (#3979)
Some initial work on integrating the Azure Functions extension with the Azure Agent extension. - Add `agentIntegration.ts` which serves as the initial entry point for where the integration lives. Contains the implementations of the various `package.json.agentMetadata` commands. - Expose create function app as a `WizardCommandConfig`, which also includes: - Adding `ui: IAzureAgentInput;` on the `IFunctionAppWizardContext` so the associated wizard step is forced by the compiler to use `IAzureAgentInput` related types. - Switching to `IAzureAgentInput` related types in `FunctionAppStackStep.ts` - Switching to `IAzureAgentInput` related types in `getStackPicks.ts` - Using the new option on `SiteNameStep` to give the step appropriate agent metadata information for its UI prompts - Appropriately using the new `skipExecute` option when creating the `AzureWizard` for the command - Appropriately setting the `showLoadingPrompt` option when creating the `AzureWizard` for the command - Expose create function project and deploy to function app as `SimpleCommandConfig`s (quick way to get them exposed via agent, ideally they should be exposed as `WizardCommandConfig`s instead, what lucky dev will get to do that work? 🙃) - Write some sample benchmarks for Azure Functions extension related functionality (note: there's no verification of buttons because buttons are in a messed up state right now, please don't ask).
1 parent 154599c commit c433add

File tree

9 files changed

+559
-200
lines changed

9 files changed

+559
-200
lines changed

package-lock.json

Lines changed: 370 additions & 179 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,10 +1154,11 @@
11541154
"@azure/core-client": "^1.7.3",
11551155
"@azure/core-rest-pipeline": "^1.11.0",
11561156
"@azure/storage-blob": "^12.5.0",
1157-
"@microsoft/vscode-azext-azureappservice": "^2.3.6",
1157+
"@microsoft/vscode-azext-azureappservice": "^3.1.0",
11581158
"@microsoft/vscode-azext-azureappsettings": "^0.2.0",
1159-
"@microsoft/vscode-azext-azureutils": "2.0.2",
1160-
"@microsoft/vscode-azext-utils": "^2.1.0",
1159+
"@microsoft/vscode-azext-azureutils": "^3.0.0",
1160+
"@microsoft/vscode-azext-serviceconnector": "^0.1.3",
1161+
"@microsoft/vscode-azext-utils": "^2.3.1",
11611162
"@microsoft/vscode-azureresources-api": "^2.0.4",
11621163
"cross-fetch": "^4.0.0",
11631164
"escape-string-regexp": "^4.0.0",
@@ -1176,5 +1177,12 @@
11761177
},
11771178
"extensionDependencies": [
11781179
"ms-azuretools.vscode-azureresourcegroups"
1179-
]
1180+
],
1181+
"agentMetadata": {
1182+
"version": "1.0",
1183+
"getCommandsCommandId": "azureFunctions.agent.getCommands",
1184+
"runWizardCommandWithoutExecutionCommandId": "azureFunctions.agent.runWizardCommandWithoutExecution",
1185+
"runWizardCommandWithInputsCommandId": "azureFunctions.agent.runWizardCommandWithInputs",
1186+
"getAgentBenchmarkConfigsCommandId": "azureFunctions.agent.getAgentBenchmarkConfigs"
1187+
}
11801188
}

src/agent/agentIntegration.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzExtUserInputWithInputQueue, callWithTelemetryAndErrorHandling, type AgentBenchmarkConfig, type AzureUserInputQueue, type IAzureUserInput, type SimpleCommandConfig, type WizardCommandConfig } from '@microsoft/vscode-azext-utils';
7+
import { createFunctionApp } from '../commands/createFunctionApp/createFunctionApp';
8+
9+
const createFunctionAppCommandName = "createFunctionApp";
10+
const createFunctionProjectCommandName = "createFunctionProject";
11+
const deployToFunctionAppCommandName = "deployToFunctionApp";
12+
13+
export async function getCommands(): Promise<(WizardCommandConfig | SimpleCommandConfig)[]> {
14+
return [
15+
{
16+
type: "wizard",
17+
name: createFunctionAppCommandName,
18+
commandId: "azureFunctions.createFunctionApp",
19+
displayName: "Create Function App",
20+
intentDescription: "This is best when users ask to create a Function App resource in Azure. They may refer to a Function App as 'Function App', 'function', 'function resource', 'function app resource', 'function app' etc. This command is not useful if the user is asking how to do something, or if something is possible.",
21+
requiresAzureLogin: true,
22+
},
23+
{
24+
type: "simple",
25+
name: createFunctionProjectCommandName,
26+
commandId: "azureFunctions.createNewProject",
27+
displayName: "Create Function Project",
28+
intentDescription: "This is best when users ask to create a new function project in VS Code. They may also refer to creating a function project by asking to create a project based upon a function project template.",
29+
requiresAzureLogin: true,
30+
},
31+
{
32+
type: "simple",
33+
name: deployToFunctionAppCommandName,
34+
commandId: "azureFunctions.deploy",
35+
displayName: "Deploy to Function App",
36+
intentDescription: "This is best when users ask to create a deploy their function project or their code to a function app. They may refer to a Function App as 'Function App', 'function', 'function resource', 'function app resource', 'function app' etc. This is not best if they ask to deploy to a slot.",
37+
requiresAzureLogin: true,
38+
},
39+
];
40+
}
41+
42+
export async function runWizardCommandWithoutExecution(command: WizardCommandConfig, ui: IAzureUserInput): Promise<void> {
43+
if (command.commandId === 'azureFunctions.createFunctionApp') {
44+
await callWithTelemetryAndErrorHandling('azureFunctions.createFunctionAppViaAgent', async (context) => {
45+
return await createFunctionApp({ ...context, ui: ui, skipExecute: true });
46+
});
47+
} else {
48+
throw new Error('Unknown command: ' + command.commandId);
49+
}
50+
}
51+
52+
export async function runWizardCommandWithInputs(command: WizardCommandConfig, inputsQueue: AzureUserInputQueue): Promise<void> {
53+
if (command.commandId === 'azureFunctions.createFunctionApp') {
54+
await callWithTelemetryAndErrorHandling('azureFunctions.createFunctionAppViaAgent', async (context) => {
55+
const azureUserInput = new AzExtUserInputWithInputQueue(context, inputsQueue);
56+
return await createFunctionApp({ ...context, ui: azureUserInput });
57+
});
58+
} else {
59+
throw new Error('Unknown command: ' + command.commandId);
60+
}
61+
}
62+
63+
export function getAgentBenchmarkConfigs(): AgentBenchmarkConfig[] {
64+
return [
65+
{
66+
name: "Learn 1 - How to run code on blob change",
67+
prompt: "How can I use azure functions to run code whenever a blob is changed?",
68+
acceptableHandlerChains: [
69+
["functions", "learn"],
70+
],
71+
},
72+
{
73+
name: "Learn 2 - Functions vs WebApps",
74+
prompt: "How do I write a lambda in Azure?",
75+
acceptableHandlerChains: [
76+
["functions", "learn"],
77+
],
78+
},
79+
{
80+
name: "Create Function App 1",
81+
prompt: "I want to create a function app",
82+
acceptableHandlerChains: [
83+
["functions", createFunctionAppCommandName],
84+
],
85+
},
86+
{
87+
name: "Create Function App 2",
88+
prompt: "I want to create a func app",
89+
acceptableHandlerChains: [
90+
["functions", createFunctionAppCommandName],
91+
],
92+
},
93+
{
94+
name: "Create Function App 3",
95+
prompt: "I want to create a resource that lets me run serverless code",
96+
acceptableHandlerChains: [
97+
["functions", createFunctionAppCommandName],
98+
],
99+
},
100+
{
101+
name: "Create Function App 4",
102+
prompt: "create func app",
103+
acceptableHandlerChains: [
104+
["functions", createFunctionAppCommandName],
105+
],
106+
},
107+
{
108+
name: "Create Function App 5",
109+
prompt: "func app create",
110+
acceptableHandlerChains: [
111+
["functions", createFunctionAppCommandName],
112+
],
113+
},
114+
{
115+
name: "Create Function App 5",
116+
prompt: "Hi, I would like you to create for me a very specific type of resource, that of course being an azure function app. Thank you!",
117+
acceptableHandlerChains: [
118+
["functions", createFunctionAppCommandName],
119+
],
120+
},
121+
{
122+
name: "Create Function App 6",
123+
prompt: "I want to be able to run code, but not pay for compute usage when my code isn't running, can you create a resource that helps me do that?",
124+
acceptableHandlerChains: [
125+
["functions", createFunctionAppCommandName],
126+
],
127+
}
128+
];
129+
}

src/commands/createFunctionApp/IFunctionAppWizardContext.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice';
7-
import { type ExecuteActivityContext, type ICreateChildImplContext } from '@microsoft/vscode-azext-utils';
7+
import { type ExecuteActivityContext, type IAzureAgentInput, type ICreateChildImplContext } from '@microsoft/vscode-azext-utils';
88
import { type FuncVersion } from '../../FuncVersion';
99
import { type DurableBackendValues } from '../../constants';
1010
import { type AppStackMajorVersion, type AppStackMinorVersion } from './stacks/models/AppStackModel';
@@ -28,4 +28,6 @@ export interface IFunctionAppWizardContext extends IAppServiceWizardContext, ICr
2828
hasAzureStorageConnection?: boolean;
2929
hasEventHubsConnection?: boolean;
3030
hasSqlDbConnection?: boolean;
31+
32+
ui: IAzureAgentInput;
3133
}

src/commands/createFunctionApp/createFunctionApp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ export async function createFunctionApp(context: IActionContext & Partial<ICreat
4242
return funcAppNode.fullId;
4343
}
4444

45-
export async function createFunctionAppAdvanced(context: IActionContext, subscription?: AzExtParentTreeItem | string, nodesOrNewResourceGroupName?: string | (string | AzExtParentTreeItem)[]): Promise<string> {
45+
export async function createFunctionAppAdvanced(context: IActionContext, subscription?: AzExtParentTreeItem | string, nodesOrNewResourceGroupName?: string | (string | AzExtParentTreeItem)[]): Promise<string | undefined> {
4646
return await createFunctionApp({ ...context, advancedCreation: true }, subscription, nodesOrNewResourceGroupName);
4747
}

src/commands/createFunctionApp/stacks/FunctionAppStackStep.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { setLocationsTask, SiteOSStep, WebsiteOS } from '@microsoft/vscode-azext-azureappservice';
7-
import { AzureWizardPromptStep, openUrl, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
7+
import { AzureWizardPromptStep, openUrl, type AgentQuickPickItem, type AgentQuickPickOptions, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
88
import { noRuntimeStacksAvailableLabel } from '../../../constants';
99
import { getMajorVersion, promptForFuncVersion } from '../../../FuncVersion';
1010
import { localize } from '../../../localize';
@@ -18,7 +18,16 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
1818

1919
let result: FullFunctionAppStack | undefined;
2020
while (true) {
21-
result = (await context.ui.showQuickPick(this.getPicks(context), { placeHolder, enableGrouping: true })).data;
21+
const options: AgentQuickPickOptions = {
22+
placeHolder,
23+
enableGrouping: true,
24+
agentMetadata: {
25+
parameterDisplayTitle: 'Runtime Stack',
26+
parameterDisplayDescription: 'The runtime stack to use for the function app.'
27+
}
28+
};
29+
const picks = this.getPicks(context);
30+
result = (await context.ui.showQuickPick(picks, options)).data;
2231
if (!result) {
2332
context.version = await promptForFuncVersion(context);
2433
} else {
@@ -53,8 +62,8 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
5362
return { promptSteps };
5463
}
5564

56-
private async getPicks(context: IFunctionAppWizardContext): Promise<IAzureQuickPickItem<FullFunctionAppStack | undefined>[]> {
57-
let picks: IAzureQuickPickItem<FullFunctionAppStack | undefined>[] = await getStackPicks(context);
65+
private async getPicks(context: IFunctionAppWizardContext): Promise<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[]> {
66+
let picks: AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[] = await getStackPicks(context);
5867
if (picks.filter(p => p.label !== noRuntimeStacksAvailableLabel).length === 0) {
5968
// if every runtime only has noRuntimeStackAvailable quickpick items, reset picks to []
6069
picks = [];
@@ -72,15 +81,17 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
7281
data: undefined,
7382
onPicked: async () => {
7483
await openUrl('https://aka.ms/function-runtime-host-warning');
75-
}
84+
},
85+
agentMetadata: { notApplicableToAgentPick: true }
7686
})
7787
}
7888

7989
picks.push({
8090
label: localize('changeFuncVersion', '$(gear) Change Azure Functions version'),
8191
description: localize('currentFuncVersion', 'Current: {0}', context.version) + (isEol ? ' $(warning)' : ''),
8292
data: undefined,
83-
suppressPersistence: true
93+
suppressPersistence: true,
94+
agentMetadata: { notApplicableToAgentPick: true }
8495
});
8596

8697
return picks;

src/commands/createFunctionApp/stacks/getStackPicks.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { type ServiceClient } from '@azure/core-client';
77
import { createPipelineRequest } from '@azure/core-rest-pipeline';
88
import { createGenericClient, type AzExtPipelineResponse } from '@microsoft/vscode-azext-azureutils';
9-
import { openUrl, parseError, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
9+
import { openUrl, parseError, type AgentQuickPickItem, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
1010
import { funcVersionLink } from '../../../FuncVersion';
1111
import { hiddenStacksSetting, noRuntimeStacksAvailableLabel } from '../../../constants';
1212
import { previewDescription } from '../../../constants-nls';
@@ -18,9 +18,9 @@ import { backupStacks } from './backupStacks';
1818
import { type AppStackMinorVersion } from './models/AppStackModel';
1919
import { type FunctionAppRuntimes, type FunctionAppStack } from './models/FunctionAppStackModel';
2020

21-
export async function getStackPicks(context: IFunctionAppWizardContext): Promise<IAzureQuickPickItem<FullFunctionAppStack | undefined>[]> {
21+
export async function getStackPicks(context: IFunctionAppWizardContext): Promise<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[]> {
2222
const stacks: FunctionAppStack[] = (await getStacks(context)).filter(s => !context.stackFilter || context.stackFilter === s.value);
23-
const picks: IAzureQuickPickItem<FullFunctionAppStack | undefined>[] = [];
23+
const picks: AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[] = [];
2424
let hasEndOfLife = false;
2525
let stackHasPicks: boolean;
2626

@@ -77,7 +77,8 @@ export async function getStackPicks(context: IFunctionAppWizardContext): Promise
7777
label: minorVersion.displayText,
7878
description,
7979
group: stack.displayText,
80-
data: { stack, majorVersion, minorVersion }
80+
data: { stack, majorVersion, minorVersion },
81+
agentMetadata: {}
8182
});
8283
stackHasPicks = true;
8384
}
@@ -87,7 +88,8 @@ export async function getStackPicks(context: IFunctionAppWizardContext): Promise
8788
picks.push({
8889
label: noRuntimeStacksAvailableLabel,
8990
group: stack.displayText,
90-
data: undefined
91+
data: undefined,
92+
agentMetadata: { notApplicableToAgentPick: true }
9193
});
9294
}
9395
}
@@ -102,12 +104,13 @@ export async function getStackPicks(context: IFunctionAppWizardContext): Promise
102104
getPriority(p1.data.minorVersion.stackSettings) - getPriority(p2.data.minorVersion.stackSettings); // otherwise sort based on priority
103105
});
104106
if (hasEndOfLife) {
105-
(picks as IAzureQuickPickItem<FullFunctionAppStack | undefined>[]).push({
107+
picks.push({
106108
label: localize('endOfLife', `$(extensions-warning-message) Some stacks have an end of support deadline coming up. Learn more...`),
107109
onPicked: async () => {
108110
await openUrl(funcVersionLink);
109111
},
110-
data: undefined
112+
data: undefined,
113+
agentMetadata: { notApplicableToAgentPick: true }
111114
});
112115
}
113116
return picks;

src/commands/registerCommands.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { registerSiteCommand } from '@microsoft/vscode-azext-azureappservice';
77
import { AppSettingTreeItem, AppSettingsTreeItem } from '@microsoft/vscode-azext-azureappsettings';
88
import { registerCommand, registerCommandWithTreeNodeUnwrapping, unwrapTreeNodeCommandCallback, type AzExtParentTreeItem, type AzExtTreeItem, type IActionContext } from '@microsoft/vscode-azext-utils';
99
import { commands } from "vscode";
10+
import { getAgentBenchmarkConfigs, getCommands, runWizardCommandWithInputs, runWizardCommandWithoutExecution } from '../agent/agentIntegration';
1011
import { ext } from '../extensionVariables';
1112
import { installOrUpdateFuncCoreTools } from '../funcCoreTools/installOrUpdateFuncCoreTools';
1213
import { uninstallFuncCoreTools } from '../funcCoreTools/uninstallFuncCoreTools';
@@ -54,6 +55,12 @@ import { disableFunction, enableFunction } from './updateDisabledState';
5455
import { viewProperties } from './viewProperties';
5556

5657
export function registerCommands(): void {
58+
59+
commands.registerCommand('azureFunctions.agent.getCommands', getCommands);
60+
commands.registerCommand('azureFunctions.agent.runWizardCommandWithoutExecution', runWizardCommandWithoutExecution);
61+
commands.registerCommand('azureFunctions.agent.runWizardCommandWithInputs', runWizardCommandWithInputs);
62+
commands.registerCommand('azureFunctions.agent.getAgentBenchmarkConfigs', getAgentBenchmarkConfigs);
63+
5764
registerCommandWithTreeNodeUnwrapping('azureFunctions.addBinding', addBinding);
5865
registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.add', async (context: IActionContext, node?: AzExtParentTreeItem) => await createChildNode(context, new RegExp(AppSettingsTreeItem.contextValue), node));
5966
registerCommandWithTreeNodeUnwrapping('azureFunctions.appSettings.decrypt', decryptLocalSettings);

src/tree/SubscriptionTreeItem.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { isProjectCV, isRemoteProjectCV } from './projectContextValues';
2929
export interface ICreateFunctionAppContext extends ICreateChildImplContext {
3030
newResourceGroupName?: string;
3131
workspaceFolder?: WorkspaceFolder;
32+
skipExecute?: boolean;
3233
}
3334

3435
export class SubscriptionTreeItem extends SubscriptionTreeItemBase {
@@ -97,7 +98,7 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase {
9798

9899
const promptSteps: AzureWizardPromptStep<IAppServiceWizardContext>[] = [];
99100
const executeSteps: AzureWizardExecuteStep<IAppServiceWizardContext>[] = [];
100-
promptSteps.push(new SiteNameStep());
101+
promptSteps.push(new SiteNameStep("functionApp"));
101102
promptSteps.push(new FunctionAppStackStep());
102103

103104
const storageAccountCreateOptions: INewStorageAccountDefaults = {
@@ -147,7 +148,14 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase {
147148
executeSteps.push(new FunctionAppCreateStep());
148149

149150
const title: string = localize('functionAppCreatingTitle', 'Create new Function App in Azure');
150-
const wizard: AzureWizard<IAppServiceWizardContext> = new AzureWizard(wizardContext, { promptSteps, executeSteps, title });
151+
152+
const wizard: AzureWizard<IAppServiceWizardContext> = new AzureWizard(wizardContext, {
153+
promptSteps,
154+
executeSteps,
155+
title,
156+
showLoadingPrompt: context.skipExecute !== true,
157+
skipExecute: context.skipExecute === true
158+
});
151159

152160
await wizard.prompt();
153161
// if the providers aren't registered yet, await it here because it is required by this point

0 commit comments

Comments
 (0)