Merge branch 'SciSharp:master' into master

This commit is contained in:
geffzhang 2025-03-17 07:24:01 +08:00 committed by GitHub
commit 404a3ed772
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
296 changed files with 4749 additions and 1263 deletions

View file

@ -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}

View file

@ -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>

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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";
}

View file

@ -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);
}

View file

@ -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()

View file

@ -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()
{

View file

@ -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;

View file

@ -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";
}

View file

@ -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);
}

View file

@ -15,4 +15,9 @@ public class InstructHookBase : IInstructHook
{
return;
}
public virtual async Task OnResponseGenerated(InstructResponseModel response)
{
return;
}
}

View file

@ -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!;
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Instructs.Settings;
public class InstructionSettings
{
public bool EnableLog { get; set; }
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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)

View file

@ -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();
}

View file

@ -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()
{

View file

@ -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,

View file

@ -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; }
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Realtime;
public interface IRealtimeHook
{
string[] OnModelTranscriptPrompt(Agent agent);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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()
{

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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);

View file

@ -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;
}

View file

@ -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; }

View file

@ -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; }
}

View file

@ -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);

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Utilities;
public class DateTimePagination<T> : PagedItems<T>
{
public DateTime? NextTime { get; set; }
}

View file

@ -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 _);
}
}

View file

@ -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();
}

View file

@ -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; }
}

View file

@ -29,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NCrontab" Version="3.3.3" />
<PackageReference Include="NCrontab" />
</ItemGroup>
<ItemGroup>

View file

@ -97,7 +97,7 @@ public class CrontabService : ICrontabService, ITaskFeeder
Status = TaskStatus.Scheduled,
Enabled = !x.Disabled,
Description = cron.Description,
LastExecutedDateTime = cron.LastExecutionTime
LastExecutionTime = cron.LastExecutionTime
}));
}

View file

@ -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>

View file

@ -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);
}
}
}

View 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>();
}
}

View file

@ -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;
}
}

View 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;

View file

@ -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);
}

View file

@ -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() ?? [];
}

View file

@ -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.";

View file

@ -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>

View file

@ -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>();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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}");
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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;

View 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>();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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);
}
}
}
}

View file

@ -37,5 +37,7 @@ public class RoutingPlugin : IBotSharpPlugin
services.AddScoped<IRoutingReasoner, HFReasoner>();
services.AddScoped<IRoutingReasoner, OneStepForwardReasoner>();
services.AddScoped<IAgentUtilityHook, RoutingUtilityHook>();
}
}

View file

@ -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?";
}

View file

@ -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()
};

View file

@ -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>();

View file

@ -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;

View file

@ -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;

View file

@ -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",

View file

@ -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" ]
}
}

View file

@ -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 -%}

View file

@ -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" ]
}
}

View file

@ -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.

View file

@ -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