refine observer

This commit is contained in:
Jicheng Lu 2025-08-01 13:23:46 -05:00
parent 10759a7939
commit e32d820440
21 changed files with 215 additions and 153 deletions

View file

@ -2,6 +2,5 @@ namespace BotSharp.Abstraction.MessageHub.Models;
public class HubObserveData<TData> : ObserveDataBase where TData : class, new()
{
public string EventName { get; set; } = null!;
public TData Data { get; set; } = null!;
}

View file

@ -2,6 +2,6 @@ namespace BotSharp.Abstraction.MessageHub.Models;
public class ObserveDataBase
{
public IServiceProvider ServiceProvider { get; set; } = null!;
public string EventName { get; set; } = null!;
public string RefId { get; set; } = null!;
}

View file

@ -19,4 +19,10 @@ public class ObserverSubscription<T>
Observer = observer;
Subscription = subscription;
}
public void UnSubscribe()
{
Observer.Deactivate();
Subscription.Dispose();
}
}

View file

@ -1,9 +1,41 @@
namespace BotSharp.Abstraction.MessageHub.Observers;
public abstract class BotSharpObserverBase<T>
public abstract class BotSharpObserverBase<T> : IBotSharpObserver<T>
{
protected bool _active = false;
protected BotSharpObserverBase()
{
}
public virtual string Name => string.Empty;
public virtual bool Active => _active;
public virtual void Activate()
{
_active = true;
}
public virtual void Deactivate()
{
_active = false;
}
public virtual void OnCompleted()
{
}
public virtual void OnError(Exception error)
{
}
public virtual void OnNext(T value)
{
}
}

View file

@ -2,8 +2,8 @@ namespace BotSharp.Abstraction.MessageHub.Observers;
public interface IBotSharpObserver<T> : IObserver<T>
{
//string Name { get; }
bool IsActive { get; }
string Name { get; }
bool Active { get; }
void Activate();
void Deactivate();
}

View file

@ -4,5 +4,7 @@ namespace BotSharp.Abstraction.MessageHub.Services;
public interface IObserverService
{
ObserverSubscriptionContainer<T> RegisterObservers<T>(string refId) where T : ObserveDataBase;
IDisposable SubscribeObservers<T>(string refId, IEnumerable<string>? names = null) where T : ObserveDataBase;
void UnSubscribeObservers<T>(IEnumerable<string>? names = null) where T : ObserveDataBase;
}

View file

@ -1,49 +0,0 @@
using BotSharp.Abstraction.MessageHub.Models;
namespace BotSharp.Abstraction.MessageHub.Services;
public class ObserverSubscriptionContainer<T> : IDisposable
{
private IList<ObserverSubscription<T>> _subscriptions = [];
private bool _disposed = false;
public ObserverSubscriptionContainer()
{
}
public ObserverSubscriptionContainer(
IList<ObserverSubscription<T>> subscriptions)
{
_subscriptions = subscriptions;
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
#if DEBUG
Console.WriteLine($"Start disposing subscriptions...");
#endif
// Unregister all observers
foreach (var item in _subscriptions)
{
item.Observer.Deactivate();
item.Subscription.Dispose();
}
_subscriptions.Clear();
#if DEBUG
Console.WriteLine($"End disposing subscriptions...");
#endif
}
_disposed = true;
}
}
}

View file

@ -49,7 +49,8 @@ public class ConversationPlugin : IBotSharpPlugin, IBotSharpAppPlugin
});
services.AddSingleton<MessageHub<HubObserveData<RoleDialogModel>>>();
//services.AddScoped<IBotSharpObserver<HubObserveData<RoleDialogModel>>, ConversationObserver>();
services.AddScoped<ObserverSubscriptionContainer<HubObserveData<RoleDialogModel>>>();
services.AddScoped<IBotSharpObserver<HubObserveData<RoleDialogModel>>, ConversationObserver>();
services.AddScoped<IObserverService, ObserverService>();
services.AddScoped<IConversationStorage, ConversationStorage>();
@ -77,8 +78,6 @@ public class ConversationPlugin : IBotSharpPlugin, IBotSharpAppPlugin
public void Configure(IApplicationBuilder app)
{
//var services = app.ApplicationServices;
//var queue = services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
//var logger = services.GetRequiredService<ILogger<MessageHub<HubObserveData<RoleDialogModel>>>>();
}
}

View file

@ -29,19 +29,17 @@ public class GetWeatherFn : IFunctionCallback
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId,
ServiceProvider = _services
RefId = conv.ConversationId
});
await Task.Delay(1500);
message.Indication = $"Still working on it, {args?.City}";
message.Indication = $"Still working on it... Hold on, {args?.City}";
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId,
ServiceProvider = _services
RefId = conv.ConversationId
});
await Task.Delay(1500);

View file

