Seamless Integration: Enhancing your Static Web App by adding an Azure Functions backed AI Chatbot
Published Mar 11 2024 07:41 PM 2,156 Views
Microsoft

Introduction

Incorporating an AI chatbot into your static web app has never been easier, thanks to the seamless integration offered by Azure's ecosystem. Azure Static Web Apps, paired with the robust capabilities of Azure Functions and OpenAI's sophisticated AI models, simplifies the deployment and management of conversational AI agents. This powerful combination removes the traditional complexities associated with developing and integrating AI chatbots, allowing developers to enhance their websites with intelligent, interactive conversations effortlessly. Whether you're looking to improve user engagement, provide instant support, or gather user feedback through natural language interactions, Azure provides an all-in-one solution to bring your chatbot to life with minimal hassle.

 

Prerequisites

 

 

Step-by-Step Guide

For this walkthrough, we will showcase how to seamlessly integrate a chatbot into a bookshop website, elevating user engagement by offering personalized book recommendations. 🪄
 

Deploy an Azure Function App with OpenAI Extension

This guide will help you deploy a chatbot using the Azure Functions OpenAI Extension. Developed by Azure and available as a NuGet package, this extension simplifies the process of getting your chatbot APIs operational within minutes.
 

Getting Started

 

1. Clone the Extension and Sample Repository: Start by cloning the repository from https://github.com/Azure/azure-functions-openai-extension/. To better understand how this integration works I also recommend reading the docs regarding the general requirements as well as the chatbot sample specific instructions. 

2. Navigate to the Sample Chatbot Project: Once cloned, navigate to the sample chatbot project located at samples/chat/nodejs. We will use this sample project to deploy to our Azure Function App and expose our chatbot endpoints that will later on be consumed by our Static Web App.

3. Configure Environment Variables for testing: To connect to your OpenAI resource locally during testing, configure the required environment variables in the samples/chat/nodejs/local.settings.json file. Depending on your setup, use one of the following templates:

  • For Azure OpenAI resource you will have to add CHAT_MODEL_DEPLOYMENT_NAME, AZURE_OPENAI_KEY and AZURE_OPENAI_ENDPOINT:
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "CHAT_MODEL_DEPLOYMENT_NAME":  "<your-deployment_name>",
    "AZURE_OPENAI_KEY": "<your_key_here>",
    "AZURE_OPENAI_ENDPOINT": "https://<your-openai-endpoint>.openai.azure.com/" 
  }
}
  • Or, for direct OpenAI API access you will have to add OPENAI_KEY. You can also override the CHAT_MODEL_DEPLOYMENT_NAME if needed:
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "CHAT_MODEL_DEPLOYMENT_NAME":  "gpt-3.5-turbo"
    "OPENAI_KEY": "<your_openai_key_here>"     
  }
}

 4. Start the Application Locally: Test your chatbot by running it locally. Use the following commands:

azurite --silent --location c:\azurite --debug c:\azurite\debug.log --blobPort 8888 --queuePort 9999 --tablePort 11111

npm install && dotnet build --output bin && npm run build && npm run start
This will start up the chat bot for you and should log the following:
anninakeller_0-1710205754861.png
5. Interact with Your Chatbot via REST API: You can now interact with your chatbot via the REST API. To create a new chat session, send messages and list all messages with responses, use the following examples:
  • Create a chat session:
PUT http://localhost:7071/api/chats/TailoredTales

{
    "instructions": "You are TailoredTales, a guide in the quest for the perfect read. Respond with brief, engaging hints, leading seekers to books that fit as if destined, blending curiosity with the promise of a great discovery."
}
  • To send messages to the bot, execute the following request:
POST http://localhost:7071/api/chats/TailoredTales

What's a captivating book that blends mystery with a touch of magic, ideal for someone who loves both genres and is looking for a new book?
  • To list all questions with answers for later use in populating your chatbot window, execute the following:
