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
1 change: 1 addition & 0 deletions Bottomly.ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddSource("MongoDB.Driver.Core.Extensions.DiagnosticSources")
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
tracing.Filter = context =>
Expand Down
35 changes: 10 additions & 25 deletions Bottomly.Tests/LlmBot/LlmBotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ public void Create_SetsUsernameAndText()
public class BottomlyUserNoteTests
{
[Fact]
public void Create_SetsUsernameAndNote()
public void Create_SetsAllProperties()
{
var note = BottomlyUserNote.Create("bob", "likes cats");
var note = BottomlyUserNote.Create("bob", "Bob Jones", Gender.Male, SassLevel.Frequent, "likes cats");

note.Username.ShouldBe("bob");
note.Note.ShouldBe("likes cats");
note.FullName.ShouldBe("Bob Jones");
note.Gender.ShouldBe(Gender.Male);
note.SassLevel.ShouldBe(SassLevel.Frequent);
note.MiscInfo.ShouldBe("likes cats");
}
}

Expand All @@ -35,7 +38,7 @@ public class MessageHistoryContextTests
public void Create_SetsMessageHistoryAndUserNotes()
{
var messages = new List<BottomlyInputMessage> { BottomlyInputMessage.Create("alice", "hi") };
var notes = new List<BottomlyUserNote> { BottomlyUserNote.Create("alice", "some note") };
var notes = new List<BottomlyUserNote> { BottomlyUserNote.Create("alice", "Alice Smith", Gender.Female, SassLevel.Moderate, "some note") };

var ctx = MessageHistoryContext.Create(messages, notes);

Expand Down Expand Up @@ -126,7 +129,7 @@ public void ToChatContext_IncludesMessageHistoryAndUserInfo()
};
var notes = new List<BottomlyUserNote>
{
BottomlyUserNote.Create("alice", "likes tea")
BottomlyUserNote.Create("alice", "Alice Smith", Gender.Female, SassLevel.Moderate, "likes tea")
};
var ctx = MessageHistoryContext.Create(messages, notes);

Expand All @@ -138,6 +141,8 @@ public void ToChatContext_IncludesMessageHistoryAndUserInfo()
result.ShouldContain("hello");
result.ShouldContain("bob");
result.ShouldContain("world");
result.ShouldContain("\"full_name\"");
result.ShouldContain("Alice Smith");
result.ShouldContain("likes tea");
}

Expand Down Expand Up @@ -186,24 +191,4 @@ public void ToArray_ContainsThreeMessages()

[Fact]
public void SystemPrompt_MentionsBottomly() => FullPromptContext.SystemPrompt.Text.ShouldContain("Bottomly");
}

public class MemberNoteTests
{
[Fact]
public void Note_ContainsAllMemberInfo()
{
var member = new Member
{
FullName = "Alice Smith",
Gender = Gender.Female,
SassLevel = SassLevel.Frequent,
MiscInfo = "Drinks tea"
};

member.Note.ShouldContain("Alice Smith");
member.Note.ShouldContain("Female");
member.Note.ShouldContain("Frequent");
member.Note.ShouldContain("Drinks tea");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class MessageContextExtensionsTests
private readonly SlackParser _parser = new(new Mock<IMemberRepository>().Object);

[Fact]
public void CreateFromMember_MapsUsernameAndNote()
public void CreateFromMember_MapsAllProperties()
{
var member = new Member
{
Expand All @@ -28,7 +28,10 @@ public void CreateFromMember_MapsUsernameAndNote()
var note = BottomlyUserNote.CreateFromMember(member);

note.Username.ShouldBe("alice");
note.Note.ShouldContain("Alice Smith");
note.FullName.ShouldBe("Alice Smith");
note.Gender.ShouldBe(Gender.Female);
note.SassLevel.ShouldBe(SassLevel.Moderate);
note.MiscInfo.ShouldBe("Loves gardening");
}

[Fact]
Expand Down
9 changes: 8 additions & 1 deletion Bottomly/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection;
using Bottomly.Commands;
using Bottomly.Slack.MessageEventHandlers;
using Bottomly.Slack.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Expand All @@ -17,7 +18,13 @@ public void RegisterEventHandlers(Assembly assembly, Type[] exclude) =>
.Where(t => t.Name != nameof(HelpHandler))
.Where(t => !exclude.Contains(t))
.ToList()
.ForEach(t => builder.Services.AddSingleton(typeof(IMessageEventHandler), t));
.ForEach(t =>
{
builder.Services.AddSingleton(t);
builder.Services.AddSingleton<IMessageEventHandler>(sp =>
new TracingMessageEventHandlerDecorator(
(IMessageEventHandler)sp.GetRequiredService(t)));
});

public void RegisterCommands(Assembly assembly) =>
assembly.GetTypes()
Expand Down
15 changes: 12 additions & 3 deletions Bottomly/LlmBot/BottomlyUserNote.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
using Bottomly.Models;

namespace Bottomly.LlmBot;

public record BottomlyUserNote
{
private BottomlyUserNote(string username, string note) => (Username, Note) = (username, note);
private BottomlyUserNote(string username, string fullName, Gender gender, SassLevel sassLevel, string miscInfo) =>
(Username, FullName, Gender, SassLevel, MiscInfo) = (username, fullName, gender, sassLevel, miscInfo);

public string Username { get; private init; } = "";
public string Note { get; private init; } = "";
public string FullName { get; private init; } = "";
public Gender Gender { get; private init; } = Gender.Unknown;
public SassLevel SassLevel { get; private init; } = SassLevel.Moderate;
public string MiscInfo { get; private init; } = "";

public static BottomlyUserNote Create(string username, string note) => new(username, note);
public static BottomlyUserNote Create(string username, string fullName, Gender gender, SassLevel sassLevel,
string miscInfo) =>
new(username, fullName, gender, sassLevel, miscInfo);
}
11 changes: 9 additions & 2 deletions Bottomly/LlmBot/FullPromptContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public static string ToChatContext(this MessageHistoryContext historyContext)
{
var payload = new PromptContextPayload(
historyContext.MessageHistory.Select(m => new MessageHistoryEntry(m.Username, m.Text)).ToList(),
historyContext.UserNotes.Select(n => new UserInfoEntry(n.Username, n.Note)).ToList()
historyContext.UserNotes.Select(n => new UserInfoEntry(n.Username, n.FullName, n.Gender.ToString(),
n.SassLevel.ToString(), n.MiscInfo)).ToList()
);
return JsonSerializer.Serialize(payload, JsonOptions);
}
Expand All @@ -64,5 +65,11 @@ private record MessageHistoryEntry(
private record UserInfoEntry(
[property: JsonPropertyName("username")]
string Username,
[property: JsonPropertyName("note")] string Note);
[property: JsonPropertyName("full_name")]
string FullName,
[property: JsonPropertyName("gender")] string Gender,
[property: JsonPropertyName("sass_level")]
string SassLevel,
[property: JsonPropertyName("misc_info")]
string MiscInfo);
}
7 changes: 0 additions & 7 deletions Bottomly/Models/Member.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,4 @@ public class Member
/// </summary>
[BsonElement("_cls")]
public string Cls { get; set; } = string.Empty;

[BsonIgnore]
public string Note =>
$"FullName: {FullName}\n" +
$"Gender: {Gender}\n" +
$"SassLevel: {SassLevel}\n" +
$"MiscInfo: {MiscInfo}";
}
11 changes: 8 additions & 3 deletions Bottomly/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@
builder.AddMongoDBClient("mongodb");
builder.Services.AddSingleton<IMongoDatabase>(sp =>
{
var client = sp.GetRequiredService<IMongoClient>();
var configuration = sp.GetRequiredService<IConfiguration>();
var connectionString = configuration.GetConnectionString("mongodb")!;
var databaseName = MongoUrl.Create(connectionString).DatabaseName ?? "bottomly";
return client.GetDatabase(databaseName);
var mongoUrl = MongoUrl.Create(connectionString);
var settings = MongoClientSettings.FromUrl(mongoUrl);
settings.ClusterConfigurator = cb =>
cb.Subscribe(new MongoDB.Driver.Core.Extensions.DiagnosticSources.DiagnosticsActivityEventSubscriber(
new MongoDB.Driver.Core.Extensions.DiagnosticSources.InstrumentationOptions { CaptureCommandText = true }));
var instrumentedClient = new MongoClient(settings);
var databaseName = mongoUrl.DatabaseName ?? "bottomly";
return instrumentedClient.GetDatabase(databaseName);
});

builder.Services.AddBottomlyConfiguration(builder.Configuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal static class MessageContextExtensions
extension(BottomlyUserNote bottomlyUserNote)
{
public static BottomlyUserNote CreateFromMember(Member member) =>
BottomlyUserNote.Create(member.Username, member.Note);
BottomlyUserNote.Create(member.Username, member.FullName, member.Gender, member.SassLevel, member.MiscInfo);
}

extension(BottomlyInputMessage bottomlyInputMessage)
Expand Down
11 changes: 8 additions & 3 deletions Bottomly/Slack/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Bottomly.Slack.MembershipEventHandlers;
using Bottomly.Slack.MessageEventHandlers;
using Bottomly.Slack.ReactionHandlers;
using Bottomly.Slack.Telemetry;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SlackNet.Events;
Expand All @@ -28,12 +29,16 @@ public static IServiceCollection AddBottomlySlack(this IServiceCollection servic
.RegisterEventHandler<MessageEvent, SlackMessageEventDispatcher>()
.RegisterEventHandler<ReactionAdded, SlackReactionEventDispatcher>());

// Help handler (also registered as singleton for direct injection into SlackWorker)
// Help handler — concrete type for its own resolution, decorated version keyed for SlackWorker injection
services.AddSingleton<HelpHandler>();
services.AddKeyedSingleton<IMessageEventHandler>(SlackWorker.HelpHandlerKey,
(sp, _) => new TracingMessageEventHandlerDecorator(sp.GetRequiredService<HelpHandler>()));

// Reaction handlers
// Reaction handlers (wrapped in tracing decorator)
services.AddSingleton<KarmaReactionMap>();
services.AddSingleton<IReactionHandler, AddKarmaReactionHandler>();
services.AddSingleton<IReactionHandler>(sp =>
new TracingReactionHandlerDecorator(
ActivatorUtilities.CreateInstance<AddKarmaReactionHandler>(sp)));

// Membership handlers
services.AddSingleton<MemberJoinedEventHandler>();
Expand Down
38 changes: 36 additions & 2 deletions Bottomly/Slack/SlackEventDispatchers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Bottomly.Slack.MembershipEventHandlers;
using Bottomly.Telemetry;
using SlackNet;
using SlackNet.Events;
using System.Diagnostics;

namespace Bottomly.Slack;

Expand All @@ -9,15 +11,47 @@ namespace Bottomly.Slack;
/// </summary>
public class SlackMessageEventDispatcher(SlackWorker worker) : IEventHandler<MessageEvent>
{
public Task Handle(MessageEvent slackEvent) => worker.ProcessMessageAsync(slackEvent);
public async Task Handle(MessageEvent slackEvent)
{
using var activity = BottomlyActivitySource.Instance.StartActivity("slack.message.process");
activity?.SetTag("slack.channel", slackEvent.Channel);
activity?.SetTag("slack.user", slackEvent.User);
activity?.SetTag("slack.text_preview", slackEvent.Text?[..Math.Min(slackEvent.Text.Length, 100)]);

try
{
await worker.ProcessMessageAsync(slackEvent);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}

/// <summary>
/// Bridges SlackNet's IEventHandler&lt;ReactionAdded&gt; to our SlackWorker reaction processing pipeline.
/// </summary>
public class SlackReactionEventDispatcher(SlackWorker worker) : IEventHandler<ReactionAdded>
{
public Task Handle(ReactionAdded slackEvent) => worker.ProcessReactionAsync(slackEvent);
public async Task Handle(ReactionAdded slackEvent)
{
using var activity = BottomlyActivitySource.Instance.StartActivity("slack.reaction.process");
activity?.SetTag("slack.reaction", slackEvent.Reaction);
if (slackEvent.Item is ReactionMessage reactionMessage)
activity?.SetTag("slack.channel", reactionMessage.Channel);

try
{
await worker.ProcessReactionAsync(slackEvent);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}

public class SlackMemberAddedEventDispatcher(MemberJoinedEventHandler handler)
Expand Down
5 changes: 4 additions & 1 deletion Bottomly/Slack/SlackWorker.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Bottomly.Repositories;
using Bottomly.Slack.MessageEventHandlers;
using Bottomly.Slack.ReactionHandlers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SlackNet;
Expand All @@ -12,12 +13,14 @@ namespace Bottomly.Slack;
public class SlackWorker(
ISlackSocketModeClient socketClient,
IEnumerable<IMessageEventHandler> eventHandlers,
HelpHandler helpMessageHandler,
[FromKeyedServices("help")] IMessageEventHandler helpMessageHandler,
IEnumerable<IReactionHandler> reactionHandlers,
IMemberRepository memberRepository,
ILogger<SlackWorker> logger)
: BackgroundService
{
internal const string HelpHandlerKey = "help";

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Starting Slack Socket Mode connection...");
Expand Down
53 changes: 53 additions & 0 deletions Bottomly/Slack/Telemetry/SlackTracingDecorators.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics;
using Bottomly.Slack.MessageEventHandlers;
using Bottomly.Slack.ReactionHandlers;
using Bottomly.Telemetry;
using SlackNet.Events;

namespace Bottomly.Slack.Telemetry;

internal sealed class TracingMessageEventHandlerDecorator(IMessageEventHandler inner) : IMessageEventHandler
{
public bool CanHandle(MessageEvent message) => inner.CanHandle(message);

public string BuildHelpMessage() => inner.BuildHelpMessage();

public async Task HandleAsync(MessageEvent message)
{
using var activity = BottomlyActivitySource.Instance.StartActivity("slack.handler.invoke");
activity?.SetTag("slack.handler", inner.GetType().Name);
activity?.SetTag("slack.text_preview", message.Text?[..Math.Min(message.Text.Length, 100)]);

try
{
await inner.HandleAsync(message);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}

internal sealed class TracingReactionHandlerDecorator(IReactionHandler inner) : IReactionHandler
{
public bool CanHandle(ReactionAdded reaction) => inner.CanHandle(reaction);

public async Task HandleAsync(ReactionAdded reaction)
{
using var activity = BottomlyActivitySource.Instance.StartActivity("slack.reaction.handler.invoke");
activity?.SetTag("slack.handler", inner.GetType().Name);
activity?.SetTag("slack.reaction", reaction.Reaction);

try
{
await inner.HandleAsync(reaction);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}
9 changes: 9 additions & 0 deletions Bottomly/Telemetry/BottomlyActivitySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Diagnostics;

namespace Bottomly.Telemetry;

internal static class BottomlyActivitySource
{
internal const string Name = "Bottomly";
internal static readonly ActivitySource Instance = new(Name);
}
Loading