Skip to content

Creating the Backend API

These changes are already available in the repository. These instructions walk you through the process followed to create the backend API from the Console application:

  1. Start by creating a new directory:

    mkdir -p workshop/dotnet/App/backend
    
  2. Next create a new SDK .NET project:

    cd workshop/dotnet/App/
    dotnet new webapi -n backend --no-openapi --force
    cd backend
    
  3. Build project to confirm it is successful:

    dotnet build
    
    Build succeeded.
        0 Warning(s)
        0 Error(s)
    
  4. Add the following nuget packages:

    dotnet add package Microsoft.AspNetCore.Mvc
    dotnet add package Swashbuckle.AspNetCore
    
  5. Replace the contents of Program.cs in the project directory with the following code. This file initializes and loads the required services and configuration for the API, namely configuring CORS protection, enabling controllers for the API and exposing Swagger document:

    using Microsoft.AspNetCore.Antiforgery;
    using Extensions;
    using System.Text.Json.Serialization;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers();
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    // See: https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    // Required to generate enumeration values in Swagger doc
    builder.Services.AddControllersWithViews().AddJsonOptions(options => 
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
    builder.Services.AddOutputCache();
    builder.Services.AddAntiforgery(options => { 
        options.HeaderName = "X-CSRF-TOKEN-HEADER"; 
        options.FormFieldName = "X-CSRF-TOKEN-FORM"; });
    builder.Services.AddHttpClient();
    builder.Services.AddDistributedMemoryCache();
    // Add Semantic Kernel services
    builder.Services.AddSkServices();
    
    // Load user secrets
    builder.Configuration.AddUserSecrets<Program>();
    
    var app = builder.Build();
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseOutputCache();
    app.UseRouting();
    app.UseCors();
    app.UseAntiforgery();
    app.MapControllers();
    
    app.Use(next => context =>
    {
        var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
        var tokens = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokens?.RequestToken ?? string.Empty, new CookieOptions() { HttpOnly = false });
        return next(context);
    });
    
    app.Map("/", () => Results.Redirect("/swagger"));
    
    app.MapControllerRoute(
        "default",
        "{controller=ChatController}");
    
    app.Run();
    
  6. Next we need to create Extensions directory to and add service extensions:

    mkdir Extensions
    cd Extensions
    
  7. In the Extensions directory create a ServiceExtensions.cs class with the following code to initialize the semantic kernel:

    using Core.Utilities.Config;
    using Core.Utilities.Models;
    // Add import for Plugins
    using Core.Utilities.Plugins;
    // Add import required for StockService
    using Core.Utilities.Services;
    using Microsoft.SemanticKernel;
    
    namespace Extensions;
    
    public static class ServiceExtensions
    {
        public static void AddSkServices(this IServiceCollection services) 
        {
            services.AddSingleton<Kernel>(_ => 
            {
                IKernelBuilder builder = KernelBuilderProvider.CreateKernelWithChatCompletion();
                // Enable tracing
                builder.Services.AddLogging(services => services.AddConsole().SetMinimumLevel(LogLevel.Trace));
                Kernel kernel = builder.Build();
    
                // Step 2 - Initialize Time plugin and registration in the kernel
                kernel.Plugins.AddFromObject(new TimeInformationPlugin());
    
                // Step 6 - Initialize Stock Data Plugin and register it in the kernel
                HttpClient httpClient = new();
                StockDataPlugin stockDataPlugin = new(new StocksService(httpClient));
                kernel.Plugins.AddFromObject(stockDataPlugin);
    
                return kernel;
            });
        }
    
    }
    
  8. Next we need to create a Controllers directory to add REST API controller classes:

    cd ..
    mkdir Controllers
    cd Controllers
    
  9. Within the Controllers directory create a ChatController.cs file which exposes a reply method mapped to the chat path and the HTTP POST method:

    using Core.Utilities.Models;
    using Core.Utilities.Extensions;
    // Add import required for StockService
    using Microsoft.SemanticKernel;
    using Microsoft.SemanticKernel.Connectors.OpenAI;
    // Add ChatCompletion import
    using Microsoft.SemanticKernel.ChatCompletion;
    // Add import for Agents
    using Microsoft.SemanticKernel.Agents;
    // Temporarily added to enable Semantic Kernel tracing
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    
    using Microsoft.AspNetCore.Mvc;
    
    namespace Controllers;
    
    [ApiController]
    [Route("sk")]
    public class ChatController : ControllerBase {
    
        private readonly Kernel _kernel;
        private readonly OpenAIPromptExecutionSettings _promptExecutionSettings;
    
        private readonly ChatCompletionAgent _stockSentimentAgent;
        public ChatController(Kernel kernel)
        {
            _kernel = kernel;
            _promptExecutionSettings = new()
            {
                // Add Auto invoke kernel functions as the tool call behavior
                ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
            };
            _stockSentimentAgent = GetStockSemanticAgentAsync().Result;
    
        }
    
        /// <summary>
        /// Get StockSemanticAgent instance
        /// </summary>
        /// <returns></returns>
        private async Task<ChatCompletionAgent> GetStockSemanticAgentAsync()
        {
            // Get StockSemanticAgent instance
            ChatCompletionAgent stockSentimentAgent =
                new()
                {
                    Name = "StockSentimentAgent",
                    Instructions =
                        """
                        Your responsibility is to find the stock sentiment for a given Stock.
    
                        RULES:
                        - Use stock sentiment scale from 1 to 10 where stock sentiment is 1 for sell and 10 for buy.
                        - Only use reliable sources such as Yahoo Finance, MarketWatch, Fidelity and similar.
                        - Provide the rating in your response and a recommendation to buy, hold or sell.
                        - Include the reasoning behind your recommendation.
                        - Include the source of the sentiment in your response.
                        """,
                    Kernel = _kernel,
                    Arguments = new KernelArguments(new OpenAIPromptExecutionSettings() { 
                        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()})
                };
            return stockSentimentAgent;
        }
    
        [HttpPost("/chat")]
        public async Task<ChatResponse> ReplyAsync([FromBody]ChatRequest request)
        {
            // Get chatCompletionService and initialize chatHistory wiht system prompt
            var chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();
            var chatHistory = new ChatHistory();
            if (request.MessageHistory.Count == 0) { 
                chatHistory.AddSystemMessage("You are a friendly financial advisor that only emits financial advice in a creative and funny tone");
            }
            else {
                chatHistory = request.ToChatHistory();
            }
            KernelArguments kernelArgs = new(_promptExecutionSettings);
    
            // Initialize fullMessage variable and add user input to chat history
            string fullMessage = "";
            if (request.InputMessage != null)
            {
                chatHistory.AddUserMessage(request.InputMessage);
    
                // Invoke stockSentimentAgent chat completion with kernel arguments
                await foreach (var chatUpdate in _stockSentimentAgent.InvokeAsync(chatHistory, kernelArgs))
                {
                    Console.Write(chatUpdate.Content);
                    fullMessage += chatUpdate.Content ?? "";
                }
                chatHistory.AddAssistantMessage(fullMessage);
            }
            var chatResponse = new ChatResponse(fullMessage, chatHistory.FromChatHistory());    
            return chatResponse;
        }
    
    
    }
    
  10. Within the Controllers directory create a PluginInfoController.cs file which exposes a method mapped to the /puginInfo/metadata path and the HTTP GET method to print out all plugin information loaded in the kernel:

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.SemanticKernel;
    using Microsoft.AspNetCore.Mvc;
    using Core.Utilities.Models;
    using Core.Utilities.Extensions;
    
    namespace Controllers;
    
    [ApiController]
    [Route("sk")]
    public class PluginInfoController : ControllerBase {
    
        private readonly Kernel _kernel;
    
        public PluginInfoController(Kernel kernel)
        {
            _kernel = kernel;
        }
    
        /// <summary>
        /// Get the metadata for all the plugins and functions.
        /// </summary>
        /// <returns></returns>
        [HttpGet("/puginInfo/metadata")]
        public async Task<IList<PluginFunctionMetadata>> GetPluginInfoMetadata()
        {
            var functions = _kernel.Plugins.GetFunctionsMetadata().ToPluginFunctionMetadataList();
            return functions;
        }
    }
    

Running the Backend API locally

  1. To run API locally first copy valid appsettings.json from completed Lessons/Lesson3 into backend directory:

    #cd into backend directory
    cd ../
    cp ../../Lessons/Lesson3/appsettings.json .
    
  2. Next run using dotnet run:

    dotnet run
    ...
    info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
    
  3. Application will start on specified port (port may be different). Navigate to http://localhost:5000 or corresponding forwarded address (if using Github CodeSpace) and it should redirect you to the swagger UI page.

  4. You can either test the chat endpoint using the "Try it out" feature from within Swagger UI, or via command line using curl command:

    curl -X 'POST' \
    'http://localhost:5000/chat' \
    -H 'accept: text/plain' \
    -H 'Content-Type: application/json' \
    -d '{
    "inputMessage": "What is Microsoft stock sentiment?",
    "messageHistory": [
    ]
    }'
    
  5. You can also test the pluginInfo/metadata endpoint using the "Try it out" feature from within Swagger UI, or via command line using curl command:

    curl -X 'GET' \
    'http://localhost:5000/pluginInfo/metadata' \
    -H 'accept: text/plain'