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