Simple Chat: This simple chat application utilizes OpenAI's language models to generate real-time completion responses.
Documents QA Chat: This chat application goes beyond simple conversations. Users can upload up to 10.pdfand.docxdocuments, which are then processed to create vector embeddings. These embeddings are stored inChromaDBfor efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response.
Asystemnode pool in a dedicated subnet. The default node pool hosts only critical system pods and services. The worker nodes have node taint which prevents application pods from beings scheduled on this node pool.
Ausernode pool hosting user workloads and artifacts in a dedicated subnet.
Azure OpenAI Service: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with the security and enterprise promise of Azure. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
User-defined Managed Identity: a user-defined managed identity used by the AKS cluster to create additional resources like load balancers and managed disks in Azure.
Azure Virtual Machine: Terraform modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
Azure Bastion Host: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
Azure NAT Gateway: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
Azure Storage Account: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
Azure Container Registry: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Azure Monitor workspace: AnAzure Monitor workspaceis a unique environment for data collected byAzure Monitor. Each workspace has its own data repository, configuration, and permissions. Log Analytics workspaces contain logs and metrics data from multiple Azure resources, whereas Azure Monitor workspaces currently contain only metrics related toPrometheus. Azure Monitor managed service for Prometheus allows you to collect and analyze metrics at scale using a Prometheus-compatible monitoring solution, based on thePrometheus. This fully managed service allows you to use thePrometheus query language (PromQL)to analyze and alert on the performance of monitored infrastructure and workloads without having to operate the underlying infrastructure. The primary method for visualizing Prometheus metrics isAzure Managed Grafana. You can connect yourAzure Monitor workspaceto anAzure Managed Grafanato visualize Prometheus metrics using a set of built-in and custom Grafana dashboards.
Azure Managed Grafana: anAzure Managed Grafanainstance used to visualize thePrometheus metricsgenerated by theAzure Kubernetes Service(AKS)cluster deployed by the Bicep modules.Azure Managed Grafanais a fully managed service for analytics and monitoring solutions. It's supported by Grafana Enterprise, which provides extensible data visualizations. This managed service allows to quickly and easily deploy Grafana dashboards with built-in high availability and control access with Azure security.
NGINX Ingress Controller: this sample compares the managed and unmanaged NGINX Ingress Controller. While the managed version is installed using theApplication routing add-on, the unmanaged version is deployed using theHelm Terraform Provider. You can use the Helm provider to deploy software packages in Kubernetes. The provider needs to be configured with the proper credentials before it can be used.
Cert-Manager: thecert-managerpackage andLet's Encryptcertificate authority are used to issue a TLS/SSL certificate to the chat applications.
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, which is designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
Azure OpenAI Service provides REST API access to OpenAI's powerful language models including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task including but not limited to 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.
TheChat Completion API, which is part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction an important skill to develop. For more information, seeIntroduction to prompt engineering.
Prompt construction can be difficult. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However it's important to understand that each model behaves differently, so the learnings may not apply equally to all models.
Prompt engineering refers to the process of creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
Vector Databases
Avector databaseis a specialized database that goes beyond traditional storage by organizing information to simplify the search for similar items. Instead of merely storing words or numbers, it leverages vector embeddings - unique numerical representations of data. These embeddings capture meaning, context, and relationships. For instance, words are represented as vectors, whereas similar words have similar vector values.
The applications of vector databases are numerous and powerful. In language processing, they facilitate the discovery of related documents or sentences. By comparing the vector embeddings of different texts, finding similar or related information becomes faster and more efficient. This capability benefits search engines and recommendation systems, which can suggest relevant articles or products based on user interests.
In the realm of image analysis, vector databases excel in finding visually similar images. By representing images as vectors, a simple comparison of vector values can identify visually similar images. This capability is highly valuable for tasks like reverse image search or content-based image retrieval.
Additionally, vector databases find applications in fraud detection, anomaly detection, and clustering. By comparing vector embeddings of data points, unusual patterns can be detected, and similar items can be grouped together, aiding in effective data analysis and decision-making. This is a list of Azure services that are suitable for use as a vector database in a retrieval-augmented generation (RAG) solution:
Azure Cosmos DB for MongoDB vCore: vCore-based Azure Cosmos DB for MongoDB provides developers with a fully managed MongoDB-compatible database service for building modern applications with a familiar architecture. Developers can enjoy the benefits of native Azure integrations, low total cost of ownership (TCO), and the familiar vCore architecture when migrating existing applications or building new ones. Azure Cosmos DB for MongoDB features built-in vector database capabilities enabling your data and vectors to be stored together for efficient and accurate vector searches.
Azure Cosmos DB for NoSQL: Azure Cosmos DB for NoSQL is a globally distributed database service designed for scalable and high performance applications. It offers an industry-leading 99.999% Service Level Agreement (SLA), ensuring high availability for your mission-critical applications. With sub-10ms point reads and instant autoscale, it provides lightning-fast data access and seamless scalability. Its flexible, schemaless data model allows for agile and adaptable application development. Moreover, Azure Cosmos DB’s built-in vector index using DiskANN enables fast, accurate, and cost-effective vector search at any scale, enhancing the efficiency and effectiveness of your data-driven applications.
Azure Cosmos DB for PostgreSQL You can use the natively integrated vector database in Azure Cosmos DB for PostgreSQL, which offers an efficient way to store, index, and search high-dimensional vector data directly alongside other application data. This approach removes the necessity of migrating your data to costlier alternative vector databases and provides a seamless integration of your AI-driven applications.
Azure Cache for Redis Azure Cache for Redis can be used as a vector database by combining it models like Azure OpenAI for Retrieval-Augmented Generative AI and analysis scenarios.
Here is a list of the most popular vector databases:
ChromaDBis a powerful database solution that stores and retrieves vector embeddings efficiently. It is commonly used in AI applications, including chatbots and document analysis systems. By storing embeddings in ChromaDB, users can easily search and retrieve similar vectors, enabling faster and more accurate matching or recommendation processes. ChromaDB offers excellent scalability high performance, and supports various indexing techniques to optimize search operations. It is a versatile tool that enhances the functionality and efficiency of AI applications that rely on vector embeddings.
Facebook AI Similarity Search (FAISS)is another widely used vector database. Facebook AI Research develops it and offers highly optimized algorithms for similarity search and clustering of vector embeddings. FAISS is known for its speed and scalability, making it suitable for large-scale applications. It offers different indexing methods like flat, IVF (Inverted File System), and HNSW (Hierarchical Navigable Small World) to organize and search vector data efficiently.
SingleStore: SingleStore aims to deliver the world’s fastest distributed SQL database for data-intensive applications: SingleStoreDB, which combines transactional + analytical workloads in a single platform.
Astra DB: DataStax Astra DB is a cloud-native, multi-cloud, fully managed database-as-a-service based on Apache Cassandra, which aims to accelerate application development and reduce deployment time for applications from weeks to minutes.
Milvus: Milvus is an open source vector database built to power embedding similarity search and AI applications. Milvus makes unstructured data search more accessible and provides a consistent user experience regardless of the deployment environment. Milvus 2.0 is a cloud-native vector database with storage and computation separated by design. All components in this refactored version of Milvus are stateless to enhance elasticity and flexibility.
Qdrant: Qdrant is a vector similarity search engine and database for AI applications. Along with open-source, Qdrant is also available in the cloud. It provides a production-ready service with an API to store, search, and manage points—vectors with an additional payload. Qdrant is tailored to extended filtering support. It makes it useful for all sorts of neural-network or semantic-based matching, faceted search, and other applications.
Pinecone: Pinecone is a fully managed vector database that makes adding vector search to production applications accessible. It combines state-of-the-art vector search libraries, advanced features such as filtering, and distributed infrastructure to provide high performance and reliability at any scale.
Vespa: Vespa is a platform for applications combining data and AI, online. By building such applications on Vespa helps users avoid integration work to get features, and it can scale to support any amount of traffic and data. To deliver that, Vespa provides a broad range of query capabilities, a computation engine with support for modern machine-learned models, hands-off operability, data management, and application development support. It is free and open source to use under the Apache 2.0 license.
Zilliz: Milvus is an open-source vector database, with over 18,409 stars on GitHub and 3.4 million+ downloads. Milvus supports billion-scale vector search and has over 1,000 enterprise users. Zilliz Cloud provides a fully-managed Milvus service made by the creators of Milvus. This helps to simplify the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure. As a DBaaS, Zilliz simplifies the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure.
Weaviate: Weaviate is an open-source vector database used to store data objects and vector embeddings from ML-models, and scale into billions of data objects from the same name company in Amsterdam. Users can index billions of data objects to search through and combine multiple search techniques, such as keyword-based and vector search, to provide search experiences.
LangChainis a software framework designed to streamline the development of applications usinglarge language models (LLMs). It serves as a language model integration framework, facilitating various applications like document analysis and summarization, chatbots, and code analysis.
LangChain's integrations cover an extensive range of systems, tools, and services, making it a comprehensive solution for language model-based applications. LangChain integrates with the major cloud platforms such as Microsoft Azure, Amazon AWS, and Google, and with API wrappers for various purposes like news, movie information, and weather, as well as support for Bash, web scraping, and more. It also supports multiple language models, including those from OpenAI, Anthropic, and Hugging Face. Moreover, LangChain offers various functionalities for document handling, code generation, analysis, debugging, and interaction with databases and other data sources.
Chainlit
Chainlitis an open-source Python package that is specifically designed to create user interfaces (UIs) for AI applications. It simplifies the process of building interactive chats and interfaces, making developing AI-powered applications faster and more efficient. While Streamlit is a general-purpose UI library, Chainlit is purpose-built for AI applications and seamlessly integrates with other AI technologies such asLangChain,LlamaIndex, andLangFlow.
With Chainlit, developers can easily create intuitive UIs for their AI models, including ChatGPT-like applications. It provides a user-friendly interface for users to interact with AI models, enabling conversational experiences and information retrieval. Chainlit also offers unique features, such as the ability to display theChain of Thought, which allows users to explore the reasoning process directly within the UI. This feature enhances transparency and enables users to understand how the AI arrives at its responses or recommendations.
For more information, see the following resources:
Managed NGINX Ingress Controller for Azure Kubernetes Service
One way to route HTTP and secure HTTPS traffic to applications in an Azure Kubernetes Service (AKS) cluster is by using the Kubernetes Ingress object. The application routing add-on for AKS enables you to create, configure, and manage one or more Ingress controllers within your AKS cluster using theNGINX Ingress Controller.
The application routing add-on with NGINX provides several features, including:
Easy configuration of managed NGINX Ingress controllers based on the Kubernetes NGINX Ingress controller.
Integration with Azure DNS for public and private zone management.
SSL termination with certificates stored in Azure Key Vault.
To enable theapplication routing add-onon an existing cluster, you can use Azure CLI, as shown in the following code snippet.
az aks approuting enable -g <ResourceGroupName> -n <ClusterName>
Once enabled, you can connect to your AKS cluster, deploy applications, and create Ingress objects with appropriate annotations for routing. There are some limitations to be aware of, such as the maximum number of supported Azure DNS zones and namespace editing restrictions. It's recommended to review theapplication routing add-on configurationfor additional information on SSL encryption and DNS integration.
If you are familiar with the NGINX ingress controller, you can just replace thenginxingress class name inside an ingress object with the name of the ingress controller deployed by the application routing addon, that, by default is equal towebapprouting.kubernetes.azure.com:
If you leveragecert-managerand with Let's Encrypt certificate authority to issue TLS/SSL certificates to your application, make sure to create an issuer or a cluster issuer for the ingress class of the managed NGINX ingress controller installed by the application routing add-on. This can be done using the sample code provided below:
Ensure that you replaceadmin@contoso.comwith your own email address to receive notifications from Let's Encrypt. By using this configuration,cert-managerwill be able to issue certificates for the ingress class of the managed NGINX ingress controller when using the application routing add-on. Please note that the server URLhttps://acme-v02.api.letsencrypt.org/directoryis the Let's Encrypt production server. You can also use the staging serverhttps://acme-staging-v02.api.letsencrypt.org/directoryfor testing purposes to avoid rate limits. Ensure that the issuer or cluster issuer resource is deployed to your Kubernetes cluster, andcert-manageris properly installed and configured. For more detailed steps and instructions, refer toManaged nginx Ingress with the application routing add-on.
Deploy the Terraform modules
Before deploying the Terraform modules in theterraformfolder, specify a value for the following variables in theterraform.tfvarsvariable definitions file.
name_prefix = "Contoso"
domain = "contoso.com"
kubernetes_version = "1.28.3"
namespace = "chainlit"
service_account_name = "chainlit-sa"
ssh_public_key = "XXXXXXX"
vm_enabled = true
location = "eastus"
admin_group_object_ids = ["XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"]
web_app_routing_enabled = true
dns_zone_name = "babosbird.com"
dns_zone_resource_group_name = "DnsResourceGroup"
grafana_admin_user_object_id = "0c5267b2-01f3-4a59-970e-0d9218d5412e"
vnet_integration_enabled = true
openai_deployments = [
{
name = "gpt-35-turbo-16k"
model = {
name = "gpt-35-turbo-16k"
version = "0613"
}
},
{
name = "text-embedding-ada-002"
model = {
name = "text-embedding-ada-002"
version = "2"
}
}
]
Description:
prefix: specifies a prefix for all the Azure resources.
domain: specifies the domain part (e.g., subdomain.domain) of the hostname of the ingress object used to expose the chatbot via theNGINX Ingress Controller.
kubernetes_version: specifies the Kubernetes version installed on the AKS cluster.
namespace: specifies the namespace of the workload application that accesses the Azure OpenAI Service.
service_account_name: specifies the name of the service account of the workload application that accesses the Azure OpenAI Service.
ssh_public_key: specifies the SSH public key used for the AKS nodes and jumpbox virtual machine.
vm_enabled: a boleean value that specifies whether deploying or not a jumpbox virtual machine in the same virtual network of the AKS cluster.
location: specifies the region (e.g., westeurope) where deploying the Azure resources.
admin_group_object_ids: when deploying an AKS cluster with Microsoft Entra ID and Azure RBAC integration, this array parameter contains the list of Microsoft Entra ID group object IDs that will have the admin role of the cluster.
openai_deployments: specifies the list of theAzure OpenAI Service modelsto create. This sample needs aGPTmodel for chat completions and an embeddings model.
NOTE We suggest reading sensitive configuration data such as passwords or SSH keys from a pre-existing Azure Key Vault resource. For more information, seeReferencing Azure Key Vault secrets in Terraform. Before proceeding, also make sure to run theregister-preview-features.shBash script in theterraformfolder to register any preview feature used by the AKS cluster.
OpenAI Terraform Module
The following table contains the code from theopenai.tfTerraform module used to deploy theAzure OpenAI Service.
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, which were common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable features like Microsoft Entra ID for authentication. In our case, we need to specify a custom subdomain for ourAzure OpenAI Serviceas our chatbot application will use an Microsoft Entra ID security token to access it. By default, themain.tfmodule sets the value of thecustom_subdomain_nameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
The following table shows the code of the Terraform module used to deploy theAzure Kubernetes Service (AKS)cluster with a user-assigned managed identity. For more information on theazurerm_kubernetes_clusterresource, seeTerraformdocumentation.
This module allows to deploy anAzure Kubernetes Servicecluster with the following extensions and features:
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
NOTE You can deploy an AKS resource as a public cluster withAPI Server VNet Integrationenabled. During the installation, you can use Terraform modules that make use of theHelm,Kubectl, andKubernetesTerraform Providers to install packages and create Kubernetes entities. Once the installation is complete, you can turn the cluster to private.
Private Endpoint and Private DNS Zone Terraform Modules
The following code snippet from themain.tfTerraform module creates the user-defined managed identity used by the chatbot to acquire a security token from Microsoft Entra ID viaMicrosoft Entra Workload ID.
The above code snippet performs the following steps:
Creates a new user-defined managed identity.
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
Federate the managed identity with the service account used by the chatbot. The following information is necessary to create the federated identity credentials:
The Kubernetes service account name.
The Kubernetes namespace that will host the chatbot application.
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for thechainlit-saservice account were created correctly, as shown in the following picture.
Use Microsoft Entra Workload ID with Azure Kubernetes Service (AKS)
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Microsoft Entra ID application credentials or managed identities to access Microsoft Entra ID protected resources, such as Azure Key Vault and Microsoft Graph. Microsoft Entra Workload ID integrates with the capabilities native to Kubernetes to federate with external identity providers.
resource "helm_release" "nginx_ingress_controller" {
name = "ingress-nginx"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
namespace = "ingress-basic"
create_namespace = true
timeout = 600
set {
name = "controller.metrics.enabled"
value = "true"
}
set {
name = "controller.metrics.serviceMonitor.enabled"
value = "true"
}
set {
name = "controller.metrics.serviceMonitor.additionalLabels.release"
value = "prometheus"
}
set {
name = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/azure-load-balancer-health-probe-request-path"
value = "/healthz"
}
set {
name = "controller.nodeSelector.kubernetes\\.io/os"
value = "linux"
}
set {
name = "controller.replicaCount"
value = "${var.nginx_replica_count}"
}
depends_on = [helm_release.prometheus]
}
Instead, the following module is used to create the workload namespace and service account:
In particular, thekubectl_manifestresource makes use of variables to set the following service account annotations and labels necessary to Microsoft Entra Workload ID. For more information, seeService account labels and annotations.
Simple Chat Application
The Simple Chat Application is a large language model-based chatbot that allows users to submit general-purpose questions to aGPTmodel, which generates and streams back human-like and engaging conversational responses. The following picture shows the welcome screen of the chat application.
You can modify the welcome screen in markdown by editing thechainlit.mdfile at the project's root. If you do not want a welcome screen, leave the file empty. The following picture shows what happens when a user submits a new message in the chat.
Chainlit can render messages in markdown format as shown by the following prompt:
Chainlit also provides classes to support the following elements:
Audio: TheAudioclass allows you to display an audio player for a specific audio file in the chatbot user interface. You must provide either a URL or a path or content bytes.
Avatar: TheAvatarclass allows you to display an avatar image next to a message instead of the author's name. You need to send the element once. Next,, if an avatar's name matches an author's name, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
File: TheFileclass allows you to display a button that lets users download the content of the file. You must provide either a URL or a path or content bytes.
Image: TheImageclass is designed to create and handle image elements to be sent and displayed in the chatbot user interface. You must provide either a URL or a path or content bytes.
Pdf: ThePdfclass allows you to display a PDF hosted remotely or locally in the chatbot UI. This class either takes a URL of a PDF hosted online or the path of a local PDF.
Pyplot: ThePyplotclass allows you to display aMatplotlibpyplot chart in the chatbot UI. This class takes a pyplot figure.
TaskList: TheTaskListclass allows you to display a task list next to the chatbot UI.
Text: TheTextclass allows you to display a text element in the chatbot UI. This class takes a string and creates a text element that can be sent to the UI. It supports the markdown syntax for formatting text. You must provide either a URL or a path or content bytes.
You can click the user icon on the UI to access the chat settings and choose, for example, between the light and dark theme.
The application is built in Python. Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
# Import packages
import os
import sys
from openai import AsyncAzureOpenAI
import logging
import chainlit as cl
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from dotenv import load_dotenv
from dotenv import dotenv_values
# Load environment variables from .env file
if os.path.exists(".env"):
load_dotenv(override=True)
config = dotenv_values(".env")
These are the libraries used by the chat application:
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
openai: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
logging: This module provides flexible logging of messages.
chainlit as cl: This imports theChainlitlibrary and aliases it ascl. Chainlit is used to create the UI of the application.
from azure.identity import DefaultAzureCredential, get_bearer_token_provider: when theopenai_typeproperty value isazure_ad,aDefaultAzureCredentialobject from theAzure Identity client library for Pythonis used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity federated with the service account.
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
pip install -r requirements.txt --upgrade
Next, the code reads the value of the environment variables used to initialize Azure OpenAI objects. In addition, it creates a token provider for Azure OpenAI.
Here's a brief explanation of each variable and related environment variable:
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
api_base: The base URL for the OpenAI API.
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
api_type: A string representing the type of the OpenAI API.
api_version: A string representing the version of the OpenAI API.
engine: The engine used for OpenAI API calls.
model: The model used for OpenAI API calls.
system_content: The content of the system message used for OpenAI API calls.
max_retries: The maximum number of retries for OpenAI API calls.
timeout: The timeout in seconds.
debug: When debug is equal totrue,t, or1, the logger writes the chat completion answers.
In the next section, the code creates theAsyncAzureOpenAIclient object used by the application to communicate with the Azure OpenAI Service instance. When theapi_typeis equal toazure, the code initializes the object with the API key. Otherwise, it initializes theazure_ad_token_providerproperty to the token provider created earlier. Then the code creates a logger.
The backoff time is calculated using thebackoff_in_secondsandattemptvariables. It follows the formulabackoff_in_seconds * 2 ** attempt + random.uniform(0, 1). This formula increases the backoff time exponentially with each attempt and adds a random value between 0 and 1 to avoid synchronized retries.
Next, the code defines a function calledstart_chatthat is used to initialize the UI when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
cl.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
cl.user_session.set(): This API call sets a value in theuser_sessiondictionary. In this case, it initializes themessage_historyin the user's session with a system content message, which indicates the start of the chat.
Finally, the application defines the method called whenever the user sends a new message in the chat.
@cl.on_message
async def on_message(message: cl.Message):
message_history = cl.user_session.get("message_history")
message_history.append({"role": "user", "content": message.content})
logger.info("Question: [%s]", message.content)
# Create the Chainlit response message
msg = cl.Message(content="")
async for stream_resp in await openai.chat.completions.create(
model=model,
messages=message_history,
temperature=temperature,
stream=True,
):
if stream_resp and len(stream_resp.choices) > 0:
token = stream_resp.choices[0].delta.content or ""
await msg.stream_token(token)
if debug:
logger.info("Answer: [%s]", msg.content)
message_history.append({"role": "assistant", "content": msg.content})
await msg.send()
Here is a detailed explanation of the function steps:
cl.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
cl.user_session.get(): This API call retrieves a value from the user's session data stored in theuser_sessiondictionary. In this case, it fetches themessage_historyfrom the user's session to maintain the chat history.
message_history.append(): This API call appends a new message to themessage_historylist. It is used to add the user's message and the assistant's response to the chat history.
cl.Message(): This API call creates a ChainlitMessageobject. TheMessageclass is designed to send, stream, edit, or remove messages in the chatbot user interface. In this sample, theMessageobject is used to stream the OpenAI response in the chat.
msg.stream_token(): Thestream_tokenmethod of theMessageclass streams a token to the response message. It is used to send the response from the OpenAI Chat API in chunks to ensure real-time streaming in the chat.
await openai.chat.completions.create(): This API call sends a message to theOpenAI Chat APIin an asynchronous mode and streams the response. It uses the providedmessage_historyas context for generating the assistant's response.
Below, you can read the complete code of the application.
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
chainlit run app.py -w
Documents QA Chat
The Documents QA Chat application allows users to submit up to 10.pdfand.docxdocuments. The application processes the uploaded documents to create vector embeddings. These embeddings are stored inChromaDBvector database for efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response. The following picture shows the chat application interface. As you can see, you can click theBrowsebutton and choose up to 10.pdfand.docxdocuments to upload. Alternatively, you can just drag and drop the files over the control area.
After uploading the documents, the application creates and stores embeddings toChromaDBvector database. During the phase, the UI shows a messageProcessing <file-1>, <file-2>..., as shown in the following picture:
When the code finished creating embeddings, the UI is ready to receive user's questions:
As your chat application grows in complexity, understanding the individual steps for generating a specific answer can become challenging. To solve this issue, Chainlit allows you to easily explore the reasoning process right from the user interface using theChain of Thought. If you are using theLangChainintegration, every intermediary step is automatically sent and displayed in the Chainlit UI just clicking and expanding the steps, as shown in the following picture:
To see the text chunks that were used by the large language model to originate the response, you can click the sources links, as shown in the following picture:
In theChain of Thought, below the step used to invoke the OpenAI chat completion API, you can find an
Inspect in prompt playground icon. Clicking on it opens the Prompt Playground dialog which allows you to modify and iterate on the prompt as needed.
As shown in the following picture, you can click and edit the value of the highlighted variables in the user prompt:
You can then click and edit the user question.
Then, you can click the submit button to test the effect of your changes, as shown in the following picture.
Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
# Import packages
import os
import io
import sys
import logging
import chainlit as cl
from chainlit.playground.config import AzureChatOpenAI
from pypdf import PdfReader
from docx import Document
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from dotenv import load_dotenv
from dotenv import dotenv_values
from langchain.embeddings import AzureOpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.chroma import Chroma
from langchain.chains import RetrievalQAWithSourcesChain
from langchain.chat_models import AzureChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
# Load environment variables from .env file
if os.path.exists(".env"):
load_dotenv(override=True)
config = dotenv_values(".env")
These are the libraries used by the chat application:
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
time: This module provides various time-related functions for time manipulation and measurement.
openai: the OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
logging: This module provides flexible logging of messages.
chainlit as cl: This imports theChainlitlibrary and aliases it ascl.Chainlit is used to create the UI of the application.
AzureChatOpenAIfromchainlit.playground.config import: you need to importAzureChatOpenAIfromchainlit.playground.configto use the Chainlit Playground.
DefaultAzureCredentialfromazure.identity: when theopenai_typeproperty value isazure_ad, aDefaultAzureCredentialobject from theAzure Identity client library for Python - version 1.13.0is used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity, whose client ID is defined in theAZURE_CLIENT_IDenvironment variable.
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
langchain: Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not. However, using these LLMs in isolation is often insufficient for creating a truly powerful app - the real power comes when you can combine them with other sources of computation or knowledge. LangChain library aims to assist in the development of those types of applications.
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
pip install -r requirements.txt --upgrade
Next, the code reads environment variables and configures the OpenAI settings.
# Read environment variables
temperature = float(os.environ.get("TEMPERATURE", 0.9))
api_base = os.getenv("AZURE_OPENAI_BASE")
api_key = os.getenv("AZURE_OPENAI_KEY")
api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure")
api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-12-01-preview")
chat_completion_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
embeddings_deployment = os.getenv("AZURE_OPENAI_ADA_DEPLOYMENT")
model = os.getenv("AZURE_OPENAI_MODEL")
max_size_mb = int(os.getenv("CHAINLIT_MAX_SIZE_MB", 100))
max_files = int(os.getenv("CHAINLIT_MAX_FILES", 10))
text_splitter_chunk_size = int(os.getenv("TEXT_SPLITTER_CHUNK_SIZE", 1000))
text_splitter_chunk_overlap = int(os.getenv("TEXT_SPLITTER_CHUNK_OVERLAP", 10))
embeddings_chunk_size = int(os.getenv("EMBEDDINGS_CHUNK_SIZE", 16))
max_retries = int(os.getenv("MAX_RETRIES", 5))
retry_min_seconds = int(os.getenv("RETRY_MIN_SECONDS", 1))
retry_max_seconds = int(os.getenv("RETRY_MAX_SECONDS", 5))
timeout = int(os.getenv("TIMEOUT", 30))
debug = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
# Configure system prompt
system_template = """Use the following pieces of context to answer the users question.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.
The "SOURCES" part should be a reference to the source of the document from which you got your answer.
Example of your response should be:
\`\`\`
The answer is foo
SOURCES: xyz
\`\`\`
Begin!
----------------
{summaries}"""
messages = [
SystemMessagePromptTemplate.from_template(system_template),
HumanMessagePromptTemplate.from_template("{question}"),
]
prompt = ChatPromptTemplate.from_messages(messages)
chain_type_kwargs = {"prompt": prompt}
# Configure a logger
logging.basicConfig(
stream=sys.stdout,
format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
# Create Token Provider
if api_type == "azure_ad":
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
# Setting the environment variables for the playground
if api_type == "azure":
os.environ["AZURE_OPENAI_API_KEY"] = api_key
os.environ["AZURE_OPENAI_API_VERSION"] = api_version
os.environ["AZURE_OPENAI_ENDPOINT"] = api_base
os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"] = chat_completion_deployment
Here's a brief explanation of each variable and related environment variable:
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
api_base: The base URL for the OpenAI API.
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
api_type: A string representing the type of the OpenAI API.
api_version: A string representing the version of the OpenAI API.
chat_completion_deployment: the name of the Azure OpenAI GPT model for chat completion.
embeddings_deployment: the name of the Azure OpenAI deployment for embeddings.
model: The model used for chat completion calls (e.g,gpt-35-turbo-16k).
max_size_mb: the maximum size for the uploaded documents.
max_files: the maximum number of documents that can be uploaded.
text_splitter_chunk_size: the maximum chunk size used by theRecursiveCharacterTextSplitterobject.
text_splitter_chunk_overlap: the maximum chunk overlap used by theRecursiveCharacterTextSplitterobject.
embeddings_chunk_size: the maximum chunk size used by theOpenAIEmbeddingsobject.
max_retries: The maximum number of retries for OpenAI API calls.
retry_min_seconds: the minimum number of seconds before a retry.
retry_max_seconds: the maximum number of seconds before a retry.
timeout: The timeout in seconds.
system_template: The content of the system message used for OpenAI API calls.
debug: When debug is equal totrue,t, or1, the logger switches to verbose mode.
Next, the code defines a function calledstart_chatthat is used to initialize the when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
cl.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
The following code is used to initialize the large language model (LLM) chain used to reply to questions on the content of the uploaded documents.
# Initialize the file list to None
files = None
# Wait for the user to upload a file
while files == None:
files = await cl.AskFileMessage(
content=f"Please upload up to {max_files} `.pdf` or `.docx` files to begin.",
accept=[
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
],
max_size_mb=max_size_mb,
max_files=max_files,
timeout=86400,
raise_on_timeout=False,
).send()
TheAskFileMessageAPI call prompts the user to upload up to a specified number of.pdfor.docxfiles. The uploaded files are stored in thefilesvariable. The process continues until the user uploads files. For more information, seeAskFileMessage.
The following code processes each uploaded file by extracting its content.
The text content of each file is stored in the listall_texts.
This code performs text processing and chunking. It checks the file extension to read the file content accordingly, depending on if it's a.pdfor a.docxdocument.
Metadata is created for each chunk and stored in themetadataslist.
# Create a message to inform the user that the files are being processed
content = ""
if len(files) == 1:
content = f"Processing `{files[0].name}`..."
else:
files_names = [f"`{f.name}`" for f in files]
content = f"Processing {', '.join(files_names)}..."
logger.info(content)
msg = cl.Message(content=content, author="Chatbot")
await msg.send()
# Create a list to store the texts of each file
all_texts = []
# Process each file uplodaded by the user
for file in files:
# Read file contents
with open(file.path, "rb") as uploaded_file:
file_contents = uploaded_file.read()
logger.info("[%d] bytes were read from %s", len(file_contents), file.path)
# Create an in-memory buffer from the file content
bytes = io.BytesIO(file_contents)
# Get file extension
extension = file.name.split(".")[-1]
# Initialize the text variable
text = ""
# Read the file
if extension == "pdf":
reader = PdfReader(bytes)
for i in range(len(reader.pages)):
text += reader.pages[i].extract_text()
if debug:
logger.info("[%s] read from %s", text, file.path)
elif extension == "docx":
doc = Document(bytes)
paragraph_list = []
for paragraph in doc.paragraphs:
paragraph_list.append(paragraph.text)
if debug:
logger.info("[%s] read from %s", paragraph.text, file.path)
text = "\n".join(paragraph_list)
# Split the text into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=text_splitter_chunk_size,
chunk_overlap=text_splitter_chunk_overlap,
)
texts = text_splitter.split_text(text)
# Add the chunks and metadata to the list
all_texts.extend(texts)
# Create a metadata for each chunk
metadatas = [{"source": f"{i}-pl"} for i in range(len(all_texts))]
The next piece of code performs the following steps:
It creates anAzureOpenAIEmbeddingsconfigured to use the embeddings model in the Azure OpenAI Service to create embeddings from text chunks.
It creates aChromaDBvector database using theOpenAIEmbeddingsobject, the text chunks list, and the metadata list.
It creates anAzureChatOpenAILangChain object based on the GPR model hosted in Azure OpenAI Service.
It stores the metadata and text chunks in the user session using thecl.user_session.set()API call.
It creates a message to inform the user that the files are ready for queries, and finally returns thechain.
Thecl.user_session.set("chain", chain)call stores the LLM chain in theuser_sessiondictionary for later use.
The next section create the LangChain LLM chain.
# Create a Chroma vector store
if api_type == "azure":
embeddings = AzureOpenAIEmbeddings(
openai_api_version=api_version,
openai_api_type=api_type,
openai_api_key=api_key,
azure_endpoint=api_base,
azure_deployment=embeddings_deployment,
max_retries=max_retries,
retry_min_seconds=retry_min_seconds,
retry_max_seconds=retry_max_seconds,
chunk_size=embeddings_chunk_size,
timeout=timeout,
)
else:
embeddings = AzureOpenAIEmbeddings(
openai_api_version=api_version,
openai_api_type=api_type,
azure_endpoint=api_base,
azure_ad_token_provider=token_provider,
azure_deployment=embeddings_deployment,
max_retries=max_retries,
retry_min_seconds=retry_min_seconds,
retry_max_seconds=retry_max_seconds,
chunk_size=embeddings_chunk_size,
timeout=timeout,
)
# Create a Chroma vector store
db = await cl.make_async(Chroma.from_texts)(
all_texts, embeddings, metadatas=metadatas
)
# Create an AzureChatOpenAI llm
if api_type == "azure":
llm = AzureChatOpenAI(
openai_api_type=api_type,
openai_api_version=api_version,
openai_api_key=api_key,
azure_endpoint=api_base,
temperature=temperature,
azure_deployment=chat_completion_deployment,
streaming=True,
max_retries=max_retries,
timeout=timeout,
)
else:
llm = AzureChatOpenAI(
openai_api_type=api_type,
openai_api_version=api_version,
azure_endpoint=api_base,
api_key=api_key,
temperature=temperature,
azure_deployment=chat_completion_deployment,
azure_ad_token_provider=token_provider,
streaming=True,
max_retries=max_retries,
timeout=timeout,
)
# Create a chain that uses the Chroma vector store
chain = RetrievalQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=db.as_retriever(),
return_source_documents=True,
chain_type_kwargs=chain_type_kwargs,
)
# Save the metadata and texts in the user session
cl.user_session.set("metadatas", metadatas)
cl.user_session.set("texts", all_texts)
# Create a message to inform the user that the files are ready for queries
content = ""
if len(files) == 1:
content = f"`{files[0].name}` processed. You can now ask questions!"
logger.info(content)
else:
files_names = [f"`{f.name}`" for f in files]
content = f"{', '.join(files_names)} processed. You can now ask questions."
logger.info(content)
msg.content = content
msg.author = "Chatbot"
await msg.update()
# Store the chain in the user session
cl.user_session.set("chain", chain)
The following code handles the communication with the OpenAI API and incorporates retrying logic in case the API calls fail due to specific errors.
cl.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
cl.user_session.get("chain"): this call retrieves the LLM chain from theuser_sessiondictionary.
cl.AsyncLangchainCallbackHandler: this call creates a LangChain callback handler.
await chain.acall: The asynchronous call to theRetrievalQAWithSourcesChain.acallexecutes the LLM chain with the user message as an input.
@cl.on_message
async def main(message: cl.Message):
# Retrieve the chain from the user session
chain = cl.user_session.get("chain")
# Create a callback handler
cb = cl.AsyncLangchainCallbackHandler()
# Get the response from the chain
response = await chain.acall(message.content, callbacks=[cb])
logger.info("Question: [%s]", message.content)
The code below extracts the answers and sources from the API response and formats them to be sent as a message.
Theanswerandsourcesare obtained from theresponsedictionary.
The sources are then processed to find corresponding texts in the user session metadata (metadatas) and createsource_elementsusingcl.Text().
cl.Message().send(): theMessageAPI creates and displays a message containing the answer and sources, if available.
The last command sets theAZURE_OPENAI_API_KEYenvironment variable to a security key to access Azure OpenAI returned by the token provider. This key is used by the Chainlit playground.
# Get the answer and sources from the response
answer = response["answer"]
sources = response["sources"].strip()
source_elements = []
if debug:
logger.info("Answer: [%s]", answer)
# Get the metadata and texts from the user session
metadatas = cl.user_session.get("metadatas")
all_sources = [m["source"] for m in metadatas]
texts = cl.user_session.get("texts")
if sources:
found_sources = []
# Add the sources to the message
for source in sources.split(","):
source_name = source.strip().replace(".", "")
# Get the index of the source
try:
index = all_sources.index(source_name)
except ValueError:
continue
text = texts[index]
found_sources.append(source_name)
# Create the text element referenced in the message
source_elements.append(cl.Text(content=text, name=source_name))
if found_sources:
answer += f"\nSources: {', '.join(found_sources)}"
else:
answer += "\nNo sources found"
await cl.Message(content=answer, elements=source_elements).send()
# Setting the AZURE_OPENAI_API_KEY environment variable for the playground
if api_type == "azure_ad":
os.environ["AZURE_OPENAI_API_KEY"] = token_provider()
Below, you can read the complete code of the application.
# Import packages
import os
import io
import sys
import logging
import chainlit as cl
from chainlit.playground.config import AzureChatOpenAI
from pypdf import PdfReader
from docx import Document
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from dotenv import load_dotenv
from dotenv import dotenv_values
from langchain.embeddings import AzureOpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.chroma import Chroma
from langchain.chains import RetrievalQAWithSourcesChain
from langchain.chat_models import AzureChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
# Load environment variables from .env file
if os.path.exists(".env"):
load_dotenv(override=True)
config = dotenv_values(".env")
# Read environment variables
temperature = float(os.environ.get("TEMPERATURE", 0.9))
api_base = os.getenv("AZURE_OPENAI_BASE")
api_key = os.getenv("AZURE_OPENAI_KEY")
api_type = os.environ.get("AZURE_OPENAI_TYPE", "azure")
api_version = os.environ.get("AZURE_OPENAI_VERSION", "2023-12-01-preview")
chat_completion_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
embeddings_deployment = os.getenv("AZURE_OPENAI_ADA_DEPLOYMENT")
model = os.getenv("AZURE_OPENAI_MODEL")
max_size_mb = int(os.getenv("CHAINLIT_MAX_SIZE_MB", 100))
max_files = int(os.getenv("CHAINLIT_MAX_FILES", 10))
max_files = int(os.getenv("CHAINLIT_MAX_FILES", 10))
text_splitter_chunk_size = int(os.getenv("TEXT_SPLITTER_CHUNK_SIZE", 1000))
text_splitter_chunk_overlap = int(os.getenv("TEXT_SPLITTER_CHUNK_OVERLAP", 10))
embeddings_chunk_size = int(os.getenv("EMBEDDINGS_CHUNK_SIZE", 16))
max_retries = int(os.getenv("MAX_RETRIES", 5))
retry_min_seconds = int(os.getenv("RETRY_MIN_SECONDS", 1))
retry_max_seconds = int(os.getenv("RETRY_MAX_SECONDS", 5))
timeout = int(os.getenv("TIMEOUT", 30))
debug = os.getenv("DEBUG", "False").lower() in ("true", "1", "t")
# Configure system prompt
system_template = """Use the following pieces of context to answer the users question.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.
The "SOURCES" part should be a reference to the source of the document from which you got your answer.
Example of your response should be:
```
The answer is foo
SOURCES: xyz
```
Begin!
----------------
{summaries}"""
messages = [
SystemMessagePromptTemplate.from_template(system_template),
HumanMessagePromptTemplate.from_template("{question}"),
]
prompt = ChatPromptTemplate.from_messages(messages)
chain_type_kwargs = {"prompt": prompt}
# Configure a logger
logging.basicConfig(
stream=sys.stdout,
format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
# Create Token Provider
if api_type == "azure_ad":
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
# Setting the environment variables for the playground
if api_type == "azure":
os.environ["AZURE_OPENAI_API_KEY"] = api_key
os.environ["AZURE_OPENAI_API_VERSION"] = api_version
os.environ["AZURE_OPENAI_ENDPOINT"] = api_base
os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"] = chat_completion_deployment
@cl.on_chat_start
async def start():
await cl.Avatar(
name="Chatbot", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png"
).send()
await cl.Avatar(
name="Error", url="https://cdn-icons-png.flaticon.com/512/8649/8649595.png"
).send()
await cl.Avatar(
name="You",
url="https://media.architecturaldigest.com/photos/5f241de2c850b2a36b415024/master/w_1600%2Cc_limit/Luke-logo.png",
).send()
# Initialize the file list to None
files = None
# Wait for the user to upload a file
while files == None:
files = await cl.AskFileMessage(
content=f"Please upload up to {max_files} `.pdf` or `.docx` files to begin.",
accept=[
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
],
max_size_mb=max_size_mb,
max_files=max_files,
timeout=86400,
raise_on_timeout=False,
).send()
# Create a message to inform the user that the files are being processed
content = ""
if len(files) == 1:
content = f"Processing `{files[0].name}`..."
else:
files_names = [f"`{f.name}`" for f in files]
content = f"Processing {', '.join(files_names)}..."
logger.info(content)
msg = cl.Message(content=content, author="Chatbot")
await msg.send()
# Create a list to store the texts of each file
all_texts = []
# Process each file uplodaded by the user
for file in files:
# Read file contents
with open(file.path, "rb") as uploaded_file:
file_contents = uploaded_file.read()
logger.info("[%d] bytes were read from %s", len(file_contents), file.path)
# Create an in-memory buffer from the file content
bytes = io.BytesIO(file_contents)
# Get file extension
extension = file.name.split(".")[-1]
# Initialize the text variable
text = ""
# Read the file
if extension == "pdf":
reader = PdfReader(bytes)
for i in range(len(reader.pages)):
text += reader.pages[i].extract_text()
if debug:
logger.info("[%s] read from %s", text, file.path)
elif extension == "docx":
doc = Document(bytes)
paragraph_list = []
for paragraph in doc.paragraphs:
paragraph_list.append(paragraph.text)
if debug:
logger.info("[%s] read from %s", paragraph.text, file.path)
text = "\n".join(paragraph_list)
# Split the text into chunks
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=text_splitter_chunk_size,
chunk_overlap=text_splitter_chunk_overlap,
)
texts = text_splitter.split_text(text)
# Add the chunks and metadata to the list
all_texts.extend(texts)
# Create a metadata for each chunk
metadatas = [{"source": f"{i}-pl"} for i in range(len(all_texts))]
# Create a Chroma vector store
if api_type == "azure":
embeddings = AzureOpenAIEmbeddings(
openai_api_version=api_version,
openai_api_type=api_type,
openai_api_key=api_key,
azure_endpoint=api_base,
azure_deployment=embeddings_deployment,
max_retries=max_retries,
retry_min_seconds=retry_min_seconds,
retry_max_seconds=retry_max_seconds,
chunk_size=embeddings_chunk_size,
timeout=timeout,
)
else:
embeddings = AzureOpenAIEmbeddings(
openai_api_version=api_version,
openai_api_type=api_type,
azure_endpoint=api_base,
azure_ad_token_provider=token_provider,
azure_deployment=embeddings_deployment,
max_retries=max_retries,
retry_min_seconds=retry_min_seconds,
retry_max_seconds=retry_max_seconds,
chunk_size=embeddings_chunk_size,
timeout=timeout,
)
# Create a Chroma vector store
db = await cl.make_async(Chroma.from_texts)(
all_texts, embeddings, metadatas=metadatas
)
# Create an AzureChatOpenAI llm
if api_type == "azure":
llm = AzureChatOpenAI(
openai_api_type=api_type,
openai_api_version=api_version,
openai_api_key=api_key,
azure_endpoint=api_base,
temperature=temperature,
azure_deployment=chat_completion_deployment,
streaming=True,
max_retries=max_retries,
timeout=timeout,
)
else:
llm = AzureChatOpenAI(
openai_api_type=api_type,
openai_api_version=api_version,
azure_endpoint=api_base,
api_key=api_key,
temperature=temperature,
azure_deployment=chat_completion_deployment,
azure_ad_token_provider=token_provider,
streaming=True,
max_retries=max_retries,
timeout=timeout,
)
# Create a chain that uses the Chroma vector store
chain = RetrievalQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=db.as_retriever(),
return_source_documents=True,
chain_type_kwargs=chain_type_kwargs,
)
# Save the metadata and texts in the user session
cl.user_session.set("metadatas", metadatas)
cl.user_session.set("texts", all_texts)
# Create a message to inform the user that the files are ready for queries
content = ""
if len(files) == 1:
content = f"`{files[0].name}` processed. You can now ask questions!"
logger.info(content)
else:
files_names = [f"`{f.name}`" for f in files]
content = f"{', '.join(files_names)} processed. You can now ask questions."
logger.info(content)
msg.content = content
msg.author = "Chatbot"
await msg.update()
# Store the chain in the user session
cl.user_session.set("chain", chain)
@cl.on_message
async def main(message: cl.Message):
# Retrieve the chain from the user session
chain = cl.user_session.get("chain")
# Create a callback handler
cb = cl.AsyncLangchainCallbackHandler()
# Get the response from the chain
response = await chain.acall(message.content, callbacks=[cb])
logger.info("Question: [%s]", message.content)
# Get the answer and sources from the response
answer = response["answer"]
sources = response["sources"].strip()
source_elements = []
if debug:
logger.info("Answer: [%s]", answer)
# Get the metadata and texts from the user session
metadatas = cl.user_session.get("metadatas")
all_sources = [m["source"] for m in metadatas]
texts = cl.user_session.get("texts")
if sources:
found_sources = []
# Add the sources to the message
for source in sources.split(","):
source_name = source.strip().replace(".", "")
# Get the index of the source
try:
index = all_sources.index(source_name)
except ValueError:
continue
text = texts[index]
found_sources.append(source_name)
# Create the text element referenced in the message
source_elements.append(cl.Text(content=text, name=source_name))
if found_sources:
answer += f"\nSources: {', '.join(found_sources)}"
else:
answer += "\nNo sources found"
await cl.Message(content=answer, elements=source_elements).send()
# Setting the AZURE_OPENAI_API_KEY environment variable for the playground
if api_type == "azure_ad":
os.environ["AZURE_OPENAI_API_KEY"] = token_provider()
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
chainlit run app.py -w
Build Docker Images
You can use thesrc/01-build-docker-images.shBash script to build the Docker container image for each container app.
#!/bin/bash
# Variables
source ./00-variables.sh
# Use a for loop to build the docker images using the array index
for index in ${!images[@]}; do
# Build the docker image
docker build -t ${images[$index]}:$tag -f Dockerfile --build-arg FILENAME=${filenames[$index]} --build-arg PORT=$port .
done
Before running any script in thesrcfolder, make sure to customize the value of the variables inside the00-variables.shfile located in the same folder. This file is embedded in all the scripts and contains the following variables:
TheDockerfileunder thesrcfolder is parametric and can be used to build the container images for both chat applications.
# app/Dockerfile
# # Stage 1 - Install build dependencies
# A Dockerfile must start with a FROM instruction which sets the base image for the container.
# The Python images come in many flavors, each designed for a specific use case.
# The python:3.11-slim image is a good base image for most applications.
# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.
# The slim image is a good choice because it is small and contains only the packages needed to run Python.
# For more information, see:
# * https://hub.docker.com/_/python
# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker
FROM python:3.11-slim AS builder
# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.
# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.
# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir
WORKDIR /app
# Set environment variables.
# The ENV instruction sets the environment variable <key> to the value <value>.
# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.
# For more information, see: https://docs.docker.com/engine/reference/builder/#env
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Install git so that we can clone the app code from a remote repo using the RUN instruction.
# The RUN comand has 2 forms:
# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
# * RUN ["executable", "param1", "param2"] (exec form)
# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results.
# The resulting committed image will be used for the next step in the Dockerfile.
# For more information, see: https://docs.docker.com/engine/reference/builder/#run
RUN apt-get update && apt-get install -y \
build-essential \
curl \
software-properties-common \
git \
&& rm -rf /var/lib/apt/lists/*
# Create a virtualenv to keep dependencies together
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Clone the requirements.txt which contains dependencies to WORKDIR
# COPY has two forms:
# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)
# * COPY ["<src>",... "<dest>"] (this form is required for paths containing whitespace)
# For more information, see: https://docs.docker.com/engine/reference/builder/#copy
COPY requirements.txt .
# Install the Python dependencies
RUN pip install --no-cache-dir --no-deps -r requirements.txt
# Stage 2 - Copy only necessary files to the runner stage
# The FROM instruction initializes a new build stage for the application
FROM python:3.11-slim
# Define the filename to copy as an argument
ARG FILENAME
# Deefine the port to run the application on as an argument
ARG PORT=8000
# Set an environment variable
ENV FILENAME=${FILENAME}
# Sets the working directory to /app
WORKDIR /app
# Copy the virtual environment from the builder stage
COPY --from=builder /opt/venv /opt/venv
# Set environment variables
ENV PATH="/opt/venv/bin:$PATH"
# Clone the $FILENAME containing the application code
COPY $FILENAME .
# Copy the chainlit.md file to the working directory
COPY chainlit.md .
# Copy the .chainlit folder to the working directory
COPY ./.chainlit ./.chainlit
# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.
# For more information, see: https://docs.docker.com/engine/reference/builder/#expose
EXPOSE $PORT
# The ENTRYPOINT instruction has two forms:
# * ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred)
# * ENTRYPOINT command param1 param2 (shell form)
# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.
# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint
CMD chainlit run $FILENAME --port=$PORT
Test applications locally
You can use thesrc/02-run-docker-container.shBash script to test the containers for thesender,processor, andreceiverapplications.
Push Docker containers to the Azure Container Registry
You can use thesrc/03-push-docker-image.shBash script to push the Docker container images for thesender,processor, andreceiverapplications to theAzure Container Registry (ACR).
#!/bin/bash
# Variables
source ./00-variables.sh
# Login to ACR
az acr login --name $acrName
# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry.
loginServer=$(az acr show --name $acrName --query loginServer --output tsv)
# Use a for loop to tag and push the local docker images to the Azure Container Registry
for index in ${!images[@]}; do
# Tag the local sender image with the loginServer of ACR
docker tag ${images[$index],,}:$tag $loginServer/${images[$index],,}:$tag
# Push the container image to ACR
docker push $loginServer/${images[$index],,}:$tag
done
Deployment Scripts
If you deployed the Azure infrastructure using the Terraform modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
Scripts:
09-deploy-apps.sh
10-configure-dns.sh
YAML manifests:
configMap.yml
deployment.yml
ingress.yml
service.yml
If you instead want to deploy the application in your AKS cluster, make sure to run all of the scripts in order.
The09-deploy-apps.shscript creates the configmap, deployment, service, and ingress Kubernetes objects for thechatanddocsapplications. This script makes use of theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
#!/bin/bash
# Variables
source ./00-variables.sh
# Attach ACR to AKS cluster
if [[ $attachAcr == true ]]; then
echo "Attaching ACR $acrName to AKS cluster $aksClusterName..."
az aks update \
--name $aksClusterName \
--resource-group $aksResourceGroupName \
--attach-acr $acrName
fi
# Check if namespace exists in the cluster
result=$(kubectl get namespace -o jsonpath="{.items[?(@.metadata.name=='$namespace')].metadata.name}")
if [[ -n $result ]]; then
echo "$namespace namespace already exists in the cluster"
else
echo "$namespace namespace does not exist in the cluster"
echo "creating $namespace namespace in the cluster..."
kubectl create namespace $namespace
fi
# Create config map
cat $configMapTemplate |
yq "(.data.TITLE)|="\""$title"\" |
yq "(.data.LABEL)|="\""$label"\" |
yq "(.data.TEMPERATURE)|="\""$temperature"\" |
yq "(.data.IMAGE_WIDTH)|="\""$imageWidth"\" |
yq "(.data.AZURE_OPENAI_TYPE)|="\""$openAiType"\" |
yq "(.data.AZURE_OPENAI_BASE)|="\""$openAiBase"\" |
yq "(.data.AZURE_OPENAI_MODEL)|="\""$openAiModel"\" |
yq "(.data.AZURE_OPENAI_DEPLOYMENT)|="\""$openAiDeployment"\" |
kubectl apply -n $namespace -f -
# Create deployment
cat $deploymentTemplate |
yq "(.spec.template.spec.containers[0].image)|="\""$image"\" |
yq "(.spec.template.spec.containers[0].imagePullPolicy)|="\""$imagePullPolicy"\" |
yq "(.spec.template.spec.serviceAccountName)|="\""$serviceAccountName"\" |
kubectl apply -n $namespace -f -
# Create deployment
kubectl apply -f $serviceTemplate -n $namespace
The 10-configure-dns.shscript creates an A record in the Azure Public DNS Zone to expose thechatanddocsapplications via a given subdomain (e.g.,https://chat.contoso.com).
# Variables
source ./00-variables.sh
subdomains=($docsSubdomain $chatSubdomain)
# Install jq if not installed
path=$(which jq)
if [[ -z $path ]]; then
echo 'Installing jq...'
apt install -y jq
fi
# Choose the ingress controller to use
if [[ $ingressClassName == "nginx" ]]; then
ingressNamespace=$nginxNamespace
ingressServiceName="${nginxReleaseName}-controller"
else
ingressNamespace=$webAppRoutingNamespace
ingressServiceName=$webAppRoutingServiceName
fi
# Retrieve the public IP address of the NGINX ingress controller
echo "Retrieving the external IP address of the [$ingressClassName] NGINX ingress controller..."
publicIpAddress=$(kubectl get service -o json -n $ingressNamespace |
jq -r '.items[] |
select(.spec.type == "LoadBalancer" and .metadata.name == "'$ingressServiceName'") |
.status.loadBalancer.ingress[0].ip')
if [ -n "$publicIpAddress" ]; then
echo "[$publicIpAddress] external IP address of the [$ingressClassName] NGINX ingress controller successfully retrieved"
else
echo "Failed to retrieve the external IP address of the [$ingressClassName] NGINX ingress controller"
exit
fi
for subdomain in ${subdomains[@]}; do
# Check if an A record for todolist subdomain exists in the DNS Zone
echo "Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone..."
ipv4Address=$(az network dns record-set a list \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--query "[?name=='$subdomain'].ARecords[].ipv4Address" \
--output tsv \
--only-show-errors)
if [[ -n $ipv4Address ]]; then
echo "An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address"
if [[ $ipv4Address == $publicIpAddress ]]; then
echo "The [$ipv4Address] ip address of the existing A record is equal to the ip address of the ingress"
echo "No additional step is required"
continue
else
echo "The [$ipv4Address] ip address of the existing A record is different than the ip address of the ingress"
fi
# Retrieving name of the record set relative to the zone
echo "Retrieving the name of the record set relative to the [$dnsZoneName] zone..."
recordSetName=$(az network dns record-set a list \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--query "[?name=='$subdomain'].name" \
--output tsv \
--only-show-errors 2>/dev/null)
if [[ -n $recordSetName ]]; then
echo "[$recordSetName] record set name successfully retrieved"
else
echo "Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone"
exit
fi
# Remove the A record
echo "Removing the A record from the record set relative to the [$dnsZoneName] zone..."
az network dns record-set a remove-record \
--ipv4-address $ipv4Address \
--record-set-name $recordSetName \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--only-show-errors 1>/dev/null
if [[ $? == 0 ]]; then
echo "[$ipv4Address] ip address successfully removed from the [$recordSetName] record set"
else
echo "Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set"
exit
fi
fi
# Create the A record
echo "Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address..."
az network dns record-set a add-record \
--zone-name $dnsZoneName \
--resource-group $dnsZoneResourceGroupName \
--record-set-name $subdomain \
--ipv4-address $publicIpAddress \
--only-show-errors 1>/dev/null
if [[ $? == 0 ]]; then
echo "A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone"
else
echo "Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone"
fi
done
YAML manifests
Below you can read the YAML manifests used to deploy thechatchatbot to AKS. For brevity, I will cover only the installation of this application, but you can find all the YAML manifests in the companion GitHub repository. Thechat-configmap.ymldefines a value for the environment variables passed to the application container. The configmap does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Microsoft Entra ID security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Microsoft Entra ID security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
AZURE_OPENAI_VERSION: A string representing the version of the OpenAI API.
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
AZURE_OPENAI_SYSTEM_MESSAGE: The content of the system message used for OpenAI API calls. You can use it to describe the assistant's personality.
Thechat-deployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create.azure.workload.identity/uselabel is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
Theingress.ymlmanifest defines a Kubernetesingressobject used to expose the service via theNGINX Ingress Controller. This project deploys a managed NGINX Ingress Controller using the application routing add-on and an unmanaged instance of the NGINX Ingress Controller using the Helm Terrafom Provider and related chart. The Terraform module creates twoclusterissuerobjects, one for the managed and one for the unmanaged version of the NGINX Ingress Controller. You can run the following command to see the two ingress classes:
kubectl get ingressclass
Executing the command will produce a result as follows:
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 4d
webapprouting.kubernetes.azure.com webapprouting.kubernetes.azure.com/nginx <none> 4d22h
Run the following command to retrieve the cluster issuers used by thecert-manager:
kubectl get clusterissuer
The above command should return a result as follows:
NAME READY AGE
letsencrypt-nginx True 4d2h
letsencrypt-webapprouting True 4d2h
Thechat-ingresscontains the code of the ingress object used to expose thechatapplication. This version of the ingress makes use of the unmanaged instance of the NGINX Ingress Controller.
The ingress object defines the following annotations:
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
You can delete the resource group using the following Azure CLI command when you no longer need the resources you created. This will remove all the Azure resources.
az group delete --name <resource-group-name>
Alternatively, you can use the following PowerShell cmdlet to delete the resource group and all the Azure resources.
and I could not find any reference to how to configure the application routing addon to deploy a manager NGINX ingress controller where the service is exposed via an internal load balancer. In this case, I suggest that you disable the installation of the managed NGINX ingress controller via the application routing addon by setting the http_application_routing_enabled Terraform variable to false. Then, modify the the nginx_ingress_controller.tf module under terraform/modules/kubernetes add the following parameter:
I didn't test it, but this should configure the NGINX ingress controller installed via Helm chart to expose the service via an internal load balancer. Hope this helps.
P.S. If you liked the article, please give a star also to the GitHub project, thanks
Thanks a lots for sharing the full content article, tons of things here to learn.
In my Azure infra Public IP is not allowed, hence while trying to install Managed NGINX Ingress Controller using below command:
az aks approuting enable -g ResourceGroupName -n ClusterName
NGINX Ingress failing to pick public Load balancer IP and it's failing.
Can we do this with internal load balancer? Please suggest!
"}},"componentScriptGroups({\"componentId\":\"custom.widget.MicrosoftFooter\"})":{"__typename":"ComponentScriptGroups","scriptGroups":{"__typename":"ComponentScriptGroupsDefinition","afterInteractive":{"__typename":"PageScriptGroupDefinition","group":"AFTER_INTERACTIVE","scriptIds":[]},"lazyOnLoad":{"__typename":"PageScriptGroupDefinition","group":"LAZY_ON_LOAD","scriptIds":[]}},"componentScripts":[]},"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/community/NavbarDropdownToggle\"]})":[{"__ref":"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/common/QueryHandler\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageCoverImage\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeTitle\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageTimeToRead\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageSubject\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageSubject-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserLink\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserLink-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserRank\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageTime\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageTime-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageBody\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageBody-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageCustomFields\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageCustomFields-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageRevision\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageRevision-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageReplyButton\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageAuthorBio\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageAuthorBio-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/ranks/UserRankLabel\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserRegistrationDate\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeDescription\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505309751"}],"message({\"id\":\"message:4122360\"})":{"__ref":"BlogReplyMessage:message:4122360"},"message({\"id\":\"message:4122356\"})":{"__ref":"BlogReplyMessage:message:4122356"},"message({\"id\":\"message:4028301\"})":{"__ref":"BlogReplyMessage:message:4028301"},"message({\"id\":\"message:4028226\"})":{"__ref":"BlogReplyMessage:message:4028226"},"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"components/tags/TagView/TagViewChip\"]})":[{"__ref":"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/common/Pager/PagerLoadMore\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/common/Pager/PagerLoadMore-1745505309751"}],"cachedText({\"lastModified\":\"1745505309751\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeIcon\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505309751"}]},"CachedAsset:pages-1745487429215":{"__typename":"CachedAsset","id":"pages-1745487429215","value":[{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogViewAllPostsPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId/all-posts/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CasePortalPage","type":"CASE_PORTAL","urlPath":"/caseportal","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CreateGroupHubPage","type":"GROUP_HUB","urlPath":"/groups/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CaseViewPage","type":"CASE_DETAILS","urlPath":"/case/:caseId/:caseNumber","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"InboxPage","type":"COMMUNITY","urlPath":"/inbox","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"HelpFAQPage","type":"COMMUNITY","urlPath":"/help","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaMessagePage","type":"IDEA_POST","urlPath":"/idea/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaViewAllIdeasPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/all-ideas/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"LoginPage","type":"USER","urlPath":"/signin","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogPostPage","type":"BLOG","urlPath":"/category/:categoryId/blogs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"UserBlogPermissions.Page","type":"COMMUNITY","urlPath":"/c/user-blog-permissions/page","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ThemeEditorPage","type":"COMMUNITY","urlPath":"/designer/themes","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbViewAllArticlesPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId/all-articles/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"AllEvents","type":"CUSTOM","urlPath":"/Events","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"OccasionEditPage","type":"EVENT","urlPath":"/event/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"OAuthAuthorizationAllowPage","type":"USER","urlPath":"/auth/authorize/allow","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"PageEditorPage","type":"COMMUNITY","urlPath":"/designer/pages","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"PostPage","type":"COMMUNITY","urlPath":"/category/:categoryId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumBoardPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbBoardPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"EventPostPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"UserBadgesPage","type":"COMMUNITY","urlPath":"/users/:login/:userId/badges","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"GroupHubMembershipAction","type":"GROUP_HUB","urlPath":"/membership/join/:nodeId/:membershipType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"MaintenancePage","type":"COMMUNITY","urlPath":"/maintenance","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaReplyPage","type":"IDEA_REPLY","urlPath":"/idea/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"UserSettingsPage","type":"USER","urlPath":"/mysettings/:userSettingsTab","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"GroupHubsPage","type":"GROUP_HUB","urlPath":"/groups","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumPostPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"OccasionRsvpActionPage","type":"OCCASION","urlPath":"/event/:boardId/:messageSubject/:messageId/rsvp/:responseType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"VerifyUserEmailPage","type":"USER","urlPath":"/verifyemail/:userId/:verifyEmailToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"AllOccasionsPage","type":"OCCASION","urlPath":"/category/:categoryId/events/:boardId/all-events/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"EventBoardPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbReplyPage","type":"TKB_REPLY","urlPath":"/kb/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaBoardPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CommunityGuideLinesPage","type":"COMMUNITY","urlPath":"/communityguidelines","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CaseCreatePage","type":"SALESFORCE_CASE_CREATION","urlPath":"/caseportal/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbEditPage","type":"TKB","urlPath":"/kb/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForgotPasswordPage","type":"USER","urlPath":"/forgotpassword","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaEditPage","type":"IDEA","urlPath":"/idea/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TagPage","type":"COMMUNITY","urlPath":"/tag/:tagName","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogBoardPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"OccasionMessagePage","type":"OCCASION_TOPIC","urlPath":"/event/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ManageContentPage","type":"COMMUNITY","urlPath":"/managecontent","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ClosedMembershipNodeNonMembersPage","type":"GROUP_HUB","urlPath":"/closedgroup/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CommunityPage","type":"COMMUNITY","urlPath":"/","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumMessagePage","type":"FORUM_TOPIC","urlPath":"/discussions/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"IdeaPostPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"CommunityHub.Page","type":"CUSTOM","urlPath":"/Directory","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogMessagePage","type":"BLOG_ARTICLE","urlPath":"/blog/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"RegistrationPage","type":"USER","urlPath":"/register","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"EditGroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumEditPage","type":"FORUM","urlPath":"/discussions/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ResetPasswordPage","type":"USER","urlPath":"/resetpassword/:userId/:resetPasswordToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"AllBlogs.Page","type":"CUSTOM","urlPath":"/blogs","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbMessagePage","type":"TKB_ARTICLE","urlPath":"/kb/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogEditPage","type":"BLOG","urlPath":"/blog/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ManageUsersPage","type":"USER","urlPath":"/users/manage/:tab?/:manageUsersTab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumReplyPage","type":"FORUM_REPLY","urlPath":"/discussions/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"PrivacyPolicyPage","type":"COMMUNITY","urlPath":"/privacypolicy","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"NotificationPage","type":"COMMUNITY","urlPath":"/notifications","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"UserPage","type":"USER","urlPath":"/users/:login/:userId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"OccasionReplyPage","type":"OCCASION_REPLY","urlPath":"/event/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ManageMembersPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/manage/:tab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"SearchResultsPage","type":"COMMUNITY","urlPath":"/search","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"BlogReplyPage","type":"BLOG_REPLY","urlPath":"/blog/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"GroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TermsOfServicePage","type":"COMMUNITY","urlPath":"/termsofservice","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"CategoryPage","type":"CATEGORY","urlPath":"/category/:categoryId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"ForumViewAllTopicsPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/all-topics/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"TkbPostPage","type":"TKB","urlPath":"/category/:categoryId/kbs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429215,"localOverride":null,"page":{"id":"GroupHubPostPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"}],"localOverride":false},"CachedAsset:text:en_US-components/context/AppContext/AppContextProvider-0":{"__typename":"CachedAsset","id":"text:en_US-components/context/AppContext/AppContextProvider-0","value":{"noCommunity":"Cannot find community","noUser":"Cannot find current user","noNode":"Cannot find node with id {nodeId}","noMessage":"Cannot find message with id {messageId}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-0":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-0","value":{"title":"Loading..."},"localOverride":false},"User:user:-1":{"__typename":"User","id":"user:-1","uid":-1,"login":"Deleted","email":"","avatar":null,"rank":null,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":"ANONYMOUS","registrationTime":null,"confirmEmailStatus":false,"registrationAccessLevel":"VIEW","ssoRegistrationFields":[]},"ssoId":null,"profileSettings":{"__typename":"ProfileSettings","dateDisplayStyle":{"__typename":"InheritableStringSettingWithPossibleValues","key":"layout.friendly_dates_enabled","value":"false","localValue":"true","possibleValues":["true","false"]},"dateDisplayFormat":{"__typename":"InheritableStringSetting","key":"layout.format_pattern_date","value":"MMM dd yyyy","localValue":"MM-dd-yyyy"},"language":{"__typename":"InheritableStringSettingWithPossibleValues","key":"profile.language","value":"en-US","localValue":"en","possibleValues":["en-US"]}},"deleted":false},"Theme:customTheme1":{"__typename":"Theme","id":"customTheme1"},"Category:category:FastTrack":{"__typename":"Category","id":"category:FastTrack","entityType":"CATEGORY","displayId":"FastTrack","nodeType":"category","depth":3,"title":"Microsoft FastTrack","shortTitle":"Microsoft FastTrack","parent":{"__ref":"Category:category:products-services"}},"Category:category:top":{"__typename":"Category","id":"category:top","displayId":"top","nodeType":"category","depth":0,"title":"Top","entityType":"CATEGORY","shortTitle":"Top"},"Category:category:communities":{"__typename":"Category","id":"category:communities","displayId":"communities","nodeType":"category","depth":1,"parent":{"__ref":"Category:category:top"},"title":"Communities","entityType":"CATEGORY","shortTitle":"Communities"},"Category:category:products-services":{"__typename":"Category","id":"category:products-services","displayId":"products-services","nodeType":"category","depth":2,"parent":{"__ref":"Category:category:communities"},"title":"Products","entityType":"CATEGORY","shortTitle":"Products"},"Blog:board:FastTrackforAzureBlog":{"__typename":"Blog","id":"board:FastTrackforAzureBlog","entityType":"BLOG","displayId":"FastTrackforAzureBlog","nodeType":"board","depth":4,"conversationStyle":"BLOG","title":"FastTrack for Azure","description":"","avatar":null,"profileSettings":{"__typename":"ProfileSettings","language":null},"parent":{"__ref":"Category:category:FastTrack"},"ancestors":{"__typename":"CoreNodeConnection","edges":[{"__typename":"CoreNodeEdge","node":{"__ref":"Community:community:gxcuf89792"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:communities"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:products-services"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:FastTrack"}}]},"userContext":{"__typename":"NodeUserContext","canAddAttachments":false,"canUpdateNode":false,"canPostMessages":false,"isSubscribed":false},"boardPolicies":{"__typename":"BoardPolicies","canPublishArticleOnCreate":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","args":[]}}},"shortTitle":"FastTrack for Azure","repliesProperties":{"__typename":"RepliesProperties","sortOrder":"REVERSE_PUBLISH_TIME","repliesFormat":"threaded"},"eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/","tagProperties":{"__typename":"TagNodeProperties","tagsEnabled":{"__typename":"PolicyResult","failureReason":null}},"requireTags":true,"tagType":"PRESET_ONLY"},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc","height":512,"width":512,"mimeType":"image/png"},"Rank:rank:4":{"__typename":"Rank","id":"rank:4","position":6,"name":"Microsoft","color":"333333","icon":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}"},"rankStyle":"OUTLINE"},"User:user:988334":{"__typename":"User","id":"user:988334","uid":988334,"login":"paolosalvatori","deleted":false,"avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/dS05ODgzMzQtMzg1MjYyaTE4QTU5MkIyQUVCMkM0MDE"},"rank":{"__ref":"Rank:rank:4"},"email":"","messagesCount":67,"biography":null,"topicsCount":30,"kudosReceivedCount":160,"kudosGivenCount":29,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2021-03-05T07:56:49.951-08:00","confirmEmailStatus":null},"followersCount":null,"solutionsCount":0,"entityType":"USER","eventPath":"community:gxcuf89792/user:988334"},"BlogTopicMessage:message:4024070":{"__typename":"BlogTopicMessage","uid":4024070,"subject":"Create an Azure OpenAI, LangChain, ChromaDB, and Chainlit chat app in AKS using Terraform","id":"message:4024070","revisionNum":7,"repliesCount":4,"author":{"__ref":"User:user:988334"},"depth":0,"hasGivenKudo":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"conversation":{"__ref":"Conversation:conversation:4024070"},"messagePolicies":{"__typename":"MessagePolicies","canPublishArticleOnEdit":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","args":[]}},"canModerateSpamMessage":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","key":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","args":[]}}},"contentWorkflow":{"__typename":"ContentWorkflow","state":"PUBLISH","scheduledPublishTime":null,"scheduledTimezone":null,"userContext":{"__typename":"MessageWorkflowContext","canSubmitForReview":null,"canEdit":false,"canRecall":null,"canSubmitForPublication":null,"canReturnToAuthor":null,"canPublish":null,"canReturnToReview":null,"canSchedule":false},"shortScheduledTimezone":null},"readOnly":false,"editFrozen":false,"moderationData":{"__ref":"ModerationData:moderation_data:4024070"},"teaser":"
This sample shows how to create two AKS-hosted chat applications that use OpenAI, LangChain, ChromaDB, and Chainlit using Python and deploy them to an AKS environment built in Terraform.
Simple Chat: This simple chat application utilizes OpenAI's language models to generate real-time completion responses.
\n
Documents QA Chat: This chat application goes beyond simple conversations. Users can upload up to 10.pdfand.docxdocuments, which are then processed to create vector embeddings. These embeddings are stored inChromaDBfor efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response.
Asystemnode pool in a dedicated subnet. The default node pool hosts only critical system pods and services. The worker nodes have node taint which prevents application pods from beings scheduled on this node pool.
\n
Ausernode pool hosting user workloads and artifacts in a dedicated subnet.
\n
\n
\n
Azure OpenAI Service: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with the security and enterprise promise of Azure. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
\n
User-defined Managed Identity: a user-defined managed identity used by the AKS cluster to create additional resources like load balancers and managed disks in Azure.
Azure Virtual Machine: Terraform modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
\n
Azure Bastion Host: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
\n
Azure NAT Gateway: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
\n
Azure Storage Account: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
\n
Azure Container Registry: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Azure Monitor workspace: AnAzure Monitor workspaceis a unique environment for data collected byAzure Monitor. Each workspace has its own data repository, configuration, and permissions. Log Analytics workspaces contain logs and metrics data from multiple Azure resources, whereas Azure Monitor workspaces currently contain only metrics related toPrometheus. Azure Monitor managed service for Prometheus allows you to collect and analyze metrics at scale using a Prometheus-compatible monitoring solution, based on thePrometheus. This fully managed service allows you to use thePrometheus query language (PromQL)to analyze and alert on the performance of monitored infrastructure and workloads without having to operate the underlying infrastructure. The primary method for visualizing Prometheus metrics isAzure Managed Grafana. You can connect yourAzure Monitor workspaceto anAzure Managed Grafanato visualize Prometheus metrics using a set of built-in and custom Grafana dashboards.
\n
Azure Managed Grafana: anAzure Managed Grafanainstance used to visualize thePrometheus metricsgenerated by theAzure Kubernetes Service(AKS)cluster deployed by the Bicep modules.Azure Managed Grafanais a fully managed service for analytics and monitoring solutions. It's supported by Grafana Enterprise, which provides extensible data visualizations. This managed service allows to quickly and easily deploy Grafana dashboards with built-in high availability and control access with Azure security.
\n
NGINX Ingress Controller: this sample compares the managed and unmanaged NGINX Ingress Controller. While the managed version is installed using theApplication routing add-on, the unmanaged version is deployed using theHelm Terraform Provider. You can use the Helm provider to deploy software packages in Kubernetes. The provider needs to be configured with the proper credentials before it can be used.
\n
Cert-Manager: thecert-managerpackage andLet's Encryptcertificate authority are used to issue a TLS/SSL certificate to the chat applications.
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, which is designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
\n
Azure OpenAI Service provides REST API access to OpenAI's powerful language models including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task including but not limited to 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.
\n
TheChat Completion API, which is part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
\n
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction an important skill to develop. For more information, seeIntroduction to prompt engineering.
\n
Prompt construction can be difficult. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However it's important to understand that each model behaves differently, so the learnings may not apply equally to all models.
\n
Prompt engineering refers to the process of creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
\n
\n
Vector Databases
\n
Avector databaseis a specialized database that goes beyond traditional storage by organizing information to simplify the search for similar items. Instead of merely storing words or numbers, it leverages vector embeddings - unique numerical representations of data. These embeddings capture meaning, context, and relationships. For instance, words are represented as vectors, whereas similar words have similar vector values.
\n
The applications of vector databases are numerous and powerful. In language processing, they facilitate the discovery of related documents or sentences. By comparing the vector embeddings of different texts, finding similar or related information becomes faster and more efficient. This capability benefits search engines and recommendation systems, which can suggest relevant articles or products based on user interests.
\n
In the realm of image analysis, vector databases excel in finding visually similar images. By representing images as vectors, a simple comparison of vector values can identify visually similar images. This capability is highly valuable for tasks like reverse image search or content-based image retrieval.
\n
Additionally, vector databases find applications in fraud detection, anomaly detection, and clustering. By comparing vector embeddings of data points, unusual patterns can be detected, and similar items can be grouped together, aiding in effective data analysis and decision-making. This is a list of Azure services that are suitable for use as a vector database in a retrieval-augmented generation (RAG) solution:
\n
\n
\n
\n
Azure Cosmos DB for MongoDB vCore: vCore-based Azure Cosmos DB for MongoDB provides developers with a fully managed MongoDB-compatible database service for building modern applications with a familiar architecture. Developers can enjoy the benefits of native Azure integrations, low total cost of ownership (TCO), and the familiar vCore architecture when migrating existing applications or building new ones. Azure Cosmos DB for MongoDB features built-in vector database capabilities enabling your data and vectors to be stored together for efficient and accurate vector searches.
\n
\n
\n
Azure Cosmos DB for NoSQL: Azure Cosmos DB for NoSQL is a globally distributed database service designed for scalable and high performance applications. It offers an industry-leading 99.999% Service Level Agreement (SLA), ensuring high availability for your mission-critical applications. With sub-10ms point reads and instant autoscale, it provides lightning-fast data access and seamless scalability. Its flexible, schemaless data model allows for agile and adaptable application development. Moreover, Azure Cosmos DB’s built-in vector index using DiskANN enables fast, accurate, and cost-effective vector search at any scale, enhancing the efficiency and effectiveness of your data-driven applications.
\n
\n
\n
Azure Cosmos DB for PostgreSQL You can use the natively integrated vector database in Azure Cosmos DB for PostgreSQL, which offers an efficient way to store, index, and search high-dimensional vector data directly alongside other application data. This approach removes the necessity of migrating your data to costlier alternative vector databases and provides a seamless integration of your AI-driven applications.
\n
\n
\n
Azure Cache for Redis Azure Cache for Redis can be used as a vector database by combining it models like Azure OpenAI for Retrieval-Augmented Generative AI and analysis scenarios.
\n
\n
\n
\n
Here is a list of the most popular vector databases:
\n
\n
\n
ChromaDBis a powerful database solution that stores and retrieves vector embeddings efficiently. It is commonly used in AI applications, including chatbots and document analysis systems. By storing embeddings in ChromaDB, users can easily search and retrieve similar vectors, enabling faster and more accurate matching or recommendation processes. ChromaDB offers excellent scalability high performance, and supports various indexing techniques to optimize search operations. It is a versatile tool that enhances the functionality and efficiency of AI applications that rely on vector embeddings.
\n
Facebook AI Similarity Search (FAISS)is another widely used vector database. Facebook AI Research develops it and offers highly optimized algorithms for similarity search and clustering of vector embeddings. FAISS is known for its speed and scalability, making it suitable for large-scale applications. It offers different indexing methods like flat, IVF (Inverted File System), and HNSW (Hierarchical Navigable Small World) to organize and search vector data efficiently.
\n
SingleStore: SingleStore aims to deliver the world’s fastest distributed SQL database for data-intensive applications: SingleStoreDB, which combines transactional + analytical workloads in a single platform.
\n
Astra DB: DataStax Astra DB is a cloud-native, multi-cloud, fully managed database-as-a-service based on Apache Cassandra, which aims to accelerate application development and reduce deployment time for applications from weeks to minutes.
\n
Milvus: Milvus is an open source vector database built to power embedding similarity search and AI applications. Milvus makes unstructured data search more accessible and provides a consistent user experience regardless of the deployment environment. Milvus 2.0 is a cloud-native vector database with storage and computation separated by design. All components in this refactored version of Milvus are stateless to enhance elasticity and flexibility.
\n
Qdrant: Qdrant is a vector similarity search engine and database for AI applications. Along with open-source, Qdrant is also available in the cloud. It provides a production-ready service with an API to store, search, and manage points—vectors with an additional payload. Qdrant is tailored to extended filtering support. It makes it useful for all sorts of neural-network or semantic-based matching, faceted search, and other applications.
\n
Pinecone: Pinecone is a fully managed vector database that makes adding vector search to production applications accessible. It combines state-of-the-art vector search libraries, advanced features such as filtering, and distributed infrastructure to provide high performance and reliability at any scale.
\n
Vespa: Vespa is a platform for applications combining data and AI, online. By building such applications on Vespa helps users avoid integration work to get features, and it can scale to support any amount of traffic and data. To deliver that, Vespa provides a broad range of query capabilities, a computation engine with support for modern machine-learned models, hands-off operability, data management, and application development support. It is free and open source to use under the Apache 2.0 license.
\n
Zilliz: Milvus is an open-source vector database, with over 18,409 stars on GitHub and 3.4 million+ downloads. Milvus supports billion-scale vector search and has over 1,000 enterprise users. Zilliz Cloud provides a fully-managed Milvus service made by the creators of Milvus. This helps to simplify the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure. As a DBaaS, Zilliz simplifies the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure.
\n
Weaviate: Weaviate is an open-source vector database used to store data objects and vector embeddings from ML-models, and scale into billions of data objects from the same name company in Amsterdam. Users can index billions of data objects to search through and combine multiple search techniques, such as keyword-based and vector search, to provide search experiences.
LangChainis a software framework designed to streamline the development of applications usinglarge language models (LLMs). It serves as a language model integration framework, facilitating various applications like document analysis and summarization, chatbots, and code analysis.
\n
LangChain's integrations cover an extensive range of systems, tools, and services, making it a comprehensive solution for language model-based applications. LangChain integrates with the major cloud platforms such as Microsoft Azure, Amazon AWS, and Google, and with API wrappers for various purposes like news, movie information, and weather, as well as support for Bash, web scraping, and more. It also supports multiple language models, including those from OpenAI, Anthropic, and Hugging Face. Moreover, LangChain offers various functionalities for document handling, code generation, analysis, debugging, and interaction with databases and other data sources.
\n
\n
Chainlit
\n
Chainlitis an open-source Python package that is specifically designed to create user interfaces (UIs) for AI applications. It simplifies the process of building interactive chats and interfaces, making developing AI-powered applications faster and more efficient. While Streamlit is a general-purpose UI library, Chainlit is purpose-built for AI applications and seamlessly integrates with other AI technologies such asLangChain,LlamaIndex, andLangFlow.
\n
With Chainlit, developers can easily create intuitive UIs for their AI models, including ChatGPT-like applications. It provides a user-friendly interface for users to interact with AI models, enabling conversational experiences and information retrieval. Chainlit also offers unique features, such as the ability to display theChain of Thought, which allows users to explore the reasoning process directly within the UI. This feature enhances transparency and enables users to understand how the AI arrives at its responses or recommendations.
\n
For more information, see the following resources:
Managed NGINX Ingress Controller for Azure Kubernetes Service
\n
One way to route HTTP and secure HTTPS traffic to applications in an Azure Kubernetes Service (AKS) cluster is by using the Kubernetes Ingress object. The application routing add-on for AKS enables you to create, configure, and manage one or more Ingress controllers within your AKS cluster using theNGINX Ingress Controller.
\n
The application routing add-on with NGINX provides several features, including:
\n
\n
\n
Easy configuration of managed NGINX Ingress controllers based on the Kubernetes NGINX Ingress controller.
\n
Integration with Azure DNS for public and private zone management.
\n
SSL termination with certificates stored in Azure Key Vault.
\n
\n
To enable theapplication routing add-onon an existing cluster, you can use Azure CLI, as shown in the following code snippet.
\n
\n
az aks approuting enable -g <ResourceGroupName> -n <ClusterName>
\n
\n
Once enabled, you can connect to your AKS cluster, deploy applications, and create Ingress objects with appropriate annotations for routing. There are some limitations to be aware of, such as the maximum number of supported Azure DNS zones and namespace editing restrictions. It's recommended to review theapplication routing add-on configurationfor additional information on SSL encryption and DNS integration.
\n
If you are familiar with the NGINX ingress controller, you can just replace thenginxingress class name inside an ingress object with the name of the ingress controller deployed by the application routing addon, that, by default is equal towebapprouting.kubernetes.azure.com:
If you leveragecert-managerand with Let's Encrypt certificate authority to issue TLS/SSL certificates to your application, make sure to create an issuer or a cluster issuer for the ingress class of the managed NGINX ingress controller installed by the application routing add-on. This can be done using the sample code provided below:
Ensure that you replaceadmin@contoso.comwith your own email address to receive notifications from Let's Encrypt. By using this configuration,cert-managerwill be able to issue certificates for the ingress class of the managed NGINX ingress controller when using the application routing add-on. Please note that the server URLhttps://acme-v02.api.letsencrypt.org/directoryis the Let's Encrypt production server. You can also use the staging serverhttps://acme-staging-v02.api.letsencrypt.org/directoryfor testing purposes to avoid rate limits. Ensure that the issuer or cluster issuer resource is deployed to your Kubernetes cluster, andcert-manageris properly installed and configured. For more detailed steps and instructions, refer toManaged nginx Ingress with the application routing add-on.
\n
\n
Deploy the Terraform modules
\n
Before deploying the Terraform modules in theterraformfolder, specify a value for the following variables in theterraform.tfvarsvariable definitions file.
\n
\n
\n
name_prefix = \"Contoso\"\ndomain = \"contoso.com\"\nkubernetes_version = \"1.28.3\"\nnamespace = \"chainlit\"\nservice_account_name = \"chainlit-sa\"\nssh_public_key = \"XXXXXXX\"\nvm_enabled = true\nlocation = \"eastus\"\nadmin_group_object_ids = [\"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"] \nweb_app_routing_enabled = true\ndns_zone_name = \"babosbird.com\"\ndns_zone_resource_group_name = \"DnsResourceGroup\"\n\ngrafana_admin_user_object_id = \"0c5267b2-01f3-4a59-970e-0d9218d5412e\"\nvnet_integration_enabled = true\nopenai_deployments = [\n {\n name = \"gpt-35-turbo-16k\"\n model = {\n name = \"gpt-35-turbo-16k\"\n version = \"0613\"\n }\n },\n {\n name = \"text-embedding-ada-002\"\n model = {\n name = \"text-embedding-ada-002\"\n version = \"2\"\n }\n }\n]\n
\n
\n
\n
Description:
\n
\n
\n
prefix: specifies a prefix for all the Azure resources.
\n
domain: specifies the domain part (e.g., subdomain.domain) of the hostname of the ingress object used to expose the chatbot via theNGINX Ingress Controller.
\n
kubernetes_version: specifies the Kubernetes version installed on the AKS cluster.
\n
namespace: specifies the namespace of the workload application that accesses the Azure OpenAI Service.
\n
service_account_name: specifies the name of the service account of the workload application that accesses the Azure OpenAI Service.
\n
ssh_public_key: specifies the SSH public key used for the AKS nodes and jumpbox virtual machine.
\n
vm_enabled: a boleean value that specifies whether deploying or not a jumpbox virtual machine in the same virtual network of the AKS cluster.
\n
location: specifies the region (e.g., westeurope) where deploying the Azure resources.
\n
admin_group_object_ids: when deploying an AKS cluster with Microsoft Entra ID and Azure RBAC integration, this array parameter contains the list of Microsoft Entra ID group object IDs that will have the admin role of the cluster.
openai_deployments: specifies the list of theAzure OpenAI Service modelsto create. This sample needs aGPTmodel for chat completions and an embeddings model.
\n
\n
\n
NOTE We suggest reading sensitive configuration data such as passwords or SSH keys from a pre-existing Azure Key Vault resource. For more information, seeReferencing Azure Key Vault secrets in Terraform. Before proceeding, also make sure to run theregister-preview-features.shBash script in theterraformfolder to register any preview feature used by the AKS cluster.
\n
\n
\n
OpenAI Terraform Module
\n
The following table contains the code from theopenai.tfTerraform module used to deploy theAzure OpenAI Service.
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, which were common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable features like Microsoft Entra ID for authentication. In our case, we need to specify a custom subdomain for ourAzure OpenAI Serviceas our chatbot application will use an Microsoft Entra ID security token to access it. By default, themain.tfmodule sets the value of thecustom_subdomain_nameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
The following table shows the code of the Terraform module used to deploy theAzure Kubernetes Service (AKS)cluster with a user-assigned managed identity. For more information on theazurerm_kubernetes_clusterresource, seeTerraformdocumentation.
This module allows to deploy anAzure Kubernetes Servicecluster with the following extensions and features:
\n
\n
\n
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
\n
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
\n
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
\n
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
NOTE You can deploy an AKS resource as a public cluster withAPI Server VNet Integrationenabled. During the installation, you can use Terraform modules that make use of theHelm,Kubectl, andKubernetesTerraform Providers to install packages and create Kubernetes entities. Once the installation is complete, you can turn the cluster to private.
\n
\n
\n
Private Endpoint and Private DNS Zone Terraform Modules
The following code snippet from themain.tfTerraform module creates the user-defined managed identity used by the chatbot to acquire a security token from Microsoft Entra ID viaMicrosoft Entra Workload ID.
The above code snippet performs the following steps:
\n
\n
\n
Creates a new user-defined managed identity.
\n
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
\n
Federate the managed identity with the service account used by the chatbot. The following information is necessary to create the federated identity credentials:\n
\n
The Kubernetes service account name.
\n
The Kubernetes namespace that will host the chatbot application.
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
\n
\n
\n
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for thechainlit-saservice account were created correctly, as shown in the following picture.
\n
\n
\n
Use Microsoft Entra Workload ID with Azure Kubernetes Service (AKS)
\n
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Microsoft Entra ID application credentials or managed identities to access Microsoft Entra ID protected resources, such as Azure Key Vault and Microsoft Graph. Microsoft Entra Workload ID integrates with the capabilities native to Kubernetes to federate with external identity providers.
resource \"helm_release\" \"nginx_ingress_controller\" {\n name = \"ingress-nginx\"\n repository = \"https://kubernetes.github.io/ingress-nginx\"\n chart = \"ingress-nginx\"\n namespace = \"ingress-basic\"\n create_namespace = true\n timeout = 600\n\n set {\n name = \"controller.metrics.enabled\"\n value = \"true\"\n }\n\n set {\n name = \"controller.metrics.serviceMonitor.enabled\"\n value = \"true\"\n }\n \n set {\n name = \"controller.metrics.serviceMonitor.additionalLabels.release\"\n value = \"prometheus\"\n }\n\n\n set {\n name = \"controller.service.annotations.service\\\\.beta\\\\.kubernetes\\\\.io/azure-load-balancer-health-probe-request-path\"\n value = \"/healthz\"\n }\n\n set {\n name = \"controller.nodeSelector.kubernetes\\\\.io/os\"\n value = \"linux\"\n }\n\n set {\n name = \"controller.replicaCount\"\n value = \"${var.nginx_replica_count}\"\n }\n\n depends_on = [helm_release.prometheus]\n}
\n
\n
Instead, the following module is used to create the workload namespace and service account:
In particular, thekubectl_manifestresource makes use of variables to set the following service account annotations and labels necessary to Microsoft Entra Workload ID. For more information, seeService account labels and annotations.
\n
\n
Simple Chat Application
\n
The Simple Chat Application is a large language model-based chatbot that allows users to submit general-purpose questions to aGPTmodel, which generates and streams back human-like and engaging conversational responses. The following picture shows the welcome screen of the chat application.
\n
\n
\n
You can modify the welcome screen in markdown by editing thechainlit.mdfile at the project's root. If you do not want a welcome screen, leave the file empty. The following picture shows what happens when a user submits a new message in the chat.
\n
\n
\n
Chainlit can render messages in markdown format as shown by the following prompt:
\n
\n
\n
Chainlit also provides classes to support the following elements:
\n
\n
\n
Audio: TheAudioclass allows you to display an audio player for a specific audio file in the chatbot user interface. You must provide either a URL or a path or content bytes.
\n
Avatar: TheAvatarclass allows you to display an avatar image next to a message instead of the author's name. You need to send the element once. Next,, if an avatar's name matches an author's name, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
File: TheFileclass allows you to display a button that lets users download the content of the file. You must provide either a URL or a path or content bytes.
\n
Image: TheImageclass is designed to create and handle image elements to be sent and displayed in the chatbot user interface. You must provide either a URL or a path or content bytes.
\n
Pdf: ThePdfclass allows you to display a PDF hosted remotely or locally in the chatbot UI. This class either takes a URL of a PDF hosted online or the path of a local PDF.
\n
Pyplot: ThePyplotclass allows you to display aMatplotlibpyplot chart in the chatbot UI. This class takes a pyplot figure.
\n
TaskList: TheTaskListclass allows you to display a task list next to the chatbot UI.
\n
Text: TheTextclass allows you to display a text element in the chatbot UI. This class takes a string and creates a text element that can be sent to the UI. It supports the markdown syntax for formatting text. You must provide either a URL or a path or content bytes.
\n
\n
\n
You can click the user icon on the UI to access the chat settings and choose, for example, between the light and dark theme.
\n
\n
\n
The application is built in Python. Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
These are the libraries used by the chat application:
\n
\n\n
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
\n
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
\n
openai: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
logging: This module provides flexible logging of messages.
\n
chainlit as cl: This imports theChainlitlibrary and aliases it ascl. Chainlit is used to create the UI of the application.
\n
from azure.identity import DefaultAzureCredential, get_bearer_token_provider: when theopenai_typeproperty value isazure_ad,aDefaultAzureCredentialobject from theAzure Identity client library for Pythonis used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity federated with the service account.
\n
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n\n
\n
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
\n
\n
pip install -r requirements.txt --upgrade
\n
\n
Next, the code reads the value of the environment variables used to initialize Azure OpenAI objects. In addition, it creates a token provider for Azure OpenAI.
Here's a brief explanation of each variable and related environment variable:
\n
\n\n
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
\n
api_base: The base URL for the OpenAI API.
\n
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
\n
api_type: A string representing the type of the OpenAI API.
\n
api_version: A string representing the version of the OpenAI API.
\n
engine: The engine used for OpenAI API calls.
\n
model: The model used for OpenAI API calls.
\n
system_content: The content of the system message used for OpenAI API calls.
\n
max_retries: The maximum number of retries for OpenAI API calls.
\n
timeout: The timeout in seconds.
\n
debug: When debug is equal totrue,t, or1, the logger writes the chat completion answers.
\n\n
\n
In the next section, the code creates theAsyncAzureOpenAIclient object used by the application to communicate with the Azure OpenAI Service instance. When theapi_typeis equal toazure, the code initializes the object with the API key. Otherwise, it initializes theazure_ad_token_providerproperty to the token provider created earlier. Then the code creates a logger.
The backoff time is calculated using thebackoff_in_secondsandattemptvariables. It follows the formulabackoff_in_seconds * 2 ** attempt + random.uniform(0, 1). This formula increases the backoff time exponentially with each attempt and adds a random value between 0 and 1 to avoid synchronized retries.
\n
Next, the code defines a function calledstart_chatthat is used to initialize the UI when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
\n
\n
\n
cl.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
\n
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
cl.user_session.set(): This API call sets a value in theuser_sessiondictionary. In this case, it initializes themessage_historyin the user's session with a system content message, which indicates the start of the chat.
\n
\n
Finally, the application defines the method called whenever the user sends a new message in the chat.
\n
\n
@cl.on_message\nasync def on_message(message: cl.Message):\n message_history = cl.user_session.get(\"message_history\")\n message_history.append({\"role\": \"user\", \"content\": message.content})\n logger.info(\"Question: [%s]\", message.content)\n\n # Create the Chainlit response message\n msg = cl.Message(content=\"\")\n\n async for stream_resp in await openai.chat.completions.create(\n model=model,\n messages=message_history,\n temperature=temperature,\n stream=True,\n ):\n if stream_resp and len(stream_resp.choices) > 0:\n token = stream_resp.choices[0].delta.content or \"\"\n await msg.stream_token(token)\n\n if debug:\n logger.info(\"Answer: [%s]\", msg.content)\n\n message_history.append({\"role\": \"assistant\", \"content\": msg.content})\n await msg.send()
\n
\n
Here is a detailed explanation of the function steps:
\n
\n
\n
cl.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
\n
cl.user_session.get(): This API call retrieves a value from the user's session data stored in theuser_sessiondictionary. In this case, it fetches themessage_historyfrom the user's session to maintain the chat history.
\n
message_history.append(): This API call appends a new message to themessage_historylist. It is used to add the user's message and the assistant's response to the chat history.
\n
cl.Message(): This API call creates a ChainlitMessageobject. TheMessageclass is designed to send, stream, edit, or remove messages in the chatbot user interface. In this sample, theMessageobject is used to stream the OpenAI response in the chat.
\n
msg.stream_token(): Thestream_tokenmethod of theMessageclass streams a token to the response message. It is used to send the response from the OpenAI Chat API in chunks to ensure real-time streaming in the chat.
\n
await openai.chat.completions.create(): This API call sends a message to theOpenAI Chat APIin an asynchronous mode and streams the response. It uses the providedmessage_historyas context for generating the assistant's response.
\n
\n
Below, you can read the complete code of the application.
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
\n
\n
chainlit run app.py -w
\n
\n
Documents QA Chat
\n
The Documents QA Chat application allows users to submit up to 10.pdfand.docxdocuments. The application processes the uploaded documents to create vector embeddings. These embeddings are stored inChromaDBvector database for efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response. The following picture shows the chat application interface. As you can see, you can click theBrowsebutton and choose up to 10.pdfand.docxdocuments to upload. Alternatively, you can just drag and drop the files over the control area.
\n
\n
\n
After uploading the documents, the application creates and stores embeddings toChromaDBvector database. During the phase, the UI shows a messageProcessing <file-1>, <file-2>..., as shown in the following picture:
\n
\n
\n
When the code finished creating embeddings, the UI is ready to receive user's questions:
\n
\n
\n
As your chat application grows in complexity, understanding the individual steps for generating a specific answer can become challenging. To solve this issue, Chainlit allows you to easily explore the reasoning process right from the user interface using theChain of Thought. If you are using theLangChainintegration, every intermediary step is automatically sent and displayed in the Chainlit UI just clicking and expanding the steps, as shown in the following picture:
\n
\n
\n
To see the text chunks that were used by the large language model to originate the response, you can click the sources links, as shown in the following picture:
\n
\n
\n
In theChain of Thought, below the step used to invoke the OpenAI chat completion API, you can find an
\n
Inspect in prompt playground icon. Clicking on it opens the Prompt Playground dialog which allows you to modify and iterate on the prompt as needed.
\n
\n
\n
As shown in the following picture, you can click and edit the value of the highlighted variables in the user prompt:
\n
\n
\n
You can then click and edit the user question.
\n
\n
\n
Then, you can click the submit button to test the effect of your changes, as shown in the following picture.
\n
\n
\n
Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
These are the libraries used by the chat application:
\n
\n\n
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
\n
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
\n
time: This module provides various time-related functions for time manipulation and measurement.
\n
openai: the OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
logging: This module provides flexible logging of messages.
\n
chainlit as cl: This imports theChainlitlibrary and aliases it ascl.Chainlit is used to create the UI of the application.
\n
AzureChatOpenAIfromchainlit.playground.config import: you need to importAzureChatOpenAIfromchainlit.playground.configto use the Chainlit Playground.
\n
DefaultAzureCredentialfromazure.identity: when theopenai_typeproperty value isazure_ad, aDefaultAzureCredentialobject from theAzure Identity client library for Python - version 1.13.0is used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity, whose client ID is defined in theAZURE_CLIENT_IDenvironment variable.
\n
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n
langchain: Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not. However, using these LLMs in isolation is often insufficient for creating a truly powerful app - the real power comes when you can combine them with other sources of computation or knowledge. LangChain library aims to assist in the development of those types of applications.
\n\n
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
\n
\n
pip install -r requirements.txt --upgrade
\n
\n
Next, the code reads environment variables and configures the OpenAI settings.
\n
\n
# Read environment variables\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-12-01-preview\")\nchat_completion_deployment = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nembeddings_deployment = os.getenv(\"AZURE_OPENAI_ADA_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\nmax_size_mb = int(os.getenv(\"CHAINLIT_MAX_SIZE_MB\", 100))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\ntext_splitter_chunk_size = int(os.getenv(\"TEXT_SPLITTER_CHUNK_SIZE\", 1000))\ntext_splitter_chunk_overlap = int(os.getenv(\"TEXT_SPLITTER_CHUNK_OVERLAP\", 10))\nembeddings_chunk_size = int(os.getenv(\"EMBEDDINGS_CHUNK_SIZE\", 16))\nmax_retries = int(os.getenv(\"MAX_RETRIES\", 5))\nretry_min_seconds = int(os.getenv(\"RETRY_MIN_SECONDS\", 1))\nretry_max_seconds = int(os.getenv(\"RETRY_MAX_SECONDS\", 5))\ntimeout = int(os.getenv(\"TIMEOUT\", 30))\ndebug = os.getenv(\"DEBUG\", \"False\").lower() in (\"true\", \"1\", \"t\")\n\n# Configure system prompt\nsystem_template = \"\"\"Use the following pieces of context to answer the users question.\nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\nALWAYS return a \"SOURCES\" part in your answer.\nThe \"SOURCES\" part should be a reference to the source of the document from which you got your answer.\n\nExample of your response should be:\n\n\\`\\`\\`\nThe answer is foo\nSOURCES: xyz\n\\`\\`\\`\n\nBegin!\n----------------\n{summaries}\"\"\"\nmessages = [\n SystemMessagePromptTemplate.from_template(system_template),\n HumanMessagePromptTemplate.from_template(\"{question}\"),\n]\nprompt = ChatPromptTemplate.from_messages(messages)\nchain_type_kwargs = {\"prompt\": prompt}\n\n# Configure a logger\nlogging.basicConfig(\n stream=sys.stdout,\n format=\"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s\",\n level=logging.INFO,\n)\nlogger = logging.getLogger(__name__)\n\n# Create Token Provider\nif api_type == \"azure_ad\":\n token_provider = get_bearer_token_provider(\n DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n )\n\n# Setting the environment variables for the playground\nif api_type == \"azure\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = api_key\nos.environ[\"AZURE_OPENAI_API_VERSION\"] = api_version\nos.environ[\"AZURE_OPENAI_ENDPOINT\"] = api_base\nos.environ[\"AZURE_OPENAI_DEPLOYMENT_NAME\"] = chat_completion_deployment
\n
\n
Here's a brief explanation of each variable and related environment variable:
\n
\n\n
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
\n
api_base: The base URL for the OpenAI API.
\n
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
\n
api_type: A string representing the type of the OpenAI API.
\n
api_version: A string representing the version of the OpenAI API.
\n
chat_completion_deployment: the name of the Azure OpenAI GPT model for chat completion.
\n
embeddings_deployment: the name of the Azure OpenAI deployment for embeddings.
\n
model: The model used for chat completion calls (e.g,gpt-35-turbo-16k).
\n
max_size_mb: the maximum size for the uploaded documents.
\n
max_files: the maximum number of documents that can be uploaded.
\n
text_splitter_chunk_size: the maximum chunk size used by theRecursiveCharacterTextSplitterobject.
\n
text_splitter_chunk_overlap: the maximum chunk overlap used by theRecursiveCharacterTextSplitterobject.
\n
embeddings_chunk_size: the maximum chunk size used by theOpenAIEmbeddingsobject.
\n
max_retries: The maximum number of retries for OpenAI API calls.
\n
retry_min_seconds: the minimum number of seconds before a retry.
\n
retry_max_seconds: the maximum number of seconds before a retry.
\n
timeout: The timeout in seconds.
\n
system_template: The content of the system message used for OpenAI API calls.
\n
debug: When debug is equal totrue,t, or1, the logger switches to verbose mode.
\n\n
\n
Next, the code defines a function calledstart_chatthat is used to initialize the when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
\n
\n
\n
cl.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
\n
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
\n
\n
The following code is used to initialize the large language model (LLM) chain used to reply to questions on the content of the uploaded documents.
\n
\n
# Initialize the file list to None\n files = None\n\n # Wait for the user to upload a file\n while files == None:\n files = await cl.AskFileMessage(\n content=f\"Please upload up to {max_files} `.pdf` or `.docx` files to begin.\",\n accept=[\n \"application/pdf\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n max_size_mb=max_size_mb,\n max_files=max_files,\n timeout=86400,\n raise_on_timeout=False,\n ).send()
\n
\n
TheAskFileMessageAPI call prompts the user to upload up to a specified number of.pdfor.docxfiles. The uploaded files are stored in thefilesvariable. The process continues until the user uploads files. For more information, seeAskFileMessage.
\n
The following code processes each uploaded file by extracting its content.
\n
\n\n
The text content of each file is stored in the listall_texts.
\n
This code performs text processing and chunking. It checks the file extension to read the file content accordingly, depending on if it's a.pdfor a.docxdocument.
Metadata is created for each chunk and stored in themetadataslist.
\n\n
\n
# Create a message to inform the user that the files are being processed\n content = \"\"\n if len(files) == 1:\n content = f\"Processing `{files[0].name}`...\"\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"Processing {', '.join(files_names)}...\"\n logger.info(content)\n msg = cl.Message(content=content, author=\"Chatbot\")\n await msg.send()\n\n # Create a list to store the texts of each file\n all_texts = []\n\n # Process each file uplodaded by the user\n for file in files:\n # Read file contents\n with open(file.path, \"rb\") as uploaded_file:\n file_contents = uploaded_file.read()\n\n logger.info(\"[%d] bytes were read from %s\", len(file_contents), file.path)\n\n # Create an in-memory buffer from the file content\n bytes = io.BytesIO(file_contents)\n\n # Get file extension\n extension = file.name.split(\".\")[-1]\n\n # Initialize the text variable\n text = \"\"\n\n # Read the file\n if extension == \"pdf\":\n reader = PdfReader(bytes)\n for i in range(len(reader.pages)):\n text += reader.pages[i].extract_text()\n if debug:\n logger.info(\"[%s] read from %s\", text, file.path)\n elif extension == \"docx\":\n doc = Document(bytes)\n paragraph_list = []\n for paragraph in doc.paragraphs:\n paragraph_list.append(paragraph.text)\n if debug:\n logger.info(\"[%s] read from %s\", paragraph.text, file.path)\n text = \"\\n\".join(paragraph_list)\n\n # Split the text into chunks\n text_splitter = RecursiveCharacterTextSplitter(\n chunk_size=text_splitter_chunk_size,\n chunk_overlap=text_splitter_chunk_overlap,\n )\n texts = text_splitter.split_text(text)\n\n # Add the chunks and metadata to the list\n all_texts.extend(texts)\n\n # Create a metadata for each chunk\n metadatas = [{\"source\": f\"{i}-pl\"} for i in range(len(all_texts))]
\n
\n
The next piece of code performs the following steps:
\n
\n\n
It creates anAzureOpenAIEmbeddingsconfigured to use the embeddings model in the Azure OpenAI Service to create embeddings from text chunks.
\n
It creates aChromaDBvector database using theOpenAIEmbeddingsobject, the text chunks list, and the metadata list.
\n
It creates anAzureChatOpenAILangChain object based on the GPR model hosted in Azure OpenAI Service.
It stores the metadata and text chunks in the user session using thecl.user_session.set()API call.
\n
It creates a message to inform the user that the files are ready for queries, and finally returns thechain.
\n
Thecl.user_session.set(\"chain\", chain)call stores the LLM chain in theuser_sessiondictionary for later use.
\n\n
The next section create the LangChain LLM chain.
\n
\n
# Create a Chroma vector store\n if api_type == \"azure\":\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n else:\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n azure_endpoint=api_base,\n azure_ad_token_provider=token_provider,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n\n # Create a Chroma vector store\n db = await cl.make_async(Chroma.from_texts)(\n all_texts, embeddings, metadatas=metadatas\n )\n\n # Create an AzureChatOpenAI llm\n if api_type == \"azure\":\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n else:\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n azure_endpoint=api_base,\n api_key=api_key,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n azure_ad_token_provider=token_provider,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n\n # Create a chain that uses the Chroma vector store\n chain = RetrievalQAWithSourcesChain.from_chain_type(\n llm=llm,\n chain_type=\"stuff\",\n retriever=db.as_retriever(),\n return_source_documents=True,\n chain_type_kwargs=chain_type_kwargs,\n )\n\n # Save the metadata and texts in the user session\n cl.user_session.set(\"metadatas\", metadatas)\n cl.user_session.set(\"texts\", all_texts)\n\n # Create a message to inform the user that the files are ready for queries\n content = \"\"\n if len(files) == 1:\n content = f\"`{files[0].name}` processed. You can now ask questions!\"\n logger.info(content)\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"{', '.join(files_names)} processed. You can now ask questions.\"\n logger.info(content)\n msg.content = content\n msg.author = \"Chatbot\"\n await msg.update()\n\n # Store the chain in the user session\n cl.user_session.set(\"chain\", chain)
\n
\n
The following code handles the communication with the OpenAI API and incorporates retrying logic in case the API calls fail due to specific errors.
\n
\n
\n
cl.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
\n
cl.user_session.get(\"chain\"): this call retrieves the LLM chain from theuser_sessiondictionary.
\n
cl.AsyncLangchainCallbackHandler: this call creates a LangChain callback handler.
\n
await chain.acall: The asynchronous call to theRetrievalQAWithSourcesChain.acallexecutes the LLM chain with the user message as an input.
\n
\n
\n
@cl.on_message\nasync def main(message: cl.Message):\n # Retrieve the chain from the user session\n chain = cl.user_session.get(\"chain\")\n\n # Create a callback handler\n cb = cl.AsyncLangchainCallbackHandler()\n\n # Get the response from the chain\n response = await chain.acall(message.content, callbacks=[cb])\n logger.info(\"Question: [%s]\", message.content)
\n
\n
The code below extracts the answers and sources from the API response and formats them to be sent as a message.
\n
\n
Theanswerandsourcesare obtained from theresponsedictionary.
\n
The sources are then processed to find corresponding texts in the user session metadata (metadatas) and createsource_elementsusingcl.Text().
\n
cl.Message().send(): theMessageAPI creates and displays a message containing the answer and sources, if available.
\n
The last command sets theAZURE_OPENAI_API_KEYenvironment variable to a security key to access Azure OpenAI returned by the token provider. This key is used by the Chainlit playground.
\n
\n
\n
\n
# Get the answer and sources from the response\n answer = response[\"answer\"]\n sources = response[\"sources\"].strip()\n source_elements = []\n\n if debug:\n logger.info(\"Answer: [%s]\", answer)\n\n # Get the metadata and texts from the user session\n metadatas = cl.user_session.get(\"metadatas\")\n all_sources = [m[\"source\"] for m in metadatas]\n texts = cl.user_session.get(\"texts\")\n\n if sources:\n found_sources = []\n\n # Add the sources to the message\n for source in sources.split(\",\"):\n source_name = source.strip().replace(\".\", \"\")\n # Get the index of the source\n try:\n index = all_sources.index(source_name)\n except ValueError:\n continue\n text = texts[index]\n found_sources.append(source_name)\n # Create the text element referenced in the message\n source_elements.append(cl.Text(content=text, name=source_name))\n\n if found_sources:\n answer += f\"\\nSources: {', '.join(found_sources)}\"\n else:\n answer += \"\\nNo sources found\"\n\n await cl.Message(content=answer, elements=source_elements).send()\n\n # Setting the AZURE_OPENAI_API_KEY environment variable for the playground\n if api_type == \"azure_ad\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = token_provider()\n
\n
\n
\n
Below, you can read the complete code of the application.
\n
\n
# Import packages\nimport os\nimport io\nimport sys\nimport logging\nimport chainlit as cl\nfrom chainlit.playground.config import AzureChatOpenAI\nfrom pypdf import PdfReader\nfrom docx import Document\nfrom azure.identity import DefaultAzureCredential, get_bearer_token_provider\nfrom dotenv import load_dotenv\nfrom dotenv import dotenv_values\nfrom langchain.embeddings import AzureOpenAIEmbeddings\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain.vectorstores.chroma import Chroma\nfrom langchain.chains import RetrievalQAWithSourcesChain\nfrom langchain.chat_models import AzureChatOpenAI\nfrom langchain.prompts.chat import (\n ChatPromptTemplate,\n SystemMessagePromptTemplate,\n HumanMessagePromptTemplate,\n)\n\n# Load environment variables from .env file\nif os.path.exists(\".env\"):\n load_dotenv(override=True)\n config = dotenv_values(\".env\")\n\n# Read environment variables\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-12-01-preview\")\nchat_completion_deployment = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nembeddings_deployment = os.getenv(\"AZURE_OPENAI_ADA_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\nmax_size_mb = int(os.getenv(\"CHAINLIT_MAX_SIZE_MB\", 100))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\ntext_splitter_chunk_size = int(os.getenv(\"TEXT_SPLITTER_CHUNK_SIZE\", 1000))\ntext_splitter_chunk_overlap = int(os.getenv(\"TEXT_SPLITTER_CHUNK_OVERLAP\", 10))\nembeddings_chunk_size = int(os.getenv(\"EMBEDDINGS_CHUNK_SIZE\", 16))\nmax_retries = int(os.getenv(\"MAX_RETRIES\", 5))\nretry_min_seconds = int(os.getenv(\"RETRY_MIN_SECONDS\", 1))\nretry_max_seconds = int(os.getenv(\"RETRY_MAX_SECONDS\", 5))\ntimeout = int(os.getenv(\"TIMEOUT\", 30))\ndebug = os.getenv(\"DEBUG\", \"False\").lower() in (\"true\", \"1\", \"t\")\n\n# Configure system prompt\nsystem_template = \"\"\"Use the following pieces of context to answer the users question.\nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\nALWAYS return a \"SOURCES\" part in your answer.\nThe \"SOURCES\" part should be a reference to the source of the document from which you got your answer.\n\nExample of your response should be:\n\n```\nThe answer is foo\nSOURCES: xyz\n```\n\nBegin!\n----------------\n{summaries}\"\"\"\nmessages = [\n SystemMessagePromptTemplate.from_template(system_template),\n HumanMessagePromptTemplate.from_template(\"{question}\"),\n]\nprompt = ChatPromptTemplate.from_messages(messages)\nchain_type_kwargs = {\"prompt\": prompt}\n\n# Configure a logger\nlogging.basicConfig(\n stream=sys.stdout,\n format=\"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s\",\n level=logging.INFO,\n)\nlogger = logging.getLogger(__name__)\n\n# Create Token Provider\nif api_type == \"azure_ad\":\n token_provider = get_bearer_token_provider(\n DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n )\n\n# Setting the environment variables for the playground\nif api_type == \"azure\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = api_key\nos.environ[\"AZURE_OPENAI_API_VERSION\"] = api_version\nos.environ[\"AZURE_OPENAI_ENDPOINT\"] = api_base\nos.environ[\"AZURE_OPENAI_DEPLOYMENT_NAME\"] = chat_completion_deployment\n\n\n@cl.on_chat_start\nasync def start():\n await cl.Avatar(\n name=\"Chatbot\", url=\"https://cdn-icons-png.flaticon.com/512/8649/8649595.png\"\n ).send()\n await cl.Avatar(\n name=\"Error\", url=\"https://cdn-icons-png.flaticon.com/512/8649/8649595.png\"\n ).send()\n await cl.Avatar(\n name=\"You\",\n url=\"https://media.architecturaldigest.com/photos/5f241de2c850b2a36b415024/master/w_1600%2Cc_limit/Luke-logo.png\",\n ).send()\n\n # Initialize the file list to None\n files = None\n\n # Wait for the user to upload a file\n while files == None:\n files = await cl.AskFileMessage(\n content=f\"Please upload up to {max_files} `.pdf` or `.docx` files to begin.\",\n accept=[\n \"application/pdf\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n max_size_mb=max_size_mb,\n max_files=max_files,\n timeout=86400,\n raise_on_timeout=False,\n ).send()\n\n # Create a message to inform the user that the files are being processed\n content = \"\"\n if len(files) == 1:\n content = f\"Processing `{files[0].name}`...\"\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"Processing {', '.join(files_names)}...\"\n logger.info(content)\n msg = cl.Message(content=content, author=\"Chatbot\")\n await msg.send()\n\n # Create a list to store the texts of each file\n all_texts = []\n\n # Process each file uplodaded by the user\n for file in files:\n # Read file contents\n with open(file.path, \"rb\") as uploaded_file:\n file_contents = uploaded_file.read()\n\n logger.info(\"[%d] bytes were read from %s\", len(file_contents), file.path)\n\n # Create an in-memory buffer from the file content\n bytes = io.BytesIO(file_contents)\n\n # Get file extension\n extension = file.name.split(\".\")[-1]\n\n # Initialize the text variable\n text = \"\"\n\n # Read the file\n if extension == \"pdf\":\n reader = PdfReader(bytes)\n for i in range(len(reader.pages)):\n text += reader.pages[i].extract_text()\n if debug:\n logger.info(\"[%s] read from %s\", text, file.path)\n elif extension == \"docx\":\n doc = Document(bytes)\n paragraph_list = []\n for paragraph in doc.paragraphs:\n paragraph_list.append(paragraph.text)\n if debug:\n logger.info(\"[%s] read from %s\", paragraph.text, file.path)\n text = \"\\n\".join(paragraph_list)\n\n # Split the text into chunks\n text_splitter = RecursiveCharacterTextSplitter(\n chunk_size=text_splitter_chunk_size,\n chunk_overlap=text_splitter_chunk_overlap,\n )\n texts = text_splitter.split_text(text)\n\n # Add the chunks and metadata to the list\n all_texts.extend(texts)\n\n # Create a metadata for each chunk\n metadatas = [{\"source\": f\"{i}-pl\"} for i in range(len(all_texts))]\n\n # Create a Chroma vector store\n if api_type == \"azure\":\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n else:\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n azure_endpoint=api_base,\n azure_ad_token_provider=token_provider,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n\n # Create a Chroma vector store\n db = await cl.make_async(Chroma.from_texts)(\n all_texts, embeddings, metadatas=metadatas\n )\n\n # Create an AzureChatOpenAI llm\n if api_type == \"azure\":\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n else:\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n azure_endpoint=api_base,\n api_key=api_key,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n azure_ad_token_provider=token_provider,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n\n # Create a chain that uses the Chroma vector store\n chain = RetrievalQAWithSourcesChain.from_chain_type(\n llm=llm,\n chain_type=\"stuff\",\n retriever=db.as_retriever(),\n return_source_documents=True,\n chain_type_kwargs=chain_type_kwargs,\n )\n\n # Save the metadata and texts in the user session\n cl.user_session.set(\"metadatas\", metadatas)\n cl.user_session.set(\"texts\", all_texts)\n\n # Create a message to inform the user that the files are ready for queries\n content = \"\"\n if len(files) == 1:\n content = f\"`{files[0].name}` processed. You can now ask questions!\"\n logger.info(content)\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"{', '.join(files_names)} processed. You can now ask questions.\"\n logger.info(content)\n msg.content = content\n msg.author = \"Chatbot\"\n await msg.update()\n\n # Store the chain in the user session\n cl.user_session.set(\"chain\", chain)\n\n\n@cl.on_message\nasync def main(message: cl.Message):\n # Retrieve the chain from the user session\n chain = cl.user_session.get(\"chain\")\n\n # Create a callback handler\n cb = cl.AsyncLangchainCallbackHandler()\n\n # Get the response from the chain\n response = await chain.acall(message.content, callbacks=[cb])\n logger.info(\"Question: [%s]\", message.content)\n\n # Get the answer and sources from the response\n answer = response[\"answer\"]\n sources = response[\"sources\"].strip()\n source_elements = []\n\n if debug:\n logger.info(\"Answer: [%s]\", answer)\n\n # Get the metadata and texts from the user session\n metadatas = cl.user_session.get(\"metadatas\")\n all_sources = [m[\"source\"] for m in metadatas]\n texts = cl.user_session.get(\"texts\")\n\n if sources:\n found_sources = []\n\n # Add the sources to the message\n for source in sources.split(\",\"):\n source_name = source.strip().replace(\".\", \"\")\n # Get the index of the source\n try:\n index = all_sources.index(source_name)\n except ValueError:\n continue\n text = texts[index]\n found_sources.append(source_name)\n # Create the text element referenced in the message\n source_elements.append(cl.Text(content=text, name=source_name))\n\n if found_sources:\n answer += f\"\\nSources: {', '.join(found_sources)}\"\n else:\n answer += \"\\nNo sources found\"\n\n await cl.Message(content=answer, elements=source_elements).send()\n\n # Setting the AZURE_OPENAI_API_KEY environment variable for the playground\n if api_type == \"azure_ad\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = token_provider()
\n
\n
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
\n
\n
chainlit run app.py -w
\n
\n
Build Docker Images
\n
You can use thesrc/01-build-docker-images.shBash script to build the Docker container image for each container app.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Use a for loop to build the docker images using the array index\nfor index in ${!images[@]}; do\n # Build the docker image\n docker build -t ${images[$index]}:$tag -f Dockerfile --build-arg FILENAME=${filenames[$index]} --build-arg PORT=$port .\ndone
\n
\n
Before running any script in thesrcfolder, make sure to customize the value of the variables inside the00-variables.shfile located in the same folder. This file is embedded in all the scripts and contains the following variables:
TheDockerfileunder thesrcfolder is parametric and can be used to build the container images for both chat applications.
\n
\n
# app/Dockerfile\n\n# # Stage 1 - Install build dependencies\n\n# A Dockerfile must start with a FROM instruction which sets the base image for the container.\n# The Python images come in many flavors, each designed for a specific use case.\n# The python:3.11-slim image is a good base image for most applications.\n# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.\n# The slim image is a good choice because it is small and contains only the packages needed to run Python.\n# For more information, see: \n# * https://hub.docker.com/_/python \n# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker\nFROM python:3.11-slim AS builder\n\n# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.\n# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir\nWORKDIR /app\n\n# Set environment variables. \n# The ENV instruction sets the environment variable <key> to the value <value>.\n# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#env\nENV PYTHONDONTWRITEBYTECODE 1\nENV PYTHONUNBUFFERED 1\n\n# Install git so that we can clone the app code from a remote repo using the RUN instruction.\n# The RUN comand has 2 forms:\n# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)\n# * RUN [\"executable\", \"param1\", \"param2\"] (exec form)\n# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. \n# The resulting committed image will be used for the next step in the Dockerfile.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#run\nRUN apt-get update && apt-get install -y \\\n build-essential \\\n curl \\\n software-properties-common \\\n git \\\n && rm -rf /var/lib/apt/lists/*\n\n# Create a virtualenv to keep dependencies together\nRUN python -m venv /opt/venv\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the requirements.txt which contains dependencies to WORKDIR\n# COPY has two forms:\n# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)\n# * COPY [\"<src>\",... \"<dest>\"] (this form is required for paths containing whitespace)\n# For more information, see: https://docs.docker.com/engine/reference/builder/#copy\nCOPY requirements.txt .\n\n# Install the Python dependencies\nRUN pip install --no-cache-dir --no-deps -r requirements.txt\n\n# Stage 2 - Copy only necessary files to the runner stage\n\n# The FROM instruction initializes a new build stage for the application\nFROM python:3.11-slim\n\n# Define the filename to copy as an argument\nARG FILENAME\n\n# Deefine the port to run the application on as an argument\nARG PORT=8000\n\n# Set an environment variable\nENV FILENAME=${FILENAME}\n\n# Sets the working directory to /app\nWORKDIR /app\n\n# Copy the virtual environment from the builder stage\nCOPY --from=builder /opt/venv /opt/venv\n\n# Set environment variables\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the $FILENAME containing the application code\nCOPY $FILENAME .\n\n# Copy the chainlit.md file to the working directory\nCOPY chainlit.md .\n\n# Copy the .chainlit folder to the working directory\nCOPY ./.chainlit ./.chainlit\n\n# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#expose\nEXPOSE $PORT\n\n# The ENTRYPOINT instruction has two forms:\n# * ENTRYPOINT [\"executable\", \"param1\", \"param2\"] (exec form, preferred)\n# * ENTRYPOINT command param1 param2 (shell form)\n# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint\nCMD chainlit run $FILENAME --port=$PORT
\n
\n
Test applications locally
\n
You can use thesrc/02-run-docker-container.shBash script to test the containers for thesender,processor, andreceiverapplications.
Push Docker containers to the Azure Container Registry
\n
You can use thesrc/03-push-docker-image.shBash script to push the Docker container images for thesender,processor, andreceiverapplications to theAzure Container Registry (ACR).
\n
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Login to ACR\naz acr login --name $acrName \n\n# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. \nloginServer=$(az acr show --name $acrName --query loginServer --output tsv)\n\n# Use a for loop to tag and push the local docker images to the Azure Container Registry\nfor index in ${!images[@]}; do\n # Tag the local sender image with the loginServer of ACR\n docker tag ${images[$index],,}:$tag $loginServer/${images[$index],,}:$tag\n\n # Push the container image to ACR\n docker push $loginServer/${images[$index],,}:$tag\ndone
\n
\n
\n
Deployment Scripts
\n
If you deployed the Azure infrastructure using the Terraform modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
\n
Scripts:
\n
\n
\n
09-deploy-apps.sh
\n
10-configure-dns.sh
\n
\n
\n
YAML manifests:
\n
\n
\n
configMap.yml
\n
deployment.yml
\n
ingress.yml
\n
service.yml
\n
\n
\n
If you instead want to deploy the application in your AKS cluster, make sure to run all of the scripts in order.
\n
The09-deploy-apps.shscript creates the configmap, deployment, service, and ingress Kubernetes objects for thechatanddocsapplications. This script makes use of theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Attach ACR to AKS cluster\nif [[ $attachAcr == true ]]; then\n echo \"Attaching ACR $acrName to AKS cluster $aksClusterName...\"\n az aks update \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --attach-acr $acrName\nfi\n\n# Check if namespace exists in the cluster\nresult=$(kubectl get namespace -o jsonpath=\"{.items[?(@.metadata.name=='$namespace')].metadata.name}\")\n\nif [[ -n $result ]]; then\n echo \"$namespace namespace already exists in the cluster\"\nelse\n echo \"$namespace namespace does not exist in the cluster\"\n echo \"creating $namespace namespace in the cluster...\"\n kubectl create namespace $namespace\nfi\n\n# Create config map\ncat $configMapTemplate |\n yq \"(.data.TITLE)|=\"\\\"\"$title\"\\\" |\n yq \"(.data.LABEL)|=\"\\\"\"$label\"\\\" |\n yq \"(.data.TEMPERATURE)|=\"\\\"\"$temperature\"\\\" |\n yq \"(.data.IMAGE_WIDTH)|=\"\\\"\"$imageWidth\"\\\" |\n yq \"(.data.AZURE_OPENAI_TYPE)|=\"\\\"\"$openAiType\"\\\" |\n yq \"(.data.AZURE_OPENAI_BASE)|=\"\\\"\"$openAiBase\"\\\" |\n yq \"(.data.AZURE_OPENAI_MODEL)|=\"\\\"\"$openAiModel\"\\\" |\n yq \"(.data.AZURE_OPENAI_DEPLOYMENT)|=\"\\\"\"$openAiDeployment\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\ncat $deploymentTemplate |\n yq \"(.spec.template.spec.containers[0].image)|=\"\\\"\"$image\"\\\" |\n yq \"(.spec.template.spec.containers[0].imagePullPolicy)|=\"\\\"\"$imagePullPolicy\"\\\" |\n yq \"(.spec.template.spec.serviceAccountName)|=\"\\\"\"$serviceAccountName\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\nkubectl apply -f $serviceTemplate -n $namespace
\n
\n
The 10-configure-dns.shscript creates an A record in the Azure Public DNS Zone to expose thechatanddocsapplications via a given subdomain (e.g.,https://chat.contoso.com).
\n
\n
# Variables\nsource ./00-variables.sh\nsubdomains=($docsSubdomain $chatSubdomain)\n\n# Install jq if not installed\npath=$(which jq)\n\nif [[ -z $path ]]; then\n echo 'Installing jq...'\n apt install -y jq\nfi\n\n# Choose the ingress controller to use\nif [[ $ingressClassName == \"nginx\" ]]; then\n ingressNamespace=$nginxNamespace\n ingressServiceName=\"${nginxReleaseName}-controller\"\nelse\n ingressNamespace=$webAppRoutingNamespace\n ingressServiceName=$webAppRoutingServiceName\nfi\n\n# Retrieve the public IP address of the NGINX ingress controller\necho \"Retrieving the external IP address of the [$ingressClassName] NGINX ingress controller...\"\npublicIpAddress=$(kubectl get service -o json -n $ingressNamespace |\n jq -r '.items[] | \n select(.spec.type == \"LoadBalancer\" and .metadata.name == \"'$ingressServiceName'\") |\n .status.loadBalancer.ingress[0].ip')\n\nif [ -n \"$publicIpAddress\" ]; then\n echo \"[$publicIpAddress] external IP address of the [$ingressClassName] NGINX ingress controller successfully retrieved\"\nelse\n echo \"Failed to retrieve the external IP address of the [$ingressClassName] NGINX ingress controller\"\n exit\nfi\n\nfor subdomain in ${subdomains[@]}; do\n # Check if an A record for todolist subdomain exists in the DNS Zone\n echo \"Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone...\"\n ipv4Address=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].ARecords[].ipv4Address\" \\\n --output tsv \\\n --only-show-errors)\n\n if [[ -n $ipv4Address ]]; then\n echo \"An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address\"\n\n if [[ $ipv4Address == $publicIpAddress ]]; then\n echo \"The [$ipv4Address] ip address of the existing A record is equal to the ip address of the ingress\"\n echo \"No additional step is required\"\n continue\n else\n echo \"The [$ipv4Address] ip address of the existing A record is different than the ip address of the ingress\"\n fi\n # Retrieving name of the record set relative to the zone\n echo \"Retrieving the name of the record set relative to the [$dnsZoneName] zone...\"\n\n recordSetName=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].name\" \\\n --output tsv \\\n --only-show-errors 2>/dev/null)\n\n if [[ -n $recordSetName ]]; then\n echo \"[$recordSetName] record set name successfully retrieved\"\n else\n echo \"Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone\"\n exit\n fi\n\n # Remove the A record\n echo \"Removing the A record from the record set relative to the [$dnsZoneName] zone...\"\n\n az network dns record-set a remove-record \\\n --ipv4-address $ipv4Address \\\n --record-set-name $recordSetName \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --only-show-errors 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$ipv4Address] ip address successfully removed from the [$recordSetName] record set\"\n else\n echo \"Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set\"\n exit\n fi\n fi\n\n # Create the A record\n echo \"Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address...\"\n az network dns record-set a add-record \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --record-set-name $subdomain \\\n --ipv4-address $publicIpAddress \\\n --only-show-errors 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone\"\n else\n echo \"Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone\"\n fi\ndone
\n
\n
YAML manifests
\n
Below you can read the YAML manifests used to deploy thechatchatbot to AKS. For brevity, I will cover only the installation of this application, but you can find all the YAML manifests in the companion GitHub repository. Thechat-configmap.ymldefines a value for the environment variables passed to the application container. The configmap does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
\n
\n
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Microsoft Entra ID security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Microsoft Entra ID security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
\n
AZURE_OPENAI_VERSION: A string representing the version of the OpenAI API.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_SYSTEM_MESSAGE: The content of the system message used for OpenAI API calls. You can use it to describe the assistant's personality.
\n
\n
\n
Thechat-deployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create.azure.workload.identity/uselabel is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
Theingress.ymlmanifest defines a Kubernetesingressobject used to expose the service via theNGINX Ingress Controller. This project deploys a managed NGINX Ingress Controller using the application routing add-on and an unmanaged instance of the NGINX Ingress Controller using the Helm Terrafom Provider and related chart. The Terraform module creates twoclusterissuerobjects, one for the managed and one for the unmanaged version of the NGINX Ingress Controller. You can run the following command to see the two ingress classes:
\n
\n
kubectl get ingressclass
\n
\n
Executing the command will produce a result as follows:
\n
\n
NAME CONTROLLER PARAMETERS AGE\nnginx k8s.io/ingress-nginx <none> 4d\nwebapprouting.kubernetes.azure.com webapprouting.kubernetes.azure.com/nginx <none> 4d22h
\n
\n
Run the following command to retrieve the cluster issuers used by thecert-manager:
\n
\n
kubectl get clusterissuer
\n
\n
The above command should return a result as follows:
\n
\n
NAME READY AGE\nletsencrypt-nginx True 4d2h\nletsencrypt-webapprouting True 4d2h
\n
\n
Thechat-ingresscontains the code of the ingress object used to expose thechatapplication. This version of the ingress makes use of the unmanaged instance of the NGINX Ingress Controller.
The ingress object defines the following annotations:
\n
\n
\n
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
You can delete the resource group using the following Azure CLI command when you no longer need the resources you created. This will remove all the Azure resources.
\n
\n
az group delete --name <resource-group-name>
\n
\n
Alternatively, you can use the following PowerShell cmdlet to delete the resource group and all the Azure resources.
Simple Chat: This simple chat application utilizes OpenAI's language models to generate real-time completion responses.
\n
Documents QA Chat: This chat application goes beyond simple conversations. Users can upload up to 10.pdfand.docxdocuments, which are then processed to create vector embeddings. These embeddings are stored inChromaDBfor efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response.
Asystemnode pool in a dedicated subnet. The default node pool hosts only critical system pods and services. The worker nodes have node taint which prevents application pods from beings scheduled on this node pool.
\n
Ausernode pool hosting user workloads and artifacts in a dedicated subnet.
\n
\n
\n
Azure OpenAI Service: anAzure OpenAI Servicewith aGPT-3.5model used by the chatbot application. Azure OpenAI Service gives customers advanced language AI with OpenAI GPT-4, GPT-3, Codex, and DALL-E models with the security and enterprise promise of Azure. Azure OpenAI co-develops the APIs with OpenAI, ensuring compatibility and a smooth transition from one to the other.
\n
User-defined Managed Identity: a user-defined managed identity used by the AKS cluster to create additional resources like load balancers and managed disks in Azure.
Azure Virtual Machine: Terraform modules can optionally create a jump-box virtual machine to manage the private AKS cluster.
\n
Azure Bastion Host: a separate Azure Bastion is deployed in the AKS cluster virtual network to provide SSH connectivity to both agent nodes and virtual machines.
\n
Azure NAT Gateway: a bring-your-own (BYO)Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads. The NAT Gateway is associated to theSystemSubnet,UserSubnet, andPodSubnetsubnets. TheoutboundTypeproperty of the cluster is set touserAssignedNatGatewayto specify that a BYO NAT Gateway is used for outbound connections. NOTE: you can update theoutboundTypeafter cluster creation and this will deploy or remove resources as required to put the cluster into the new egress configuration. For more information, seeUpdating outboundType after cluster creation.
\n
Azure Storage Account: this storage account is used to store the boot diagnostics logs of both the service provider and service consumer virtual machines. Boot Diagnostics is a debugging feature that allows you to view console output and screenshots to diagnose virtual machine status.
\n
Azure Container Registry: an Azure Container Registry (ACR) to build, store, and manage container images and artifacts in a private registry for all container deployments.
Azure Monitor workspace: AnAzure Monitor workspaceis a unique environment for data collected byAzure Monitor. Each workspace has its own data repository, configuration, and permissions. Log Analytics workspaces contain logs and metrics data from multiple Azure resources, whereas Azure Monitor workspaces currently contain only metrics related toPrometheus. Azure Monitor managed service for Prometheus allows you to collect and analyze metrics at scale using a Prometheus-compatible monitoring solution, based on thePrometheus. This fully managed service allows you to use thePrometheus query language (PromQL)to analyze and alert on the performance of monitored infrastructure and workloads without having to operate the underlying infrastructure. The primary method for visualizing Prometheus metrics isAzure Managed Grafana. You can connect yourAzure Monitor workspaceto anAzure Managed Grafanato visualize Prometheus metrics using a set of built-in and custom Grafana dashboards.
\n
Azure Managed Grafana: anAzure Managed Grafanainstance used to visualize thePrometheus metricsgenerated by theAzure Kubernetes Service(AKS)cluster deployed by the Bicep modules.Azure Managed Grafanais a fully managed service for analytics and monitoring solutions. It's supported by Grafana Enterprise, which provides extensible data visualizations. This managed service allows to quickly and easily deploy Grafana dashboards with built-in high availability and control access with Azure security.
\n
NGINX Ingress Controller: this sample compares the managed and unmanaged NGINX Ingress Controller. While the managed version is installed using theApplication routing add-on, the unmanaged version is deployed using theHelm Terraform Provider. You can use the Helm provider to deploy software packages in Kubernetes. The provider needs to be configured with the proper credentials before it can be used.
\n
Cert-Manager: thecert-managerpackage andLet's Encryptcertificate authority are used to issue a TLS/SSL certificate to the chat applications.
TheAzure OpenAI Serviceis a platform offered by Microsoft Azure that provides cognitive services powered byOpenAImodels. One of the models available through this service is theChatGPTmodel, which is designed for interactive conversational tasks. It allows developers to integrate natural language understanding and generation capabilities into their applications.
\n
Azure OpenAI Service provides REST API access to OpenAI's powerful language models including theGPT-3,CodexandEmbeddingsmodel series. In addition, the newGPT-4andChatGPTmodel series have now reached general availability. These models can be easily adapted to your specific task including but not limited to 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.
\n
TheChat Completion API, which is part of the Azure OpenAI Service, provides a dedicated interface for interacting with theChatGPTandGPT-4 models. This API is currently in preview and is the preferred method for accessing these models. The GPT-4 models can only be accessed through this API.
\n
GPT-3,GPT-3.5, andGPT-4models from OpenAI are prompt-based. With prompt-based models, the user interacts with the model by entering a text prompt, to which the model responds with a text completion. This completion is the model’s continuation of the input text. While these models are extremely powerful, their behavior is also very sensitive to the prompt. This makes prompt construction an important skill to develop. For more information, seeIntroduction to prompt engineering.
\n
Prompt construction can be difficult. In practice, the prompt acts to configure the model weights to complete the desired task, but it's more of an art than a science, often requiring experience and intuition to craft a successful prompt. The goal of this article is to help get you started with this learning process. It attempts to capture general concepts and patterns that apply to all GPT models. However it's important to understand that each model behaves differently, so the learnings may not apply equally to all models.
\n
Prompt engineering refers to the process of creating instructions called prompts for Large Language Models (LLMs), such as OpenAI’s ChatGPT. With the immense potential of LLMs to solve a wide range of tasks, leveraging prompt engineering can empower us to save significant time and facilitate the development of impressive applications. It holds the key to unleashing the full capabilities of these huge models, transforming how we interact and benefit from them. For more information, seePrompt engineering techniques.
\n
\n
Vector Databases
\n
Avector databaseis a specialized database that goes beyond traditional storage by organizing information to simplify the search for similar items. Instead of merely storing words or numbers, it leverages vector embeddings - unique numerical representations of data. These embeddings capture meaning, context, and relationships. For instance, words are represented as vectors, whereas similar words have similar vector values.
\n
The applications of vector databases are numerous and powerful. In language processing, they facilitate the discovery of related documents or sentences. By comparing the vector embeddings of different texts, finding similar or related information becomes faster and more efficient. This capability benefits search engines and recommendation systems, which can suggest relevant articles or products based on user interests.
\n
In the realm of image analysis, vector databases excel in finding visually similar images. By representing images as vectors, a simple comparison of vector values can identify visually similar images. This capability is highly valuable for tasks like reverse image search or content-based image retrieval.
\n
Additionally, vector databases find applications in fraud detection, anomaly detection, and clustering. By comparing vector embeddings of data points, unusual patterns can be detected, and similar items can be grouped together, aiding in effective data analysis and decision-making. This is a list of Azure services that are suitable for use as a vector database in a retrieval-augmented generation (RAG) solution:
\n
\n
\n
\n
Azure Cosmos DB for MongoDB vCore: vCore-based Azure Cosmos DB for MongoDB provides developers with a fully managed MongoDB-compatible database service for building modern applications with a familiar architecture. Developers can enjoy the benefits of native Azure integrations, low total cost of ownership (TCO), and the familiar vCore architecture when migrating existing applications or building new ones. Azure Cosmos DB for MongoDB features built-in vector database capabilities enabling your data and vectors to be stored together for efficient and accurate vector searches.
\n
\n
\n
Azure Cosmos DB for NoSQL: Azure Cosmos DB for NoSQL is a globally distributed database service designed for scalable and high performance applications. It offers an industry-leading 99.999% Service Level Agreement (SLA), ensuring high availability for your mission-critical applications. With sub-10ms point reads and instant autoscale, it provides lightning-fast data access and seamless scalability. Its flexible, schemaless data model allows for agile and adaptable application development. Moreover, Azure Cosmos DB’s built-in vector index using DiskANN enables fast, accurate, and cost-effective vector search at any scale, enhancing the efficiency and effectiveness of your data-driven applications.
\n
\n
\n
Azure Cosmos DB for PostgreSQL You can use the natively integrated vector database in Azure Cosmos DB for PostgreSQL, which offers an efficient way to store, index, and search high-dimensional vector data directly alongside other application data. This approach removes the necessity of migrating your data to costlier alternative vector databases and provides a seamless integration of your AI-driven applications.
\n
\n
\n
Azure Cache for Redis Azure Cache for Redis can be used as a vector database by combining it models like Azure OpenAI for Retrieval-Augmented Generative AI and analysis scenarios.
\n
\n
\n
\n
Here is a list of the most popular vector databases:
\n
\n
\n
ChromaDBis a powerful database solution that stores and retrieves vector embeddings efficiently. It is commonly used in AI applications, including chatbots and document analysis systems. By storing embeddings in ChromaDB, users can easily search and retrieve similar vectors, enabling faster and more accurate matching or recommendation processes. ChromaDB offers excellent scalability high performance, and supports various indexing techniques to optimize search operations. It is a versatile tool that enhances the functionality and efficiency of AI applications that rely on vector embeddings.
\n
Facebook AI Similarity Search (FAISS)is another widely used vector database. Facebook AI Research develops it and offers highly optimized algorithms for similarity search and clustering of vector embeddings. FAISS is known for its speed and scalability, making it suitable for large-scale applications. It offers different indexing methods like flat, IVF (Inverted File System), and HNSW (Hierarchical Navigable Small World) to organize and search vector data efficiently.
\n
SingleStore: SingleStore aims to deliver the world’s fastest distributed SQL database for data-intensive applications: SingleStoreDB, which combines transactional + analytical workloads in a single platform.
\n
Astra DB: DataStax Astra DB is a cloud-native, multi-cloud, fully managed database-as-a-service based on Apache Cassandra, which aims to accelerate application development and reduce deployment time for applications from weeks to minutes.
\n
Milvus: Milvus is an open source vector database built to power embedding similarity search and AI applications. Milvus makes unstructured data search more accessible and provides a consistent user experience regardless of the deployment environment. Milvus 2.0 is a cloud-native vector database with storage and computation separated by design. All components in this refactored version of Milvus are stateless to enhance elasticity and flexibility.
\n
Qdrant: Qdrant is a vector similarity search engine and database for AI applications. Along with open-source, Qdrant is also available in the cloud. It provides a production-ready service with an API to store, search, and manage points—vectors with an additional payload. Qdrant is tailored to extended filtering support. It makes it useful for all sorts of neural-network or semantic-based matching, faceted search, and other applications.
\n
Pinecone: Pinecone is a fully managed vector database that makes adding vector search to production applications accessible. It combines state-of-the-art vector search libraries, advanced features such as filtering, and distributed infrastructure to provide high performance and reliability at any scale.
\n
Vespa: Vespa is a platform for applications combining data and AI, online. By building such applications on Vespa helps users avoid integration work to get features, and it can scale to support any amount of traffic and data. To deliver that, Vespa provides a broad range of query capabilities, a computation engine with support for modern machine-learned models, hands-off operability, data management, and application development support. It is free and open source to use under the Apache 2.0 license.
\n
Zilliz: Milvus is an open-source vector database, with over 18,409 stars on GitHub and 3.4 million+ downloads. Milvus supports billion-scale vector search and has over 1,000 enterprise users. Zilliz Cloud provides a fully-managed Milvus service made by the creators of Milvus. This helps to simplify the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure. As a DBaaS, Zilliz simplifies the process of deploying and scaling vector search applications by eliminating the need to create and maintain complex data infrastructure.
\n
Weaviate: Weaviate is an open-source vector database used to store data objects and vector embeddings from ML-models, and scale into billions of data objects from the same name company in Amsterdam. Users can index billions of data objects to search through and combine multiple search techniques, such as keyword-based and vector search, to provide search experiences.
LangChainis a software framework designed to streamline the development of applications usinglarge language models (LLMs). It serves as a language model integration framework, facilitating various applications like document analysis and summarization, chatbots, and code analysis.
\n
LangChain's integrations cover an extensive range of systems, tools, and services, making it a comprehensive solution for language model-based applications. LangChain integrates with the major cloud platforms such as Microsoft Azure, Amazon AWS, and Google, and with API wrappers for various purposes like news, movie information, and weather, as well as support for Bash, web scraping, and more. It also supports multiple language models, including those from OpenAI, Anthropic, and Hugging Face. Moreover, LangChain offers various functionalities for document handling, code generation, analysis, debugging, and interaction with databases and other data sources.
\n
\n
Chainlit
\n
Chainlitis an open-source Python package that is specifically designed to create user interfaces (UIs) for AI applications. It simplifies the process of building interactive chats and interfaces, making developing AI-powered applications faster and more efficient. While Streamlit is a general-purpose UI library, Chainlit is purpose-built for AI applications and seamlessly integrates with other AI technologies such asLangChain,LlamaIndex, andLangFlow.
\n
With Chainlit, developers can easily create intuitive UIs for their AI models, including ChatGPT-like applications. It provides a user-friendly interface for users to interact with AI models, enabling conversational experiences and information retrieval. Chainlit also offers unique features, such as the ability to display theChain of Thought, which allows users to explore the reasoning process directly within the UI. This feature enhances transparency and enables users to understand how the AI arrives at its responses or recommendations.
\n
For more information, see the following resources:
Managed NGINX Ingress Controller for Azure Kubernetes Service
\n
One way to route HTTP and secure HTTPS traffic to applications in an Azure Kubernetes Service (AKS) cluster is by using the Kubernetes Ingress object. The application routing add-on for AKS enables you to create, configure, and manage one or more Ingress controllers within your AKS cluster using theNGINX Ingress Controller.
\n
The application routing add-on with NGINX provides several features, including:
\n
\n
\n
Easy configuration of managed NGINX Ingress controllers based on the Kubernetes NGINX Ingress controller.
\n
Integration with Azure DNS for public and private zone management.
\n
SSL termination with certificates stored in Azure Key Vault.
\n
\n
To enable theapplication routing add-onon an existing cluster, you can use Azure CLI, as shown in the following code snippet.
\n
\n
az aks approuting enable -g <ResourceGroupName> -n <ClusterName>
\n
\n
Once enabled, you can connect to your AKS cluster, deploy applications, and create Ingress objects with appropriate annotations for routing. There are some limitations to be aware of, such as the maximum number of supported Azure DNS zones and namespace editing restrictions. It's recommended to review theapplication routing add-on configurationfor additional information on SSL encryption and DNS integration.
\n
If you are familiar with the NGINX ingress controller, you can just replace thenginxingress class name inside an ingress object with the name of the ingress controller deployed by the application routing addon, that, by default is equal towebapprouting.kubernetes.azure.com:
If you leveragecert-managerand with Let's Encrypt certificate authority to issue TLS/SSL certificates to your application, make sure to create an issuer or a cluster issuer for the ingress class of the managed NGINX ingress controller installed by the application routing add-on. This can be done using the sample code provided below:
Ensure that you replaceadmin@contoso.comwith your own email address to receive notifications from Let's Encrypt. By using this configuration,cert-managerwill be able to issue certificates for the ingress class of the managed NGINX ingress controller when using the application routing add-on. Please note that the server URLhttps://acme-v02.api.letsencrypt.org/directoryis the Let's Encrypt production server. You can also use the staging serverhttps://acme-staging-v02.api.letsencrypt.org/directoryfor testing purposes to avoid rate limits. Ensure that the issuer or cluster issuer resource is deployed to your Kubernetes cluster, andcert-manageris properly installed and configured. For more detailed steps and instructions, refer toManaged nginx Ingress with the application routing add-on.
\n
\n
Deploy the Terraform modules
\n
Before deploying the Terraform modules in theterraformfolder, specify a value for the following variables in theterraform.tfvarsvariable definitions file.
\n
\n
\nname_prefix = \"Contoso\"\ndomain = \"contoso.com\"\nkubernetes_version = \"1.28.3\"\nnamespace = \"chainlit\"\nservice_account_name = \"chainlit-sa\"\nssh_public_key = \"XXXXXXX\"\nvm_enabled = true\nlocation = \"eastus\"\nadmin_group_object_ids = [\"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"] \nweb_app_routing_enabled = true\ndns_zone_name = \"babosbird.com\"\ndns_zone_resource_group_name = \"DnsResourceGroup\"\n\ngrafana_admin_user_object_id = \"0c5267b2-01f3-4a59-970e-0d9218d5412e\"\nvnet_integration_enabled = true\nopenai_deployments = [\n {\n name = \"gpt-35-turbo-16k\"\n model = {\n name = \"gpt-35-turbo-16k\"\n version = \"0613\"\n }\n },\n {\n name = \"text-embedding-ada-002\"\n model = {\n name = \"text-embedding-ada-002\"\n version = \"2\"\n }\n }\n]\n\n
\n
\n
Description:
\n
\n
\n
prefix: specifies a prefix for all the Azure resources.
\n
domain: specifies the domain part (e.g., subdomain.domain) of the hostname of the ingress object used to expose the chatbot via theNGINX Ingress Controller.
\n
kubernetes_version: specifies the Kubernetes version installed on the AKS cluster.
\n
namespace: specifies the namespace of the workload application that accesses the Azure OpenAI Service.
\n
service_account_name: specifies the name of the service account of the workload application that accesses the Azure OpenAI Service.
\n
ssh_public_key: specifies the SSH public key used for the AKS nodes and jumpbox virtual machine.
\n
vm_enabled: a boleean value that specifies whether deploying or not a jumpbox virtual machine in the same virtual network of the AKS cluster.
\n
location: specifies the region (e.g., westeurope) where deploying the Azure resources.
\n
admin_group_object_ids: when deploying an AKS cluster with Microsoft Entra ID and Azure RBAC integration, this array parameter contains the list of Microsoft Entra ID group object IDs that will have the admin role of the cluster.
openai_deployments: specifies the list of theAzure OpenAI Service modelsto create. This sample needs aGPTmodel for chat completions and an embeddings model.
\n
\n
\n
NOTE We suggest reading sensitive configuration data such as passwords or SSH keys from a pre-existing Azure Key Vault resource. For more information, seeReferencing Azure Key Vault secrets in Terraform. Before proceeding, also make sure to run theregister-preview-features.shBash script in theterraformfolder to register any preview feature used by the AKS cluster.
\n
\n
\n
OpenAI Terraform Module
\n
The following table contains the code from theopenai.tfTerraform module used to deploy theAzure OpenAI Service.
Azure Cognitive Services use custom subdomain names for each resource created through theAzure portal,Azure Cloud Shell,Azure CLI,Bicep,Azure Resource Manager (ARM), orTerraform. Unlike regional endpoints, which were common for all customers in a specific Azure region, custom subdomain names are unique to the resource. Custom subdomain names are required to enable features like Microsoft Entra ID for authentication. In our case, we need to specify a custom subdomain for ourAzure OpenAI Serviceas our chatbot application will use an Microsoft Entra ID security token to access it. By default, themain.tfmodule sets the value of thecustom_subdomain_nameparameter to the lowercase name of the Azure OpenAI resource. For more information on custom subdomains, seeCustom subdomain names for Cognitive Services.
The following table shows the code of the Terraform module used to deploy theAzure Kubernetes Service (AKS)cluster with a user-assigned managed identity. For more information on theazurerm_kubernetes_clusterresource, seeTerraformdocumentation.
This module allows to deploy anAzure Kubernetes Servicecluster with the following extensions and features:
\n
\n
\n
API Server VNET Integrationallows you to enable network communication between the API server and the cluster nodes without requiring a private link or tunnel. AKS clusters with API Server VNET integration provide a series of advantages, for example, they can have public network access or private cluster mode enabled or disabled without redeploying the cluster. For more information, seeCreate an Azure Kubernetes Service cluster with API Server VNet Integration.
\n
Azure NAT Gatewayto manage outbound connections initiated by AKS-hosted workloads.
\n
Event-driven Autoscaling (KEDA) add-onis a single-purpose and lightweight component that strives to make application autoscaling simple and is a CNCF Incubation project.
\n
Vertical Pod Autoscalingallows you to automatically sets resource requests and limits on containers per workload based on past usage. VPA makes certain pods are scheduled onto nodes that have the required CPU and memory resources. For more information, seeKubernetes Vertical Pod Autoscaling.
NOTE You can deploy an AKS resource as a public cluster withAPI Server VNet Integrationenabled. During the installation, you can use Terraform modules that make use of theHelm,Kubectl, andKubernetesTerraform Providers to install packages and create Kubernetes entities. Once the installation is complete, you can turn the cluster to private.
\n
\n
\n
Private Endpoint and Private DNS Zone Terraform Modules
The following code snippet from themain.tfTerraform module creates the user-defined managed identity used by the chatbot to acquire a security token from Microsoft Entra ID viaMicrosoft Entra Workload ID.
The above code snippet performs the following steps:
\n
\n
\n
Creates a new user-defined managed identity.
\n
Assign the new managed identity to the Cognitive Services User role with the resource group as a scope.
\n
Federate the managed identity with the service account used by the chatbot. The following information is necessary to create the federated identity credentials:\n
\n
The Kubernetes service account name.
\n
The Kubernetes namespace that will host the chatbot application.
Open the Azure Portal, and navigate to the resource group. Open the Azure Open AI Service resource, navigate toKeys and Endpoint, and check that the endpoint contains a custom subdomain rather than the regional Cognitive Services endpoint.
\n
\n
\n
Open to the<Prefix>WorkloadManagedIdentitymanaged identity, navigate to theFederated credentials, and verify that the federated identity credentials for thechainlit-saservice account were created correctly, as shown in the following picture.
\n
\n
\n
Use Microsoft Entra Workload ID with Azure Kubernetes Service (AKS)
\n
Workloads deployed on an Azure Kubernetes Services (AKS) cluster require Microsoft Entra ID application credentials or managed identities to access Microsoft Entra ID protected resources, such as Azure Key Vault and Microsoft Graph. Microsoft Entra Workload ID integrates with the capabilities native to Kubernetes to federate with external identity providers.
resource \"helm_release\" \"nginx_ingress_controller\" {\n name = \"ingress-nginx\"\n repository = \"https://kubernetes.github.io/ingress-nginx\"\n chart = \"ingress-nginx\"\n namespace = \"ingress-basic\"\n create_namespace = true\n timeout = 600\n\n set {\n name = \"controller.metrics.enabled\"\n value = \"true\"\n }\n\n set {\n name = \"controller.metrics.serviceMonitor.enabled\"\n value = \"true\"\n }\n \n set {\n name = \"controller.metrics.serviceMonitor.additionalLabels.release\"\n value = \"prometheus\"\n }\n\n\n set {\n name = \"controller.service.annotations.service\\\\.beta\\\\.kubernetes\\\\.io/azure-load-balancer-health-probe-request-path\"\n value = \"/healthz\"\n }\n\n set {\n name = \"controller.nodeSelector.kubernetes\\\\.io/os\"\n value = \"linux\"\n }\n\n set {\n name = \"controller.replicaCount\"\n value = \"${var.nginx_replica_count}\"\n }\n\n depends_on = [helm_release.prometheus]\n}
\n
\n
Instead, the following module is used to create the workload namespace and service account:
In particular, thekubectl_manifestresource makes use of variables to set the following service account annotations and labels necessary to Microsoft Entra Workload ID. For more information, seeService account labels and annotations.
\n
\n
Simple Chat Application
\n
The Simple Chat Application is a large language model-based chatbot that allows users to submit general-purpose questions to aGPTmodel, which generates and streams back human-like and engaging conversational responses. The following picture shows the welcome screen of the chat application.
\n
\n
\n
You can modify the welcome screen in markdown by editing thechainlit.mdfile at the project's root. If you do not want a welcome screen, leave the file empty. The following picture shows what happens when a user submits a new message in the chat.
\n
\n
\n
Chainlit can render messages in markdown format as shown by the following prompt:
\n
\n
\n
Chainlit also provides classes to support the following elements:
\n
\n
\n
Audio: TheAudioclass allows you to display an audio player for a specific audio file in the chatbot user interface. You must provide either a URL or a path or content bytes.
\n
Avatar: TheAvatarclass allows you to display an avatar image next to a message instead of the author's name. You need to send the element once. Next,, if an avatar's name matches an author's name, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
File: TheFileclass allows you to display a button that lets users download the content of the file. You must provide either a URL or a path or content bytes.
\n
Image: TheImageclass is designed to create and handle image elements to be sent and displayed in the chatbot user interface. You must provide either a URL or a path or content bytes.
\n
Pdf: ThePdfclass allows you to display a PDF hosted remotely or locally in the chatbot UI. This class either takes a URL of a PDF hosted online or the path of a local PDF.
\n
Pyplot: ThePyplotclass allows you to display aMatplotlibpyplot chart in the chatbot UI. This class takes a pyplot figure.
\n
TaskList: TheTaskListclass allows you to display a task list next to the chatbot UI.
\n
Text: TheTextclass allows you to display a text element in the chatbot UI. This class takes a string and creates a text element that can be sent to the UI. It supports the markdown syntax for formatting text. You must provide either a URL or a path or content bytes.
\n
\n
\n
You can click the user icon on the UI to access the chat settings and choose, for example, between the light and dark theme.
\n
\n
\n
The application is built in Python. Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
These are the libraries used by the chat application:
\n
\n\n
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
\n
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
\n
openai: The OpenAI Python library provides convenient access to the OpenAI API from applications written in Python. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
logging: This module provides flexible logging of messages.
\n
chainlit as cl: This imports theChainlitlibrary and aliases it ascl. Chainlit is used to create the UI of the application.
\n
from azure.identity import DefaultAzureCredential, get_bearer_token_provider: when theopenai_typeproperty value isazure_ad,aDefaultAzureCredentialobject from theAzure Identity client library for Pythonis used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity federated with the service account.
\n
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n\n
\n
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
\n
\n
pip install -r requirements.txt --upgrade
\n
\n
Next, the code reads the value of the environment variables used to initialize Azure OpenAI objects. In addition, it creates a token provider for Azure OpenAI.
Here's a brief explanation of each variable and related environment variable:
\n
\n\n
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
\n
api_base: The base URL for the OpenAI API.
\n
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
\n
api_type: A string representing the type of the OpenAI API.
\n
api_version: A string representing the version of the OpenAI API.
\n
engine: The engine used for OpenAI API calls.
\n
model: The model used for OpenAI API calls.
\n
system_content: The content of the system message used for OpenAI API calls.
\n
max_retries: The maximum number of retries for OpenAI API calls.
\n
timeout: The timeout in seconds.
\n
debug: When debug is equal totrue,t, or1, the logger writes the chat completion answers.
\n\n
\n
In the next section, the code creates theAsyncAzureOpenAIclient object used by the application to communicate with the Azure OpenAI Service instance. When theapi_typeis equal toazure, the code initializes the object with the API key. Otherwise, it initializes theazure_ad_token_providerproperty to the token provider created earlier. Then the code creates a logger.
The backoff time is calculated using thebackoff_in_secondsandattemptvariables. It follows the formulabackoff_in_seconds * 2 ** attempt + random.uniform(0, 1). This formula increases the backoff time exponentially with each attempt and adds a random value between 0 and 1 to avoid synchronized retries.
\n
Next, the code defines a function calledstart_chatthat is used to initialize the UI when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
\n
\n
\n
.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
\n
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
cl.user_session.set(): This API call sets a value in theuser_sessiondictionary. In this case, it initializes themessage_historyin the user's session with a system content message, which indicates the start of the chat.
\n
\n
Finally, the application defines the method called whenever the user sends a new message in the chat.
\n
\n
@cl.on_message\nasync def on_message(message: cl.Message):\n message_history = cl.user_session.get(\"message_history\")\n message_history.append({\"role\": \"user\", \"content\": message.content})\n logger.info(\"Question: [%s]\", message.content)\n\n # Create the Chainlit response message\n msg = cl.Message(content=\"\")\n\n async for stream_resp in await openai.chat.completions.create(\n model=model,\n messages=message_history,\n temperature=temperature,\n stream=True,\n ):\n if stream_resp and len(stream_resp.choices) > 0:\n token = stream_resp.choices[0].delta.content or \"\"\n await msg.stream_token(token)\n\n if debug:\n logger.info(\"Answer: [%s]\", msg.content)\n\n message_history.append({\"role\": \"assistant\", \"content\": msg.content})\n await msg.send()
\n
\n
Here is a detailed explanation of the function steps:
\n
\n
\n
.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
\n
cl.user_session.get(): This API call retrieves a value from the user's session data stored in theuser_sessiondictionary. In this case, it fetches themessage_historyfrom the user's session to maintain the chat history.
\n
message_history.append(): This API call appends a new message to themessage_historylist. It is used to add the user's message and the assistant's response to the chat history.
\n
cl.Message(): This API call creates a ChainlitMessageobject. TheMessageclass is designed to send, stream, edit, or remove messages in the chatbot user interface. In this sample, theMessageobject is used to stream the OpenAI response in the chat.
\n
msg.stream_token(): Thestream_tokenmethod of theMessageclass streams a token to the response message. It is used to send the response from the OpenAI Chat API in chunks to ensure real-time streaming in the chat.
\n
await openai.chat.completions.create(): This API call sends a message to theOpenAI Chat APIin an asynchronous mode and streams the response. It uses the providedmessage_historyas context for generating the assistant's response.
\n
\n
Below, you can read the complete code of the application.
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
\n
\n
chainlit run app.py -w
\n
\n
Documents QA Chat
\n
The Documents QA Chat application allows users to submit up to 10.pdfand.docxdocuments. The application processes the uploaded documents to create vector embeddings. These embeddings are stored inChromaDBvector database for efficient retrieval. Users can pose questions about the uploaded documents and view theChain of Thought, enabling easy exploration of the reasoning process. The completion message contains links to the text chunks in the documents that were used as a source for the response. The following picture shows the chat application interface. As you can see, you can click theBrowsebutton and choose up to 10.pdfand.docxdocuments to upload. Alternatively, you can just drag and drop the files over the control area.
\n
\n
\n
After uploading the documents, the application creates and stores embeddings toChromaDBvector database. During the phase, the UI shows a messageProcessing <file-1>, <file-2>..., as shown in the following picture:
\n
\n
\n
When the code finished creating embeddings, the UI is ready to receive user's questions:
\n
\n
\n
As your chat application grows in complexity, understanding the individual steps for generating a specific answer can become challenging. To solve this issue, Chainlit allows you to easily explore the reasoning process right from the user interface using theChain of Thought. If you are using theLangChainintegration, every intermediary step is automatically sent and displayed in the Chainlit UI just clicking and expanding the steps, as shown in the following picture:
\n
\n
\n
To see the text chunks that were used by the large language model to originate the response, you can click the sources links, as shown in the following picture:
\n
\n
\n
In theChain of Thought, below the step used to invoke the OpenAI chat completion API, you can find an
\n
Inspect in prompt playground icon. Clicking on it opens the Prompt Playground dialog which allows you to modify and iterate on the prompt as needed.
\n
\n
\n
As shown in the following picture, you can click and edit the value of the highlighted variables in the user prompt:
\n
\n
\n
You can then click and edit the user question.
\n
\n
\n
Then, you can click the submit button to test the effect of your changes, as shown in the following picture.
\n
\n
\n
Let's take a look at the individual parts of the application code. In the following section, the Python code starts by importing the necessary packages/modules.
These are the libraries used by the chat application:
\n
\n\n
os: This module provides a way of interacting with the operating system, enabling the code to access environment variables, file paths, etc.
\n
sys: This module provides access to some variables used or maintained by the interpreter and functions that interact with the interpreter.
\n
time: This module provides various time-related functions for time manipulation and measurement.
\n
openai: the OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language. It includes a pre-defined set of classes for API resources that initialize themselves dynamically from API responses, which makes it compatible with a wide range of versions of the OpenAI API. You can find usage examples for the OpenAI Python library in ourAPI referenceand theOpenAI Cookbook.
\n
logging: This module provides flexible logging of messages.
\n
chainlit as cl: This imports theChainlitlibrary and aliases it ascl.Chainlit is used to create the UI of the application.
\n
AzureChatOpenAIfromchainlit.playground.config import: you need to importAzureChatOpenAIfromchainlit.playground.configto use the Chainlit Playground.
\n
DefaultAzureCredentialfromazure.identity: when theopenai_typeproperty value isazure_ad, aDefaultAzureCredentialobject from theAzure Identity client library for Python - version 1.13.0is used to acquire security token from the Microsoft Entra ID using the credentials of the user-defined managed identity, whose client ID is defined in theAZURE_CLIENT_IDenvironment variable.
\n
load_dotenvanddotenv_valuesfromdotenv:Python-dotenvreads key-value pairs from a.envfile and can set them as environment variables. It helps in the development of applications following the12-factorprinciples.
\n
langchain: Large language models (LLMs) are emerging as a transformative technology, enabling developers to build applications that they previously could not. However, using these LLMs in isolation is often insufficient for creating a truly powerful app - the real power comes when you can combine them with other sources of computation or knowledge. LangChain library aims to assist in the development of those types of applications.
\n\n
Therequirements.txtfile under thesrcfolder contains the list of packages used by the chat applications. You can restore these packages in your environment using the following command:
\n
\n
pip install -r requirements.txt --upgrade
\n
\n
Next, the code reads environment variables and configures the OpenAI settings.
\n
\n
# Read environment variables\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-12-01-preview\")\nchat_completion_deployment = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nembeddings_deployment = os.getenv(\"AZURE_OPENAI_ADA_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\nmax_size_mb = int(os.getenv(\"CHAINLIT_MAX_SIZE_MB\", 100))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\ntext_splitter_chunk_size = int(os.getenv(\"TEXT_SPLITTER_CHUNK_SIZE\", 1000))\ntext_splitter_chunk_overlap = int(os.getenv(\"TEXT_SPLITTER_CHUNK_OVERLAP\", 10))\nembeddings_chunk_size = int(os.getenv(\"EMBEDDINGS_CHUNK_SIZE\", 16))\nmax_retries = int(os.getenv(\"MAX_RETRIES\", 5))\nretry_min_seconds = int(os.getenv(\"RETRY_MIN_SECONDS\", 1))\nretry_max_seconds = int(os.getenv(\"RETRY_MAX_SECONDS\", 5))\ntimeout = int(os.getenv(\"TIMEOUT\", 30))\ndebug = os.getenv(\"DEBUG\", \"False\").lower() in (\"true\", \"1\", \"t\")\n\n# Configure system prompt\nsystem_template = \"\"\"Use the following pieces of context to answer the users question.\nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\nALWAYS return a \"SOURCES\" part in your answer.\nThe \"SOURCES\" part should be a reference to the source of the document from which you got your answer.\n\nExample of your response should be:\n\n\\`\\`\\`\nThe answer is foo\nSOURCES: xyz\n\\`\\`\\`\n\nBegin!\n----------------\n{summaries}\"\"\"\nmessages = [\n SystemMessagePromptTemplate.from_template(system_template),\n HumanMessagePromptTemplate.from_template(\"{question}\"),\n]\nprompt = ChatPromptTemplate.from_messages(messages)\nchain_type_kwargs = {\"prompt\": prompt}\n\n# Configure a logger\nlogging.basicConfig(\n stream=sys.stdout,\n format=\"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s\",\n level=logging.INFO,\n)\nlogger = logging.getLogger(__name__)\n\n# Create Token Provider\nif api_type == \"azure_ad\":\n token_provider = get_bearer_token_provider(\n DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n )\n\n# Setting the environment variables for the playground\nif api_type == \"azure\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = api_key\nos.environ[\"AZURE_OPENAI_API_VERSION\"] = api_version\nos.environ[\"AZURE_OPENAI_ENDPOINT\"] = api_base\nos.environ[\"AZURE_OPENAI_DEPLOYMENT_NAME\"] = chat_completion_deployment
\n
\n
Here's a brief explanation of each variable and related environment variable:
\n
\n\n
temperature: A float value representing the temperature forCreate chat completionmethod of the OpenAI API. It is fetched from the environment variables with a default value of 0.9.
\n
api_base: The base URL for the OpenAI API.
\n
api_key: The API key for the OpenAI API. The value of this variable can be null when using a user-assigned managed identity to acquire a security token to access Azure OpenAI.
\n
api_type: A string representing the type of the OpenAI API.
\n
api_version: A string representing the version of the OpenAI API.
\n
chat_completion_deployment: the name of the Azure OpenAI GPT model for chat completion.
\n
embeddings_deployment: the name of the Azure OpenAI deployment for embeddings.
\n
model: The model used for chat completion calls (e.g,gpt-35-turbo-16k).
\n
max_size_mb: the maximum size for the uploaded documents.
\n
max_files: the maximum number of documents that can be uploaded.
\n
text_splitter_chunk_size: the maximum chunk size used by theRecursiveCharacterTextSplitterobject.
\n
text_splitter_chunk_overlap: the maximum chunk overlap used by theRecursiveCharacterTextSplitterobject.
\n
embeddings_chunk_size: the maximum chunk size used by theOpenAIEmbeddingsobject.
\n
max_retries: The maximum number of retries for OpenAI API calls.
\n
retry_min_seconds: the minimum number of seconds before a retry.
\n
retry_max_seconds: the maximum number of seconds before a retry.
\n
timeout: The timeout in seconds.
\n
system_template: The content of the system message used for OpenAI API calls.
\n
debug: When debug is equal totrue,t, or1, the logger switches to verbose mode.
\n\n
\n
Next, the code defines a function calledstart_chatthat is used to initialize the when the user connects to the application or clicks theNew Chatbutton.
Here is a brief explanation of the function steps:
\n
\n
\n
.on_chat_start: Theon_chat_startdecorator registers a callback functionstart_chat()to be called when the Chainlit chat starts. It is used to set up the chat and sendavatarsfor the Chatbot, Error, and User participants in the chat.
\n
cl.Avatar(): theAvatarclass allows you to display an avatar image next to a message instead of the author name. You need to send the element once. Next if the name of an avatar matches the name of an author, the avatar will be automatically displayed. You must provide either a URL or a path or content bytes.
\n
\n
\n
The following code is used to initialize the large language model (LLM) chain used to reply to questions on the content of the uploaded documents.
\n
\n
# Initialize the file list to None\n files = None\n\n # Wait for the user to upload a file\n while files == None:\n files = await cl.AskFileMessage(\n content=f\"Please upload up to {max_files} `.pdf` or `.docx` files to begin.\",\n accept=[\n \"application/pdf\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n max_size_mb=max_size_mb,\n max_files=max_files,\n timeout=86400,\n raise_on_timeout=False,\n ).send()
\n
\n
TheAskFileMessageAPI call prompts the user to upload up to a specified number of.pdfor.docxfiles. The uploaded files are stored in thefilesvariable. The process continues until the user uploads files. For more information, seeAskFileMessage.
\n
The following code processes each uploaded file by extracting its content.
\n
\n\n
The text content of each file is stored in the listall_texts.
\n
This code performs text processing and chunking. It checks the file extension to read the file content accordingly, depending on if it's a.pdfor a.docxdocument.
Metadata is created for each chunk and stored in themetadataslist.
\n\n
\n
# Create a message to inform the user that the files are being processed\n content = \"\"\n if len(files) == 1:\n content = f\"Processing `{files[0].name}`...\"\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"Processing {', '.join(files_names)}...\"\n logger.info(content)\n msg = cl.Message(content=content, author=\"Chatbot\")\n await msg.send()\n\n # Create a list to store the texts of each file\n all_texts = []\n\n # Process each file uplodaded by the user\n for file in files:\n # Read file contents\n with open(file.path, \"rb\") as uploaded_file:\n file_contents = uploaded_file.read()\n\n logger.info(\"[%d] bytes were read from %s\", len(file_contents), file.path)\n\n # Create an in-memory buffer from the file content\n bytes = io.BytesIO(file_contents)\n\n # Get file extension\n extension = file.name.split(\".\")[-1]\n\n # Initialize the text variable\n text = \"\"\n\n # Read the file\n if extension == \"pdf\":\n reader = PdfReader(bytes)\n for i in range(len(reader.pages)):\n text += reader.pages[i].extract_text()\n if debug:\n logger.info(\"[%s] read from %s\", text, file.path)\n elif extension == \"docx\":\n doc = Document(bytes)\n paragraph_list = []\n for paragraph in doc.paragraphs:\n paragraph_list.append(paragraph.text)\n if debug:\n logger.info(\"[%s] read from %s\", paragraph.text, file.path)\n text = \"\\n\".join(paragraph_list)\n\n # Split the text into chunks\n text_splitter = RecursiveCharacterTextSplitter(\n chunk_size=text_splitter_chunk_size,\n chunk_overlap=text_splitter_chunk_overlap,\n )\n texts = text_splitter.split_text(text)\n\n # Add the chunks and metadata to the list\n all_texts.extend(texts)\n\n # Create a metadata for each chunk\n metadatas = [{\"source\": f\"{i}-pl\"} for i in range(len(all_texts))]
\n
\n
The next piece of code performs the following steps:
\n
\n\n
It creates anAzureOpenAIEmbeddingsconfigured to use the embeddings model in the Azure OpenAI Service to create embeddings from text chunks.
\n
It creates aChromaDBvector database using theOpenAIEmbeddingsobject, the text chunks list, and the metadata list.
\n
It creates anAzureChatOpenAILangChain object based on the GPR model hosted in Azure OpenAI Service.
It stores the metadata and text chunks in the user session using thecl.user_session.set()API call.
\n
It creates a message to inform the user that the files are ready for queries, and finally returns thechain.
\n
Thecl.user_session.set(\"chain\", chain)call stores the LLM chain in theuser_sessiondictionary for later use.
\n\n
The next section create the LangChain LLM chain.
\n
\n
# Create a Chroma vector store\n if api_type == \"azure\":\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n else:\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n azure_endpoint=api_base,\n azure_ad_token_provider=token_provider,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n\n # Create a Chroma vector store\n db = await cl.make_async(Chroma.from_texts)(\n all_texts, embeddings, metadatas=metadatas\n )\n\n # Create an AzureChatOpenAI llm\n if api_type == \"azure\":\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n else:\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n azure_endpoint=api_base,\n api_key=api_key,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n azure_ad_token_provider=token_provider,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n\n # Create a chain that uses the Chroma vector store\n chain = RetrievalQAWithSourcesChain.from_chain_type(\n llm=llm,\n chain_type=\"stuff\",\n retriever=db.as_retriever(),\n return_source_documents=True,\n chain_type_kwargs=chain_type_kwargs,\n )\n\n # Save the metadata and texts in the user session\n cl.user_session.set(\"metadatas\", metadatas)\n cl.user_session.set(\"texts\", all_texts)\n\n # Create a message to inform the user that the files are ready for queries\n content = \"\"\n if len(files) == 1:\n content = f\"`{files[0].name}` processed. You can now ask questions!\"\n logger.info(content)\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"{', '.join(files_names)} processed. You can now ask questions.\"\n logger.info(content)\n msg.content = content\n msg.author = \"Chatbot\"\n await msg.update()\n\n # Store the chain in the user session\n cl.user_session.set(\"chain\", chain)
\n
\n
The following code handles the communication with the OpenAI API and incorporates retrying logic in case the API calls fail due to specific errors.
\n
\n
\n
.on_message: Theon_messagedecorator registers a callback functionmain(message: str)to be called when the user submits a new message in the chat. It is the main function responsible for handling the chat logic.
\n
cl.user_session.get(\"chain\"): this call retrieves the LLM chain from theuser_sessiondictionary.
\n
cl.AsyncLangchainCallbackHandler: this call creates a LangChain callback handler.
\n
await chain.acall: The asynchronous call to theRetrievalQAWithSourcesChain.acallexecutes the LLM chain with the user message as an input.
\n
\n
\n
@cl.on_message\nasync def main(message: cl.Message):\n # Retrieve the chain from the user session\n chain = cl.user_session.get(\"chain\")\n\n # Create a callback handler\n cb = cl.AsyncLangchainCallbackHandler()\n\n # Get the response from the chain\n response = await chain.acall(message.content, callbacks=[cb])\n logger.info(\"Question: [%s]\", message.content)
\n
\n
The code below extracts the answers and sources from the API response and formats them to be sent as a message.
\n
\n
Theanswerandsourcesare obtained from theresponsedictionary.
\n
The sources are then processed to find corresponding texts in the user session metadata (metadatas) and createsource_elementsusingcl.Text().
\n
cl.Message().send(): theMessageAPI creates and displays a message containing the answer and sources, if available.
\n
The last command sets theAZURE_OPENAI_API_KEYenvironment variable to a security key to access Azure OpenAI returned by the token provider. This key is used by the Chainlit playground.
\n
\n
\n
\n # Get the answer and sources from the response\n answer = response[\"answer\"]\n sources = response[\"sources\"].strip()\n source_elements = []\n\n if debug:\n logger.info(\"Answer: [%s]\", answer)\n\n # Get the metadata and texts from the user session\n metadatas = cl.user_session.get(\"metadatas\")\n all_sources = [m[\"source\"] for m in metadatas]\n texts = cl.user_session.get(\"texts\")\n\n if sources:\n found_sources = []\n\n # Add the sources to the message\n for source in sources.split(\",\"):\n source_name = source.strip().replace(\".\", \"\")\n # Get the index of the source\n try:\n index = all_sources.index(source_name)\n except ValueError:\n continue\n text = texts[index]\n found_sources.append(source_name)\n # Create the text element referenced in the message\n source_elements.append(cl.Text(content=text, name=source_name))\n\n if found_sources:\n answer += f\"\\nSources: {', '.join(found_sources)}\"\n else:\n answer += \"\\nNo sources found\"\n\n await cl.Message(content=answer, elements=source_elements).send()\n\n # Setting the AZURE_OPENAI_API_KEY environment variable for the playground\n if api_type == \"azure_ad\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = token_provider()\n\n
\n
\n
Below, you can read the complete code of the application.
\n
\n
# Import packages\nimport os\nimport io\nimport sys\nimport logging\nimport chainlit as cl\nfrom chainlit.playground.config import AzureChatOpenAI\nfrom pypdf import PdfReader\nfrom docx import Document\nfrom azure.identity import DefaultAzureCredential, get_bearer_token_provider\nfrom dotenv import load_dotenv\nfrom dotenv import dotenv_values\nfrom langchain.embeddings import AzureOpenAIEmbeddings\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain.vectorstores.chroma import Chroma\nfrom langchain.chains import RetrievalQAWithSourcesChain\nfrom langchain.chat_models import AzureChatOpenAI\nfrom langchain.prompts.chat import (\n ChatPromptTemplate,\n SystemMessagePromptTemplate,\n HumanMessagePromptTemplate,\n)\n\n# Load environment variables from .env file\nif os.path.exists(\".env\"):\n load_dotenv(override=True)\n config = dotenv_values(\".env\")\n\n# Read environment variables\ntemperature = float(os.environ.get(\"TEMPERATURE\", 0.9))\napi_base = os.getenv(\"AZURE_OPENAI_BASE\")\napi_key = os.getenv(\"AZURE_OPENAI_KEY\")\napi_type = os.environ.get(\"AZURE_OPENAI_TYPE\", \"azure\")\napi_version = os.environ.get(\"AZURE_OPENAI_VERSION\", \"2023-12-01-preview\")\nchat_completion_deployment = os.getenv(\"AZURE_OPENAI_DEPLOYMENT\")\nembeddings_deployment = os.getenv(\"AZURE_OPENAI_ADA_DEPLOYMENT\")\nmodel = os.getenv(\"AZURE_OPENAI_MODEL\")\nmax_size_mb = int(os.getenv(\"CHAINLIT_MAX_SIZE_MB\", 100))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\nmax_files = int(os.getenv(\"CHAINLIT_MAX_FILES\", 10))\ntext_splitter_chunk_size = int(os.getenv(\"TEXT_SPLITTER_CHUNK_SIZE\", 1000))\ntext_splitter_chunk_overlap = int(os.getenv(\"TEXT_SPLITTER_CHUNK_OVERLAP\", 10))\nembeddings_chunk_size = int(os.getenv(\"EMBEDDINGS_CHUNK_SIZE\", 16))\nmax_retries = int(os.getenv(\"MAX_RETRIES\", 5))\nretry_min_seconds = int(os.getenv(\"RETRY_MIN_SECONDS\", 1))\nretry_max_seconds = int(os.getenv(\"RETRY_MAX_SECONDS\", 5))\ntimeout = int(os.getenv(\"TIMEOUT\", 30))\ndebug = os.getenv(\"DEBUG\", \"False\").lower() in (\"true\", \"1\", \"t\")\n\n# Configure system prompt\nsystem_template = \"\"\"Use the following pieces of context to answer the users question.\nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\nALWAYS return a \"SOURCES\" part in your answer.\nThe \"SOURCES\" part should be a reference to the source of the document from which you got your answer.\n\nExample of your response should be:\n\n```\nThe answer is foo\nSOURCES: xyz\n```\n\nBegin!\n----------------\n{summaries}\"\"\"\nmessages = [\n SystemMessagePromptTemplate.from_template(system_template),\n HumanMessagePromptTemplate.from_template(\"{question}\"),\n]\nprompt = ChatPromptTemplate.from_messages(messages)\nchain_type_kwargs = {\"prompt\": prompt}\n\n# Configure a logger\nlogging.basicConfig(\n stream=sys.stdout,\n format=\"[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s\",\n level=logging.INFO,\n)\nlogger = logging.getLogger(__name__)\n\n# Create Token Provider\nif api_type == \"azure_ad\":\n token_provider = get_bearer_token_provider(\n DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\"\n )\n\n# Setting the environment variables for the playground\nif api_type == \"azure\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = api_key\nos.environ[\"AZURE_OPENAI_API_VERSION\"] = api_version\nos.environ[\"AZURE_OPENAI_ENDPOINT\"] = api_base\nos.environ[\"AZURE_OPENAI_DEPLOYMENT_NAME\"] = chat_completion_deployment\n\n\n@cl.on_chat_start\nasync def start():\n await cl.Avatar(\n name=\"Chatbot\", url=\"https://cdn-icons-png.flaticon.com/512/8649/8649595.png\"\n ).send()\n await cl.Avatar(\n name=\"Error\", url=\"https://cdn-icons-png.flaticon.com/512/8649/8649595.png\"\n ).send()\n await cl.Avatar(\n name=\"You\",\n url=\"https://media.architecturaldigest.com/photos/5f241de2c850b2a36b415024/master/w_1600%2Cc_limit/Luke-logo.png\",\n ).send()\n\n # Initialize the file list to None\n files = None\n\n # Wait for the user to upload a file\n while files == None:\n files = await cl.AskFileMessage(\n content=f\"Please upload up to {max_files} `.pdf` or `.docx` files to begin.\",\n accept=[\n \"application/pdf\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n ],\n max_size_mb=max_size_mb,\n max_files=max_files,\n timeout=86400,\n raise_on_timeout=False,\n ).send()\n\n # Create a message to inform the user that the files are being processed\n content = \"\"\n if len(files) == 1:\n content = f\"Processing `{files[0].name}`...\"\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"Processing {', '.join(files_names)}...\"\n logger.info(content)\n msg = cl.Message(content=content, author=\"Chatbot\")\n await msg.send()\n\n # Create a list to store the texts of each file\n all_texts = []\n\n # Process each file uplodaded by the user\n for file in files:\n # Read file contents\n with open(file.path, \"rb\") as uploaded_file:\n file_contents = uploaded_file.read()\n\n logger.info(\"[%d] bytes were read from %s\", len(file_contents), file.path)\n\n # Create an in-memory buffer from the file content\n bytes = io.BytesIO(file_contents)\n\n # Get file extension\n extension = file.name.split(\".\")[-1]\n\n # Initialize the text variable\n text = \"\"\n\n # Read the file\n if extension == \"pdf\":\n reader = PdfReader(bytes)\n for i in range(len(reader.pages)):\n text += reader.pages[i].extract_text()\n if debug:\n logger.info(\"[%s] read from %s\", text, file.path)\n elif extension == \"docx\":\n doc = Document(bytes)\n paragraph_list = []\n for paragraph in doc.paragraphs:\n paragraph_list.append(paragraph.text)\n if debug:\n logger.info(\"[%s] read from %s\", paragraph.text, file.path)\n text = \"\\n\".join(paragraph_list)\n\n # Split the text into chunks\n text_splitter = RecursiveCharacterTextSplitter(\n chunk_size=text_splitter_chunk_size,\n chunk_overlap=text_splitter_chunk_overlap,\n )\n texts = text_splitter.split_text(text)\n\n # Add the chunks and metadata to the list\n all_texts.extend(texts)\n\n # Create a metadata for each chunk\n metadatas = [{\"source\": f\"{i}-pl\"} for i in range(len(all_texts))]\n\n # Create a Chroma vector store\n if api_type == \"azure\":\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n else:\n embeddings = AzureOpenAIEmbeddings(\n openai_api_version=api_version,\n openai_api_type=api_type,\n azure_endpoint=api_base,\n azure_ad_token_provider=token_provider,\n azure_deployment=embeddings_deployment,\n max_retries=max_retries,\n retry_min_seconds=retry_min_seconds,\n retry_max_seconds=retry_max_seconds,\n chunk_size=embeddings_chunk_size,\n timeout=timeout,\n )\n\n # Create a Chroma vector store\n db = await cl.make_async(Chroma.from_texts)(\n all_texts, embeddings, metadatas=metadatas\n )\n\n # Create an AzureChatOpenAI llm\n if api_type == \"azure\":\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n openai_api_key=api_key,\n azure_endpoint=api_base,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n else:\n llm = AzureChatOpenAI(\n openai_api_type=api_type,\n openai_api_version=api_version,\n azure_endpoint=api_base,\n api_key=api_key,\n temperature=temperature,\n azure_deployment=chat_completion_deployment,\n azure_ad_token_provider=token_provider,\n streaming=True,\n max_retries=max_retries,\n timeout=timeout,\n )\n\n # Create a chain that uses the Chroma vector store\n chain = RetrievalQAWithSourcesChain.from_chain_type(\n llm=llm,\n chain_type=\"stuff\",\n retriever=db.as_retriever(),\n return_source_documents=True,\n chain_type_kwargs=chain_type_kwargs,\n )\n\n # Save the metadata and texts in the user session\n cl.user_session.set(\"metadatas\", metadatas)\n cl.user_session.set(\"texts\", all_texts)\n\n # Create a message to inform the user that the files are ready for queries\n content = \"\"\n if len(files) == 1:\n content = f\"`{files[0].name}` processed. You can now ask questions!\"\n logger.info(content)\n else:\n files_names = [f\"`{f.name}`\" for f in files]\n content = f\"{', '.join(files_names)} processed. You can now ask questions.\"\n logger.info(content)\n msg.content = content\n msg.author = \"Chatbot\"\n await msg.update()\n\n # Store the chain in the user session\n cl.user_session.set(\"chain\", chain)\n\n\n@cl.on_message\nasync def main(message: cl.Message):\n # Retrieve the chain from the user session\n chain = cl.user_session.get(\"chain\")\n\n # Create a callback handler\n cb = cl.AsyncLangchainCallbackHandler()\n\n # Get the response from the chain\n response = await chain.acall(message.content, callbacks=[cb])\n logger.info(\"Question: [%s]\", message.content)\n\n # Get the answer and sources from the response\n answer = response[\"answer\"]\n sources = response[\"sources\"].strip()\n source_elements = []\n\n if debug:\n logger.info(\"Answer: [%s]\", answer)\n\n # Get the metadata and texts from the user session\n metadatas = cl.user_session.get(\"metadatas\")\n all_sources = [m[\"source\"] for m in metadatas]\n texts = cl.user_session.get(\"texts\")\n\n if sources:\n found_sources = []\n\n # Add the sources to the message\n for source in sources.split(\",\"):\n source_name = source.strip().replace(\".\", \"\")\n # Get the index of the source\n try:\n index = all_sources.index(source_name)\n except ValueError:\n continue\n text = texts[index]\n found_sources.append(source_name)\n # Create the text element referenced in the message\n source_elements.append(cl.Text(content=text, name=source_name))\n\n if found_sources:\n answer += f\"\\nSources: {', '.join(found_sources)}\"\n else:\n answer += \"\\nNo sources found\"\n\n await cl.Message(content=answer, elements=source_elements).send()\n\n # Setting the AZURE_OPENAI_API_KEY environment variable for the playground\n if api_type == \"azure_ad\":\n os.environ[\"AZURE_OPENAI_API_KEY\"] = token_provider()
\n
\n
You can run the application locally using the following command. The-wflag` indicates auto-reload whenever we make changes live in our application code.
\n
\n
chainlit run app.py -w
\n
\n
Build Docker Images
\n
You can use thesrc/01-build-docker-images.shBash script to build the Docker container image for each container app.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Use a for loop to build the docker images using the array index\nfor index in ${!images[@]}; do\n # Build the docker image\n docker build -t ${images[$index]}:$tag -f Dockerfile --build-arg FILENAME=${filenames[$index]} --build-arg PORT=$port .\ndone
\n
\n
Before running any script in thesrcfolder, make sure to customize the value of the variables inside the00-variables.shfile located in the same folder. This file is embedded in all the scripts and contains the following variables:
TheDockerfileunder thesrcfolder is parametric and can be used to build the container images for both chat applications.
\n
\n
# app/Dockerfile\n\n# # Stage 1 - Install build dependencies\n\n# A Dockerfile must start with a FROM instruction which sets the base image for the container.\n# The Python images come in many flavors, each designed for a specific use case.\n# The python:3.11-slim image is a good base image for most applications.\n# It is a minimal image built on top of Debian Linux and includes only the necessary packages to run Python.\n# The slim image is a good choice because it is small and contains only the packages needed to run Python.\n# For more information, see: \n# * https://hub.docker.com/_/python \n# * https://docs.streamlit.io/knowledge-base/tutorials/deploy/docker\nFROM python:3.11-slim AS builder\n\n# The WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.\n# If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#workdir\nWORKDIR /app\n\n# Set environment variables. \n# The ENV instruction sets the environment variable <key> to the value <value>.\n# This value will be in the environment of all “descendant” Dockerfile commands and can be replaced inline in many as well.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#env\nENV PYTHONDONTWRITEBYTECODE 1\nENV PYTHONUNBUFFERED 1\n\n# Install git so that we can clone the app code from a remote repo using the RUN instruction.\n# The RUN comand has 2 forms:\n# * RUN <command> (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)\n# * RUN [\"executable\", \"param1\", \"param2\"] (exec form)\n# The RUN instruction will execute any commands in a new layer on top of the current image and commit the results. \n# The resulting committed image will be used for the next step in the Dockerfile.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#run\nRUN apt-get update && apt-get install -y \\\n build-essential \\\n curl \\\n software-properties-common \\\n git \\\n && rm -rf /var/lib/apt/lists/*\n\n# Create a virtualenv to keep dependencies together\nRUN python -m venv /opt/venv\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the requirements.txt which contains dependencies to WORKDIR\n# COPY has two forms:\n# * COPY <src> <dest> (this copies the files from the local machine to the container's own filesystem)\n# * COPY [\"<src>\",... \"<dest>\"] (this form is required for paths containing whitespace)\n# For more information, see: https://docs.docker.com/engine/reference/builder/#copy\nCOPY requirements.txt .\n\n# Install the Python dependencies\nRUN pip install --no-cache-dir --no-deps -r requirements.txt\n\n# Stage 2 - Copy only necessary files to the runner stage\n\n# The FROM instruction initializes a new build stage for the application\nFROM python:3.11-slim\n\n# Define the filename to copy as an argument\nARG FILENAME\n\n# Deefine the port to run the application on as an argument\nARG PORT=8000\n\n# Set an environment variable\nENV FILENAME=${FILENAME}\n\n# Sets the working directory to /app\nWORKDIR /app\n\n# Copy the virtual environment from the builder stage\nCOPY --from=builder /opt/venv /opt/venv\n\n# Set environment variables\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Clone the $FILENAME containing the application code\nCOPY $FILENAME .\n\n# Copy the chainlit.md file to the working directory\nCOPY chainlit.md .\n\n# Copy the .chainlit folder to the working directory\nCOPY ./.chainlit ./.chainlit\n\n# The EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#expose\nEXPOSE $PORT\n\n# The ENTRYPOINT instruction has two forms:\n# * ENTRYPOINT [\"executable\", \"param1\", \"param2\"] (exec form, preferred)\n# * ENTRYPOINT command param1 param2 (shell form)\n# The ENTRYPOINT instruction allows you to configure a container that will run as an executable.\n# For more information, see: https://docs.docker.com/engine/reference/builder/#entrypoint\nCMD chainlit run $FILENAME --port=$PORT
\n
\n
Test applications locally
\n
You can use thesrc/02-run-docker-container.shBash script to test the containers for thesender,processor, andreceiverapplications.
Push Docker containers to the Azure Container Registry
\n
You can use thesrc/03-push-docker-image.shBash script to push the Docker container images for thesender,processor, andreceiverapplications to theAzure Container Registry (ACR).
\n
\n
\n#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Login to ACR\naz acr login --name $acrName \n\n# Retrieve ACR login server. Each container image needs to be tagged with the loginServer name of the registry. \nloginServer=$(az acr show --name $acrName --query loginServer --output tsv)\n\n# Use a for loop to tag and push the local docker images to the Azure Container Registry\nfor index in ${!images[@]}; do\n # Tag the local sender image with the loginServer of ACR\n docker tag ${images[$index],,}:$tag $loginServer/${images[$index],,}:$tag\n\n # Push the container image to ACR\n docker push $loginServer/${images[$index],,}:$tag\ndone\n
\n
\n
Deployment Scripts
\n
If you deployed the Azure infrastructure using the Terraform modules provided with this sample, you only need to deploy the application using the following scripts and YAML templates in thescriptsfolder.
\n
Scripts:
\n
\n
\n
09-deploy-apps.sh
\n
10-configure-dns.sh
\n
\n
\n
YAML manifests:
\n
\n
\n
configMap.yml
\n
deployment.yml
\n
ingress.yml
\n
service.yml
\n
\n
\n
If you instead want to deploy the application in your AKS cluster, make sure to run all of the scripts in order.
\n
The09-deploy-apps.shscript creates the configmap, deployment, service, and ingress Kubernetes objects for thechatanddocsapplications. This script makes use of theyqtool to customize the manifests with the value of the variables defined in the00-variables.shfile. This tool is a lightweight and portable command-line YAML, JSON and XML processor that usesjqlike syntax but works with YAML files as well as json, xml, properties, csv and tsv. It doesn't yet support everything jq does - but it does support the most common operations and functions, and more is being added continuously.
\n
\n
#!/bin/bash\n\n# Variables\nsource ./00-variables.sh\n\n# Attach ACR to AKS cluster\nif [[ $attachAcr == true ]]; then\n echo \"Attaching ACR $acrName to AKS cluster $aksClusterName...\"\n az aks update \\\n --name $aksClusterName \\\n --resource-group $aksResourceGroupName \\\n --attach-acr $acrName\nfi\n\n# Check if namespace exists in the cluster\nresult=$(kubectl get namespace -o jsonpath=\"{.items[?(@.metadata.name=='$namespace')].metadata.name}\")\n\nif [[ -n $result ]]; then\n echo \"$namespace namespace already exists in the cluster\"\nelse\n echo \"$namespace namespace does not exist in the cluster\"\n echo \"creating $namespace namespace in the cluster...\"\n kubectl create namespace $namespace\nfi\n\n# Create config map\ncat $configMapTemplate |\n yq \"(.data.TITLE)|=\"\\\"\"$title\"\\\" |\n yq \"(.data.LABEL)|=\"\\\"\"$label\"\\\" |\n yq \"(.data.TEMPERATURE)|=\"\\\"\"$temperature\"\\\" |\n yq \"(.data.IMAGE_WIDTH)|=\"\\\"\"$imageWidth\"\\\" |\n yq \"(.data.AZURE_OPENAI_TYPE)|=\"\\\"\"$openAiType\"\\\" |\n yq \"(.data.AZURE_OPENAI_BASE)|=\"\\\"\"$openAiBase\"\\\" |\n yq \"(.data.AZURE_OPENAI_MODEL)|=\"\\\"\"$openAiModel\"\\\" |\n yq \"(.data.AZURE_OPENAI_DEPLOYMENT)|=\"\\\"\"$openAiDeployment\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\ncat $deploymentTemplate |\n yq \"(.spec.template.spec.containers[0].image)|=\"\\\"\"$image\"\\\" |\n yq \"(.spec.template.spec.containers[0].imagePullPolicy)|=\"\\\"\"$imagePullPolicy\"\\\" |\n yq \"(.spec.template.spec.serviceAccountName)|=\"\\\"\"$serviceAccountName\"\\\" |\n kubectl apply -n $namespace -f -\n\n# Create deployment\nkubectl apply -f $serviceTemplate -n $namespace
\n
\n
The 10-configure-dns.shscript creates an A record in the Azure Public DNS Zone to expose thechatanddocsapplications via a given subdomain (e.g.,https://chat.contoso.com).
\n
\n
# Variables\nsource ./00-variables.sh\nsubdomains=($docsSubdomain $chatSubdomain)\n\n# Install jq if not installed\npath=$(which jq)\n\nif [[ -z $path ]]; then\n echo 'Installing jq...'\n apt install -y jq\nfi\n\n# Choose the ingress controller to use\nif [[ $ingressClassName == \"nginx\" ]]; then\n ingressNamespace=$nginxNamespace\n ingressServiceName=\"${nginxReleaseName}-controller\"\nelse\n ingressNamespace=$webAppRoutingNamespace\n ingressServiceName=$webAppRoutingServiceName\nfi\n\n# Retrieve the public IP address of the NGINX ingress controller\necho \"Retrieving the external IP address of the [$ingressClassName] NGINX ingress controller...\"\npublicIpAddress=$(kubectl get service -o json -n $ingressNamespace |\n jq -r '.items[] | \n select(.spec.type == \"LoadBalancer\" and .metadata.name == \"'$ingressServiceName'\") |\n .status.loadBalancer.ingress[0].ip')\n\nif [ -n \"$publicIpAddress\" ]; then\n echo \"[$publicIpAddress] external IP address of the [$ingressClassName] NGINX ingress controller successfully retrieved\"\nelse\n echo \"Failed to retrieve the external IP address of the [$ingressClassName] NGINX ingress controller\"\n exit\nfi\n\nfor subdomain in ${subdomains[@]}; do\n # Check if an A record for todolist subdomain exists in the DNS Zone\n echo \"Retrieving the A record for the [$subdomain] subdomain from the [$dnsZoneName] DNS zone...\"\n ipv4Address=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].ARecords[].ipv4Address\" \\\n --output tsv \\\n --only-show-errors)\n\n if [[ -n $ipv4Address ]]; then\n echo \"An A record already exists in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$ipv4Address] IP address\"\n\n if [[ $ipv4Address == $publicIpAddress ]]; then\n echo \"The [$ipv4Address] ip address of the existing A record is equal to the ip address of the ingress\"\n echo \"No additional step is required\"\n continue\n else\n echo \"The [$ipv4Address] ip address of the existing A record is different than the ip address of the ingress\"\n fi\n # Retrieving name of the record set relative to the zone\n echo \"Retrieving the name of the record set relative to the [$dnsZoneName] zone...\"\n\n recordSetName=$(az network dns record-set a list \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --query \"[?name=='$subdomain'].name\" \\\n --output tsv \\\n --only-show-errors 2>/dev/null)\n\n if [[ -n $recordSetName ]]; then\n echo \"[$recordSetName] record set name successfully retrieved\"\n else\n echo \"Failed to retrieve the name of the record set relative to the [$dnsZoneName] zone\"\n exit\n fi\n\n # Remove the A record\n echo \"Removing the A record from the record set relative to the [$dnsZoneName] zone...\"\n\n az network dns record-set a remove-record \\\n --ipv4-address $ipv4Address \\\n --record-set-name $recordSetName \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --only-show-errors 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"[$ipv4Address] ip address successfully removed from the [$recordSetName] record set\"\n else\n echo \"Failed to remove the [$ipv4Address] ip address from the [$recordSetName] record set\"\n exit\n fi\n fi\n\n # Create the A record\n echo \"Creating an A record in [$dnsZoneName] DNS zone for the [$subdomain] subdomain with [$publicIpAddress] IP address...\"\n az network dns record-set a add-record \\\n --zone-name $dnsZoneName \\\n --resource-group $dnsZoneResourceGroupName \\\n --record-set-name $subdomain \\\n --ipv4-address $publicIpAddress \\\n --only-show-errors 1>/dev/null\n\n if [[ $? == 0 ]]; then\n echo \"A record for the [$subdomain] subdomain with [$publicIpAddress] IP address successfully created in [$dnsZoneName] DNS zone\"\n else\n echo \"Failed to create an A record for the $subdomain subdomain with [$publicIpAddress] IP address in [$dnsZoneName] DNS zone\"\n fi\ndone
\n
\n
YAML manifests
\n
Below you can read the YAML manifests used to deploy thechatchatbot to AKS. For brevity, I will cover only the installation of this application, but you can find all the YAML manifests in the companion GitHub repository. Thechat-configmap.ymldefines a value for the environment variables passed to the application container. The configmap does not define any environment variable for the OpenAI key as the container.
These are the parameters defined by the configmap:
\n
\n
\n
TEMPERATURE: the temperature used by the OpenAI API to generate the response.
\n
AZURE_OPENAI_TYPE: specifyazureif you want to let the application use the API key to authenticate against OpenAI. In this case, make sure to provide the Key in theAZURE_OPENAI_KEYenvironment variable. If you want to authenticate using an Microsoft Entra ID security token, you need to specifyazure_adas a value. In this case, don't need to provide any value in theAZURE_OPENAI_KEYenvironment variable.
\n
AZURE_OPENAI_BASE: the URL of your Azure OpenAI resource. If you use the API key to authenticate against OpenAI, you can specify the regional endpoint of your Azure OpenAI Service (e.g.,https://eastus.api.cognitive.microsoft.com/). If you instead plan to use Microsoft Entra ID security tokens for authentication, you need to deploy your Azure OpenAI Service with a subdomain and specify the resource-specific endpoint url (e.g.,https://myopenai.openai.azure.com/).
\n
AZURE_OPENAI_KEY: the key of your Azure OpenAI resource. If you setAZURE_OPENAI_TYPEtoazure_adyou can leave this parameter empty.
\n
AZURE_OPENAI_VERSION: A string representing the version of the OpenAI API.
\n
AZURE_OPENAI_DEPLOYMENT: the name of the ChatGPT deployment used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_MODEL: the name of the ChatGPT model used by your Azure OpenAI resource, for examplegpt-35-turbo.
\n
AZURE_OPENAI_SYSTEM_MESSAGE: The content of the system message used for OpenAI API calls. You can use it to describe the assistant's personality.
\n
\n
\n
Thechat-deployment.ymlmanifest is used create a Kubernetesdeploymentthat defines the application pods to create.azure.workload.identity/uselabel is required in the pod template spec. Only pods with this label will be mutated by the azure-workload-identity mutating admission webhook to inject the Azure specific environment variables and the projected service account token volume.
Theingress.ymlmanifest defines a Kubernetesingressobject used to expose the service via theNGINX Ingress Controller. This project deploys a managed NGINX Ingress Controller using the application routing add-on and an unmanaged instance of the NGINX Ingress Controller using the Helm Terrafom Provider and related chart. The Terraform module creates twoclusterissuerobjects, one for the managed and one for the unmanaged version of the NGINX Ingress Controller. You can run the following command to see the two ingress classes:
\n
\n
kubectl get ingressclass
\n
\n
Executing the command will produce a result as follows:
\n
\n
NAME CONTROLLER PARAMETERS AGE\nnginx k8s.io/ingress-nginx <none> 4d\nwebapprouting.kubernetes.azure.com webapprouting.kubernetes.azure.com/nginx <none> 4d22h
\n
\n
Run the following command to retrieve the cluster issuers used by thecert-manager:
\n
\n
kubectl get clusterissuer
\n
\n
The above command should return a result as follows:
\n
\n
NAME READY AGE\nletsencrypt-nginx True 4d2h\nletsencrypt-webapprouting True 4d2h
\n
\n
Thechat-ingresscontains the code of the ingress object used to expose thechatapplication. This version of the ingress makes use of the unmanaged instance of the NGINX Ingress Controller.
The ingress object defines the following annotations:
\n
\n
\n
cert-manager.io/cluster-issuer: specifies the name of a cert-manager.io ClusterIssuer to acquire the certificate required for this Ingress. It does not matter which namespace your Ingress resides, as ClusterIssuers are non-namespaced resources. In this sample, the cert-manager is instructed to use theletsencrypt-nginxClusterIssuer that you can create using the06-create-cluster-issuer.shscript.
You can delete the resource group using the following Azure CLI command when you no longer need the resources you created. This will remove all the Azure resources.
\n
\n
az group delete --name <resource-group-name>
\n
\n
Alternatively, you can use the following PowerShell cmdlet to delete the resource group and all the Azure resources.
This sample shows how to create two AKS-hosted chat applications that use OpenAI, LangChain, ChromaDB, and Chainlit using Python and deploy them to an AKS environment built in Terraform.
\n
\n
","introduction":"","coverImage":null,"coverImageProperties":{"__typename":"CoverImageProperties","style":"STANDARD","titlePosition":"BOTTOM","altText":""},"currentRevision":{"__ref":"Revision:revision:4024070_7"},"latestVersion":{"__typename":"FriendlyVersion","major":"6","minor":"0"},"metrics":{"__typename":"MessageMetrics","views":27743},"visibilityScope":"PUBLIC","canonicalUrl":null,"seoTitle":null,"seoDescription":null,"placeholder":false,"originalMessageForPlaceholder":null,"contributors":{"__typename":"UserConnection","edges":[]},"nonCoAuthorContributors":{"__typename":"UserConnection","edges":[]},"coAuthors":{"__typename":"UserConnection","edges":[]},"blogMessagePolicies":{"__typename":"BlogMessagePolicies","canDoAuthoringActionsOnBlog":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","key":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","args":[]}}},"archivalData":null,"replies":{"__typename":"MessageConnection","edges":[{"__typename":"MessageEdge","cursor":"MjUuMXwyLjF8aXwxMHwxMzI6MHxpbnQsNDEyMjM2MCw0MTIyMzYw","node":{"__ref":"BlogReplyMessage:message:4122360"}},{"__typename":"MessageEdge","cursor":"MjUuMXwyLjF8aXwxMHwxMzI6MHxpbnQsNDEyMjM2MCw0MTIyMzU2","node":{"__ref":"BlogReplyMessage:message:4122356"}},{"__typename":"MessageEdge","cursor":"MjUuMXwyLjF8aXwxMHwxMzI6MHxpbnQsNDEyMjM2MCw0MDI4MzAx","node":{"__ref":"BlogReplyMessage:message:4028301"}},{"__typename":"MessageEdge","cursor":"MjUuMXwyLjF8aXwxMHwxMzI6MHxpbnQsNDEyMjM2MCw0MDI4MjI2","node":{"__ref":"BlogReplyMessage:message:4028226"}}],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}},"customFields":[],"revisions({\"constraints\":{\"isPublished\":{\"eq\":true}},\"first\":1})":{"__typename":"RevisionConnection","totalCount":7}},"Conversation:conversation:4024070":{"__typename":"Conversation","id":"conversation:4024070","solved":false,"topic":{"__ref":"BlogTopicMessage:message:4024070"},"lastPostingActivityTime":"2024-05-07T05:36:43.762-07:00","lastPostTime":"2024-04-24T04:58:06.855-07:00","unreadReplyCount":4,"isSubscribed":false},"ModerationData:moderation_data:4024070":{"__typename":"ModerationData","id":"moderation_data:4024070","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExMWk0RkI1M0YwNTY0QkQzNUNF?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExMWk0RkI1M0YwNTY0QkQzNUNF?revision=7","title":"architecture.png","associationType":"TEASER","width":862,"height":853,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExMmlGRkYwM0NGQkNFNjRGNjU2?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExMmlGRkYwM0NGQkNFNjRGNjU2?revision=7","title":"architecture.png","associationType":"BODY","width":862,"height":853,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExNGk2MzlBOEVCOTczMkE0MjYz?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExNGk2MzlBOEVCOTczMkE0MjYz?revision=7","title":"workload.png","associationType":"BODY","width":1206,"height":464,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExNWlGMkZFRDc0MjAzMEMyOTE5?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExNWlGMkZFRDc0MjAzMEMyOTE5?revision=7","title":"containers.png","associationType":"BODY","width":633,"height":152,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExN2kwNDE3MEU1MEQ2MzQzQkZF?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExN2kwNDE3MEU1MEQ2MzQzQkZF?revision=7","title":"openai.png","associationType":"BODY","width":1007,"height":617,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExOGk1RTBDNTI2OUMyREU5RDBD?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExOGk1RTBDNTI2OUMyREU5RDBD?revision=7","title":"federatedidentitycredentials.png","associationType":"BODY","width":892,"height":686,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExOWk0NzlFNThDNjI3QUNENzM2?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDExOWk0NzlFNThDNjI3QUNENzM2?revision=7","title":"chainlit-welcome-screen.png","associationType":"BODY","width":1399,"height":1072,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE4OGk1ODE3ODQxQTMzQzY0REMz?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE4OGk1ODE3ODQxQTMzQzY0REMz?revision=7","title":"chainlit-simple-chat.png","associationType":"BODY","width":1358,"height":1132,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5MWkxQ0I1QzI0Q0VFNTVBRjNC?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5MWkxQ0I1QzI0Q0VFNTVBRjNC?revision=7","title":"chainlit-format-result.png","associationType":"BODY","width":1399,"height":1154,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyM2k3NzU5RjAxNkVEODBEQjU4?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyM2k3NzU5RjAxNkVEODBEQjU4?revision=7","title":"chainlit-dark-mode.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyNmkzM0FBNzE1RTQ3RDkwNUI4?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyNmkzM0FBNzE1RTQ3RDkwNUI4?revision=7","title":"chainlit-before-upload.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyOGk1QUFBNDZBMjFCODc5RkIz?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEyOGk1QUFBNDZBMjFCODc5RkIz?revision=7","title":"chainlit-processing-documents.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5Mmk3RDJFMjQ5RTM0QzJGMjgz?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5Mmk3RDJFMjQ5RTM0QzJGMjgz?revision=7","title":"chainlit-document-reply.png","associationType":"BODY","width":1358,"height":1132,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5M2k5OUZFOUQ1RjM1N0MyOTAz?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5M2k5OUZFOUQ1RjM1N0MyOTAz?revision=7","title":"chainlit-chain-of-thought.png","associationType":"BODY","width":1358,"height":3039,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5NGkzOTUxODk3QThGQjc5N0E1?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDE5NGkzOTUxODk3QThGQjc5N0E1?revision=7","title":"chainlit-source.png","associationType":"BODY","width":1358,"height":1132,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzM2kyRTg1QzY5REM0MEM2NzBE?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzM2kyRTg1QzY5REM0MEM2NzBE?revision=7","title":"chainlit-prompt-playground.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNGlFREU5MDY4REM0MjUzNzdG?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNGlFREU5MDY4REM0MjUzNzdG?revision=7","title":"chainlit-prompt-playground-variable.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNWk4NjY5NzY3RDQ4QTYyOUI5?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNWk4NjY5NzY3RDQ4QTYyOUI5?revision=7","title":"chainlit-prompt-playground-question.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNmkyQkZDRDkwRDY4ODZEQzEx?revision=7\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00MDI0MDcwLTU0MDEzNmkyQkZDRDkwRDY4ODZEQzEx?revision=7","title":"chainlit-prompt-playground-reply.png","associationType":"BODY","width":1399,"height":1124,"altText":null},"Revision:revision:4024070_7":{"__typename":"Revision","id":"revision:4024070_7","lastEditTime":"2024-05-07T05:36:43.762-07:00"},"CachedAsset:theme:customTheme1-1744326567581":{"__typename":"CachedAsset","id":"theme:customTheme1-1744326567581","value":{"id":"customTheme1","animation":{"fast":"150ms","normal":"250ms","slow":"500ms","slowest":"750ms","function":"cubic-bezier(0.07, 0.91, 0.51, 1)","__typename":"AnimationThemeSettings"},"avatar":{"borderRadius":"50%","collections":["default"],"__typename":"AvatarThemeSettings"},"basics":{"browserIcon":{"imageAssetName":"favicon-1730836283320.png","imageLastModified":"1730836286415","__typename":"ThemeAsset"},"customerLogo":{"imageAssetName":"favicon-1730836271365.png","imageLastModified":"1730836274203","__typename":"ThemeAsset"},"maximumWidthOfPageContent":"1300px","oneColumnNarrowWidth":"800px","gridGutterWidthMd":"30px","gridGutterWidthXs":"10px","pageWidthStyle":"WIDTH_OF_BROWSER","__typename":"BasicsThemeSettings"},"buttons":{"borderRadiusSm":"3px","borderRadius":"3px","borderRadiusLg":"5px","paddingY":"5px","paddingYLg":"7px","paddingYHero":"var(--lia-bs-btn-padding-y-lg)","paddingX":"12px","paddingXLg":"16px","paddingXHero":"60px","fontStyle":"NORMAL","fontWeight":"700","textTransform":"NONE","disabledOpacity":0.5,"primaryTextColor":"var(--lia-bs-white)","primaryTextHoverColor":"var(--lia-bs-white)","primaryTextActiveColor":"var(--lia-bs-white)","primaryBgColor":"var(--lia-bs-primary)","primaryBgHoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.85))","primaryBgActiveColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.7))","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","primaryBorderActive":"1px solid transparent","primaryBorderFocus":"1px solid var(--lia-bs-white)","primaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","secondaryTextColor":"var(--lia-bs-gray-900)","secondaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","secondaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","secondaryBgColor":"var(--lia-bs-gray-200)","secondaryBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","secondaryBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","secondaryBorder":"1px solid transparent","secondaryBorderHover":"1px solid transparent","secondaryBorderActive":"1px solid transparent","secondaryBorderFocus":"1px solid transparent","secondaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","tertiaryTextColor":"var(--lia-bs-gray-900)","tertiaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","tertiaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","tertiaryBgColor":"transparent","tertiaryBgHoverColor":"transparent","tertiaryBgActiveColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.04)","tertiaryBorder":"1px solid transparent","tertiaryBorderHover":"1px solid hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","tertiaryBorderActive":"1px solid transparent","tertiaryBorderFocus":"1px solid transparent","tertiaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","destructiveTextColor":"var(--lia-bs-danger)","destructiveTextHoverColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.95))","destructiveTextActiveColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.9))","destructiveBgColor":"var(--lia-bs-gray-200)","destructiveBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","destructiveBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","destructiveBorder":"1px solid transparent","destructiveBorderHover":"1px solid transparent","destructiveBorderActive":"1px solid transparent","destructiveBorderFocus":"1px solid transparent","destructiveBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","__typename":"ButtonsThemeSettings"},"border":{"color":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","mainContent":"NONE","sideContent":"LIGHT","radiusSm":"3px","radius":"5px","radiusLg":"9px","radius50":"100vw","__typename":"BorderThemeSettings"},"boxShadow":{"xs":"0 0 0 1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.08), 0 3px 0 -1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.16)","sm":"0 2px 4px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.12)","md":"0 5px 15px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","lg":"0 10px 30px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","__typename":"BoxShadowThemeSettings"},"cards":{"bgColor":"var(--lia-panel-bg-color)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":"var(--lia-box-shadow-xs)","__typename":"CardsThemeSettings"},"chip":{"maxWidth":"300px","height":"30px","__typename":"ChipThemeSettings"},"coreTypes":{"defaultMessageLinkColor":"var(--lia-bs-link-color)","defaultMessageLinkDecoration":"none","defaultMessageLinkFontStyle":"NORMAL","defaultMessageLinkFontWeight":"400","defaultMessageFontStyle":"NORMAL","defaultMessageFontWeight":"400","forumColor":"#4099E2","forumFontFamily":"var(--lia-bs-font-family-base)","forumFontWeight":"var(--lia-default-message-font-weight)","forumLineHeight":"var(--lia-bs-line-height-base)","forumFontStyle":"var(--lia-default-message-font-style)","forumMessageLinkColor":"var(--lia-default-message-link-color)","forumMessageLinkDecoration":"var(--lia-default-message-link-decoration)","forumMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","forumMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","forumSolvedColor":"#148563","blogColor":"#1CBAA0","blogFontFamily":"var(--lia-bs-font-family-base)","blogFontWeight":"var(--lia-default-message-font-weight)","blogLineHeight":"1.75","blogFontStyle":"var(--lia-default-message-font-style)","blogMessageLinkColor":"var(--lia-default-message-link-color)","blogMessageLinkDecoration":"var(--lia-default-message-link-decoration)","blogMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","blogMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","tkbColor":"#4C6B90","tkbFontFamily":"var(--lia-bs-font-family-base)","tkbFontWeight":"var(--lia-default-message-font-weight)","tkbLineHeight":"1.75","tkbFontStyle":"var(--lia-default-message-font-style)","tkbMessageLinkColor":"var(--lia-default-message-link-color)","tkbMessageLinkDecoration":"var(--lia-default-message-link-decoration)","tkbMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","tkbMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaColor":"#4099E2","qandaFontFamily":"var(--lia-bs-font-family-base)","qandaFontWeight":"var(--lia-default-message-font-weight)","qandaLineHeight":"var(--lia-bs-line-height-base)","qandaFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkColor":"var(--lia-default-message-link-color)","qandaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","qandaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaSolvedColor":"#3FA023","ideaColor":"#FF8000","ideaFontFamily":"var(--lia-bs-font-family-base)","ideaFontWeight":"var(--lia-default-message-font-weight)","ideaLineHeight":"var(--lia-bs-line-height-base)","ideaFontStyle":"var(--lia-default-message-font-style)","ideaMessageLinkColor":"var(--lia-default-message-link-color)","ideaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","ideaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","ideaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","contestColor":"#FCC845","contestFontFamily":"var(--lia-bs-font-family-base)","contestFontWeight":"var(--lia-default-message-font-weight)","contestLineHeight":"var(--lia-bs-line-height-base)","contestFontStyle":"var(--lia-default-message-link-font-style)","contestMessageLinkColor":"var(--lia-default-message-link-color)","contestMessageLinkDecoration":"var(--lia-default-message-link-decoration)","contestMessageLinkFontStyle":"ITALIC","contestMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","occasionColor":"#D13A1F","occasionFontFamily":"var(--lia-bs-font-family-base)","occasionFontWeight":"var(--lia-default-message-font-weight)","occasionLineHeight":"var(--lia-bs-line-height-base)","occasionFontStyle":"var(--lia-default-message-font-style)","occasionMessageLinkColor":"var(--lia-default-message-link-color)","occasionMessageLinkDecoration":"var(--lia-default-message-link-decoration)","occasionMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","occasionMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","grouphubColor":"#333333","categoryColor":"#949494","communityColor":"#FFFFFF","productColor":"#949494","__typename":"CoreTypesThemeSettings"},"colors":{"black":"#000000","white":"#FFFFFF","gray100":"#F7F7F7","gray200":"#F7F7F7","gray300":"#E8E8E8","gray400":"#D9D9D9","gray500":"#CCCCCC","gray600":"#717171","gray700":"#707070","gray800":"#545454","gray900":"#333333","dark":"#545454","light":"#F7F7F7","primary":"#0069D4","secondary":"#333333","bodyText":"#1E1E1E","bodyBg":"#FFFFFF","info":"#409AE2","success":"#41C5AE","warning":"#FCC844","danger":"#BC341B","alertSystem":"#FF6600","textMuted":"#707070","highlight":"#FFFCAD","outline":"var(--lia-bs-primary)","custom":["#D3F5A4","#243A5E"],"__typename":"ColorsThemeSettings"},"divider":{"size":"3px","marginLeft":"4px","marginRight":"4px","borderRadius":"50%","bgColor":"var(--lia-bs-gray-600)","bgColorActive":"var(--lia-bs-gray-600)","__typename":"DividerThemeSettings"},"dropdown":{"fontSize":"var(--lia-bs-font-size-sm)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius-sm)","dividerBg":"var(--lia-bs-gray-300)","itemPaddingY":"5px","itemPaddingX":"20px","headerColor":"var(--lia-bs-gray-700)","__typename":"DropdownThemeSettings"},"email":{"link":{"color":"#0069D4","hoverColor":"#0061c2","decoration":"none","hoverDecoration":"underline","__typename":"EmailLinkSettings"},"border":{"color":"#e4e4e4","__typename":"EmailBorderSettings"},"buttons":{"borderRadiusLg":"5px","paddingXLg":"16px","paddingYLg":"7px","fontWeight":"700","primaryTextColor":"#ffffff","primaryTextHoverColor":"#ffffff","primaryBgColor":"#0069D4","primaryBgHoverColor":"#005cb8","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","__typename":"EmailButtonsSettings"},"panel":{"borderRadius":"5px","borderColor":"#e4e4e4","__typename":"EmailPanelSettings"},"__typename":"EmailThemeSettings"},"emoji":{"skinToneDefault":"#ffcd43","skinToneLight":"#fae3c5","skinToneMediumLight":"#e2cfa5","skinToneMedium":"#daa478","skinToneMediumDark":"#a78058","skinToneDark":"#5e4d43","__typename":"EmojiThemeSettings"},"heading":{"color":"var(--lia-bs-body-color)","fontFamily":"Segoe UI","fontStyle":"NORMAL","fontWeight":"400","h1FontSize":"34px","h2FontSize":"32px","h3FontSize":"28px","h4FontSize":"24px","h5FontSize":"20px","h6FontSize":"16px","lineHeight":"1.3","subHeaderFontSize":"11px","subHeaderFontWeight":"500","h1LetterSpacing":"normal","h2LetterSpacing":"normal","h3LetterSpacing":"normal","h4LetterSpacing":"normal","h5LetterSpacing":"normal","h6LetterSpacing":"normal","subHeaderLetterSpacing":"2px","h1FontWeight":"var(--lia-bs-headings-font-weight)","h2FontWeight":"var(--lia-bs-headings-font-weight)","h3FontWeight":"var(--lia-bs-headings-font-weight)","h4FontWeight":"var(--lia-bs-headings-font-weight)","h5FontWeight":"var(--lia-bs-headings-font-weight)","h6FontWeight":"var(--lia-bs-headings-font-weight)","__typename":"HeadingThemeSettings"},"icons":{"size10":"10px","size12":"12px","size14":"14px","size16":"16px","size20":"20px","size24":"24px","size30":"30px","size40":"40px","size50":"50px","size60":"60px","size80":"80px","size120":"120px","size160":"160px","__typename":"IconsThemeSettings"},"imagePreview":{"bgColor":"var(--lia-bs-gray-900)","titleColor":"var(--lia-bs-white)","controlColor":"var(--lia-bs-white)","controlBgColor":"var(--lia-bs-gray-800)","__typename":"ImagePreviewThemeSettings"},"input":{"borderColor":"var(--lia-bs-gray-600)","disabledColor":"var(--lia-bs-gray-600)","focusBorderColor":"var(--lia-bs-primary)","labelMarginBottom":"10px","btnFontSize":"var(--lia-bs-font-size-sm)","focusBoxShadow":"0 0 0 3px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","checkLabelMarginBottom":"2px","checkboxBorderRadius":"3px","borderRadiusSm":"var(--lia-bs-border-radius-sm)","borderRadius":"var(--lia-bs-border-radius)","borderRadiusLg":"var(--lia-bs-border-radius-lg)","formTextMarginTop":"4px","textAreaBorderRadius":"var(--lia-bs-border-radius)","activeFillColor":"var(--lia-bs-primary)","__typename":"InputThemeSettings"},"loading":{"dotDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.2)","dotLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.5)","barDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.06)","barLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.4)","__typename":"LoadingThemeSettings"},"link":{"color":"var(--lia-bs-primary)","hoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) - 10%))","decoration":"none","hoverDecoration":"underline","__typename":"LinkThemeSettings"},"listGroup":{"itemPaddingY":"15px","itemPaddingX":"15px","borderColor":"var(--lia-bs-gray-300)","__typename":"ListGroupThemeSettings"},"modal":{"contentTextColor":"var(--lia-bs-body-color)","contentBg":"var(--lia-bs-white)","backgroundBg":"var(--lia-bs-black)","smSize":"440px","mdSize":"760px","lgSize":"1080px","backdropOpacity":0.3,"contentBoxShadowXs":"var(--lia-bs-box-shadow-sm)","contentBoxShadow":"var(--lia-bs-box-shadow)","headerFontWeight":"700","__typename":"ModalThemeSettings"},"navbar":{"position":"FIXED","background":{"attachment":null,"clip":null,"color":"var(--lia-bs-white)","imageAssetName":"","imageLastModified":"0","origin":null,"position":"CENTER_CENTER","repeat":"NO_REPEAT","size":"COVER","__typename":"BackgroundProps"},"backgroundOpacity":0.8,"paddingTop":"15px","paddingBottom":"15px","borderBottom":"1px solid var(--lia-bs-border-color)","boxShadow":"var(--lia-bs-box-shadow-sm)","brandMarginRight":"30px","brandMarginRightSm":"10px","brandLogoHeight":"30px","linkGap":"10px","linkJustifyContent":"flex-start","linkPaddingY":"5px","linkPaddingX":"10px","linkDropdownPaddingY":"9px","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkColor":"var(--lia-bs-body-color)","linkHoverColor":"var(--lia-bs-primary)","linkFontSize":"var(--lia-bs-font-size-sm)","linkFontStyle":"NORMAL","linkFontWeight":"400","linkTextTransform":"NONE","linkLetterSpacing":"normal","linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkBgColor":"transparent","linkBgHoverColor":"transparent","linkBorder":"none","linkBorderHover":"none","linkBoxShadow":"none","linkBoxShadowHover":"none","linkTextBorderBottom":"none","linkTextBorderBottomHover":"none","dropdownPaddingTop":"10px","dropdownPaddingBottom":"15px","dropdownPaddingX":"10px","dropdownMenuOffset":"2px","dropdownDividerMarginTop":"10px","dropdownDividerMarginBottom":"10px","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","controllerIconColor":"var(--lia-bs-body-color)","controllerIconHoverColor":"var(--lia-bs-body-color)","controllerTextColor":"var(--lia-nav-controller-icon-color)","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","controllerHighlightColor":"hsla(30, 100%, 50%)","controllerHighlightTextColor":"var(--lia-yiq-light)","controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerColor":"var(--lia-nav-controller-icon-color)","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","hamburgerBgColor":"transparent","hamburgerBgHoverColor":"transparent","hamburgerBorder":"none","hamburgerBorderHover":"none","collapseMenuMarginLeft":"20px","collapseMenuDividerBg":"var(--lia-nav-link-color)","collapseMenuDividerOpacity":0.16,"__typename":"NavbarThemeSettings"},"pager":{"textColor":"var(--lia-bs-link-color)","textFontWeight":"var(--lia-font-weight-md)","textFontSize":"var(--lia-bs-font-size-sm)","__typename":"PagerThemeSettings"},"panel":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-bs-border-radius)","borderColor":"var(--lia-bs-border-color)","boxShadow":"none","__typename":"PanelThemeSettings"},"popover":{"arrowHeight":"8px","arrowWidth":"16px","maxWidth":"300px","minWidth":"100px","headerBg":"var(--lia-bs-white)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius)","boxShadow":"0 0.5rem 1rem hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.15)","__typename":"PopoverThemeSettings"},"prism":{"color":"#000000","bgColor":"#f5f2f0","fontFamily":"var(--font-family-monospace)","fontSize":"var(--lia-bs-font-size-base)","fontWeightBold":"var(--lia-bs-font-weight-bold)","fontStyleItalic":"italic","tabSize":2,"highlightColor":"#b3d4fc","commentColor":"#62707e","punctuationColor":"#6f6f6f","namespaceOpacity":"0.7","propColor":"#990055","selectorColor":"#517a00","operatorColor":"#906736","operatorBgColor":"hsla(0, 0%, 100%, 0.5)","keywordColor":"#0076a9","functionColor":"#d3284b","variableColor":"#c14700","__typename":"PrismThemeSettings"},"rte":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":" var(--lia-panel-box-shadow)","customColor1":"#bfedd2","customColor2":"#fbeeb8","customColor3":"#f8cac6","customColor4":"#eccafa","customColor5":"#c2e0f4","customColor6":"#2dc26b","customColor7":"#f1c40f","customColor8":"#e03e2d","customColor9":"#b96ad9","customColor10":"#3598db","customColor11":"#169179","customColor12":"#e67e23","customColor13":"#ba372a","customColor14":"#843fa1","customColor15":"#236fa1","customColor16":"#ecf0f1","customColor17":"#ced4d9","customColor18":"#95a5a6","customColor19":"#7e8c8d","customColor20":"#34495e","customColor21":"#000000","customColor22":"#ffffff","defaultMessageHeaderMarginTop":"40px","defaultMessageHeaderMarginBottom":"20px","defaultMessageItemMarginTop":"0","defaultMessageItemMarginBottom":"10px","diffAddedColor":"hsla(170, 53%, 51%, 0.4)","diffChangedColor":"hsla(43, 97%, 63%, 0.4)","diffNoneColor":"hsla(0, 0%, 80%, 0.4)","diffRemovedColor":"hsla(9, 74%, 47%, 0.4)","specialMessageHeaderMarginTop":"40px","specialMessageHeaderMarginBottom":"20px","specialMessageItemMarginTop":"0","specialMessageItemMarginBottom":"10px","__typename":"RteThemeSettings"},"tags":{"bgColor":"var(--lia-bs-gray-200)","bgHoverColor":"var(--lia-bs-gray-400)","borderRadius":"var(--lia-bs-border-radius-sm)","color":"var(--lia-bs-body-color)","hoverColor":"var(--lia-bs-body-color)","fontWeight":"var(--lia-font-weight-md)","fontSize":"var(--lia-font-size-xxs)","textTransform":"UPPERCASE","letterSpacing":"0.5px","__typename":"TagsThemeSettings"},"toasts":{"borderRadius":"var(--lia-bs-border-radius)","paddingX":"12px","__typename":"ToastsThemeSettings"},"typography":{"fontFamilyBase":"Segoe UI","fontStyleBase":"NORMAL","fontWeightBase":"400","fontWeightLight":"300","fontWeightNormal":"400","fontWeightMd":"500","fontWeightBold":"700","letterSpacingSm":"normal","letterSpacingXs":"normal","lineHeightBase":"1.5","fontSizeBase":"16px","fontSizeXxs":"11px","fontSizeXs":"12px","fontSizeSm":"14px","fontSizeLg":"20px","fontSizeXl":"24px","smallFontSize":"14px","customFonts":[{"source":"SERVER","name":"Segoe UI","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"},{"style":"NORMAL","weight":"300","__typename":"FontStyleData"},{"style":"NORMAL","weight":"600","__typename":"FontStyleData"},{"style":"NORMAL","weight":"700","__typename":"FontStyleData"},{"style":"ITALIC","weight":"400","__typename":"FontStyleData"}],"assetNames":["SegoeUI-normal-400.woff2","SegoeUI-normal-300.woff2","SegoeUI-normal-600.woff2","SegoeUI-normal-700.woff2","SegoeUI-italic-400.woff2"],"__typename":"CustomFont"},{"source":"SERVER","name":"MWF Fluent Icons","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"}],"assetNames":["MWFFluentIcons-normal-400.woff2"],"__typename":"CustomFont"}],"__typename":"TypographyThemeSettings"},"unstyledListItem":{"marginBottomSm":"5px","marginBottomMd":"10px","marginBottomLg":"15px","marginBottomXl":"20px","marginBottomXxl":"25px","__typename":"UnstyledListItemThemeSettings"},"yiq":{"light":"#ffffff","dark":"#000000","__typename":"YiqThemeSettings"},"colorLightness":{"primaryDark":0.36,"primaryLight":0.74,"primaryLighter":0.89,"primaryLightest":0.95,"infoDark":0.39,"infoLight":0.72,"infoLighter":0.85,"infoLightest":0.93,"successDark":0.24,"successLight":0.62,"successLighter":0.8,"successLightest":0.91,"warningDark":0.39,"warningLight":0.68,"warningLighter":0.84,"warningLightest":0.93,"dangerDark":0.41,"dangerLight":0.72,"dangerLighter":0.89,"dangerLightest":0.95,"__typename":"ColorLightnessThemeSettings"},"localOverride":false,"__typename":"Theme"},"localOverride":false},"CachedAsset:text:en_US-components/common/EmailVerification-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/common/EmailVerification-1745505309751","value":{"email.verification.title":"Email Verification Required","email.verification.message.update.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. To change your email, visit My Settings.","email.verification.message.resend.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. Resend email."},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-1745505309751","value":{"title":"Loading..."},"localOverride":false},"CachedAsset:quilt:o365.prod:pages/blogs/BlogMessagePage:board:FastTrackforAzureBlog-1745502712787":{"__typename":"CachedAsset","id":"quilt:o365.prod:pages/blogs/BlogMessagePage:board:FastTrackforAzureBlog-1745502712787","value":{"id":"BlogMessagePage","container":{"id":"Common","headerProps":{"backgroundImageProps":null,"backgroundColor":null,"addComponents":null,"removeComponents":["community.widget.bannerWidget"],"componentOrder":null,"__typename":"QuiltContainerSectionProps"},"headerComponentProps":{"community.widget.breadcrumbWidget":{"disableLastCrumbForDesktop":false}},"footerProps":null,"footerComponentProps":null,"items":[{"id":"blog-article","layout":"ONE_COLUMN","bgColor":null,"showTitle":null,"showDescription":null,"textPosition":null,"textColor":null,"sectionEditLevel":"LOCKED","bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"OneColumnQuiltSection","columnMap":{"main":[{"id":"blogs.widget.blogArticleWidget","className":"lia-blog-container","props":null,"__typename":"QuiltComponent"}],"__typename":"OneSectionColumns"}},{"id":"section-1729184836777","layout":"MAIN_SIDE","bgColor":"transparent","showTitle":false,"showDescription":false,"textPosition":"CENTER","textColor":"var(--lia-bs-body-color)","sectionEditLevel":null,"bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"MainSideQuiltSection","columnMap":{"main":[],"side":[],"__typename":"MainSideSectionColumns"}}],"__typename":"QuiltContainer"},"__typename":"Quilt","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-pages/blogs/BlogMessagePage-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-pages/blogs/BlogMessagePage-1745505309751","value":{"title":"{contextMessageSubject} | {communityTitle}","errorMissing":"This blog post cannot be found","name":"Blog Message Page","section.blog-article.title":"Blog Post","archivedMessageTitle":"This Content Has Been Archived","section.section-1729184836777.title":"","section.section-1729184836777.description":"","section.CncIde.title":"Blog Post","section.tifEmD.description":"","section.tifEmD.title":""},"localOverride":false},"CachedAsset:quiltWrapper:o365.prod:Common:1745505310860":{"__typename":"CachedAsset","id":"quiltWrapper:o365.prod:Common:1745505310860","value":{"id":"Common","header":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"community.widget.navbarWidget","props":{"showUserName":true,"showRegisterLink":true,"useIconLanguagePicker":true,"useLabelLanguagePicker":true,"className":"QuiltComponent_lia-component-edit-mode__0nCcm","links":{"sideLinks":[],"mainLinks":[{"children":[],"linkType":"INTERNAL","id":"gxcuf89792","params":{},"routeName":"CommunityPage"},{"children":[],"linkType":"EXTERNAL","id":"external-link","url":"/Directory","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft365","params":{"categoryId":"microsoft365"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows","params":{"categoryId":"Windows"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-microsoft365-copilot-link","params":{"categoryId":"Microsoft365Copilot"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-teams","params":{"categoryId":"MicrosoftTeams"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-securityand-compliance","params":{"categoryId":"microsoft-security"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"azure","params":{"categoryId":"Azure"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-content_management-link","params":{"categoryId":"Content_Management"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"exchange","params":{"categoryId":"Exchange"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows-server","params":{"categoryId":"Windows-Server"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"outlook","params":{"categoryId":"Outlook"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-endpoint-manager","params":{"categoryId":"microsoftintune"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-2","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities","url":"/","target":"BLANK"},{"children":[{"linkType":"INTERNAL","id":"a-i","params":{"categoryId":"AI"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"education-sector","params":{"categoryId":"EducationSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"partner-community","params":{"categoryId":"PartnerCommunity"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"i-t-ops-talk","params":{"categoryId":"ITOpsTalk"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"healthcare-and-life-sciences","params":{"categoryId":"HealthcareAndLifeSciences"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-mechanics","params":{"categoryId":"MicrosoftMechanics"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"public-sector","params":{"categoryId":"PublicSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"s-m-b","params":{"categoryId":"MicrosoftforNonprofits"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"io-t","params":{"categoryId":"IoT"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"startupsat-microsoft","params":{"categoryId":"StartupsatMicrosoft"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"driving-adoption","params":{"categoryId":"DrivingAdoption"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-1","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities-1","url":"/","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external","url":"/Blogs","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external-1","url":"/Events","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft-learn-1","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-learn-blog","params":{"boardId":"MicrosoftLearnBlog","categoryId":"MicrosoftLearn"},"routeName":"BlogBoardPage"},{"linkType":"EXTERNAL","id":"external-10","url":"https://learningroomdirectory.microsoft.com/","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-3","url":"https://docs.microsoft.com/learn/dynamics365/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-4","url":"https://docs.microsoft.com/learn/m365/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-5","url":"https://docs.microsoft.com/learn/topics/sci/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-6","url":"https://docs.microsoft.com/learn/powerplatform/?wt.mc_id=techcom_header-webpage-powerplatform","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-7","url":"https://docs.microsoft.com/learn/github/?wt.mc_id=techcom_header-webpage-github","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-8","url":"https://docs.microsoft.com/learn/teams/?wt.mc_id=techcom_header-webpage-teams","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-9","url":"https://docs.microsoft.com/learn/dotnet/?wt.mc_id=techcom_header-webpage-dotnet","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-2","url":"https://docs.microsoft.com/learn/azure/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"}],"linkType":"INTERNAL","id":"microsoft-learn","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"children":[],"linkType":"INTERNAL","id":"community-info-center","params":{"categoryId":"Community-Info-Center"},"routeName":"CategoryPage"}]},"style":{"boxShadow":"var(--lia-bs-box-shadow-sm)","controllerHighlightColor":"hsla(30, 100%, 50%)","linkFontWeight":"400","dropdownDividerMarginBottom":"10px","hamburgerBorderHover":"none","linkBoxShadowHover":"none","linkFontSize":"14px","backgroundOpacity":0.8,"controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerBgColor":"transparent","hamburgerColor":"var(--lia-nav-controller-icon-color)","linkTextBorderBottom":"none","brandLogoHeight":"30px","linkBgHoverColor":"transparent","linkLetterSpacing":"normal","collapseMenuDividerOpacity":0.16,"dropdownPaddingBottom":"15px","paddingBottom":"15px","dropdownMenuOffset":"2px","hamburgerBgHoverColor":"transparent","borderBottom":"1px solid var(--lia-bs-border-color)","hamburgerBorder":"none","dropdownPaddingX":"10px","brandMarginRightSm":"10px","linkBoxShadow":"none","collapseMenuDividerBg":"var(--lia-nav-link-color)","linkColor":"var(--lia-bs-body-color)","linkJustifyContent":"flex-start","dropdownPaddingTop":"10px","controllerHighlightTextColor":"var(--lia-yiq-dark)","controllerTextColor":"var(--lia-nav-controller-icon-color)","background":{"imageAssetName":"","color":"var(--lia-bs-white)","size":"COVER","repeat":"NO_REPEAT","position":"CENTER_CENTER","imageLastModified":""},"linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkHoverColor":"var(--lia-bs-body-color)","position":"FIXED","linkBorder":"none","linkTextBorderBottomHover":"2px solid var(--lia-bs-body-color)","brandMarginRight":"30px","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","linkBorderHover":"none","collapseMenuMarginLeft":"20px","linkFontStyle":"NORMAL","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","linkPaddingX":"10px","linkPaddingY":"5px","paddingTop":"15px","linkTextTransform":"NONE","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","linkBgColor":"transparent","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkDropdownPaddingY":"9px","controllerIconColor":"var(--lia-bs-body-color)","dropdownDividerMarginTop":"10px","linkGap":"10px","controllerIconHoverColor":"var(--lia-bs-body-color)"},"showSearchIcon":false,"languagePickerStyle":"iconAndLabel"},"__typename":"QuiltComponent"},{"id":"community.widget.breadcrumbWidget","props":{"backgroundColor":"transparent","linkHighlightColor":"var(--lia-bs-primary)","visualEffects":{"showBottomBorder":true},"linkTextColor":"var(--lia-bs-gray-700)"},"__typename":"QuiltComponent"},{"id":"custom.widget.community_banner","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"usePageWidth":false,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"},{"id":"custom.widget.HeroBanner","props":{"widgetVisibility":"signedInOrAnonymous","usePageWidth":false,"useTitle":true,"cMax_items":3,"useBackground":false,"title":"","lazyLoad":false,"widgetChooser":"custom.widget.HeroBanner"},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"footer":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"custom.widget.MicrosoftFooter","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"__typename":"QuiltWrapper","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-components/common/ActionFeedback-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/common/ActionFeedback-1745505309751","value":{"joinedGroupHub.title":"Welcome","joinedGroupHub.message":"You are now a member of this group and are subscribed to updates.","groupHubInviteNotFound.title":"Invitation Not Found","groupHubInviteNotFound.message":"Sorry, we could not find your invitation to the group. The owner may have canceled the invite.","groupHubNotFound.title":"Group Not Found","groupHubNotFound.message":"The grouphub you tried to join does not exist. It may have been deleted.","existingGroupHubMember.title":"Already Joined","existingGroupHubMember.message":"You are already a member of this group.","accountLocked.title":"Account Locked","accountLocked.message":"Your account has been locked due to multiple failed attempts. Try again in {lockoutTime} minutes.","editedGroupHub.title":"Changes Saved","editedGroupHub.message":"Your group has been updated.","leftGroupHub.title":"Goodbye","leftGroupHub.message":"You are no longer a member of this group and will not receive future updates.","deletedGroupHub.title":"Deleted","deletedGroupHub.message":"The group has been deleted.","groupHubCreated.title":"Group Created","groupHubCreated.message":"{groupHubName} is ready to use","accountClosed.title":"Account Closed","accountClosed.message":"The account has been closed and you will now be redirected to the homepage","resetTokenExpired.title":"Reset Password Link has Expired","resetTokenExpired.message":"Try resetting your password again","invalidUrl.title":"Invalid URL","invalidUrl.message":"The URL you're using is not recognized. Verify your URL and try again.","accountClosedForUser.title":"Account Closed","accountClosedForUser.message":"{userName}'s account is closed","inviteTokenInvalid.title":"Invitation Invalid","inviteTokenInvalid.message":"Your invitation to the community has been canceled or expired.","inviteTokenError.title":"Invitation Verification Failed","inviteTokenError.message":"The url you are utilizing is not recognized. Verify your URL and try again","pageNotFound.title":"Access Denied","pageNotFound.message":"You do not have access to this area of the community or it doesn't exist","eventAttending.title":"Responded as Attending","eventAttending.message":"You'll be notified when there's new activity and reminded as the event approaches","eventInterested.title":"Responded as Interested","eventInterested.message":"You'll be notified when there's new activity and reminded as the event approaches","eventNotFound.title":"Event Not Found","eventNotFound.message":"The event you tried to respond to does not exist.","redirectToRelatedPage.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.message":"The content you are trying to access is archived","redirectToRelatedPage.message":"The content you are trying to access is archived","relatedUrl.archivalLink.flyoutMessage":"The content you are trying to access is archived View Archived Content"},"localOverride":false},"CachedAsset:component:custom.widget.community_banner-en-1744400828201":{"__typename":"CachedAsset","id":"component:custom.widget.community_banner-en-1744400828201","value":{"component":{"id":"custom.widget.community_banner","template":{"id":"community_banner","markupLanguage":"HANDLEBARS","style":".community-banner {\n a.top-bar.btn {\n top: 0px;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0px;\n background: #0068b8;\n color: white;\n padding: 10px 0px;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0px !important;\n font-size: 14px;\n }\n}\n","texts":null,"defaults":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.community_banner","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_community_banner_community-banner_1x9u2_1 {\n a.custom_widget_community_banner_top-bar_1x9u2_2.custom_widget_community_banner_btn_1x9u2_2 {\n top: 0;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0;\n background: #0068b8;\n color: white;\n padding: 0.625rem 0;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0 !important;\n font-size: 0.875rem;\n }\n}\n","tokens":{"community-banner":"custom_widget_community_banner_community-banner_1x9u2_1","top-bar":"custom_widget_community_banner_top-bar_1x9u2_2","btn":"custom_widget_community_banner_btn_1x9u2_2"}},"form":null},"localOverride":false},"CachedAsset:component:custom.widget.HeroBanner-en-1744400828201":{"__typename":"CachedAsset","id":"component:custom.widget.HeroBanner-en-1744400828201","value":{"component":{"id":"custom.widget.HeroBanner","template":{"id":"HeroBanner","markupLanguage":"REACT","style":null,"texts":{"searchPlaceholderText":"Search this community","followActionText":"Follow","unfollowActionText":"Following","searchOnHoverText":"Please enter your search term(s) and then press return key to complete a search.","blogs.sidebar.pagetitle":"Latest Blogs | Microsoft Tech Community","followThisNode":"Follow this node","unfollowThisNode":"Unfollow this node"},"defaults":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.HeroBanner","form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"__typename":"Component","localOverride":false},"globalCss":null,"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"}},"localOverride":false},"CachedAsset:component:custom.widget.MicrosoftFooter-en-1744400828201":{"__typename":"CachedAsset","id":"component:custom.widget.MicrosoftFooter-en-1744400828201","value":{"component":{"id":"custom.widget.MicrosoftFooter","template":{"id":"MicrosoftFooter","markupLanguage":"HANDLEBARS","style":".context-uhf {\n min-width: 280px;\n font-size: 15px;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.c-uhff-link {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.c-uhff {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.c-uhff-nav {\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n .c-heading-4 {\n color: #616161;\n word-break: break-word;\n font-size: 15px;\n line-height: 20px;\n padding: 36px 0 4px;\n font-weight: 600;\n }\n .c-uhff-nav-row {\n .c-uhff-nav-group {\n display: block;\n float: left;\n min-height: 1px;\n vertical-align: text-top;\n padding: 0 12px;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.c-list.f-bare {\n font-size: 11px;\n line-height: 16px;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 8px 0;\n margin: 0;\n }\n }\n }\n }\n}\n.c-uhff-base {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 30px 5% 16px;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.c-uhff-ccpa {\n font-size: 11px;\n line-height: 16px;\n float: left;\n margin: 3px 0;\n }\n a.c-uhff-ccpa:hover {\n text-decoration: underline;\n }\n ul.c-list {\n font-size: 11px;\n line-height: 16px;\n float: right;\n margin: 3px 0;\n color: #616161;\n li {\n padding: 0 24px 4px 0;\n display: inline-block;\n }\n }\n .c-list.f-bare {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 30px 24px 16px;\n }\n}\n\n.social-share {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n\n.sharing-options {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 43px;\n border-radius: 0px 7px 7px 0px;\n}\n.linkedin-icon {\n border-top-right-radius: 7px;\n}\n.linkedin-icon:hover {\n border-radius: 0;\n}\n.social-share-rss-image {\n border-bottom-right-radius: 7px;\n}\n.social-share-rss-image:hover {\n border-radius: 0;\n}\n\n.social-link-footer {\n position: relative;\n display: block;\n margin: -2px 0;\n transition: all 0.2s ease;\n}\n.social-link-footer:hover .linkedin-icon {\n border-radius: 0;\n}\n.social-link-footer:hover .social-share-rss-image {\n border-radius: 0;\n}\n\n.social-link-footer img {\n width: 40px;\n height: auto;\n transition: filter 0.3s ease;\n}\n\n.social-share-list {\n width: 40px;\n}\n.social-share-rss-image {\n width: 40px;\n}\n\n.share-icon {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n\n.share-icon:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n\n.share-icon:hover .label {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n\n.label {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 10px;\n top: 50%;\n transform: translateY(-50%);\n height: 40px;\n border-radius: 0 6px 6px 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px 5px 20px 8px;\n margin-left: -1px;\n}\n.linkedin {\n background-color: #0474b4;\n}\n.facebook {\n background-color: #3c5c9c;\n}\n.twitter {\n background-color: white;\n color: black;\n}\n.reddit {\n background-color: #fc4404;\n}\n.mail {\n background-color: #848484;\n}\n.bluesky {\n background-color: white;\n color: black;\n}\n.rss {\n background-color: #ec7b1c;\n}\n#RSS {\n width: 40px;\n height: 40px;\n}\n\n@media (max-width: 991px) {\n .social-share {\n display: none;\n }\n}\n","texts":{"New tab":"What's New","New 1":"Surface Laptop Studio 2","New 2":"Surface Laptop Go 3","New 3":"Surface Pro 9","New 4":"Surface Laptop 5","New 5":"Surface Studio 2+","New 6":"Copilot in Windows","New 7":"Microsoft 365","New 8":"Windows 11 apps","Store tab":"Microsoft Store","Store 1":"Account Profile","Store 2":"Download Center","Store 3":"Microsoft Store Support","Store 4":"Returns","Store 5":"Order tracking","Store 6":"Certified Refurbished","Store 7":"Microsoft Store Promise","Store 8":"Flexible Payments","Education tab":"Education","Edu 1":"Microsoft in education","Edu 2":"Devices for education","Edu 3":"Microsoft Teams for Education","Edu 4":"Microsoft 365 Education","Edu 5":"How to buy for your school","Edu 6":"Educator Training and development","Edu 7":"Deals for students and parents","Edu 8":"Azure for students","Business tab":"Business","Bus 1":"Microsoft Cloud","Bus 2":"Microsoft Security","Bus 3":"Dynamics 365","Bus 4":"Microsoft 365","Bus 5":"Microsoft Power Platform","Bus 6":"Microsoft Teams","Bus 7":"Microsoft Industry","Bus 8":"Small Business","Developer tab":"Developer & IT","Dev 1":"Azure","Dev 2":"Developer Center","Dev 3":"Documentation","Dev 4":"Microsoft Learn","Dev 5":"Microsoft Tech Community","Dev 6":"Azure Marketplace","Dev 7":"AppSource","Dev 8":"Visual Studio","Company tab":"Company","Com 1":"Careers","Com 2":"About Microsoft","Com 3":"Company News","Com 4":"Privacy at Microsoft","Com 5":"Investors","Com 6":"Diversity and inclusion","Com 7":"Accessiblity","Com 8":"Sustainibility"},"defaults":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.MicrosoftFooter","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_MicrosoftFooter_context-uhf_105bp_1 {\n min-width: 17.5rem;\n font-size: 0.9375rem;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-link_105bp_12 {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff_105bp_12 {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35 {\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n .custom_widget_MicrosoftFooter_c-heading-4_105bp_49 {\n color: #616161;\n word-break: break-word;\n font-size: 0.9375rem;\n line-height: 1.25rem;\n padding: 2.25rem 0 0.25rem;\n font-weight: 600;\n }\n .custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57 {\n .custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58 {\n display: block;\n float: left;\n min-height: 0.0625rem;\n vertical-align: text-top;\n padding: 0 0.75rem;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 0.5rem 0;\n margin: 0;\n }\n }\n }\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff-base_105bp_94 {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 1.875rem 5% 1rem;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: left;\n margin: 0.1875rem 0;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107:hover {\n text-decoration: underline;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: right;\n margin: 0.1875rem 0;\n color: #616161;\n li {\n padding: 0 1.5rem 0.25rem 0;\n display: inline-block;\n }\n }\n .custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 1.875rem 1.5rem 1rem;\n }\n}\n.custom_widget_MicrosoftFooter_social-share_105bp_138 {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n.custom_widget_MicrosoftFooter_sharing-options_105bp_146 {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 2.6875rem;\n border-radius: 0 0.4375rem 0.4375rem 0;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-top-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-bottom-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 {\n position: relative;\n display: block;\n margin: -0.125rem 0;\n transition: all 0.2s ease;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 img {\n width: 2.5rem;\n height: auto;\n transition: filter 0.3s ease;\n}\n.custom_widget_MicrosoftFooter_social-share-list_105bp_188 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195 {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover .custom_widget_MicrosoftFooter_label_105bp_207 {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n.custom_widget_MicrosoftFooter_label_105bp_207 {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 0.625rem;\n top: 50%;\n transform: translateY(-50%);\n height: 2.5rem;\n border-radius: 0 0.375rem 0.375rem 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.25rem 0.3125rem 1.25rem 0.5rem;\n margin-left: -0.0625rem;\n}\n.custom_widget_MicrosoftFooter_linkedin_105bp_156 {\n background-color: #0474b4;\n}\n.custom_widget_MicrosoftFooter_facebook_105bp_237 {\n background-color: #3c5c9c;\n}\n.custom_widget_MicrosoftFooter_twitter_105bp_240 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_reddit_105bp_244 {\n background-color: #fc4404;\n}\n.custom_widget_MicrosoftFooter_mail_105bp_247 {\n background-color: #848484;\n}\n.custom_widget_MicrosoftFooter_bluesky_105bp_250 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_rss_105bp_254 {\n background-color: #ec7b1c;\n}\n#custom_widget_MicrosoftFooter_RSS_105bp_1 {\n width: 2.5rem;\n height: 2.5rem;\n}\n@media (max-width: 991px) {\n .custom_widget_MicrosoftFooter_social-share_105bp_138 {\n display: none;\n }\n}\n","tokens":{"context-uhf":"custom_widget_MicrosoftFooter_context-uhf_105bp_1","c-uhff-link":"custom_widget_MicrosoftFooter_c-uhff-link_105bp_12","c-uhff":"custom_widget_MicrosoftFooter_c-uhff_105bp_12","c-uhff-nav":"custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35","c-heading-4":"custom_widget_MicrosoftFooter_c-heading-4_105bp_49","c-uhff-nav-row":"custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57","c-uhff-nav-group":"custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58","c-list":"custom_widget_MicrosoftFooter_c-list_105bp_78","f-bare":"custom_widget_MicrosoftFooter_f-bare_105bp_78","c-uhff-base":"custom_widget_MicrosoftFooter_c-uhff-base_105bp_94","c-uhff-ccpa":"custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107","social-share":"custom_widget_MicrosoftFooter_social-share_105bp_138","sharing-options":"custom_widget_MicrosoftFooter_sharing-options_105bp_146","linkedin-icon":"custom_widget_MicrosoftFooter_linkedin-icon_105bp_156","social-share-rss-image":"custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162","social-link-footer":"custom_widget_MicrosoftFooter_social-link-footer_105bp_169","social-share-list":"custom_widget_MicrosoftFooter_social-share-list_105bp_188","share-icon":"custom_widget_MicrosoftFooter_share-icon_105bp_195","label":"custom_widget_MicrosoftFooter_label_105bp_207","linkedin":"custom_widget_MicrosoftFooter_linkedin_105bp_156","facebook":"custom_widget_MicrosoftFooter_facebook_105bp_237","twitter":"custom_widget_MicrosoftFooter_twitter_105bp_240","reddit":"custom_widget_MicrosoftFooter_reddit_105bp_244","mail":"custom_widget_MicrosoftFooter_mail_105bp_247","bluesky":"custom_widget_MicrosoftFooter_bluesky_105bp_250","rss":"custom_widget_MicrosoftFooter_rss_105bp_254","RSS":"custom_widget_MicrosoftFooter_RSS_105bp_1"}},"form":null},"localOverride":false},"CachedAsset:text:en_US-components/community/Breadcrumb-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/Breadcrumb-1745505309751","value":{"navLabel":"Breadcrumbs","dropdown":"Additional parent page navigation"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBanner-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBanner-1745505309751","value":{"messageMarkedAsSpam":"This post has been marked as spam","messageMarkedAsSpam@board:TKB":"This article has been marked as spam","messageMarkedAsSpam@board:BLOG":"This post has been marked as spam","messageMarkedAsSpam@board:FORUM":"This discussion has been marked as spam","messageMarkedAsSpam@board:OCCASION":"This event has been marked as spam","messageMarkedAsSpam@board:IDEA":"This idea has been marked as spam","manageSpam":"Manage Spam","messageMarkedAsAbuse":"This post has been marked as abuse","messageMarkedAsAbuse@board:TKB":"This article has been marked as abuse","messageMarkedAsAbuse@board:BLOG":"This post has been marked as abuse","messageMarkedAsAbuse@board:FORUM":"This discussion has been marked as abuse","messageMarkedAsAbuse@board:OCCASION":"This event has been marked as abuse","messageMarkedAsAbuse@board:IDEA":"This idea has been marked as abuse","preModCommentAuthorText":"This comment will be published as soon as it is approved","preModCommentModeratorText":"This comment is awaiting moderation","messageMarkedAsOther":"This post has been rejected due to other reasons","messageMarkedAsOther@board:TKB":"This article has been rejected due to other reasons","messageMarkedAsOther@board:BLOG":"This post has been rejected due to other reasons","messageMarkedAsOther@board:FORUM":"This discussion has been rejected due to other reasons","messageMarkedAsOther@board:OCCASION":"This event has been rejected due to other reasons","messageMarkedAsOther@board:IDEA":"This idea has been rejected due to other reasons","messageArchived":"This post was archived on {date}","relatedUrl":"View Related Content","relatedContentText":"Showing related content","archivedContentLink":"View Archived Content"},"localOverride":false},"Category:category:Exchange":{"__typename":"Category","id":"category:Exchange","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Outlook":{"__typename":"Category","id":"category:Outlook","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Community-Info-Center":{"__typename":"Category","id":"category:Community-Info-Center","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:EducationSector":{"__typename":"Category","id":"category:EducationSector","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:DrivingAdoption":{"__typename":"Category","id":"category:DrivingAdoption","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Azure":{"__typename":"Category","id":"category:Azure","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows-Server":{"__typename":"Category","id":"category:Windows-Server","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftTeams":{"__typename":"Category","id":"category:MicrosoftTeams","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PublicSector":{"__typename":"Category","id":"category:PublicSector","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft365":{"__typename":"Category","id":"category:microsoft365","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:IoT":{"__typename":"Category","id":"category:IoT","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:HealthcareAndLifeSciences":{"__typename":"Category","id":"category:HealthcareAndLifeSciences","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:ITOpsTalk":{"__typename":"Category","id":"category:ITOpsTalk","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftLearn":{"__typename":"Category","id":"category:MicrosoftLearn","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Blog:board:MicrosoftLearnBlog":{"__typename":"Blog","id":"board:MicrosoftLearnBlog","blogPolicies":{"__typename":"BlogPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}},"boardPolicies":{"__typename":"BoardPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:AI":{"__typename":"Category","id":"category:AI","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftMechanics":{"__typename":"Category","id":"category:MicrosoftMechanics","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftforNonprofits":{"__typename":"Category","id":"category:MicrosoftforNonprofits","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:StartupsatMicrosoft":{"__typename":"Category","id":"category:StartupsatMicrosoft","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PartnerCommunity":{"__typename":"Category","id":"category:PartnerCommunity","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Microsoft365Copilot":{"__typename":"Category","id":"category:Microsoft365Copilot","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows":{"__typename":"Category","id":"category:Windows","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Content_Management":{"__typename":"Category","id":"category:Content_Management","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft-security":{"__typename":"Category","id":"category:microsoft-security","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoftintune":{"__typename":"Category","id":"category:microsoftintune","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"QueryVariables:TopicReplyList:message:4024070:7":{"__typename":"QueryVariables","id":"TopicReplyList:message:4024070:7","value":{"id":"message:4024070","first":10,"sorts":{"postTime":{"direction":"DESC"}},"repliesFirst":3,"repliesFirstDepthThree":1,"repliesSorts":{"postTime":{"direction":"DESC"}},"useAvatar":true,"useAuthorLogin":true,"useAuthorRank":true,"useBody":true,"useKudosCount":true,"useTimeToRead":false,"useMedia":false,"useReadOnlyIcon":false,"useRepliesCount":true,"useSearchSnippet":false,"useAcceptedSolutionButton":false,"useSolvedBadge":false,"useAttachments":false,"attachmentsFirst":5,"useTags":true,"useNodeAncestors":false,"useUserHoverCard":false,"useNodeHoverCard":false,"useModerationStatus":true,"usePreviewSubjectModal":false,"useMessageStatus":true}},"ROOT_MUTATION":{"__typename":"Mutation"},"CachedAsset:text:en_US-components/community/Navbar-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/Navbar-1745505309751","value":{"community":"Community Home","inbox":"Inbox","manageContent":"Manage Content","tos":"Terms of Service","forgotPassword":"Forgot Password","themeEditor":"Theme Editor","edit":"Edit Navigation Bar","skipContent":"Skip to content","gxcuf89792":"Tech Community","external-1":"Events","s-m-b":"Nonprofit Community","windows-server":"Windows Server","education-sector":"Education Sector","driving-adoption":"Driving Adoption","Common-content_management-link":"Content Management","microsoft-learn":"Microsoft Learn","s-q-l-server":"Content Management","partner-community":"Microsoft Partner Community","microsoft365":"Microsoft 365","external-9":".NET","external-8":"Teams","external-7":"Github","products-services":"Products","external-6":"Power Platform","communities-1":"Topics","external-5":"Microsoft Security","planner":"Outlook","external-4":"Microsoft 365","external-3":"Dynamics 365","azure":"Azure","healthcare-and-life-sciences":"Healthcare and Life Sciences","external-2":"Azure","microsoft-mechanics":"Microsoft Mechanics","microsoft-learn-1":"Community","external-10":"Learning Room Directory","microsoft-learn-blog":"Blog","windows":"Windows","i-t-ops-talk":"ITOps Talk","external-link-1":"View All","microsoft-securityand-compliance":"Microsoft Security","public-sector":"Public Sector","community-info-center":"Lounge","external-link-2":"View All","microsoft-teams":"Microsoft Teams","external":"Blogs","microsoft-endpoint-manager":"Microsoft Intune","startupsat-microsoft":"Startups at Microsoft","exchange":"Exchange","a-i":"AI and Machine Learning","io-t":"Internet of Things (IoT)","Common-microsoft365-copilot-link":"Microsoft 365 Copilot","outlook":"Microsoft 365 Copilot","external-link":"Community Hubs","communities":"Products"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarHamburgerDropdown-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarHamburgerDropdown-1745505309751","value":{"hamburgerLabel":"Side Menu"},"localOverride":false},"CachedAsset:text:en_US-components/community/BrandLogo-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/BrandLogo-1745505309751","value":{"logoAlt":"Khoros","themeLogoAlt":"Brand Logo"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarTextLinks-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarTextLinks-1745505309751","value":{"more":"More"},"localOverride":false},"CachedAsset:text:en_US-components/authentication/AuthenticationLink-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/authentication/AuthenticationLink-1745505309751","value":{"title.login":"Sign In","title.registration":"Register","title.forgotPassword":"Forgot Password","title.multiAuthLogin":"Sign In"},"localOverride":false},"CachedAsset:text:en_US-components/nodes/NodeLink-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/nodes/NodeLink-1745505309751","value":{"place":"Place {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageView/MessageViewStandard-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageView/MessageViewStandard-1745505309751","value":{"anonymous":"Anonymous","author":"{messageAuthorLogin}","authorBy":"{messageAuthorLogin}","board":"{messageBoardTitle}","replyToUser":" to {parentAuthor}","showMoreReplies":"Show More","replyText":"Reply","repliesText":"Replies","markedAsSolved":"Marked as Solved","movedMessagePlaceholder.BLOG":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.TKB":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.FORUM":"{count, plural, =0 {This reply has been} other {These replies have been} }","movedMessagePlaceholder.IDEA":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.OCCASION":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholderUrlText":"moved.","messageStatus":"Status: ","statusChanged":"Status changed: {previousStatus} to {currentStatus}","statusAdded":"Status added: {status}","statusRemoved":"Status removed: {status}","labelExpand":"expand replies","labelCollapse":"collapse replies","unhelpfulReason.reason1":"Content is outdated","unhelpfulReason.reason2":"Article is missing information","unhelpfulReason.reason3":"Content is for a different Product","unhelpfulReason.reason4":"Doesn't match what I was searching for"},"localOverride":false},"CachedAsset:text:en_US-components/messages/ThreadedReplyList-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/ThreadedReplyList-1745505309751","value":{"title":"{count, plural, one{# Reply} other{# Replies}}","title@board:BLOG":"{count, plural, one{# Comment} other{# Comments}}","title@board:TKB":"{count, plural, one{# Comment} other{# Comments}}","title@board:IDEA":"{count, plural, one{# Comment} other{# Comments}}","title@board:OCCASION":"{count, plural, one{# Comment} other{# Comments}}","noRepliesTitle":"No Replies","noRepliesTitle@board:BLOG":"No Comments","noRepliesTitle@board:TKB":"No Comments","noRepliesTitle@board:IDEA":"No Comments","noRepliesTitle@board:OCCASION":"No Comments","noRepliesDescription":"Be the first to reply","noRepliesDescription@board:BLOG":"Be the first to comment","noRepliesDescription@board:TKB":"Be the first to comment","noRepliesDescription@board:IDEA":"Be the first to comment","noRepliesDescription@board:OCCASION":"Be the first to comment","messageReadOnlyAlert:BLOG":"Comments have been turned off for this post","messageReadOnlyAlert:TKB":"Comments have been turned off for this article","messageReadOnlyAlert:IDEA":"Comments have been turned off for this idea","messageReadOnlyAlert:FORUM":"Replies have been turned off for this discussion","messageReadOnlyAlert:OCCASION":"Comments have been turned off for this event"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyCallToAction-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyCallToAction-1745505309751","value":{"leaveReply":"Leave a reply...","leaveReply@board:BLOG@message:root":"Leave a comment...","leaveReply@board:TKB@message:root":"Leave a comment...","leaveReply@board:IDEA@message:root":"Leave a comment...","leaveReply@board:OCCASION@message:root":"Leave a comment...","repliesTurnedOff.FORUM":"Replies are turned off for this topic","repliesTurnedOff.BLOG":"Comments are turned off for this topic","repliesTurnedOff.TKB":"Comments are turned off for this topic","repliesTurnedOff.IDEA":"Comments are turned off for this topic","repliesTurnedOff.OCCASION":"Comments are turned off for this topic","infoText":"Stop poking me!"},"localOverride":false},"ModerationData:moderation_data:4122360":{"__typename":"ModerationData","id":"moderation_data:4122360","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:4122360":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:988334"},"id":"message:4122360","revisionNum":1,"uid":4122360,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:4024070"},"conversation":{"__ref":"Conversation:conversation:4024070"},"subject":"Re: Create an Azure OpenAI, LangChain, ChromaDB, and Chainlit chat app in AKS using Terraform","moderationData":{"__ref":"ModerationData:moderation_data:4122360"},"body":"
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"213","kudosSumWeight":1,"repliesCount":0,"postTime":"2024-04-24T04:58:06.855-07:00","lastPublishTime":"2024-04-24T04:58:06.855-07:00","metrics":{"__typename":"MessageMetrics","views":3211},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:4024070/message:4122360","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"Rank:rank:37":{"__typename":"Rank","id":"rank:37","position":18,"name":"Copper Contributor","color":"333333","icon":null,"rankStyle":"TEXT"},"User:user:2437950":{"__typename":"User","id":"user:2437950","uid":2437950,"login":"Nchakik","biography":null,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2024-04-24T04:47:19.355-07:00"},"deleted":false,"email":"","avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/m_assets/avatars/default/avatar-8.svg?time=0"},"rank":{"__ref":"Rank:rank:37"},"entityType":"USER","eventPath":"community:gxcuf89792/user:2437950"},"ModerationData:moderation_data:4122356":{"__typename":"ModerationData","id":"moderation_data:4122356","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:4122356":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:2437950"},"id":"message:4122356","revisionNum":1,"uid":4122356,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:4024070"},"conversation":{"__ref":"Conversation:conversation:4024070"},"subject":"Re: Create an Azure OpenAI, LangChain, ChromaDB, and Chainlit chat app in AKS using Terraform","moderationData":{"__ref":"ModerationData:moderation_data:4122356"},"body":"
Hi, it'is not a better option to use Azure Container Apps instead of AKS ?
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"82","kudosSumWeight":0,"repliesCount":0,"postTime":"2024-04-24T04:51:35.015-07:00","lastPublishTime":"2024-04-24T04:51:35.015-07:00","metrics":{"__typename":"MessageMetrics","views":3214},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:4024070/message:4122356","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"ModerationData:moderation_data:4028301":{"__typename":"ModerationData","id":"moderation_data:4028301","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:4028301":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:988334"},"id":"message:4028301","revisionNum":1,"uid":4028301,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:4024070"},"conversation":{"__ref":"Conversation:conversation:4024070"},"subject":"Re: Create an Azure OpenAI, LangChain, ChromaDB, and Chainlit chat app in AKS using Terraform","moderationData":{"__ref":"ModerationData:moderation_data:4028301"},"body":"
Hi Ajmal_Yazdani, this is really an interesting question! I checked the documentation at:
and I could not find any reference to how to configure the application routing addon to deploy a manager NGINX ingress controller where the service is exposed via an internal load balancer. In this case, I suggest that you disable the installation of the managed NGINX ingress controller via the application routing addon by setting the http_application_routing_enabled Terraform variable to false. Then, modify the the nginx_ingress_controller.tf module under terraform/modules/kubernetes add the following parameter:
I didn't test it, but this should configure the NGINX ingress controller installed via Helm chart to expose the service via an internal load balancer. Hope this helps.
P.S. If you liked the article, please give a star also to the GitHub project, thanks
\n
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"208","kudosSumWeight":0,"repliesCount":0,"postTime":"2024-01-12T09:13:55.657-08:00","lastPublishTime":"2024-01-12T09:13:55.657-08:00","metrics":{"__typename":"MessageMetrics","views":11044},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:4024070/message:4028301","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"User:user:2247115":{"__typename":"User","id":"user:2247115","uid":2247115,"login":"Ajmal_Yazdani","biography":null,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2024-01-12T03:23:51.794-08:00"},"deleted":false,"email":"","avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/m_assets/avatars/default/avatar-6.svg?time=0"},"rank":{"__ref":"Rank:rank:37"},"entityType":"USER","eventPath":"community:gxcuf89792/user:2247115"},"ModerationData:moderation_data:4028226":{"__typename":"ModerationData","id":"moderation_data:4028226","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"BlogReplyMessage:message:4028226":{"__typename":"BlogReplyMessage","author":{"__ref":"User:user:2247115"},"id":"message:4028226","revisionNum":1,"uid":4028226,"depth":1,"hasGivenKudo":false,"subscribed":false,"board":{"__ref":"Blog:board:FastTrackforAzureBlog"},"parent":{"__ref":"BlogTopicMessage:message:4024070"},"conversation":{"__ref":"Conversation:conversation:4024070"},"subject":"Re: Create an Azure OpenAI, LangChain, ChromaDB, and Chainlit chat app in AKS using Terraform","moderationData":{"__ref":"ModerationData:moderation_data:4028226"},"body":"
Thanks a lots for sharing the full content article, tons of things here to learn.
In my Azure infra Public IP is not allowed, hence while trying to install Managed NGINX Ingress Controller using below command:
az aks approuting enable -g ResourceGroupName -n ClusterName
NGINX Ingress failing to pick public Load balancer IP and it's failing.
Can we do this with internal load balancer? Please suggest!
","body@stripHtml({\"removeProcessingText\":false,\"removeSpoilerMarkup\":false,\"removeTocMarkup\":false,\"truncateLength\":200})@stringLength":"228","kudosSumWeight":1,"repliesCount":0,"postTime":"2024-01-12T08:00:54.753-08:00","lastPublishTime":"2024-01-12T08:00:54.753-08:00","metrics":{"__typename":"MessageMetrics","views":11079},"visibilityScope":"PUBLIC","placeholder":false,"originalMessageForPlaceholder":null,"entityType":"BLOG_REPLY","eventPath":"category:FastTrack/category:products-services/category:communities/community:gxcuf89792board:FastTrackforAzureBlog/message:4024070/message:4028226","replies":{"__typename":"MessageConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"customFields":[],"attachments":{"__typename":"AttachmentConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}}},"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarDropdownToggle-1745505309751","value":{"ariaLabelClosed":"Press the down arrow to open the menu"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/QueryHandler-1745505309751","value":{"title":"Query Handler"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCoverImage-1745505309751","value":{"coverImageTitle":"Cover Image"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeTitle-1745505309751","value":{"nodeTitle":"{nodeTitle, select, community {Community} other {{nodeTitle}}} "},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTimeToRead-1745505309751","value":{"minReadText":"{min} MIN READ"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageSubject-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageSubject-1745505309751","value":{"noSubject":"(no subject)"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserLink-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserLink-1745505309751","value":{"authorName":"View Profile: {author}","anonymous":"Anonymous"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserRank-1745505309751","value":{"rankName":"{rankName}","userRank":"Author rank {rankName}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTime-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTime-1745505309751","value":{"postTime":"Published: {time}","lastPublishTime":"Last Update: {time}","conversation.lastPostingActivityTime":"Last posting activity time: {time}","conversation.lastPostTime":"Last post time: {time}","moderationData.rejectTime":"Rejected time: {time}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBody-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBody-1745505309751","value":{"showMessageBody":"Show More","mentionsErrorTitle":"{mentionsType, select, board {Board} user {User} message {Message} other {}} No Longer Available","mentionsErrorMessage":"The {mentionsType} you are trying to view has been removed from the community.","videoProcessing":"Video is being processed. Please try again in a few minutes.","bannerTitle":"Video provider requires cookies to play the video. Accept to continue or {url} it directly on the provider's site.","buttonTitle":"Accept","urlText":"watch"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCustomFields-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCustomFields-1745505309751","value":{"CustomField.default.label":"Value of {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageRevision-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageRevision-1745505309751","value":{"lastUpdatedDatePublished":"{publishCount, plural, one{Published} other{Updated}} {date}","lastUpdatedDateDraft":"Created {date}","version":"Version {major}.{minor}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyButton-1745505309751","value":{"repliesCount":"{count}","title":"Reply","title@board:BLOG@message:root":"Comment","title@board:TKB@message:root":"Comment","title@board:IDEA@message:root":"Comment","title@board:OCCASION@message:root":"Comment"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageAuthorBio-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageAuthorBio-1745505309751","value":{"sendMessage":"Send Message","actionMessage":"Follow this blog board to get notified when there's new activity","coAuthor":"CO-PUBLISHER","contributor":"CONTRIBUTOR","userProfile":"View Profile","iconlink":"Go to {name} {type}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserAvatar-1745505309751","value":{"altText":"{login}'s avatar","altTextGeneric":"User's avatar"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/ranks/UserRankLabel-1745505309751","value":{"altTitle":"Icon for {rankName} rank"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserRegistrationDate-1745505309751","value":{"noPrefix":"{date}","withPrefix":"Joined {date}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeAvatar-1745505309751","value":{"altTitle":"Node avatar for {nodeTitle}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeDescription-1745505309751","value":{"description":"{description}"},"localOverride":false},"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-components/tags/TagView/TagViewChip-1745505309751","value":{"tagLabelName":"Tag name {tagName}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Pager/PagerLoadMore-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Pager/PagerLoadMore-1745505309751","value":{"loadMore":"Show More"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505309751":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeIcon-1745505309751","value":{"contentType":"Content Type {style, select, FORUM {Forum} BLOG {Blog} TKB {Knowledge Base} IDEA {Ideas} OCCASION {Events} other {}} icon"},"localOverride":false}}}},"page":"/blogs/BlogMessagePage/BlogMessagePage","query":{"boardId":"fasttrackforazureblog","messageSubject":"create-an-azure-openai-langchain-chromadb-and-chainlit-chat-app-in-aks-using-ter","messageId":"4024070"},"buildId":"HEhyUrv5OXNBIbfCLaOrw","runtimeConfig":{"buildInformationVisible":false,"logLevelApp":"info","logLevelMetrics":"info","openTelemetryClientEnabled":false,"openTelemetryConfigName":"o365","openTelemetryServiceVersion":"25.1.0","openTelemetryUniverse":"prod","openTelemetryCollector":"http://localhost:4318","openTelemetryRouteChangeAllowedTime":"5000","apolloDevToolsEnabled":false,"inboxMuteWipFeatureEnabled":false},"isFallback":false,"isExperimentalCompile":false,"dynamicIds":["./components/community/Navbar/NavbarWidget.tsx","./components/community/Breadcrumb/BreadcrumbWidget.tsx","./components/customComponent/CustomComponent/CustomComponent.tsx","./components/blogs/BlogArticleWidget/BlogArticleWidget.tsx","./components/external/components/ExternalComponent.tsx","./components/messages/MessageView/MessageViewStandard/MessageViewStandard.tsx","./components/messages/ThreadedReplyList/ThreadedReplyList.tsx","../shared/client/components/common/List/UnstyledList/UnstyledList.tsx","./components/messages/MessageView/MessageView.tsx","../shared/client/components/common/List/UnwrappedList/UnwrappedList.tsx","./components/tags/TagView/TagView.tsx","./components/tags/TagView/TagViewChip/TagViewChip.tsx","../shared/client/components/common/Pager/PagerLoadMore/PagerLoadMore.tsx"],"appGip":true,"scriptLoader":[{"id":"analytics","src":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/pagescripts/1730819800000/analytics.js?page.id=BlogMessagePage&entity.id=board%3Afasttrackforazureblog&entity.id=message%3A4024070","strategy":"afterInteractive"}]}