Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/refine-agent-call-stats

This commit is contained in:
Jicheng Lu 2025-02-19 10:11:52 -06:00
commit 25502b3dcb
22 changed files with 297 additions and 93 deletions

44
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: build
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v1
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Set env
run: |
echo "DOTNET_CLI_TELEMETRY_OPTOUT=1" >> $GITHUB_ENV
echo "DOTNET_hostBuilder:reloadConfigOnChange=false" >> $GITHUB_ENV
- name: Install required workloads
run: |
dotnet workload install aspire --source https://aka.ms/dotnet8/nuget/index.json --source https://api.nuget.org/v3/index.json
- name: Clean
run: |
dotnet clean ./BotSharp.sln --configuration Release
dotnet nuget locals all --clear
- name: Build
run: dotnet build ./BotSharp.sln -c Release
- name: Test
run: |
cd ./tests/UnitTest
dotnet test --logger "console;verbosity=detailed"
cd ../BotSharp.Plugin.SemanticKernel.UnitTests
dotnet test --logger "console;verbosity=detailed"

View file

@ -71,5 +71,5 @@ public interface IConversationService
/// <param name="convLimit">conversation limit</param>
/// <param name="preLoad">if pre-loading, then keys are not filter by the search query</param>
/// <returns></returns>
Task<List<string>> GetConversationStateSearhKeys(string query, int convlimit = 100, int keyLimit = 10, bool preLoad = false);
Task<List<string>> GetConversationStateSearhKeys(string query, int convLimit = 100, int keyLimit = 10, bool preload = false);
}

View file

@ -58,13 +58,17 @@ public class DialogElement
[JsonPropertyName("payload")]
public string? Payload { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("data")]
public object? Data { get; set; }
public DialogElement()
{
}
public DialogElement(DialogMetaData meta, string content, string? richContent = null,
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null)
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null, object? data = null)
{
MetaData = meta;
Content = content;
@ -72,6 +76,7 @@ public class DialogElement
SecondaryContent = secondaryContent;
SecondaryRichContent = secondaryRichContent;
Payload = payload;
Data = data;
}
public override string ToString()

View file

@ -148,7 +148,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
=> throw new NotImplementedException();
List<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
=> throw new NotImplementedException();
List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperlimit = 100)
List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
=> throw new NotImplementedException();
#endregion

View file

@ -222,17 +222,17 @@ public partial class ConversationService : IConversationService
_state.Save();
}
public async Task<List<string>> GetConversationStateSearhKeys(string query, int convlimit = 100, int keyLimit = 10, bool preLoad = false)
public async Task<List<string>> GetConversationStateSearhKeys(string query, int convLimit = 100, int keyLimit = 10, bool preload = false)
{
var keys = new List<string>();
if (!preLoad && string.IsNullOrWhiteSpace(query))
if (!preload && string.IsNullOrWhiteSpace(query))
{
return keys;
}
var db = _services.GetRequiredService<IBotSharpRepository>();
keys = db.GetConversationStateSearchKeys(convUpperlimit: convlimit);
keys = preLoad ? keys : keys.Where(x => x.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList();
keys = db.GetConversationStateSearchKeys(convUpperLimit: convLimit);
keys = preload ? keys : keys.Where(x => x.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList();
return keys.OrderBy(x => x).Take(keyLimit).ToList();
}
}

View file

@ -15,6 +15,7 @@
******************************************************************************/
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.SideCar;
namespace BotSharp.Core.Conversations.Services;
@ -26,6 +27,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
private readonly ILogger _logger;
private readonly IServiceProvider _services;
private readonly IBotSharpRepository _db;
private readonly IConversationSideCar _sidecar;
private string _conversationId;
/// <summary>
/// States in the current round of conversation
@ -39,10 +41,12 @@ public class ConversationStateService : IConversationStateService, IDisposable
public ConversationStateService(
IServiceProvider services,
IBotSharpRepository db,
IConversationSideCar sidecar,
ILogger<ConversationStateService> logger)
{
_services = services;
_db = db;
_sidecar = sidecar;
_logger = logger;
_curStates = new ConversationState();
_historyStates = new ConversationState();
@ -138,15 +142,20 @@ public class ConversationStateService : IConversationStateService, IDisposable
_conversationId = !isReadOnly ? conversationId : null;
Reset();
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var curMsgId = routingCtx.MessageId;
var endNodes = new Dictionary<string, string>();
if (_sidecar?.IsEnabled() == true)
{
return endNodes;
}
_historyStates = _db.GetConversationStates(conversationId);
if (_historyStates.IsNullOrEmpty())
{
return endNodes;
}
var endNodes = new Dictionary<string, string>();
if (_historyStates.IsNullOrEmpty()) return endNodes;
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var curMsgId = routingCtx.MessageId;
var dialogs = _db.GetConversationDialogs(conversationId);
var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User)
.GroupBy(x => x.MetaData?.MessageId)
@ -210,7 +219,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
public void Save()
{
if (_conversationId == null)
if (_conversationId == null || _sidecar?.IsEnabled() == true)
{
Reset();
return;

View file

@ -52,7 +52,8 @@ public class ConversationStorage : IConversationStorage
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
});
}
else
@ -83,7 +84,8 @@ public class ConversationStorage : IConversationStorage
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
});
}
@ -121,7 +123,8 @@ public class ConversationStorage : IConversationStorage
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
});
}
else
@ -152,7 +155,8 @@ public class ConversationStorage : IConversationStorage
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
});
}
}
@ -196,7 +200,8 @@ public class ConversationStorage : IConversationStorage
RichContent = richContent,
SecondaryContent = secondaryContent,
SecondaryRichContent = secondaryRichContent,
Payload = payload
Payload = payload,
Data = dialog.Data
};
results.Add(record);

