Skip to content

Commit

Permalink
feat: let user input OpenAI keys in F5 (#13130)
Browse files Browse the repository at this point in the history
* feat: let user input OpenAI keys in F5
  • Loading branch information
nliu-ms authored Feb 10, 2025
1 parent c375400 commit 8e68bc3
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 7 deletions.
3 changes: 3 additions & 0 deletions packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,9 @@
"driver.prerequisite.summary.testTool.installed": "Teams App Test Tool is installed.",
"driver.file.createOrUpdateEnvironmentFile.description": "Create or update variables to env file.",
"driver.file.createOrUpdateEnvironmentFile.summary": "Variables have been generated successfully to %s.",
"driver.file.createOrUpdateEnvironmentFile.OpenAIKey.validation": "OpenAI key cannot be empty.",
"driver.file.createOrUpdateEnvironmentFile.OpenAIDeploymentEndpoint.validation": "Azure OpenAI endpoint must be a valid URL.",
"driver.file.createOrUpdateEnvironmentFile.OpenAIDeploymentName.validation": "Azure OpenAI deployment name cannot be empty.",
"driver.file.createOrUpdateJsonFile.description": "Create or update JSON file.",
"driver.file.createOrUpdateJsonFile.summary": "Json file has been successfully generated to %s.",
"driver.file.progressBar.appsettings": "Generating json file...",
Expand Down
11 changes: 11 additions & 0 deletions packages/fx-core/src/component/configManager/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "./interface";
import { MissingEnvironmentVariablesError } from "../../error";
import { setErrorContext } from "../../common/globalVars";
import { OpenAIEnvironmentVariables } from "../constants";

function resolveDriverDef(
def: DriverDefinition,
Expand Down Expand Up @@ -100,6 +101,16 @@ export function resolveString(
resolved.push(envVar);
newVal = newVal.replace(matches[0], envVal);
}
} else if (
envVar === OpenAIEnvironmentVariables.SECRET_AZURE_OPENAI_API_KEY ||
envVar === OpenAIEnvironmentVariables.AZURE_OPENAI_ENDPOINT ||
envVar === OpenAIEnvironmentVariables.AZURE_OPENAI_DEPLOYMENT_NAME ||
envVar === OpenAIEnvironmentVariables.SECRET_OPENAI_API_KEY
) {
if (envVal) {
resolved.push(envVar);
newVal = newVal.replace(matches[0], envVal);
}
} else {
if (!envVal) {
unresolved.push(envVar);
Expand Down
9 changes: 9 additions & 0 deletions packages/fx-core/src/component/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,12 @@ export const KiotaLastCommands = {
createDeclarativeCopilotWithManifest: "createDeclarativeCopilotWithManifest",
addPlugin: "addPlugin",
};

export const OpenAIEnvironmentVariables = {
OPENAI_API_KEY: "OPENAI_API_KEY",
AZURE_OPENAI_API_KEY: "AZURE_OPENAI_API_KEY",
SECRET_OPENAI_API_KEY: "SECRET_OPENAI_API_KEY",
SECRET_AZURE_OPENAI_API_KEY: "SECRET_AZURE_OPENAI_API_KEY",
AZURE_OPENAI_ENDPOINT: "AZURE_OPENAI_ENDPOINT",
AZURE_OPENAI_DEPLOYMENT_NAME: "AZURE_OPENAI_DEPLOYMENT_NAME",
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as os from "os";
import * as path from "path";
import { Service } from "typedi";
import { hooks } from "@feathersjs/hooks/lib";
import { FxError, Result, SystemError, UserError } from "@microsoft/teamsfx-api";
import { FxError, Result, SystemError, UserError, Void, ok } from "@microsoft/teamsfx-api";
import { getLocalizedString } from "../../../common/localizeUtils";
import { InvalidActionInputError, assembleError } from "../../../error/common";
import { wrapRun } from "../../utils/common";
Expand All @@ -17,6 +17,13 @@ import { ExecutionResult, StepDriver } from "../interface/stepDriver";
import { addStartAndEndTelemetry } from "../middleware/addStartAndEndTelemetry";
import { GenerateEnvArgs } from "./interface/generateEnvArgs";
import { pathUtils } from "../../utils/pathUtils";
import { OpenAIEnvironmentVariables } from "../../constants";
import {
azureOpenAIKeyQuestion,
azureOpenAIDeploymentNameQuestion,
azureOpenAIEndpointQuestion,
openAIKeyQuestion,
} from "../../../question";

const actionName = "file/createOrUpdateEnvironmentFile";
const helpLink = "https://aka.ms/teamsfx-actions/file-createOrUpdateEnvironmentFile";
Expand Down Expand Up @@ -64,12 +71,16 @@ export class CreateOrUpdateEnvironmentFileDriver implements StepDriver {
await fs.ensureFile(target);
const envs = dotenv.parse(await fs.readFile(target));
context.logProvider?.debug(`Existing envs: ${JSON.stringify(envs)}`);
const map = new Map<string, string>();
const res = await this.askForOpenAIEnvironmentVariables(context, args, map);
if (res.isErr()) {
throw res.error;
}
const updatedEnvs = Object.entries({ ...envs, ...args.envs }).map(
([key, value]) => `${key}=${value}`
);
context.logProvider?.debug(`Updated envs: ${JSON.stringify(updatedEnvs)}`);
await fs.writeFile(target, updatedEnvs.join(os.EOL));
const map = new Map<string, string>();
const envFilePathRes = await pathUtils.getEnvFilePath(
context.projectPath,
process.env.TEAMSFX_ENV || "dev"
Expand Down Expand Up @@ -106,6 +117,120 @@ export class CreateOrUpdateEnvironmentFileDriver implements StepDriver {
}
}

/**
* Pop up input text to input OpenAI environment variables, or return UserCancel error.
* @param ctx
* @param args The arguments passed to the driver.
* @param envOutput Used to store the resolved environment variables, which will be written to the environment file.
* @returns
*/
async askForOpenAIEnvironmentVariables(
ctx: DriverContext,
args: GenerateEnvArgs,
envOutput: Map<string, string>
): Promise<Result<Void, FxError>> {
const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g;
if (args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_API_KEY]) {
const matches = placeHolderReg.exec(
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_API_KEY]
);
if (matches != null && matches.length > 1) {
const result = await ctx.ui!.inputText({
name: azureOpenAIKeyQuestion().name,
title: azureOpenAIKeyQuestion().title as string,
password: azureOpenAIKeyQuestion().password,
validation: (input: string): string | undefined => {
if (input.length < 1) {
return getLocalizedString(
"driver.file.createOrUpdateEnvironmentFile.OpenAIKey.validation"
);
}
},
});
if (result.isErr()) {
return result;
} else {
envOutput.set(matches[1], result.value.result!);
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_API_KEY] = result.value.result!;
}
}
}

if (args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_ENDPOINT]) {
const matches = placeHolderReg.exec(
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_ENDPOINT]
);
if (matches != null && matches.length > 1) {
const result = await ctx.ui!.inputText({
name: azureOpenAIEndpointQuestion().name,
title: azureOpenAIEndpointQuestion().title as string,
validation: (input: string): string | undefined => {
if (!input.startsWith("https://") && !input.startsWith("http://")) {
return getLocalizedString(
"driver.file.createOrUpdateEnvironmentFile.OpenAIDeploymentEndpoint.validation"
);
}
},
});
if (result.isErr()) {
return result;
} else {
envOutput.set(matches[1], result.value.result!);
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_ENDPOINT] = result.value.result!;
}
}
}

if (args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_DEPLOYMENT_NAME]) {
const matches = placeHolderReg.exec(
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_DEPLOYMENT_NAME]
);
if (matches != null && matches.length > 1) {
const result = await ctx.ui!.inputText({
name: azureOpenAIDeploymentNameQuestion().name,
title: azureOpenAIDeploymentNameQuestion().title as string,
validation: (input: string): string | undefined => {
if (input.length < 1) {
return getLocalizedString(
"driver.file.createOrUpdateEnvironmentFile.OpenAIDeploymentName.validation"
);
}
},
});
if (result.isErr()) {
return result;
} else {
envOutput.set(matches[1], result.value.result!);
args.envs[OpenAIEnvironmentVariables.AZURE_OPENAI_DEPLOYMENT_NAME] = result.value.result!;
}
}
}

if (args.envs[OpenAIEnvironmentVariables.OPENAI_API_KEY]) {
const matches = placeHolderReg.exec(args.envs[OpenAIEnvironmentVariables.OPENAI_API_KEY]);
if (matches != null && matches.length > 1) {
const result = await ctx.ui!.inputText({
name: openAIKeyQuestion().name,
title: openAIKeyQuestion().title as string,
validation: (input: string): string | undefined => {
if (input.length < 1) {
return getLocalizedString(
"driver.file.createOrUpdateEnvironmentFile.OpenAIKey.validation"
);
}
},
});
if (result.isErr()) {
return result;
} else {
envOutput.set(matches[1], result.value.result!);
args.envs[OpenAIEnvironmentVariables.OPENAI_API_KEY] = result.value.result!;
}
}
}
return ok(Void);
}

private validateArgs(args: GenerateEnvArgs): void {
const invalidParameters: string[] = [];
if (!args.target || typeof args.target !== "string" || args.target.length === 0) {
Expand Down
8 changes: 4 additions & 4 deletions packages/fx-core/src/question/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ function llmServiceQuestion(): SingleSelectQuestion {
};
}

function openAIKeyQuestion(): TextInputQuestion {
export function openAIKeyQuestion(): TextInputQuestion {
return {
type: "text",
password: true,
Expand All @@ -1157,7 +1157,7 @@ function openAIKeyQuestion(): TextInputQuestion {
};
}

function azureOpenAIKeyQuestion(): TextInputQuestion {
export function azureOpenAIKeyQuestion(): TextInputQuestion {
return {
type: "text",
password: true,
Expand All @@ -1169,7 +1169,7 @@ function azureOpenAIKeyQuestion(): TextInputQuestion {
};
}

function azureOpenAIEndpointQuestion(): TextInputQuestion {
export function azureOpenAIEndpointQuestion(): TextInputQuestion {
return {
type: "text",
name: QuestionNames.AzureOpenAIEndpoint,
Expand All @@ -1180,7 +1180,7 @@ function azureOpenAIEndpointQuestion(): TextInputQuestion {
};
}

function azureOpenAIDeploymentNameQuestion(): TextInputQuestion {
export function azureOpenAIDeploymentNameQuestion(): TextInputQuestion {
return {
type: "text",
name: QuestionNames.AzureOpenAIDeploymentName,
Expand Down
27 changes: 27 additions & 0 deletions packages/fx-core/tests/component/configManager/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ describe("v3 lifecyle", () => {

describe("when dealing with unresolved placeholders", async () => {
const sandbox = sinon.createSandbox();
let restoreFn: RestoreFn | undefined = undefined;

before(() => {
sandbox
Expand All @@ -477,6 +478,13 @@ describe("v3 lifecyle", () => {
after(() => {
mockedDriverContext.progressBar = undefined;
sandbox.restore();
if (restoreFn) {
restoreFn();
}
});

restoreFn = mockedEnv({
AZURE_OPENAI_DEPLOYMENT_NAME: "gpt-4o",
});

it("should return unresolved placeholders", async () => {
Expand Down Expand Up @@ -565,6 +573,25 @@ describe("v3 lifecyle", () => {
);
});

it("should escape OpenAI related env keys", async () => {
const driverDefs: DriverDefinition[] = [];
driverDefs.push({
uses: "file/createOrUpdateEnvironmentFile",
with: {
target: ".env",
envs: {
OPENAI_API_KEY: "${{SECRET_OPENAI_API_KEY}}",
AZURE_OPENAI_DEPLOYMENT_NAME: "${{AZURE_OPENAI_DEPLOYMENT_NAME}}",
},
},
});

const lifecycle = new Lifecycle("deploy", driverDefs, "1.0.0");

const unresolved = lifecycle.resolvePlaceholders();
assert.equal(unresolved.length, 0);
});

describe("execute()", async () => {
it("should return unresolved placeholders in env field", async () => {
const driverDefs: DriverDefinition[] = [];
Expand Down
Loading

0 comments on commit 8e68bc3

Please sign in to comment.