Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CrestApps.Core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
<Project Path="src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.A2AClient/CrestApps.Core.Mvc.Samples.A2AClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Samples.McpClient/CrestApps.Core.Mvc.Samples.McpClient.csproj" />
<Project Path="src/Startup/CrestApps.Core.Blazor.Web/CrestApps.Core.Blazor.Web.csproj" />
<Project Path="src/Startup/CrestApps.Core.Mvc.Web/CrestApps.Core.Mvc.Web.csproj" />
<Project Path="src/Startup/CrestApps.Core.Startup.Shared/CrestApps.Core.Startup.Shared.csproj" />
</Folder>
<Folder Name="/src/Stores/">
<Project Path="src/Stores/CrestApps.Core.Data.EntityCore/CrestApps.Core.Data.EntityCore.csproj" />
Expand All @@ -62,6 +64,7 @@
<Project Path="src/Utilities/CrestApps.Core.Support/CrestApps.Core.Support.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/CrestApps.Core.Tests.Samples/CrestApps.Core.Tests.Samples.csproj" />
<Project Path="tests/CrestApps.Core.Tests/CrestApps.Core.Tests.csproj" />
</Folder>
</Solution>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<ItemGroup>
<!-- Testing Packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="Microsoft.Playwright" Version="1.52.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="xunit.analyzers" Version="1.27.0" />
<PackageVersion Include="xunit.runner.inproc" Version="3.2.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public interface IAIClientProvider
/// <param name = "connection">The connection entry containing provider configuration.</param>
/// <param name = "deploymentName">The optional deployment name to use.</param>
/// <returns>A <see cref = "ValueTask{IImageGenerator}"/> representing the asynchronous operation.</returns>

#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
ValueTask<IImageGenerator> GetImageGeneratorAsync(AIProviderConnectionEntry connection, string deploymentName = null);
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace CrestApps.Core.AI.Exceptions;

public class AIDeploymentConfigurationException : Exception
{
public AIDeploymentConfigurationException(string message)
: base(message)
{
}

public AIDeploymentConfigurationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace CrestApps.Core.AI.Exceptions;

public sealed class AIDeploymentNotFoundException : AIDeploymentConfigurationException
{
public AIDeploymentNotFoundException(string message)
: base(message)
{
}

public AIDeploymentNotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using CrestApps.Core.Infrastructure.Indexing.Models;
using CrestApps.Core.Models;

namespace CrestApps.Core.Infrastructure.Indexing;

public interface ISearchIndexProfileProvisioningService
{
Task<ValidationResultDetails> CreateAsync(SearchIndexProfile profile, CancellationToken cancellationToken = default);
}
30 changes: 27 additions & 3 deletions src/Primitives/CrestApps.Core.AI.Chat/Hubs/AIChatHubCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CrestApps.Core.AI.Clients;
using CrestApps.Core.AI.Completions;
using CrestApps.Core.AI.Deployments;
using CrestApps.Core.AI.Exceptions;
using CrestApps.Core.AI.Models;
using CrestApps.Core.AI.Orchestration;
using CrestApps.Core.AI.Profiles;
Expand Down Expand Up @@ -122,9 +123,19 @@ protected virtual string GetNotAuthorizedMessage()

protected virtual string GetFriendlyErrorMessage(Exception ex)
{
if (AIHubErrorMessageHelper.IsInvalidChatModelSettingsFailure(ex))
{
return GetInvalidChatModelSettingsMessage();
}

return "An error occurred processing your message.";
}

protected virtual string GetInvalidChatModelSettingsMessage()
{
return "The chat model settings are missing or invalid. Update the Chat model in the AI Profile or the global AI settings.";
}

protected virtual string GetOnlyChatProfilesMessage()
{
return "Only chat profiles can start chat sessions.";
Expand Down Expand Up @@ -898,7 +909,18 @@ protected virtual async Task ProcessChatPromptAsync(ChannelWriter<CompletionPart
};
await promptStore.CreateAsync(userPromptRecord);
var existingPrompts = await promptStore.GetPromptsAsync(chatSession.SessionId);
var conversationHistory = existingPrompts.Where(x => !x.IsGeneratedPrompt).Select(p => new ChatMessage(p.Role, p.Content)).ToList();
var conversationHistorySource = existingPrompts.ToList();

if (!conversationHistorySource.Any(x => x.ItemId == userPromptRecord.ItemId))
{
conversationHistorySource.Add(userPromptRecord);
}

var conversationHistory = conversationHistorySource
.OrderBy(x => x.CreatedUtc)
.Where(x => !x.IsGeneratedPrompt)
.Select(p => new ChatMessage(p.Role, p.Content))
.ToList();
// Resolve the chat response handler for this session.
var chatMode = profile.TryGetSettings<ChatModeProfileSettings>(out var chatModeSettings) ? chatModeSettings.ChatMode : ChatMode.TextInput;
var handler = handlerResolver.Resolve(chatSession.ResponseHandlerName, chatMode);
Expand Down Expand Up @@ -1009,7 +1031,8 @@ protected virtual async Task ProcessGeneratedPromptAsync(ChannelWriter<Completio
};
var completionContext = await completionContextBuilder.BuildAsync(profile);
var deploymentManager = services.GetRequiredService<IAIDeploymentManager>();
var chatDeployment = await deploymentManager.ResolveOrDefaultAsync(AIDeploymentType.Chat, deploymentName: completionContext.ChatDeploymentName) ?? throw new InvalidOperationException("Unable to resolve a chat deployment for the profile.");
var chatDeployment = await deploymentManager.ResolveOrDefaultAsync(AIDeploymentType.Chat, deploymentName: completionContext.ChatDeploymentName)
?? throw new AIDeploymentNotFoundException("Unable to resolve a chat deployment for the profile.");
using var builder = ZString.CreateStringBuilder();
var contentItemIds = new HashSet<string>();
var references = new Dictionary<string, AICompletionReference>();
Expand Down Expand Up @@ -1048,7 +1071,8 @@ protected virtual async Task ProcessUtilityAsync(ChannelWriter<CompletionPartial
var deploymentManager = services.GetRequiredService<IAIDeploymentManager>();
var messageId = GenerateId();
var completionContext = await completionContextBuilder.BuildAsync(profile);
var chatDeployment = await deploymentManager.ResolveOrDefaultAsync(AIDeploymentType.Chat, deploymentName: completionContext.ChatDeploymentName) ?? throw new InvalidOperationException("Unable to resolve a chat deployment for the profile.");
var chatDeployment = await deploymentManager.ResolveOrDefaultAsync(AIDeploymentType.Chat, deploymentName: completionContext.ChatDeploymentName)
?? throw new AIDeploymentNotFoundException("Unable to resolve a chat deployment for the profile.");
var references = new Dictionary<string, AICompletionReference>();
await foreach (var chunk in completionService.CompleteStreamingAsync(chatDeployment, [new ChatMessage(ChatRole.User, prompt)], completionContext, cancellationToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,19 @@ protected virtual string GetNotAuthorizedMessage()

protected virtual string GetFriendlyErrorMessage(Exception ex)
{
if (AIHubErrorMessageHelper.IsInvalidChatModelSettingsFailure(ex))
{
return GetInvalidChatModelSettingsMessage();
}

return "An error occurred while processing your message.";
}

protected virtual string GetInvalidChatModelSettingsMessage()
{
return "The chat model settings are missing or invalid. Update the Chat model in this chat interaction, the linked AI Profile, or the global AI settings.";
}

protected virtual string GetConversationNotEnabledMessage()
{
return "Conversation mode is not enabled for chat interactions.";
Expand Down Expand Up @@ -849,7 +859,15 @@ protected virtual async Task HandlePromptAsync(
}

var existingPrompts = await promptStore.GetPromptsAsync(itemId);
var conversationHistory = existingPrompts
var conversationHistorySource = existingPrompts.ToList();

if (!conversationHistorySource.Any(x => x.ItemId == userPrompt.ItemId))
{
conversationHistorySource.Add(userPrompt);
}

var conversationHistory = conversationHistorySource
.OrderBy(x => x.CreatedUtc)
.Where(x => !x.IsGeneratedPrompt)
.Select(p => new ChatMessage(p.Role, p.Text))
.ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,33 @@ public static async Task<IResult> HandleAsync(
return TypedResults.Forbid();
}

var document = await documentStore.FindByIdAsync(requestModel.DocumentId);
var documentInfo = interaction.Documents?.FirstOrDefault(document => document.DocumentId == requestModel.DocumentId);

if (documentInfo == null && document != null)
{
documentInfo = new ChatDocumentInfo
{
DocumentId = document.ItemId,
FileName = document.FileName,
FileSize = document.FileSize,
ContentType = document.ContentType,
};
}

if (documentInfo == null)
{
return TypedResults.NotFound("Document not found.");
}

interaction.Documents.Remove(documentInfo);

var document = await documentStore.FindByIdAsync(requestModel.DocumentId);
if (interaction.Documents != null)
{
var attachedDocument = interaction.Documents.FirstOrDefault(existingDocument => existingDocument.DocumentId == requestModel.DocumentId);
if (attachedDocument != null)
{
interaction.Documents.Remove(attachedDocument);
}
}

var chunkIds = new List<string>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using CrestApps.Core.AI.Tooling;
using CrestApps.Core.Infrastructure.Indexing;
using CrestApps.Core.Infrastructure.Indexing.Models;
using CrestApps.Core.Models;
using CrestApps.Core.Templates.Services;
using Cysharp.Text;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -27,7 +28,6 @@ internal sealed class DocumentPreemptiveRagHandler : IPreemptiveRagHandler
private readonly IAIDeploymentManager _deploymentManager;
private readonly ISearchIndexProfileStore _indexProfileStore;
private readonly ITemplateService _templateService;
private readonly InteractionDocumentOptions _options;
private readonly IAITextNormalizer _textNormalizer;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
Expand All @@ -37,7 +37,6 @@ public DocumentPreemptiveRagHandler(
IAIDeploymentManager deploymentManager,
ISearchIndexProfileStore indexProfileStore,
ITemplateService templateService,
IOptions<InteractionDocumentOptions> options,
IAITextNormalizer textNormalizer,
IServiceProvider serviceProvider,
ILogger<DocumentPreemptiveRagHandler> logger)
Expand All @@ -46,7 +45,6 @@ public DocumentPreemptiveRagHandler(
_deploymentManager = deploymentManager;
_indexProfileStore = indexProfileStore;
_templateService = templateService;
_options = options.Value;
_textNormalizer = textNormalizer;
_serviceProvider = serviceProvider;
_logger = logger;
Expand Down Expand Up @@ -74,14 +72,20 @@ public ValueTask<bool> CanHandleAsync(OrchestrationContextBuiltContext context)

public async Task HandleAsync(PreemptiveRagContext context)
{
if (string.IsNullOrEmpty(_options.IndexProfileName))
var snapshotSettings = _serviceProvider.GetService<IOptionsSnapshot<InteractionDocumentOptions>>()?.Value;
var optionsSettings = _serviceProvider.GetRequiredService<IOptions<InteractionDocumentOptions>>().Value;
var defaultSettings = !string.IsNullOrWhiteSpace(snapshotSettings?.IndexProfileName)
? snapshotSettings
: optionsSettings;

if (string.IsNullOrEmpty(defaultSettings.IndexProfileName))
{
return;
}

try
{
await InjectPreemptiveRagContextAsync(context, ResolveSettings(context.Resource, _options));
await InjectPreemptiveRagContextAsync(context, ResolveSettings(context.Resource, defaultSettings));
}
catch (Exception ex)
{
Expand Down Expand Up @@ -286,8 +290,8 @@ context.Resource is not AIProfile ||

private static InteractionDocumentOptions ResolveSettings(object resource, InteractionDocumentOptions defaults)
{
if (resource is AIProfile profile &&
profile.TryGet<DocumentsMetadata>(out var metadata))
if (resource is CatalogItem item &&
item.TryGet<DocumentsMetadata>(out var metadata))
{
return new InteractionDocumentOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using CrestApps.Core.AI.Tooling;
using CrestApps.Core.Infrastructure.Indexing;
using CrestApps.Core.Infrastructure.Indexing.Models;
using CrestApps.Core.Models;
using Cysharp.Text;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -92,7 +93,11 @@ protected override async ValueTask<object> InvokeCoreAsync(AIFunctionArguments a
_ => false,
};

var defaultSettings = arguments.Services.GetRequiredService<IOptions<InteractionDocumentOptions>>().Value;
var snapshotSettings = arguments.Services.GetService<IOptionsSnapshot<InteractionDocumentOptions>>()?.Value;
var optionsSettings = arguments.Services.GetRequiredService<IOptions<InteractionDocumentOptions>>().Value;
var defaultSettings = !string.IsNullOrWhiteSpace(snapshotSettings?.IndexProfileName)
? snapshotSettings
: optionsSettings;
var settings = ResolveSettings(executionContext?.Resource, defaultSettings);

if (string.IsNullOrWhiteSpace(settings.IndexProfileName))
Expand Down Expand Up @@ -223,8 +228,8 @@ protected override async ValueTask<object> InvokeCoreAsync(AIFunctionArguments a

private static InteractionDocumentOptions ResolveSettings(object resource, InteractionDocumentOptions defaults)
{
if (resource is AIProfile profile &&
profile.TryGet<DocumentsMetadata>(out var metadata))
if (resource is CatalogItem item &&
item.TryGet<DocumentsMetadata>(out var metadata))
{
return new InteractionDocumentOptions
{
Expand Down
22 changes: 22 additions & 0 deletions src/Primitives/CrestApps.Core.AI/AIHubErrorMessageHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using CrestApps.Core.AI.Exceptions;
using Microsoft.Extensions.Localization;

namespace CrestApps.Core.AI;
Expand Down Expand Up @@ -58,6 +59,19 @@ HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden
return S["Our service is currently unavailable. Please try again later."];
}

public static bool IsInvalidChatModelSettingsFailure(Exception ex)
{
foreach (var current in EnumerateExceptions(ex))
{
if (current is AIDeploymentConfigurationException)
{
return true;
}
}

return false;
}

private static int? TryGetClientResultStatusCode(Exception ex)
{
if (ex is null)
Expand Down Expand Up @@ -127,4 +141,12 @@ private static string ExtractRetryAfterMessage(string message)

return sentence.Trim();
}

private static IEnumerable<Exception> EnumerateExceptions(Exception ex)
{
for (var current = ex; current is not null; current = current.InnerException)
{
yield return current;
}
}
}
Loading
Loading