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:
-
Start by creating a new directory:
-
Next create a new SDK .NET project:
-
Build project to confirm it is successful:
-
Add the following nuget packages:
-
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();
-
Next we need to create
Extensions
directory to and add service extensions: -
In the
Extensions
directory create aServiceExtensions.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; }); } }
-
Next we need to create a
Controllers
directory to add REST API controller classes: -
Within the
Controllers
directory create aChatController.cs
file which exposes a reply method mapped to thechat
path and theHTTP 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; } }
-
Within the
Controllers
directory create aPluginInfoController.cs
file which exposes a method mapped to the/puginInfo/metadata
path and theHTTP 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¶
-
To run API locally first copy valid
appsettings.json
from completedLessons/Lesson3
intobackend
directory: -
Next run using
dotnet run
: -
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.
-
You can either test the
chat
endpoint using the "Try it out" feature from within Swagger UI, or via command line usingcurl
command: -
You can also test the
pluginInfo/metadata
endpoint using the "Try it out" feature from within Swagger UI, or via command line usingcurl
command: