Merge branch 'SciSharp:master' into master

This commit is contained in:
Joanna Ren 2024-11-05 16:20:59 -06:00 committed by GitHub
commit 24ae09e3fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 751 additions and 54 deletions

View file

@ -117,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Graph", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.AudioHandler", "src\Plugins\BotSharp.Plugin.AudioHandler\BotSharp.Plugin.AudioHandler.csproj", "{F57F4862-F8D4-44A1-AC12-5C131B5C9785}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.SideCar", "src\Infrastructure\BotSharp.Core.SideCar\BotSharp.Core.SideCar.csproj", "{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -469,6 +471,14 @@ Global
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.Build.0 = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.ActiveCfg = Release|Any CPU
{F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.Build.0 = Release|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.ActiveCfg = Debug|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.Build.0 = Debug|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.Build.0 = Release|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.ActiveCfg = Release|Any CPU
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -525,6 +535,7 @@ Global
{97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
{EBFE97DA-D0BA-48BA-8B5D-083B60348D1D} = {97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1}
{F57F4862-F8D4-44A1-AC12-5C131B5C9785} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
@ -38,6 +38,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Rougamo.Fody" Version="4.0.0" />
<PackageReference Include="AspectInjector" Version="2.8.2" />
</ItemGroup>
</Project>

View file

@ -30,6 +30,8 @@ public class PageActionArgs
/// </summary>
public string[]? IncludeResponseUrls { get; set; }
public List<string>? Selectors { get; set; }
/// <summary>
/// If set to true, the response will be stored in memory
/// </summary>

View file

@ -19,4 +19,8 @@ public interface IConversationStateService
bool RemoveState(string name);
void CleanStates(params string[] excludedStates);
void Save();
ConversationState GetCurrentState();
void SetCurrentState(ConversationState state);
void ResetCurrentState();
}

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Conversations.Models;
public class ConversationContext
{
public ConversationState State { get; set; }
public List<DialogElement> Dialogs { get; set; } = new();
public List<ConversationBreakpoint> Breakpoints { get; set; } = new();
public int RecursiveCounter { get; set; }
public Stack<string> RoutingStack { get; set; } = new();
}

View file

@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Files.Models;
public class FileInformation
{
/// <summary>
/// External file url
/// External file url for display
/// </summary>
[JsonPropertyName("file_url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@ -35,4 +35,11 @@ public class FileInformation
[JsonPropertyName("file_extension")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FileExtension { get; set; } = string.Empty;
/// <summary>
/// External file url for download
/// </summary>
[JsonPropertyName("file_download_url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FileDownloadUrl { get; set; } = string.Empty;
}

View file

@ -3,5 +3,6 @@ namespace BotSharp.Abstraction.Plugins.Models
public class PluginFilter
{
public Pagination Pager { get; set; } = new Pagination();
public IEnumerable<string>? Names { get; set; }
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Shared;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Translation.Models;
using BotSharp.Abstraction.Users.Models;
@ -8,7 +9,7 @@ using BotSharp.Abstraction.VectorStorage.Models;
namespace BotSharp.Abstraction.Repositories;
public interface IBotSharpRepository
public interface IBotSharpRepository : IHaveServiceProvider
{
#region Plugin
PluginConfig GetPluginConfig();

View file

@ -17,4 +17,15 @@ public interface IRoutingContext
void PopTo(string agentId, string reason);
void Replace(string agentId, string? reason = null);
void Empty(string? reason = null);
int CurrentRecursionDepth { get; }
int GetRecursiveCounter();
void IncreaseRecursiveCounter();
void SetRecursiveCounter(int counter);
void ResetRecursiveCounter();
Stack<string> GetAgentStack();
void SetAgentStack(Stack<string> stack);
void ResetAgentStack();
}

View file

@ -27,7 +27,11 @@ public interface IRoutingService
RoutingRule[] GetRulesByAgentId(string id);
List<RoutingHandlerDef> GetHandlers(Agent router);
void ResetRecursiveCounter();
//void ResetRecursiveCounter();
//int GetRecursiveCounter();
//void SetRecursiveCounter(int counter);
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs);
Task<bool> InvokeFunction(string name, RoleDialogModel messages);
Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs);

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Shared;
public interface IHaveServiceProvider
{
IServiceProvider ServiceProvider { get; }
}

View file

@ -0,0 +1,168 @@
using AspectInjector.Broker;
using BotSharp.Abstraction.Shared;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace BotSharp.Abstraction.SideCar.Attributes;
[Aspect(Scope.PerInstance)]
public class SideCarAspect
{
[Advice(Kind.Around)]
public object Handle(
[Argument(Source.Target)] Func<object[], object> target,
[Argument(Source.Arguments)] object[] args,
[Argument(Source.Instance)] object instance,
[Argument(Source.ReturnType)] Type retType,
[Argument(Source.Name)] string name,
[Argument(Source.Metadata)] MethodBase metaData,
[Argument(Source.Triggers)] Attribute[] triggers)
{
object value;
var serviceProvider = ((IHaveServiceProvider)instance).ServiceProvider;
if (typeof(Task).IsAssignableFrom(retType))
{
var syncResultType = retType.IsConstructedGenericType ? retType.GenericTypeArguments[0] : typeof(void);
value = CallAsyncMethod(serviceProvider, syncResultType, name, target, args);
}
else
{
value = CallSyncMethod(serviceProvider, retType, name, target, args);
}
return value;
}
private static MethodInfo GetMethod(string name)
{
return typeof(SideCarAspect).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static);
}
private object CallAsyncMethod(IServiceProvider serviceProvider, Type retType, string methodName, Func<object[], object> target, object[] args)
{
var sidecar = serviceProvider.GetService<IConversationSideCar>();
var sidecarMethod = sidecar?.GetType()?.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
object value;
var enabled = sidecar != null && sidecar.IsEnabled() && sidecarMethod != null;
if (retType == typeof(void))
{
if (enabled)
{
value = GetMethod(nameof(CallAsync)).Invoke(this, [sidecar, sidecarMethod, args]);
}
else
{
value = GetMethod(nameof(WrapAsync)).Invoke(this, [target, args]);
}
}
else
{
if (enabled)
{
value = GetMethod(nameof(CallGenericAsync)).MakeGenericMethod(retType).Invoke(this, [sidecar, sidecarMethod, args]);
}
else
{
value = GetMethod(nameof(WrapGenericAsync)).MakeGenericMethod(retType).Invoke(this, [target, args]);
}
}
return value;
}
private object CallSyncMethod(IServiceProvider serviceProvider, Type retType, string methodName, Func<object[], object> target, object[] args)
{
var sidecar = serviceProvider.GetService<IConversationSideCar>();
var sidecarMethod = sidecar?.GetType()?.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
object value;
var enabled = sidecar != null && sidecarMethod != null && sidecar.IsEnabled();
if (retType == typeof(void))
{
if (enabled)
{
value = GetMethod(nameof(CallSync)).Invoke(this, [sidecar, sidecarMethod, args]);
}
else
{
value = GetMethod(nameof(WrapSync)).Invoke(this, [target, args]);
}
}
else
{
if (enabled)
{
value = GetMethod(nameof(CallGenericSync)).MakeGenericMethod(retType).Invoke(this, [sidecar, sidecarMethod, args]);
}
else
{
value = GetMethod(nameof(WrapGenericSync)).MakeGenericMethod(retType).Invoke(this, [target, args]);
}
}
return value;
}
#region Call Side car method
private static async Task<T> CallGenericAsync<T>(object instance, MethodInfo method, object[] args)
{
var res = await (Task<T>)method.Invoke(instance, args);
return res;
}
private static async Task CallAsync(object instance, MethodInfo method, object[] args)
{
await (Task)method.Invoke(instance, args);
return;
}
private static T CallGenericSync<T>(object instance, MethodInfo method, object[] args)
{
var res = (T)method.Invoke(instance, args);
return res;
}
private static void CallSync(object instance, MethodInfo method, object[] args)
{
method.Invoke(instance, args);
return;
}
#endregion
#region Call original method
private static T WrapGenericSync<T>(Func<object[], object> target, object[] args)
{
T res;
res = (T)target(args);
return res;
}
private static async Task<T> WrapGenericAsync<T>(Func<object[], object> target, object[] args)
{
T res;
res = await (Task<T>)target(args);
return res;
}
private static void WrapSync(Func<object[], object> target, object[] args)
{
target(args);
return;
}
private static async Task WrapAsync(Func<object[], object> target, object[] args)
{
await (Task)target(args);
return;
}
#endregion
}

View file

@ -0,0 +1,13 @@
using AspectInjector.Broker;
namespace BotSharp.Abstraction.SideCar.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
[Injection(typeof(SideCarAspect))]
public class SideCarAttribute : Attribute
{
public SideCarAttribute()
{
}
}

View file

@ -0,0 +1,13 @@
namespace BotSharp.Abstraction.SideCar;
public interface IConversationSideCar
{
string Provider { get; }
bool IsEnabled();
void AppendConversationDialogs(string conversationId, List<DialogElement> messages);
List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
Task<RoleDialogModel> SendMessage(string agentId, string text, PostbackMessageModel? postback = null, List<MessageState>? states = null);
}

View file

@ -25,9 +25,10 @@ public interface IAuthenticationHook
/// <summary>
/// User authenticated successfully
/// </summary>
/// <param name="user"></param>
/// <param name="token"></param>
/// <returns></returns>
bool UserAuthenticated(JwtSecurityToken token)
bool UserAuthenticated(User user, Token token)
=> true;
/// <summary>

View file

@ -13,8 +13,6 @@ public interface IUserIdentity
/// </summary>
string UserLanguage { get; }
string? Phone { get; }
string? AffiliateId { get; }
string? EmployeeId { get; }
string Type { get; }
string Role { get; }
string? RegionCode { get; }

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<OutputPath>$(SolutionDir)packages</OutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
using BotSharp.Abstraction.Plugins;
using BotSharp.Abstraction.Settings;
using BotSharp.Core.SideCar.Services;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Core.SideCar;
public class BotSharpSideCarPlugin : IBotSharpPlugin
{
public string Id => "06e5a276-bba0-45af-9625-889267c341c9";
public string Name => "Side car";
public string Description => "Provides side car for calling agent cluster in conversation";
public SettingsMeta Settings => new SettingsMeta("SideCar");
public object GetNewSettingsInstance() => new SideCarSettings();
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
var settings = new SideCarSettings();
config.Bind("SideCar", settings);
services.AddSingleton(settings);
if (settings.Conversation.Provider == "botsharp")
{
services.AddScoped<IConversationSideCar, BotSharpConversationSideCar>();
}
}
}

View file

@ -0,0 +1,135 @@
namespace BotSharp.Core.SideCar.Services;
public class BotSharpConversationSideCar : IConversationSideCar
{
private readonly IServiceProvider _services;
private readonly ILogger<BotSharpConversationSideCar> _logger;
private Stack<ConversationContext> contextStack = new();
private bool enabled = false;
public string Provider => "botsharp";
public BotSharpConversationSideCar(
IServiceProvider services,
ILogger<BotSharpConversationSideCar> logger)
{
_services = services;
_logger = logger;
}
public bool IsEnabled()
{
return enabled;
}
public void AppendConversationDialogs(string conversationId, List<DialogElement> messages)
{
if (contextStack.IsNullOrEmpty()) return;
var top = contextStack.Peek();
top.Dialogs.AddRange(messages);
}
public List<DialogElement> GetConversationDialogs(string conversationId)
{
if (contextStack.IsNullOrEmpty())
{
return new List<DialogElement>();
}
return contextStack.Peek().Dialogs;
}
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
{
if (contextStack.IsNullOrEmpty()) return;
var top = contextStack.Peek().Breakpoints;
top.Add(breakpoint);
}
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{
if (contextStack.IsNullOrEmpty())
{
return null;
}
var top = contextStack.Peek().Breakpoints;
return top.LastOrDefault();
}
public async Task<RoleDialogModel> SendMessage(string agentId, string text,
PostbackMessageModel? postback = null, List<MessageState>? states = null)
{
BeforeExecute();
var response = await InnerExecute(agentId, text, postback, states);
AfterExecute();
return response;
}
private async Task<RoleDialogModel> InnerExecute(string agentId, string text,
PostbackMessageModel? postback = null, List<MessageState>? states = null)
{
var conv = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingService>();
var state = _services.GetRequiredService<IConversationStateService>();
var inputMsg = new RoleDialogModel(AgentRole.User, text);
routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId);
states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
var response = new RoleDialogModel(AgentRole.Assistant, string.Empty);
await conv.SendMessage(agentId, inputMsg,
replyMessage: postback,
async msg =>
{
response.Content = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content;
response.FunctionName = msg.FunctionName;
response.RichContent = msg.SecondaryRichContent ?? msg.RichContent;
response.Instruction = msg.Instruction;
response.Data = msg.Data;
});
return response;
}
private void BeforeExecute()
{
enabled = true;
var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
var node = new ConversationContext
{
State = state.GetCurrentState(),
Dialogs = new(),
Breakpoints = new(),
RecursiveCounter = routing.Context.GetRecursiveCounter(),
RoutingStack = routing.Context.GetAgentStack()
};
contextStack.Push(node);
// Reset
state.ResetCurrentState();
routing.Context.ResetRecursiveCounter();
routing.Context.ResetAgentStack();
}
private void AfterExecute()
{
var state = _services.GetRequiredService<IConversationStateService>();
var routing = _services.GetRequiredService<IRoutingService>();
var node = contextStack.Pop();
// Recover
state.SetCurrentState(node.State);
routing.Context.SetRecursiveCounter(node.RecursiveCounter);
routing.Context.SetAgentStack(node.RoutingStack);
enabled = false;
}
}

View file

@ -0,0 +1,11 @@
namespace BotSharp.Core.SideCar.Settings;
public class SideCarSettings
{
public BaseSetting Conversation { get; set; }
}
public class BaseSetting
{
public string Provider { get; set; }
}

View file

@ -0,0 +1,20 @@
global using System;
global using System.Collections.Generic;
global using System.Text;
global using System.Threading.Tasks;
global using System.Linq;
global using System.Text.Json;
global using System.Net.Mime;
global using System.Net.Http;
global using System.Threading;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Conversations;
global using BotSharp.Abstraction.Conversations.Enums;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Models;
global using BotSharp.Abstraction.Routing;
global using BotSharp.Abstraction.SideCar;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Core.SideCar.Settings;

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Users.Enums;
using System.IO;
using System.Text.RegularExpressions;
@ -25,6 +26,20 @@ public partial class AgentService
var user = _db.GetUserById(_user.Id);
_db.BulkInsertAgents(new List<Agent> { agentRecord });
if (!UserConstant.AdminRoles.Contains(user.Role))
{
_db.BulkInsertUserAgents(new List<UserAgent>
{
new UserAgent
{
UserId = user.Id,
AgentId = agentRecord.Id,
Actions = new List<string> { UserAction.Edit, UserAction.Chat },
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
}
});
}
Utilities.ClearCache();
return await Task.FromResult(agentRecord);

View file

@ -1,7 +1,6 @@
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Routing.Planning;
namespace BotSharp.Core.Conversations.Services;
@ -90,7 +89,7 @@ public partial class ConversationService
response = await routing.InstructDirect(agent, message);
}
routing.ResetRecursiveCounter();
routing.Context.ResetRecursiveCounter();
}
await HandleAssistantMessage(response, onMessageReceived);

