diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs new file mode 100644 index 00000000..98aaf454 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Agents.Enums; + +public class BuiltInAgentId +{ + public const string AIAssistant = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"; + public const string Chatbot = "01e2fc5c-2c89-4ec7-8470-7688608b496c"; + public const string HumanSupport = "01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 65424adb..b03aa12b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -10,6 +10,12 @@ $(SolutionDir)packages + + + + + + True diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs new file mode 100644 index 00000000..1a6920c8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionArgs.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Browsing.Models; + +public class BrowserActionArgs +{ + +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs index b3576b90..63c592b7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/BrowserActionResult.cs @@ -8,6 +8,7 @@ public class BrowserActionResult public string Selector { get; set; } public string Body { get; set; } public bool IsHighlighted { get; set; } + public DateTime? ExecutedAt { get; set; } = DateTime.UtcNow; public override string ToString() { diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs index 3b96c760..9dd5334a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementLocatingArgs.cs @@ -31,4 +31,5 @@ public class ElementLocatingArgs /// Draw outline around the element /// public bool Highlight { get; set; } + public string HighlightColor { get; set; } = "red"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs index 99591b1e..f583febb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs @@ -11,4 +11,11 @@ public class PageActionArgs public string Url { get; set; } = null!; public bool OpenNewTab { get; set; } = false; + + /// + /// On page data fetched + /// + public DataFetched? OnDataFetched { get; set; } } + +public delegate void DataFetched(string url, string data); diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 6adcc8ad..8c3a0e81 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Tasks.Models; +using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Models; namespace BotSharp.Abstraction.Repositories; @@ -88,4 +89,10 @@ public interface IBotSharpRepository #region Statistics void IncrementConversationCount(); #endregion + + #region Translation + IEnumerable GetTranslationMemories(IEnumerable queries); + bool SaveTranslationMemories(IEnumerable inputs); + + #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemory.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemory.cs new file mode 100644 index 00000000..88460268 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemory.cs @@ -0,0 +1,25 @@ +namespace BotSharp.Abstraction.Translation.Models; + +public class TranslationMemory +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("original_text")] + public string OriginalText { get; set; } + + [JsonPropertyName("hash_text")] + public string HashText { get; set; } + + [JsonPropertyName("translations")] + public List Translations { get; set; } = new List(); +} + +public class TranslationMemoryItem +{ + [JsonPropertyName("translated_text")] + public string TranslatedText { get; set; } + + [JsonPropertyName("language")] + public string Language { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemoryQuery.cs b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemoryQuery.cs new file mode 100644 index 00000000..be5191f8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Translation/Models/TranslationMemoryQuery.cs @@ -0,0 +1,48 @@ +namespace BotSharp.Abstraction.Translation.Models; + +public class TranslationMemoryQuery +{ + public string OriginalText { get; set; } + public string HashText { get; set; } + public string Language { get; set; } + + public TranslationMemoryQuery() + { + + } + + public override string ToString() + { + return $"[Origin: {OriginalText}] translates to [{Language}]"; + } +} + +public class TranslationMemoryInput : TranslationMemoryQuery +{ + public string TranslatedText { get; set; } + + public TranslationMemoryInput() + { + + } + + public override string ToString() + { + return $"[Origin: {OriginalText}] -> [Translation: {TranslatedText}] (Language: {Language})"; + } +} + +public class TranslationMemoryOutput: TranslationMemoryQuery +{ + public string TranslatedText { get; set; } + + public TranslationMemoryOutput() + { + + } + + public override string ToString() + { + return $"[Origin: {OriginalText}] -> [Translation: {TranslatedText}] (Language: {Language})"; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs index 9ada5f63..ad4f2e3f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs @@ -37,6 +37,8 @@ public class Pagination { get { return (Page - 1) * Size; } } + + public bool ReturnTotal { get; set; } = true; } public class PagedItems diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index fa572e67..43bd9c7f 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -17,9 +17,9 @@ public class AgentPlugin : IBotSharpPlugin public string[] AgentIds => new string[] { - "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", - "01e2fc5c-2c89-4ec7-8470-7688608b496c", - "01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b" + BuiltInAgentId.AIAssistant, + BuiltInAgentId.Chatbot, + BuiltInAgentId.HumanSupport }; public object GetNewSettingsInstance() => diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index e72ff4a1..3997a390 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -180,8 +180,7 @@ - - + @@ -192,4 +191,8 @@ + + + + diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs index 449d2c3f..4cb26888 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs @@ -4,11 +4,11 @@ namespace BotSharp.Core.Infrastructures; public static class Utilities { - public static string HashText(string password, string salt) + public static string HashTextMd5(string text) { using var md5 = System.Security.Cryptography.MD5.Create(); - var data = md5.ComputeHash(Encoding.UTF8.GetBytes(password + salt)); + var data = md5.ComputeHash(Encoding.UTF8.GetBytes(text)); var sb = new StringBuilder(); foreach (var c in data) { @@ -17,6 +17,19 @@ public static class Utilities return sb.ToString(); } + public static string HashTextSha256(string text) + { + using var sha256 = System.Security.Cryptography.SHA256.Create(); + + var data = sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); + var sb = new StringBuilder(); + foreach(var c in data) + { + sb.Append(c.ToString("x2")); + } + return sb.ToString(); + } + public static (string, string) SplitAsTuple(this string str, string sep) { var splits = str.Split(sep); diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 6bba6c2a..5ce76901 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks.Models; +using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Models; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -224,4 +225,11 @@ public class BotSharpDbContext : Database, IBotSharpRepository throw new NotImplementedException(); } #endregion + + #region Translation + public IEnumerable GetTranslationMemories(IEnumerable queries) + => throw new NotImplementedException(); + public bool SaveTranslationMemories(IEnumerable inputs) => + throw new NotImplementedException(); + #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs new file mode 100644 index 00000000..e52e09c2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Translation.cs @@ -0,0 +1,156 @@ +using BotSharp.Abstraction.Translation.Models; +using System.IO; + +namespace BotSharp.Core.Repository; + +public partial class FileRepository +{ + public IEnumerable GetTranslationMemories(IEnumerable queries) + { + var list = new List(); + if (queries.IsNullOrEmpty()) + { + return list; + } + + var dir = Path.Combine(_dbSettings.FileRepository, "translation"); + var file = Path.Combine(dir, TRANSLATION_MEMORY_FILE); + if (!Directory.Exists(dir) || !File.Exists(file)) + { + return list; + } + + var content = File.ReadAllText(file); + if (string.IsNullOrWhiteSpace(content)) + { + return list; + } + + var memories = ReadTranslationMemoryContent(content); + foreach (var query in queries) + { + if (string.IsNullOrWhiteSpace(query.HashText) || string.IsNullOrWhiteSpace(query.Language)) + { + continue; + } + + var foundMemory = memories.FirstOrDefault(x => x.HashText.Equals(query.HashText)); + if (foundMemory == null) continue; + + var foundItem = foundMemory.Translations?.FirstOrDefault(x => x.Language.Equals(query.Language)); + if (foundItem == null) continue; + + list.Add(new TranslationMemoryOutput + { + OriginalText = query.OriginalText, + TranslatedText = foundItem.TranslatedText, + HashText = foundMemory.HashText, + Language = foundItem.Language, + }); + } + + return list; + } + + public bool SaveTranslationMemories(IEnumerable inputs) + { + if (inputs.IsNullOrEmpty()) return false; + + try + { + var dir = Path.Combine(_dbSettings.FileRepository, "translation"); + var file = Path.Combine(dir, TRANSLATION_MEMORY_FILE); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var content = string.Empty; + if (File.Exists(file)) + { + content = File.ReadAllText(file); + } + + var memories = ReadTranslationMemoryContent(content); + + foreach (var input in inputs) + { + if (string.IsNullOrWhiteSpace(input.OriginalText) || + string.IsNullOrWhiteSpace(input.TranslatedText) || + string.IsNullOrWhiteSpace(input.HashText) || + string.IsNullOrWhiteSpace(input.Language)) + { + continue; + } + + var newItem = new TranslationMemoryItem + { + TranslatedText = input.TranslatedText, + Language = input.Language + }; + + var foundMemory = memories?.FirstOrDefault(x => x.HashText.Equals(input.HashText)); + if (foundMemory == null) + { + var newMemory = new TranslationMemory + { + Id = Guid.NewGuid().ToString(), + OriginalText = input.OriginalText, + HashText = input.HashText, + Translations = new List { newItem } + }; + + if (memories == null) + { + memories = new List { newMemory }; + } + else + { + memories.Add(newMemory); + } + } + else + { + var foundItem = foundMemory.Translations?.FirstOrDefault(x => x.Language.Equals(input.Language)); + if (foundItem != null) continue; + + if (foundMemory.Translations == null) + { + foundMemory.Translations = new List { newItem }; + } + else + { + foundMemory.Translations.Add(newItem); + } + } + } + + var json = JsonSerializer.Serialize(memories, _options); + File.WriteAllText(file, json); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving translation memories: {ex.Message}"); + return false; + } + } + + private List ReadTranslationMemoryContent(string? content) + { + var memories = new List(); + + try + { + if (string.IsNullOrWhiteSpace(content)) + { + return memories; + } + + memories = JsonSerializer.Deserialize>(content, _options) ?? new List(); + } + catch {} + + return memories; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 2094b821..a30142be 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -17,6 +17,7 @@ public partial class FileRepository : IBotSharpRepository private readonly AgentSettings _agentSettings; private readonly ConversationSetting _conversationSettings; private readonly StatisticsSettings _statisticsSetting; + private readonly ILogger _logger; private JsonSerializerOptions _options; private const string AGENT_FILE = "agent.json"; @@ -34,19 +35,22 @@ public partial class FileRepository : IBotSharpRepository private const string PLUGIN_CONFIG_FILE = "config.json"; private const string AGENT_TASK_PREFIX = "#metadata"; private const string AGENT_TASK_SUFFIX = "/metadata"; + private const string TRANSLATION_MEMORY_FILE = "memory.json"; public FileRepository( IServiceProvider services, BotSharpDatabaseSettings dbSettings, AgentSettings agentSettings, ConversationSetting conversationSettings, - StatisticsSettings statisticsSettings) + StatisticsSettings statisticsSettings, + ILogger logger) { _services = services; _dbSettings = dbSettings; _agentSettings = agentSettings; _conversationSettings = conversationSettings; _statisticsSetting = statisticsSettings; + _logger = logger; _options = new JsonSerializerOptions { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs index 6df03a08..d815ee5a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs @@ -32,7 +32,7 @@ public partial class TwoStagePlanner : IPlaner if (_plan1st.IsNullOrEmpty() && _plan2nd.IsNullOrEmpty()) { Directory.CreateDirectory(tempDir); - _md5 = Utilities.HashText(string.Join(".", dialogs.Where(x => x.Role == AgentRole.User)), "botsharp"); + _md5 = Utilities.HashTextMd5($"{string.Join(".", dialogs.Where(x => x.Role == AgentRole.User))}{"botsharp"}"); var filePath = Path.Combine(tempDir, $"{_md5}-1st.json"); FirstStagePlan[] items = new FirstStagePlan[0]; if (File.Exists(filePath)) diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs index ec33f609..399f9c0d 100644 --- a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs @@ -1,4 +1,3 @@ -using Amazon.Runtime.Internal.Transform; using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Options; @@ -13,17 +12,21 @@ namespace BotSharp.Core.Translation; public class TranslationService : ITranslationService { private readonly IServiceProvider _services; + private readonly IBotSharpRepository _db; private readonly ILogger _logger; private readonly BotSharpOptions _options; private Agent _router; private string _messageId; private IChatCompletion _completion; - public TranslationService(IServiceProvider services, + public TranslationService( + IServiceProvider services, + IBotSharpRepository db, ILogger logger, BotSharpOptions options) { _services = services; + _db = db; _logger = logger; _options = options; } @@ -56,41 +59,73 @@ public class TranslationService : ITranslationService model: _router?.LlmConfig?.Model); var template = _router.Templates.First(x => x.Name == "translation_prompt").Content; + var map = new Dictionary(); var keys = unique.ToArray(); - var texts = unique.ToArray() + + #region Search memory + var queries = keys.Select(x => new TranslationMemoryQuery + { + OriginalText = x, + HashText = Utilities.HashTextSha256(x), + Language = language + }).ToList(); + var memories = _db.GetTranslationMemories(queries); + var memoryHashes = memories.Select(x => x.HashText).ToList(); + + foreach (var memory in memories) + { + map[memory.OriginalText] = memory.TranslatedText; + } + + var outOfMemoryList = queries.Where(x => !memoryHashes.Contains(x.HashText)).ToList(); + #endregion + + var texts = outOfMemoryList.ToArray() .Select((text, i) => new TranslationInput { Id = i + 1, - Text = text + Text = text.OriginalText }).ToList(); try { - var translatedStringList = await InnerTranslate(texts, language, template); - - int retry = 0; - while (translatedStringList.Texts.Length != texts.Count && retry < 3) + if (!texts.IsNullOrEmpty()) { - translatedStringList = await InnerTranslate(texts, language, template); - retry++; + 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)) + { + var inputLanguage = string.IsNullOrEmpty(translatedStringList.InputLanguage) ? LanguageType.ENGLISH : translatedStringList.InputLanguage; + states.SetState(StateConst.LANGUAGE, inputLanguage, activeRounds: 1); + } + + var translatedTexts = translatedStringList.Texts; + var memoryInputs = new List(); + + for (var i = 0; i < texts.Count; i++) + { + map[outOfMemoryList[i].OriginalText] = translatedTexts[i].Text; + memoryInputs.Add(new TranslationMemoryInput + { + OriginalText = outOfMemoryList[i].OriginalText, + HashText = outOfMemoryList[i].HashText, + TranslatedText = translatedTexts[i].Text, + Language = language + }); + } + + _db.SaveTranslationMemories(memoryInputs); } - - // Override language if it's Unknown, it's used to output the corresponding language. - var states = _services.GetRequiredService(); - if (!states.ContainsState(StateConst.LANGUAGE)) - { - var inputLanguage = string.IsNullOrEmpty(translatedStringList.InputLanguage) ? LanguageType.ENGLISH : translatedStringList.InputLanguage; - states.SetState(StateConst.LANGUAGE, inputLanguage, activeRounds: 1); - } - - var translatedTexts = translatedStringList.Texts; - var map = new Dictionary(); - - for (var i = 0; i < texts.Count; i++) - { - map[keys[i]] = translatedTexts[i].Text; - } - + clonedData = Assign(clonedData, map); } catch (Exception ex) diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 81a7a79c..aadeb2e1 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -61,7 +61,7 @@ public class UserService : IUserService record.Phone = "+" + Regex.Match(user.Phone, @"\d+").Value; } record.Salt = Guid.NewGuid().ToString("N"); - record.Password = Utilities.HashText(user.Password, record.Salt); + record.Password = Utilities.HashTextMd5($"{user.Password}{record.Salt}"); if (_setting.NewUserVerification) { diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index af7bc560..709285e2 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -36,4 +36,3 @@ global using BotSharp.Core.Conversations.Services; global using BotSharp.Core.Infrastructures; global using BotSharp.Core.Users.Services; global using Aspects.Cache; -global using Console = Colorful.Console; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/TranslationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/TranslationController.cs new file mode 100644 index 00000000..ac7686a1 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/TranslationController.cs @@ -0,0 +1,30 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Translation; +using BotSharp.OpenAPI.ViewModels.Translations; + +namespace BotSharp.OpenAPI.Controllers; + +[Authorize] +[ApiController] +public class TranslationController : ControllerBase +{ + private readonly IServiceProvider _services; + + public TranslationController(IServiceProvider services) + { + _services = services; + } + + [HttpPost("/translate")] + public async Task Translate([FromBody] TranslationRequestModel model) + { + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(BuiltInAgentId.AIAssistant); + var translator = _services.GetRequiredService(); + var text = await translator.Translate(agent, Guid.NewGuid().ToString(), model.Text, language: model.ToLang); + return new TranslationResponseModel + { + Text = text + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationRequestModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationRequestModel.cs new file mode 100644 index 00000000..588dedfc --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationRequestModel.cs @@ -0,0 +1,9 @@ +using BotSharp.Abstraction.Infrastructures.Enums; + +namespace BotSharp.OpenAPI.ViewModels.Translations; + +public class TranslationRequestModel +{ + public string Text { get; set; } = null!; + public string ToLang { get; set; } = LanguageType.CHINESE; +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationResponseModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationResponseModel.cs new file mode 100644 index 00000000..0c547d2b --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Translations/TranslationResponseModel.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Translations; + +public class TranslationResponseModel +{ + public string Text { get; set; } = null!; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string FromLang { get; set; } = null!; +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/TranslationMemoryDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/TranslationMemoryDocument.cs new file mode 100644 index 00000000..54c11b3e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/TranslationMemoryDocument.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class TranslationMemoryDocument : MongoBase +{ + public string OriginalText { get; set; } + public string HashText { get; set; } + public List Translations { get; set; } = new List(); +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/TranslationMemoryMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/TranslationMemoryMongoElement.cs new file mode 100644 index 00000000..0d090b78 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/TranslationMemoryMongoElement.cs @@ -0,0 +1,25 @@ +namespace BotSharp.Plugin.MongoStorage.Models; + +public class TranslationMemoryMongoElement +{ + public string TranslatedText { get; set; } + public string Language { get; set; } + + public static TranslationMemoryMongoElement ToMongoElement(TranslationMemoryItem item) + { + return new TranslationMemoryMongoElement + { + TranslatedText = item.TranslatedText, + Language = item.Language + }; + } + + public static TranslationMemoryItem ToDomainElement(TranslationMemoryMongoElement element) + { + return new TranslationMemoryItem + { + TranslatedText = element.TranslatedText, + Language = element.Language + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index ca691e05..dfbbfbaa 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -1,5 +1,3 @@ -using BotSharp.Plugin.MongoStorage.Collections; - namespace BotSharp.Plugin.MongoStorage; public class MongoDbContext @@ -117,4 +115,7 @@ public class MongoDbContext public IMongoCollection Plugins => Database.GetCollection($"{_collectionPrefix}_Plugins"); + + public IMongoCollection TranslationMemories + => Database.GetCollection($"{_collectionPrefix}_TranslationMemories"); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index abdfb21b..2fe77b39 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -2,8 +2,6 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; -using BotSharp.Plugin.MongoStorage.Collections; -using BotSharp.Plugin.MongoStorage.Models; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs index a3df61da..a00858c4 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.AgentTask.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Tasks.Models; -using BotSharp.Plugin.MongoStorage.Collections; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index aefd0db5..bed3a255 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -1,9 +1,5 @@ using BotSharp.Abstraction.Conversations.Models; -using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Repositories.Models; -using BotSharp.Plugin.MongoStorage.Collections; -using BotSharp.Plugin.MongoStorage.Models; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs index 94adacc3..31631230 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Log.cs @@ -1,6 +1,4 @@ using BotSharp.Abstraction.Loggers.Models; -using BotSharp.Plugin.MongoStorage.Collections; -using BotSharp.Plugin.MongoStorage.Models; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Plugin.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Plugin.cs index c6bc60b3..b26da408 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Plugin.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Plugin.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Plugin.MongoStorage.Collections; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs index 923cf5fd..eb05768d 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs @@ -1,8 +1,5 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Users.Models; -using BotSharp.Plugin.MongoStorage.Collections; -using BotSharp.Plugin.MongoStorage.Models; - namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Translation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Translation.cs new file mode 100644 index 00000000..5f0e987e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Translation.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.Logging; +using System.Threading; + +namespace BotSharp.Plugin.MongoStorage.Repository; + +public partial class MongoRepository +{ + public IEnumerable GetTranslationMemories(IEnumerable queries) + { + var list = new List(); + if (queries.IsNullOrEmpty()) + { + return list; + } + + var hashTexts = queries.Where(x => !string.IsNullOrEmpty(x.HashText)).Select(x => x.HashText).ToList(); + var filter = Builders.Filter.In(x => x.HashText, hashTexts); + var memories = _dc.TranslationMemories.Find(filter).ToList(); + if (memories.IsNullOrEmpty()) return list; + + foreach (var query in queries) + { + if (string.IsNullOrWhiteSpace(query.HashText) || string.IsNullOrWhiteSpace(query.Language)) + { + continue; + } + + var foundMemory = memories.FirstOrDefault(x => x.HashText.Equals(query.HashText)); + if (foundMemory == null) continue; + + var foundItem = foundMemory.Translations?.FirstOrDefault(x => x.Language.Equals(query.Language)); + if (foundItem == null) continue; + + list.Add(new TranslationMemoryOutput + { + OriginalText = query.OriginalText, + TranslatedText = foundItem.TranslatedText, + HashText = foundMemory.HashText, + Language = foundItem.Language, + }); + } + + return list; + } + + public bool SaveTranslationMemories(IEnumerable inputs) + { + if (inputs.IsNullOrEmpty()) return false; + + var hashTexts = inputs.Where(x => !string.IsNullOrEmpty(x.HashText)).Select(x => x.HashText).ToList(); + var filter = Builders.Filter.In(x => x.HashText, hashTexts); + var memories = _dc.TranslationMemories.Find(filter)?.ToList() ?? new List(); + + var newMemories = new List(); + var updateMemories = new List(); + + try + { + foreach (var input in inputs) + { + if (string.IsNullOrWhiteSpace(input.OriginalText) || + string.IsNullOrWhiteSpace(input.TranslatedText) || + string.IsNullOrWhiteSpace(input.HashText) || + string.IsNullOrWhiteSpace(input.Language)) + { + continue; + } + + var newItem = new TranslationMemoryMongoElement + { + TranslatedText = input.TranslatedText, + Language = input.Language + }; + + var foundMemory = memories?.FirstOrDefault(x => x.HashText.Equals(input.HashText)); + if (foundMemory == null) + { + newMemories.Add(new TranslationMemoryDocument + { + Id = Guid.NewGuid().ToString(), + OriginalText = input.OriginalText, + HashText = input.HashText, + Translations = new List { newItem } + }); + } + else + { + var foundItem = foundMemory.Translations?.FirstOrDefault(x => x.Language.Equals(input.Language)); + if (foundItem != null) continue; + + if (foundMemory.Translations == null) + { + foundMemory.Translations = new List { newItem }; + } + else + { + foundMemory.Translations.Add(newItem); + } + updateMemories.Add(foundMemory); + } + } + + if (!newMemories.IsNullOrEmpty()) + { + _dc.TranslationMemories.InsertMany(newMemories); + } + + if (!updateMemories.IsNullOrEmpty()) + { + foreach (var mem in updateMemories) + { + var updateFilter = Builders.Filter.Eq(x => x.Id, mem.Id); + _dc.TranslationMemories.ReplaceOne(updateFilter, mem); + Thread.Sleep(50); + } + } + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving translation memories: {ex.Message}"); + return false; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index 9a3ce3f4..11f48287 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Users.Models; -using BotSharp.Plugin.MongoStorage.Collections; namespace BotSharp.Plugin.MongoStorage.Repository; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 02d21c43..133edba1 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Users.Models; +using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -8,12 +9,17 @@ public partial class MongoRepository : IBotSharpRepository { private readonly MongoDbContext _dc; private readonly IServiceProvider _services; + private readonly ILogger _logger; private UpdateOptions _options; - public MongoRepository(MongoDbContext dc, IServiceProvider services) + public MongoRepository( + MongoDbContext dc, + IServiceProvider services, + ILogger logger) { _dc = dc; _services = services; + _logger = logger; _options = new UpdateOptions { IsUpsert = true, diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs index ca13f204..7c74b648 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs @@ -7,8 +7,11 @@ global using BotSharp.Abstraction.Repositories; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Translation.Models; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using MongoDB.Bson; global using MongoDB.Driver; -global using MongoDB.Bson.Serialization.Attributes; \ No newline at end of file +global using MongoDB.Bson.Serialization.Attributes; +global using BotSharp.Plugin.MongoStorage.Collections; +global using BotSharp.Plugin.MongoStorage.Models; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs index 31780fb4..75d7f8ed 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs @@ -21,4 +21,3 @@ global using BotSharp.Plugin.SqlDriver.Hooks; global using BotSharp.Plugin.SqlDriver.Services; global using BotSharp.Plugin.SqlHero.Settings; global using System.Drawing; -global using Console = Colorful.Console; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs index 3f7d4d3a..69e94f13 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs @@ -1,3 +1,4 @@ +using Microsoft.Playwright; using System.IO; namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; @@ -6,31 +7,41 @@ public class PlaywrightInstance : IDisposable { IPlaywright _playwright; Dictionary _contexts = new Dictionary(); + Dictionary> _pages = new Dictionary>(); + + /// + /// ContextId and BrowserContext + /// public Dictionary Contexts => _contexts; - public IPage GetPage(string id) + /// + /// ContextId and Pages + /// + public Dictionary> Pages => _pages; + + public IPage GetPage(string id, string? pattern = null) { InitInstance(id).Wait(); return _contexts[id].Pages.LastOrDefault(); } - public async Task InitInstance(string id) + public async Task InitInstance(string ctxId) { if (_playwright == null) { _playwright = await Playwright.CreateAsync(); } - return await InitContext(id); + return await InitContext(ctxId); } - public async Task InitContext(string id) + public async Task InitContext(string ctxId) { - if (_contexts.ContainsKey(id)) - return _contexts[id]; + if (_contexts.ContainsKey(ctxId)) + return _contexts[ctxId]; - string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{id}"; + string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{ctxId}"; - _contexts[id] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions + _contexts[ctxId] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions { #if DEBUG Headless = false, @@ -42,63 +53,110 @@ public class PlaywrightInstance : IDisposable [ "--enable-automation", ], - Args = + Args = [ "--disable-infobars", "--test-type" // "--start-maximized" ] }); + _pages[ctxId] = new List(); - _contexts[id].Page += async (sender, e) => + _contexts[ctxId].Page += async (sender, page) => { - e.Close += async (sender, e) => + _pages[ctxId].Add(page); + page.Close += async (sender, e) => { + _pages[ctxId].Remove(e); Serilog.Log.Information($"Page is closed: {e.Url}"); }; - Serilog.Log.Information($"New page is created: {e.Url}"); - await e.SetViewportSizeAsync(1280, 800); + Serilog.Log.Information($"New page is created: {page.Url}"); + if (!page.IsClosed) + { + await page.SetViewportSizeAsync(1600, 900); + } + + /*page.Response += async (sender, e) => + { + Serilog.Log.Information($"Response: {e.Url}"); + if (e.Headers.ContainsKey("content-type") && e.Headers["content-type"].Contains("application/json")) + { + var json = await e.JsonAsync(); + Serilog.Log.Information(json.ToString()); + } + };*/ }; - _contexts[id].Close += async (sender, e) => + _contexts[ctxId].Close += async (sender, e) => { Serilog.Log.Warning($"Playwright browser context is closed"); - _contexts.Remove(id); + _pages.Remove(ctxId); + _contexts.Remove(ctxId); }; - return _contexts[id]; + return _contexts[ctxId]; } - public async Task NewPage(string id) + public async Task NewPage(string ctxId, DataFetched? fetched) { - await InitContext(id); - return await _contexts[id].NewPageAsync(); - } + await InitContext(ctxId); + var page = await _contexts[ctxId].NewPageAsync(); - public async Task Wait(string id) - { - if (_contexts.ContainsKey(id)) + if (fetched != null) + { + page.Response += async (sender, e) => + { + if (e.Headers.ContainsKey("content-type") && + e.Headers["content-type"].Contains("application/json") && + e.Request.ResourceType == "fetch") + { + Serilog.Log.Information($"Response: {e.Url}"); + var json = await e.JsonAsync(); + fetched(e.Url.ToLower(), JsonSerializer.Serialize(json)); + } + }; + } + + return page; + } + + /// + /// Wait page and network until timeout in seconds + /// + /// + /// seconds + /// + public async Task Wait(string ctxId, int timeout = 60) + { + foreach (var page in _pages[ctxId]) { - var page = _contexts[id].Pages.Last(); await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); - await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions + { + Timeout = 1000 * timeout + }); } await Task.Delay(100); } - public async Task Close(string id) + public async Task Close(string ctxId) { - if (_contexts.ContainsKey(id)) + if (_contexts.ContainsKey(ctxId)) { - await _contexts[id].CloseAsync(); + await _contexts[ctxId].CloseAsync(); } } - public async Task CloseCurrentPage(string id) + public async Task CloseCurrentPage(string ctxId) { - if (_contexts.ContainsKey(id)) + var pages = _pages[ctxId].ToArray(); + for (var i = 0; i < pages.Length; i++) { - await GetPage(id).CloseAsync(); + var page = _pages[ctxId].FirstOrDefault(); + if (page != null) + { + await page.CloseAsync(); + } } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs index 4e505997..4fb91660 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver @@ -9,7 +11,7 @@ public partial class PlaywrightWebDriver try { // Check if the page is already open - if (!args.OpenNewTab && context.Pages.Count > 0) + /*if (!args.OpenNewTab && context.Pages.Count > 0) { foreach (var p in context.Pages) { @@ -18,14 +20,15 @@ public partial class PlaywrightWebDriver // Disable this due to performance issue, some page is too large // result.Body = await p.ContentAsync(); result.IsSuccess = true; - await p.BringToFrontAsync(); + // await p.BringToFrontAsync(); return result; } } - } + }*/ - var page = args.OpenNewTab ? await _instance.NewPage(message.ContextId) : + var page = args.OpenNewTab ? await _instance.NewPage(message.ContextId, fetched: args.OnDataFetched) : _instance.GetPage(message.ContextId); + var response = await page.GotoAsync(args.Url); await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); await page.WaitForLoadStateAsync(LoadState.NetworkIdle); @@ -50,4 +53,14 @@ public partial class PlaywrightWebDriver return result; } + + private void Page_Response1(object sender, IResponse e) + { + throw new NotImplementedException(); + } + + private void Page_Response(object sender, IResponse e) + { + throw new NotImplementedException(); + } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs index c5f7d48c..4bbd84ac 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs @@ -1,4 +1,4 @@ -using System.Xml.Linq; +using System.Web; namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; @@ -88,7 +88,8 @@ public partial class PlaywrightWebDriver }*/ var html = await locator.InnerHTMLAsync(); - result.Body = html; + // fix if html has & + result.Body = HttpUtility.HtmlDecode(html); result.IsSuccess = true; } else if (count > 1) @@ -129,7 +130,7 @@ public partial class PlaywrightWebDriver await page.EvaluateAsync($@" (element) => {{ - element.style.outline = '2px solid red'; + element.style.outline = '2px solid {location.HighlightColor}'; }}", handle); result.IsHighlighted = true;