@ -2,49 +2,39 @@ using BotSharp.Abstraction.MessageHub.Observers;
namespace BotSharp.Core.MessageHub.Observers;
public class ConversationObserver : IBotSharpObserver<HubObserveData<RoleDialogModel>>
public class ConversationObserver : BotSharpObserverBase<HubObserveData<RoleDialogModel>>
{
private readonly ILogger<ConversationObserver> _logger;
private IServiceProvider _services;
private bool _isActive;
private readonly IServiceProvider _services;
public ConversationObserver(
ILogger<ConversationObserver> logger)
IServiceProvider services,
ILogger<ConversationObserver> logger) : base()
{
_services = services;
_logger = logger;
}
public bool IsActive => _isActive;
public override string Name => nameof(ConversationObserver);
public void Activate()
{
_isActive = true;
}
public void Deactivate()
{
_isActive = false;
}
public void OnCompleted()
public override void OnCompleted()
{
_logger.LogWarning($"{nameof(ConversationObserver)} receives complete notification.");
}
public void OnError(Exception error)
public override void OnError(Exception error)
{
_logger.LogError(error, $"{nameof(ConversationObserver)} receives error notification: {error.Message}");
}
public void OnNext(HubObserveData<RoleDialogModel> value)
public override void OnNext(HubObserveData<RoleDialogModel> value)
{
_services = value.ServiceProvider;
//var progress = _services.GetRequiredService<IConversationProgressService>();
var conv = _services.GetRequiredService<IConversationService>();
if (value.EventName == ChatEvent.OnIndicationReceived)
{
#if !DEBUG
_logger.LogCritical($"Receiving {value.EventName} ({value.Data.Indication}) in {nameof(ConversationObserver)}");
#if DEBUG
_logger.LogCritical($"Receiving {value.EventName} ({value.Data.Indication}) in {nameof(ConversationObserver)} - {conv.ConversationId}");
#endif
//progress.OnFunctionExecuting(value.Data).ConfigureAwait(false).GetAwaiter().GetResult();
}

View file

@ -7,25 +7,38 @@ namespace BotSharp.Core.MessageHub.Services;
public class ObserverService : IObserverService
{
private readonly IServiceProvider _services;
private readonly ILogger<ObserverService> _logger;
public ObserverService(
IServiceProvider services)
IServiceProvider services,
ILogger<ObserverService> logger)
{
_services = services;
_logger = logger;
}
public ObserverSubscriptionContainer<T> RegisterObservers<T>(string refId) where T : ObserveDataBase
public IDisposable SubscribeObservers<T>(string refId, IEnumerable<string>? names = null) where T : ObserveDataBase
{
var subscriptions = new List<ObserverSubscription<T>>();
var container = _services.GetRequiredService<ObserverSubscriptionContainer<T>>();
var observers = _services.GetServices<IBotSharpObserver<T>>()
.Where(x => !x.IsActive)
.Where(x => !x.Active)
.ToList();
if (!names.IsNullOrEmpty())
{
observers = observers.Where(x => names.Contains(x.Name)).ToList();
}
if (observers.IsNullOrEmpty())
{
return new();
return container;
}
#if DEBUG
_logger.LogCritical($"Subscribe observers: {string.Join(",", observers.Select(x => x.Name))}");
#endif
var subscriptions = new List<ObserverSubscription<T>>();
var messageHub = _services.GetRequiredService<MessageHub<T>>();
foreach (var observer in observers)
{
@ -38,6 +51,22 @@ public class ObserverService : IObserverService
});
}
return new(subscriptions);
container.Append(subscriptions);
return container;
}
public void UnSubscribeObservers<T>(IEnumerable<string>? names = null) where T : ObserveDataBase
{
var container = _services.GetRequiredService<ObserverSubscriptionContainer<T>>();
var subscriptions = container.GetSubscriptions(names);
#if DEBUG
_logger.LogCritical($"UnSubscribe observers: {string.Join(",", subscriptions.Select(x => x.Observer.Name))}");
#endif
foreach (var sub in subscriptions)
{
sub.UnSubscribe();
}
}
}

View file

@ -0,0 +1,64 @@
namespace BotSharp.Core.MessageHub.Services;
public class ObserverSubscriptionContainer<T> : IDisposable
{
private readonly ILogger<ObserverSubscriptionContainer<T>> _logger;
private List<ObserverSubscription<T>> _subscriptions = [];
private bool _disposed = false;
public ObserverSubscriptionContainer(
ILogger<ObserverSubscriptionContainer<T>> logger)
{
_logger = logger;
}
public List<ObserverSubscription<T>> GetSubscriptions(IEnumerable<string>? names = null)
{
if (!names.IsNullOrEmpty())
{
return _subscriptions.Where(x => names.Contains(x.Observer.Name)).ToList();
}
return _subscriptions;
}
public void Append(List<ObserverSubscription<T>> subscriptions)
{
_subscriptions = _subscriptions.Concat(subscriptions).DistinctBy(x => x.Observer.Name).ToList();
}
public void Remove(IEnumerable<string>? names = null)
{
if (!names.IsNullOrEmpty())
{
_subscriptions = _subscriptions.Where(x => !names.Contains(x.Observer.Name)).ToList();
return;
}
_subscriptions.Clear();
}
public void Clear()
{
_subscriptions.Clear();
}
public void Dispose()
{
if (!_disposed)
{
#if DEBUG
_logger.LogCritical($"Start disposing subscriptions...");
#endif
// UnSubscribe all observers
foreach (var sub in _subscriptions)
{
sub.UnSubscribe();
}
_subscriptions.Clear();
#if DEBUG
_logger.LogCritical($"End disposing subscriptions...");
#endif
_disposed = true;
}
}
}

View file

@ -31,8 +31,7 @@ public partial class RoutingService
{
EventName = ChatEvent.OnIndicationReceived,
Data = clonedMessage,
RefId = conv.ConversationId,
ServiceProvider = _services
RefId = conv.ConversationId
});
var hooks = _services.GetHooksOrderByPriority<IConversationHook>(clonedMessage.CurrentAgentId);
@ -67,7 +66,7 @@ public partial class RoutingService
}
catch (JsonException ex)
{
_logger.LogError($"The input does not contain any JSON tokens:\r\n{message.Content}\r\n{ex.Message}");
_logger.LogError(ex, $"The input does not contain any JSON tokens:\r\n{message.Content}\r\n{ex.Message}");
message.StopCompletion = true;
message.Content = ex.Message;
}
@ -75,7 +74,7 @@ public partial class RoutingService
{
message.StopCompletion = true;
message.Content = ex.Message;
_logger.LogError(ex.ToString());
_logger.LogError(ex, ex.ToString());
}
// Make sure content has been populated

View file

@ -7,7 +7,7 @@ using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Users.Dtos;
using BotSharp.Core.Infrastructures;
using System.ComponentModel;
using BotSharp.Core.MessageHub.Observers;
namespace BotSharp.OpenAPI.Controllers;
@ -346,8 +346,8 @@ public class ConversationController : ControllerBase
[FromRoute] string conversationId,
[FromBody] NewMessageModel input)
{
var observerService = _services.GetRequiredService<IObserverService>();
using var container = observerService.RegisterObservers<HubObserveData<RoleDialogModel>>(conversationId);
var observer = _services.GetRequiredService<IObserverService>();
using var container = observer.SubscribeObservers<HubObserveData<RoleDialogModel>>(conversationId);
var conv = _services.GetRequiredService<IConversationService>();
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text)
@ -363,7 +363,6 @@ public class ConversationController : ControllerBase
SetStates(conv, input);
var response = new ChatResponseModel();
await conv.SendMessage(agentId, inputMsg,
replyMessage: input.Postback,
async msg =>

View file

@ -1,4 +1,4 @@
using Azure;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.MessageHub.Models;
@ -213,6 +213,7 @@ public class ChatCompletionProvider : IChatCompletion
var (prompt, messages, options) = PrepareOptions(agent, conversations);
var hub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
var conv = _services.GetRequiredService<IConversationService>();
var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
var contentHooks = _services.GetHooks<IContentGeneratingHook>(agent.Id);
@ -224,8 +225,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "BeforeReceiveLlmStreamMessage",
EventName = ChatEvent.BeforeReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
CurrentAgentId = agent.Id,
@ -268,8 +269,8 @@ public class ChatCompletionProvider : IChatCompletion
};
hub.Push(new()
{
ServiceProvider = _services,
EventName = "OnReceiveLlmStreamMessage",
EventName = ChatEvent.OnReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = content
});
}
@ -311,8 +312,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "AfterReceiveLlmStreamMessage",
EventName = ChatEvent.AfterReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});

View file

@ -39,8 +39,6 @@ public class ChatHubPlugin : IBotSharpPlugin, IBotSharpAppPlugin
public void Configure(IApplicationBuilder app)
{
//var services = app.ApplicationServices;
//var queue = services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
//var logger = services.GetRequiredService<ILogger<MessageHub<HubObserveData<RoleDialogModel>>>>();
}
}

View file

