This commit is contained in:
Joanna Ren 2024-12-16 17:23:07 -06:00
commit 80e292a361
43 changed files with 347 additions and 85 deletions

View file

@ -17,7 +17,8 @@ public enum AgentField
Response,
Sample,
LlmConfig,
Utility
Utility,
MaxMessageCount
}
public enum AgentTaskField

View file

@ -104,6 +104,12 @@ public class Agent
/// </summary>
public string? InheritAgentId { get; set; }
/// <summary>
/// Maximum message count when load conversation
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? MaxMessageCount { get; set; }
public List<RoutingRule> RoutingRules { get; set; } = new();
/// <summary>
@ -133,6 +139,8 @@ public class Agent
Knowledges = agent.Knowledges,
IsPublic = agent.IsPublic,
Disabled = agent.Disabled,
MergeUtility = agent.MergeUtility,
MaxMessageCount = agent.MaxMessageCount,
Profiles = agent.Profiles,
RoutingRules = agent.RoutingRules,
LlmConfig = agent.LlmConfig,

View file

@ -2,27 +2,23 @@ namespace BotSharp.Abstraction.Conversations;
public abstract class ConversationHookBase : IConversationHook
{
protected Agent _agent;
public Agent Agent => _agent;
public Agent Agent { get; private set; }
protected Conversation _conversation;
public Conversation Conversation => _conversation;
public Conversation Conversation { get; private set; }
protected List<RoleDialogModel> _dialogs;
public List<RoleDialogModel> Dialogs => _dialogs;
public List<RoleDialogModel> Dialogs { get; private set; }
protected int _priority = 0;
public int Priority => _priority;
public int Priority { get; protected set; } = 0;
public IConversationHook SetAgent(Agent agent)
{
_agent = agent;
Agent = agent;
return this;
}
public IConversationHook SetConversation(Conversation conversation)
{
_conversation = conversation;
Conversation = conversation;
return this;
}
@ -37,7 +33,7 @@ public abstract class ConversationHookBase : IConversationHook
public virtual Task OnDialogsLoaded(List<RoleDialogModel> dialogs)
{
_dialogs = dialogs;
Dialogs = dialogs;
return Task.CompletedTask;
}

View file

@ -0,0 +1,20 @@
namespace BotSharp.Abstraction.Conversations;
public class ConversationHookProvider
{
public IEnumerable<IConversationHook> Hooks { get; }
private readonly Lazy<IEnumerable<IConversationHook>> _hooksOrderByPriority;
public IEnumerable<IConversationHook> HooksOrderByPriority
=> _hooksOrderByPriority.Value;
public ConversationHookProvider(IEnumerable<IConversationHook> conversationHooks)
{
Hooks = conversationHooks;
_hooksOrderByPriority = new Lazy<IEnumerable<IConversationHook>>(() =>
{
return conversationHooks.OrderBy(hook => hook.Priority).ToArray();
});
}
}

View file

@ -12,6 +12,7 @@ public interface IConversationService
Task<Conversation> GetConversation(string id);
Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter);
Task<Conversation> UpdateConversationTitle(string id, string title);
Task<Conversation> UpdateConversationTitleAlias(string id, string titleAlias);
Task<bool> UpdateConversationTags(string conversationId, List<string> tags);
Task<bool> UpdateConversationMessage(string conversationId, UpdateMessageRequest request);
Task<List<Conversation>> GetLastConversations();

View file

@ -13,6 +13,7 @@ public class Conversation
/// </summary>
public string? TaskId { get; set; }
public string Title { get; set; } = string.Empty;
public string TitleAlias { get; set; } = string.Empty;
[JsonIgnore]
public List<DialogElement> Dialogs { get; set; } = new();

View file

@ -14,6 +14,15 @@ public class CrontabItem : ScheduleTaskArgs
[JsonPropertyName("execution_result")]
public string ExecutionResult { get; set; } = null!;
[JsonPropertyName("execution_count")]
public int ExecutionCount { get; set; }
[JsonPropertyName("max_execution_count")]
public int MaxExecutionCount { get; set; }
[JsonPropertyName("expire_seconds")]
public int ExpireSeconds { get; set; } = 60;
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;

View file

@ -8,6 +8,7 @@ public class ConversationFilter
/// </summary>
public string? Id { get; set; }
public string? Title { get; set; }
public string? TitleAlias { get; set; }
public string? AgentId { get; set; }
public string? Status { get; set; }
public string? Channel { get; set; }

View file

@ -53,7 +53,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
#region Agent
void UpdateAgent(Agent agent, AgentField field);
Agent? GetAgent(string agentId);
Agent? GetAgent(string agentId, bool basicsOnly = false);
List<Agent> GetAgents(AgentFilter filter);
List<UserAgent> GetUserAgents(string userId);
void BulkInsertAgents(List<Agent> agents);
@ -86,6 +86,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
Conversation GetConversation(string conversationId);
PagedItems<Conversation> GetConversations(ConversationFilter filter);
void UpdateConversationTitle(string conversationId, string title);
void UpdateConversationTitleAlias(string conversationId, string titleAlias);
bool UpdateConversationTags(string conversationId, List<string> tags);
bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request);
void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint);

View file

@ -47,7 +47,7 @@ public class BasicAgentHook : AgentHookBase
var entryAgentId = routing.EntryAgentId;
if (!string.IsNullOrEmpty(entryAgentId))
{
var entryAgent = db.GetAgent(entryAgentId);
var entryAgent = db.GetAgent(entryAgentId, basicsOnly: true);
var (fns, tps) = GetUniqueContent(entryAgent?.Utilities);
functionNames = functionNames.Concat(fns).Distinct().ToList();
templateNames = templateNames.Concat(tps).Distinct().ToList();

View file

@ -5,7 +5,7 @@ namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
public static ConcurrentDictionary<string, Dictionary<string,string>> AgentParameterTypes = new();
public static ConcurrentDictionary<string, Dictionary<string, string>> AgentParameterTypes = new();
[MemoryCache(10 * 60, perInstanceCache: true)]
public async Task<Agent> LoadAgent(string id)
@ -106,14 +106,14 @@ public partial class AgentService
{
var agentId = agent.Id ?? agent.Name;
if (AgentParameterTypes.ContainsKey(agentId)) return;
AddOrUpdateRoutesParameters(agentId, agent.RoutingRules);
AddOrUpdateFunctionsParameters(agentId, agent.Functions);
}
private void AddOrUpdateRoutesParameters(string agentId, List<RoutingRule> routingRules)
{
if(!AgentParameterTypes.TryGetValue(agentId, out var parameterTypes))
if (!AgentParameterTypes.TryGetValue(agentId, out var parameterTypes))
{
parameterTypes = new();
}

View file

@ -28,6 +28,7 @@ public partial class AgentService
record.IsPublic = agent.IsPublic;
record.Disabled = agent.Disabled;
record.MergeUtility = agent.MergeUtility;
record.MaxMessageCount = agent.MaxMessageCount;
record.Type = agent.Type;
record.Profiles = agent.Profiles ?? [];
record.RoutingRules = agent.RoutingRules ?? [];

View file

@ -28,7 +28,7 @@ public partial class ConversationService
var dialogs = conv.GetDialogHistory();
var statistics = _services.GetRequiredService<ITokenStatistics>();
var hooks = _services.GetServices<IConversationHook>().ToList();
var hookProvider = _services.GetRequiredService<ConversationHookProvider>();
RoleDialogModel response = message;
bool stopCompletion = false;
@ -44,9 +44,7 @@ public partial class ConversationService
message.Payload = replyMessage.Payload;
}
// Before chat completion hook
hooks = ReOrderConversationHooks(hooks);
foreach (var hook in hooks)
foreach (var hook in hookProvider.HooksOrderByPriority)
{
hook.SetAgent(agent)
.SetConversation(conversation);
@ -173,18 +171,4 @@ public partial class ConversationService
// Add to dialog history
_storage.Append(_conversationId, response);
}
private List<IConversationHook> ReOrderConversationHooks(List<IConversationHook> hooks)
{
var target = "ChatHubConversationHook";
var chathub = hooks.FirstOrDefault(x => x.GetType().Name == target);
var otherHooks = hooks.Where(x => x.GetType().Name != target).ToList();
if (chathub != null)
{
var newHooks = new List<IConversationHook> { chathub }.Concat(otherHooks);
return newHooks.ToList();
}
return hooks;
}
}

View file

@ -9,7 +9,7 @@ public partial class ConversationService : IConversationService
var deleteMessageIds = db.TruncateConversation(conversationId, messageId, cleanLog: true);
fileStorage.DeleteMessageFiles(conversationId, deleteMessageIds, messageId, newMessageId);
var hooks = _services.GetServices<IConversationHook>().ToList();
var hooks = _services.GetServices<IConversationHook>();
foreach (var hook in hooks)
{
await hook.OnMessageDeleted(conversationId, messageId);

View file

@ -31,9 +31,9 @@ public partial class ConversationService : IConversationService
states.CleanStates(excludedStates);
}
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
var hooks = _services
.GetRequiredService<ConversationHookProvider>()
.HooksOrderByPriority;
// Before executing functions
foreach (var hook in hooks)

View file

@ -51,6 +51,14 @@ public partial class ConversationService : IConversationService
return conversation;
}
public async Task<Conversation> UpdateConversationTitleAlias(string id, string titleAlias)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
db.UpdateConversationTitleAlias(id, titleAlias);
var conversation = db.GetConversation(id);
return conversation;
}
public async Task<bool> UpdateConversationTags(string conversationId, List<string> tags)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
@ -103,7 +111,8 @@ public partial class ConversationService : IConversationService
db.CreateNewConversation(record);
var hooks = _services.GetServices<IConversationHook>().ToList();
var hooks = _services.GetServices<IConversationHook>();
foreach (var hook in hooks)
{
// If user connect agent first time
@ -153,7 +162,10 @@ public partial class ConversationService : IConversationService
}
}
return dialogs.TakeLast(lastCount).ToList();
var agentMsgCount = GetAgentMessageCount();
var count = agentMsgCount.HasValue && agentMsgCount.Value > 0 ? agentMsgCount.Value : lastCount;
return dialogs.TakeLast(count).ToList();
}
public void SetConversationId(string conversationId, List<MessageState> states, bool isReadOnly = false)
@ -192,4 +204,16 @@ public partial class ConversationService : IConversationService
{
return !string.IsNullOrWhiteSpace(_conversationId);
}
private int? GetAgentMessageCount()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var routingCtx = _services.GetRequiredService<IRoutingContext>();
if (string.IsNullOrEmpty(routingCtx.EntryAgentId)) return null;
var agent = db.GetAgent(routingCtx.EntryAgentId, basicsOnly: true);
return agent?.MaxMessageCount;
}
}

View file

@ -15,45 +15,45 @@ public class EvaluationConversationHook : ConversationHookBase
public override Task OnMessageReceived(RoleDialogModel message)
{
if (_conversation != null && _convSettings.EnableExecutionLog)
if (Conversation != null && _convSettings.EnableExecutionLog)
{
_logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}");
_logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}");
}
return base.OnMessageReceived(message);
}
public override Task OnFunctionExecuted(RoleDialogModel message)
{
if (_conversation != null && _convSettings.EnableExecutionLog)
if (Conversation != null && _convSettings.EnableExecutionLog)
{
_logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.FunctionName}({message.FunctionArgs}) => {message.Content}");
_logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.FunctionName}({message.FunctionArgs}) => {message.Content}");
}
return base.OnFunctionExecuted(message);
}
public override Task OnResponseGenerated(RoleDialogModel message)
{
if (_conversation != null && _convSettings.EnableExecutionLog)
if (Conversation != null && _convSettings.EnableExecutionLog)
{
_logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}");
}
_logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}");
}
return base.OnResponseGenerated(message);
}
public override Task OnHumanInterventionNeeded(RoleDialogModel message)
{
if (_conversation != null && _convSettings.EnableExecutionLog)
if (Conversation != null && _convSettings.EnableExecutionLog)
{
_logger.Append(_conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})");
_logger.Append(Conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})");
}
return base.OnHumanInterventionNeeded(message);
}
public override Task OnConversationEnding(RoleDialogModel message)
{
if (_conversation != null && _convSettings.EnableExecutionLog)
if (Conversation != null && _convSettings.EnableExecutionLog)
{
_logger.Append(_conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})");
_logger.Append(Conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})");
}
return base.OnConversationEnding(message);
}

View file

@ -16,7 +16,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository
#endregion
#region Agent
public Agent GetAgent(string agentId)
public Agent GetAgent(string agentId, bool basicsOnly = false)
=> throw new NotImplementedException();
public List<Agent> GetAgents(AgentFilter filter)
@ -105,6 +105,8 @@ public class BotSharpDbContext : Database, IBotSharpRepository
public void UpdateConversationTitle(string conversationId, string title)
=> throw new NotImplementedException();
public void UpdateConversationTitleAlias(string conversationId, string titleAlias)
=> throw new NotImplementedException();
public bool UpdateConversationTags(string conversationId, List<string> tags)
=> throw new NotImplementedException();

View file

@ -57,6 +57,9 @@ namespace BotSharp.Core.Repository
case AgentField.Utility:
UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities);
break;
case AgentField.MaxMessageCount:
UpdateAgentMaxMessageCount(agent.Id, agent.MaxMessageCount);
break;
case AgentField.All:
UpdateAgentAllFields(agent);
break;
@ -283,6 +286,17 @@ namespace BotSharp.Core.Repository
File.WriteAllText(agentFile, json);
}
private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount)
{
var (agent, agentFile) = GetAgentFromFile(agentId);
if (agent == null) return;
agent.MaxMessageCount = maxMessageCount;
agent.UpdatedDateTime = DateTime.UtcNow;
var json = JsonSerializer.Serialize(agent, _options);
File.WriteAllText(agentFile, json);
}
private void UpdateAgentAllFields(Agent inputAgent)
{
var (agent, agentFile) = GetAgentFromFile(inputAgent.Id);
@ -298,6 +312,7 @@ namespace BotSharp.Core.Repository
agent.Utilities = inputAgent.Utilities;
agent.RoutingRules = inputAgent.RoutingRules;
agent.LlmConfig = inputAgent.LlmConfig;
agent.MaxMessageCount = inputAgent.MaxMessageCount;
agent.UpdatedDateTime = DateTime.UtcNow;
var json = JsonSerializer.Serialize(agent, _options);
File.WriteAllText(agentFile, json);
@ -329,7 +344,7 @@ namespace BotSharp.Core.Repository
return responses;
}
public Agent? GetAgent(string agentId)
public Agent? GetAgent(string agentId, bool basicsOnly = false)
{
var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir);
var dir = Directory.GetDirectories(agentDir).FirstOrDefault(x => x.Split(Path.DirectorySeparatorChar).Last() == agentId);
@ -342,6 +357,8 @@ namespace BotSharp.Core.Repository
var record = JsonSerializer.Deserialize<Agent>(json, _options);
if (record == null) return null;
if (basicsOnly) return record;
var (defaultInstruction, channelInstructions) = FetchInstructions(dir);
var functions = FetchFunctions(dir);
var samples = FetchSamples(dir);

View file

@ -134,6 +134,22 @@ namespace BotSharp.Core.Repository
}
}
}
public void UpdateConversationTitleAlias(string conversationId, string titleAlias)
{
var convDir = FindConversationDirectory(conversationId);
if (!string.IsNullOrEmpty(convDir))
{
var convFile = Path.Combine(convDir, CONVERSATION_FILE);
var content = File.ReadAllText(convFile);
var record = JsonSerializer.Deserialize<Conversation>(content, _options);
if (record != null)
{
record.TitleAlias = titleAlias;
record.UpdatedTime = DateTime.UtcNow;
File.WriteAllText(convFile, JsonSerializer.Serialize(record, _options));
}
}
}
public bool UpdateConversationTags(string conversationId, List<string> tags)
{
@ -356,6 +372,10 @@ namespace BotSharp.Core.Repository
{
matched = matched && record.Title.Contains(filter.Title);
}
if (filter?.TitleAlias != null)
{
matched = matched && record.TitleAlias.Contains(filter.TitleAlias);
}
if (filter?.AgentId != null)
{
matched = matched && record.AgentId == filter.AgentId;

View file

@ -15,9 +15,9 @@ public class HumanInterventionNeededFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
var hooks = _services
.GetRequiredService<ConversationHookProvider>()
.HooksOrderByPriority;
foreach (var hook in hooks)
{

View file

@ -18,9 +18,9 @@ public partial class RoutingService
var clonedMessage = RoleDialogModel.From(message);
clonedMessage.FunctionName = name;
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
var hooks = _services
.GetRequiredService<ConversationHookProvider>()
.HooksOrderByPriority;
var progressService = _services.GetService<IConversationProgressService>();

View file

@ -34,7 +34,7 @@ public class RateLimitConversationHook : ConversationHookBase
}
// Check message sending frequency
var userSents = _dialogs.Where(x => x.Role == AgentRole.User)
var userSents = Dialogs.Where(x => x.Role == AgentRole.User)
.TakeLast(2).ToList();
if (userSents.Count > 1)

View file

@ -223,6 +223,30 @@ public class ConversationController : ControllerBase
return response != null;
}
[HttpPut("/conversation/{conversationId}/update-title-alias")]
public async Task<bool> UpdateConversationTitleAlias([FromRoute] string conversationId, [FromBody] UpdateConversationTitleAliasModel newTile)
{
var userService = _services.GetRequiredService<IUserService>();
var conversationService = _services.GetRequiredService<IConversationService>();
var user = await userService.GetUser(_user.Id);
var filter = new ConversationFilter
{
Id = conversationId,
UserId = user.Role != UserRole.Admin ? user.Id : null
};
var conversations = await conversationService.GetConversations(filter);
if (conversations.Items.IsNullOrEmpty())
{
return false;
}
var response = await conversationService.UpdateConversationTitleAlias(conversationId, newTile.NewTitleAlias);
return response != null;
}
[HttpPut("/conversation/{conversationId}/update-tags")]
public async Task<bool> UpdateConversationTags([FromRoute] string conversationId, [FromBody] UpdateConversationRequest request)
{

View file

@ -51,6 +51,8 @@ public class AgentCreationModel
public bool MergeUtility { get; set; }
public int? MaxMessageCount { get; set; }
public List<AgentUtility> Utilities { get; set; } = new();
public List<RoutingRuleUpdateModel> RoutingRules { get; set; } = new();
public AgentLlmConfig? LlmConfig { get; set; }
@ -72,6 +74,7 @@ public class AgentCreationModel
Type = Type,
Disabled = Disabled,
MergeUtility = MergeUtility,
MaxMessageCount = MaxMessageCount,
Profiles = Profiles,
RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? new List<RoutingRule>(),
LlmConfig = LlmConfig

View file

@ -57,6 +57,9 @@ public class AgentUpdateModel
public bool Disabled { get; set; }
[JsonPropertyName("max_message_count")]
public int? MaxMessageCount { get; set; }
/// <summary>
/// Profile by channel
/// </summary>
@ -77,6 +80,7 @@ public class AgentUpdateModel
IsPublic = IsPublic,
Disabled = Disabled,
MergeUtility = MergeUtility,
MaxMessageCount = MaxMessageCount,
Type = Type,
Profiles = Profiles ?? new List<string>(),
RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? new List<RoutingRule>(),

View file

@ -46,6 +46,10 @@ public class AgentViewModel
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public AgentLlmConfig? LlmConfig { get; set; }
[JsonPropertyName("max_message_count")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? MaxMessageCount { get; set; }
public PluginDef Plugin { get; set; }
public IEnumerable<string>? Actions { get; set; }
@ -75,6 +79,7 @@ public class AgentViewModel
Disabled = agent.Disabled,
MergeUtility = agent.MergeUtility,
IconUrl = agent.IconUrl,
MaxMessageCount = agent.MaxMessageCount,
Profiles = agent.Profiles ?? new List<string>(),
RoutingRules = agent.RoutingRules,
LlmConfig = agent.LlmConfig,

View file

@ -15,6 +15,9 @@ public class ConversationViewModel
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("title_alias")]
public string TitleAlias { get; set; } = string.Empty;
public UserViewModel User { get; set; } = new UserViewModel();
public string Event { get; set; }
@ -48,6 +51,7 @@ public class ConversationViewModel
},
AgentId = sess.AgentId,
Title = sess.Title,
TitleAlias = sess.TitleAlias,
Channel = sess.Channel,
Status = sess.Status,
TaskId = sess.TaskId,

View file

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace BotSharp.OpenAPI.ViewModels.Conversations;
public class UpdateConversationTitleAliasModel
{
[Required]
public string NewTitleAlias { get; set; }
}

View file

@ -22,6 +22,7 @@ public class ChatHubPlugin : IBotSharpPlugin
services.AddScoped<IConversationHook, ChatHubConversationHook>();
services.AddScoped<IConversationHook, StreamingLogHook>();
services.AddScoped<IConversationHook, WelcomeHook>();
services.AddScoped<ConversationHookProvider>();
services.AddScoped<IRoutingHook, StreamingLogHook>();
services.AddScoped<IContentGeneratingHook, StreamingLogHook>();
services.AddScoped<ICrontabHook, ChatHubCrontabHook>();

View file

@ -29,6 +29,7 @@ public class ChatHubConversationHook : ConversationHookBase
_chatHub = chatHub;
_user = user;
_options = options;
Priority = -1; // Make sure this hook is the top one.
}
public override async Task OnConversationInitialized(Conversation conversation)

