Merge branch 'SciSharp:master' into master
This commit is contained in:
commit
404a3ed772
11
BotSharp.sln
11
BotSharp.sln
|
|
@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Rules", "src\
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI", "src\Plugins\BotSharp.Plugin.DeepSeekAI\BotSharp.Plugin.DeepSeekAI.csproj", "{AF329442-B48E-4B48-A18A-1C869D1BA6F5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Realtime", "src\Infrastructure\BotSharp.Core.Realtime\BotSharp.Core.Realtime.csproj", "{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -519,6 +521,14 @@ Global
|
|||
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -580,6 +590,7 @@ Global
|
|||
{F812BAAE-5A7D-4DF7-8E71-70696B51C61F} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
|
||||
{AFD64412-4D6A-452E-82A2-79E5D8842E29} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
|
||||
{AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
|
||||
{7ACD8C95-C66B-436A-80E7-541A57D8C3F4} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<BotSharpVersion>4.0.0</BotSharpVersion>
|
||||
<BotSharpVersion>4.1.0</BotSharpVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<MSExtensionsVersion>8.0.0</MSExtensionsVersion>
|
||||
<AspNetCoreVersion>2.3.0</AspNetCoreVersion>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="EntityFramework" Version="6.4.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="$(MSExtensionsVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MSExtensionsVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MSExtensionsVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MSExtensionsVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Memory.Data" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="Serilog" Version="2.10.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageVersion Include="Rougamo.Fody" Version="4.0.4" />
|
||||
<PackageVersion Include="Aspects.Cache" Version="2.0.4" />
|
||||
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
|
||||
|
|
@ -27,14 +29,96 @@
|
|||
<PackageVersion Include="Nanoid" Version="3.1.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
|
||||
<PackageVersion Include="Anthropic.SDK" Version="4.4.2" />
|
||||
<PackageVersion Include="Anthropic.SDK" Version="4.7.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageVersion Include="NAudio" Version="2.2.1" />
|
||||
<PackageVersion Include="NAudio.Core" Version="2.2.1" />
|
||||
<PackageVersion Include="Whisper.net" Version="1.5.0" />
|
||||
<PackageVersion Include="Whisper.net.Runtime" Version="1.5.0" />
|
||||
<PackageVersion Include="NCrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.1.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.1.0" />
|
||||
<PackageVersion Include="MailKit" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="MySql.Data" Version="9.0.0" />
|
||||
<PackageVersion Include="NPOI" Version="2.7.1" />
|
||||
<PackageVersion Include="LLMSharp.Google.Palm" Version="1.0.2" />
|
||||
<PackageVersion Include="Mscc.GenerativeAI" Version="2.2.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||
<PackageVersion Include="Refit" Version="8.0.0" />
|
||||
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
|
||||
<PackageVersion Include="Jint" Version="4.1.0" />
|
||||
<PackageVersion Include="PdfPig" Version="0.1.8" />
|
||||
<PackageVersion Include="TensorFlow.Keras" Version="0.15.0" />
|
||||
<PackageVersion Include="LangChain.Providers.Google.VertexAI" Version="0.15.3-dev.58" />
|
||||
<PackageVersion Include="LLamaSharp" Version="0.20.0" />
|
||||
<PackageVersion Include="FaissMask" Version="0.2.0" />
|
||||
<PackageVersion Include="FastText.NetWrapper" Version="1.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25161.3" />
|
||||
<PackageVersion Include="System.Text.Encodings.Web" Version="8.0.0" />
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.1.0" />
|
||||
<PackageVersion Include="Docnet.Core" Version="2.7.0-alpha.1" />
|
||||
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="13.8.0" />
|
||||
<PackageVersion Include="Magick.NET.Core" Version="13.8.0" />
|
||||
<PackageVersion Include="OpenCvSharp4.runtime.win" Version="4.9.0.20240103" />
|
||||
<PackageVersion Include="Sdcb.PaddleInference" Version="2.5.0.1" />
|
||||
<PackageVersion Include="Sdcb.PaddleInference.runtime.win64.mkl" Version="2.5.1" />
|
||||
<PackageVersion Include="Sdcb.PaddleOCR" Version="2.7.0.1" />
|
||||
<PackageVersion Include="Sdcb.PaddleOCR.Models.LocalV3" Version="2.7.0.1" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="8.0.7" />
|
||||
<PackageVersion Include="pythonnet" Version="3.0.4" />
|
||||
<PackageVersion Include="Qdrant.Client" Version="1.13.0" />
|
||||
<PackageVersion Include="Selenium.WebDriver" Version="4.27.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Abstractions" Version="1.16.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.16.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
|
||||
<PackageVersion Include="Sdcb.SparkDesk" Version="3.1.0" />
|
||||
<PackageVersion Include="MySqlConnector" Version="2.3.7" />
|
||||
<PackageVersion Include="Npgsql" Version="8.0.5" />
|
||||
<PackageVersion Include="Tencent.QCloud.Cos.Sdk" Version="5.4.39" />
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.7.27" />
|
||||
<PackageVersion Include="StrongGrid" Version="0.108.0" />
|
||||
<PackageVersion Include="Twilio.AspNet.Common" Version="8.1.1" />
|
||||
<PackageVersion Include="Twilio.AspNet.Core" Version="8.1.1" />
|
||||
<PackageVersion Include="Microsoft.Playwright" Version="1.49.0" />
|
||||
<PackageVersion Include="Senparc.Weixin.MP.Middleware" Version="1.2.1" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="3.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="8.2.0" />
|
||||
<PackageVersion Include="Aspire.Hosting.NodeJs" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.16.0" />
|
||||
<PackageVersion Include="Moq" Version="4.20.70" />
|
||||
<PackageVersion Include="xunit" Version="2.9.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageVersion Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="BotSharp.Logger" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.OpenAPI" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.Dashboard" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.AzureOpenAI" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.GoogleAI" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.HuggingFace" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.KnowledgeBase" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.MetaAI" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.LLamaSharp" Version="$(BotSharpVersion)" />
|
||||
<PackageVersion Include="BotSharp.Plugin.ChatHub" Version="$(BotSharpVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.5" />
|
||||
<PackageVersion Include="AspNet.Security.OAuth.GitHub" Version="8.0.0" />
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.0" />
|
||||
<PackageReference Include="Aspire.Hosting.NodeJs" Version="8.0.1" />
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" />
|
||||
<PackageReference Include="Aspire.Hosting.NodeJs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@
|
|||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="Serilog.Sinks.OpenTelemetry" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public interface IAgentService
|
|||
Task<Agent> CreateAgent(Agent agent);
|
||||
Task<string> RefreshAgents();
|
||||
Task<PagedItems<Agent>> GetAgents(AgentFilter filter);
|
||||
Task<List<IdName>> GetAgentOptions();
|
||||
Task<List<IdName>> GetAgentOptions(List<string>? agentIds = null);
|
||||
|
||||
/// <summary>
|
||||
/// Load agent configurations and trigger hooks
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
<PackageIcon>Icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
|
||||
<OutputPath>$(SolutionDir)packages</OutputPath>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ public class PageActionArgs
|
|||
/// <summary>
|
||||
/// This value has to be set to true if you want to get the page XHR/ Fetch responses
|
||||
/// </summary>
|
||||
public bool OpenNewTab { get; set; } = false;
|
||||
[JsonPropertyName("open_new_tab")]
|
||||
public bool OpenNewTab { get; set; } = true;
|
||||
[JsonPropertyName("open_blank_page")]
|
||||
public bool OpenBlankPage { get; set; } = true;
|
||||
|
||||
public bool EnableResponseCallback { get; set; } = false;
|
||||
|
||||
|
|
@ -47,4 +50,6 @@ public class PageActionArgs
|
|||
public int WaitTime { get; set; }
|
||||
|
||||
public bool ReadInnerHTMLAsBody { get; set; } = false;
|
||||
[JsonPropertyName("keep_browser_open")]
|
||||
public bool KeepBrowserOpen { get; set; } = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,10 @@ namespace BotSharp.Abstraction.Browsing.Settings;
|
|||
public class WebBrowsingSettings
|
||||
{
|
||||
public string Driver { get; set; } = "Playwright";
|
||||
public bool Headless { get; set; }
|
||||
// Default timeout in milliseconds
|
||||
public float DefaultTimeout { get; set; } = 30000;
|
||||
public bool IsEnableScreenshot { get; set; }
|
||||
// Default wait time in seconds after page is opened
|
||||
public int DefaultWaitTime { get; set; } = 5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ public static class MessageTypeName
|
|||
public const string Notification = "notification";
|
||||
public const string FunctionCall = "function";
|
||||
public const string Audio = "audio";
|
||||
public const string Error = "error";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public interface IConversationService
|
|||
Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter);
|
||||
Task<Conversation> UpdateConversationTitle(string id, string title);
|
||||
Task<Conversation> UpdateConversationTitleAlias(string id, string titleAlias);
|
||||
Task<bool> UpdateConversationTags(string conversationId, List<string> tags);
|
||||
Task<bool> UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags);
|
||||
Task<bool> UpdateConversationMessage(string conversationId, UpdateMessageRequest request);
|
||||
Task<List<Conversation>> GetLastConversations();
|
||||
Task<List<string>> GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable<string> excludeAgentIds);
|
||||
|
|
@ -27,20 +27,17 @@ public interface IConversationService
|
|||
/// <param name="newMessageId">If not null, delete messages while input a new message; otherwise delete messages only</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> TruncateConversation(string conversationId, string messageId, string? newMessageId = null);
|
||||
Task<List<ContentLogOutputModel>> GetConversationContentLogs(string conversationId);
|
||||
Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId);
|
||||
|
||||
/// <summary>
|
||||
/// Send message to LLM
|
||||
/// </summary>
|
||||
/// <param name="agentId"></param>
|
||||
/// <param name="lastDalog"></param>
|
||||
/// <param name="lastDialog"></param>
|
||||
/// <param name="replyMessage"></param>
|
||||
/// <param name="onResponseReceived">Received the response from AI Agent</param>
|
||||
/// <param name="onFunctionExecuting">This delegate is useful when you want to report progress on UI</param>
|
||||
/// <param name="onFunctionExecuted">This delegate is useful when you want to report progress on UI</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> SendMessage(string agentId,
|
||||
RoleDialogModel lastDalog,
|
||||
RoleDialogModel lastDialog,
|
||||
PostbackMessageModel? replyMessage,
|
||||
Func<RoleDialogModel, Task> onResponseReceived);
|
||||
|
||||
|
|
@ -64,12 +61,7 @@ public interface IConversationService
|
|||
|
||||
void SaveStates();
|
||||
|
||||
/// <summary>
|
||||
/// Get conversation keys for searching
|
||||
/// </summary>
|
||||
/// <param name="query">search query</param>
|
||||
/// <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(ConversationStateKeysFilter filter);
|
||||
|
||||
Task<bool> MigrateLatestStates(int batchSize = 100, int errorLimit = 10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,17 +58,13 @@ 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, object? data = null)
|
||||
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null)
|
||||
{
|
||||
MetaData = meta;
|
||||
Content = content;
|
||||
|
|
@ -76,7 +72,6 @@ public class DialogElement
|
|||
SecondaryContent = secondaryContent;
|
||||
SecondaryRichContent = secondaryRichContent;
|
||||
Payload = payload;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ public class RoleDialogModel : ITrackableMessage
|
|||
[JsonPropertyName("generated_images")]
|
||||
public List<ImageGeneration> GeneratedImages { get; set; } = new List<ImageGeneration>();
|
||||
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
|
||||
public string RenderedInstruction { get; set; } = string.Empty;
|
||||
|
||||
private RoleDialogModel()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -47,6 +47,17 @@ public static class FileUtility
|
|||
return $"data:{contentType};base64,{base64}";
|
||||
}
|
||||
|
||||
public static BinaryData BuildBinaryDataFromFile(IFormFile file)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
file.CopyTo(stream);
|
||||
stream.Position = 0;
|
||||
var binary = BinaryData.FromStream(stream);
|
||||
stream.Close();
|
||||
|
||||
return binary;
|
||||
}
|
||||
|
||||
public static string GetFileContentType(string fileName)
|
||||
{
|
||||
string contentType;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,12 @@ public class StateConst
|
|||
public const string NEXT_ACTION_REASON = "next_action_reason";
|
||||
public const string USER_GOAL_AGENT = "user_goal_agent";
|
||||
public const string AGENT_REDIRECTION_REASON = "agent_redirection_reason";
|
||||
// lazy or eager
|
||||
public const string ROUTING_MODE = "routing_mode";
|
||||
public const string LAZY_ROUTING_AGENT_ID = "lazy_routing_agent_id";
|
||||
|
||||
public const string LANGUAGE = "language";
|
||||
|
||||
public const string SUB_CONVERSATION_ID = "sub_conversation_id";
|
||||
public const string ORIGIN_CONVERSATION_ID = "origin_conversation_id";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public interface IInstructHook
|
|||
string SelfId { get; }
|
||||
Task BeforeCompletion(Agent agent, RoleDialogModel message);
|
||||
Task AfterCompletion(Agent agent, InstructResult result);
|
||||
Task OnResponseGenerated(InstructResponseModel response);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,9 @@ public class InstructHookBase : IInstructHook
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public virtual async Task OnResponseGenerated(InstructResponseModel response)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
namespace BotSharp.Abstraction.Instructs.Models;
|
||||
|
||||
public class InstructResponseModel
|
||||
{
|
||||
public string? AgentId { get; set; }
|
||||
public string Provider { get; set; } = default!;
|
||||
public string Model { get; set; } = default!;
|
||||
public string? TemplateName { get; set; }
|
||||
public string UserMessage { get; set; } = default!;
|
||||
public string? SystemInstruction { get; set; }
|
||||
public string CompletionText { get; set; } = default!;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Instructs.Settings;
|
||||
|
||||
public class InstructionSettings
|
||||
{
|
||||
public bool EnableLog { get; set; }
|
||||
}
|
||||
|
|
@ -60,6 +60,14 @@ public interface IKnowledgeService
|
|||
Task<FileBinaryDataModel> GetKnowledgeDocumentBinaryData(string collectionName, Guid fileId);
|
||||
#endregion
|
||||
|
||||
#region Snapshot
|
||||
Task<IEnumerable<VectorCollectionSnapshot>> GetVectorCollectionSnapshots(string collectionName);
|
||||
Task<VectorCollectionSnapshot?> CreateVectorCollectionSnapshot(string collectionName);
|
||||
Task<BinaryData> DownloadVectorCollectionSnapshot(string collectionName, string snapshotFileName);
|
||||
Task<bool> RecoverVectorCollectionFromSnapshot(string collectionName, string snapshotFileName, BinaryData snapshotData);
|
||||
Task<bool> DeleteVectorCollectionSnapshot(string collectionName, string snapshotName);
|
||||
#endregion
|
||||
|
||||
#region Common
|
||||
Task<bool> RefreshVectorKnowledgeConfigs(VectorCollectionConfigsModel configs);
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Functions.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Loggers;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -37,4 +39,13 @@ public interface IContentGeneratingHook
|
|||
/// <param name="content"></param>
|
||||
/// <returns></returns>
|
||||
Task OnRenderingTemplate(Agent agent, string name, string content) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Realtime session updated
|
||||
/// </summary>
|
||||
/// <param name="agent"></param>
|
||||
/// <param name="instruction"></param>
|
||||
/// <param name="functions"></param>
|
||||
/// <returns></returns>
|
||||
Task OnSessionUpdated(Agent agent, string instruction, FunctionDef[] functions) => Task.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace BotSharp.Abstraction.Loggers.Models;
|
||||
|
||||
public class InstructionLogModel
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("agent_id")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
[JsonPropertyName("agent_name")]
|
||||
[JsonIgnore]
|
||||
public string? AgentName { get; set; }
|
||||
|
||||
[JsonPropertyName("provider")]
|
||||
public string Provider { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("template_name")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? TemplateName { get; set; }
|
||||
|
||||
[JsonPropertyName("user_message")]
|
||||
public string UserMessage { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("system_instruction")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? SystemInstruction { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_text")]
|
||||
public string CompletionText { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("user_id")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? UserName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, string> States { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("states")]
|
||||
public Dictionary<string, JsonDocument> InnerStates { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("created_time")]
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
|
||||
namespace BotSharp.Abstraction.Loggers.Services;
|
||||
|
||||
public interface ILoggerService
|
||||
{
|
||||
#region Conversation
|
||||
Task<DateTimePagination<ContentLogOutputModel>> GetConversationContentLogs(string conversationId, ConversationLogFilter filter);
|
||||
Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(string conversationId, ConversationLogFilter filter);
|
||||
#endregion
|
||||
|
||||
#region Instruction
|
||||
Task<PagedItems<InstructionLogModel>> GetInstructionLogs(InstructLogFilter filter);
|
||||
Task<List<string>> GetInstructionLogSearchKeys(InstructLogKeysFilter filter);
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ public interface IAudioCompletion
|
|||
{
|
||||
string Provider { get; }
|
||||
|
||||
string Model { get; }
|
||||
|
||||
Task<string> GenerateTextFromAudioAsync(Stream audio, string audioFileName, string? text = null);
|
||||
Task<BinaryData> GenerateAudioFromTextAsync(string text);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ public interface IChatCompletion
|
|||
/// </summary>
|
||||
string Provider { get; }
|
||||
|
||||
string Model { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set model name, one provider can consume different model or version(s)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public interface IImageCompletion
|
|||
/// </summary>
|
||||
string Provider { get; }
|
||||
|
||||
string Model { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set model name, one provider can consume different model or version(s)
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ public interface IRealTimeCompletion
|
|||
{
|
||||
string Provider { get; }
|
||||
string Model { get; }
|
||||
|
||||
void SetModelName(string model);
|
||||
|
||||
Task Connect(RealtimeHubConnection conn,
|
||||
Action onModelReady,
|
||||
Action<string> onModelAudioDeltaReceived,
|
||||
Action<string, string> onModelAudioDeltaReceived,
|
||||
Action onModelAudioResponseDone,
|
||||
Action<string> onAudioTranscriptDone,
|
||||
Action<List<RoleDialogModel>> onModelResponseDone,
|
||||
|
|
@ -24,9 +23,11 @@ public interface IRealTimeCompletion
|
|||
Task Disconnect();
|
||||
|
||||
Task<RealtimeSession> CreateSession(Agent agent, List<RoleDialogModel> conversations);
|
||||
Task UpdateInitialSession(RealtimeHubConnection conn);
|
||||
Task<string> UpdateSession(RealtimeHubConnection conn, bool interruptResponse = true);
|
||||
Task InsertConversationItem(RoleDialogModel message);
|
||||
Task RemoveConversationItem(string itemId);
|
||||
Task TriggerModelInference(string? instructions = null);
|
||||
Task CancelModelResponse();
|
||||
Task<List<RoleDialogModel>> OnResponsedDone(RealtimeHubConnection conn, string response);
|
||||
Task<RoleDialogModel> OnConversationItemCreated(RealtimeHubConnection conn, string response);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ public interface ITextCompletion
|
|||
/// The LLM provider like Microsoft Azure, OpenAI, ClaudAI
|
||||
/// </summary>
|
||||
string Provider { get; }
|
||||
string Model { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set model name, one provider can consume different model or version(s)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ public interface ITextEmbedding
|
|||
/// The Embedding provider like Microsoft Azure, OpenAI, ClaudAI
|
||||
/// </summary>
|
||||
string Provider { get; }
|
||||
string Model { get; }
|
||||
|
||||
void SetModelName(string model);
|
||||
|
||||
|
||||
Task<float[]> GetVectorAsync(string text);
|
||||
Task<List<float[]>> GetVectorsAsync(List<string> texts);
|
||||
void SetModelName(string model);
|
||||
|
||||
void SetDimension(int dimension);
|
||||
int GetDimension();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,24 @@ namespace BotSharp.Abstraction.Models;
|
|||
public class KeyValue
|
||||
{
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; }
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
public string? Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Key: {Key}, Value: {Value}";
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyValue<T>
|
||||
{
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public T? Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Options;
|
|||
|
||||
public class BotSharpOptions
|
||||
{
|
||||
private readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions()
|
||||
public readonly static JsonSerializerOptions defaultJsonOptions = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ public class PluginMenuDef(string label, string? link = null, string? icon = nul
|
|||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Link { get; set; } = link;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EmbeddingData? EmbeddingInfo { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? IsHeader { get; set; }
|
||||
|
||||
|
|
@ -30,3 +33,35 @@ public class PluginMenuDef(string label, string? link = null, string? icon = nul
|
|||
return $"{Label} {Link} {Weight}";
|
||||
}
|
||||
}
|
||||
|
||||
public class EmbeddingData
|
||||
{
|
||||
/// <summary>
|
||||
/// Embedding source, e.g., tableau
|
||||
/// </summary>
|
||||
public string Source { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Embedding url
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Html tag
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? HtmlTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Javascript script src
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ScriptSrc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Javascript script type, e.g., module
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ScriptType { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Realtime;
|
||||
|
||||
public interface IRealtimeHook
|
||||
{
|
||||
string[] OnModelTranscriptPrompt(Agent agent);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Realtime.Models;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
|
|
@ -8,5 +9,11 @@ namespace BotSharp.Abstraction.Realtime;
|
|||
/// </summary>
|
||||
public interface IRealtimeHub
|
||||
{
|
||||
Task Listen(WebSocket userWebSocket, Func<string, RealtimeHubConnection> onUserMessageReceived);
|
||||
RealtimeHubConnection HubConn { get; }
|
||||
RealtimeHubConnection SetHubConnection(string conversationId);
|
||||
|
||||
IRealTimeCompletion Completer { get; }
|
||||
IRealTimeCompletion SetCompleter(string provider);
|
||||
|
||||
Task Listen(WebSocket userWebSocket, Action<string> onUserMessageReceived);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Abstraction.Realtime.Models;
|
||||
|
||||
public class ModelTurnDetection
|
||||
{
|
||||
public int PrefixPadding { get; set; } = 300;
|
||||
|
||||
public int SilenceDuration { get; set; } = 800;
|
||||
|
||||
public float Threshold { get; set; } = 0.8f;
|
||||
}
|
||||
|
|
@ -1,14 +1,34 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BotSharp.Abstraction.Realtime.Models;
|
||||
|
||||
public class RealtimeHubConnection
|
||||
{
|
||||
public string Event { get; set; } = null!;
|
||||
public string StreamId { get; set; } = null!;
|
||||
public string EntryAgentId { get; set; } = null!;
|
||||
public string? LastAssistantItemId { get; set; } = null!;
|
||||
public long LatestMediaTimestamp { get; set; }
|
||||
public long? ResponseStartTimestamp { get; set; }
|
||||
public string KeypadInputBuffer { get; set; } = string.Empty;
|
||||
public ConcurrentQueue<string> MarkQueue { get; set; } = new();
|
||||
public string CurrentAgentId { get; set; } = null!;
|
||||
public string ConversationId { get; set; } = null!;
|
||||
public string Data { get; set; } = string.Empty;
|
||||
public string Model { get; set; } = null!;
|
||||
public Func<string, object> OnModelMessageReceived { get; set; } = null!;
|
||||
public Func<object> OnModelAudioResponseDone { get; set; } = null!;
|
||||
public Func<object> OnModelUserInterrupted { get; set; } = null!;
|
||||
|
||||
public void ResetResponseState()
|
||||
{
|
||||
MarkQueue.Clear();
|
||||
LastAssistantItemId = null;
|
||||
ResponseStartTimestamp = null;
|
||||
}
|
||||
|
||||
public void ResetStreamState()
|
||||
{
|
||||
ResponseStartTimestamp = null;
|
||||
LatestMediaTimestamp = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
namespace BotSharp.Abstraction.Realtime.Models;
|
||||
|
||||
public class RealtimeModelSettings
|
||||
{
|
||||
public float Temperature { get; set; } = 0.8f;
|
||||
public int MaxResponseOutputTokens { get; set; } = 512;
|
||||
public ModelTurnDetection TurnDetection { get; set; } = new();
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@ public class ConversationFilter
|
|||
/// <summary>
|
||||
/// Check whether each key in the list is in the conversation states and its value equals to target value if not empty
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValue>? States { get; set; } = [];
|
||||
public List<KeyValue>? States { get; set; }
|
||||
|
||||
public IEnumerable<string>? Tags { get; set; } = [];
|
||||
public List<string>? Tags { get; set; }
|
||||
|
||||
public static ConversationFilter Empty()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
namespace BotSharp.Abstraction.Repositories.Filters;
|
||||
|
||||
public class ConversationLogFilter
|
||||
{
|
||||
public int Size { get; set; } = 20;
|
||||
public DateTime StartTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ConversationLogFilter()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static ConversationLogFilter Empty()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
namespace BotSharp.Abstraction.Repositories.Filters;
|
||||
|
||||
public class ConversationStateKeysFilter
|
||||
{
|
||||
public string? Query { get; set; }
|
||||
public int KeyLimit { get; set; } = 10;
|
||||
public int ConvLimit { get; set; } = 100;
|
||||
public bool PreLoad { get; set; }
|
||||
public List<string>? AgentIds { get; set; }
|
||||
public List<string>? UserIds { get; set; }
|
||||
|
||||
public ConversationStateKeysFilter()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static ConversationStateKeysFilter Empty()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
namespace BotSharp.Abstraction.Repositories.Filters;
|
||||
|
||||
public class InstructLogFilter : Pagination
|
||||
{
|
||||
public List<string>? AgentIds { get; set; }
|
||||
public List<string>? Providers { get; set; }
|
||||
public List<string>? Models { get; set; }
|
||||
public List<string>? TemplateNames { get; set; }
|
||||
public List<string>? UserIds { get; set; }
|
||||
public List<KeyValue>? States { get; set; }
|
||||
|
||||
public static InstructLogFilter Empty()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
namespace BotSharp.Abstraction.Repositories.Filters;
|
||||
|
||||
public class InstructLogKeysFilter
|
||||
{
|
||||
public string? Query { get; set; }
|
||||
public int KeyLimit { get; set; } = 10;
|
||||
public int LogLimit { get; set; } = 100;
|
||||
public bool PreLoad { get; set; }
|
||||
public List<string>? AgentIds { get; set; }
|
||||
public List<string>? UserIds { get; set; }
|
||||
|
||||
public InstructLogKeysFilter()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static InstructLogKeysFilter Empty()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
|
|||
=> throw new NotImplementedException();
|
||||
void UpdateConversationTitleAlias(string conversationId, string titleAlias)
|
||||
=> throw new NotImplementedException();
|
||||
bool UpdateConversationTags(string conversationId, List<string> tags)
|
||||
bool UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
=> throw new NotImplementedException();
|
||||
bool AppendConversationTags(string conversationId, List<string> tags)
|
||||
=> throw new NotImplementedException();
|
||||
|
|
@ -148,7 +148,11 @@ 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(ConversationStateKeysFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
List<string> GetConversationsToMigrate(int batchSize = 100)
|
||||
=> throw new NotImplementedException();
|
||||
bool MigrateConvsersationLatestStates(string conversationId)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
|
|
@ -160,17 +164,28 @@ public interface IBotSharpRepository : IHaveServiceProvider
|
|||
#region Conversation Content Log
|
||||
void SaveConversationContentLog(ContentLogOutputModel log)
|
||||
=> throw new NotImplementedException();
|
||||
List<ContentLogOutputModel> GetConversationContentLogs(string conversationId)
|
||||
DateTimePagination<ContentLogOutputModel> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
#region Conversation State Log
|
||||
void SaveConversationStateLog(ConversationStateLogModel log)
|
||||
=> throw new NotImplementedException();
|
||||
List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
|
||||
DateTimePagination<ConversationStateLogModel> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
#region Instruction Log
|
||||
bool SaveInstructionLogs(IEnumerable<InstructionLogModel> logs)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
PagedItems<InstructionLogModel> GetInstructionLogs(InstructLogFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
List<string> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
#endregion
|
||||
|
||||
#region Statistics
|
||||
BotSharpStats? GetGlobalStats(string metric, string dimension, string dimRefVal, DateTime recordTime, StatsInterval interval)
|
||||
=> throw new NotImplementedException();
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ public interface IRoutingContext
|
|||
bool IsEmpty { get; }
|
||||
string IntentName { get; set; }
|
||||
int AgentCount { get; }
|
||||
void Push(string agentId, string? reason = null);
|
||||
void Pop(string? reason = null);
|
||||
void PopTo(string agentId, string reason);
|
||||
void Replace(string agentId, string? reason = null);
|
||||
void Push(string agentId, string? reason = null, bool updateLazyRouting = true);
|
||||
void Pop(string? reason = null, bool updateLazyRouting = true);
|
||||
void PopTo(string agentId, string reason, bool updateLazyRouting = true);
|
||||
void Replace(string agentId, string? reason = null, bool updateLazyRouting = true);
|
||||
void Empty(string? reason = null);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Abstraction.Routing.Models;
|
||||
|
||||
public class FallbackArgs
|
||||
{
|
||||
[JsonPropertyName("fallback_reason")]
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("user_question")]
|
||||
public string Question { get; set; } = null;
|
||||
}
|
||||
|
|
@ -12,10 +12,6 @@ public class RoutingArgs
|
|||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? NextActionReason { get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
[JsonPropertyName("conversation_end")]
|
||||
public bool ConversationEnd { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ namespace BotSharp.Abstraction.Tasks.Models;
|
|||
|
||||
public class AgentTask
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
|
||||
|
|
@ -19,9 +22,15 @@ public class AgentTask
|
|||
/// </summary>
|
||||
public string Status { get; set; } = TaskStatus.New;
|
||||
|
||||
public DateTime? LastExecutedDateTime { get; set; }
|
||||
public DateTime? NextExecutionDateTime { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public DateTime? LastExecutionTime { get; set; }
|
||||
|
||||
public DateTime CreatedDateTime { get; set; }
|
||||
public DateTime UpdatedDateTime { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public DateTime? NextExecutionTime { get; set; }
|
||||
|
||||
[JsonPropertyName("created_time")]
|
||||
public DateTime CreatedTime { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_time")]
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public interface IUserService
|
|||
Task<PagedItems<User>> GetUsers(UserFilter filter);
|
||||
Task<List<User>> SearchLoginUsers(User filter);
|
||||
Task<User?> GetUserDetails(string userId, bool includeAgent = false);
|
||||
Task<bool> IsAdminUser(string userId);
|
||||
Task<(bool, User?)> IsAdminUser(string userId);
|
||||
Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null);
|
||||
Task<bool> UpdateUser(User user, bool isUpdateUserAgents = false);
|
||||
Task<User> CreateUser(User user);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Utilities;
|
||||
|
||||
public class DateTimePagination<T> : PagedItems<T>
|
||||
{
|
||||
public DateTime? NextTime { get; set; }
|
||||
}
|
||||
|
|
@ -92,4 +92,20 @@ public static class StringExtensions
|
|||
|
||||
return JsonSerializer.Deserialize<T[]>(text, options);
|
||||
}
|
||||
|
||||
public static bool IsPrimitiveValue(this string value)
|
||||
{
|
||||
return int.TryParse(value, out _) ||
|
||||
long.TryParse(value, out _) ||
|
||||
double.TryParse(value, out _) ||
|
||||
float.TryParse(value, out _) ||
|
||||
bool.TryParse(value, out _) ||
|
||||
char.TryParse(value, out _) ||
|
||||
byte.TryParse(value, out _) ||
|
||||
sbyte.TryParse(value, out _) ||
|
||||
short.TryParse(value, out _) ||
|
||||
ushort.TryParse(value, out _) ||
|
||||
uint.TryParse(value, out _) ||
|
||||
ulong.TryParse(value, out _);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,36 @@ public interface IVectorDb
|
|||
{
|
||||
string Provider { get; }
|
||||
|
||||
Task<bool> DoesCollectionExist(string collectionName);
|
||||
Task<IEnumerable<string>> GetCollections();
|
||||
Task<StringIdPagedItems<VectorCollectionData>> GetPagedCollectionData(string collectionName, VectorFilter filter);
|
||||
Task<IEnumerable<VectorCollectionData>> GetCollectionData(string collectionName, IEnumerable<Guid> ids, bool withPayload = false, bool withVector = false);
|
||||
Task<bool> CreateCollection(string collectionName, int dimension);
|
||||
Task<bool> DeleteCollection(string collectionName);
|
||||
Task<bool> Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary<string, object>? payload = null);
|
||||
Task<IEnumerable<VectorCollectionData>> Search(string collectionName, float[] vector, IEnumerable<string>? fields, int limit = 5, float confidence = 0.5f, bool withVector = false);
|
||||
Task<bool> DeleteCollectionData(string collectionName, List<Guid> ids);
|
||||
Task<bool> DeleteCollectionAllData(string collectionName);
|
||||
Task<bool> DoesCollectionExist(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<string>> GetCollections()
|
||||
=> throw new NotImplementedException();
|
||||
Task<StringIdPagedItems<VectorCollectionData>> GetPagedCollectionData(string collectionName, VectorFilter filter)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionData>> GetCollectionData(string collectionName, IEnumerable<Guid> ids,
|
||||
bool withPayload = false, bool withVector = false)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> CreateCollection(string collectionName, int dimension)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollection(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> Upsert(string collectionName, Guid id, float[] vector, string text, Dictionary<string, object>? payload = null)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionData>> Search(string collectionName, float[] vector, IEnumerable<string>? fields,
|
||||
int limit = 5, float confidence = 0.5f, bool withVector = false)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionData(string collectionName, List<Guid> ids)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionAllData(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<IEnumerable<VectorCollectionSnapshot>> GetCollectionSnapshots(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<VectorCollectionSnapshot?> CreateCollectionShapshot(string collectionName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<BinaryData> DownloadCollectionSnapshot(string collectionName, string snapshotFileName)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> RecoverCollectionFromShapshot(string collectionName, string snapshotFileName, BinaryData snapshotData)
|
||||
=> throw new NotImplementedException();
|
||||
Task<bool> DeleteCollectionShapshot(string collectionName, string snapshotName)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
namespace BotSharp.Abstraction.VectorStorage.Models;
|
||||
|
||||
public class VectorCollectionSnapshot
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
public long Size { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public string? CheckSum { get; set; }
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NCrontab" Version="3.3.3" />
|
||||
<PackageReference Include="NCrontab" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public class CrontabService : ICrontabService, ITaskFeeder
|
|||
Status = TaskStatus.Scheduled,
|
||||
Enabled = !x.Disabled,
|
||||
Description = cron.Description,
|
||||
LastExecutedDateTime = cron.LastExecutionTime
|
||||
LastExecutionTime = cron.LastExecutionTime
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
<LangVersion>$(LangVersion)</LangVersion>
|
||||
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
|
||||
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
|
||||
<OutputPath>$(SolutionDir)packages</OutputPath>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
|
||||
<ProjectReference Include="..\BotSharp.Core\BotSharp.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using BotSharp.Abstraction.Utilities;
|
||||
|
||||
namespace BotSharp.Core.Realtime.Hooks;
|
||||
|
||||
public class RealtimeConversationHook : ConversationHookBase, IConversationHook
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
public RealtimeConversationHook(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public async Task OnFunctionExecuting(RoleDialogModel message)
|
||||
{
|
||||
var hub = _services.GetRequiredService<IRealtimeHub>();
|
||||
if (hub.HubConn == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Save states
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
states.SaveStateByArgs(message.FunctionArgs?.JsonContent<JsonDocument>() ?? JsonDocument.Parse("{}"));
|
||||
}
|
||||
|
||||
public async Task OnFunctionExecuted(RoleDialogModel message)
|
||||
{
|
||||
var hub = _services.GetRequiredService<IRealtimeHub>();
|
||||
if (hub.HubConn == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
|
||||
message.Role = AgentRole.Function;
|
||||
|
||||
if (message.FunctionName == "route_to_agent")
|
||||
{
|
||||
var inst = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs ?? "{}") ?? new();
|
||||
message.Content = $"I'm your AI assistant '{inst.AgentName}' to help with: '{inst.NextActionReason}'";
|
||||
hub.HubConn.CurrentAgentId = routing.Context.GetCurrentAgentId();
|
||||
|
||||
var instruction = await hub.Completer.UpdateSession(hub.HubConn);
|
||||
await hub.Completer.InsertConversationItem(message);
|
||||
await hub.Completer.TriggerModelInference($"{instruction}\r\n\r\nAssist user task: {inst.NextActionReason}");
|
||||
}
|
||||
else if (message.FunctionName == "util-routing-fallback_to_router")
|
||||
{
|
||||
var inst = JsonSerializer.Deserialize<FallbackArgs>(message.FunctionArgs ?? "{}") ?? new();
|
||||
message.Content = $"Returned to Router due to {inst.Reason}";
|
||||
hub.HubConn.CurrentAgentId = routing.Context.GetCurrentAgentId();
|
||||
|
||||
var instruction = await hub.Completer.UpdateSession(hub.HubConn);
|
||||
await hub.Completer.InsertConversationItem(message);
|
||||
await hub.Completer.TriggerModelInference(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update session for changed states
|
||||
var instruction = await hub.Completer.UpdateSession(hub.HubConn);
|
||||
await hub.Completer.InsertConversationItem(message);
|
||||
await hub.Completer.TriggerModelInference(instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Infrastructure/BotSharp.Core.Realtime/RealtimePlugin.cs
Normal file
27
src/Infrastructure/BotSharp.Core.Realtime/RealtimePlugin.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using BotSharp.Abstraction.Plugins;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Core.Realtime.Hooks;
|
||||
using BotSharp.Core.Realtime.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BotSharp.Core.Realtime;
|
||||
|
||||
public class RealtimePlugin : IBotSharpPlugin
|
||||
{
|
||||
public string Id => "68c1c737-5c21-49de-b141-cd5c8d9bf978";
|
||||
public string Name => "Realtime Hub";
|
||||
public string? IconUrl => "https://thumbs.dreamstime.com/b/microphone-icon-sound-waves-voice-command-recording-message-sign-349007898.jpg";
|
||||
public string Description => "Build low-latency, multi-modal experiences with the Realtime API.";
|
||||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped(provider =>
|
||||
{
|
||||
var settingService = provider.GetRequiredService<ISettingService>();
|
||||
return settingService.Bind<RealtimeModelSettings>("RealtimeModel");
|
||||
});
|
||||
|
||||
services.AddScoped<IRealtimeHub, RealtimeHub>();
|
||||
services.AddScoped<IConversationHook, RealtimeConversationHook>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
namespace BotSharp.Core.Realtime.Services;
|
||||
|
||||
public class RealtimeHub : IRealtimeHub
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private RealtimeHubConnection _conn;
|
||||
public RealtimeHubConnection HubConn => _conn;
|
||||
|
||||
private IRealTimeCompletion _completer;
|
||||
public IRealTimeCompletion Completer => _completer;
|
||||
|
||||
public RealtimeHub(IServiceProvider services, ILogger<RealtimeHub> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Listen(WebSocket userWebSocket,
|
||||
Action<string> onUserMessageReceived)
|
||||
{
|
||||
var buffer = new byte[1024 * 16];
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
result = await userWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
|
||||
if (string.IsNullOrEmpty(receivedText))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
onUserMessageReceived(receivedText);
|
||||
|
||||
if (_conn.Event == "user_connected")
|
||||
{
|
||||
await ConnectToModel(userWebSocket);
|
||||
}
|
||||
else if (_conn.Event == "user_data_received")
|
||||
{
|
||||
await _completer.AppenAudioBuffer(_conn.Data);
|
||||
}
|
||||
else if (_conn.Event == "user_dtmf_received")
|
||||
{
|
||||
await HandleUserDtmfReceived();
|
||||
}
|
||||
else if (_conn.Event == "user_disconnected")
|
||||
{
|
||||
await _completer.Disconnect();
|
||||
await HandleUserDisconnected();
|
||||
}
|
||||
} while (!result.CloseStatus.HasValue);
|
||||
|
||||
await userWebSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task ConnectToModel(WebSocket userWebSocket)
|
||||
{
|
||||
var hookProvider = _services.GetRequiredService<ConversationHookProvider>();
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
convService.SetConversationId(_conn.ConversationId, []);
|
||||
var conversation = await convService.GetConversation(_conn.ConversationId);
|
||||
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agent = await agentService.LoadAgent(conversation.AgentId);
|
||||
_conn.CurrentAgentId = agent.Id;
|
||||
|
||||
// Set model
|
||||
var model = agent.LlmConfig.Model;
|
||||
if (!model.Contains("-realtime-"))
|
||||
{
|
||||
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
|
||||
model = llmProviderService.GetProviderModel("openai", "gpt-4", realTime: true).Name;
|
||||
}
|
||||
|
||||
_completer.SetModelName(model);
|
||||
_conn.Model = model;
|
||||
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.Push(agent.Id);
|
||||
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
var dialogs = convService.GetDialogHistory();
|
||||
if (dialogs.Count == 0)
|
||||
{
|
||||
dialogs.Add(new RoleDialogModel(AgentRole.User, "Hi"));
|
||||
storage.Append(_conn.ConversationId, dialogs.First());
|
||||
}
|
||||
|
||||
routing.Context.SetDialogs(dialogs);
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
await _completer.Connect(_conn,
|
||||
onModelReady: async () =>
|
||||
{
|
||||
// Not TriggerModelInference, waiting for user utter.
|
||||
var instruction = await _completer.UpdateSession(_conn);
|
||||
|
||||
// Trigger model inference if there is no audio file in the conversation
|
||||
if (!states.ContainsState("init_audio_file"))
|
||||
{
|
||||
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
|
||||
{
|
||||
await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _completer.TriggerModelInference("Reply based on the conversation context.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Push dialogs into model context
|
||||
foreach (var message in dialogs)
|
||||
{
|
||||
await _completer.InsertConversationItem(message);
|
||||
}
|
||||
|
||||
await _completer.TriggerModelInference($"{instruction}\r\n\r\nAssist user without repeating your previous statement.");
|
||||
}
|
||||
},
|
||||
onModelAudioDeltaReceived: async (audioDeltaData, itemId) =>
|
||||
{
|
||||
var data = _conn.OnModelMessageReceived(audioDeltaData);
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
|
||||
// If this is the first delta of a new response, set the start timestamp
|
||||
if (!_conn.ResponseStartTimestamp.HasValue)
|
||||
{
|
||||
_conn.ResponseStartTimestamp = _conn.LatestMediaTimestamp;
|
||||
_logger.LogDebug($"Setting start timestamp for new response: {_conn.ResponseStartTimestamp}ms");
|
||||
}
|
||||
// Record last assistant item ID for interruption handling
|
||||
if (!string.IsNullOrEmpty(itemId))
|
||||
{
|
||||
_conn.LastAssistantItemId = itemId;
|
||||
}
|
||||
|
||||
// Send mark messages to Media Streams so we know if and when AI response playback is finished
|
||||
await SendMark(userWebSocket, _conn);
|
||||
},
|
||||
onModelAudioResponseDone: async () =>
|
||||
{
|
||||
var data = _conn.OnModelAudioResponseDone();
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
},
|
||||
onAudioTranscriptDone: async transcript =>
|
||||
{
|
||||
|
||||
},
|
||||
onModelResponseDone: async messages =>
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// Invoke function
|
||||
if (message.MessageType == MessageTypeName.FunctionCall &&
|
||||
!string.IsNullOrEmpty(message.FunctionName))
|
||||
{
|
||||
await routing.InvokeFunction(message.FunctionName, message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// append output audio transcript to conversation
|
||||
dialogs.Add(message);
|
||||
storage.Append(_conn.ConversationId, message);
|
||||
|
||||
foreach (var hook in hookProvider.HooksOrderByPriority)
|
||||
{
|
||||
hook.SetAgent(agent)
|
||||
.SetConversation(conversation);
|
||||
|
||||
await hook.OnResponseGenerated(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onConversationItemCreated: async response =>
|
||||
{
|
||||
|
||||
},
|
||||
onInputAudioTranscriptionCompleted: async message =>
|
||||
{
|
||||
// append input audio transcript to conversation
|
||||
dialogs.Add(message);
|
||||
storage.Append(_conn.ConversationId, message);
|
||||
|
||||
foreach (var hook in hookProvider.HooksOrderByPriority)
|
||||
{
|
||||
hook.SetAgent(agent)
|
||||
.SetConversation(conversation);
|
||||
|
||||
await hook.OnMessageReceived(message);
|
||||
}
|
||||
},
|
||||
onUserInterrupted: async () =>
|
||||
{
|
||||
// Reset states
|
||||
_conn.ResetResponseState();
|
||||
|
||||
var data = _conn.OnModelUserInterrupted();
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendMark(WebSocket userWebSocket, RealtimeHubConnection conn)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(conn.StreamId))
|
||||
{
|
||||
var markEvent = new
|
||||
{
|
||||
@event = "mark",
|
||||
streamSid = conn.StreamId,
|
||||
mark = new { name = "responsePart" }
|
||||
};
|
||||
await SendEventToUser(userWebSocket, markEvent);
|
||||
conn.MarkQueue.Enqueue("responsePart");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUserDtmfReceived()
|
||||
{
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
var hookProvider = _services.GetRequiredService<ConversationHookProvider>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agent = await agentService.LoadAgent(_conn.CurrentAgentId);
|
||||
var dialogs = routing.Context.GetDialogs();
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var conversation = await convService.GetConversation(_conn.ConversationId);
|
||||
|
||||
var message = new RoleDialogModel(AgentRole.User, _conn.Data)
|
||||
{
|
||||
CurrentAgentId = routing.Context.GetCurrentAgentId()
|
||||
};
|
||||
dialogs.Add(message);
|
||||
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
storage.Append(_conn.ConversationId, message);
|
||||
|
||||
foreach (var hook in hookProvider.HooksOrderByPriority)
|
||||
{
|
||||
hook.SetAgent(agent)
|
||||
.SetConversation(conversation);
|
||||
|
||||
await hook.OnMessageReceived(message);
|
||||
}
|
||||
|
||||
await _completer.InsertConversationItem(message);
|
||||
var instruction = await _completer.UpdateSession(_conn);
|
||||
await _completer.TriggerModelInference($"{instruction}\r\n\r\nReply based on the user input: {message.Content}");
|
||||
}
|
||||
|
||||
private async Task HandleUserDisconnected()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private async Task SendEventToUser(WebSocket webSocket, object message)
|
||||
{
|
||||
var data = JsonSerializer.Serialize(message);
|
||||
var buffer = Encoding.UTF8.GetBytes(data);
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public RealtimeHubConnection SetHubConnection(string conversationId)
|
||||
{
|
||||
_conn = new RealtimeHubConnection
|
||||
{
|
||||
ConversationId = conversationId
|
||||
};
|
||||
|
||||
return _conn;
|
||||
}
|
||||
|
||||
public IRealTimeCompletion SetCompleter(string provider)
|
||||
{
|
||||
_completer = _services.GetServices<IRealTimeCompletion>().First(x => x.Provider == provider);
|
||||
return _completer;
|
||||
}
|
||||
}
|
||||
17
src/Infrastructure/BotSharp.Core.Realtime/Using.cs
Normal file
17
src/Infrastructure/BotSharp.Core.Realtime/Using.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
global using Microsoft.Extensions.Logging;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
global using System.Net.WebSockets;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
|
||||
global using BotSharp.Abstraction.Realtime;
|
||||
global using BotSharp.Abstraction.Realtime.Models;
|
||||
global using BotSharp.Abstraction.MLTasks;
|
||||
global using BotSharp.Abstraction.Conversations.Enums;
|
||||
global using BotSharp.Abstraction.Routing.Models;
|
||||
global using BotSharp.Abstraction.Conversations;
|
||||
global using BotSharp.Abstraction.Agents;
|
||||
global using BotSharp.Abstraction.Routing;
|
||||
global using BotSharp.Abstraction.Agents.Enums;
|
||||
global using BotSharp.Abstraction.Conversations.Models;
|
||||
|
|
@ -185,8 +185,8 @@ public partial class AgentService
|
|||
Enabled = parsedTask.Enabled,
|
||||
Content = parsedTask.Content,
|
||||
AgentId = agentId,
|
||||
CreatedDateTime = parsedTask.CreatedDateTime,
|
||||
UpdatedDateTime = parsedTask.UpdatedDateTime
|
||||
CreatedTime = parsedTask.CreatedTime,
|
||||
UpdatedTime = parsedTask.UpdatedTime
|
||||
};
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ public partial class AgentService
|
|||
#if !DEBUG
|
||||
[SharpCache(10, perInstanceCache: true)]
|
||||
#endif
|
||||
public async Task<List<IdName>> GetAgentOptions()
|
||||
public async Task<List<IdName>> GetAgentOptions(List<string>? agentIds)
|
||||
{
|
||||
var agents = _db.GetAgents(AgentFilter.Empty());
|
||||
var agents = _db.GetAgents(new AgentFilter
|
||||
{
|
||||
AgentIds = !agentIds.IsNullOrEmpty() ? agentIds : null
|
||||
});
|
||||
return agents?.Select(x => new IdName(x.Id, x.Name))?.OrderBy(x => x.Name)?.ToList() ?? [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public partial class AgentService
|
|||
|
||||
var userIdentity = _services.GetRequiredService<IUserIdentity>();
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var isValid = await userService.IsAdminUser(userIdentity.Id);
|
||||
var (isValid, _) = await userService.IsAdminUser(userIdentity.Id);
|
||||
if (!isValid)
|
||||
{
|
||||
return "Unauthorized user.";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
|
||||
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
|
||||
<OutputPath>$(SolutionDir)packages</OutputPath>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
@ -66,6 +65,8 @@
|
|||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.get_remaining_task.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\agent.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-redirect_to_agent.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json" />
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\select_file_prompt.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.executor.liquid" />
|
||||
|
|
@ -146,6 +148,15 @@
|
|||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\functions\route_to_agent.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ using BotSharp.Abstraction.Templating;
|
|||
using BotSharp.Core.Templating;
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Realtime;
|
||||
using BotSharp.Core.Realtime;
|
||||
|
||||
namespace BotSharp.Core;
|
||||
|
||||
|
|
@ -173,7 +172,5 @@ public static class BotSharpCoreExtensions
|
|||
});
|
||||
|
||||
services.AddSingleton(loader);
|
||||
|
||||
services.AddScoped<IRealtimeHub, RealtimeHub>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
using BotSharp.Abstraction.Repositories;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public partial class ConversationService
|
||||
{
|
||||
public async Task<List<ContentLogOutputModel>> GetConversationContentLogs(string conversationId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationContentLogs(conversationId);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<ConversationStateLogModel>> GetConversationStateLogs(string conversationId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationStateLogs(conversationId);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
using NetTopologySuite.Algorithm;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public partial class ConversationService
|
||||
{
|
||||
public async Task<bool> MigrateLatestStates(int batchSize = 100, int errorLimit = 10)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var isSuccess = true;
|
||||
var errorCount = 0;
|
||||
var batchNum = 0;
|
||||
var info = string.Empty;
|
||||
var error = string.Empty;
|
||||
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n#Start migrating Conversation Latest States...\r\n");
|
||||
#else
|
||||
_logger.LogInformation($"#Start migrating Conversation Latest States...");
|
||||
#endif
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var convIds = db.GetConversationsToMigrate(batchSize);
|
||||
|
||||
while (!convIds.IsNullOrEmpty())
|
||||
{
|
||||
batchNum++;
|
||||
var innerSw = Stopwatch.StartNew();
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n#Start migrating Conversation Latest States (batch number: {batchNum})\r\n");
|
||||
#else
|
||||
_logger.LogInformation($"#Start migrating Conversation Latest States (batch number: {batchNum})");
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < convIds.Count; i++)
|
||||
{
|
||||
var convId = convIds.ElementAt(i);
|
||||
try
|
||||
{
|
||||
var done = db.MigrateConvsersationLatestStates(convId);
|
||||
info = $"Conversation {convId} latest states have been migrated ({i + 1}/{convIds.Count})!";
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n{info}\r\n");
|
||||
#else
|
||||
_logger.LogInformation($"{info}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorCount++;
|
||||
error = $"Conversation {convId} latest states fail to be migrated! ({i + 1}/{convIds.Count})\r\n{ex.Message}\r\n{ex.InnerException}";
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n{error}\r\n");
|
||||
#else
|
||||
_logger.LogError($"{error}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount >= errorLimit)
|
||||
{
|
||||
error = $"\r\nErrors exceed limit => stop the migration!\r\n";
|
||||
#if DEBUG
|
||||
Console.WriteLine($"{error}");
|
||||
#else
|
||||
_logger.LogError($"{error}");
|
||||
#endif
|
||||
innerSw.Stop();
|
||||
isSuccess = false;
|
||||
break;
|
||||
}
|
||||
|
||||
innerSw.Stop();
|
||||
info = $"#Done migrating Conversation Latest States (batch number: {batchNum}) " +
|
||||
$"(Total time: {innerSw.Elapsed.Hours} hrs, {innerSw.Elapsed.Minutes} mins, {innerSw.Elapsed.Seconds} seconds)";
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n{info}\r\n");
|
||||
#else
|
||||
_logger.LogInformation($"{info}");
|
||||
#endif
|
||||
|
||||
await Task.Delay(100);
|
||||
convIds = db.GetConversationsToMigrate(batchSize);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
info = $"#Done with migrating Conversation Latest States! " +
|
||||
$"(Total time: {sw.Elapsed.Days} days, {sw.Elapsed.Hours} hrs, {sw.Elapsed.Minutes} mins, {sw.Elapsed.Seconds} seconds)";
|
||||
#if DEBUG
|
||||
Console.WriteLine($"\r\n{info}\r\n");
|
||||
#else
|
||||
_logger.LogInformation($"{info}");
|
||||
#endif
|
||||
|
||||
return isSuccess;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
|
@ -36,7 +37,17 @@ public partial class ConversationService
|
|||
// Enqueue receiving agent first in case it stop completion by OnMessageReceived
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(_conversationId, message.MessageId);
|
||||
routing.Context.Push(agent.Id, reason: "request started");
|
||||
|
||||
// Check the routing mode
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
|
||||
routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false);
|
||||
|
||||
if (routingMode == "lazy")
|
||||
{
|
||||
message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId);
|
||||
routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false);
|
||||
}
|
||||
|
||||
// Save payload in order to assign the payload before hook is invoked
|
||||
if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload))
|
||||
|
|
@ -77,7 +88,7 @@ public partial class ConversationService
|
|||
{
|
||||
agent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
}
|
||||
|
||||
|
||||
if (agent.Type == AgentType.Routing)
|
||||
{
|
||||
response = await routing.InstructLoop(message, dialogs);
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ public partial class ConversationService : IConversationService
|
|||
return conversation;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateConversationTags(string conversationId, List<string> tags)
|
||||
public async Task<bool> UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
return db.UpdateConversationTags(conversationId, tags);
|
||||
return db.UpdateConversationTags(conversationId, toAddTags, toDeleteTags);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateConversationMessage(string conversationId, UpdateMessageRequest request)
|
||||
|
|
@ -222,17 +222,26 @@ 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(ConversationStateKeysFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = ConversationStateKeysFilter.Empty();
|
||||
}
|
||||
|
||||
var keys = new List<string>();
|
||||
if (!preload && string.IsNullOrWhiteSpace(query))
|
||||
if (!filter.PreLoad && string.IsNullOrWhiteSpace(filter.Query))
|
||||
{
|
||||
return keys;
|
||||
}
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
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();
|
||||
|
||||
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
|
||||
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : [];
|
||||
keys = db.GetConversationStateSearchKeys(filter);
|
||||
keys = filter.PreLoad ? keys : keys.Where(x => x.Contains(filter.Query ?? string.Empty, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
return keys.OrderBy(x => x).Take(filter.KeyLimit).ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,6 @@ public class ConversationStateService : IConversationStateService
|
|||
{
|
||||
if (_conversationId == null || _sidecar?.IsEnabled() == true)
|
||||
{
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +252,6 @@ public class ConversationStateService : IConversationStateService
|
|||
}
|
||||
|
||||
_db.UpdateConversationStates(_conversationId, states);
|
||||
Reset();
|
||||
_logger.LogInformation($"Saved states of conversation {_conversationId}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ public class ConversationStorage : IConversationStorage
|
|||
MetaData = meta,
|
||||
Content = dialog.Content,
|
||||
SecondaryContent = dialog.SecondaryContent,
|
||||
Payload = dialog.Payload,
|
||||
Data = dialog.Data
|
||||
Payload = dialog.Payload
|
||||
});
|
||||
}
|
||||
else
|
||||
|
|
@ -84,8 +83,7 @@ public class ConversationStorage : IConversationStorage
|
|||
SecondaryContent = dialog.SecondaryContent,
|
||||
RichContent = richContent,
|
||||
SecondaryRichContent = secondaryRichContent,
|
||||
Payload = dialog.Payload,
|
||||
Data = dialog.Data
|
||||
Payload = dialog.Payload
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -123,8 +121,7 @@ public class ConversationStorage : IConversationStorage
|
|||
MetaData = meta,
|
||||
Content = dialog.Content,
|
||||
SecondaryContent = dialog.SecondaryContent,
|
||||
Payload = dialog.Payload,
|
||||
Data = dialog.Data
|
||||
Payload = dialog.Payload
|
||||
});
|
||||
}
|
||||
else
|
||||
|
|
@ -155,8 +152,7 @@ public class ConversationStorage : IConversationStorage
|
|||
SecondaryContent = dialog.SecondaryContent,
|
||||
RichContent = richContent,
|
||||
SecondaryRichContent = secondaryRichContent,
|
||||
Payload = dialog.Payload,
|
||||
Data = dialog.Data
|
||||
Payload = dialog.Payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -200,8 +196,7 @@ public class ConversationStorage : IConversationStorage
|
|||
RichContent = richContent,
|
||||
SecondaryContent = secondaryContent,
|
||||
SecondaryRichContent = secondaryRichContent,
|
||||
Payload = payload,
|
||||
Data = dialog.Data
|
||||
Payload = payload
|
||||
};
|
||||
results.Add(record);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,12 +60,15 @@ public class TokenStatistics : ITokenStatistics
|
|||
stat.SetState("llm_total_cost", total_cost, isNeedVersion: false, source: StateSource.Application);
|
||||
|
||||
// Save stats
|
||||
var metric = StatsMetric.AgentLlmCost;
|
||||
var dim = "agent";
|
||||
var agentId = message.CurrentAgentId ?? string.Empty;
|
||||
var globalStats = _services.GetRequiredService<IBotSharpStatsService>();
|
||||
var body = new BotSharpStatsInput
|
||||
{
|
||||
Metric = StatsMetric.AgentLlmCost,
|
||||
Dimension = "agent",
|
||||
DimRefVal = message.CurrentAgentId,
|
||||
Metric = metric,
|
||||
Dimension = dim,
|
||||
DimRefVal = agentId,
|
||||
RecordTime = DateTime.UtcNow,
|
||||
IntervalType = StatsInterval.Day,
|
||||
Data = [
|
||||
|
|
@ -75,7 +78,7 @@ public class TokenStatistics : ITokenStatistics
|
|||
new StatsKeyValuePair("completion_cost_total", deltaCompletionCost)
|
||||
]
|
||||
};
|
||||
globalStats.UpdateStats("global-llm-cost", body);
|
||||
globalStats.UpdateStats($"global-{metric}-{dim}-{agentId}", body);
|
||||
}
|
||||
|
||||
public void PrintStatistics()
|
||||
|
|
@ -110,6 +113,10 @@ public class TokenStatistics : ITokenStatistics
|
|||
|
||||
public void StopTimer()
|
||||
{
|
||||
if (_timer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_timer.Stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Instructs.Models;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Files.Services;
|
||||
|
|
@ -6,10 +8,11 @@ public partial class FileInstructService
|
|||
{
|
||||
public async Task<string> ReadImages(string? provider, string? model, string text, IEnumerable<InstructFileModel> images, string? agentId = null)
|
||||
{
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai", model: model ?? "gpt-4o", multiModal: true);
|
||||
var message = await completion.GetChatCompletions(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString(),
|
||||
Id = innerAgentId,
|
||||
}, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, text)
|
||||
|
|
@ -17,16 +20,55 @@ public partial class FileInstructService
|
|||
Files = images?.Select(x => new BotSharpFile { FileUrl = x.FileUrl, FileData = x.FileData }).ToList() ?? new List<BotSharpFile>()
|
||||
}
|
||||
});
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = text,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message.Content;
|
||||
}
|
||||
|
||||
public async Task<RoleDialogModel> GenerateImage(string? provider, string? model, string text, string? agentId = null)
|
||||
{
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-3");
|
||||
var message = await completion.GetImageGeneration(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString(),
|
||||
Id = innerAgentId,
|
||||
}, new RoleDialogModel(AgentRole.User, text));
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = text,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +79,7 @@ public partial class FileInstructService
|
|||
throw new ArgumentException($"Cannot find image url or data!");
|
||||
}
|
||||
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
|
||||
var bytes = await DownloadFile(image);
|
||||
using var stream = new MemoryStream();
|
||||
|
|
@ -46,10 +89,29 @@ public partial class FileInstructService
|
|||
var fileName = $"{image.FileName ?? "image"}.{image.FileExtension ?? "png"}";
|
||||
var message = await completion.GetImageVariation(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString()
|
||||
Id = innerAgentId
|
||||
}, new RoleDialogModel(AgentRole.User, string.Empty), stream, fileName);
|
||||
|
||||
stream.Close();
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = string.Empty,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +122,7 @@ public partial class FileInstructService
|
|||
throw new ArgumentException($"Cannot find image url or data!");
|
||||
}
|
||||
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
|
||||
var bytes = await DownloadFile(image);
|
||||
using var stream = new MemoryStream();
|
||||
|
|
@ -69,10 +132,29 @@ public partial class FileInstructService
|
|||
var fileName = $"{image.FileName ?? "image"}.{image.FileExtension ?? "png"}";
|
||||
var message = await completion.GetImageEdits(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString()
|
||||
Id = innerAgentId
|
||||
}, new RoleDialogModel(AgentRole.User, text), stream, fileName);
|
||||
|
||||
stream.Close();
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = text,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +166,7 @@ public partial class FileInstructService
|
|||
throw new ArgumentException($"Cannot find image/mask url or data");
|
||||
}
|
||||
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetImageCompletion(_services, provider: provider ?? "openai", model: model ?? "dall-e-2");
|
||||
var imageBytes = await DownloadFile(image);
|
||||
var maskBytes = await DownloadFile(mask);
|
||||
|
|
@ -100,11 +183,30 @@ public partial class FileInstructService
|
|||
var maskName = $"{mask.FileName ?? "mask"}.{mask.FileExtension ?? "png"}";
|
||||
var message = await completion.GetImageEdits(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString()
|
||||
Id = innerAgentId
|
||||
}, new RoleDialogModel(AgentRole.User, text), imageStream, imageName, maskStream, maskName);
|
||||
|
||||
imageStream.Close();
|
||||
maskStream.Close();
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = text,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using BotSharp.Abstraction.Files.Converters;
|
||||
using BotSharp.Abstraction.Instructs.Models;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
|
||||
namespace BotSharp.Core.Files.Services;
|
||||
|
||||
|
|
@ -23,11 +25,12 @@ public partial class FileInstructService
|
|||
var images = await ConvertPdfToImages(pdfFiles);
|
||||
if (images.IsNullOrEmpty()) return content;
|
||||
|
||||
var innerAgentId = agentId ?? Guid.Empty.ToString();
|
||||
var completion = CompletionProvider.GetChatCompletion(_services, provider: provider ?? "openai",
|
||||
model: model, modelId: modelId ?? "gpt-4", multiModal: true);
|
||||
var message = await completion.GetChatCompletions(new Agent()
|
||||
{
|
||||
Id = agentId ?? Guid.Empty.ToString(),
|
||||
Id = innerAgentId,
|
||||
}, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, prompt)
|
||||
|
|
@ -35,6 +38,25 @@ public partial class FileInstructService
|
|||
Files = images.Select(x => new BotSharpFile { FileStorageUrl = x }).ToList()
|
||||
}
|
||||
});
|
||||
|
||||
var hooks = _services.GetServices<IInstructHook>();
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != agentId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = innerAgentId,
|
||||
Provider = completion.Provider,
|
||||
Model = completion.Model,
|
||||
UserMessage = prompt,
|
||||
CompletionText = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
return message.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using BotSharp.Abstraction.Instructs.Settings;
|
||||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BotSharp.Core.Instructs;
|
||||
|
|
@ -11,13 +13,25 @@ public class InsturctionPlugin : IBotSharpPlugin
|
|||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
|
||||
services.AddScoped(provider =>
|
||||
{
|
||||
var settingService = provider.GetRequiredService<ISettingService>();
|
||||
return settingService.Bind<InstructionSettings>("Instruction");
|
||||
});
|
||||
}
|
||||
|
||||
public bool AttachMenu(List<PluginMenuDef> menu)
|
||||
{
|
||||
var section = menu.First(x => x.Label == "Apps");
|
||||
menu.Add(new PluginMenuDef("Instruction", link: "page/instruction", icon: "bx bx-book-content", weight: section.Weight + 5));
|
||||
menu.Add(new PluginMenuDef("Instruction", icon: "bx bx-book-content", weight: section.Weight + 5)
|
||||
{
|
||||
SubMenu = new List<PluginMenuDef>
|
||||
{
|
||||
new PluginMenuDef("Testing", link: "page/instruction/testing"),
|
||||
new PluginMenuDef("Log", link: "page/instruction/log")
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ public partial class InstructService
|
|||
}
|
||||
}
|
||||
|
||||
var provider = string.Empty;
|
||||
var model = string.Empty;
|
||||
|
||||
// Render prompt
|
||||
var prompt = string.IsNullOrEmpty(templateName) ?
|
||||
agentService.RenderedInstruction(agent) :
|
||||
|
|
@ -57,11 +60,18 @@ public partial class InstructService
|
|||
};
|
||||
if (completer is ITextCompletion textCompleter)
|
||||
{
|
||||
instruction = null;
|
||||
provider = textCompleter.Provider;
|
||||
model = textCompleter.Model;
|
||||
|
||||
var result = await textCompleter.GetCompletion(prompt, agentId, message.MessageId);
|
||||
response.Text = result;
|
||||
}
|
||||
else if (completer is IChatCompletion chatCompleter)
|
||||
{
|
||||
provider = chatCompleter.Provider;
|
||||
model = chatCompleter.Model;
|
||||
|
||||
if (instruction == "#TEMPLATE#")
|
||||
{
|
||||
instruction = prompt;
|
||||
|
|
@ -93,6 +103,16 @@ public partial class InstructService
|
|||
}
|
||||
|
||||
await hook.AfterCompletion(agent, response);
|
||||
await hook.OnResponseGenerated(new InstructResponseModel
|
||||
{
|
||||
AgentId = agentId,
|
||||
Provider = provider,
|
||||
Model = model,
|
||||
TemplateName = templateName,
|
||||
UserMessage = prompt,
|
||||
SystemInstruction = instruction,
|
||||
CompletionText = response.Text
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
|
|
|
|||
15
src/Infrastructure/BotSharp.Core/Loggers/LoggerPlugin.cs
Normal file
15
src/Infrastructure/BotSharp.Core/Loggers/LoggerPlugin.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BotSharp.Core.Loggers;
|
||||
|
||||
public class LoggerPlugin : IBotSharpPlugin
|
||||
{
|
||||
public string Id => "ea1aade7-7e29-4f13-a78b-2b1835aa4fea";
|
||||
public string Name => "Logger";
|
||||
public string Description => "Provide log service";
|
||||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
{
|
||||
services.AddScoped<ILoggerService, LoggerService>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
|
||||
namespace BotSharp.Core.Loggers.Services;
|
||||
|
||||
public partial class LoggerService
|
||||
{
|
||||
public async Task<DateTimePagination<ContentLogOutputModel>> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = ConversationLogFilter.Empty();
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationContentLogs(conversationId, filter);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
|
||||
|
||||
public async Task<DateTimePagination<ConversationStateLogModel>> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = ConversationLogFilter.Empty();
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetConversationStateLogs(conversationId, filter);
|
||||
return await Task.FromResult(logs);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
using BotSharp.Abstraction.Users.Models;
|
||||
|
||||
namespace BotSharp.Core.Loggers.Services;
|
||||
|
||||
public partial class LoggerService
|
||||
{
|
||||
public async Task<PagedItems<InstructionLogModel>> GetInstructionLogs(InstructLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = InstructLogFilter.Empty();
|
||||
}
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
|
||||
if (!isAdmin && user?.Id == null) return new();
|
||||
|
||||
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : null;
|
||||
|
||||
var agents = new List<Agent>();
|
||||
var users = new List<User>();
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var logs = db.GetInstructionLogs(filter);
|
||||
var agentIds = logs.Items.Where(x => !string.IsNullOrEmpty(x.AgentId)).Select(x => x.AgentId).ToList();
|
||||
var userIds = logs.Items.Where(x => !string.IsNullOrEmpty(x.UserId)).Select(x => x.UserId).ToList();
|
||||
agents = db.GetAgents(new AgentFilter
|
||||
{
|
||||
AgentIds = agentIds,
|
||||
Pager = new Pagination { Size = filter.Size }
|
||||
});
|
||||
|
||||
if (isAdmin)
|
||||
{
|
||||
users = db.GetUserByIds(userIds);
|
||||
}
|
||||
|
||||
var items = logs.Items.Select(x =>
|
||||
{
|
||||
x.AgentName = !string.IsNullOrEmpty(x.AgentId) ? agents.FirstOrDefault(a => a.Id == x.AgentId)?.Name : null;
|
||||
|
||||
if (!isAdmin)
|
||||
{
|
||||
x.UserName = user != null ? $"{user.FirstName} {user.LastName}" : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = !string.IsNullOrEmpty(x.UserId) ? users.FirstOrDefault(u => u.Id == x.UserId) : null;
|
||||
x.UserName = found != null ? $"{found.FirstName} {found.LastName}" : null;
|
||||
}
|
||||
return x;
|
||||
}).ToList();
|
||||
|
||||
return new PagedItems<InstructionLogModel>
|
||||
{
|
||||
Items = items,
|
||||
Count = logs.Count
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = InstructLogKeysFilter.Empty();
|
||||
}
|
||||
|
||||
var keys = new List<string>();
|
||||
if (!filter.PreLoad && string.IsNullOrWhiteSpace(filter.Query))
|
||||
{
|
||||
return keys;
|
||||
}
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
|
||||
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
|
||||
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : null;
|
||||
keys = db.GetInstructionLogSearchKeys(filter);
|
||||
keys = filter.PreLoad ? keys : keys.Where(x => x.Contains(filter.Query ?? string.Empty, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
return keys.OrderBy(x => x).Take(filter.KeyLimit).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace BotSharp.Core.Loggers.Services;
|
||||
|
||||
public partial class LoggerService : ILoggerService
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IUserIdentity _user;
|
||||
private readonly ILogger<LoggerService> _logger;
|
||||
|
||||
public LoggerService(
|
||||
IServiceProvider services,
|
||||
IUserIdentity user,
|
||||
ILogger<LoggerService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_user = user;
|
||||
_logger = logger;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
using BotSharp.Abstraction.Realtime;
|
||||
using System.Net.WebSockets;
|
||||
using System;
|
||||
using BotSharp.Abstraction.Realtime.Models;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
|
||||
namespace BotSharp.Core.Realtime;
|
||||
|
||||
public class RealtimeHub : IRealtimeHub
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
public RealtimeHub(IServiceProvider services, ILogger<RealtimeHub> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Listen(WebSocket userWebSocket,
|
||||
Func<string, RealtimeHubConnection> onUserMessageReceived)
|
||||
{
|
||||
var buffer = new byte[1024 * 4];
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
var llmProviderService = _services.GetRequiredService<ILlmProviderService>();
|
||||
var model = llmProviderService.GetProviderModel("openai", "gpt-4",
|
||||
realTime: true).Name;
|
||||
|
||||
var completer = _services.GetServices<IRealTimeCompletion>().First(x => x.Provider == "openai");
|
||||
completer.SetModelName(model);
|
||||
|
||||
do
|
||||
{
|
||||
result = await userWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
_logger.LogDebug($"Received from user: {receivedText}");
|
||||
if (string.IsNullOrEmpty(receivedText))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var conn = onUserMessageReceived(receivedText);
|
||||
conn.Model = model;
|
||||
|
||||
if (conn.Event == "user_connected")
|
||||
{
|
||||
await ConnectToModel(completer, userWebSocket, conn);
|
||||
}
|
||||
else if (conn.Event == "user_data_received")
|
||||
{
|
||||
await completer.AppenAudioBuffer(conn.Data);
|
||||
}
|
||||
else if (conn.Event == "user_disconnected")
|
||||
{
|
||||
await completer.Disconnect();
|
||||
}
|
||||
} while (!result.CloseStatus.HasValue);
|
||||
|
||||
await userWebSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task ConnectToModel(IRealTimeCompletion completer, WebSocket userWebSocket, RealtimeHubConnection conn)
|
||||
{
|
||||
var hookProvider = _services.GetRequiredService<ConversationHookProvider>();
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
convService.SetConversationId(conn.ConversationId, []);
|
||||
var conversation = await convService.GetConversation(conn.ConversationId);
|
||||
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agent = await agentService.LoadAgent(conversation.AgentId);
|
||||
conn.EntryAgentId = agent.Id;
|
||||
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
var dialogs = convService.GetDialogHistory();
|
||||
routing.Context.SetDialogs(dialogs);
|
||||
|
||||
await completer.Connect(conn,
|
||||
onModelReady: async () =>
|
||||
{
|
||||
// Control initial session
|
||||
await completer.UpdateInitialSession(conn);
|
||||
|
||||
|
||||
// Add dialog history
|
||||
foreach (var item in dialogs)
|
||||
{
|
||||
await completer.InsertConversationItem(item);
|
||||
}
|
||||
|
||||
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
|
||||
{
|
||||
// await completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await completer.TriggerModelInference("Reply based on the conversation context.");
|
||||
}
|
||||
},
|
||||
onModelAudioDeltaReceived: async audioDeltaData =>
|
||||
{
|
||||
var data = conn.OnModelMessageReceived(audioDeltaData);
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
},
|
||||
onModelAudioResponseDone: async () =>
|
||||
{
|
||||
var data = conn.OnModelAudioResponseDone();
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
},
|
||||
onAudioTranscriptDone: async transcript =>
|
||||
{
|
||||
|
||||
},
|
||||
onModelResponseDone: async messages =>
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// Invoke function
|
||||
if (message.MessageType == "function_call")
|
||||
{
|
||||
await routing.InvokeFunction(message.FunctionName, message);
|
||||
message.Role = AgentRole.Function;
|
||||
await completer.InsertConversationItem(message);
|
||||
await completer.TriggerModelInference("Reply based on the function's output.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// append transcript to conversation
|
||||
storage.Append(conn.ConversationId, message);
|
||||
dialogs.Add(message);
|
||||
|
||||
foreach (var hook in hookProvider.HooksOrderByPriority)
|
||||
{
|
||||
hook.SetAgent(agent)
|
||||
.SetConversation(conversation);
|
||||
|
||||
if (!string.IsNullOrEmpty(message.Content))
|
||||
{
|
||||
await hook.OnMessageReceived(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onConversationItemCreated: async response =>
|
||||
{
|
||||
|
||||
},
|
||||
onInputAudioTranscriptionCompleted: async message =>
|
||||
{
|
||||
// append transcript to conversation
|
||||
storage.Append(conn.ConversationId, message);
|
||||
dialogs.Add(message);
|
||||
},
|
||||
onUserInterrupted: async () =>
|
||||
{
|
||||
var data = conn.OnModelUserInterrupted();
|
||||
await SendEventToUser(userWebSocket, data);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SendEventToUser(WebSocket webSocket, object message)
|
||||
{
|
||||
var data = JsonSerializer.Serialize(message);
|
||||
var buffer = Encoding.UTF8.GetBytes(data);
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
|
@ -129,8 +129,8 @@ public partial class FileRepository
|
|||
Name = task.Name,
|
||||
Description = task.Description,
|
||||
Enabled = task.Enabled,
|
||||
CreatedDateTime = DateTime.UtcNow,
|
||||
UpdatedDateTime = DateTime.UtcNow
|
||||
CreatedTime = DateTime.UtcNow,
|
||||
UpdatedTime = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var fileContent = BuildAgentTaskFileContent(metaData, task.Content);
|
||||
|
|
@ -163,8 +163,8 @@ public partial class FileRepository
|
|||
Name = parsedTask.Name,
|
||||
Description = parsedTask.Description,
|
||||
Enabled = parsedTask.Enabled,
|
||||
CreatedDateTime = parsedTask.CreatedDateTime,
|
||||
UpdatedDateTime = DateTime.UtcNow
|
||||
CreatedTime = parsedTask.CreatedTime,
|
||||
UpdatedTime = DateTime.UtcNow
|
||||
};
|
||||
var content = parsedTask.Content;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
using BotSharp.Abstraction.Users.Models;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Repository;
|
||||
|
|
@ -33,13 +34,19 @@ public partial class FileRepository
|
|||
var stateFile = Path.Combine(dir, STATE_FILE);
|
||||
if (!File.Exists(stateFile))
|
||||
{
|
||||
File.WriteAllText(stateFile, JsonSerializer.Serialize(new List<StateKeyValue>(), _options));
|
||||
File.WriteAllText(stateFile, "[]");
|
||||
}
|
||||
|
||||
var latestStateFile = Path.Combine(dir, CONV_LATEST_STATE_FILE);
|
||||
if (!File.Exists(latestStateFile))
|
||||
{
|
||||
File.WriteAllText(latestStateFile, "{}");
|
||||
}
|
||||
|
||||
var breakpointFile = Path.Combine(dir, BREAKPOINT_FILE);
|
||||
if (!File.Exists(breakpointFile))
|
||||
{
|
||||
File.WriteAllText(breakpointFile, JsonSerializer.Serialize(new List<ConversationBreakpoint>(), _options));
|
||||
File.WriteAllText(breakpointFile, "[]");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +158,7 @@ public partial class FileRepository
|
|||
}
|
||||
}
|
||||
|
||||
public bool UpdateConversationTags(string conversationId, List<string> tags)
|
||||
public bool UpdateConversationTags(string conversationId, List<string> toAddTags, List<string> toDeleteTags)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId)) return false;
|
||||
|
||||
|
|
@ -163,7 +170,11 @@ public partial class FileRepository
|
|||
|
||||
var json = File.ReadAllText(convFile);
|
||||
var conv = JsonSerializer.Deserialize<Conversation>(json, _options);
|
||||
conv.Tags = tags ?? new();
|
||||
|
||||
var tags = conv.Tags ?? [];
|
||||
tags = tags.Concat(toAddTags).Distinct().ToList();
|
||||
conv.Tags = tags.Where(x => !toDeleteTags.Contains(x, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
conv.UpdatedTime = DateTime.UtcNow;
|
||||
File.WriteAllText(convFile, JsonSerializer.Serialize(conv, _options));
|
||||
return true;
|
||||
|
|
@ -300,14 +311,21 @@ public partial class FileRepository
|
|||
if (states.IsNullOrEmpty()) return;
|
||||
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (!string.IsNullOrEmpty(convDir))
|
||||
if (string.IsNullOrEmpty(convDir)) return;
|
||||
|
||||
var stateFile = Path.Combine(convDir, STATE_FILE);
|
||||
if (File.Exists(stateFile))
|
||||
{
|
||||
var stateFile = Path.Combine(convDir, STATE_FILE);
|
||||
if (File.Exists(stateFile))
|
||||
{
|
||||
var stateStr = JsonSerializer.Serialize(states, _options);
|
||||
File.WriteAllText(stateFile, stateStr);
|
||||
}
|
||||
var stateStr = JsonSerializer.Serialize(states, _options);
|
||||
File.WriteAllText(stateFile, stateStr);
|
||||
}
|
||||
|
||||
var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE);
|
||||
if (File.Exists(latestStateFile))
|
||||
{
|
||||
var latestStates = BuildLatestStates(states);
|
||||
var stateStr = JsonSerializer.Serialize(latestStates, _options);
|
||||
File.WriteAllText(latestStateFile, stateStr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -427,25 +445,63 @@ public partial class FileRepository
|
|||
}
|
||||
|
||||
// Check states
|
||||
if (filter != null && !filter.States.IsNullOrEmpty())
|
||||
if (matched && filter != null && !filter.States.IsNullOrEmpty())
|
||||
{
|
||||
var stateFile = Path.Combine(d, STATE_FILE);
|
||||
var convStates = CollectConversationStates(stateFile);
|
||||
foreach (var pair in filter.States)
|
||||
var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE);
|
||||
var convStates = CollectConversationLatestStates(latestStateFile);
|
||||
|
||||
if (convStates.IsNullOrEmpty())
|
||||
{
|
||||
if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue;
|
||||
|
||||
var foundState = convStates.FirstOrDefault(x => x.Key.IsEqualTo(pair.Key));
|
||||
if (foundState == null)
|
||||
matched = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var pair in filter.States)
|
||||
{
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pair.Value))
|
||||
{
|
||||
var curValue = foundState.Values.LastOrDefault()?.Data;
|
||||
matched = matched && pair.Value.IsEqualTo(curValue);
|
||||
var components = pair.Key.Split(".").ToList();
|
||||
var primaryKey = components[0];
|
||||
if (convStates.TryGetValue(primaryKey, out var doc))
|
||||
{
|
||||
var elem = doc.RootElement.GetProperty("data");
|
||||
if (components.Count < 2)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.Value))
|
||||
{
|
||||
if (elem.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
else if (elem.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
matched = elem.EnumerateArray().Where(x => x.ValueKind != JsonValueKind.Null)
|
||||
.Select(x => x.ToString())
|
||||
.Any(x => x == pair.Value);
|
||||
}
|
||||
else if (elem.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
matched = elem.GetString() == pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = elem.GetRawText() == pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var paths = components.Where((_, idx) => idx > 0);
|
||||
var found = FindState(elem, paths, pair.Value);
|
||||
matched = found != null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
|
||||
if (!matched) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -500,16 +556,6 @@ public partial class FileRepository
|
|||
batchSize = batchLimit;
|
||||
}
|
||||
|
||||
if (bufferHours <= 0)
|
||||
{
|
||||
bufferHours = 12;
|
||||
}
|
||||
|
||||
if (messageLimit <= 0)
|
||||
{
|
||||
messageLimit = 2;
|
||||
}
|
||||
|
||||
foreach (var d in Directory.GetDirectories(dir))
|
||||
{
|
||||
var convFile = Path.Combine(d, CONVERSATION_FILE);
|
||||
|
|
@ -585,8 +631,9 @@ public partial class FileRepository
|
|||
// Handle truncated states
|
||||
var refTime = dialogs.ElementAt(foundIdx).MetaData.CreatedTime;
|
||||
var stateDir = Path.Combine(convDir, STATE_FILE);
|
||||
var latestStateDir = Path.Combine(convDir, CONV_LATEST_STATE_FILE);
|
||||
var states = CollectConversationStates(stateDir);
|
||||
isSaved = HandleTruncatedStates(stateDir, states, messageId, refTime);
|
||||
isSaved = HandleTruncatedStates(stateDir, latestStateDir, states, messageId, refTime);
|
||||
|
||||
// Handle truncated breakpoints
|
||||
var breakpointDir = Path.Combine(convDir, BREAKPOINT_FILE);
|
||||
|
|
@ -605,7 +652,7 @@ public partial class FileRepository
|
|||
#if !DEBUG
|
||||
[SharpCache(10)]
|
||||
#endif
|
||||
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
|
||||
public List<string> GetConversationStateSearchKeys(ConversationStateKeysFilter filter)
|
||||
{
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
|
||||
if (!Directory.Exists(dir)) return [];
|
||||
|
|
@ -616,26 +663,29 @@ public partial class FileRepository
|
|||
foreach (var d in Directory.GetDirectories(dir))
|
||||
{
|
||||
var convFile = Path.Combine(d, CONVERSATION_FILE);
|
||||
var stateFile = Path.Combine(d, STATE_FILE);
|
||||
if (!File.Exists(convFile) || !File.Exists(stateFile))
|
||||
var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE);
|
||||
if (!File.Exists(convFile) || !File.Exists(latestStateFile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var convJson = File.ReadAllText(convFile);
|
||||
var stateJson = File.ReadAllText(stateFile);
|
||||
var stateJson = File.ReadAllText(latestStateFile);
|
||||
var conv = JsonSerializer.Deserialize<Conversation>(convJson, _options);
|
||||
var states = JsonSerializer.Deserialize<List<StateKeyValue>>(stateJson, _options);
|
||||
if (conv == null || conv.DialogCount < messageLowerLimit)
|
||||
var states = JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(stateJson, _options);
|
||||
if (conv == null
|
||||
|| states.IsNullOrEmpty()
|
||||
|| (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(conv.AgentId))
|
||||
|| (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(conv.UserId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stateKeys = states?.Select(x => x.Key)?.Distinct()?.ToList() ?? [];
|
||||
var stateKeys = states?.Select(x => x.Key)?.ToList() ?? [];
|
||||
keys.AddRange(stateKeys);
|
||||
count++;
|
||||
|
||||
if (count >= convUpperLimit)
|
||||
if (count >= filter.ConvLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
@ -645,6 +695,56 @@ public partial class FileRepository
|
|||
}
|
||||
|
||||
|
||||
|
||||
public List<string> GetConversationsToMigrate(int batchSize = 100)
|
||||
{
|
||||
var baseDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
|
||||
if (!Directory.Exists(baseDir)) return [];
|
||||
|
||||
var convIds = new List<string>();
|
||||
var dirs = Directory.GetDirectories(baseDir);
|
||||
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
var latestStateFile = Path.Combine(dir, CONV_LATEST_STATE_FILE);
|
||||
if (File.Exists(latestStateFile)) continue;
|
||||
|
||||
var convId = dir.Split(Path.DirectorySeparatorChar).Last();
|
||||
if (string.IsNullOrEmpty(convId)) continue;
|
||||
|
||||
convIds.Add(convId);
|
||||
if (convIds.Count >= batchSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return convIds;
|
||||
}
|
||||
|
||||
|
||||
public bool MigrateConvsersationLatestStates(string conversationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId)) return false;
|
||||
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stateFile = Path.Combine(convDir, STATE_FILE);
|
||||
var states = CollectConversationStates(stateFile);
|
||||
var latestStates = BuildLatestStates(states);
|
||||
|
||||
var latestStateFile = Path.Combine(convDir, CONV_LATEST_STATE_FILE);
|
||||
var stateStr = JsonSerializer.Serialize(latestStates, _options);
|
||||
File.WriteAllText(latestStateFile, stateStr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#region Private methods
|
||||
private string? FindConversationDirectory(string conversationId)
|
||||
{
|
||||
|
|
@ -713,7 +813,7 @@ public partial class FileRepository
|
|||
return isSaved;
|
||||
}
|
||||
|
||||
private bool HandleTruncatedStates(string stateDir, List<StateKeyValue> states, string refMsgId, DateTime refTime)
|
||||
private bool HandleTruncatedStates(string stateDir, string latestStateDir, List<StateKeyValue> states, string refMsgId, DateTime refTime)
|
||||
{
|
||||
var truncatedStates = new List<StateKeyValue>();
|
||||
foreach (var state in states)
|
||||
|
|
@ -734,6 +834,10 @@ public partial class FileRepository
|
|||
}
|
||||
|
||||
var isSaved = SaveTruncatedStates(stateDir, truncatedStates);
|
||||
if (isSaved)
|
||||
{
|
||||
SaveTruncatedLatestStates(latestStateDir, truncatedStates);
|
||||
}
|
||||
return isSaved;
|
||||
}
|
||||
|
||||
|
|
@ -804,6 +908,17 @@ public partial class FileRepository
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool SaveTruncatedLatestStates(string latestStateDir, List<StateKeyValue> states)
|
||||
{
|
||||
if (string.IsNullOrEmpty(latestStateDir) || states == null) return false;
|
||||
if (!File.Exists(latestStateDir)) File.Create(latestStateDir);
|
||||
|
||||
var latestStates = BuildLatestStates(states);
|
||||
var stateStr = JsonSerializer.Serialize(latestStates, _options);
|
||||
File.WriteAllText(latestStateDir, stateStr);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SaveTruncatedBreakpoints(string breakpointDir, List<ConversationBreakpoint> breakpoints)
|
||||
{
|
||||
if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) return false;
|
||||
|
|
@ -814,22 +929,113 @@ public partial class FileRepository
|
|||
return true;
|
||||
}
|
||||
|
||||
private string? EncodeText(string? text)
|
||||
private Dictionary<string, JsonDocument> CollectConversationLatestStates(string latestStateDir)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
if (string.IsNullOrEmpty(latestStateDir) || !File.Exists(latestStateDir)) return [];
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var encoded = Convert.ToBase64String(bytes);
|
||||
return encoded;
|
||||
var str = File.ReadAllText(latestStateDir);
|
||||
var states = JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(str, _options);
|
||||
return states ?? [];
|
||||
}
|
||||
|
||||
private string? DecodeText(string? text)
|
||||
private Dictionary<string, JsonDocument> BuildLatestStates(List<StateKeyValue> states)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
var endNodes = new Dictionary<string, JsonDocument>();
|
||||
if (states.IsNullOrEmpty())
|
||||
{
|
||||
return endNodes;
|
||||
}
|
||||
|
||||
var decoded = Convert.FromBase64String(text);
|
||||
var origin = Encoding.UTF8.GetString(decoded);
|
||||
return origin;
|
||||
foreach (var pair in states)
|
||||
{
|
||||
var value = pair.Values?.LastOrDefault();
|
||||
if (value == null || !value.Active) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(value.Data) }, _options);
|
||||
var json = JsonDocument.Parse(jsonStr);
|
||||
endNodes[pair.Key] = json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var str = JsonSerializer.Serialize(new { Data = value.Data }, _options);
|
||||
var json = JsonDocument.Parse(str);
|
||||
endNodes[pair.Key] = json;
|
||||
}
|
||||
}
|
||||
|
||||
return endNodes;
|
||||
}
|
||||
|
||||
private JsonElement? FindState(JsonElement? root, IEnumerable<string> paths, string? targetValue)
|
||||
{
|
||||
var elem = root;
|
||||
|
||||
if (elem == null || paths.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < paths.Count(); i++)
|
||||
{
|
||||
if (elem == null) return null;
|
||||
|
||||
var field = paths.ElementAt(i);
|
||||
if (elem.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
if (!elem.Value.EnumerateArray().IsNullOrEmpty())
|
||||
{
|
||||
foreach (var item in elem.Value.EnumerateArray())
|
||||
{
|
||||
var subPaths = paths.Where((_, idx) => idx >= i);
|
||||
elem = FindState(item, subPaths, targetValue);
|
||||
if (elem != null)
|
||||
{
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (elem.Value.ValueKind == JsonValueKind.Object && elem.Value.TryGetProperty(field, out var prop))
|
||||
{
|
||||
elem = prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (elem != null && !string.IsNullOrWhiteSpace(targetValue))
|
||||
{
|
||||
if (elem.Value.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (elem.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var isInArray = elem.Value.EnumerateArray().Where(x => x.ValueKind != JsonValueKind.Null)
|
||||
.Select(x => x.ToString())
|
||||
.Any(x => x == targetValue);
|
||||
return isInArray ? elem : null;
|
||||
}
|
||||
else if ((elem.Value.ValueKind == JsonValueKind.String && elem.Value.GetString() == targetValue)
|
||||
|| (elem.Value.ValueKind != JsonValueKind.String && elem.Value.GetRawText() == targetValue))
|
||||
{
|
||||
return elem;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Loggers.Models;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Repository
|
||||
|
|
@ -54,26 +55,34 @@ namespace BotSharp.Core.Repository
|
|||
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
|
||||
}
|
||||
|
||||
public List<ContentLogOutputModel> GetConversationContentLogs(string conversationId)
|
||||
public DateTimePagination<ContentLogOutputModel> GetConversationContentLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
var logs = new List<ContentLogOutputModel>();
|
||||
if (string.IsNullOrEmpty(conversationId)) return logs;
|
||||
if (string.IsNullOrEmpty(conversationId)) return new();
|
||||
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir)) return logs;
|
||||
if (string.IsNullOrEmpty(convDir)) return new();
|
||||
|
||||
var logDir = Path.Combine(convDir, "content_log");
|
||||
if (!Directory.Exists(logDir)) return logs;
|
||||
if (!Directory.Exists(logDir)) return new();
|
||||
|
||||
var logs = new List<ContentLogOutputModel>();
|
||||
foreach (var file in Directory.GetFiles(logDir))
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<ContentLogOutputModel>(text);
|
||||
if (log == null) continue;
|
||||
if (log == null || log.CreatedTime >= filter.StartTime) continue;
|
||||
|
||||
logs.Add(log);
|
||||
}
|
||||
return logs.OrderBy(x => x.CreatedTime).ToList();
|
||||
|
||||
logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList();
|
||||
logs.Reverse();
|
||||
return new DateTimePagination<ContentLogOutputModel>
|
||||
{
|
||||
Items = logs,
|
||||
Count = logs.Count,
|
||||
NextTime = logs.FirstOrDefault()?.CreatedTime
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
@ -99,26 +108,222 @@ namespace BotSharp.Core.Repository
|
|||
File.WriteAllText(file, JsonSerializer.Serialize(log, _options));
|
||||
}
|
||||
|
||||
public List<ConversationStateLogModel> GetConversationStateLogs(string conversationId)
|
||||
public DateTimePagination<ConversationStateLogModel> GetConversationStateLogs(string conversationId, ConversationLogFilter filter)
|
||||
{
|
||||
var logs = new List<ConversationStateLogModel>();
|
||||
if (string.IsNullOrEmpty(conversationId)) return logs;
|
||||
if (string.IsNullOrEmpty(conversationId)) return new();
|
||||
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir)) return logs;
|
||||
if (string.IsNullOrEmpty(convDir)) return new();
|
||||
|
||||
var logDir = Path.Combine(convDir, "state_log");
|
||||
if (!Directory.Exists(logDir)) return logs;
|
||||
if (!Directory.Exists(logDir)) return new();
|
||||
|
||||
var logs = new List<ConversationStateLogModel>();
|
||||
foreach (var file in Directory.GetFiles(logDir))
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<ConversationStateLogModel>(text);
|
||||
if (log == null) continue;
|
||||
if (log == null || log.CreatedTime >= filter.StartTime) continue;
|
||||
|
||||
logs.Add(log);
|
||||
}
|
||||
return logs.OrderBy(x => x.CreatedTime).ToList();
|
||||
|
||||
logs = logs.OrderByDescending(x => x.CreatedTime).Take(filter.Size).ToList();
|
||||
logs.Reverse();
|
||||
return new DateTimePagination<ConversationStateLogModel>
|
||||
{
|
||||
Items = logs,
|
||||
Count = logs.Count,
|
||||
NextTime = logs.FirstOrDefault()?.CreatedTime
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Instruction Log
|
||||
public bool SaveInstructionLogs(IEnumerable<InstructionLogModel> logs)
|
||||
{
|
||||
if (logs.IsNullOrEmpty()) return false;
|
||||
|
||||
var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER);
|
||||
if (!Directory.Exists(baseDir))
|
||||
{
|
||||
Directory.CreateDirectory(baseDir);
|
||||
}
|
||||
|
||||
foreach (var log in logs)
|
||||
{
|
||||
var file = Path.Combine(baseDir, $"{Guid.NewGuid()}.json");
|
||||
log.InnerStates = BuildLogStates(log.States);
|
||||
var text = JsonSerializer.Serialize(log, _options);
|
||||
File.WriteAllText(file, text);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public PagedItems<InstructionLogModel> GetInstructionLogs(InstructLogFilter filter)
|
||||
{
|
||||
if (filter == null)
|
||||
{
|
||||
filter = InstructLogFilter.Empty();
|
||||
}
|
||||
|
||||
var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER);
|
||||
if (!Directory.Exists(baseDir))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
var logs = new List<InstructionLogModel>();
|
||||
var files = Directory.GetFiles(baseDir);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<InstructionLogModel>(json, _options);
|
||||
if (log == null) continue;
|
||||
|
||||
var matched = true;
|
||||
if (!filter.AgentIds.IsNullOrEmpty())
|
||||
{
|
||||
matched = matched && filter.AgentIds.Contains(log.AgentId);
|
||||
}
|
||||
if (!filter.Providers.IsNullOrEmpty())
|
||||
{
|
||||
matched = matched && filter.Providers.Contains(log.Provider);
|
||||
}
|
||||
if (!filter.Models.IsNullOrEmpty())
|
||||
{
|
||||
matched = matched && filter.Models.Contains(log.Model);
|
||||
}
|
||||
if (!filter.TemplateNames.IsNullOrEmpty())
|
||||
{
|
||||
matched = matched && filter.TemplateNames.Contains(log.TemplateName);
|
||||
}
|
||||
if (!filter.UserIds.IsNullOrEmpty())
|
||||
{
|
||||
matched = matched && filter.UserIds.Contains(log.UserId);
|
||||
}
|
||||
|
||||
// Check states
|
||||
if (matched && filter != null && !filter.States.IsNullOrEmpty())
|
||||
{
|
||||
var logStates = log.InnerStates;
|
||||
if (logStates.IsNullOrEmpty())
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var pair in filter.States)
|
||||
{
|
||||
if (pair == null || string.IsNullOrWhiteSpace(pair.Key)) continue;
|
||||
|
||||
var components = pair.Key.Split(".").ToList();
|
||||
var primaryKey = components[0];
|
||||
if (logStates.TryGetValue(primaryKey, out var doc))
|
||||
{
|
||||
var elem = doc.RootElement.GetProperty("data");
|
||||
if (components.Count < 2)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.Value))
|
||||
{
|
||||
if (elem.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
else if (elem.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
matched = elem.EnumerateArray().Where(x => x.ValueKind != JsonValueKind.Null)
|
||||
.Select(x => x.ToString())
|
||||
.Any(x => x == pair.Value);
|
||||
}
|
||||
else if (elem.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
matched = elem.GetString() == pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = elem.GetRawText() == pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var paths = components.Where((_, idx) => idx > 0);
|
||||
var found = FindState(elem, paths, pair.Value);
|
||||
matched = found != null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
|
||||
if (!matched) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) continue;
|
||||
|
||||
log.Id = Path.GetFileNameWithoutExtension(file);
|
||||
logs.Add(log);
|
||||
}
|
||||
|
||||
var records = logs.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size);
|
||||
records = records.Select(x =>
|
||||
{
|
||||
var states = x.InnerStates.ToDictionary(p => p.Key, p =>
|
||||
{
|
||||
var data = p.Value.RootElement.GetProperty("data");
|
||||
return data.ValueKind != JsonValueKind.Null ? data.ToString() : null;
|
||||
});
|
||||
x.States = states ?? [];
|
||||
return x;
|
||||
}).ToList();
|
||||
|
||||
return new PagedItems<InstructionLogModel>
|
||||
{
|
||||
Items = records,
|
||||
Count = logs.Count()
|
||||
};
|
||||
}
|
||||
|
||||
public List<string> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER);
|
||||
if (!Directory.Exists(baseDir))
|
||||
{
|
||||
return keys;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var files = Directory.GetFiles(baseDir);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var log = JsonSerializer.Deserialize<InstructionLogModel>(json, _options);
|
||||
if (log == null) continue;
|
||||
|
||||
if (log == null
|
||||
|| log.InnerStates.IsNullOrEmpty()
|
||||
|| (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(log.UserId))
|
||||
|| (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(log.AgentId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stateKeys = log.InnerStates.Select(x => x.Key)?.ToList() ?? [];
|
||||
keys.AddRange(stateKeys);
|
||||
count++;
|
||||
|
||||
if (count >= filter.LogLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return keys.Distinct().ToList();
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
@ -141,6 +346,28 @@ namespace BotSharp.Core.Repository
|
|||
|
||||
return logIndexes.IsNullOrEmpty() ? 0 : logIndexes.Max() + 1;
|
||||
}
|
||||
|
||||
private Dictionary<string, JsonDocument> BuildLogStates(Dictionary<string, string> states)
|
||||
{
|
||||
var dic = new Dictionary<string, JsonDocument>();
|
||||
foreach (var pair in states)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonStr = JsonSerializer.Serialize(new { Data = JsonDocument.Parse(pair.Value) }, _options);
|
||||
var json = JsonDocument.Parse(jsonStr);
|
||||
dic[pair.Key] = json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
var str = JsonSerializer.Serialize(new { Data = pair.Value }, _options);
|
||||
var json = JsonDocument.Parse(str);
|
||||
dic[pair.Key] = json;
|
||||
}
|
||||
}
|
||||
|
||||
return dic;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using BotSharp.Abstraction.Users.Models;
|
|||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Tasks.Models;
|
||||
|
||||
|
||||
namespace BotSharp.Core.Repository;
|
||||
|
||||
public partial class FileRepository : IBotSharpRepository
|
||||
|
|
@ -34,6 +33,7 @@ public partial class FileRepository : IBotSharpRepository
|
|||
private const string DIALOG_FILE = "dialogs.json";
|
||||
private const string STATE_FILE = "state.json";
|
||||
private const string BREAKPOINT_FILE = "breakpoint.json";
|
||||
private const string CONV_LATEST_STATE_FILE = "latest-state.json";
|
||||
private const string TRANSLATION_MEMORY_FILE = "memory.json";
|
||||
|
||||
private const string USERS_FOLDER = "users";
|
||||
|
|
@ -54,6 +54,7 @@ public partial class FileRepository : IBotSharpRepository
|
|||
private const string STATS_FILE = "stats.json";
|
||||
|
||||
private const string CRON_FILE = "cron.json";
|
||||
private const string INSTRUCTION_LOG_FOLDER = "instruction-logs";
|
||||
|
||||
public FileRepository(
|
||||
IServiceProvider services,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ namespace BotSharp.Core.Routing.Functions;
|
|||
|
||||
public class FallbackToRouterFn : IFunctionCallback
|
||||
{
|
||||
public string Name => "fallback_to_router";
|
||||
public string Name => "util-routing-fallback_to_router";
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public FallbackToRouterFn(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
|
|
@ -14,30 +15,10 @@ public class FallbackToRouterFn : IFunctionCallback
|
|||
|
||||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var agents = await agentService.GetAgents(new AgentFilter
|
||||
{
|
||||
AgentNames = [args.AgentName]
|
||||
});
|
||||
var targetAgent = agents.Items.FirstOrDefault();
|
||||
if (targetAgent == null)
|
||||
{
|
||||
message.Content = $"Can't find routing agent {args.AgentName}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var dialogs = conv.GetDialogHistory();
|
||||
|
||||
var args = JsonSerializer.Deserialize<FallbackArgs>(message.FunctionArgs);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.Replace(targetAgent.Id);
|
||||
message.CurrentAgentId = targetAgent.Id;
|
||||
|
||||
var response = await routing.InstructLoop(message, dialogs);
|
||||
|
||||
message.Content = response.Content;
|
||||
message.StopCompletion = true;
|
||||
routing.Context.PopTo(routing.Context.EntryAgentId, "pop to entry agent");
|
||||
message.Content = args.Question;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
namespace BotSharp.Core.Routing.Hooks;
|
||||
|
||||
public class RoutingUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
private static string PREFIX = "util-routing-";
|
||||
private static string REDIRECT_TO_AGENT = $"{PREFIX}redirect_to_agent";
|
||||
private static string FALLBACK_TO_ROUTER = $"{PREFIX}fallback_to_router";
|
||||
|
||||
public void AddUtilities(List<AgentUtility> utilities)
|
||||
{
|
||||
var utility = new AgentUtility
|
||||
{
|
||||
Name = "routing.tools",
|
||||
Functions = [new($"{REDIRECT_TO_AGENT}"), new($"{FALLBACK_TO_ROUTER}")],
|
||||
Templates = [new($"{REDIRECT_TO_AGENT}.fn"), new($"{FALLBACK_TO_ROUTER}.fn")]
|
||||
};
|
||||
|
||||
utilities.Add(utility);
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ public class NaiveReasoner : IRoutingReasoner
|
|||
};
|
||||
var response = await completion.GetChatCompletions(router, dialogs);
|
||||
|
||||
inst = response.Content.JsonContent<FunctionCallFromLlm>();
|
||||
inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
|
@ -79,7 +80,7 @@ public class RoutingContext : IRoutingContext
|
|||
/// </summary>
|
||||
/// <param name="agentId">Id or Name</param>
|
||||
/// <param name="reason"></param>
|
||||
public void Push(string agentId, string? reason = null)
|
||||
public void Push(string agentId, string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
// Convert id to name
|
||||
if (!Guid.TryParse(agentId, out _))
|
||||
|
|
@ -99,13 +100,15 @@ public class RoutingContext : IRoutingContext
|
|||
HookEmitter.Emit<IRoutingHook>(_services, async hook =>
|
||||
await hook.OnAgentEnqueued(agentId, preAgentId, reason: reason)
|
||||
).Wait();
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop current agent
|
||||
/// </summary>
|
||||
public void Pop(string? reason = null)
|
||||
public void Pop(string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
{
|
||||
|
|
@ -149,15 +152,17 @@ public class RoutingContext : IRoutingContext
|
|||
_stack.Push(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
|
||||
public void PopTo(string agentId, string reason)
|
||||
public void PopTo(string agentId, string reason, bool updateLazyRouting = true)
|
||||
{
|
||||
var currentAgentId = GetCurrentAgentId();
|
||||
while (!string.IsNullOrEmpty(currentAgentId) &&
|
||||
currentAgentId != agentId)
|
||||
{
|
||||
Pop(reason);
|
||||
Pop(reason, updateLazyRouting: updateLazyRouting);
|
||||
currentAgentId = GetCurrentAgentId();
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +186,7 @@ public class RoutingContext : IRoutingContext
|
|||
return _stack.ToArray().Contains(agentId);
|
||||
}
|
||||
|
||||
public void Replace(string agentId, string? reason = null)
|
||||
public void Replace(string agentId, string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
var fromAgent = agentId;
|
||||
var toAgent = agentId;
|
||||
|
|
@ -200,6 +205,8 @@ public class RoutingContext : IRoutingContext
|
|||
await hook.OnAgentReplaced(fromAgent, toAgent, reason: reason)
|
||||
).Wait();
|
||||
}
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
|
||||
public void Empty(string? reason = null)
|
||||
|
|
@ -275,4 +282,24 @@ public class RoutingContext : IRoutingContext
|
|||
{
|
||||
_dialogs = [];
|
||||
}
|
||||
|
||||
private void UpdateLazyRoutingAgent(bool updateLazyRouting)
|
||||
{
|
||||
if (!updateLazyRouting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set next handling agent for lazy routing mode
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
|
||||
if (routingMode == "lazy")
|
||||
{
|
||||
var agentId = GetCurrentAgentId();
|
||||
if (agentId != BuiltInAgentId.Fallback)
|
||||
{
|
||||
states.SetState(StateConst.LAZY_ROUTING_AGENT_ID, agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,7 @@ public class RoutingPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IRoutingReasoner, HFReasoner>();
|
||||
|
||||
services.AddScoped<IRoutingReasoner, OneStepForwardReasoner>();
|
||||
|
||||
services.AddScoped<IAgentUtilityHook, RoutingUtilityHook>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public partial class RoutingService
|
|||
// Handle output routing exception.
|
||||
if (agent.Type == AgentType.Routing)
|
||||
{
|
||||
// Forgot about what situation needs to handle in this way
|
||||
response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class AgentTaskService : IAgentTaskService
|
|||
|
||||
return new PagedItems<AgentTask>
|
||||
{
|
||||
Items = items.OrderByDescending(x => x.UpdatedDateTime)
|
||||
Items = items.OrderByDescending(x => x.UpdatedTime)
|
||||
.Skip(filter.Pager.Offset).Take(filter.Pager.Size),
|
||||
Count = items.Count()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Agents;
|
||||
using BotSharp.Abstraction.Conversations.Enums;
|
||||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Translation;
|
||||
using System;
|
||||
|
|
@ -26,6 +27,11 @@ namespace BotSharp.Logger.Hooks
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_states.GetState("channel") == ConversationChannel.Phone)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle multi-language for output
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
|
|
|||
|
|
@ -418,23 +418,24 @@ public class UserService : IUserService
|
|||
return users;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAdminUser(string userId)
|
||||
[SharpCache(10, perInstanceCache: true)]
|
||||
public async Task<(bool, User?)> IsAdminUser(string userId)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var user = db.GetUserById(userId);
|
||||
return user != null && UserConstant.AdminRoles.Contains(user.Role);
|
||||
var isAdmin = user != null && UserConstant.AdminRoles.Contains(user.Role);
|
||||
return (isAdmin, user);
|
||||
}
|
||||
|
||||
public async Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var user = db.GetUserById(_user.Id);
|
||||
var (isAdmin, user) = await IsAdminUser(_user.Id);
|
||||
var auth = new UserAuthorization();
|
||||
|
||||
if (user == null) return auth;
|
||||
|
||||
auth.IsAdmin = UserConstant.AdminRoles.Contains(user.Role);
|
||||
|
||||
auth.IsAdmin = isAdmin;
|
||||
var role = db.GetRoles(new RoleFilter { Names = [user.Role] }).FirstOrDefault();
|
||||
var permissions = user.Permissions?.Any() == true ? user.Permissions : role?.Permissions ?? [];
|
||||
auth.Permissions = permissions;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ global using BotSharp.Abstraction.SideCar.Attributes;
|
|||
global using BotSharp.Abstraction.Statistics.Models;
|
||||
global using BotSharp.Abstraction.Statistics.Enums;
|
||||
global using BotSharp.Abstraction.Statistics.Services;
|
||||
global using BotSharp.Abstraction.Loggers.Services;
|
||||
global using BotSharp.Abstraction.Infrastructures.Events;
|
||||
global using BotSharp.Core.Repository;
|
||||
global using BotSharp.Core.Routing;
|
||||
global using BotSharp.Core.Agents.Services;
|
||||
|
|
@ -46,4 +48,4 @@ global using BotSharp.Core.Conversations.Services;
|
|||
global using BotSharp.Core.Infrastructures;
|
||||
global using BotSharp.Core.Users.Services;
|
||||
global using BotSharp.Core.Statistics.Services;
|
||||
global using BotSharp.Abstraction.Infrastructures.Events;
|
||||
global using BotSharp.Core.Loggers.Services;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d",
|
||||
"name": "Fallback Agent",
|
||||
"description": "Don't have sufficient confidence to trigger any of existing agent.",
|
||||
"description": "Handle initiated conversation without specific task given yet or don't have sufficient confidence to handle user task.",
|
||||
"type": "task",
|
||||
"createdDateTime": "2024-05-07T10:00:00Z",
|
||||
"updatedDateTime": "2024-05-07T10:00:00Z",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "route_to_agent",
|
||||
"description": "Route request to appropriate AI agent.",
|
||||
"visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"next_action_agent": {
|
||||
"type": "string",
|
||||
"description": "Agent for next action based on user latest response"
|
||||
},
|
||||
"next_action_reason": {
|
||||
"type": "string",
|
||||
"description": "The reason why route to this agent."
|
||||
},
|
||||
"user_goal_agent": {
|
||||
"type": "string",
|
||||
"description": "Agent who can acheive user initial task."
|
||||
},
|
||||
"conversation_end": {
|
||||
"type": "boolean",
|
||||
"description": "User is ending the conversation."
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"description": "Required parameters of next action agent"
|
||||
}
|
||||
},
|
||||
"required": [ "next_action_agent", "user_goal_agent", "next_action_reason", "args" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ Follow these steps to handle user request:
|
|||
2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service.
|
||||
3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments.
|
||||
4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared.
|
||||
{% if routing_mode != 'lazy' %}
|
||||
5. Response must be in JSON format.
|
||||
{% endif %}
|
||||
|
||||
{% if routing_requirements and routing_requirements != empty %}
|
||||
[REQUIREMENTS]
|
||||
|
|
@ -14,6 +16,7 @@ Follow these steps to handle user request:
|
|||
{%- endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if routing_mode != 'lazy' %}
|
||||
[FUNCTIONS]
|
||||
{% for handler in routing_handlers -%}
|
||||
# {{ handler.description}}
|
||||
|
|
@ -26,6 +29,7 @@ Parameters:
|
|||
{%- endif %}
|
||||
{{ "\r\n" }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
|
||||
[AGENTS]
|
||||
{% for agent in routing_agents -%}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "util-routing-fallback_to_router",
|
||||
"description": "Return to the Router to find the appropriate agent who can handle the user's request.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fallback_reason": {
|
||||
"type": "string",
|
||||
"description": "The reason why you need to reach out to other agent."
|
||||
},
|
||||
"user_question": {
|
||||
"type": "string",
|
||||
"description": "User question or statement."
|
||||
}
|
||||
},
|
||||
"required": [ "fallback_reason" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
Carefully consider whether the current user request is related to your responsibilities. Only when it is not relevant should you consider Return to the Router.
|
||||
|
|
@ -15,6 +15,7 @@ public static class BotSharpLoggerExtensions
|
|||
services.AddScoped<IContentGeneratingHook, VerboseLogHook>();
|
||||
services.AddScoped<IContentGeneratingHook, GlobalStatsConversationHook>();
|
||||
services.AddScoped<IConversationHook, RateLimitConversationHook>();
|
||||
services.AddScoped<IInstructHook, InstructionLogHook>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue