Skip to content

Commit

Permalink
Clean up sample code
Browse files Browse the repository at this point in the history
- Inject Kernel and AI services, then just use Kernel throughout, including for embedding generation
- Remove use of Newtonsoft.Json
- Use modern C# syntax, e.g. file-scoped namespaces, primary constructors, top-level statements, etc.
- Fix logging to follow best practices, e.g. the logging string shouldn't vary on a given call site
- Remove unnecessary fields/parameters/etc.
- Address all warnings in project, e.g. nullable reference types
- Fix formatting, e.g. inconsistent whitespace at the start of lines
  • Loading branch information
stephentoub committed May 3, 2024
1 parent 275bd0b commit b30b97e
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 528 deletions.
7 changes: 4 additions & 3 deletions src/ContosoChatAPI/ContosoChatAPI/ContosoChatAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);SKEXP0001;SKEXP0010;SKEXP0040</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -21,9 +22,9 @@
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.1.1" />
<PackageReference Include="Azure.Search.Documents" Version="11.6.0-beta.3" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.39.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.9.1" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Liquid" Version="1.9.0-nightly-240430.6-09778cd6-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Prompty" Version="1.9.0-nightly-240430.6-09778cd6-alpha" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.9.0" />
<PackageReference Include="Microsoft.SemanticKernel.PromptTemplates.Liquid" Version="1.9.0-nightly-240430.6-09778cd6-alpha" />
<PackageReference Include="Microsoft.SemanticKernel.Prompty" Version="1.9.0-nightly-240430.6-09778cd6-alpha" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
using ContosoChatAPI.Services;
using Microsoft.AspNetCore.Mvc;

namespace ContosoChatAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class ContosoChatController : ControllerBase
{

private readonly ILogger<ContosoChatController> _logger;
private readonly ChatService chatService;

public ContosoChatController(ILogger<ContosoChatController> logger, ChatService chatService)
{
_logger = logger;
this.chatService = chatService;
}

namespace ContosoChatAPI.Controllers;

[ApiController]
[Route("[controller]")]
internal sealed class ContosoChatController(ILogger<ContosoChatController> logger, ChatService chatService) : ControllerBase
{
[HttpPost(Name = "PostChatRequest")]
public async Task<string> Post(string customerId, string question, List<string> chatHistory)
{
string result = await chatService.GetResponseAsync(customerId, question, chatHistory.ToList());
_logger.LogInformation(result);
return result;
string result = await chatService.GetResponseAsync(customerId, question, chatHistory);
logger.LogInformation("Result: {Result}", result);
return result;
}
}
}
87 changes: 38 additions & 49 deletions src/ContosoChatAPI/ContosoChatAPI/Data/AISearchData.cs
Original file line number Diff line number Diff line change
@@ -1,63 +1,52 @@
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Models;
using Azure.AI.OpenAI;

namespace ContosoChatAPI.Data
{
public class AISearchData
{
private readonly SearchClient _searchClient;
private readonly SearchIndexClient _searchIndexClient;
readonly string _indexName;
namespace ContosoChatAPI.Data;

public AISearchData(IConfiguration config, SearchClient searchClient, SearchIndexClient searchIndexClient)
{
_indexName = config["AzureAISearch:index_name"];
_searchClient = searchClient;
_searchIndexClient = searchIndexClient;
}
internal sealed class AISearchData(SearchClient searchClient)
{
private readonly SearchClient _searchClient = searchClient;

public async Task<List<Dictionary<string, string>>> RetrieveDocumentationAsync(
string question,
Embeddings embedding)
public async Task<List<Dictionary<string, string?>>> RetrieveDocumentationAsync(string question, ReadOnlyMemory<float> embedding)
{
var searchOptions = new SearchOptions
{
var searchOptions = new SearchOptions
VectorSearch = new()
{
VectorSearch = new()
Queries =
{
Queries = {
new VectorizedQuery(embedding.Data[0].Embedding.ToArray()) {
KNearestNeighborsCount = 3,
Fields = { "contentVector" } } }
},
Size = 3,
Select = { "id", "title", "content", "url" },
QueryType = SearchQueryType.Semantic,
SemanticSearch = new()
{
SemanticConfigurationName = "default",
QueryCaption = new(QueryCaptionType.Extractive),
QueryAnswer = new(QueryAnswerType.Extractive),
},
};
new VectorizedQuery(embedding)
{
KNearestNeighborsCount = 3,
Fields = { "contentVector" }
}
}
},
Size = 3,
Select = { "id", "title", "content", "url" },
QueryType = SearchQueryType.Semantic,
SemanticSearch = new()
{
SemanticConfigurationName = "default",
QueryCaption = new(QueryCaptionType.Extractive),
QueryAnswer = new(QueryAnswerType.Extractive),
},
};

var results = await _searchClient.SearchAsync<SearchDocument>(question, searchOptions);
var results = await _searchClient.SearchAsync<SearchDocument>(question, searchOptions);

var docs = new List<Dictionary<string, string>>();
await foreach (var doc in results.Value.GetResultsAsync())
var docs = new List<Dictionary<string, string?>>();
await foreach (var doc in results.Value.GetResultsAsync())
{
docs.Add(new()
{
var docInfo = new Dictionary<string, string>
{
{ "id", doc.Document["id"].ToString() },
{ "title", doc.Document["title"].ToString() },
{ "content", doc.Document["content"].ToString() },
{ "url", doc.Document["url"].ToString() }
};
docs.Add(docInfo);
}

return docs;
{ "id", doc.Document["id"].ToString() },
{ "title", doc.Document["title"].ToString() },
{ "content", doc.Document["content"].ToString() },
{ "url", doc.Document["url"].ToString() }
});
}

return docs;
}
}
55 changes: 22 additions & 33 deletions src/ContosoChatAPI/ContosoChatAPI/Data/CustomerData.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,35 @@
using Microsoft.Azure.Cosmos;

namespace ContosoChatAPI.Data
{
public class CustomerData
{
private readonly CosmosClient _cosmosClient;
private readonly ILogger<CustomerData> logger;
private readonly string _databaseName;
private readonly string _containerName;
namespace ContosoChatAPI.Data;

public CustomerData(CosmosClient cosmosClient, ILogger<CustomerData> logger, IConfiguration config)
{
_cosmosClient = cosmosClient;
this.logger = logger;
_databaseName = config["CosmosDb:databaseName"];
_containerName = config["CosmosDb:containerName"];
}
internal sealed class CustomerData(CosmosClient cosmosClient, ILogger<CustomerData> logger, IConfiguration config)
{
private readonly CosmosClient _cosmosClient = cosmosClient;
private readonly ILogger<CustomerData> logger = logger;
private readonly string _databaseName = config["CosmosDb:databaseName"]!;
private readonly string _containerName = config["CosmosDb:containerName"]!;

public async Task<Dictionary<string, object>> GetCustomerAsync(string customerId)
public async Task<Dictionary<string, object>> GetCustomerAsync(string customerId)
{
try
{
var container = _cosmosClient.GetContainer(_databaseName, _containerName);
var partitionKey = new PartitionKey(customerId);

try
{
var response = await container.ReadItemAsync<Dictionary<string, object>>(customerId, partitionKey);
var customer = response.Resource;
var response = await container.ReadItemAsync<Dictionary<string, object>>(customerId, new PartitionKey(customerId));
var customer = response.Resource;

// Limit orders to the first 2 items
if (customer.ContainsKey("orders") && customer["orders"] is List<object> orders)
{
customer["orders"] = orders.Take(2).ToList();
}

return customer;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
// Limit orders to the first 2 items
if (customer.TryGetValue("orders", out object? orders) && orders is List<object> list)
{
logger.LogError($"Customer with ID {customerId} not found.");
throw;
customer["orders"] = list.Take(2).ToList();
}
}

return customer;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
logger.LogError("Customer with ID {CustomerID} not found.", customerId);
throw;
}
}
}
38 changes: 0 additions & 38 deletions src/ContosoChatAPI/ContosoChatAPI/Data/EmbeddingData.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,54 @@
using System.Text.Json;
using Microsoft.Azure.Cosmos;

namespace ContosoChatAPI.Data
namespace ContosoChatAPI.Data;

internal sealed class GenerateCustomerInfo(ILogger<GenerateCustomerInfo> logger, IConfiguration config, CosmosClient cosmosClient)
{
public class GenerateCustomerInfo
{
private ILogger<GenerateCustomerInfo> _logger;
private IConfiguration _config;
private readonly CosmosClient _cosmosClient;
private readonly string _indexName;
private readonly string _databaseName;
private readonly string _containerName;
private readonly ILogger<GenerateCustomerInfo> _logger = logger;
private readonly CosmosClient _cosmosClient = cosmosClient;
private readonly string _databaseName = config["CosmosDb:databaseName"]!;
private readonly string _containerName = config["CosmosDb:containerName"]!;

public GenerateCustomerInfo(ILogger<GenerateCustomerInfo> logger, IConfiguration config, CosmosClient cosmosClient)
public async Task PopulateCosmosAsync()
{
try
{
_logger = logger;
_config = config;
_cosmosClient = cosmosClient;
_indexName = config["AzureAISearch:index_name"];
_databaseName = config["CosmosDb:databaseName"];
_containerName = config["CosmosDb:containerName"];
}
var database = await _cosmosClient.CreateDatabaseIfNotExistsAsync(_databaseName);
var container = await database.Database.CreateContainerIfNotExistsAsync(_containerName, "/id");

public async Task PopulateCosmosAsync()
{
try
{
var database = await _cosmosClient.CreateDatabaseIfNotExistsAsync(_databaseName);
var container = await database.Database.CreateContainerIfNotExistsAsync(_containerName, "/id");
var numDocs = 0;

var numDocs = 0;
var query = new QueryDefinition("SELECT VALUE COUNT(1) FROM c");
using (var iterator = container.Container.GetItemQueryIterator<int>(query))
{
var result = await iterator.ReadNextAsync();
numDocs = result.FirstOrDefault();
}

var query = new QueryDefinition("SELECT VALUE COUNT(1) FROM c");
using (var iterator = container.Container.GetItemQueryIterator<int>(query))
{
var result = await iterator.ReadNextAsync();
numDocs = result.FirstOrDefault();
}
if (numDocs == 0)
{
_logger.LogInformation("Creating CosmosDB container {ContainerName} in database {DatabaseName}...", _containerName, _databaseName);

if (numDocs == 0)
var jsonFiles = Directory.GetFiles("./Data/sample_data/customer_info", "*.json");
foreach (string file in jsonFiles)
{
_logger.LogInformation($"Creating CosmosDB container {_containerName} in database {_databaseName}...");
var content = await File.ReadAllTextAsync(file);
var customer = JsonSerializer.Deserialize<Dictionary<string, object>>(content)!;

var jsonFiles = Directory.GetFiles("./Data/sample_data/customer_info", "*.json");
foreach (string file in jsonFiles)
{
var content = await File.ReadAllTextAsync(file);
var customer = JsonSerializer.Deserialize<Dictionary<string, object>>(content);
await container.Container.CreateItemStreamAsync(new MemoryStream(Encoding.UTF8.GetBytes(content)), new PartitionKey(customer["id"].ToString()));

await container.Container.CreateItemStreamAsync(new MemoryStream(Encoding.UTF8.GetBytes(content)), new PartitionKey(customer["id"].ToString()));

_logger.LogInformation($"Upserted item with id {customer["id"]}");
}
}
else
{
_logger.LogInformation("CosmosDB container already populated, nothing to do.");
_logger.LogInformation("Upserted item with id {CustomerID}", customer["id"]);
}
}
catch (Exception ex)
else
{
_logger.LogError("Error in Cosmos: " + ex.Message);
_logger.LogInformation("CosmosDB container already populated, nothing to do.");
}
}

catch (Exception ex)
{
_logger.LogError(ex, "Error populating Cosmos");
}
}
}
Loading

0 comments on commit b30b97e

Please sign in to comment.