Skip to content

Commit f35fddd

Browse files
authored
Merge pull request #44 from h2floh/feature/speech
Adding Speech capability
2 parents 1c3acd4 + f4cb986 commit f35fddd

13 files changed

+427
-476
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,4 +351,7 @@ MigrationBackup/
351351

352352
# Terraform
353353
.terraform
354-
*.tfstate*
354+
*.tfstate*
355+
356+
# AppSettings
357+
appsettings.json

Deploy/DeployInfrastructure.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ $success = $success -and $LASTEXITCODE
6969
# Clean Up
7070
Remove-Item -Path $azureBotRegions
7171

72+
# 2. Retrieve Bot Data from Terraform infrastructure execution
73+
Write-Host "## 2. Retrieve Bot Data from Terraform infrastructure execution"
74+
$Bot = Get-TerraformOutput("bot") | ConvertFrom-Json
75+
$success = $success -and $?
76+
77+
# 3. Retrive DirectlineKey and register it to KV
78+
Write-Host "## 3. Retrive DirectlineKey and register it to KV"
79+
$directline = $(az bot directline show --resource-group $Bot.resource_group --name $Bot.name --with-secrets true) | ConvertFrom-Json
80+
$success = $success -and $?
81+
# create a secret in Key Vault called DirectlineKey
82+
az keyvault secret set --vault-name $Bot.name --name 'DirectlineKey' --value $directline.properties.properties.sites.key > $null
83+
7284
# Check successful execution
7385
Write-ExecutionStatus -success $success
7486
exit $success

Deploy/IaC/regions.tf

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,44 @@ resource "azurerm_cognitive_account" "LUISRegion" {
129129
sku_name = "S0"
130130
}
131131

132+
// For every region add the Speech KEY to KeyVault
133+
resource "azurerm_key_vault_secret" "SpeechKeyRegion" {
134+
for_each = local.azure_bot_regions
135+
136+
name = "SpeechAPIKey${each.key}"
137+
value = azurerm_cognitive_account.SpeechRegion[each.key].primary_access_key
138+
key_vault_id = azurerm_key_vault.GeoBot.id
139+
140+
depends_on = [
141+
azurerm_key_vault_access_policy.currentClient
142+
]
143+
}
144+
145+
// For every region add the Speech endpoint URL to KeyVault
146+
resource "azurerm_key_vault_secret" "SpeechEndpointRegion" {
147+
for_each = local.azure_bot_regions
148+
149+
name = "SpeechAPIHostName${each.key}"
150+
value = azurerm_cognitive_account.SpeechRegion[each.key].endpoint
151+
key_vault_id = azurerm_key_vault.GeoBot.id
152+
153+
depends_on = [
154+
azurerm_key_vault_access_policy.currentClient
155+
]
156+
}
157+
158+
// Create a Speech Endpoint/Key for every Region
159+
resource "azurerm_cognitive_account" "SpeechRegion" {
160+
for_each = local.azure_bot_regions
161+
162+
name = "${var.bot_name}Speech${each.key}"
163+
location = azurerm_resource_group.Region[each.key].location
164+
resource_group_name = azurerm_resource_group.Region[each.key].name
165+
kind = "SpeechServices"
166+
167+
sku {
168+
name = "S0"
169+
tier = "Standard"
170+
}
171+
}
172+

Deploy/RetrieveWebChatLink.ps1

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,27 @@ $success = $True
3232
# Tell who you are (See HelperFunction.ps1)
3333
Write-WhoIAm
3434

35-
# 1. Export SSL Certificate
35+
# 1. Retrieve Bot Data from Terraform infrastructure execution
3636
Write-Host "## 1. Retrieve Bot Data from Terraform infrastructure execution"
3737
$Bot = Get-TerraformOutput("bot") | ConvertFrom-Json
3838
$success = $success -and $?
3939

