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();