View file

@ -605,7 +605,7 @@ public partial class FileRepository
#if !DEBUG
[SharpCache(10)]
#endif
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperlimit = 100)
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
{
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
if (!Directory.Exists(dir)) return [];
@ -635,7 +635,7 @@ public partial class FileRepository
keys.AddRange(stateKeys);
count++;
if (count >= convUpperlimit)
if (count >= convUpperLimit)
{
break;
}

View file

@ -79,9 +79,8 @@ public class ConversationController : ControllerBase
[HttpGet("/conversation/{conversationId}/dialogs")]
public async Task<IEnumerable<ChatResponseModel>> GetDialogs([FromRoute] string conversationId)
{
var conv = _services.GetRequiredService<IConversationService>();
conv.SetConversationId(conversationId, new List<MessageState>(), isReadOnly: true);
var history = conv.GetDialogHistory(fromBreakpoint: false);
var storage = _services.GetRequiredService<IConversationStorage>();
var history = storage.GetDialogs(conversationId);
var userService = _services.GetRequiredService<IUserService>();
var agentService = _services.GetRequiredService<IAgentService>();
@ -555,10 +554,10 @@ public class ConversationController : ControllerBase
#region Search state keys
[HttpGet("/conversation/state/keys")]
public async Task<List<string>> GetConversationStateKeys([FromQuery] string query, [FromQuery] int keyLimit = 10, [FromQuery] bool preLoad = false)
public async Task<List<string>> GetConversationStateKeys([FromQuery] string query, [FromQuery] int keyLimit = 10, [FromQuery] int convLimit = 100, [FromQuery] bool preload = false)
{
var convService = _services.GetRequiredService<IConversationService>();
var keys = await convService.GetConversationStateSearhKeys(query, keyLimit: keyLimit, preLoad: preLoad);
var keys = await convService.GetConversationStateSearhKeys(query, keyLimit: keyLimit, convLimit: convLimit, preload: preload);
return keys;
}
#endregion

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Interpreters.Settings;
using BotSharp.Core.Crontab.Abstraction;
using BotSharp.Plugin.ChatHub.Hooks;
using Microsoft.Extensions.Configuration;
@ -18,6 +17,10 @@ public class ChatHubPlugin : IBotSharpPlugin
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
var settings = new ChatHubSettings();
config.Bind("ChatHub", settings);
services.AddSingleton(x => settings);
// Register hooks
services.AddScoped<IConversationHook, ChatHubConversationHook>();
services.AddScoped<IConversationHook, StreamingLogHook>();

View file

@ -0,0 +1,7 @@
namespace BotSharp.Plugin.ChatHub.Enums;
public static class EventDispatchType
{
public const string Group = "group";
public const string User = "user";
}

View file

@ -9,8 +9,9 @@ public class ChatHubConversationHook : ConversationHookBase
private readonly IHubContext<SignalRHub> _chatHub;
private readonly IUserIdentity _user;
private readonly BotSharpOptions _options;
private readonly ChatHubSettings _settings;
#region Event
#region Events
private const string INIT_CLIENT_CONVERSATION = "OnConversationInitFromClient";
private const string RECEIVE_CLIENT_MESSAGE = "OnMessageReceivedFromClient";
private const string RECEIVE_ASSISTANT_MESSAGE = "OnMessageReceivedFromAssistant";
@ -23,12 +24,14 @@ public class ChatHubConversationHook : ConversationHookBase
IServiceProvider services,
IHubContext<SignalRHub> chatHub,
BotSharpOptions options,
ChatHubSettings settings,
IUserIdentity user)
{
_services = services;
_chatHub = chatHub;
_user = user;
_options = options;
_settings = settings;
Priority = -1; // Make sure this hook is the top one.
}
@ -42,7 +45,7 @@ public class ChatHubConversationHook : ConversationHookBase
var user = await userService.GetUser(conv.User.Id);
conv.User = UserViewModel.FromUser(user);
await InitClientConversation(conv);
await InitClientConversation(conv.Id, conv);
await base.OnConversationInitialized(conversation);
}
@ -63,7 +66,7 @@ public class ChatHubConversationHook : ConversationHookBase
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Sender = UserViewModel.FromUser(sender)
};
await ReceiveClientMessage(model);
await ReceiveClientMessage(conv.ConversationId, model);
// Send typing-on to client
var action = new ConversationSenderActionModel
@ -71,7 +74,8 @@ public class ChatHubConversationHook : ConversationHookBase
ConversationId = conv.ConversationId,
SenderAction = SenderActionEnum.TypingOn
};
await GenerateSenderAction(action);
await GenerateSenderAction(conv.ConversationId, action);
await base.OnMessageReceived(message);
}
@ -84,7 +88,8 @@ public class ChatHubConversationHook : ConversationHookBase
SenderAction = SenderActionEnum.TypingOn,
Indication = message.Indication
};
await GenerateSenderAction(action);
await GenerateSenderAction(conv.ConversationId, action);
await base.OnFunctionExecuting(message);
}
@ -121,8 +126,8 @@ public class ChatHubConversationHook : ConversationHookBase
SenderAction = SenderActionEnum.TypingOff
};
await GenerateSenderAction(action);
await ReceiveAssistantMessage(json);
await GenerateSenderAction(conv.ConversationId, action);
await ReceiveAssistantMessage(conv.ConversationId, json);
await base.OnResponseGenerated(message);
}
@ -146,7 +151,7 @@ public class ChatHubConversationHook : ConversationHookBase
}
}, _options.JsonSerializerOptions);
await GenerateNotification(json);
await GenerateNotification(conv.ConversationId, json);
await base.OnNotificationGenerated(message);
}
@ -158,7 +163,8 @@ public class ChatHubConversationHook : ConversationHookBase
ConversationId = conversationId,
MessageId = messageId
};
await DeleteMessage(model);
await DeleteMessage(conversationId, model);
await base.OnMessageDeleted(conversationId, messageId);
}
@ -169,34 +175,77 @@ public class ChatHubConversationHook : ConversationHookBase
return sidecar == null || !sidecar.IsEnabled();
}
private async Task InitClientConversation(ConversationViewModel conversation)
private async Task InitClientConversation(string conversationId, ConversationViewModel conversation)
{
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);
}
}
private async Task ReceiveClientMessage(ChatResponseModel model)
private async Task ReceiveClientMessage(string conversationId, ChatResponseModel model)
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_CLIENT_MESSAGE, model);
}
}
private async Task ReceiveAssistantMessage(string? json)
private async Task ReceiveAssistantMessage(string conversationId, string? json)
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
}
private async Task GenerateSenderAction(ConversationSenderActionModel action)
private async Task GenerateSenderAction(string conversationId, ConversationSenderActionModel action)
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_SENDER_ACTION, action);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_SENDER_ACTION, action);
}
}
private async Task DeleteMessage(ChatResponseModel model)
private async Task DeleteMessage(string conversationId, ChatResponseModel model)
{
await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(DELETE_MESSAGE, model);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(DELETE_MESSAGE, model);
}
}
private async Task GenerateNotification(string? json)
private async Task GenerateNotification(string conversationId, string? json)
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(GENERATE_NOTIFICATION, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(GENERATE_NOTIFICATION, json);
}
}
#endregion
}

