From dfd2881847656d34bcf81a526eee7f574a8b9151 Mon Sep 17 00:00:00 2001 From: Devis Lucato Date: Mon, 25 Nov 2024 16:13:17 -0800 Subject: [PATCH] .NET Connector improvements (#263) --- .../dotnet-01-echo-bot.csproj | 5 +- .../dotnet-02-message-types-demo/MyAgent.cs | 28 +- .../dotnet-02-message-types-demo.csproj | 6 +- .../dotnet-03-simple-chatbot/MyAgent.cs | 9 +- .../MyWorkbenchConnector.cs | 5 +- .../dotnet-03-simple-chatbot/Program.cs | 4 +- .../Properties/launchSettings.json | 21 ++ .../dotnet-03-simple-chatbot/appsettings.json | 28 +- .../dotnet-03-simple-chatbot.csproj | 14 +- .../dotnet/SemanticWorkbench.sln.DotSettings | 304 +++++++++++++++++- .../AgentConfig/AgentConfigBase.cs | 10 +- .../WorkbenchConnector/Models/Conversation.cs | 4 +- .../WorkbenchConnector/Models/ServiceInfo.cs | 2 +- .../dotnet/WorkbenchConnector/Webservice.cs | 6 +- .../WorkbenchConnector/WorkbenchConfig.cs | 15 +- .../WorkbenchConnector/WorkbenchConnector.cs | 172 ++++++++-- .../WorkbenchConnector.csproj | 6 +- 17 files changed, 551 insertions(+), 88 deletions(-) create mode 100644 examples/dotnet/dotnet-03-simple-chatbot/Properties/launchSettings.json diff --git a/examples/dotnet/dotnet-01-echo-bot/dotnet-01-echo-bot.csproj b/examples/dotnet/dotnet-01-echo-bot/dotnet-01-echo-bot.csproj index d26fe821..aba6760c 100644 --- a/examples/dotnet/dotnet-01-echo-bot/dotnet-01-echo-bot.csproj +++ b/examples/dotnet/dotnet-01-echo-bot/dotnet-01-echo-bot.csproj @@ -6,6 +6,7 @@ enable AgentExample AgentExample + $(NoWarn);CA1515;IDE0290; @@ -27,7 +28,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,7 +36,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/examples/dotnet/dotnet-02-message-types-demo/MyAgent.cs b/examples/dotnet/dotnet-02-message-types-demo/MyAgent.cs index cf865fab..218fbd00 100644 --- a/examples/dotnet/dotnet-02-message-types-demo/MyAgent.cs +++ b/examples/dotnet/dotnet-02-message-types-demo/MyAgent.cs @@ -87,20 +87,20 @@ public override Task ReceiveMessageAsync( Message message, CancellationToken cancellationToken = default) { - switch (this.Config.Behavior.ToLowerInvariant()) + return this.Config.Behavior.ToLowerInvariant() switch { - case "echo": return this.EchoExampleAsync(conversationId, message, cancellationToken); - case "reverse": return this.ReverseExampleAsync(conversationId, message, cancellationToken); - case "safety check": return this.SafetyCheckExampleAsync(conversationId, message, cancellationToken); - case "markdown sample": return this.MarkdownExampleAsync(conversationId, message, cancellationToken); - case "html sample": return this.HTMLExampleAsync(conversationId, message, cancellationToken); - case "code sample": return this.CodeExampleAsync(conversationId, message, cancellationToken); - case "json sample": return this.JSONExampleAsync(conversationId, message, cancellationToken); - case "mermaid sample": return this.MermaidExampleAsync(conversationId, message, cancellationToken); - case "music sample": return this.MusicExampleAsync(conversationId, message, cancellationToken); - case "none": return this.NoneExampleAsync(conversationId, message, cancellationToken); - default: return this.NoneExampleAsync(conversationId, message, cancellationToken); - } + "echo" => this.EchoExampleAsync(conversationId, message, cancellationToken), + "reverse" => this.ReverseExampleAsync(conversationId, message, cancellationToken), + "safety check" => this.SafetyCheckExampleAsync(conversationId, message, cancellationToken), + "markdown sample" => this.MarkdownExampleAsync(conversationId, message, cancellationToken), + "html sample" => this.HTMLExampleAsync(conversationId, message, cancellationToken), + "code sample" => this.CodeExampleAsync(conversationId, message, cancellationToken), + "json sample" => this.JSONExampleAsync(conversationId, message, cancellationToken), + "mermaid sample" => this.MermaidExampleAsync(conversationId, message, cancellationToken), + "music sample" => this.MusicExampleAsync(conversationId, message, cancellationToken), + "none" => this.NoneExampleAsync(conversationId, message, cancellationToken), + _ => this.NoneExampleAsync(conversationId, message, cancellationToken) + }; } // Check text with Azure Content Safety @@ -111,7 +111,7 @@ public override Task ReceiveMessageAsync( Response? result = await this._contentSafety.AnalyzeTextAsync(text, cancellationToken).ConfigureAwait(false); bool isSafe = result.HasValue && result.Value.CategoriesAnalysis.All(x => x.Severity is 0); - IEnumerable report = result.HasValue ? result.Value.CategoriesAnalysis.Select(x => $"{x.Category}: {x.Severity}") : Array.Empty(); + IEnumerable report = result.HasValue ? result.Value.CategoriesAnalysis.Select(x => $"{x.Category}: {x.Severity}") : []; return (isSafe, report); } diff --git a/examples/dotnet/dotnet-02-message-types-demo/dotnet-02-message-types-demo.csproj b/examples/dotnet/dotnet-02-message-types-demo/dotnet-02-message-types-demo.csproj index 0610a1df..558bb740 100644 --- a/examples/dotnet/dotnet-02-message-types-demo/dotnet-02-message-types-demo.csproj +++ b/examples/dotnet/dotnet-02-message-types-demo/dotnet-02-message-types-demo.csproj @@ -6,7 +6,7 @@ enable AgentExample AgentExample - $(NoWarn);CA1308;CA1861; + $(NoWarn);CA1308;CA1861;CA1515;IDE0290; @@ -31,7 +31,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -39,7 +39,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/examples/dotnet/dotnet-03-simple-chatbot/MyAgent.cs b/examples/dotnet/dotnet-03-simple-chatbot/MyAgent.cs index a25ede73..e8045e2c 100644 --- a/examples/dotnet/dotnet-03-simple-chatbot/MyAgent.cs +++ b/examples/dotnet/dotnet-03-simple-chatbot/MyAgent.cs @@ -102,7 +102,14 @@ public override async Task ReceiveMessageAsync( { // Show some status while working... await this.SetAgentStatusAsync(conversationId, "Thinking...", cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + this.Log.LogWarning(e, "Something went wrong while setting temporary status"); + } + try + { // Update the chat history to include the message received var conversation = await base.AddMessageToHistoryAsync(conversationId, message, cancellationToken).ConfigureAwait(false); @@ -319,7 +326,7 @@ private IChatCompletionService GetChatCompletionService() Response? result = await this._contentSafety.AnalyzeTextAsync(text, cancellationToken).ConfigureAwait(false); bool isSafe = result.HasValue && result.Value.CategoriesAnalysis.All(x => x.Severity is 0); - IEnumerable report = result.HasValue ? result.Value.CategoriesAnalysis.Select(x => $"{x.Category}: {x.Severity}") : Array.Empty(); + IEnumerable report = result.HasValue ? result.Value.CategoriesAnalysis.Select(x => $"{x.Category}: {x.Severity}") : []; return (isSafe, report); } diff --git a/examples/dotnet/dotnet-03-simple-chatbot/MyWorkbenchConnector.cs b/examples/dotnet/dotnet-03-simple-chatbot/MyWorkbenchConnector.cs index 7ac2098b..1b4d6068 100644 --- a/examples/dotnet/dotnet-03-simple-chatbot/MyWorkbenchConnector.cs +++ b/examples/dotnet/dotnet-03-simple-chatbot/MyWorkbenchConnector.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticWorkbench.Connector; namespace AgentExample; -public sealed class MyWorkbenchConnector : WorkbenchConnector +internal sealed class MyWorkbenchConnector : WorkbenchConnector { private readonly IServiceProvider _sp; private readonly IConfiguration _appConfig; @@ -15,11 +16,13 @@ public MyWorkbenchConnector( IServiceProvider sp, IConfiguration appConfig, IAgentServiceStorage storage, + IServer httpServer, ILoggerFactory? loggerFactory = null) : base( workbenchConfig: appConfig.GetSection("Workbench").Get(), defaultAgentConfig: appConfig.GetSection("Agent").Get(), storage: storage, + httpServer: httpServer, logger: loggerFactory?.CreateLogger() ?? new NullLogger()) { this._sp = sp; diff --git a/examples/dotnet/dotnet-03-simple-chatbot/Program.cs b/examples/dotnet/dotnet-03-simple-chatbot/Program.cs index 02567a29..3cbc9a53 100644 --- a/examples/dotnet/dotnet-03-simple-chatbot/Program.cs +++ b/examples/dotnet/dotnet-03-simple-chatbot/Program.cs @@ -35,8 +35,8 @@ internal static async Task Main(string[] args) app.UseCors(CORSPolicyName); // Connect to workbench backend, keep alive, and accept incoming requests - var connectorEndpoint = app.Configuration.GetSection("Workbench").Get()!.ConnectorEndpoint; - using var agentService = app.UseAgentWebservice(connectorEndpoint, true); + var connectorApiPrefix = app.Configuration.GetSection("Workbench").Get()!.ConnectorApiPrefix; + using var agentService = app.UseAgentWebservice(connectorApiPrefix, true); await agentService.ConnectAsync().ConfigureAwait(false); // Start app and webservice diff --git a/examples/dotnet/dotnet-03-simple-chatbot/Properties/launchSettings.json b/examples/dotnet/dotnet-03-simple-chatbot/Properties/launchSettings.json new file mode 100644 index 00000000..b8f34920 --- /dev/null +++ b/examples/dotnet/dotnet-03-simple-chatbot/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/examples/dotnet/dotnet-03-simple-chatbot/appsettings.json b/examples/dotnet/dotnet-03-simple-chatbot/appsettings.json index a7dfef8d..4cc1f4d3 100644 --- a/examples/dotnet/dotnet-03-simple-chatbot/appsettings.json +++ b/examples/dotnet/dotnet-03-simple-chatbot/appsettings.json @@ -4,11 +4,17 @@ // Unique ID of the service. Semantic Workbench will store this event to identify the server // so you should keep the value fixed to match the conversations tracked across service restarts. "ConnectorId": "AgentExample03", - // The endpoint of your service, where semantic workbench will send communications too. - // This should match hostname, port, protocol and path of the web service. You can use - // this also to route semantic workbench through a proxy or a gateway if needed. - "ConnectorEndpoint": "http://127.0.0.1:9103/myagents", - // Semantic Workbench endpoint. + // The host where the connector receives requests sent by the workbench. + // Locally, this is usually "http://127.0.0.1:" + // On Azure, this will be something like "https://contoso.azurewebsites.net" + // Leave this setting empty to use "127.0.0.1" and autodetect the port in use. + // You can use an env var to set this value, e.g. Workbench__ConnectorHost=https://contoso.azurewebsites.net + "ConnectorHost": "", + // This is the prefix of all the endpoints exposed by the connector + "ConnectorApiPrefix": "/myagents", + // Semantic Workbench backend endpoint. + // The connector connects to this workbench endpoint to register its presence. + // The workbench connects back to the connector to send events (see ConnectorHost and ConnectorApiPrefix). "WorkbenchEndpoint": "http://127.0.0.1:3000", // Name of your agent service "ConnectorName": ".NET Multi Agent Service", @@ -48,18 +54,6 @@ "Endpoint": "https://api.openai.com/v1/", "ApiKey": "sk-..." }, - // Web service settings - "AllowedHosts": "*", - "Kestrel": { - "Endpoints": { - "Http": { - "Url": "http://*:9103" - } - // "Https": { - // "Url": "https://*:19103 - // } - } - }, // .NET Logger settings "Logging": { "LogLevel": { diff --git a/examples/dotnet/dotnet-03-simple-chatbot/dotnet-03-simple-chatbot.csproj b/examples/dotnet/dotnet-03-simple-chatbot/dotnet-03-simple-chatbot.csproj index 1da7a33e..76de73c4 100644 --- a/examples/dotnet/dotnet-03-simple-chatbot/dotnet-03-simple-chatbot.csproj +++ b/examples/dotnet/dotnet-03-simple-chatbot/dotnet-03-simple-chatbot.csproj @@ -6,13 +6,9 @@ enable AgentExample AgentExample - $(NoWarn);SKEXP0010;CA1861; + $(NoWarn);SKEXP0010;CA1861;CA1515;IDE0290;CA1031;CA1812; - - - - @@ -34,7 +30,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -42,7 +38,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -60,4 +56,8 @@ + + + + \ No newline at end of file diff --git a/libraries/dotnet/SemanticWorkbench.sln.DotSettings b/libraries/dotnet/SemanticWorkbench.sln.DotSettings index 8732803f..cf383862 100644 --- a/libraries/dotnet/SemanticWorkbench.sln.DotSettings +++ b/libraries/dotnet/SemanticWorkbench.sln.DotSettings @@ -1,8 +1,306 @@  + False + False + True + True + FullFormat + True + True + True + True + True + SOLUTION + False + SUGGESTION + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + SUGGESTION + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + ERROR + True + Field, Property, Event, Method + True + True + True + NEXT_LINE + True + True + True + True + True + True + 1 + 1 + True + True + True + ALWAYS + True + True + False + 512 + True + Copyright (c) Microsoft. All rights reserved. ABC + ACS AI + AIGPT + AMQP + API + BOM CORS - HTML + DB + DI + GPT + GRPC + HMAC + HTTP + IM + IO + IOS JSON - LLM - UI \ No newline at end of file + JWT + HTML + MQ + MQTT + MS + MSAL + OCR + OID + OK + OS + PR + QA + SHA + SK + SKHTTP + SSL + TTL + UI + UID + URL + XML + YAML + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"><ElementKinds><Kind Name="LOCAL_VARIABLE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local functions"><ElementKinds><Kind Name="LOCAL_FUNCTION" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"><ElementKinds><Kind Name="PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /></Policy> + + 2 + False + True + Console + PushToShowHints + True + True + True + True + True + True + True + True + True + True + True + True + False + TRACE + 8201 + Automatic + True + True + True + True + True + 2.0 + InCSharpFile + pragma + True + #pragma warning disable CA0000 // reason + +#pragma warning restore CA0000 + True + True + False + True + guid() + 0 + True + True + False + False + True + 2.0 + InCSharpFile + aaa + True + [Fact] +public void It$SOMENAME$() +{ + // Arrange + + // Act + + // Assert + +} + True + True + MSFT copyright + True + 2.0 + InCSharpFile + copy + // Copyright (c) Microsoft. All rights reserved. + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + DO_NOT_SHOW + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + diff --git a/libraries/dotnet/WorkbenchConnector/AgentConfig/AgentConfigBase.cs b/libraries/dotnet/WorkbenchConnector/AgentConfig/AgentConfigBase.cs index 739d0b9b..2cb399f7 100644 --- a/libraries/dotnet/WorkbenchConnector/AgentConfig/AgentConfigBase.cs +++ b/libraries/dotnet/WorkbenchConnector/AgentConfig/AgentConfigBase.cs @@ -10,11 +10,11 @@ public abstract class AgentConfigBase { public object ToWorkbenchFormat() { - Dictionary result = new(); - Dictionary defs = new(); - Dictionary properties = new(); - Dictionary jsonSchema = new(); - Dictionary uiSchema = new(); + Dictionary result = []; + Dictionary defs = []; + Dictionary properties = []; + Dictionary jsonSchema = []; + Dictionary uiSchema = []; foreach (var property in this.GetType().GetProperties()) { diff --git a/libraries/dotnet/WorkbenchConnector/Models/Conversation.cs b/libraries/dotnet/WorkbenchConnector/Models/Conversation.cs index e437ff26..f24bbd91 100644 --- a/libraries/dotnet/WorkbenchConnector/Models/Conversation.cs +++ b/libraries/dotnet/WorkbenchConnector/Models/Conversation.cs @@ -19,11 +19,11 @@ public class Conversation [JsonPropertyName("participants")] [JsonPropertyOrder(2)] - public Dictionary Participants { get; set; } = new(); + public Dictionary Participants { get; set; } = []; [JsonPropertyName("messages")] [JsonPropertyOrder(3)] - public List Messages { get; set; } = new(); + public List Messages { get; set; } = []; public Conversation() { diff --git a/libraries/dotnet/WorkbenchConnector/Models/ServiceInfo.cs b/libraries/dotnet/WorkbenchConnector/Models/ServiceInfo.cs index 9d1e2402..8aa72b3a 100644 --- a/libraries/dotnet/WorkbenchConnector/Models/ServiceInfo.cs +++ b/libraries/dotnet/WorkbenchConnector/Models/ServiceInfo.cs @@ -19,7 +19,7 @@ public class ServiceInfo(TAgentConfig cfg) public string Description { get; set; } = string.Empty; [JsonPropertyName("metadata")] - public Dictionary Metadata { get; set; } = new(); + public Dictionary Metadata { get; set; } = []; [JsonPropertyName("default_config")] public object DefaultConfiguration => cfg.ToWorkbenchFormat() ?? new(); diff --git a/libraries/dotnet/WorkbenchConnector/Webservice.cs b/libraries/dotnet/WorkbenchConnector/Webservice.cs index 811a7fe7..26f64eb8 100644 --- a/libraries/dotnet/WorkbenchConnector/Webservice.cs +++ b/libraries/dotnet/WorkbenchConnector/Webservice.cs @@ -26,7 +26,7 @@ private sealed class SemanticWorkbenchWebservice } public static WorkbenchConnector UseAgentWebservice( - this IEndpointRouteBuilder builder, string endpoint, bool enableCatchAll = false) + this IEndpointRouteBuilder builder, string prefix, bool enableCatchAll = false) where TAgentConfig : AgentConfigBase, new() { WorkbenchConnector? workbenchConnector = builder.ServiceProvider.GetService>(); @@ -35,8 +35,6 @@ public static WorkbenchConnector UseAgentWebservice( throw new InvalidOperationException("Unable to create instance of " + nameof(WorkbenchConnector)); } - string prefix = new Uri(endpoint).AbsolutePath; - builder .UseFetchServiceInfo(prefix) .UseCreateAgentEndpoint(prefix) @@ -525,7 +523,7 @@ private static IEndpointRouteBuilder UseCreateConversationEventEndpoint - /// The endpoint of your service, where semantic workbench will send communications too. - /// This should match hostname, port, protocol and path of the web service. You can use - /// this also to route semantic workbench through a proxy or a gateway if needed. + /// The host where the connector receives requests sent by the workbench. + /// Locally, this is usually "http://127.0.0.1:[some port]" + /// On Azure, this will be something like "https://contoso.azurewebsites.net" + /// Leave this setting empty to use "127.0.0.1" and autodetect the port in use. + /// You can use an env var to set this value, e.g. Workbench__ConnectorHost=https://contoso.azurewebsites.net /// - public string ConnectorEndpoint { get; set; } = "http://127.0.0.1:9001/myagents"; + public string ConnectorHost { get; set; } = string.Empty; + + /// + /// This is the prefix of all the endpoints exposed by the connector + /// + public string ConnectorApiPrefix { get; set; } = "/myagents"; /// /// Unique ID of the service. Semantic Workbench will store this event to identify the server diff --git a/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.cs b/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.cs index c67d0934..3dd3288c 100644 --- a/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.cs +++ b/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.cs @@ -8,6 +8,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.Logging; namespace Microsoft.SemanticWorkbench.Connector; @@ -19,25 +21,33 @@ public abstract class WorkbenchConnector : IDisposable protected WorkbenchConfig WorkbenchConfig { get; private set; } protected TAgentConfig DefaultAgentConfig { get; private set; } protected HttpClient HttpClient { get; private set; } + protected string ConnectorEndpoint { get; private set; } = string.Empty; protected ILogger Log { get; private set; } protected Dictionary> Agents { get; private set; } + private Timer? _initTimer; private Timer? _pingTimer; + private readonly IServer _httpServer; + protected WorkbenchConnector( WorkbenchConfig? workbenchConfig, TAgentConfig? defaultAgentConfig, IAgentServiceStorage storage, + IServer httpServer, ILogger logger) { + this._httpServer = httpServer; this.WorkbenchConfig = workbenchConfig ?? new(); this.DefaultAgentConfig = defaultAgentConfig ?? new(); this.Log = logger; this.Storage = storage; - this.HttpClient = new HttpClient(); - this.HttpClient.BaseAddress = new Uri(this.WorkbenchConfig.WorkbenchEndpoint); - this.Agents = new Dictionary>(); + this.HttpClient = new HttpClient + { + BaseAddress = new Uri(this.WorkbenchConfig.WorkbenchEndpoint) + }; + this.Agents = []; this.Log.LogTrace("Service instance created"); } @@ -61,9 +71,11 @@ public virtual ServiceInfo GetServiceInfo() /// Async task cancellation token public virtual async Task ConnectAsync(CancellationToken cancellationToken = default) { - this.Log.LogInformation("Connecting {1} {2} {3}...", this.WorkbenchConfig.ConnectorName, this.WorkbenchConfig.ConnectorId, this.WorkbenchConfig.ConnectorEndpoint); + this.Log.LogInformation("Connecting {ConnectorName} {ConnectorId} to {WorkbenchEndpoint}...", + this.WorkbenchConfig.ConnectorName, this.WorkbenchConfig.ConnectorId, this.WorkbenchConfig.WorkbenchEndpoint); #pragma warning disable CS4014 // ping runs in the background without blocking - this._pingTimer ??= new Timer(_ => this.PingSemanticWorkbenchBackendAsync(cancellationToken), null, 0, 10000); + this._initTimer ??= new Timer(_ => this.Init(), null, 0, 500); + this._pingTimer ??= new Timer(_ => this.PingSemanticWorkbenchBackendAsync(cancellationToken), null, Timeout.Infinite, Timeout.Infinite); #pragma warning restore CS4014 List agents = await this.Storage.GetAllAgentsAsync(cancellationToken).ConfigureAwait(false); @@ -132,7 +144,7 @@ public virtual async Task DeleteAgentAsync( /// /// Set a state content, visible in the state inspector. - /// The content is visibile in the state inspector, on the right side panel. + /// The content is visible in the state inspector, on the right side panel. /// /// Agent instance ID /// Conversation ID @@ -370,25 +382,100 @@ public virtual async Task DeleteFileAsync( await this.SendAsync(HttpMethod.Delete, url, null, agentId, "DeleteFile", cancellationToken).ConfigureAwait(false); } + public virtual void DisablePingTimer() + { + this._pingTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + + public virtual void DisableInitTimer() + { + this._initTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + + public virtual void EnablePingTimer() + { + this._pingTimer?.Change(TimeSpan.FromMilliseconds(PingFrequencyMS), TimeSpan.FromMilliseconds(PingFrequencyMS)); + } + + public virtual void EnableInitTimer() + { + this._initTimer?.Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(500)); + } + + /// + /// Detect the port where the connector is listening and define the value of this.ConnectorEndpoint + /// which is then passed to the workbench backend for incoming connections. + /// + public virtual void Init() + { + this.DisableInitTimer(); + + if (!string.IsNullOrWhiteSpace(this.ConnectorEndpoint)) + { + this.Log.LogTrace("Init complete, connector endpoint: {Endpoint}", this.ConnectorEndpoint); + this.EnablePingTimer(); + return; + } + + try + { + this.Log.LogTrace("Init in progress..."); + IServerAddressesFeature? feat = this._httpServer.Features.Get(); + if (feat == null || feat.Addresses.Count == 0) + { + this.EnableInitTimer(); + return; + } + + // Example: http://[::]:64351 + string first = feat.Addresses.First().Replace("[::]", "host", StringComparison.OrdinalIgnoreCase); + Uri uri = new(first); + this.ConnectorEndpoint = uri.Port > 0 + ? $"{uri.Scheme}://127.0.0.1:{uri.Port}/{this.WorkbenchConfig.ConnectorApiPrefix.TrimStart('/')}" + : $"{uri.Scheme}://127.0.0.1:/{this.WorkbenchConfig.ConnectorApiPrefix.TrimStart('/')}"; + + this.Log.LogTrace("Init complete, connector endpoint: {Endpoint}", this.ConnectorEndpoint); + this.EnablePingTimer(); + } +#pragma warning disable CA1031 + catch (Exception e) + { + this.Log.LogError(e, "Init error"); + this.EnableInitTimer(); + } +#pragma warning restore CA1031 + } + public virtual async Task PingSemanticWorkbenchBackendAsync(CancellationToken cancellationToken) { - this.Log.LogTrace("Pinging workbench backend"); - string path = Constants.AgentServiceRegistration.Path - .Replace(Constants.AgentServiceRegistration.Placeholder, this.WorkbenchConfig.ConnectorId, StringComparison.OrdinalIgnoreCase); + this.DisablePingTimer(); - var data = new + try { - name = $"{this.WorkbenchConfig.ConnectorName} [{this.WorkbenchConfig.ConnectorId}]", - description = this.WorkbenchConfig.ConnectorDescription, - url = this.WorkbenchConfig.ConnectorEndpoint, - online_expires_in_seconds = 20 - }; + string path = Constants.AgentServiceRegistration.Path + .Replace(Constants.AgentServiceRegistration.Placeholder, this.WorkbenchConfig.ConnectorId, StringComparison.OrdinalIgnoreCase); + this.Log.LogTrace("Pinging workbench backend at {Path}", path); - await this.SendAsync(HttpMethod.Put, path, data, null, "PingSWBackend", cancellationToken).ConfigureAwait(false); + var data = new + { + name = $"{this.WorkbenchConfig.ConnectorName} [{this.WorkbenchConfig.ConnectorId}]", + description = this.WorkbenchConfig.ConnectorDescription, + url = this.ConnectorEndpoint, + online_expires_in_seconds = 2 + (int)(PingFrequencyMS / 1000) + }; + + await this.SendAsync(HttpMethod.Put, path, data, null, "PingSWBackend", cancellationToken).ConfigureAwait(false); + } + finally + { + this.EnablePingTimer(); + } } #region internals =========================================================================== + private const int PingFrequencyMS = 20000; + public void Dispose() { this.Dispose(true); @@ -413,20 +500,25 @@ protected virtual async Task SendAsync( string description, CancellationToken cancellationToken) { + url = url.TrimStart('/'); try { this.Log.LogTrace("Preparing request: {2}", description); HttpRequestMessage request = this.PrepareRequest(method, url, data, agentId); - this.Log.LogTrace("Sending request {0} {1} [{2}]", method, url.HtmlEncode(), description); + this.Log.LogTrace("Sending request {Method} {BaseAddress}{Path} [{Description}]", method, this.HttpClient.BaseAddress, url, description); + this.Log.LogTrace("{0}: {1}", description, ToCurl(this.HttpClient, request, data)); HttpResponseMessage result = await this.HttpClient .SendAsync(request, cancellationToken) .ConfigureAwait(false); request.Dispose(); + this.Log.LogTrace("Response status code: {StatusCodeInt} {StatusCode}", (int)result.StatusCode, result.StatusCode); + return result; } catch (HttpRequestException e) { - this.Log.LogError("HTTP request failed: {0}. Request: {1} {2} [{3}]", e.Message.HtmlEncode(), method, url.HtmlEncode(), description); + this.Log.LogError("HTTP request failed: {Message} [{Error}, {Exception}, Status Code: {StatusCode}]. Request: {Method} {URL} [{RequestDescription}]", + e.Message.HtmlEncode(), e.HttpRequestError.ToString("G"), e.GetType().FullName, e.StatusCode, method, url, description); throw; } catch (Exception e) @@ -445,17 +537,59 @@ protected virtual HttpRequestMessage PrepareRequest( HttpRequestMessage request = new(method, url); if (Constants.HttpMethodsWithBody.Contains(method)) { - request.Content = new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json"); + var json = JsonSerializer.Serialize(data); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + this.Log.LogTrace("Request body: {Content}", json); } request.Headers.Add(Constants.HeaderServiceId, this.WorkbenchConfig.ConnectorId); + this.Log.LogTrace("Request header: {Content}: {Value}", Constants.HeaderServiceId, this.WorkbenchConfig.ConnectorId); if (!string.IsNullOrEmpty(agentId)) { request.Headers.Add(Constants.HeaderAgentId, agentId); + this.Log.LogTrace("Request header: {Content}: {Value}", Constants.HeaderAgentId, agentId); } return request; } +#pragma warning disable CA1305 + private static string ToCurl(HttpClient httpClient, HttpRequestMessage? request, object? data) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(request.RequestUri); + + var curl = new StringBuilder("curl -v "); + + foreach (var header in request.Headers) + { + foreach (var value in header.Value) + { + curl.Append($"-H '{header.Key}: {value}' "); + } + } + + if (request.Content?.Headers != null) + { + foreach (var header in request.Content.Headers) + { + foreach (var value in header.Value) + { + curl.Append($"-H '{header.Key}: {value}' "); + } + } + } + + if (Constants.HttpMethodsWithBody.Contains(request.Method)) + { + curl.Append($"--data '{JsonSerializer.Serialize(data)}' "); + } + + curl.Append($"-X {request.Method.Method} '{httpClient.BaseAddress}{request.RequestUri}' "); + + return curl.ToString().TrimEnd(); + } +#pragma warning restore CA1305 + #endregion } diff --git a/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.csproj b/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.csproj index b51381ae..9d00bae8 100644 --- a/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.csproj +++ b/libraries/dotnet/WorkbenchConnector/WorkbenchConnector.csproj @@ -9,7 +9,7 @@ enable 12 LatestMajor - $(NoWarn);IDE0130;CA2254;CA1812;CA1813; + $(NoWarn);IDE0130;CA2254;CA1812;CA1813;IDE0290; @@ -31,7 +31,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -39,7 +39,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive