diff --git a/Directory.Build.props b/Directory.Build.props index 99a8ba4c..068b6e13 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,8 +2,8 @@ net8.0 10.0 - 1.3.1 - false + 1.4.0 + true false \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs index 9c27d7ff..272abf0d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs @@ -3,7 +3,8 @@ namespace BotSharp.Abstraction.Files; public interface IBotSharpFileService { string GetDirectory(string conversationId); - IEnumerable GetConversationFiles(string conversationId, string messageId); + IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2); + IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false); string? GetMessageFile(string conversationId, string messageId, string fileName); void SaveMessageFiles(string conversationId, string messageId, List files); @@ -17,4 +18,11 @@ public interface IBotSharpFileService /// bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null); bool DeleteConversationFiles(IEnumerable conversationIds); + + /// + /// Get file bytes and content type from data, e.g., "data:image/png;base64,aaaaaaaaa" + /// + /// + /// + (string, byte[]) GetFileInfoFromData(string data); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs index f679d52e..de226f58 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/BotSharpFile.cs @@ -4,14 +4,14 @@ namespace BotSharp.Abstraction.Files.Models; public class BotSharpFile { [JsonPropertyName("file_name")] - public string FileName { get; set; } + public string FileName { get; set; } = string.Empty; + /// + /// File data, e.g., "data:image/png;base64,aaaaaaaa" + /// [JsonPropertyName("file_data")] - public string FileData { get; set; } + public string FileData { get; set; } = string.Empty; - [JsonPropertyName("content_type")] - public string ContentType { get; set; } - - [JsonPropertyName("file_size")] - public int FileSize { get; set; } + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } = string.Empty; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs new file mode 100644 index 00000000..3ec63fd8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/MessageFileModel.cs @@ -0,0 +1,32 @@ +namespace BotSharp.Abstraction.Files.Models; + +public class MessageFileModel +{ + [JsonPropertyName("message_id")] + public string MessageId { get; set; } + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_storage_url")] + public string FileStorageUrl { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + public MessageFileModel() + { + + } + + public override string ToString() + { + return $"File name: {FileName}, File type: {FileType}, Content type: {ContentType}"; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/Models/OutputFileModel.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/OutputFileModel.cs deleted file mode 100644 index 962b01e1..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Models/OutputFileModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace BotSharp.Abstraction.Files.Models; - -public class OutputFileModel -{ - [JsonPropertyName("file_url")] - public string FileUrl { get; set; } - - [JsonPropertyName("file_name")] - public string FileName { get; set; } - - [JsonPropertyName("file_type")] - public string FileType { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs index 75fd60e6..20762fe0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/ILlmProviderService.cs @@ -6,6 +6,6 @@ public interface ILlmProviderService { LlmModelSetting GetSetting(string provider, string model); List GetProviders(); - LlmModelSetting GetProviderModel(string provider, string id); + LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null); List GetProviderModels(string provider); } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs index 1faf3c52..b86578fe 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/Settings/LlmModelSetting.cs @@ -27,6 +27,11 @@ public class LlmModelSetting public string Endpoint { get; set; } public LlmModelType Type { get; set; } = LlmModelType.Chat; + /// + /// If true, allow sending images/vidoes to this model + /// + public bool MultiModal { get; set; } + /// /// Prompt cost per 1K token /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs new file mode 100644 index 00000000..6897ca42 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationInput.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Translation.Models; + +public class TranslationInput +{ + [JsonPropertyName("id")] + public int Id { get; set; } = -1; + + [JsonPropertyName("text")] + public string Text { get; set; } = null!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs index 52ad54ec..b15bfef4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationOutput.cs @@ -9,5 +9,5 @@ public class TranslationOutput public string OutputLanguage { get; set; } = LanguageType.ENGLISH; [JsonPropertyName("texts")] - public string[] Texts { get; set; } = Array.Empty(); + public TranslationInput[] Texts { get; set; } = Array.Empty(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 35d74aa4..debf68f4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -6,6 +6,6 @@ public interface IUserService { Task GetUser(string id); Task CreateUser(User user); - Task GetToken(string authorization); + Task GetToken(string authorization); Task GetMyProfile(); } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 8d05c40b..0ddc52e8 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -159,6 +159,7 @@ + diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs index ad687274..d7e961be 100644 --- a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.StaticFiles; using System.IO; using System.Threading; @@ -7,16 +8,22 @@ public class BotSharpFileService : IBotSharpFileService { private readonly BotSharpDatabaseSettings _dbSettings; private readonly IServiceProvider _services; + private readonly ILogger _logger; private readonly string _baseDir; + private readonly IEnumerable _allowedTypes = new List { "image/png", "image/jpeg" }; private const string CONVERSATION_FOLDER = "conversations"; private const string FILE_FOLDER = "files"; + private const int MIN_OFFSET = 1; + private const int MAX_OFFSET = 5; public BotSharpFileService( BotSharpDatabaseSettings dbSettings, + ILogger logger, IServiceProvider services) { _dbSettings = dbSettings; + _logger = logger; _services = services; _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository); } @@ -31,29 +38,67 @@ public class BotSharpFileService : IBotSharpFileService return dir; } - public IEnumerable GetConversationFiles(string conversationId, string messageId) + public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2) { - var outputFiles = new List(); - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) + var files = new List(); + if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) { - return outputFiles; + return files; } - foreach (var file in Directory.GetFiles(dir)) + if (offset <= 0) { - var fileName = Path.GetFileNameWithoutExtension(file); - var extension = Path.GetExtension(file); - var fileType = extension.Substring(1); - var model = new OutputFileModel() - { - FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", - FileName = fileName, - FileType = fileType - }; - outputFiles.Add(model); + offset = MIN_OFFSET; } - return outputFiles; + else if (offset > MAX_OFFSET) + { + offset = MAX_OFFSET; + } + + var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); + files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); + return files; + } + + public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) + { + var files = new List(); + if (messageIds.IsNullOrEmpty()) return files; + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (string.IsNullOrEmpty(dir)) + { + continue; + } + + foreach (var file in Directory.GetFiles(dir)) + { + var contentType = GetFileContentType(file); + if (imageOnly && !_allowedTypes.Contains(contentType)) + { + continue; + } + + var fileName = Path.GetFileNameWithoutExtension(file); + var extension = Path.GetExtension(file); + var fileType = extension.Substring(1); + + var model = new MessageFileModel() + { + MessageId = messageId, + FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", + FileStorageUrl = file, + FileName = fileName, + FileType = fileType, + ContentType = contentType + }; + files.Add(model); + } + } + + return files; } public string? GetMessageFile(string conversationId, string messageId, string fileName) @@ -75,19 +120,26 @@ public class BotSharpFileService : IBotSharpFileService var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); if (string.IsNullOrEmpty(dir)) return; - for (int i = 0; i < files.Count; i++) + try { - var file = files[i]; - if (string.IsNullOrEmpty(file.FileData)) + for (int i = 0; i < files.Count; i++) { - continue; - } + var file = files[i]; + if (string.IsNullOrEmpty(file.FileData)) + { + continue; + } - var bytes = GetFileBytes(file.FileData); - var fileType = Path.GetExtension(file.FileName); - var fileName = $"{i + 1}{fileType}"; - Thread.Sleep(100); - File.WriteAllBytes(Path.Combine(dir, fileName), bytes); + var (_, bytes) = GetFileInfoFromData(file.FileData); + var fileType = Path.GetExtension(file.FileName); + var fileName = $"{i + 1}{fileType}"; + Thread.Sleep(100); + File.WriteAllBytes(Path.Combine(dir, fileName), bytes); + } + } + catch (Exception ex) + { + _logger.LogError($"Error when saving conversation files: {ex.Message}"); } } @@ -137,6 +189,23 @@ public class BotSharpFileService : IBotSharpFileService return true; } + public (string, byte[]) GetFileInfoFromData(string data) + { + if (string.IsNullOrEmpty(data)) + { + return (string.Empty, new byte[0]); + } + + var typeStartIdx = data.IndexOf(':'); + var typeEndIdx = data.IndexOf(';'); + var contentType = data.Substring(typeStartIdx + 1, typeEndIdx - typeStartIdx - 1); + + var base64startIdx = data.IndexOf(','); + var base64Str = data.Substring(base64startIdx + 1); + + return (contentType, Convert.FromBase64String(base64Str)); + } + #region Private methods private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) { @@ -170,54 +239,16 @@ public class BotSharpFileService : IBotSharpFileService return dir; } - private byte[] GetFileBytes(string data) + private string GetFileContentType(string filePath) { - if (string.IsNullOrEmpty(data)) + string contentType; + var provider = new FileExtensionContentTypeProvider(); + if (!provider.TryGetContentType(filePath, out contentType)) { - return new byte[0]; + contentType = string.Empty; } - var startIdx = data.IndexOf(','); - var base64Str = data.Substring(startIdx + 1); - return Convert.FromBase64String(base64Str); - } - - private string GetFileType(string data) - { - if (string.IsNullOrEmpty(data)) - { - return string.Empty; - } - - var startIdx = data.IndexOf(':'); - var endIdx = data.IndexOf(';'); - var fileType = data.Substring(startIdx + 1, endIdx - startIdx - 1); - return fileType; - } - - private string ParseFileFormat(string type) - { - var parsed = string.Empty; - switch (type) - { - case "image/png": - parsed = ".png"; - break; - case "image/jpeg": - case "image/jpg": - parsed = ".jpeg"; - break; - case "application/pdf": - parsed = ".pdf"; - break; - case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": - parsed = ".xlsx"; - break; - case "text/plain": - parsed = ".txt"; - break; - } - return parsed; + return contentType; } #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs index c55ed9a8..4655b1de 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/CompletionProvider.cs @@ -35,10 +35,13 @@ public class CompletionProvider public static IChatCompletion GetChatCompletion(IServiceProvider services, string? provider = null, string? model = null, + string? modelId = null, + bool? multiModal = null, AgentLlmConfig? agentConfig = null) { var completions = services.GetServices(); - (provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig); + (provider, model) = GetProviderAndModel(services, provider: provider, model: model, modelId: modelId, + multiModal: multiModal, agentConfig: agentConfig); var completer = completions.FirstOrDefault(x => x.Provider == provider); if (completer == null) @@ -47,7 +50,7 @@ public class CompletionProvider logger.LogError($"Can't resolve completion provider by {provider}"); } - completer.SetModelName(model); + completer?.SetModelName(model); return completer; } @@ -55,6 +58,8 @@ public class CompletionProvider private static (string, string) GetProviderAndModel(IServiceProvider services, string? provider = null, string? model = null, + string? modelId = null, + bool? multiModal = null, AgentLlmConfig? agentConfig = null) { var agentSetting = services.GetRequiredService(); @@ -73,11 +78,11 @@ public class CompletionProvider { model = state.GetState("model", model ?? "gpt-35-turbo-4k"); } - else if (state.ContainsState("model_id")) + else if (state.ContainsState("model_id") || !string.IsNullOrEmpty(modelId)) { - var modelId = state.GetState("model_id"); + var modelIdentity = state.ContainsState("model_id") ? state.GetState("model_id") : modelId; var llmProviderService = services.GetRequiredService(); - model = llmProviderService.GetProviderModel(provider, modelId)?.Name; + model = llmProviderService.GetProviderModel(provider, modelIdentity, multiModal: multiModal)?.Name; } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs index eb92ac51..8320bdb7 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LlmProviderService.cs @@ -44,11 +44,15 @@ public class LlmProviderService : ILlmProviderService ?.Models ?? new List(); } - public LlmModelSetting GetProviderModel(string provider, string id) + public LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null) { var models = GetProviderModels(provider) - .Where(x => x.Id == id) - .ToList(); + .Where(x => x.Id == id); + + if (multiModal.HasValue) + { + models = models.Where(x => x.MultiModal == multiModal); + } var random = new Random(); var index = random.Next(0, models.Count()); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 6bdac0ef..6046c5bb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -17,18 +17,18 @@ public partial class RoutingService return false; } - var provide = agent.LlmConfig.Provider; + var provider = agent.LlmConfig.Provider; var model = agent.LlmConfig.Model; - if (provide == null || model == null) + if (provider == null || model == null) { var agentSettings = _services.GetRequiredService(); - provide = agentSettings.LlmConfig.Provider; + provider = agentSettings.LlmConfig.Provider; model = agentSettings.LlmConfig.Model; } var chatCompletion = CompletionProvider.GetChatCompletion(_services, - provider: provide, + provider: provider, model: model); var message = dialogs.Last(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 5e0ca8ac..1a9ce427 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -88,7 +88,7 @@ public partial class RoutingService : IRoutingService { var translator = _services.GetRequiredService(); - var language = states.GetState(StateConst.LANGUAGE, LanguageType.UNKNOWN); + var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH); if (language != LanguageType.ENGLISH) { message.SecondaryContent = message.Content; diff --git a/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs b/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs index 5119bc91..463013bf 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/ResponseTemplateService.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; using System.Reflection; diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index 3bdbf7e1..33b27177 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -3,6 +3,7 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; +using BotSharp.Abstraction.Translation.Models; using Fluid; namespace BotSharp.Core.Templating; @@ -30,6 +31,7 @@ public class TemplateRender : ITemplateRender _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); + _options.MemberAccessStrategy.Register(); } public string Render(string template, Dictionary dict) diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs index d24fd7d4..97e997c7 100644 --- a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs @@ -6,6 +6,7 @@ using BotSharp.Abstraction.Templating; using BotSharp.Abstraction.Translation.Models; using System.Collections; using System.Reflection; +using System.Text.Encodings.Web; namespace BotSharp.Core.Translation; @@ -57,12 +58,23 @@ public class TranslationService : ITranslationService var keys = unique.ToArray(); var texts = unique.ToArray() - .Select((text, i) => $"{i + 1}. \"{text}\"") - .ToList(); - var translatedStringList = await InnerTranslate(texts, language, template); + .Select((text, i) => new TranslationInput + { + Id = i + 1, + Text = text + }).ToList(); try { + var translatedStringList = await InnerTranslate(texts, language, template); + + int retry = 0; + while (translatedStringList.Texts.Length != texts.Count && retry < 3) + { + translatedStringList = await InnerTranslate(texts, language, template); + retry++; + } + // Override language if it's Unknown, it's used to output the corresponding language. var states = _services.GetRequiredService(); if (!states.ContainsState(StateConst.LANGUAGE)) @@ -76,7 +88,7 @@ public class TranslationService : ITranslationService for (var i = 0; i < texts.Count; i++) { - map[keys[i]] = translatedTexts[i]; + map[keys[i]] = translatedTexts[i].Text; } clonedData = Assign(clonedData, map); @@ -297,15 +309,19 @@ public class TranslationService : ITranslationService /// /// /// - private async Task InnerTranslate(List texts, string language, string template) + private async Task InnerTranslate(List texts, string language, string template) { + var options = new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + var jsonString = JsonSerializer.Serialize(texts, options); var translator = new Agent { Id = Guid.Empty.ToString(), Name = "Translator", + Instruction = "You are a translation expert.", TemplateDict = new Dictionary { - { "text_list", texts }, + { "text_list", jsonString }, + { "text_list_size", texts.Count }, { StateConst.LANGUAGE, language } } }; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index d51b6f4a..de4c12c8 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -65,7 +65,7 @@ public class UserService : IUserService return record; } - public async Task GetToken(string authorization) + public async Task GetToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); var (id, password) = base64.SplitAsTuple(":"); @@ -77,13 +77,14 @@ public class UserService : IUserService record = db.GetUserByUserName(id); } + User? user = null; var hooks = _services.GetServices(); if (record == null || record.Source != "internal") { // check 3rd party user foreach (var hook in hooks) { - var user = await hook.Authenticate(id, password); + user = await hook.Authenticate(id, password); if (user == null) { continue; @@ -114,7 +115,7 @@ public class UserService : IUserService } } - if (record == null) + if ((!hooks.IsNullOrEmpty() && user == null) || record == null) { return default; } diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid index 9d9672b5..6b3a8677 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/translation_prompt.liquid @@ -1,7 +1,6 @@ -{% for text in text_list %} -{{ text }} -{% endfor %} +{{ text_list }} ===== -Translate the above sentences in the list into {{ language }}. -Output the translated text in JSON {"input_lang":"", "output_lang":"{{ language }}", "texts":[]}, input_lang is based on the original sentences. \ No newline at end of file +Translate all the above sentences into {{ language }}. +Output the translated text in JSON {"input_lang":"original text language", "output_count": {{ text_list_size }}, "output_lang":"{{ language }}", "texts":[{"id": 1, "text":""},{"id": 2, "text":""}]}. +The "output_count" must equal the length of the "texts" array in the output. diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 810bc08d..80374a6d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,8 +1,4 @@ using BotSharp.Abstraction.Routing; -using Newtonsoft.Json.Serialization; -using Newtonsoft.Json; -using BotSharp.Abstraction.Files.Models; -using BotSharp.Abstraction.Files; namespace BotSharp.OpenAPI.Controllers; @@ -138,6 +134,35 @@ public class ConversationController : ControllerBase return result; } + [HttpGet("/conversation/{conversationId}/user")] + public async Task GetConversationUser([FromRoute] string conversationId) + { + var service = _services.GetRequiredService(); + var conversations = await service.GetConversations(new ConversationFilter + { + Id = conversationId + }); + + var userService = _services.GetRequiredService(); + var conversation = conversations?.Items?.FirstOrDefault(); + var userId = conversation == null ? _user.Id : conversation.UserId; + var user = await userService.GetUser(userId); + if (user == null) + { + return new UserViewModel + { + Id = _user.Id, + UserName = _user.UserName, + FirstName = _user.FirstName, + LastName = _user.LastName, + Email = _user.Email, + Source = "Unknown" + }; + } + + return UserViewModel.FromUser(user); + } + [HttpDelete("/conversation/{conversationId}")] public async Task DeleteConversation([FromRoute] string conversationId) { @@ -232,7 +257,11 @@ public class ConversationController : ControllerBase conv.SetConversationId(conversationId, input.States); SetStates(conv, input); - var response = new ChatResponseModel(); + var response = new ChatResponseModel + { + ConversationId = conversationId, + MessageId = inputMsg.MessageId, + }; Response.StatusCode = 200; Response.Headers.Append(Microsoft.Net.Http.Headers.HeaderNames.ContentType, "text/event-stream"); @@ -241,6 +270,7 @@ public class ConversationController : ControllerBase await conv.SendMessage(agentId, inputMsg, replyMessage: input.Postback, + // responsed generated async msg => { response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; @@ -249,18 +279,21 @@ public class ConversationController : ControllerBase response.Instruction = msg.Instruction; response.Data = msg.Data; - await OnChunkReceived(Response, msg); + await OnChunkReceived(Response, response); }, + // executing async msg => { - var message = new RoleDialogModel(AgentRole.Function, msg.Content) + var indicator = new ChatResponseModel { - FunctionArgs = msg.FunctionArgs, - FunctionName = msg.FunctionName, - Indication = msg.Indication + ConversationId = conversationId, + MessageId = msg.MessageId, + Text = msg.Indication, + Function = "indicating", }; - await OnChunkReceived(Response, message); + await OnChunkReceived(Response, indicator); }, + // executed async msg => { @@ -274,14 +307,9 @@ public class ConversationController : ControllerBase // await OnEventCompleted(Response); } - private async Task OnChunkReceived(HttpResponse response, RoleDialogModel message) + private async Task OnChunkReceived(HttpResponse response, ChatResponseModel message) { - var json = JsonConvert.SerializeObject(message, new JsonSerializerSettings - { - Formatting = Formatting.None, - ContractResolver = new CamelCasePropertyNamesContractResolver(), - NullValueHandling = NullValueHandling.Ignore, - }); + var json = JsonSerializer.Serialize(message); var buffer = Encoding.UTF8.GetBytes($"data:{json}\n"); await response.Body.WriteAsync(buffer, 0, buffer.Length); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs index a24357a2..cfae602c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs @@ -38,10 +38,11 @@ public class FileController : ControllerBase } [HttpGet("/conversation/{conversationId}/files/{messageId}")] - public IEnumerable GetConversationFiles([FromRoute] string conversationId, [FromRoute] string messageId) + public IEnumerable GetMessageFiles([FromRoute] string conversationId, [FromRoute] string messageId) { var fileService = _services.GetRequiredService(); - return fileService.GetConversationFiles(conversationId, messageId); + var files = fileService.GetMessageFiles(conversationId, new List { messageId }); + return files?.Select(x => MessageFileViewModel.Transform(x))?.ToList() ?? new List(); } [HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")] diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs index 985526d9..45120a26 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs @@ -11,10 +11,12 @@ namespace BotSharp.OpenAPI.Controllers; public class InstructModeController : ControllerBase { private readonly IServiceProvider _services; + private readonly ILogger _logger; - public InstructModeController(IServiceProvider services) + public InstructModeController(IServiceProvider services, ILogger logger) { _services = services; + _logger = logger; } [HttpPost("/instruct/{agentId}")] @@ -72,4 +74,33 @@ public class InstructModeController : ControllerBase }); return message.Content; } + + [HttpPost("/instruct/multi-modal")] + public async Task MultiModalCompletion([FromBody] IncomingMessageModel input) + { + var state = _services.GetRequiredService(); + input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + + try + { + var completion = CompletionProvider.GetChatCompletion(_services, provider: input.Provider ?? "openai", + modelId: input.ModelId ?? "gpt-4", multiModal: true); + var message = await completion.GetChatCompletions(new Agent() + { + Id = Guid.Empty.ToString(), + }, new List + { + new RoleDialogModel(AgentRole.User, input.Text) + { + Files = input.Files + } + }); + return message.Content; + } + catch (Exception ex) + { + _logger.LogError($"Error in analyzing files. {ex.Message}"); + return $"Error in analyzing files."; + } + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Using.cs b/src/Infrastructure/BotSharp.OpenAPI/Using.cs index f3ba775f..8771b81c 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Using.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Using.cs @@ -28,4 +28,5 @@ global using BotSharp.Abstraction.Files.Models; global using BotSharp.Abstraction.Files; global using BotSharp.OpenAPI.ViewModels.Conversations; global using BotSharp.OpenAPI.ViewModels.Users; -global using BotSharp.OpenAPI.ViewModels.Agents; \ No newline at end of file +global using BotSharp.OpenAPI.ViewModels.Agents; +global using BotSharp.OpenAPI.ViewModels.Files; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs new file mode 100644 index 00000000..a9eb33bd --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Files; + +public class MessageFileViewModel +{ + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + public MessageFileViewModel() + { + + } + + public static MessageFileViewModel Transform(MessageFileModel model) + { + return new MessageFileViewModel + { + FileUrl = model.FileUrl, + FileName = model.FileName, + FileType = model.FileType, + ContentType = model.ContentType + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj index 357e827f..40f54f60 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj @@ -1,9 +1,13 @@ - + - net8.0 - enable + netstandard2.1 enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs index ceb477d6..d00446fc 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/Using.cs @@ -1,3 +1,9 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Threading.Tasks; +global using System.Linq; +global using System.Text.Json; global using Anthropic.SDK; global using Anthropic.SDK.Constants; global using Anthropic.SDK.Messaging; diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index 07a3ff8f..990a74ab 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -4,14 +4,19 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Files; +using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Utilities; using BotSharp.Plugin.AzureOpenAI.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Threading.Tasks; namespace BotSharp.Plugin.AzureOpenAI.Providers; @@ -218,6 +223,17 @@ public class ChatCompletionProvider : IChatCompletion protected (string, ChatCompletionsOptions) PrepareOptions(Agent agent, List conversations) { var agentService = _services.GetRequiredService(); + var fileService = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + var settingsService = _services.GetRequiredService(); + var settings = settingsService.GetSetting(Provider, _model); + var allowMultiModal = settings != null && settings.MultiModal; + + var chatFiles = new List(); + if (allowMultiModal) + { + chatFiles = fileService.GetChatImages(state.GetConversationId(), conversations, offset: 2).ToList(); + } var chatCompletionsOptions = new ChatCompletionsOptions(); @@ -279,17 +295,63 @@ public class ChatCompletionProvider : IChatCompletion else if (message.Role == ChatRole.User) { var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content; - var userMessage = new ChatRequestUserMessage(text) - { - // To display Planner name in log - Name = message.FunctionName, - }; - if (!string.IsNullOrEmpty(message.ImageUrl)) + ChatRequestUserMessage userMessage = null; + if (allowMultiModal) { - var uri = new Uri(message.ImageUrl); - userMessage.MultimodalContentItems.Add( - new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + var chatItems = new List() + { + new ChatMessageTextContentItem(text) + }; + + var files = chatFiles.Where(x => x.MessageId == message.MessageId).ToList(); + if (!files.IsNullOrEmpty()) + { + foreach (var file in files) + { + using var stream = File.OpenRead(file.FileStorageUrl); + chatItems.Add(new ChatMessageImageContentItem(stream, file.ContentType, ChatMessageImageDetailLevel.Low)); + } + } + + if (!message.Files.IsNullOrEmpty()) + { + foreach (var file in message.Files) + { + if (!string.IsNullOrEmpty(file.FileUrl)) + { + var uri = new Uri(file.FileUrl); + chatItems.Add(new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + } + else if (!string.IsNullOrEmpty(file.FileData)) + { + var (contentType, bytes) = fileService.GetFileInfoFromData(file.FileData); + using var stream = new MemoryStream(bytes, 0, bytes.Length); + chatItems.Add(new ChatMessageImageContentItem(stream, contentType, ChatMessageImageDetailLevel.Low)); + } + } + } + + //if (!string.IsNullOrEmpty(message.ImageUrl)) + //{ + // var uri = new Uri(message.ImageUrl); + // userMessage.MultimodalContentItems.Add( + // new ChatMessageImageContentItem(uri, ChatMessageImageDetailLevel.Low)); + //} + + userMessage = new ChatRequestUserMessage(chatItems) + { + // To display Planner name in log + Name = message.FunctionName, + }; + } + else + { + userMessage = new ChatRequestUserMessage(text) + { + // To display Planner name in log + Name = message.FunctionName, + }; } chatCompletionsOptions.Messages.Add(userMessage); @@ -301,7 +363,7 @@ public class ChatCompletionProvider : IChatCompletion } // https://community.openai.com/t/cheat-sheet-mastering-temperature-and-top-p-in-chatgpt-api-a-few-tips-and-tricks-on-controlling-the-creativity-deterministic-output-of-prompt-responses/172683 - var state = _services.GetRequiredService(); + //var state = _services.GetRequiredService(); var temperature = float.Parse(state.GetState("temperature", "0.0")); var samplingFactor = float.Parse(state.GetState("sampling_factor", "0.0")); chatCompletionsOptions.Temperature = temperature; @@ -347,9 +409,12 @@ public class ChatCompletionProvider : IChatCompletion else if (x.Role == ChatRole.User) { var m = x as ChatRequestUserMessage; + var content = m.Content ?? string.Join(", ", m.MultimodalContentItems + .Where(m => m is ChatMessageTextContentItem) + .Select(m => (m as ChatMessageTextContentItem)?.Text)); return !string.IsNullOrEmpty(m.Name) && m.Name != "route_to_agent" ? - $"{m.Name}: {m.Content}" : - $"{m.Role}: {m.Content}"; + $"{m.Name}: {content}" : + $"{m.Role}: {content}"; } else if (x.Role == ChatRole.Assistant) { diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index ab662546..0d578f99 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Loggers.Enums; @@ -49,6 +50,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnMessageReceived(RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"{GetMessageContent(message)}"; var input = new ContentLogInputModel(conversationId, message) @@ -63,6 +66,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"{GetMessageContent(message)}"; var replyContent = JsonSerializer.Serialize(replyMsg, _options.JsonSerializerOptions); log += $"\r\n```json\r\n{replyContent}\r\n```"; @@ -81,6 +86,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; var log = $"{agent.Name} is using template {name}"; var message = new RoleDialogModel(AgentRole.System, log) @@ -104,12 +110,11 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnFunctionExecuting(RoleDialogModel message) { - if (message.FunctionName == "route_to_agent") - { - return; - } - var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + + if (message.FunctionName == "route_to_agent") return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); message.FunctionArgs = message.FunctionArgs ?? "{}"; var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); @@ -127,12 +132,11 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnFunctionExecuted(RoleDialogModel message) { - if (message.FunctionName == "route_to_agent") - { - return; - } - var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + + if (message.FunctionName == "route_to_agent") return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); message.FunctionArgs = message.FunctionArgs ?? "{}"; // var args = JsonSerializer.Serialize(JsonDocument.Parse(message.FunctionArgs), _options.JsonSerializerOptions); @@ -159,6 +163,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR if (!_convSettings.ShowVerboseLog) return; var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = tokenStats.Prompt; @@ -180,8 +186,10 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR /// public override async Task OnResponseGenerated(RoleDialogModel message) { - var conv = _services.GetRequiredService(); + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var conv = _services.GetRequiredService(); await _chatHub.Clients.User(_user.Id).SendAsync("OnConversateStateLogGenerated", BuildStateLog(conv.ConversationId, _state.GetStates(), message)); if (message.Role == AgentRole.Assistant) @@ -208,6 +216,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnTaskCompleted(RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"{GetMessageContent(message)}"; var agent = await _agentService.LoadAgent(message.CurrentAgentId); @@ -223,6 +233,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnConversationEnding(RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"Conversation ended"; var agent = await _agentService.LoadAgent(message.CurrentAgentId); @@ -237,6 +249,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnBreakpointUpdated(string conversationId, bool resetStates) { + if (string.IsNullOrEmpty(conversationId)) return; + var log = $"Conversation breakpoint is updated"; if (resetStates) { @@ -263,6 +277,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public override async Task OnStateChanged(StateChangeModel stateChange) { + var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + if (stateChange == null) return; await _chatHub.Clients.User(_user.Id).SendAsync("OnStateChangeGenerated", BuildStateChangeLog(stateChange)); @@ -273,6 +290,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentEnqueued(string agentId, string preAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(agentId); // Agent queue log @@ -298,6 +317,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentDequeued(string agentId, string currentAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(agentId); var currentAgent = await _agentService.LoadAgent(currentAgentId); @@ -324,6 +345,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var fromAgent = await _agentService.LoadAgent(fromAgentId); var toAgent = await _agentService.LoadAgent(toAgentId); @@ -350,6 +373,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnAgentQueueEmptied(string agentId, string? reason = null) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; // Agent queue log var log = $"Agent queue is empty"; @@ -374,6 +398,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = JsonSerializer.Serialize(instruct, _options.JsonSerializerOptions); log = $"```json\r\n{log}\r\n```"; @@ -391,6 +417,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, RoleDialogModel message) { var conversationId = _state.GetConversationId(); + if (string.IsNullOrEmpty(conversationId)) return; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); var log = $"Revised user goal agent to {instruct.OriginalAgent}";