Skip to content

Commit

Permalink
feat: Add Autocomplete support
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Jun 3, 2024
1 parent 3c0b4e4 commit dcb8bf0
Show file tree
Hide file tree
Showing 16 changed files with 448 additions and 98 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ dotnet_diagnostic.S1075.severity = suggestion # S1075: URIs should not be hardco
dotnet_diagnostic.S1186.severity = suggestion # S1186: Methods should not be empty
dotnet_diagnostic.S2292.severity = suggestion # S2292: Trivial properties should be auto-implemented
dotnet_diagnostic.S4158.severity = none # BUGGY with C#9 code - doesnt understand local methods
dotnet_diagnostic.S4456.severity = suggestion # S4456: Split this method into two, one handling parameters check and the other handling the iterator

# Razor specific rules
[*.{cs,razor}]
Expand Down
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js]
- [Comments](./docs/Comments/Readme.md)
- [Storage Provider](./docs/Storage/Readme.md)
- [Search Engine Optimization (SEO)](./docs/SEO/Readme.md)
- [AI Autocomplete](./docs/Autocomplete/Readme.md)

## Installation

Expand Down
5 changes: 5 additions & 0 deletions docs/Autocomplete/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Autocomplete
The blog can utilize Microsofts **Semantic Kernel** to either generate or enhance the current blog post.
There are toggles to provide the AI with the Title, Descritpion, Markdown Content and Tags. You can also instruct the AI only to append content and not rewrite the whole article.

You can find the configuration in the `appsettings.json` file under the `AI` section.
11 changes: 11 additions & 0 deletions docs/Setup/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ The appsettings.json file has a lot of options to customize the content of the b
"BackgroundUrl": "assets/profile-background.webp",
"ProfilePictureUrl": "assets/profile-picture.webp"
},
"AI": {
"DeploymentName": "gpt-4o",
"EndpointUrl": "https://the-url",
"ModelId": "gpt-4o",
"ApiKey": "key"
},
"PersistenceProvider": "InMemory",
"ConnectionString": "",
"DatabaseName": "",
Expand Down Expand Up @@ -63,6 +69,11 @@ The appsettings.json file has a lot of options to customize the content of the b
| Introduction | | Is used for the introduction part of the blog |
| Description | MarkdownString | Small introduction text for yourself. This is also used for `<meta name="description">` tag. For this the markup will be converted to plain text |
| BackgroundUrl | string | Url or path to the background image. (Optional) |
| AI | | The AI section used for Semantic Kernel |
| DeploymentName | string | Name of the deployment |
| EndpointUrl | string | Url to the endpoint |
| ModelId | string | Model Id |
| ApiKey | string | Api Key |
| ProfilePictureUrl | string | Url or path to your profile picture |
| PersistenceProvider | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `InMemory`, `MySql`). More in-depth explanation [here](./../Storage/Readme.md) |
| ConnectionString | string | Is used for connection to a database. Not used when `InMemoryStorageProvider` is used |
Expand Down
19 changes: 19 additions & 0 deletions src/LinkDotNet.Blog.Domain/AiSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace LinkDotNet.Blog.Domain;

public class AiSettings
{
public const string AiSettingsSection = "AI";

public string DeploymentName { get; set; }

public string EndpointUrl { get; set; }

public string ModelId { get; set; }

public string ApiKey { get; set; }

public bool IsEnabled => !string.IsNullOrEmpty(DeploymentName)
&& !string.IsNullOrEmpty(EndpointUrl)
&& !string.IsNullOrEmpty(ModelId)
&& !string.IsNullOrEmpty(ApiKey);
}
13 changes: 13 additions & 0 deletions src/LinkDotNet.Blog.Web/ConfigurationExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static void AddConfiguration(this IServiceCollection services)
.AddApplicationConfiguration()
.AddAuthenticationConfigurations()
.AddIntroductionConfigurations()
.AddAiConfigurations()
.AddSocialConfigurations()
.AddProfileInformationConfigurations()
.AddGiscusConfiguration()
Expand Down Expand Up @@ -66,6 +67,18 @@ private static IServiceCollection AddIntroductionConfigurations(this IServiceCol
return services;
}

private static IServiceCollection AddAiConfigurations(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);

services.AddOptions<AiSettings>()
.Configure<IConfiguration>((settings, config) =>
{
config.GetSection(AiSettings.AiSettingsSection).Bind(settings);
});
return services;
}

private static IServiceCollection AddSocialConfigurations(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
Expand Down
22 changes: 22 additions & 0 deletions src/LinkDotNet.Blog.Web/Fakes/FakeCompletionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

namespace LinkDotNet.Blog.Web.Fakes;

internal sealed class FakeCompletionService : IChatCompletionService
{
public IReadOnlyDictionary<string, object> Attributes { get; }

public Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings executionSettings = null,
Kernel kernel = null, CancellationToken cancellationToken = new CancellationToken()) =>
throw new NotImplementedException();

public IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(ChatHistory chatHistory,
PromptExecutionSettings executionSettings = null, Kernel kernel = null,
CancellationToken cancellationToken = new CancellationToken()) =>
throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
@using System.Threading
@using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services
@implements IDisposable
<ModalDialog @ref="Dialog" Title="AutoComplete Content">
<form>
<div class="mb-3">
<label for="systemMessage" class="form-label">Message</label>
<TextAreaWithShortcuts Class="form-control" Id="systemMessage" Rows="2" Placeholder="Enter message..." @bind-Value="@options.Prompt"></TextAreaWithShortcuts>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's title in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeTitle" @bind-value="@options.IncludeTitle">
<label class="form-check-label" for="includeTitle">Include Title</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's short description in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeShortDescription" @bind-value="@options.IncludeShortDescription">
<label class="form-check-label" for="includeShortDescription">Include Short Description</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's tags in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeTags" @bind-value="@options.IncludeTags">
<label class="form-check-label" for="includeTags">Include Tags</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's content in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeContent" @bind-value="@options.IncludeContent">
<label class="form-check-label" for="includeContent">Include Content</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Keep original text and append AI-generated content, or allow AI to rewrite the content.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="keepOriginalText" @bind-value="@options.KeepOriginalText">
<label class="form-check-label" for="keepOriginalText">Keep Original Text</label>
</div>
</div>

<button type="button" class="btn btn-primary btn-sm" @onclick="Generate" disabled="@(!options.CanGenerate)">Generate</button>
<button type="button" class="btn btn-primary btn-sm" @onclick="Stop" disabled="@(options.CanGenerate)">Stop</button>

<div class="mt-3">
<label for="outputField" class="form-label">Output</label>
<textarea class="form-control" id="outputField" rows="6" readonly>@options.Content</textarea>
</div>

<div class="d-flex mt-3 justify-content-end gap-3">
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancel</button>
<button type="button" class="btn btn-success" @onclick="Save" disabled="@(!options.AllowSave)">Save</button>
</div>
</form>
</ModalDialog>

@code {
[Inject] private AutocompleteService AutocompleteService { get; set; }

[Parameter] public CreateNewModel Model { get; set; }
[Parameter] public EventCallback<string> ContentGenerated { get; set; }

private ModalDialog Dialog { get; set; }
private Options options = new ();
private CancellationTokenSource cts = new();

public void Open()
{
Dialog.Open();
StateHasChanged();
}

public void Dispose()
{
cts.Cancel();
cts.Dispose();
}

private void Cancel()
{
options = new();
cts.Cancel();
cts.Dispose();
cts = new();
Dialog.Close();
}

private async Task Generate()
{
options.CanGenerate = false;
options.Content = string.Empty;
var completeOptions = new AutocompleteOptions(
options.IncludeTitle ? Model.Title : string.Empty,
options.IncludeShortDescription ? Model.ShortDescription : string.Empty,
options.IncludeTags ? Model.Tags : string.Empty,
options.IncludeContent ? Model.Content : string.Empty,
options.Prompt,
options.KeepOriginalText);

await foreach (var token in AutocompleteService.GetAutocomplete(completeOptions, cts.Token))
{
if (string.IsNullOrEmpty(token))
{
continue;
}

options.Content += token;
StateHasChanged();
}

options.AllowSave = true;
options.CanGenerate = true;
}

private void Stop()
{
cts.Cancel();
cts.Dispose();
cts = new();
options.CanGenerate = true;
options.AllowSave = false;
}

private async Task Save()
{
await ContentGenerated.InvokeAsync(options.Content);
options = new();
Dialog.Close();
}

private sealed class Options
{
public string Prompt { get; set; }
public bool IncludeTitle { get; set; } = true;
public bool IncludeShortDescription { get; set; } = true;
public bool IncludeTags { get; set; } = true;
public bool IncludeContent { get; set; } = true;
public bool KeepOriginalText { get; set; }
public string Content { get; set; }

public bool AllowSave { get; set; }
public bool CanGenerate { get; set; } = true;
}
}
Loading

0 comments on commit dcb8bf0

Please sign in to comment.