40-
# 2. Retrieve DirectLine Secret and generate Link
41-
Write-Host "## 2. Retrieve DirectLine Secret and generate Link"
42-
$directline = $(az bot directline show --resource-group $Bot.resource_group --name $Bot.name --with-secrets true) | ConvertFrom-Json
40+
# 2. Retrieve Bot endpoint information using Azure CLI
41+
Write-Host "## 2. Retrieve Bot endpoint information using Azure CLI"
42+
$botSettings = $(az bot show -n $Bot.name -g $Bot.resource_group) | ConvertFrom-Json
4343
$success = $success -and $?
44-
$queryparams = "?bot=$($Bot.name)&key=$($directline.properties.properties.sites.key)"
45-
$webchathtmlfile = Get-ItemProperty -Path "$(Get-ScriptPath)/../WebChat/index.html"
46-
44+
$endpoint = $botSettings.properties.endpoint
45+
# If this bot using azure web app, we need to replace '/api/messages'
46+
if ($endpoint -like "*/api/messages") {
47+
$endpoint = $endpoint.Replace("/api/messages", "")
48+
}
49+
Write-Host "endpoint: $endpoint"
50+
51+
# 3. Generate Link
52+
Write-Host "## 3. Generate Link"
4753
Write-Host -ForegroundColor Green "`n`n### If you were lucky and there were no errors in between your Geo Distributed Bot is ready!`n### If you are just testing you can use this link to open a WebChat to your Bot from any browser.`n### E.g. if you want to test it from different VM's or VPN connections."
48-
Write-Host -ForegroundColor Red "### Do not use this link if you want to go to production since the Directline Key will get exposed on the network (query params are not encrypted):"
49-
Write-Host -ForegroundColor Red "### https://h2floh.github.io/GeoDistributedAzureBot/WebChat/index.html$queryparams"
50-
Write-Host -ForegroundColor Yellow "###`n### Use this link on your local computer (if you cloned the repo to your local computer) in order to not expose your Directline Key:"
51-
Write-Host -ForegroundColor Yellow "### $($webchathtmlfile.FullName)$queryparams"
54+
Write-Host -ForegroundColor Yellow "###`n### Use this link with your browser"
55+
Write-Host -ForegroundColor Yellow "### $endpoint/"
5256

5357
# Return execution status
5458
Write-ExecutionStatus -success $success
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace GeoBot.Controllers
8+
{
9+
[ApiController]
10+
public class DirectlineController : ControllerBase
11+
{
12+
private readonly Directline directline;
13+
public DirectlineController(Directline directline)
14+
{
15+
this.directline = directline;
16+
}
17+
18+
[Route("directline/token")]
19+
[HttpGet]
20+
public async Task<DirectlineToken> GetAsync()
21+
{
22+
DirectlineToken directlineToken = new DirectlineToken
23+
{
24+
token = await directline.GetDirectlineToken()
25+
};
26+
27+
return directlineToken;
28+
}
29+
}
30+
31+
public class DirectlineToken
32+
{
33+
public string token { get; set; }
34+
}
35+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace GeoBot.Controllers
8+
{
9+
// This ASP Controller is created to handle healthcheck requests from TrafficManager.
10+
[ApiController]
11+
public class SpeechController : ControllerBase
12+
{
13+
private readonly Speech speech;
14+
15+
public SpeechController(Speech speech)
16+
{
17+
this.speech = speech;
18+
}
19+
20+
[Route("speech/token")]
21+
[HttpGet]
22+
public async Task<SpeechToken> GetTokenAsync()
23+
{
24+
SpeechToken speechToken = new SpeechToken
25+
{
26+
token = await speech.GetSpeechToken(),
27+
region = speech.GetSpeechRegion()
28+
};
29+
30+
return speechToken;
31+
}
32+
}
33+
34+
public class SpeechToken
35+
{
36+
public string token { get; set; }
37+
public string region { get; set; }
38+
}
39+
40+
}

GeoBot/GeoBot/Directline.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Microsoft.Extensions.Configuration;
2+
using Microsoft.Extensions.Logging;
3+
using Newtonsoft.Json;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Http;
8+
using System.Net.Http.Headers;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace GeoBot
13+
{
14+
public class Directline
15+
{
16+
protected readonly IConfiguration configuration;
17+
protected readonly ILogger logger;
18+
private static readonly HttpClient httpClient = new HttpClient();
19+
20+
public Directline(IConfiguration configuration, ILogger<Directline> logger)
21+
{
22+
this.configuration = configuration;
23+
this.logger = logger;
24+
}
25+
26+
public async Task<string> GetDirectlineToken()
27+
{
28+
// Check on Speech Endpoint
29+
var directlineIsConfigured = !string.IsNullOrEmpty(configuration["DirectlineKey"]);
30+
if (directlineIsConfigured)
31+
{
32+
var directlineKey = configuration["DirectlineKey"];
33+
var directlineUrl = $"https://directline.botframework.com/v3/directline/tokens/generate";
34+
35+
using (var client = new HttpClient())
36+
{
37+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, directlineUrl);
38+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", directlineKey);
39+
var userId = $"dl_{Guid.NewGuid()}";
40+
41+
request.Content = new StringContent(
42+
JsonConvert.SerializeObject(
43+
new { User = new { Id = userId } }),
44+
Encoding.UTF8,
45+
"application/json");
46+
47+
var result = await client.SendAsync(request);
48+
string token = String.Empty;
49+
50+
if (result.IsSuccessStatusCode)
51+
{
52+
var body = await result.Content.ReadAsStringAsync();
53+
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
54+
55+
return token;
56+
}
57+
else
58+
{
59+
return "There is something wrong when issue the directline token";
60+
}
61+
}
62+
}
63+
else
64+
{
65+
// In Case Directline is not configured
66+
return null;
67+
}
68+
}
69+
}
70+
}

GeoBot/GeoBot/Healthcheck.cs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Net;
55
using System.Net.Http;
6+
using System.Net.Http.Headers;
67
using System.Security.Policy;
78
using System.Text;
89
using System.Text.RegularExpressions;
@@ -11,6 +12,7 @@
1112
using Microsoft.Azure.Cosmos;
1213
using Microsoft.Extensions.Configuration;
1314
using Microsoft.Extensions.Logging;
15+
using Newtonsoft.Json;
1416

1517
namespace GeoBot
1618
{
@@ -39,9 +41,14 @@ public async Task CheckHealthAsync(HttpResponse response)
3941
var cosmosDBresult = await CheckCosmosDB();
4042
response.Headers.Add("CosmosDBInnerStatusCode", cosmosDBresult.StatusCode);
4143
response.Headers.Add("CosmosDBInnerStatusReason", cosmosDBresult.StatusMessage);
44+
// Check Speech
45+
var speechResult = await CheckSpeech();
46+
response.Headers.Add("SpeechInnerStatusCode", speechResult.StatusCode);
47+
response.Headers.Add("SpeechInnerStatusReason", speechResult.StatusMessage);
4248

4349
// Check overall success
44-
if (lUISresult.Success && cosmosDBresult.Success) {
50+
if (lUISresult.Success && cosmosDBresult.Success && speechResult.Success)
51+
{
4552
response.StatusCode = 200;
4653
}
4754
}
@@ -85,12 +92,12 @@ private async Task<HealthcheckResult> CheckCosmosDB()
8592
// read container - to check if until container level all is working
8693
using (CosmosClient client = new CosmosClient(configuration["CosmosDBStateStoreEndpoint"], configuration["CosmosDBStateStoreKey"]))
8794
{
88-
95+
8996
var container = client.GetContainer(configuration["CosmosDBStateStoreDatabaseId"],
9097
configuration["CosmosDBStateStoreCollectionId"]);
91-
98+
9299
var readContainer = await container.ReadContainerAsync();
93-
100+
94101
// Return Success
95102
return new HealthcheckResult(true, readContainer.StatusCode.ToString(), "CosmosDB account, database and container accessible");
96103
}
@@ -113,7 +120,7 @@ private async Task<HealthcheckResult> CheckCosmosDB()
113120

114121
return new HealthcheckResult(false, statusCode, e.Message);
115122
}
116-
123+
117124
}
118125
else
119126
{
@@ -122,6 +129,55 @@ private async Task<HealthcheckResult> CheckCosmosDB()
122129
}
123130
}
124131

132+
private async Task<HealthcheckResult> CheckSpeech()
133+
{
134+
// Check on LUIS Endpoint
135+
var speechKeyKey = "SpeechAPIKey" + configuration["region"];
136+
var speechHostNameKey = "SpeechAPIHostName" + configuration["region"];
137+
138+
var speechIsConfigured = !string.IsNullOrEmpty(configuration[speechKeyKey]) && !string.IsNullOrEmpty(configuration[speechHostNameKey]);
139+
if (speechIsConfigured)
140+
{
141+
try
142+
{
143+
// Get Speech service token
144+
Speech speech = new Speech(configuration, null);
145+
var speechTokenResult = await speech.GetSpeechToken();
146+
147+
var speechUrl = $"https://{configuration["region"]}.tts.speech.microsoft.com/cognitiveservices/voices/list";
148+
149+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", speechTokenResult);
150+
var requestMessage = new HttpRequestMessage(HttpMethod.Get, speechUrl);
151+
var responseMessage = await httpClient.SendAsync(requestMessage);
152+
153+
return new HealthcheckResult(responseMessage.IsSuccessStatusCode, responseMessage.StatusCode.ToString(), responseMessage.ReasonPhrase);
154+
}
155+
catch (Exception e)
156+
{
157+
// Return failure
158+
var statusCode = "503";
159+
// Try to extract status code from expeption message: Response status code does not indicate success: 401
160+
try
161+
{
162+
var rx = new Regex(@"Response status code does not indicate success: (\d{3})");
163+
Match match = rx.Match(e.Message);
164+
if (match.Success)
165+
{
166+
statusCode = match.Groups[1].Value;
167+
}
168+
}
169+
catch { }
170+
171+
return new HealthcheckResult(false, statusCode, e.Message);
172+
}
173+
}
174+
else
175+
{
176+
return new HealthcheckResult(true, "200", "Speech not configured");
177+
}
178+
179+
}
180+
125181
// Data Object
126182
internal class HealthcheckResult
127183
{
@@ -137,3 +193,10 @@ internal HealthcheckResult(bool success, string statusCode, string statusMessage
137193
}
138194
}
139195
}
196+
197+
public class DirectLineToken
198+
{
199+
public string conversationId { get; set; }
200+
public string token { get; set; }
201+
public int expires_in { get; set; }
202+
}

0 commit comments

Comments
 (0)