diff --git a/templates/csharp/custom-copilot-rag-custom-api/.gitignore b/templates/csharp/custom-copilot-rag-custom-api/.gitignore new file mode 100644 index 0000000000..0755b1d291 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.gitignore @@ -0,0 +1,30 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.json +appsettings.TestTool.json +.deployment + +# User-specific files +*.user + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Notification local store +.notification.localstore.json +.notification.testtoolstore.json + +# devTools +devTools/ \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/README.md.tpl b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/README.md.tpl new file mode 100644 index 0000000000..ccafb6c965 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/README.md.tpl @@ -0,0 +1,85 @@ +# Overview of the Chat With Your Data (Using Custom API) template + +This template showcases how to build an AI-powered intelligent chatbot that can understand natural language to invoke the API defined in the OpenAPI description document, so you can enable your users to chat with the data provided through API service. +The app template is built using the Teams AI library, which provides the capabilities to build AI-based Teams applications. +## Get started with the template + +> **Prerequisites** +> +> To run the template in your local dev machine, you will need: +> +{{#useOpenAI}} +> - an account with [OpenAI](https://platform.openai.com). +{{/useOpenAI}} +{{#useAzureOpenAI}} +> - [Azure OpenAI](https://aka.ms/oai/access) resource +{{/useAzureOpenAI}} + +### Debug bot app in Teams App Test Tool +{{#useOpenAI}} +1. Ensure your OpenAI API Key is filled in `appsettings.TestTool.json`. + ``` + "OpenAI": { + "ApiKey": "" + } + ``` +{{/useOpenAI}} +{{#useAzureOpenAI}} +1. Ensure your Azure OpenAI settings are filled in `appsettings.TestTool.json`. + ``` + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "OpenAIDeploymentName": "" + } + ``` +{{/useAzureOpenAI}} +1. Select `Teams App Test Tool (browser)` in debug dropdown menu. +1. Press F5, or select the Debug > Start Debugging menu in Visual Studio. +1. In Teams App Test Tool from the launched browser, type and send anything to your bot to trigger a response. + +**Congratulations**! You are running an application that can now interact with users in Teams App Test Tool: + +![custom api template](https://github.com/OfficeDev/TeamsFx/assets/63089166/81f985a1-b81d-4c27-a82a-73a9b65ece1f) + +### Debug bot app in Teams Web Client + +{{#useOpenAI}} +1. Ensure your OpenAI API Key is filled in `env/.env.local.user`. + ``` + SECRET_OPENAI_API_KEY="" + ``` +{{/useOpenAI}} +{{#useAzureOpenAI}} +1. Ensure your Azure OpenAI settings are filled in `env/.env.local.user`. + ``` + SECRET_AZURE_OPENAI_API_KEY="" + AZURE_OPENAI_ENDPOINT="" + AZURE_OPENAI_DEPLOYMENT_NAME="" + ``` +{{/useAzureOpenAI}} +1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel. +1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies. +1. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to. +1. Press F5, or select the Debug > Start Debugging menu in Visual Studio. +1. In the launched browser, select the Add button to load the app in Teams. +1. In the chat bar, type and send anything to your bot to trigger a response. + +> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging). + +## Extend the template + +- Follow [Build a Basic AI Chatbot in Teams](https://aka.ms/teamsfx-basic-ai-chatbot) to extend the template with more AI capabilities. +- Understand more about [build your own data ingestion](https://aka.ms/teamsfx-rag-bot#build-your-own-data-ingestion). + +## Additional information and references + +- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals) +- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli) +- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples) + +## Report an issue + +Select Visual Studio > Help > Send Feedback > Report a Problem. +Or, you can create an issue directly in our GitHub repository: +https://github.com/OfficeDev/TeamsFx/issues. diff --git a/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/launchSettings.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/launchSettings.json.tpl new file mode 100644 index 0000000000..515f8764dc --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/launchSettings.json.tpl @@ -0,0 +1,25 @@ +{ + "profiles": { +{{#enableTestToolByDefault}} + // Launch project within Teams App Test Tool + "Teams App Test Tool (browser)": { + "commandName": "Project", + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, +{{/enableTestToolByDefault}} + // Launch project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + }, +{{^enableTestToolByDefault}} + // Launch project within Teams App Test Tool + "Teams App Test Tool (browser)": { + "commandName": "Project", + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + }, +{{/enableTestToolByDefault}} + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl new file mode 100644 index 0000000000..a31df153ea --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.tpl @@ -0,0 +1,6 @@ + + + + + + diff --git a/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl new file mode 100644 index 0000000000..541a09bd78 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{NewProjectTypeName}}.{{NewProjectTypeExt}}.user.tpl @@ -0,0 +1,14 @@ + + + + ProjectDebugger + + +{{#enableTestToolByDefault}} + Teams App Test Tool (browser) +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + Microsoft Teams (browser) +{{/enableTestToolByDefault}} + + \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl new file mode 100644 index 0000000000..b069676f95 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/.{{NewProjectTypeName}}/{{ProjectName}}.slnLaunch.user.tpl @@ -0,0 +1,78 @@ +[ +{{#enableTestToolByDefault}} + { + "Name": "Teams App Test Tool (browser)", + "Projects": [ + { + "Path": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Teams App Test Tool (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}.csproj", + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}\\{{ProjectName}}.csproj", + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Teams App Test Tool" + } + ] + }, +{{/enableTestToolByDefault}} + { + "Name": "Microsoft Teams (browser)", + "Projects": [ + { + "Path": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Microsoft Teams (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}.csproj", + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}\\{{ProjectName}}.csproj", + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Start Project" + } + ] +{{#enableTestToolByDefault}} + } +{{/enableTestToolByDefault}} +{{^enableTestToolByDefault}} + }, + { + "Name": "Teams App Test Tool (browser)", + "Projects": [ + { + "Path": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}", + "Action": "StartWithoutDebugging", + "DebugTarget": "Teams App Test Tool (browser)" + }, + { +{{#PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}.csproj", + "Name": "{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + "Path": "{{ProjectName}}\\{{ProjectName}}.csproj", + "Name": "{{ProjectName}}\\{{ProjectName}}.csproj", +{{/PlaceProjectFileInSolutionDir}} + "Action": "Start", + "DebugTarget": "Teams App Test Tool" + } + ] + } +{{/enableTestToolByDefault}} +] \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/APIActions.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/APIActions.cs.tpl new file mode 100644 index 0000000000..6bd130b0c6 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/APIActions.cs.tpl @@ -0,0 +1,63 @@ +using AdaptiveCards.Templating; +using AdaptiveCards; +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.AI.Action; +using Microsoft.Teams.AI.AI; +using Microsoft.Teams.AI.State; +using Newtonsoft.Json.Linq; +using Microsoft.Bot.Schema; +using RestSharp; +using OpenAPIClient; + +namespace {{SafeProjectName}} +{ + public class APIActions + { + private APIClient Client; + + public APIActions(string specPath) + { + Client = new APIClient(specPath); + } + + private static IMessageActivity RenderCardToMessage(string cardTemplatePath, string data) + { + try + { + var templateString = File.ReadAllText(cardTemplatePath); + AdaptiveCardTemplate template = new AdaptiveCardTemplate(templateString); + var cardBody = template.Expand(data); + + Attachment attachment = new Attachment() + { + ContentType = AdaptiveCard.ContentType, + Content = JObject.Parse(cardBody) + }; + + return MessageFactory.Attachment(attachment); + } + catch (Exception ex) { + throw new Exception("Failed to render adaptive card: " + ex.Message); + } + } + + private static RequestParams ParseRequestParams(Dictionary args) + { + RequestParams requestParam = new RequestParams + { + PathObject = args.ContainsKey("path") ? args["path"] : null, + HeaderObject = args.ContainsKey("header") ? args["header"] : null, + QueryObject = args.ContainsKey("query") ? args["query"] : null, + RequestBody = args.ContainsKey("requestBody") ? args["requestBody"] : null + }; + return requestParam; + } + + [Action(AIConstants.UnknownActionName)] + public async Task UnknownAction([ActionTurnContext] TurnContext turnContext, [ActionName] string action) + { + await turnContext.SendActivityAsync(MessageFactory.Text("[lights off]")); + return "unknown action"; + } + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/APIBot.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/APIBot.cs.tpl new file mode 100644 index 0000000000..012a24dc4d --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/APIBot.cs.tpl @@ -0,0 +1,14 @@ +using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI; + +namespace {{SafeProjectName}} +{ + public class APIBot : Application + { + public APIBot(ApplicationOptions options, string specPath) : base(options) + { + // Registering action handlers that will be hooked up to the planner. + AI.ImportActions(new APIActions(specPath)); + } + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/AdapterWithErrorHandler.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/AdapterWithErrorHandler.cs.tpl new file mode 100644 index 0000000000..6456467fef --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/AdapterWithErrorHandler.cs.tpl @@ -0,0 +1,34 @@ +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Builder.TraceExtensions; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; + +namespace {{SafeProjectName}} +{ + public class AdapterWithErrorHandler : CloudAdapter + { + public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) + : base(auth, logger) + { + OnTurnError = async (turnContext, exception) => + { + // Log any leaked exception from the application. + // NOTE: In production environment, you should consider logging this to + // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how + // to add telemetry capture to your bot. + logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + + // Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat. + if (turnContext.Activity.Type == ActivityTypes.Message) + { + // Send a message to the user + await turnContext.SendActivityAsync($"The bot encountered an unhandled error: {exception.Message}"); + await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); + + // Send a trace activity + await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); + } + }; + } + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/Config.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/Config.cs.tpl new file mode 100644 index 0000000000..723df72f15 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Config.cs.tpl @@ -0,0 +1,38 @@ +namespace {{SafeProjectName}} +{ + public class ConfigOptions + { + public string BOT_ID { get; set; } + public string BOT_PASSWORD { get; set; } + public string BOT_TYPE { get; set; } + public string BOT_TENANT_ID { get; set; } +{{#useOpenAI}} + public OpenAIConfigOptions OpenAI { get; set; } +{{/useOpenAI}} +{{#useAzureOpenAI}} + public AzureConfigOptions Azure { get; set; } +{{/useAzureOpenAI}} + } + +{{#useOpenAI}} + /// + /// Options for Open AI + /// + public class OpenAIConfigOptions + { + public string ApiKey { get; set; } + public string DefaultModel = "gpt-3.5-turbo"; + } +{{/useOpenAI}} +{{#useAzureOpenAI}} + /// + /// Options for Azure OpenAI and Azure Content Safety + /// + public class AzureConfigOptions + { + public string OpenAIApiKey { get; set; } + public string OpenAIEndpoint { get; set; } + public string OpenAIDeploymentName { get; set; } + } +{{/useAzureOpenAI}} +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/Controllers/BotController.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/Controllers/BotController.cs.tpl new file mode 100644 index 0000000000..3a737fffc7 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Controllers/BotController.cs.tpl @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace {{SafeProjectName}}.Controllers +{ + [Route("api/messages")] + [ApiController] + public class BotController : ControllerBase + { + private readonly CloudAdapter Adapter; + private readonly IBot Bot; + + public BotController(CloudAdapter adapter, IBot bot) + { + Adapter = adapter; + Bot = bot; + } + + [HttpPost] + public async Task PostAsync(CancellationToken cancellationToken = default) + { + await Adapter.ProcessAsync(Request, Response, Bot, cancellationToken); + } + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/APIClientExceptions.cs b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/APIClientExceptions.cs new file mode 100644 index 0000000000..9094e1b7e0 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/APIClientExceptions.cs @@ -0,0 +1,55 @@ +using RestSharp; + +namespace OpenAPIClient +{ + public class APIClientException: Exception + { + public APIClientException(string message) : base(message) + { + } + } + + public class ParseOpenAPISpecException : APIClientException + { + public ParseOpenAPISpecException(string message) : base(message) + { + } + } + + public class APINotExistException : APIClientException + { + public APINotExistException(string apiPath, Method httpMethod) + : base($"API {httpMethod.ToString()} {apiPath} does not exist in the OpenAPI specification file.") + { + } + } + + public class InvalidServerUrlExcpetion : APIClientException + { + public InvalidServerUrlExcpetion(string serverUrl) + : base($"Server URL '{serverUrl}' is invalid. It should use the HTTP/HTTPS protocol with an absolute path.") + { + } + } + + public class ParameterNotObjectException : APIClientException + { + public ParameterNotObjectException(string paramInfo) : base($"Parameter: {paramInfo} is not an object.") + { + } + } + + public class SerializeParameterFailedException : APIClientException + { + public SerializeParameterFailedException(string message) : base(message) + { + } + } + + public class RequestFailedException : APIClientException + { + public RequestFailedException(RestResponse response) : base($"Request failed with status: {response.ResponseStatus}, status code: {response.StatusCode}, error message: {response.ErrorMessage}") + { + } + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/OpenAPIClient.cs b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/OpenAPIClient.cs new file mode 100644 index 0000000000..039341df1f --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/OpenAPIClient.cs @@ -0,0 +1,145 @@ +using RestSharp; +using System.Text.Json; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using rentu_vs_ai_bot_test; +using RestSharp.Authenticators; + +namespace OpenAPIClient +{ + public class APIClient + { + private RestClient RestClient; + private OpenApiDocument Doc; + + public APIClient(string specPath) + { + try + { + using (var stream = new FileStream(specPath, FileMode.Open, FileAccess.Read)) + { + Doc = new OpenApiStreamReader().Read(stream, out var diagnostic); + } + } + catch (Exception ex) { + throw new ParseOpenAPISpecException("Parse OpenAPI spec file failed with error: " + ex.Message); + } + + IAuthenticator authenticator = null; + + // You can add auth using below code + /* + authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator( + "YOUR_ACCESS_TOKEN", "Bearer" + ); + + authenticator = new HttpBasicAuthenticator("username", "password"); + + authenticator = new JwtAuthenticator("YOUR_JWT_TOKEN"); + */ + + var options = new RestClientOptions() + { + Authenticator = authenticator + }; + + RestClient = new RestClient(options); + } + + public async Task CallAsync(string path, Method httpMethod, RequestParams param) + { + OperationType operationType = MethodToOperationTypeMap[httpMethod]; + + if (!Doc.Paths.ContainsKey(path) || !Doc.Paths[path].Operations.ContainsKey(operationType)) + { + throw new APINotExistException(path, httpMethod); + } + + var operationObj = Doc.Paths[path].Operations[operationType]; + var serverUrl = GetAPIServerUrl(path, operationType); + + if (string.IsNullOrEmpty(serverUrl) || !Uri.TryCreate(serverUrl, UriKind.Absolute, out var uriResult) || + (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps)) + { + throw new InvalidServerUrlExcpetion(serverUrl); + } + + var request = new RestRequest(serverUrl + path); + + ProcessParameters(param.PathObject, operationObj, ParameterStyle.Simple, false, (key, value) => request.AddUrlSegment(key, value)); + + ProcessParameters(param.QueryObject, operationObj, ParameterStyle.Form, true, (key, value) => request.AddQueryParameter(key, value)); + + ProcessParameters(param.HeaderObject, operationObj, ParameterStyle.Simple, false, (key, value) => request.AddHeader(key, value)); + + if (param.RequestBody != null) + { + request.AddJsonBody(param.RequestBody, ContentType.Json); + } + + var response = await RestClient.ExecuteAsync(request, httpMethod, CancellationToken.None); + + if (response.ResponseStatus == ResponseStatus.Completed && response.StatusCode == System.Net.HttpStatusCode.OK) + { + return response; + } + + throw new RequestFailedException(response); + } + + private KeyValuePair GetParameterKeyValuePair(JsonProperty property, OpenApiOperation operationObj , ParameterStyle defaultStyle, bool defaultExplode) + { + var key = property.Name; + var value = property.Value; + + var parameterDefinition = operationObj.Parameters.FirstOrDefault(p => p.Name == key); + + var style = parameterDefinition?.Style ?? defaultStyle; + var explode = parameterDefinition?.Explode ?? defaultExplode; + + var valueResult = ParameterSerializer.Serialize(value, style, explode, key); + return new KeyValuePair(key, valueResult); + } + + private static readonly Dictionary MethodToOperationTypeMap = new Dictionary + { + { Method.Get, OperationType.Get }, + { Method.Post, OperationType.Post }, + { Method.Put, OperationType.Put }, + { Method.Delete, OperationType.Delete }, + { Method.Head, OperationType.Head }, + { Method.Options, OperationType.Options }, + { Method.Patch, OperationType.Patch } + }; + + private string GetAPIServerUrl(string path, OperationType operationType) + { + var rootServerUrl = Doc.Servers?.FirstOrDefault()?.Url; + var apiLevelServerUrl = Doc.Paths[path].Servers?.FirstOrDefault()?.Url; + var methodServerUrl = Doc.Paths[path].Operations[operationType].Servers?.FirstOrDefault()?.Url; + var serverUrl = methodServerUrl ?? apiLevelServerUrl ?? rootServerUrl; + return serverUrl; + } + + private void ProcessParameters(object paramObj, OpenApiOperation operationObj, ParameterStyle style, bool flag, Action addParameter) + { + if (paramObj != null) + { + var jsonElement = (JsonElement)paramObj; + + if (jsonElement.ValueKind == JsonValueKind.Object) + { + foreach (JsonProperty property in jsonElement.EnumerateObject()) + { + var paramKeyValuePair = GetParameterKeyValuePair(property, operationObj, style, flag); + addParameter(paramKeyValuePair.Key, paramKeyValuePair.Value); + } + } + else + { + throw new ParameterNotObjectException(paramObj?.ToString()); + } + } + } + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/ParameterSerializer.cs b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/ParameterSerializer.cs new file mode 100644 index 0000000000..969b3b3d37 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/ParameterSerializer.cs @@ -0,0 +1,89 @@ +using Microsoft.OpenApi.Models; +using System.Text.Json; + +namespace OpenAPIClient +{ + // serialize parameters based on schema style and explode property: https://swagger.io/specification/v3 + internal class ParameterSerializer + { + internal static string Serialize(JsonElement value, ParameterStyle style, bool explode, string parentKey = "") + { + try + { + switch (value.ValueKind) + { + case JsonValueKind.Array: + return SerializeArray(value, style, explode, parentKey); + case JsonValueKind.Object: + return SerializeObject(value, style, explode, parentKey); + default: + return value.ToString(); + } + } + catch (Exception ex) { + throw new SerializeParameterFailedException($"Serialize {value} with explode: {explode}, style: {style} failed due to error: " + ex.Message); + } + } + + private static string SerializeArray(JsonElement arrayElement, ParameterStyle style, bool explode, string parentKey) + { + var values = arrayElement.EnumerateArray().Select(e => Serialize(e, style, explode, parentKey)).ToList(); + + if (style == ParameterStyle.Simple) + { + return string.Join(",", values); + } + else if (style == ParameterStyle.Form) + { + return explode ? string.Join("&", values.Select(v => $"{parentKey}={v}")) : string.Join(",", values); + } + else if (style == ParameterStyle.Matrix) + { + return explode ? string.Join(";", values.Select(v => $"{parentKey}={v}")) : string.Join(";", values); + } + else if (style == ParameterStyle.Label) + { + return explode ? string.Join(".", values.Select(v => $"{parentKey}={v}")) : string.Join(".", values); + } + else if (style == ParameterStyle.SpaceDelimited) + { + return string.Join(" ", values); + } + else if(style == ParameterStyle.PipeDelimited) + { + return string.Join("|", values); + } + + return string.Join(",", values); // Default to simple style + } + + private static string SerializeObject(JsonElement objectElement, ParameterStyle style, bool explode, string parentKey) + { + var keyValuePairs = objectElement.EnumerateObject().Select(p => + { + var key = p.Name; + var value = Serialize(p.Value, style, explode, key); + return style == ParameterStyle.DeepObject ? $"{parentKey}[{key}]={value}" : $"{key}={value}"; + }); + + if (style == ParameterStyle.Simple) + { + return string.Join(",", keyValuePairs); + } + else if (style == ParameterStyle.Form) + { + return explode ? string.Join("&", keyValuePairs) : string.Join(",", keyValuePairs); + } + else if (style == ParameterStyle.Matrix) + { + return explode ? string.Join(";", keyValuePairs) : string.Join(";", keyValuePairs); + } + else if (style == ParameterStyle.DeepObject) + { + return string.Join("&", keyValuePairs); + } + + return string.Join(",", keyValuePairs); // Default to simple style + } + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/RequestParams.cs b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/RequestParams.cs new file mode 100644 index 0000000000..7ea0a64713 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/OpenAPIClient/RequestParams.cs @@ -0,0 +1,10 @@ +namespace OpenAPIClient +{ + public class RequestParams() + { + public object PathObject; + public object QueryObject; + public object HeaderObject; + public object RequestBody; + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl b/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl new file mode 100644 index 0000000000..75b018e932 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Program.cs.tpl @@ -0,0 +1,129 @@ +using {{SafeProjectName}}; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Teams.AI; +using Microsoft.Teams.AI.AI.Models; +using Microsoft.Teams.AI.AI.Planners; +using Microsoft.Teams.AI.AI.Prompts; +using Microsoft.Teams.AI.State; +using RestSharp; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); +builder.Services.AddHttpContextAccessor(); + +// Prepare Configuration for ConfigurationBotFrameworkAuthentication +var config = builder.Configuration.Get(); +builder.Configuration["MicrosoftAppType"] = config.BOT_TYPE; +builder.Configuration["MicrosoftAppId"] = config.BOT_ID; +builder.Configuration["MicrosoftAppPassword"] = config.BOT_PASSWORD; +builder.Configuration["MicrosoftAppTenantId"] = config.BOT_TENANT_ID; +// Create the Bot Framework Authentication to be used with the Bot Adapter. +builder.Services.AddSingleton(); + +// Create the Cloud Adapter with error handling enabled. +// Note: some classes expect a BotAdapter and some expect a BotFrameworkHttpAdapter, so +// register the same adapter instance for both types. +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetService()); +builder.Services.AddSingleton(sp => sp.GetService()); + +builder.Services.AddSingleton(); + +{{#useOpenAI}} +builder.Services.AddSingleton(sp => new( + new OpenAIModelOptions(config.OpenAI.ApiKey, config.OpenAI.DefaultModel) + { + LogRequests = true + }, + sp.GetService() +)); +{{/useOpenAI}} +{{#useAzureOpenAI}} +builder.Services.AddSingleton(sp => new( + new AzureOpenAIModelOptions( + config.Azure.OpenAIApiKey, + config.Azure.OpenAIDeploymentName, + config.Azure.OpenAIEndpoint + ) + { + LogRequests = true + }, + sp.GetService() +)); +{{/useAzureOpenAI}} + +// Create the bot as transient. In this case the ASP Controller is expecting an IBot. +builder.Services.AddTransient(sp => +{ + // Create loggers + ILoggerFactory loggerFactory = sp.GetService(); + + // Create Prompt Manager + PromptManager prompts = new(new() + { + PromptFolder = "./Prompts" + }); + + prompts.AddFunction("get_actions", async (context, memory, functions, tokenizer, args) => + { + var skpromptContent = File.ReadAllText("./Prompts/chat/actions.json"); + return await Task.FromResult(skpromptContent); + }); + + // Create ActionPlanner + ActionPlanner planner = new( + options: new( + model: sp.GetService(), + prompts: prompts, + defaultPrompt: async (context, state, planner) => + { + PromptTemplate template = prompts.GetPrompt("chat"); + return await Task.FromResult(template); + } + ) + { LogRepairs = true, MaxRepairAttempts = 5 }, + loggerFactory: loggerFactory + ); + + var bot = new APIBot(new() + { + Storage = sp.GetService(), + AI = new(planner), + LoggerFactory = loggerFactory, + }, + "./apiSpecificationFile/openapi.yaml"); + + bot.OnConversationUpdate("membersAdded", async (turnContext, turnState, cancellationToken) => + { + var welcomeText = "I am an AI bot can help you call APIs"; + foreach (var member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync(MessageFactory.Text(welcomeText), cancellationToken); + } + } + }); + + return bot; +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +app.Run(); \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/config.json b/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/config.json new file mode 100644 index 0000000000..1bcdf9f2c1 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/config.json @@ -0,0 +1,20 @@ +{ + "schema": 1.1, + "description": "A bot that can chat and call APIs", + "type": "completion", + "completion": { + "completion_type": "chat", + "include_history": false, + "include_input": true, + "max_input_tokens": 5000, + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + }, + "augmentation": { + "augmentation_type": "sequence" + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/skprompt.txt b/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/skprompt.txt new file mode 100644 index 0000000000..d87ed35996 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Prompts/chat/skprompt.txt @@ -0,0 +1 @@ +The following is a conversation with an AI assistant. \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/Properties/launchSettings.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/Properties/launchSettings.json.tpl new file mode 100644 index 0000000000..823a6c4155 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/Properties/launchSettings.json.tpl @@ -0,0 +1,96 @@ +{ + "profiles": { +{{^isNewProjectTypeEnabled}} +{{#enableTestToolByDefault}} + // Debug project within Teams App Test Tool + "Teams App Test Tool (browser)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "TestTool", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.testtoolstore.json" + }, + "hotReloadProfile": "aspnetcore" + }, +{{/enableTestToolByDefault}} + // Debug project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}", + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + }, +{{^enableTestToolByDefault}} + // Debug project within Teams App Test Tool + "Teams App Test Tool (browser)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchTestTool": true, + "launchUrl": "http://localhost:56150", + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "TestTool", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.testtoolstore.json" + }, + "hotReloadProfile": "aspnetcore" + }, +{{/enableTestToolByDefault}} + //// Uncomment following profile to debug project only (without launching Teams) + //, + //"Start Project (not in Teams)": { + // "commandName": "Project", + // "dotnetRunMessages": true, + // "applicationUrl": "https://localhost:7130;http://localhost:5130", + // "environmentVariables": { + // "ASPNETCORE_ENVIRONMENT": "Development" + // }, + // "hotReloadProfile": "aspnetcore" + //} +{{/isNewProjectTypeEnabled}} +{{#isNewProjectTypeEnabled}} +{{#enableTestToolByDefault}} + "Teams App Test Tool": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "TestTool", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.testtoolstore.json" + }, + "hotReloadProfile": "aspnetcore" + }, +{{/enableTestToolByDefault}} + "Start Project": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + }, +{{^enableTestToolByDefault}} + "Teams App Test Tool": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "TestTool", + "TEAMSFX_NOTIFICATION_STORE_FILENAME": ".notification.testtoolstore.json" + }, + "hotReloadProfile": "aspnetcore" + }, +{{/enableTestToolByDefault}} +{{/isNewProjectTypeEnabled}} + } +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/appPackage/color.png b/templates/csharp/custom-copilot-rag-custom-api/appPackage/color.png new file mode 100644 index 0000000000..01aa37e347 Binary files /dev/null and b/templates/csharp/custom-copilot-rag-custom-api/appPackage/color.png differ diff --git a/templates/csharp/custom-copilot-rag-custom-api/appPackage/manifest.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/appPackage/manifest.json.tpl new file mode 100644 index 0000000000..7d6a5403f9 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/appPackage/manifest.json.tpl @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json", + "manifestVersion": "1.17", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "Teams App, Inc.", + "websiteUrl": "https://www.example.com", + "privacyUrl": "https://www.example.com/privacy", + "termsOfUseUrl": "https://www.example.com/termofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "{{appName}}${{APP_NAME_SUFFIX}}", + "full": "full name for {{appName}}" + }, + "description": { + "short": "Short description of {{appName}}", + "full": "Full description of {{appName}}" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": [ + "personal", + "team", + "groupChat" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "composeExtensions": [ + ], + "configurableTabs": [], + "staticTabs": [], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [] +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/appPackage/outline.png b/templates/csharp/custom-copilot-rag-custom-api/appPackage/outline.png new file mode 100644 index 0000000000..f7a4c86447 Binary files /dev/null and b/templates/csharp/custom-copilot-rag-custom-api/appPackage/outline.png differ diff --git a/templates/csharp/custom-copilot-rag-custom-api/appsettings.Development.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/appsettings.Development.json.tpl new file mode 100644 index 0000000000..b4c276e507 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/appsettings.Development.json.tpl @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.Teams.AI": "Trace" + } + }, + "AllowedHosts": "*", + "BOT_ID": "", + "BOT_PASSWORD": "", + "BOT_TYPE": "", +{{#useOpenAI}} + "OpenAI": { + "ApiKey": "" + } +{{/useOpenAI}} +{{#useAzureOpenAI}} + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "OpenAIDeploymentName": "" + } +{{/useAzureOpenAI}} +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/appsettings.TestTool.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/appsettings.TestTool.json.tpl new file mode 100644 index 0000000000..a5bb41ed7f --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/appsettings.TestTool.json.tpl @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "BOT_ID": "", + "BOT_PASSWORD": "", +{{#useOpenAI}} + "OpenAI": { + "ApiKey": "{{{originalOpenAIKey}}}" + } +{{/useOpenAI}} +{{#useAzureOpenAI}} + "Azure": { + "OpenAIApiKey": "{{{originalAzureOpenAIKey}}}", + "OpenAIEndpoint": "{{{azureOpenAIEndpoint}}}", + "OpenAIDeploymentName": "{{{azureOpenAIDeploymentName}}}" + } +{{/useAzureOpenAI}} +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/appsettings.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/appsettings.json.tpl new file mode 100644 index 0000000000..615523a351 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/appsettings.json.tpl @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "BOT_ID": "", + "BOT_PASSWORD": "", + "BOT_TYPE": "", + "BOT_TENANT_ID": "", +{{#useOpenAI}} + "OpenAI": { + "ApiKey": "" + } +{{/useOpenAI}} +{{#useAzureOpenAI}} + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "OpenAIDeploymentName": "" + } +{{/useAzureOpenAI}} +} \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev b/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev new file mode 100644 index 0000000000..df4f9da508 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev @@ -0,0 +1,15 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev +APP_NAME_SUFFIX=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev.user.tpl b/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev.user.tpl new file mode 100644 index 0000000000..4ade46a738 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/env/.env.dev.user.tpl @@ -0,0 +1,11 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +{{#useOpenAI}} +SECRET_OPENAI_API_KEY={{{openAIKey}}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +SECRET_AZURE_OPENAI_API_KEY={{{azureOpenAIKey}}} +AZURE_OPENAI_ENDPOINT={{{azureOpenAIEndpoint}}} +AZURE_OPENAI_DEPLOYMENT_NAME={{{azureOpenAIDeploymentName}}} +{{/useAzureOpenAI}} diff --git a/templates/csharp/custom-copilot-rag-custom-api/env/.env.local b/templates/csharp/custom-copilot-rag-custom-api/env/.env.local new file mode 100644 index 0000000000..2646096121 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/env/.env.local @@ -0,0 +1,10 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local +APP_NAME_SUFFIX=local + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_DOMAIN= \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/env/.env.local.user.tpl b/templates/csharp/custom-copilot-rag-custom-api/env/.env.local.user.tpl new file mode 100644 index 0000000000..001fdb88aa --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/env/.env.local.user.tpl @@ -0,0 +1,12 @@ +# This file includes environment variables that will not be committed to git by default. You can set these environment variables in your CI/CD system for your project. + +# Secrets. Keys prefixed with `SECRET_` will be masked in Teams Toolkit logs. +SECRET_BOT_PASSWORD= +{{#useOpenAI}} +SECRET_OPENAI_API_KEY={{{openAIKey}}} +{{/useOpenAI}} +{{#useAzureOpenAI}} +SECRET_AZURE_OPENAI_API_KEY={{{azureOpenAIKey}}} +AZURE_OPENAI_ENDPOINT={{{azureOpenAIEndpoint}}} +AZURE_OPENAI_DEPLOYMENT_NAME={{{azureOpenAIDeploymentName}}} +{{/useAzureOpenAI}} diff --git a/templates/csharp/custom-copilot-rag-custom-api/infra/azure.bicep.tpl b/templates/csharp/custom-copilot-rag-custom-api/infra/azure.bicep.tpl new file mode 100644 index 0000000000..96c41cb295 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/infra/azure.bicep.tpl @@ -0,0 +1,122 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string +{{#useOpenAI}} +@secure() +param openAIApiKey string +{{/useOpenAI}} +{{#useAzureOpenAI}} +@secure() +param azureOpenAIApiKey string + +param azureOpenAIEndpoint string +param azureOpenAIDeploymentName string +{{/useAzureOpenAI}} + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param identityName string = resourceBaseName +param location string = resourceGroup().location + +resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + location: location + name: identityName +} + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: identity.properties.clientId + } + { + name: 'BOT_TENANT_ID' + value: identity.properties.tenantId + } + { + name: 'BOT_TYPE' + value: 'UserAssignedMsi' + } +{{#useOpenAI}} + { + name: 'OpenAI__ApiKey' + value: openAIApiKey + } +{{/useOpenAI}} +{{#useAzureOpenAI}} + { + name: 'Azure__OpenAIApiKey' + value: azureOpenAIApiKey + } + { + name: 'Azure__OpenAIEndpoint' + value: azureOpenAIEndpoint + } + { + name: 'Azure__OpenAIDeploymentName' + value: azureOpenAIDeploymentName + } +{{/useAzureOpenAI}} + ] + ftpsState: 'FtpsOnly' + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity.id}': {} + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + identityClientId: identity.properties.clientId + identityResourceId: identity.id + identityTenantId: identity.properties.tenantId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName +output BOT_ID string = identity.properties.clientId +output BOT_TENANT_ID string = identity.properties.tenantId diff --git a/templates/csharp/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl b/templates/csharp/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl new file mode 100644 index 0000000000..ca23a97286 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/infra/azure.parameters.json.tpl @@ -0,0 +1,31 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "bot${{RESOURCE_SUFFIX}}" + }, +{{#useOpenAI}} + "openAIApiKey": { + "value": "${{SECRET_OPENAI_API_KEY}}" + }, +{{/useOpenAI}} +{{#useAzureOpenAI}} + "azureOpenAIApiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenAIEndpoint": { + "value": "${{AZURE_OPENAI_ENDPOINT}}" + }, + "azureOpenAIDeploymentName": { + "value": "${{AZURE_OPENAI_DEPLOYMENT_NAME}}" + }, +{{/useAzureOpenAI}} + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "{{appName}}" + } + } + } \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/azurebot.bicep b/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/azurebot.bicep new file mode 100644 index 0000000000..a5a27b8fe4 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/azurebot.bicep @@ -0,0 +1,42 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param identityResourceId string +param identityClientId string +param identityTenantId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: identityClientId + msaAppMSIResourceId: identityResourceId + msaAppTenantId:identityTenantId + msaAppType:'UserAssignedMSI' + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/readme.md b/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/readme.md new file mode 100644 index 0000000000..d5416243cd --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/templates/csharp/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl b/templates/csharp/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl new file mode 100644 index 0000000000..e7c1245c56 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/teamsapp.local.yml.tpl @@ -0,0 +1,113 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.7 + +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Create or reuse an existing Microsoft Entra application for bot. + - uses: aadApp/create + with: + # The Microsoft Entra application's display name + name: {{appName}}${{APP_NAME_SUFFIX}} + generateClientSecret: true + signInAudience: AzureADMultipleOrgs + writeToEnvironmentFile: + # The Microsoft Entra application's client id created for bot. + clientId: BOT_ID + # The Microsoft Entra application's client secret created for bot. + clientSecret: SECRET_BOT_PASSWORD + # The Microsoft Entra application's object id created for bot. + objectId: BOT_OBJECT_ID + + # Generate runtime appsettings to JSON file + - uses: file/createOrUpdateJsonFile + with: +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + target: ../appsettings.Development.json +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + target: ../{{appName}}/appsettings.Development.json +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} +{{^isNewProjectTypeEnabled}} + target: ./appsettings.Development.json +{{/isNewProjectTypeEnabled}} + content: + BOT_TYPE: 'MultiTenant' + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} +{{#useOpenAI}} + OpenAI: + ApiKey: ${{SECRET_OPENAI_API_KEY}} +{{/useOpenAI}} +{{#useAzureOpenAI}} + Azure: + OpenAIApiKey: ${{SECRET_AZURE_OPENAI_API_KEY}} + OpenAIEndpoint: ${{AZURE_OPENAI_ENDPOINT}} + OpenAIDeploymentName: ${{AZURE_OPENAI_DEPLOYMENT_NAME}} +{{/useAzureOpenAI}} + + # Create or update the bot registration on dev.botframework.com + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: {{appName}} + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip +{{^isNewProjectTypeEnabled}} + + # Create or update debug profile in lauchsettings file + - uses: file/createOrUpdateJsonFile + with: + target: ./Properties/launchSettings.json + content: + profiles: + Microsoft Teams (browser): + commandName: "Project" + dotnetRunMessages: true + launchBrowser: true + launchUrl: "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}" + applicationUrl: "http://localhost:5130" + environmentVariables: + ASPNETCORE_ENVIRONMENT: "Development" + hotReloadProfile: "aspnetcore" +{{/isNewProjectTypeEnabled}} diff --git a/templates/csharp/custom-copilot-rag-custom-api/teamsapp.yml.tpl b/templates/csharp/custom-copilot-rag-custom-api/teamsapp.yml.tpl new file mode 100644 index 0000000000..74710a4229 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/teamsapp.yml.tpl @@ -0,0 +1,101 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/v1.7/yaml.schema.json +# Visit https://aka.ms/teamsfx-v5.0-guide for details on this file +# Visit https://aka.ms/teamsfx-actions for details on actions +version: v1.7 + +environmentFolderPath: ./env + +# Triggered when 'teamsapp provision' is executed +provision: + # Creates a Teams app + - uses: teamsApp/create + with: + # Teams app name + name: {{appName}}${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-bot + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Validate using manifest schema + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + # Build Teams app package with latest env value + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputFolder: ./appPackage/build + # Validate app package using validation rules + - uses: teamsApp/validateAppPackage + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + # Apply the Teams app manifest to an existing Teams app in + # Teams Developer Portal. + # Will use the app id in manifest file to determine which Teams app to update. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsapp deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release {{ProjectName}}.csproj +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} + # Deploy your application to Azure App Service using the zip deploy feature. + # For additional details, refer to https://aka.ms/zip-deploy-to-app-services. + - uses: azureAppService/zipDeploy + with: + # Deploy base folder + artifactFolder: bin/Release/{{TargetFramework}}/publish + # The resource id of the cloud resource to be deployed to. + # This key will be generated by arm/deploy action automatically. + # You can replace it with your existing Azure Resource id + # or add it to your environment variable file. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}} +{{#isNewProjectTypeEnabled}} +{{#PlaceProjectFileInSolutionDir}} + workingDirectory: .. +{{/PlaceProjectFileInSolutionDir}} +{{^PlaceProjectFileInSolutionDir}} + workingDirectory: ../{{ProjectName}} +{{/PlaceProjectFileInSolutionDir}} +{{/isNewProjectTypeEnabled}} diff --git a/templates/csharp/custom-copilot-rag-custom-api/{{ProjectName}}.csproj.tpl b/templates/csharp/custom-copilot-rag-custom-api/{{ProjectName}}.csproj.tpl new file mode 100644 index 0000000000..a0f4bc0920 --- /dev/null +++ b/templates/csharp/custom-copilot-rag-custom-api/{{ProjectName}}.csproj.tpl @@ -0,0 +1,60 @@ + + + + {{TargetFramework}} + enable + + +{{^isNewProjectTypeEnabled}} + + + + + + + + + + + + +{{/isNewProjectTypeEnabled}} + + + + + + + + + + + + + + PreserveNewest + PreserveNewest + + + + + + PreserveNewest + PreserveNewest + + + + + + + + PreserveNewest + None + + + + PreserveNewest + None + + +