diff --git a/Box.V2.Test.Integration/BoxAIManagerIntegrationTests.cs b/Box.V2.Test.Integration/BoxAIManagerIntegrationTests.cs new file mode 100644 index 000000000..367edc304 --- /dev/null +++ b/Box.V2.Test.Integration/BoxAIManagerIntegrationTests.cs @@ -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() { new BoxAIAskItem() { Id = uploadedFile.Id } }, + Mode = AiAskMode.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() + { + new BoxAIAskItem() { Id = uploadedFile1.Id }, + new BoxAIAskItem() { Id = uploadedFile2.Id } + }, + Mode = AiAskMode.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() { new BoxAITextGenItem() { Id = uploadedFile.Id } }, + DialogueHistory = new List() + { + 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"); + }); + } + } +} diff --git a/Box.V2.Test.Integration/Configuration/Commands/DisposableCommands/CreateFileCommand.cs b/Box.V2.Test.Integration/Configuration/Commands/DisposableCommands/CreateFileCommand.cs index 51bcdf5aa..2e15d755b 100644 --- a/Box.V2.Test.Integration/Configuration/Commands/DisposableCommands/CreateFileCommand.cs +++ b/Box.V2.Test.Integration/Configuration/Commands/DisposableCommands/CreateFileCommand.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using System.Threading.Tasks; using Box.V2.Models; @@ -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 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 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); } diff --git a/Box.V2.Test.Integration/Configuration/IntegrationTestBase.cs b/Box.V2.Test.Integration/Configuration/IntegrationTestBase.cs index 499c1f921..df3ffe071 100644 --- a/Box.V2.Test.Integration/Configuration/IntegrationTestBase.cs +++ b/Box.V2.Test.Integration/Configuration/IntegrationTestBase.cs @@ -216,7 +216,8 @@ public static string ReadFromJson(string path) return File.ReadAllText(filePath); } - public static async Task CreateSmallFile(string parentId = "0", CommandScope commandScope = CommandScope.Test, CommandAccessLevel accessLevel = CommandAccessLevel.User) + public static async Task CreateSmallFile(string parentId = "0", CommandScope commandScope = CommandScope.Test, + CommandAccessLevel accessLevel = CommandAccessLevel.User) { var path = GetSmallFilePath(); var ext = ""; @@ -233,6 +234,14 @@ public static async Task CreateSmallFileAsAdmin(string parentId) return await CreateSmallFile(parentId, CommandScope.Test, CommandAccessLevel.Admin); } + public static async Task 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)); diff --git a/Box.V2.Test/Box.V2.Test.csproj b/Box.V2.Test/Box.V2.Test.csproj index d86972715..d6abee884 100644 --- a/Box.V2.Test/Box.V2.Test.csproj +++ b/Box.V2.Test/Box.V2.Test.csproj @@ -27,6 +27,12 @@ + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/Box.V2.Test/BoxAIManagerTest.cs b/Box.V2.Test/BoxAIManagerTest.cs new file mode 100644 index 000000000..f61784a39 --- /dev/null +++ b/Box.V2.Test/BoxAIManagerTest.cs @@ -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(It.IsAny())) + .Returns(Task.FromResult>(new BoxResponse() + { + Status = ResponseStatus.Success, + ContentString = LoadFixtureFromJson("Fixtures/BoxAI/SendAiQuestion200.json") + })) + .Callback(r => boxRequest = r); + + var requestBody = new BoxAIAskRequest() + { + Mode = AiAskMode.single_item_qa, + Prompt = "What is the value provided by public APIs based on this document?", + Items = new System.Collections.Generic.List() + { + new BoxAIAskItem() { Id = "9842787262" } + } + }; + + /*** Act ***/ + BoxAIResponse response = await _aiManager.SendAIQuestionAsync(requestBody); + + /*** 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(It.IsAny())) + .Returns(Task.FromResult>(new BoxResponse() + { + Status = ResponseStatus.Success, + ContentString = LoadFixtureFromJson("Fixtures/BoxAI/SendAITextGenRequestSuccess200.json") + })) + .Callback(r => boxRequest = r); + + var requestBody = new BoxAITextGenRequest() + { + Prompt = "Write an email to a client about the importance of public APIs", + Items = new List() + { + new BoxAITextGenItem() { Id = "12345678", Content = "More information about public APIs" } + }, + DialogueHistory = new List() + { + 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); + + /*** 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); + } + } +} diff --git a/Box.V2.Test/BoxResourceManagerTest.cs b/Box.V2.Test/BoxResourceManagerTest.cs index 73dbc8ecc..3d4471a26 100644 --- a/Box.V2.Test/BoxResourceManagerTest.cs +++ b/Box.V2.Test/BoxResourceManagerTest.cs @@ -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() { @@ -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")); diff --git a/Box.V2.Test/Fixtures/BoxAI/SendAIQuestion200.json b/Box.V2.Test/Fixtures/BoxAI/SendAIQuestion200.json new file mode 100644 index 000000000..f11b0636e --- /dev/null +++ b/Box.V2.Test/Fixtures/BoxAI/SendAIQuestion200.json @@ -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" +} diff --git a/Box.V2.Test/Fixtures/BoxAI/SendAITextGenRequestSuccess200.json b/Box.V2.Test/Fixtures/BoxAI/SendAITextGenRequestSuccess200.json new file mode 100644 index 000000000..f11b0636e --- /dev/null +++ b/Box.V2.Test/Fixtures/BoxAI/SendAITextGenRequestSuccess200.json @@ -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" +} diff --git a/Box.V2/Box.V2.csproj b/Box.V2/Box.V2.csproj index 99dd3b0f8..8cb88f951 100644 --- a/Box.V2/Box.V2.csproj +++ b/Box.V2/Box.V2.csproj @@ -124,6 +124,7 @@ + @@ -143,6 +144,7 @@ + @@ -168,6 +170,8 @@ + + @@ -250,6 +254,7 @@ + diff --git a/Box.V2/BoxClient.cs b/Box.V2/BoxClient.cs index c26b19b40..31972140b 100644 --- a/Box.V2/BoxClient.cs +++ b/Box.V2/BoxClient.cs @@ -139,6 +139,7 @@ private void InitManagers() SignRequestsManager = new BoxSignRequestsManager(Config, _service, _converter, Auth, _asUser, _suppressNotifications); SignTemplatesManager = new BoxSignTemplatesManager(Config, _service, _converter, Auth, _asUser, _suppressNotifications); FileRequestsManager = new BoxFileRequestsManager(Config, _service, _converter, Auth, _asUser, _suppressNotifications); + BoxAIManager = new BoxAIManager(Config, _service, _converter, Auth, _asUser, _suppressNotifications); // Init Resource Plugins Manager ResourcePlugins = new BoxResourcePlugins(); @@ -295,5 +296,10 @@ public IBoxClient AddResourcePlugin() where T : BoxResourceManager /// The manager that represents all of the file requests endpoints. /// public IBoxFileRequestsManager FileRequestsManager { get; private set; } + + /// + /// The manager that represents all of the AI endpoints. + /// + public IBoxAIManager BoxAIManager { get; private set; } } } diff --git a/Box.V2/Config/BoxConfig.cs b/Box.V2/Config/BoxConfig.cs index 90eda8121..922a92d25 100644 --- a/Box.V2/Config/BoxConfig.cs +++ b/Box.V2/Config/BoxConfig.cs @@ -297,6 +297,10 @@ public string JWTAudience /// public Uri FileRequestsEndpointWithPathUri { get { return new Uri(BoxApiUri, Constants.FileRequestsWithPathString); } } /// + /// Gets the AI endpoint URI. + /// + public Uri AIEndpointWithPathUri { get { return new Uri(BoxApiUri, Constants.AIWithPathString); } } + /// /// The web proxy for HttpRequestHandler /// public IWebProxy WebProxy { get; private set; } diff --git a/Box.V2/Config/Constants.cs b/Box.V2/Config/Constants.cs index 8aff1debf..922cb9d57 100644 --- a/Box.V2/Config/Constants.cs +++ b/Box.V2/Config/Constants.cs @@ -1,5 +1,3 @@ -using System; - namespace Box.V2.Config { public static class Constants @@ -62,6 +60,9 @@ public static class Constants public const string SignTemplatesString = @"sign_templates"; public const string SignTemplatesWithPathString = @"sign_templates/"; public const string FileRequestsWithPathString = @"file_requests/"; + public const string AIWithPathString = @"ai/"; + public const string AIAskEndpointString = @"ask"; + public const string AITextGenEndpointString = @"text_gen"; /// @@ -124,6 +125,7 @@ public static class Constants public const string SignTemplatesEndpointString = BoxApiUriString + SignTemplatesString; public const string SignTemplatesWithPathEndpointString = BoxApiUriString + SignTemplatesWithPathString; public const string FileRequestsWithPathEndpointString = BoxApiUriString + FileRequestsWithPathString; + public const string AIWithPathEndpointString = BoxApiUriString + AIWithPathString; /*** Endpoint Paths ***/ public const string ItemsPathString = @"{0}/items"; diff --git a/Box.V2/Config/IBoxConfig.cs b/Box.V2/Config/IBoxConfig.cs index 3447be206..cd5774403 100644 --- a/Box.V2/Config/IBoxConfig.cs +++ b/Box.V2/Config/IBoxConfig.cs @@ -143,6 +143,10 @@ public interface IBoxConfig /// Uri FileRequestsEndpointWithPathUri { get; } /// + /// Gets the AI endpoint URI. + /// + Uri AIEndpointWithPathUri { get; } + /// /// The web proxy for HttpRequestHandler /// IWebProxy WebProxy { get; } diff --git a/Box.V2/IBoxClient.cs b/Box.V2/IBoxClient.cs index 69dca0cca..adc263304 100644 --- a/Box.V2/IBoxClient.cs +++ b/Box.V2/IBoxClient.cs @@ -153,5 +153,10 @@ public interface IBoxClient /// The manager that represents all of the file requests endpoints. /// IBoxFileRequestsManager FileRequestsManager { get; } + + /// + /// The manager that represents all of the AI endpoints. + /// + IBoxAIManager BoxAIManager { get; } } } diff --git a/Box.V2/Managers/BoxAIManager.cs b/Box.V2/Managers/BoxAIManager.cs new file mode 100644 index 000000000..8193c2f04 --- /dev/null +++ b/Box.V2/Managers/BoxAIManager.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using Box.V2.Auth; +using Box.V2.Config; +using Box.V2.Converter; +using Box.V2.Extensions; +using Box.V2.Models; +using Box.V2.Services; + +namespace Box.V2.Managers +{ + /// + /// The manager that represents all of the AI endpoints. + /// + public class BoxAIManager : BoxResourceManager, IBoxAIManager + { + public BoxAIManager(IBoxConfig config, IBoxService service, IBoxConverter converter, IAuthRepository auth, string asUser = null, bool? suppressNotifications = null) + : base(config, service, converter, auth, asUser, suppressNotifications) { } + + /// + /// Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text. + /// + /// AI ask request + /// Response for AI question + public async Task SendAIQuestionAsync(BoxAIAskRequest aiAskRequest) + { + var request = new BoxRequest(_config.AIEndpointWithPathUri, Constants.AIAskEndpointString) + .Method(RequestMethod.Post) + .Payload(_converter.Serialize(aiAskRequest)); + + IBoxResponse response = await ToResponseAsync(request).ConfigureAwait(false); + + return response.ResponseObject; + } + + /// + /// Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text. + /// + /// AI ask request + /// Response for AI text gen request + public async Task SendAITextGenRequestAsync(BoxAITextGenRequest aiTextGenRequest) + { + var request = new BoxRequest(_config.AIEndpointWithPathUri, Constants.AITextGenEndpointString) + .Method(RequestMethod.Post) + .Payload(_converter.Serialize(aiTextGenRequest)); + + IBoxResponse response = await ToResponseAsync(request).ConfigureAwait(false); + + return response.ResponseObject; + } + } +} diff --git a/Box.V2/Managers/IBoxAIManager.cs b/Box.V2/Managers/IBoxAIManager.cs new file mode 100644 index 000000000..633e0e266 --- /dev/null +++ b/Box.V2/Managers/IBoxAIManager.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Box.V2.Models; + +namespace Box.V2.Managers +{ + /// + /// The manager that represents all of the AI endpoints. + /// + public interface IBoxAIManager + { + /// + /// Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text. + /// + /// AI ask request + /// Response for AI question + Task SendAIQuestionAsync(BoxAIAskRequest aiAskRequest); + + /// + /// Sends an AI request to supported LLMs and returns an answer specifically focused on the creation of new text. + /// + /// AI ask request + /// Response for AI text gen request + Task SendAITextGenRequestAsync(BoxAITextGenRequest aiTextGenRequest); + } +} diff --git a/Box.V2/Models/BoxAIResponse.cs b/Box.V2/Models/BoxAIResponse.cs new file mode 100644 index 000000000..5317a1b6d --- /dev/null +++ b/Box.V2/Models/BoxAIResponse.cs @@ -0,0 +1,30 @@ +using System; +using Newtonsoft.Json; + +namespace Box.V2.Models +{ + public class BoxAIResponse + { + public const string FieldAnswer = "answer"; + public const string FieldCompletionReason = "completion_reason"; + public const string FieldCreatedAt = "created_at"; + + /// + /// The answer provided by the LLM. + /// + [JsonProperty(PropertyName = FieldAnswer)] + public string Answer { get; set; } + + /// + /// The answer provided by the LLM. + /// + [JsonProperty(PropertyName = FieldCompletionReason)] + public string CompletionReason { get; set; } + + /// + /// The ISO date formatted timestamp of when the answer to the prompt was created. + /// + [JsonProperty(PropertyName = FieldCreatedAt)] + public DateTimeOffset CreatedAt { get; set; } + } +} diff --git a/Box.V2/Models/Request/BoxAIAskRequest.cs b/Box.V2/Models/Request/BoxAIAskRequest.cs new file mode 100644 index 000000000..5e5014c2a --- /dev/null +++ b/Box.V2/Models/Request/BoxAIAskRequest.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Box.V2.Models +{ + /// + /// Model used to send request to Box AI Ask API + /// + public class BoxAIAskRequest + { + public const string FieldItems = "items"; + public const string FieldMode = "mode"; + public const string FieldPrompt = "prompt"; + + /// + /// The items to be processed by the LLM, often files. + /// Note: Box AI handles documents with text representations up to 1MB in size, or a maximum of 25 files, whichever comes first. + /// If the file size exceeds 1MB, the first 1MB of text representation will be processed. + /// If you set mode parameter to single_item_qa, the items array can have one element only. + /// + [JsonProperty(PropertyName = FieldItems)] + public List Items { get; set; } + + /// + /// The mode specifies if this request is for a single or multiple items.If you select single_item_qa the items array can have one element only. + /// Selecting multiple_item_qa allows you to provide up to 25 items. + /// + [JsonProperty(PropertyName = FieldMode)] + [JsonConverter(typeof(StringEnumConverter))] + public AiAskMode Mode { get; set; } + + /// + /// The prompt provided by the client to be answered by the LLM.The prompt's length is limited to 10000 characters. + /// + [JsonProperty(PropertyName = FieldPrompt)] + public string Prompt { get; set; } + } + + /// + /// The items to be processed by the LLM, often files. + /// Note: Box AI handles documents with text representations up to 1MB in size, or a maximum of 25 files, whichever comes first. + /// If the file size exceeds 1MB, the first 1MB of text representation will be processed. + /// If you set mode parameter to single_item_qa, the items array can have one element only. + /// + public class BoxAIAskItem + { + public const string FieldId = "id"; + public const string FieldType = "type"; + public const string FieldContent = "content"; + + /// + /// The id of the item. + /// + [JsonProperty(PropertyName = FieldId)] + public string Id { get; set; } + + /// + /// The type of the item. Value is always file. + /// + [JsonProperty(PropertyName = FieldType)] + public string Type => "file"; + + /// + /// The content of the item, often the text representation. + /// + [JsonProperty(PropertyName = FieldContent)] + public string Content { get; set; } + } + + /// + /// The mode specifies if this request is for a single or multiple items. If you select single_item_qa the items array can have one element only. + /// Selecting multiple_item_qa allows you to provide up to 25 items. + /// + public enum AiAskMode + { + multiple_item_qa, + single_item_qa + } +} diff --git a/Box.V2/Models/Request/BoxAITextGenRequest.cs b/Box.V2/Models/Request/BoxAITextGenRequest.cs new file mode 100644 index 000000000..de1401ccc --- /dev/null +++ b/Box.V2/Models/Request/BoxAITextGenRequest.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Box.V2.Models +{ + /// + /// Model used to send request to Box AI Text Gen API + /// + public class BoxAITextGenRequest + { + public const string FieldDialogueHistory = "dialogue_history"; + public const string FieldItems = "items"; + public const string FieldPrompt = "prompt"; + + /// + /// The history of prompts and answers previously passed to the LLM.This provides additional context to the LLM in generating the response. + /// + [JsonProperty(PropertyName = FieldDialogueHistory)] + public List DialogueHistory { get; set; } + + /// + /// The items to be processed by the LLM, often files.The array can include exactly one element. + /// Note: Box AI handles documents with text representations up to 1MB in size. + /// If the file size exceeds 1MB, the first 1MB of text representation will be processed. + /// + [JsonProperty(PropertyName = FieldItems)] + public List Items { get; set; } + + /// + /// The prompt provided by the client to be answered by the LLM. The prompt's length is limited to 10000 characters. + /// + [JsonProperty(PropertyName = FieldPrompt)] + public string Prompt { get; set; } + } + + /// + /// The history of prompts and answers previously passed to the LLM.This provides additional context to the LLM in generating the response. + /// + public class BoxAIDialogueHistory + { + public const string FieldAnswer = "answer"; + public const string FieldCreatedAt = "created_at"; + public const string FieldPrompt = "prompt"; + + /// + /// The answer previously provided by the LLM. + /// + [JsonProperty(PropertyName = FieldAnswer)] + public string Answer { get; set; } + + /// + /// The ISO date formatted timestamp of when the previous answer to the prompt was created. + /// + [JsonProperty(PropertyName = FieldCreatedAt)] + public DateTimeOffset CreatedAt { get; set; } + + /// + /// The prompt previously provided by the client and answered by the LLM. + /// + [JsonProperty(PropertyName = FieldPrompt)] + public string Prompt { get; set; } + } + + /// + /// The items to be processed by the LLM, often files.The array can include exactly one element. + /// Note: Box AI handles documents with text representations up to 1MB in size. + /// If the file size exceeds 1MB, the first 1MB of text representation will be processed. + /// + public class BoxAITextGenItem + { + public const string FieldId = "id"; + public const string FieldType = "type"; + public const string FieldContent = "content"; + + /// + /// The id of the item. + /// + [JsonProperty(PropertyName = FieldId)] + public string Id { get; set; } + + /// + /// The type of the item. Value is always file. + /// + [JsonProperty(PropertyName = FieldType)] + public string Type => "file"; + + /// + /// The content to use as context for generating new text or editing existing text. + /// + [JsonProperty(PropertyName = FieldContent)] + public string Content { get; set; } + } +} diff --git a/docs/README.md b/docs/README.md index 7483d9679..3be95e3b1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,6 +3,7 @@ Under this directory you can find documentation and samples for available SDK functionalities. You can read more about supported resources on [API reference](https://developer.box.com/reference/). +- [AI](ai.md) - [Authentication](authentication.md) - [Classifications](classifications.md) - [Collaboration Whitelist](collaboration-whitelist.md) diff --git a/docs/ai.md b/docs/ai.md new file mode 100644 index 000000000..76b0d8cc9 --- /dev/null +++ b/docs/ai.md @@ -0,0 +1,60 @@ +AI +== + +AI allows to send an intelligence request to supported large language models and returns +an answer based on the provided prompt and items. + + + + +- [Send AI question](#send-ai-request) +- [Send AI text generation request](#send-ai-text-generation-request) + + + +Send AI question +-------------------------- + +To send an AI request, use `BoxAIManager.SendAIQuestionAsync(BoxAIAskRequest aiAskRequest)` method +In the request you have to provide a prompt, a list of items that your prompt refers to and a mode of the request. +There are two modes available: `single_item_qa` and `multiple_item_qa`, which specifies if this request refers to +for a single or multiple items. + + +```c# +BoxAIResponse response = await client.BoxAIManager.SendAIQuestionAsync( + new BoxAIAskRequest + { + Prompt = "What is the name of the file?", + Items = new List() { new BoxAIAskItem() { Id = "12345" } }, + Mode = AiAskMode.single_item_qa + }; +); +``` + +NOTE: The AI endpoint may return a 412 status code if you use for your request a file which has just been updated to the box. +It usually takes a few seconds for the file to be indexed and available for the AI endpoint. + +Send AI text generation request +-------------- + +To send an AI request specifically focused on the creation of new text, call +`BoxAIManager.SendAITextGenRequestAsync(BoxAiTextGenRequest aiTextGenRequest)` method. +In the request you have to provide a prompt, a list of items that your prompt refers to and optionally a dialogue history, +which provides additional context to the LLM in generating the response. + + +```c# +BoxAIResponse response = await client.BoxAIManager.SendAITextGenRequestAsync( + new BoxAITextGenRequest + { + Prompt = "What is the name of the file?", + Items = new List() { new BoxAITextGenItem() { Id = "12345" } }, + DialogueHistory = new List() + { + new BoxAIDialogueHistory() { Prompt = "What is the name of the file?", Answer = "MyFile", CreatedAt = DateTimeOffset.Parse("2024-05-16T15:26:57-07:00") } + new BoxAIDialogueHistory() { Prompt = "What is the size of the file?", Answer = "10kb", CreatedAt = DateTimeOffset.Parse("2024-05-16T15:26:57-07:00") } + } + }; +); +``` \ No newline at end of file