Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions src/Infrastructure/BotSharp.Core/Demo/Functions/GetFunEventsFn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,14 @@ public GetFunEventsFn(IServiceProvider services)
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<WeatherLocation>(message.FunctionArgs);
var conv = _services.GetRequiredService<IConversationService>();
var messageHub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();

await Task.Delay(1000);

message.Indication = $"Start querying event data in {args?.City}";
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId
});
_services.GetHub().PushIndication(message, $"Start querying event data in {args?.City}");

await Task.Delay(1500);

message.Indication = $"Still searching events in {args?.City}";
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId
});
_services.GetHub().PushIndication(message, $"Still searching events in {args?.City}");

await Task.Delay(1500);

Expand Down
16 changes: 2 additions & 14 deletions src/Infrastructure/BotSharp.Core/Demo/Functions/GetWeatherFn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@ public async Task<bool> Execute(RoleDialogModel message)

await Task.Delay(1000);

message.Indication = $"Start querying weather data in {args?.City}";
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId
});
_services.GetHub().PushIndication(message, $"Start querying weather data in {args?.City}");

await Task.Delay(1500);

Expand All @@ -49,13 +43,7 @@ public async Task<bool> Execute(RoleDialogModel message)
});
#endif

message.Indication = $"Still working on it... Hold on, {args?.City}";
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = conv.ConversationId
});
_services.GetHub().PushIndication(message, $"Still working on it... Hold on, {args?.City}");

await Task.Delay(1500);

Expand Down
42 changes: 42 additions & 0 deletions src/Infrastructure/BotSharp.Core/MessageHub/ConversationHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace BotSharp.Core.MessageHub;

/// <summary>
/// Pushing events into one conversation. The id is captured when the hub is taken, on the thread
/// that asked for it -- a reporter running on a transport's own thread can keep this and push
/// without resolving a scoped service again. Don't hold one past the conversation it was taken for.
/// </summary>
public sealed class ConversationHub
{
private readonly MessageHub<HubObserveData<RoleDialogModel>> _hub;

internal ConversationHub(MessageHub<HubObserveData<RoleDialogModel>> hub, string? conversationId)
{
_hub = hub;
ConversationId = conversationId;
}

/// <summary>Where every push goes; null when taken outside a conversation.</summary>
public string? ConversationId { get; }

/// <summary>
/// Raise the "working on it" line in chat. <paramref name="message"/> is pushed as-is and its
/// <c>Indication</c> overwritten, so clone it when the original is still in use by a call in flight.
/// </summary>
public void PushIndication(RoleDialogModel message, string? indication)
{
// Nothing to announce, or no one to announce it to: subscribers match on RefId.
if (string.IsNullOrWhiteSpace(indication) || string.IsNullOrWhiteSpace(ConversationId))
{
return;
}

message.Indication = indication;

_hub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = message,
RefId = ConversationId
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace BotSharp.Core.MessageHub;

public static class MessageHubExtensions
{
/// <summary>
/// The hub bound to the conversation this call is running in.
/// </summary>
public static ConversationHub GetHub(this IServiceProvider services)
{
var conv = services.GetRequiredService<IConversationService>();
var hub = services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
return new ConversationHub(hub, conv.ConversationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,12 @@ public Task<string> GetIndicatorAsync(RoleDialogModel message)
/// </summary>
private sealed class ToolProgressIndicator : IProgress<ProgressNotificationValue>
{
private readonly MessageHub<HubObserveData<RoleDialogModel>> _hub;
private readonly string _conversationId;
private readonly ConversationHub _hub;
private readonly RoleDialogModel _message;

private ToolProgressIndicator(
MessageHub<HubObserveData<RoleDialogModel>> hub,
string conversationId,
RoleDialogModel message)
private ToolProgressIndicator(ConversationHub hub, RoleDialogModel message)
{
_hub = hub;
_conversationId = conversationId;
_message = message;
}

Expand All @@ -106,22 +101,16 @@ private ToolProgressIndicator(
/// tool invoked outside one, from a task or a test. Null is the right answer there rather
/// than a reporter that drops everything: it also tells the server not to bother sending.
/// <para>
/// The conversation id is read HERE, on the thread that starts the call, and captured.
/// <see cref="Report"/> runs on whichever thread the MCP transport is reading on, and
/// resolving a scoped service from there to ask again would be a race for a value that
/// The hub is taken HERE, on the thread that starts the call, so it carries the conversation
/// id with it. <see cref="Report"/> runs on whichever thread the MCP transport is reading on,
/// and resolving a scoped service from there to ask again would be a race for a value that
/// cannot change during the call.
/// </para>
/// </summary>
public static ToolProgressIndicator? For(IServiceProvider services, RoleDialogModel message)
{
var conversationId = services.GetRequiredService<IConversationService>().ConversationId;
if (string.IsNullOrWhiteSpace(conversationId))
{
return null;
}

var hub = services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
return new ToolProgressIndicator(hub, conversationId, message);
var hub = services.GetHub();
return string.IsNullOrWhiteSpace(hub.ConversationId) ? null : new ToolProgressIndicator(hub, message);
}

/// <summary>
Expand All @@ -145,15 +134,7 @@ public void Report(ProgressNotificationValue value)

// Cloned: this is pushed to observers that read it, and the function's own message is
// still being used by the call in flight. Its indication is not ours to overwrite.
var indication = RoleDialogModel.From(_message);
indication.Indication = value.Message;

_hub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = indication,
RefId = _conversationId
});
_hub.PushIndication(RoleDialogModel.From(_message), value.Message);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,10 @@ public async Task<bool> InvokeFunction(string name, RoleDialogModel message, Inv
// Clone message
var clonedMessage = RoleDialogModel.From(message);
clonedMessage.FunctionName = name;
// Assigned even when empty: the hooks below log this field, and the value From() copied
// would otherwise linger there.
clonedMessage.Indication = await funcExecutor.GetIndicatorAsync(message);

// An empty indicator means the callee has nothing worth announcing for this call.
if (!string.IsNullOrEmpty(clonedMessage.Indication))
{
var conv = _services.GetRequiredService<IConversationService>();
var messageHub = _services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>();
messageHub.Push(new()
{
EventName = ChatEvent.OnIndicationReceived,
Data = clonedMessage,
RefId = conv.ConversationId
});
}
_services.GetHub().PushIndication(clonedMessage, clonedMessage.Indication);

var hooks = _services.GetHooksOrderByPriority<IConversationHook>(clonedMessage.CurrentAgentId);
foreach (var hook in hooks)
Expand Down
Loading