commit
4e9c311b65
|
|
@ -8,7 +8,7 @@ public interface IConversationService
|
|||
IConversationStateService States { get; }
|
||||
string ConversationId { get; }
|
||||
Task<Conversation> NewConversation(Conversation conversation);
|
||||
void SetConversationId(string conversationId, List<string> states);
|
||||
void SetConversationId(string conversationId, List<MessageState> states);
|
||||
Task<Conversation> GetConversation(string id);
|
||||
Task<PagedItems<Conversation>> GetConversations(ConversationFilter filter);
|
||||
Task<Conversation> UpdateConversationTitle(string id, string title);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public interface IConversationStateService
|
|||
string GetState(string name, string defaultValue = "");
|
||||
bool ContainsState(string name);
|
||||
Dictionary<string, string> GetStates();
|
||||
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true);
|
||||
IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1);
|
||||
void SaveStateByArgs(JsonDocument args);
|
||||
void CleanStates();
|
||||
void Save();
|
||||
|
|
|
|||
|
|
@ -24,14 +24,10 @@ public class Conversation
|
|||
|
||||
public string Channel { get; set; } = ConversationChannel.OpenAPI;
|
||||
|
||||
public int DialogCount { get; set; }
|
||||
|
||||
public DateTime UpdatedTime { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// The default value will be same as CreatedTime
|
||||
/// It used to insert a breakpoint in the conversation to hide the previous dialogs.
|
||||
/// </summary>
|
||||
public DateTime Breakpoint { get; set; } = DateTime.UtcNow.AddMilliseconds(-100);
|
||||
}
|
||||
|
||||
public class DialogElement
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
public class ConversationBreakpoint
|
||||
{
|
||||
[JsonPropertyName("message_id")]
|
||||
public string? MessageId { get; set; }
|
||||
|
||||
[JsonPropertyName("breakpoint")]
|
||||
public DateTime Breakpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("created_time")]
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
public class ConversationState : Dictionary<string, List<StateValue>>
|
||||
public class ConversationState : Dictionary<string, StateKeyValue>
|
||||
{
|
||||
public ConversationState()
|
||||
{
|
||||
|
|
@ -11,7 +11,7 @@ public class ConversationState : Dictionary<string, List<StateValue>>
|
|||
{
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
this[pair.Key] = pair.Values;
|
||||
this[pair.Key] = pair;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ public class RoleDialogModel : ITrackableMessage
|
|||
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Remember to set Message.Content as well
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public RichContent<IRichMessage>? RichContent { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ namespace BotSharp.Abstraction.Conversations.Models;
|
|||
public class StateKeyValue
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public bool Versioning { get; set; }
|
||||
public List<StateValue> Values { get; set; } = new List<StateValue>();
|
||||
|
||||
public StateKeyValue()
|
||||
|
|
@ -20,6 +21,16 @@ public class StateKeyValue
|
|||
public class StateValue
|
||||
{
|
||||
public string Data { get; set; }
|
||||
|
||||
[JsonPropertyName("message_id")]
|
||||
public string? MessageId { get; set; }
|
||||
|
||||
public bool Active { get; set; }
|
||||
|
||||
[JsonPropertyName("active_rounds")]
|
||||
public int ActiveRounds { get; set; }
|
||||
|
||||
[JsonPropertyName("update_time")]
|
||||
public DateTime UpdateTime { get; set; }
|
||||
|
||||
public StateValue()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ public class ConversationSetting
|
|||
public bool EnableExecutionLog { get; set; }
|
||||
public bool EnableContentLog { get; set; }
|
||||
public bool EnableStateLog { get; set; }
|
||||
public CleanConversationSetting CleanSetting { get; set; }
|
||||
public RateLimitSetting RateLimit { get; set; }
|
||||
public CleanConversationSetting CleanSetting { get; set; } = new CleanConversationSetting();
|
||||
public RateLimitSetting RateLimit { get; set; } = new RateLimitSetting();
|
||||
}
|
||||
|
||||
public class CleanConversationSetting
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class RichContentJsonConverter : JsonConverter<IRichMessage>
|
|||
if (root.TryGetProperty("rich_type", out JsonElement element))
|
||||
{
|
||||
var richType = element.GetString();
|
||||
res = parser.ParseRichMessage(richType, jsonText, options);
|
||||
res = parser.ParseRichMessage(richType, jsonText, root, options);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class TemplateMessageJsonConverter : JsonConverter<ITemplateMessage>
|
|||
if (root.TryGetProperty("template_type", out JsonElement element))
|
||||
{
|
||||
var templateType = element.GetString();
|
||||
res = parser.ParseTemplateMessage(templateType, jsonText, options);
|
||||
res = parser.ParseTemplateMessage(templateType, jsonText, root, options);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class MessageParser
|
|||
{
|
||||
}
|
||||
|
||||
public IRichMessage? ParseRichMessage(string richType, string jsonText, JsonSerializerOptions options)
|
||||
public IRichMessage? ParseRichMessage(string richType, string jsonText, JsonElement root, JsonSerializerOptions options)
|
||||
{
|
||||
IRichMessage? res = null;
|
||||
|
||||
|
|
@ -36,11 +36,22 @@ public class MessageParser
|
|||
{
|
||||
res = JsonSerializer.Deserialize<TextMessage>(jsonText, options);
|
||||
}
|
||||
else if (richType == RichTypeEnum.GenericTemplate)
|
||||
{
|
||||
if (root.TryGetProperty("element_type", out var element))
|
||||
{
|
||||
var elementType = element.GetString();
|
||||
if (elementType == typeof(GenericElement).Name)
|
||||
{
|
||||
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public ITemplateMessage? ParseTemplateMessage(string templateType, string jsonText, JsonSerializerOptions options)
|
||||
public ITemplateMessage? ParseTemplateMessage(string templateType, string jsonText, JsonElement root, JsonSerializerOptions options)
|
||||
{
|
||||
ITemplateMessage? res = null;
|
||||
|
||||
|
|
@ -60,6 +71,17 @@ public class MessageParser
|
|||
{
|
||||
res = JsonSerializer.Deserialize<ProductTemplateMessage>(jsonText, options);
|
||||
}
|
||||
else if (templateType == TemplateTypeEnum.Generic)
|
||||
{
|
||||
if (root.TryGetProperty("element_type", out var element))
|
||||
{
|
||||
var elementType = element.GetString();
|
||||
if (elementType == typeof(GenericElement).Name)
|
||||
{
|
||||
res = JsonSerializer.Deserialize<GenericTemplateMessage<GenericElement>>(jsonText, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ public class MessageConfig : TruncateMessageRequest
|
|||
[JsonPropertyName("model")]
|
||||
public virtual string? Model { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Model name
|
||||
/// </summary>
|
||||
[JsonPropertyName("model_id")]
|
||||
public virtual string? ModelId { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The sampling temperature to use that controls the apparent creativity of generated completions.
|
||||
/// </summary>
|
||||
|
|
@ -28,7 +34,7 @@ public class MessageConfig : TruncateMessageRequest
|
|||
/// <summary>
|
||||
/// Conversation states from input
|
||||
/// </summary>
|
||||
public List<string> States { get; set; } = new List<string>();
|
||||
public List<MessageState> States { get; set; } = new List<MessageState>();
|
||||
|
||||
/// <summary>
|
||||
/// Agent task id
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
namespace BotSharp.Abstraction.Models;
|
||||
|
||||
public class MessageState
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("active_rounds")]
|
||||
public int ActiveRounds { get; set; } = -1;
|
||||
|
||||
public MessageState()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MessageState(string key, string value, int activeRounds = -1)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
ActiveRounds = activeRounds;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ public class ConversationFilter
|
|||
public string? Status { get; set; }
|
||||
public string? Channel { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Agent task id
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ public interface IBotSharpRepository
|
|||
Conversation GetConversation(string conversationId);
|
||||
PagedItems<Conversation> GetConversations(ConversationFilter filter);
|
||||
void UpdateConversationTitle(string conversationId, string title);
|
||||
void UpdateConversationBreakpoint(string conversationId, DateTime breakpoint);
|
||||
void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint);
|
||||
DateTime GetConversationBreakpoint(string conversationId);
|
||||
List<Conversation> GetLastConversations();
|
||||
List<string> GetIdleConversations(int batchSize, int messageLimit, int bufferHours);
|
||||
bool TruncateConversation(string conversationId, string messageId, bool cleanLog = false);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ public class RoutingRule
|
|||
|
||||
public bool Required { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? RedirectTo { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonPropertyName("redirect_to_agent")]
|
||||
public string? RedirectToAgentName { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
||||
namespace BotSharp.Core.Agents.Services;
|
||||
|
|
@ -7,7 +5,7 @@ namespace BotSharp.Core.Agents.Services;
|
|||
public partial class AgentService
|
||||
{
|
||||
#if !DEBUG
|
||||
[MemoryCache(10 * 60)]
|
||||
[MemoryCache(10 * 60, perInstanceCache: true)]
|
||||
#endif
|
||||
public async Task<PagedItems<Agent>> GetAgents(AgentFilter filter)
|
||||
{
|
||||
|
|
@ -29,7 +27,7 @@ public partial class AgentService
|
|||
}
|
||||
|
||||
#if !DEBUG
|
||||
[MemoryCache(10 * 60)]
|
||||
[MemoryCache(10 * 60, perInstanceCache: true)]
|
||||
#endif
|
||||
public async Task<Agent> GetAgent(string id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,13 +27,12 @@ public partial class ConversationService
|
|||
#endif
|
||||
|
||||
message.CurrentAgentId = agent.Id;
|
||||
message.CreatedAt = DateTime.UtcNow;
|
||||
if (string.IsNullOrEmpty(message.SenderId))
|
||||
{
|
||||
message.SenderId = _user.Id;
|
||||
}
|
||||
|
||||
_storage.Append(_conversationId, message);
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var dialogs = conv.GetDialogHistory();
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ public partial class ConversationService
|
|||
hook.SetAgent(agent)
|
||||
.SetConversation(conversation);
|
||||
|
||||
if (replyMessage == null)
|
||||
if (replyMessage == null || string.IsNullOrEmpty(replyMessage.FunctionName))
|
||||
{
|
||||
await hook.OnMessageReceived(message);
|
||||
}
|
||||
|
|
@ -72,6 +71,12 @@ public partial class ConversationService
|
|||
}
|
||||
}
|
||||
|
||||
// Persist to storage
|
||||
_storage.Append(_conversationId, message);
|
||||
|
||||
// Add to thread
|
||||
dialogs.Add(RoleDialogModel.From(message));
|
||||
|
||||
if (!stopCompletion)
|
||||
{
|
||||
// Routing with reasoning
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ public partial class ConversationService : IConversationService
|
|||
public async Task UpdateBreakpoint(bool resetStates = false)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
db.UpdateConversationBreakpoint(_conversationId, DateTime.UtcNow);
|
||||
var routingCtx = _services.GetRequiredService<IRoutingContext>();
|
||||
var messageId = routingCtx.MessageId;
|
||||
db.UpdateConversationBreakpoint(_conversationId, messageId, DateTime.UtcNow);
|
||||
|
||||
// Reset states
|
||||
if (resetStates)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Models;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public partial class ConversationService : IConversationService
|
||||
|
|
@ -111,8 +113,8 @@ public partial class ConversationService : IConversationService
|
|||
if (fromBreakpoint)
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var conversation = db.GetConversation(_conversationId);
|
||||
dialogs = dialogs.Where(x => x.CreatedAt >= conversation.Breakpoint).ToList();
|
||||
var breakpoint = db.GetConversationBreakpoint(_conversationId);
|
||||
dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint).ToList();
|
||||
}
|
||||
|
||||
return dialogs
|
||||
|
|
@ -120,10 +122,10 @@ public partial class ConversationService : IConversationService
|
|||
.ToList();
|
||||
}
|
||||
|
||||
public void SetConversationId(string conversationId, List<string> states)
|
||||
public void SetConversationId(string conversationId, List<MessageState> states)
|
||||
{
|
||||
_conversationId = conversationId;
|
||||
_state.Load(_conversationId);
|
||||
states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1]));
|
||||
states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Users.Enums;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -31,7 +33,7 @@ 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)
|
||||
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
|
|
@ -42,12 +44,12 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
var currentValue = value.ToString();
|
||||
var hooks = _services.GetServices<IConversationHook>();
|
||||
|
||||
if (_states.TryGetValue(name, out var values))
|
||||
if (ContainsState(name) && _states.TryGetValue(name, out var pair))
|
||||
{
|
||||
preValue = values?.LastOrDefault()?.Data ?? string.Empty;
|
||||
preValue = pair?.Values.LastOrDefault()?.Data ?? string.Empty;
|
||||
}
|
||||
|
||||
if (!_states.ContainsKey(name) || preValue != currentValue)
|
||||
if (!ContainsState(name) || preValue != currentValue)
|
||||
{
|
||||
_logger.LogInformation($"[STATE] {name} = {value}");
|
||||
foreach (var hook in hooks)
|
||||
|
|
@ -55,19 +57,30 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
hook.OnStateChanged(name, preValue, currentValue).Wait();
|
||||
}
|
||||
|
||||
var stateValue = new StateValue
|
||||
var routingCtx = _services.GetRequiredService<IRoutingContext>();
|
||||
var newPair = new StateKeyValue
|
||||
{
|
||||
Data = currentValue,
|
||||
UpdateTime = DateTime.UtcNow
|
||||
Key = name,
|
||||
Versioning = isNeedVersion
|
||||
};
|
||||
|
||||
if (!_states.ContainsKey(name) || !isNeedVersion)
|
||||
var newValue = new StateValue
|
||||
{
|
||||
_states[name] = new List<StateValue> { stateValue };
|
||||
Data = currentValue,
|
||||
MessageId = routingCtx.MessageId,
|
||||
Active = true,
|
||||
ActiveRounds = activeRounds > 0 ? activeRounds : -1,
|
||||
UpdateTime = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
if (!isNeedVersion || !_states.ContainsKey(name))
|
||||
{
|
||||
newPair.Values = new List<StateValue> { newValue };
|
||||
_states[name] = newPair;
|
||||
}
|
||||
else
|
||||
{
|
||||
_states[name].Add(stateValue);
|
||||
_states[name].Values.Add(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,16 +91,45 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
{
|
||||
_conversationId = conversationId;
|
||||
|
||||
var routingCtx = _services.GetRequiredService<IRoutingContext>();
|
||||
var curMsgId = routingCtx.MessageId;
|
||||
_states = _db.GetConversationStates(_conversationId);
|
||||
var dialogs = _db.GetConversationDialogs(_conversationId);
|
||||
var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User || x.MetaData?.Role == UserRole.Client)
|
||||
.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())
|
||||
{
|
||||
foreach (var state in _states)
|
||||
{
|
||||
var value = state.Value?.LastOrDefault()?.Data ?? string.Empty;
|
||||
curStates[state.Key] = value;
|
||||
_logger.LogInformation($"[STATE] {state.Key} : {value}");
|
||||
var value = state.Value?.Values?.LastOrDefault();
|
||||
if (value == null || !value.Active) continue;
|
||||
|
||||
if (value.ActiveRounds > 0)
|
||||
{
|
||||
var stateMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(x.MetaData?.MessageId) && x.MetaData.MessageId == value.MessageId);
|
||||
if (stateMsgIndex >= 0 && curMsgIndex - stateMsgIndex >= value.ActiveRounds)
|
||||
{
|
||||
state.Value.Values.Add(new StateValue
|
||||
{
|
||||
Data = value.Data,
|
||||
MessageId = !string.IsNullOrEmpty(curMsgId) ? curMsgId : value.MessageId,
|
||||
Active = false,
|
||||
ActiveRounds = value.ActiveRounds,
|
||||
UpdateTime = DateTime.UtcNow
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var data = value.Data ?? string.Empty;
|
||||
curStates[state.Key] = data;
|
||||
_logger.LogInformation($"[STATE] {state.Key} : {data}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +154,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
|
||||
foreach (var dic in _states)
|
||||
{
|
||||
states.Add(new StateKeyValue(dic.Key, dic.Value));
|
||||
states.Add(dic.Value);
|
||||
}
|
||||
|
||||
_db.UpdateConversationStates(_conversationId, states);
|
||||
|
|
@ -121,7 +163,27 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
|
||||
public void CleanStates()
|
||||
{
|
||||
_states.Clear();
|
||||
var routingCtx = _services.GetRequiredService<IRoutingContext>();
|
||||
var curMsgId = routingCtx.MessageId;
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
foreach (var key in _states.Keys)
|
||||
{
|
||||
var value = _states[key];
|
||||
if (value == null || !value.Versioning || value.Values.IsNullOrEmpty()) continue;
|
||||
|
||||
var lastValue = value.Values.LastOrDefault();
|
||||
if (lastValue == null || !lastValue.Active) continue;
|
||||
|
||||
value.Values.Add(new StateValue
|
||||
{
|
||||
Data = lastValue.Data,
|
||||
MessageId = !string.IsNullOrEmpty(curMsgId) ? curMsgId : lastValue.MessageId,
|
||||
Active = false,
|
||||
ActiveRounds = lastValue.ActiveRounds,
|
||||
UpdateTime = utcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetStates()
|
||||
|
|
@ -129,19 +191,22 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
var curStates = new Dictionary<string, string>();
|
||||
foreach (var state in _states)
|
||||
{
|
||||
curStates[state.Key] = state.Value?.LastOrDefault()?.Data ?? string.Empty;
|
||||
var value = state.Value?.Values?.LastOrDefault();
|
||||
if (value == null || !value.Active) continue;
|
||||
|
||||
curStates[state.Key] = value.Data ?? string.Empty;
|
||||
}
|
||||
return curStates;
|
||||
}
|
||||
|
||||
public string GetState(string name, string defaultValue = "")
|
||||
{
|
||||
if (!_states.ContainsKey(name) || _states[name].IsNullOrEmpty())
|
||||
if (!_states.ContainsKey(name) || _states[name].Values.IsNullOrEmpty() || !_states[name].Values.Last().Active)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return _states[name].Last().Data;
|
||||
return _states[name].Values.Last().Data;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -152,8 +217,9 @@ public class ConversationStateService : IConversationStateService, IDisposable
|
|||
public bool ContainsState(string name)
|
||||
{
|
||||
return _states.ContainsKey(name)
|
||||
&& !_states[name].IsNullOrEmpty()
|
||||
&& !string.IsNullOrEmpty(_states[name].Last().Data);
|
||||
&& !_states[name].Values.IsNullOrEmpty()
|
||||
&& _states[name].Values.LastOrDefault()?.Active == true
|
||||
&& !string.IsNullOrEmpty(_states[name].Values.Last().Data);
|
||||
}
|
||||
|
||||
public void SaveStateByArgs(JsonDocument args)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using BotSharp.Abstraction.Conversations.Enums;
|
|||
using BotSharp.Abstraction.Evaluations;
|
||||
using BotSharp.Abstraction.Evaluations.Models;
|
||||
using BotSharp.Abstraction.Evaluations.Settings;
|
||||
using BotSharp.Abstraction.Models;
|
||||
using BotSharp.Abstraction.Templating;
|
||||
using System.Drawing;
|
||||
|
||||
|
|
@ -88,15 +89,19 @@ public class EvaluatingService : IEvaluatingService
|
|||
private async Task<RoleDialogModel> SendMessage(string agentId, string conversationId, string text)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
conv.SetConversationId(conversationId, new List<string>
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, text);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
|
||||
conv.SetConversationId(conversationId, new List<MessageState>
|
||||
{
|
||||
$"channel={ConversationChannel.OpenAPI}"
|
||||
new MessageState("channel", ConversationChannel.OpenAPI)
|
||||
});
|
||||
|
||||
RoleDialogModel response = default;
|
||||
|
||||
await conv.SendMessage(agentId,
|
||||
new RoleDialogModel(AgentRole.User, text),
|
||||
inputMsg,
|
||||
replyMessage: null,
|
||||
async msg => response = msg,
|
||||
_ => Task.CompletedTask,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.MLTasks.Settings;
|
||||
|
||||
|
|
@ -11,31 +10,25 @@ public class CompletionProvider
|
|||
string? model = null,
|
||||
AgentLlmConfig? agentConfig = null)
|
||||
{
|
||||
var state = services.GetRequiredService<IConversationStateService>();
|
||||
var agentSetting = services.GetRequiredService<AgentSettings>();
|
||||
|
||||
if (string.IsNullOrEmpty(provider))
|
||||
{
|
||||
provider = agentConfig?.Provider ?? agentSetting.LlmConfig?.Provider;
|
||||
provider = state.GetState("provider", provider ?? "azure-openai");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model))
|
||||
{
|
||||
model = agentConfig?.Model ?? agentSetting.LlmConfig?.Model;
|
||||
model = state.GetState("model", model ?? "gpt-35-turbo-4k");
|
||||
}
|
||||
|
||||
var settingsService = services.GetRequiredService<ILlmProviderService>();
|
||||
|
||||
(provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig);
|
||||
|
||||
var settings = settingsService.GetSetting(provider, model);
|
||||
|
||||
if (settings.Type == LlmModelType.Text)
|
||||
{
|
||||
return GetTextCompletion(services, provider: provider, model: model);
|
||||
return GetTextCompletion(services,
|
||||
provider: provider,
|
||||
model: model,
|
||||
agentConfig: agentConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetChatCompletion(services, provider: provider, model: model);
|
||||
return GetChatCompletion(services,
|
||||
provider: provider,
|
||||
model: model,
|
||||
agentConfig: agentConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,20 +38,7 @@ public class CompletionProvider
|
|||
AgentLlmConfig? agentConfig = null)
|
||||
{
|
||||
var completions = services.GetServices<IChatCompletion>();
|
||||
var agentSetting = services.GetRequiredService<AgentSettings>();
|
||||
var state = services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
if (string.IsNullOrEmpty(provider))
|
||||
{
|
||||
provider = agentConfig?.Provider ?? agentSetting.LlmConfig?.Provider;
|
||||
provider = state.GetState("provider", provider ?? "azure-openai");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model))
|
||||
{
|
||||
model = agentConfig?.Model ?? agentSetting.LlmConfig?.Model;
|
||||
model = state.GetState("model", model ?? "gpt-35-turbo-4k");
|
||||
}
|
||||
(provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig);
|
||||
|
||||
var completer = completions.FirstOrDefault(x => x.Provider == provider);
|
||||
if (completer == null)
|
||||
|
|
@ -72,12 +52,11 @@ public class CompletionProvider
|
|||
return completer;
|
||||
}
|
||||
|
||||
public static ITextCompletion GetTextCompletion(IServiceProvider services,
|
||||
string? provider = null,
|
||||
private static (string, string) GetProviderAndModel(IServiceProvider services,
|
||||
string? provider = null,
|
||||
string? model = null,
|
||||
AgentLlmConfig? agentConfig = null)
|
||||
{
|
||||
var completions = services.GetServices<ITextCompletion>();
|
||||
var agentSetting = services.GetRequiredService<AgentSettings>();
|
||||
var state = services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
|
|
@ -90,9 +69,32 @@ public class CompletionProvider
|
|||
if (string.IsNullOrEmpty(model))
|
||||
{
|
||||
model = agentConfig?.Model ?? agentSetting.LlmConfig?.Model;
|
||||
model = state.GetState("model", model ?? "gpt-35-turbo-instruct");
|
||||
if (state.ContainsState("model"))
|
||||
{
|
||||
model = state.GetState("model", model ?? "gpt-35-turbo-4k");
|
||||
}
|
||||
else if (state.ContainsState("model_id"))
|
||||
{
|
||||
var modelId = state.GetState("model_id");
|
||||
var llmProviderService = services.GetRequiredService<ILlmProviderService>();
|
||||
model = llmProviderService.GetProviderModel(provider, modelId)?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
state.SetState("provider", provider);
|
||||
state.SetState("model", model);
|
||||
|
||||
return (provider, model);
|
||||
}
|
||||
public static ITextCompletion GetTextCompletion(IServiceProvider services,
|
||||
string? provider = null,
|
||||
string? model = null,
|
||||
AgentLlmConfig? agentConfig = null)
|
||||
{
|
||||
var completions = services.GetServices<ITextCompletion>();
|
||||
|
||||
(provider, model) = GetProviderAndModel(services, provider: provider, model: model, agentConfig: agentConfig);
|
||||
|
||||
var completer = completions.FirstOrDefault(x => x.Provider == provider);
|
||||
if (completer == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ public partial class InstructService : IInstructService
|
|||
|
||||
var completer = CompletionProvider.GetCompletion(_services,
|
||||
agentConfig: agent.LlmConfig);
|
||||
|
||||
var response = new InstructResult
|
||||
{
|
||||
MessageId = message.MessageId
|
||||
|
|
|
|||
|
|
@ -189,9 +189,12 @@ public class BotSharpDbContext : Database, IBotSharpRepository
|
|||
public void UpdateConversationTitle(string conversationId, string title)
|
||||
=> new NotImplementedException();
|
||||
|
||||
public void UpdateConversationBreakpoint(string conversationId, DateTime breakpoint)
|
||||
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
|
||||
=> new NotImplementedException();
|
||||
|
||||
|
||||
public DateTime GetConversationBreakpoint(string conversationId)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
|
||||
=> new NotImplementedException();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ namespace BotSharp.Core.Repository
|
|||
{
|
||||
public void CreateNewConversation(Conversation conversation)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
conversation.CreatedTime = utcNow;
|
||||
conversation.UpdatedTime = utcNow;
|
||||
|
||||
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, conversation.Id);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
|
|
@ -42,6 +46,20 @@ namespace BotSharp.Core.Repository
|
|||
}).ToList();
|
||||
File.WriteAllText(stateFile, JsonSerializer.Serialize(initialStates, _options));
|
||||
}
|
||||
|
||||
var breakpointFile = Path.Combine(dir, BREAKPOINT_FILE);
|
||||
if (!File.Exists(breakpointFile))
|
||||
{
|
||||
var initialBreakpoints = new List<ConversationBreakpoint>
|
||||
{
|
||||
new ConversationBreakpoint()
|
||||
{
|
||||
Breakpoint = utcNow.AddMilliseconds(-100),
|
||||
CreatedTime = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
File.WriteAllText(breakpointFile, JsonSerializer.Serialize(initialBreakpoints, _options));
|
||||
}
|
||||
}
|
||||
|
||||
public bool DeleteConversations(IEnumerable<string> conversationIds)
|
||||
|
|
@ -117,6 +135,7 @@ namespace BotSharp.Core.Repository
|
|||
var conv = JsonSerializer.Deserialize<Conversation>(json, _options);
|
||||
if (conv != null)
|
||||
{
|
||||
conv.DialogCount += dialogs.Count();
|
||||
conv.UpdatedTime = DateTime.UtcNow;
|
||||
File.WriteAllText(convFile, JsonSerializer.Serialize(conv, _options));
|
||||
}
|
||||
|
|
@ -141,23 +160,63 @@ namespace BotSharp.Core.Repository
|
|||
}
|
||||
}
|
||||
|
||||
public void UpdateConversationBreakpoint(string conversationId, DateTime breakpoint)
|
||||
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
|
||||
{
|
||||
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)
|
||||
var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE);
|
||||
|
||||
if (!File.Exists(breakpointFile))
|
||||
{
|
||||
record.UpdatedTime = DateTime.UtcNow;
|
||||
record.Breakpoint = breakpoint;
|
||||
File.WriteAllText(convFile, JsonSerializer.Serialize(record, _options));
|
||||
File.Create(breakpointFile);
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(breakpointFile);
|
||||
var records = JsonSerializer.Deserialize<List<ConversationBreakpoint>>(content, _options);
|
||||
var newBreakpoint = new List<ConversationBreakpoint>()
|
||||
{
|
||||
new ConversationBreakpoint
|
||||
{
|
||||
MessageId = messageId,
|
||||
Breakpoint = breakpoint,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
if (records != null && !records.IsNullOrEmpty())
|
||||
{
|
||||
records = records.Concat(newBreakpoint).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
records = newBreakpoint;
|
||||
}
|
||||
|
||||
File.WriteAllText(breakpointFile, JsonSerializer.Serialize(records, _options));
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime GetConversationBreakpoint(string conversationId)
|
||||
{
|
||||
var convDir = FindConversationDirectory(conversationId);
|
||||
if (string.IsNullOrEmpty(convDir))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var breakpointFile = Path.Combine(convDir, BREAKPOINT_FILE);
|
||||
if (!File.Exists(breakpointFile))
|
||||
{
|
||||
File.Create(breakpointFile);
|
||||
}
|
||||
|
||||
var content = File.ReadAllText(breakpointFile);
|
||||
var records = JsonSerializer.Deserialize<List<ConversationBreakpoint>>(content, _options);
|
||||
|
||||
return records?.LastOrDefault()?.Breakpoint ?? default;
|
||||
}
|
||||
|
||||
public ConversationState GetConversationStates(string conversationId)
|
||||
{
|
||||
var states = new List<StateKeyValue>();
|
||||
|
|
@ -256,12 +315,34 @@ namespace BotSharp.Core.Repository
|
|||
if (record == null) continue;
|
||||
|
||||
var matched = true;
|
||||
if (filter?.Id != null) matched = matched && record.Id == filter.Id;
|
||||
if (filter?.AgentId != null) matched = matched && record.AgentId == filter.AgentId;
|
||||
if (filter?.Status != null) matched = matched && record.Status == filter.Status;
|
||||
if (filter?.Channel != null) matched = matched && record.Channel == filter.Channel;
|
||||
if (filter?.UserId != null) matched = matched && record.UserId == filter.UserId;
|
||||
if (filter?.TaskId != null) matched = matched && record.TaskId == filter.TaskId;
|
||||
if (filter?.Id != null)
|
||||
{
|
||||
matched = matched && record.Id == filter.Id;
|
||||
}
|
||||
if (filter?.AgentId != null)
|
||||
{
|
||||
matched = matched && record.AgentId == filter.AgentId;
|
||||
}
|
||||
if (filter?.Status != null)
|
||||
{
|
||||
matched = matched && record.Status == filter.Status;
|
||||
}
|
||||
if (filter?.Channel != null)
|
||||
{
|
||||
matched = matched && record.Channel == filter.Channel;
|
||||
}
|
||||
if (filter?.UserId != null)
|
||||
{
|
||||
matched = matched && record.UserId == filter.UserId;
|
||||
}
|
||||
if (filter?.TaskId != null)
|
||||
{
|
||||
matched = matched && record.TaskId == filter.TaskId;
|
||||
}
|
||||
if (filter?.StartTime != null)
|
||||
{
|
||||
matched = matched && record.CreatedTime >= filter.StartTime.Value;
|
||||
}
|
||||
|
||||
// Check states
|
||||
if (filter != null && !filter.States.IsNullOrEmpty())
|
||||
|
|
@ -331,23 +412,40 @@ namespace BotSharp.Core.Repository
|
|||
batchSize = batchLimit;
|
||||
}
|
||||
|
||||
if (bufferHours <= 0)
|
||||
{
|
||||
bufferHours = 12;
|
||||
}
|
||||
|
||||
if (messageLimit <= 0)
|
||||
{
|
||||
messageLimit = 2;
|
||||
}
|
||||
|
||||
foreach (var d in Directory.GetDirectories(dir))
|
||||
{
|
||||
var convFile = Path.Combine(d, CONVERSATION_FILE);
|
||||
if (!File.Exists(convFile))
|
||||
{
|
||||
Directory.Delete(d, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(convFile);
|
||||
var conv = JsonSerializer.Deserialize<Conversation>(json, _options);
|
||||
if (conv == null || conv.UpdatedTime > utcNow.AddHours(-bufferHours))
|
||||
|
||||
if (conv == null)
|
||||
{
|
||||
Directory.Delete(d, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (conv.UpdatedTime > utcNow.AddHours(-bufferHours))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dialogs = GetConversationDialogs(conv.Id);
|
||||
if (dialogs.Count <= messageLimit)
|
||||
if (conv.DialogCount <= messageLimit)
|
||||
{
|
||||
ids.Add(conv.Id);
|
||||
if (ids.Count >= batchSize)
|
||||
|
|
@ -376,14 +474,19 @@ namespace BotSharp.Core.Repository
|
|||
if (foundIdx < 0) return false;
|
||||
|
||||
// Handle truncated dialogs
|
||||
var isSaved = HandleTruncatedDialogs(dialogDir, dialogs, foundIdx);
|
||||
var isSaved = HandleTruncatedDialogs(convDir, dialogDir, dialogs, foundIdx);
|
||||
if (!isSaved) return false;
|
||||
|
||||
// Handle truncated states
|
||||
var refTime = dialogs.ElementAt(foundIdx).MetaData.CreateTime;
|
||||
var stateDir = Path.Combine(convDir, STATE_FILE);
|
||||
var states = CollectConversationStates(stateDir);
|
||||
isSaved = HandleTruncatedStates(stateDir, states, refTime);
|
||||
isSaved = HandleTruncatedStates(stateDir, states, messageId, refTime);
|
||||
|
||||
// Handle truncated breakpoints
|
||||
var breakpointDir = Path.Combine(convDir, BREAKPOINT_FILE);
|
||||
var breakpoints = CollectConversationBreakpoints(breakpointDir);
|
||||
isSaved = HandleTruncatedBreakpoints(breakpointDir, breakpoints, messageId);
|
||||
|
||||
// Remove logs
|
||||
if (cleanLog)
|
||||
|
|
@ -445,7 +548,7 @@ namespace BotSharp.Core.Repository
|
|||
foreach (var element in dialogs)
|
||||
{
|
||||
var meta = element.MetaData;
|
||||
var createTime = meta.CreateTime.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.InvariantCulture);
|
||||
var createTime = meta.CreateTime.ToString("MM/dd/yyyy hh:mm:ss.ffffff tt", CultureInfo.InvariantCulture);
|
||||
var metaStr = $"{createTime}|{meta.Role}|{meta.AgentId}|{meta.MessageId}|{meta.SenderId}|{meta.FunctionName}|{element.RichContent}";
|
||||
dialogTexts.Add(metaStr);
|
||||
var content = $" - {element.Content}";
|
||||
|
|
@ -467,19 +570,47 @@ namespace BotSharp.Core.Repository
|
|||
return states ?? new List<StateKeyValue>();
|
||||
}
|
||||
|
||||
private bool HandleTruncatedDialogs(string dialogDir, List<DialogElement> dialogs, int foundIdx)
|
||||
private List<ConversationBreakpoint> CollectConversationBreakpoints(string breakpointFile)
|
||||
{
|
||||
var breakpoints = new List<ConversationBreakpoint>();
|
||||
if (!File.Exists(breakpointFile)) return breakpoints;
|
||||
|
||||
var content = File.ReadAllText(breakpointFile);
|
||||
if (string.IsNullOrEmpty(content)) return breakpoints;
|
||||
|
||||
breakpoints = JsonSerializer.Deserialize<List<ConversationBreakpoint>>(content, _options);
|
||||
return breakpoints ?? new List<ConversationBreakpoint>();
|
||||
}
|
||||
|
||||
private bool HandleTruncatedDialogs(string convDir, string dialogDir, List<DialogElement> dialogs, int foundIdx)
|
||||
{
|
||||
var truncatedDialogs = dialogs.Where((x, idx) => idx < foundIdx).ToList();
|
||||
var isSaved = SaveTruncatedDialogs(dialogDir, truncatedDialogs);
|
||||
var convFile = Path.Combine(convDir, CONVERSATION_FILE);
|
||||
var convJson = File.ReadAllText(convFile);
|
||||
var conv = JsonSerializer.Deserialize<Conversation>(convJson, _options);
|
||||
if (conv != null)
|
||||
{
|
||||
conv.DialogCount = truncatedDialogs.Count;
|
||||
File.WriteAllText(convFile, JsonSerializer.Serialize(conv, _options));
|
||||
}
|
||||
return isSaved;
|
||||
}
|
||||
|
||||
private bool HandleTruncatedStates(string stateDir, List<StateKeyValue> states, DateTime refTime)
|
||||
private bool HandleTruncatedStates(string stateDir, List<StateKeyValue> states, string refMsgId, DateTime refTime)
|
||||
{
|
||||
var truncatedStates = new List<StateKeyValue>();
|
||||
foreach (var state in states)
|
||||
{
|
||||
var values = state.Values.Where(x => x.UpdateTime < refTime).ToList();
|
||||
if (!state.Versioning)
|
||||
{
|
||||
truncatedStates.Add(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = state.Values.Where(x => x.MessageId != refMsgId)
|
||||
.Where(x => x.UpdateTime < refTime)
|
||||
.ToList();
|
||||
if (values.Count == 0) continue;
|
||||
|
||||
state.Values = values;
|
||||
|
|
@ -490,6 +621,16 @@ namespace BotSharp.Core.Repository
|
|||
return isSaved;
|
||||
}
|
||||
|
||||
private bool HandleTruncatedBreakpoints(string breakpointDir, List<ConversationBreakpoint> breakpoints, string refMessageId)
|
||||
{
|
||||
var targetIdx = breakpoints.FindIndex(x => x.MessageId == refMessageId);
|
||||
var truncatedBreakpoints = breakpoints?.Where((x, idx) => idx < targetIdx)?
|
||||
.ToList() ?? new List<ConversationBreakpoint>();
|
||||
|
||||
var isSaved = SaveTruncatedBreakpoints(breakpointDir, truncatedBreakpoints);
|
||||
return isSaved;
|
||||
}
|
||||
|
||||
private bool HandleTruncatedLogs(string convDir, DateTime refTime)
|
||||
{
|
||||
var contentLogDir = Path.Combine(convDir, "content_log");
|
||||
|
|
@ -547,6 +688,16 @@ namespace BotSharp.Core.Repository
|
|||
File.WriteAllText(stateDir, stateStr);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SaveTruncatedBreakpoints(string breakpointDir, List<ConversationBreakpoint> breakpoints)
|
||||
{
|
||||
if (string.IsNullOrEmpty(breakpointDir) || breakpoints == null) return false;
|
||||
if (!File.Exists(breakpointDir)) File.Create(breakpointDir);
|
||||
|
||||
var breakpointStr = JsonSerializer.Serialize(breakpoints, _options);
|
||||
File.WriteAllText(breakpointDir, breakpointStr);
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,11 +71,7 @@ namespace BotSharp.Core.Repository
|
|||
log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
|
||||
var convDir = FindConversationDirectory(log.ConversationId);
|
||||
if (string.IsNullOrEmpty(convDir))
|
||||
{
|
||||
convDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, log.ConversationId);
|
||||
Directory.CreateDirectory(convDir);
|
||||
}
|
||||
if (string.IsNullOrEmpty(convDir)) return;
|
||||
|
||||
var logDir = Path.Combine(convDir, "content_log");
|
||||
if (!Directory.Exists(logDir))
|
||||
|
|
@ -120,11 +116,7 @@ namespace BotSharp.Core.Repository
|
|||
log.MessageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
|
||||
var convDir = FindConversationDirectory(log.ConversationId);
|
||||
if (string.IsNullOrEmpty(convDir))
|
||||
{
|
||||
convDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, log.ConversationId);
|
||||
Directory.CreateDirectory(convDir);
|
||||
}
|
||||
if (string.IsNullOrEmpty(convDir)) return;
|
||||
|
||||
var logDir = Path.Combine(convDir, "state_log");
|
||||
if (!Directory.Exists(logDir))
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public partial class FileRepository : IBotSharpRepository
|
|||
private const string STATS_FILE = "stats.json";
|
||||
private const string DIALOG_FILE = "dialogs.txt";
|
||||
private const string STATE_FILE = "state.json";
|
||||
private const string BREAKPOINT_FILE = "breakpoint.json";
|
||||
private const string EXECUTION_LOG_FILE = "execution.log";
|
||||
private const string PLUGIN_CONFIG_FILE = "config.json";
|
||||
private const string AGENT_TASK_PREFIX = "#metadata";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ public class TaskCompletedRoutingHandler : RoutingHandlerBase, IRoutingHandler
|
|||
|
||||
public List<string> Planers => new List<string>
|
||||
{
|
||||
nameof(NaivePlanner),
|
||||
nameof(HFPlanner)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@ Route to the last handling agent in priority.
|
|||
{% if expected_next_action_agent != empty -%}
|
||||
Expected next action agent is {{ expected_next_action_agent }}.
|
||||
{%- endif %}
|
||||
If user completes the task, use function task_completed.
|
||||
If user wants to speak to customer service, use function human_intervention_needed.
|
||||
|
|
@ -52,7 +52,8 @@ public class RateLimitConversationHook : ConversationHookBase
|
|||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var results = await convService.GetConversations(new ConversationFilter
|
||||
{
|
||||
UserId = user.Id
|
||||
UserId = user.Id,
|
||||
StartTime = DateTime.UtcNow.AddHours(-24),
|
||||
});
|
||||
|
||||
if (results.Count > rateLimit.MaxConversationPerDay)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,18 @@ namespace BotSharp.OpenAPI.BackgroundServices
|
|||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Conversation Timeout Service is running.");
|
||||
_logger.LogInformation("Conversation Timeout Service is running...");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await DoWork(stoppingToken);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task DoWork(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Conversation Timeout Service is doing work...");
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
|
|
@ -25,7 +36,6 @@ namespace BotSharp.OpenAPI.BackgroundServices
|
|||
try
|
||||
{
|
||||
await CleanIdleConversationsAsync();
|
||||
await CloseIdleConversationsAsync(TimeSpan.FromMinutes(10));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -43,37 +53,6 @@ namespace BotSharp.OpenAPI.BackgroundServices
|
|||
await base.StopAsync(stoppingToken);
|
||||
}
|
||||
|
||||
private async Task CloseIdleConversationsAsync(TimeSpan conversationIdleTimeout)
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
var conversationService = scope.ServiceProvider.GetRequiredService<IConversationService>();
|
||||
var hooks = scope.ServiceProvider.GetServices<IConversationHook>()
|
||||
.OrderBy(x => x.Priority)
|
||||
.ToList();
|
||||
var moment = DateTime.UtcNow.Add(-conversationIdleTimeout);
|
||||
var conversations = (await conversationService.GetLastConversations()).Where(c => c.CreatedTime <= moment);
|
||||
foreach (var conversation in conversations)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = new RoleDialogModel(AgentRole.Assistant, "End the conversation due to timeout.")
|
||||
{
|
||||
StopCompletion = true,
|
||||
FunctionName = "conversation_end"
|
||||
};
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
await hook.OnConversationEnding(response);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error occurred closing conversation #{conversation.Id}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanIdleConversationsAsync()
|
||||
{
|
||||
using var scope = _services.CreateScope();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Abstraction.Users.Models;
|
||||
|
||||
namespace BotSharp.OpenAPI.Controllers;
|
||||
|
|
@ -65,7 +66,7 @@ public class ConversationController : ControllerBase
|
|||
public async Task<IEnumerable<ChatResponseModel>> GetDialogs([FromRoute] string conversationId)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
conv.SetConversationId(conversationId, new List<string>());
|
||||
conv.SetConversationId(conversationId, new List<MessageState>());
|
||||
var history = conv.GetDialogHistory(fromBreakpoint: false);
|
||||
|
||||
var userService = _services.GetRequiredService<IUserService>();
|
||||
|
|
@ -161,6 +162,9 @@ public class ConversationController : ControllerBase
|
|||
}
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
|
||||
|
||||
conv.SetConversationId(conversationId, input.States);
|
||||
conv.States.SetState("channel", input.Channel)
|
||||
.SetState("provider", input.Provider)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ public class InstructModeController : ControllerBase
|
|||
[FromBody] InstructMessageModel input)
|
||||
{
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
input.States.ForEach(x => state.SetState(x.Split('=')[0], x.Split('=')[1]));
|
||||
input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
|
||||
state.SetState("provider", input.Provider)
|
||||
.SetState("model", input.Model)
|
||||
.SetState("model_id", input.ModelId)
|
||||
.SetState("instruction", input.Instruction)
|
||||
.SetState("input_text", input.Text);
|
||||
|
||||
|
|
@ -43,9 +44,10 @@ public class InstructModeController : ControllerBase
|
|||
public async Task<string> TextCompletion([FromBody] IncomingMessageModel input)
|
||||
{
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
input.States.ForEach(x => state.SetState(x.Split('=')[0], x.Split('=')[1]));
|
||||
input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
|
||||
state.SetState("provider", input.Provider)
|
||||
.SetState("model", input.Model);
|
||||
.SetState("model", input.Model)
|
||||
.SetState("model_id", input.ModelId);
|
||||
|
||||
var textCompletion = CompletionProvider.GetTextCompletion(_services);
|
||||
return await textCompletion.GetCompletion(input.Text, Guid.Empty.ToString(), Guid.NewGuid().ToString());
|
||||
|
|
@ -55,9 +57,10 @@ public class InstructModeController : ControllerBase
|
|||
public async Task<string> ChatCompletion([FromBody] IncomingMessageModel input)
|
||||
{
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
input.States.ForEach(x => state.SetState(x.Split('=')[0], x.Split('=')[1]));
|
||||
input.States.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds));
|
||||
state.SetState("provider", input.Provider)
|
||||
.SetState("model", input.Model);
|
||||
.SetState("model", input.Model)
|
||||
.SetState("model_id", input.ModelId);
|
||||
|
||||
var textCompletion = CompletionProvider.GetChatCompletion(_services);
|
||||
var message = await textCompletion.GetChatCompletions(new Agent()
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ namespace BotSharp.OpenAPI.ViewModels.Agents;
|
|||
|
||||
public class RoutingRuleUpdateModel
|
||||
{
|
||||
public string Field { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string? Field { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? FieldType { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public string? RedirectTo { get; set; }
|
||||
|
||||
|
|
@ -20,6 +22,8 @@ public class RoutingRuleUpdateModel
|
|||
{
|
||||
Field = model.Field,
|
||||
Description = model.Description,
|
||||
Type = model.Type,
|
||||
FieldType = model.FieldType,
|
||||
Required = model.Required,
|
||||
RedirectTo = model.RedirectTo
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using BotSharp.Abstraction.Evaluations.Settings;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.Plugins;
|
||||
using BotSharp.Abstraction.Settings;
|
||||
|
|
@ -6,7 +5,6 @@ using BotSharp.Plugin.AzureOpenAI.Providers;
|
|||
using BotSharp.Plugin.AzureOpenAI.Settings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace BotSharp.Platform.AzureAi;
|
||||
|
||||
|
|
@ -16,8 +14,8 @@ namespace BotSharp.Platform.AzureAi;
|
|||
public class AzureOpenAiPlugin : IBotSharpPlugin
|
||||
{
|
||||
public string Id => "65185362-392c-44fd-a023-95a198824436";
|
||||
public string Name => "Azure OpenAI";
|
||||
public string Description => "Azure OpenAI Service including text generation, text to image and other AI services.";
|
||||
public string Name => "OpenAI/ Azure OpenAI";
|
||||
public string Description => "OpenAI/ Azure OpenAI Service including text generation, text to image and other AI services.";
|
||||
public string IconUrl => "https://nanfor.com/cdn/shop/files/cursos-propios-Azure-openAI.jpg?v=1692877741";
|
||||
|
||||
public void RegisterDI(IServiceCollection services, IConfiguration config)
|
||||
|
|
@ -30,5 +28,6 @@ public class AzureOpenAiPlugin : IBotSharpPlugin
|
|||
|
||||
services.AddScoped<ITextCompletion, TextCompletionProvider>();
|
||||
services.AddScoped<IChatCompletion, ChatCompletionProvider>();
|
||||
services.AddScoped<IChatCompletion, OpenAiChatCompletionProvider>();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.14" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.15" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ namespace BotSharp.Plugin.AzureOpenAI.Providers;
|
|||
|
||||
public class ChatCompletionProvider : IChatCompletion
|
||||
{
|
||||
private readonly AzureOpenAiSettings _settings;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private string _model;
|
||||
protected readonly AzureOpenAiSettings _settings;
|
||||
protected readonly IServiceProvider _services;
|
||||
protected readonly ILogger _logger;
|
||||
|
||||
public string Provider => "azure-openai";
|
||||
protected string _model;
|
||||
|
||||
public virtual string Provider => "azure-openai";
|
||||
|
||||
public ChatCompletionProvider(AzureOpenAiSettings settings,
|
||||
ILogger<ChatCompletionProvider> logger,
|
||||
|
|
@ -45,7 +45,7 @@ public class ChatCompletionProvider : IChatCompletion
|
|||
await hook.BeforeGenerating(agent, conversations);
|
||||
}
|
||||
|
||||
var client = ProviderHelper.GetClient(_model, _services);
|
||||
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
||||
var (prompt, chatCompletionsOptions) = PrepareOptions(agent, conversations);
|
||||
chatCompletionsOptions.DeploymentName = _model;
|
||||
var response = client.GetChatCompletions(chatCompletionsOptions);
|
||||
|
|
@ -104,7 +104,7 @@ public class ChatCompletionProvider : IChatCompletion
|
|||
await hook.BeforeGenerating(agent, conversations);
|
||||
}
|
||||
|
||||
var client = ProviderHelper.GetClient(_model, _services);
|
||||
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
||||
var (prompt, chatCompletionsOptions) = PrepareOptions(agent, conversations);
|
||||
|
||||
chatCompletionsOptions.DeploymentName = _model;
|
||||
|
|
@ -161,7 +161,7 @@ public class ChatCompletionProvider : IChatCompletion
|
|||
|
||||
public async Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
|
||||
{
|
||||
var client = ProviderHelper.GetClient(_model, _services);
|
||||
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
||||
var (prompt, chatCompletionsOptions) = PrepareOptions(agent, conversations);
|
||||
chatCompletionsOptions.DeploymentName = _model;
|
||||
var response = await client.GetChatCompletionsStreamingAsync(chatCompletionsOptions);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
using BotSharp.Plugin.AzureOpenAI.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace BotSharp.Plugin.AzureOpenAI.Providers;
|
||||
|
||||
public class OpenAiChatCompletionProvider : ChatCompletionProvider
|
||||
{
|
||||
public override string Provider => "openai";
|
||||
|
||||
public OpenAiChatCompletionProvider(AzureOpenAiSettings settings,
|
||||
ILogger<OpenAiChatCompletionProvider> logger,
|
||||
IServiceProvider services) : base(settings, logger, services)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,13 @@ namespace BotSharp.Plugin.AzureOpenAI.Providers;
|
|||
|
||||
public class ProviderHelper
|
||||
{
|
||||
public static OpenAIClient GetClient(string model, IServiceProvider services)
|
||||
public static OpenAIClient GetClient(string provider, string model, IServiceProvider services)
|
||||
{
|
||||
var settingsService = services.GetRequiredService<ILlmProviderService>();
|
||||
var settings = settingsService.GetSetting("azure-openai", model);
|
||||
var client = new OpenAIClient(new Uri(settings.Endpoint), new AzureKeyCredential(settings.ApiKey));
|
||||
var settings = settingsService.GetSetting(provider, model);
|
||||
var client = provider == "openai" ?
|
||||
new OpenAIClient($"{settings.ApiKey}") :
|
||||
new OpenAIClient(new Uri(settings.Endpoint), new AzureKeyCredential(settings.ApiKey));
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public class TextCompletionProvider : ITextCompletion
|
|||
message
|
||||
})).ToArray());
|
||||
|
||||
var client = ProviderHelper.GetClient(_model, _services);
|
||||
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
||||
|
||||
var completionsOptions = new CompletionsOptions()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -195,14 +195,19 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
|
|||
log += ", states are reset";
|
||||
}
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
var agentId = routing.Context.ConversationId;
|
||||
var agentId = routing.Context.GetCurrentAgentId();
|
||||
var agent = await _agentService.LoadAgent(agentId);
|
||||
|
||||
var input = new ContentLogInputModel()
|
||||
{
|
||||
Name = agent.Name,
|
||||
AgentId = agentId,
|
||||
ConversationId = conversationId,
|
||||
Source = ContentLogSource.FunctionCall,
|
||||
Source = ContentLogSource.HardRule,
|
||||
Message = new RoleDialogModel(AgentRole.Assistant, "OnBreakpointUpdated")
|
||||
{
|
||||
MessageId = _routingCtx.MessageId
|
||||
},
|
||||
Log = log
|
||||
};
|
||||
await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input));
|
||||
|
|
@ -340,7 +345,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
|
|||
Role = input.Message.Role,
|
||||
Content = input.Log,
|
||||
Source = input.Source,
|
||||
CreateTime = DateTime.UtcNow
|
||||
CreateTime = input.Message.CreatedAt
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(output, _options.JsonSerializerOptions);
|
||||
|
|
@ -362,7 +367,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
|
|||
ConversationId = conversationId,
|
||||
MessageId = message.MessageId,
|
||||
States = states,
|
||||
CreateTime = DateTime.UtcNow
|
||||
CreateTime = message.CreatedAt
|
||||
};
|
||||
|
||||
var convSettings = _services.GetRequiredService<ConversationSetting>();
|
||||
|
|
@ -374,4 +379,4 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
|
|||
|
||||
return JsonSerializer.Serialize(log, _options.JsonSerializerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.MLTasks.Settings;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
|
||||
namespace BotSharp.Plugin.ChatbotUI.Controllers;
|
||||
|
||||
|
|
@ -72,6 +73,9 @@ public class ChatbotUiController : ControllerBase
|
|||
.Name;
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(input.ConversationId, message.MessageId);
|
||||
|
||||
conv.SetConversationId(input.ConversationId, input.States);
|
||||
conv.States.SetState("channel", input.Channel)
|
||||
.SetState("provider", "azure-openai")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Conversations.Enums;
|
||||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Messaging.JsonConverters;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
using BotSharp.Abstraction.Models;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace BotSharp.Plugin.MetaMessenger.Services;
|
||||
|
|
@ -52,15 +55,18 @@ public class MessageHandleService
|
|||
});
|
||||
|
||||
// Go to LLM
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, message);
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
conv.SetConversationId(sender, new List<string>
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(sender, inputMsg.MessageId);
|
||||
conv.SetConversationId(sender, new List<MessageState>
|
||||
{
|
||||
$"channel={ConversationChannel.Messenger}"
|
||||
new MessageState("channel", ConversationChannel.Messenger)
|
||||
});
|
||||
|
||||
var replies = new List<IRichMessage>();
|
||||
var result = await conv.SendMessage(agentId,
|
||||
new RoleDialogModel(AgentRole.User, message),
|
||||
inputMsg,
|
||||
replyMessage: null,
|
||||
async msg =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public class ConversationDocument : MongoBase
|
|||
public string Title { get; set; }
|
||||
public string Channel { get; set; }
|
||||
public string Status { get; set; }
|
||||
public int DialogCount { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
public DateTime Breakpoint { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ public class ConversationStateDocument : MongoBase
|
|||
{
|
||||
public string ConversationId { get; set; }
|
||||
public List<StateMongoElement> States { get; set; }
|
||||
public List<BreakpointMongoElement> Breakpoints { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
namespace BotSharp.Plugin.MongoStorage.Models;
|
||||
|
||||
public class BreakpointMongoElement
|
||||
{
|
||||
public string? MessageId { get; set; }
|
||||
public DateTime Breakpoint { get; set; }
|
||||
public DateTime CreatedTime { get; set; }
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ namespace BotSharp.Plugin.MongoStorage.Models;
|
|||
public class StateMongoElement
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public bool Versioning { get; set; }
|
||||
public List<StateValueMongoElement> Values { get; set; }
|
||||
|
||||
public static StateMongoElement ToMongoElement(StateKeyValue state)
|
||||
|
|
@ -12,6 +13,7 @@ public class StateMongoElement
|
|||
return new StateMongoElement
|
||||
{
|
||||
Key = state.Key,
|
||||
Versioning = state.Versioning,
|
||||
Values = state.Values?.Select(x => StateValueMongoElement.ToMongoElement(x))?.ToList() ?? new List<StateValueMongoElement>()
|
||||
};
|
||||
}
|
||||
|
|
@ -21,6 +23,7 @@ public class StateMongoElement
|
|||
return new StateKeyValue
|
||||
{
|
||||
Key = state.Key,
|
||||
Versioning = state.Versioning,
|
||||
Values = state.Values?.Select(x => StateValueMongoElement.ToDomainElement(x))?.ToList() ?? new List<StateValue>()
|
||||
};
|
||||
}
|
||||
|
|
@ -29,6 +32,9 @@ public class StateMongoElement
|
|||
public class StateValueMongoElement
|
||||
{
|
||||
public string Data { get; set; }
|
||||
public string? MessageId { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public int ActiveRounds { get; set; }
|
||||
public DateTime UpdateTime { get; set; }
|
||||
|
||||
public static StateValueMongoElement ToMongoElement(StateValue element)
|
||||
|
|
@ -36,6 +42,9 @@ public class StateValueMongoElement
|
|||
return new StateValueMongoElement
|
||||
{
|
||||
Data = element.Data,
|
||||
MessageId = element.MessageId,
|
||||
Active = element.Active,
|
||||
ActiveRounds = element.ActiveRounds,
|
||||
UpdateTime = element.UpdateTime
|
||||
};
|
||||
}
|
||||
|
|
@ -45,6 +54,9 @@ public class StateValueMongoElement
|
|||
return new StateValue
|
||||
{
|
||||
Data = element.Data,
|
||||
MessageId = element.MessageId,
|
||||
Active = element.Active,
|
||||
ActiveRounds = element.ActiveRounds,
|
||||
UpdateTime = element.UpdateTime
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public partial class MongoRepository
|
|||
{
|
||||
if (conversation == null) return;
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var convDoc = new ConversationDocument
|
||||
{
|
||||
Id = !string.IsNullOrEmpty(conversation.Id) ? conversation.Id : Guid.NewGuid().ToString(),
|
||||
|
|
@ -22,9 +23,8 @@ public partial class MongoRepository
|
|||
Channel = conversation.Channel,
|
||||
TaskId = conversation.TaskId,
|
||||
Status = conversation.Status,
|
||||
CreatedTime = conversation.CreatedTime,
|
||||
UpdatedTime = conversation.UpdatedTime,
|
||||
Breakpoint = conversation.Breakpoint,
|
||||
CreatedTime = utcNow,
|
||||
UpdatedTime = utcNow
|
||||
};
|
||||
|
||||
var dialogDoc = new ConversationDialogDocument
|
||||
|
|
@ -44,11 +44,21 @@ public partial class MongoRepository
|
|||
}
|
||||
}).ToList();
|
||||
|
||||
var initialBreakpoints = new List<BreakpointMongoElement>()
|
||||
{
|
||||
new BreakpointMongoElement
|
||||
{
|
||||
Breakpoint = utcNow.AddMilliseconds(-100),
|
||||
CreatedTime = utcNow
|
||||
}
|
||||
};
|
||||
|
||||
var stateDoc = new ConversationStateDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ConversationId = convDoc.Id,
|
||||
States = initialStates
|
||||
States = initialStates,
|
||||
Breakpoints = initialBreakpoints
|
||||
};
|
||||
|
||||
_dc.Conversations.InsertOne(convDoc);
|
||||
|
|
@ -123,7 +133,8 @@ public partial class MongoRepository
|
|||
var filterDialog = Builders<ConversationDialogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var dialogElements = dialogs.Select(x => DialogMongoElement.ToMongoElement(x)).ToList();
|
||||
var updateDialog = Builders<ConversationDialogDocument>.Update.PushEach(x => x.Dialogs, dialogElements);
|
||||
var updateConv = Builders<ConversationDocument>.Update.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
var updateConv = Builders<ConversationDocument>.Update.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Inc(x => x.DialogCount, dialogs.Count);
|
||||
|
||||
_dc.ConversationDialogs.UpdateOne(filterDialog, updateDialog);
|
||||
_dc.Conversations.UpdateOne(filterConv, updateConv);
|
||||
|
|
@ -141,16 +152,38 @@ public partial class MongoRepository
|
|||
_dc.Conversations.UpdateOne(filterConv, updateConv);
|
||||
}
|
||||
|
||||
public void UpdateConversationBreakpoint(string conversationId, DateTime breakpoint)
|
||||
public void UpdateConversationBreakpoint(string conversationId, string messageId, DateTime breakpoint)
|
||||
{
|
||||
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.Breakpoint, breakpoint);
|
||||
var newBreakpoint = new BreakpointMongoElement()
|
||||
{
|
||||
MessageId = messageId,
|
||||
Breakpoint = breakpoint,
|
||||
CreatedTime = DateTime.UtcNow
|
||||
};
|
||||
var filterState = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var updateState = Builders<ConversationStateDocument>.Update.Push(x => x.Breakpoints, newBreakpoint);
|
||||
|
||||
_dc.Conversations.UpdateOne(filterConv, updateConv);
|
||||
_dc.ConversationStates.UpdateOne(filterState, updateState);
|
||||
}
|
||||
|
||||
public DateTime GetConversationBreakpoint(string conversationId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var filter = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var state = _dc.ConversationStates.Find(filter).FirstOrDefault();
|
||||
|
||||
if (state == null || state.Breakpoints.IsNullOrEmpty())
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return state.Breakpoints.LastOrDefault()?.Breakpoint ?? default;
|
||||
}
|
||||
|
||||
public ConversationState GetConversationStates(string conversationId)
|
||||
|
|
@ -168,13 +201,16 @@ public partial class MongoRepository
|
|||
|
||||
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conversationId) || states.IsNullOrEmpty()) return;
|
||||
if (string.IsNullOrEmpty(conversationId) || states == null) return;
|
||||
|
||||
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
|
||||
var filterStates = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var saveStates = states.Select(x => StateMongoElement.ToMongoElement(x)).ToList();
|
||||
var updateStates = Builders<ConversationStateDocument>.Update.Set(x => x.States, saveStates);
|
||||
var updateConv = Builders<ConversationDocument>.Update.Set(x => x.UpdatedTime, DateTime.UtcNow);
|
||||
|
||||
_dc.ConversationStates.UpdateOne(filterStates, updateStates);
|
||||
_dc.Conversations.UpdateOne(filterConv, updateConv);
|
||||
}
|
||||
|
||||
public void UpdateConversationStatus(string conversationId, string status)
|
||||
|
|
@ -220,9 +256,9 @@ public partial class MongoRepository
|
|||
Status = conv.Status,
|
||||
Dialogs = dialogElements,
|
||||
States = curStates,
|
||||
DialogCount = conv.DialogCount,
|
||||
CreatedTime = conv.CreatedTime,
|
||||
UpdatedTime = conv.UpdatedTime,
|
||||
Breakpoint = conv.Breakpoint
|
||||
UpdatedTime = conv.UpdatedTime
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -232,15 +268,37 @@ public partial class MongoRepository
|
|||
var builder = Builders<ConversationDocument>.Filter;
|
||||
var filters = new List<FilterDefinition<ConversationDocument>>() { builder.Empty };
|
||||
|
||||
if (!string.IsNullOrEmpty(filter.Id)) filters.Add(builder.Eq(x => x.Id, filter.Id));
|
||||
if (!string.IsNullOrEmpty(filter.AgentId)) filters.Add(builder.Eq(x => x.AgentId, filter.AgentId));
|
||||
if (!string.IsNullOrEmpty(filter.Status)) filters.Add(builder.Eq(x => x.Status, filter.Status));
|
||||
if (!string.IsNullOrEmpty(filter.Channel)) filters.Add(builder.Eq(x => x.Channel, filter.Channel));
|
||||
if (!string.IsNullOrEmpty(filter.UserId)) filters.Add(builder.Eq(x => x.UserId, filter.UserId));
|
||||
if (!string.IsNullOrEmpty(filter.TaskId)) filters.Add(builder.Eq(x => x.TaskId, filter.TaskId));
|
||||
if (!string.IsNullOrEmpty(filter?.Id))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.Id, filter.Id));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(filter?.AgentId))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.AgentId, filter.AgentId));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(filter?.Status))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.Status, filter.Status));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(filter?.Channel))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.Channel, filter.Channel));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(filter?.UserId))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.UserId, filter.UserId));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(filter?.TaskId))
|
||||
{
|
||||
filters.Add(builder.Eq(x => x.TaskId, filter.TaskId));
|
||||
}
|
||||
if (filter?.StartTime != null)
|
||||
{
|
||||
filters.Add(builder.Gte(x => x.CreatedTime, filter.StartTime.Value));
|
||||
}
|
||||
|
||||
// Check states
|
||||
if (!filter.States.IsNullOrEmpty())
|
||||
if (filter != null && !filter.States.IsNullOrEmpty())
|
||||
{
|
||||
var targetConvIds = new List<string>();
|
||||
|
||||
|
|
@ -286,9 +344,9 @@ public partial class MongoRepository
|
|||
Title = conv.Title,
|
||||
Channel = conv.Channel,
|
||||
Status = conv.Status,
|
||||
DialogCount = conv.DialogCount,
|
||||
CreatedTime = conv.CreatedTime,
|
||||
UpdatedTime = conv.UpdatedTime,
|
||||
Breakpoint = conv.Breakpoint
|
||||
UpdatedTime = conv.UpdatedTime
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -313,9 +371,9 @@ public partial class MongoRepository
|
|||
Title = c.Title,
|
||||
Channel = c.Channel,
|
||||
Status = c.Status,
|
||||
DialogCount = c.DialogCount,
|
||||
CreatedTime = c.CreatedTime,
|
||||
UpdatedTime = c.UpdatedTime,
|
||||
Breakpoint = c.Breakpoint
|
||||
UpdatedTime = c.UpdatedTime
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
|
@ -331,11 +389,21 @@ public partial class MongoRepository
|
|||
batchSize = batchLimit;
|
||||
}
|
||||
|
||||
if (bufferHours <= 0)
|
||||
{
|
||||
bufferHours = 12;
|
||||
}
|
||||
|
||||
if (messageLimit <= 0)
|
||||
{
|
||||
messageLimit = 2;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var skip = (page - 1) * batchSize;
|
||||
var candidates = _dc.Conversations.AsQueryable()
|
||||
.Where(x => x.UpdatedTime <= utcNow.AddHours(-bufferHours))
|
||||
.Where(x => (x.DialogCount <= messageLimit) && x.UpdatedTime <= utcNow.AddHours(-bufferHours))
|
||||
.Skip(skip)
|
||||
.Take(batchSize)
|
||||
.Select(x => x.Id)
|
||||
|
|
@ -345,13 +413,8 @@ public partial class MongoRepository
|
|||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var targets = _dc.ConversationDialogs.AsQueryable()
|
||||
.Where(x => candidates.Contains(x.ConversationId) && x.Dialogs != null && x.Dialogs.Count <= messageLimit)
|
||||
.Select(x => x.ConversationId)
|
||||
.ToList();
|
||||
|
||||
conversationIds = conversationIds.Concat(targets).ToList();
|
||||
|
||||
conversationIds = conversationIds.Concat(candidates).Distinct().ToList();
|
||||
if (conversationIds.Count >= batchSize)
|
||||
{
|
||||
break;
|
||||
|
|
@ -381,23 +444,54 @@ public partial class MongoRepository
|
|||
var refTime = foundDialog.Dialogs.ElementAt(foundIdx).MetaData.CreateTime;
|
||||
var stateFilter = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
|
||||
var foundStates = _dc.ConversationStates.Find(stateFilter).FirstOrDefault();
|
||||
if (foundStates == null || foundStates.States.IsNullOrEmpty()) return false;
|
||||
|
||||
var truncatedStates = new List<StateMongoElement>();
|
||||
foreach (var state in foundStates.States)
|
||||
if (foundStates != null)
|
||||
{
|
||||
var values = state.Values.Where(x => x.UpdateTime < refTime).ToList();
|
||||
if (values.Count == 0) continue;
|
||||
// Truncate states
|
||||
if (!foundStates.States.IsNullOrEmpty())
|
||||
{
|
||||
var truncatedStates = new List<StateMongoElement>();
|
||||
foreach (var state in foundStates.States)
|
||||
{
|
||||
if (!state.Versioning)
|
||||
{
|
||||
truncatedStates.Add(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Values = values;
|
||||
truncatedStates.Add(state);
|
||||
var values = state.Values.Where(x => x.MessageId != messageId)
|
||||
.Where(x => x.UpdateTime < refTime)
|
||||
.ToList();
|
||||
if (values.Count == 0) continue;
|
||||
|
||||
state.Values = values;
|
||||
truncatedStates.Add(state);
|
||||
}
|
||||
foundStates.States = truncatedStates;
|
||||
}
|
||||
|
||||
// Truncate breakpoints
|
||||
if (!foundStates.Breakpoints.IsNullOrEmpty())
|
||||
{
|
||||
var breakpoints = foundStates.Breakpoints ?? new List<BreakpointMongoElement>();
|
||||
var targetIdx = breakpoints.FindIndex(x => x.MessageId == messageId);
|
||||
var truncatedBreakpoints = breakpoints.Where((x, idx) => idx < targetIdx).ToList();
|
||||
foundStates.Breakpoints = truncatedBreakpoints;
|
||||
}
|
||||
|
||||
// Update
|
||||
_dc.ConversationStates.ReplaceOne(stateFilter, foundStates);
|
||||
}
|
||||
|
||||
// Save
|
||||
// Save dialogs
|
||||
foundDialog.Dialogs = truncatedDialogs;
|
||||
foundStates.States = truncatedStates;
|
||||
_dc.ConversationDialogs.ReplaceOne(dialogFilter, foundDialog);
|
||||
_dc.ConversationStates.ReplaceOne(stateFilter, foundStates);
|
||||
|
||||
// Update conversation
|
||||
var convFilter = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
|
||||
var updateConv = Builders<ConversationDocument>.Update.Set(x => x.UpdatedTime, DateTime.UtcNow)
|
||||
.Set(x => x.DialogCount, truncatedDialogs.Count);
|
||||
_dc.Conversations.UpdateOne(convFilter, updateConv);
|
||||
|
||||
// Remove logs
|
||||
if (cleanLog)
|
||||
|
|
|
|||
|
|
@ -64,13 +64,13 @@ public partial class MongoRepository
|
|||
{
|
||||
if (log == null) return;
|
||||
|
||||
var conversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
var messageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId);
|
||||
if (found == null) return;
|
||||
|
||||
var logDoc = new ConversationContentLogDocument
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
MessageId = messageId,
|
||||
ConversationId = log.ConversationId,
|
||||
MessageId = log.MessageId,
|
||||
Name = log.Name,
|
||||
AgentId = log.AgentId,
|
||||
Role = log.Role,
|
||||
|
|
@ -109,13 +109,13 @@ public partial class MongoRepository
|
|||
{
|
||||
if (log == null) return;
|
||||
|
||||
var conversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
var messageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
|
||||
var found = _dc.Conversations.AsQueryable().FirstOrDefault(x => x.Id == log.ConversationId);
|
||||
if (found == null) return;
|
||||
|
||||
var logDoc = new ConversationStateLogDocument
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
MessageId = messageId,
|
||||
ConversationId = log.ConversationId,
|
||||
MessageId = log.MessageId,
|
||||
States = log.States,
|
||||
CreateTime = log.CreateTime
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using BotSharp.Plugin.Twilio.Services;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Controllers;
|
||||
|
||||
|
|
@ -47,18 +48,22 @@ public class TwilioVoiceController : TwilioController
|
|||
{
|
||||
string sessionId = $"TwilioVoice_{input.CallSid}";
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, input.SpeechResult);
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
conv.SetConversationId(sessionId, new List<string>
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(sessionId, inputMsg.MessageId);
|
||||
|
||||
conv.SetConversationId(sessionId, new List<MessageState>
|
||||
{
|
||||
$"channel={ConversationChannel.Phone}",
|
||||
$"calling_phone={input.DialCallSid}"
|
||||
new MessageState("channel", ConversationChannel.Phone),
|
||||
new MessageState("calling_phone", input.DialCallSid)
|
||||
});
|
||||
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
VoiceResponse response = default;
|
||||
|
||||
var result = await conv.SendMessage(agentId,
|
||||
new RoleDialogModel(AgentRole.User, input.SpeechResult),
|
||||
inputMsg,
|
||||
replyMessage: null,
|
||||
async msg =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Conversations;
|
||||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using BotSharp.Abstraction.Models;
|
||||
using BotSharp.Abstraction.Repositories.Filters;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Abstraction.Users.Models;
|
||||
using BotSharp.Plugin.WeChat.Users;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -50,9 +53,13 @@ namespace BotSharp.Plugin.WeChat
|
|||
.OrderByDescending(_ => _.CreatedTime)
|
||||
.FirstOrDefault()?.Id;
|
||||
|
||||
conversationService.SetConversationId(latestConversationId, new List<string>
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, message);
|
||||
var routing = _service.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(latestConversationId, inputMsg.MessageId);
|
||||
|
||||
conversationService.SetConversationId(latestConversationId, new List<MessageState>
|
||||
{
|
||||
"channel=wechat"
|
||||
new MessageState("channel", "wechat")
|
||||
});
|
||||
|
||||
latestConversationId ??= (await conversationService.NewConversation(new Conversation()
|
||||
|
|
@ -61,8 +68,8 @@ namespace BotSharp.Plugin.WeChat
|
|||
AgentId = AgentId
|
||||
}))?.Id;
|
||||
|
||||
var result = await conversationService.SendMessage(AgentId,
|
||||
new RoleDialogModel("user", message),
|
||||
var result = await conversationService.SendMessage(AgentId,
|
||||
inputMsg,
|
||||
replyMessage: null,
|
||||
async msg =>
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue