Merge branch 'master' into MetaGLM

This commit is contained in:
geffzhang 2024-06-06 08:49:24 +08:00 committed by GitHub
commit 49020edd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
255 changed files with 5258 additions and 1297 deletions

View file

@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SparkDesk",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MetaGLM", "src\Plugins\BotSharp.Plugin.MetaGLM\BotSharp.Plugin.MetaGLM.csproj", "{CCF745F2-0C95-4ED0-983B-507C528B39EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.AnthropicAI", "src\Plugins\BotSharp.Plugin.AnthropicAI\BotSharp.Plugin.AnthropicAI.csproj", "{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -343,6 +345,14 @@ Global
{CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|Any CPU.Build.0 = Release|Any CPU
{CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.ActiveCfg = Release|Any CPU
{CCF745F2-0C95-4ED0-983B-507C528B39EA}.Release|x64.Build.0 = Release|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.ActiveCfg = Debug|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Debug|x64.Build.0 = Debug|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|Any CPU.Build.0 = Release|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.ActiveCfg = Release|Any CPU
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -386,6 +396,7 @@ Global
{267998C1-55C2-4ADC-8361-2CDFA5EA6D6C} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{289E25C8-63F1-4D52-9909-207724DB40CB} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{CCF745F2-0C95-4ED0-983B-507C528B39EA} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{806A0B0E-FEFF-420E-B5B2-C9FCBF890A8C} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<BotSharpVersion>1.2.1</BotSharpVersion>
<BotSharpVersion>1.5.1</BotSharpVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

View file

@ -10,7 +10,7 @@ namespace BotSharp.Abstraction.Agents;
public interface IAgentService
{
Task<Agent> CreateAgent(Agent agent);
Task RefreshAgents();
Task<string> RefreshAgents();
Task<PagedItems<Agent>> GetAgents(AgentFilter filter);
/// <summary>
@ -26,6 +26,8 @@ public interface IAgentService
bool RenderFunction(Agent agent, FunctionDef def);
FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def);
/// <summary>
/// Get agent detail without trigger any hook.
/// </summary>
@ -35,9 +37,18 @@ public interface IAgentService
Task<bool> DeleteAgent(string id);
Task UpdateAgent(Agent agent, AgentField updateField);
Task UpdateAgentFromFile(string id);
/// <summary>
/// Path existing templates of agent, cannot create new or delete templates
/// </summary>
/// <param name="agent"></param>
/// <returns></returns>
Task<string> PatchAgentTemplate(Agent agent);
Task<string> UpdateAgentFromFile(string id);
string GetDataDir();
string GetAgentDataDir(string agentId);
List<Agent> GetAgentsByUser(string userId);
PluginDef GetPlugin(string agentId);
}

View file

@ -5,6 +5,8 @@ public class AgentSettings
public string DataDir { get; set; } = string.Empty;
public string TemplateFormat { get; set; } = "liquid";
public string HostAgentId { get; set; } = string.Empty;
public bool EnableTranslator { get; set; } = false;
public bool EnableHttpHandler { get; set; } = false;
/// <summary>
/// This is the default LLM config for agent

View file

@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Browsing.Enums;
public enum BroswerActionEnum
{
Click = 1,
InputText = 2,
}

View file

@ -4,8 +4,8 @@ namespace BotSharp.Abstraction.Browsing;
public interface IWebBrowser
{
Task<BrowserActionResult> LaunchBrowser(string conversationId, string? url);
Task<BrowserActionResult> ScreenshotAsync(string conversationId, string path);
Task<BrowserActionResult> LaunchBrowser(string contextId, string? url);
Task<BrowserActionResult> ScreenshotAsync(string contextId, string path);
Task<BrowserActionResult> ScrollPageAsync(BrowserActionParams actionParams);
Task<BrowserActionResult> ActionOnElement(MessageInfo message, ElementLocatingArgs location, ElementActionArgs action);
@ -19,10 +19,11 @@ public interface IWebBrowser
Task<BrowserActionResult> ChangeListValue(BrowserActionParams actionParams);
Task<BrowserActionResult> CheckRadioButton(BrowserActionParams actionParams);
Task<BrowserActionResult> ChangeCheckbox(BrowserActionParams actionParams);
Task<BrowserActionResult> GoToPage(string conversationId, string url);
Task<BrowserActionResult> GoToPage(string contextId, string url, bool openNewTab = false);
Task<string> ExtractData(BrowserActionParams actionParams);
Task<T> EvaluateScript<T>(string conversationId, string script);
Task CloseBrowser(string conversationId);
Task<BrowserActionResult> SendHttpRequest(BrowserActionParams actionParams);
Task<string> GetAttributeValue(MessageInfo message, ElementLocatingArgs location, BrowserActionResult result);
Task<T> EvaluateScript<T>(string contextId, string script);
Task CloseBrowser(string contextId);
Task CloseCurrentPage(string contextId);
Task<BrowserActionResult> SendHttpRequest(MessageInfo message, HttpRequestParams actionParams);
Task<BrowserActionResult> GetAttributeValue(MessageInfo message, ElementLocatingArgs location);
}

View file

@ -4,14 +4,14 @@ public class BrowserActionParams
{
public Agent Agent { get; set; }
public BrowsingContextIn Context { get; set; }
public string ConversationId { get; set; }
public string ContextId { get; set; }
public string MessageId { get; set; }
public BrowserActionParams(Agent agent, BrowsingContextIn context, string conversationId, string messageId)
public BrowserActionParams(Agent agent, BrowsingContextIn context, string contextId, string messageId)
{
Agent = agent;
Context = context;
ConversationId = conversationId;
ContextId = contextId;
MessageId = messageId;
}
}

View file

@ -3,8 +3,14 @@ namespace BotSharp.Abstraction.Browsing.Models;
public class BrowserActionResult
{
public bool IsSuccess { get; set; }
public string ErrorMessage { get; set; }
public string StackTrace { get; set; }
public string? Message { get; set; }
public string? StackTrace { get; set; }
public string Selector { get; set; }
public string Body { get; set; }
public bool IsHighlighted { get; set; }
public override string ToString()
{
return $"{IsSuccess} - {Selector}";
}
}

View file

@ -40,10 +40,4 @@ public class BrowsingContextIn
[JsonPropertyName("direction")]
public string? Direction { get; set; }
/// <summary>
/// Http request payload
/// </summary>
[JsonPropertyName("payload")]
public string? Payload { get; set; }
}

View file

@ -1,12 +1,34 @@
using BotSharp.Abstraction.Browsing.Enums;
namespace BotSharp.Abstraction.Browsing.Models;
public class ElementActionArgs
{
private string _action;
public string Action => _action;
public BroswerActionEnum Action { get; set; }
public ElementActionArgs(string action)
public string? Content { get; set; }
public ElementPosition? Position { get; set; }
public string? PressKey { get; set; }
/// <summary>
/// Required for deserialization
/// </summary>
public ElementActionArgs()
{
_action = action;
}
public ElementActionArgs(BroswerActionEnum action, ElementPosition? position = null)
{
Action = action;
Position = position;
}
public ElementActionArgs(BroswerActionEnum action, string content)
{
Action = action;
Content = content;
}
}

View file

@ -5,6 +5,9 @@ public class ElementLocatingArgs
[JsonPropertyName("match_rule")]
public string MatchRule { get; set; } = string.Empty;
[JsonPropertyName("tag")]
public string? Tag { get; set; } = null!;
[JsonPropertyName("text")]
public string? Text { get; set; }
@ -20,5 +23,12 @@ public class ElementLocatingArgs
[JsonPropertyName("selector")]
public string? Selector { get; set; }
public bool Parent { get; set; }
public bool FailIfMultiple { get; set; }
/// <summary>
/// Draw outline around the element
/// </summary>
public bool Highlight { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Browsing.Models;
public class ElementPosition
{
public float X { get; set; } = default!;
public float Y { get; set; } = default!;
}

View file

@ -0,0 +1,25 @@
using System.Net.Http;
namespace BotSharp.Abstraction.Browsing.Models;
public class HttpRequestParams
{
[JsonPropertyName("url")]
public string Url { get; set; } = string.Empty;
[JsonPropertyName("method")]
public HttpMethod Method { get; set; }
/// <summary>
/// Http request payload
/// </summary>
[JsonPropertyName("payload")]
public string? Payload { get; set; }
public HttpRequestParams(string url, HttpMethod method, string? payload = null)
{
Method = method;
Url = url;
Payload = payload;
}
}

View file

@ -3,6 +3,6 @@ namespace BotSharp.Abstraction.Browsing.Models;
public class MessageInfo
{
public string AgentId { get; set; }
public string ConversationId { get; set; }
public string ContextId { get; set; }
public string MessageId { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Browsing.Settings;
public class WebBrowsingSettings
{
public string Driver { get; set; } = "Playwright";
}

View file

@ -44,6 +44,9 @@ public abstract class ConversationHookBase : IConversationHook
public virtual Task OnConversationEnding(RoleDialogModel message)
=> Task.CompletedTask;
public virtual Task OnNewTaskDetected(RoleDialogModel message, string reason)
=> Task.CompletedTask;
public virtual Task OnTaskCompleted(RoleDialogModel message)
=> Task.CompletedTask;

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Conversations.Enums;
public class StateDataType
{
public const string String = "string";
public const string Boolean = "boolean";
public const string Number = "number";
public const string Currency = "currency";
public const string Date = "date";
}

View file

@ -0,0 +1,8 @@
namespace BotSharp.Abstraction.Conversations.Enums;
public class StateSource
{
public const string External = "external";
public const string Application = "application";
public const string User = "user";
}

View file

@ -1,6 +0,0 @@
namespace BotSharp.Abstraction.Conversations;
public interface IConversationAttachmentService
{
string GetDirectory(string conversationId);
}

View file

@ -64,6 +64,13 @@ public interface IConversationHook
Task OnResponseGenerated(RoleDialogModel message);
/// <summary>
/// LLM detected user requested a new task different from previous topic.
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task OnNewTaskDetected(RoleDialogModel message, string reason);
/// <summary>
/// LLM detected the current task is completed.
/// It's useful for the situation of multiple tasks in the same conversation.

View file

@ -15,7 +15,15 @@ public interface IConversationService
Task<List<Conversation>> GetLastConversations();
Task<List<string>> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
Task<bool> DeleteConversations(IEnumerable<string> ids);
Task<bool> TruncateConversation(string conversationId, string messageId);
/// <summary>
/// Truncate conversation
/// </summary>
/// <param name="conversationId">Target conversation id</param>
/// <param name="messageId">Target message id to delete</param>
/// <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);
@ -42,6 +50,10 @@ public interface IConversationService
/// Use this feature when you want to hide some context from LLM.
/// </summary>
/// <param name="resetStates">Whether to reset all states</param>
/// <param name="reason">Append user init words</param>
/// <param name="excludedStates"></param>
/// <returns></returns>
Task UpdateBreakpoint(bool resetStates = false);
Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates);
Task<string> GetConversationSummary(IEnumerable<string> conversationId);
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using System.Text.Json;
namespace BotSharp.Abstraction.Conversations;
@ -8,12 +9,14 @@ namespace BotSharp.Abstraction.Conversations;
public interface IConversationStateService
{
string GetConversationId();
Dictionary<string, string> Load(string conversationId);
Dictionary<string, string> Load(string conversationId, bool isReadOnly = false);
string GetState(string name, string defaultValue = "");
bool ContainsState(string name);
Dictionary<string, string> GetStates();
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1);
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true,
int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false);
void SaveStateByArgs(JsonDocument args);
void CleanStates();
bool RemoveState(string name);
void CleanStates(params string[] excludedStates);
void Save();
}

View file

@ -32,34 +32,63 @@ public class Conversation
public class DialogElement
{
[JsonPropertyName("meta_data")]
public DialogMetaData MetaData { get; set; }
[JsonPropertyName("content")]
public string Content { get; set; }
[JsonPropertyName("secondary_content")]
public string? SecondaryContent { get; set; }
[JsonPropertyName("rich_content")]
public string? RichContent { get; set; }
[JsonPropertyName("secondary_rich_content")]
public string? SecondaryRichContent { get; set; }
[JsonPropertyName("payload")]
public string? Payload { get; set; }
public DialogElement()
{
}
public DialogElement(DialogMetaData meta, string content, string? richContent = null)
public DialogElement(DialogMetaData meta, string content, string? richContent = null,
string? secondaryContent = null, string? secondaryRichContent = null, string? payload = null)
{
MetaData = meta;
Content = content;
RichContent = richContent;
SecondaryContent = secondaryContent;
SecondaryRichContent = secondaryRichContent;
Payload = payload;
}
public override string ToString()
{
return $"{MetaData.Role}: {Content} [{MetaData.CreateTime}]";
return $"{MetaData.Role}: {Content} [{MetaData?.CreateTime}]";
}
}
public class DialogMetaData
{
[JsonPropertyName("role")]
public string Role { get; set; }
[JsonPropertyName("agent_id")]
public string AgentId { get; set; }
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("function_name")]
public string? FunctionName { get; set; }
[JsonPropertyName("sender_id")]
public string? SenderId { get; set; }
[JsonPropertyName("create_at")]
public DateTime CreateTime { get; set; }
}

View file

@ -10,4 +10,7 @@ public class ConversationBreakpoint
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
[JsonPropertyName("reason")]
public string? Reason { get; set; }
}

View file

@ -6,6 +6,10 @@ public class ConversationSenderActionModel
{
[JsonPropertyName("conversation_id")]
public string ConversationId { get; set; }
[JsonPropertyName("sender_action")]
public SenderActionEnum SenderAction { get; set; }
[JsonPropertyName("indication")]
public string? Indication { get; set; }
}

View file

@ -9,4 +9,6 @@ public class IncomingMessageModel : MessageConfig
/// Postback message
/// </summary>
public PostbackMessageModel? Postback { get; set; }
public List<BotSharpFile> Files { get; set; } = new List<BotSharpFile>();
}

View file

@ -26,6 +26,20 @@ public class RoleDialogModel : ITrackableMessage
public string Content { get; set; }
public string? SecondaryContent { get; set; }
/// <summary>
/// Postback content
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("payload")]
public string? Payload { get; set; }
/// <summary>
/// Indicator message used to provide UI feedback for function execution
/// </summary>
public string? Indication { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string CurrentAgentId { get; set; }
@ -35,6 +49,12 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionName { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ToolCallId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PostbackFunctionName { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionArgs { get; set; }
@ -54,6 +74,9 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RichContent<IRichMessage>? RichContent { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RichContent<IRichMessage>? SecondaryRichContent { get; set; }
/// <summary>
/// Stop conversation completion
/// </summary>
@ -62,6 +85,8 @@ public class RoleDialogModel : ITrackableMessage
public FunctionCallFromLlm Instruction { get; set; }
public List<BotSharpFile> Files { get; set; } = new List<BotSharpFile>();
private RoleDialogModel()
{
}
@ -95,6 +120,8 @@ public class RoleDialogModel : ITrackableMessage
MessageId = source.MessageId,
FunctionArgs = source.FunctionArgs,
FunctionName = source.FunctionName,
ToolCallId = source.ToolCallId,
PostbackFunctionName = source.PostbackFunctionName,
RichContent = source.RichContent,
StopCompletion = source.StopCompletion,
Instruction = source.Instruction,

View file

@ -12,14 +12,23 @@ public class StateChangeModel
public string Name { get; set; }
[JsonPropertyName("before_value")]
public string BeforeValue { get; set; }
public string? BeforeValue { get; set; }
[JsonPropertyName("before_active_rounds")]
public int? BeforeActiveRounds { get; set; }
[JsonPropertyName("after_value")]
public string AfterValue { get; set; }
public string? AfterValue { get; set; }
[JsonPropertyName("after_active_rounds")]
public int? AfterActiveRounds { get; set; }
[JsonPropertyName("data_type")]
public string DataType { get; set; }
[JsonPropertyName("source")]
public string Source { get; set; }
[JsonPropertyName("readonly")]
public bool Readonly { get; set; }
}

View file

@ -1,9 +1,12 @@
using BotSharp.Abstraction.Conversations.Enums;
namespace BotSharp.Abstraction.Conversations.Models;
public class StateKeyValue
{
public string Key { get; set; }
public bool Versioning { get; set; }
public bool Readonly { get; set; }
public List<StateValue> Values { get; set; } = new List<StateValue>();
public StateKeyValue()
@ -16,6 +19,12 @@ public class StateKeyValue
Key = key;
Values = values;
}
public override string ToString()
{
var lastValue = Values.LastOrDefault();
return $"{Key} => ({lastValue?.ToString()})";
}
}
public class StateValue
@ -30,6 +39,12 @@ public class StateValue
[JsonPropertyName("active_rounds")]
public int ActiveRounds { get; set; }
[JsonPropertyName("data_type")]
public string DataType { get; set; } = StateDataType.String;
[JsonPropertyName("source")]
public string Source { get; set; }
[JsonPropertyName("update_time")]
public DateTime UpdateTime { get; set; }
@ -37,4 +52,11 @@ public class StateValue
{
}
public override string ToString()
{
var isActive = Active ? "Yes" : "No";
var activeRounds = ActiveRounds <= 0 ? "infinity" : ActiveRounds.ToString();
return $"Data: {Data}, Active: {isActive}, Active rounds: {activeRounds}, Source: {Source}";
}
}

View file

@ -3,6 +3,6 @@ namespace BotSharp.Abstraction.Conversations.Settings;
public class RateLimitSetting
{
public int MaxConversationPerDay { get; set; } = 100;
public int MaxInputLengthPerRequest { get; set; } = 256;
public int MaxInputLengthPerRequest { get; set; } = 512;
public int MinTimeSecondsBetweenMessages { get; set; } = 2;
}

View file

@ -0,0 +1,31 @@
namespace BotSharp.Abstraction.Files;
public interface IBotSharpFileService
{
string GetDirectory(string conversationId);
IEnumerable<MessageFileModel> GetChatImages(string conversationId, List<RoleDialogModel> conversations, int offset = 2);
IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, bool imageOnly = false);
string GetMessageFile(string conversationId, string messageId, string fileName);
bool SaveMessageFiles(string conversationId, string messageId, List<BotSharpFile> files);
string GetUserAvatar();
bool SaveUserAvatar(BotSharpFile file);
/// <summary>
/// Delete files under messages
/// </summary>
/// <param name="conversationId">Conversation Id</param>
/// <param name="messageIds">Files in these messages will be deleted</param>
/// <param name="targetMessageId">The starting message to delete</param>
/// <param name="newMessageId">If not null, delete messages while input a new message; otherwise, delete messages only</param>
/// <returns></returns>
bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId, string? newMessageId = null);
bool DeleteConversationFiles(IEnumerable<string> conversationIds);
/// <summary>
/// Get file bytes and content type from data, e.g., "data:image/png;base64,aaaaaaaaa"
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
(string, byte[]) GetFileInfoFromData(string data);
}

View file

@ -0,0 +1,17 @@
namespace BotSharp.Abstraction.Files.Models;
public class BotSharpFile
{
[JsonPropertyName("file_name")]
public string FileName { get; set; } = string.Empty;
/// <summary>
/// File data, e.g., "data:image/png;base64,aaaaaaaa"
/// </summary>
[JsonPropertyName("file_data")]
public string FileData { get; set; } = string.Empty;
[JsonPropertyName("file_url")]
public string FileUrl { get; set; } = string.Empty;
}

View file

@ -0,0 +1,32 @@
namespace BotSharp.Abstraction.Files.Models;
public class MessageFileModel
{
[JsonPropertyName("message_id")]
public string MessageId { get; set; }
[JsonPropertyName("file_url")]
public string FileUrl { get; set; }
[JsonPropertyName("file_storage_url")]
public string FileStorageUrl { get; set; }
[JsonPropertyName("file_name")]
public string FileName { get; set; }
[JsonPropertyName("file_type")]
public string FileType { get; set; }
[JsonPropertyName("content_type")]
public string ContentType { get; set; }
public MessageFileModel()
{
}
public override string ToString()
{
return $"File name: {FileName}, File type: {FileType}, Content type: {ContentType}";
}
}

View file

@ -3,5 +3,11 @@ namespace BotSharp.Abstraction.Functions;
public interface IFunctionCallback
{
string Name { get; }
/// <summary>
/// Indicator message used to provide UI feedback for function execution
/// </summary>
string Indication => string.Empty;
Task<bool> Execute(RoleDialogModel message);
}

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Routing.Models;
using System.Text.Json;
namespace BotSharp.Abstraction.Functions.Models;

View file

@ -13,7 +13,7 @@ public class FunctionCallingResponse
[JsonPropertyName("content")]
public string? Content { get; set; }
[JsonPropertyName("function_name")]
[JsonPropertyName("function")]
public string? FunctionName { get; set; }
[JsonPropertyName("args")]

View file

@ -2,8 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models;
public class FunctionDef
{
public string Name { get; set; }
public string Description { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("description")]
public string Description { get; set; } = null!;
[JsonPropertyName("visibility_expression")]
public string? VisibilityExpression { get; set; }
@ -11,6 +14,7 @@ public class FunctionDef
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Impact { get; set; }
[JsonPropertyName("parameters")]
public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef();
public override string ToString()

View file

@ -19,6 +19,11 @@ public class FunctionParametersDef
[JsonPropertyName("required")]
public List<string> Required { get; set; } = new List<string>();
public override string ToString()
{
return $"{{\"type\":\"{Type}\", \"properties\":{JsonSerializer.Serialize(Properties)}, \"required\":[{string.Join(",", Required.Select(x => "\"" + x + "\""))}]}}";
}
public FunctionParametersDef()
{

View file

@ -2,10 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models;
public class ParameterPropertyDef : NameDesc
{
public ParameterPropertyDef(string name, string description, string type = "string")
public ParameterPropertyDef(string name, string description, string type = "string", bool required = false)
: base(name, description)
{
Type = type;
Required = required;
}
[JsonPropertyName("required")]

View file

@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Google.Models;
public class GoogleVideoResult
{
public string Kind { get; set; }
public IList<VideoItem> Items { get; set; } = new List<VideoItem>();
public List<VideoItem> Items { get; set; } = new List<VideoItem>();
}
public class VideoItem
@ -25,6 +25,7 @@ public class VideoSnippet
{
public string Title { get; set; }
public string Description { get; set; }
public string ChannelId { get; set; }
public string ChannelTitle { get; set; }
public VideoThumbnails Thumbnails { get; set; }
}

View file

@ -19,4 +19,5 @@ public class YoutubeSettings
public string Endpoint { get; set; }
public string Part { get; set; }
public string RegionCode { get; set; }
public IList<string> Channels { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Http.Settings;
public class HttpSettings
{
public string BaseAddress { get; set; } = string.Empty;
public string Origin { get; set; } = string.Empty;
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Infrastructures.Enums;
public class LanguageType
{
public const string UNKNOWN = "Unknown";
public const string ENGLISH = "English";
public const string SPANISH = "Spanish";
public const string CHINESE = "Chinese";
}

View file

@ -5,5 +5,8 @@ public class StateConst
public const string EXPECTED_ACTION_AGENT = "expected_next_action_agent";
public const string EXPECTED_GOAL_AGENT = "expected_user_goal_agent";
public const string NEXT_ACTION_AGENT = "next_action_agent";
public const string NEXT_ACTION_REASON = "next_action_reason";
public const string USER_GOAL_AGENT = "user_goal_agent";
public const string LANGUAGE = "language";
}

View file

@ -6,6 +6,6 @@ public interface ILlmProviderService
{
LlmModelSetting GetSetting(string provider, string model);
List<string> GetProviders();
LlmModelSetting GetProviderModel(string provider, string id);
LlmModelSetting GetProviderModel(string provider, string id, bool? multiModal = null);
List<LlmModelSetting> GetProviderModels(string provider);
}

View file

@ -27,6 +27,11 @@ public class LlmModelSetting
public string Endpoint { get; set; }
public LlmModelType Type { get; set; } = LlmModelType.Chat;
/// <summary>
/// If true, allow sending images/vidoes to this model
/// </summary>
public bool MultiModal { get; set; }
/// <summary>
/// Prompt cost per 1K token
/// </summary>

View file

@ -1,17 +1,17 @@
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Messaging.Enums;
using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
using BotSharp.Abstraction.Messaging.Models.RichContent;
using System.Text.Json;
using System.Reflection;
namespace BotSharp.Core.Messaging;
namespace BotSharp.Abstraction.Messaging;
public static class MessageParser
public static class BotSharpMessageParser
{
public static IRichMessage? ParseRichMessage(JsonElement root, JsonSerializerOptions options)
{
IRichMessage? res = null;
Type? targetType = null;
JsonElement element;
var jsonText = root.GetRawText();
@ -20,47 +20,52 @@ public static class MessageParser
var richType = element.GetString();
if (richType == RichTypeEnum.ButtonTemplate)
{
res = JsonSerializer.Deserialize<ButtonTemplateMessage>(jsonText, options);
targetType = typeof(ButtonTemplateMessage);
}
else if (richType == RichTypeEnum.MultiSelectTemplate)
{
res = JsonSerializer.Deserialize<MultiSelectTemplateMessage>(jsonText, options);
targetType = typeof(MultiSelectTemplateMessage);
}
else if (richType == RichTypeEnum.QuickReply)
{
res = JsonSerializer.Deserialize<QuickReplyMessage>(jsonText, options);
targetType = typeof(QuickReplyMessage);
}
else if (richType == RichTypeEnum.CouponTemplate)
{
res = JsonSerializer.Deserialize<CouponTemplateMessage>(jsonText, options);
targetType = typeof(CouponTemplateMessage);
}
else if (richType == RichTypeEnum.Text)
{
res = JsonSerializer.Deserialize<TextMessage>(jsonText, options);
targetType = typeof(TextMessage);
}
else if (richType == RichTypeEnum.GenericTemplate)
{
if (root.TryGetProperty("element_type", out element))
{
var elementType = element.GetString();
if (elementType == typeof(GenericElement).Name)
var wrapperType = typeof(GenericTemplateMessage<>);
var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType);
if (wrapperType != null && genericType != null)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
}
else if (elementType == typeof(ButtonElement).Name)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<ButtonElement>>(jsonText, options);
targetType = wrapperType.MakeGenericType(genericType);
}
}
}
}
if (targetType != null)
{
res = JsonSerializer.Deserialize(jsonText, targetType, options) as IRichMessage;
}
return res;
}
public static ITemplateMessage? ParseTemplateMessage(JsonElement root, JsonSerializerOptions options)
{
ITemplateMessage? res = null;
Type? targetType = null;
JsonElement element;
var jsonText = root.GetRawText();
@ -69,37 +74,41 @@ public static class MessageParser
var templateType = element.GetString();
if (templateType == TemplateTypeEnum.Button)
{
res = JsonSerializer.Deserialize<ButtonTemplateMessage>(jsonText, options);
targetType = typeof(ButtonTemplateMessage);
}
else if (templateType == TemplateTypeEnum.MultiSelect)
{
res = JsonSerializer.Deserialize<MultiSelectTemplateMessage>(jsonText, options);
targetType = typeof(MultiSelectTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Coupon)
{
res = JsonSerializer.Deserialize<CouponTemplateMessage>(jsonText, options);
targetType = typeof(CouponTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Product)
{
res = JsonSerializer.Deserialize<ProductTemplateMessage>(jsonText, options);
targetType = typeof(ProductTemplateMessage);
}
else if (templateType == TemplateTypeEnum.Generic)
{
if (root.TryGetProperty("element_type", out element))
{
var elementType = element.GetString();
if (elementType == typeof(GenericElement).Name)
var wrapperType = typeof(GenericTemplateMessage<>);
var genericType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(x => x.Name == elementType);
if (wrapperType != null && genericType != null)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
}
else if (elementType == typeof(ButtonElement).Name)
{
res = JsonSerializer.Deserialize<GenericTemplateMessage<ButtonElement>>(jsonText, options);
targetType = wrapperType.MakeGenericType(genericType);
}
}
}
}
if (targetType != null)
{
res = JsonSerializer.Deserialize(jsonText, targetType, options) as ITemplateMessage;
}
return res;
}
}

View file

@ -12,6 +12,7 @@ public static class EditorTypeEnum
public const string DateTimePicker = "datetime-picker";
public const string DateTimeRangePicker = "datetime-range-picker";
public const string Email = "email";
public const string File = "file";
/// <summary>
/// Regex, set the expression in editor_attributes

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging;
public interface IRichMessage

View file

@ -1,4 +1,3 @@
using BotSharp.Core.Messaging;
using System.Text.Json;
namespace BotSharp.Abstraction.Messaging.JsonConverters;
@ -9,8 +8,7 @@ public class RichContentJsonConverter : JsonConverter<IRichMessage>
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var root = jsonDoc.RootElement;
var jsonText = root.GetRawText();
var res = MessageParser.ParseRichMessage(root, options);
var res = BotSharpMessageParser.ParseRichMessage(root, options);
return res;
}

View file

@ -1,4 +1,3 @@
using BotSharp.Core.Messaging;
using System.Text.Json;
namespace BotSharp.Abstraction.Messaging.JsonConverters;
@ -9,8 +8,7 @@ public class TemplateMessageJsonConverter : JsonConverter<ITemplateMessage>
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var root = jsonDoc.RootElement;
var jsonText = root.GetRawText();
var res = MessageParser.ParseTemplateMessage(root, options);
var res = BotSharpMessageParser.ParseTemplateMessage(root, options);
return res;
}

View file

@ -5,13 +5,30 @@ namespace BotSharp.Abstraction.Messaging.Models.RichContent;
/// </summary>
public class ElementButton
{
public string Type { get; set; }
public string Type { get; set; } = "web_url";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Url { get; set; }
public string? Url { get; set; }
public string Title { get; set; }
[Translate]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[Translate]
public string? Description { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Payload { get; set; }
public string? Payload { get; set; }
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
[JsonPropertyName("is_secondary")]
public bool IsSecondary { get; set; }
[JsonPropertyName("post_action_disclaimer")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[Translate]
public string? PostActionDisclaimer { get; set; }
}

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent;
public class QuickReplyMessage : IRichMessage

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent;
public class RichContent<T> where T : IRichMessage

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
/// <summary>
@ -11,33 +9,15 @@ public class ButtonTemplateMessage : IRichMessage, ITemplateMessage
public string RichType => RichTypeEnum.ButtonTemplate;
[JsonPropertyName("text")]
[Translate]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("template_type")]
public string TemplateType => TemplateTypeEnum.Button;
[JsonPropertyName("buttons")]
public ButtonElement[] Buttons { get; set; } = new ButtonElement[0];
public ElementButton[] Buttons { get; set; } = new ElementButton[0];
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
}
public class ButtonElement
{
/// <summary>
/// web_url, postback, phone_number
/// </summary>
public string Type { get; set; } = "web_url";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Url { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Payload { get; set; }
public string Title { get; set; } = string.Empty;
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
}

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
/// <summary>
@ -10,6 +8,7 @@ public class CouponTemplateMessage : IRichMessage, ITemplateMessage
{
[JsonPropertyName("rich_type")]
public string RichType => RichTypeEnum.CouponTemplate;
[JsonPropertyName("text")]
public string Text { get; set; }
public string Title { get; set; }

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
@ -8,6 +6,7 @@ public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
public string RichType => RichTypeEnum.GenericTemplate;
[JsonPropertyName("text")]
[Translate]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("template_type")]
@ -19,16 +18,24 @@ public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
[JsonPropertyName("is_popup")]
public bool IsPopup { get; set; }
[JsonPropertyName("element_type")]
public string ElementType => typeof(T).Name;
}
public class GenericElement
{
[Translate]
public string Title { get; set; }
[Translate]
public string Subtitle { get; set; }
[JsonPropertyName("image_url")]
public string ImageUrl { get; set; }
[JsonPropertyName("default_action")]
public ElementAction DefaultAction { get; set; }
public ElementButton[] Buttons { get; set; }

View file

@ -1,12 +1,12 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage
{
[JsonPropertyName("rich_type")]
public string RichType => RichTypeEnum.MultiSelectTemplate;
[JsonPropertyName("text")]
[Translate]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("template_type")]
@ -21,6 +21,7 @@ public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage
public class OptionElement
{
[Translate]
public string Title { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string? Payload { get; set; }

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent.Template;
public class ProductTemplateMessage : IRichMessage, ITemplateMessage
@ -8,6 +6,7 @@ public class ProductTemplateMessage : IRichMessage, ITemplateMessage
public string RichType => RichTypeEnum.GenericTemplate;
[JsonPropertyName("text")]
[Translate]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("template_type")]

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Messaging.Enums;
namespace BotSharp.Abstraction.Messaging.Models.RichContent;
public class TextMessage : IRichMessage
@ -7,6 +5,7 @@ public class TextMessage : IRichMessage
[JsonPropertyName("rich_type")]
public string RichType => RichTypeEnum.Text;
[Translate]
public string Text { get; set; } = string.Empty;
public TextMessage(string text)

View file

@ -19,6 +19,9 @@ public class PluginMenuDef
[JsonIgnore]
public int Weight { get; set; }
[JsonIgnore]
public List<string>? Roles { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<PluginMenuDef>? SubMenu { get; set; }

View file

@ -7,6 +7,7 @@ public class BotSharpDatabaseSettings : DatabaseBasicSettings
public string BotSharpMongoDb { get; set; }
public string TablePrefix { get; set; }
public DbConnectionSetting BotSharp { get; set; }
public string Redis { get; set; }
}
public class DatabaseBasicSettings

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Repositories.Enums;
public static class RepositoryEnum
{
public const string FileRepository = nameof(FileRepository);
public const string MongoRepository = nameof(MongoRepository);
}

View file

@ -1,7 +1,6 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Repositories.Models;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Users.Models;
@ -18,10 +17,11 @@ public interface IBotSharpRepository
#endregion
#region User
User? GetUserByEmail(string email);
User? GetUserById(string id);
User? GetUserByUserName(string userName);
void CreateUser(User user);
User? GetUserByEmail(string email) => throw new NotImplementedException();
User? GetUserById(string id) => throw new NotImplementedException();
User? GetUserByUserName(string userName) => throw new NotImplementedException();
void CreateUser(User user) => throw new NotImplementedException();
void UpdateUserVerified(string userId) => throw new NotImplementedException();
#endregion
#region Agent
@ -32,8 +32,10 @@ public interface IBotSharpRepository
void BulkInsertAgents(List<Agent> agents);
void BulkInsertUserAgents(List<UserAgent> userAgents);
bool DeleteAgents();
bool DeleteAgent(string agentId);
List<string> GetAgentResponses(string agentId, string prefix, string intent);
string GetAgentTemplate(string agentId, string templateName);
bool PatchAgentTemplate(string agentId, AgentTemplate template);
#endregion
#region Agent Task
@ -42,7 +44,7 @@ public interface IBotSharpRepository
void InsertAgentTask(AgentTask task);
void BulkInsertAgentTasks(List<AgentTask> tasks);
void UpdateAgentTask(AgentTask task, AgentTaskField field);
bool DeleteAgentTask(string agentId, string taskId);
bool DeleteAgentTask(string agentId, List<string> taskIds);
bool DeleteAgentTasks();
#endregion
@ -50,7 +52,6 @@ public interface IBotSharpRepository
void CreateNewConversation(Conversation conversation);
bool DeleteConversations(IEnumerable<string> conversationIds);
List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationDialogElements(string conversationId, List<DialogContentUpdateModel> updateElements);
void AppendConversationDialogs(string conversationId, List<DialogElement> dialogs);
ConversationState GetConversationStates(string conversationId);
void UpdateConversationStates(string conversationId, List<StateKeyValue> states);
@ -58,11 +59,11 @@ public interface IBotSharpRepository
Conversation GetConversation(string conversationId);
PagedItems<Conversation> GetConversations(ConversationFilter filter);
void UpdateConversationTitle(string conversationId, string title);
void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint);
DateTime GetConversationBreakpoint(string conversationId);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);
ConversationBreakpoint? GetConversationBreakpoint(string conversationId);
List<Conversation> GetLastConversations();
List<string> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false);
IEnumerable<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false);
#endregion
#region Execution Log

View file

@ -3,7 +3,8 @@ namespace BotSharp.Abstraction.Routing;
public interface IRoutingContext
{
string GetCurrentAgentId();
string PreviousAgentId();
string FirstGoalAgentId();
bool ContainsAgentId(string agentId);
string OriginAgentId { get; }
string ConversationId { get; }
string MessageId { get; }
@ -13,6 +14,7 @@ public interface IRoutingContext
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 Empty(string? reason = null);
}

View file

@ -16,5 +16,5 @@ public interface IRoutingHandler
void SetDialogs(List<RoleDialogModel> dialogs);
Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message);
Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message, Func<RoleDialogModel, Task> onFunctionExecuting);
}

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.Abstraction.Routing;
public interface IRoutingService
@ -30,9 +28,9 @@ public interface IRoutingService
List<RoutingHandlerDef> GetHandlers(Agent router);
void ResetRecursiveCounter();
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs);
Task<bool> InvokeFunction(string name, RoleDialogModel message);
Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs);
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs, Func<RoleDialogModel, Task> onFunctionExecuting);
Task<bool> InvokeFunction(string name, RoleDialogModel messages, Func<RoleDialogModel, Task>? onFunctionExecuting = null);
Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs, Func<RoleDialogModel, Task> onFunctionExecuting);
/// <summary>
/// Talk to a specific Agent directly, bypassing the Router
@ -44,5 +42,5 @@ public interface IRoutingService
Task<string> GetConversationContent(List<RoleDialogModel> dialogs, int maxDialogCount = 50);
bool HasMissingRequiredField(RoleDialogModel message, out string agentId);
(bool, string) HasMissingRequiredField(RoleDialogModel message, out string agentId);
}

View file

@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Routing.Models;
public class RoutingArgs
{
[JsonPropertyName("function")]
public string Function { get; set; }
public string Function { get; set; } = string.Empty;
/// <summary>
/// The reason why you select this function or agent
@ -19,30 +19,36 @@ public class RoutingArgs
[JsonPropertyName("conversation_end")]
public bool ConversationEnd { get; set; }
[JsonPropertyName("task_completed")]
public bool TaskCompleted { get; set; }
[JsonPropertyName("is_new_task")]
public bool IsNewTask { get; set; }
/// <summary>
/// The content of replying to user
/// </summary>
[JsonPropertyName("response")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Response { get; set; }
public string Response { get; set; } = string.Empty;
/// <summary>
/// Agent for next action based on user latest response
/// </summary>
[JsonPropertyName("next_action_agent")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string AgentName { get; set; }
public string AgentName { get; set; } = string.Empty;
/// <summary>
/// Agent who can achieve user original goal
/// </summary>
[JsonPropertyName("user_goal_agent")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string OriginalAgent { get; set; }
public string OriginalAgent { get; set; } = string.Empty;
[JsonPropertyName("user_goal_description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string UserGoal { get; set; }
public string UserGoal { get; set; } = string.Empty;
public override string ToString()
{

View file

@ -7,5 +7,6 @@ public interface IExecutor
Task<RoleDialogModel> Execute(IRoutingService routing,
FunctionCallFromLlm inst,
RoleDialogModel message,
List<RoleDialogModel> dialogs);
List<RoleDialogModel> dialogs,
Func<RoleDialogModel, Task> onFunctionExecuting);
}

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Translation.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public class TranslateAttribute : Attribute
{
public TranslateAttribute()
{
}
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Translation;
public interface ITranslationService
{
Task<T> Translate<T>(Agent router, string messageId, T data, string language = "Spanish", bool clone = true) where T : class;
}

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Translation.Models;
public class TranslationInput
{
[JsonPropertyName("id")]
public int Id { get; set; } = -1;
[JsonPropertyName("text")]
public string Text { get; set; } = null!;
}

View file

@ -0,0 +1,13 @@
namespace BotSharp.Abstraction.Translation.Models;
public class TranslationOutput
{
[JsonPropertyName("input_lang")]
public string InputLanguage { get; set; } = null!;
[JsonPropertyName("output_lang")]
public string OutputLanguage { get; set; } = LanguageType.ENGLISH;
[JsonPropertyName("texts")]
public TranslationInput[] Texts { get; set; } = Array.Empty<TranslationInput>();
}

View file

@ -8,4 +8,5 @@ public interface IAuthenticationHook
Task<User> Authenticate(string id, string password);
void AddClaims(List<Claim> claims);
void BeforeSending(Token token);
Task UserCreated(User user);
}

View file

@ -8,4 +8,5 @@ public interface IUserIdentity
string FirstName { get; }
string LastName { get; }
string FullName { get; }
string? UserLanguage { get; }
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Users.Models;
using BotSharp.OpenAPI.ViewModels.Users;
namespace BotSharp.Abstraction.Users;
@ -6,6 +7,9 @@ public interface IUserService
{
Task<User> GetUser(string id);
Task<User> CreateUser(User user);
Task<Token> GetToken(string authorization);
Task<Token> ActiveUser(UserActivationModel model);
Task<Token?> GetToken(string authorization);
Task<User> GetMyProfile();
Task<bool> VerifyUserNameExisting(string userName);
Task<bool> VerifyEmailExisting(string email);
}

View file

@ -14,6 +14,8 @@ public class User
public string Source { get; set; } = "internal";
public string? ExternalId { get; set; }
public string Role { get; set; } = UserRole.Client;
public string? VerificationCode { get; set; }
public bool Verified { get; set; }
public DateTime UpdatedTime { get; set; } = DateTime.UtcNow;
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
}

View file

@ -0,0 +1,7 @@
namespace BotSharp.OpenAPI.ViewModels.Users;
public class UserActivationModel
{
public string UserName { get; set; }
public string VerificationCode { get; set; }
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Users.Settings;
public class AccountSetting
{
/// <summary>
/// Whether to enable verification code to verify the authenticity of new users
/// </summary>
public bool NewUserVerification { get; set; }
}

View file

@ -14,3 +14,6 @@ global using BotSharp.Abstraction.Models;
global using BotSharp.Abstraction.Routing.Models;
global using BotSharp.Abstraction.Routing.Planning;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Abstraction.Translation.Attributes;
global using BotSharp.Abstraction.Messaging.Enums;
global using BotSharp.Abstraction.Files.Models;

View file

@ -13,19 +13,26 @@ public class Pagination
public int Size
{
get
get
{
if (_size <= 0) return 20;
if (_size > 100) return 100;
return _size;
}
set
return _size > 0 ? _size : 1;
}
set
{
_size = value;
}
}
}
/// <summary>
/// Sort by field
/// </summary>
public string? Sort { get; set; }
/// <summary>
/// Sort order: asc or desc
/// </summary>
public string Order { get; set; } = "asc";
public int Offset
{
get { return (Page - 1) * Size; }

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Users.Enums;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Core.Agents;
@ -14,6 +15,13 @@ public class AgentPlugin : IBotSharpPlugin
public SettingsMeta Settings =>
new SettingsMeta("Agent");
public string[] AgentIds => new string[]
{
"01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a",
"01e2fc5c-2c89-4ec7-8470-7688608b496c",
"01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b"
};
public object GetNewSettingsInstance() =>
new AgentSettings();
@ -36,8 +44,8 @@ public class AgentPlugin : IBotSharpPlugin
{
SubMenu = new List<PluginMenuDef>
{
new PluginMenuDef("Routing", link: "page/agent/router"), // icon: "bx bx-map-pin"
new PluginMenuDef("Evaluating", link: "page/agent/evaluator"), // icon: "bx bx-task"
new PluginMenuDef("Routing", link: "page/agent/router") { Roles = new List<string> { UserRole.Admin } }, // icon: "bx bx-map-pin"
new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List<string> { UserRole.Admin } }, // icon: "bx bx-task"
new PluginMenuDef("Agents", link: "page/agent"), // icon: "bx bx-bot"
}
});

View file

@ -1,8 +1,4 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Users.Models;
using System.IO;
using System.Text.RegularExpressions;
@ -26,32 +22,13 @@ public partial class AgentService
var dbSettings = _services.GetRequiredService<BotSharpDatabaseSettings>();
var agentSettings = _services.GetRequiredService<AgentSettings>();
var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir);
var foundAgent = FetchAgentFileByName(agent.Name, filePath);
if (foundAgent != null)
{
agentRecord.SetId(foundAgent.Id)
.SetName(foundAgent.Name)
.SetDescription(foundAgent.Description)
.SetIsPublic(foundAgent.IsPublic)
.SetDisabled(foundAgent.Disabled)
.SetAgentType(foundAgent.Type)
.SetProfiles(foundAgent.Profiles)
.SetRoutingRules(foundAgent.RoutingRules)
.SetInstruction(foundAgent.Instruction)
.SetTemplates(foundAgent.Templates)
.SetFunctions(foundAgent.Functions)
.SetResponses(foundAgent.Responses)
.SetLlmConfig(foundAgent.LlmConfig);
}
var user = _db.GetUserById(_user.Id);
var userAgentRecord = new UserAgent
{
Id = Guid.NewGuid().ToString(),
UserId = user.Id,
AgentId = foundAgent?.Id ?? agentRecord.Id,
AgentId = agentRecord.Id,
Editable = false,
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
@ -65,7 +42,7 @@ public partial class AgentService
Utilities.ClearCache();
return agentRecord;
return await Task.FromResult(agentRecord);
}
private Agent FetchAgentFileByName(string agentName, string filePath)

View file

@ -1,9 +1,20 @@
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
public async Task<bool> DeleteAgent(string id)
{
throw new NotImplementedException();
var user = _db.GetUserById(_user.Id);
var agent = _db.GetAgentsByUser(_user.Id).FirstOrDefault(x => x.Id.IsEqualTo(id));
if (user?.Role != UserRole.Admin && agent == null)
{
return false;
}
var deleted = _db.DeleteAgent(id);
return await Task.FromResult(deleted);
}
}

View file

@ -1,55 +1,89 @@
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Repositories.Enums;
using System.IO;
namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
public async Task RefreshAgents()
public async Task<string> RefreshAgents()
{
var isAgentDeleted = _db.DeleteAgents();
var isTaskDeleted = _db.DeleteAgentTasks();
if (!isAgentDeleted) return;
string refreshResult;
var dbSettings = _services.GetRequiredService<BotSharpDatabaseSettings>();
if (dbSettings.Default == RepositoryEnum.FileRepository)
{
refreshResult = $"Invalid database repository setting: {dbSettings.Default}";
_logger.LogWarning(refreshResult);
return refreshResult;
}
var agentDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
dbSettings.FileRepository,
_agentSettings.DataDir);
if (!Directory.Exists(agentDir))
{
refreshResult = $"Cannot find the directory: {agentDir}";
return refreshResult;
}
var user = _db.GetUserById(_user.Id);
var agents = new List<Agent>();
var userAgents = new List<UserAgent>();
var agentTasks = new List<AgentTask>();
var refreshedAgents = new List<string>();
foreach (var dir in Directory.GetDirectories(agentDir))
{
var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json"));
var agent = JsonSerializer.Deserialize<Agent>(agentJson, _options);
if (agent == null) continue;
try
{
var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json"));
var agent = JsonSerializer.Deserialize<Agent>(agentJson, _options);
if (agent == null)
{
_logger.LogError($"Cannot find agent in file directory: {dir}");
continue;
}
var functions = FetchFunctionsFromFile(dir);
var instruction = FetchInstructionFromFile(dir);
var responses = FetchResponsesFromFile(dir);
var templates = FetchTemplatesFromFile(dir);
var samples = FetchSamplesFromFile(dir);
agent.SetInstruction(instruction)
.SetTemplates(templates)
.SetFunctions(functions)
.SetResponses(responses)
.SetSamples(samples);
agents.Add(agent);
var functions = FetchFunctionsFromFile(dir);
var instruction = FetchInstructionFromFile(dir);
var responses = FetchResponsesFromFile(dir);
var templates = FetchTemplatesFromFile(dir);
var samples = FetchSamplesFromFile(dir);
agent.SetInstruction(instruction)
.SetTemplates(templates)
.SetFunctions(functions)
.SetResponses(responses)
.SetSamples(samples);
var userAgent = BuildUserAgent(agent.Id, user.Id);
userAgents.Add(userAgent);
var userAgent = BuildUserAgent(agent.Id, user.Id);
var tasks = FetchTasksFromFile(dir);
var tasks = FetchTasksFromFile(dir);
agentTasks.AddRange(tasks);
var isAgentDeleted = _db.DeleteAgent(agent.Id);
if (isAgentDeleted)
{
await Task.Delay(100);
_db.BulkInsertAgents(new List<Agent> { agent });
_db.BulkInsertUserAgents(new List<UserAgent> { userAgent });
_db.BulkInsertAgentTasks(tasks);
refreshedAgents.Add(agent.Name);
_logger.LogInformation($"Agent {agent.Name} has been migrated.");
}
}
catch (Exception ex)
{
_logger.LogError($"Failed to migrate agent in file directory: {dir}\r\nError: {ex.Message}");
}
}
_db.BulkInsertAgents(agents);
_db.BulkInsertUserAgents(userAgents);
_db.BulkInsertAgentTasks(agentTasks);
if (!refreshedAgents.IsNullOrEmpty())
{
Utilities.ClearCache();
refreshResult = $"Agents are migrated!\r\n{string.Join("\r\n", refreshedAgents)}";
}
else
{
refreshResult = "No agent gets refreshed!";
}
Utilities.ClearCache();
_logger.LogInformation(refreshResult);
return refreshResult;
}
}

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.Templating;
using Newtonsoft.Json.Linq;
namespace BotSharp.Core.Agents.Services;
@ -32,6 +33,64 @@ public partial class AgentService
return true;
}
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def)
{
var parameterDef = def?.Parameters;
var propertyDef = parameterDef?.Properties;
if (propertyDef == null) return null;
var visibleExpress = "visibility_expression";
var root = propertyDef.RootElement;
var iterator = root.EnumerateObject();
var visibleProps = new List<string>();
while (iterator.MoveNext())
{
var prop = iterator.Current;
var name = prop.Name;
var node = prop.Value;
var matched = true;
if (node.TryGetProperty(visibleExpress, out var element))
{
var expression = element.GetString();
var render = _services.GetRequiredService<ITemplateRender>();
var result = render.Render(expression, new Dictionary<string, object>
{
{ "states", agent.TemplateDict }
});
matched = result == "visible";
}
if (matched)
{
visibleProps.Add(name);
}
}
var rootObject = JObject.Parse(root.GetRawText());
var clonedRoot = rootObject.DeepClone() as JObject;
var required = parameterDef?.Required ?? new List<string>();
foreach (var property in rootObject.Properties())
{
if (visibleProps.Contains(property.Name))
{
var value = clonedRoot.GetValue(property.Name) as JObject;
if (value != null && value.ContainsKey(visibleExpress))
{
value.Remove(visibleExpress);
}
}
else
{
clonedRoot.Remove(property.Name);
required.Remove(property.Name);
}
}
parameterDef.Properties = JsonSerializer.Deserialize<JsonDocument>(clonedRoot.ToString());
parameterDef.Required = required;
return parameterDef; ;
}
public string RenderedTemplate(Agent agent, string templateName)
{
// render liquid template

View file

@ -1,7 +1,6 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Repositories.Enums;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Users.Enums;
using System.IO;
namespace BotSharp.Core.Agents.Services;
@ -10,6 +9,10 @@ public partial class AgentService
{
public async Task UpdateAgent(Agent agent, AgentField updateField)
{
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
if (user?.Role != UserRole.Admin) return;
if (agent == null || string.IsNullOrEmpty(agent.Id)) return;
var record = _db.GetAgent(agent.Id);
@ -39,21 +42,41 @@ public partial class AgentService
await Task.CompletedTask;
}
public async Task UpdateAgentFromFile(string id)
public async Task<string> UpdateAgentFromFile(string id)
{
var agent = _db.GetAgent(id);
if (agent == null) return;
string updateResult;
var dbSettings = _services.GetRequiredService<BotSharpDatabaseSettings>();
var agentSettings = _services.GetRequiredService<AgentSettings>();
if (dbSettings.Default == RepositoryEnum.FileRepository)
{
updateResult = $"Invalid database repository setting: {dbSettings.Default}";
_logger.LogWarning(updateResult);
return updateResult;
}
var agent = _db.GetAgent(id);
if (agent == null)
{
updateResult = $"Cannot find agent ${id}";
_logger.LogError(updateResult);
return updateResult;
}
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
dbSettings.FileRepository,
agentSettings.DataDir);
var clonedAgent = Agent.Clone(agent);
var foundAgent = FetchAgentFileById(agent.Id, filePath);
if (foundAgent != null)
if (foundAgent == null)
{
updateResult = $"Cannot find agent {agent.Name} in file directory: {filePath}";
_logger.LogError(updateResult);
return updateResult;
}
try
{
clonedAgent.SetId(foundAgent.Id)
.SetName(foundAgent.Name)
@ -71,15 +94,77 @@ public partial class AgentService
.SetLlmConfig(foundAgent.LlmConfig);
_db.UpdateAgent(clonedAgent, AgentField.All);
Utilities.ClearCache();
}
await Task.CompletedTask;
updateResult = $"Agent {agent.Name} has been migrated!";
_logger.LogInformation(updateResult);
return updateResult;
}
catch (Exception ex)
{
updateResult = $"Failed to migrate agent {agent.Name} in file directory {filePath}.\r\nError: {ex.Message}";
_logger.LogError(updateResult);
return updateResult;
}
}
private Agent FetchAgentFileById(string agentId, string filePath)
public async Task<string> PatchAgentTemplate(Agent agent)
{
var patchResult = string.Empty;
if (agent == null || agent.Templates.IsNullOrEmpty())
{
patchResult = $"Null agent instance or empty input templates";
_logger.LogWarning(patchResult);
return patchResult;
}
var record = _db.GetAgent(agent.Id);
if (record == null)
{
patchResult = $"Cannot find agent {agent.Id}";
_logger.LogWarning(patchResult);
return patchResult;
}
var successTemplates = new List<string>();
var failTemplates = new List<string>();
foreach (var template in agent.Templates)
{
if (template == null) continue;
var result = _db.PatchAgentTemplate(agent.Id, template);
if (result)
{
successTemplates.Add(template.Name);
_logger.LogInformation($"Template {template.Name} is updated successfully!");
}
else
{
failTemplates.Add(template.Name);
_logger.LogWarning($"Template {template.Name} is failed to be updated!");
}
}
Utilities.ClearCache();
if (!successTemplates.IsNullOrEmpty())
{
patchResult += $"Success templates:\n{string.Join('\n', successTemplates)}\n\n";
}
if (!failTemplates.IsNullOrEmpty())
{
patchResult += $"Failed templates:\n{string.Join('\n', failTemplates)}";
}
return patchResult;
}
private Agent? FetchAgentFileById(string agentId, string filePath)
{
if (!Directory.Exists(filePath)) return null;
foreach (var dir in Directory.GetDirectories(filePath))
{
var agentJson = File.ReadAllText(Path.Combine(dir, "agent.json"));

View file

@ -47,4 +47,10 @@ public partial class AgentService : IAgentService
}
return dir;
}
public List<Agent> GetAgentsByUser(string userId)
{
var agents = _db.GetAgentsByUser(userId);
return agents;
}
}

View file

@ -46,11 +46,17 @@
</PropertyGroup>
<ItemGroup>
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json" />
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\instruction.liquid" />
<None Remove="data\agents\01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d\agent.json" />
<None Remove="data\agents\01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d\instruction.liquid" />
<None Remove="data\agents\01e2fc5c-2c89-4ec7-8470-7688608b496c\agent.json" />
<None Remove="data\agents\01e2fc5c-2c89-4ec7-8470-7688608b496c\instruction.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\agent.json" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\instruction.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\.welcome.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\conversation.summary.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.hf.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.naive.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.sequential.get_remaining_task.liquid" />
@ -59,6 +65,7 @@
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.plan.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.task.liquid" />
<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\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instruction.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.executor.liquid" />
@ -67,6 +74,21 @@
</ItemGroup>
<ItemGroup>
<Content Include="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-0af7-49e6-ad7a-a760bd12dc4d\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01e2fc5c-2c89-4ec7-8470-7688608b496c\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -106,6 +128,9 @@
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\.welcome.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -118,6 +143,9 @@
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.reviewer.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\conversation.summary.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\plugins\config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -133,9 +161,11 @@
<ItemGroup>
<PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="Colorful.Console" Version="1.2.15" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="6.3.0" />
<PackageReference Include="Fluid.Core" Version="2.7.0" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.2.1" />
<PackageReference Include="Fluid.Core" Version="2.8.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Nanoid" Version="3.0.0" />
<PackageReference Include="RedLock.net" Version="2.3.2" />
</ItemGroup>
<ItemGroup>

View file

@ -5,6 +5,7 @@ using BotSharp.Core.Plugins;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Messaging.JsonConverters;
using BotSharp.Abstraction.Users.Settings;
namespace BotSharp.Core;
@ -14,9 +15,11 @@ public static class BotSharpCoreExtensions
{
services.AddScoped<ISettingService, SettingService>();
services.AddScoped<IUserService, UserService>();
services.AddSingleton<DistributedLocker>();
RegisterPlugins(services, config);
ConfigureBotSharpOptions(services, configOptions);
return services;
}
@ -82,6 +85,10 @@ public static class BotSharpCoreExtensions
return settingService.Bind<PluginSettings>("PluginLoader");
});
var accountSettings = new AccountSetting();
config.Bind("Account", accountSettings);
services.AddScoped(x => accountSettings);
var loader = new PluginLoader(services, config, pluginSettings);
loader.Load(assembly =>
{

View file

@ -1,13 +1,17 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Google.Settings;
using BotSharp.Abstraction.Instructs;
using BotSharp.Abstraction.Messaging;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Routing.Planning;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Templating;
using BotSharp.Core.Files;
using BotSharp.Core.Instructs;
using BotSharp.Core.Messaging;
using BotSharp.Core.Routing.Planning;
using BotSharp.Core.Templating;
using BotSharp.Core.Translation;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Core.Conversations;
@ -31,10 +35,17 @@ public class ConversationPlugin : IBotSharpPlugin
return settingService.Bind<ConversationSetting>("Conversation");
});
services.AddScoped(provider =>
{
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<GoogleApiSettings>("GoogleApi");
});
services.AddScoped<IConversationStorage, ConversationStorage>();
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IConversationStateService, ConversationStateService>();
services.AddScoped<IConversationAttachmentService, ConversationAttachmentService>();
services.AddScoped<IBotSharpFileService, BotSharpFileService>();
services.AddScoped<ITranslationService, TranslationService>();
// Rich content messaging
services.AddScoped<IRichContentService, RichContentService>();

View file

@ -1,28 +0,0 @@
using BotSharp.Abstraction.Repositories;
using System.IO;
namespace BotSharp.Core.Conversations.Services;
public class ConversationAttachmentService : IConversationAttachmentService
{
private readonly BotSharpDatabaseSettings _dbSettings;
private readonly IServiceProvider _services;
public ConversationAttachmentService(
BotSharpDatabaseSettings dbSettings,
IServiceProvider services)
{
_dbSettings = dbSettings;
_services = services;
}
public string GetDirectory(string conversationId)
{
var dir = Path.Combine(_dbSettings.FileRepository, "conversations", conversationId, "attachments");
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
return dir;
}
}

View file

@ -27,7 +27,6 @@ public partial class ConversationService
#endif
message.CurrentAgentId = agent.Id;
message.CreatedAt = DateTime.UtcNow;
if (string.IsNullOrEmpty(message.SenderId))
{
message.SenderId = _user.Id;
@ -47,6 +46,17 @@ public partial class ConversationService
routing.Context.SetMessageId(_conversationId, message.MessageId);
routing.Context.Push(agent.Id);
// Save message files
var fileService = _services.GetRequiredService<IBotSharpFileService>();
fileService.SaveMessageFiles(_conversationId, message.MessageId, message.Files);
message.Files?.Clear();
// Save payload
if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload))
{
message.Payload = replyMessage.Payload;
}
// Before chat completion hook
foreach (var hook in hooks)
{
@ -71,19 +81,13 @@ public partial class ConversationService
}
}
// Persist to storage
_storage.Append(_conversationId, message);
// Add to thread
dialogs.Add(RoleDialogModel.From(message));
if (!stopCompletion)
{
// Routing with reasoning
var settings = _services.GetRequiredService<RoutingSettings>();
response = agent.Type == AgentType.Routing ?
await routing.InstructLoop(message, dialogs) :
await routing.InstructLoop(message, dialogs, onFunctionExecuting) :
await routing.InstructDirect(agent, message);
routing.ResetRecursiveCounter();
@ -136,7 +140,7 @@ public partial class ConversationService
response.RichContent is RichContent<IRichMessage> template &&
string.IsNullOrEmpty(template.Message.Text))
{
template.Message.Text = response.Content;
template.Message.Text = response.SecondaryContent ?? response.Content;
}
// Only read content from RichContent for UI rendering. When richContent is null, create a basic text message for richContent.
@ -144,30 +148,40 @@ public partial class ConversationService
response.RichContent = response.RichContent ?? new RichContent<IRichMessage>
{
Recipient = new Recipient { Id = state.GetConversationId() },
Message = new TextMessage(response.Content)
Message = new TextMessage(response.SecondaryContent ?? response.Content)
};
var hooks = _services.GetServices<IConversationHook>().ToList();
// Patch return function name
if (response.PostbackFunctionName != null)
{
response.FunctionName = response.PostbackFunctionName;
}
if (response.Instruction != null)
{
var conversation = _services.GetRequiredService<IConversationService>();
var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason);
// Emit conversation task completed hook
if (response.Instruction.TaskCompleted)
{
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnTaskCompleted(response)
);
}
// Emit conversation ending hook
if (response.Instruction.ConversationEnd)
{
foreach (var hook in hooks)
{
await hook.OnConversationEnding(response);
}
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnConversationEnding(response)
);
}
}
foreach (var hook in hooks)
{
await hook.OnResponseGenerated(response);
}
await HookEmitter.Emit<IConversationHook>(_services, async hook =>
await hook.OnResponseGenerated(response)
);
await onResponseReceived(response);

View file

@ -0,0 +1,119 @@
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Templating;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService
{
public async Task<string> GetConversationSummary(IEnumerable<string> conversationIds)
{
if (conversationIds.IsNullOrEmpty()) return string.Empty;
var routing = _services.GetRequiredService<IRoutingService>();
var agentService = _services.GetRequiredService<IAgentService>();
var contents = new List<string>();
foreach ( var conversationId in conversationIds)
{
if (string.IsNullOrEmpty(conversationId)) continue;
var dialogs = _storage.GetDialogs(conversationId);
if (dialogs.IsNullOrEmpty()) continue;
var content = GetConversationContent(dialogs);
if (string.IsNullOrWhiteSpace(content)) continue;
contents.Add(content);
}
if (contents.IsNullOrEmpty()) return string.Empty;
var router = await agentService.LoadAgent(AIAssistant);
var prompt = GetPrompt(router, contents);
var summary = await Summarize(router, prompt);
return summary;
}
private string GetPrompt(Agent agent, List<string> contents)
{
var template = agent.Templates.First(x => x.Name == "conversation.summary").Content;
var render = _services.GetRequiredService<ITemplateRender>();
var texts = new List<string>();
for (int i = 0; i < contents.Count; i++)
{
texts.Add($"{contents[i]}");
}
return render.Render(template, new Dictionary<string, object>
{
{ "texts", texts }
});
}
private async Task<string> Summarize(Agent agent, string prompt)
{
var provider = "openai";
string? model;
var providerService = _services.GetRequiredService<ILlmProviderService>();
var modelSettings = providerService.GetProviderModels(provider);
var modelSetting = modelSettings.FirstOrDefault(x => x.Name.IsEqualTo("gpt4-turbo") || x.Name.IsEqualTo("gpt-4o"));
if (modelSetting != null)
{
model = modelSetting.Name;
}
else
{
provider = agent?.LlmConfig?.Provider;
model = agent?.LlmConfig?.Model;
if (provider == null || model == null)
{
var agentSettings = _services.GetRequiredService<AgentSettings>();
provider = agentSettings.LlmConfig.Provider;
model = agentSettings.LlmConfig.Model;
}
}
var chatCompletion = CompletionProvider.GetChatCompletion(_services, provider, model);
var response = await chatCompletion.GetChatCompletions(new Agent
{
Id = agent.Id,
Name = agent.Name,
Instruction = prompt
}, new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, "Please summarize the conversations.")
});
return response.Content;
}
private string GetConversationContent(List<RoleDialogModel> dialogs, int maxDialogCount = 50)
{
var conversation = "";
foreach (var dialog in dialogs.TakeLast(maxDialogCount))
{
var role = dialog.Role;
if (role == AgentRole.Function) continue;
if (role != AgentRole.User)
{
role = AgentRole.Assistant;
}
conversation += $"{role}: {dialog.Payload ?? dialog.Content}\r\n";
}
if (string.IsNullOrEmpty(conversation))
{
return string.Empty;
}
return conversation + "\r\n";
}
}

View file

@ -2,15 +2,19 @@ namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService
{
public async Task<bool> TruncateConversation(string conversationId, string messageId)
public async Task<bool> TruncateConversation(string conversationId, string messageId, string? newMessageId = null)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var isSaved = db.TruncateConversation(conversationId, messageId, true);
var fileService = _services.GetRequiredService<IBotSharpFileService>();
var deleteMessageIds = db.TruncateConversation(conversationId, messageId, cleanLog: true);
fileService.DeleteMessageFiles(conversationId, deleteMessageIds, messageId, newMessageId);
var hooks = _services.GetServices<IConversationHook>().ToList();
foreach (var hook in hooks)
{
await hook.OnMessageDeleted(conversationId, messageId);
}
return await Task.FromResult(isSaved);
return await Task.FromResult(true);
}
}

View file

@ -1,19 +1,34 @@
using BotSharp.Abstraction.Infrastructures.Enums;
namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService : IConversationService
{
public async Task UpdateBreakpoint(bool resetStates = false)
public async Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var messageId = routingCtx.MessageId;
db.UpdateConversationBreakpoint(_conversationId, messageId, DateTime.UtcNow);
db.UpdateConversationBreakpoint(_conversationId, new ConversationBreakpoint
{
MessageId = messageId,
Breakpoint = DateTime.UtcNow,
Reason = reason
});
// Reset states
if (resetStates)
{
var states = _services.GetRequiredService<IConversationStateService>();
states.CleanStates();
// keep language state
if (excludedStates == null) excludedStates = new string[] { };
if (!excludedStates.Contains(StateConst.LANGUAGE))
{
excludedStates = excludedStates.Append(StateConst.LANGUAGE).ToArray();
}
states.CleanStates(excludedStates);
}
var hooks = _services.GetServices<IConversationHook>()

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Models;
namespace BotSharp.Core.Conversations.Services;
@ -11,6 +12,8 @@ public partial class ConversationService : IConversationService
private readonly IConversationStorage _storage;
private readonly IConversationStateService _state;
private string _conversationId;
private const string AIAssistant = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a";
public string ConversationId => _conversationId;
public IConversationStateService States => _state;
@ -34,7 +37,9 @@ public partial class ConversationService : IConversationService
public async Task<bool> DeleteConversations(IEnumerable<string> ids)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var fileService = _services.GetRequiredService<IBotSharpFileService>();
var isDeleted = db.DeleteConversations(ids);
fileService.DeleteConversationFiles(ids);
return await Task.FromResult(isDeleted);
}
@ -114,7 +119,14 @@ public partial class ConversationService : IConversationService
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var breakpoint = db.GetConversationBreakpoint(_conversationId);
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint).ToList();
if (breakpoint != null)
{
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList();
if (!string.IsNullOrEmpty(breakpoint.Reason))
{
dialogs.Insert(0, new RoleDialogModel(AgentRole.User, breakpoint.Reason));
}
}
}
return dialogs
@ -126,6 +138,6 @@ public partial class ConversationService : IConversationService
{
_conversationId = conversationId;
_state.Load(_conversationId);
states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External));
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.Core.Conversations.Services;
@ -9,9 +10,16 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
private ConversationState _states;
private string _conversationId;
private readonly IBotSharpRepository _db;
private string _conversationId;
/// <summary>
/// States in the current round of conversation
/// </summary>
private ConversationState _curStates;
/// <summary>
/// States in the previous rounds of conversation
/// </summary>
private ConversationState _historyStates;
public ConversationStateService(ILogger<ConversationStateService> logger,
IServiceProvider services,
@ -20,7 +28,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
_logger = logger;
_services = services;
_db = db;
_states = new ConversationState();
_curStates = new ConversationState();
_historyStates = new ConversationState();
}
public string GetConversationId() => _conversationId;
@ -33,7 +42,8 @@ public class ConversationStateService : IConversationStateService, IDisposable
/// <param name="value"></param>
/// <param name="isNeedVersion">whether the state is related to message or not</param>
/// <returns></returns>
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1)
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true,
int activeRounds = -1, string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false)
{
if (value == null)
{
@ -46,34 +56,41 @@ public class ConversationStateService : IConversationStateService, IDisposable
var curActiveRounds = activeRounds > 0 ? activeRounds : -1;
int? preActiveRounds = null;
if (ContainsState(name) && _states.TryGetValue(name, out var pair))
if (ContainsState(name) && _curStates.TryGetValue(name, out var pair))
{
var lastNode = pair?.Values?.LastOrDefault();
preActiveRounds = lastNode?.ActiveRounds;
preValue = lastNode?.Data ?? string.Empty;
var leafNode = pair?.Values?.LastOrDefault();
preActiveRounds = leafNode?.ActiveRounds;
preValue = leafNode?.Data ?? string.Empty;
}
_logger.LogInformation($"[STATE] {name} = {value}");
var routingCtx = _services.GetRequiredService<IRoutingContext>();
foreach (var hook in hooks)
if (!ContainsState(name) || preValue != currentValue || preActiveRounds != curActiveRounds)
{
hook.OnStateChanged(new StateChangeModel
foreach (var hook in hooks)
{
ConversationId = _conversationId,
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = preValue,
BeforeActiveRounds = preActiveRounds,
AfterValue = currentValue,
AfterActiveRounds = curActiveRounds
}).Wait();
hook.OnStateChanged(new StateChangeModel
{
ConversationId = _conversationId,
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = preValue,
BeforeActiveRounds = preActiveRounds,
AfterValue = currentValue,
AfterActiveRounds = curActiveRounds,
DataType = valueType,
Source = source,
Readonly = readOnly
}).Wait();
}
}
var newPair = new StateKeyValue
{
Key = name,
Versioning = isNeedVersion
Versioning = isNeedVersion,
Readonly = readOnly
};
var newValue = new StateValue
@ -82,76 +99,94 @@ public class ConversationStateService : IConversationStateService, IDisposable
MessageId = routingCtx.MessageId,
Active = true,
ActiveRounds = curActiveRounds,
DataType = valueType,
Source = source,
UpdateTime = DateTime.UtcNow,
};
if (!isNeedVersion || !_states.ContainsKey(name))
if (!isNeedVersion || !_curStates.ContainsKey(name))
{
newPair.Values = new List<StateValue> { newValue };
_states[name] = newPair;
_curStates[name] = newPair;
}
else
{
_states[name].Values.Add(newValue);
_curStates[name].Values.Add(newValue);
}
return this;
}
public Dictionary<string, string> Load(string conversationId)
public Dictionary<string, string> Load(string conversationId, bool isReadOnly = false)
{
_conversationId = conversationId;
_conversationId = !isReadOnly ? conversationId : null;
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var curMsgId = routingCtx.MessageId;
_states = _db.GetConversationStates(_conversationId);
var dialogs = _db.GetConversationDialogs(_conversationId);
_historyStates = _db.GetConversationStates(conversationId);
var dialogs = _db.GetConversationDialogs(conversationId);
var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User || x.MetaData?.Role == UserRole.Client)
.GroupBy(x => x.MetaData?.MessageId)
.Select(g => g.First())
.OrderBy(x => x.MetaData?.CreateTime)
.ToList();
var curMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(curMsgId) && x.MetaData?.MessageId == curMsgId);
curMsgIndex = curMsgIndex < 0 ? userDialogs.Count() : curMsgIndex;
var curStates = new Dictionary<string, string>();
if (!_states.IsNullOrEmpty())
var endNodes = new Dictionary<string, string>();
if (_historyStates.IsNullOrEmpty()) return endNodes;
foreach (var state in _historyStates)
{
foreach (var state in _states)
var key = state.Key;
var value = state.Value;
var leafNode = value?.Values?.LastOrDefault();
if (leafNode == null) continue;
_curStates[key] = new StateKeyValue
{
var value = state.Value?.Values?.LastOrDefault();
if (value == null || !value.Active) continue;
Key = key,
Versioning = value.Versioning,
Readonly = value.Readonly,
Values = new List<StateValue> { leafNode }
};
if (value.ActiveRounds > 0)
if (!leafNode.Active) continue;
// Handle state active rounds
if (leafNode.ActiveRounds > 0)
{
var stateMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(x.MetaData?.MessageId) && x.MetaData.MessageId == leafNode.MessageId);
if (stateMsgIndex >= 0 && curMsgIndex - stateMsgIndex >= leafNode.ActiveRounds)
{
var stateMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(x.MetaData?.MessageId) && x.MetaData.MessageId == value.MessageId);
if (stateMsgIndex >= 0 && curMsgIndex - stateMsgIndex >= value.ActiveRounds)
_curStates[key].Values.Add(new StateValue
{
state.Value.Values.Add(new StateValue
{
Data = value.Data,
MessageId = curMsgId,
Active = false,
ActiveRounds = value.ActiveRounds,
UpdateTime = DateTime.UtcNow
});
continue;
}
Data = leafNode.Data,
MessageId = curMsgId,
Active = false,
ActiveRounds = leafNode.ActiveRounds,
DataType = leafNode.DataType,
Source = leafNode.Source,
UpdateTime = DateTime.UtcNow
});
continue;
}
var data = value.Data ?? string.Empty;
curStates[state.Key] = data;
_logger.LogInformation($"[STATE] {state.Key} : {data}");
}
var data = leafNode.Data ?? string.Empty;
endNodes[state.Key] = data;
_logger.LogInformation($"[STATE] {key} : {data}");
}
_logger.LogInformation($"Loaded conversation states: {_conversationId}");
_logger.LogInformation($"Loaded conversation states: {conversationId}");
var hooks = _services.GetServices<IConversationHook>();
foreach (var hook in hooks)
{
hook.OnStateLoaded(_states).Wait();
hook.OnStateLoaded(_curStates).Wait();
}
return curStates;
return endNodes;
}
public void Save()
@ -163,35 +198,106 @@ public class ConversationStateService : IConversationStateService, IDisposable
var states = new List<StateKeyValue>();
foreach (var dic in _states)
foreach (var pair in _curStates)
{
states.Add(dic.Value);
var key = pair.Key;
var curValue = pair.Value;
if (!_historyStates.TryGetValue(key, out var historyValue)
|| historyValue == null
|| historyValue.Values.IsNullOrEmpty()
|| !curValue.Versioning)
{
states.Add(curValue);
}
else
{
var historyValues = historyValue.Values.Take(historyValue.Values.Count - 1).ToList();
var newValues = historyValues.Concat(curValue.Values).ToList();
var updatedNode = new StateKeyValue
{
Key = pair.Key,
Versioning = curValue.Versioning,
Readonly = curValue.Readonly,
Values = newValues
};
states.Add(updatedNode);
}
}
_db.UpdateConversationStates(_conversationId, states);
_logger.LogInformation($"Saved states of conversation {_conversationId}");
}
public void CleanStates()
public bool RemoveState(string name)
{
if (!ContainsState(name)) return false;
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var value = _curStates[name];
var leafNode = value?.Values?.LastOrDefault();
if (value == null || !value.Versioning || leafNode == null) return false;
_curStates[name].Values.Add(new StateValue
{
Data = leafNode.Data,
MessageId = routingCtx.MessageId,
Active = false,
ActiveRounds = leafNode.ActiveRounds,
DataType = leafNode.DataType,
Source = leafNode.Source,
UpdateTime = DateTime.UtcNow
});
var hooks = _services.GetServices<IConversationHook>();
foreach (var hook in hooks)
{
hook.OnStateChanged(new StateChangeModel
{
ConversationId = _conversationId,
MessageId = routingCtx.MessageId,
Name = name,
BeforeValue = leafNode.Data,
BeforeActiveRounds = leafNode.ActiveRounds,
AfterValue = null,
AfterActiveRounds = leafNode.ActiveRounds,
DataType = leafNode.DataType,
Source = leafNode.Source,
Readonly = value.Readonly
}).Wait();
}
return true;
}
public void CleanStates(params string[] excludedStates)
{
var routingCtx = _services.GetRequiredService<IRoutingContext>();
var curMsgId = routingCtx.MessageId;
var utcNow = DateTime.UtcNow;
foreach (var key in _states.Keys)
foreach (var key in _curStates.Keys)
{
var value = _states[key];
// skip state
if (excludedStates.Contains(key))
{
continue;
}
var value = _curStates[key];
if (value == null || !value.Versioning || value.Values.IsNullOrEmpty()) continue;
var lastValue = value.Values.LastOrDefault();
if (lastValue == null || !lastValue.Active) continue;
var leafNode = value.Values.LastOrDefault();
if (leafNode == null || !leafNode.Active) continue;
value.Values.Add(new StateValue
{
Data = lastValue.Data,
Data = leafNode.Data,
MessageId = curMsgId,
Active = false,
ActiveRounds = lastValue.ActiveRounds,
ActiveRounds = leafNode.ActiveRounds,
DataType = leafNode.DataType,
Source = leafNode.Source,
UpdateTime = utcNow
});
}
@ -199,25 +305,25 @@ public class ConversationStateService : IConversationStateService, IDisposable
public Dictionary<string, string> GetStates()
{
var curStates = new Dictionary<string, string>();
foreach (var state in _states)
var endNodes = new Dictionary<string, string>();
foreach (var state in _curStates)
{
var value = state.Value?.Values?.LastOrDefault();
if (value == null || !value.Active) continue;
curStates[state.Key] = value.Data ?? string.Empty;
endNodes[state.Key] = value.Data ?? string.Empty;
}
return curStates;
return endNodes;
}
public string GetState(string name, string defaultValue = "")
{
if (!_states.ContainsKey(name) || _states[name].Values.IsNullOrEmpty() || !_states[name].Values.Last().Active)
if (!_curStates.ContainsKey(name) || _curStates[name].Values.IsNullOrEmpty() || !_curStates[name].Values.Last().Active)
{
return defaultValue;
}
return _states[name].Values.Last().Data;
return _curStates[name].Values.Last().Data;
}
public void Dispose()
@ -227,10 +333,10 @@ public class ConversationStateService : IConversationStateService, IDisposable
public bool ContainsState(string name)
{
return _states.ContainsKey(name)
&& !_states[name].Values.IsNullOrEmpty()
&& _states[name].Values.LastOrDefault()?.Active == true
&& !string.IsNullOrEmpty(_states[name].Values.Last().Data);
return _curStates.ContainsKey(name)
&& !_curStates[name].Values.IsNullOrEmpty()
&& _curStates[name].Values.LastOrDefault()?.Active == true
&& !string.IsNullOrEmpty(_curStates[name].Values.Last().Data);
}
public void SaveStateByArgs(JsonDocument args)
@ -244,9 +350,17 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
foreach (JsonProperty property in root.EnumerateObject())
{
if (!string.IsNullOrEmpty(property.Value.ToString()))
var propertyValue = property.Value;
var stateValue = propertyValue.ToString();
if (!string.IsNullOrEmpty(stateValue))
{
SetState(property.Name, property.Value);
if (propertyValue.ValueKind == JsonValueKind.True ||
propertyValue.ValueKind == JsonValueKind.False)
{
stateValue = stateValue?.ToLower();
}
SetState(property.Name, stateValue, source: StateSource.Application);
}
}
}

View file

@ -8,8 +8,8 @@ namespace BotSharp.Core.Conversations.Services;
public class ConversationStorage : IConversationStorage
{
private readonly BotSharpDatabaseSettings _dbSettings;
private readonly BotSharpOptions _options;
private readonly IServiceProvider _services;
private readonly JsonSerializerOptions _jsonOptions;
public ConversationStorage(
BotSharpDatabaseSettings dbSettings,
@ -18,7 +18,7 @@ public class ConversationStorage : IConversationStorage
{
_dbSettings = dbSettings;
_services = services;
_jsonOptions = InitJsonSerilizerOptions(options);
_options = options;
}
public void Append(string conversationId, RoleDialogModel dialog)
@ -28,11 +28,11 @@ public class ConversationStorage : IConversationStorage
var dialogElements = new List<DialogElement>();
// Prevent duplicate record to be inserted
var dialogs = db.GetConversationDialogs(conversationId);
/*var dialogs = db.GetConversationDialogs(conversationId);
if (dialogs.Any(x => x.MetaData.MessageId == dialog.MessageId && x.Content == dialog.Content))
{
return;
}
}*/
if (dialog.Role == AgentRole.Function)
{
@ -50,7 +50,13 @@ public class ConversationStorage : IConversationStorage
{
return;
}
dialogElements.Add(new DialogElement(meta, content));
dialogElements.Add(new DialogElement
{
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
Payload = dialog.Payload
});
}
else
{
@ -70,8 +76,17 @@ public class ConversationStorage : IConversationStorage
return;
}
var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _jsonOptions) : null;
dialogElements.Add(new DialogElement(meta, content, richContent));
var richContent = dialog.RichContent != null ? JsonSerializer.Serialize(dialog.RichContent, _options.JsonSerializerOptions) : null;
var secondaryRichContent = dialog.SecondaryRichContent != null ? JsonSerializer.Serialize(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null;
dialogElements.Add(new DialogElement
{
MetaData = meta,
Content = dialog.Content,
SecondaryContent = dialog.SecondaryContent,
RichContent = richContent,
SecondaryRichContent = secondaryRichContent,
Payload = dialog.Payload
});
}
db.AppendConversationDialogs(conversationId, dialogElements);
@ -88,6 +103,8 @@ public class ConversationStorage : IConversationStorage
{
var meta = dialog.MetaData;
var content = dialog.Content;
var secondaryContent = dialog.SecondaryContent;
var payload = string.IsNullOrEmpty(dialog.Payload) ? null : dialog.Payload;
var role = meta.Role;
var currentAgentId = meta.AgentId;
var messageId = meta.MessageId;
@ -95,7 +112,9 @@ public class ConversationStorage : IConversationStorage
var senderId = role == AgentRole.Function ? currentAgentId : meta.SenderId;
var createdAt = meta.CreateTime;
var richContent = !string.IsNullOrEmpty(dialog.RichContent) ?
JsonSerializer.Deserialize<RichContent<IRichMessage>>(dialog.RichContent, _jsonOptions) : null;
JsonSerializer.Deserialize<RichContent<IRichMessage>>(dialog.RichContent, _options.JsonSerializerOptions) : null;
var secondaryRichContent = !string.IsNullOrEmpty(dialog.SecondaryRichContent) ?
JsonSerializer.Deserialize<RichContent<IRichMessage>>(dialog.SecondaryRichContent, _options.JsonSerializerOptions) : null;
var record = new RoleDialogModel(role, content)
{
@ -104,7 +123,10 @@ public class ConversationStorage : IConversationStorage
CreatedAt = createdAt,
SenderId = senderId,
FunctionName = function,
RichContent = richContent
RichContent = richContent,
SecondaryContent = secondaryContent,
SecondaryRichContent = secondaryRichContent,
Payload = payload
};
results.Add(record);
@ -140,21 +162,4 @@ public class ConversationStorage : IConversationStorage
}
return Path.Combine(dir, "dialogs.txt");
}
private JsonSerializerOptions InitJsonSerilizerOptions(BotSharpOptions botSharOptions)
{
var options = botSharOptions.JsonSerializerOptions;
var jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
PropertyNamingPolicy = options.PropertyNamingPolicy ?? JsonNamingPolicy.CamelCase,
AllowTrailingCommas = options.AllowTrailingCommas,
};
foreach (var converter in options.Converters)
{
jsonOptions.Converters.Add(converter);
}
return jsonOptions;
}
}

Some files were not shown because too many files have changed in this diff Show more