diff --git a/samples/ExampleBlazorApp/ExampleBlazorApp.csproj b/samples/ExampleBlazorApp/ExampleBlazorApp.csproj
index 0699f52..1c1c3fc 100644
--- a/samples/ExampleBlazorApp/ExampleBlazorApp.csproj
+++ b/samples/ExampleBlazorApp/ExampleBlazorApp.csproj
@@ -12,8 +12,8 @@
-
+
diff --git a/samples/ExampleBlazorApp/Program.cs b/samples/ExampleBlazorApp/Program.cs
index 16aeed6..9291a1f 100644
--- a/samples/ExampleBlazorApp/Program.cs
+++ b/samples/ExampleBlazorApp/Program.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using ExampleBlazorApp.Components;
-using SmartComponents.Inference.OpenAI;
+using Microsoft.Extensions.AI.Abstractions;
using SmartComponents.LocalEmbeddings;
var builder = WebApplication.CreateBuilder(args);
@@ -12,7 +12,6 @@
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddSmartComponents()
- .WithInferenceBackend()
.WithAntiforgeryValidation();
builder.Services.AddSingleton();
diff --git a/samples/ExampleMvcRazorPagesApp/ExampleMvcRazorPagesApp.csproj b/samples/ExampleMvcRazorPagesApp/ExampleMvcRazorPagesApp.csproj
index db40a57..ff3e10b 100644
--- a/samples/ExampleMvcRazorPagesApp/ExampleMvcRazorPagesApp.csproj
+++ b/samples/ExampleMvcRazorPagesApp/ExampleMvcRazorPagesApp.csproj
@@ -9,8 +9,8 @@
-
+
diff --git a/samples/ExampleMvcRazorPagesApp/Program.cs b/samples/ExampleMvcRazorPagesApp/Program.cs
index 024507b..91fca00 100644
--- a/samples/ExampleMvcRazorPagesApp/Program.cs
+++ b/samples/ExampleMvcRazorPagesApp/Program.cs
@@ -1,7 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using SmartComponents.Inference.OpenAI;
+using Microsoft.Extensions.AI.Abstractions;
using SmartComponents.LocalEmbeddings;
var builder = WebApplication.CreateBuilder(args);
@@ -11,7 +11,6 @@
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddSmartComponents()
- .WithInferenceBackend()
.WithAntiforgeryValidation();
builder.Services.AddSingleton();
diff --git a/src/SmartComponents.AspNetCore/DefaultSmartComponentsBuilder.cs b/src/SmartComponents.AspNetCore/DefaultSmartComponentsBuilder.cs
index 607b98b..5b5ed2c 100644
--- a/src/SmartComponents.AspNetCore/DefaultSmartComponentsBuilder.cs
+++ b/src/SmartComponents.AspNetCore/DefaultSmartComponentsBuilder.cs
@@ -2,21 +2,21 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
-using SmartComponents.StaticAssets.Inference;
+using Microsoft.Extensions.AI.Abstractions;
namespace Microsoft.AspNetCore.Builder;
internal sealed class DefaultSmartComponentsBuilder(IServiceCollection services) : ISmartComponentsBuilder
{
- public ISmartComponentsBuilder WithInferenceBackend(string? name) where T : class, IInferenceBackend
+ public ISmartComponentsBuilder WithInferenceBackend(string? name) where T : class, IChatClient
{
if (string.IsNullOrEmpty(name))
{
- services.AddSingleton();
+ services.AddSingleton();
}
else
{
- services.AddKeyedSingleton(name);
+ services.AddKeyedSingleton(name);
}
return this;
diff --git a/src/SmartComponents.AspNetCore/ISmartComponentsBuilder.cs b/src/SmartComponents.AspNetCore/ISmartComponentsBuilder.cs
index 4486137..4a2b46f 100644
--- a/src/SmartComponents.AspNetCore/ISmartComponentsBuilder.cs
+++ b/src/SmartComponents.AspNetCore/ISmartComponentsBuilder.cs
@@ -1,13 +1,13 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using SmartComponents.StaticAssets.Inference;
+using Microsoft.Extensions.AI.Abstractions;
namespace Microsoft.AspNetCore.Builder;
public interface ISmartComponentsBuilder
{
- public ISmartComponentsBuilder WithInferenceBackend(string? name = null) where T : class, IInferenceBackend;
+ public ISmartComponentsBuilder WithInferenceBackend(string? name = null) where T : class, IChatClient;
public ISmartComponentsBuilder WithAntiforgeryValidation();
}
diff --git a/src/SmartComponents.AspNetCore/SmartComponentsServiceCollectionExtensions.cs b/src/SmartComponents.AspNetCore/SmartComponentsServiceCollectionExtensions.cs
index 9258818..48b4f4e 100644
--- a/src/SmartComponents.AspNetCore/SmartComponentsServiceCollectionExtensions.cs
+++ b/src/SmartComponents.AspNetCore/SmartComponentsServiceCollectionExtensions.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
@@ -9,10 +9,10 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.AI.Abstractions;
using SmartComponents.AspNetCore;
using SmartComponents.Inference;
using SmartComponents.Infrastructure;
-using SmartComponents.StaticAssets.Inference;
namespace Microsoft.AspNetCore.Builder;
@@ -40,7 +40,7 @@ public Action Configure(Action next) =
builder.UseEndpoints(app =>
{
- var smartPasteEndpoint = app.MapPost("/_smartcomponents/smartpaste", async ([FromServices] IInferenceBackend inference, HttpContext httpContext, [FromServices] IAntiforgery antiforgery, [FromServices] SmartPasteInference smartPasteInference) =>
+ var smartPasteEndpoint = app.MapPost("/_smartcomponents/smartpaste", async ([FromServices] IChatClient inference, HttpContext httpContext, [FromServices] IAntiforgery antiforgery, [FromServices] SmartPasteInference smartPasteInference) =>
{
// The rules about whether antiforgery are enabled by default vary across different
// ASP.NET Core versions. To make it consistent, we disable the default enablement on
@@ -62,7 +62,7 @@ public Action Configure(Action next) =
return result.BadRequest ? Results.BadRequest() : Results.Content(result.Response!);
});
- var smartTextAreaEndpoint = app.MapPost("/_smartcomponents/smarttextarea", async ([FromServices] IInferenceBackend inference, HttpContext httpContext, [FromServices] IAntiforgery antiforgery, [FromServices] SmartTextAreaInference smartTextAreaInference) =>
+ var smartTextAreaEndpoint = app.MapPost("/_smartcomponents/smarttextarea", async ([FromServices] IChatClient inference, HttpContext httpContext, [FromServices] IAntiforgery antiforgery, [FromServices] SmartTextAreaInference smartTextAreaInference) =>
{
if (validateAntiforgery)
{
diff --git a/src/SmartComponents.Inference.OpenAI/ApiConfig.cs b/src/SmartComponents.Inference.OpenAI/ApiConfig.cs
deleted file mode 100644
index 5a2cc86..0000000
--- a/src/SmartComponents.Inference.OpenAI/ApiConfig.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using Microsoft.Extensions.Configuration;
-
-namespace SmartComponents.Inference.OpenAI;
-
-internal class ApiConfig
-{
- public string? ApiKey { get; }
- public string? DeploymentName { get; }
- public Uri? Endpoint { get; }
- public bool SelfHosted { get; }
-
- public ApiConfig(IConfiguration config)
- {
- var configSection = config.GetRequiredSection("SmartComponents");
-
- SelfHosted = configSection.GetValue("SelfHosted") ?? false;
-
- if (SelfHosted)
- {
- Endpoint = configSection.GetValue("Endpoint")
- ?? throw new InvalidOperationException("Missing required configuration value: SmartComponents:Endpoint. This is required for SelfHosted inference.");
-
- // Ollama uses this, but other self-hosted backends might not, so it's optional.
- DeploymentName = configSection.GetValue("DeploymentName");
-
- // Ollama doesn't use this, but other self-hosted backends might do, so it's optional.
- ApiKey = configSection.GetValue("ApiKey");
- }
- else
- {
- // If set, we assume Azure OpenAI. If not, we assume OpenAI.
- Endpoint = configSection.GetValue("Endpoint");
-
- // For Azure OpenAI, it's your deployment name. For OpenAI, it's the model name.
- DeploymentName = configSection.GetValue("DeploymentName")
- ?? throw new InvalidOperationException("Missing required configuration value: SmartComponents:DeploymentName");
-
- ApiKey = configSection.GetValue("ApiKey")
- ?? throw new InvalidOperationException("Missing required configuration value: SmartComponents:ApiKey");
- }
- }
-}
diff --git a/src/SmartComponents.Inference.OpenAI/OpenAIInferenceBackend.cs b/src/SmartComponents.Inference.OpenAI/OpenAIInferenceBackend.cs
deleted file mode 100644
index faa7cf2..0000000
--- a/src/SmartComponents.Inference.OpenAI/OpenAIInferenceBackend.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using Azure;
-using Azure.AI.OpenAI;
-using Microsoft.Extensions.Configuration;
-using SmartComponents.StaticAssets.Inference;
-
-namespace SmartComponents.Inference.OpenAI;
-
-public class OpenAIInferenceBackend(IConfiguration configuration)
- : IInferenceBackend
-{
- public async Task GetChatResponseAsync(ChatParameters options)
- {
-#if DEBUG
- if (ResponseCache.TryGetCachedResponse(options, out var cachedResponse))
- {
- return cachedResponse!;
- }
-#endif
-
- var apiConfig = new ApiConfig(configuration);
- var client = CreateClient(apiConfig);
- var chatCompletionsOptions = new ChatCompletionsOptions
- {
- DeploymentName = apiConfig.DeploymentName,
- Temperature = options.Temperature ?? 0f,
- NucleusSamplingFactor = options.TopP ?? 1,
- MaxTokens = options.MaxTokens ?? 200,
- FrequencyPenalty = options.FrequencyPenalty ?? 0,
- PresencePenalty = options.PresencePenalty ?? 0,
- ResponseFormat = options.RespondJson ? ChatCompletionsResponseFormat.JsonObject : ChatCompletionsResponseFormat.Text,
- };
-
- foreach (var message in options.Messages ?? Enumerable.Empty())
- {
- chatCompletionsOptions.Messages.Add(message.Role switch
- {
- ChatMessageRole.System => new ChatRequestSystemMessage(message.Text),
- ChatMessageRole.User => new ChatRequestUserMessage(message.Text),
- ChatMessageRole.Assistant => new ChatRequestAssistantMessage(message.Text),
- _ => throw new InvalidOperationException($"Unknown chat message role: {message.Role}")
- });
- }
-
- if (options.StopSequences is { } stopSequences)
- {
- foreach (var stopSequence in stopSequences)
- {
- chatCompletionsOptions.StopSequences.Add(stopSequence);
- }
- }
-
- var completionsResponse = await client.GetChatCompletionsAsync(chatCompletionsOptions);
-
- var response = completionsResponse.Value.Choices.FirstOrDefault()?.Message.Content ?? string.Empty;
-
-#if DEBUG
- ResponseCache.SetCachedResponse(options, response);
-#endif
-
- return response;
- }
-
- private static OpenAIClient CreateClient(ApiConfig apiConfig)
- {
- if (apiConfig.SelfHosted)
- {
- var transport = new SelfHostedLlmTransport(apiConfig.Endpoint!);
- return new OpenAIClient(apiConfig.ApiKey, new() { Transport = transport });
- }
- else if (apiConfig.Endpoint is null)
- {
- // OpenAI
- return new OpenAIClient(apiConfig.ApiKey);
- }
- else
- {
- // Azure OpenAI
- return new OpenAIClient(
- apiConfig.Endpoint,
- new AzureKeyCredential(apiConfig.ApiKey!));
- }
- }
-}
diff --git a/src/SmartComponents.Inference.OpenAI/ResponseCache.cs b/src/SmartComponents.Inference.OpenAI/ResponseCache.cs
deleted file mode 100644
index 9435b4d..0000000
--- a/src/SmartComponents.Inference.OpenAI/ResponseCache.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-#if DEBUG
-using System;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using SmartComponents.StaticAssets.Inference;
-
-namespace SmartComponents.Inference.OpenAI;
-
-// This is primarily so that E2E tests running in CI don't have to call OpenAI for real, so that:
-// [1] We don't have to make the API keys available to CI
-// [2] There's no risk of random failures due to network issues or the nondeterminism of the AI responses
-// It will not be used in real apps in production. Its other benefit is reducing OpenAI usage during local development.
-
-internal static class ResponseCache
-{
- static bool IsEnabled = Environment.GetEnvironmentVariable("SMARTCOMPONENTS_E2E_TEST") == "true";
-
- readonly static Lazy CacheDir = new(() =>
- {
- var dir = Path.Combine(GetSolutionDirectory(), "test", "CachedResponses");
- Directory.CreateDirectory(dir);
- return dir;
- });
-
- public static bool TryGetCachedResponse(ChatParameters request, out string? response)
- {
- if (!IsEnabled)
- {
- response = null;
- return false;
- }
-
- var filePath = GetCacheFilePath(request);
- if (File.Exists(filePath))
- {
- Console.WriteLine("Using cached response for " + Path.GetFileName(filePath));
- response = File.ReadAllText(filePath);
- return true;
- }
- else
- {
- Console.WriteLine("Did not find cached response for " + Path.GetFileName(filePath));
- response = null;
- return false;
- }
- }
-
- public static void SetCachedResponse(ChatParameters request, string response)
- {
- if (IsEnabled)
- {
- var filePath = GetCacheFilePath(request);
- File.WriteAllText(filePath, response);
- File.WriteAllText(filePath.Replace(".response.txt", ".request.json"), GetCacheKeyInput(request));
- }
- }
-
- private static string GetCacheFilePath(ChatParameters request)
- => GetCacheFilePath(request, request.Messages.LastOrDefault()?.Text ?? "no_messages");
-
- private static string GetCacheFilePath(T request, string summary)
- => Path.Combine(CacheDir.Value, $"{GetCacheKey(request, summary)}.response.txt");
-
- private static string GetSolutionDirectory()
- {
- const string filename = "SmartComponents.sln";
- var dir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!);
- while (dir != null)
- {
- if (dir.EnumerateFiles(filename).Any())
- {
- return dir.FullName;
- }
-
- dir = dir.Parent;
- }
-
- throw new InvalidOperationException($"Could not find directory containing {filename}");
- }
-
- private static string GetCacheKeyInput(T request)
- {
- return JsonSerializer.Serialize(request).Replace("\\r", "");
- }
-
- private static string GetCacheKey(T request, string summary)
- {
- var json = GetCacheKeyInput(request);
- var sha256 = SHA256.Create();
- var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
-
- var sb = new StringBuilder();
- for (var i = 0; i < 8; i++)
- {
- sb.Append(hash[i].ToString("x2"));
- }
-
- sb.Append("_");
- sb.Append(ToShortSafeString(summary));
-
- return sb.ToString();
- }
-
- private static string ToShortSafeString(string summary)
- {
- // This is just to make the cache filenames more recognizable. Won't help much if there's a common long prefix.
- var sb = new StringBuilder();
- foreach (var c in summary)
- {
- if (char.IsLetterOrDigit(c))
- {
- sb.Append(c);
- }
- else if (c == ' ')
- {
- sb.Append('_');
- }
-
- if (sb.Length >= 30)
- {
- break;
- }
- }
- return sb.ToString();
- }
-}
-#endif
diff --git a/src/SmartComponents.Inference.OpenAI/SelfHostedLlmTransport.cs b/src/SmartComponents.Inference.OpenAI/SelfHostedLlmTransport.cs
deleted file mode 100644
index 9c79915..0000000
--- a/src/SmartComponents.Inference.OpenAI/SelfHostedLlmTransport.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Threading.Tasks;
-using Azure.Core;
-using Azure.Core.Pipeline;
-
-namespace SmartComponents.Inference.OpenAI;
-
-///
-/// Used to resolve queries using Ollama or anything else that exposes an OpenAI-compatible
-/// endpoint with a scheme/host/port set of your choice.
-///
-internal class SelfHostedLlmTransport(Uri endpoint) : HttpClientTransport
-{
- public override ValueTask ProcessAsync(HttpMessage message)
- {
- message.Request.Uri.Scheme = endpoint.Scheme;
- message.Request.Uri.Host = endpoint.Host;
- message.Request.Uri.Port = endpoint.Port;
- return base.ProcessAsync(message);
- }
-}
diff --git a/src/SmartComponents.Inference.OpenAI/SmartComponents.Inference.OpenAI.csproj b/src/SmartComponents.Inference.OpenAI/SmartComponents.Inference.OpenAI.csproj
deleted file mode 100644
index ed231ad..0000000
--- a/src/SmartComponents.Inference.OpenAI/SmartComponents.Inference.OpenAI.csproj
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- netstandard2.0
- latest
- true
- enable
-
-
-
-
-
-
-
-
-
diff --git a/src/SmartComponents.Inference/IInferenceBackend.cs b/src/SmartComponents.Inference/IInferenceBackend.cs
deleted file mode 100644
index 0ed0771..0000000
--- a/src/SmartComponents.Inference/IInferenceBackend.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Threading.Tasks;
-
-namespace SmartComponents.StaticAssets.Inference;
-
-public interface IInferenceBackend
-{
- Task GetChatResponseAsync(ChatParameters options);
-}