diff --git a/packages/fx-core/src/component/generator/apiSpec/generator.ts b/packages/fx-core/src/component/generator/apiSpec/generator.ts index fc8cbf0b11..e2b0dbb73a 100644 --- a/packages/fx-core/src/component/generator/apiSpec/generator.ts +++ b/packages/fx-core/src/component/generator/apiSpec/generator.ts @@ -312,7 +312,19 @@ export class SpecGenerator extends DefaultTemplateGenerator { const openapiSpecFileName = getTemplateInfosState.isYaml ? DefaultApiSpecYamlFileName : DefaultApiSpecJsonFileName; - const openapiSpecPath = path.join(apiSpecFolderPath, openapiSpecFileName); + + let openapiSpecPath = path.join(apiSpecFolderPath, openapiSpecFileName); + + if (getTemplateInfosState.templateName === forCustomCopilotRagCustomApi) { + const language = inputs[QuestionNames.ProgrammingLanguage] as ProgrammingLanguage; + if (language === ProgrammingLanguage.CSharp) { + openapiSpecPath = path.join( + destinationPath, + DefaultApiSpecFolderName, + openapiSpecFileName + ); + } + } await fs.ensureDir(apiSpecFolderPath); diff --git a/packages/fx-core/src/component/generator/apiSpec/helper.ts b/packages/fx-core/src/component/generator/apiSpec/helper.ts index 6cae3c6821..6e05dda9c8 100644 --- a/packages/fx-core/src/component/generator/apiSpec/helper.ts +++ b/packages/fx-core/src/component/generator/apiSpec/helper.ts @@ -1036,7 +1036,12 @@ function parseSpec(spec: OpenAPIV3.Document): [SpecObject[], boolean] { return [res, needAuth]; } -const commonLanguages = [ProgrammingLanguage.TS, ProgrammingLanguage.JS, ProgrammingLanguage.PY]; +const commonLanguages = [ + ProgrammingLanguage.TS, + ProgrammingLanguage.JS, + ProgrammingLanguage.PY, + ProgrammingLanguage.CSharp, +]; async function updatePromptForCustomApi( spec: OpenAPIV3.Document, @@ -1058,7 +1063,10 @@ async function updateAdaptiveCardForCustomApi( destinationPath: string ): Promise { if (commonLanguages.includes(language as ProgrammingLanguage)) { - const adaptiveCardsFolderPath = path.join(destinationPath, "src", "adaptiveCards"); + let adaptiveCardsFolderPath = path.join(destinationPath, "src", "adaptiveCards"); + if (language === ProgrammingLanguage.CSharp) { + adaptiveCardsFolderPath = path.join(destinationPath, "adaptiveCards"); + } await fs.ensureDir(adaptiveCardsFolderPath); for (const item of specItems) { @@ -1358,7 +1366,10 @@ export async function updateForCustomApi( destinationPath: string, openapiSpecFileName: string ): Promise { - const chatFolder = path.join(destinationPath, "src", "prompts", "chat"); + let chatFolder = path.join(destinationPath, "src", "prompts", "chat"); + if (language === ProgrammingLanguage.CSharp) { + chatFolder = path.join(destinationPath, "prompts", "Chat"); + } await fs.ensureDir(chatFolder); // 1. update prompt folder diff --git a/packages/fx-core/tests/component/generator/apiSpecGenerator.test.ts b/packages/fx-core/tests/component/generator/apiSpecGenerator.test.ts index bf9a5467a7..e4df2d914b 100644 --- a/packages/fx-core/tests/component/generator/apiSpecGenerator.test.ts +++ b/packages/fx-core/tests/component/generator/apiSpecGenerator.test.ts @@ -61,6 +61,7 @@ import mockedEnv, { RestoreFn } from "mocked-env"; import { FeatureFlagName } from "../../../src/common/featureFlags"; import * as commonUtils from "../../../src/common/utils"; import * as helper from "../../../src/component/generator/apiSpec/helper"; +import { fail } from "assert"; const teamsManifest: TeamsAppManifest = { name: { @@ -826,6 +827,18 @@ describe("updateForCustomApi", async () => { expect(data).not.to.contains("{{"); expect(data).not.to.contains("# Replace with action code"); } + + if (file.toString().endsWith("actions.json")) { + expect(file == path.join("path", "prompts", "Chat", "actions.json")).to.be.true; + } + + if (file.toString().endsWith("skprompt.txt")) { + expect(file == path.join("path", "prompts", "Chat", "skprompt.txt")).to.be.true; + } + + if (file.toString().endsWith("getHello.json")) { + expect(file == path.join("path", "adaptiveCards", "getHello.json")).to.be.true; + } }); sandbox @@ -834,6 +847,32 @@ describe("updateForCustomApi", async () => { await CopilotPluginHelper.updateForCustomApi(spec, "csharp", "path", "openapi.yaml"); }); + it("unknown language: unknown", async () => { + sandbox.stub(fs, "ensureDir").resolves(); + sandbox.stub(fs, "writeFile").callsFake((file, data) => { + if (file == path.join("path", "APIActions.cs")) { + fail("actions.json should not be created for unknown language"); + } + + if (file.toString().endsWith("actions.json")) { + fail("actions.json should not be created for unknown language"); + } + + if (file.toString().endsWith("skprompt.txt")) { + fail("actions.json should not be created for unknown language"); + } + + if (file.toString().endsWith("getHello.json")) { + fail("actions.json should not be created for unknown language"); + } + }); + + sandbox + .stub(fs, "readFile") + .resolves(Buffer.from("test code // Replace with action code {{OPENAPI_SPEC_PATH}}")); + await CopilotPluginHelper.updateForCustomApi(spec, "unknown", "path", "openapi.yaml"); + }); + it("happy path with spec without path", async () => { const limitedSpec = { openapi: "3.0.0", @@ -2265,6 +2304,58 @@ describe("SpecGenerator", async () => { assert.isTrue(generateBasedOnSpec.calledOnce); }); + it("generateCustomCopilot for csharp: success", async () => { + const inputs: Inputs = { + platform: Platform.VSCode, + projectPath: "path", + [QuestionNames.ProgrammingLanguage]: ProgrammingLanguage.CSharp, + [QuestionNames.ApiSpecLocation]: "test.yaml", + [QuestionNames.ApiOperation]: ["operation1"], + getTemplateInfosState: { + templateName: "custom-copilot-rag-custom-api", + isPlugin: false, + uri: "https://test.com", + isYaml: false, + type: ProjectType.TeamsAi, + }, + }; + const context = createContext(); + sandbox + .stub(SpecParser.prototype, "validate") + .resolves({ status: ValidationStatus.Valid, errors: [], warnings: [] }); + sandbox.stub(SpecParser.prototype, "getFilteredSpecs").resolves([ + { + openapi: "3.0.0", + info: { + title: "test", + version: "1.0", + }, + paths: {}, + }, + { + openapi: "3.0.0", + info: { + title: "test", + version: "1.0", + }, + paths: {}, + }, + ]); + sandbox.stub(CopilotPluginHelper, "updateForCustomApi").resolves(); + sandbox.stub(fs, "ensureDir").resolves(); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(teamsManifest)); + const generateBasedOnSpec = sandbox + .stub(SpecParser.prototype, "generate") + .resolves({ allSuccess: true, warnings: [] }); + sandbox.stub(pluginGeneratorHelper, "generateScaffoldingSummary").resolves(""); + + const generator = new SpecGenerator(); + const result = await generator.post(context, inputs, "projectPath"); + + assert.isTrue(result.isOk()); + assert.isTrue(generateBasedOnSpec.calledOnce); + }); + it("generateCustomCopilot: CLI with warning", async () => { const inputs: Inputs = { platform: Platform.CLI, diff --git a/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl index 10c3d0009f..0297691c4d 100644 --- a/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl +++ b/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl @@ -68,7 +68,7 @@ builder.Services.AddTransient(sp => PromptFolder = "./Prompts" }); - prompts.AddFunction("get_actions", async (context, memory, functions, tokenizer, args) => + prompts.AddFunction("getAction", async (context, memory, functions, tokenizer, args) => { var skpromptContent = File.ReadAllText("./Prompts/chat/actions.json"); return await Task.FromResult(skpromptContent);