View file

@ -98,6 +98,7 @@ public partial class ConversationService : IConversationService
var record = sess;
record.Id = sess.Id.IfNullOrEmptyAs(Guid.NewGuid().ToString());
record.UserId = sess.UserId.IfNullOrEmptyAs(foundUserId);
record.Tags = sess.Tags;
record.Title = "New Conversation";
db.CreateNewConversation(record);
@ -141,6 +142,7 @@ public partial class ConversationService : IConversationService
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var breakpoint = db.GetConversationBreakpoint(_conversationId);
if (breakpoint != null)
{
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList();
@ -151,9 +153,7 @@ public partial class ConversationService : IConversationService
}
}
return dialogs
.TakeLast(lastCount)
.ToList();
return dialogs.TakeLast(lastCount).ToList();
}
public void SetConversationId(string conversationId, List<MessageState> states, bool isReadOnly = false)

View file

@ -384,4 +384,23 @@ public class ConversationStateService : IConversationStateService, IDisposable
}
return true;
}
public ConversationState GetCurrentState()
{
var values = _curStates.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
return new ConversationState(copy ?? new());
}
public void SetCurrentState(ConversationState state)
{
var values = _curStates.Values.ToList();
var copy = JsonSerializer.Deserialize<List<StateKeyValue>>(JsonSerializer.Serialize(values));
_curStates = new ConversationState(copy ?? new());
}
public void ResetCurrentState()
{
_curStates.Clear();
}
}

View file

@ -69,6 +69,7 @@ public partial class LocalFileStorageService
{
MessageId = messageId,
FileUrl = $"/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}",
FileDownloadUrl = $"/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download",
FileStorageUrl = file,
FileName = fileName,
FileExtension = fileExtension,

View file

@ -31,7 +31,7 @@ public class DistributedLocker
}
}
public async Task<T> Lock<T>(string resource, Func<T> action, int timeoutInSeconds = 30)
public async Task Lock(string resource, Action action, int timeoutInSeconds = 30)
{
await ConnectToRedis();
@ -45,7 +45,7 @@ public class DistributedLocker
Serilog.Log.Logger.Error($"Acquire lock for {resource} failed due to after {timeout}s timeout.");
}
return action();
action();
}
}

View file

@ -126,6 +126,12 @@ public class PluginLoader
var plugins = GetPlugins(services);
var pager = filter?.Pager ?? new Pagination();
// Apply filter
if (!filter.Names.IsNullOrEmpty())
{
plugins = plugins.Where(x => filter.Names.Any(n => x.Name.IsEqualTo(n))).ToList();
}
return new PagedItems<PluginDef>
{
Items = plugins.Skip(pager.Offset).Take(pager.Size),

View file

@ -8,6 +8,8 @@ namespace BotSharp.Core.Repository;
public class BotSharpDbContext : Database, IBotSharpRepository
{
public IServiceProvider ServiceProvider => throw new NotImplementedException();
#region Plugin
public PluginConfig GetPluginConfig() => throw new NotImplementedException();
public void SavePluginConfig(PluginConfig config) => throw new NotImplementedException();
@ -90,12 +92,14 @@ public class BotSharpDbContext : Database, IBotSharpRepository
public List<string> GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable<string> excludeAgentIds)
=> throw new NotImplementedException();
[SideCar]
public List<DialogElement> GetConversationDialogs(string conversationId)
=> throw new NotImplementedException();
public ConversationState GetConversationStates(string conversationId)
=> throw new NotImplementedException();
[SideCar]
public void AppendConversationDialogs(string conversationId, List<DialogElement> dialogs)
=> throw new NotImplementedException();
@ -108,9 +112,11 @@ public class BotSharpDbContext : Database, IBotSharpRepository
public bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request)
=> throw new NotImplementedException();
[SideCar]
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
=> throw new NotImplementedException();
[SideCar]
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
=> throw new NotImplementedException();

View file

@ -1,5 +1,7 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Users.Models;
using Microsoft.Extensions.Logging;
using System.IO;
namespace BotSharp.Core.Repository
@ -188,7 +190,7 @@ namespace BotSharp.Core.Repository
// Save default instructions
var instructionFile = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{_agentSettings.TemplateFormat}");
File.WriteAllText(instructionFile, instruction ?? string.Empty);
Thread.Sleep(100);
Thread.Sleep(50);
// Save channel instructions
foreach (var ci in channelInstructions)
@ -197,7 +199,7 @@ namespace BotSharp.Core.Repository
var file = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{ci.Channel}.{_agentSettings.TemplateFormat}");
File.WriteAllText(file, ci.Instruction ?? string.Empty);
Thread.Sleep(100);
Thread.Sleep(50);
}
}
@ -451,12 +453,63 @@ namespace BotSharp.Core.Repository
public void BulkInsertAgents(List<Agent> agents)
{
_agents = [];
if (agents.IsNullOrEmpty()) return;
var baseDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir);
foreach (var agent in agents)
{
var dir = Path.Combine(baseDir, agent.Id);
if (Directory.Exists(dir)) continue;
Directory.CreateDirectory(dir);
Thread.Sleep(50);
var agentFile = Path.Combine(dir, AGENT_FILE);
var json = JsonSerializer.Serialize(agent, _options);
File.WriteAllText(agentFile, json);
if (!string.IsNullOrWhiteSpace(agent.Instruction))
{
var instDir = Path.Combine(dir, AGENT_INSTRUCTIONS_FOLDER);
Directory.CreateDirectory(instDir);
var instFile = Path.Combine(instDir, $"{AGENT_INSTRUCTION_FILE}.{_agentSettings.TemplateFormat}");
File.WriteAllText(instFile, agent.Instruction);
}
}
Reset();
}
public void BulkInsertUserAgents(List<UserAgent> userAgents)
{
_userAgents = [];
if (userAgents.IsNullOrEmpty()) return;
var groups = userAgents.GroupBy(x => x.UserId);
var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER);
foreach (var group in groups)
{
var filtered = group.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList();
if (filtered.IsNullOrEmpty()) continue;
filtered.ForEach(x => x.Id = Guid.NewGuid().ToString());
var userId = filtered.First().UserId;
var userDir = Path.Combine(usersDir, userId);
if (!Directory.Exists(userDir)) continue;
var userAgentFile = Path.Combine(userDir, USER_AGENT_FILE);
var list = new List<UserAgent>();
if (File.Exists(userAgentFile))
{
var str = File.ReadAllText(userAgentFile);
list = JsonSerializer.Deserialize<List<UserAgent>>(str, _options);
}
list.AddRange(filtered);
File.WriteAllText(userAgentFile, JsonSerializer.Serialize(list, _options));
Thread.Sleep(50);
}
Reset();
}
public bool DeleteAgents()
@ -493,8 +546,7 @@ namespace BotSharp.Core.Repository
// Delete agent folder
Directory.Delete(agentDir, true);
_agents = [];
_userAgents = [];
Reset();
return true;
}
catch
@ -502,5 +554,11 @@ namespace BotSharp.Core.Repository
return false;
}
}
private void Reset()
{
_agents = [];
_userAgents = [];
}
}
}

View file

@ -57,6 +57,7 @@ namespace BotSharp.Core.Repository
return true;
}
[SideCar]
public List<DialogElement> GetConversationDialogs(string conversationId)
{
var dialogs = new List<DialogElement>();
@ -78,6 +79,7 @@ namespace BotSharp.Core.Repository
return dialogs;
}
[SideCar]
public void AppendConversationDialogs(string conversationId, List<DialogElement> dialogs)
{
var convDir = FindConversationDirectory(conversationId);
@ -182,6 +184,7 @@ namespace BotSharp.Core.Repository
return true;
}
[SideCar]
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
{
var convDir = FindConversationDirectory(conversationId);
@ -220,6 +223,7 @@ namespace BotSharp.Core.Repository
}
}
[SideCar]
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{
var convDir = FindConversationDirectory(conversationId);

View file

@ -171,6 +171,8 @@ public partial class FileRepository : IBotSharpRepository
}
}
public IServiceProvider ServiceProvider => _services;
#region Private methods
private void DeleteBeforeCreateDirectory(string dir)

View file

@ -147,7 +147,7 @@ public class SequentialPlanner : IRoutingPlaner
context.Pop();
var routing = _services.GetRequiredService<IRoutingService>();
routing.ResetRecursiveCounter();
routing.Context.ResetRecursiveCounter();
return true;
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Abstraction.Utilities;
namespace BotSharp.Core.Routing;
@ -10,6 +9,7 @@ public class RoutingContext : IRoutingContext
private string[] _routerAgentIds;
private string _conversationId;
private string _messageId;
private int _currentRecursionDepth = 0;
public RoutingContext(IServiceProvider services, RoutingSettings setting)
{
@ -20,9 +20,9 @@ public class RoutingContext : IRoutingContext
public int AgentCount => _stack.Count;
public string ConversationId => _conversationId;
public string MessageId => _messageId;
public int CurrentRecursionDepth => _currentRecursionDepth;
private Stack<string> _stack { get; set; }
= new Stack<string>();
private Stack<string> _stack { get; set; } = new();
/// <summary>
/// Intent name
@ -208,4 +208,39 @@ public class RoutingContext : IRoutingContext
_conversationId = conversationId;
_messageId = messageId;
}
public int GetRecursiveCounter()
{
return _currentRecursionDepth;
}
public void IncreaseRecursiveCounter()
{
_currentRecursionDepth++;
}
public void SetRecursiveCounter(int counter)
{
_currentRecursionDepth = counter;
}
public void ResetRecursiveCounter()
{
_currentRecursionDepth = 0;
}
public Stack<string> GetAgentStack()
{
return new Stack<string>(_stack);
}
public void SetAgentStack(Stack<string> stack)
{
_stack = new Stack<string>(stack);
}
public void ResetAgentStack()
{
_stack.Clear();
}
}

View file

@ -4,14 +4,15 @@ namespace BotSharp.Core.Routing;
public partial class RoutingService
{
private int _currentRecursionDepth = 0;
//private int _currentRecursionDepth = 0;
public async Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs)
{
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
_currentRecursionDepth++;
if (_currentRecursionDepth > agent.LlmConfig.MaxRecursionDepth)
//_currentRecursionDepth++;
Context.IncreaseRecursiveCounter();
if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth)
{
_logger.LogWarning($"Current recursive call depth greater than {agent.LlmConfig.MaxRecursionDepth}, which will cause unexpected result.");
return false;
@ -36,8 +37,7 @@ public partial class RoutingService
if (response.Role == AgentRole.Function)
{
message = RoleDialogModel.From(message,
role: AgentRole.Function);
message = RoleDialogModel.From(message, role: AgentRole.Function);
if (response.FunctionName != null && response.FunctionName.Contains("/"))
{
response.FunctionName = response.FunctionName.Split("/").Last();
@ -57,9 +57,7 @@ public partial class RoutingService
response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
}
message = RoleDialogModel.From(message,
role: AgentRole.Assistant,
content: response.Content);
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
message.CurrentAgentId = agent.Id;
dialogs.Add(message);
}

View file

@ -16,12 +16,23 @@ public partial class RoutingService : IRoutingService
public IRoutingContext Context => _context;
public Agent Router => _router;
public void ResetRecursiveCounter()
{
_currentRecursionDepth = 0;
}
//public int GetRecursiveCounter()
//{
// return _currentRecursionDepth;
//}
public RoutingService(IServiceProvider services,
//public void SetRecursiveCounter(int counter)
//{
// _currentRecursionDepth = counter;
//}
//public void ResetRecursiveCounter()
//{
// _currentRecursionDepth = 0;
//}
public RoutingService(
IServiceProvider services,
RoutingSettings settings,
IRoutingContext context,
ILogger<RoutingService> logger)

View file

@ -70,18 +70,15 @@ public class UserIdentity : IUserIdentity
[JsonPropertyName("phone")]
public string? Phone => _claims?.FirstOrDefault(x => x.Type == "phone")?.Value;
[JsonPropertyName("affiliateId")]
public string? AffiliateId => _claims?.FirstOrDefault(x => x.Type == "affiliateId")?.Value;
[JsonPropertyName("employeeId")]
public string? EmployeeId => _claims?.FirstOrDefault(x => x.Type == "employeeId")?.Value;
[JsonPropertyName("type")]
public string? Type => _claims?.FirstOrDefault(x => x.Type == "type")?.Value;
[JsonPropertyName("role")]
public string? Role => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value;
[JsonPropertyName("regionCode")]
public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "regionCode")?.Value;
/// <summary>
/// US, CA, etc.
/// </summary>
[JsonPropertyName("region_code")]
public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "region_code")?.Value;
}

View file

@ -296,7 +296,7 @@ public class UserService : IUserService
var (token, jwt) = BuildToken(record);
foreach (var hook in hooks)
{
hook.UserAuthenticated(jwt);
hook.UserAuthenticated(record, token);
}
return token;

View file

@ -33,6 +33,7 @@ global using BotSharp.Abstraction.Files.Utilities;
global using BotSharp.Abstraction.Translation.Attributes;
global using BotSharp.Abstraction.Messaging.Enums;
global using BotSharp.Abstraction.Knowledges.Models;
global using BotSharp.Abstraction.SideCar.Attributes;
global using BotSharp.Core.Repository;
global using BotSharp.Core.Routing;
global using BotSharp.Core.Agents.Services;

View file

@ -1,6 +1,7 @@
using Azure;
using BotSharp.Abstraction.Files.Constants;
using BotSharp.Abstraction.Files.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Users.Enums;
@ -488,6 +489,26 @@ public class ConversationController : ControllerBase
}
return BuildFileResult(file);
}
[HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download")]
public IActionResult DownloadMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName)
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName);
if (string.IsNullOrEmpty(file))
{
return NotFound();
}
var fName = file.Split(Path.DirectorySeparatorChar).Last();
var contentType = FileUtility.GetFileContentType(fName);
var stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read);
var bytes = new byte[stream.Length];
stream.Read(bytes, 0, (int)stream.Length);
stream.Position = 0;
return new FileStreamResult(stream, contentType) { FileDownloadName = fName };
}
#endregion
#region Private methods

View file

@ -19,6 +19,10 @@ public class MessageFileViewModel
[JsonPropertyName("file_source")]
public string FileSource { get; set; }
[JsonPropertyName("file_download_url")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FileDownloadUrl { get; set; }
public MessageFileViewModel()
{
@ -32,7 +36,8 @@ public class MessageFileViewModel
FileName = model.FileName,
FileExtension = model.FileExtension,
ContentType = model.ContentType,
FileSource = model.FileSource
FileSource = model.FileSource,
FileDownloadUrl = model.FileDownloadUrl
};
}
}

View file

@ -58,7 +58,7 @@ public class UserViewModel
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
Phone = user.Phone?.Substring(0, 3) == "+86" ? user.Phone.Substring(3) : user.Phone,
Phone = !string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone,
Type = user.Type,
Role = user.Role,
Source = user.Source,

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.SideCar;
using Microsoft.AspNetCore.SignalR;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -32,6 +33,8 @@ public class ChatHubConversationHook : ConversationHookBase
public override async Task OnConversationInitialized(Conversation conversation)
{
if (!AllowSendingMessage()) return;
var userService = _services.GetRequiredService<IUserService>();
var conv = ConversationViewModel.FromSession(conversation);
@ -44,6 +47,8 @@ public class ChatHubConversationHook : ConversationHookBase
public override async Task OnMessageReceived(RoleDialogModel message)
{
if (!AllowSendingMessage()) return;
var conv = _services.GetRequiredService<IConversationService>();
var userService = _services.GetRequiredService<IUserService>();
var sender = await userService.GetMyProfile();
@ -90,6 +95,8 @@ public class ChatHubConversationHook : ConversationHookBase
public override async Task OnResponseGenerated(RoleDialogModel message)
{
if (!AllowSendingMessage()) return;
var conv = _services.GetRequiredService<IConversationService>();
var json = JsonSerializer.Serialize(new ChatResponseModel()
{
@ -156,6 +163,12 @@ public class ChatHubConversationHook : ConversationHookBase
}
#region Private methods
private bool AllowSendingMessage()
{
var sidecar = _services.GetService<IConversationSideCar>();
return sidecar == null || !sidecar.IsEnabled();
}
private async Task InitClientConversation(ConversationViewModel conversation)
{
await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation);

View file

@ -37,6 +37,7 @@ public class WebSocketsMiddleware
var regexes = new List<Regex>
{
new Regex(@"/conversation/(.*?)/message/(.*?)/(.*?)/file/(.*?)/(.*?)", RegexOptions.IgnoreCase),
new Regex(@"/conversation/(.*?)/message/(.*?)/(.*?)/file/(.*?)/(.*?)/download", RegexOptions.IgnoreCase),
new Regex(@"/user/avatar", RegexOptions.IgnoreCase),
new Regex(@"/knowledge/document/(.*?)/file/(.*?)", RegexOptions.IgnoreCase)
};

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Plugin.KnowledgeBase.Converters;
using BotSharp.Plugin.KnowledgeBase.Hooks;
using BotSharp.Plugin.KnowledgeBase.Services;
@ -25,7 +26,6 @@ public class KnowledgeBasePlugin : IBotSharpPlugin
services.AddSingleton<IPdf2TextConverter, PigPdf2TextConverter>();
services.AddScoped<IAgentUtilityHook, KnowledgeBaseUtilityHook>();
services.AddScoped<IAgentHook, KnowledgeBaseAgentHook>();
services.AddScoped<IKnowledgeService, KnowledgeService>();
}
@ -34,6 +34,7 @@ public class KnowledgeBasePlugin : IBotSharpPlugin
var section = menu.First(x => x.Label == "Apps");
menu.Add(new PluginMenuDef("Knowledge Base", icon: "bx bx-book-open", weight: section.Weight + 1)
{
Roles = new List<string> { UserRole.Root, UserRole.Admin },
SubMenu = new List<PluginMenuDef>
{
new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"),

View file

@ -429,10 +429,13 @@ public partial class MongoRepository
{
if (userAgents.IsNullOrEmpty()) return;
var userAgentDocs = userAgents.Select(x => new UserAgentDocument
var filtered = userAgents.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList();
if (filtered.IsNullOrEmpty()) return;
var userAgentDocs = filtered.Select(x => new UserAgentDocument
{
Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(),
UserId = !string.IsNullOrEmpty(x.UserId) ? x.UserId : string.Empty,
UserId = x.UserId,
AgentId = x.AgentId,
Actions = x.Actions,
CreatedTime = x.CreatedTime,

View file

@ -70,6 +70,7 @@ public partial class MongoRepository
|| contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0;
}
[SideCar]
public List<DialogElement> GetConversationDialogs(string conversationId)
{
var dialogs = new List<DialogElement>();
@ -83,6 +84,7 @@ public partial class MongoRepository
return formattedDialog ?? new List<DialogElement>();
}
[SideCar]
public void AppendConversationDialogs(string conversationId, List<DialogElement> dialogs)
{
if (string.IsNullOrEmpty(conversationId)) return;
@ -159,6 +161,7 @@ public partial class MongoRepository
return true;
}
[SideCar]
public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint)
{
if (string.IsNullOrEmpty(conversationId)) return;
@ -176,6 +179,7 @@ public partial class MongoRepository
_dc.ConversationStates.UpdateOne(filterState, updateState);
}
[SideCar]
public ConversationBreakpoint? GetConversationBreakpoint(string conversationId)
{
if (string.IsNullOrEmpty(conversationId))

View file

@ -22,4 +22,6 @@ public partial class MongoRepository : IBotSharpRepository
IsUpsert = true,
};
}
public IServiceProvider ServiceProvider => _services;
}

View file

@ -8,6 +8,7 @@ global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Abstraction.Plugins;
global using BotSharp.Abstraction.Translation.Models;
global using BotSharp.Abstraction.SideCar.Attributes;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using MongoDB.Bson;

View file

@ -92,7 +92,7 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner
}
var routing = _services.GetRequiredService<IRoutingService>();
routing.ResetRecursiveCounter();
routing.Context.ResetRecursiveCounter();
return true;
}

View file

@ -66,6 +66,7 @@ public partial class TencentCosService
{
MessageId = messageId,
FileUrl = BuilFileUrl(file),
FileDownloadUrl = BuilFileUrl(file),
FileStorageUrl = file,
FileName = fileName,
FileExtension = fileExtension,

View file

@ -1,3 +1,5 @@
using Microsoft.Playwright;
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
public partial class PlaywrightWebDriver
@ -47,15 +49,37 @@ public partial class PlaywrightWebDriver
var response = await page.GotoAsync(args.Url, new PageGotoOptions
{
Timeout = args.Timeout
Timeout = args.Timeout > 0 ? args.Timeout : 30000
});
if (args.Selectors != null)
{
// 使用传入的选择器列表进行并行等待
var tasks =args.Selectors.Select(selector =>
page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions
{
Timeout = args.Timeout > 0 ? args.Timeout : 30000
})
).ToArray();
await Task.WhenAll(tasks);
// 在此处提取所有选择器的 HTML 内容
var contentTasks = args.Selectors.Select(selector => page.InnerHTMLAsync(selector)).ToArray();
var contents = await Task.WhenAll(contentTasks);
result.IsSuccess = true;
result.Body = string.Join(", ", contents.Select((content, index) => $"{args.Selectors[index]}: {content}"));
return result;
}
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
if (args.WaitForNetworkIdle)
{
await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions
{
Timeout = args.Timeout
Timeout = args.Timeout > 0 ? args.Timeout : 30000
});
}

View file

@ -29,6 +29,7 @@
<ItemGroup>
<ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" />
<ProjectReference Include="..\BotSharp.ServiceDefaults\BotSharp.ServiceDefaults.csproj" />
<ProjectReference Include="..\Infrastructure\BotSharp.Core.SideCar\BotSharp.Core.SideCar.csproj" />
</ItemGroup>

View file

@ -151,6 +151,12 @@
}
},
"SideCar": {
"Conversation": {
"Provider": "botsharp"
}
},
"WebBrowsing": {
"Driver": "Playwright"
},
@ -321,6 +327,7 @@
"PluginLoader": {
"Assemblies": [
"BotSharp.Core",
"BotSharp.Core.SideCar",
"BotSharp.Logger",
"BotSharp.Plugin.MongoStorage",
"BotSharp.Plugin.Dashboard",