Skip to content

Commit

Permalink
feat: add support for AI (#965)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwwoda committed Jul 22, 2024
1 parent 2c8eedc commit a9e130a
Show file tree
Hide file tree
Showing 21 changed files with 647 additions and 15 deletions.
99 changes: 99 additions & 0 deletions Box.V2.Test.Integration/BoxAIManagerIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Box.V2.Models;
using Box.V2.Test.Integration.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Box.V2.Test.Integration
{
[TestClass]
public class BoxAIManagerIntegrationTests : TestInFolder
{
[TestMethod]
public async Task SendAIQuestionAsync_ForSingleItem_ReturnsValidResponse()
{
var fileName = "[Single Item AI] Test File.txt";
var fileContent = "Test file";
var uploadedFile = await CreateSmallFromMemoryStream(FolderId, fileName, fileContent);

var request = new BoxAIAskRequest
{
Prompt = "What is the name of the file?",
Items = new List<BoxAIAskItem>() { new BoxAIAskItem() { Id = uploadedFile.Id } },
Mode = BoxAIAskMode.single_item_qa
};

await Retry(async () =>
{
var response = await UserClient.BoxAIManager.SendAIQuestionAsync(request);
Assert.IsTrue(response.Answer.Contains(fileContent));
Assert.IsTrue(response.CreatedAt < DateTimeOffset.Now);
Assert.AreEqual(response.CompletionReason, "done");
});
}

[TestMethod]
public async Task SendAIQuestionAsync_ForMultipleItems_ReturnsValidResponse()
{
var fileContent = "Test file";

var fileName1 = "[Multi Item AI] First Test File.txt";
var uploadedFile1 = await CreateSmallFromMemoryStream(FolderId, fileName1, fileContent);

var fileName2 = "[Multi Item AI] Second test file.txt";
var uploadedFile2 = await CreateSmallFromMemoryStream(FolderId, fileName2, fileContent);

var request = new BoxAIAskRequest
{
Prompt = "What is the content of these files?",
Items = new List<BoxAIAskItem>()
{
new BoxAIAskItem() { Id = uploadedFile1.Id },
new BoxAIAskItem() { Id = uploadedFile2.Id }
},
Mode = BoxAIAskMode.multiple_item_qa
};

await Retry(async () =>
{
var response = await UserClient.BoxAIManager.SendAIQuestionAsync(request);
Assert.IsTrue(response.Answer.Contains(fileContent));
Assert.IsTrue(response.CreatedAt < DateTimeOffset.Now);
Assert.AreEqual(response.CompletionReason, "done");
});
}

[TestMethod]
public async Task SendTextGenRequestAsync_ForValidPayload_ReturnsValidResponse()
{
var fileName = "[AI Text Gen] Test File.txt";
var fileContent = "Test File";
var uploadedFile = await CreateSmallFromMemoryStream(FolderId, fileName, fileContent);
var date1 = DateTimeOffset.Parse("2013-05-16T15:27:57-07:00");
var date2 = DateTimeOffset.Parse("2013-05-16T15:26:57-07:00");

var request = new BoxAITextGenRequest
{
Prompt = "What is the name of the file?",
Items = new List<BoxAITextGenItem>() { new BoxAITextGenItem() { Id = uploadedFile.Id } },
DialogueHistory = new List<BoxAIDialogueHistory>()
{
new BoxAIDialogueHistory() { Prompt = "What is the name of the file?", Answer = fileContent, CreatedAt = date1 },
new BoxAIDialogueHistory() { Prompt = "What is the size of the file?", Answer = "10kb", CreatedAt = date2 }
}
};

await Retry(async () =>
{
var response = await UserClient.BoxAIManager.SendAITextGenRequestAsync(request);
Assert.IsTrue(response.Answer.Contains(fileContent));
Assert.IsTrue(response.CreatedAt < DateTimeOffset.Now);
Assert.AreEqual(response.CompletionReason, "done");
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Box.V2.Models;

Expand All @@ -9,40 +10,63 @@ public class CreateFileCommand : CommandBase, IDisposableCommand
private readonly string _fileName;
private readonly string _filePath;
private readonly string _folderId;
private readonly string _content;
private readonly bool _isFileStream;

public string FileId;
public BoxFile File;

public CreateFileCommand(string fileName, string filePath, string folderId = "0", CommandScope scope = CommandScope.Test, CommandAccessLevel accessLevel = CommandAccessLevel.User) : base(scope, accessLevel)
public CreateFileCommand(string fileName, string filePath, string folderId = "0", CommandScope scope = CommandScope.Test,
CommandAccessLevel accessLevel = CommandAccessLevel.User, string content = "") : base(scope, accessLevel)
{
_fileName = fileName;
_filePath = filePath;
_folderId = folderId;
_content = content;
if (!string.IsNullOrEmpty(_filePath) && !string.IsNullOrEmpty(_content))
{
throw new System.Exception("You can't have both filePath and content filled");
}
_isFileStream = !string.IsNullOrEmpty(_filePath);
}

public async Task<string> Execute(IBoxClient client)
{
using (var fileStream = new FileStream(_filePath, FileMode.OpenOrCreate))
if (_isFileStream)
{
var requestParams = new BoxFileRequest()
using (var fileStream = new FileStream(_filePath, FileMode.OpenOrCreate))
{
Name = _fileName,
Parent = new BoxRequestEntity() { Id = _folderId }
};

var response = await client.FilesManager.UploadAsync(requestParams, fileStream);
File = response;
FileId = File.Id;
return FileId;
return await UploadFileAsync(client, fileStream);
}
}

var byteArray = Encoding.UTF8.GetBytes(_content);
using (var memoryStream = new MemoryStream(byteArray))
{
return await UploadFileAsync(client, memoryStream);
}
}

private async Task<string> UploadFileAsync(IBoxClient client, Stream stream)
{
var requestParams = new BoxFileRequest()
{
Name = _fileName,
Parent = new BoxRequestEntity() { Id = _folderId }
};

var response = await client.FilesManager.UploadAsync(requestParams, stream);
File = response;
FileId = File.Id;
return FileId;
}

public async Task Dispose(IBoxClient client)
{
await client.FilesManager.DeleteAsync(FileId);

// for some reason file uploaded as admin cannot be purged from trash
if(AccessLevel != CommandAccessLevel.Admin)
if (AccessLevel != CommandAccessLevel.Admin)
{
await client.FilesManager.PurgeTrashedAsync(FileId);
}
Expand Down
11 changes: 10 additions & 1 deletion Box.V2.Test.Integration/Configuration/IntegrationTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ public static string ReadFromJson(string path)
return File.ReadAllText(filePath);
}

public static async Task<BoxFile> CreateSmallFile(string parentId = "0", CommandScope commandScope = CommandScope.Test, CommandAccessLevel accessLevel = CommandAccessLevel.User)
public static async Task<BoxFile> CreateSmallFile(string parentId = "0", CommandScope commandScope = CommandScope.Test,
CommandAccessLevel accessLevel = CommandAccessLevel.User)
{
var path = GetSmallFilePath();
var ext = "";
Expand All @@ -233,6 +234,14 @@ public static async Task<BoxFile> CreateSmallFileAsAdmin(string parentId)
return await CreateSmallFile(parentId, CommandScope.Test, CommandAccessLevel.Admin);
}

public static async Task<BoxFile> CreateSmallFromMemoryStream(string parentId = "0", string filename = "", string content = "",
CommandScope commandScope = CommandScope.Test, CommandAccessLevel accessLevel = CommandAccessLevel.User)
{
var createFileCommand = new CreateFileCommand(filename, "", parentId, commandScope, accessLevel, content);
await ExecuteCommand(createFileCommand);
return createFileCommand.File;
}

public static async Task DeleteFile(string fileId)
{
await ExecuteCommand(new DeleteFileCommand(fileId));
Expand Down
6 changes: 6 additions & 0 deletions Box.V2.Test/Box.V2.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
<Reference Include="System.Net.Http.WebRequest" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures\BoxAI\SendAITextGenRequestSuccess200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures\BoxAI\SendAIQuestion200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Fixtures\BoxFileRequest\UpdateFileRequest200.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
112 changes: 112 additions & 0 deletions Box.V2.Test/BoxAIManagerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Box.V2.Managers;
using Box.V2.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Box.V2.Test
{
[TestClass]
public class BoxAIManagerTest : BoxResourceManagerTest
{
private readonly BoxAIManager _aiManager;

public BoxAIManagerTest()
{
_aiManager = new BoxAIManager(Config.Object, Service, Converter, AuthRepository);
}

[TestMethod]
public async Task SendAiQuestionAsync_Success()
{
/** Arrange **/
IBoxRequest boxRequest = null;
Handler.Setup(h => h.ExecuteAsync<BoxAIResponse>(It.IsAny<IBoxRequest>()))
.Returns(Task.FromResult<IBoxResponse<BoxAIResponse>>(new BoxResponse<BoxAIResponse>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxAI/SendAiQuestion200.json")
}))
.Callback<IBoxRequest>(r => boxRequest = r);

var requestBody = new BoxAIAskRequest()
{
Mode = BoxAIAskMode.single_item_qa,
Prompt = "What is the value provided by public APIs based on this document?",
Items = new List<BoxAIAskItem>()
{
new BoxAIAskItem() { Id = "9842787262" }
}
};

/*** Act ***/
BoxAIResponse response = await _aiManager.SendAIQuestionAsync(requestBody);

Check warning on line 45 in Box.V2.Test/BoxAIManagerTest.cs

View workflow job for this annotation

GitHub Actions / Build and Test - Core

Consider calling ConfigureAwait on the awaited task

/*** Assert ***/
// Request check
Assert.IsNotNull(boxRequest);
Assert.AreEqual(RequestMethod.Post, boxRequest.Method);
Assert.AreEqual(new Uri("https://api.box.com/2.0/ai/ask"), boxRequest.AbsoluteUri);

// Response check
Assert.AreEqual("Public APIs are important because of key and important reasons.", response.Answer);
Assert.AreEqual("done", response.CompletionReason);
Assert.AreEqual(DateTimeOffset.Parse("2012-12-12T10:53:43-08:00"), response.CreatedAt);
}


[TestMethod]
public async Task SendAiGenerateTextRequestAsync_Success()
{
/** Arrange **/
IBoxRequest boxRequest = null;
Handler.Setup(h => h.ExecuteAsync<BoxAIResponse>(It.IsAny<IBoxRequest>()))
.Returns(Task.FromResult<IBoxResponse<BoxAIResponse>>(new BoxResponse<BoxAIResponse>()
{
Status = ResponseStatus.Success,
ContentString = LoadFixtureFromJson("Fixtures/BoxAI/SendAITextGenRequestSuccess200.json")
}))
.Callback<IBoxRequest>(r => boxRequest = r);

var requestBody = new BoxAITextGenRequest()
{
Prompt = "Write an email to a client about the importance of public APIs",
Items = new List<BoxAITextGenItem>()
{
new BoxAITextGenItem() { Id = "12345678", Content = "More information about public APIs" }
},
DialogueHistory = new List<BoxAIDialogueHistory>()
{
new BoxAIDialogueHistory()
{
Prompt = "Make my email about public APIs sound more professional",
Answer = "Here is the first draft of your professional email about public APIs",
CreatedAt = DateTimeOffset.Parse("2013-12-12T10:53:43-08:00")
},
new BoxAIDialogueHistory()
{
Prompt = "Can you add some more information?",
Answer = "Public API schemas provide necessary information to integrate with APIs...",
CreatedAt = DateTimeOffset.Parse("2013-12-12T11:20:43-08:00")
}
}
};

/*** Act ***/
BoxAIResponse response = await _aiManager.SendAITextGenRequestAsync(requestBody);

Check warning on line 98 in Box.V2.Test/BoxAIManagerTest.cs

View workflow job for this annotation

GitHub Actions / Build and Test - Core

Consider calling ConfigureAwait on the awaited task

/*** Assert ***/
// Request check
Assert.IsNotNull(boxRequest);
Assert.AreEqual(RequestMethod.Post, boxRequest.Method);
Assert.AreEqual(new Uri("https://api.box.com/2.0/ai/text_gen"), boxRequest.AbsoluteUri);

// Response check
Assert.AreEqual("Public APIs are important because of key and important reasons.", response.Answer);
Assert.AreEqual("done", response.CompletionReason);
Assert.AreEqual(DateTimeOffset.Parse("2012-12-12T10:53:43-08:00"), response.CreatedAt);
}
}
}
2 changes: 2 additions & 0 deletions Box.V2.Test/BoxResourceManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public abstract class BoxResourceManagerTest
protected Uri SignRequestUri = new Uri(Constants.SignRequestsEndpointString);
protected Uri SignRequestWithPathUri = new Uri(Constants.SignRequestsWithPathEndpointString);
protected Uri FileRequestsWithPathUri = new Uri(Constants.FileRequestsWithPathEndpointString);
protected Uri AIWithPathUri = new Uri(Constants.AIWithPathEndpointString);

protected BoxResourceManagerTest()
{
Expand All @@ -52,6 +53,7 @@ protected BoxResourceManagerTest()
Config.SetupGet(x => x.SignTemplatesEndpointUri).Returns(new Uri(Constants.SignTemplatesEndpointString));
Config.SetupGet(x => x.SignTemplatesEndpointWithPathUri).Returns(new Uri(Constants.SignTemplatesWithPathEndpointString));
Config.SetupGet(x => x.FileRequestsEndpointWithPathUri).Returns(FileRequestsWithPathUri);
Config.SetupGet(x => x.AIEndpointWithPathUri).Returns(AIWithPathUri);
Config.SetupGet(x => x.RetryStrategy).Returns(new InstantRetryStrategy());

AuthRepository = new AuthRepository(Config.Object, Service, Converter, new OAuthSession("fakeAccessToken", "fakeRefreshToken", 3600, "bearer"));
Expand Down
5 changes: 5 additions & 0 deletions Box.V2.Test/Fixtures/BoxAI/SendAIQuestion200.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"answer": "Public APIs are important because of key and important reasons.",
"completion_reason": "done",
"created_at": "2012-12-12T10:53:43-08:00"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"answer": "Public APIs are important because of key and important reasons.",
"completion_reason": "done",
"created_at": "2012-12-12T10:53:43-08:00"
}
5 changes: 5 additions & 0 deletions Box.V2/Box.V2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
<Compile Include="Extensions\BoxExtensions.cs" />
<Compile Include="Extensions\UriExtensions.cs" />
<Compile Include="JWTAuth\JWTAuthRepository.cs" />
<Compile Include="Managers\BoxAIManager.cs" />
<Compile Include="Managers\BoxCollaborationWhitelistManager.cs" />
<Compile Include="Managers\BoxCollectionsManager.cs" />
<Compile Include="Managers\BoxDevicePinManager.cs" />
Expand All @@ -142,6 +143,7 @@
<Compile Include="Managers\BoxLegalHoldPoliciesManager.cs" />
<Compile Include="Managers\BoxRecentItemsManager.cs" />
<Compile Include="Managers\BoxWebLinksManager.cs" />
<Compile Include="Managers\IBoxAIManager.cs" />
<Compile Include="Managers\IBoxCollaborationsManager.cs" />
<Compile Include="Managers\IBoxCollaborationWhitelistManager.cs" />
<Compile Include="Managers\IBoxCollectionsManager.cs" />
Expand All @@ -167,6 +169,8 @@
<Compile Include="Managers\IBoxWebhooksManager.cs" />
<Compile Include="Managers\IBoxWebLinksManager.cs" />
<Compile Include="Managers\IBoxFileRequestsManager.cs" />
<Compile Include="Models\BoxAIResponse.cs" />
<Compile Include="Models\Request\BoxAIAskRequest.cs" />
<Compile Include="Models\BoxApplication.cs" />
<Compile Include="Models\BoxAssignmentCounts.cs" />
<Compile Include="Models\BoxClassification.cs" />
Expand Down Expand Up @@ -249,6 +253,7 @@
<Compile Include="Models\BoxGroupMembership.cs" />
<Compile Include="Models\Request\BoxActionableByRequest.cs" />
<Compile Include="Models\BoxSessionParts.cs" />
<Compile Include="Models\Request\BoxAITextGenRequest.cs" />
<Compile Include="Models\Request\BoxFileRequestUpdateRequest.cs" />
<Compile Include="Models\Request\BoxFileRequestCopyRequest.cs" />
<Compile Include="Models\Request\BoxFileUploadSessionRequest.cs" />
Expand Down
Loading

0 comments on commit a9e130a

Please sign in to comment.