Merge pull request #370 from SciSharp/master

merge latest code
This commit is contained in:
geffzhang 2024-03-28 15:05:00 +08:00 committed by GitHub
commit 4e9c311b65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 709 additions and 254 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,6 +68,7 @@ public partial class InstructService : IInstructService
var completer = CompletionProvider.GetCompletion(_services,
agentConfig: agent.LlmConfig);
var response = new InstructResult
{
MessageId = message.MessageId

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,6 @@ public class TaskCompletedRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<string> Planers => new List<string>
{
nameof(NaivePlanner),
nameof(HFPlanner)
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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