Merge branch 'master' of https://github.com/SciSharp/BotSharp into features/add-chart-handler

This commit is contained in:
Jicheng Lu 2025-07-10 15:50:11 -05:00
commit 7f7d4733e9
13 changed files with 65 additions and 52 deletions

View file

@ -13,5 +13,5 @@ public interface IRealtimeHub
IRealTimeCompletion Completer { get; }
Task ConnectToModel(Func<string, Task>? responseToUser = null, Func<string, Task>? init = null);
Task ConnectToModel(Func<string, Task>? responseToUser = null, Func<string, Task>? init = null, List<MessageState>? initStates = null);
}

View file

@ -79,7 +79,7 @@ public class SideCarAttribute : AsyncMoAttribute
object? res = null;
var isHandled = false;
var enabled = instance != null && instance.IsEnabled() && method != null;
var enabled = instance != null && instance.IsEnabled && method != null;
if (!enabled)
{
return (isHandled, value);
@ -112,7 +112,7 @@ public class SideCarAttribute : AsyncMoAttribute
object? value = null;
var isHandled = false;
var enabled = instance != null && instance.IsEnabled() && method != null;
var enabled = instance != null && instance.IsEnabled && method != null;
if (!enabled)
{
return (isHandled, value);

View file

@ -5,8 +5,8 @@ namespace BotSharp.Abstraction.SideCar;
public interface IConversationSideCar
{
string Provider { get; }
bool IsEnabled { get; }
bool IsEnabled();
void AppendConversationDialogs(string conversationId, List<DialogElement> messages);
List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Options;
using BotSharp.Core.Infrastructures;
@ -22,10 +23,10 @@ public class RealtimeHub : IRealtimeHub
_logger = logger;
}
public async Task ConnectToModel(Func<string, Task>? responseToUser = null, Func<string, Task>? init = null)
public async Task ConnectToModel(Func<string, Task>? responseToUser = null, Func<string, Task>? init = null, List<MessageState>? initStates = null)
{
var convService = _services.GetRequiredService<IConversationService>();
convService.SetConversationId(_conn.ConversationId, []);
convService.SetConversationId(_conn.ConversationId, initStates ?? []);
var conversation = await convService.GetConversation(_conn.ConversationId);
var routing = _services.GetRequiredService<IRoutingService>();

View file

@ -14,7 +14,6 @@
limitations under the License.
******************************************************************************/
using BotSharp.Abstraction.SideCar.Models;
using BotSharp.Core.Infrastructures;
namespace BotSharp.Core.SideCar.Services;
@ -31,6 +30,7 @@ public class BotSharpConversationSideCar : IConversationSideCar
private string _conversationId = string.Empty;
public string Provider => "botsharp";
public bool IsEnabled => _enabled;
public BotSharpConversationSideCar(
IServiceProvider services,
@ -40,11 +40,6 @@ public class BotSharpConversationSideCar : IConversationSideCar
_logger = logger;
}
public bool IsEnabled()
{
return _enabled;
}
public void AppendConversationDialogs(string conversationId, List<DialogElement> messages)
{
if (!IsValid(conversationId))
@ -99,17 +94,22 @@ public class BotSharpConversationSideCar : IConversationSideCar
top.State = new ConversationState(states);
}
public async Task<RoleDialogModel> SendMessage(string agentId, string text,
public async Task<RoleDialogModel> SendMessage(
string agentId,
string text,
PostbackMessageModel? postback = null,
List<MessageState>? states = null,
List<DialogElement>? dialogs = null,
SideCarOptions? options = null)
{
_sideCarOptions = options;
_logger.LogInformation($"Entering side car conversation...");
BeforeExecute(dialogs);
var response = await InnerExecute(agentId, text, postback, states);
AfterExecute();
_logger.LogInformation($"Existing side car conversation...");
return response;
}

View file

@ -159,7 +159,7 @@ public class ConversationStateService : IConversationStateService
Reset();
var endNodes = new Dictionary<string, string>();
if (_sidecar?.IsEnabled() == true)
if (_sidecar?.IsEnabled == true)
{
return endNodes;
}
@ -234,7 +234,7 @@ public class ConversationStateService : IConversationStateService
public void Save()
{
if (_conversationId == null || _sidecar?.IsEnabled() == true)
if (_conversationId == null || _sidecar?.IsEnabled == true)
{
return;
}

View file

@ -25,7 +25,10 @@ public class RateLimitConversationHook : ConversationHookBase
public override async Task OnMessageReceived(RoleDialogModel message)
{
var settings = _services.GetRequiredService<ConversationSetting>();
var states = _services.GetRequiredService<IConversationStateService>();
var rateLimit = settings.RateLimit;
var channel = states.GetState("channel");
// Check max input length
var charCount = message.Content.Length;
@ -45,7 +48,7 @@ public class RateLimitConversationHook : ConversationHookBase
var userSents = Dialogs.Where(x => x.Role == AgentRole.User)
.TakeLast(2).ToList();
if (userSents.Count > 1)
if (channel != ConversationChannel.Phone && userSents.Count > 1)
{
var seconds = (DateTime.UtcNow - userSents.First().CreatedAt).TotalSeconds;
if (seconds < rateLimit.MinTimeSecondsBetweenMessages)
@ -56,9 +59,6 @@ public class RateLimitConversationHook : ConversationHookBase
}
}
var states = _services.GetRequiredService<IConversationStateService>();
var channel = states.GetState("channel");
// Check the number of conversations
if (channel != ConversationChannel.Phone && channel != ConversationChannel.Email && channel != ConversationChannel.Database)
{

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Realtime.Models.Session;
using BotSharp.Core.Session;
using Microsoft.AspNetCore.Http;
@ -62,9 +63,11 @@ public class ChatStreamMiddleware
var hub = services.GetRequiredService<IRealtimeHub>();
var conn = hub.SetHubConnection(conversationId);
conn.CurrentAgentId = agentId;
InitEvents(conn);
// load conversation and state
var convService = services.GetRequiredService<IConversationService>();
var state = services.GetRequiredService<IConversationStateService>();
convService.SetConversationId(conversationId, []);
await convService.GetConversationRecordOrCreateNew(agentId);
@ -79,7 +82,8 @@ public class ChatStreamMiddleware
var (eventType, data) = MapEvents(conn, receivedText);
if (eventType == "start")
{
await ConnectToModel(hub, webSocket);
var states = InitStates(data);
await ConnectToModel(hub, webSocket, states);
}
else if (eventType == "media")
{
@ -95,25 +99,26 @@ public class ChatStreamMiddleware
}
}
convService.SaveStates();
await _session.DisconnectAsync();
_session.Dispose();
}
private async Task ConnectToModel(IRealtimeHub hub, WebSocket webSocket)
private async Task ConnectToModel(IRealtimeHub hub, WebSocket webSocket, List<MessageState>? states = null)
{
await hub.ConnectToModel(async data =>
await hub.ConnectToModel(responseToUser: async data =>
{
if (_session != null)
{
await _session.SendEventAsync(data);
}
});
}, initStates: states);
}
private (string, string) MapEvents(RealtimeHubConnection conn, string receivedText)
{
var response = JsonSerializer.Deserialize<ChatStreamEventResponse>(receivedText);
string data = string.Empty;
var data = response?.Body?.Payload ?? string.Empty;
switch (response.Event)
{
@ -121,13 +126,16 @@ public class ChatStreamMiddleware
conn.ResetStreamState();
break;
case "media":
var mediaResponse = JsonSerializer.Deserialize<ChatStreamMediaEventResponse>(receivedText);
data = mediaResponse?.Body?.Payload ?? string.Empty;
break;
case "disconnect":
break;
}
return (response.Event, data);
}
private void InitEvents(RealtimeHubConnection conn)
{
conn.OnModelMessageReceived = message =>
JsonSerializer.Serialize(new
{
@ -147,7 +155,18 @@ public class ChatStreamMiddleware
{
@event = "clear"
});
}
return (response.Event, data);
private List<MessageState> InitStates(string data)
{
try
{
var states = JsonSerializer.Deserialize<List<MessageState>>(data, BotSharpOptions.defaultJsonOptions);
return states ?? [];
}
catch
{
return [];
}
}
}

View file

@ -179,7 +179,7 @@ public class ChatHubConversationHook : ConversationHookBase
private bool AllowSendingMessage()
{
var sidecar = _services.GetService<IConversationSideCar>();
return sidecar == null || !sidecar.IsEnabled();
return sidecar == null || !sidecar.IsEnabled;
}
private async Task InitClientConversation(string conversationId, ConversationDto conversation)

View file

@ -6,15 +6,12 @@ internal class ChatStreamEventResponse
{
[JsonPropertyName("event")]
public string Event { get; set; }
}
internal class ChatStreamMediaEventResponse : ChatStreamEventResponse
{
[JsonPropertyName("body")]
public MediaEventResponseBody Body { get; set; }
public ChatStreamEventResponseBody Body { get; set; }
}
internal class MediaEventResponseBody
internal class ChatStreamEventResponseBody
{
[JsonPropertyName("payload")]
public string Payload { get; set; }

View file

@ -329,17 +329,13 @@ public class GoogleRealTimeProvider : IRealTimeCompletion
var words = new List<string>();
HookEmitter.Emit<IRealtimeHook>(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)), agent.Id);
var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x =>
var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x => new FunctionDef
{
var fn = new FunctionDef
{
Name = x.Name ?? string.Empty,
Description = x.Description ?? string.Empty,
Parameters = x.Parameters != null
Name = x.Name ?? string.Empty,
Description = x.Description ?? string.Empty,
Parameters = x.Parameters != null
? JsonSerializer.Deserialize<FunctionParametersDef>(JsonSerializer.Serialize(x.Parameters))
: null
};
return fn;
}).ToArray();
await HookEmitter.Emit<IContentGeneratingHook>(_services,

View file

@ -326,15 +326,11 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
var (prompt, messages, options) = PrepareOptions(agent, []);
var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent?.Description ?? string.Empty;
var functions = options.Tools.Select(x =>
var functions = options.Tools.Select(x => new FunctionDef
{
var fn = new FunctionDef
{
Name = x.FunctionName,
Description = x.FunctionDescription
};
fn.Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters);
return fn;
Name = x.FunctionName,
Description = x.FunctionDescription,
Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters)
}).ToArray();
var realtimeModelSettings = _services.GetRequiredService<RealtimeModelSettings>();

View file

@ -53,25 +53,29 @@ public class TwilioInboundController : TwilioController
instruction.SpeechPaths.Add(request.InitAudioFile);
}
// Before creating session
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
}, request.AgentId);
var (agent, conversationId) = await InitConversation(request);
request.ConversationId = conversationId.Id;
instruction.AgentId = request.AgentId;
instruction.ConversationId = request.ConversationId;
// After creating session
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, request.AgentId);
if (twilio.MachineDetected(request))
{
response = new VoiceResponse();
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook => await hook.OnVoicemailStarting(request), request.AgentId);
@ -119,7 +123,7 @@ public class TwilioInboundController : TwilioController
await Task.Delay(1500);
await twilio.StartRecording(request.CallSid, request.AgentId, request.ConversationId);
});
}
}
return TwiML(response);
}
@ -204,7 +208,7 @@ public class TwilioInboundController : TwilioController
storage.Append(conversation.Id, new RoleDialogModel(AgentRole.User, request.Intent)
{
CurrentAgentId = conversation.Id,
CurrentAgentId = agent.Id,
CreatedAt = DateTime.UtcNow
});
}