refine conversation states

This commit is contained in:
Jicheng Lu 2023-12-31 22:56:10 -06:00
parent 150699f605
commit 010c92db1a
18 changed files with 272 additions and 95 deletions

View file

@ -12,7 +12,7 @@ public interface IConversationStateService
string GetState(string name, string defaultValue = "");
bool ContainsState(string name);
ConversationState GetStates();
IConversationStateService SetState<T>(string name, T value);
IConversationStateService SetState<T>(string name, T value, bool isConst = false);
void SaveStateByArgs(JsonDocument args);
void CleanState();
void Save();

View file

@ -0,0 +1,22 @@
namespace BotSharp.Abstraction.Conversations.Models;
public class ConversationHistoryState : Dictionary<string, List<HistoryStateValue>>
{
public ConversationHistoryState()
{
}
public ConversationHistoryState(List<HistoryStateKeyValue> pairs)
{
foreach (var pair in pairs)
{
this[pair.Key] = pair.Values;
}
}
//public List<StateKeyValue> ToKeyValueList()
//{
// return this.Select(x => new StateKeyValue(x.Key, x.Value)).ToList();
//}
}

View file

@ -0,0 +1,30 @@
namespace BotSharp.Abstraction.Conversations.Models;
public class HistoryStateKeyValue
{
public string Key { get; set; }
public List<HistoryStateValue> Values { get; set; } = new List<HistoryStateValue>();
public HistoryStateKeyValue()
{
}
public HistoryStateKeyValue(string key, List<HistoryStateValue> values)
{
Key = key;
Values = values;
}
}
public class HistoryStateValue
{
public string? MessageId { get; set; }
public string Data { get; set; }
public DateTime UpdateTime { get; set; }
public HistoryStateValue()
{
}
}

View file

@ -33,8 +33,8 @@ public interface IBotSharpRepository
List<DialogElement> GetConversationDialogs(string conversationId);
void UpdateConversationDialogElements(string conversationId, List<DialogContentUpdateModel> updateElements);
void AppendConversationDialogs(string conversationId, List<DialogElement> dialogs);
List<StateKeyValue> GetConversationStates(string conversationId);
void UpdateConversationStates(string conversationId, List<StateKeyValue> states);
List<HistoryStateKeyValue> GetConversationStates(string conversationId);
void UpdateConversationStates(string conversationId, List<HistoryStateKeyValue> states);
void UpdateConversationStatus(string conversationId, string status);
Conversation GetConversation(string conversationId);
List<Conversation> GetConversations(ConversationFilter filter);

View file