View file

@ -23,8 +23,9 @@ public class ReadImageFn : IFunctionCallback
var agentService = _services.GetRequiredService<IAgentService>();
var wholeDialogs = conv.GetDialogHistory();
var dialogs = AssembleFiles(conv.ConversationId, wholeDialogs);
var agent = await agentService.LoadAgent(BuiltInAgentId.UtilityAssistant);
var dialogs = AssembleFiles(conv.ConversationId, args?.ImageUrls, wholeDialogs);
var agentId = !string.IsNullOrWhiteSpace(message.CurrentAgentId) ? message.CurrentAgentId : BuiltInAgentId.UtilityAssistant;
var agent = await agentService.LoadAgent(agentId);
var fileAgent = new Agent
{
Id = agent?.Id ?? Guid.Empty.ToString(),
@ -38,7 +39,7 @@ public class ReadImageFn : IFunctionCallback
return true;
}
private List<RoleDialogModel> AssembleFiles(string conversationId, List<RoleDialogModel> dialogs)
private List<RoleDialogModel> AssembleFiles(string conversationId, IEnumerable<string>? imageUrls, List<RoleDialogModel> dialogs)
{
if (dialogs.IsNullOrEmpty())
{
@ -66,6 +67,18 @@ public class ReadImageFn : IFunctionCallback
}).ToList();
}
if (!imageUrls.IsNullOrEmpty())
{
var lastDialog = dialogs.Last();
var files = lastDialog.Files ?? [];
var addnFiles = imageUrls.Select(x => x?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => new BotSharpFile { FileUrl = x }).ToList();
files.AddRange(addnFiles);
lastDialog.Files = files;
}
return dialogs;
}

View file

@ -11,4 +11,12 @@ public class LlmContextIn
[JsonPropertyName("image_description")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ImageDescription { get; set; }
//[JsonPropertyName("image_url")]
//[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
//public string? ImageUrl { get; set; }
[JsonPropertyName("image_urls")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IEnumerable<string>? ImageUrls { get; set; }
}

View file

@ -7,6 +7,14 @@
"user_request": {
"type": "string",
"description": "The request posted by user, which is related to analyzing requested images. User can request for multiple images to process at one time."
},
"image_urls": {
"type": "array",
"description": "The image, photo or picture urls that user requests for analysis. They typically start with 'http' or 'https'. If user doesn't include any url, then leave this array empty. Please remove any duplicated urls",
"items": {
"type": "string",
"description": "The image, photo or picture url that user requests for analysis. It typically starts with http or https."
}
}
},
"required": [ "user_request" ]

View file

@ -11,6 +11,7 @@ public class AgentDocument : MongoBase
public bool IsPublic { get; set; }
public bool Disabled { get; set; }
public bool MergeUtility { get; set; }
public int? MaxMessageCount { get; set; }
public List<ChannelInstructionMongoElement> ChannelInstructions { get; set; }
public List<AgentTemplateMongoElement> Templates { get; set; }
public List<FunctionDefMongoElement> Functions { get; set; }

View file

@ -6,6 +6,7 @@ public class ConversationDocument : MongoBase
public string UserId { get; set; }
public string? TaskId { get; set; }
public string Title { get; set; }
public string TitleAlias { get; set; }
public string Channel { get; set; }
public string ChannelId { get; set; }
public string Status { get; set; }

View file

