Skip to content

Commit

Permalink
Add CRUD API for Copilot comments (#6754)
Browse files Browse the repository at this point in the history
* Create CRUD API for copilot comments feature

* Delete unused code

* Remove comments

* Update status codes

* Remove unused fields

* Apply review changes
  • Loading branch information
yeojunh authored Aug 11, 2023
1 parent 2e58b08 commit 49201ef
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/dotnet/APIView/APIViewWeb/APIViewWeb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.6" />
<PackageReference Include="Azure.Data.AppConfiguration" Version="1.2.0" />
<PackageReference Include="Azure.Identity" Version="1.8.0" />
<PackageReference Include="Azure.Search.Documents" Version="11.4.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
<PackageReference Include="CsvHelper" Version="27.2.1" />
<!-- Adding NU1701 suppression as these three libraries have not updated to more modern versions of .net, although in testing they work as required. -->
Expand All @@ -49,6 +51,7 @@
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="16.170.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.9" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-19367-01" PrivateAssets="All" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="Octokit" Version="3.0.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<CopilotCommentsController> logger)
{
_copilotManager = copilotManager;
_logger = logger;
}

[HttpPost]
public async Task<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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);
}
}
}
193 changes: 193 additions & 0 deletions src/dotnet/APIView/APIViewWeb/Managers/CopilotCommentsManager.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<UpdateResult> UpdateDocumentAsync(string user, string id, string badCode, string goodCode, string language, string comment, string[] guidelineIds)
{
var filter = GetIdFilter(id);

var updateBuilder = Builders<CopilotCommentModel>.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<CopilotCommentModel>.Update;
var update = updateBuilder
.Set("IsDeleted", true)
.Set("ModifiedOn", DateTime.UtcNow)
.Set("ModifiedBy", user);

return _copilotCommentsRepository.DeleteDocumentAsync(filter, update);
}

public async Task<string> GetDocumentAsync(string id)
{
var filter = GetIdFilter(id);
var document = await _copilotCommentsRepository.GetDocumentAsync(filter);

var documentJson = JsonConvert.SerializeObject(document);

return documentJson;
}

private FilterDefinition<CopilotCommentModel> GetIdFilter(string id)
{
return Builders<CopilotCommentModel>.Filter
.Where(f => f.Id.ToString() == id && f.IsDeleted == false);
}

private async Task<float[]> 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;
}
}
}
13 changes: 13 additions & 0 deletions src/dotnet/APIView/APIViewWeb/Managers/ICopilotCommentsManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Threading.Tasks;
using MongoDB.Driver;

namespace APIViewWeb.Managers
{
public interface ICopilotCommentsManager
{
public Task<string> CreateDocumentAsync(string user, string badCode, string goodCode, string language, string comment, string[] guidelineIds);
public Task<UpdateResult> UpdateDocumentAsync(string user, string id, string badCode, string goodCode, string language, string comment, string[] guidelineIds);
public Task<string> GetDocumentAsync(string id);
public Task DeleteDocumentAsync(string user, string id);
}
}
21 changes: 21 additions & 0 deletions src/dotnet/APIView/APIViewWeb/Models/CopilotCommentModel.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<CopilotCommentModel> _collection;

public CopilotCommentsRepository(IConfiguration configuration)
{
_mongoClient = new MongoClient(configuration["CosmosMongoDB:ConnectionString"]);
_database = _mongoClient.GetDatabase(configuration["CosmosMongoDB:DatabaseName"]);
_collection = _database.GetCollection<CopilotCommentModel>(configuration["CosmosMongoDB:CollectionName"]);
}

public async Task<string> InsertDocumentAsync(CopilotCommentModel document)
{
await _collection.InsertOneAsync(document);
return document.Id.ToString();
}

public async Task<UpdateResult> UpdateDocumentAsync(
FilterDefinition<CopilotCommentModel> filter,
UpdateDefinition<CopilotCommentModel> update)
{
return await _collection.UpdateOneAsync(filter, update);
}

public Task DeleteDocumentAsync(
FilterDefinition<CopilotCommentModel> filter,
UpdateDefinition<CopilotCommentModel> update)
{
return _collection.UpdateOneAsync(filter, update);
}

public Task<CopilotCommentModel> GetDocumentAsync(FilterDefinition<CopilotCommentModel> filter)
{
return _collection.Find(filter).FirstOrDefaultAsync();
}
}
}
Loading

0 comments on commit 49201ef

Please sign in to comment.