This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| wiki:ai:chatbot-helpdesk-sentiment [2025/06/24 18:30] – created ddehamer | wiki:ai:chatbot-helpdesk-sentiment [2025/07/02 21:10] (current) – zhein | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== AI Chatbot | + | ====== AI Chatbot |
| ===== Requirements ===== | ===== Requirements ===== | ||
| Line 12: | Line 13: | ||
| ===== Prerequisites ===== | ===== Prerequisites ===== | ||
| + | |||
| Install These | Install These | ||
| - | < | + | |
| + | < | ||
| brew install dotnet-sdk #This will be version 9 which will not work but gives you the rest of the stuff needed. | brew install dotnet-sdk #This will be version 9 which will not work but gives you the rest of the stuff needed. | ||
| brew install azure-cli | brew install azure-cli | ||
| </ | </ | ||
| - | Install this from the webpage | + | Assuming you already have created a Resource Group named don-test-rg, |
| - | [[https:// | + | |
| - | Then these | + | Install this from the webpage and follow directions. [[https:// |
| - | < | + | |
| + | Create a folder in your workspace and save the files at the end of this document. | ||
| + | |||
| + | Open a terminal, set your folder to the one where the files are saved, then run these commands: | ||
| + | |||
| + | These commands are only needed for Windows users: | ||
| + | <code -> | ||
| + | dotnet nuget add source https:// | ||
| + | npm install -g azure-functions-core-tools@4 --unsafe-perm true | ||
| + | dotnet nuget locals all --clear | ||
| + | dotnet restore | ||
| + | </ | ||
| + | |||
| + | The rest of the instructions are for every OS: | ||
| + | |||
| + | < | ||
| dotnet add package Microsoft.Azure.Functions.Worker.Extensions.OpenApi --version 1.4.0 | dotnet add package Microsoft.Azure.Functions.Worker.Extensions.OpenApi --version 1.4.0 | ||
| dotnet --list-sdks | dotnet --list-sdks | ||
| Line 28: | Line 45: | ||
| dotnet add package Azure.AI.OpenAI --version 2.0.0 | dotnet add package Azure.AI.OpenAI --version 2.0.0 | ||
| </ | </ | ||
| + | |||
| + | Create Storage Account | ||
| + | |||
| + | <code -> | ||
| + | az storage account create \ | ||
| + | --name dehamerfuncstorage \ | ||
| + | --location eastus \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --sku Standard_LRS | ||
| + | </ | ||
| + | |||
| + | Create Function Plan for Windows | ||
| + | |||
| + | <code -> | ||
| + | az functionapp plan create \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --name dehamerfuncplan \ | ||
| + | --location eastus \ | ||
| + | --sku B1 \ | ||
| + | --is-linux false | ||
| + | </ | ||
| + | |||
| + | Create Function App | ||
| + | |||
| + | <code -> | ||
| + | az functionapp create \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --plan dehamerfuncplan \ | ||
| + | --storage-account dehamerfunctstorage \ | ||
| + | --runtime dotnet-isolated \ | ||
| + | --functions-version 4 \ | ||
| + | --os-type Windows | ||
| + | </ | ||
| + | |||
| + | Create TexAnalytics | ||
| + | |||
| + | <code -> | ||
| + | az cognitiveservices account create \ | ||
| + | --name dehamersentimentai \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --location eastus \ | ||
| + | --kind TextAnalytics \ | ||
| + | --sku S \ | ||
| + | --custom-domain dehamersentimentai | ||
| + | |||
| + | az cognitiveservices account update \ | ||
| + | --name dehamersentimentai \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --set properties.publicNetworkAccess=Enabled | ||
| + | </ | ||
| + | |||
| + | Get keys and endpoint for export and app settings | ||
| + | |||
| + | <code -> | ||
| + | export TEXT_ANALYTICS_ENDPOINT=`az cognitiveservices account show \ | ||
| + | --name dehamersentimentai \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --query " | ||
| + | |||
| + | export TEXT_ANALYTICS_KEY=`az cognitiveservices account keys list \ | ||
| + | --name dehamersentamentai \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --query " | ||
| + | |||
| + | az functionapp config appsettings set \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --settings \ | ||
| + | TEXT_ANALYTICS_ENDPOINT=$TEXT_ANALYTICS_ENDPOINT \ | ||
| + | TEXT_ANALYTICS_KEY=$TEXT_ANALYTICS_KEY | ||
| + | | ||
| + | export OPENAI_KEY=`az cognitiveservices account keys list \ | ||
| + | --name don-openai-useast \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --query " | ||
| + | |||
| + | export OPENAI_ENDPOINT=https:// | ||
| + | |||
| + | export OPENAI_DEPLOYMENT=gpt-4.1 | ||
| + | |||
| + | az functionapp config appsettings set \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --settings \ | ||
| + | OPENAI_KEY=$OPENAI_KEY \ | ||
| + | OPENAI_ENDPOINT=$OPENAI_ENDPOINT | ||
| + | OPENAI_DEPLOYMENT=$OPENAI_DEPLOYMENT | ||
| + | | ||
| + | #Confirm they were set | ||
| + | az functionapp config appsettings list \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --query " | ||
| + | </ | ||
| + | |||
| + | Create a directory to store the functionapp files | ||
| + | |||
| + | <code -> | ||
| + | mkdir ~/ | ||
| + | cd ~/ | ||
| + | </ | ||
| + | |||
| + | Save the FeedbackFunction.cs, | ||
| + | |||
| + | <code - FeedbackFunction.cs> | ||
| + | using System.Net.Http.Headers; | ||
| + | using System.Text; | ||
| + | using System.Text.Json; | ||
| + | using Azure; | ||
| + | using Azure.AI.TextAnalytics; | ||
| + | using Microsoft.Azure.Functions.Worker; | ||
| + | using Microsoft.Azure.Functions.Worker.Http; | ||
| + | using Microsoft.Extensions.Logging; | ||
| + | |||
| + | public class DeHamerFeedbackFunction | ||
| + | { | ||
| + | private readonly ILogger _logger; | ||
| + | |||
| + | public DeHamerFeedbackFunction(ILoggerFactory loggerFactory) | ||
| + | { | ||
| + | _logger = loggerFactory.CreateLogger< | ||
| + | } | ||
| + | |||
| + | [Function(" | ||
| + | public async Task< | ||
| + | [HttpTrigger(AuthorizationLevel.Anonymous, | ||
| + | HttpRequestData req) | ||
| + | { | ||
| + | _logger.LogInformation(" | ||
| + | |||
| + | var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); | ||
| + | var data = JsonSerializer.Deserialize< | ||
| + | string feedback = data? | ||
| + | |||
| + | // Get Text Analytics config | ||
| + | var textEndpoint = Environment.GetEnvironmentVariable(" | ||
| + | var textKey = Environment.GetEnvironmentVariable(" | ||
| + | |||
| + | _logger.LogInformation(" | ||
| + | |||
| + | var credentials = new AzureKeyCredential(textKey); | ||
| + | var client = new TextAnalyticsClient(new Uri(textEndpoint), | ||
| + | var documentSentiment = await client.AnalyzeSentimentAsync(feedback); | ||
| + | var sentiment = documentSentiment.Value.Sentiment.ToString().ToLower(); | ||
| + | |||
| + | _logger.LogInformation(" | ||
| + | |||
| + | // Compose prompt | ||
| + | string prompt = sentiment switch | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | _ => $" | ||
| + | }; | ||
| + | |||
| + | // OpenAI Setup | ||
| + | var openaiEndpoint = Environment.GetEnvironmentVariable(" | ||
| + | var openaiKey = Environment.GetEnvironmentVariable(" | ||
| + | var deployment = Environment.GetEnvironmentVariable(" | ||
| + | |||
| + | _logger.LogInformation(" | ||
| + | |||
| + | using var httpClient = new HttpClient(); | ||
| + | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(" | ||
| + | |||
| + | var payload = JsonSerializer.Serialize(new | ||
| + | { | ||
| + | messages = new[] | ||
| + | { | ||
| + | new { role = " | ||
| + | new { role = " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | _logger.LogInformation(" | ||
| + | |||
| + | var response = await httpClient.PostAsync( | ||
| + | $" | ||
| + | new StringContent(payload, | ||
| + | |||
| + | if (!response.IsSuccessStatusCode) | ||
| + | { | ||
| + | var errorDetails = await response.Content.ReadAsStringAsync(); | ||
| + | _logger.LogError(" | ||
| + | |||
| + | var errorResponse = req.CreateResponse(System.Net.HttpStatusCode.InternalServerError); | ||
| + | await errorResponse.WriteAsJsonAsync(new { error = " | ||
| + | return errorResponse; | ||
| + | } | ||
| + | |||
| + | var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); | ||
| + | _logger.LogInformation(" | ||
| + | |||
| + | var message = json.RootElement.GetProperty(" | ||
| + | |||
| + | var result = new | ||
| + | { | ||
| + | sentiment = sentiment, | ||
| + | message = message | ||
| + | }; | ||
| + | |||
| + | var responseData = req.CreateResponse(System.Net.HttpStatusCode.OK); | ||
| + | await responseData.WriteAsJsonAsync(result); | ||
| + | return responseData; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code - FeeedbackFunction.csproj> | ||
| + | <Project Sdk=" | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | <None Update=" | ||
| + | < | ||
| + | </ | ||
| + | <None Update=" | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | |||
| + | Make sure this matches the version of dotnet 8.x you install. | ||
| + | |||
| + | <code - global.json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code - host.json> | ||
| + | { | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code - local.settings.json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <code - program.cs> | ||
| + | using Microsoft.Extensions.Hosting; | ||
| + | using Microsoft.Extensions.DependencyInjection; | ||
| + | |||
| + | var host = new HostBuilder() | ||
| + | .ConfigureFunctionsWorkerDefaults() | ||
| + | .Build(); | ||
| + | |||
| + | host.Run(); | ||
| + | </ | ||
| + | |||
| + | From the FeedbackFunctionDonnet directory: | ||
| + | |||
| + | <code -> | ||
| + | dotnet clean | ||
| + | dotnet publish -c Release -o publish | ||
| + | cd publish | ||
| + | zip -r ../ | ||
| + | cd .. | ||
| + | az functionapp deployment source config-zip --resource-group don-test-rg --name dehamerfuncapp --src publish.zip | ||
| + | </ | ||
| + | |||
| + | After running you will see additional files in the FeedbackFunctionDotnet directory. | ||
| + | |||
| + | <code -> | ||
| + | ls | ||
| + | bin | ||
| + | FeedbackFunction.cs | ||
| + | </ | ||
| + | |||
| + | To test if it is working | ||
| + | |||
| + | <code -> | ||
| + | export FUNCTION_KEY=`az functionapp function keys list \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --function-name dehamerfeedbackfunction --query " | ||
| + | |||
| + | curl -X POST https:// | ||
| + | -H " | ||
| + | -H " | ||
| + | -d ' | ||
| + | </ | ||
| + | |||
| + | You should see something like: | ||
| + | |||
| + | <code -> | ||
| + | {" | ||
| + | </ | ||
| + | |||
| + | Confirmation tests | ||
| + | |||
| + | <code -> | ||
| + | az functionapp function list --name dehamerfuncapp --resource-group don-test-rg --query " | ||
| + | Name Status | ||
| + | -------------------------------------- | ||
| + | dehamerfuncapp/ | ||
| + | </ | ||
| + | |||
| + | <code -> | ||
| + | az functionapp show --name dehamerfuncapp --resource-group don-test-rg --query " | ||
| + | [ | ||
| + | " | ||
| + | " | ||
| + | ] | ||
| + | </ | ||
| + | |||
| + | <code -> | ||
| + | az functionapp function list \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --query " | ||
| + | --output tsv | ||
| + | |||
| + | https:// | ||
| + | </ | ||
| + | |||
| + | <code -> | ||
| + | az functionapp function show \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --function-name dehamerfeedbackfunction \ | ||
| + | --query " | ||
| + | |||
| + | false | ||
| + | </ | ||
| + | |||
| + | If true | ||
| + | |||
| + | <code -> | ||
| + | az functionapp function update \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --function-name dehamerfeedbackfunction \ | ||
| + | --set isDisabled=false | ||
| + | </ | ||
| + | |||
| + | Tailing logs if enabled. | ||
| + | |||
| + | <code -> | ||
| + | az webapp log tail \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg | ||
| + | </ | ||
| + | |||
| + | If you want it to autostart after issues. | ||
| + | |||
| + | <code -> | ||
| + | #Optional | ||
| + | az webapp config set \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --always-on true | ||
| + | </ | ||
| + | |||
| + | Get a list of your functionapps | ||
| + | |||
| + | <code -> | ||
| + | az functionapp function list \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --output table | ||
| + | |||
| + | Href InvokeUrlTemplate | ||
| + | -------------------------------------------------------------------------------- | ||
| + | https:// | ||
| + | </ | ||
| + | |||
| + | Only do this for testing. | ||
| + | |||
| + | <code -> | ||
| + | az functionapp cors add \ | ||
| + | --name dehamerfuncapp \ | ||
| + | --resource-group don-test-rg \ | ||
| + | --allowed-origins " | ||
| + | </ | ||
| + | |||
| + | If you have a webserver that you trust, you can create this webpage and test it: | ||
| + | |||
| + | <code - index.html> | ||
| + | < | ||
| + | <html lang=" | ||
| + | < | ||
| + | <meta charset=" | ||
| + | < | ||
| + | < | ||
| + | body { | ||
| + | background-color: | ||
| + | color: red; | ||
| + | font-family: | ||
| + | margin: 40px; | ||
| + | } | ||
| + | |||
| + | header { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | header img { | ||
| + | height: 50px; | ||
| + | margin-right: | ||
| + | } | ||
| + | |||
| + | h1 { | ||
| + | font-size: 2em; | ||
| + | } | ||
| + | |||
| + | textarea { | ||
| + | width: 100%; | ||
| + | height: 100px; | ||
| + | font-size: 1em; | ||
| + | padding: 10px; | ||
| + | } | ||
| + | |||
| + | button { | ||
| + | margin-top: 10px; | ||
| + | padding: 10px 20px; | ||
| + | font-size: 1em; | ||
| + | background-color: | ||
| + | color: white; | ||
| + | border: none; | ||
| + | cursor: pointer; | ||
| + | } | ||
| + | |||
| + | #response { | ||
| + | margin-top: 20px; | ||
| + | font-weight: | ||
| + | white-space: | ||
| + | } | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | < | ||
| + | <img src=" | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | <form id=" | ||
| + | <label for=" | ||
| + | < | ||
| + | <button type=" | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | |||
| + | < | ||
| + | document.getElementById(' | ||
| + | e.preventDefault(); | ||
| + | |||
| + | const feedback = document.getElementById(' | ||
| + | const responseDiv = document.getElementById(' | ||
| + | |||
| + | responseDiv.textContent = " | ||
| + | |||
| + | try { | ||
| + | const res = await fetch(' | ||
| + | method: ' | ||
| + | headers: { | ||
| + | ' | ||
| + | }, | ||
| + | body: JSON.stringify({ feedback }) | ||
| + | }); | ||
| + | |||
| + | if (!res.ok) { | ||
| + | throw new Error(" | ||
| + | } | ||
| + | |||
| + | const data = await res.json(); | ||
| + | responseDiv.innerHTML = `< | ||
| + | } catch (err) { | ||
| + | responseDiv.textContent = " | ||
| + | } | ||
| + | }); | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ===== Dennis Additions ===== | ||
| + | |||
| + | ==== Azure CLI vs Azure Portal Instructions ==== | ||
| + | |||
| + | | Task | Azure CLI Command | Azure Portal Steps | | ||
| + | | Create Resource Group | az group create --name don-test-rg --location eastus | Go to Azure Portal > Resource Groups > Create | | ||
| + | | Create Storage Account | az storage account create --name dehamerfuncstorage --location eastus --resource-group don-test-rg --sku Standard_LRS | Go to Storage Accounts > Create | | ||
| + | | Create Function App | az functionapp create --resource-group don-test-rg --consumption-plan-location eastus --runtime dotnet-isolated --functions-version 4 --name dehamerfuncapp --storage-account dehamerfuncstorage | Go to Function Apps > Create > Choose .NET Isolated | | ||
| + | | Deploy Code via Zip | az functionapp deployment source config-zip --src publish.zip --name dehamerfuncapp --resource-group don-test-rg | Go to Function App > Deployment Center > Zip Deploy | | ||
| + | | Create Cognitive Services Resource | az cognitiveservices account create --name dehamersentimentai --resource-group don-test-rg --kind TextAnalytics --sku S --location eastus --yes | Search ' | ||
| + | | Set App Settings | az functionapp config appsettings set --name dehamerfuncapp --resource-group don-test-rg --settings KEY=value | Go to Configuration > Application Settings > Add New | | ||
| + | |||
| + | ==== Successful Deployment Steps ==== | ||
| + | |||
| + | After resolving early build issues and switching from Linux to a Windows-hosted function plan due to SDK compatibility, | ||
| + | |||
| + | - Rewrote the function class to eliminate naming conflicts\\ - Set AuthorizationLevel to Anonymous to permit open feedback submission\\ - Used curl to verify responses from the deployed function\\ - Created an HTML frontend to POST feedback to the function endpoint | ||
| + | |||
| + | ==== Architectural Diagram ==== | ||
| + | |||
| + | {{: | ||
| + | |||
| + | [[ai_knowledge|AI Knowledge]] | ||
| + | |||