Contact centers are the front lines of customer interaction, generating vast amounts of valuable data through chat logs, call transcripts, and emails. However, manually sifting through this data to find actionable insights is often a monumental task. Imagine the scenario of a thriving online service, like a food delivery app: as usage climbs, so does the number of customer support chats, making it incredibly difficult to pinpoint recurring problems or gauge overall satisfaction from the sea of text. How can businesses effectively tap into this wealth of information?
This post explores a powerful solution: building an automated analytics platform using Azure Communication Services (ACS) combined with the intelligence of Azure OpenAI Service. We'll outline how this integration allows businesses to process, understand, and derive actionable insights from their contact center conversations at scale, transforming raw data into a strategic asset.
(Why This Azure Stack for Analytics?)
Combining ACS and Azure OpenAI offers a compelling approach for contact center analytics:
Azure Communication Services (ACS): Provides a comprehensive cloud-based platform. In terms of analytics, it serves as the central hub for chat interactions or where transcripts are ingested and stored within organized threads, ready for programmatic access via its SDKs.
Azure OpenAI Service: Offers secure access to advanced large language models (such as GPT-4). These models excel in understanding context, summarizing extensive texts, extracting key information, and analyzing sentiment, making them ideal for processing unstructured conversation data automatically.
What We'll Cover:
Setting up Azure OpenAI and Azure Communication Services.
Finding and configuring necessary credentials (Endpoints, Keys, Connection Strings).
Setting up your streamlit environment.
Writing Python code to retrieve chat messages from ACS.
Crafting prompts to instruct Azure OpenAI for analysis (summary, topic, sentiment).
Calling the Azure OpenAI API.
Processing the AI-generated insights.
2. Architecture
2.1. Data Flow:
The user interacts with the Streamlit application.
When instructed (e.g., by a button click or automatically), the Streamlit application uses the ACS SDK to fetch chat messages from relevant threads within the Azure Communication Services.
The Streamlit application prepares the chat data and formulates prompts based on the desired analysis (e.g., summarize, extract topics, analyze sentiment).
These prompts and the chat data are sent to the Azure OpenAI Service.
Azure OpenAI processes the information using its LLMs and returns the analysis results (summary, topics, sentiment) to the Streamlit application.
The Streamlit application receives the analysis results and formats them for display in the user interface, alongside the original chat messages.
2.2. Prerequisites
Before you begin, ensure you have the following:
Python: Version 3.9 or later installed.
Azure Account: An active Azure subscription. If you're new, you can create a free account with credits.
Azure OpenAI Access: Your Azure subscription must have access enabled for Azure OpenAI Service. You can request access here: https://aka.ms/oaiapply
Code Editor/Environment: An IDE like VS Code
3. Setting Up Azure Resources
We need two core Azure services: Azure OpenAI (for the AI model) and Azure Communication Services (to access chat data).
3.1 Azure OpenAI Service
Requirement: You need an existing Azure OpenAI resource and a deployed model (like gpt-35-turbo, gpt-4, gpt-4o, gpt-4o).
Collect Credentials: Once deployed, navigate to your Azure OpenAI resource in the Azure Portal. Go to the "Keys and Endpoint" section. You will need to copy:
One of the API Keys (AZURE_OPENAI_API_KEY)
The Endpoint URL (AZURE_OPENAI_ENDPOINT)
You also need the Deployment Name you gave your model (AZURE_OPENAI_DEPLOYMENT_NAME).
Note the API Version you intend to use (e.g., 2024-05-01-preview).
3.2 Azure Communication Services (ACS)
This service will store or provide access to the chat threads we want to analyze.
Action: Create the ACS Resource
Log in to the Azure Portal.
In the top search bar, type Communication Services and select it from the results.
On the Communication Services page, click the "+ Create" button. (Alternatively: Click "+ Create a resource", search for "Communication Services" in the Marketplace, select it, and click "Create").
Once you click on Create, you get the option to select from the list of services. Select Communication Services from the dropdown, as shown here:
Once you land on the Communication Services page, you need to click on the Create button, as shown here:
Once you’ve successfully created the Communication Service from the Marketplace, move on to the next step. On the Create resource form, choose the subscription and resource group that you set up back
Tags Tab (Optional): You can add tags for organization or billing purposes, but it's not required for this tutorial. Click "Next: Review + create".
Review + create Tab: Azure will validate your settings. Once validation passes, review the details and click the "Create" button.
Wait for the deployment to complete. You'll see a notification when it's done.
Action: Find ACS Credentials
Once deployed, click "Go to resource" or find your newly created Communication Services resource via the search bar or resource groups.
In the resource menu (left-hand side), under "Settings", click on "Keys".
On the Keys page, you will find:
The Endpoint URL.
Two Connection Strings (Primary and Secondary). You only need one.
Copy the Endpoint URL and one of the Connection String values. Keep these secure alongside your Azure OpenAI credentials.
4. Implementing the Streamlit Application Code
Now, we'll build the core of our application. Create a Python file named app.py (or your preferred name) in your project folder. We will add the code in logical sections.
4.1 Imports and Configuration Loading
First, we import all necessary libraries and load the configuration securely from our .env file. We also define some constants for sender names.
# --- Start of Streamlit App Code (app.py) ---
import streamlit as st
from azure.communication.chat import (
ChatClient, CommunicationTokenCredential, ChatMessageType,
ChatParticipant,
ChatThreadClient
)
from azure.communication.identity import CommunicationIdentityClient, CommunicationUserIdentifier
from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
from datetime import datetime, timedelta
import openai
import re
import os
import time
from dotenv import load_dotenv # Import dotenv
# --- Load Environment Variables ---
load_dotenv() # Load variables from .env file at the start
# --- Configuration ---
# Load from environment with error
checking
AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
AZURE_API_VERSION = os.getenv("AZURE_API_VERSION",'2024-05-01-preview') # Provide
a default API version if needed
AZURE_OPENAI_DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") # Your
model deployment name
COMMUNICATION_CONNECTION_STRING = os.getenv("COMMUNICATION_CONNECTION_STRING")
COMMUNICATION_ENDPOINT = os.getenv("COMMUNICATION_ENDPOINT")
# --- Early check for critical
environment variables ---
# Stop execution if essential configs
are missing to prevent errors later
if not all([AZURE_OPENAI_API_KEY,
AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,
COMMUNICATION_CONNECTION_STRING, COMMUNICATION_ENDPOINT]):
# Use st.error which is visible
in the Streamlit app
st.error("❌ Critical environment variables are missing! Please ensure AZURE_OPENAI_API_KEY,
AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,
COMMUNICATION_CONNECTION_STRING, and COMMUNICATION_ENDPOINT are set in your
.env file or system environment.")
st.stop() # Halt script execution if
configuration is incomplete
# --- Application Specific Settings ---
TARGET_THREAD_TOPIC = "Streamlit ACS Interactive Demo V3" # Name for
the ACS chat thread
# --- Constants for Sender Display Names
---
SENDER_USER = "User
(via Streamlit)" # Display name for messages sent by the human user
SENDER_AI_AGENT = "AI Agent" #
Display name for messages generated by OpenAI
SENDER_SYSTEM = "System" # Display name for automated system messages
# --- Configure OpenAI library (using
v0.28.1 syntax) ---
# This configures the openai library
globally for this script run
openai.api_type = "azure"
openai.api_key = AZURE_OPENAI_API_KEY
openai.api_base = AZURE_OPENAI_ENDPOINT
openai.api_version = AZURE_API_VERSION
Explanation: This section imports necessary modules from Streamlit, Azure SDKs, OpenAI, and standard Python libraries. It uses dotenv to load credentials stored in the.env file into environment variables. A crucial check is added using st.error and st.stop() to halt the app gracefully if essential configuration values are missing. Finally, it defines constants for sender display names and configures the openai library to point to your Azure OpenAI resource using the loaded credentials (note this uses syntax for openai library version 0.x).
4.2 Helper Function: Parsing the AI Summary
This function takes the raw text output from the OpenAI summarization call and extracts structured information using regular expressions.
# --- Helper Function for Parsing Summary ---
def parse_summary(text):
""" Parses the
AI response for summary sections using regex. """
parsed = {
"Topic": "Not
found", "Summary": "Not
found",
"Highlights": "Not
found", "Sentiment": "Not
found"
}
if not text:
parsed["Error"] = "AI
returned no text to parse."
return
parsed
try:
# Regex patterns look for
Markdown headings (## Heading)
# re.DOTALL makes '.' match
newline characters within sections
topic_match = re.search(r"##\s*Topic\s*\n(.*?)(?=\n##\s*(Summary|Highlights|Sentiment)|$)", text, re.IGNORECASE | re.DOTALL)
if
topic_match: parsed["Topic"] = topic_match.group(1).strip()
summary_match = re.search(r"##\s*Summary\s*\n(.*?)(?=\n##\s*(Highlights|Sentiment)|$)", text, re.IGNORECASE | re.DOTALL)
if
summary_match: parsed["Summary"] = summary_match.group(1).strip()
highlights_match = re.search(r"##\s*Highlights\s*\n(.*?)(?=\n##\s*Sentiment|$)", text, re.IGNORECASE | re.DOTALL)
if
highlights_match: parsed["Highlights"] = highlights_match.group(1).strip()
sentiment_match = re.search(r"##\s*Sentiment\s*\n(.*)", text, re.IGNORECASE | re.DOTALL)
if
sentiment_match: parsed["Sentiment"] = sentiment_match.group(1).strip()
# Check if parsing failed
completely or partially
if all(v == "Not found" for k, v in
parsed.items() if
k not in ["RawOutput","Error", "Warning"]):
parsed["Error"] = "Could
not parse specific sections. Displaying raw output."
parsed["RawOutput"] = text # Include
raw output if parsing fails
elif any(v == "Not found" for k, v in
parsed.items() if
k not in ["RawOutput","Error", "Warning"]):
parsed["Warning"] = "AI
response formatting inconsistent; some sections might be missing."
except
Exception as
e:
st.exception(f"Error during summary
parsing: {e}") # Log full
error
parsed["Error"] = f"An
error occurred during parsing. Raw response:\n{text}"
parsed["RawOutput"] = text
parsed["Topic"], parsed["Summary"], parsed["Highlights"], parsed["Sentiment"] = "Error", "Error", "Error", "Error"
return
parsed
Explanation: The parse_summary function takes the text generated by the AI summarization prompt. It uses regular expressions (re.search) to find sections starting with markdown headings like ## Topic. It extracts the content under each heading and stores it in a dictionary. It includes error handling for cases where the AI output doesn't match the expected format, returning an Error or Warning key in the dictionary along with the raw output if possible.
4.3 Helper Function: Initializing Azure Clients and ACS Thread
This is a critical function that sets up connections to Azure services and creates the specific chat thread for the application session. It's decorated with st.cache_resource to ensure it only runs once per session, improving performance and preventing accidental resource recreation.
# --- Initialize Clients and Thread (Cached Resource)
---
@st.cache_resource # IMPORTANT: Caches the returned objects for the
session
def initialize_azure_clients_and_thread():
"""
Initializes Azure OpenAI & ACS clients, creates the chat thread for
this session.
Returns a dictionary containing clients, thread_id, and error status.
Cached by Streamlit to run only once per session unless cache is
cleared.
"""
initialization_details = {
"chat_client": None, "chat_thread_client": None,
"thread_id": None, "error": None
}
try:
# --- (Re)Configure OpenAI
library within function scope if needed, ---
# --- though global config
might suffice if not changing keys ---
# openai.api_key =
AZURE_OPENAI_API_KEY
# ... (rest of openai config)
...
# --- ACS Setup: Identity
Client, User, Token ---
st.sidebar.text("Initializing
ACS Identity...")
identity_client =
CommunicationIdentityClient.from_connection_string(COMMUNICATION_CONNECTION_STRING)
user_streamlit = identity_client.create_user()
token_streamlit = identity_client.get_token(user_streamlit, ["chat"])
st.sidebar.text("ACS
Identity Ready.")
# --- Define Participant for
the Thread ---
participant_streamlit_user = ChatParticipant(
identifier=user_streamlit,
display_name=SENDER_USER,
share_history_time=datetime.utcnow() - timedelta(days=1)
)
participants = [participant_streamlit_user]
# --- Create base ACS Chat
Client ---
st.sidebar.text("Initializing
ACS Chat Client...")
chat_client_streamlit = ChatClient(COMMUNICATION_ENDPOINT,
CommunicationTokenCredential(token_streamlit.token))
initialization_details["chat_client"] = chat_client_streamlit
st.sidebar.text("ACS
Chat Client Ready.")
# --- Create the ACS Chat
Thread ---
thread_id = None
st.sidebar.text(f"Creating
ACS Thread ('{TARGET_THREAD_TOPIC}')...")
try:
create_thread_result =
chat_client_streamlit.create_chat_thread(
topic=TARGET_THREAD_TOPIC,
thread_participants=participants
)
thread_id =
create_thread_result.chat_thread.id
initialization_details["thread_id"] = thread_id
st.sidebar.success(f"Thread Created:\nID: {thread_id[:12]}...", icon="✅")
#
--- Get Thread-Specific Client ---
chat_thread_client_temp =
chat_client_streamlit.get_chat_thread_client(thread_id)
#
--- Send Initial Messages ---
st.sidebar.text("Sending initial messages...")
try:
chat_thread_client_temp.send_message(f"Chat
thread '{TARGET_THREAD_TOPIC}' started.",
sender_display_name=SENDER_SYSTEM)
time.sleep(0.5) # Small delay helps messages appear in order
chat_thread_client_temp.send_message("Hello!
I am your AI assistant. How can I help?",
sender_display_name=SENDER_AI_AGENT)
st.sidebar.info("Initial messages sent.", icon="✉️")
except Exception as msg_err:
st.sidebar.warning(f"Could not send initial messages: {msg_err}", icon="⚠️")
# --- Handle Thread Creation
Errors ---
except
ResourceExistsError:
st.sidebar.warning("Thread creation conflict
(ResourceExistsError).", icon="⚠️")
initialization_details["error"] = "Failed - Thread conflict."
return initialization_details
except
Exception as
e:
initialization_details["error"] = f"Error during thread creation: {e}"
st.exception(e)
return initialization_details
if not
thread_id:
initialization_details["error"] = "Thread ID is missing after
creation attempt."
return initialization_details
# --- Store the final
ChatThreadClient ---
chat_thread_client =
chat_client_streamlit.get_chat_thread_client(thread_id)
initialization_details["chat_thread_client"] = chat_thread_client
st.sidebar.text("Initialization
Complete.")
return
initialization_details
#
--- Handle Overall Initialization Errors ---
except
Exception as
e:
st.error("❌ Failed during Azure
client initialization process.")
st.exception(e)
initialization_details["error"] = f"Failed
overall initialization: {e}"
return
initialization_details
Explanation: This function performs the critical setup on the first run of the session.
st.cache_resource: Makes Streamlit store the returned dictionary (initialization_details) and reuse it on subsequent script reruns within the same user session, avoiding costly re-creation of clients and threads unless the cache is explicitly cleared (e.g., by the "Clear Chat" button).
ACS Identity: Connects to ACS, creates a unique user identity and a temporary chat access token for this specific app session.
ACS Chat Client: Creates the main client object used to interact with the ACS chat service (e.g., creating/deleting threads).
Thread Creation: Attempts to create a new chat thread in ACS with a defined topic and adds the app user as a participant. It handles potential errors if a thread creation conflicts.
Initial Messages: Sends introductory messages from "System" and "AI Agent" to the newly created thread.
Thread Client: Gets the specific ChatThreadClient needed to interact with messages within the created thread.
Return Value: Returns a dictionary containing the base client, the thread-specific client, the thread ID, and any error message encountered. Sidebar messages provide visual feedback during this process.
4.4 Helper Function: Getting the AI Agent's Response
This function encapsulates the logic for generating the AI's conversational reply.
# --- Helper Function to get AI chat response ---
def get_ai_agent_response(thread_client:
ChatThreadClient):
"""Fetches
history from ACS, formats it, calls OpenAI, returns AI response
text."""
if not
thread_client:
st.error("Cannot get AI response:
ChatThreadClient is not initialized.")
return "[Error: Chat client not available]"
try:
# 1. Fetch recent message
history from ACS thread
messages_for_prompt = []
history_limit = 20 # Limit
context window
message_iterator =
thread_client.list_messages(results_per_page=history_limit)
acs_messages = list(message_iterator)
acs_messages.reverse() # Process
oldest first for context
# 2. Format messages into
OpenAI ChatCompletion structure
for
msg in
acs_messages:
sender_name =
msg.sender_display_name
content = msg.content.message if
msg.content and
msg.content.message else ""
if not content: continue # Skip
empty messages
#
Map sender to OpenAI role (user/assistant)
if sender_name == SENDER_AI_AGENT: role = "assistant"
elif sender_name == SENDER_USER: role = "user"
else: role = "user"; content = f"[{sender_name}]: {content}" # Treat
others as user context
messages_for_prompt.append({"role":
role, "content": content})
if not
messages_for_prompt: # Should
not happen after init, but safeguard
return "Hello!
How may I assist you?"
# 3. Define AI's persona via
system message
system_message = {"role": "system", "content": "You
are a helpful and concise AI Agent providing support. Respond naturally to the
user's last message based on the conversation history."}
openai_messages = [system_message] + messages_for_prompt
# 4. Call Azure OpenAI API
response = openai.ChatCompletion.create(
engine=AZURE_OPENAI_DEPLOYMENT_NAME,
messages=openai_messages,
max_tokens=150,
temperature=0.7
)
ai_response_text = response['choices'][0]['message']['content']
return
ai_response_text.strip()
except
Exception as
e:
st.error("Error getting AI
response:")
st.exception(e) # Log full
error
return "[Error occurred while generating AI response.
Please check logs.]"
Explanation: This function is called whenever the AI needs to generate a reply. It fetches the recent conversation history from the ACS thread using the provided thread_client, formats it into the [{role: 'user'/'assistant', content: '...'}] structure required by the OpenAI Chat Completion API, adds a system message to guide the AI's behavior, calls the API, and returns the AI's generated text response.
4.5 Streamlit App Structure and Initialization
This sets up the main page configuration and title and calls the initialization function.
# --- Streamlit App ---
st.set_page_config(layout="wide",
page_title="AI Chat & Summary
(ACS + OpenAI)")
st.title("💬
Interactive AI Agent Chat & Summarizer")
st.markdown(f"Using
**Azure Communication Services** & **Azure OpenAI**. Date: {datetime.now().strftime('%Y-%m-%d')}")
st.info("Type
a message, click 'Send'. AI Agent responds. Use sidebar to clear/reset. Use
summary button for analysis.")
# --- Initialize Clients and Thread ---
# This calls the cached function defined
earlier
init_details =
initialize_azure_clients_and_thread()
chat_client = init_details.get("chat_client")
chat_thread_client = init_details.get("chat_thread_client")
thread_id = init_details.get("thread_id")
init_error = init_details.get("error")
# --- Sidebar ---
st.sidebar.title("Controls")
# Sidebar button logic to clear chat
if
st.sidebar.button("🧹 Clear Chat
& Start New Thread"):
if
chat_client and
thread_id:
with
st.spinner("Clearing chat
thread..."):
try:
print(f"Attempting
to delete thread: {thread_id}") # Server log
chat_client.delete_chat_thread(thread_id=thread_id)
st.sidebar.success("Previous thread deleted.")
except ResourceNotFoundError:
st.sidebar.warning("Thread already deleted or not found.")
except Exception as e:
st.sidebar.error(f"Error deleting thread: {e}")
st.exception(e)
#
Clear state, cache, and rerun
st.session_state.clear()
initialize_azure_clients_and_thread.clear() #
Clear cache
st.rerun() #
Restart script execution
else:
st.sidebar.error("Cannot
clear chat - client/thread not initialized.")
# --- Initialize Session State Variables
(Safety check) ---
# Ensures these keys exist after
potential clearing
if 'current_summary' not in
st.session_state: st.session_state.current_summary = None
if 'messages' not in
st.session_state: st.session_state.messages = []
if 'send_button_clicked' not in
st.session_state: st.session_state.send_button_clicked = False
Explanation: This section configures the web page (title, layout), displays introductory text, and then calls the cached initialize_azure_clients_and_thread function to get the necessary client objects and thread ID. It defines the sidebar, including the "Clear Chat" button which deletes the ACS thread, clears session state and the resource cache, then forces a rerun. Finally, it ensures essential keys exist in Streamlit's session_state dictionary.
4.6 Main Application Logic and UI
This is the core part that runs only if the initialization is successful. It defines the layout and handles the interactive elements.
# --- Main Application Logic ---
# Only proceed if initialization was
successful (clients/thread ready)
if
chat_thread_client and not
init_error:
# Define layout using columns
(Chat on left, Summary on right)
col1, col2 = st.columns([0.6, 0.4])
# --- Column 1: Live Chat
Interface ---
with
col1:
st.subheader(f"💬 Live Chat: {TARGET_THREAD_TOPIC}")
st.markdown("---")
# --- Display Chat Messages ---
message_container = st.container(height=600) #
Scrollable container
try:
#
Fetch latest messages from ACS on every script run
chat_messages_iterator =
chat_thread_client.list_messages(results_per_page=50)
live_messages = list(chat_messages_iterator)
#
Store messages in session state for potential use by summary function
st.session_state.messages =
live_messages
#
Render messages within the container
with message_container:
if not live_messages:
st.info("No messages yet. Send a message to start!")
else:
#
Display messages, newest at the bottom
for msg in reversed(live_messages):
sender_name =
msg.sender_display_name or "Unknown"
# Assign avatars based on sender name constants
if
sender_name == SENDER_AI_AGENT: avatar = "🤖"
elif
sender_name == SENDER_USER: avatar = "👤"
elif
sender_name == SENDER_SYSTEM: avatar = "⚙️"
else: avatar ="🧑💻" # Default
# Use Streamlit's chat elements for display
with
st.chat_message(name=sender_name, avatar=avatar):
st.caption(f"{msg.created_on.strftime('%Y-%m-%d %H:%M:%S')}") # Timestamp
st.write(msg.content.message or "*empty
message*") #
Message content
except
Exception as
e: # Handle errors fetching messages
with message_container:
st.error("Error fetching messages from ACS:")
st.exception(e)
st.markdown("---") #
Separator
# --- Send Message Input Form
---
# Use a form to group input and
button click
with
st.form(key="send_message_form", clear_on_submit=True):
new_message_content =
st.text_input("Your Message:", key="new_message_input", placeholder="Type
your message here...")
submitted = st.form_submit_button("Send Message")
if submitted:
st.session_state.send_button_clicked = True # Flag
submission
# --- Process message sending
(Runs after form check) ---
if
st.session_state.send_button_clicked:
st.session_state.send_button_clicked = False # Reset
flag
if new_message_content: #
Check if user actually typed something
#
Show spinner during processing
with st.spinner("Sending
message and getting AI response..."):
try:
# 1. Send User message to ACS
chat_thread_client.send_message(new_message_content,
sender_display_name=SENDER_USER)
# 2. Get AI response (calls helper function)
ai_reply =
get_ai_agent_response(chat_thread_client)
# 3. Send AI response back to ACS
if ai_reply:
chat_thread_client.send_message(ai_reply, sender_display_name=SENDER_AI_AGENT)
# 4. Rerun script to refresh chat display
st.rerun()
except Exception as e: # Handle
errors during send/response cycle
st.error("Error during send/response cycle:")
st.exception(e)
else:
st.warning("Please enter a message to send.")
# --- Column 2: AI Summary Area
---
with
col2:
st.subheader("✨ Conversation Summary")
st.markdown("---")
summary_error_message = None #
Placeholder for specific summary errors
# --- Generate Summary Button
and Logic ---
if
st.button("Generate Full
Conversation Summary", key="generate_summary_btn"):
st.session_state.summary_processing = True # Flag
processing
st.session_state.current_summary =
{"Info":"Processing..."} # Show
status
#
Use messages already fetched and stored in session state
fetched_messages =
st.session_state.get('messages', [])
if fetched_messages:
prompt_text = ""
#
Build prompt text from stored messages (oldest first)
for msg in reversed(fetched_messages):
#
... (build prompt_text, skipping empty content) ...
sender_name =
msg.sender_display_name if msg.sender_display_name else "Unknown"
content =
msg.content.message if
msg.content and
msg.content.message else ""
if content: prompt_text += f"{sender_name}: {content}\n"
if not prompt_text.strip():
st.session_state.current_summary = {"Info": "No
message content to summarize."}
else:
#
Prepare prompt with instructions
instruction_phrase = 'Summarize the following conversation. Extract the
main topic, a brief summary, key highlights (1-3 bullet points), and overall
sentiment. Format clearly with headings: ## Topic, ## Summary, ## Highlights,
## Sentiment.\n\n'
full_prompt =
instruction_phrase + prompt_text.strip()
#
Call OpenAI within a spinner and try/except block
with st.spinner("Generating
summary..."):
try:
response =
openai.ChatCompletion.create( # Or
Completion API
engine=AZURE_OPENAI_DEPLOYMENT_NAME,
messages=[{"role":"user","content": full_prompt}],
max_tokens=1024,
temperature=0.2
)
summary_text =
response['choices'][0]['message']['content']
# Parse and store result
st.session_state.current_summary = parse_summary(summary_text)
# Handle various OpenAI/other errors
except
openai.error.AuthenticationError as e: ... # Set
Error state
# ... other except blocks ...
except Exceptionas
e: ... # Set Error state
else: # No
messages fetched/stored
st.session_state.current_summary = {"Info": "Empty
conversation history."}
st.session_state.summary_processing = False # Unflag
processing
st.rerun() #
Rerun to update the summary display area
# --- Display Summary (or
Status) ---
# Checks the state variable and
displays Error, Warning, Info, or the parsed summary
if
st.session_state.get('current_summary'):
summary_data =
st.session_state.current_summary
#
--- Handle Error State ---
if isinstance(summary_data, dict) and "Error" in summary_data: ... #
Display st.error(...)
#
--- Handle Warning State ---
elif isinstance(summary_data, dict) and "Warning" in summary_data: ... #
Display st.warning(...) and partial data
#
--- Handle Info State ---
elif isinstance(summary_data, dict) and "Info" in summary_data: ... #
Display st.info(...)
#
--- Handle Successful Summary Display ---
elif isinstance(summary_data, dict): ... # Display
formatted markdown sections
#
--- Fallback ---
else: st.error("Summary
data unexpected format.")
# --- Default Placeholder Text
---
elif not
st.session_state.get('summary_processing', False):
st.info("Click
'Generate Summary' for an analysis...")
# --- Handle Initialization Errors ---
# This runs if the main 'if
chat_thread_client...' block is False
elif
init_error:
st.error(f"❌ Application cannot
start due to initialization errors:")
st.error(init_error) # Display
the error caught during initialization
else:
# Fallback if client is None
but no specific error was caught
st.warning("⏳ Initializing Azure
services or waiting for resources...")
# --- End of Streamlit App Code ---
Explanation: This is the main part of the application that runs if the initial Azure setup was successful.
Layout: It defines a two-column layout (st.columns).
Column 1 (Chat):
Fetches messages from the ACS thread on every rerun using chat_thread_client.list_messages().
Stores these messages in st.session_state.messages (primarily for the summary function's use).
Displays the messages chronologically in a scrollable container using st.chat_message.
Provides a text input and send button within a st.form.
Handles the form submission by sending the user's message, getting the AI response via the helper function, sending the AI response, and triggering a st.rerun to refresh the display.
Column 2 (Summary):
Includes the "Generate Full Conversation Summary" button.
When clicked, it takes the messages stored in session state, creates a summarization prompt, calls Azure OpenAI, parses the result using parse_summary, and stores the output (or error/info) in st.session_state.current_summary.
It then conditionally displays the summary information (Topic, Summary, Highlights, Sentiment) or any errors/warnings/info messages based on the content of st.session_state.current_summary.
Initialization Error Handling: The final elif/else catches cases where the chat_thread_client wasn't successfully obtained during startup and displays relevant error or waiting messages.
5. Running the Streamlit Application
With your Azure resources configured, the Python environment set up, and the code saved in app.py (or your chosen filename), you are ready to run the interactive application.
5.1. Run the App:
Open your terminal or command prompt, make sure you are in your project directory (where app.py and .env are located), and execute the command:
streamlit run app.py
5.2. Initial Loading State:
Streamlit will start its server and provide you with a local URL (usually http://localhost:8501). When you open this URL in your web browser, you will first see the application's initial loading state, similar to this image:
The page displays the title "Interactive AI Agent Chat (V3)" and the initial informational text.
You'll notice a spinner element with text like "Running: initialize_azure_clients_and_thread()". This indicates that the application is currently performing the one-time setup for the session.
During this phase (which might take a few moments, especially on the very first run or after clearing the chat), the cached function initialize_azure_clients_and_thread is executing. It connects to Azure Communication Services, creates a user identity and token, establishes a connection to Azure OpenAI, and creates the specific chat thread in ACS, sending the initial messages.
5.3. Interact with the App:
Once the initialization is complete, the spinner will disappear.
The main chat interface will load, displaying the initial "System" and "AI Agent" messages fetched from the newly created ACS thread.
Type your messages into the input box at the bottom of the chat column and click "Send Message". You will see your message appear, followed by a response from the "AI Agent" after a brief processing time (indicated by a spinner).
Navigate to the right-hand column and click "Generate Full Conversation Summary" anytime to get an AI-powered analysis of the conversation so far.
Use the "Clear Chat & Start New Thread" button in the sidebar to delete the current conversation history (by deleting the ACS thread) and start a fresh session.
5.4. Review Output:
The AI's chat responses appear directly in the chat interface. The generated summary appears in the right-hand column after you click the button. Any errors during operation should be displayed within the Streamlit interface. You can also check the terminal where you ran the streamlit run command for any server-side print statements or error logs.
6. Conclusion
You've now walked through the process of setting up Azure resources and using Streamlit to build a contact center chat analysis solution! By leveraging Azure Communication Services to access chat data and Azure OpenAI Service for intelligent analysis, you can automate the extraction of valuable insights from user or customer interactions, moving from overwhelming data volume to clear, actionable intelligence.
"}},"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\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/community/NavbarDropdownToggle\"]})":[{"__ref":"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/ranks/UserRankLabel\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserRegistrationDate\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeDescription\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505307000"}],"cachedText({\"lastModified\":\"1745505307000\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeIcon\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505307000"}]},"Theme:customTheme1":{"__typename":"Theme","id":"customTheme1"},"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":null,"possibleValues":["en-US","es-ES"]},"repliesSortOrder":{"__typename":"InheritableStringSettingWithPossibleValues","key":"config.user_replies_sort_order","value":"DEFAULT","localValue":"DEFAULT","possibleValues":["DEFAULT","LIKES","PUBLISH_TIME","REVERSE_PUBLISH_TIME"]}},"deleted":false},"CachedAsset:pages-1747123677058":{"__typename":"CachedAsset","id":"pages-1747123677058","value":[{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogViewAllPostsPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId/all-posts/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CasePortalPage","type":"CASE_PORTAL","urlPath":"/caseportal","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CreateGroupHubPage","type":"GROUP_HUB","urlPath":"/groups/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CaseViewPage","type":"CASE_DETAILS","urlPath":"/case/:caseId/:caseNumber","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"InboxPage","type":"COMMUNITY","urlPath":"/inbox","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"HelpFAQPage","type":"COMMUNITY","urlPath":"/help","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaMessagePage","type":"IDEA_POST","urlPath":"/idea/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaViewAllIdeasPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/all-ideas/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"LoginPage","type":"USER","urlPath":"/signin","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogPostPage","type":"BLOG","urlPath":"/category/:categoryId/blogs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"UserBlogPermissions.Page","type":"COMMUNITY","urlPath":"/c/user-blog-permissions/page","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ThemeEditorPage","type":"COMMUNITY","urlPath":"/designer/themes","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbViewAllArticlesPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId/all-articles/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"AllEvents","type":"CUSTOM","urlPath":"/Events","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"OccasionEditPage","type":"EVENT","urlPath":"/event/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"OAuthAuthorizationAllowPage","type":"USER","urlPath":"/auth/authorize/allow","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"PageEditorPage","type":"COMMUNITY","urlPath":"/designer/pages","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"PostPage","type":"COMMUNITY","urlPath":"/category/:categoryId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumBoardPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbBoardPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"EventPostPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"UserBadgesPage","type":"COMMUNITY","urlPath":"/users/:login/:userId/badges","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"GroupHubMembershipAction","type":"GROUP_HUB","urlPath":"/membership/join/:nodeId/:membershipType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"MaintenancePage","type":"COMMUNITY","urlPath":"/maintenance","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaReplyPage","type":"IDEA_REPLY","urlPath":"/idea/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"UserSettingsPage","type":"USER","urlPath":"/mysettings/:userSettingsTab","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"GroupHubsPage","type":"GROUP_HUB","urlPath":"/groups","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumPostPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"OccasionRsvpActionPage","type":"OCCASION","urlPath":"/event/:boardId/:messageSubject/:messageId/rsvp/:responseType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"VerifyUserEmailPage","type":"USER","urlPath":"/verifyemail/:userId/:verifyEmailToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"AllOccasionsPage","type":"OCCASION","urlPath":"/category/:categoryId/events/:boardId/all-events/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"EventBoardPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbReplyPage","type":"TKB_REPLY","urlPath":"/kb/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaBoardPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CommunityGuideLinesPage","type":"COMMUNITY","urlPath":"/communityguidelines","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CaseCreatePage","type":"SALESFORCE_CASE_CREATION","urlPath":"/caseportal/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbEditPage","type":"TKB","urlPath":"/kb/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForgotPasswordPage","type":"USER","urlPath":"/forgotpassword","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaEditPage","type":"IDEA","urlPath":"/idea/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TagPage","type":"COMMUNITY","urlPath":"/tag/:tagName","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogBoardPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"OccasionMessagePage","type":"OCCASION_TOPIC","urlPath":"/event/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ManageContentPage","type":"COMMUNITY","urlPath":"/managecontent","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ClosedMembershipNodeNonMembersPage","type":"GROUP_HUB","urlPath":"/closedgroup/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CommunityPage","type":"COMMUNITY","urlPath":"/","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumMessagePage","type":"FORUM_TOPIC","urlPath":"/discussions/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"IdeaPostPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"CommunityHub.Page","type":"CUSTOM","urlPath":"/Directory","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogMessagePage","type":"BLOG_ARTICLE","urlPath":"/blog/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"RegistrationPage","type":"USER","urlPath":"/register","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"EditGroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumEditPage","type":"FORUM","urlPath":"/discussions/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ResetPasswordPage","type":"USER","urlPath":"/resetpassword/:userId/:resetPasswordToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730819800000,"localOverride":null,"page":{"id":"AllBlogs.Page","type":"CUSTOM","urlPath":"/blogs","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbMessagePage","type":"TKB_ARTICLE","urlPath":"/kb/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogEditPage","type":"BLOG","urlPath":"/blog/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ManageUsersPage","type":"USER","urlPath":"/users/manage/:tab?/:manageUsersTab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumReplyPage","type":"FORUM_REPLY","urlPath":"/discussions/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"PrivacyPolicyPage","type":"COMMUNITY","urlPath":"/privacypolicy","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"NotificationPage","type":"COMMUNITY","urlPath":"/notifications","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"UserPage","type":"USER","urlPath":"/users/:login/:userId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"HealthCheckPage","type":"COMMUNITY","urlPath":"/health","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"OccasionReplyPage","type":"OCCASION_REPLY","urlPath":"/event/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ManageMembersPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/manage/:tab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"SearchResultsPage","type":"COMMUNITY","urlPath":"/search","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"BlogReplyPage","type":"BLOG_REPLY","urlPath":"/blog/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"GroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TermsOfServicePage","type":"COMMUNITY","urlPath":"/termsofservice","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"CategoryPage","type":"CATEGORY","urlPath":"/category/:categoryId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"ForumViewAllTopicsPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/all-topics/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"localOverride":null,"page":{"id":"TkbPostPage","type":"TKB","urlPath":"/category/:categoryId/kbs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1747123677058,"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}","userBanned":"We're sorry, but you have been banned from using this site.","userBannedReason":"You have been banned for the following reason: {reason}"},"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},"Rank:rank:37":{"__typename":"Rank","id":"rank:37","position":18,"name":"Copper Contributor","color":"333333","icon":null,"rankStyle":"TEXT"},"User:user:2612220":{"__typename":"User","id":"user:2612220","uid":2612220,"login":"PascalBurume08","deleted":false,"avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/m_assets/avatars/default/avatar-12.svg?time=0"},"rank":{"__ref":"Rank:rank:37"},"email":"","messagesCount":3,"biography":null,"topicsCount":3,"kudosReceivedCount":4,"kudosGivenCount":0,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2024-08-06T23:21:15.969-07:00","confirmEmailStatus":null},"followersCount":null,"solutionsCount":0},"Category:category:EducationSector":{"__typename":"Category","id":"category:EducationSector","entityType":"CATEGORY","displayId":"EducationSector","nodeType":"category","depth":3,"title":"Education Sector","shortTitle":"Education Sector","parent":{"__ref":"Category:category:solutions"},"categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:top":{"__typename":"Category","id":"category:top","entityType":"CATEGORY","displayId":"top","nodeType":"category","depth":0,"title":"Top","shortTitle":"Top"},"Category:category:communities":{"__typename":"Category","id":"category:communities","entityType":"CATEGORY","displayId":"communities","nodeType":"category","depth":1,"parent":{"__ref":"Category:category:top"},"title":"Communities","shortTitle":"Communities"},"Category:category:solutions":{"__typename":"Category","id":"category:solutions","entityType":"CATEGORY","displayId":"solutions","nodeType":"category","depth":2,"parent":{"__ref":"Category:category:communities"},"title":"Topics","shortTitle":"Topics"},"Blog:board:EducatorDeveloperBlog":{"__typename":"Blog","id":"board:EducatorDeveloperBlog","entityType":"BLOG","displayId":"EducatorDeveloperBlog","nodeType":"board","depth":4,"conversationStyle":"BLOG","repliesProperties":{"__typename":"RepliesProperties","sortOrder":"REVERSE_PUBLISH_TIME","repliesFormat":"threaded"},"tagProperties":{"__typename":"TagNodeProperties","tagsEnabled":{"__typename":"PolicyResult","failureReason":null}},"requireTags":false,"tagType":"FREEFORM_ONLY","description":"","title":"Educator Developer Blog","shortTitle":"Educator Developer Blog","parent":{"__ref":"Category:category:EducationSector"},"ancestors":{"__typename":"CoreNodeConnection","edges":[{"__typename":"CoreNodeEdge","node":{"__ref":"Community:community:gxcuf89792"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:communities"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:solutions"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:EducationSector"}}]},"userContext":{"__typename":"NodeUserContext","canAddAttachments":false,"canUpdateNode":false,"canPostMessages":false,"isSubscribed":false},"theme":{"__ref":"Theme:customTheme1"},"boardPolicies":{"__typename":"BoardPolicies","canViewSpamDashBoard":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.feature.moderation_spam.action.access_spam_quarantine.allowed.accessDenied","key":"error.lithium.policies.feature.moderation_spam.action.access_spam_quarantine.allowed.accessDenied","args":[]}},"canArchiveMessage":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.content_archivals.enable_content_archival_settings.accessDenied","key":"error.lithium.policies.content_archivals.enable_content_archival_settings.accessDenied","args":[]}},"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":[]}}}},"BlogTopicMessage:message:4403710":{"__typename":"BlogTopicMessage","uid":4403710,"subject":"Step-by-Step Contact Center Chat Analysis with Azure OpenAI & Communication Services","id":"message:4403710","revisionNum":9,"repliesCount":0,"author":{"__ref":"User:user:2612220"},"depth":0,"hasGivenKudo":false,"board":{"__ref":"Blog:board:EducatorDeveloperBlog"},"conversation":{"__ref":"Conversation:conversation:4403710"},"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:4403710"},"teaser":"","body":"
\n
1. Introduction
\n
Contact centers are the front lines of customer interaction, generating vast amounts of valuable data through chat logs, call transcripts, and emails. However, manually sifting through this data to find actionable insights is often a monumental task. Imagine the scenario of a thriving online service, like a food delivery app: as usage climbs, so does the number of customer support chats, making it incredibly difficult to pinpoint recurring problems or gauge overall satisfaction from the sea of text. How can businesses effectively tap into this wealth of information?
\n
This post explores a powerful solution: building an automated analytics platform using Azure Communication Services (ACS) combined with the intelligence of Azure OpenAI Service. We'll outline how this integration allows businesses to process, understand, and derive actionable insights from their contact center conversations at scale, transforming raw data into a strategic asset.
\n
(Why This Azure Stack for Analytics?)
\n
Combining ACS and Azure OpenAI offers a compelling approach for contact center analytics:
\n
\n
Azure Communication Services (ACS): Provides a comprehensive cloud-based platform. In terms of analytics, it serves as the central hub for chat interactions or where transcripts are ingested and stored within organized threads, ready for programmatic access via its SDKs.
\n
Azure OpenAI Service: Offers secure access to advanced large language models (such as GPT-4). These models excel in understanding context, summarizing extensive texts, extracting key information, and analyzing sentiment, making them ideal for processing unstructured conversation data automatically.
\n
\n
What We'll Cover:
\n
\n
Setting up Azure OpenAI and Azure Communication Services.
\n
Finding and configuring necessary credentials (Endpoints, Keys, Connection Strings).
\n
Setting up your streamlit environment.
\n
Writing Python code to retrieve chat messages from ACS.
\n
Crafting prompts to instruct Azure OpenAI for analysis (summary, topic, sentiment).
\n
Calling the Azure OpenAI API.
\n
Processing the AI-generated insights.
\n
\n
2. Architecture
\n\n
2.1. Data Flow:
\n\n
The user interacts with the Streamlit application.
\n
When instructed (e.g., by a button click or automatically), the Streamlit application uses the ACS SDK to fetch chat messages from relevant threads within the Azure Communication Services.
\n
The Streamlit application prepares the chat data and formulates prompts based on the desired analysis (e.g., summarize, extract topics, analyze sentiment).
\n
These prompts and the chat data are sent to the Azure OpenAI Service.
\n
Azure OpenAI processes the information using its LLMs and returns the analysis results (summary, topics, sentiment) to the Streamlit application.
\n
The Streamlit application receives the analysis results and formats them for display in the user interface, alongside the original chat messages.
\n\n
2.2. Prerequisites
\n
Before you begin, ensure you have the following:
\n
\n
Python: Version 3.9 or later installed.
\n
Azure Account: An active Azure subscription. If you're new, you can create a free account with credits.
\n
Azure OpenAI Access: Your Azure subscription must have access enabled for Azure OpenAI Service. You can request access here: https://aka.ms/oaiapply
\n
Code Editor/Environment: An IDE like VS Code
\n
\n
3. Setting Up Azure Resources
\n
We need two core Azure services: Azure OpenAI (for the AI model) and Azure Communication Services (to access chat data).
\n
3.1 Azure OpenAI Service
\n
\n
Requirement: You need an existing Azure OpenAI resource and a deployed model (like gpt-35-turbo, gpt-4, gpt-4o, gpt-4o).
Collect Credentials: Once deployed, navigate to your Azure OpenAI resource in the Azure Portal. Go to the \"Keys and Endpoint\" section. You will need to copy:\n
\n
One of the API Keys (AZURE_OPENAI_API_KEY)
\n
The Endpoint URL (AZURE_OPENAI_ENDPOINT)
\n
You also need the Deployment Name you gave your model (AZURE_OPENAI_DEPLOYMENT_NAME).
\n
Note the API Version you intend to use (e.g., 2024-05-01-preview).
\n
\n
\n
\n\n
3.2 Azure Communication Services (ACS)
\n
This service will store or provide access to the chat threads we want to analyze.
\n
\n
Action: Create the ACS Resource\n
\n
Log in to the Azure Portal.
\n
In the top search bar, type Communication Services and select it from the results.
\n
\n
\n
\n
On the Communication Services page, click the \"+ Create\" button. (Alternatively: Click \"+ Create a resource\", search for \"Communication Services\" in the Marketplace, select it, and click \"Create\").
\n\n
\n
\n
\n
Once you click on Create, you get the option to select from the list of services. Select Communication Services from the dropdown, as shown here:
\n
\n
\n
\n\n
\n
\n
\n
\n
Once you land on the Communication Services page, you need to click on the Create button, as shown here:
\n
\n
\n
\n\n
\n
\n
\n
Once you’ve successfully created the Communication Service from the Marketplace, move on to the next step. On the Create resource form, choose the subscription and resource group that you set up back
\n
Tags Tab (Optional): You can add tags for organization or billing purposes, but it's not required for this tutorial. Click \"Next: Review + create\".
\n
\n
\n
\n\n
\n
\n
\n
Review + create Tab: Azure will validate your settings. Once validation passes, review the details and click the \"Create\" button.
\n
Wait for the deployment to complete. You'll see a notification when it's done.
\n
\n
\n
Action: Find ACS Credentials\n
\n
Once deployed, click \"Go to resource\" or find your newly created Communication Services resource via the search bar or resource groups.
\n
In the resource menu (left-hand side), under \"Settings\", click on \"Keys\".
\n
On the Keys page, you will find:\n
\n
The Endpoint URL.
\n
Two Connection Strings (Primary and Secondary). You only need one.
\n
\n
\n
Copy the Endpoint URL and one of the Connection String values. Keep these secure alongside your Azure OpenAI credentials.
\n
\n
\n
\n\n
4. Implementing the Streamlit Application Code
\n
Now, we'll build the core of our application. Create a Python file named app.py (or your preferred name) in your project folder. We will add the code in logical sections.
\n
4.1 Imports and Configuration Loading
\n
First, we import all necessary libraries and load the configuration securely from our .env file. We also define some constants for sender names.
\n
# --- Start of Streamlit App Code (app.py) ---\nimport streamlit as st\nfrom azure.communication.chat import (\n ChatClient, CommunicationTokenCredential, ChatMessageType,\n ChatParticipant,\n ChatThreadClient\n)\nfrom azure.communication.identity import CommunicationIdentityClient, CommunicationUserIdentifier\nfrom azure.core.exceptions import ResourceExistsError, ResourceNotFoundError\nfrom datetime import datetime, timedelta\nimport openai\nimport re\nimport os\nimport time\nfrom dotenv import load_dotenv # Import dotenv\n\n# --- Load Environment Variables ---\nload_dotenv() # Load variables from .env file at the start\n\n# --- Configuration ---\n# Load from environment with error\nchecking\nAZURE_OPENAI_API_KEY = os.getenv(\"AZURE_OPENAI_API_KEY\")\nAZURE_OPENAI_ENDPOINT = os.getenv(\"AZURE_OPENAI_ENDPOINT\")\nAZURE_API_VERSION = os.getenv(\"AZURE_API_VERSION\",'2024-05-01-preview') # Provide\na default API version if needed\nAZURE_OPENAI_DEPLOYMENT_NAME = os.getenv(\"AZURE_OPENAI_DEPLOYMENT_NAME\") # Your\nmodel deployment name\n\nCOMMUNICATION_CONNECTION_STRING = os.getenv(\"COMMUNICATION_CONNECTION_STRING\")\nCOMMUNICATION_ENDPOINT = os.getenv(\"COMMUNICATION_ENDPOINT\")\n\n# --- Early check for critical\nenvironment variables ---\n# Stop execution if essential configs\nare missing to prevent errors later\nif not all([AZURE_OPENAI_API_KEY,\n AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,\n COMMUNICATION_CONNECTION_STRING, COMMUNICATION_ENDPOINT]):\n # Use st.error which is visible\n in the Streamlit app\n st.error(\"❌ Critical environment variables are missing! Please ensure AZURE_OPENAI_API_KEY,\n AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,\n COMMUNICATION_CONNECTION_STRING, and COMMUNICATION_ENDPOINT are set in your\n .env file or system environment.\")\n st.stop() # Halt script execution if\nconfiguration is incomplete\n\n# --- Application Specific Settings ---\nTARGET_THREAD_TOPIC = \"Streamlit ACS Interactive Demo V3\" # Name for\nthe ACS chat thread\n\n# --- Constants for Sender Display Names\n---\nSENDER_USER = \"User\n(via Streamlit)\" # Display name for messages sent by the human user\nSENDER_AI_AGENT = \"AI Agent\" #\nDisplay name for messages generated by OpenAI\nSENDER_SYSTEM = \"System\" # Display name for automated system messages\n\n# --- Configure OpenAI library (using\nv0.28.1 syntax) ---\n# This configures the openai library\nglobally for this script run\nopenai.api_type = \"azure\"\nopenai.api_key = AZURE_OPENAI_API_KEY\nopenai.api_base = AZURE_OPENAI_ENDPOINT\nopenai.api_version = AZURE_API_VERSION\n
\n
Explanation: This section imports necessary modules from Streamlit, Azure SDKs, OpenAI, and standard Python libraries. It uses dotenv to load credentials stored in the.env file into environment variables. A crucial check is added using st.error and st.stop() to halt the app gracefully if essential configuration values are missing. Finally, it defines constants for sender display names and configures the openai library to point to your Azure OpenAI resource using the loaded credentials (note this uses syntax for openai library version 0.x).
\n
4.2 Helper Function: Parsing the AI Summary
\n
This function takes the raw text output from the OpenAI summarization call and extracts structured information using regular expressions.
\n
# --- Helper Function for Parsing Summary ---\ndef parse_summary(text):\n \"\"\" Parses the\n AI response for summary sections using regex. \"\"\"\n parsed = {\n \"Topic\": \"Not\nfound\", \"Summary\": \"Not\nfound\",\n \"Highlights\": \"Not\nfound\", \"Sentiment\": \"Not\nfound\"\n }\n if not text:\n parsed[\"Error\"] = \"AI\nreturned no text to parse.\"\n return\nparsed\n try:\n # Regex patterns look for\n Markdown headings (## Heading)\n # re.DOTALL makes '.' match\n newline characters within sections\n topic_match = re.search(r\"##\\s*Topic\\s*\\n(.*?)(?=\\n##\\s*(Summary|Highlights|Sentiment)|$)\", text, re.IGNORECASE | re.DOTALL)\n if\ntopic_match: parsed[\"Topic\"] = topic_match.group(1).strip()\n\n summary_match = re.search(r\"##\\s*Summary\\s*\\n(.*?)(?=\\n##\\s*(Highlights|Sentiment)|$)\", text, re.IGNORECASE | re.DOTALL)\n if\nsummary_match: parsed[\"Summary\"] = summary_match.group(1).strip()\n\n highlights_match = re.search(r\"##\\s*Highlights\\s*\\n(.*?)(?=\\n##\\s*Sentiment|$)\", text, re.IGNORECASE | re.DOTALL)\n if\nhighlights_match: parsed[\"Highlights\"] = highlights_match.group(1).strip()\n\n sentiment_match = re.search(r\"##\\s*Sentiment\\s*\\n(.*)\", text, re.IGNORECASE | re.DOTALL)\n if\nsentiment_match: parsed[\"Sentiment\"] = sentiment_match.group(1).strip()\n\n # Check if parsing failed\n completely or partially\n if all(v == \"Not found\" for k, v in\nparsed.items() if\nk not in [\"RawOutput\",\"Error\", \"Warning\"]):\n parsed[\"Error\"] = \"Could\nnot parse specific sections. Displaying raw output.\"\n parsed[\"RawOutput\"] = text # Include\nraw output if parsing fails\n elif any(v == \"Not found\" for k, v in\nparsed.items() if\nk not in [\"RawOutput\",\"Error\", \"Warning\"]):\n parsed[\"Warning\"] = \"AI\nresponse formatting inconsistent; some sections might be missing.\"\n\n except\nException as\ne:\n st.exception(f\"Error during summary\nparsing: {e}\") # Log full\nerror\n parsed[\"Error\"] = f\"An\nerror occurred during parsing. Raw response:\\n{text}\"\n parsed[\"RawOutput\"] = text\n parsed[\"Topic\"], parsed[\"Summary\"], parsed[\"Highlights\"], parsed[\"Sentiment\"] = \"Error\", \"Error\", \"Error\", \"Error\"\n return\nparsed\n
\n
Explanation: The parse_summary function takes the text generated by the AI summarization prompt. It uses regular expressions (re.search) to find sections starting with markdown headings like ## Topic. It extracts the content under each heading and stores it in a dictionary. It includes error handling for cases where the AI output doesn't match the expected format, returning an Error or Warning key in the dictionary along with the raw output if possible.
\n
4.3 Helper Function: Initializing Azure Clients and ACS Thread
\n
This is a critical function that sets up connections to Azure services and creates the specific chat thread for the application session. It's decorated with st.cache_resource to ensure it only runs once per session, improving performance and preventing accidental resource recreation.
\n
# --- Initialize Clients and Thread (Cached Resource)\n---\n@st.cache_resource # IMPORTANT: Caches the returned objects for the\nsession\ndef initialize_azure_clients_and_thread():\n \"\"\"\n Initializes Azure OpenAI & ACS clients, creates the chat thread for\n this session.\n Returns a dictionary containing clients, thread_id, and error status.\n Cached by Streamlit to run only once per session unless cache is\n cleared.\n \"\"\"\n initialization_details = {\n \"chat_client\": None, \"chat_thread_client\": None,\n \"thread_id\": None, \"error\": None\n }\n try:\n # --- (Re)Configure OpenAI\n library within function scope if needed, ---\n # --- though global config\n might suffice if not changing keys ---\n # openai.api_key =\n AZURE_OPENAI_API_KEY\n # ... (rest of openai config)\n...\n\n # --- ACS Setup: Identity\n Client, User, Token ---\n st.sidebar.text(\"Initializing\n ACS Identity...\")\n identity_client =\n CommunicationIdentityClient.from_connection_string(COMMUNICATION_CONNECTION_STRING)\n user_streamlit = identity_client.create_user()\n token_streamlit = identity_client.get_token(user_streamlit, [\"chat\"])\n st.sidebar.text(\"ACS\n Identity Ready.\")\n\n # --- Define Participant for\n the Thread ---\n participant_streamlit_user = ChatParticipant(\n identifier=user_streamlit,\n display_name=SENDER_USER,\n share_history_time=datetime.utcnow() - timedelta(days=1)\n )\n participants = [participant_streamlit_user]\n\n # --- Create base ACS Chat\n Client ---\n st.sidebar.text(\"Initializing\n ACS Chat Client...\")\n chat_client_streamlit = ChatClient(COMMUNICATION_ENDPOINT,\n CommunicationTokenCredential(token_streamlit.token))\n initialization_details[\"chat_client\"] = chat_client_streamlit\n st.sidebar.text(\"ACS\n Chat Client Ready.\")\n\n # --- Create the ACS Chat\n Thread ---\n thread_id = None\n st.sidebar.text(f\"Creating\n ACS Thread ('{TARGET_THREAD_TOPIC}')...\")\n try:\n create_thread_result =\n chat_client_streamlit.create_chat_thread(\n topic=TARGET_THREAD_TOPIC,\n thread_participants=participants\n )\n thread_id =\n create_thread_result.chat_thread.id\n initialization_details[\"thread_id\"] = thread_id\n st.sidebar.success(f\"Thread Created:\\nID: {thread_id[:12]}...\", icon=\"✅\")\n\n #\n --- Get Thread-Specific Client ---\n chat_thread_client_temp =\n chat_client_streamlit.get_chat_thread_client(thread_id)\n\n #\n --- Send Initial Messages ---\n st.sidebar.text(\"Sending initial messages...\")\n try:\n chat_thread_client_temp.send_message(f\"Chat\n thread '{TARGET_THREAD_TOPIC}' started.\",\n sender_display_name=SENDER_SYSTEM)\n time.sleep(0.5) # Small delay helps messages appear in order\n chat_thread_client_temp.send_message(\"Hello!\n I am your AI assistant. How can I help?\",\n sender_display_name=SENDER_AI_AGENT)\n st.sidebar.info(\"Initial messages sent.\", icon=\"✉️\")\n except Exception as msg_err:\n st.sidebar.warning(f\"Could not send initial messages: {msg_err}\", icon=\"⚠️\")\n\n # --- Handle Thread Creation\n Errors ---\n except\n ResourceExistsError:\n st.sidebar.warning(\"Thread creation conflict\n (ResourceExistsError).\", icon=\"⚠️\")\n initialization_details[\"error\"] = \"Failed - Thread conflict.\"\n return initialization_details\n except\n Exception as\n e:\n initialization_details[\"error\"] = f\"Error during thread creation: {e}\"\n st.exception(e)\n return initialization_details\n\n if not\n thread_id:\n initialization_details[\"error\"] = \"Thread ID is missing after\n creation attempt.\"\n return initialization_details\n\n # --- Store the final\n ChatThreadClient ---\n chat_thread_client =\n chat_client_streamlit.get_chat_thread_client(thread_id)\n initialization_details[\"chat_thread_client\"] = chat_thread_client\n st.sidebar.text(\"Initialization\n Complete.\")\n return\n initialization_details\n\n #\n --- Handle Overall Initialization Errors ---\n except\n Exception as\n e:\n st.error(\"❌ Failed during Azure\n client initialization process.\")\n st.exception(e)\n initialization_details[\"error\"] = f\"Failed\n overall initialization: {e}\"\n return\n initialization_details\n
\n
Explanation: This function performs the critical setup on the first run of the session.
\n
\n
st.cache_resource: Makes Streamlit store the returned dictionary (initialization_details) and reuse it on subsequent script reruns within the same user session, avoiding costly re-creation of clients and threads unless the cache is explicitly cleared (e.g., by the \"Clear Chat\" button).
\n
ACS Identity: Connects to ACS, creates a unique user identity and a temporary chat access token for this specific app session.
\n
ACS Chat Client: Creates the main client object used to interact with the ACS chat service (e.g., creating/deleting threads).
\n
Thread Creation: Attempts to create a new chat thread in ACS with a defined topic and adds the app user as a participant. It handles potential errors if a thread creation conflicts.
\n
Initial Messages: Sends introductory messages from \"System\" and \"AI Agent\" to the newly created thread.
\n
Thread Client: Gets the specific ChatThreadClient needed to interact with messages within the created thread.
\n
Return Value: Returns a dictionary containing the base client, the thread-specific client, the thread ID, and any error message encountered. Sidebar messages provide visual feedback during this process.
\n
\n
4.4 Helper Function: Getting the AI Agent's Response
\n
This function encapsulates the logic for generating the AI's conversational reply.
\n
# --- Helper Function to get AI chat response ---\ndef get_ai_agent_response(thread_client:\nChatThreadClient):\n \"\"\"Fetches\n history from ACS, formats it, calls OpenAI, returns AI response\n text.\"\"\"\n if not\n thread_client:\n st.error(\"Cannot get AI response:\n ChatThreadClient is not initialized.\")\n return \"[Error: Chat client not available]\"\n try:\n # 1. Fetch recent message\n history from ACS thread\n messages_for_prompt = []\n history_limit = 20 # Limit\n context window\n message_iterator =\n thread_client.list_messages(results_per_page=history_limit)\n acs_messages = list(message_iterator)\n acs_messages.reverse() # Process\n oldest first for context\n\n # 2. Format messages into\n OpenAI ChatCompletion structure\n for\n msg in\n acs_messages:\n sender_name =\n msg.sender_display_name\n content = msg.content.message if\n msg.content and\n msg.content.message else \"\"\n if not content: continue # Skip\n empty messages\n\n #\n Map sender to OpenAI role (user/assistant)\n if sender_name == SENDER_AI_AGENT: role = \"assistant\"\n elif sender_name == SENDER_USER: role = \"user\"\n else: role = \"user\"; content = f\"[{sender_name}]: {content}\" # Treat\n others as user context\n\n messages_for_prompt.append({\"role\":\n role, \"content\": content})\n\n if not\n messages_for_prompt: # Should\n not happen after init, but safeguard\n return \"Hello!\n How may I assist you?\"\n\n # 3. Define AI's persona via\n system message\n system_message = {\"role\": \"system\", \"content\": \"You\n are a helpful and concise AI Agent providing support. Respond naturally to the\n user's last message based on the conversation history.\"}\n openai_messages = [system_message] + messages_for_prompt\n\n # 4. Call Azure OpenAI API\n response = openai.ChatCompletion.create(\n engine=AZURE_OPENAI_DEPLOYMENT_NAME,\n messages=openai_messages,\n max_tokens=150,\n temperature=0.7\n )\n ai_response_text = response['choices'][0]['message']['content']\n return\n ai_response_text.strip()\n\n except\n Exception as\n e:\n st.error(\"Error getting AI\n response:\")\n st.exception(e) # Log full\n error\n return \"[Error occurred while generating AI response.\n Please check logs.]\"\n
\n
Explanation: This function is called whenever the AI needs to generate a reply. It fetches the recent conversation history from the ACS thread using the provided thread_client, formats it into the [{role: 'user'/'assistant', content: '...'}] structure required by the OpenAI Chat Completion API, adds a system message to guide the AI's behavior, calls the API, and returns the AI's generated text response.
\n
4.5 Streamlit App Structure and Initialization
\n
This sets up the main page configuration and title and calls the initialization function.
\n
# --- Streamlit App ---\nst.set_page_config(layout=\"wide\",\npage_title=\"AI Chat & Summary\n(ACS + OpenAI)\")\nst.title(\"💬\nInteractive AI Agent Chat & Summarizer\")\nst.markdown(f\"Using\n**Azure Communication Services** & **Azure OpenAI**. Date: {datetime.now().strftime('%Y-%m-%d')}\")\nst.info(\"Type\na message, click 'Send'. AI Agent responds. Use sidebar to clear/reset. Use\nsummary button for analysis.\")\n\n# --- Initialize Clients and Thread ---\n# This calls the cached function defined\nearlier\ninit_details =\ninitialize_azure_clients_and_thread()\nchat_client = init_details.get(\"chat_client\")\nchat_thread_client = init_details.get(\"chat_thread_client\")\nthread_id = init_details.get(\"thread_id\")\ninit_error = init_details.get(\"error\")\n\n# --- Sidebar ---\nst.sidebar.title(\"Controls\")\n# Sidebar button logic to clear chat\nif\nst.sidebar.button(\"🧹 Clear Chat\n& Start New Thread\"):\n if\n chat_client and\n thread_id:\n with\n st.spinner(\"Clearing chat\n thread...\"):\n try:\n print(f\"Attempting\n to delete thread: {thread_id}\") # Server log\n chat_client.delete_chat_thread(thread_id=thread_id)\n st.sidebar.success(\"Previous thread deleted.\")\n except ResourceNotFoundError:\n st.sidebar.warning(\"Thread already deleted or not found.\")\n except Exception as e:\n st.sidebar.error(f\"Error deleting thread: {e}\")\n st.exception(e)\n\n #\n Clear state, cache, and rerun\n st.session_state.clear()\n initialize_azure_clients_and_thread.clear() #\n Clear cache\n st.rerun() #\n Restart script execution\n else:\n st.sidebar.error(\"Cannot\n clear chat - client/thread not initialized.\")\n\n\n# --- Initialize Session State Variables\n(Safety check) ---\n# Ensures these keys exist after\npotential clearing\nif 'current_summary' not in\nst.session_state: st.session_state.current_summary = None\nif 'messages' not in\nst.session_state: st.session_state.messages = []\nif 'send_button_clicked' not in\nst.session_state: st.session_state.send_button_clicked = False\n
\n
Explanation: This section configures the web page (title, layout), displays introductory text, and then calls the cached initialize_azure_clients_and_thread function to get the necessary client objects and thread ID. It defines the sidebar, including the \"Clear Chat\" button which deletes the ACS thread, clears session state and the resource cache, then forces a rerun. Finally, it ensures essential keys exist in Streamlit's session_state dictionary.
\n
4.6 Main Application Logic and UI
\n
This is the core part that runs only if the initialization is successful. It defines the layout and handles the interactive elements.
\n
# --- Main Application Logic ---\n# Only proceed if initialization was\nsuccessful (clients/thread ready)\nif\nchat_thread_client and not\ninit_error:\n\n # Define layout using columns\n (Chat on left, Summary on right)\n col1, col2 = st.columns([0.6, 0.4])\n\n # --- Column 1: Live Chat\n Interface ---\n with\n col1:\n st.subheader(f\"💬 Live Chat: {TARGET_THREAD_TOPIC}\")\n st.markdown(\"---\")\n\n # --- Display Chat Messages ---\n message_container = st.container(height=600) #\n Scrollable container\n try:\n #\n Fetch latest messages from ACS on every script run\n chat_messages_iterator =\n chat_thread_client.list_messages(results_per_page=50)\n live_messages = list(chat_messages_iterator)\n #\n Store messages in session state for potential use by summary function\n st.session_state.messages =\n live_messages\n\n #\n Render messages within the container\n with message_container:\n if not live_messages:\n st.info(\"No messages yet. Send a message to start!\")\n else:\n #\n Display messages, newest at the bottom\n for msg in reversed(live_messages):\n sender_name =\n msg.sender_display_name or \"Unknown\"\n # Assign avatars based on sender name constants\n if\n sender_name == SENDER_AI_AGENT: avatar = \"🤖\"\n elif\n sender_name == SENDER_USER: avatar = \"👤\"\n elif\n sender_name == SENDER_SYSTEM: avatar = \"⚙️\"\n else: avatar =\"🧑💻\" # Default\n\n # Use Streamlit's chat elements for display\n with\n st.chat_message(name=sender_name, avatar=avatar):\n st.caption(f\"{msg.created_on.strftime('%Y-%m-%d %H:%M:%S')}\") # Timestamp\n st.write(msg.content.message or \"*empty\n message*\") #\n Message content\n\n except\n Exception as\n e: # Handle errors fetching messages\n with message_container:\n st.error(\"Error fetching messages from ACS:\")\n st.exception(e)\n\n st.markdown(\"---\") #\n Separator\n\n # --- Send Message Input Form\n ---\n # Use a form to group input and\n button click\n with\n st.form(key=\"send_message_form\", clear_on_submit=True):\n new_message_content =\n st.text_input(\"Your Message:\", key=\"new_message_input\", placeholder=\"Type\n your message here...\")\n submitted = st.form_submit_button(\"Send Message\")\n if submitted:\n st.session_state.send_button_clicked = True # Flag\n submission\n\n # --- Process message sending\n (Runs after form check) ---\n if\n st.session_state.send_button_clicked:\n st.session_state.send_button_clicked = False # Reset\n flag\n\n if new_message_content: #\n Check if user actually typed something\n #\n Show spinner during processing\n with st.spinner(\"Sending\n message and getting AI response...\"):\n try:\n # 1. Send User message to ACS\n chat_thread_client.send_message(new_message_content,\n sender_display_name=SENDER_USER)\n # 2. Get AI response (calls helper function)\n ai_reply =\n get_ai_agent_response(chat_thread_client)\n # 3. Send AI response back to ACS\n if ai_reply:\n chat_thread_client.send_message(ai_reply, sender_display_name=SENDER_AI_AGENT)\n # 4. Rerun script to refresh chat display\n st.rerun()\n except Exception as e: # Handle\n errors during send/response cycle\n st.error(\"Error during send/response cycle:\")\n st.exception(e)\n else:\n st.warning(\"Please enter a message to send.\")\n\n\n # --- Column 2: AI Summary Area\n ---\n with\n col2:\n st.subheader(\"✨ Conversation Summary\")\n st.markdown(\"---\")\n\n summary_error_message = None #\n Placeholder for specific summary errors\n\n # --- Generate Summary Button\n and Logic ---\n if\n st.button(\"Generate Full\n Conversation Summary\", key=\"generate_summary_btn\"):\n st.session_state.summary_processing = True # Flag\n processing\n st.session_state.current_summary =\n {\"Info\":\"Processing...\"} # Show\n status\n\n #\n Use messages already fetched and stored in session state\n fetched_messages =\n st.session_state.get('messages', [])\n\n if fetched_messages:\n prompt_text = \"\"\n #\n Build prompt text from stored messages (oldest first)\n for msg in reversed(fetched_messages):\n #\n ... (build prompt_text, skipping empty content) ...\n sender_name =\n msg.sender_display_name if msg.sender_display_name else \"Unknown\"\n content =\n msg.content.message if\n msg.content and\n msg.content.message else \"\"\n if content: prompt_text += f\"{sender_name}: {content}\\n\"\n\n\n if not prompt_text.strip():\n st.session_state.current_summary = {\"Info\": \"No\n message content to summarize.\"}\n else:\n #\n Prepare prompt with instructions\n instruction_phrase = 'Summarize the following conversation. Extract the\n main topic, a brief summary, key highlights (1-3 bullet points), and overall\n sentiment. Format clearly with headings: ## Topic, ## Summary, ## Highlights,\n ## Sentiment.\\n\\n'\n full_prompt =\n instruction_phrase + prompt_text.strip()\n\n #\n Call OpenAI within a spinner and try/except block\n with st.spinner(\"Generating\n summary...\"):\n try:\n response =\n openai.ChatCompletion.create( # Or\n Completion API\n engine=AZURE_OPENAI_DEPLOYMENT_NAME,\n messages=[{\"role\":\"user\",\"content\": full_prompt}],\n max_tokens=1024,\n temperature=0.2\n )\n summary_text =\n response['choices'][0]['message']['content']\n # Parse and store result\n st.session_state.current_summary = parse_summary(summary_text)\n # Handle various OpenAI/other errors\n except\n openai.error.AuthenticationError as e: ... # Set\n Error state\n # ... other except blocks ...\n except Exceptionas\n e: ... # Set Error state\n\n else: # No\n messages fetched/stored\n st.session_state.current_summary = {\"Info\": \"Empty\n conversation history.\"}\n\n st.session_state.summary_processing = False # Unflag\n processing\n st.rerun() #\n Rerun to update the summary display area\n\n\n # --- Display Summary (or\n Status) ---\n # Checks the state variable and\n displays Error, Warning, Info, or the parsed summary\n if\n st.session_state.get('current_summary'):\n summary_data =\n st.session_state.current_summary\n #\n --- Handle Error State ---\n if isinstance(summary_data, dict) and \"Error\" in summary_data: ... #\n Display st.error(...)\n #\n --- Handle Warning State ---\n elif isinstance(summary_data, dict) and \"Warning\" in summary_data: ... #\n Display st.warning(...) and partial data\n #\n --- Handle Info State ---\n elif isinstance(summary_data, dict) and \"Info\" in summary_data: ... #\n Display st.info(...)\n #\n --- Handle Successful Summary Display ---\n elif isinstance(summary_data, dict): ... # Display\n formatted markdown sections\n #\n --- Fallback ---\n else: st.error(\"Summary\n data unexpected format.\")\n # --- Default Placeholder Text\n ---\n elif not\n st.session_state.get('summary_processing', False):\n st.info(\"Click\n 'Generate Summary' for an analysis...\")\n\n\n# --- Handle Initialization Errors ---\n# This runs if the main 'if\nchat_thread_client...' block is False\nelif\ninit_error:\n st.error(f\"❌ Application cannot\n start due to initialization errors:\")\n st.error(init_error) # Display\n the error caught during initialization\nelse:\n # Fallback if client is None\n but no specific error was caught\n st.warning(\"⏳ Initializing Azure\n services or waiting for resources...\")\n\n# --- End of Streamlit App Code ---\n
\n
Explanation: This is the main part of the application that runs if the initial Azure setup was successful.
\n
\n
Layout: It defines a two-column layout (st.columns).
\n
Column 1 (Chat):\n
\n
Fetches messages from the ACS thread on every rerun using chat_thread_client.list_messages().
\n
Stores these messages in st.session_state.messages (primarily for the summary function's use).
\n
Displays the messages chronologically in a scrollable container using st.chat_message.
\n
Provides a text input and send button within a st.form.
\n
Handles the form submission by sending the user's message, getting the AI response via the helper function, sending the AI response, and triggering a st.rerun to refresh the display.
\n
\n
\n
Column 2 (Summary):\n
\n
Includes the \"Generate Full Conversation Summary\" button.
\n
When clicked, it takes the messages stored in session state, creates a summarization prompt, calls Azure OpenAI, parses the result using parse_summary, and stores the output (or error/info) in st.session_state.current_summary.
\n
It then conditionally displays the summary information (Topic, Summary, Highlights, Sentiment) or any errors/warnings/info messages based on the content of st.session_state.current_summary.
\n
\n
\n
Initialization Error Handling: The final elif/else catches cases where the chat_thread_client wasn't successfully obtained during startup and displays relevant error or waiting messages.
\n
\n
5. Running the Streamlit Application
\n
With your Azure resources configured, the Python environment set up, and the code saved in app.py (or your chosen filename), you are ready to run the interactive application.
\n
5.1. Run the App:
\n
Open your terminal or command prompt, make sure you are in your project directory (where app.py and .env are located), and execute the command:
\n
streamlit run app.py
\n
5.2. Initial Loading State:
\n
Streamlit will start its server and provide you with a local URL (usually http://localhost:8501). When you open this URL in your web browser, you will first see the application's initial loading state, similar to this image:
\n\n
\n
The page displays the title \"Interactive AI Agent Chat (V3)\" and the initial informational text.
\n
You'll notice a spinner element with text like \"Running: initialize_azure_clients_and_thread()\". This indicates that the application is currently performing the one-time setup for the session.
\n
During this phase (which might take a few moments, especially on the very first run or after clearing the chat), the cached function initialize_azure_clients_and_thread is executing. It connects to Azure Communication Services, creates a user identity and token, establishes a connection to Azure OpenAI, and creates the specific chat thread in ACS, sending the initial messages.
\n
\n
5.3. Interact with the App:
\n
\n
Once the initialization is complete, the spinner will disappear.
\n
The main chat interface will load, displaying the initial \"System\" and \"AI Agent\" messages fetched from the newly created ACS thread.
\n
Type your messages into the input box at the bottom of the chat column and click \"Send Message\". You will see your message appear, followed by a response from the \"AI Agent\" after a brief processing time (indicated by a spinner).
\n
Navigate to the right-hand column and click \"Generate Full Conversation Summary\" anytime to get an AI-powered analysis of the conversation so far.
\n
Use the \"Clear Chat & Start New Thread\" button in the sidebar to delete the current conversation history (by deleting the ACS thread) and start a fresh session.
\n
\n
5.4. Review Output:
\n
The AI's chat responses appear directly in the chat interface. The generated summary appears in the right-hand column after you click the button. Any errors during operation should be displayed within the Streamlit interface. You can also check the terminal where you ran the streamlit run command for any server-side print statements or error logs.
\n\n
6. Conclusion
\n
You've now walked through the process of setting up Azure resources and using Streamlit to build a contact center chat analysis solution! By leveraging Azure Communication Services to access chat data and Azure OpenAI Service for intelligent analysis, you can automate the extraction of valuable insights from user or customer interactions, moving from overwhelming data volume to clear, actionable intelligence.
Contact centers are the front lines of customer interaction, generating vast amounts of valuable data through chat logs, call transcripts, and emails. However, manually sifting through this data to find actionable insights is often a monumental task. Imagine the scenario of a thriving online service, like a food delivery app: as usage climbs, so does the number of customer support chats, making it incredibly difficult to pinpoint recurring problems or gauge overall satisfaction from the sea of text. How can businesses effectively tap into this wealth of information?
\n
This post explores a powerful solution: building an automated analytics platform using Azure Communication Services (ACS) combined with the intelligence of Azure OpenAI Service. We'll outline how this integration allows businesses to process, understand, and derive actionable insights from their contact center conversations at scale, transforming raw data into a strategic asset.
\n
(Why This Azure Stack for Analytics?)
\n
Combining ACS and Azure OpenAI offers a compelling approach for contact center analytics:
\n
\n
Azure Communication Services (ACS): Provides a comprehensive cloud-based platform. In terms of analytics, it serves as the central hub for chat interactions or where transcripts are ingested and stored within organized threads, ready for programmatic access via its SDKs.
\n
Azure OpenAI Service: Offers secure access to advanced large language models (such as GPT-4). These models excel in understanding context, summarizing extensive texts, extracting key information, and analyzing sentiment, making them ideal for processing unstructured conversation data automatically.
\n
\n
What We'll Cover:
\n
\n
Setting up Azure OpenAI and Azure Communication Services.
\n
Finding and configuring necessary credentials (Endpoints, Keys, Connection Strings).
\n
Setting up your streamlit environment.
\n
Writing Python code to retrieve chat messages from ACS.
\n
Crafting prompts to instruct Azure OpenAI for analysis (summary, topic, sentiment).
\n
Calling the Azure OpenAI API.
\n
Processing the AI-generated insights.
\n
\n
2. Architecture
\n\n
2.1. Data Flow:
\n\n
The user interacts with the Streamlit application.
\n
When instructed (e.g., by a button click or automatically), the Streamlit application uses the ACS SDK to fetch chat messages from relevant threads within the Azure Communication Services.
\n
The Streamlit application prepares the chat data and formulates prompts based on the desired analysis (e.g., summarize, extract topics, analyze sentiment).
\n
These prompts and the chat data are sent to the Azure OpenAI Service.
\n
Azure OpenAI processes the information using its LLMs and returns the analysis results (summary, topics, sentiment) to the Streamlit application.
\n
The Streamlit application receives the analysis results and formats them for display in the user interface, alongside the original chat messages.
\n\n
2.2. Prerequisites
\n
Before you begin, ensure you have the following:
\n
\n
Python: Version 3.9 or later installed.
\n
Azure Account: An active Azure subscription. If you're new, you can create a free account with credits.
\n
Azure OpenAI Access: Your Azure subscription must have access enabled for Azure OpenAI Service. You can request access here: https://aka.ms/oaiapply
\n
Code Editor/Environment: An IDE like VS Code
\n
\n
3. Setting Up Azure Resources
\n
We need two core Azure services: Azure OpenAI (for the AI model) and Azure Communication Services (to access chat data).
\n
3.1 Azure OpenAI Service
\n
\n
Requirement: You need an existing Azure OpenAI resource and a deployed model (like gpt-35-turbo, gpt-4, gpt-4o, gpt-4o).
Collect Credentials: Once deployed, navigate to your Azure OpenAI resource in the Azure Portal. Go to the \"Keys and Endpoint\" section. You will need to copy:\n
\n
One of the API Keys (AZURE_OPENAI_API_KEY)
\n
The Endpoint URL (AZURE_OPENAI_ENDPOINT)
\n
You also need the Deployment Name you gave your model (AZURE_OPENAI_DEPLOYMENT_NAME).
\n
Note the API Version you intend to use (e.g., 2024-05-01-preview).
\n
\n
\n
\n\n
3.2 Azure Communication Services (ACS)
\n
This service will store or provide access to the chat threads we want to analyze.
\n
\n
Action: Create the ACS Resource\n
\n
Log in to the Azure Portal.
\n
In the top search bar, type Communication Services and select it from the results.
\n
\n
\n
\n
On the Communication Services page, click the \"+ Create\" button. (Alternatively: Click \"+ Create a resource\", search for \"Communication Services\" in the Marketplace, select it, and click \"Create\").
\n\n
\n
\n
\n
Once you click on Create, you get the option to select from the list of services. Select Communication Services from the dropdown, as shown here:
\n
\n
\n
\n\n
\n
\n
\n
\n
Once you land on the Communication Services page, you need to click on the Create button, as shown here:
\n
\n
\n
\n\n
\n
\n
\n
Once you’ve successfully created the Communication Service from the Marketplace, move on to the next step. On the Create resource form, choose the subscription and resource group that you set up back
\n
Tags Tab (Optional): You can add tags for organization or billing purposes, but it's not required for this tutorial. Click \"Next: Review + create\".
\n
\n
\n
\n\n
\n
\n
\n
Review + create Tab: Azure will validate your settings. Once validation passes, review the details and click the \"Create\" button.
\n
Wait for the deployment to complete. You'll see a notification when it's done.
\n
\n
\n
Action: Find ACS Credentials\n
\n
Once deployed, click \"Go to resource\" or find your newly created Communication Services resource via the search bar or resource groups.
\n
In the resource menu (left-hand side), under \"Settings\", click on \"Keys\".
\n
On the Keys page, you will find:\n
\n
The Endpoint URL.
\n
Two Connection Strings (Primary and Secondary). You only need one.
\n
\n
\n
Copy the Endpoint URL and one of the Connection String values. Keep these secure alongside your Azure OpenAI credentials.
\n
\n
\n
\n\n
4. Implementing the Streamlit Application Code
\n
Now, we'll build the core of our application. Create a Python file named app.py (or your preferred name) in your project folder. We will add the code in logical sections.
\n
4.1 Imports and Configuration Loading
\n
First, we import all necessary libraries and load the configuration securely from our .env file. We also define some constants for sender names.
\n
# --- Start of Streamlit App Code (app.py) ---\nimport streamlit as st\nfrom azure.communication.chat import (\n ChatClient, CommunicationTokenCredential, ChatMessageType,\n ChatParticipant,\n ChatThreadClient\n)\nfrom azure.communication.identity import CommunicationIdentityClient, CommunicationUserIdentifier\nfrom azure.core.exceptions import ResourceExistsError, ResourceNotFoundError\nfrom datetime import datetime, timedelta\nimport openai\nimport re\nimport os\nimport time\nfrom dotenv import load_dotenv # Import dotenv\n\n# --- Load Environment Variables ---\nload_dotenv() # Load variables from .env file at the start\n\n# --- Configuration ---\n# Load from environment with error\nchecking\nAZURE_OPENAI_API_KEY = os.getenv(\"AZURE_OPENAI_API_KEY\")\nAZURE_OPENAI_ENDPOINT = os.getenv(\"AZURE_OPENAI_ENDPOINT\")\nAZURE_API_VERSION = os.getenv(\"AZURE_API_VERSION\",'2024-05-01-preview') # Provide\na default API version if needed\nAZURE_OPENAI_DEPLOYMENT_NAME = os.getenv(\"AZURE_OPENAI_DEPLOYMENT_NAME\") # Your\nmodel deployment name\n\nCOMMUNICATION_CONNECTION_STRING = os.getenv(\"COMMUNICATION_CONNECTION_STRING\")\nCOMMUNICATION_ENDPOINT = os.getenv(\"COMMUNICATION_ENDPOINT\")\n\n# --- Early check for critical\nenvironment variables ---\n# Stop execution if essential configs\nare missing to prevent errors later\nif not all([AZURE_OPENAI_API_KEY,\n AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,\n COMMUNICATION_CONNECTION_STRING, COMMUNICATION_ENDPOINT]):\n # Use st.error which is visible\n in the Streamlit app\n st.error(\"❌ Critical environment variables are missing! Please ensure AZURE_OPENAI_API_KEY,\n AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_DEPLOYMENT_NAME,\n COMMUNICATION_CONNECTION_STRING, and COMMUNICATION_ENDPOINT are set in your\n .env file or system environment.\")\n st.stop() # Halt script execution if\nconfiguration is incomplete\n\n# --- Application Specific Settings ---\nTARGET_THREAD_TOPIC = \"Streamlit ACS Interactive Demo V3\" # Name for\nthe ACS chat thread\n\n# --- Constants for Sender Display Names\n---\nSENDER_USER = \"User\n(via Streamlit)\" # Display name for messages sent by the human user\nSENDER_AI_AGENT = \"AI Agent\" #\nDisplay name for messages generated by OpenAI\nSENDER_SYSTEM = \"System\" # Display name for automated system messages\n\n# --- Configure OpenAI library (using\nv0.28.1 syntax) ---\n# This configures the openai library\nglobally for this script run\nopenai.api_type = \"azure\"\nopenai.api_key = AZURE_OPENAI_API_KEY\nopenai.api_base = AZURE_OPENAI_ENDPOINT\nopenai.api_version = AZURE_API_VERSION\n
\n
Explanation: This section imports necessary modules from Streamlit, Azure SDKs, OpenAI, and standard Python libraries. It uses dotenv to load credentials stored in the.env file into environment variables. A crucial check is added using st.error and st.stop() to halt the app gracefully if essential configuration values are missing. Finally, it defines constants for sender display names and configures the openai library to point to your Azure OpenAI resource using the loaded credentials (note this uses syntax for openai library version 0.x).
\n
4.2 Helper Function: Parsing the AI Summary
\n
This function takes the raw text output from the OpenAI summarization call and extracts structured information using regular expressions.
\n
# --- Helper Function for Parsing Summary ---\ndef parse_summary(text):\n \"\"\" Parses the\n AI response for summary sections using regex. \"\"\"\n parsed = {\n \"Topic\": \"Not\nfound\", \"Summary\": \"Not\nfound\",\n \"Highlights\": \"Not\nfound\", \"Sentiment\": \"Not\nfound\"\n }\n if not text:\n parsed[\"Error\"] = \"AI\nreturned no text to parse.\"\n return\nparsed\n try:\n # Regex patterns look for\n Markdown headings (## Heading)\n # re.DOTALL makes '.' match\n newline characters within sections\n topic_match = re.search(r\"##\\s*Topic\\s*\\n(.*?)(?=\\n##\\s*(Summary|Highlights|Sentiment)|$)\", text, re.IGNORECASE | re.DOTALL)\n if\ntopic_match: parsed[\"Topic\"] = topic_match.group(1).strip()\n\n summary_match = re.search(r\"##\\s*Summary\\s*\\n(.*?)(?=\\n##\\s*(Highlights|Sentiment)|$)\", text, re.IGNORECASE | re.DOTALL)\n if\nsummary_match: parsed[\"Summary\"] = summary_match.group(1).strip()\n\n highlights_match = re.search(r\"##\\s*Highlights\\s*\\n(.*?)(?=\\n##\\s*Sentiment|$)\", text, re.IGNORECASE | re.DOTALL)\n if\nhighlights_match: parsed[\"Highlights\"] = highlights_match.group(1).strip()\n\n sentiment_match = re.search(r\"##\\s*Sentiment\\s*\\n(.*)\", text, re.IGNORECASE | re.DOTALL)\n if\nsentiment_match: parsed[\"Sentiment\"] = sentiment_match.group(1).strip()\n\n # Check if parsing failed\n completely or partially\n if all(v == \"Not found\" for k, v in\nparsed.items() if\nk not in [\"RawOutput\",\"Error\", \"Warning\"]):\n parsed[\"Error\"] = \"Could\nnot parse specific sections. Displaying raw output.\"\n parsed[\"RawOutput\"] = text # Include\nraw output if parsing fails\n elif any(v == \"Not found\" for k, v in\nparsed.items() if\nk not in [\"RawOutput\",\"Error\", \"Warning\"]):\n parsed[\"Warning\"] = \"AI\nresponse formatting inconsistent; some sections might be missing.\"\n\n except\nException as\ne:\n st.exception(f\"Error during summary\nparsing: {e}\") # Log full\nerror\n parsed[\"Error\"] = f\"An\nerror occurred during parsing. Raw response:\\n{text}\"\n parsed[\"RawOutput\"] = text\n parsed[\"Topic\"], parsed[\"Summary\"], parsed[\"Highlights\"], parsed[\"Sentiment\"] = \"Error\", \"Error\", \"Error\", \"Error\"\n return\nparsed\n
\n
Explanation: The parse_summary function takes the text generated by the AI summarization prompt. It uses regular expressions (re.search) to find sections starting with markdown headings like ## Topic. It extracts the content under each heading and stores it in a dictionary. It includes error handling for cases where the AI output doesn't match the expected format, returning an Error or Warning key in the dictionary along with the raw output if possible.
\n
4.3 Helper Function: Initializing Azure Clients and ACS Thread
\n
This is a critical function that sets up connections to Azure services and creates the specific chat thread for the application session. It's decorated with .cache_resource to ensure it only runs once per session, improving performance and preventing accidental resource recreation.
\n
# --- Initialize Clients and Thread (Cached Resource)\n---\n@st.cache_resource # IMPORTANT: Caches the returned objects for the\nsession\ndef initialize_azure_clients_and_thread():\n \"\"\"\n Initializes Azure OpenAI & ACS clients, creates the chat thread for\n this session.\n Returns a dictionary containing clients, thread_id, and error status.\n Cached by Streamlit to run only once per session unless cache is\n cleared.\n \"\"\"\n initialization_details = {\n \"chat_client\": None, \"chat_thread_client\": None,\n \"thread_id\": None, \"error\": None\n }\n try:\n # --- (Re)Configure OpenAI\n library within function scope if needed, ---\n # --- though global config\n might suffice if not changing keys ---\n # openai.api_key =\n AZURE_OPENAI_API_KEY\n # ... (rest of openai config)\n...\n\n # --- ACS Setup: Identity\n Client, User, Token ---\n st.sidebar.text(\"Initializing\n ACS Identity...\")\n identity_client =\n CommunicationIdentityClient.from_connection_string(COMMUNICATION_CONNECTION_STRING)\n user_streamlit = identity_client.create_user()\n token_streamlit = identity_client.get_token(user_streamlit, [\"chat\"])\n st.sidebar.text(\"ACS\n Identity Ready.\")\n\n # --- Define Participant for\n the Thread ---\n participant_streamlit_user = ChatParticipant(\n identifier=user_streamlit,\n display_name=SENDER_USER,\n share_history_time=datetime.utcnow() - timedelta(days=1)\n )\n participants = [participant_streamlit_user]\n\n # --- Create base ACS Chat\n Client ---\n st.sidebar.text(\"Initializing\n ACS Chat Client...\")\n chat_client_streamlit = ChatClient(COMMUNICATION_ENDPOINT,\n CommunicationTokenCredential(token_streamlit.token))\n initialization_details[\"chat_client\"] = chat_client_streamlit\n st.sidebar.text(\"ACS\n Chat Client Ready.\")\n\n # --- Create the ACS Chat\n Thread ---\n thread_id = None\n st.sidebar.text(f\"Creating\n ACS Thread ('{TARGET_THREAD_TOPIC}')...\")\n try:\n create_thread_result =\n chat_client_streamlit.create_chat_thread(\n topic=TARGET_THREAD_TOPIC,\n thread_participants=participants\n )\n thread_id =\n create_thread_result.chat_thread.id\n initialization_details[\"thread_id\"] = thread_id\n st.sidebar.success(f\"Thread Created:\\nID: {thread_id[:12]}...\", icon=\"✅\")\n\n #\n --- Get Thread-Specific Client ---\n chat_thread_client_temp =\n chat_client_streamlit.get_chat_thread_client(thread_id)\n\n #\n --- Send Initial Messages ---\n st.sidebar.text(\"Sending initial messages...\")\n try:\n chat_thread_client_temp.send_message(f\"Chat\n thread '{TARGET_THREAD_TOPIC}' started.\",\n sender_display_name=SENDER_SYSTEM)\n time.sleep(0.5) # Small delay helps messages appear in order\n chat_thread_client_temp.send_message(\"Hello!\n I am your AI assistant. How can I help?\",\n sender_display_name=SENDER_AI_AGENT)\n st.sidebar.info(\"Initial messages sent.\", icon=\"✉️\")\n except Exception as msg_err:\n st.sidebar.warning(f\"Could not send initial messages: {msg_err}\", icon=\"⚠️\")\n\n # --- Handle Thread Creation\n Errors ---\n except\n ResourceExistsError:\n st.sidebar.warning(\"Thread creation conflict\n (ResourceExistsError).\", icon=\"⚠️\")\n initialization_details[\"error\"] = \"Failed - Thread conflict.\"\n return initialization_details\n except\n Exception as\n e:\n initialization_details[\"error\"] = f\"Error during thread creation: {e}\"\n st.exception(e)\n return initialization_details\n\n if not\n thread_id:\n initialization_details[\"error\"] = \"Thread ID is missing after\n creation attempt.\"\n return initialization_details\n\n # --- Store the final\n ChatThreadClient ---\n chat_thread_client =\n chat_client_streamlit.get_chat_thread_client(thread_id)\n initialization_details[\"chat_thread_client\"] = chat_thread_client\n st.sidebar.text(\"Initialization\n Complete.\")\n return\n initialization_details\n\n #\n --- Handle Overall Initialization Errors ---\n except\n Exception as\n e:\n st.error(\"❌ Failed during Azure\n client initialization process.\")\n st.exception(e)\n initialization_details[\"error\"] = f\"Failed\n overall initialization: {e}\"\n return\n initialization_details\n
\n
Explanation: This function performs the critical setup on the first run of the session.
\n
\n
.cache_resource: Makes Streamlit store the returned dictionary (initialization_details) and reuse it on subsequent script reruns within the same user session, avoiding costly re-creation of clients and threads unless the cache is explicitly cleared (e.g., by the \"Clear Chat\" button).
\n
ACS Identity: Connects to ACS, creates a unique user identity and a temporary chat access token for this specific app session.
\n
ACS Chat Client: Creates the main client object used to interact with the ACS chat service (e.g., creating/deleting threads).
\n
Thread Creation: Attempts to create a new chat thread in ACS with a defined topic and adds the app user as a participant. It handles potential errors if a thread creation conflicts.
\n
Initial Messages: Sends introductory messages from \"System\" and \"AI Agent\" to the newly created thread.
\n
Thread Client: Gets the specific ChatThreadClient needed to interact with messages within the created thread.
\n
Return Value: Returns a dictionary containing the base client, the thread-specific client, the thread ID, and any error message encountered. Sidebar messages provide visual feedback during this process.
\n
\n
4.4 Helper Function: Getting the AI Agent's Response
\n
This function encapsulates the logic for generating the AI's conversational reply.
\n
# --- Helper Function to get AI chat response ---\ndef get_ai_agent_response(thread_client:\nChatThreadClient):\n \"\"\"Fetches\n history from ACS, formats it, calls OpenAI, returns AI response\n text.\"\"\"\n if not\n thread_client:\n st.error(\"Cannot get AI response:\n ChatThreadClient is not initialized.\")\n return \"[Error: Chat client not available]\"\n try:\n # 1. Fetch recent message\n history from ACS thread\n messages_for_prompt = []\n history_limit = 20 # Limit\n context window\n message_iterator =\n thread_client.list_messages(results_per_page=history_limit)\n acs_messages = list(message_iterator)\n acs_messages.reverse() # Process\n oldest first for context\n\n # 2. Format messages into\n OpenAI ChatCompletion structure\n for\n msg in\n acs_messages:\n sender_name =\n msg.sender_display_name\n content = msg.content.message if\n msg.content and\n msg.content.message else \"\"\n if not content: continue # Skip\n empty messages\n\n #\n Map sender to OpenAI role (user/assistant)\n if sender_name == SENDER_AI_AGENT: role = \"assistant\"\n elif sender_name == SENDER_USER: role = \"user\"\n else: role = \"user\"; content = f\"[{sender_name}]: {content}\" # Treat\n others as user context\n\n messages_for_prompt.append({\"role\":\n role, \"content\": content})\n\n if not\n messages_for_prompt: # Should\n not happen after init, but safeguard\n return \"Hello!\n How may I assist you?\"\n\n # 3. Define AI's persona via\n system message\n system_message = {\"role\": \"system\", \"content\": \"You\n are a helpful and concise AI Agent providing support. Respond naturally to the\n user's last message based on the conversation history.\"}\n openai_messages = [system_message] + messages_for_prompt\n\n # 4. Call Azure OpenAI API\n response = openai.ChatCompletion.create(\n engine=AZURE_OPENAI_DEPLOYMENT_NAME,\n messages=openai_messages,\n max_tokens=150,\n temperature=0.7\n )\n ai_response_text = response['choices'][0]['message']['content']\n return\n ai_response_text.strip()\n\n except\n Exception as\n e:\n st.error(\"Error getting AI\n response:\")\n st.exception(e) # Log full\n error\n return \"[Error occurred while generating AI response.\n Please check logs.]\"\n
\n
Explanation: This function is called whenever the AI needs to generate a reply. It fetches the recent conversation history from the ACS thread using the provided thread_client, formats it into the [{role: 'user'/'assistant', content: '...'}] structure required by the OpenAI Chat Completion API, adds a system message to guide the AI's behavior, calls the API, and returns the AI's generated text response.
\n
4.5 Streamlit App Structure and Initialization
\n
This sets up the main page configuration and title and calls the initialization function.
\n
# --- Streamlit App ---\nst.set_page_config(layout=\"wide\",\npage_title=\"AI Chat & Summary\n(ACS + OpenAI)\")\nst.title(\"💬\nInteractive AI Agent Chat & Summarizer\")\nst.markdown(f\"Using\n**Azure Communication Services** & **Azure OpenAI**. Date: {datetime.now().strftime('%Y-%m-%d')}\")\nst.info(\"Type\na message, click 'Send'. AI Agent responds. Use sidebar to clear/reset. Use\nsummary button for analysis.\")\n\n# --- Initialize Clients and Thread ---\n# This calls the cached function defined\nearlier\ninit_details =\ninitialize_azure_clients_and_thread()\nchat_client = init_details.get(\"chat_client\")\nchat_thread_client = init_details.get(\"chat_thread_client\")\nthread_id = init_details.get(\"thread_id\")\ninit_error = init_details.get(\"error\")\n\n# --- Sidebar ---\nst.sidebar.title(\"Controls\")\n# Sidebar button logic to clear chat\nif\nst.sidebar.button(\"🧹 Clear Chat\n& Start New Thread\"):\n if\n chat_client and\n thread_id:\n with\n st.spinner(\"Clearing chat\n thread...\"):\n try:\n print(f\"Attempting\n to delete thread: {thread_id}\") # Server log\n chat_client.delete_chat_thread(thread_id=thread_id)\n st.sidebar.success(\"Previous thread deleted.\")\n except ResourceNotFoundError:\n st.sidebar.warning(\"Thread already deleted or not found.\")\n except Exception as e:\n st.sidebar.error(f\"Error deleting thread: {e}\")\n st.exception(e)\n\n #\n Clear state, cache, and rerun\n st.session_state.clear()\n initialize_azure_clients_and_thread.clear() #\n Clear cache\n st.rerun() #\n Restart script execution\n else:\n st.sidebar.error(\"Cannot\n clear chat - client/thread not initialized.\")\n\n\n# --- Initialize Session State Variables\n(Safety check) ---\n# Ensures these keys exist after\npotential clearing\nif 'current_summary' not in\nst.session_state: st.session_state.current_summary = None\nif 'messages' not in\nst.session_state: st.session_state.messages = []\nif 'send_button_clicked' not in\nst.session_state: st.session_state.send_button_clicked = False\n
\n
Explanation: This section configures the web page (title, layout), displays introductory text, and then calls the cached initialize_azure_clients_and_thread function to get the necessary client objects and thread ID. It defines the sidebar, including the \"Clear Chat\" button which deletes the ACS thread, clears session state and the resource cache, then forces a rerun. Finally, it ensures essential keys exist in Streamlit's session_state dictionary.
\n
4.6 Main Application Logic and UI
\n
This is the core part that runs only if the initialization is successful. It defines the layout and handles the interactive elements.
\n
# --- Main Application Logic ---\n# Only proceed if initialization was\nsuccessful (clients/thread ready)\nif\nchat_thread_client and not\ninit_error:\n\n # Define layout using columns\n (Chat on left, Summary on right)\n col1, col2 = st.columns([0.6, 0.4])\n\n # --- Column 1: Live Chat\n Interface ---\n with\n col1:\n st.subheader(f\"💬 Live Chat: {TARGET_THREAD_TOPIC}\")\n st.markdown(\"---\")\n\n # --- Display Chat Messages ---\n message_container = st.container(height=600) #\n Scrollable container\n try:\n #\n Fetch latest messages from ACS on every script run\n chat_messages_iterator =\n chat_thread_client.list_messages(results_per_page=50)\n live_messages = list(chat_messages_iterator)\n #\n Store messages in session state for potential use by summary function\n st.session_state.messages =\n live_messages\n\n #\n Render messages within the container\n with message_container:\n if not live_messages:\n st.info(\"No messages yet. Send a message to start!\")\n else:\n #\n Display messages, newest at the bottom\n for msg in reversed(live_messages):\n sender_name =\n msg.sender_display_name or \"Unknown\"\n # Assign avatars based on sender name constants\n if\n sender_name == SENDER_AI_AGENT: avatar = \"🤖\"\n elif\n sender_name == SENDER_USER: avatar = \"👤\"\n elif\n sender_name == SENDER_SYSTEM: avatar = \"⚙️\"\n else: avatar =\"🧑💻\" # Default\n\n # Use Streamlit's chat elements for display\n with\n st.chat_message(name=sender_name, avatar=avatar):\n st.caption(f\"{msg.created_on.strftime('%Y-%m-%d %H:%M:%S')}\") # Timestamp\n st.write(msg.content.message or \"*empty\n message*\") #\n Message content\n\n except\n Exception as\n e: # Handle errors fetching messages\n with message_container:\n st.error(\"Error fetching messages from ACS:\")\n st.exception(e)\n\n st.markdown(\"---\") #\n Separator\n\n # --- Send Message Input Form\n ---\n # Use a form to group input and\n button click\n with\n st.form(key=\"send_message_form\", clear_on_submit=True):\n new_message_content =\n st.text_input(\"Your Message:\", key=\"new_message_input\", placeholder=\"Type\n your message here...\")\n submitted = st.form_submit_button(\"Send Message\")\n if submitted:\n st.session_state.send_button_clicked = True # Flag\n submission\n\n # --- Process message sending\n (Runs after form check) ---\n if\n st.session_state.send_button_clicked:\n st.session_state.send_button_clicked = False # Reset\n flag\n\n if new_message_content: #\n Check if user actually typed something\n #\n Show spinner during processing\n with st.spinner(\"Sending\n message and getting AI response...\"):\n try:\n # 1. Send User message to ACS\n chat_thread_client.send_message(new_message_content,\n sender_display_name=SENDER_USER)\n # 2. Get AI response (calls helper function)\n ai_reply =\n get_ai_agent_response(chat_thread_client)\n # 3. Send AI response back to ACS\n if ai_reply:\n chat_thread_client.send_message(ai_reply, sender_display_name=SENDER_AI_AGENT)\n # 4. Rerun script to refresh chat display\n st.rerun()\n except Exception as e: # Handle\n errors during send/response cycle\n st.error(\"Error during send/response cycle:\")\n st.exception(e)\n else:\n st.warning(\"Please enter a message to send.\")\n\n\n # --- Column 2: AI Summary Area\n ---\n with\n col2:\n st.subheader(\"✨ Conversation Summary\")\n st.markdown(\"---\")\n\n summary_error_message = None #\n Placeholder for specific summary errors\n\n # --- Generate Summary Button\n and Logic ---\n if\n st.button(\"Generate Full\n Conversation Summary\", key=\"generate_summary_btn\"):\n st.session_state.summary_processing = True # Flag\n processing\n st.session_state.current_summary =\n {\"Info\":\"Processing...\"} # Show\n status\n\n #\n Use messages already fetched and stored in session state\n fetched_messages =\n st.session_state.get('messages', [])\n\n if fetched_messages:\n prompt_text = \"\"\n #\n Build prompt text from stored messages (oldest first)\n for msg in reversed(fetched_messages):\n #\n ... (build prompt_text, skipping empty content) ...\n sender_name =\n msg.sender_display_name if msg.sender_display_name else \"Unknown\"\n content =\n msg.content.message if\n msg.content and\n msg.content.message else \"\"\n if content: prompt_text += f\"{sender_name}: {content}\\n\"\n\n\n if not prompt_text.strip():\n st.session_state.current_summary = {\"Info\": \"No\n message content to summarize.\"}\n else:\n #\n Prepare prompt with instructions\n instruction_phrase = 'Summarize the following conversation. Extract the\n main topic, a brief summary, key highlights (1-3 bullet points), and overall\n sentiment. Format clearly with headings: ## Topic, ## Summary, ## Highlights,\n ## Sentiment.\\n\\n'\n full_prompt =\n instruction_phrase + prompt_text.strip()\n\n #\n Call OpenAI within a spinner and try/except block\n with st.spinner(\"Generating\n summary...\"):\n try:\n response =\n openai.ChatCompletion.create( # Or\n Completion API\n engine=AZURE_OPENAI_DEPLOYMENT_NAME,\n messages=[{\"role\":\"user\",\"content\": full_prompt}],\n max_tokens=1024,\n temperature=0.2\n )\n summary_text =\n response['choices'][0]['message']['content']\n # Parse and store result\n st.session_state.current_summary = parse_summary(summary_text)\n # Handle various OpenAI/other errors\n except\n openai.error.AuthenticationError as e: ... # Set\n Error state\n # ... other except blocks ...\n except Exceptionas\n e: ... # Set Error state\n\n else: # No\n messages fetched/stored\n st.session_state.current_summary = {\"Info\": \"Empty\n conversation history.\"}\n\n st.session_state.summary_processing = False # Unflag\n processing\n st.rerun() #\n Rerun to update the summary display area\n\n\n # --- Display Summary (or\n Status) ---\n # Checks the state variable and\n displays Error, Warning, Info, or the parsed summary\n if\n st.session_state.get('current_summary'):\n summary_data =\n st.session_state.current_summary\n #\n --- Handle Error State ---\n if isinstance(summary_data, dict) and \"Error\" in summary_data: ... #\n Display st.error(...)\n #\n --- Handle Warning State ---\n elif isinstance(summary_data, dict) and \"Warning\" in summary_data: ... #\n Display st.warning(...) and partial data\n #\n --- Handle Info State ---\n elif isinstance(summary_data, dict) and \"Info\" in summary_data: ... #\n Display st.info(...)\n #\n --- Handle Successful Summary Display ---\n elif isinstance(summary_data, dict): ... # Display\n formatted markdown sections\n #\n --- Fallback ---\n else: st.error(\"Summary\n data unexpected format.\")\n # --- Default Placeholder Text\n ---\n elif not\n st.session_state.get('summary_processing', False):\n st.info(\"Click\n 'Generate Summary' for an analysis...\")\n\n\n# --- Handle Initialization Errors ---\n# This runs if the main 'if\nchat_thread_client...' block is False\nelif\ninit_error:\n st.error(f\"❌ Application cannot\n start due to initialization errors:\")\n st.error(init_error) # Display\n the error caught during initialization\nelse:\n # Fallback if client is None\n but no specific error was caught\n st.warning(\"⏳ Initializing Azure\n services or waiting for resources...\")\n\n# --- End of Streamlit App Code ---\n
\n
Explanation: This is the main part of the application that runs if the initial Azure setup was successful.
\n
\n
Layout: It defines a two-column layout (st.columns).
\n
Column 1 (Chat):\n
\n
Fetches messages from the ACS thread on every rerun using chat_thread_client.list_messages().
\n
Stores these messages in st.session_state.messages (primarily for the summary function's use).
\n
Displays the messages chronologically in a scrollable container using st.chat_message.
\n
Provides a text input and send button within a st.form.
\n
Handles the form submission by sending the user's message, getting the AI response via the helper function, sending the AI response, and triggering a st.rerun to refresh the display.
\n
\n
\n
Column 2 (Summary):\n
\n
Includes the \"Generate Full Conversation Summary\" button.
\n
When clicked, it takes the messages stored in session state, creates a summarization prompt, calls Azure OpenAI, parses the result using parse_summary, and stores the output (or error/info) in st.session_state.current_summary.
\n
It then conditionally displays the summary information (Topic, Summary, Highlights, Sentiment) or any errors/warnings/info messages based on the content of st.session_state.current_summary.
\n
\n
\n
Initialization Error Handling: The final elif/else catches cases where the chat_thread_client wasn't successfully obtained during startup and displays relevant error or waiting messages.
\n
\n
5. Running the Streamlit Application
\n
With your Azure resources configured, the Python environment set up, and the code saved in app.py (or your chosen filename), you are ready to run the interactive application.
\n
5.1. Run the App:
\n
Open your terminal or command prompt, make sure you are in your project directory (where app.py and .env are located), and execute the command:
\n
streamlit run app.py
\n
5.2. Initial Loading State:
\n
Streamlit will start its server and provide you with a local URL (usually http://localhost:8501). When you open this URL in your web browser, you will first see the application's initial loading state, similar to this image:
\n\n
\n
The page displays the title \"Interactive AI Agent Chat (V3)\" and the initial informational text.
\n
You'll notice a spinner element with text like \"Running: initialize_azure_clients_and_thread()\". This indicates that the application is currently performing the one-time setup for the session.
\n
During this phase (which might take a few moments, especially on the very first run or after clearing the chat), the cached function initialize_azure_clients_and_thread is executing. It connects to Azure Communication Services, creates a user identity and token, establishes a connection to Azure OpenAI, and creates the specific chat thread in ACS, sending the initial messages.
\n
\n
5.3. Interact with the App:
\n
\n
Once the initialization is complete, the spinner will disappear.
\n
The main chat interface will load, displaying the initial \"System\" and \"AI Agent\" messages fetched from the newly created ACS thread.
\n
Type your messages into the input box at the bottom of the chat column and click \"Send Message\". You will see your message appear, followed by a response from the \"AI Agent\" after a brief processing time (indicated by a spinner).
\n
Navigate to the right-hand column and click \"Generate Full Conversation Summary\" anytime to get an AI-powered analysis of the conversation so far.
\n
Use the \"Clear Chat & Start New Thread\" button in the sidebar to delete the current conversation history (by deleting the ACS thread) and start a fresh session.
\n
\n
5.4. Review Output:
\n
The AI's chat responses appear directly in the chat interface. The generated summary appears in the right-hand column after you click the button. Any errors during operation should be displayed within the Streamlit interface. You can also check the terminal where you ran the streamlit run command for any server-side print statements or error logs.
\n\n
6. Conclusion
\n
You've now walked through the process of setting up Azure resources and using Streamlit to build a contact center chat analysis solution! By leveraging Azure Communication Services to access chat data and Azure OpenAI Service for intelligent analysis, you can automate the extraction of valuable insights from user or customer interactions, moving from overwhelming data volume to clear, actionable intelligence.
","kudosSumWeight":0,"postTime":"2025-04-17T00:00:00.016-07:00","images":{"__typename":"AssociatedImageConnection","edges":[{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDE","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWx0R0JOOA?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDI","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVU3R3l3WA?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDM","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLXdDOEhZYQ?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDQ","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTVibEFVeg?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDU","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLUh1aFpjNg?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDY","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLURCakJUSQ?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDc","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWxoZGp2Vw?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDg","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVJPeHpyNA?revision=9\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuM3wyLjF8b3wyNXxfTlZffDk","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTRqNGYwcw?revision=9\"}"}}],"totalCount":9,"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}},"attachments":{"__typename":"AttachmentConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"tags":{"__typename":"TagConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"timeToRead":19,"rawTeaser":"","introduction":"","coverImage":null,"coverImageProperties":{"__typename":"CoverImageProperties","style":"STANDARD","titlePosition":"BOTTOM","altText":""},"currentRevision":{"__ref":"Revision:revision:4403710_9"},"latestVersion":{"__typename":"FriendlyVersion","major":"1","minor":"0"},"metrics":{"__typename":"MessageMetrics","views":284},"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":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}},"customFields":[],"revisions({\"constraints\":{\"isPublished\":{\"eq\":true}},\"first\":1})":{"__typename":"RevisionConnection","totalCount":9}},"Conversation:conversation:4403710":{"__typename":"Conversation","id":"conversation:4403710","solved":false,"topic":{"__ref":"BlogTopicMessage:message:4403710"},"lastPostingActivityTime":"2025-04-17T00:00:00.016-07:00","lastPostTime":"2025-04-17T00:00:00.016-07:00","unreadReplyCount":0,"isSubscribed":false},"ModerationData:moderation_data:4403710":{"__typename":"ModerationData","id":"moderation_data:4403710","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWx0R0JOOA?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWx0R0JOOA?revision=9","title":"image.png","associationType":"BODY","width":1171,"height":647,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVU3R3l3WA?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVU3R3l3WA?revision=9","title":"image.png","associationType":"BODY","width":1610,"height":724,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLXdDOEhZYQ?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLXdDOEhZYQ?revision=9","title":"image.png","associationType":"BODY","width":927,"height":168,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTVibEFVeg?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTVibEFVeg?revision=9","title":"image.png","associationType":"BODY","width":1057,"height":600,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLUh1aFpjNg?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLUh1aFpjNg?revision=9","title":"image.png","associationType":"BODY","width":1072,"height":619,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLURCakJUSQ?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLURCakJUSQ?revision=9","title":"image.png","associationType":"BODY","width":1064,"height":664,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWxoZGp2Vw?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLWxoZGp2Vw?revision=9","title":"image.png","associationType":"BODY","width":1317,"height":589,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVJPeHpyNA?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLVJPeHpyNA?revision=9","title":"image.png","associationType":"BODY","width":940,"height":692,"altText":""},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTRqNGYwcw?revision=9\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS00NDAzNzEwLTRqNGYwcw?revision=9","title":"image.png","associationType":"BODY","width":1222,"height":933,"altText":""},"Revision:revision:4403710_9":{"__typename":"Revision","id":"revision:4403710_9","lastEditTime":"2025-04-12T12:44:39.059-07:00"},"CachedAsset:theme:customTheme1-1747123676492":{"__typename":"CachedAsset","id":"theme:customTheme1-1747123676492","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","defaultMessageFontFamily":"var(--lia-bs-font-family-base)","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-shared/client/components/common/Loading/LoadingDot-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-1745505307000","value":{"title":"Loading..."},"localOverride":false},"CachedAsset:quilt:o365.prod:pages/blogs/BlogMessagePage:board:EducatorDeveloperBlog-1747123674709":{"__typename":"CachedAsset","id":"quilt:o365.prod:pages/blogs/BlogMessagePage:board:EducatorDeveloperBlog-1747123674709","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-components/common/EmailVerification-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/common/EmailVerification-1745505307000","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-pages/blogs/BlogMessagePage-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-pages/blogs/BlogMessagePage-1745505307000","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:1747123608412":{"__typename":"CachedAsset","id":"quiltWrapper:o365.prod:Common:1747123608412","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.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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/common/ActionFeedback-1745505307000","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},"QueryVariables:TopicReplyList:message:4403710:9":{"__typename":"QueryVariables","id":"TopicReplyList:message:4403710:9","value":{"id":"message:4403710","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:component:custom.widget.HeroBanner-en-us-1747150703148":{"__typename":"CachedAsset","id":"component:custom.widget.HeroBanner-en-us-1747150703148","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-us-1747150703148":{"__typename":"CachedAsset","id":"component:custom.widget.MicrosoftFooter-en-us-1747150703148","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/Breadcrumb-1745505307000","value":{"navLabel":"Breadcrumbs","dropdown":"Additional parent page navigation"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBanner-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBanner-1745505307000","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},"CachedAsset:text:en_US-components/messages/MessageView/MessageViewStandard-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageView/MessageViewStandard-1745505307000","value":{"anonymous":"Anonymous","author":"{messageAuthorLogin}","authorBy":"{messageAuthorLogin}","board":"{messageBoardTitle}","replyToUser":" to {parentAuthor}","showMoreReplies":"Show More","replyText":"Reply","repliesText":"Replies","markedAsSolved":"Marked as Solution","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/ThreadedReplyList-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyCallToAction-1745505307000","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},"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: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}}},"CachedAsset:text:en_US-components/community/Navbar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/Navbar-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarHamburgerDropdown-1745505307000","value":{"hamburgerLabel":"Side Menu"},"localOverride":false},"CachedAsset:text:en_US-components/community/BrandLogo-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/BrandLogo-1745505307000","value":{"logoAlt":"Khoros","themeLogoAlt":"Brand Logo"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarTextLinks-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarTextLinks-1745505307000","value":{"more":"More"},"localOverride":false},"CachedAsset:text:en_US-components/authentication/AuthenticationLink-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/authentication/AuthenticationLink-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/nodes/NodeLink-1745505307000","value":{"place":"Place {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCoverImage-1745505307000","value":{"coverImageTitle":"Cover Image"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeTitle-1745505307000","value":{"nodeTitle":"{nodeTitle, select, community {Community} other {{nodeTitle}}} "},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTimeToRead-1745505307000","value":{"minReadText":"{min} MIN READ"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageSubject-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageSubject-1745505307000","value":{"noSubject":"(no subject)"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserLink-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserLink-1745505307000","value":{"authorName":"View Profile: {author}","anonymous":"Anonymous"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserRank-1745505307000","value":{"rankName":"{rankName}","userRank":"Author rank {rankName}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTime-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTime-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBody-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCustomFields-1745505307000","value":{"CustomField.default.label":"Value of {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageRevision-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageRevision-1745505307000","value":{"lastUpdatedDatePublished":"{publishCount, plural, one{Published} other{Updated}} {date}","lastUpdatedDateDraft":"Created {date}","version":"Version {major}.{minor}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/QueryHandler-1745505307000","value":{"title":"Query Handler"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyButton-1745505307000","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-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageAuthorBio-1745505307000","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-components/community/NavbarDropdownToggle-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarDropdownToggle-1745505307000","value":{"ariaLabelClosed":"Press the down arrow to open the menu"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserAvatar-1745505307000","value":{"altText":"{login}'s avatar","altTextGeneric":"User's avatar"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/ranks/UserRankLabel-1745505307000","value":{"altTitle":"Icon for {rankName} rank"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserRegistrationDate-1745505307000","value":{"noPrefix":"{date}","withPrefix":"Joined {date}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeAvatar-1745505307000","value":{"altTitle":"Node avatar for {nodeTitle}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeDescription-1745505307000","value":{"description":"{description}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505307000":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeIcon-1745505307000","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":"educatordeveloperblog","messageSubject":"step-by-step-contact-center-chat-analysis-with-azure-openai--communication-servi","messageId":"4403710"},"buildId":"YK32GCbhJqbL-HLk4DLXM","runtimeConfig":{"buildInformationVisible":false,"logLevelApp":"info","logLevelMetrics":"info","openTelemetryClientEnabled":false,"openTelemetryConfigName":"o365","openTelemetryServiceVersion":"25.3.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/messages/MessageView/MessageViewStandard/MessageViewStandard.tsx","./components/messages/ThreadedReplyList/ThreadedReplyList.tsx","./components/external/components/ExternalComponent.tsx","./components/customComponent/CustomComponentContent/TemplateContent.tsx"],"appGip":true,"scriptLoader":[{"id":"analytics","src":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/pagescripts/1730819800000/analytics.js?page.id=BlogMessagePage&entity.id=board%3Aeducatordeveloperblog&entity.id=message%3A4403710","strategy":"afterInteractive"}]}