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
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ COPY modules/Email/src/SimpleModule.Email.Contracts/*.csproj modules/Email/src/S
COPY modules/Email/src/SimpleModule.Email/*.csproj modules/Email/src/SimpleModule.Email/
COPY modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/*.csproj modules/RateLimiting/src/SimpleModule.RateLimiting.Contracts/
COPY modules/RateLimiting/src/SimpleModule.RateLimiting/*.csproj modules/RateLimiting/src/SimpleModule.RateLimiting/
COPY modules/Chat/src/SimpleModule.Chat.Contracts/*.csproj modules/Chat/src/SimpleModule.Chat.Contracts/
COPY modules/Chat/src/SimpleModule.Chat/*.csproj modules/Chat/src/SimpleModule.Chat/

RUN dotnet restore template/SimpleModule.Host/SimpleModule.Host.csproj

Expand Down Expand Up @@ -115,6 +117,7 @@ COPY modules/Users/src/SimpleModule.Users/package.json modules/Users/src/SimpleM
COPY modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/package.json modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/
COPY modules/Email/src/SimpleModule.Email/package.json modules/Email/src/SimpleModule.Email/
COPY modules/RateLimiting/src/SimpleModule.RateLimiting/package.json modules/RateLimiting/src/SimpleModule.RateLimiting/
COPY modules/Chat/src/SimpleModule.Chat/package.json modules/Chat/src/SimpleModule.Chat/
COPY packages/SimpleModule.Client/package.json packages/SimpleModule.Client/
COPY packages/SimpleModule.Theme.Default/package.json packages/SimpleModule.Theme.Default/
COPY packages/SimpleModule.TsConfig/package.json packages/SimpleModule.TsConfig/
Expand Down
5 changes: 5 additions & 0 deletions SimpleModule.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@
<Project Path="modules/Agents/src/SimpleModule.Agents.Module/SimpleModule.Agents.Module.csproj" />
<Project Path="modules/Agents/tests/SimpleModule.Agents.Tests/SimpleModule.Agents.Tests.csproj" />
</Folder>
<Folder Name="/modules/Chat/">
<Project Path="modules/Chat/src/SimpleModule.Chat.Contracts/SimpleModule.Chat.Contracts.csproj" />
<Project Path="modules/Chat/src/SimpleModule.Chat/SimpleModule.Chat.csproj" />
<Project Path="modules/Chat/tests/SimpleModule.Chat.Tests/SimpleModule.Chat.Tests.csproj" />
</Folder>
<Folder Name="/modules/BackgroundJobs/">
<Project Path="modules/BackgroundJobs/src/SimpleModule.BackgroundJobs.Contracts/SimpleModule.BackgroundJobs.Contracts.csproj" />
<Project Path="modules/BackgroundJobs/src/SimpleModule.BackgroundJobs/SimpleModule.BackgroundJobs.csproj" />
Expand Down
15 changes: 15 additions & 0 deletions framework/SimpleModule.Agents/AgentChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,21 @@ CancellationToken cancellationToken
}
}

if (request.History is { Count: > 0 })
{
foreach (var turn in request.History)
{
if (string.IsNullOrWhiteSpace(turn.Content))
{
continue;
}
var role = string.Equals(turn.Role, "assistant", StringComparison.OrdinalIgnoreCase)
? ChatRole.Assistant
: ChatRole.User;
messages.Add(new ChatMessage(role, turn.Content));
}
}

messages.Add(new ChatMessage(ChatRole.User, request.Message));

var chatOptions = new ChatOptions
Expand Down
9 changes: 8 additions & 1 deletion framework/SimpleModule.Agents/Dtos/AgentChatRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,12 @@ namespace SimpleModule.Agents.Dtos;
public sealed record AgentChatRequest(
string Message,
string? SessionId = null,
string? ResponseType = null
string? ResponseType = null,
IReadOnlyList<AgentHistoryMessage>? History = null
);

/// <summary>
/// A prior turn in the conversation. Role must be "user" or "assistant".
/// System messages are provided by the agent definition and should not appear here.
/// </summary>
public sealed record AgentHistoryMessage(string Role, string Content);
22 changes: 22 additions & 0 deletions modules/Chat/src/SimpleModule.Chat.Contracts/ChatConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace SimpleModule.Chat.Contracts;

public static class ChatConstants
{
public const string ModuleName = "Chat";
public const string RoutePrefix = "/api/chat";
public const string ViewPrefix = "/chat";

public static class Routes
{
public const string ListConversations = "/conversations";
public const string CreateConversation = "/conversations";
public const string GetConversation = "/conversations/{id:guid}";
public const string RenameConversation = "/conversations/{id:guid}";
public const string DeleteConversation = "/conversations/{id:guid}";
public const string GetMessages = "/conversations/{id:guid}/messages";
public const string SendMessageStream = "/conversations/{id:guid}/stream";

public const string Browse = "/";
public const string Conversation = "/{id:guid}";
}
}
29 changes: 29 additions & 0 deletions modules/Chat/src/SimpleModule.Chat.Contracts/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace SimpleModule.Chat.Contracts;

public class ChatMessage
{
public ChatMessageId Id { get; set; }
public ConversationId ConversationId { get; set; }
public ChatRole Role { get; set; }
public string Content { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
}

public enum ChatRole
{
User = 0,
Assistant = 1,
System = 2,
}

public static class ChatRoleExtensions
{
public static string ToWire(ChatRole role) =>
role switch
{
ChatRole.User => "user",
ChatRole.Assistant => "assistant",
ChatRole.System => "system",
_ => "user",
};
}
6 changes: 6 additions & 0 deletions modules/Chat/src/SimpleModule.Chat.Contracts/ChatMessageId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Vogen;

namespace SimpleModule.Chat.Contracts;

[ValueObject<Guid>(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)]
public readonly partial struct ChatMessageId;
14 changes: 14 additions & 0 deletions modules/Chat/src/SimpleModule.Chat.Contracts/Conversation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace SimpleModule.Chat.Contracts;

public class Conversation
{
public ConversationId Id { get; set; }
public string UserId { get; set; } = string.Empty;
public string Title { get; set; } = "New conversation";
public string AgentName { get; set; } = string.Empty;
public bool Pinned { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

public List<ChatMessage> Messages { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Vogen;

namespace SimpleModule.Chat.Contracts;

[ValueObject<Guid>(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)]
public readonly partial struct ConversationId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace SimpleModule.Chat.Contracts;

public sealed class CreateConversationRequest
{
public string AgentName { get; set; } = string.Empty;
public string? Title { get; set; }
}

public sealed class RenameConversationRequest
{
public string Title { get; set; } = string.Empty;
}
16 changes: 16 additions & 0 deletions modules/Chat/src/SimpleModule.Chat.Contracts/IChatContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace SimpleModule.Chat.Contracts;

public interface IChatContracts
{
Task<IReadOnlyList<Conversation>> GetUserConversationsAsync(
string userId,
CancellationToken cancellationToken = default
);

Task<Conversation> StartConversationAsync(
string userId,
string agentName,
string? title,
CancellationToken cancellationToken = default
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<DefineConstants>$(DefineConstants);VOGEN_NO_VALIDATION</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Vogen" />
<ProjectReference Include="..\..\..\..\framework\SimpleModule.Core\SimpleModule.Core.csproj" />
</ItemGroup>
</Project>
39 changes: 39 additions & 0 deletions modules/Chat/src/SimpleModule.Chat/ChatDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SimpleModule.Chat.Contracts;
using SimpleModule.Chat.EntityConfigurations;
using SimpleModule.Database;

namespace SimpleModule.Chat;

public class ChatDbContext(
DbContextOptions<ChatDbContext> options,
IOptions<DatabaseOptions> dbOptions
) : DbContext(options)
{
public DbSet<Conversation> Conversations => Set<Conversation>();
public DbSet<ChatMessage> ChatMessages => Set<ChatMessage>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ConversationConfiguration());
modelBuilder.ApplyConfiguration(new ChatMessageConfiguration());
modelBuilder.ApplyModuleSchema("Chat", dbOptions.Value);
}

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<ConversationId>()
.HaveConversion<
ConversationId.EfCoreValueConverter,
ConversationId.EfCoreValueComparer
>();
configurationBuilder
.Properties<ChatMessageId>()
.HaveConversion<
ChatMessageId.EfCoreValueConverter,
ChatMessageId.EfCoreValueComparer
>();
}
}
44 changes: 44 additions & 0 deletions modules/Chat/src/SimpleModule.Chat/ChatModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SimpleModule.Chat.Contracts;
using SimpleModule.Core;
using SimpleModule.Core.Authorization;
using SimpleModule.Core.Menu;
using SimpleModule.Database;

namespace SimpleModule.Chat;

[Module(
ChatConstants.ModuleName,
RoutePrefix = ChatConstants.RoutePrefix,
ViewPrefix = ChatConstants.ViewPrefix
)]
public class ChatModule : IModule, IModuleServices, IModuleMenu
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddModuleDbContext<ChatDbContext>(configuration, ChatConstants.ModuleName);
services.AddScoped<ChatService>();
services.AddScoped<IChatContracts>(sp => sp.GetRequiredService<ChatService>());
}

public void ConfigurePermissions(PermissionRegistryBuilder builder)
{
builder.AddPermissions<ChatPermissions>();
}

public void ConfigureMenu(IMenuBuilder menus)
{
menus.Add(
new MenuItem
{
Label = "Chat",
Url = "/chat",
Icon =
"""<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.87 9.87 0 01-4-.8L3 20l1.2-3.6A7.96 7.96 0 013 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>""",
Order = 40,
Section = MenuSection.AppSidebar,
}
);
}
}
10 changes: 10 additions & 0 deletions modules/Chat/src/SimpleModule.Chat/ChatPermissions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using SimpleModule.Core.Authorization;

namespace SimpleModule.Chat;

public sealed class ChatPermissions : IModulePermissions
{
public const string View = "Chat.View";
public const string Create = "Chat.Create";
public const string ManageAll = "Chat.ManageAll";
}
Loading
Loading