@ -11,6 +11,9 @@ public class CrontabItemDocument : MongoBase
public string Cron { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int ExecutionCount { get; set; }
public int MaxExecutionCount { get; set; }
public int ExpireSeconds { get; set; }
public IEnumerable<CronTaskMongoElement> Tasks { get; set; } = [];
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
@ -25,6 +28,9 @@ public class CrontabItemDocument : MongoBase
Cron = item.Cron,
Title = item.Title,
Description = item.Description,
ExecutionCount = item.ExecutionCount,
MaxExecutionCount = item.MaxExecutionCount,
ExpireSeconds = item.ExpireSeconds,
Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToDomainElement(x))?.ToArray() ?? [],
CreatedTime = item.CreatedTime
};
@ -41,6 +47,9 @@ public class CrontabItemDocument : MongoBase
Cron = item.Cron,
Title = item.Title,
Description = item.Description,
ExecutionCount = item.ExecutionCount,
MaxExecutionCount = item.MaxExecutionCount,
ExpireSeconds = item.ExpireSeconds,
Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToMongoElement(x))?.ToList() ?? [],
CreatedTime = item.CreatedTime
};

View file

@ -9,7 +9,7 @@ public partial class MongoRepository
{
public void UpdateAgent(Agent agent, AgentField field)
{
if (agent == null || string.IsNullOrEmpty(agent.Id)) return;
if (agent == null || string.IsNullOrWhiteSpace(agent.Id)) return;
switch (field)
{
@ -58,6 +58,9 @@ public partial class MongoRepository
case AgentField.Utility:
UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities);
break;
case AgentField.MaxMessageCount:
UpdateAgentMaxMessageCount(agent.Id, agent.MaxMessageCount);
break;
case AgentField.All:
UpdateAgentAllFields(agent);
break;
@ -158,10 +161,8 @@ public partial class MongoRepository
private void UpdateAgentInstructions(string agentId, string instruction, List<ChannelInstruction>? channelInstructions)
{
if (string.IsNullOrWhiteSpace(agentId)) return;
var instructionElements = channelInstructions?.Select(x => ChannelInstructionMongoElement.ToMongoElement(x))?
.ToList() ?? new List<ChannelInstructionMongoElement>();
.ToList() ?? [];
var filter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agentId);
var update = Builders<AgentDocument>.Update
@ -200,7 +201,7 @@ public partial class MongoRepository
private void UpdateAgentResponses(string agentId, List<AgentResponse> responses)
{
if (responses == null) return;
if (responses == null || string.IsNullOrWhiteSpace(agentId)) return;
var responsesToUpdate = responses.Select(r => AgentResponseMongoElement.ToMongoElement(r)).ToList();
var filter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agentId);
@ -249,6 +250,16 @@ public partial class MongoRepository
_dc.Agents.UpdateOne(filter, update);
}
private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount)
{
var filter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agentId);
var update = Builders<AgentDocument>.Update
.Set(x => x.MaxMessageCount, maxMessageCount)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
_dc.Agents.UpdateOne(filter, update);
}
private void UpdateAgentAllFields(Agent agent)
{
var filter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agent.Id);
@ -258,6 +269,7 @@ public partial class MongoRepository
.Set(x => x.Disabled, agent.Disabled)
.Set(x => x.MergeUtility, agent.MergeUtility)
.Set(x => x.Type, agent.Type)
.Set(x => x.MaxMessageCount, agent.MaxMessageCount)
.Set(x => x.Profiles, agent.Profiles)
.Set(x => x.RoutingRules, agent.RoutingRules.Select(r => RoutingRuleMongoElement.ToMongoElement(r)).ToList())
.Set(x => x.Instruction, agent.Instruction)
@ -277,7 +289,7 @@ public partial class MongoRepository
#endregion
public Agent? GetAgent(string agentId)
public Agent? GetAgent(string agentId, bool basicsOnly = false)
{
var agent = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == agentId);
if (agent == null) return null;
@ -420,6 +432,7 @@ public partial class MongoRepository
InheritAgentId = x.InheritAgentId,
Disabled = x.Disabled,
MergeUtility = x.MergeUtility,
MaxMessageCount = x.MaxMessageCount,
Profiles = x.Profiles,
RoutingRules = x.RoutingRules?.Select(r => RoutingRuleMongoElement.ToMongoElement(r))?.ToList() ?? [],
LlmConfig = AgentLlmConfigMongoElement.ToMongoElement(x.LlmConfig),
@ -513,6 +526,7 @@ public partial class MongoRepository
Type = agentDoc.Type,
InheritAgentId = agentDoc.InheritAgentId,
Profiles = agentDoc.Profiles,
MaxMessageCount = agentDoc.MaxMessageCount
};
}
}

View file

@ -114,6 +114,17 @@ public partial class MongoRepository
_dc.Conversations.UpdateOne(filterConv, updateConv);
}
public void UpdateConversationTitleAlias(string conversationId, string titleAlias)
{
if (string.IsNullOrEmpty(conversationId)) return;
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var updateConv = Builders<ConversationDocument>.Update
.Set(x => x.UpdatedTime, DateTime.UtcNow)
.Set(x => x.TitleAlias, titleAlias);
_dc.Conversations.UpdateOne(filterConv, updateConv);
}
public bool UpdateConversationTags(string conversationId, List<string> tags)
{
@ -301,6 +312,10 @@ public partial class MongoRepository
{
convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.Title, "i")));
}
if (!string.IsNullOrEmpty(filter?.TitleAlias))
{
convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.TitleAlias, "i")));
}
if (!string.IsNullOrEmpty(filter?.AgentId))
{
convFilters.Add(convBuilder.Eq(x => x.AgentId, filter.AgentId));

View file

@ -42,7 +42,7 @@ public class RoutingConversationHook: ConversationHookBase
// Render by template
var templateService = _services.GetRequiredService<IResponseTemplateService>();
var response = await templateService.RenderIntentResponse(_agent.Id, message);
var response = await templateService.RenderIntentResponse(Agent.Id, message);
if (!string.IsNullOrEmpty(response))
{
@ -54,7 +54,7 @@ public class RoutingConversationHook: ConversationHookBase
public override async Task OnResponseGenerated(RoleDialogModel message)
{
var routerSettings = _services.GetRequiredService<RoutingSettings>();
bool saveFlag = _agent.Type != AgentType.Routing;
bool saveFlag = Agent.Type != AgentType.Routing;
if (saveFlag)
{
@ -63,7 +63,7 @@ public class RoutingConversationHook: ConversationHookBase
var rootDataPath = agentService.GetDataDir();
string rawDataDir = Path.Combine(rootDataPath, "raw_data", $"agent.{message.CurrentAgentId}.txt");
var lastThreeDialogs = _dialogs.Where(x => x.Role == AgentRole.User || x.Role == AgentRole.Assistant)
var lastThreeDialogs = Dialogs.Where(x => x.Role == AgentRole.User || x.Role == AgentRole.Assistant)
.Select(x => x.Content.Replace('\r', ' ').Replace('\n', ' '))
.TakeLast(3)
.ToArray();

View file

@ -0,0 +1,63 @@
using Microsoft.Extensions.DependencyInjection;
using BotSharp.Abstraction.Conversations;
namespace UnitTest
{
[TestClass]
public class MainTest
{
[TestMethod]
public void TestConversationHookProvider()
{
var services = new ServiceCollection();
services.AddSingleton<IConversationHook, TestHookC>();
services.AddSingleton<IConversationHook, TestHookA>();
services.AddSingleton<IConversationHook, TestHookB>();
services.AddSingleton<ConversationHookProvider>();
var serviceProvider = services.BuildServiceProvider();
var conversationHookProvider = serviceProvider.GetService<ConversationHookProvider>();
Assert.AreEqual(3, conversationHookProvider.Hooks.Count());
var prevHook = default(IConversationHook);
// Assert priority
foreach (var hook in conversationHookProvider.HooksOrderByPriority)
{
if (prevHook != null)
{
Assert.IsTrue(prevHook.Priority < hook.Priority);
}
prevHook = hook;
}
}
class TestHookA : ConversationHookBase
{
public TestHookA()
{
Priority = 1;
}
}
class TestHookB : ConversationHookBase
{
public TestHookB()
{
Priority = 2;
}
}
class TestHookC : ConversationHookBase
{
public TestHookC()
{
Priority = 3;
}
}
}
}

View file

@ -13,10 +13,14 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -1,11 +0,0 @@
namespace UnitTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}