View file

@ -11,18 +11,25 @@ public class ChatHubCrontabHook : ICrontabHook
private readonly IUserIdentity _user;
private readonly IConversationStorage _storage;
private readonly BotSharpOptions _options;
private readonly ChatHubSettings _settings;
#region Events
private const string GENERATE_NOTIFICATION = "OnNotificationGenerated";
#endregion
public ChatHubCrontabHook(IServiceProvider services,
IHubContext<SignalRHub> chatHub,
IUserIdentity user,
IConversationStorage storage,
BotSharpOptions options)
BotSharpOptions options,
ChatHubSettings settings)
{
_services = services;
_chatHub = chatHub;
_user = user;
_storage = storage;
_options = options;
_settings = settings;
}
public async Task OnCronTriggered(CrontabItem item)
@ -41,6 +48,13 @@ public class ChatHubCrontabHook : ICrontabHook
}
}, _options.JsonSerializerOptions);
await _chatHub.Clients.User(item.UserId).SendAsync("OnNotificationGenerated", json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(item.ConversationId).SendAsync(GENERATE_NOTIFICATION, json);
}
else
{
await _chatHub.Clients.User(item.UserId).SendAsync(GENERATE_NOTIFICATION, json);
}
}
}

View file

@ -9,6 +9,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
private readonly ConversationSetting _convSettings;
private readonly BotSharpOptions _options;
private readonly JsonSerializerOptions _localJsonOptions;
private readonly ChatHubSettings _settings;
private readonly IServiceProvider _services;
private readonly IHubContext<SignalRHub> _chatHub;
private readonly IConversationStateService _state;
@ -16,7 +17,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
private readonly IAgentService _agentService;
private readonly IRoutingContext _routingCtx;
#region Event
#region Events
private const string CONTENT_LOG_GENERATED = "OnConversationContentLogGenerated";
private const string STATE_LOG_GENERATED = "OnConversateStateLogGenerated";
private const string AGENT_QUEUE_CHANGED = "OnAgentQueueChanged";
@ -26,6 +27,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
public StreamingLogHook(
ConversationSetting convSettings,
BotSharpOptions options,
ChatHubSettings settings,
IServiceProvider serivces,
IHubContext<SignalRHub> chatHub,
IConversationStateService state,
@ -35,6 +37,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
{
_convSettings = convSettings;
_options = options;
_settings = settings;
_services = serivces;
_chatHub = chatHub;
_state = state;
@ -58,7 +61,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.UserInput,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg)
@ -76,7 +79,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.UserInput,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnRenderingTemplate(Agent agent, string name, string content)
@ -98,7 +101,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task BeforeGenerating(Agent agent, List<RoleDialogModel> conversations)
@ -126,7 +129,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.FunctionCall,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public override async Task OnFunctionExecuted(RoleDialogModel message)
@ -147,7 +150,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.FunctionCall,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
/// <summary>
@ -174,7 +177,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.Prompt,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
/// <summary>
@ -208,7 +211,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.AgentResponse,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
}
@ -226,7 +229,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.FunctionCall,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public override async Task OnConversationEnding(RoleDialogModel message)
@ -243,7 +246,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.FunctionCall,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public override async Task OnBreakpointUpdated(string conversationId, bool resetStates)
@ -271,7 +274,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
},
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public override async Task OnStateChanged(StateChangeModel stateChange)
@ -281,7 +284,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
if (stateChange == null) return;
await SendStateChange(stateChange);
await SendStateChange(conversationId, stateChange);
}
#endregion
@ -310,7 +313,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnAgentDequeued(string agentId, string currentAgentId, string? reason = null)
@ -338,7 +341,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnAgentReplaced(string fromAgentId, string toAgentId, string? reason = null)
@ -366,7 +369,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnAgentQueueEmptied(string agentId, string? reason = null)
@ -391,7 +394,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnRoutingInstructionReceived(FunctionCallFromLlm instruct, RoleDialogModel message)
@ -410,7 +413,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.AgentResponse,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
public async Task OnRoutingInstructionRevised(FunctionCallFromLlm instruct, RoleDialogModel message)
@ -428,32 +431,62 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
Source = ContentLogSource.HardRule,
Log = log
};
await SendContentLog(input);
await SendContentLog(conversationId, input);
}
#endregion
#region Private methods
private async Task SendContentLog(ContentLogInputModel input)
private async Task SendContentLog(string conversationId, ContentLogInputModel input)
{
await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(CONTENT_LOG_GENERATED, BuildContentLog(input));
}
}
private async Task SendStateLog(string conversationId, string agentId, Dictionary<string, string> states, RoleDialogModel message)
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_LOG_GENERATED, BuildStateLog(conversationId, agentId, states, message));
}
}
private async Task SendAgentQueueLog(string conversationId, string log)
{
await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(AGENT_QUEUE_CHANGED, BuildAgentQueueChangedLog(conversationId, log));
}
}
private async Task SendStateChange(StateChangeModel stateChange)
private async Task SendStateChange(string conversationId, StateChangeModel stateChange)
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversationId).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(STATE_CHANGED, BuildStateChangeLog(stateChange));
}
}
private string BuildContentLog(ContentLogInputModel input)
{
var output = new ContentLogOutputModel

View file

@ -9,18 +9,25 @@ public class WelcomeHook : ConversationHookBase
private readonly IUserIdentity _user;
private readonly IConversationStorage _storage;
private readonly BotSharpOptions _options;
private readonly ChatHubSettings _settings;
#region Events
private const string RECEIVE_ASSISTANT_MESSAGE = "OnMessageReceivedFromAssistant";
#endregion
public WelcomeHook(IServiceProvider services,
IHubContext<SignalRHub> chatHub,
IUserIdentity user,
IConversationStorage storage,
BotSharpOptions options)
BotSharpOptions options,
ChatHubSettings settings)
{
_services = services;
_chatHub = chatHub;
_user = user;
_storage = storage;
_options = options;
_settings = settings;
}
public override async Task OnUserAgentConnectedInitially(Conversation conversation)
@ -71,7 +78,14 @@ public class WelcomeHook : ConversationHookBase
_storage.Append(conversation.Id, dialog);
await _chatHub.Clients.User(_user.Id).SendAsync("OnMessageReceivedFromAssistant", json);
if (_settings.EventDispatchBy == EventDispatchType.Group)
{
await _chatHub.Clients.Group(conversation.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
else
{
await _chatHub.Clients.User(_user.Id).SendAsync(RECEIVE_ASSISTANT_MESSAGE, json);
}
}
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Plugin.ChatHub.Settings;
public class ChatHubSettings
{
public string EventDispatchBy { get; set; } = EventDispatchType.Group;
}

View file

@ -33,6 +33,13 @@ public class SignalRHub : Hub
if (!string.IsNullOrEmpty(conversationId))
{
_logger.LogInformation($"Connection {Context.ConnectionId} is with conversation {conversationId}");
var settings = _services.GetRequiredService<ChatHubSettings>();
if (settings.EventDispatchBy == EventDispatchType.Group)
{
await Groups.AddToGroupAsync(Context.ConnectionId, conversationId);
}
var conv = await convService.GetConversation(conversationId);
if (conv != null)
{

View file

@ -31,4 +31,6 @@ global using BotSharp.Abstraction.Routing;
global using BotSharp.Abstraction.Messaging;
global using BotSharp.Abstraction.Messaging.Enums;
global using BotSharp.Abstraction.Messaging.Models.RichContent;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Plugin.ChatHub.Settings;
global using BotSharp.Plugin.ChatHub.Enums;

View file

@ -11,6 +11,7 @@ public class DialogMongoElement
public string? RichContent { get; set; }
public string? SecondaryRichContent { get; set; }
public string? Payload { get; set; }
public object? Data { get; set; }
public DialogMongoElement()
{
@ -26,7 +27,8 @@ public class DialogMongoElement
SecondaryContent = dialog.SecondaryContent,
RichContent = dialog.RichContent,
SecondaryRichContent = dialog.SecondaryRichContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
};
}
@ -39,7 +41,8 @@ public class DialogMongoElement
SecondaryContent = dialog.SecondaryContent,
RichContent = dialog.RichContent,
SecondaryRichContent = dialog.SecondaryRichContent,
Payload = dialog.Payload
Payload = dialog.Payload,
Data = dialog.Data
};
}
}

View file

@ -614,20 +614,20 @@ public partial class MongoRepository
#if !DEBUG
[SharpCache(10)]
#endif
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperlimit = 100)
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
{
var convFilter = Builders<ConversationDocument>.Filter.Gte(x => x.DialogCount, messageLowerLimit);
var conversations = _dc.Conversations.Find(convFilter)
.SortByDescending(x => x.UpdatedTime)
.Limit(convUpperlimit)
.ToList();
var stateBuilder = Builders<ConversationStateDocument>.Filter;
var sortDef = Builders<ConversationStateDocument>.Sort.Descending(x => x.UpdatedTime);
var stateFilters = new List<FilterDefinition<ConversationStateDocument>>()
{
stateBuilder.Exists(x => x.States),
stateBuilder.Ne(x => x.States, [])
};
if (conversations.IsNullOrEmpty()) return [];
var convIds = conversations.Select(x => x.Id).ToList();
var stateFilter = Builders<ConversationStateDocument>.Filter.In(x => x.ConversationId, convIds);
var states = _dc.ConversationStates.Find(stateFilter).ToList();
var states = _dc.ConversationStates.Find(stateBuilder.And(stateFilters))
.Sort(sortDef)
.Limit(convUpperLimit)
.ToList();
var keys = states.SelectMany(x => x.States.Select(x => x.Key)).Distinct().ToList();
return keys;
}

View file

@ -222,6 +222,10 @@
"Enabled": false
},
"ChatHub": {
"EventDispatchBy": "group"
},
"SharpCache": {
"Enabled": true,
"CacheType": 1,