From 968bc203185d72406649145b548b1f2660fea48e Mon Sep 17 00:00:00 2001 From: Andrey Kerchin Date: Fri, 8 Aug 2025 04:24:10 +0500 Subject: [PATCH 1/4] + SMTP attachments --- .../Agents/SmtpNotificationAgent.cs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/AiCoreApi/SemanticKernel/Agents/SmtpNotificationAgent.cs b/AiCoreApi/SemanticKernel/Agents/SmtpNotificationAgent.cs index dabe2fe..1f9f69d 100644 --- a/AiCoreApi/SemanticKernel/Agents/SmtpNotificationAgent.cs +++ b/AiCoreApi/SemanticKernel/Agents/SmtpNotificationAgent.cs @@ -18,6 +18,7 @@ private static class AgentContentParameters public const string Cc = "cc"; public const string Subject = "subject"; public const string Body = "body"; + public const string Attachments = "attachments"; } private readonly IConnectionProcessor _connectionProcessor; @@ -46,8 +47,9 @@ public override async Task DoCall(AgentModel agent, Dictionary DoCall(AgentModel agent, Dictionary(); + if (!string.IsNullOrWhiteSpace(attachmentsRaw)) + { + var pairs = attachmentsRaw.Split(','); + foreach (var pair in pairs) + { + var parts = pair.Split(':', 2); + if (parts.Length == 2) + { + try + { + var fileName = parts[0].Trim(); + var fileContent = Convert.FromBase64String(parts[1]); + attachments.Add((fileName, fileContent)); + } + catch (FormatException ex) + { + _responseAccessor.AddDebugMessage(_debugMessageSenderName, "Attachment Parse Error", $"Invalid base64 for {parts[0]}: {ex.Message}"); + } + } + } + } + + SendEmail(smtpServer, smtpPort, smtpUser, smtpPass, smtpFrom, recipient, cc, subject, body, attachments); var response = $"Email sent to {recipient}."; _responseAccessor.AddDebugMessage(_debugMessageSenderName, "DoCall Response", response); return response; } - private void SendEmail(string host, int port, string username, string password, string from, string to, string cc, string subject, string body) + private void SendEmail( + string host, + int port, + string username, + string password, + string from, + string to, + string cc, + string subject, + string body, + List<(string FileName, byte[] Content)> attachments) { using var client = new SmtpClient(host, port) { @@ -91,6 +127,13 @@ private void SendEmail(string host, int port, string username, string password, } } + foreach (var (fileName, content) in attachments) + { + var stream = new MemoryStream(content); + var attachment = new Attachment(stream, fileName); + message.Attachments.Add(attachment); + } + client.Send(message); } } From 59bb0b70df45b7b69ff4fb9e7b2e4b0324d80088 Mon Sep 17 00:00:00 2001 From: Andrey Kerchin Date: Tue, 2 Sep 2025 21:27:35 +0500 Subject: [PATCH 2/4] CombinedAuthorize for Delete Agent in order to add it to Pipeline --- AiCoreApi/Controllers/AgentsController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/AiCoreApi/Controllers/AgentsController.cs b/AiCoreApi/Controllers/AgentsController.cs index 22dd17a..e893f30 100644 --- a/AiCoreApi/Controllers/AgentsController.cs +++ b/AiCoreApi/Controllers/AgentsController.cs @@ -68,6 +68,7 @@ public async Task Update([FromBody] AgentViewModel agentViewModel } [HttpDelete("{agentId}")] + [CombinedAuthorize] [RoleAuthorize(Role.Admin, Role.Developer)] public async Task Delete(int agentId) { From df1f12a657984a5cee81461bf54065ea7b22e02a Mon Sep 17 00:00:00 2001 From: Andrey Kerchin Date: Fri, 5 Dec 2025 00:19:05 +0500 Subject: [PATCH 3/4] Google Search Fix --- AiCoreApi/Common/ConnectionManager.cs | 15 ++- AiCoreApi/Common/ParametersHelper.cs | 21 ++-- AiCoreApi/SemanticKernel/Agents/BaseAgent.cs | 2 +- .../SemanticKernel/Agents/BaseAgentHelper.cs | 6 +- .../Agents/GoogleSearchApiAgent.cs | 118 ++++++++++++++++-- 5 files changed, 128 insertions(+), 34 deletions(-) diff --git a/AiCoreApi/Common/ConnectionManager.cs b/AiCoreApi/Common/ConnectionManager.cs index 3ee7279..ce27f41 100644 --- a/AiCoreApi/Common/ConnectionManager.cs +++ b/AiCoreApi/Common/ConnectionManager.cs @@ -6,17 +6,17 @@ namespace AiCoreApi.Common public class ConnectionManager: IConnectionManager { private readonly IConnectionProcessor _connectionProcessor; - private readonly IParametersHelper _parametersHelper; + private readonly IIngestionParametersHelper _ingestionParametersHelper; public ConnectionManager( - IConnectionProcessor connectionProcessor, - IParametersHelper parametersHelper) + IConnectionProcessor connectionProcessor, + IIngestionParametersHelper ingestionParametersHelper) { _connectionProcessor = connectionProcessor; - _parametersHelper = parametersHelper; + _ingestionParametersHelper = ingestionParametersHelper; } - private volatile List? _cache; + private List? _cache; private readonly object _cacheLock = new(); public async Task> GetConnectionModels() @@ -54,7 +54,7 @@ public async Task> GetConnectionModels() WorkspaceId = connection.WorkspaceId, Content = new Dictionary(connection.Content), }; - return await _parametersHelper.ApplySecrets(connectionCopy); + return await _ingestionParametersHelper.ApplySecrets(connectionCopy); } public async Task GetConnection(int? workspaceId = null, int? connectionId = null, ConnectionType? connectionType = null, bool isLlmConnection = false, bool isEmbeddingConnection = false, string? connectionName = null) @@ -67,8 +67,7 @@ public async Task> GetConnectionModels() && (!isLlmConnection || c.Type.IsLlmConnection()) && (!isEmbeddingConnection || c.Type.IsEmbeddingConnection()) && (string.IsNullOrEmpty(connectionName) || c.Name == connectionName) - ) - ?? null; + ); return connection; } diff --git a/AiCoreApi/Common/ParametersHelper.cs b/AiCoreApi/Common/ParametersHelper.cs index 995287d..4ac60a3 100644 --- a/AiCoreApi/Common/ParametersHelper.cs +++ b/AiCoreApi/Common/ParametersHelper.cs @@ -6,41 +6,36 @@ namespace AiCoreApi.Common { - public class ParametersHelper : ParametersHelperBase, IParametersHelper + public class IngestionParametersHelper : ParametersHelperBase, IIngestionParametersHelper { - public ParametersHelper( + public IngestionParametersHelper( RequestAccessor requestAccessor, ICacheAccessor cacheAccessor, ResponseAccessor responseAccessor, - ILogger logger, + ILogger logger, IEntraTokenProvider entraTokenProvider) : base(requestAccessor, cacheAccessor, responseAccessor, logger, entraTokenProvider) { } - - protected override string ResolveUnknownKey(string key) - { - return $"{{{{{key}}}}}"; - } } - public interface IParametersHelper + public interface IIngestionParametersHelper { Task ApplySecrets(ConnectionModel connectionModel); Task ApplySecret(string value); Task ApplyParametersAsync(string text, string debugSource, Dictionary? parameters, Dictionary? additionalParameters = null); } - public class IngestionParametersHelper : ParametersHelperBase, IIngestionParametersHelper + public class ParametersHelper : ParametersHelperBase, IParametersHelper { private readonly IIngestionProcessor _ingestionProcessor; private readonly IDataIngestionWorkerFactory _workerFactory; - public IngestionParametersHelper( + public ParametersHelper( RequestAccessor requestAccessor, ICacheAccessor cacheAccessor, ResponseAccessor responseAccessor, - ILogger logger, + ILogger logger, IEntraTokenProvider entraTokenProvider, IIngestionProcessor ingestionProcessor, IDataIngestionWorkerFactory workerFactory) @@ -89,7 +84,7 @@ private async Task ResolveDataSourceValueAsync(string key) } } - public interface IIngestionParametersHelper + public interface IParametersHelper { Task ApplySecrets(ConnectionModel connectionModel); Task ApplySecret(string value); diff --git a/AiCoreApi/SemanticKernel/Agents/BaseAgent.cs b/AiCoreApi/SemanticKernel/Agents/BaseAgent.cs index 4c9b95a..ef21c12 100644 --- a/AiCoreApi/SemanticKernel/Agents/BaseAgent.cs +++ b/AiCoreApi/SemanticKernel/Agents/BaseAgent.cs @@ -14,7 +14,7 @@ public abstract class BaseAgent : IDoCallWrapperAgent private readonly ResponseAccessor _responseAccessor; private readonly RequestAccessor _requestAccessor; private readonly MonitoringConfig _monitoringConfig; - private readonly IIngestionParametersHelper _parametersHelper; + private readonly IParametersHelper _parametersHelper; private readonly ILogger _logger; private Dictionary? _parameters; diff --git a/AiCoreApi/SemanticKernel/Agents/BaseAgentHelper.cs b/AiCoreApi/SemanticKernel/Agents/BaseAgentHelper.cs index 7e53b8c..bf7674c 100644 --- a/AiCoreApi/SemanticKernel/Agents/BaseAgentHelper.cs +++ b/AiCoreApi/SemanticKernel/Agents/BaseAgentHelper.cs @@ -11,7 +11,7 @@ public BaseAgentHelper( RequestAccessor requestAccessor, ResponseAccessor responseAccessor, MonitoringConfig monitoringConfig, - IIngestionParametersHelper parametersHelper + IParametersHelper parametersHelper ) { AgentsProcessor = agentsProcessor; @@ -25,7 +25,7 @@ IIngestionParametersHelper parametersHelper public RequestAccessor RequestAccessor { get; } public ResponseAccessor ResponseAccessor { get; } public MonitoringConfig MonitoringConfig { get; } - public IIngestionParametersHelper ParametersHelper { get; } + public IParametersHelper ParametersHelper { get; } } public interface IBaseAgentHelper @@ -34,6 +34,6 @@ public interface IBaseAgentHelper RequestAccessor RequestAccessor { get; } ResponseAccessor ResponseAccessor { get; } MonitoringConfig MonitoringConfig { get; } - IIngestionParametersHelper ParametersHelper { get; } + IParametersHelper ParametersHelper { get; } } } diff --git a/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs b/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs index a638546..323d09a 100644 --- a/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs +++ b/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs @@ -6,6 +6,7 @@ using System.Text.Json; using AiCoreApi.Common.Extensions; using System.Text.Encodings.Web; +using Microsoft.Playwright; namespace AiCoreApi.SemanticKernel.Agents { @@ -26,9 +27,10 @@ private static class AgentContentParameters public const string QueryString = "queryString"; public const string GoogleConnection = "googleSearchApiConnection"; public const string MaxContentLength = "maxContentLength"; - public const string Count = "count"; - public const string Offset = "offset"; + public const string Count = "count"; + public const string Offset = "offset"; public const string OutputType = "outputType"; + public const string WaitTimeout = "waitTimeout"; } private const int DefaultMaxContentLength = 16384; @@ -37,6 +39,7 @@ private static class AgentContentParameters private readonly ResponseAccessor _responseAccessor; private readonly IHttpClientFactory _httpClientFactory; private readonly IConnectionProcessor _connectionProcessor; + private readonly ExtendedConfig _extendedConfig; public GoogleSearchApiAgent( IBaseAgentHelper baseAgentHelper, @@ -44,12 +47,14 @@ public GoogleSearchApiAgent( ResponseAccessor responseAccessor, IHttpClientFactory httpClientFactory, IConnectionProcessor connectionProcessor, + ExtendedConfig extendedConfig, ILogger logger) : base(baseAgentHelper, logger) { _requestAccessor = requestAccessor; _responseAccessor = responseAccessor; _httpClientFactory = httpClientFactory; _connectionProcessor = connectionProcessor; + _extendedConfig = extendedConfig; } public override async Task DoCall(AgentModel agent, Dictionary parameters) @@ -75,6 +80,7 @@ public override async Task DoCall(AgentModel agent, Dictionary DoCall(AgentModel agent, Dictionary int.Parse(maxContentLength)) text = text.Substring(0, int.Parse(maxContentLength)); - pages.Add(new Dictionary { { "url", page.Url }, { "name", page.Name }, { "text", text } }); + pages.Add(new Dictionary + { + { "url", page.Url }, + { "name", page.Name }, + { "text", text } + }); } - result = JsonSerializer.Serialize(pages, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + + result = JsonSerializer.Serialize(pages, + new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + } + else if (outputType == "pagesJsonPlaywright") + { + // NEW MODE — PLAYWRIGHT PAGE LOADING + + PlaywrightInstall.EnsureInstalled(); + var playwright = await Playwright.CreateAsync(); + + await using var browser = await playwright.Chromium.LaunchAsync(new() { Headless = true }); + + var context = await browser.NewContextAsync(new BrowserNewContextOptions + { + UserAgent = "Mozilla/5.0 (compatible; GoogleSearchApiAgent/Playwright)", + Proxy = string.IsNullOrEmpty(_extendedConfig.Proxy) + ? null + : new Proxy { Server = _extendedConfig.Proxy }, + IgnoreHTTPSErrors = true + }); + + var pageObj = await context.NewPageAsync(); + + var pages = new List>(); + + foreach (var page in results) + { + var text = await CrawlWithPlaywrightAsync(page.Url, pageObj, waitTimeout); + + if (text.Length > int.Parse(maxContentLength)) + text = text.Substring(0, int.Parse(maxContentLength)); + + pages.Add(new Dictionary + { + { "url", page.Url }, + { "name", page.Name }, + { "text", text } + }); + } + + result = JsonSerializer.Serialize(pages, + new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); } else { - result = JsonSerializer.Serialize(results.Select(r => r.Snippet).ToList(), new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + // snippetTexts — default + result = JsonSerializer.Serialize( + results.Select(r => r.Snippet).ToList(), + new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); } _responseAccessor.AddDebugMessage(_debugMessageSenderName, "Execute Query String Result", result); @@ -125,15 +181,21 @@ private async Task> DoSearchAsync(string query, string apiKey, str { results.Add(new WebPage { - Name = item.GetProperty("title").GetString() ?? "", - Url = item.GetProperty("link").GetString() ?? "", - Snippet = item.GetProperty("snippet").GetString() ?? "" + Name = SafeGet(item, "title"), + Url = SafeGet(item, "link"), + Snippet = SafeGet(item, "snippet") }); } - return results; } + private static string SafeGet(JsonElement el, string propName) + { + return el.TryGetProperty(propName, out var p) + ? p.GetString() ?? "" + : ""; + } + private async Task CrawlPageTextAsync(string url) { try @@ -166,6 +228,44 @@ private async Task CrawlPageTextAsync(string url) } } + private async Task CrawlWithPlaywrightAsync(string url, IPage page, int waitTimeout) + { + try + { + await page.GotoAsync(url, new() { Timeout = waitTimeout }); + + var allText = new List(); + + async Task ProcessFrame(IFrame frame) + { + var content = await frame.ContentAsync(); + var doc = new HtmlDocument(); + doc.LoadHtml(content); + + doc.DocumentNode.Descendants() + .Where(n => n.Name is "script" or "style") + .ToList() + .ForEach(n => n.Remove()); + + var frameText = HtmlEntity.DeEntitize(doc.DocumentNode.InnerText); + + allText.AddRange( + frameText.Split('\n') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x))); + } + + foreach (var frame in page.Frames) + await ProcessFrame(frame); + + return string.Join("\n", allText); + } + catch (Exception ex) + { + _responseAccessor.AddDebugMessage(_debugMessageSenderName, "Playwright Error", $"Failed to read {url}, {ex.Message}"); + return string.Empty; + } + } private async Task SendGetRequestAsync(Uri uri, CancellationToken cancellationToken = default) { From 356131888d03f23e41fa46eae498b08f9920d621 Mon Sep 17 00:00:00 2001 From: Andrey Kerchin Date: Fri, 5 Dec 2025 00:32:13 +0500 Subject: [PATCH 4/4] review notes --- AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs b/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs index 323d09a..c83937f 100644 --- a/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs +++ b/AiCoreApi/SemanticKernel/Agents/GoogleSearchApiAgent.cs @@ -114,11 +114,11 @@ public override async Task DoCall(AgentModel agent, Dictionary