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
14 changes: 14 additions & 0 deletions CrestApps.OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrestApps.OrchardCore.Core"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrestApps.Azure.Core", "src\Core\CrestApps.Azure.Core\CrestApps.Azure.Core.csproj", "{8BB36191-F041-43ED-B6FA-1C6302ED7F5B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrestApps.OrchardCore.AI.Mcp", "src\Modules\CrestApps.OrchardCore.AI.Mcp\CrestApps.OrchardCore.AI.Mcp.csproj", "{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrestApps.OrchardCore.AI.Mcp.Core", "src\Core\CrestApps.OrchardCore.AI.Mcp.Core\CrestApps.OrchardCore.AI.Mcp.Core.csproj", "{A49E8890-01E5-4891-8420-6DDD2E51C178}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -177,6 +181,14 @@ Global
{8BB36191-F041-43ED-B6FA-1C6302ED7F5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BB36191-F041-43ED-B6FA-1C6302ED7F5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BB36191-F041-43ED-B6FA-1C6302ED7F5B}.Release|Any CPU.Build.0 = Release|Any CPU
{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240}.Release|Any CPU.Build.0 = Release|Any CPU
{A49E8890-01E5-4891-8420-6DDD2E51C178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A49E8890-01E5-4891-8420-6DDD2E51C178}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A49E8890-01E5-4891-8420-6DDD2E51C178}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A49E8890-01E5-4891-8420-6DDD2E51C178}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -206,6 +218,8 @@ Global
{19389C50-D06F-4A15-96CC-303A46A9D497} = {F691A110-A6F0-4CFB-AA67-6A6469457B3A}
{86FFE9F8-0E1D-475E-8E86-B38F217F141D} = {EC2B9CAB-56C8-4421-ACA2-43C09B71DAB5}
{8BB36191-F041-43ED-B6FA-1C6302ED7F5B} = {EC2B9CAB-56C8-4421-ACA2-43C09B71DAB5}
{3ED9AAF7-BCFB-4456-A2FB-1A290C5C6240} = {C8D22F16-3D2B-4053-B0D5-A1F5EA5A91C2}
{A49E8890-01E5-4891-8420-6DDD2E51C178} = {EC2B9CAB-56C8-4421-ACA2-43C09B71DAB5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0E9F9B00-7078-4EA8-87A9-0B2E74375F1E}
Expand Down
36 changes: 19 additions & 17 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<OrchardCoreVersion>[2.1.0,3.0.0)</OrchardCoreVersion>
</PropertyGroup>
<ItemGroup>
<!-- Miscellaneous Packages -->
<PackageVersion Include="NuGet.Versioning" Version="6.13.2" />
<PackageVersion Include="YesSql.Abstractions" Version="5.1.1" />
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.8" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.7" />
</ItemGroup>
<ItemGroup>
<!-- OrchardCore Packages -->
<PackageVersion Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />
<PackageVersion Include="OrchardCore.Admin.Abstractions" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Abstractions" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.Application.Cms.Core.Targets" Version="$(OrchardCoreVersion)" />
<PackageVersion Include="OrchardCore.ContentFields" Version="$(OrchardCoreVersion)" />
Expand Down Expand Up @@ -45,29 +51,25 @@
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageVersion Include="Azure.Identity" Version="1.13.2" />
<PackageVersion Include="Azure.ResourceManager.CognitiveServices" Version="1.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
<PackageVersion Include="Microsoft.Extensions.AI.Ollama" Version="9.3.0-preview.1.25114.11" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25114.11" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.3.0-preview.1.25114.11" />
</ItemGroup>
<ItemGroup>
<!-- Miscellaneous Packages -->
<PackageVersion Include="NuGet.Versioning" Version="6.13.2" />
<PackageVersion Include="YesSql.Abstractions" Version="5.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.4.0-preview.1.25207.5" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.4.0-preview.1.25207.5" />
<PackageVersion Include="Microsoft.Extensions.AI.Ollama" Version="9.4.0-preview.1.25207.5" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.0-preview.1.25207.5" />
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.4.0-preview.1.25207.5" />
</ItemGroup>
<ItemGroup>
<!-- Testing Packages -->
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Aspire Packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.1.0" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="9.1.0" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.Ollama" Version="9.1.1-beta.169" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.2.0" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="9.2.0" />
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.Ollama" Version="9.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!-- .Net9 Packages -->
Expand All @@ -78,4 +80,4 @@
<!-- .Net8 Packages -->
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.10.0" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using CrestApps.OrchardCore.AI.Models;

namespace CrestApps.OrchardCore.AI;

public interface IAICompletionServiceHandler
{
/// <summary>
/// Called on every request to configure the <see cref="ChatOptions"/> in the <see cref="CompletionServiceConfigureContext"/>.
/// This allows dynamic customization of the completion behavior depending on the request context.
/// </summary>
/// <param name="context">
/// The <see cref="CompletionServiceConfigureContext"/> that provides access to request-specific options and settings.
/// </param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ConfigureAsync(CompletionServiceConfigureContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.AI;

namespace CrestApps.OrchardCore.AI.Models;

public sealed class CompletionServiceConfigureContext
{
public ChatOptions ChatOptions { get; }

public readonly AIProfile Profile;

public bool IsFunctionInvocationSupported { get; }

public CompletionServiceConfigureContext(
ChatOptions chatOptions,
AIProfile profile,
bool isFunctionInvocationSupported)
{
ArgumentNullException.ThrowIfNull(chatOptions);
ArgumentNullException.ThrowIfNull(profile);

ChatOptions = chatOptions;
Profile = profile;
IsFunctionInvocationSupported = isFunctionInvocationSupported;
}
}
2 changes: 1 addition & 1 deletion src/Core/CrestApps.OrchardCore.AI.Core/AIOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Localization;

namespace CrestApps.OrchardCore.AI;
namespace CrestApps.OrchardCore.AI.Core;

public sealed class AIOptions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using CrestApps.OrchardCore.AI.Core.Models;
using CrestApps.OrchardCore.AI.Models;
using OrchardCore.Entities;

namespace CrestApps.OrchardCore.AI.Core.Handlers;

public sealed class FunctionInvocationAICompletionServiceHandler : IAICompletionServiceHandler
{
private readonly IAIToolsService _toolsService;

public FunctionInvocationAICompletionServiceHandler(IAIToolsService toolsService)
{
_toolsService = toolsService;
}

public async Task ConfigureAsync(CompletionServiceConfigureContext context)
{
if (!context.IsFunctionInvocationSupported ||
!(context?.Profile.TryGet<AIProfileFunctionInvocationMetadata>(out var funcMetadata) ?? false))
{
return;
}

context.ChatOptions.Tools ??= [];

if (funcMetadata.Names is not null && funcMetadata.Names.Length > 0)
{
foreach (var name in funcMetadata.Names)
{
var tool = await _toolsService.GetByNameAsync(name);

if (tool is null)
{
continue;
}

context.ChatOptions.Tools.Add(tool);
}
}

if (funcMetadata.InstanceIds is not null && funcMetadata.InstanceIds.Length > 0)
{
foreach (var instanceId in funcMetadata.InstanceIds)
{
var tool = await _toolsService.GetByInstanceIdAsync(instanceId);

if (tool is null)
{
continue;
}

context.ChatOptions.Tools.Add(tool);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Text.Json.Serialization;

namespace CrestApps.OrchardCore.AI.Core.Models;

public sealed class DefaultAIOptions
Expand All @@ -16,15 +14,9 @@ public sealed class DefaultAIOptions

public int PastMessagesCount = 10;

public int? MaximumIterationsPerRequest { get; set; } = 1;
public int MaximumIterationsPerRequest { get; set; } = 1;

public bool EnableOpenTelemetry { get; set; }

public bool EnableDistributedCaching { get; set; } = true;

/// <summary>
/// This property is set via code and not the setting.
/// </summary>
[JsonIgnore]
public bool EnableFunctionInvocation { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace CrestApps.OrchardCore.AI.ViewModels;
namespace CrestApps.OrchardCore.AI.Core.Models;

public class ToolEntry
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public DeploymentAwareAICompletionClient(
ILoggerFactory loggerFactory,
AIProviderOptions providerOptions,
DefaultAIOptions defaultOptions,
IAIToolsService toolsService,
IEnumerable<IAICompletionServiceHandler> handlers,
IModelStore<AIDeployment> deploymentStore)
: base(name, distributedCache, loggerFactory, providerOptions, defaultOptions, toolsService)
: base(name, distributedCache, loggerFactory, providerOptions, defaultOptions, handlers)
{
_store = deploymentStore;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using OrchardCore.Entities;
using OrchardCore.Modules;

namespace CrestApps.OrchardCore.AI.Core.Services;

Expand All @@ -14,10 +15,10 @@ public abstract class NamedAICompletionClient : AICompletionServiceBase, IAIComp

public const string DefaultLogCategory = "AICompletionService";

private readonly IAIToolsService _toolsService;
private readonly DefaultAIOptions _defaultOptions;
private readonly IDistributedCache _distributedCache;
private readonly ILoggerFactory _loggerFactory;
private readonly IEnumerable<IAICompletionServiceHandler> _handlers;
private readonly DefaultAIOptions _defaultOptions;
private readonly ILogger _logger;

public NamedAICompletionClient(
Expand All @@ -26,16 +27,16 @@ public NamedAICompletionClient(
ILoggerFactory loggerFactory,
AIProviderOptions providerOptions,
DefaultAIOptions defaultOptions,
IAIToolsService toolsService)
IEnumerable<IAICompletionServiceHandler> handlers)
: base(providerOptions)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
Name = name;
_distributedCache = distributedCache;
_loggerFactory = loggerFactory;
_toolsService = toolsService;
_defaultOptions = defaultOptions;
_logger = loggerFactory.CreateLogger(DefaultLogCategory);
_handlers = handlers;
}

public string Name { get; }
Expand All @@ -56,7 +57,7 @@ protected virtual void ConfigureFunctionInvocation(FunctionInvokingChatClient cl

protected virtual bool SupportFunctionInvocation(AICompletionContext context, string modelName)
{
return !context.DisableTools && _defaultOptions.EnableFunctionInvocation;
return !context.DisableTools;
}

protected virtual void ConfigureLogger(LoggingChatClient client)
Expand Down Expand Up @@ -166,44 +167,15 @@ private async Task<ChatOptions> GetChatOptionsAsync(AICompletionContext context,
MaxOutputTokens = metadata.MaxTokens ?? _defaultOptions.MaxOutputTokens,
};

if (SupportFunctionInvocation(context, deploymentName) && (context?.Profile.TryGet<AIProfileFunctionInvocationMetadata>(out var funcMetadata) ?? false))
var supportFunctions = SupportFunctionInvocation(context, deploymentName);

var configureContext = new CompletionServiceConfigureContext(chatOptions, context.Profile, supportFunctions);

await _handlers.InvokeAsync((handler, ctx) => handler.ConfigureAsync(ctx), configureContext, _logger);

if (!supportFunctions || (chatOptions.Tools is not null && chatOptions.Tools.Count == 0))
{
chatOptions.Tools = [];

if (funcMetadata.Names is not null && funcMetadata.Names.Length > 0)
{
foreach (var name in funcMetadata.Names)
{
var tool = await _toolsService.GetByNameAsync(name);

if (tool is null)
{
continue;
}

chatOptions.Tools.Add(tool);
}
}

if (funcMetadata.InstanceIds is not null && funcMetadata.InstanceIds.Length > 0)
{
foreach (var instanceId in funcMetadata.InstanceIds)
{
var tool = await _toolsService.GetByInstanceIdAsync(instanceId);

if (tool is null)
{
continue;
}

chatOptions.Tools.Add(tool);
}
}

if (chatOptions.Tools.Count == 0)
{
chatOptions.Tools = null;
}
chatOptions.Tools = null;
}

ConfigureChatOptions(chatOptions, deploymentName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Title>CrestApps OrchardCore AI MCP Core</Title>
<Description>
$(CrestAppsDescription)

Core services project for the AI MCP module.
</Description>
<PackageTags>$(PackageTags) AI MCP ModelContextProtocol</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Infrastructure.Abstractions" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Abstractions\CrestApps.OrchardCore.Abstractions\CrestApps.OrchardCore.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using CrestApps.OrchardCore.AI.Mcp.Core.Models;
using ModelContextProtocol.Protocol.Transport;

namespace CrestApps.OrchardCore.AI.Mcp.Core;

public interface IMcpClientTransportProvider
{
/// <summary>
/// Determines whether this provider can handle the specified <see cref="McpConnection"/>.
/// </summary>
/// <param name="connection">The MCP connection to evaluate.</param>
/// <returns>
/// <c>true</c> if this provider supports the given connection; otherwise, <c>false</c>.
/// </returns>
bool CanHandle(McpConnection connection);

/// <summary>
/// Gets an <see cref="IClientTransport"/> instance for the specified <see cref="McpConnection"/>.
/// </summary>
/// <param name="connection">The MCP connection for which to obtain a transport.</param>
/// <returns>An <see cref="IClientTransport"/> that can be used with the given connection.</returns>
IClientTransport Get(McpConnection connection);
}
Loading