Create A Powerful Chatbot Using Azure OpenAI With .NET 6 And Angular 16

Introduction 

Azure OpenAI is a cloud-based service offered by Microsoft Azure that gives access to OpenAI's powerful AI models, including GPT (Generative Pretrained Transformer), Codex, and DALL-E. This collaboration brings OpenAI's advanced natural language processing and generation tools into the Azure cloud platform, giving businesses and developers the ability to integrate AI capabilities into their applications and services while receiving help from the security and enterprise-grade capabilities of Microsoft Azure. 

Customers of Azure OpenAI service can use these AI models for various tasks such as language translation, content generation, code autocompletion, and more, scaling their usage according to their needs while using Microsoft's infrastructure. This service emphasizes responsible AI use and includes features to promote safe and ethical applications of the technology. 

Azure OpenAI Service supplies REST API access to OpenAI's powerful language models including the GPT-4, GPT-3.5-Turbo, and Embeddings model series. In addition, the new GPT-4 and GPT-3.5-Turbo model series have now reached general availability. These models can be easily adapted to your task including content generation, summarization, semantic search, and natural language to code translation. Users can access the service through REST APIs, Python SDK, or our web-based interface in the Azure OpenAI Studio. 

How do we get access to Azure OpenAI? 

Azure Open AI access is currently limited as Microsoft navigates high demand, upcoming product improvements, and Microsoft’s commitment to responsible AI. For now, Microsoft is working with customers with an existing partnership with Microsoft, lower risk use cases, and those committed to incorporating mitigations. 

More specific information is included in the application form.  

Apply here for getting access to Azure OpenAI resources: 

Apply now 

In this post, we will see all the steps to create an Azure OpenAI resource and create a deployment with any of the existing OpenAI models. We will create a .NET 6 Web API and use Azure OpenAI SDK to create chat service. We will also create an Angular 16 application and consume .NET 6 Web API and build a powerful chatbot. 

Create an Open AI resource in Azure portal. 

We can create an Open AI resource in Azure portal now. 

Click the Create button and continue further. 

As we discussed earlier, one must request and fill in a form to get permission from Microsoft to create Azure OpenAI resources. It will take 10 to 14 days to get approval from Microsoft.  

Choose a resource/create a new resource and choose a valid name for our resource. Currently Microsoft gives standard S0 pricing tier for Azure OpenAI resources. 

After creating the resource, we can go to Keys and Endpoint section and get the values. We will be using this key and endpoint later in our .NET 6 Web API application. 

Create a new Deployment with GPT models. 

Goto the Azure OpenAI Studio and choose deployments tab. 

In our plan, Micrsoft has given four different models to create a deployment. We can choose any of the base models. 

Please choose Advance options to select the tokens per minute rate limit. Microsoft allows 240K tokens per minute quota available for entire OpenAI resources created under a single subscription. Here, I have chosen 120K tokens per minute. So that in the same subscription, I can make one or more resources or deployments easily.  

Create .NET 6 Web API using Visual Studio 2022. 

Create a Web API using .NET 6 SDK. 

Add below NuGet library to the project. 

  • Azure.AI.OpenAI 

As of today (12th November 2023), this library is still in preview mode. Hopefully, Microsoft will release a stable version of the library soon.  

We can add the configurations below to the appsettings.json file. 

appsettings.json 

{
  "AzureOpenAI": {
    "OpenAIUrl": "https://sarathlal-open-ai.openai.azure.com/",
    "OpenAIKey": "0fe032d5b5fb4b9683092c48d0f2dfe9",
    "OpenAIDeploymentModel": "sarathlal-deployment"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Create the Message model class. 

Message.cs 

namespace AzureOpenAI.NET6;

public class Message
{
    public long TimeStamp { get; set; }
    public string? Body { get; set; }
    public bool IsRequest { get; set; }
    public Message(string body, bool isRequest)
    {
        TimeStamp = ((DateTimeOffset)DateTime.Now).ToUnixTimeMilliseconds();
        Body = body;
        IsRequest = isRequest;
    }
}

Message is having a property IsRequest and this will decide whether the request is an input from user or not.  

We can create our main service ChatService and add important logic inside it.  

ChatService.cs 

using Azure;
using Azure.AI.OpenAI;

namespace AzureOpenAI.NET6;

public class ChatService
{
    private readonly IConfiguration _configuration;

    private readonly string systemMessage = "You are an AI assistant that helps people find information about Sports.  " +
        "For anything other than Sports, respond with 'I can only answer questions about Sports.'";

    public ChatService(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    public async Task<Message> GetResponse(List<Message> messagechain)
    {
        OpenAIClient client = new(
            new Uri(_configuration.GetSection("AzureOpenAI")["OpenAIUrl"]!),
            new AzureKeyCredential(_configuration.GetSection("AzureOpenAI")["OpenAIKey"]!));


        ChatCompletionsOptions options = new()
        {
            Temperature = (float)0.7,
            MaxTokens = 800,
            NucleusSamplingFactor = (float)0.95,
            FrequencyPenalty = 0,
            PresencePenalty = 0,
            DeploymentName = _configuration.GetSection("AzureOpenAI")["OpenAIDeploymentModel"]!
        };
        options.Messages.Add(new ChatMessage(ChatRole.System, systemMessage));
        foreach (var msg in messagechain)
        {
            if (msg.IsRequest)
            {
                options.Messages.Add(new ChatMessage(ChatRole.User, msg.Body));
            }
            else
            {
                options.Messages.Add(new ChatMessage(ChatRole.Assistant, msg.Body));
            }
        }

        Response<ChatCompletions> resp = await client.GetChatCompletionsAsync(options);

        ChatCompletions completions = resp.Value;

        string response = completions.Choices[0].Message.Content;
        Message responseMessage = new(response, false);
        return responseMessage;
    }
}

Please note that we have added a system message.  

  private readonly string systemMessage = "You are an AI assistant that helps people find information about Sports.  " +
        "For anything other than Sports, respond with 'I can only answer questions about Sports.'";

This system message will append to every request, and this will handle the scope of the chatbot. Currently, we have chosen sports as the desired subject. Hence, users can’t ask any non-sports related questions.  

When the user starts chatting, all the requests and responses are kept in the same message array and pass to Backend API from Angular.  

Create ChatController and add below logic inside it.  

ChatController.cs 

using Microsoft.AspNetCore.Mvc;

namespace AzureOpenAI.NET6.Controllers;

[Route("api/[controller]")]
[ApiController]
public class ChatController : ControllerBase
{
    private readonly ChatService _service;
    public ChatController(ChatService service)
    {
        _service = service;
    }
    [HttpPost("PostMessage")]
    public async Task<ActionResult> PostMessage([FromBody] List<Message> messages)
    {
        Message response = await _service.GetResponse(messages);
        messages.Add(response);
        return Ok(messages);
    }
}

We can inject the dependency for ChatService in Program class. Also add the CORS entries.  

Program.cs 

builder.Services.AddSingleton<ChatService>();

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        builder =>
        {
            builder.WithOrigins("http://localhost:4200")
                   .AllowAnyHeader()
                   .AllowAnyMethod();
        });
});

.....
app.UseCors();

We have completed the backend code. We can create Angular 16 project add services and components.  

Create Angular 16 project using Angular CLI 

ng new AzureOpenAIAngular16 

We can choose the default routing and styling options and continue.  

Create Message model class same as backend.  

message.model.ts 

export class Message {
    timeStamp: number;
    body: string;
    isRequest!: boolean;
    constructor(body: string, isRequest: boolean) {
      this.timeStamp = Date.now();
      this.body = body;
      this.isRequest = isRequest;
    }
  }
  

Create loading service and add code given below.  

ng generate service Loading  

loading.service.ts 

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private _isLoading = new BehaviorSubject<boolean>(false);

  get isLoading$() {
    return this._isLoading.asObservable();
  }

  showLoader() {
    this._isLoading.next(true);
  }

  hideLoader() {
    this._isLoading.next(false);
  }
}

Now, we can create loading component. 

ng generate component loading  

Copy the code below to component class, template and style files.  

loading.component.ts 

import { Component } from '@angular/core';
import { LoadingService } from '../loading.service';

@Component({
  selector: 'app-loading',
  templateUrl: './loading.component.html',
  styleUrls: ['./loading.component.css']
})
export class LoadingComponent {
  isLoading$ = this.loadingService.isLoading$;

  constructor(private loadingService: LoadingService) { }
}

loading.component.html 

<div *ngIf="isLoading$ | async" class="loader-overlay">
    <div class="small progress">
        <div></div>
    </div>
</div>

loading.component.css 

.loader-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .progress {
      position: relative;
      width: 5em;
      height: 5em;
      margin: 0 0.5em;
      font-size: 12px;
      text-indent: 999em;
      overflow: hidden;
      -webkit-animation: progress_ani 1s infinite steps(8);
      animation: progress_ani 1s infinite steps(8);
    background: none;
  }
  
  .small.progress {
      font-size: 8px;
  }
  
  .progress:after,
  .progress:before,
  .progress > div:after,
  .progress > div:before {
      content: "";
      position: absolute;
      top: 0;
      left: 2.25em;
      width: 0.5em;
      height: 1.5em;
      border-radius: 0.2em;
      background: #eee;
      box-shadow: 0 3.5em #eee;
      -webkit-transform-origin: 50% 2.5em;
      transform-origin: 50% 2.5em;
  }
  
  .progress:before {
      background: #555;
  }
  
  .progress:after {
      -webkit-transform: rotate(-45deg);
      transform: rotate(-45deg);
      background: #777;
  }
  
  .progress > div:before {
      -webkit-transform: rotate(-90deg);
      transform: rotate(-90deg);
      background: #999;
  }
  
  .progress > div:after {
      -webkit-transform: rotate(-135deg);
      transform: rotate(-135deg);
      background: #bbb;
  }
  
  @-webkit-keyframes progress_ani {
      to {
          -webkit-transform: rotate(1turn);
          transform: rotate(1turn);
      }
  }
  
  @keyframes progress_ani {
      to {
          -webkit-transform: rotate(1turn);
          transform: rotate(1turn);
      }
  }
  

Create ChatService and add the code below. 

ng generate service chat 

chat.service.ts 

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Message } from './message.model';
@Injectable({
  providedIn: 'root'
})
export class ChatService {
  private apiUrl = 'https://localhost:5000/api/chat/postmessage'; // .NET backend URL

  constructor(private http: HttpClient) { }

  sendMessage(messages: Message[]) {
    return this.http.post<Message[]>(this.apiUrl, messages );
  }
}

Import FormsModule and HttpClientModule in App Module.  

app.module.ts 

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { LoadingComponent } from './loading/loading.component';

@NgModule({
  declarations: [
    AppComponent,
    LoadingComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Replace App Component class, template and stylesheet files with below code.  

app.component.ts 

import { AfterViewChecked, ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { ChatService } from './chat.service';
import { Message } from './message.model';
import { LoadingService } from './loading.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements AfterViewChecked {
  @ViewChild('scrollContainer') private scrollContainer!: ElementRef;
  messages: Message[] = [];
  newMessage = '';

  constructor(
    private chatService: ChatService,
    private loadingService:LoadingService,
    private cd: ChangeDetectorRef
  ) {}

  ngAfterViewChecked() {
    this.scrollToBottom();
  }

  scrollToBottom(): void {
    try {
      this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight;
    } catch (err) {}
  }

  sendMessage() {
    this.messages.push(new Message(this.newMessage, true));
    this.loadingService.showLoader();
    this.chatService.sendMessage(this.messages).subscribe({
      next: (response) => {
        console.log('Response received successful');
        this.messages = response;
        this.newMessage = '';
        this.cd.detectChanges();
        this.loadingService.hideLoader();
      },
      error: (err) => {
        console.error('Upload error:', err);
        this.cd.detectChanges();
        this.loadingService.hideLoader();
      },
      complete: () => console.info('Request completed'),
    });
  }
}

app.component.html 

<div class="content" role="main">
  <app-loading></app-loading>
</div>

<div class="chatbot-container">
  <div id="header">
    <h1>Chatbot with Azure OpenAI</h1>
  </div>
  <div id="chatbot">
    <div id="conversation">
      <div class="chatbot-message">
        <p class="chatbot-text">Hi! 👋 Welcome to Azure Open AI Chatbot</p>
      </div>
      <div #scrollContainer class="scroll-container">
        <div *ngFor="let message of messages">
          <div *ngIf="message.isRequest == true">
            <div class="chatbot-message user-message">
              <p class="chatbot-text">{{ message.body }}</p>
              <span class="chat-time">{{ message.timeStamp | date : "shortTime" }}</span>
            </div>
          </div>
          <div *ngIf="message.isRequest == false">
            <div class="chatbot-message chatbot">
              <p class="chatbot-text">{{ message.body }}</p>
              <span class="chat-time">{{ message.timeStamp | date : "shortTime" }}</span>
            </div>
          </div>
        </div>
      </div>

    </div>
    <form id="input-form">
      <div class="message-container">
        <input
          id="input-field"
          type="text"
          placeholder="Type your message here"
          [(ngModel)]="newMessage"
          name="newMessage"
        />
        <button id="submit-button" type="submit" (click)="sendMessage()">
          <img class="send-icon" src="../assets/send-message.png" alt="" />
        </button>
      </div>
    </form>
  </div>
</div>

app.component.css 


.scroll-container {
    max-height: 200px;
    overflow-y: auto;
  }
  
  .content {
    display: flex;
    margin: 82px auto 32px;
    padding: 0 16px;
    max-width: 960px;
    flex-direction: column;
    align-items: center;
  }
  
  /* Responsive Styles */
  @media screen and (max-width: 767px) {
    .card-container > *:not(.circle-link),
    .terminal {
      width: 100%;
    }
  
    .card:not(.highlight-card) {
      height: 16px;
      margin: 8px 0;
    }
  
    .card.highlight-card span {
      margin-left: 72px;
    }
  
    svg#rocket-smoke {
      right: 120px;
      transform: rotate(-5deg);
    }
  }
  
  @media screen and (max-width: 575px) {
    svg#rocket-smoke {
      display: none;
      visibility: hidden;
    }
  }
  

Please note that we have added some styles for the chatbot in style.css file as well.  

style.css 

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap');

body {
  display: flex;
  justify-content: center;
  font-family: 'Roboto', sans-serif;
  background: #cab5ed;

}

.chatbot-container {
  width: 900px;
  margin: 0 auto;
  background-color: #f5f5f5;
  border: 1px solid #cccccc;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

#chatbot {
  background-color: #f5f5f5;
  border: 1px solid #eef1f5;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
  border-radius: 4px;
}

#header {
  background-color: #3047ec;
  color: #ffffff;
  padding: 20px;

}

#header h1 {
  font-size: 24px;
  font-weight: 500;
  margin: 0;
}

message-container {
  background: #ffffff;
  width: 100%;
  display: flex;
  align-items: center;
}

#conversation {
  height: 300px;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
}

@keyframes message-fade-in {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.chatbot-message {
  display: flex;
  align-items: flex-start;
  position: relative;
  font-size: 16px;
  line-height: 20px;
  border-radius: 20px;
  word-wrap: break-word;
  white-space: pre-wrap;
  max-width: 100%;
  padding: 0 15px;
}

.user-message {
  justify-content: flex-end;
}

.chatbot-text {
  background-color: white;
  color: #333333;
  font-size: 16px;
  padding: 15px;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

#input-form {
  display: flex;
  align-items: center;
  border-top: 1px solid #eef1f5;
}

#input-field {
  flex: 1;
  height: 60px;
  border: 1px solid #eef1f5;
  border-radius: 4px;
  padding: 0 10px;
  font-family: 'Roboto', sans-serif;
  font-size: 16px;
  transition: border-color 0.3s;
  background: #ffffff;
  color: #333333;
  border: none;
}

.send-icon {
  margin-right: 10px;
  cursor: pointer;
}

#input-field:focus {
  border-color: #333333;
  outline: none;
}

#submit-button {
  background-color: transparent;
  border: none;
}

p[sentTime]::after {
  content: attr(sentTime);
  position: absolute;
  top: -3px;
  font-size: 14px;
  color: gray;
}

.chatbot p[sentTime]::after {
  left: 15px;
}

.user-message p[sentTime]::after {
  right: 15px;
}

.chat-time {
  position: absolute;
  font-size: 10px;
  margin-top: -3px;
  color: gray;
}

::-webkit-scrollbar {
  width: 10px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background: #888;
}

::-webkit-scrollbar-thumb:hover {
  background: #555;
}

.chatbot-message.chatbot .chatbot-text {
  background-color: #f5f5f5 !important;
}

.message-container {
  background: #ffffff;
  width: 100%;
  display: flex;
  align-items: center;
}

We have completed the Angular coding also. We can run both backend and frontend applications.  

Please note that we have chosen System message for ChatRole.System as sports. Hence, users can ask only sports related questions.  

We can ask any questions related to sports. 

Ask any questions related to the same context. Every time we ask a question, all the earlier questions and answers are also passed to backend from Angular app. If you refresh the screen and ask this second question, we can see a different response now. 

Because we have cleared all the earlier messages by refreshing the screen and now asked this question. Hence, OpenAI can’t understand the context correctly.  

Now, we can ask a non-sports related question.  

Though we have asked a very general question, OpenAI can’t answer it because it is a non-sports related question.  

This is an extremely basic example of a chatbot created with Azure OpenAI. We can use more features of Azure OpenAI and create more powerful chatbots.  

Conclusion 

In this post, we have seen all the steps to create an Azure OpenAI resource in Azure portal and create a deployment under existing Azure OpenAI models. We have created a chatbot application with .NET 6 and Angular 16. We have used “Azure.AI.OpenAI” NuGet library to create chat service in .NET 6. We have consumed this chat service from Angular 16 app and created a powerful chatbot.