@ -120,6 +120,6 @@ public partial class ConversationService : IConversationService
{
_conversationId = conversationId;
_state.Load(_conversationId);
states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1]));
states.ForEach(x => _state.SetState(x.Split('=')[0], x.Split('=')[1], true));
}
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Repositories;
using System.IO;
namespace BotSharp.Core.Conversations.Services;
@ -11,26 +10,33 @@ public class ConversationStateService : IConversationStateService, IDisposable
private readonly ILogger _logger;
private readonly IServiceProvider _services;
private ConversationState _states;
private BotSharpDatabaseSettings _dbSettings;
private ConversationHistoryState _historyStates;
private string _conversationId;
private readonly IBotSharpRepository _db;
private List<StateKeyValue> _savedStates;
public ConversationStateService(ILogger<ConversationStateService> logger,
IServiceProvider services,
BotSharpDatabaseSettings dbSettings,
IServiceProvider services,
IBotSharpRepository db)
{
_logger = logger;
_services = services;
_dbSettings = dbSettings;
_db = db;
_states = new ConversationState();
_historyStates = new ConversationHistoryState();
}
public string GetConversationId() => _conversationId;
public IConversationStateService SetState<T>(string name, T value)
/// <summary>
/// Set conversation state
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="isConst">whether the state is related to message or not</param>
/// <returns></returns>
public IConversationStateService SetState<T>(string name, T value, bool isConst = false)
{
if (value == null)
{
@ -40,6 +46,7 @@ public class ConversationStateService : IConversationStateService, IDisposable
var currentValue = value.ToString();
var hooks = _services.GetServices<IConversationHook>();
string preValue = _states.ContainsKey(name) ? _states[name] : "";
if (!_states.ContainsKey(name) || _states[name] != currentValue)
{
_states[name] = currentValue;
@ -48,6 +55,28 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
hook.OnStateChanged(name, preValue, currentValue).Wait();
}
var historyStateValue = new HistoryStateValue
{
Data = currentValue,
UpdateTime = DateTime.UtcNow
};
if (!_historyStates.ContainsKey(name))
{
_historyStates[name] = new List<HistoryStateValue>();
}
if (isConst)
{
_historyStates[name] = new List<HistoryStateValue> { historyStateValue };
}
else
{
var messageId = GetCurrentMessageId();
historyStateValue.MessageId = messageId ?? string.Empty;
_historyStates[name].Add(historyStateValue);
}
}
return this;
@ -57,14 +86,16 @@ public class ConversationStateService : IConversationStateService, IDisposable
{
_conversationId = conversationId;
_savedStates = _db.GetConversationStates(_conversationId).ToList();
var savedStates = _db.GetConversationStates(_conversationId).ToList();
_historyStates = new ConversationHistoryState(savedStates);
if (!_savedStates.IsNullOrEmpty())
if (!savedStates.IsNullOrEmpty())
{
foreach (var data in _savedStates)
foreach (var state in savedStates)
{
_states[data.Key] = data.Value;
_logger.LogInformation($"[STATE] {data.Key} : {data.Value}");
var value = state.Values.LastOrDefault()?.Data ?? string.Empty;
_states[state.Key] = value;
_logger.LogInformation($"[STATE] {state.Key} : {value}");
}
}
@ -85,24 +116,23 @@ public class ConversationStateService : IConversationStateService, IDisposable
return;
}
var states = new List<StateKeyValue>();
var historyStates = new List<HistoryStateKeyValue>();
foreach (var dic in _states)
foreach (var dic in _historyStates)
{
states.Add(new StateKeyValue(dic.Key, dic.Value));
historyStates.Add(new HistoryStateKeyValue(dic.Key, dic.Value));
}
_db.UpdateConversationStates(_conversationId, states);
_logger.LogInformation($"Saved state {_conversationId}");
_db.UpdateConversationStates(_conversationId, historyStates);
_logger.LogInformation($"Saved states of conversation {_conversationId}");
}
public void CleanState()
{
//File.Delete(_file);
}
public ConversationState GetStates()
=> _states;
public ConversationState GetStates() => _states;
public string GetState(string name, string defaultValue = "")
{
@ -142,4 +172,12 @@ public class ConversationStateService : IConversationStateService, IDisposable
}
}
}
private string? GetCurrentMessageId()
{
if (string.IsNullOrEmpty(_conversationId)) return null;
var dialogs = _db.GetConversationDialogs(_conversationId);
return dialogs.LastOrDefault()?.MetaData?.MessageId;
}
}

View file

@ -152,7 +152,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository
throw new NotImplementedException();
}
public List<StateKeyValue> GetConversationStates(string conversationId)
public List<HistoryStateKeyValue> GetConversationStates(string conversationId)
{
throw new NotImplementedException();
}
@ -165,7 +165,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository
{
throw new NotImplementedException();
}
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
public void UpdateConversationStates(string conversationId, List<HistoryStateKeyValue> states)
{
throw new NotImplementedException();
}

View file

@ -644,7 +644,7 @@ public class FileRepository : IBotSharpRepository
var stateDir = Path.Combine(dir, "state.dict");
if (!File.Exists(stateDir))
{
File.WriteAllText(stateDir, string.Empty);
File.WriteAllText(stateDir, "[]");
}
}
@ -730,33 +730,31 @@ public class FileRepository : IBotSharpRepository
}
}
}
public List<StateKeyValue> GetConversationStates(string conversationId)
public List<HistoryStateKeyValue> GetConversationStates(string conversationId)
{
var curStates = new List<StateKeyValue>();
var curStates = new List<HistoryStateKeyValue>();
var convDir = FindConversationDirectory(conversationId);
if (!string.IsNullOrEmpty(convDir))
{
var stateDir = Path.Combine(convDir, "state.dict");
curStates = CollectConversationStates(stateDir);
var stateFile = Path.Combine(convDir, "state.dict");
curStates = CollectConversationStates(stateFile);
}
return curStates;
}
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
public void UpdateConversationStates(string conversationId, List<HistoryStateKeyValue> states)
{
var localStates = new List<string>();
if (states.IsNullOrEmpty()) return;
var convDir = FindConversationDirectory(conversationId);
if (!string.IsNullOrEmpty(convDir))
{
var stateDir = Path.Combine(convDir, "state.dict");
if (File.Exists(stateDir))
var stateFile = Path.Combine(convDir, "state.dict");
if (File.Exists(stateFile))
{
foreach (var data in states)
{
localStates.Add($"{data.Key}={data.Value}");
}
File.WriteAllLines(stateDir, localStates);
var stateStr = JsonSerializer.Serialize(states, _options);
File.WriteAllText(stateFile, stateStr);
}
}
}
@ -796,8 +794,13 @@ public class FileRepository : IBotSharpRepository
var stateFile = Path.Combine(convDir, "state.dict");
if (record != null)
{
var states = CollectConversationStates(stateFile);
record.States = new ConversationState(states);
var historyStates = CollectConversationStates(stateFile);
var recentStates = historyStates.Select(x => new StateKeyValue
{
Key = x.Key,
Value = x.Values.LastOrDefault()?.Data ?? string.Empty
}).ToList();
record.States = new ConversationState(recentStates);
}
return record;
@ -1082,18 +1085,16 @@ public class FileRepository : IBotSharpRepository
return dialogTexts;
}
private List<StateKeyValue> CollectConversationStates(string stateDir)
private List<HistoryStateKeyValue> CollectConversationStates(string stateFile)
{
var states = new List<StateKeyValue>();
if (!File.Exists(stateDir)) return states;
var states = new List<HistoryStateKeyValue>();
if (!File.Exists(stateFile)) return states;
var dict = File.ReadAllLines(stateDir);
foreach (var line in dict)
{
var data = line.Split('=');
states.Add(new StateKeyValue(data[0], data[1]));
}
return states;
var stateStr = File.ReadAllText(stateFile);
if (string.IsNullOrEmpty(stateStr)) return states;
states = JsonSerializer.Deserialize<List<HistoryStateKeyValue>>(stateStr, _options);
return states ?? new List<HistoryStateKeyValue>();
}
private int GetNextLlmCompletionLogIndex(string logDir, string id)

View file

@ -117,11 +117,11 @@ public class ConversationController : ControllerBase
{
var conv = _services.GetRequiredService<IConversationService>();
conv.SetConversationId(conversationId, input.States);
conv.States.SetState("channel", input.Channel)
.SetState("provider", input.Provider)
.SetState("model", input.Model)
.SetState("temperature", input.Temperature)
.SetState("sampling_factor", input.SamplingFactor);
conv.States.SetState("channel", input.Channel, true)
.SetState("provider", input.Provider, true)
.SetState("model", input.Model, true)
.SetState("temperature", input.Temperature, true)
.SetState("sampling_factor", input.SamplingFactor, true);
var response = new ChatResponseModel();
var inputMsg = new RoleDialogModel(AgentRole.User, input.Text);

View file

@ -22,10 +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]));
state.SetState("provider", input.Provider)
.SetState("model", input.Model)
.SetState("instruction", input.Instruction)
input.States.ForEach(x => state.SetState(x.Split('=')[0], x.Split('=')[1], true));
state.SetState("provider", input.Provider, true)
.SetState("model", input.Model, true)
.SetState("instruction", input.Instruction, true)
.SetState("input_text", input.Text);
var instructor = _services.GetRequiredService<IInstructService>();

View file

@ -75,11 +75,11 @@ public class ChatbotUiController : ControllerBase
var conv = _services.GetRequiredService<IConversationService>();
conv.SetConversationId(input.ConversationId, input.States);
conv.States.SetState("channel", input.Channel)
.SetState("provider", input.Provider)
.SetState("model", input.Model)
.SetState("temperature", input.Temperature)
.SetState("sampling_factor", input.SamplingFactor);
conv.States.SetState("channel", input.Channel, true)
.SetState("provider", input.Provider, true)
.SetState("model", input.Model, true)
.SetState("temperature", input.Temperature, true)
.SetState("sampling_factor", input.SamplingFactor, true);
var result = await conv.SendMessage(input.AgentId,
message,

View file

@ -1,5 +1,3 @@
using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class ConversationDocument : MongoBase
@ -9,7 +7,6 @@ public class ConversationDocument : MongoBase
public string Title { get; set; }
public string Channel { get; set; }
public string Status { get; set; }
public List<StateKeyValue> States { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
}

View file

@ -0,0 +1,9 @@
using BotSharp.Plugin.MongoStorage.Models;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class ConversationStateDocument : MongoBase
{
public string ConversationId { get; set; }
public List<StateMongoElement> States { get; set; }
}

View file

@ -5,5 +5,5 @@ namespace BotSharp.Plugin.MongoStorage.Collections;
public class LlmCompletionLogDocument : MongoBase
{
public string ConversationId { get; set; }
public List<PromptLogElement> Logs { get; set; }
public List<PromptLogMongoElement> Logs { get; set; }
}

View file

@ -1,6 +1,6 @@
namespace BotSharp.Plugin.MongoStorage.Models;
public class PromptLogElement
public class PromptLogMongoElement
{
public string MessageId { get; set; }
public string AgentId { get; set; }

View file

@ -0,0 +1,54 @@
using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Plugin.MongoStorage.Models;
public class StateMongoElement
{
public string Key { get; set; }
public List<StateValueMongoElement> Values { get; set; }
public static StateMongoElement ToMongoElement(HistoryStateKeyValue state)
{
return new StateMongoElement
{
Key = state.Key,
Values = state.Values?.Select(x => StateValueMongoElement.ToMongoElement(x))?.ToList() ?? new List<StateValueMongoElement>()
};
}
public static HistoryStateKeyValue ToDomainElement(StateMongoElement state)
{
return new HistoryStateKeyValue
{
Key = state.Key,
Values = state.Values?.Select(x => StateValueMongoElement.ToDomainElement(x))?.ToList() ?? new List<HistoryStateValue>()
};
}
}
public class StateValueMongoElement
{
public string? MessageId { get; set; }
public string Data { get; set; }
public DateTime UpdateTime { get; set; }
public static StateValueMongoElement ToMongoElement(HistoryStateValue element)
{
return new StateValueMongoElement
{
MessageId = element.MessageId,
Data = element.Data,
UpdateTime = element.UpdateTime
};
}
public static HistoryStateValue ToDomainElement(StateValueMongoElement element)
{
return new HistoryStateValue
{
MessageId = element.MessageId,
Data = element.Data,
UpdateTime = element.UpdateTime
};
}
}

View file

@ -37,6 +37,9 @@ public class MongoDbContext
public IMongoCollection<ConversationDialogDocument> ConversationDialogs
=> Database.GetCollection<ConversationDialogDocument>($"{_collectionPrefix}_ConversationDialogs");
public IMongoCollection<ConversationStateDocument> ConversationStates
=> Database.GetCollection<ConversationStateDocument>($"{_collectionPrefix}_ConversationStates");
public IMongoCollection<ExecutionLogDocument> ExectionLogs
=> Database.GetCollection<ExecutionLogDocument>($"{_collectionPrefix}_ExecutionLogs");

View file

@ -447,7 +447,7 @@ public class MongoRepository : IBotSharpRepository
{
query = query.Where(x => x.Name.ToLower() == filter.AgentName.ToLower());
}
if (filter.Disabled.HasValue)
{
query = query.Where(x => x.Disabled == filter.Disabled);
@ -514,9 +514,9 @@ public class MongoRepository : IBotSharpRepository
public List<Agent> GetAgentsByUser(string userId)
{
var agentIds = (from ua in _dc.UserAgents.AsQueryable()
join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id
where ua.UserId == userId || u.ExternalId == userId
select ua.AgentId).ToList();
join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id
where ua.UserId == userId || u.ExternalId == userId
select ua.AgentId).ToList();
var filter = new AgentFilter
{
@ -608,7 +608,7 @@ public class MongoRepository : IBotSharpRepository
{
return false;
}
}
#endregion
@ -625,7 +625,6 @@ public class MongoRepository : IBotSharpRepository
Title = conversation.Title,
Channel = conversation.Channel,
Status = conversation.Status,
States = conversation.States?.ToKeyValueList() ?? new List<StateKeyValue>(),
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow,
};
@ -637,8 +636,16 @@ public class MongoRepository : IBotSharpRepository
Dialogs = new List<DialogMongoElement>()
};
var states = new ConversationStateDocument
{
Id = Guid.NewGuid().ToString(),
ConversationId = conv.Id,
States = new List<StateMongoElement>()
};
_dc.Conversations.InsertOne(conv);
_dc.ConversationDialogs.InsertOne(dialog);
_dc.ConversationStates.InsertOne(states);
}
public bool DeleteConversation(string conversationId)
@ -647,14 +654,16 @@ public class MongoRepository : IBotSharpRepository
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var filterDialog = Builders<ConversationDialogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterSates = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterExeLog = Builders<ExecutionLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterPromptLog = Builders<LlmCompletionLogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var exeLogDeleted = _dc.ExectionLogs.DeleteMany(filterExeLog);
var promptLogDeleted = _dc.LlmCompletionLogs.DeleteMany(filterPromptLog);
var statesDeleted = _dc.ConversationStates.DeleteMany(filterSates);
var dialogDeleted = _dc.ConversationDialogs.DeleteMany(filterDialog);
var convDeleted = _dc.Conversations.DeleteMany(filterConv);
return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0
return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0 || statesDeleted.DeletedCount > 0
|| exeLogDeleted.DeletedCount > 0 || promptLogDeleted.DeletedCount > 0;
}
@ -688,7 +697,7 @@ public class MongoRepository : IBotSharpRepository
}
return x;
}).ToList();
_dc.ConversationDialogs.ReplaceOne(filterDialog, foundDialog);
}
@ -727,30 +736,37 @@ public class MongoRepository : IBotSharpRepository
_dc.Conversations.UpdateOne(filterConv, updateConv);
}
public List<StateKeyValue> GetConversationStates(string conversationId)
public List<HistoryStateKeyValue> GetConversationStates(string conversationId)
{
var states = new List<StateKeyValue>();
var states = new List<HistoryStateKeyValue>();
if (string.IsNullOrEmpty(conversationId)) return states;
var filter = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var foundConversation = _dc.Conversations.Find(filter).FirstOrDefault();
var savedStates = foundConversation?.States ?? new List<StateKeyValue>();
var filter = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var foundStates = _dc.ConversationStates.Find(filter).FirstOrDefault();
if (foundStates == null || foundStates.States.IsNullOrEmpty()) return states;
var savedStates = foundStates.States.Select(x => StateMongoElement.ToDomainElement(x)).ToList();
return savedStates;
}
public void UpdateConversationStates(string conversationId, List<StateKeyValue> states)
public void UpdateConversationStates(string conversationId, List<HistoryStateKeyValue> states)
{
if (string.IsNullOrEmpty(conversationId)) return;
if (string.IsNullOrEmpty(conversationId) || states.IsNullOrEmpty()) return;
var filter = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var foundConv = _dc.Conversations.Find(filter).FirstOrDefault();
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var foundConv = _dc.Conversations.Find(filterConv).FirstOrDefault();
if (foundConv == null) return;
var update = Builders<ConversationDocument>.Update
.Set(x => x.States, states)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
var filterStates = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var foundStates = _dc.ConversationStates.Find(filterStates).FirstOrDefault();
if (foundStates == null) return;
_dc.Conversations.UpdateOne(filter, update);
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)
@ -774,14 +790,21 @@ public class MongoRepository : IBotSharpRepository
var filterConv = Builders<ConversationDocument>.Filter.Eq(x => x.Id, conversationId);
var filterDialog = Builders<ConversationDialogDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var filterState = Builders<ConversationStateDocument>.Filter.Eq(x => x.ConversationId, conversationId);
var conv = _dc.Conversations.Find(filterConv).FirstOrDefault();
var dialog = _dc.ConversationDialogs.Find(filterDialog).FirstOrDefault();
var states = _dc.ConversationStates.Find(filterState).FirstOrDefault();
if (conv == null) return null;
var dialogElements = dialog?.Dialogs?.Select(x => DialogMongoElement.ToDomainElement(x))?.ToList() ?? new List<DialogElement>();
var recentStates = states.States?.Select(x => new StateKeyValue
{
Key = x.Key,
Value = x.Values.LastOrDefault()?.Data ?? string.Empty
})?.ToList() ?? new List<StateKeyValue>();
return new Conversation
{
Id = conv.Id.ToString(),
@ -791,7 +814,7 @@ public class MongoRepository : IBotSharpRepository
Channel = conv.Channel,
Status = conv.Status,
Dialogs = dialogElements,
States = new ConversationState(conv.States ?? new List<StateKeyValue>()),
States = new ConversationState(recentStates),
CreatedTime = conv.CreatedTime,
UpdatedTime = conv.UpdatedTime
};
@ -938,7 +961,7 @@ public class MongoRepository : IBotSharpRepository
var conversationId = log.ConversationId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var messageId = log.MessageId.IfNullOrEmptyAs(Guid.NewGuid().ToString());
var logElement = new PromptLogElement
var logElement = new PromptLogMongoElement
{
MessageId = messageId,
AgentId = log.AgentId,