Skip to content
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
4 changes: 2 additions & 2 deletions CS/ReportingApp/ReportingApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
<PackageReference Include="Azure.AI.OpenAI.Assistants" Version="1.0.0-beta.4" />
<PackageReference Include="DevExpress.AIIntegration.OpenAI" Version="25.1.*-*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.4.3-preview.1.25230.7" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.3-preview.1.25230.7" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.5.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.5.0-preview.1.25265.7" />
<PackageReference Include="System.Data.SQLite" Version="1.0.117" />
<PackageReference Include="DevExpress.AspNetCore.Reporting" Version="25.1.*-*" />
<PackageReference Include="DevExpress.AIIntegration.Web" Version="25.1.*-*" />
Expand Down
13 changes: 5 additions & 8 deletions CS/ReportingApp/Services/AIAssistantCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,26 @@ 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<OpenAIFile> fileResponse = await fileClient.UploadFileAsync(data, fileName, FileUploadPurpose.Assistants, ct);
OpenAIFile file = fileResponse.Value;

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);

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<Assistant> assistantResponse = await assistantClient.CreateAssistantAsync(deployment, assistantCreationOptions, ct);
ClientResult<AssistantThread> threadResponse = await assistantClient.CreateThreadAsync(cancellationToken: ct);

Expand Down
14 changes: 10 additions & 4 deletions CS/ReportingApp/Services/AIAssistantProvider.cs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,7 +19,7 @@ public class AIAssistantProvider : IAIAssistantProvider {
private ConcurrentDictionary<string, IAIAssistant> Assistants { get; set; } = new ();

private async Task<string> 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();
Expand All @@ -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<string> 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<string> CreateUserAssistant() {
string dirPath = Path.Combine(environment.ContentRootPath, "Data");
string filePath = Path.Combine(dirPath, DOCUMENTATION_FILE_NAME);
Expand Down
17 changes: 11 additions & 6 deletions CS/ReportingApp/wwwroot/js/aiIntegration.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const createAssistantTab = (function() {
const createAssistantTab = (function() {

let lastUserQuery;
let errorList = [];
Expand Down Expand Up @@ -34,11 +34,16 @@ const createAssistantTab = (function() {
}

function normalizeAIResponse(text) {
text = text.replace(/【\d+:\d+†[^\】]+】/g, "");
let html = marked.parse(text);
if(/<p>\.\s*<\/p>\s*$/.test(html))
html = html.replace(/<p>\.\s*<\/p>\s*$/, "")
return html;
if (text) {
text = text.replace(/【\d+:\d+†[^\】]+】/g, "");
let html = marked.parse(text);
if (/<p>\.\s*<\/p>\s*$/.test(html))
html = html.replace(/<p>\.\s*<\/p>\s*$/, "")
return html;
}
else {
return "Please try again later."
}
}

function copyText(text) {
Expand Down
59 changes: 38 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<!-- default badges list -->
![](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)
Expand All @@ -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.

Expand Down Expand Up @@ -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<string> CreateAssistant(AssistantType assistantType, Stream data);
Task<string> CreateAssistant(AssistantType assistantType);
Task<string> CreateDocumentAssistant(Stream data);
Task<string> 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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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<DocumentOperationResponse> 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

Expand Down Expand Up @@ -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<string> 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:
Expand Down