GET http://localhost:7071/api/chats/TailoredTales?timestampUTC=2023-11-9T22:00
Now, you're all set to deploy your chatbot to an Azure Functions App.
 

Deploy to an Azure Function App

Now that we have our code ready, it's time to deploy it to an Azure Function app. The process is straightforward, with just a few additional steps to ensure everything runs smoothly. Here's how you can deploy to a Function app:
 
1. Change function auth level to anonymous: Auth will fully be handled by the Static Web Apps Linked Backends integration. Change all authLevel in samples/chat/src/functions/app.ts from function to anonymous.
2. Choose Your Deployment Method: Azure offers various deployment methods, making it flexible to deploy your code. You can explore different options in the Azure Functions Deployment Technologies documentation. You will simply want to deploy the chatbot sample project. 
3. Setting Environment Variables: When deploying, it's essential to configure the necessary environment variables for your Function app. You will need to add any variables also needed for local testing like the AZURE_OPENAI_ENDPOINT as well as the  CHAT_MODEL_DEPLOYMENT_NAME. Learn how to set application settings like these in the Azure Function App Settings guide. :light_bulb: Instead of setting the AZURE_OPEN_AI_KEY or uploading the secret to the a Key Vault you can simply add a System Managed Identity and assign the user/function app Cognitive Services OpenAI User role on the Azure OpenAI resource.
4. Testing on Your Function: Once deployed, you can perform the same operations as tested locally, but this time against your function host in Azure. The setup ensures that your chatbot works seamlessly in the Azure environment.
 
By following these steps, you'll have your chatbot up and running on Azure Function App, ready to interact with users in a production environment.
 

Develop and deploy the front end to Azure Static Web Apps

The next thing we want to do is create a simple React.js app that uses these endpoints to render a chat window into our web app.
 

Let's create a new React app

 

1. Set Up the Project: Create a new project for your front-end app. Initialize it with the chosen framework or tools. For this demo we will be using React.js. We will also install axios to make HTTP requests to interact with our chatbot backend. Feel free to use any other technology you are comfortable with.

npx create-react-app chatbot-app

cd chatbot-app

npm install axios
2. Create the ChatService.js: This file is responsible for facilitating the integration between the frontend application and the chatbot's backend API using the axios library.
  • It can create a new chat session by calling the createChat API.
  • It can post messages to an existing chat session using the postMessage API.
  • It can retrieve the state of a chat session at a specific timestamp using the getChatState API.
  • The <function-hostname>  in the BASE_URL is only required for local testing. As soon as we have linked the deployed Static Web App to our Function App the integration will fully handle routing and authentication for us and we can simple make calls to the Static Web Apps hostname by changing the BASE_URL to /api/chats. For now replace the value with either the local host running the chatbot app or your functions hostname (In that case you will have to also configure CORS on the function app). 
import axios from 'axios';

const BASE_URL = '<function-hostname>/api/chats/';

export const createChat = async (chatId, instructions) => {
  try {
    const response = await axios.put(`${BASE_URL}${chatId}`, {
      instructions,
    });
    return response.data;
  } catch (error) {
    console.error("Error creating chat:", error);
    throw error;
  }
};

export const postMessage = async (chatId, message) => {
  try {
    await axios.post(`${BASE_URL}${chatId}`, message, {
      headers: {
        'Content-Type': 'text/plain',
      },
    });
  } catch (error) {
    console.error("Error posting message:", error);
    throw error;
  }
};

export const getChatState = async (chatId, timestampUTC) => {
  try {
    const response = await axios.get(`${BASE_URL}${chatId}`, {
      params: { timestampUTC },
    });
    return response.data;
  } catch (error) {
    console.error("Error getting chat state:", error);
    throw error;
  }
};

3. Creating the Chat Box Component: We are creating a component responsible for creating a chat interface and handling chat interactions with a chatbot backend. Here's a summary of its functionality:

  • State Management: It uses React state hooks to manage the following state variables:
    • chatId: Stores the unique identifier for the chat session.
    • message: Stores the user's input message.
    • messages: Stores the chat messages exchanged between the user and the chatbot.
    • lastUpdate: Keeps track of the timestamp of the last chat message update.
  • Initialization: When the component mounts, it initializes a chat session by calling the createChat function from the ChatService and sets the chatId.
  • Polling for Messages: It sets up a polling mechanism (pollForMessages) to check for new chatbot responses at regular intervals. When new responses are received, they are added to the messages state.
  • Message Submission: When the user submits a message, it adds the user's message to the chat interface, calls the postMessage function to send the message to the chatbot backend, and triggers the polling for new chatbot responses.
  • Scrolling: It ensures that the chat interface automatically scrolls to display the latest messages at the bottom.
  • Rendered Elements: It renders a chat interface with the chat messages and an input field for users to type their messages.
  • Conditional Rendering: Messages from the chatbot are displayed with an "Assistant" label.
import React, { useState, useEffect, useRef } from 'react';
import { createChat, postMessage, getChatState } from './ChatService';
import './ChatBox.css';

const ChatBox = () => {
  const [chatId, setChatId] = useState('');
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);
  const [lastUpdate, setLastUpdate] = useState(new Date().toISOString());

  const messagesEndRef = useRef(null);

  useEffect(() => {
    const initChat = async () => {
      const chatSessionId = `chat_${new Date().getTime()}`;
      const instructions = "You are TailoredTales, a guide in the quest for the perfect read. Respond with brief, engaging hints, leading seekers to books that fit as if destined, blending curiosity with the promise of a great discovery.";
      setChatId(chatSessionId);
      await createChat(chatSessionId, instructions);
    };

    initChat();
  }, []);

  const pollForMessages = async () => {
    const maxPollingDuration = 10000;
    const pollingInterval = 200;
    let totalPollingTime = 0;
    
    const poll = setInterval(() => {
      getChatState(chatId, lastUpdate).then(data => {
        if (data !== null) {
          const assistantMessages = data.RecentMessages.filter(msg => msg.Role === 'assistant');
          if (assistantMessages.length > 0) {
            setMessages(prevMessages => [...prevMessages, ...assistantMessages]);
            setLastUpdate(new Date().toISOString());
            clearInterval(poll);
          }
        }
      });
  
      totalPollingTime += pollingInterval;
      if (totalPollingTime >= maxPollingDuration) {
        clearInterval(poll);
      }
    }, pollingInterval);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (message) {
      const tempMessage = {
        Content: message,
        Role: 'user',
        id: new Date().getTime(),
      };
      setMessages(prevMessages => [...prevMessages, tempMessage]);
      await postMessage(chatId, message);
      setMessage('');
      pollForMessages();
    }
  };

  useEffect(() => {
    const messagesContainer = messagesEndRef.current;
    if (messagesContainer) {
      messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }
  }, [messages]);
  
  return (
    <div className="chatbox-container">
      <div className="chatbox-header">
        Tailored Tales
      </div>
      <div className="chatbox-messages" ref={messagesEndRef}>
        {messages.map((msg, index) => (
          <div key={index} className={`message ${msg.Role}`}>
            {msg.Role === 'assistant' && <div className="message-role">Assistant</div>}
            <span>{msg.Content}</span>
          </div>
        ))}
      </div>
      <form onSubmit={handleSubmit} className="chatbox-form">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type here..."
          className="chatbox-input"
        />
      </form>
    </div>
  );
};

export default ChatBox;
4. Render the Chat Box: Don't forget to add your new chat box component and render it in you App.js.
import React from 'react';
import ChatBox from './ChatBox';

function App() {
  return (
    <div className="App">
      <ChatBox />
    </div>
  );
}

export default App;
5. Add some styling: To make our chatbot also look good we can add this CSS file to pin the chat box to the bottom right corner and add some basic styling.
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
* {
  font-family: 'Inter', sans-serif;
}

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

.chatbox-container {
  position: fixed;
  bottom: 10px;
  right: 10px;
  width: 320px;
  height: 600px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background-color: #fff;
  border-radius: 16px;
  box-shadow: rgba(9, 30, 66, 0.25) 0px 1px 1px, rgba(9, 30, 66, 0.13) 0px 0px 1px 1px;
  font-family: 'Inter', sans-serif;
  padding-top: 0;
}

.chatbox-header {
  padding: 12px 20px;
  border-top-left-radius: 8px;
  border-top-right-radius: 8px;
  font-size: 1.2em;
  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05);
  text-align: left;
  font-weight: 900;
}

.chatbox-messages {
  flex-grow: 1;
  padding: 15px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
  margin-top: 0;
  scrollbar-width: none;
}

.message {
  max-width: 75%;
  word-wrap: break-word;
  padding: 10px 14px;
  border-radius: 18px;
  line-height: 1.4;
  position: relative;
  margin-bottom: 4px;
}

.message.user {
  align-self: flex-end;
  background-color: #5851ff;
  color: #fff;
  border-bottom-right-radius: 4px;
}

.message.assistant {
  align-self: flex-start;
  background-color: #efefef;
  color: #333;
  border-bottom-left-radius: 4px;
}

.chatbox-form {
  display: flex;
  padding: 10px 15px;
  background-color: #ffffff;
  box-shadow: 0 -2px 2px rgba(0, 0, 0, 0.1);
  border-bottom-left-radius: 16px;
  border-bottom-right-radius: 16px;
}

.chatbox-input {
  flex-grow: 1;
  margin-right: 8px;
  padding: 10px;
  border: 0px solid #d1d1d4;
  border-radius: 18px;
  background-color: #ffffff;
  outline: none;
}

.message-role {
  font-size: 0.7rem;
  color: #6c757d;
  margin-bottom: 2px;
}

 

Deploy Your React App to Azure Static Web Apps

To make your bookshop website accessible to your users, you'll need to deploy your React app to Azure Static Web Apps (Quickstart: Build your first static web app). Follow these steps, including creating a GitHub repository and setting up continuous integration
 
1. Create a GitHub Repository: If your React app isn't already in a GitHub repository, create a new one. You can follow GitHub's guide on creating a new repository.
2. Push Your Code to GitHub: Push your React app's code to the newly created GitHub repository. You can use Git commands or a Git client for this.
3. Access Azure Portal: Log in to the Azure Portal.
4. Create a Static Web App: Navigate to Azure Static Web Apps in the Azure Portal and create a new static web app by specifying the source control as your GitHub repository.
5. Access you site: Once the deployment is finished you can access your site through the automatically created default hostname. You can check teh status of your deployment in your GitHub action run.
 

Link your Function App to your Static Web Apps

Simplifying the integration process, Azure offers the "Azure Static Web Apps Linked Backends" feature (Bring your own functions to Azure Static Web Apps documentation), allowing you to seamlessly link your static web application to backend services, including your Azure Function App where the chatbot logic resides. Follow these steps to set up the integration effortlessly:
 
1. Access Azure Portal: Log in to the Azure Portal.
2. Select Your Static Web App: Locate and select the Azure Static Web App that hosts your chatbot website.
3. Navigate to APIs: In the Static Web App settings, find the "APIs" section.
4. Add a Backend: Click the "Link" button to configure the integration.
5. Select Your Function App: Choose your Azure Function App from the available list of backends.

anninakeller_0-1710207110257.png
You are all done! 🥳

You can now go to your static web app and start asking your Tailored Tales bot anything about books.

 

anninakeller_0-1710210588432.png

 

Links

 

Annina Keller is a software engineer on the Azure Static Web Apps team. (Twitter: @anninake)

 

Co-Authors
Version history
Last update:
‎Mar 11 2024 07:40 PM
Updated by: