Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for AI #965

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading