Skip to content

Commit 6c73dea

Browse files
committed
Update to isolated worker model
1 parent e6d92ec commit 6c73dea

16 files changed

+135
-109
lines changed

azuredeploy.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@
268268
},
269269
{
270270
"name": "FUNCTIONS_WORKER_RUNTIME",
271-
"value": "dotnet"
271+
"value": "dotnet-isolated"
272272
},
273273
{
274274
"name": "AzureWebJobsFeatureFlags",

src/Azure.AISearch.FunctionApp.DotNet/Azure.AISearch.FunctionApp.csproj

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
<PropertyGroup>
33
<TargetFramework>net8.0</TargetFramework>
44
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
5+
<OutputType>Exe</OutputType>
56
<Nullable>enable</Nullable>
67
<ImplicitUsings>enable</ImplicitUsings>
78
<UserSecretsId>aspnet-Azure.AISearch.FunctionApp-f7443d62-1b7f-4015-9c0f-643ec0cf8bf6</UserSecretsId>
89
</PropertyGroup>
910
<ItemGroup>
1011
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
1112
<PackageReference Include="Azure.Search.Documents" Version="11.5.0-beta.5" />
12-
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
13+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
14+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
15+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.3.2" />
16+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.4" />
1317
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
14-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
15-
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.4.1" />
1618
<PackageReference Include="Microsoft.SemanticKernel" Version="1.17.1" />
1719
</ItemGroup>
1820
<ItemGroup>
@@ -24,6 +26,9 @@
2426
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
2527
</None>
2628
</ItemGroup>
29+
<ItemGroup>
30+
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
31+
</ItemGroup>
2732
<PropertyGroup>
2833
<NoWarn>$(NoWarn);SKEXP0050</NoWarn>
2934
</PropertyGroup>

src/Azure.AISearch.FunctionApp.DotNet/ChunkEmbedPush.cs

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1+
using System.Net;
2+
using System.Text.Json;
13
using Azure.AISearch.FunctionApp.Models;
24
using Azure.AISearch.FunctionApp.Services;
35
using Microsoft.AspNetCore.Http;
46
using Microsoft.AspNetCore.Mvc;
5-
using Microsoft.Azure.WebJobs;
6-
using Microsoft.Azure.WebJobs.Extensions.Http;
7+
using Microsoft.Azure.Functions.Worker;
8+
using Microsoft.Azure.Functions.Worker.Http;
79
using Microsoft.Extensions.Configuration;
810
using Microsoft.Extensions.Logging;
9-
using Newtonsoft.Json;
1011

1112
namespace Azure.AISearch.FunctionApp;
1213

1314
public class ChunkEmbedPush
1415
{
16+
private readonly ILogger logger;
1517
private readonly AppSettings settings;
1618
private readonly SemanticKernelChunkingService chunkingService;
1719
private readonly AzureOpenAIEmbeddingService embeddingService;
1820
private readonly AzureCognitiveSearchService searchService;
1921

20-
public ChunkEmbedPush(IConfiguration configuration, SemanticKernelChunkingService chunkingService, AzureOpenAIEmbeddingService embeddingService, AzureCognitiveSearchService searchService)
22+
public ChunkEmbedPush(ILoggerFactory loggerFactory, IConfiguration configuration, SemanticKernelChunkingService chunkingService, AzureOpenAIEmbeddingService embeddingService, AzureCognitiveSearchService searchService)
2123
{
22-
this.settings = configuration.Get<AppSettings>();
24+
this.logger = loggerFactory.CreateLogger<ChunkEmbedPush>();
25+
var settings = configuration.Get<AppSettings>();
26+
ArgumentNullException.ThrowIfNull(settings);
27+
this.settings = settings;
2328
this.chunkingService = chunkingService;
2429
this.embeddingService = embeddingService;
2530
this.searchService = searchService;
2631
}
2732

28-
[FunctionName(nameof(ChunkEmbedPush))]
29-
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "POST")] HttpRequest request, ILogger log)
33+
[Function(nameof(ChunkEmbedPush))]
34+
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "POST")] HttpRequest request)
3035
{
31-
log.LogInformation("Skill request received");
36+
this.logger.LogInformation("Skill request received");
3237

3338
// Use basic API key authentication for demo purposes to avoid a dependency on the Function App keys.
3439
if (!string.IsNullOrWhiteSpace(this.settings.TextEmbedderFunctionApiKey))
@@ -41,62 +46,65 @@ public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous,
4146
}
4247

4348
// Get the skill request.
44-
var skillRequestJson = await request.ReadAsStringAsync();
45-
var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(skillRequestJson);
4649
var skillResponse = new SkillResponse();
47-
48-
if (skillRequest?.Values != null)
50+
using var bodyReader = new StreamReader(request.Body);
51+
var skillRequestJson = await bodyReader.ReadToEndAsync();
52+
if (!string.IsNullOrWhiteSpace(skillRequestJson))
4953
{
50-
// Process all records in the request.
51-
foreach (var record in skillRequest.Values)
54+
var skillRequest = JsonSerializer.Deserialize<SkillRequest>(skillRequestJson);
55+
if (skillRequest?.Values != null)
5256
{
53-
log.LogInformation($"Processing record \"{record.RecordId}\" with document id \"{record.Data.DocumentId}\" and filepath \"{record.Data.FilePath}\".");
54-
var responseRecord = new SkillResponseRecord
57+
// Process all records in the request.
58+
foreach (var record in skillRequest.Values)
5559
{
56-
RecordId = record.RecordId
57-
};
58-
skillResponse.Values.Add(responseRecord);
59-
60-
// Use default settings if not specified in the request.
61-
record.Data.NumTokens = record.Data.NumTokens ?? this.settings.TextEmbedderNumTokens ?? 2048;
62-
record.Data.TokenOverlap = record.Data.TokenOverlap ?? this.settings.TextEmbedderTokenOverlap ?? 0;
63-
record.Data.MinChunkSize = record.Data.MinChunkSize ?? this.settings.TextEmbedderMinChunkSize ?? 10;
64-
record.Data.EmbeddingDeploymentName = record.Data.EmbeddingDeploymentName ?? this.settings.OpenAIEmbeddingDeployment ?? throw new InvalidOperationException("No embedding deployment name specified.");
60+
this.logger.LogInformation($"Processing record \"{record.RecordId}\" with document id \"{record.Data.DocumentId}\" and filepath \"{record.Data.FilePath}\".");
61+
var responseRecord = new SkillResponseRecord
62+
{
63+
RecordId = record.RecordId
64+
};
65+
skillResponse.Values.Add(responseRecord);
6566

66-
if (!string.IsNullOrWhiteSpace(record.Data.Text))
67-
{
68-
// Generate chunks for the text in the record.
69-
log.LogInformation($"Chunking to {record.Data.NumTokens} tokens (min chunk size is {record.Data.MinChunkSize}, token overlap is {record.Data.TokenOverlap}).");
70-
var chunks = this.chunkingService.GetChunks(record.Data);
71-
var chunksToProcess = chunks.Where(c => this.chunkingService.EstimateChunkSize(c) >= record.Data.MinChunkSize).ToList();
72-
responseRecord.Data.SkippedChunks = chunks.Count - chunksToProcess.Count;
73-
log.LogInformation($"Skipping {responseRecord.Data.SkippedChunks} chunk(s) with an estimated token size below the minimum chunk size.");
67+
// Use default settings if not specified in the request.
68+
record.Data.NumTokens = record.Data.NumTokens ?? this.settings.TextEmbedderNumTokens ?? 2048;
69+
record.Data.TokenOverlap = record.Data.TokenOverlap ?? this.settings.TextEmbedderTokenOverlap ?? 0;
70+
record.Data.MinChunkSize = record.Data.MinChunkSize ?? this.settings.TextEmbedderMinChunkSize ?? 10;
71+
record.Data.EmbeddingDeploymentName = record.Data.EmbeddingDeploymentName ?? this.settings.OpenAIEmbeddingDeployment ?? throw new InvalidOperationException("No embedding deployment name specified.");
7472

75-
log.LogInformation($"Generating embeddings for {chunks.Count} chunk(s) using deployment \"{record.Data.EmbeddingDeploymentName}\".");
76-
var index = 0;
77-
var documentChunks = new List<DocumentChunk>();
78-
foreach (var chunk in chunks)
73+
if (!string.IsNullOrWhiteSpace(record.Data.Text))
7974
{
80-
// For each chunk, generate an embedding.
81-
var embedding = await this.embeddingService.GetEmbeddingAsync(record.Data.EmbeddingDeploymentName, chunk);
75+
// Generate chunks for the text in the record.
76+
this.logger.LogInformation($"Chunking to {record.Data.NumTokens} tokens (min chunk size is {record.Data.MinChunkSize}, token overlap is {record.Data.TokenOverlap}).");
77+
var chunks = this.chunkingService.GetChunks(record.Data);
78+
var chunksToProcess = chunks.Where(c => this.chunkingService.EstimateChunkSize(c) >= record.Data.MinChunkSize).ToList();
79+
responseRecord.Data.SkippedChunks = chunks.Count - chunksToProcess.Count;
80+
this.logger.LogInformation($"Skipping {responseRecord.Data.SkippedChunks} chunk(s) with an estimated token size below the minimum chunk size.");
8281

83-
// For each chunk with its embedding, create a document to be stored in the search index.
84-
var documentChunk = new DocumentChunk
82+
this.logger.LogInformation($"Generating embeddings for {chunks.Count} chunk(s) using deployment \"{record.Data.EmbeddingDeploymentName}\".");
83+
var index = 0;
84+
var documentChunks = new List<DocumentChunk>();
85+
foreach (var chunk in chunks)
8586
{
86-
Id = $"{record.Data.DocumentId}-{index}",
87-
Content = chunk,
88-
ContentVector = embedding,
89-
SourceDocumentId = record.Data.DocumentId,
90-
SourceDocumentTitle = record.Data.Title,
91-
SourceDocumentFilePath = record.Data.FilePath
92-
};
93-
documentChunks.Add(documentChunk);
94-
index++;
95-
}
87+
// For each chunk, generate an embedding.
88+
var embedding = await this.embeddingService.GetEmbeddingAsync(record.Data.EmbeddingDeploymentName, chunk);
89+
90+
// For each chunk with its embedding, create a document to be stored in the search index.
91+
var documentChunk = new DocumentChunk
92+
{
93+
Id = $"{record.Data.DocumentId}-{index}",
94+
Content = chunk,
95+
ContentVector = embedding,
96+
SourceDocumentId = record.Data.DocumentId,
97+
SourceDocumentTitle = record.Data.Title,
98+
SourceDocumentFilePath = record.Data.FilePath
99+
};
100+
documentChunks.Add(documentChunk);
101+
index++;
102+
}
96103

97-
// Store the document chunks in the search index.
98-
log.LogInformation($"Uploading {documentChunks.Count} document chunk(s) to search service.");
99-
await this.searchService.UploadDocumentChunksAsync(record.Data.DocumentId, documentChunks);
104+
// Store the document chunks in the search index.
105+
this.logger.LogInformation($"Uploading {documentChunks.Count} document chunk(s) to search service.");
106+
await this.searchService.UploadDocumentChunksAsync(record.Data.DocumentId, documentChunks);
107+
}
100108
}
101109
}
102110
}

src/Azure.AISearch.FunctionApp.DotNet/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS installer-env
1+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS installer-env
22

33
COPY . /src/dotnet-function-app
44
RUN cd /src/dotnet-function-app && \
55
mkdir -p /home/site/wwwroot && \
66
dotnet publish *.csproj --output /home/site/wwwroot
77

88
# To enable ssh & remote debugging on app service change the base image to the one below
9-
# FROM mcr.microsoft.com/azure-functions/dotnet:4-appservice
10-
FROM mcr.microsoft.com/azure-functions/dotnet:4
9+
# FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4.0-dotnet-isolated8.0-appservice
10+
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0
1111
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
1212
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
1313

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillRequest
66
{
7-
[JsonProperty("values")]
7+
[JsonPropertyName("values")]
88
public IList<SkillRequestRecord> Values { get; set; } = new List<SkillRequestRecord>();
99
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillRequestRecord
66
{
7-
[JsonProperty("recordId")]
7+
[JsonPropertyName("recordId")]
88
public string? RecordId { get; set; }
99

10-
[JsonProperty("data")]
10+
[JsonPropertyName("data")]
1111
public SkillRequestRecordData Data { get; set; } = new SkillRequestRecordData();
1212
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillRequestRecordData
66
{
7-
[JsonProperty("document_id")]
7+
[JsonPropertyName("document_id")]
88
public string? DocumentId { get; set; }
99

10-
[JsonProperty("text")]
10+
[JsonPropertyName("text")]
1111
public string? Text { get; set; }
1212

13-
[JsonProperty("filepath")]
13+
[JsonPropertyName("filepath")]
1414
public string? FilePath { get; set; }
1515

16-
[JsonProperty("title")]
16+
[JsonPropertyName("title")]
1717
public string? Title { get; set; }
1818

19-
[JsonProperty("fieldname")]
19+
[JsonPropertyName("fieldname")]
2020
public string? FieldName { get; set; }
2121

22-
[JsonProperty("num_tokens")]
22+
[JsonPropertyName("num_tokens")]
2323
public decimal? NumTokens { get; set; }
2424

25-
[JsonProperty("token_overlap")]
25+
[JsonPropertyName("token_overlap")]
2626
public decimal? TokenOverlap { get; set; }
2727

28-
[JsonProperty("min_chunk_size")]
28+
[JsonPropertyName("min_chunk_size")]
2929
public decimal? MinChunkSize { get; set; }
3030

31-
[JsonProperty("embedding_deployment_name")]
31+
[JsonPropertyName("embedding_deployment_name")]
3232
public string? EmbeddingDeploymentName { get; set; }
3333
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillResponse
66
{
7-
[JsonProperty("values")]
7+
[JsonPropertyName("values")]
88
public IList<SkillResponseRecord> Values { get; set; } = new List<SkillResponseRecord>();
99
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillResponseMessage
66
{
7-
[JsonProperty("message")]
7+
[JsonPropertyName("message")]
88
public string? Message { get; set; }
99
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
using Newtonsoft.Json;
1+
using System.Text.Json.Serialization;
22

33
namespace Azure.AISearch.FunctionApp.Models;
44

55
public class SkillResponseRecord
66
{
7-
[JsonProperty("recordId")]
7+
[JsonPropertyName("recordId")]
88
public string? RecordId { get; set; }
99

10-
[JsonProperty("data")]
10+
[JsonPropertyName("data")]
1111
public SkillResponseRecordData Data { get; set; } = new SkillResponseRecordData();
1212

13-
[JsonProperty("errors")]
13+
[JsonPropertyName("errors")]
1414
public IList<SkillResponseMessage> Errors { get; set; } = new List<SkillResponseMessage>();
1515

16-
[JsonProperty("warnings")]
16+
[JsonPropertyName("warnings")]
1717
public IList<SkillResponseMessage> Warnings { get; set; } = new List<SkillResponseMessage>();
1818
}

0 commit comments

Comments
 (0)