From 42f6e6424128871579774fcd1ffcdd7166ae9729 Mon Sep 17 00:00:00 2001 From: Richasy Date: Tue, 25 Jun 2024 20:12:37 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=BE=A4=E8=81=8A=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Console/RodelChat.Console/ChatService.cs | 160 +++++++++++++++--- src/Core/RodelChat.Core/ChatClient.Helper.cs | 41 +++++ .../RodelChat.Core/ChatClient.Properties.cs | 3 + src/Core/RodelChat.Core/ChatClient.cs | 63 ++++++- .../Providers/AnthropicProvider.cs | 2 +- .../Providers/GeminiProvider.cs | 2 +- .../Providers/HunYuanProvider.cs | 2 +- .../RodelChat.Core/Providers/ProviderBase.cs | 2 +- .../Providers/QianFanProvider.cs | 2 +- .../Providers/SparkDeskProvider.cs | 2 +- .../RodelChat.Core/Providers/ZhiPuProvider.cs | 2 +- src/Core/RodelChat.Core/RodelChat.Core.csproj | 2 + .../Client/IChatClient.cs | 18 +- .../RodelChat.Interfaces/Client/IProvider.cs | 2 +- src/Core/RodelChat.Models/Client/ChatGroup.cs | 30 ++++ .../Client/ChatGroupPreset.cs | 73 ++++++++ .../RodelChat.Models/Client/ChatMessage.cs | 6 + .../ChatServicePageViewModel.Agents.cs | 2 +- .../ChatServicePageViewModel.Services.cs | 2 +- .../ChatServicePageViewModel.Sessions.cs | 2 +- src/Libs/semantic-kernel | 2 +- src/RodelAgent.sln | 57 +++++++ 22 files changed, 439 insertions(+), 38 deletions(-) create mode 100644 src/Core/RodelChat.Models/Client/ChatGroup.cs create mode 100644 src/Core/RodelChat.Models/Client/ChatGroupPreset.cs diff --git a/src/Console/RodelChat.Console/ChatService.cs b/src/Console/RodelChat.Console/ChatService.cs index 7bcf955e..5f43a871 100644 --- a/src/Console/RodelChat.Console/ChatService.cs +++ b/src/Console/RodelChat.Console/ChatService.cs @@ -1,6 +1,7 @@ // Copyright (c) Rodel. All rights reserved. -using System.Text.Json; +#define USE_GROUP + using Microsoft.Extensions.Hosting; using RodelAgent.Interfaces; using RodelAgent.Statics; @@ -15,6 +16,7 @@ public sealed class ChatService : IHostedService { private readonly IChatClient _chatClient; + private readonly IChatParametersFactory _chatParametersFactory; private readonly IStringResourceToolkit _localizer; private ProviderType _currentType; private ChatSession _currentSession; @@ -23,10 +25,12 @@ public sealed class ChatService : IHostedService /// Initializes a new instance of the class. /// public ChatService( + IChatParametersFactory chatParametersFactory, IChatClient chatClient, IHostApplicationLifetime lifetime, IStringResourceToolkit localizer) { + _chatParametersFactory = chatParametersFactory; _chatClient = chatClient; _localizer = localizer; lifetime.ApplicationStopping.Register(_chatClient.Dispose); @@ -37,11 +41,27 @@ public async Task StartAsync(CancellationToken cancellationToken) { try { -#if USE_PRESET - var preset = AskSessionPreset(); - var session = _chatClient.CreateSession(preset); - _currentType = session.Provider; - await RunAIAsync(session); +#if USE_GROUP + var agents = CreateNewsAgents(); + var message = AskInput(); + var preset = new ChatGroupPreset + { + Agents = agents.Select(a => a.Id).ToList(), + Id = "group", + MaxRounds = 6, + Name = "Group", + TerminateText = "approve", + }; + var chatMsg = ChatMessage.CreateUserMessage(message); + await _chatClient.SendGroupMessageAsync( + chatMsg, + preset, + (response) => + { + HandleMessageResponse(response); + }, + CancellationToken.None, + [.. agents]); #else var provider = AskProvider(); await RunAIAsync(provider); @@ -71,6 +91,111 @@ private async Task RunAIAsync(ChatSession session) await LoopMessageAsync(session); } + private List CreateCoderAgents() + { + _ = this; + var progamManagerText = + """ + You are a program manager which will take the requirement and create a plan for creating app. Program Manager understands the + user requirements and form the detail documents with requirements and costing. + """; + + var softwareEngineerText = + """ + You are Software Engieer, and your goal is develop web app using HTML and JavaScript (JS) by taking into consideration all + the requirements given by Program Manager. + """; + + var managerText = + """ + You are manager which will review software engineer code, and make sure all client requirements are completed. + Once all client requirements are completed, you can approve the request by just responding "approve" + """; + + var programManager = new ChatSessionPreset + { + Name = "Program Manager", + Provider = ProviderType.AzureOpenAI, + Model = "gpt-4o", + Id = "program-manager", + SystemInstruction = progamManagerText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.AzureOpenAI), + }; + + var softwareEngineer = new ChatSessionPreset + { + Name = "Software Engineer", + Provider = ProviderType.ZhiPu, + Model = "glm-4", + Id = "software-engineer", + SystemInstruction = softwareEngineerText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.ZhiPu), + }; + + var manager = new ChatSessionPreset + { + Name = "Manager", + Provider = ProviderType.AzureOpenAI, + Model = "gpt-4o", + Id = "manager", + SystemInstruction = managerText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.AzureOpenAI), + }; + + return [programManager, softwareEngineer, manager]; + } + + private List CreateNewsAgents() + { + var reporterText = + """ + You are a reporter which will take the news and create a news article. Reporter understands the news and form the detail article with + news and images. + """; + var editorText = + """ + You are editor, and your goal is to review the news article, and make sure all the news are correct. + """; + + var publishManagerText = + """ + You are publish manager which will review editor news article, and make sure all news are correct. + Once all news are correct, you can approve the request by just responding "approve" + """; + + var reporter = new ChatSessionPreset + { + Name = "Reporter", + Provider = ProviderType.AzureOpenAI, + Model = "gpt-4o", + Id = "reporter", + SystemInstruction = reporterText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.AzureOpenAI), + }; + + var editor = new ChatSessionPreset + { + Name = "Editor", + Provider = ProviderType.ZhiPu, + Model = "glm-4", + Id = "editor", + SystemInstruction = editorText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.ZhiPu), + }; + + var publishManager = new ChatSessionPreset + { + Name = "Publish Manager", + Provider = ProviderType.AzureOpenAI, + Model = "gpt-4o", + Id = "publish-manager", + SystemInstruction = publishManagerText, + Parameters = _chatParametersFactory.CreateChatParameters(ProviderType.AzureOpenAI), + }; + + return [reporter, editor, publishManager]; + } + private async Task LoopMessageAsync(ChatSession session) { #if USE_SYSTEM_PROMPT @@ -127,24 +252,6 @@ private ProviderType AskProvider() return provider; } - private ChatSessionPreset AskSessionPreset() - { - // Get presets file list in Presets folder without extension name. - var presetFiles = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "Presets"), "*.json"); - var presetNames = presetFiles.Select(p => Path.GetFileNameWithoutExtension(p)!).ToList(); - - var presetName = AnsiConsole.Prompt( - new SelectionPrompt() - .Title(GetString("SelectPreset")) - .PageSize(10) - .AddChoices(presetNames)); - - var presetFile = Path.Combine(AppContext.BaseDirectory, "Presets", $"{presetName}.json"); - var presetContent = File.ReadAllText(presetFile); - var preset = JsonSerializer.Deserialize(presetContent); - return preset; - } - private ChatModel AskModel(ProviderType type) { var models = _chatClient.GetModels(type); @@ -264,6 +371,11 @@ private void PrintAssistantMessage(ChatMessage response) Padding = new Padding(2, 2, 2, 2), }; + if (!string.IsNullOrEmpty(response.Author)) + { + panel.Header = new PanelHeader(response.Author); + } + AnsiConsole.Write(panel); } diff --git a/src/Core/RodelChat.Core/ChatClient.Helper.cs b/src/Core/RodelChat.Core/ChatClient.Helper.cs index 8917f8e2..ee2e8007 100644 --- a/src/Core/RodelChat.Core/ChatClient.Helper.cs +++ b/src/Core/RodelChat.Core/ChatClient.Helper.cs @@ -1,5 +1,6 @@ // Copyright (c) Rodel. All rights reserved. +using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using RodelAgent.Models.Abstractions; @@ -134,4 +135,44 @@ private BaseFieldParameters GetChatParameters(ProviderType type, BaseFieldParame return parameters; } + + private string EncodeName(string input) + { + var encoded = new StringBuilder(); + foreach (var c in input) + { + if (_nameEncodePattern.IsMatch(c.ToString())) + { + encoded.Append(c); + } + else + { + encoded.Append('_').Append(((int)c).ToString("X4")); + } + } + + return encoded.ToString(); + } + + private string DecodeName(string input) + { + _ = this; + var decoded = new StringBuilder(); + for (var i = 0; i < input.Length; i++) + { + if (input[i] == '_') + { + var hexCode = input.Substring(i + 1, 4); + var charCode = Convert.ToInt32(hexCode, 16); + decoded.Append((char)charCode); + i += 4; + } + else + { + decoded.Append(input[i]); + } + } + + return decoded.ToString(); + } } diff --git a/src/Core/RodelChat.Core/ChatClient.Properties.cs b/src/Core/RodelChat.Core/ChatClient.Properties.cs index c561fe0d..b41ab380 100644 --- a/src/Core/RodelChat.Core/ChatClient.Properties.cs +++ b/src/Core/RodelChat.Core/ChatClient.Properties.cs @@ -1,5 +1,6 @@ // Copyright (c) Rodel. All rights reserved. +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using RodelChat.Interfaces.Client; using RodelChat.Models.Client; @@ -15,6 +16,8 @@ public sealed partial class ChatClient private readonly IChatParametersFactory _parameterFactory; private readonly ILogger _logger; private readonly List _dllPaths = new(); + private readonly Regex _nameEncodePattern = new Regex("^[a-zA-Z0-9_-]+$"); + private bool _disposedValue; private string _preferDllPath; diff --git a/src/Core/RodelChat.Core/ChatClient.cs b/src/Core/RodelChat.Core/ChatClient.cs index 291fb909..3a56aa49 100644 --- a/src/Core/RodelChat.Core/ChatClient.cs +++ b/src/Core/RodelChat.Core/ChatClient.cs @@ -5,6 +5,8 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; using RodelAgent.Models.Abstractions; using RodelChat.Interfaces.Client; @@ -33,7 +35,7 @@ public ChatClient( } /// - public void LoadSessions(List sessions) + public void LoadChatSessions(List sessions) { Sessions.Clear(); Sessions.AddRange(sessions); @@ -167,6 +169,47 @@ public async Task SendMessageAsync( } } + /// + public async Task SendGroupMessageAsync(ChatMessage message, ChatGroupPreset preset, Action messageAction = null, CancellationToken cancellationToken = default, params ChatSessionPreset[] agents) + { + var chatAgents = new List(); + foreach (var agentId in preset.Agents) + { + var agent = agents.FirstOrDefault(p => p.Id == agentId) + ?? throw new ArgumentException("Agent not found."); + var provider = _providerFactory.GetOrCreateProvider(agent.Provider); + var kernel = FindKernelProvider(agent.Provider, agent.Model) + ?? throw new KernelException($"Parse {agent.Name} failed, because provider config invalid."); + var chatAgent = new ChatCompletionAgent + { + Instructions = agent.SystemInstruction, + ExecutionSettings = provider.ConvertExecutionSettings(agent), + Id = agent.Id, + Kernel = kernel, + Name = EncodeName(agent.Name), + }; + + chatAgents.Add(chatAgent); + } + + var groupChat = new AgentGroupChat(chatAgents.ToArray()) + { + ExecutionSettings = + new() + { + TerminationStrategy = new CustomTerminationStrategy(preset.MaxRounds, preset.TerminateText), + }, + }; + groupChat.AddChatMessage(ConvertToKernelMessage(message)); + await foreach (var content in groupChat.InvokeAsync(cancellationToken)) + { + var assistantName = DecodeName(content.AuthorName); + var msg = ChatMessage.CreateAssistantMessage(content.Content); + msg.Author = assistantName; + messageAction?.Invoke(msg); + } + } + /// public void Dispose() { @@ -275,4 +318,22 @@ await Task.Run(() => return !string.IsNullOrEmpty(assemblyPath) && File.Exists(assemblyPath) ? Assembly.LoadFile(assemblyPath) : default; } + + private sealed class CustomTerminationStrategy : TerminationStrategy + { + private readonly string? _terminateText; + + public CustomTerminationStrategy(int maxRounds, string? terminateText = default) + { + MaximumIterations = maxRounds; + _terminateText = terminateText; + } + + protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) + { + return string.IsNullOrEmpty(_terminateText) + ? Task.FromResult(false) + : Task.FromResult(history[history.Count - 1].Content?.Contains(_terminateText, StringComparison.OrdinalIgnoreCase) ?? false); + } + } } diff --git a/src/Core/RodelChat.Core/Providers/AnthropicProvider.cs b/src/Core/RodelChat.Core/Providers/AnthropicProvider.cs index 7df7b2bc..e9237684 100644 --- a/src/Core/RodelChat.Core/Providers/AnthropicProvider.cs +++ b/src/Core/RodelChat.Core/Providers/AnthropicProvider.cs @@ -38,7 +38,7 @@ public AnthropicProvider(AnthropicClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new AnthropicPromptExecutionSettings { MaxTokens = sessionData.Parameters.GetValueOrDefault(nameof(AnthropicChatParameters.MaxTokens)), diff --git a/src/Core/RodelChat.Core/Providers/GeminiProvider.cs b/src/Core/RodelChat.Core/Providers/GeminiProvider.cs index 3a761da8..62ebf204 100644 --- a/src/Core/RodelChat.Core/Providers/GeminiProvider.cs +++ b/src/Core/RodelChat.Core/Providers/GeminiProvider.cs @@ -38,7 +38,7 @@ public GeminiProvider(GeminiClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new GeminiPromptExecutionSettings { TopP = sessionData.Parameters.GetValueOrDefault(nameof(GeminiChatParameters.TopP)), diff --git a/src/Core/RodelChat.Core/Providers/HunYuanProvider.cs b/src/Core/RodelChat.Core/Providers/HunYuanProvider.cs index b2e6dd1a..aed3c804 100644 --- a/src/Core/RodelChat.Core/Providers/HunYuanProvider.cs +++ b/src/Core/RodelChat.Core/Providers/HunYuanProvider.cs @@ -44,7 +44,7 @@ public HunYuanProvider(HunYuanClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new HunYuanPromptExecutionSettings { Temperature = sessionData.Parameters.GetValueOrDefault(nameof(HunYuanChatParameters.Temperature)), diff --git a/src/Core/RodelChat.Core/Providers/ProviderBase.cs b/src/Core/RodelChat.Core/Providers/ProviderBase.cs index 1ebfc43a..65cfc0f6 100644 --- a/src/Core/RodelChat.Core/Providers/ProviderBase.cs +++ b/src/Core/RodelChat.Core/Providers/ProviderBase.cs @@ -134,7 +134,7 @@ public void Release() /// /// 会话. /// 执行设置. - public virtual PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public virtual PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new OpenAIPromptExecutionSettings { PresencePenalty = sessionData.Parameters.GetValueOrDefault(nameof(OpenAIChatParameters.FrequencyPenalty)), diff --git a/src/Core/RodelChat.Core/Providers/QianFanProvider.cs b/src/Core/RodelChat.Core/Providers/QianFanProvider.cs index 53c1501f..a443b201 100644 --- a/src/Core/RodelChat.Core/Providers/QianFanProvider.cs +++ b/src/Core/RodelChat.Core/Providers/QianFanProvider.cs @@ -44,7 +44,7 @@ public QianFanProvider(QianFanClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new QianFanPromptExecutionSettings { MaxTokens = sessionData.Parameters.GetValueOrDefault(nameof(QianFanChatParameters.MaxOutputTokens)), diff --git a/src/Core/RodelChat.Core/Providers/SparkDeskProvider.cs b/src/Core/RodelChat.Core/Providers/SparkDeskProvider.cs index b4b93d38..05380de7 100644 --- a/src/Core/RodelChat.Core/Providers/SparkDeskProvider.cs +++ b/src/Core/RodelChat.Core/Providers/SparkDeskProvider.cs @@ -49,7 +49,7 @@ public SparkDeskProvider(SparkDeskClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new SparkDeskPromptExecutionSettings { MaxTokens = sessionData.Parameters.GetValueOrDefault(nameof(SparkDeskChatParameters.MaxTokens)), diff --git a/src/Core/RodelChat.Core/Providers/ZhiPuProvider.cs b/src/Core/RodelChat.Core/Providers/ZhiPuProvider.cs index 1137c20f..9c278a1a 100644 --- a/src/Core/RodelChat.Core/Providers/ZhiPuProvider.cs +++ b/src/Core/RodelChat.Core/Providers/ZhiPuProvider.cs @@ -38,7 +38,7 @@ public ZhiPuProvider(ZhiPuClientConfig config) } /// - public override PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData) + public override PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData) => new OpenAIPromptExecutionSettings { MaxTokens = sessionData.Parameters.GetValueOrDefault(nameof(ZhiPuChatParameters.MaxTokens)), diff --git a/src/Core/RodelChat.Core/RodelChat.Core.csproj b/src/Core/RodelChat.Core/RodelChat.Core.csproj index 5763a0fc..aadc0239 100644 --- a/src/Core/RodelChat.Core/RodelChat.Core.csproj +++ b/src/Core/RodelChat.Core/RodelChat.Core.csproj @@ -4,9 +4,11 @@ net8.0 enable enable + SKEXP0110;SKEXP0010;SKEXP0001 + diff --git a/src/Core/RodelChat.Interfaces/Client/IChatClient.cs b/src/Core/RodelChat.Interfaces/Client/IChatClient.cs index cc370b82..2831c6e9 100644 --- a/src/Core/RodelChat.Interfaces/Client/IChatClient.cs +++ b/src/Core/RodelChat.Interfaces/Client/IChatClient.cs @@ -21,7 +21,7 @@ public interface IChatClient : IDisposable /// 加载会话列表. /// /// 会话列表. - void LoadSessions(List sessions); + void LoadChatSessions(List sessions); /// /// 获取预定义模型. @@ -68,6 +68,22 @@ Task SendMessageAsync( List plugins = null, CancellationToken cancellationToken = default); + /// + /// 发送群组消息. + /// + /// 群组消息. + /// 群组预设. + /// 消息生成事件. + /// 终止令牌. + /// 助理列表. + /// . + Task SendGroupMessageAsync( + ChatMessage message, + ChatGroupPreset preset, + Action messageAction = default, + CancellationToken cancellationToken = default, + params ChatSessionPreset[] agents); + /// /// 从 DLL 中检索插件. /// diff --git a/src/Core/RodelChat.Interfaces/Client/IProvider.cs b/src/Core/RodelChat.Interfaces/Client/IProvider.cs index 50841bd5..8f8bf4af 100644 --- a/src/Core/RodelChat.Interfaces/Client/IProvider.cs +++ b/src/Core/RodelChat.Interfaces/Client/IProvider.cs @@ -36,7 +36,7 @@ public interface IProvider /// /// 会话. /// 执行设置. - PromptExecutionSettings ConvertExecutionSettings(ChatSession sessionData); + PromptExecutionSettings ConvertExecutionSettings(ChatSessionPreset sessionData); /// /// 获取模型列表. diff --git a/src/Core/RodelChat.Models/Client/ChatGroup.cs b/src/Core/RodelChat.Models/Client/ChatGroup.cs new file mode 100644 index 00000000..7189187d --- /dev/null +++ b/src/Core/RodelChat.Models/Client/ChatGroup.cs @@ -0,0 +1,30 @@ +// Copyright (c) Rodel. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RodelChat.Models.Client; + +/// +/// 聊天群组. +/// +public sealed class ChatGroup +{ + /// + /// 群组标题. + /// + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// 群组预设标识. + /// + [JsonPropertyName("preset_id")] + public string PresetId { get; set; } + + /// + /// 获取或设置历史记录. + /// + [JsonPropertyName("messages")] + public List? Messages { get; set; } +} diff --git a/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs b/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs new file mode 100644 index 00000000..c751b22f --- /dev/null +++ b/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs @@ -0,0 +1,73 @@ +// Copyright (c) Rodel. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RodelChat.Models.Client; + +/// +/// 聊天群组预设. +/// +public class ChatGroupPreset +{ + /// + /// 会话标识符. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// 预设名称. + /// + [JsonPropertyName("name")] + [JsonRequired] + public string Name { get; set; } = null!; + + /// + /// 群组成员. + /// + [JsonPropertyName("agents")] + public IList? Agents { get; set; } + + /// + /// 表情头像. + /// + [JsonPropertyName("emoji")] + public string? Emoji { get; set; } + + /// + /// 最大会话轮次. + /// + [JsonPropertyName("max_rounds")] + public int MaxRounds { get; set; } + + /// + /// 终结文本. + /// + [JsonPropertyName("terminate_text")] + public string? TerminateText { get; set; } + + /// + /// 克隆当前实例. + /// + /// . + public ChatGroupPreset Clone() + { + return new ChatGroupPreset + { + Id = Id, + Name = Name, + Agents = Agents, + Emoji = Emoji, + MaxRounds = MaxRounds, + TerminateText = TerminateText, + }; + } + + /// + public override bool Equals(object? obj) => obj is ChatGroupPreset preset && Id == preset.Id; + + /// + public override int GetHashCode() => HashCode.Combine(Id); +} diff --git a/src/Core/RodelChat.Models/Client/ChatMessage.cs b/src/Core/RodelChat.Models/Client/ChatMessage.cs index 2cb151ac..a696b70c 100644 --- a/src/Core/RodelChat.Models/Client/ChatMessage.cs +++ b/src/Core/RodelChat.Models/Client/ChatMessage.cs @@ -20,6 +20,12 @@ public sealed class ChatMessage [JsonPropertyName("role")] public MessageRole Role { get; set; } + /// + /// 发送者名称. + /// + [JsonPropertyName("author")] + public string Author { get; set; } + /// /// 消息内容. /// diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs index 6d53d53d..6b85a206 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs @@ -118,7 +118,7 @@ await GlobalDependencies.ServiceProvider.GetRequiredService() } CheckHistorySessionStatus(); - _chatClient.LoadSessions(sessions); + _chatClient.LoadChatSessions(sessions); SettingsToolkit.WriteLocalSetting(SettingNames.LastSelectedAgent, presetVM.Data.Id); CreateNewSession(); return; diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs index 294f504e..9a74ee9e 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs @@ -49,7 +49,7 @@ private async Task SetSelectedChatServiceAsync(ChatServiceItemViewModel chatVM) } CheckHistorySessionStatus(); - _chatClient.LoadSessions(sessions); + _chatClient.LoadChatSessions(sessions); SettingsToolkit.WriteLocalSetting(SettingNames.LastSelectedChatService, chatVM.ProviderType); CreateNewSession(); } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs index 3272d4c4..a47d704f 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs @@ -251,7 +251,7 @@ await GlobalDependencies.ServiceProvider.GetRequiredService() } CheckHistorySessionStatus(); - _chatClient.LoadSessions(sessions); + _chatClient.LoadChatSessions(sessions); SettingsToolkit.WriteLocalSetting(SettingNames.LastSelectedSessionPreset, presetVM.Data.Id); CreateNewSession(); return; diff --git a/src/Libs/semantic-kernel b/src/Libs/semantic-kernel index d06f50a8..a95ca9ab 160000 --- a/src/Libs/semantic-kernel +++ b/src/Libs/semantic-kernel @@ -1 +1 @@ -Subproject commit d06f50a8953c6245c77cbd967527d3c9929ae60d +Subproject commit a95ca9ab9f2403876b5effdb196b8121dee8e4b8 diff --git a/src/RodelAgent.sln b/src/RodelAgent.sln index 6bb40a3f..43027933 100644 --- a/src/RodelAgent.sln +++ b/src/RodelAgent.sln @@ -103,6 +103,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Console", "Console", "{62FE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migration.V1", "Libs\Migration.V1\Migration.V1.csproj", "{2D5FBCDF-9872-4A70-8077-C14473F0D231}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agents.Core", "Libs\semantic-kernel\dotnet\src\Agents\Core\Agents.Core.csproj", "{002DBA94-2E18-4620-9084-B82ECF0AD0BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agents.Abstractions", "Libs\semantic-kernel\dotnet\src\Agents\Abstractions\Agents.Abstractions.csproj", "{6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agents.OpenAI", "Libs\semantic-kernel\dotnet\src\Agents\OpenAI\Agents.OpenAI.csproj", "{FBBCDEBB-135B-439D-906A-4E299D3F0A6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -779,6 +785,54 @@ Global {2D5FBCDF-9872-4A70-8077-C14473F0D231}.Release|x64.Build.0 = Release|Any CPU {2D5FBCDF-9872-4A70-8077-C14473F0D231}.Release|x86.ActiveCfg = Release|Any CPU {2D5FBCDF-9872-4A70-8077-C14473F0D231}.Release|x86.Build.0 = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|ARM64.Build.0 = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|x64.Build.0 = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Debug|x86.Build.0 = Debug|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|Any CPU.Build.0 = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|ARM64.ActiveCfg = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|ARM64.Build.0 = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|x64.ActiveCfg = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|x64.Build.0 = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|x86.ActiveCfg = Release|Any CPU + {002DBA94-2E18-4620-9084-B82ECF0AD0BC}.Release|x86.Build.0 = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|ARM64.Build.0 = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|x64.ActiveCfg = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|x64.Build.0 = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|x86.ActiveCfg = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Debug|x86.Build.0 = Debug|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|Any CPU.Build.0 = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|ARM64.ActiveCfg = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|ARM64.Build.0 = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|x64.ActiveCfg = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|x64.Build.0 = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|x86.ActiveCfg = Release|Any CPU + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69}.Release|x86.Build.0 = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|ARM64.Build.0 = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|x64.Build.0 = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Debug|x86.Build.0 = Debug|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|Any CPU.Build.0 = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|ARM64.ActiveCfg = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|ARM64.Build.0 = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|x64.ActiveCfg = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|x64.Build.0 = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|x86.ActiveCfg = Release|Any CPU + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -829,6 +883,9 @@ Global {8309B380-FD4D-46EE-9FA7-3E0C98916478} = {62FE84D9-2966-496C-9548-EE5421944A23} {032F8E61-FF2F-4748-A59A-542DE9EAB54A} = {4B9E7C1C-23A5-4F32-A3E5-88F38D466FC0} {2D5FBCDF-9872-4A70-8077-C14473F0D231} = {4B9E7C1C-23A5-4F32-A3E5-88F38D466FC0} + {002DBA94-2E18-4620-9084-B82ECF0AD0BC} = {4B9E7C1C-23A5-4F32-A3E5-88F38D466FC0} + {6FC5B579-8860-4E64-AEFE-8AC15D6FAF69} = {4B9E7C1C-23A5-4F32-A3E5-88F38D466FC0} + {FBBCDEBB-135B-439D-906A-4E299D3F0A6E} = {4B9E7C1C-23A5-4F32-A3E5-88F38D466FC0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B237A43F-37BA-406A-B5D0-24FB28F3F1E3} From 824514c71158ba0a3fcfa37c99f327bf041e62d6 Mon Sep 17 00:00:00 2001 From: Anran Zhang Date: Wed, 26 Jun 2024 10:17:40 +0800 Subject: [PATCH 02/13] Update --- src/Console/RodelChat.Console/ChatService.cs | 11 +++-- src/Core/RodelAgent.Context/Assets/chat.db | Bin 20480 -> 28672 bytes src/Core/RodelAgent.Context/ChatDbContext.cs | 5 ++ src/Core/RodelAgent.Context/DbService.cs | 20 ++++++-- src/Core/RodelAgent.Context/MigrationUtils.cs | 6 ++- .../20240626011246_ChatAddGroup.Designer.cs | 38 +++++++++++++++ .../Migrations/20240626011246_ChatAddGroup.cs | 46 ++++++++++++++++++ .../Migrations/ChatDbContextModelSnapshot.cs | 2 +- .../RodelChat.Core/ChatClient.Properties.cs | 5 ++ src/Core/RodelChat.Core/ChatClient.cs | 36 +++++++++++--- .../Client/IChatClient.cs | 23 +++++++-- src/Core/RodelChat.Models/Client/ChatGroup.cs | 23 ++++++++- .../Client/ChatGroupPreset.cs | 2 +- .../RodelChat.Models/Client/ChatSession.cs | 2 + 14 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.Designer.cs create mode 100644 src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.cs diff --git a/src/Console/RodelChat.Console/ChatService.cs b/src/Console/RodelChat.Console/ChatService.cs index 5f43a871..6ba24ddb 100644 --- a/src/Console/RodelChat.Console/ChatService.cs +++ b/src/Console/RodelChat.Console/ChatService.cs @@ -50,18 +50,19 @@ public async Task StartAsync(CancellationToken cancellationToken) Id = "group", MaxRounds = 6, Name = "Group", - TerminateText = "approve", + TerminateText = ["approve", "批准"], }; + var group = _chatClient.CreateSession(preset); var chatMsg = ChatMessage.CreateUserMessage(message); await _chatClient.SendGroupMessageAsync( + group.Id, chatMsg, - preset, (response) => { HandleMessageResponse(response); }, - CancellationToken.None, - [.. agents]); + agents, + CancellationToken.None); #else var provider = AskProvider(); await RunAIAsync(provider); @@ -160,7 +161,7 @@ news and images. var publishManagerText = """ You are publish manager which will review editor news article, and make sure all news are correct. - Once all news are correct, you can approve the request by just responding "approve" + Once all news are correct, you can approve the request (with 'approve' keyword) and give final news article to publish. """; var reporter = new ChatSessionPreset diff --git a/src/Core/RodelAgent.Context/Assets/chat.db b/src/Core/RodelAgent.Context/Assets/chat.db index 423720e29a76f66bebfce1997a8481586fe92bd6..927dad3ada9815f4528e453b0995d0383a397ade 100644 GIT binary patch delta 475 zcmZozz}WDBae}lUI|Bm)2(tn)6A*Ju)G?N1XV6P~#moPLfrXEUf$tLkT0UDo9^TWu zr#Ch(!@AnmMU32^fv$1(+~~(>Gt@Gh*am;@`!Wae}lU3j+fKD-go~%S0VxQ5FWhv{$_RKNwi}co_ID@vr6M;aj*_P+$hH zS)&j$ySS_@V`F(qVp2|OaB6XJW`15VjAn5Ta&-)GRS0o(@^MvAf(cK~;nn7VbLR8v zFzK^Q=HrtQhKa=+!dR@EFYy^MvN7`CXW+lTS public DbSet Sessions { get; set; } + /// + /// 群组会话列表. + /// + public DbSet Groups { get; set; } + /// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlite($"Data Source={_dbPath}"); diff --git a/src/Core/RodelAgent.Context/DbService.cs b/src/Core/RodelAgent.Context/DbService.cs index a1c7d754..bc9cdcfb 100644 --- a/src/Core/RodelAgent.Context/DbService.cs +++ b/src/Core/RodelAgent.Context/DbService.cs @@ -67,16 +67,27 @@ public async Task> GetAllChatSessionAsync() return await _chatDb.Sessions.Select(p => p.Value).ToListAsync(); } + /// + /// 获取所有群组会话数据. + /// + /// JSON 列表. + public async Task> GetAllChatGroupAsync() + { + _chatDb ??= await MigrationUtils.GetChatDbAsync(_workingDirectory); + return await _chatDb.Groups.Select(p => p.Value).ToListAsync(); + } + /// /// 添加或更新聊天数据. /// /// 会话标识符. /// 会话 JSON 数据. + /// 是否为群组数据. /// . - public async Task AddOrUpdateChatDataAsync(string dataId, string value) + public async Task AddOrUpdateChatDataAsync(string dataId, string value, bool isGroup = false) { _chatDb ??= await MigrationUtils.GetChatDbAsync(_workingDirectory); - var dataset = _chatDb.Sessions; + var dataset = isGroup ? _chatDb.Groups : _chatDb.Sessions; var data = await dataset.FirstOrDefaultAsync(x => x.Id == dataId); if (data is null) { @@ -95,11 +106,12 @@ public async Task AddOrUpdateChatDataAsync(string dataId, string value) /// 移除聊天会话. /// /// 数据标识符. + /// 是否为群组消息. /// . - public async Task RemoveChatDataAsync(string dataId) + public async Task RemoveChatDataAsync(string dataId, bool isGroup = false) { _chatDb ??= await MigrationUtils.GetChatDbAsync(_workingDirectory); - var dataset = _chatDb.Sessions; + var dataset = isGroup ? _chatDb.Groups : _chatDb.Sessions; var data = await dataset.FirstOrDefaultAsync(x => x.Id == dataId); if (data is not null) { diff --git a/src/Core/RodelAgent.Context/MigrationUtils.cs b/src/Core/RodelAgent.Context/MigrationUtils.cs index 53db8692..223a89fb 100644 --- a/src/Core/RodelAgent.Context/MigrationUtils.cs +++ b/src/Core/RodelAgent.Context/MigrationUtils.cs @@ -1,5 +1,7 @@ // Copyright (c) Rodel. All rights reserved. +using Microsoft.EntityFrameworkCore; + namespace RodelAgent.Context; /// @@ -26,7 +28,9 @@ public static async Task GetSecretDbAsync(string workDir) public static async Task GetChatDbAsync(string workDir) { await CheckDatabaseExistInternalAsync("chat.db", workDir); - return new ChatDbContext(Path.Combine(workDir, "chat.db")); + var chatDbContext = new ChatDbContext(Path.Combine(workDir, "chat.db")); + await chatDbContext.Database.MigrateAsync(); + return chatDbContext; } /// diff --git a/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.Designer.cs b/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.Designer.cs new file mode 100644 index 00000000..1fdc3579 --- /dev/null +++ b/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.Designer.cs @@ -0,0 +1,38 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RodelAgent.Context; + +#nullable disable + +namespace RodelAgent.Context.Migrations +{ + [DbContext(typeof(ChatDbContext))] + [Migration("20240626011246_ChatAddGroup")] + partial class ChatAddGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.5"); + + modelBuilder.Entity("RodelAgent.Models.Common.Metadata", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Metadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.cs b/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.cs new file mode 100644 index 00000000..11f2b717 --- /dev/null +++ b/src/Core/RodelAgent.Context/Migrations/20240626011246_ChatAddGroup.cs @@ -0,0 +1,46 @@ +// + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RodelAgent.Context.Migrations +{ + /// + public partial class ChatAddGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_Sessions", + table: "Sessions"); + + migrationBuilder.RenameTable( + name: "Sessions", + newName: "Metadata"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Metadata", + table: "Metadata", + column: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_Metadata", + table: "Metadata"); + + migrationBuilder.RenameTable( + name: "Metadata", + newName: "Sessions"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Sessions", + table: "Sessions", + column: "Id"); + } + } +} diff --git a/src/Core/RodelAgent.Context/Migrations/ChatDbContextModelSnapshot.cs b/src/Core/RodelAgent.Context/Migrations/ChatDbContextModelSnapshot.cs index 3508b6e1..eb9915df 100644 --- a/src/Core/RodelAgent.Context/Migrations/ChatDbContextModelSnapshot.cs +++ b/src/Core/RodelAgent.Context/Migrations/ChatDbContextModelSnapshot.cs @@ -27,7 +27,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Sessions"); + b.ToTable("Metadata"); }); #pragma warning restore 612, 618 } diff --git a/src/Core/RodelChat.Core/ChatClient.Properties.cs b/src/Core/RodelChat.Core/ChatClient.Properties.cs index b41ab380..746c5b23 100644 --- a/src/Core/RodelChat.Core/ChatClient.Properties.cs +++ b/src/Core/RodelChat.Core/ChatClient.Properties.cs @@ -25,4 +25,9 @@ public sealed partial class ChatClient /// 会话列表. /// public List Sessions { get; } + + /// + /// 群组会话列表. + /// + public List Groups { get; } } diff --git a/src/Core/RodelChat.Core/ChatClient.cs b/src/Core/RodelChat.Core/ChatClient.cs index 3a56aa49..6d1a7749 100644 --- a/src/Core/RodelChat.Core/ChatClient.cs +++ b/src/Core/RodelChat.Core/ChatClient.cs @@ -29,6 +29,7 @@ public ChatClient( ILogger logger) { Sessions = new List(); + Groups = new List(); _logger = logger; _providerFactory = providerFactory; _parameterFactory = parameterFactory; @@ -41,6 +42,13 @@ public void LoadChatSessions(List sessions) Sessions.AddRange(sessions); } + /// + public void LoadGroupSessions(List groups) + { + Groups.Clear(); + Groups.AddRange(groups); + } + /// public List GetPredefinedModels(ProviderType type) { @@ -105,6 +113,18 @@ public ChatSession CreateSession(ChatSessionPreset preset) return session; } + /// + public ChatGroup CreateSession(ChatGroupPreset preset) + { + var id = Guid.NewGuid().ToString("N"); + + var presetJson = JsonSerializer.Serialize(preset); + var newPreset = JsonSerializer.Deserialize(presetJson); + var session = ChatGroup.CreateGroup(id, newPreset); + Groups.Add(session); + return session; + } + /// public List GetModels(ProviderType type) => GetProvider(type).GetModelList(); @@ -170,10 +190,12 @@ public async Task SendMessageAsync( } /// - public async Task SendGroupMessageAsync(ChatMessage message, ChatGroupPreset preset, Action messageAction = null, CancellationToken cancellationToken = default, params ChatSessionPreset[] agents) + public async Task SendGroupMessageAsync(string groupId, ChatMessage message, Action messageAction = null, List agents = null, CancellationToken cancellationToken = default) { + var group = Groups.FirstOrDefault(g => g.Id == groupId) + ?? throw new ArgumentException("Group not found."); var chatAgents = new List(); - foreach (var agentId in preset.Agents) + foreach (var agentId in group.Agents) { var agent = agents.FirstOrDefault(p => p.Id == agentId) ?? throw new ArgumentException("Agent not found."); @@ -197,7 +219,7 @@ public async Task SendGroupMessageAsync(ChatMessage message, ChatGroupPreset pre ExecutionSettings = new() { - TerminationStrategy = new CustomTerminationStrategy(preset.MaxRounds, preset.TerminateText), + TerminationStrategy = new CustomTerminationStrategy(group.MaxRounds, group.TerminateText), }, }; groupChat.AddChatMessage(ConvertToKernelMessage(message)); @@ -321,9 +343,9 @@ await Task.Run(() => private sealed class CustomTerminationStrategy : TerminationStrategy { - private readonly string? _terminateText; + private readonly IList? _terminateText; - public CustomTerminationStrategy(int maxRounds, string? terminateText = default) + public CustomTerminationStrategy(int maxRounds, IList? terminateText = default) { MaximumIterations = maxRounds; _terminateText = terminateText; @@ -331,9 +353,9 @@ public CustomTerminationStrategy(int maxRounds, string? terminateText = default) protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) { - return string.IsNullOrEmpty(_terminateText) + return _terminateText is null || _terminateText.Count == 0 ? Task.FromResult(false) - : Task.FromResult(history[history.Count - 1].Content?.Contains(_terminateText, StringComparison.OrdinalIgnoreCase) ?? false); + : Task.FromResult(_terminateText.Any(p => history[history.Count - 1].Content?.Contains(p, StringComparison.InvariantCultureIgnoreCase) ?? false)); } } } diff --git a/src/Core/RodelChat.Interfaces/Client/IChatClient.cs b/src/Core/RodelChat.Interfaces/Client/IChatClient.cs index 2831c6e9..c02ce830 100644 --- a/src/Core/RodelChat.Interfaces/Client/IChatClient.cs +++ b/src/Core/RodelChat.Interfaces/Client/IChatClient.cs @@ -23,6 +23,12 @@ public interface IChatClient : IDisposable /// 会话列表. void LoadChatSessions(List sessions); + /// + /// 加载群组会话列表. + /// + /// 群组会话列表. + void LoadGroupSessions(List groups); + /// /// 获取预定义模型. /// @@ -43,6 +49,13 @@ public interface IChatClient : IDisposable /// 会话信息. ChatSession CreateSession(ChatSessionPreset preset); + /// + /// 创建新群组会话. + /// + /// 群组预设. + /// 群组会话. + ChatGroup CreateSession(ChatGroupPreset preset); + /// /// 获取模型列表. /// @@ -71,18 +84,18 @@ Task SendMessageAsync( /// /// 发送群组消息. /// + /// 群组标识符. /// 群组消息. - /// 群组预设. /// 消息生成事件. - /// 终止令牌. /// 助理列表. + /// 终止令牌. /// . Task SendGroupMessageAsync( + string groupId, ChatMessage message, - ChatGroupPreset preset, Action messageAction = default, - CancellationToken cancellationToken = default, - params ChatSessionPreset[] agents); + List agents = default, + CancellationToken cancellationToken = default); /// /// 从 DLL 中检索插件. diff --git a/src/Core/RodelChat.Models/Client/ChatGroup.cs b/src/Core/RodelChat.Models/Client/ChatGroup.cs index 7189187d..c6e990f9 100644 --- a/src/Core/RodelChat.Models/Client/ChatGroup.cs +++ b/src/Core/RodelChat.Models/Client/ChatGroup.cs @@ -8,7 +8,7 @@ namespace RodelChat.Models.Client; /// /// 聊天群组. /// -public sealed class ChatGroup +public sealed class ChatGroup : ChatGroupPreset { /// /// 群组标题. @@ -27,4 +27,25 @@ public sealed class ChatGroup /// [JsonPropertyName("messages")] public List? Messages { get; set; } + + /// + /// 创建会话. + /// + /// 标识符. + /// 预设. + /// 群组会话. + public static ChatGroup CreateGroup(string id, ChatGroupPreset preset) + { + return new ChatGroup + { + Title = preset.Name, + PresetId = preset.Id, + Messages = new List(), + Agents = preset.Agents, + Emoji = preset.Emoji, + Name = preset.Name, + MaxRounds = preset.MaxRounds, + TerminateText = preset.TerminateText, + }; + } } diff --git a/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs b/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs index c751b22f..24d25ef1 100644 --- a/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs +++ b/src/Core/RodelChat.Models/Client/ChatGroupPreset.cs @@ -46,7 +46,7 @@ public class ChatGroupPreset /// 终结文本. /// [JsonPropertyName("terminate_text")] - public string? TerminateText { get; set; } + public IList? TerminateText { get; set; } /// /// 克隆当前实例. diff --git a/src/Core/RodelChat.Models/Client/ChatSession.cs b/src/Core/RodelChat.Models/Client/ChatSession.cs index b853a211..a30cfcef 100644 --- a/src/Core/RodelChat.Models/Client/ChatSession.cs +++ b/src/Core/RodelChat.Models/Client/ChatSession.cs @@ -61,6 +61,8 @@ public static ChatSession CreateSession(string newId, ChatSessionPreset preset) SystemInstruction = preset.SystemInstruction, StopSequences = preset.StopSequences, FilterCharacters = preset.FilterCharacters, + Emoji = preset.Emoji, + Plugins = preset.Plugins, }; } } From 8037bf08c463e9275e22fd6efb4da1ee65d6fe6c Mon Sep 17 00:00:00 2001 From: Anran Zhang Date: Wed, 26 Jun 2024 13:16:53 +0800 Subject: [PATCH 03/13] Update --- src/Core/RodelChat.Core/ChatClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/RodelChat.Core/ChatClient.cs b/src/Core/RodelChat.Core/ChatClient.cs index 6d1a7749..8fa9f85f 100644 --- a/src/Core/RodelChat.Core/ChatClient.cs +++ b/src/Core/RodelChat.Core/ChatClient.cs @@ -227,7 +227,9 @@ public async Task SendGroupMessageAsync(string groupId, ChatMessage message, Act { var assistantName = DecodeName(content.AuthorName); var msg = ChatMessage.CreateAssistantMessage(content.Content); + msg.Time = DateTimeOffset.Now; msg.Author = assistantName; + group.Messages.Add(msg); messageAction?.Invoke(msg); } } From ff440327f19c09f3a60f66289cf3d6f0e212357e Mon Sep 17 00:00:00 2001 From: Anran Zhang Date: Wed, 26 Jun 2024 16:09:13 +0800 Subject: [PATCH 04/13] Update --- .../GroupPresetModuleViewModel.Properties.cs | 51 ++++++++++++++ .../GroupPresetModuleViewModel.cs | 68 +++++++++++++++++++ .../Items/GroupPresetItemViewModel.cs | 26 +++++++ 3 files changed, 145 insertions(+) create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.Properties.cs create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.cs create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Items/GroupPresetItemViewModel.cs diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.Properties.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.Properties.cs new file mode 100644 index 00000000..e1ac095e --- /dev/null +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.Properties.cs @@ -0,0 +1,51 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelAgent.Interfaces; +using RodelAgent.UI.ViewModels.Items; + +namespace RodelAgent.UI.ViewModels.Components; + +/// +/// 群组预设模块视图模型. +/// +public sealed partial class GroupPresetModuleViewModel +{ + private readonly IStorageService _storageService; + + [ObservableProperty] + private string _name; + + [ObservableProperty] + private int _maxRounds; + + [ObservableProperty] + private bool _isNoAgentSelected; + + [ObservableProperty] + private bool _isAgentsEmpty; + + /// + /// 关闭请求事件. + /// + public event EventHandler CloseRequested; + + /// + /// 全部助理. + /// + public ObservableCollection TotalAgents { get; } = new(); + + /// + /// 已选助理. + /// + public ObservableCollection SelectedAgents { get; } = new(); + + /// + /// 终止文本. + /// + public ObservableCollection TerminateText { get; } = new(); + + /// + /// 是否为手动退出. + /// + public bool IsManualClose { get; set; } +} diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.cs new file mode 100644 index 00000000..1fb81ce9 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/GroupPresetModuleViewModel/GroupPresetModuleViewModel.cs @@ -0,0 +1,68 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelAgent.Interfaces; +using RodelAgent.UI.ViewModels.Items; + +namespace RodelAgent.UI.ViewModels.Components; + +/// +/// 群组预设模块视图模型. +/// +public sealed partial class GroupPresetModuleViewModel : ViewModelBase +{ + /// + /// Initializes a new instance of the class. + /// + public GroupPresetModuleViewModel( + IStorageService storageService) + : base(default) + { + _storageService = storageService; + SelectedAgents.CollectionChanged += (sender, e) => CheckAgentCount(); + } + + /// + /// 设置数据. + /// + /// . + public async Task SetDataAsync(GroupPresetItemViewModel data) + { + Data = data; + Name = data.Name; + MaxRounds = data.Data.MaxRounds; + TotalAgents.Clear(); + SelectedAgents.Clear(); + TerminateText.Clear(); + + var agents = await _storageService.GetChatAgentsAsync(); + foreach (var agent in agents) + { + TotalAgents.Add(new ChatPresetItemViewModel(agent)); + } + + foreach (var item in data.Data.Agents) + { + var a = TotalAgents.FirstOrDefault(p => p.Data.Id == item); + if (a == null) + { + continue; + } + + SelectedAgents.Add(a); + } + + if (data.Data.TerminateText != null) + { + foreach (var item in data.Data.TerminateText) + { + TerminateText.Add(item); + } + } + } + + private void CheckAgentCount() + { + IsAgentsEmpty = TotalAgents.Count == 0; + IsNoAgentSelected = SelectedAgents.Count == 0; + } +} diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Items/GroupPresetItemViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Items/GroupPresetItemViewModel.cs new file mode 100644 index 00000000..314a5d18 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/ViewModels/Items/GroupPresetItemViewModel.cs @@ -0,0 +1,26 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelChat.Models.Client; + +namespace RodelAgent.UI.ViewModels.Items; + +/// +/// 群组预设项视图模型. +/// +public sealed partial class GroupPresetItemViewModel : ViewModelBase +{ + [ObservableProperty] + private string _name; + + [ObservableProperty] + private bool _isSelected; + + /// + /// Initializes a new instance of the class. + /// + public GroupPresetItemViewModel(ChatGroupPreset data) + : base(data) + { + Name = data.Name; + } +} From 74e2560049c45ef4b7b9789337b2d786c8e08512 Mon Sep 17 00:00:00 2001 From: Richasy Date: Wed, 26 Jun 2024 21:12:05 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BE=A4=E7=BB=84?= =?UTF-8?q?=E7=9A=84=E6=B7=BB=E5=8A=A0/=E4=BF=AE=E6=94=B9/=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RodelAgent.Interfaces/IStorageService.cs | 48 +++ .../Client/ChatClientConfiguration.cs | 13 - .../Constants/SettingNames.cs | 1 + src/Desktop/RodelAgent.UI/App.xaml | 2 + .../Controls/Base/PresetAvatar.xaml.cs | 23 +- .../Controls/Chat/AgentsSection.xaml.cs | 2 +- .../Chat/ChatPresetSettingsDialog.xaml | 9 +- .../Chat/ChatPresetSettingsDialog.xaml.cs | 10 + .../Controls/Chat/ChatServiceHeader.xaml | 9 + .../Controls/Chat/ChatSessionListPanel.xaml | 6 +- .../Controls/Chat/GroupPresetControlBase.cs | 16 + .../Chat/GroupPresetSettingsDialog.xaml | 82 +++++ .../Chat/GroupPresetSettingsDialog.xaml.cs | 72 +++++ .../Controls/Chat/GroupsSection.xaml | 58 ++++ .../Controls/Chat/GroupsSection.xaml.cs | 35 +++ .../Controls/Chat/PresetGroupPanel.xaml | 279 ++++++++++++++++++ .../Controls/Chat/PresetGroupPanel.xaml.cs | 157 ++++++++++ .../Controls/Chat/PresetModelPanel.xaml.cs | 7 + .../ChatGroupPresetItemControl.cs | 55 ++++ .../ChatGroupPresetItemControl.xaml | 55 ++++ .../Items/ChatSessionPresetItemControl.xaml | 48 --- .../ChatSessionPresetItemControl.cs} | 27 +- .../ChatSessionPresetItemControl.xaml | 90 ++++++ .../StorageService/StorageService.Chat.cs | 233 ++++++++++++--- .../StorageService/StorageService.cs | 2 + .../RodelAgent.UI/GlobalDependencies.cs | 1 + .../RodelAgent.UI/Pages/ChatServicePage.xaml | 1 + .../Pages/ChatServicePage.xaml.cs | 5 + .../Resources/en-US/Resources.resw | 35 ++- .../Resources/zh-Hans-CN/Resources.resw | 33 +++ .../RodelAgent.UI/RodelAgent.UI.csproj | 24 +- .../ChatGroupViewModel.Messages.cs | 213 +++++++++++++ .../ChatGroupViewModel.Properties.cs | 80 +++++ .../ChatGroupViewModel/ChatGroupViewModel.cs | 142 +++++++++ .../ChatSessionViewModel.cs | 4 +- .../GroupPresetModuleViewModel.cs | 35 +++ .../ChatServicePageViewModel.Agents.cs | 17 +- .../ChatServicePageViewModel.Configuration.cs | 6 - .../ChatServicePageViewModel.Groups.cs | 141 +++++++++ .../ChatServicePageViewModel.Properties.cs | 27 +- .../ChatServicePageViewModel.Services.cs | 4 +- .../ChatServicePageViewModel.Sessions.cs | 24 +- .../ChatServicePageViewModel.cs | 16 +- 43 files changed, 1964 insertions(+), 183 deletions(-) create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetControlBase.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetSettingsDialog.xaml create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetSettingsDialog.xaml.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupsSection.xaml create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupsSection.xaml.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupPresetItemControl/ChatGroupPresetItemControl.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupPresetItemControl/ChatGroupPresetItemControl.xaml delete mode 100644 src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl.xaml rename src/Desktop/RodelAgent.UI/Controls/Items/{ChatSessionPresetItemControl.xaml.cs => ChatSessionPresetItemControl/ChatSessionPresetItemControl.cs} (68%) create mode 100644 src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl/ChatSessionPresetItemControl.xaml create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs create mode 100644 src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs diff --git a/src/Core/RodelAgent.Interfaces/IStorageService.cs b/src/Core/RodelAgent.Interfaces/IStorageService.cs index 40332ebd..818f631a 100644 --- a/src/Core/RodelAgent.Interfaces/IStorageService.cs +++ b/src/Core/RodelAgent.Interfaces/IStorageService.cs @@ -134,6 +134,27 @@ Task SetAudioConfigAsync(audioConstants.ProviderType type, T config) /// . Task RemoveChatSessionAsync(string sessionId); + /// + /// 获取指定预设的群组会话. + /// + /// 预设标识符. + /// 会话列表. + Task?> GetChatGroupSessionsAsync(string presetId); + + /// + /// 添加或更新群组会话. + /// + /// 会话. + /// . + Task AddOrUpdateChatGroupSessionAsync(ChatGroup session); + + /// + /// 移除聊天群组会话. + /// + /// 会话标识符. + /// . + Task RemoveChatGroupSessionAsync(string sessionId); + /// /// 获取聊天会话预设. /// @@ -181,6 +202,33 @@ Task SetAudioConfigAsync(audioConstants.ProviderType type, T config) /// . Task RemoveChatAgentAsync(string agentId); + /// + /// 移除群组. + /// + /// 群组标识符. + /// . + Task RemoveChatGroupPresetAsync(string presetId); + + /// + /// 获取会话群组预设列表. + /// + /// 助理列表. + Task> GetChatGroupPresetsAsync(); + + /// + /// 获取指定 ID 的群组会话预设. + /// + /// 预设 ID. + /// . + Task GetChatGroupPresetByIdAsync(string presetId); + + /// + /// 添加或更新群组预设. + /// + /// 群组信息. + /// . + Task AddOrUpdateChatGroupPresetAsync(ChatGroupPreset preset); + /// /// 获取指定供应商的翻译会话. /// diff --git a/src/Core/RodelChat.Models/Client/ChatClientConfiguration.cs b/src/Core/RodelChat.Models/Client/ChatClientConfiguration.cs index 214c38d8..eac8a605 100644 --- a/src/Core/RodelChat.Models/Client/ChatClientConfiguration.cs +++ b/src/Core/RodelChat.Models/Client/ChatClientConfiguration.cs @@ -124,12 +124,6 @@ public sealed class ChatClientConfiguration /// [JsonPropertyName("silicon_flow")] public SiliconFlowClientConfig? SiliconFlow { get; set; } - - /// - /// 本地模型配置. - /// - [JsonPropertyName("local")] - public LocalModelConfig? Local { get; set; } } /// @@ -319,13 +313,6 @@ public override bool IsValid() => IsCustomModelNotEmpty() && !string.IsNullOrEmpty(Endpoint); } -/// -/// 本地模型配置. -/// -public sealed class LocalModelConfig : ConfigBase -{ -} - /// /// 配置基类. /// diff --git a/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs b/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs index 8000ca51..0ca86710 100644 --- a/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs +++ b/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs @@ -39,4 +39,5 @@ public enum SettingNames AudioHistoryColumnWidth, AudioServicePageIsEnterSend, IsMigrating, + LastSelectedGroup, } diff --git a/src/Desktop/RodelAgent.UI/App.xaml b/src/Desktop/RodelAgent.UI/App.xaml index 03196f08..07a62441 100644 --- a/src/Desktop/RodelAgent.UI/App.xaml +++ b/src/Desktop/RodelAgent.UI/App.xaml @@ -17,6 +17,8 @@ + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Base/PresetAvatar.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Base/PresetAvatar.xaml.cs index c06169bd..341eb1c6 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Base/PresetAvatar.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Controls/Base/PresetAvatar.xaml.cs @@ -36,6 +36,11 @@ public PresetAvatar() Unloaded += OnUnloaded; } + /// + /// 是否为聊天预设. + /// + public bool IsChatPreset { get; set; } = true; + /// /// 预设 ID. /// @@ -84,13 +89,25 @@ private async void CheckAvatarAsync() return; } - var preset = await GlobalDependencies.ServiceProvider.GetRequiredService().GetChatSessionPresetByIdAsync(PresetId); - if (!string.IsNullOrEmpty(preset.Emoji)) + var emojiText = string.Empty; + var storageService = GlobalDependencies.ServiceProvider.GetRequiredService(); + if (IsChatPreset) + { + var preset = await storageService.GetChatSessionPresetByIdAsync(PresetId); + emojiText = preset?.Emoji; + } + else + { + var preset = await storageService.GetChatGroupPresetByIdAsync(PresetId); + emojiText = preset?.Emoji; + } + + if (!string.IsNullOrEmpty(emojiText)) { AgentAvatar.Visibility = Visibility.Collapsed; DefaultIcon.Visibility = Visibility.Collapsed; EmojiAvatar.Visibility = Visibility.Visible; - var emoji = EmojiStatics.GetEmojis().FirstOrDefault(x => x.Unicode == preset.Emoji); + var emoji = EmojiStatics.GetEmojis().FirstOrDefault(x => x.Unicode == emojiText); EmojiAvatar.Text = emoji?.ToEmoji(); } else diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/AgentsSection.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/AgentsSection.xaml.cs index 1fcedcf3..71707a2d 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/AgentsSection.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/AgentsSection.xaml.cs @@ -3,7 +3,7 @@ namespace RodelAgent.UI.Controls.Chat; /// -/// 本地模型区. +/// 助理区. /// public sealed partial class AgentsSection : ChatServicePageControlBase { diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatPresetSettingsDialog.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatPresetSettingsDialog.xaml index cd71763c..fa71cb48 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatPresetSettingsDialog.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatPresetSettingsDialog.xaml @@ -58,14 +58,7 @@ - + () + .ShowTip(StringNames.MustFillRequireFields, InfoType.Warning); + btn.IsEnabled = true; + return; + } + try { await ModelPanel.SaveAvatarAsync(); diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatServiceHeader.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatServiceHeader.xaml index 4bbb8a28..dbea701f 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatServiceHeader.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatServiceHeader.xaml @@ -62,6 +62,15 @@ Symbol="MoreHorizontal" /> + + + + + - + Visibility="{x:Bind ViewModel.IsChatHistorySessionsEmpty, Mode=OneWay, Converter={StaticResource BoolToVisibilityReverseConverter}}"> + + Visibility="{x:Bind ViewModel.IsChatHistorySessionsEmpty, Mode=OneWay}"> +/// 群组预设控件基类. +/// +public abstract class GroupPresetControlBase : ReactiveUserControl +{ + /// + /// Initializes a new instance of the class. + /// + protected GroupPresetControlBase() => ViewModel = ServiceProvider.GetRequiredService(); +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetSettingsDialog.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetSettingsDialog.xaml new file mode 100644 index 00000000..e69bba50 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupPresetSettingsDialog.xaml @@ -0,0 +1,82 @@ + + + + + 320 + 800 + 184 + 1999 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHeader.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHeader.xaml.cs new file mode 100644 index 00000000..8b4c7e60 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHeader.xaml.cs @@ -0,0 +1,51 @@ +// Copyright (c) Rodel. All rights reserved. + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 群组会话头部. +/// +public sealed partial class ChatGroupHeader : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupHeader() + { + InitializeComponent(); + ShareButton.Visibility = GlobalFeatureSwitcher.IsChatShareEnabled ? Visibility.Visible : Visibility.Collapsed; + } + + private void ShowRename() + { + TitleContainer.Visibility = Visibility.Collapsed; + RenameBox.Visibility = Visibility.Visible; + RenameBox.Text = ViewModel.Data.Title ?? string.Empty; + RenameBox.Focus(FocusState.Programmatic); + } + + private void HideRenameAndSave() + { + TitleContainer.Visibility = Visibility.Visible; + RenameBox.Visibility = Visibility.Collapsed; + if (RenameBox.Text != (ViewModel.Data.Title ?? string.Empty)) + { + ViewModel.ChangeTitleCommand.Execute(RenameBox.Text); + } + } + + private void OnRenameBoxLostFocus(object sender, RoutedEventArgs e) + => HideRenameAndSave(); + + private void OnRenameBoxPreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Enter) + { + HideRenameAndSave(); + e.Handled = true; + } + } + + private void OnTitleTapped(object sender, TappedRoutedEventArgs e) + => ShowRename(); +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml new file mode 100644 index 00000000..956dc2dc --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml.cs new file mode 100644 index 00000000..50810e28 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml.cs @@ -0,0 +1,51 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelAgent.UI.ViewModels.Components; + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 聊天会话历史. +/// +public sealed partial class ChatGroupHistory : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupHistory() + { + InitializeComponent(); + Unloaded += OnUnloaded; + } + + /// + protected override void OnViewModelChanged(DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is ChatGroupViewModel oldVm) + { + oldVm.RequestScrollToBottom -= OnRequestScrollToBottomAsync; + } + + if (e.NewValue is ChatGroupViewModel newVm) + { + newVm.RequestScrollToBottom += OnRequestScrollToBottomAsync; + } + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + if (ViewModel is not null) + { + ViewModel.RequestScrollToBottom -= OnRequestScrollToBottomAsync; + } + } + + private async void OnRequestScrollToBottomAsync(object sender, EventArgs e) + { + if (MessageViewer is not null) + { + await Task.Delay(200); + MessageViewer.ChangeView(0, MessageViewer.ScrollableHeight + MessageViewer.ActualHeight + MessageViewer.VerticalOffset, default); + } + } +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml new file mode 100644 index 00000000..acd27501 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml.cs new file mode 100644 index 00000000..3e9f9d7c --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupInput.xaml.cs @@ -0,0 +1,104 @@ +// Copyright (c) Rodel. All rights reserved. + +using Microsoft.UI.Input; +using RodelAgent.UI.ViewModels.Components; +using Windows.System; +using Windows.UI.Core; + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 群组会话输入. +/// +public sealed partial class ChatGroupInput : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupInput() + { + InitializeComponent(); + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + /// + protected override void OnViewModelChanged(DependencyPropertyChangedEventArgs e) + { + if (e.OldValue is ChatGroupViewModel oldVm) + { + oldVm.RequestFocusInput -= OnRequestFocusInput; + } + + if (e.NewValue is ChatGroupViewModel newVm) + { + newVm.RequestFocusInput += OnRequestFocusInput; + } + + CheckEnterSendItem(); + } + + private void OnRequestFocusInput(object sender, EventArgs e) + { + InputBox.Focus(FocusState.Programmatic); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + => CheckEnterSendItem(); + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + if (ViewModel is not null) + { + ViewModel.RequestFocusInput -= OnRequestFocusInput; + } + } + + private async void OnInputBoxPreviewKeyDownAsync(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter) + { + var shiftState = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift); + var isShiftDown = shiftState == CoreVirtualKeyStates.Down || shiftState == (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked); + var ctrlState = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control); + var isCtrlDown = ctrlState == CoreVirtualKeyStates.Down || ctrlState == (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked); + + if ((ViewModel.IsEnterSend && !isShiftDown) + || (!ViewModel.IsEnterSend && isCtrlDown)) + { + e.Handled = true; + await ViewModel.SendCommand.ExecuteAsync(default); + } + } + } + + private void OnCtrlEnterSendItemClick(object sender, RoutedEventArgs e) + { + ViewModel.IsEnterSend = false; + CheckEnterSendItem(); + } + + private void OnEnterSendItemClick(object sender, RoutedEventArgs e) + { + ViewModel.IsEnterSend = true; + CheckEnterSendItem(); + } + + private void CheckEnterSendItem() + { + if (EnterSendItem != null && ViewModel != null) + { + EnterSendItem.IsChecked = ViewModel.IsEnterSend; + CtrlEnterSendItem.IsChecked = !ViewModel.IsEnterSend; + } + } + + private void OnCleanMessageButtonClick(object sender, RoutedEventArgs e) + => CleanMessageTip.IsOpen = true; + + private void OnClearMessageActionButtonClick(TeachingTip sender, object args) + { + ViewModel.ClearMessageCommand.Execute(default); + sender.IsOpen = false; + } +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml new file mode 100644 index 00000000..e4396b56 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml.cs new file mode 100644 index 00000000..a2713af4 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml.cs @@ -0,0 +1,32 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelAgent.UI.ViewModels.Components; + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 群组会话列表面板. +/// +public sealed partial class ChatGroupListPanel : ChatServicePageControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupListPanel() => InitializeComponent(); + + private void OnItemClick(object sender, ViewModels.Components.ChatGroupViewModel e) + => ViewModel.SetSelectedGroupSessionCommand.Execute(e); + + private async void OnRenameItemClickAsync(object sender, RoutedEventArgs e) + { + var context = (sender as FrameworkElement)?.DataContext as ChatGroupViewModel; + var dialog = new SessionRenameDialog(context); + await dialog.ShowAsync(); + } + + private void OnDeleteItemClick(object sender, RoutedEventArgs e) + { + var context = (sender as FrameworkElement)?.DataContext as ChatGroupViewModel; + ViewModel.RemoveGroupCommand.Execute(context); + } +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml new file mode 100644 index 00000000..05f0f501 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml.cs new file mode 100644 index 00000000..3986245a --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupPanel.xaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) Rodel. All rights reserved. + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 群组会话面板. +/// +public sealed partial class ChatGroupPanel : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupPanel() => InitializeComponent(); +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatMessageItemControl.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatMessageItemControl.xaml index eb8cad62..d8f8d123 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatMessageItemControl.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatMessageItemControl.xaml @@ -40,8 +40,24 @@ + + + + + - + Orientation="Horizontal"> + + + + + + InitializeComponent(); private void OnPresetItemClick(object sender, ViewModels.Items.GroupPresetItemViewModel e) - => ViewModel.SetSelectedGroupCommand.Execute(e); + => ViewModel.SetSelectedGroupPresetCommand.Execute(e); private void OnPresetItemDeleteClick(object sender, RoutedEventArgs e) { diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/SessionRenameDialog.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/SessionRenameDialog.xaml.cs index bc757037..58bbe9fb 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/SessionRenameDialog.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/SessionRenameDialog.xaml.cs @@ -10,6 +10,7 @@ namespace RodelAgent.UI.Controls.Chat; public sealed partial class SessionRenameDialog : AppContentDialog { private readonly ChatSessionViewModel _sessionVM; + private readonly ChatGroupViewModel _groupVM; /// /// Initializes a new instance of the class. @@ -21,14 +22,37 @@ public SessionRenameDialog(ChatSessionViewModel sessionVM) RenameBox.Text = _sessionVM.Data.Title ?? string.Empty; } + /// + /// Initializes a new instance of the class. + /// + public SessionRenameDialog(ChatGroupViewModel groupVM) + { + InitializeComponent(); + _groupVM = groupVM; + RenameBox.Text = _groupVM.Data.Title ?? string.Empty; + } + private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { var newTitle = RenameBox.Text; - if (newTitle == (_sessionVM.Data.Title ?? string.Empty)) + + if (_groupVM != null) { - return; + if (newTitle == (_groupVM.Data.Title ?? string.Empty)) + { + return; + } + + _groupVM.ChangeTitleCommand.Execute(newTitle); } + else if (_sessionVM != null) + { + if (newTitle == (_sessionVM.Data.Title ?? string.Empty)) + { + return; + } - _sessionVM.ChangeTitleCommand.Execute(newTitle); + _sessionVM.ChangeTitleCommand.Execute(newTitle); + } } } diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/SystemInstructionPanel.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/SystemInstructionPanel.xaml.cs index 86302522..9f660fa2 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/SystemInstructionPanel.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/SystemInstructionPanel.xaml.cs @@ -33,13 +33,18 @@ private void UpdateInstruction() private void OnSystemBoxTextChanged(object sender, TextChangedEventArgs e) { + if (ViewModel is null) + { + return; + } + ViewModel.Data.SystemInstruction = SystemBox.Text; _textChanged = true; } private void OnSystemBoxLostFocus(object sender, RoutedEventArgs e) { - if (_textChanged) + if (_textChanged && ViewModel is not null) { ViewModel.SaveSessionToDatabaseCommand.Execute(default); } diff --git a/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.cs b/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.cs new file mode 100644 index 00000000..bb235989 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.cs @@ -0,0 +1,31 @@ +// Copyright (c) Rodel. All rights reserved. + +using RodelAgent.UI.ViewModels.Components; + +namespace RodelAgent.UI.Controls.Items; + +/// +/// 群组会话项控件. +/// +public sealed class ChatGroupItemControl : ReactiveControl +{ + /// + /// Initializes a new instance of the class. + /// + public ChatGroupItemControl() => DefaultStyleKey = typeof(ChatGroupItemControl); + + /// + /// 条目被点击. + /// + public event EventHandler Click; + + /// + protected override void OnApplyTemplate() + { + var rootCard = GetTemplateChild("RootCard") as CardPanel; + if (rootCard != null) + { + rootCard.Click += (sender, e) => Click?.Invoke(this, ViewModel); + } + } +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.xaml b/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.xaml new file mode 100644 index 00000000..4b3e0bff --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Items/ChatGroupItemControl/ChatGroupItemControl.xaml @@ -0,0 +1,43 @@ + + + + diff --git a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml index 55742b35..3611a89b 100644 --- a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml +++ b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml @@ -73,21 +73,25 @@ - + Visibility="{x:Bind ViewModel.IsGroupChat, Mode=OneWay, Converter={StaticResource BoolToVisibilityReverseConverter}}"> + + + - + - + @@ -125,7 +129,10 @@ ViewModel="{x:Bind ViewModel.CurrentSession, Mode=OneWay}" Visibility="{x:Bind ViewModel.IsSystemInstructionVisible, Mode=OneWay}" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs index 521a9cf9..a6d9d394 100644 --- a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs @@ -61,6 +61,7 @@ private void UpdateExtraSizer() } ExtraSizer.Maximum = height; + ExtraSizer2.Maximum = height; } private void InitializePanelType() diff --git a/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj b/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj index cfbfadc2..7d677987 100644 --- a/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj +++ b/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj @@ -50,6 +50,11 @@ + + + + + @@ -65,6 +70,7 @@ + @@ -575,6 +581,24 @@ MSBuild:Compile + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + Always diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs index df2dbfe9..adb1c61e 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs @@ -126,6 +126,7 @@ private async Task SendMessageInternalAsync(bool addUserMsg = true) DeleteMessageAsync)); } + UserInput = string.Empty; var selectedAgents = Agents.Select(p => p.Data).ToList(); await _chatClient.SendGroupMessageAsync( SessionId, @@ -134,7 +135,7 @@ await _chatClient.SendGroupMessageAsync( { _ = _dispatcherQueue.TryEnqueue(() => { - if (_cancellationTokenSource is null || _cancellationTokenSource.IsCancellationRequested) + if (_cancellationTokenSource is null && Messages.Count == 0) { return; } @@ -159,10 +160,13 @@ await _chatClient.SendGroupMessageAsync( if (response.Role == MessageRole.Assistant) { - Messages.Add(new ChatMessageItemViewModel( - response, - EditMessageAsync, - DeleteMessageAsync)); + var msg = new ChatMessageItemViewModel( + response, + EditMessageAsync, + DeleteMessageAsync); + msg.Author = name; + msg.AgentId = agent?.Data.Id; + Messages.Add(msg); } else if (response.Role == MessageRole.Client) { diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs index a9dcbc75..74ea6c6f 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs @@ -78,7 +78,7 @@ private async Task SaveSessionToDatabaseAsync(bool force = false) } await _storageService.AddOrUpdateChatGroupSessionAsync(Data); - GlobalDependencies.ServiceProvider.GetService().CheckCurrentSessionExistCommand.Execute(this); + GlobalDependencies.ServiceProvider.GetService().CheckCurrentGroupExistCommand.Execute(this); } [RelayCommand] @@ -109,9 +109,16 @@ private async Task InitializeAgentsAsync() { Agents.Clear(); var storageService = GlobalDependencies.ServiceProvider.GetRequiredService(); + await Task.Delay(200); var agents = await storageService.GetChatAgentsAsync(); - foreach (var agent in agents) + foreach (var agentId in Data.Agents) { + var agent = agents.FirstOrDefault(p => p.Id == agentId); + if (agent is null) + { + continue; + } + var a = new ChatPresetItemViewModel(agent); Agents.Add(a); } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs index c85ab20c..cae4ec10 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs @@ -30,6 +30,12 @@ public sealed partial class ChatMessageItemViewModel : ViewModelBase /// Initializes a new instance of the class. /// diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs index 6ff1d112..e321653e 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Agents.cs @@ -102,7 +102,7 @@ private async Task SetSelectedAgentAsync(ChatPresetItemViewModel presetVM) { SetSelectedChatServiceCommand.Execute(default); SetSelectedSessionPresetCommand.Execute(default); - SetSelectedGroupCommand.Execute(default); + SetSelectedGroupPresetCommand.Execute(default); HistoryChatSessions.Clear(); var service = AvailableServices.FirstOrDefault(p => p.ProviderType == presetVM.Data.Provider); if (service == null) diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs index bb960c4c..575ed0b5 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs @@ -27,8 +27,8 @@ private async Task ResetGroupsAsync() if (SettingsToolkit.IsSettingKeyExist(SettingNames.LastSelectedGroup)) { var lastSelectedGroup = SettingsToolkit.ReadLocalSetting(SettingNames.LastSelectedGroup, string.Empty); - var agent = GroupPresets.FirstOrDefault(p => p.Data.Id == lastSelectedGroup); - SetSelectedAgentCommand.Execute(agent); + var group = GroupPresets.FirstOrDefault(p => p.Data.Id == lastSelectedGroup); + SetSelectedGroupPresetCommand.Execute(group); } IsGroupsEmpty = GroupPresets.Count == 0; @@ -70,7 +70,7 @@ private async Task AddGroupAsync() } [RelayCommand] - private async Task SetSelectedGroupAsync(GroupPresetItemViewModel presetVM) + private async Task SetSelectedGroupPresetAsync(GroupPresetItemViewModel presetVM) { foreach (var item in GroupPresets) { @@ -138,4 +138,77 @@ private async Task DeleteGroupAsync(GroupPresetItemViewModel presetVM) IsGroupsEmpty = GroupPresets.Count == 0; await _storageService.RemoveChatGroupPresetAsync(presetVM.Data.Id); } + + [RelayCommand] + private void CheckCurrentGroupExist(ChatGroupViewModel vm) + { + if (CurrentGroup != vm) + { + return; + } + + if (!string.IsNullOrEmpty(vm.Data.PresetId) + && GroupPresets.FirstOrDefault(p => p.IsSelected)?.Data.Id != CurrentGroup.Data.PresetId) + { + return; + } + + var sourceSession = HistoryGroupSessions.FirstOrDefault(p => p.SessionId == CurrentGroup.SessionId); + if (sourceSession != null) + { + sourceSession.Title = CurrentGroup.Title; + sourceSession.Data.Title = CurrentGroup.Title; + return; + } + + HistoryGroupSessions.Insert(0, CurrentGroup); + SetSelectedGroupSession(CurrentGroup); + } + + [RelayCommand] + private void SetSelectedGroupSession(ChatGroupViewModel groupVM) + { + foreach (var item in HistoryChatSessions) + { + item.IsSelected = groupVM != null && item.Equals(groupVM); + } + + if (groupVM != null && CurrentGroup != groupVM) + { + CurrentGroup = groupVM; + } + } + + [RelayCommand] + private async Task RemoveGroupAsync(ChatGroupViewModel groupVM) + { + if (groupVM == null) + { + return; + } + + if (CurrentGroup == groupVM) + { + CreateNewSession(); + } + + groupVM.CancelMessageCommand.Execute(default); + await _storageService.RemoveChatGroupSessionAsync(groupVM.SessionId); + HistoryGroupSessions.Remove(groupVM); + } + + [RelayCommand] + private async Task RemoveAllGroupsAsync() + { + foreach (var session in HistoryGroupSessions) + { + session.IsSelected = false; + session.CancelMessageCommand.Execute(default); + await _storageService.RemoveChatGroupSessionAsync(session.Data.Id); + } + + CurrentGroup = default; + HistoryGroupSessions.Clear(); + CreateNewSessionCommand.Execute(default); + } } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs index 9314fb65..9ae93da0 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs @@ -76,9 +76,15 @@ public sealed partial class ChatServicePageViewModel [ObservableProperty] private ChatSessionViewModel _currentSession; + [ObservableProperty] + private ChatGroupViewModel _currentGroup; + [ObservableProperty] private bool _isDeletingPluginsNotEmpty; + [ObservableProperty] + private bool _isGroupChat; + /// /// 可用的聊天服务. /// @@ -99,11 +105,6 @@ public sealed partial class ChatServicePageViewModel /// public ObservableCollection SessionPresets { get; } = new(); - /// - /// 本地模型预设. - /// - public ObservableCollection LocalModelPresets { get; } = new(); - /// /// 助理列表. /// diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs index fdfdfd6e..1ff77137 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Services.cs @@ -41,6 +41,7 @@ private async Task SetSelectedChatServiceAsync(ChatServiceItemViewModel chatVM) { SetSelectedAgentCommand.Execute(default); SetSelectedSessionPresetCommand.Execute(default); + SetSelectedGroupPresetCommand.Execute(default); HistoryChatSessions.Clear(); var sessions = await _storageService.GetChatSessionsAsync(chatVM.ProviderType); foreach (var session in sessions) diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs index 7056261e..57e80ff5 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Sessions.cs @@ -55,6 +55,18 @@ private void CreateNewSession() SetSelectedSession(default); CreateNewChatSessionInternal(sessionPresetVM); } + else if (SettingsToolkit.IsSettingKeyExist(SettingNames.LastSelectedGroup)) + { + var lastGroup = SettingsToolkit.ReadLocalSetting(SettingNames.LastSelectedGroup, string.Empty); + var groupVM = GroupPresets.FirstOrDefault(p => p.Data.Id == lastGroup); + if (groupVM == null) + { + return; + } + + SetSelectedSession(default); + CreateNewChatGroupInternal(groupVM); + } } [RelayCommand] @@ -122,8 +134,7 @@ private void CheckCurrentSessionExist(ChatSessionViewModel vm) if (!string.IsNullOrEmpty(vm.Data.PresetId) && AgentPresets.FirstOrDefault(p => p.IsSelected)?.Data.Id != CurrentSession.Data.PresetId - && SessionPresets.FirstOrDefault(p => p.IsSelected)?.Data.Id != CurrentSession.Data.PresetId - && !LocalModelPresets.Any(p => p.Data.Id == CurrentSession.Data.PresetId)) + && SessionPresets.FirstOrDefault(p => p.IsSelected)?.Data.Id != CurrentSession.Data.PresetId) { return; } @@ -235,6 +246,7 @@ private async Task SetSelectedSessionPresetAsync(ChatPresetItemViewModel presetV { SetSelectedChatServiceCommand.Execute(default); SetSelectedAgentCommand.Execute(default); + SetSelectedGroupPresetCommand.Execute(default); HistoryChatSessions.Clear(); var service = AvailableServices.FirstOrDefault(p => p.ProviderType == presetVM.Data.Provider); if (service == null) @@ -262,6 +274,7 @@ await GlobalDependencies.ServiceProvider.GetRequiredService() private void CreateNewChatSessionInternal(ChatServiceItemViewModel serviceVM) { + ExitGroupChat(); CurrentSession?.SaveSessionToDatabaseCommand.ExecuteAsync(default); var defaultModel = SettingsToolkit.ReadLocalSetting($"{serviceVM.ProviderType}DefaultModel", string.Empty); var hasModel = !string.IsNullOrEmpty(defaultModel) && (serviceVM.ServerModels.Any(p => p.Id == defaultModel) || serviceVM.CustomModels.Any(p => p.Id == defaultModel)); @@ -275,19 +288,30 @@ private void CreateNewChatSessionInternal(ChatServiceItemViewModel serviceVM) CurrentSession.ResetPluginsCommand.Execute(default); } - private void CreateNewChatSessionInternal(ChatModelItemViewModel modelItem) - { - CurrentSession?.SaveSessionToDatabaseCommand.ExecuteAsync(default); - var presetVM = LocalModelPresets.FirstOrDefault(p => p.Data.Model == modelItem.Id); - CreateNewChatSessionInternal(presetVM); - } - private void CreateNewChatSessionInternal(ChatPresetItemViewModel preset) { + ExitGroupChat(); CurrentSession?.SaveSessionToDatabaseCommand.ExecuteAsync(default); var newSession = _chatClient.CreateSession(preset.Data); CurrentSession = new ChatSessionViewModel(newSession, _chatClient); CurrentSession.Title = preset.Data.Name; CurrentSession.ResetPluginsCommand.Execute(default); } + + private void CreateNewChatGroupInternal(GroupPresetItemViewModel preset) + { + CurrentSession?.SaveSessionToDatabaseCommand.ExecuteAsync(default); + CurrentSession = default; + CurrentGroup?.SaveSessionToDatabaseCommand.ExecuteAsync(default); + var newSession = _chatClient.CreateSession(preset.Data); + CurrentGroup = new ChatGroupViewModel(newSession, _chatClient); + IsGroupChat = true; + } + + private void ExitGroupChat() + { + IsGroupChat = false; + CurrentGroup?.SaveSessionToDatabaseCommand.ExecuteAsync(default); + CurrentGroup = default; + } } From 2167b4461913f92664d7a6667238b8b226b10ded Mon Sep 17 00:00:00 2001 From: Richasy Date: Thu, 27 Jun 2024 09:15:25 +0800 Subject: [PATCH 07/13] Update --- .../Constants/ChatGroupPanelType.cs | 19 +++++++ .../Constants/SettingNames.cs | 1 + .../Controls/Chat/ChatGroupListPanel.xaml | 2 +- .../Controls/Chat/GroupAgentsPanel.xaml | 27 +++++++++ .../Controls/Chat/GroupAgentsPanel.xaml.cs | 14 +++++ .../Controls/Chat/GroupOptionsPanel.xaml | 57 +++++++++++++++++++ .../Controls/Chat/GroupOptionsPanel.xaml.cs | 31 ++++++++++ .../RodelAgent.UI/Pages/ChatServicePage.xaml | 25 +++++--- .../Pages/ChatServicePage.xaml.cs | 44 +++++++++++--- .../Resources/en-US/Resources.resw | 3 + .../Resources/zh-Hans-CN/Resources.resw | 3 + .../RodelAgent.UI/RodelAgent.UI.csproj | 11 ++++ .../ChatGroupViewModel.Properties.cs | 5 ++ .../ChatGroupViewModel/ChatGroupViewModel.cs | 19 +++++++ .../ChatServicePageViewModel.Properties.cs | 11 +++- .../ChatServicePageViewModel.cs | 28 ++++++--- 16 files changed, 274 insertions(+), 26 deletions(-) create mode 100644 src/Desktop/RodelAgent.UI.Models/Constants/ChatGroupPanelType.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupAgentsPanel.xaml create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupAgentsPanel.xaml.cs create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml create mode 100644 src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml.cs diff --git a/src/Desktop/RodelAgent.UI.Models/Constants/ChatGroupPanelType.cs b/src/Desktop/RodelAgent.UI.Models/Constants/ChatGroupPanelType.cs new file mode 100644 index 00000000..201f1248 --- /dev/null +++ b/src/Desktop/RodelAgent.UI.Models/Constants/ChatGroupPanelType.cs @@ -0,0 +1,19 @@ +// Copyright (c) Rodel. All rights reserved. + +namespace RodelAgent.UI.Models.Constants; + +/// +/// 聊天群面板类型. +/// +public enum ChatGroupPanelType +{ + /// + /// 助理. + /// + Agents, + + /// + /// 群组选项. + /// + GroupOptions, +} diff --git a/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs b/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs index 0ca86710..ac7d74c0 100644 --- a/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs +++ b/src/Desktop/RodelAgent.UI.Models/Constants/SettingNames.cs @@ -40,4 +40,5 @@ public enum SettingNames AudioServicePageIsEnterSend, IsMigrating, LastSelectedGroup, + ChatGroupPanelType, } diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml index e4396b56..b3a4a4ed 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupListPanel.xaml @@ -101,7 +101,7 @@ VerticalAlignment="Center" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{StaticResource ControlCornerRadius}" - Visibility="{x:Bind ViewModel.IsChatHistorySessionsEmpty, Mode=OneWay}"> + Visibility="{x:Bind ViewModel.IsGroupHistorySessionsEmpty, Mode=OneWay}"> + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/GroupAgentsPanel.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupAgentsPanel.xaml.cs new file mode 100644 index 00000000..27e61239 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupAgentsPanel.xaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) Rodel. All rights reserved. + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 聊天群助理面板. +/// +public sealed partial class GroupAgentsPanel : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public GroupAgentsPanel() => InitializeComponent(); +} diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml new file mode 100644 index 00000000..d28efe5c --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml.cs b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml.cs new file mode 100644 index 00000000..e8692c25 --- /dev/null +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/GroupOptionsPanel.xaml.cs @@ -0,0 +1,31 @@ +// Copyright (c) Rodel. All rights reserved. + +namespace RodelAgent.UI.Controls.Chat; + +/// +/// 聊天群选项面板. +/// +public sealed partial class GroupOptionsPanel : ChatGroupControlBase +{ + /// + /// Initializes a new instance of the class. + /// + public GroupOptionsPanel() + { + InitializeComponent(); + } + + private void OnMaxRoundsChanged(object sender, RangeBaseValueChangedEventArgs e) + { + if (e.NewValue < 1 || ViewModel is null) + { + return; + } + + ViewModel.MaxRounds = (int)e.NewValue; + ViewModel.CheckMaxRoundsCommand.Execute(default); + } + + private void OnTerminateTextSubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + => ViewModel.CheckMaxRoundsCommand.Execute(default); +} diff --git a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml index 3611a89b..81eeb9ae 100644 --- a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml +++ b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml @@ -110,10 +110,10 @@ + SelectionChanged="OnSessionPanelTypeChanged" /> - + - + - + + (); @@ -77,18 +78,39 @@ private void InitializePanelType() Text = stringToolkit.GetString(name), }; - PanelTypeSelector.Items.Add(item); - if (ViewModel.PanelType == (ChatSessionPanelType)i) + SessionPanelTypeSelector.Items.Add(item); + if (ViewModel.SessionPanelType == (ChatSessionPanelType)i) { - PanelTypeSelector.SelectedItem = item; + SessionPanelTypeSelector.SelectedItem = item; } } } - private void OnPanelTypeChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + private void InitializeGroupPanelType() { - var currentType = (ChatSessionPanelType)PanelTypeSelector.SelectedItem.Tag; - ViewModel.PanelType = currentType; + var names = Enum.GetNames(typeof(ChatGroupPanelType)); + var stringToolkit = ServiceProvider.GetRequiredService(); + for (var i = 0; i < names.Length; i++) + { + var name = names[i]; + var item = new SelectorBarItem + { + Tag = (ChatGroupPanelType)i, + Text = stringToolkit.GetString(name), + }; + + GroupPanelTypeSelector.Items.Add(item); + if (ViewModel.GroupPanelType == (ChatGroupPanelType)i) + { + GroupPanelTypeSelector.SelectedItem = item; + } + } + } + + private void OnSessionPanelTypeChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + { + var currentType = (ChatSessionPanelType)SessionPanelTypeSelector.SelectedItem.Tag; + ViewModel.SessionPanelType = currentType; } private void OnSessionParameterChanged(object sender, EventArgs e) @@ -99,6 +121,12 @@ private void OnRestartButtonClick(object sender, RoutedEventArgs e) AppInstance.GetCurrent().UnregisterKey(); _ = AppInstance.Restart(default); } + + private void OnGroupPanelTypeChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args) + { + var currentType = (ChatGroupPanelType)GroupPanelTypeSelector.SelectedItem.Tag; + ViewModel.GroupPanelType = currentType; + } } /// diff --git a/src/Desktop/RodelAgent.UI/Resources/en-US/Resources.resw b/src/Desktop/RodelAgent.UI/Resources/en-US/Resources.resw index 5d7a2657..ebb2f792 100644 --- a/src/Desktop/RodelAgent.UI/Resources/en-US/Resources.resw +++ b/src/Desktop/RodelAgent.UI/Resources/en-US/Resources.resw @@ -510,6 +510,9 @@ An understandable name + + Group options + Groups diff --git a/src/Desktop/RodelAgent.UI/Resources/zh-Hans-CN/Resources.resw b/src/Desktop/RodelAgent.UI/Resources/zh-Hans-CN/Resources.resw index 2f6bac00..a63587f3 100644 --- a/src/Desktop/RodelAgent.UI/Resources/zh-Hans-CN/Resources.resw +++ b/src/Desktop/RodelAgent.UI/Resources/zh-Hans-CN/Resources.resw @@ -510,6 +510,9 @@ 一个易懂的名称 + + 群组选项 + 群组 diff --git a/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj b/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj index 7d677987..99fa1264 100644 --- a/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj +++ b/src/Desktop/RodelAgent.UI/RodelAgent.UI.csproj @@ -57,6 +57,8 @@ + + @@ -599,6 +601,9 @@ MSBuild:Compile + + MSBuild:Compile + Always @@ -847,4 +852,10 @@ + + + + + + diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs index cd825f60..84f647f5 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs @@ -77,4 +77,9 @@ public sealed partial class ChatGroupViewModel /// 助理列表. /// public ObservableCollection Agents { get; } = new(); + + /// + /// 终止文本列表. + /// + public ObservableCollection TerminateText { get; } = new(); } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs index 74ea6c6f..7393d976 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs @@ -41,6 +41,15 @@ private void Initialize(ChatGroup data) GroupName = data.Name; Title = data.Title ?? ResourceToolkit.GetLocalizedString(StringNames.RandomChat); MaxRounds = data.MaxRounds; + + if (data.TerminateText != null && data.TerminateText.Count > 0) + { + foreach (var item in data.TerminateText) + { + TerminateText.Add(item); + } + } + if (data.Messages != null && data.Messages.Count > 0) { foreach (var message in data.Messages) @@ -104,6 +113,16 @@ private void CheckMaxRounds() } } + [RelayCommand] + private void CheckTerminateText() + { + if (TerminateText != null) + { + Data.TerminateText = new List(TerminateText); + SaveSessionToDatabaseCommand.Execute(default); + } + } + [RelayCommand] private async Task InitializeAgentsAsync() { diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs index 9ae93da0..97d9d608 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Properties.cs @@ -55,6 +55,12 @@ public sealed partial class ChatServicePageViewModel [ObservableProperty] private bool _isSessionOptionsVisible; + [ObservableProperty] + private bool _isAgentsSectionVisible; + + [ObservableProperty] + private bool _isGroupOptionsVisible; + [ObservableProperty] private bool _isAgentsEmpty; @@ -71,7 +77,10 @@ public sealed partial class ChatServicePageViewModel private bool _isPluginEmpty; [ObservableProperty] - private ChatSessionPanelType _panelType; + private ChatSessionPanelType _sessionPanelType; + + [ObservableProperty] + private ChatGroupPanelType _groupPanelType; [ObservableProperty] private ChatSessionViewModel _currentSession; diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.cs index 98555c1d..056afd76 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.cs @@ -35,8 +35,10 @@ public ChatServicePageViewModel( ExtraColumnWidth = SettingsToolkit.ReadLocalSetting(SettingNames.ChatServicePageExtraColumnWidth, 240d); ExtraColumnVisible = SettingsToolkit.ReadLocalSetting(SettingNames.ChatServicePageExtraColumnVisible, true); ExtraRowHeight = SettingsToolkit.ReadLocalSetting(SettingNames.ChatServicePageExtraRowHeight, 400d); - PanelType = SettingsToolkit.ReadLocalSetting(SettingNames.ChatSessionPanelType, ChatSessionPanelType.SystemInstruction); - CheckPanelType(); + SessionPanelType = SettingsToolkit.ReadLocalSetting(SettingNames.ChatSessionPanelType, ChatSessionPanelType.SystemInstruction); + GroupPanelType = SettingsToolkit.ReadLocalSetting(SettingNames.ChatGroupPanelType, ChatGroupPanelType.Agents); + CheckSessionPanelType(); + CheckGroupPanelType(); IsServiceSectionVisible = true; IsAvailableServicesEmpty = AvailableServices.Count == 0; IsAgentsEmpty = AgentPresets.Count == 0; @@ -51,10 +53,16 @@ public ChatServicePageViewModel( AttachIsRunningToAsyncCommand(p => IsPluginLoading = p, ResetPluginsCommand); } - private void CheckPanelType() + private void CheckSessionPanelType() { - IsSystemInstructionVisible = PanelType == ChatSessionPanelType.SystemInstruction; - IsSessionOptionsVisible = PanelType == ChatSessionPanelType.SessionOptions; + IsSystemInstructionVisible = SessionPanelType == ChatSessionPanelType.SystemInstruction; + IsSessionOptionsVisible = SessionPanelType == ChatSessionPanelType.SessionOptions; + } + + private void CheckGroupPanelType() + { + IsAgentsSectionVisible = GroupPanelType == ChatGroupPanelType.Agents; + IsGroupOptionsVisible = GroupPanelType == ChatGroupPanelType.GroupOptions; } private void OnHistorySessionsCountChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -110,9 +118,15 @@ partial void OnIsServiceSectionVisibleChanged(bool value) } } - partial void OnPanelTypeChanged(ChatSessionPanelType value) + partial void OnSessionPanelTypeChanged(ChatSessionPanelType value) { SettingsToolkit.WriteLocalSetting(SettingNames.ChatSessionPanelType, value); - CheckPanelType(); + CheckSessionPanelType(); + } + + partial void OnGroupPanelTypeChanged(ChatGroupPanelType value) + { + SettingsToolkit.WriteLocalSetting(SettingNames.ChatGroupPanelType, value); + CheckGroupPanelType(); } } From 35462903b59184f848783669a435c91596655039 Mon Sep 17 00:00:00 2001 From: Richasy Date: Thu, 27 Jun 2024 17:19:54 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BE=A4=E7=BB=84?= =?UTF-8?q?=E5=8A=A9=E7=90=86=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Core/RodelChat.Core/ChatClient.Helper.cs | 20 ++++-- src/Core/RodelChat.Core/ChatClient.cs | 12 +++- .../Controls/Chat/ChatGroupHistory.xaml | 11 ---- .../Controls/Chat/GroupAgentsPanel.xaml | 2 +- .../ChatSessionPresetItemControl.xaml | 64 +++++++++++-------- .../StorageService/StorageService.Chat.cs | 2 +- .../ChatGroupViewModel.Messages.cs | 24 ++++++- .../ChatGroupViewModel.Properties.cs | 2 +- .../ChatGroupViewModel/ChatGroupViewModel.cs | 20 ++++-- .../ChatPresetModuleViewModel.cs | 1 + .../ChatServicePageViewModel.Groups.cs | 14 +++- 11 files changed, 116 insertions(+), 56 deletions(-) diff --git a/src/Core/RodelChat.Core/ChatClient.Helper.cs b/src/Core/RodelChat.Core/ChatClient.Helper.cs index ee2e8007..467e5a4c 100644 --- a/src/Core/RodelChat.Core/ChatClient.Helper.cs +++ b/src/Core/RodelChat.Core/ChatClient.Helper.cs @@ -15,12 +15,6 @@ namespace RodelChat.Core; /// public sealed partial class ChatClient { - private static Microsoft.SemanticKernel.ChatMessageContent ConvertToKernelMessage(ChatMessage message) - { - var role = ConvertToRole(message.Role); - return new Microsoft.SemanticKernel.ChatMessageContent(role, ConvertToContentItemCollection(message.Content.ToArray())); - } - private static AuthorRole ConvertToRole(MessageRole role) => role switch { @@ -48,7 +42,7 @@ private static ChatMessageContentItemCollection ConvertToContentItemCollection(M return items; } - private static ChatHistory GetChatHistory(ChatSession session) + private ChatHistory GetChatHistory(ChatSession session) { var history = new ChatHistory(); if (!string.IsNullOrEmpty(session.SystemInstruction)) @@ -74,6 +68,18 @@ private static ChatHistory GetChatHistory(ChatSession session) return history; } + private Microsoft.SemanticKernel.ChatMessageContent ConvertToKernelMessage(ChatMessage message) + { + var role = ConvertToRole(message.Role); + var msg = new Microsoft.SemanticKernel.ChatMessageContent(role, ConvertToContentItemCollection(message.Content.ToArray())); + if (!string.IsNullOrEmpty(message.Author)) + { + msg.AuthorName = EncodeName(message.Author); + } + + return msg; + } + private PromptExecutionSettings GetExecutionSettings(ChatSession session) => GetProvider(session.Provider).ConvertExecutionSettings(session); diff --git a/src/Core/RodelChat.Core/ChatClient.cs b/src/Core/RodelChat.Core/ChatClient.cs index 8fa9f85f..3616c085 100644 --- a/src/Core/RodelChat.Core/ChatClient.cs +++ b/src/Core/RodelChat.Core/ChatClient.cs @@ -222,7 +222,17 @@ public async Task SendGroupMessageAsync(string groupId, ChatMessage message, Act TerminationStrategy = new CustomTerminationStrategy(group.MaxRounds, group.TerminateText), }, }; - groupChat.AddChatMessage(ConvertToKernelMessage(message)); + + if (message.Role == MessageRole.User) + { + group.Messages.Add(message); + } + + foreach (var item in group.Messages) + { + groupChat.AddChatMessage(ConvertToKernelMessage(item)); + } + await foreach (var content in groupChat.InvokeAsync(cancellationToken)) { var assistantName = DecodeName(content.AuthorName); diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml index 956dc2dc..3ad006c8 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/ChatGroupHistory.xaml @@ -39,17 +39,6 @@ - - - - + diff --git a/src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl/ChatSessionPresetItemControl.xaml b/src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl/ChatSessionPresetItemControl.xaml index e021de3b..188ccf65 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl/ChatSessionPresetItemControl.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Items/ChatSessionPresetItemControl/ChatSessionPresetItemControl.xaml @@ -55,34 +55,44 @@ - - - - - - - + + + + + + + + + + + diff --git a/src/Desktop/RodelAgent.UI/Extensions/StorageService/StorageService.Chat.cs b/src/Desktop/RodelAgent.UI/Extensions/StorageService/StorageService.Chat.cs index 9cec1373..12987bd5 100644 --- a/src/Desktop/RodelAgent.UI/Extensions/StorageService/StorageService.Chat.cs +++ b/src/Desktop/RodelAgent.UI/Extensions/StorageService/StorageService.Chat.cs @@ -218,7 +218,7 @@ private async Task InitializeChatGroupSessionsAsync() } } - _chatGroups = chatGroups; + _chatGroups = chatGroups.OrderByDescending(p => p.Messages?.LastOrDefault()?.Time ?? DateTimeOffset.MinValue).ToList(); } private async Task InitializeChatGroupPresetsAsync() diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs index adb1c61e..810834a8 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs @@ -74,6 +74,7 @@ private void CancelMessage() }); } + ResetAgentSelection(); RequestFocusInput?.Invoke(this, EventArgs.Empty); } @@ -115,6 +116,7 @@ private async Task SendMessageInternalAsync(bool addUserMsg = true) _currentGeneratingIndex = 0; ErrorText = string.Empty; UpdateGeneratingTip(); + UpdateAgentSelection(); var chatMessage = CreateUserMessage(); @@ -147,7 +149,7 @@ await _chatClient.SendGroupMessageAsync( var index = Agents.IndexOf(agent); if (index >= Agents.Count - 1) { - index = 0; + index = IsResponding ? 0 : _currentGeneratingIndex; } else { @@ -156,6 +158,7 @@ await _chatClient.SendGroupMessageAsync( _currentGeneratingIndex = index; UpdateGeneratingTip(); + UpdateAgentSelection(); } if (response.Role == MessageRole.Assistant) @@ -178,6 +181,7 @@ await _chatClient.SendGroupMessageAsync( selectedAgents, _cancellationTokenSource.Token); + ResetAgentSelection(); if (_cancellationTokenSource is null || _cancellationTokenSource.IsCancellationRequested) { return; @@ -191,11 +195,20 @@ await _chatClient.SendGroupMessageAsync( private void UpdateGeneratingTip() { - var currentAgent = Agents.Count > _currentGeneratingIndex ? Agents[_currentGeneratingIndex] : default; + var currentAgent = Agents.Count > _currentGeneratingIndex && _currentGeneratingIndex >= 0 ? Agents[_currentGeneratingIndex] : default; var name = currentAgent?.Name ?? string.Empty; GeneratingTipText = $"{name} {ResourceToolkit.GetLocalizedString(StringNames.Generating)}".Trim(); } + private void UpdateAgentSelection() + { + for (var i = 0; i < Agents.Count; i++) + { + var agent = Agents[i]; + agent.IsSelected = i == _currentGeneratingIndex; + } + } + private Task EditMessageAsync(ChatMessage msg) => SaveSessionToDatabaseAsync(); @@ -213,5 +226,12 @@ private void HandleSendMessageException(Exception ex) ErrorText = ex.Message; _logger.LogDebug(ex, "Failed to send message."); CancelMessage(); + ResetAgentSelection(); + } + + private void ResetAgentSelection() + { + _currentGeneratingIndex = -1; + UpdateAgentSelection(); } } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs index 84f647f5..748b256c 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Properties.cs @@ -18,7 +18,7 @@ public sealed partial class ChatGroupViewModel private readonly IStorageService _storageService; private readonly ILogger _logger; private CancellationTokenSource _cancellationTokenSource; - private int _currentGeneratingIndex; + private int _currentGeneratingIndex = -1; [ObservableProperty] private string _title; diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs index 7393d976..4e8bcdcd 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs @@ -30,17 +30,19 @@ public ChatGroupViewModel(ChatGroup data, IChatClient chatClient) _storageService = GlobalDependencies.ServiceProvider.GetService(); IsEnterSend = SettingsToolkit.ReadLocalSetting(SettingNames.ChatServicePageIsEnterSend, true); Messages.CollectionChanged += OnMessageCountChanged; - Initialize(data); + InitializeCommand.Execute(data); AttachIsRunningToAsyncCommand(p => IsResponding = p, SendCommand); AttachExceptionHandlerToAsyncCommand(HandleSendMessageException, SendCommand); } - private void Initialize(ChatGroup data) + [RelayCommand] + private async Task InitializeAsync(ChatGroup data) { GroupName = data.Name; Title = data.Title ?? ResourceToolkit.GetLocalizedString(StringNames.RandomChat); MaxRounds = data.MaxRounds; + await InitializeAgentsCommand.ExecuteAsync(default); if (data.TerminateText != null && data.TerminateText.Count > 0) { @@ -60,14 +62,22 @@ private void Initialize(ChatGroup data) } var vm = new ChatMessageItemViewModel(message, EditMessageAsync, DeleteMessageAsync); + + var agent = Agents.FirstOrDefault(p => p.Name == message.Author); + if (agent is not null) + { + vm.Author = agent.Name; + vm.AgentId = agent.Data.Id; + } + Messages.Add(vm); } } - InitializeAgentsCommand.Execute(default); CheckChatEmpty(); CheckLastMessageTime(); RequestFocusInput?.Invoke(this, EventArgs.Empty); + RequestScrollToBottom?.Invoke(this, EventArgs.Empty); } [RelayCommand] @@ -128,7 +138,7 @@ private async Task InitializeAgentsAsync() { Agents.Clear(); var storageService = GlobalDependencies.ServiceProvider.GetRequiredService(); - await Task.Delay(200); + await Task.Delay(500); var agents = await storageService.GetChatAgentsAsync(); foreach (var agentId in Data.Agents) { @@ -141,6 +151,8 @@ private async Task InitializeAgentsAsync() var a = new ChatPresetItemViewModel(agent); Agents.Add(a); } + + UpdateAgentSelection(); } private void CheckChatEmpty() diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatPresetModuleViewModel/ChatPresetModuleViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatPresetModuleViewModel/ChatPresetModuleViewModel.cs index 7f3217ee..32fc255f 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatPresetModuleViewModel/ChatPresetModuleViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatPresetModuleViewModel/ChatPresetModuleViewModel.cs @@ -116,6 +116,7 @@ private async Task SavePresetInternalAsync(ChatSessionPresetType type) case ChatSessionPresetType.Agent: await _storageService.AddOrUpdateChatAgentAsync(Data.Data); pageVM.ResetAgentsCommand.Execute(default); + pageVM.ReloadGroupAgentsCommand.Execute(Data.Data.Id); break; case ChatSessionPresetType.Session: await _storageService.AddOrUpdateChatSessionPresetAsync(Data.Data); diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs index 575ed0b5..510258d4 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Pages/ChatServicePageViewModel/ChatServicePageViewModel.Groups.cs @@ -168,7 +168,7 @@ private void CheckCurrentGroupExist(ChatGroupViewModel vm) [RelayCommand] private void SetSelectedGroupSession(ChatGroupViewModel groupVM) { - foreach (var item in HistoryChatSessions) + foreach (var item in HistoryGroupSessions) { item.IsSelected = groupVM != null && item.Equals(groupVM); } @@ -211,4 +211,16 @@ private async Task RemoveAllGroupsAsync() HistoryGroupSessions.Clear(); CreateNewSessionCommand.Execute(default); } + + [RelayCommand] + private void ReloadGroupAgents(string agentId) + { + foreach (var group in HistoryGroupSessions) + { + if (group.Agents.Any(p => p.Data.Id == agentId)) + { + group.InitializeAgentsCommand.Execute(default); + } + } + } } From b39c7ad51fd8a4aef15c51cc68b7c07519205c49 Mon Sep 17 00:00:00 2001 From: Richasy Date: Thu, 27 Jun 2024 22:59:46 +0800 Subject: [PATCH 09/13] Add doc --- docs/assets/zh/create-group-item.png | Bin 0 -> 32793 bytes docs/assets/zh/group-dialog.png | Bin 0 -> 94177 bytes docs/group-chat.md | 70 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 docs/assets/zh/create-group-item.png create mode 100644 docs/assets/zh/group-dialog.png create mode 100644 docs/group-chat.md diff --git a/docs/assets/zh/create-group-item.png b/docs/assets/zh/create-group-item.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1240d59039d636b73124d0903a45c52c630408 GIT binary patch literal 32793 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�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 F{eK&E*X{rS literal 0 HcmV?d00001 diff --git a/docs/assets/zh/group-dialog.png b/docs/assets/zh/group-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..10301a7af8d707aa11946f8dc52339d99a3dd753 GIT binary patch literal 94177 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 G^nU<*XDyHb literal 0 HcmV?d00001 diff --git a/docs/group-chat.md b/docs/group-chat.md new file mode 100644 index 00000000..618098c3 --- /dev/null +++ b/docs/group-chat.md @@ -0,0 +1,70 @@ +# 群组聊天(实验) + +> [!TIP] +> 群组聊天是一项实验性功能,在 `2.2307.1.0` 中开始提供,目前并不是所有模型都能适用,具体的限制条件请查看 [限制条件](#限制条件) + +我们知道,模型是可以扮演角色执行特定任务的,那么你是否想过,让不同的模型扮演不同的角色,然后让它们协同工作,解决目标问题呢? + +群组聊天就是对这个方向的探索。 + +## 前置条件 + +既然是群组聊天,那你就需要先创建“群员”。 + +这个群员就是 **助理** ,你需要先按照 [助理与预设](./agent-preset) 中的指引创建两个以上的助理,然后才能把它们归集到一个群组中。 + +## 限制条件 + +1. 有些模型仅支持一对一(其消息队列严格要求一条 AI 信息,一条用户信息交替),这类模型(比如文心一言)是不受群组支持的,因为群组内一次会产生多条助理信息。 +2. 不同模型的上下文窗口不同,请尽量把上下文窗口小的模型放在群成员前列。 +3. 群组成员按照先后顺序发言,用户只能在每轮发言结束后调整目标,而不能中途插入(但可以中途取消)。 + +## 简单示例 + +### 创建助理 + +接下来,我们可以创建三个助理,来帮助我们生成一个 HTML 的计算器。(取自 [Step by Step guide to develop AI Multi-Agent system using Microsoft Semantic Kernel and GPT-4o](https://medium.com/@akshaykokane09/step-by-step-guide-to-develop-ai-multi-agent-system-using-microsoft-semantic-kernel-and-gpt-4o-f5991af40ea6)) + +|助理名称|产品经理| +|-|-| +|指令|You are a program manager which will take the requirement and create a plan for creating app. Program Manager understands the user requirements and form the detail documents with requirements and costing.| + +|助理名称|软件工程师| +|-|-| +|指令|You are Software Engieer, and your goal is develop web app using HTML and JavaScript (JS) by taking into consideration all the requirements given by Program Manager.| + +|助理名称|项目经理| +|-|-| +|指令|You are manager which will review software engineer code, and make sure all client requirements are completed. Once all client requirements are completed, you can approve the request by just responding "approve"| + +创建助理时,你可以随意设定其模型,只要该模型符合 [限制条件](#限制条件) 即可。 + +### 创建群组 + +
+ +![创建群组](./assets/zh/create-group-item.png) + +
+ +在聊天界面左侧面板顶部的溢出菜单中选择 `创建群组`。 + +在弹出的群组面板中进行基本的配置。 + +![群组面板](./assets/zh/group-dialog.png) + +除了选择助理外,你会注意到还有两个参数: + +1. **终止文本** + 这是一个 `刹车片`,当 AI 生成的文本中出现你设定的终止文本后,应用会判断目标达成,然后终止当前的群组对话。在当前示例中,项目经理最终会判断目标是否达成,并给出 `approve` 的判定,所以我们这里的终止文本就是 `approve`。 +2. **最大会话轮次** + 所有助理依次生成完内容记作一轮,AI 会根据你设定的目标进行一轮又一轮的磋商,为了避免我们的 token 管理失控,你可以设定最大会话轮次,达到该轮次,则强行中断 AI 们的讨论。根据我们的预设,考虑到项目经理可能提出修改意见,这里我们可以把最大轮次设置为5。 + +> [!IMPORTANT] +> 目前群组会话会按照你设置的助理顺序从上往下依次调用,所以在创建群组时请规划好执行顺序。 + +### 开始对话 + +创建群组后,你就能在聊天界面的左侧导航面板中找到你创建的群组了。 + +点击即可开始对话。 \ No newline at end of file From e22cd1307b7da698a260c2cc34ee16f128e5343b Mon Sep 17 00:00:00 2001 From: Anran Zhang Date: Fri, 28 Jun 2024 10:48:04 +0800 Subject: [PATCH 10/13] Update --- docs/assets/en/create-group-item.png | Bin 0 -> 11953 bytes docs/assets/en/group-dialog.png | Bin 0 -> 39845 bytes docs/en/group-chat.md | 80 ++++++++++++++++++ docs/group-chat.md | 12 ++- src/Core/RodelChat.Models/Client/ChatGroup.cs | 1 - .../Controls/Chat/PresetGroupPanel.xaml | 17 +++- 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 docs/assets/en/create-group-item.png create mode 100644 docs/assets/en/group-dialog.png create mode 100644 docs/en/group-chat.md diff --git a/docs/assets/en/create-group-item.png b/docs/assets/en/create-group-item.png new file mode 100644 index 0000000000000000000000000000000000000000..4768be1e5092b701cb8cc1f01f7f677adfbf48e5 GIT binary patch literal 11953 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

A@$0w4|OYu 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 E4>Mg2ApigX literal 0 HcmV?d00001 diff --git a/docs/assets/en/group-dialog.png b/docs/assets/en/group-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..03d612c127cc33132de8420e3c9a3027dd2d7f33 GIT binary patch literal 39845 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 [!TIP] +> Group chat is an experimental feature that became available in version `2.2307.1.0`. Currently, it is not applicable to all models. For specific limitations, please refer to [Limitations](#limitations). + +We know that models can take on specific roles to perform certain tasks. Have you ever thought about letting different models play different roles and then working together to solve target problems? + +Group chat is an exploration in this direction. + +## Prerequisites + +Since it's a group chat, you need to create "group members" first. + +These group members are **agents**. You need to create two or more agents according to the guidelines in [Agent and Preset](./agent-preset) before you can gather them into a group. + +## Limitations + +1. Some models only support one-on-one interactions (their message queue strictly requires an alternating sequence of one AI message and one user message). Such models (e.g., ERNIE `QianFan`) do not support group chats because multiple agent messages are generated at once in a group. +2. Different models have different context windows. Please try to place models with smaller context windows at the front of the group members list. +3. Group members speak in order, and users can only adjust the target after each round of speaking but cannot interrupt in the middle (though they can cancel in the middle). + +## Simple Example + +### Creating agents + +Next, we can create three agents to help us generate an HTML calculator (taken from [Step by Step guide to develop AI Multi-Agent system using Microsoft Semantic Kernel and GPT-4](https://medium.com/@akshaykokane09/step-by-step-guide-to-develop-ai-multi-agent-system-using-microsoft-semantic-kernel-and-gpt-4o-f5991af40ea6)). + +| Agent Name | Program Manager | +| -------------- | ---------------- | +| Instruction | You are a program manager which will take the requirement and create a plan for creating an app. The Program Manager understands the user requirements and forms the detailed documents with requirements and costing. | + +| Agent Name | Software Engineer | +| -------------- | ----------------- | +| Instruction | You are a Software Engineer, and your goal is to develop a web app using HTML and JavaScript (JS) by considering all the requirements given by the Program Manager. | + +| Agent Name | Manager | +| -------------- | --------------- | +| Instruction | You are a manager who will review the software engineer's code and make sure all client requirements are completed. Once all client requirements are completed, you can approve the request by just responding "approve". | + +When creating agents, you can set their models freely as long as the models meet the [Limitations](#limitations). + +### Creating a Group + +

+ +![Creating a Group](./assets/en/create-group-item.png) + +
+ +Select `Create Group` from the overflow menu at the top of the left panel in the chat interface. + +Configure the basic settings in the group panel that pops up. + +![Group Panel](./assets/en/group-dialog.png) + +Besides choosing agents, you'll notice two other parameters: + +1. **Termination Text** + This is a `brake pad`. When the AI-generated text contains the termination text you set, the application will judge that the goal has been achieved and terminate the current group conversation. In this example, the project manager will ultimately determine if the goal has been achieved and give the "approve" verdict, so our termination text here is `approve`. +2. **Maximum Session Rounds** + A complete round is considered after all agents generate their content once. AI will negotiate for one round after another based on your set goal. To avoid token management getting out of control, you can set a maximum session round. Once it reaches this round, it will forcibly interrupt the AI discussion. Based on our preset, considering that the project manager might suggest modifications, we can set the maximum rounds to 5. + +> [!IMPORTANT] +> Currently, group sessions will call agents sequentially from top to bottom according to the order you set. So, please plan the execution order when creating the group. + +### Start the Conversation + +Once the group is created, you can find it in the left navigation panel of the chat interface. + +Click to start the conversation. + +In the input box, you can enter the following: + +```text +I want to develop a calculator app. It should have a basic calculator appearance and get final approval from the project manager. +``` + +Then, you will see the `Product Manager`, `Software Engineer`, and `Project Manager` speak in turn, ultimately generating code that includes `HTML`, `CSS`, and `JavaScript`. + +This is the charm of AI groups. \ No newline at end of file diff --git a/docs/group-chat.md b/docs/group-chat.md index 618098c3..dde1be75 100644 --- a/docs/group-chat.md +++ b/docs/group-chat.md @@ -67,4 +67,14 @@ 创建群组后,你就能在聊天界面的左侧导航面板中找到你创建的群组了。 -点击即可开始对话。 \ No newline at end of file +点击即可开始对话。 + +在输入框中,我们可以输入以下内容: + +```text +我想开发一个计算器应用。具备基础的计算器外观,并从项目经理处获得最终许可。 +``` + +然后,你就可以看到 `产品经理`,`软件工程师` 和 `项目经理` 依次发言,最终生成了一份包含 `HTML`, `CSS` 和 `Javascript` 的代码。 + +这就是 AI 群组的魅力。 \ No newline at end of file diff --git a/src/Core/RodelChat.Models/Client/ChatGroup.cs b/src/Core/RodelChat.Models/Client/ChatGroup.cs index 2defd42c..3495a404 100644 --- a/src/Core/RodelChat.Models/Client/ChatGroup.cs +++ b/src/Core/RodelChat.Models/Client/ChatGroup.cs @@ -39,7 +39,6 @@ public static ChatGroup CreateGroup(string id, ChatGroupPreset preset) return new ChatGroup { Id = id, - Title = preset.Name, PresetId = preset.Id, Messages = new List(), Agents = preset.Agents, diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml index 42ad5e8d..f6a56eef 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml @@ -233,11 +233,20 @@ + VerticalAlignment="Stretch"> + + Date: Fri, 28 Jun 2024 10:57:38 +0800 Subject: [PATCH 11/13] Update --- src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml b/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml index f6a56eef..23e7ab74 100644 --- a/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml +++ b/src/Desktop/RodelAgent.UI/Controls/Chat/PresetGroupPanel.xaml @@ -246,7 +246,6 @@ Date: Fri, 28 Jun 2024 11:35:02 +0800 Subject: [PATCH 12/13] Update --- docs/en/group-chat.md | 4 ++-- src/Core/RodelChat.Core/ChatClient.cs | 2 ++ src/Core/RodelChat.Models/Client/ChatMessage.cs | 8 +++++++- src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs | 4 ++-- .../ChatGroupViewModel/ChatGroupViewModel.Messages.cs | 7 ++----- .../Components/ChatGroupViewModel/ChatGroupViewModel.cs | 6 ++---- .../ViewModels/Items/ChatMessageItemViewModel.cs | 2 ++ 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/en/group-chat.md b/docs/en/group-chat.md index eb88505c..73bc8437 100644 --- a/docs/en/group-chat.md +++ b/docs/en/group-chat.md @@ -43,7 +43,7 @@ When creating agents, you can set their models freely as long as the models meet
-![Creating a Group](./assets/en/create-group-item.png) +![Creating a Group](../assets/en/create-group-item.png)
@@ -51,7 +51,7 @@ Select `Create Group` from the overflow menu at the top of the left panel in the Configure the basic settings in the group panel that pops up. -![Group Panel](./assets/en/group-dialog.png) +![Group Panel](../assets/en/group-dialog.png) Besides choosing agents, you'll notice two other parameters: diff --git a/src/Core/RodelChat.Core/ChatClient.cs b/src/Core/RodelChat.Core/ChatClient.cs index 3616c085..2e37ae60 100644 --- a/src/Core/RodelChat.Core/ChatClient.cs +++ b/src/Core/RodelChat.Core/ChatClient.cs @@ -236,9 +236,11 @@ public async Task SendGroupMessageAsync(string groupId, ChatMessage message, Act await foreach (var content in groupChat.InvokeAsync(cancellationToken)) { var assistantName = DecodeName(content.AuthorName); + var agent = agents.FirstOrDefault(p => p.Name == assistantName); var msg = ChatMessage.CreateAssistantMessage(content.Content); msg.Time = DateTimeOffset.Now; msg.Author = assistantName; + msg.AuthorId = agent?.Id ?? string.Empty; group.Messages.Add(msg); messageAction?.Invoke(msg); } diff --git a/src/Core/RodelChat.Models/Client/ChatMessage.cs b/src/Core/RodelChat.Models/Client/ChatMessage.cs index a696b70c..00347543 100644 --- a/src/Core/RodelChat.Models/Client/ChatMessage.cs +++ b/src/Core/RodelChat.Models/Client/ChatMessage.cs @@ -24,7 +24,13 @@ public sealed class ChatMessage /// 发送者名称. /// [JsonPropertyName("author")] - public string Author { get; set; } + public string? Author { get; set; } + + /// + /// 发送者 ID. + /// + [JsonPropertyName("author_id")] + public string? AuthorId { get; set; } ///
/// 消息内容. diff --git a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs index 5b7d5b8d..02918d33 100644 --- a/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs +++ b/src/Desktop/RodelAgent.UI/Pages/ChatServicePage.xaml.cs @@ -23,7 +23,7 @@ public ChatServicePage() } /// - protected override void OnPageLoaded() + protected override async void OnPageLoaded() { if (ViewModel.IsAvailableServicesEmpty) { @@ -32,7 +32,7 @@ protected override void OnPageLoaded() if (ViewModel.IsAgentsEmpty) { - ViewModel.ResetAgentsCommand.Execute(default); + await ViewModel.ResetAgentsCommand.ExecuteAsync(default); } if (ViewModel.IsSessionPresetsEmpty) diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs index 810834a8..2d2273e7 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.Messages.cs @@ -153,7 +153,7 @@ await _chatClient.SendGroupMessageAsync( } else { - index++; + index = IsResponding ? index + 1 : _currentGeneratingIndex; } _currentGeneratingIndex = index; @@ -167,8 +167,6 @@ await _chatClient.SendGroupMessageAsync( response, EditMessageAsync, DeleteMessageAsync); - msg.Author = name; - msg.AgentId = agent?.Data.Id; Messages.Add(msg); } else if (response.Role == MessageRole.Client) @@ -181,14 +179,13 @@ await _chatClient.SendGroupMessageAsync( selectedAgents, _cancellationTokenSource.Token); - ResetAgentSelection(); if (_cancellationTokenSource is null || _cancellationTokenSource.IsCancellationRequested) { return; } await SaveSessionToDatabaseAsync(); - + ResetAgentSelection(); RequestFocusInput?.Invoke(this, EventArgs.Empty); _cancellationTokenSource = null; } diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs index 4e8bcdcd..b2837058 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Components/ChatGroupViewModel/ChatGroupViewModel.cs @@ -63,11 +63,10 @@ private async Task InitializeAsync(ChatGroup data) var vm = new ChatMessageItemViewModel(message, EditMessageAsync, DeleteMessageAsync); - var agent = Agents.FirstOrDefault(p => p.Name == message.Author); - if (agent is not null) + var agent = Agents.FirstOrDefault(p => p.Data.Id == message.AuthorId); + if (agent is not null && agent.Name != vm.Author) { vm.Author = agent.Name; - vm.AgentId = agent.Data.Id; } Messages.Add(vm); @@ -138,7 +137,6 @@ private async Task InitializeAgentsAsync() { Agents.Clear(); var storageService = GlobalDependencies.ServiceProvider.GetRequiredService(); - await Task.Delay(500); var agents = await storageService.GetChatAgentsAsync(); foreach (var agentId in Data.Agents) { diff --git a/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs b/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs index cae4ec10..2a31f9e1 100644 --- a/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs +++ b/src/Desktop/RodelAgent.UI/ViewModels/Items/ChatMessageItemViewModel.cs @@ -45,6 +45,8 @@ public ChatMessageItemViewModel( Func deleteFunc) : base(message) { + AgentId = message.AuthorId ?? string.Empty; + Author = message.Author ?? string.Empty; Content = message.Content.FirstOrDefault(p => p.Type == ChatContentType.Text)?.Text ?? string.Empty; IsAssistant = message.Role == MessageRole.Assistant; IsUser = message.Role == MessageRole.User; From 1cccb0040d5bd5b68f6a00adc30584bc4e21f42f Mon Sep 17 00:00:00 2001 From: Anran Zhang Date: Fri, 28 Jun 2024 12:17:00 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ README.zh-CN.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 689398f9..7dd6e629 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A Windows desktop application that integrates chat, text-to-image, text-to-speech, and machine translation, supports the current mainstream AI services, and offers an excellent desktop AI experience. + + English · [简体中文](./README.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 14c0cb2e..8e413698 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,6 +8,8 @@ 集聊天、文生图、文本转语音、机器翻译于一身的 Windows 桌面应用,支持目前主流的 AI 服务,提供优秀的桌面 AI 体验。 + + [English](./README.md) · 简体中文