@ -3,49 +3,37 @@ using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.MessageHub.Models;
using BotSharp.Abstraction.MessageHub.Observers;
using BotSharp.Abstraction.SideCar;
using BotSharp.Core.MessageHub.Observers;
using System.Runtime.CompilerServices;
namespace BotSharp.Plugin.ChatHub.Observers;
public class ChatHubObserver : IBotSharpObserver<HubObserveData<RoleDialogModel>>
public class ChatHubObserver : BotSharpObserverBase<HubObserveData<RoleDialogModel>>
{
private readonly ILogger _logger;
private IServiceProvider _services;
private bool _isActive = false;
private readonly IServiceProvider _services;
public ChatHubObserver(
ILogger<ChatHubObserver> logger)
IServiceProvider services,
ILogger<ChatHubObserver> logger) : base()
{
_services = services;
_logger = logger;
}
public bool IsActive => _isActive;
public override string Name => nameof(ChatHubObserver);
public void Activate()
{
_isActive = true;
}
public void Deactivate()
{
_isActive = false;
}
public void OnCompleted()
public override void OnCompleted()
{
_logger.LogWarning($"{nameof(ChatHubObserver)} receives complete notification.");
}
public void OnError(Exception error)
public override void OnError(Exception error)
{
_logger.LogError(error, $"{nameof(ChatHubObserver)} receives error notification: {error.Message}");
}
public void OnNext(HubObserveData<RoleDialogModel> value)
public override void OnNext(HubObserveData<RoleDialogModel> value)
{
_services = value.ServiceProvider;
var message = value.Data;
var model = new ChatResponseDto();
var action = new ConversationSenderActionModel();

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.MessageHub.Models;
@ -180,6 +181,7 @@ public class ChatCompletionProvider : IChatCompletion
var (prompt, messages, options) = PrepareOptions(agent, conversations);
var hub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
var conv = _services.GetRequiredService<IConversationService>();
var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
var contentHooks = _services.GetHooks<IContentGeneratingHook>(agent.Id);
@ -191,8 +193,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "BeforeReceiveLlmStreamMessage",
EventName = ChatEvent.BeforeReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
CurrentAgentId = agent.Id,
@ -235,8 +237,8 @@ public class ChatCompletionProvider : IChatCompletion
};
hub.Push(new()
{
ServiceProvider = _services,
EventName = "OnReceiveLlmStreamMessage",
EventName = ChatEvent.OnReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = content
});
}
@ -278,8 +280,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "AfterReceiveLlmStreamMessage",
EventName = ChatEvent.AfterReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MessageHub.Models;
@ -184,12 +185,13 @@ public class ChatCompletionProvider : IChatCompletion
}
var hub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
var conv = _services.GetRequiredService<IConversationService>();
var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
hub.Push(new()
{
ServiceProvider = _services,
EventName = "BeforeReceiveLlmStreamMessage",
EventName = ChatEvent.BeforeReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
CurrentAgentId = agent.Id,
@ -216,8 +218,8 @@ public class ChatCompletionProvider : IChatCompletion
};
hub.Push(new()
{
ServiceProvider = _services,
EventName = "OnReceiveLlmStreamMessage",
EventName = ChatEvent.OnReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = content
});
}
@ -231,8 +233,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "AfterReceiveLlmStreamMessage",
EventName = ChatEvent.AfterReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});

View file

@ -189,6 +189,7 @@ public class ChatCompletionProvider : IChatCompletion
var (prompt, messages, options) = PrepareOptions(agent, conversations);
var hub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
var conv = _services.GetRequiredService<IConversationService>();
var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
var contentHooks = _services.GetHooks<IContentGeneratingHook>(agent.Id);
@ -200,8 +201,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = ChatEvent.BeforeReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
CurrentAgentId = agent.Id,
@ -244,8 +245,8 @@ public class ChatCompletionProvider : IChatCompletion
};
hub.Push(new()
{
ServiceProvider = _services,
EventName = ChatEvent.OnReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = content
});
}
@ -287,8 +288,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = ChatEvent.AfterReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});

View file

@ -1,10 +1,11 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MessageHub.Models;
using BotSharp.Core.Infrastructures.Streams;
using BotSharp.Core.MessageHub;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.SparkDesk.Providers;
@ -153,11 +154,12 @@ public class ChatCompletionProvider : IChatCompletion
var (prompt, messages, funcall) = PrepareOptions(agent, conversations);
var messageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
var hub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
var conv = _services.GetRequiredService<IConversationService>();
hub.Push(new()
{
ServiceProvider = _services,
EventName = "BeforeReceiveLlmStreamMessage",
EventName = ChatEvent.BeforeReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = new RoleDialogModel(AgentRole.Assistant, string.Empty)
{
CurrentAgentId = agent.Id,
@ -197,8 +199,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "OnReceiveLlmStreamMessage",
EventName = ChatEvent.OnReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});
}
@ -212,8 +214,8 @@ public class ChatCompletionProvider : IChatCompletion
hub.Push(new()
{
ServiceProvider = _services,
EventName = "AfterReceiveLlmStreamMessage",
EventName = ChatEvent.AfterReceiveLlmStreamMessage,
RefId = conv.ConversationId,
Data = responseMessage
});