Skip to content

Commit b9de4e0

Browse files
committed
AI summary iteration 1
1 parent f5ff2a6 commit b9de4e0

File tree

12 files changed

+259
-12
lines changed

12 files changed

+259
-12
lines changed

Directory.Packages.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageVersion Include="FluentValidation" Version="10.4.0" />
1313
<PackageVersion Include="FluentValidation.AspNetCore" Version="10.4.0" />
1414
<PackageVersion Include="Fody" Version="6.6.4" />
15+
<PackageVersion Include="Markdown" Version="2.2.1" />
1516
<PackageVersion Include="MediatR" Version="11.1.0" />
1617
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
1718
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
@@ -21,6 +22,7 @@
2122
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
2223
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
2324
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
25+
<PackageVersion Include="Microsoft.Extensions.AI.Ollama" Version="9.0.0-preview.9.24556.5" />
2426
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
2527
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
2628
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0" />
@@ -36,7 +38,7 @@
3638
<PackageVersion Include="NSubstitute " Version="4.2.1" />
3739
<PackageVersion Include="NSubstitute.Analyzers.CSharp " Version="1.0.12" />
3840
<PackageVersion Include="nunit" Version="4.2.2" />
39-
<PackageVersion Include="NUnit.Analyzers" Version="4.3.0"/>
41+
<PackageVersion Include="NUnit.Analyzers" Version="4.3.0" />
4042
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
4143
<PackageVersion Include="QRCoder" Version="1.3.6" />
4244
<PackageVersion Include="Scrutor" Version="4.2.0" />

Return.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
23
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JS/@EntryIndexedValue">JS</s:String>
34
<s:Boolean x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0B65405DF419BA43A07276C44DAEC848/@KeyIndexDefined">True</s:Boolean>
45
<s:String x:Key="/Default/PatternsAndTemplates/LiveTemplates/Template/=0B65405DF419BA43A07276C44DAEC848/Description/@EntryValue">EntityTypeConfiguration</s:String>

src/Return.Application/Retrospectives/Queries/GetRetrospectiveStatus/RetrospectiveStatus.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// ******************************************************************************
22
// © Sebastiaan Dammann | damsteen.nl
3-
//
3+
//
44
// File: : RetrospectiveStatus.cs
55
// Project : Return.Application
66
// ******************************************************************************
@@ -21,6 +21,7 @@ public sealed class RetrospectiveStatus {
2121
public bool IsVotingAllowed => this.Stage == RetrospectiveStage.Voting;
2222
public bool IsEditingNotesAllowed => this.Stage == RetrospectiveStage.Writing;
2323
public bool IsDeletingNotesAllowed => this.Stage == RetrospectiveStage.Writing;
24+
public bool IsSummarizingAllowed => this.Stage == RetrospectiveStage.Finished;
2425
public bool IsGroupingAllowed(bool isFacilitator) => this.Stage == RetrospectiveStage.Grouping && isFacilitator;
2526

2627
public RetrospectiveWorkflowStatus WorkflowStatus { get; }
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
@using System.Diagnostics
2+
@using System.Net.Security
3+
@using System.Linq
4+
@using System.Text
5+
@using HeyRed.MarkdownSharp
6+
@using Microsoft.Extensions.AI
7+
@using Microsoft.Extensions.Logging
8+
@using Microsoft.Extensions.Primitives
9+
@using Return.Application.Common.Models
10+
@inherits Return.Web.Components.ShowcaseBase
11+
@inject IChatClient ChatClient
12+
13+
@if (this.Data is null)
14+
{
15+
<p>Loading...</p>
16+
return;
17+
}
18+
19+
<button type="button"
20+
class="button is-link is-outlined full-width"
21+
data-test-element-id="toggle-view-button"
22+
disabled="@IsLoadingSummary"
23+
@onclick="@(this.LoadSummary)">
24+
<span class="icon"><span class="fa-solid fa-quote-left"></span></span><span>Summarize this retrospective</span>
25+
</button>
26+
27+
@if (Summary is null)
28+
{
29+
<p></p>
30+
} else if (IsLoadingSummary && Summary is null)
31+
{
32+
<p class="summarizer">
33+
<i class="fa-solid fa-gears"></i>
34+
<i>Thinking...</i>
35+
</p>
36+
}
37+
else
38+
{
39+
<div class="summarizer">
40+
@if (IsLoadingSummary) {
41+
<i class="fa-solid fa-gears"></i>
42+
<text>&nbsp;</text>
43+
}
44+
45+
@{
46+
MarkupString summaryHtml = new(Summary);
47+
}
48+
49+
@summaryHtml
50+
</div>
51+
}
52+
53+
@code {
54+
private bool IsLoadingSummary { get; set; }
55+
private string? Summary { get; set; }
56+
57+
private async Task LoadSummary()
58+
{
59+
if (this.IsLoadingSummary || this.Data == null) return;
60+
61+
this.IsLoadingSummary = true;
62+
Summary = null;
63+
StateHasChanged();
64+
65+
ChatOptions chatOptions = new()
66+
{
67+
Temperature = 0.2f,
68+
ResponseFormat = ChatResponseFormat.Text,
69+
};
70+
71+
string laneNames = String.Join("/", this.Data.Items.Select(x => x.Lane.Name).Distinct());
72+
List<ChatMessage> chatMessages =
73+
[
74+
new(
75+
ChatRole.System,
76+
$@"A retrospective has been performed. Summarize the following messages, prefixed with categories '{laneNames}', in a maximum of 60 words per category. Spend more words on highly voted notes and less on lower voted notes.
77+
Don't call out individual notes, just summarize the general sentiment. Limit making up additional context or words, keep it to the given messages. Use markdown syntax for headings, subheadings and lists.
78+
"
79+
)
80+
];
81+
82+
Logger.LogInformation("Compose system message: {Msg}", chatMessages[0].Text);
83+
84+
var byLane =
85+
from item in this.Data.Items
86+
group item by item.Lane.Name into g
87+
select new { Lane = g.Key, Notes = g.OrderByDescending(x => x.Votes.Count) };
88+
89+
StringBuilder msgBuilder = new();
90+
foreach (var group in byLane)
91+
{
92+
foreach (var item in group.Notes)
93+
{
94+
msgBuilder.Clear();
95+
96+
msgBuilder.Append($"Category \"{item.Lane.Name}\" ");
97+
98+
if (item is { NoteGroup: { } ng })
99+
{
100+
msgBuilder.AppendLine($" - multiple items grouped \"{item.NoteGroup.Title}\" (voted {item.Votes.Count} times): ");
101+
foreach (RetrospectiveNote note in ng.Notes)
102+
{
103+
msgBuilder.Append($"- {note.Text}");
104+
}
105+
}
106+
107+
if (item is { Note: { } n })
108+
{
109+
msgBuilder.AppendLine($"- voted {item.Votes.Count} times: ");
110+
msgBuilder.AppendLine(n.Text);
111+
}
112+
113+
chatMessages.Add(
114+
new(
115+
ChatRole.User,
116+
msgBuilder.ToString()
117+
)
118+
);
119+
120+
Logger.LogDebug("Composed sub-chatmessage: {Msg}", msgBuilder.ToString());
121+
}
122+
}
123+
124+
long startTime = Stopwatch.GetTimestamp();
125+
126+
Logger.LogDebug("Invoking AI with {Count} messages", chatMessages.Count);
127+
try
128+
{
129+
Summary = null;
130+
131+
Markdown markdown = new();
132+
string rawSummary = String.Empty;
133+
134+
await foreach (StreamingChatCompletionUpdate subText in this.ChatClient.CompleteStreamingAsync(chatMessages, chatOptions))
135+
{
136+
//Logger.LogTrace("Streamed sub text: {@Update}", subText.Text);
137+
138+
if (subText is { Text: { } text })
139+
{
140+
rawSummary += text;
141+
this.Summary = markdown.Transform(rawSummary);
142+
143+
StateHasChanged();
144+
}
145+
}
146+
147+
this.Summary = markdown.Transform(rawSummary);
148+
Logger.LogTrace("Total generated summary: {RawSummary}", rawSummary);
149+
}
150+
catch (Exception ex)
151+
{
152+
Logger.LogError(ex, "Error invoking AI");
153+
Summary = ex.ToString();
154+
}
155+
156+
Logger.LogDebug("Completed AI invocation in {Elapsed}", Stopwatch.GetElapsedTime(startTime));
157+
IsLoadingSummary = false;
158+
}
159+
160+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// ******************************************************************************
2+
// © 2024 Sebastiaan Dammann | damsteen.nl
3+
//
4+
// File: : AISettings.cs
5+
// Project : Return.Web
6+
// ******************************************************************************
7+
8+
namespace Return.Web.Configuration;
9+
10+
using System.ComponentModel.DataAnnotations;
11+
12+
public class AISettings
13+
{
14+
[Required]
15+
public required string Url { get; set; }
16+
17+
/// <summary>
18+
/// https://ollama.com/search?c=tools
19+
/// </summary>
20+
public string? Model { get; set; }
21+
}

src/Return.Web/Pages/RetrospectiveLobby.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
{
2727
<Showcase />
2828
}
29+
30+
@if (this.RetrospectiveStatus is { IsSummarizingAllowed: true })
31+
{
32+
<RetrospectiveSummarizer/>
33+
}
2934
</div>
3035

3136
<div class="column">

src/Return.Web/Return.Web.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
<ItemGroup>
1414
<PackageReference Include="Blazored.FluentValidation" />
15+
<PackageReference Include="Markdown" />
16+
<PackageReference Include="Microsoft.Extensions.AI.Ollama" />
1517

1618
<PackageReference Include="NReco.Logging.File" />
1719
<PackageReference Include="FluentValidation.AspNetCore" />

src/Return.Web/Startup.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Return.Web;
1717
using FluentValidation;
1818
using Infrastructure;
1919
using MediatR;
20+
using Microsoft.Extensions.AI;
2021
using Microsoft.Extensions.Logging;
2122
using Microsoft.Extensions.Options;
2223
using Middleware;
@@ -62,11 +63,23 @@ public void ConfigureServices(IServiceCollection services) {
6263

6364
services.Configure<SecuritySettings>(this.Configuration.GetSection("Security"));
6465

66+
services.Configure<AISettings>(this.Configuration.GetSection("AI"));
67+
6568
// Framework
6669
services.AddRazorPages();
6770
services.AddServerSideBlazor();
6871
services.AddValidatorsFromAssembly(typeof(IUrlGenerator).Assembly, ServiceLifetime.Scoped);
6972
services.AddDataProtection();
73+
74+
services.AddScoped<IChatClient>(static sp =>
75+
{
76+
IOptionsSnapshot<AISettings> aiSettings = sp.GetRequiredService<IOptionsSnapshot<AISettings>>();
77+
78+
return new OllamaChatClient(
79+
aiSettings.Value.Url,
80+
aiSettings.Value.Model
81+
);
82+
});
7083
}
7184

7285
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.summarizer {
2+
margin: 2em 0;
3+
padding: 1em;
4+
border: 1px dashed #ccc;
5+
6+
h1,
7+
h2,
8+
h3,
9+
h4 {
10+
font-weight: bold;
11+
}
12+
13+
h1 {
14+
font-size: 140%;
15+
}
16+
17+
h2 {
18+
font-size: 130%;
19+
}
20+
21+
h3 {
22+
font-size: 120%;
23+
}
24+
25+
h4 {
26+
font-size: 110%;
27+
}
28+
}

src/Return.Web/_scss/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ $reveal-shadow-spread: 20px;
4343
@import 'online-list';
4444
@import 'retrospective-progress';
4545
@import 'showcase';
46+
@import 'summarizer';
4647
@import 'vote';

0 commit comments

Comments
 (0)