diff --git a/CS/ReportingApp/ReportingApp.csproj b/CS/ReportingApp/ReportingApp.csproj index 8c3bfa6..9b01f26 100644 --- a/CS/ReportingApp/ReportingApp.csproj +++ b/CS/ReportingApp/ReportingApp.csproj @@ -51,8 +51,8 @@ - - + + diff --git a/CS/ReportingApp/Services/AIAssistantCreator.cs b/CS/ReportingApp/Services/AIAssistantCreator.cs index c516512..91324bd 100644 --- a/CS/ReportingApp/Services/AIAssistantCreator.cs +++ b/CS/ReportingApp/Services/AIAssistantCreator.cs @@ -20,7 +20,7 @@ public AIAssistantCreator(OpenAIClient client, string deployment) { this.deployment = deployment; } - public async Task<(string assistantId, string threadId)> CreateAssistantAsync(Stream data, string fileName, string instructions, bool useFileSearchTool = true, CancellationToken ct = default) { + public async Task<(string assistantId, string threadId)> CreateAssistantAndThreadAsync(Stream data, string fileName, string instructions, CancellationToken ct = default) { data.Position = 0; ClientResult fileResponse = await fileClient.UploadFileAsync(data, fileName, FileUploadPurpose.Assistants, ct); @@ -28,7 +28,7 @@ public AIAssistantCreator(OpenAIClient client, string deployment) { var resources = new ToolResources() { CodeInterpreter = new CodeInterpreterToolResources(), - FileSearch = useFileSearchTool ? new FileSearchToolResources() : null + FileSearch = new FileSearchToolResources() }; resources.FileSearch?.NewVectorStores.Add(new VectorStoreCreationHelper([file.Id])); resources.CodeInterpreter.FileIds.Add(file.Id); @@ -36,13 +36,10 @@ public AIAssistantCreator(OpenAIClient client, string deployment) { AssistantCreationOptions assistantCreationOptions = new AssistantCreationOptions() { Name = Guid.NewGuid().ToString(), Instructions = instructions, - ToolResources = resources + ToolResources = resources, + Tools = { new CodeInterpreterToolDefinition(), + new FileSearchToolDefinition() } }; - assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); - if (useFileSearchTool) { - assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); - } - ClientResult assistantResponse = await assistantClient.CreateAssistantAsync(deployment, assistantCreationOptions, ct); ClientResult threadResponse = await assistantClient.CreateThreadAsync(cancellationToken: ct); diff --git a/CS/ReportingApp/Services/AIAssistantProvider.cs b/CS/ReportingApp/Services/AIAssistantProvider.cs index 4e06ec7..dc960c5 100644 --- a/CS/ReportingApp/Services/AIAssistantProvider.cs +++ b/CS/ReportingApp/Services/AIAssistantProvider.cs @@ -1,9 +1,9 @@ -using System; +using DevExpress.AIIntegration.Services.Assistant; +using Microsoft.AspNetCore.Hosting; +using System; using System.Collections.Concurrent; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using DevExpress.AIIntegration.Services.Assistant; namespace ReportingApp.Services { public class AIAssistantProvider : IAIAssistantProvider { @@ -19,7 +19,7 @@ public class AIAssistantProvider : IAIAssistantProvider { private ConcurrentDictionary Assistants { get; set; } = new (); private async Task CreateAssistant(Stream data, string fileName, string prompt) { - (string assistantId, string threadId) = await assistantCreator.CreateAssistantAsync(data, fileName, prompt); + (string assistantId, string threadId) = await assistantCreator.CreateAssistantAndThreadAsync(data, fileName, prompt); IAIAssistant assistant = await assistantFactory.GetAssistant(assistantId, threadId); await assistant.InitializeAsync(); @@ -35,9 +35,15 @@ public AIAssistantProvider(IAIAssistantFactory assistantFactory, IWebHostEnviron this.environment = environment; this.assistantCreator = assistantCreator; } + + // Creates a Data Analysis Assistant for Web Document Viewer. + // This assistant analyzes report content and answers questions related to information within the report. public async Task CreateDocumentAssistant(Stream data) { return await CreateAssistant(data, Guid.NewGuid().ToString() + ".pdf", DOCUMENT_ASSISTANT_PROMPT); } + + // Creates a UI Asisstant for Web Report Designer. + // This assistant explains how to use the Designer UI to accomplish various tasks. public async Task CreateUserAssistant() { string dirPath = Path.Combine(environment.ContentRootPath, "Data"); string filePath = Path.Combine(dirPath, DOCUMENTATION_FILE_NAME); diff --git a/CS/ReportingApp/wwwroot/js/aiIntegration.js b/CS/ReportingApp/wwwroot/js/aiIntegration.js index 9799b30..3e726b2 100644 --- a/CS/ReportingApp/wwwroot/js/aiIntegration.js +++ b/CS/ReportingApp/wwwroot/js/aiIntegration.js @@ -1,4 +1,4 @@ -const createAssistantTab = (function() { +const createAssistantTab = (function() { let lastUserQuery; let errorList = []; @@ -34,11 +34,16 @@ const createAssistantTab = (function() { } function normalizeAIResponse(text) { - text = text.replace(/【\d+:\d+†[^\】]+】/g, ""); - let html = marked.parse(text); - if(/

\.\s*<\/p>\s*$/.test(html)) - html = html.replace(/

\.\s*<\/p>\s*$/, "") - return html; + if (text) { + text = text.replace(/【\d+:\d+†[^\】]+】/g, ""); + let html = marked.parse(text); + if (/

\.\s*<\/p>\s*$/.test(html)) + html = html.replace(/

\.\s*<\/p>\s*$/, "") + return html; + } + else { + return "Please try again later." + } } function copyText(text) { diff --git a/README.md b/README.md index 700b2ff..22abb15 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -![](https://img.shields.io/endpoint?url=https://codecentral.devexpress.com/api/v1/VersionRange/853003889/25.1.3%2B) [![](https://img.shields.io/badge/Open_in_DevExpress_Support_Center-FF7200?style=flat-square&logo=DevExpress&logoColor=white)](https://supportcenter.devexpress.com/ticket/details/T1252182) [![](https://img.shields.io/badge/📖_How_to_use_DevExpress_Examples-e9f6fc?style=flat-square)](https://docs.devexpress.com/GeneralInformation/403183) [![](https://img.shields.io/badge/💬_Leave_Feedback-feecdd?style=flat-square)](#does-this-example-address-your-development-requirementsobjectives) @@ -18,7 +17,9 @@ The AI assistant's role depends on the associated DevExpress Reports component: > [!Note] > We use the following versions of the `Microsoft.Extensions.AI.*` libraries in our source code: > -> v25.1.2+ | **9.4.3-preview.1.25230.7** +> - Microsoft.Extensions.AI.Abstractions: **9.5.0** +> - Microsoft.Extensions.AI: **9.5.0** +> - Microsoft.Extensions.AI.OpenAI: **9.5.0-preview.1.25265.7** > > We do not guarantee compatibility or correct operation with higher versions. @@ -76,20 +77,28 @@ Files to Review: #### AI Assistant Provider -On the server side, the `AIAssistantProvider` service manages assistants. An `IAIAssistantFactory` instance creates assistants with keys specified in previous steps. +On the server side, the `AIAssistantProvider` service manages assistants. ```cs public interface IAIAssistantProvider { IAIAssistant GetAssistant(string assistantName); - Task CreateAssistant(AssistantType assistantType, Stream data); - Task CreateAssistant(AssistantType assistantType); + Task CreateDocumentAssistant(Stream data); + Task CreateUserAssistant(); void DisposeAssistant(string assistantName); } ``` +The `AIAssistantCreator.CreateAssistantAsync` method uploads a file to OpenAI, configures tool resources, creates an assistant with specified instructions and tools, initializes a new thread, and returns the assistant and thread IDs. The generated assistant and thread IDs are then passed to the `IAIAssistantFactory.GetAssistant` method, which returns an `IAIAssistant` instance. The created instance is added to the application's assistant collection and is referenced by its unique name. + +For information on OpenAI Assistants, refer to the following documents: +- [OpenAI Assistants API overview](https://platform.openai.com/docs/assistants/overview) +- [Azure OpenAI: OpenAI Assistants client library for .NET](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.openai.assistants-readme?view=azure-dotnet-preview) +- [OpenAI .NET API library](https://github.com/openai/openai-dotnet) + Files to Review: - [AIAssistantProvider.cs](./CS/ReportingApp/Services/AIAssistantProvider.cs) - [IAIAssistantProvider.cs](./CS/ReportingApp/Services/IAIAssistantProvider.cs) +- [AIAssistantCreator.cs](./CS/ReportingApp/Services/AIAssistantCreator.cs) ### Web Document Viewer (Data Analysis Assistant) @@ -132,7 +141,7 @@ On the `BeforeRender` event, add a new tab (a container for the assistant interf #### Access the Assistant -Once the document is ready, the `DocumentReady` event handler sends a request to the server and obtains the assistant's ID: +Once the document is ready, the `DocumentReady` event handler sends a request to the server and obtains the assistant name: ```js async function DocumentReady(sender, args) { @@ -144,7 +153,27 @@ async function DocumentReady(sender, args) { } ``` -The [`PerformCustomDocumentOperation`](https://docs.devexpress.com/XtraReports/js-ASPxClientWebDocumentViewer?p=netframework#js_aspxclientwebdocumentviewer_performcustomdocumentoperation) method exports the report to PDF and creates an assistant based on the exported document. See [AIDocumentOperationService.cs](./CS/ReportingApp/Services/AIDocumentOperationService.cs) for implementation details. +The [`PerformCustomDocumentOperation`](https://docs.devexpress.com/XtraReports/js-ASPxClientWebDocumentViewer?p=netframework#js_aspxclientwebdocumentviewer_performcustomdocumentoperation) method exports the report to PDF and creates an assistant based on the exported document: + +```cs +// ... +public override async Task PerformOperationAsync(DocumentOperationRequest request, PrintingSystemBase printingSystem, PrintingSystemBase printingSystemWithEditingFields) { + using(var stream = new MemoryStream()) { + printingSystem.ExportToPdf(stream, printingSystem.ExportOptions.Pdf); + var assistantName = await AIAssistantProvider.CreateDocumentAssistant(stream); + return new DocumentOperationResponse { + DocumentId = request.DocumentId, + CustomData = assistantName, + Succeeded = true + }; + } +} +``` + +See the following files for implementation details: + +- [AIDocumentOperationService.cs](./CS/ReportingApp/Services/AIDocumentOperationService.cs) +- [AIAssistantProvider.cs](./CS/ReportingApp/Services/AIAssistantProvider.cs) #### Communicate with the Assistant @@ -239,21 +268,9 @@ async function BeforeRender(sender, args) { } ``` -The `AIAssistantProvider` service creates an assistant using the provided PDF documentation (the *documentation.pdf* file): +The `AIAssistantProvider.CreateUserAssistant` method creates an assistant using the *documentation.pdf* file ([end-user documentation for Web Reporting Controls](https://github.com/DevExpress/dotnet-eud) in the PDF format) and the specified prompt. See the [AIAssistantProvider.cs](./CS/ReportingApp/Services/AIAssistantProvider.cs) file for implementation details. + -```cs -// ... -public async Task CreateAssistant(AssistantType assistantType, Stream data) { - var assistantName = Guid.NewGuid().ToString(); - var assistant = await assistantFactory.CreateAssistant(assistantName); - Assistants.TryAdd(assistantName, assistant); - var prompt = GetPrompt(assistantType); - if(assistantType == AssistantType.UserAssistant) { - await LoadDocumentation(assistant, prompt); - } - return assistantName; -} -``` #### Communicate with the Assistant Each time a user sends a message, the [`onMessageEntered`](https://js.devexpress.com/jQuery/Documentation/24_2/ApiReference/UI_Components/dxChat/Configuration/#onMessageEntered) event handler passes the request to the assistant: