Skip to content

Commit 293e920

Browse files
author
David Sanchez (MSFT)
authored
Merge pull request microsoft#23 from fpelaez/fix/petfunct-SignalR
fix az function to use SignalR and complete the steps to run it locally
2 parents efc0340 + 5b0b726 commit 293e920

File tree

4 files changed

+187
-39
lines changed

4 files changed

+187
-39
lines changed

Documents/AzureFunction.md

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ Open the solution `SmartHotel360.Website.sln` and open the project `SmartHotel36
1818

1919
You can deploy these resources automatically with the [Azure Deployment Guide.](AzureDeployment.md#Creating-The-Azure-Resources)
2020

21+
Once the resources are created you need to do some manual steps:
22+
23+
1. Create a blob storage container called `pets` in the storage account and ensure it has public access
24+
25+
![Public Blob Storage](./Images/blob.png)
26+
27+
2. [Create a database](https://docs.microsoft.com/en-us/azure/cosmos-db/create-sql-api-dotnet#add-a-collection) called `pets` in Cosmos DB. Then add a collection called `checks`.
28+
29+
![Cosmos DB Collection](./Images/collection.png)
30+
2131
## Deploy to Azure
2232

2333
To deploy the Azure Function just publish it from Visual Studio following the instructions in the "Publish to Azure" section of the [Azure Function Documentation](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs).
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,218 @@
1+
using Gremlin.Net.Driver;
2+
using Gremlin.Net.Structure.IO.GraphSON;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.Azure.Documents;
5+
using Microsoft.Azure.Documents.Client;
6+
using Microsoft.Azure.WebJobs;
7+
using Microsoft.Azure.WebJobs.Extensions.Http;
8+
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
9+
using Microsoft.Azure.WebJobs.Host;
10+
using Microsoft.ProjectOxford.Vision;
111
using System;
12+
using System.Collections.Generic;
213
using System.IO;
314
using System.Linq;
415
using System.Net.Http;
516
using System.Threading.Tasks;
6-
using Microsoft.Azure.WebJobs;
7-
using Microsoft.Azure.Documents;
8-
using System.Collections.Generic;
9-
using Microsoft.Azure.WebJobs.Host;
10-
using Microsoft.ProjectOxford.Vision;
11-
using Microsoft.Azure.Documents.Client;
17+
1218

1319
namespace PetCheckerFunction
1420
{
1521
public static class PetChecker
1622
{
1723
[FunctionName("PetChecker")]
18-
public static async Task RunPetChecker([CosmosDBTrigger("pets", "checks", ConnectionStringSetting = "constr", CreateLeaseCollectionIfNotExists = true)] IReadOnlyList<Document> document, TraceWriter log)
24+
public static async Task RunPetChecker(
25+
[CosmosDBTrigger("pets", "checks", ConnectionStringSetting = "constr",
26+
CreateLeaseCollectionIfNotExists=true)] IReadOnlyList<Document> document,
27+
[SignalR(HubName = "petcheckin", ConnectionStringSetting = "AzureSignalRConnectionString")] IAsyncCollector<SignalRMessage> sender,
28+
TraceWriter log)
1929
{
20-
var httpClient = new HttpClient();
30+
var sendingResponse = false;
2131
try
2232
{
2333
foreach (dynamic doc in document)
2434
{
35+
sendingResponse = false;
2536
var isProcessed = doc.IsApproved != null;
2637
if (isProcessed)
2738
{
2839
continue;
2940
}
30-
var url = doc.MediaUrl;
41+
42+
var url = doc.MediaUrl.ToString();
3143
var uploaded = (DateTime)doc.Created;
3244
log.Info($">>> Processing image in {url} upladed at {uploaded.ToString()}");
33-
var res = await httpClient.GetAsync(url);
34-
using (var stream = await res.Content.ReadAsStreamAsync() as Stream)
45+
46+
using (var httpClient = new HttpClient())
3547
{
48+
49+
var res = await httpClient.GetAsync(url);
50+
var stream = await res.Content.ReadAsStreamAsync() as Stream;
3651
log.Info($"--- Image succesfully downloaded from storage");
37-
(bool allowed, string message) = await PassesImageModerationAsync(stream, log);
52+
var (allowed, message, tags) = await PassesImageModerationAsync(stream, log);
3853
log.Info($"--- Image analyzed. It was {(allowed ? string.Empty : "NOT")} approved");
3954
doc.IsApproved = allowed;
4055
doc.Message = message;
4156
log.Info($"--- Updating CosmosDb document to have historical data");
4257
await UpsertDocument(doc, log);
43-
log.Info($"<<< Image in {url} processed!");
58+
log.Info($"--- Updating Graph");
59+
await InsertInGraph(tags, doc, log);
60+
log.Info("--- Sending SignalR response.");
61+
sendingResponse = true;
62+
await SendSignalRResponse(sender, allowed, message);
63+
log.Info($"<<< Done! Image in {url} processed!");
4464
}
4565
}
4666
}
47-
finally
67+
catch (Exception ex)
4868
{
49-
httpClient?.Dispose();
69+
var msg = $"Error {ex.Message} ({ex.GetType().Name})";
70+
log.Info("!!! " + msg);
71+
72+
if (ex is AggregateException aggex)
73+
{
74+
foreach (var innex in aggex.InnerExceptions)
75+
{
76+
log.Info($"!!! (inner) Error {innex.Message} ({innex.GetType().Name})");
77+
}
78+
}
79+
80+
if (!sendingResponse)
81+
{
82+
await SendSignalRResponse(sender, false, msg);
83+
}
84+
throw ex;
5085
}
5186
}
52-
private static async Task UpsertDocument(dynamic doc, TraceWriter log)
87+
88+
private static Task SendSignalRResponse(IAsyncCollector<SignalRMessage> sender, bool isOk, string message)
89+
{
90+
return sender.AddAsync(new SignalRMessage()
91+
{
92+
Target = "ProcessDone",
93+
Arguments = new[] { new {
94+
processedAt = DateTime.UtcNow,
95+
accepted = isOk,
96+
message
97+
}}
98+
});
99+
100+
}
101+
102+
private static async Task InsertInGraph(IEnumerable<string> tags, dynamic doc, TraceWriter log)
103+
{
104+
var hostname = await GetSecret("gremlin_endpoint");
105+
var port = await GetSecret("gremlin_port");
106+
var database = "pets";
107+
var collection = "checks";
108+
var authKey = Environment.GetEnvironmentVariable("gremlin_key");
109+
var portToUse = 443;
110+
portToUse = int.TryParse(port, out portToUse) ? portToUse : 443;
111+
112+
var gremlinServer = new GremlinServer(hostname, portToUse, enableSsl: true,
113+
username: "/dbs/" + database + "/colls/" + collection,
114+
password: authKey);
115+
var gremlinClient = new GremlinClient(gremlinServer, new GraphSON2Reader(), new GraphSON2Writer(), GremlinClient.GraphSON2MimeType);
116+
foreach (var tag in tags)
117+
{
118+
log.Info("--- --- Checking vertex for tag " + tag);
119+
await TryAddTag(gremlinClient, tag, log);
120+
}
121+
122+
var queries = AddPetToGraphQueries(doc, tags);
123+
log.Info("--- --- Adding vertex for pet checkin ");
124+
foreach (string query in queries)
125+
{
126+
await gremlinClient.SubmitAsync<dynamic>(query);
127+
}
128+
}
129+
130+
private static async Task TryAddTag(GremlinClient gremlinClient, string tag, TraceWriter log)
53131
{
54-
var endpoint = Environment.GetEnvironmentVariable("cosmos_uri");
55-
var auth = Environment.GetEnvironmentVariable("cosmos_key");
56-
using (var client = new DocumentClient(new Uri(endpoint), auth))
132+
var query = $"g.V('{tag}')";
133+
var response = await gremlinClient.SubmitAsync<dynamic>(query);
134+
135+
if (!response.Any())
57136
{
58-
var dbName = "pets";
59-
var colName = "checks";
60-
doc.Analyzed = DateTime.UtcNow;
61-
await client.UpsertDocumentAsync(
62-
UriFactory.CreateDocumentCollectionUri(dbName, colName), doc);
63-
log.Info($"--- CosmosDb document updated.");
137+
log.Info("--- --- Adding vertex for tag " + tag);
138+
await gremlinClient.SubmitAsync<dynamic>(AddTagToGraphQuery(tag));
64139
}
65140
}
66-
public static async Task<(bool, string)> PassesImageModerationAsync(Stream image, TraceWriter log)
141+
142+
private static IEnumerable<string> AddPetToGraphQueries(dynamic doc, IEnumerable<string> tags)
143+
{
144+
var id = doc.id.ToString();
145+
146+
var msg = (doc.Message?.ToString() ?? "").Replace("'", "\'");
147+
148+
yield return $"g.addV('checkin').property('id','{id}').property('description','{msg}')";
149+
foreach (var tag in tags)
150+
{
151+
yield return $"g.V('{id}').addE('seems').to(g.V('{tag}'))";
152+
}
153+
}
154+
155+
private static string AddTagToGraphQuery(string tag) => $"g.addV('tag').property('id', '{tag}').property('value', '{tag}')";
156+
157+
private static async Task UpsertDocument(dynamic doc, TraceWriter log)
158+
{
159+
var endpoint = await GetSecret("cosmos_uri");
160+
var auth = await GetSecret("cosmos_key");
161+
162+
var client = new DocumentClient(new Uri(endpoint), auth);
163+
var dbName = "pets";
164+
var colName = "checks";
165+
doc.Analyzed = DateTime.UtcNow;
166+
await client.UpsertDocumentAsync(
167+
UriFactory.CreateDocumentCollectionUri(dbName, colName), doc);
168+
log.Info($"--- CosmosDb document updated.");
169+
}
170+
171+
private static async Task<string> GetSecret(string secretName)
67172
{
68-
log.Info("--- Creating VisionApi client and analyzing image");
69-
var key = Environment.GetEnvironmentVariable("MicrosoftVisionApiKey");
70-
var endpoint = Environment.GetEnvironmentVariable("MicrosoftVisionApiEndpoint");
71-
var client = new VisionServiceClient(key, endpoint);
72-
var features = new VisualFeature[] { VisualFeature.Description };
73-
var result = await client.AnalyzeImageAsync(image, features);
74-
log.Info($"--- Image analyzed with tags: {String.Join(",", result.Description.Tags)}");
75-
if (!int.TryParse(Environment.GetEnvironmentVariable("MicrosoftVisionNumTags"), out var tagsToFetch))
173+
174+
return Environment.GetEnvironmentVariable(secretName);
175+
}
176+
177+
public static async Task<(bool allowd, string message, string[] tags)> PassesImageModerationAsync(Stream image, TraceWriter log)
178+
{
179+
try
76180
{
77-
tagsToFetch = 5;
181+
log.Info("--- Creating VisionApi client and analyzing image");
182+
183+
var key = await GetSecret("MicrosoftVisionApiKey");
184+
var endpoint = await GetSecret("MicrosoftVisionApiEndpoint");
185+
var numTags = await GetSecret("MicrosoftVisionNumTags");
186+
var client = new VisionServiceClient(key, endpoint);
187+
var features = new VisualFeature[] { VisualFeature.Description };
188+
var result = await client.AnalyzeImageAsync(image, features);
189+
190+
log.Info($"--- Image analyzed with tags: {String.Join(",", result.Description.Tags)}");
191+
if (!int.TryParse(numTags, out var tagsToFetch))
192+
{
193+
tagsToFetch = 5;
194+
}
195+
var fetchedTags = result?.Description?.Tags.Take(tagsToFetch).ToArray() ?? new string[0];
196+
bool isAllowed = fetchedTags.Contains("dog");
197+
string message = result?.Description?.Captions.FirstOrDefault()?.Text;
198+
return (isAllowed, message, fetchedTags);
199+
}
200+
catch (Exception ex)
201+
{
202+
log.Info("Vision API error! " + ex.Message);
203+
return (false, "error " + ex.Message, new string[0]);
78204
}
79-
bool isAllowed = result.Description.Tags.Take(tagsToFetch).Contains("dog");
80-
string message = result?.Description?.Captions.FirstOrDefault()?.Text;
81-
return (isAllowed, message);
82205
}
206+
207+
[FunctionName(nameof(SignalRInfo))]
208+
public static IActionResult SignalRInfo(
209+
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequestMessage req,
210+
[SignalRConnectionInfo(HubName = "petcheckin")] SignalRConnectionInfo info)
211+
{
212+
return info != null
213+
? (ActionResult)new OkObjectResult(info)
214+
: new NotFoundObjectResult("Failed to load SignalR Info.");
215+
}
216+
83217
}
84218
}

Source/SmartHotel360.WebsiteFunction/SmartHotel360.WebsiteFunction.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
<None Remove="lib\Microsoft.ProjectOxford.Vision.dll" />
88
</ItemGroup>
99
<ItemGroup>
10-
<PackageReference Include="Gremlin.Net" Version="3.3.4" />
10+
<PackageReference Include="Gremlin.Net" Version="3.3.4" />
11+
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.0" />
1112
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.1.0-preview" />
1213
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.23" />
1314
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.1" />

Source/SmartHotel360.WebsiteFunction/local.settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
"MicrosoftVisionApiEndpoint": "<Cognitive Services Vision API URL (eg. https://southcentralus.api.cognitive.microsoft.com/vision/v1.0)>",
1111
"MicrosoftVisionNumTags": "10",
1212
"AzureSignalRConnectionString": "<Connection String to the SignalR Service instance>"
13+
},
14+
"Host": {
15+
"CORS": "*"
1316
}
1417
}

0 commit comments

Comments
 (0)