From 49201efc7d745580afff341db9becd64d2b56943 Mon Sep 17 00:00:00 2001
From: Yeojun Han <44177345+yeojunh@users.noreply.github.com>
Date: Fri, 11 Aug 2023 11:07:02 -0700
Subject: [PATCH] Add CRUD API for Copilot comments (#6754)
* Create CRUD API for copilot comments feature
* Delete unused code
* Remove comments
* Update status codes
* Remove unused fields
* Apply review changes
---
.../APIView/APIViewWeb/APIViewWeb.csproj | 3 +
.../Controllers/CopilotCommentsController.cs | 91 +++++++++
.../Managers/CopilotCommentsManager.cs | 193 ++++++++++++++++++
.../Managers/ICopilotCommentsManager.cs | 13 ++
.../APIViewWeb/Models/CopilotCommentModel.cs | 21 ++
.../Repositories/CopilotCommentsRepository.cs | 48 +++++
.../ICopilotCommentsRepository.cs | 21 ++
src/dotnet/APIView/APIViewWeb/Startup.cs | 2 +
8 files changed, 392 insertions(+)
create mode 100644 src/dotnet/APIView/APIViewWeb/Controllers/CopilotCommentsController.cs
create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/CopilotCommentsManager.cs
create mode 100644 src/dotnet/APIView/APIViewWeb/Managers/ICopilotCommentsManager.cs
create mode 100644 src/dotnet/APIView/APIViewWeb/Models/CopilotCommentModel.cs
create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/CopilotCommentsRepository.cs
create mode 100644 src/dotnet/APIView/APIViewWeb/Repositories/ICopilotCommentsRepository.cs
diff --git a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj
index a17d5f2e4d0..9f8e4072bcc 100644
--- a/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj
+++ b/src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj
@@ -23,8 +23,10 @@
+
+
@@ -49,6 +51,7 @@
+
diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/CopilotCommentsController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/CopilotCommentsController.cs
new file mode 100644
index 00000000000..dca7029115c
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Controllers/CopilotCommentsController.cs
@@ -0,0 +1,91 @@
+using System.Text.Json;
+using System.Threading.Tasks;
+using APIViewWeb.Managers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace APIViewWeb.Controllers
+{
+ public class CopilotCommentsController : Controller
+ {
+ private readonly ICopilotCommentsManager _copilotManager;
+ private readonly ILogger _logger;
+
+ public CopilotCommentsController(ICopilotCommentsManager copilotManager, ILogger logger)
+ {
+ _copilotManager = copilotManager;
+ _logger = logger;
+ }
+
+ [HttpPost]
+ public async Task CreateDocument(string badCode, string language, string goodCode = null, string comment = null, string[] guidelineIds = null)
+ {
+ if (badCode == null)
+ {
+ _logger.LogInformation("Request does not have the required badCode field for CREATE.");
+ return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
+ }
+
+ var id = await _copilotManager.CreateDocumentAsync(User.GetGitHubLogin(), badCode, goodCode, language, comment, guidelineIds);
+ _logger.LogInformation("Added a new document to database.");
+ return StatusCode(statusCode: StatusCodes.Status201Created, id);
+
+ }
+
+ [HttpPut]
+ public async Task UpdateDocument(string id, string badCode = null, string language = null, string goodCode = null, string comment = null, string[] guidelineIds = null)
+ {
+ if (id == null)
+ {
+ _logger.LogInformation("Request does not have the required ID field for UPDATE.");
+ return StatusCode(statusCode: StatusCodes.Status500InternalServerError);
+ }
+
+ var result = await _copilotManager.UpdateDocumentAsync(User.GetGitHubLogin(), id, badCode, goodCode, language, comment, guidelineIds);
+ if (result.ModifiedCount > 0)
+ {
+ _logger.LogInformation("Found existing document with ID. Updating document.");
+ var document = await _copilotManager.GetDocumentAsync(id);
+ return Ok(document);
+ } else
+ {
+ _logger.LogInformation("Could not find a match for the given ID.");
+ return StatusCode(statusCode: StatusCodes.Status404NotFound);
+ }
+ }
+
+ [HttpGet]
+ public async Task GetDocument(string id)
+ {
+ if (id == null)
+ {
+ _logger.LogInformation("Request does not have the required ID field for GET.");
+ return StatusCode(statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ var document = await _copilotManager.GetDocumentAsync(id);
+ if (document != null)
+ {
+ return Ok(document);
+ } else
+ {
+ _logger.LogInformation("No document with this id exists in the database.");
+ return StatusCode(statusCode: StatusCodes.Status404NotFound);
+ }
+ }
+
+ [HttpDelete]
+ public async Task DeleteDocument(string id)
+ {
+ if (id == null)
+ {
+ _logger.LogInformation("Request does not have the required ID field for DELETE.");
+ return StatusCode(statusCode: StatusCodes.Status400BadRequest);
+ }
+
+ await _copilotManager.DeleteDocumentAsync(User.GetGitHubLogin(), id);
+ return StatusCode(statusCode: StatusCodes.Status204NoContent);
+ }
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CopilotCommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CopilotCommentsManager.cs
new file mode 100644
index 00000000000..1758d748737
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Managers/CopilotCommentsManager.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Threading.Tasks;
+using APIViewWeb.Models;
+using APIViewWeb.Repositories;
+using Microsoft.Extensions.Configuration;
+using Azure;
+using Azure.AI.OpenAI;
+using MongoDB.Driver;
+using MongoDB.Bson;
+using System.Linq;
+using Newtonsoft.Json;
+
+namespace APIViewWeb.Managers
+{
+ public class CopilotCommentsManager : ICopilotCommentsManager
+ {
+ private readonly ICopilotCommentsRepository _copilotCommentsRepository;
+ private readonly IConfiguration _configuration;
+ private readonly OpenAIClient _openAIClient;
+
+ public CopilotCommentsManager(
+ ICopilotCommentsRepository copilotCommentsRepository,
+ IConfiguration configuration
+ )
+ {
+ _copilotCommentsRepository = copilotCommentsRepository;
+ _configuration = configuration;
+
+ _openAIClient = new OpenAIClient(
+ new Uri(_configuration["OpenAI:Endpoint"]),
+ new AzureKeyCredential(_configuration["OpenAI:Key"]));
+ }
+
+ public async Task CreateDocumentAsync(string user, string badCode, string goodCode, string language, string comment, string[] guidelineIds)
+ {
+ var embedding = await GetEmbeddingsAsync(badCode);
+
+ var document = new CopilotCommentModel()
+ {
+ BadCode = badCode,
+ GoodCode = goodCode,
+ Embedding = embedding,
+ Language = language,
+ Comment = comment,
+ GuidelineIds = guidelineIds,
+ ModifiedOn = DateTime.UtcNow,
+ ModifiedBy = user
+ };
+
+ return await _copilotCommentsRepository.InsertDocumentAsync(document);
+ }
+
+ public async Task UpdateDocumentAsync(string user, string id, string badCode, string goodCode, string language, string comment, string[] guidelineIds)
+ {
+ var filter = GetIdFilter(id);
+
+ var updateBuilder = Builders.Update;
+ var update = updateBuilder
+ .Set("ModifiedOn", DateTime.UtcNow)
+ .Set("ModifiedBy", user);
+
+ if (goodCode != null)
+ {
+ update = update.Set("GoodCode", goodCode);
+ }
+
+ if (language != null)
+ {
+ update = update.Set("Language", language);
+ }
+
+ if (comment != null)
+ {
+ update = update.Set("Comment", comment);
+ }
+
+ if (guidelineIds != null)
+ {
+ update = update.Set("GuidelineIds", guidelineIds);
+ }
+
+ if (badCode != null)
+ {
+ var embedding = await GetEmbeddingsAsync(badCode);
+ update = update
+ .Set("BadCode", badCode)
+ .Set("Embedding", embedding);
+ }
+
+ return await _copilotCommentsRepository.UpdateDocumentAsync(filter, update);
+ }
+
+ public Task DeleteDocumentAsync(string user, string id)
+ {
+ var filter = GetIdFilter(id);
+
+ var updateBuilder = Builders.Update;
+ var update = updateBuilder
+ .Set("IsDeleted", true)
+ .Set("ModifiedOn", DateTime.UtcNow)
+ .Set("ModifiedBy", user);
+
+ return _copilotCommentsRepository.DeleteDocumentAsync(filter, update);
+ }
+
+ public async Task GetDocumentAsync(string id)
+ {
+ var filter = GetIdFilter(id);
+ var document = await _copilotCommentsRepository.GetDocumentAsync(filter);
+
+ var documentJson = JsonConvert.SerializeObject(document);
+
+ return documentJson;
+ }
+
+ private FilterDefinition GetIdFilter(string id)
+ {
+ return Builders.Filter
+ .Where(f => f.Id.ToString() == id && f.IsDeleted == false);
+ }
+
+ private async Task GetEmbeddingsAsync(string badCode)
+ {
+ /*
+ * Structure of Embeddings object
+ * {
+ * {
+ * "Data",
+ * [
+ * {
+ * "EmbeddingsItem",
+ * {
+ * { "Embedding", [ "float1", "float2" ]},
+ * { "Index", "1"}
+ * }
+ * }
+ * ]
+ * },
+ * {
+ * "Usage",
+ * {
+ * { "PromptTokens", "1"},
+ * { "TotalTokens", "2"}
+ * }
+ * }
+ * }
+ */
+
+ var options = new EmbeddingsOptions(badCode);
+ var response = await _openAIClient.GetEmbeddingsAsync(_configuration["OpenAI:Model"], options);
+ var embeddings = response.Value;
+
+ var embedding = embeddings.Data[0].Embedding.ToArray();
+
+ return embedding;
+ }
+
+ public BsonDocument EmbeddingToBson(Embeddings embedding)
+ {
+ var data = embedding.Data;
+
+ var embeddingUsageBson = new BsonDocument
+ {
+ { "promptTokens", embedding.Usage.PromptTokens },
+ { "TotalTokens", embedding.Usage.TotalTokens }
+ };
+
+ var embeddingData = new BsonArray();
+
+ var embeddingBson = new BsonDocument
+ {
+ { "Data", embeddingData },
+ { "Usage", embeddingUsageBson }
+ };
+
+ foreach (var embeddingItem in data)
+ {
+ var vectors = embeddingItem.Embedding;
+ var vectorsBson = BsonArray.Create(vectors);
+
+ var embeddingItemBson = new BsonDocument
+ {
+ { "Embedding", vectorsBson },
+ { "Index", embeddingItem.Index }
+ };
+
+ embeddingData.Add(embeddingItemBson);
+ }
+
+ return embeddingBson;
+ }
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Managers/ICopilotCommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/ICopilotCommentsManager.cs
new file mode 100644
index 00000000000..d6129455528
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Managers/ICopilotCommentsManager.cs
@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+using MongoDB.Driver;
+
+namespace APIViewWeb.Managers
+{
+ public interface ICopilotCommentsManager
+ {
+ public Task CreateDocumentAsync(string user, string badCode, string goodCode, string language, string comment, string[] guidelineIds);
+ public Task UpdateDocumentAsync(string user, string id, string badCode, string goodCode, string language, string comment, string[] guidelineIds);
+ public Task GetDocumentAsync(string id);
+ public Task DeleteDocumentAsync(string user, string id);
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Models/CopilotCommentModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CopilotCommentModel.cs
new file mode 100644
index 00000000000..59c9c39f561
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Models/CopilotCommentModel.cs
@@ -0,0 +1,21 @@
+using System;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace APIViewWeb.Models
+{
+ public class CopilotCommentModel
+ {
+ [BsonId]
+ public ObjectId Id { get; set; }
+ public string BadCode { get; set; }
+ public string GoodCode { get; set; } = null;
+ public float[] Embedding { get; set; }
+ public string Language { get; set; }
+ public string Comment { get; set; } = null;
+ public string[] GuidelineIds { get; set; } = null;
+ public DateTime ModifiedOn { get; set; }
+ public string ModifiedBy { get; set; }
+ public bool IsDeleted { get; set; } = false;
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/CopilotCommentsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/CopilotCommentsRepository.cs
new file mode 100644
index 00000000000..415f33e222b
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Repositories/CopilotCommentsRepository.cs
@@ -0,0 +1,48 @@
+using System.Threading.Tasks;
+using APIViewWeb.Models;
+using Microsoft.Azure.Cosmos.Serialization.HybridRow;
+using Microsoft.Extensions.Configuration;
+using MongoDB.Driver;
+using MongoDB.Driver.Linq;
+
+namespace APIViewWeb.Repositories
+{
+ public class CopilotCommentsRepository : ICopilotCommentsRepository
+ {
+ private readonly MongoClient _mongoClient;
+ private readonly IMongoDatabase _database;
+ private readonly IMongoCollection _collection;
+
+ public CopilotCommentsRepository(IConfiguration configuration)
+ {
+ _mongoClient = new MongoClient(configuration["CosmosMongoDB:ConnectionString"]);
+ _database = _mongoClient.GetDatabase(configuration["CosmosMongoDB:DatabaseName"]);
+ _collection = _database.GetCollection(configuration["CosmosMongoDB:CollectionName"]);
+ }
+
+ public async Task InsertDocumentAsync(CopilotCommentModel document)
+ {
+ await _collection.InsertOneAsync(document);
+ return document.Id.ToString();
+ }
+
+ public async Task UpdateDocumentAsync(
+ FilterDefinition filter,
+ UpdateDefinition update)
+ {
+ return await _collection.UpdateOneAsync(filter, update);
+ }
+
+ public Task DeleteDocumentAsync(
+ FilterDefinition filter,
+ UpdateDefinition update)
+ {
+ return _collection.UpdateOneAsync(filter, update);
+ }
+
+ public Task GetDocumentAsync(FilterDefinition filter)
+ {
+ return _collection.Find(filter).FirstOrDefaultAsync();
+ }
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Repositories/ICopilotCommentsRepository.cs b/src/dotnet/APIView/APIViewWeb/Repositories/ICopilotCommentsRepository.cs
new file mode 100644
index 00000000000..4db05c356e8
--- /dev/null
+++ b/src/dotnet/APIView/APIViewWeb/Repositories/ICopilotCommentsRepository.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using APIViewWeb.Models;
+using MongoDB.Driver;
+
+namespace APIViewWeb.Repositories
+{
+ public interface ICopilotCommentsRepository
+ {
+ public Task InsertDocumentAsync(CopilotCommentModel document);
+
+ public Task UpdateDocumentAsync(
+ FilterDefinition filter,
+ UpdateDefinition update);
+
+ public Task DeleteDocumentAsync(
+ FilterDefinition filter,
+ UpdateDefinition update);
+
+ public Task GetDocumentAsync(FilterDefinition filter);
+ }
+}
diff --git a/src/dotnet/APIView/APIViewWeb/Startup.cs b/src/dotnet/APIView/APIViewWeb/Startup.cs
index 4b7dc57184c..54e85ba7809 100644
--- a/src/dotnet/APIView/APIViewWeb/Startup.cs
+++ b/src/dotnet/APIView/APIViewWeb/Startup.cs
@@ -93,6 +93,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -102,6 +103,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();