Tracking conversation state.
This commit is contained in:
parent
19a31510c0
commit
d55490dc07
|
|
@ -10,5 +10,6 @@ public interface IAgentService
|
|||
Task<Agent> GetAgent(string id);
|
||||
Task<bool> DeleteAgent(string id);
|
||||
Task UpdateAgent(Agent agent);
|
||||
string GetDataDir();
|
||||
string GetAgentDataDir(string agentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Agents.Models;
|
||||
|
||||
public class Agent
|
||||
|
|
@ -26,5 +28,5 @@ public class Agent
|
|||
/// <summary>
|
||||
/// Domain knowledges
|
||||
/// </summary>
|
||||
public string Knowledges { get; set;}
|
||||
public string Knowledges { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ public abstract class ConversationCompletionHookBase : IConversationCompletionHo
|
|||
return this;
|
||||
}
|
||||
|
||||
public virtual Task OnStateLoaded(ConversationState state)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task BeforeCompletion()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ public interface IConversationCompletionHook
|
|||
|
||||
IChatCompletion ChatCompletion { get; }
|
||||
IConversationCompletionHook SetChatCompletion(IChatCompletion chatCompletion);
|
||||
|
||||
|
||||
Task OnStateLoaded(ConversationState state);
|
||||
Task BeforeCompletion();
|
||||
Task OnFunctionExecuting(string name, string args);
|
||||
Task AfterCompletion(RoleDialogModel message);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,6 @@ public interface IConversationService
|
|||
Task DeleteConversation(string id);
|
||||
Task<bool> SendMessage(string agentId, string conversationId, RoleDialogModel lastDalog, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting);
|
||||
Task<bool> SendMessage(string agentId, string conversationId, List<RoleDialogModel> wholeDialogs, Func<RoleDialogModel, Task> onMessageReceived);
|
||||
List<RoleDialogModel> GetDialogHistory(string agentId, string conversationId, int lastCount = 20);
|
||||
List<RoleDialogModel> GetDialogHistory(string conversationId, int lastCount = 20);
|
||||
Task CleanHistory(string agentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
namespace BotSharp.Abstraction.Conversations;
|
||||
|
||||
/// <summary>
|
||||
/// Conversation state service to track the context in the conversation lifecycle
|
||||
/// </summary>
|
||||
public interface IConversationStateService
|
||||
{
|
||||
ConversationState Load(string conversationId);
|
||||
string GetState(string name);
|
||||
void Save();
|
||||
}
|
||||
|
|
@ -4,7 +4,8 @@ namespace BotSharp.Abstraction.Conversations;
|
|||
|
||||
public interface IConversationStorage
|
||||
{
|
||||
void InitStorage(string agentId, string conversationId);
|
||||
void Append(string agentId, string conversationId, RoleDialogModel dialog);
|
||||
List<RoleDialogModel> GetDialogs(string agentId, string conversationId);
|
||||
void InitStorage(string conversationId);
|
||||
void Append(string conversationId, RoleDialogModel dialog);
|
||||
List<RoleDialogModel> GetDialogs(string conversationId);
|
||||
string GetConversationDataDir();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ public class Conversation
|
|||
|
||||
public DateTime UpdatedTime { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ConversationState State { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
namespace BotSharp.Abstraction.Conversations.Models;
|
||||
|
||||
public class ConversationState : Dictionary<string, string>
|
||||
{
|
||||
}
|
||||
|
|
@ -6,5 +6,5 @@ public interface IKnowledgeService
|
|||
{
|
||||
Task Feed(KnowledgeFeedModel knowledge);
|
||||
Task<string> GetKnowledges(KnowledgeRetrievalModel retrievalModel);
|
||||
Task<string> GetAnswer(KnowledgeRetrievalModel retrievalModel);
|
||||
Task<List<RetrievedResult>> GetAnswer(KnowledgeRetrievalModel retrievalModel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BotSharp.Abstraction.Knowledges.Models;
|
||||
|
||||
public class RetrievedResult
|
||||
{
|
||||
public int Paragraph { get; set; }
|
||||
|
||||
[JsonPropertyName("cite_source")]
|
||||
public string CiteSource { get; set; } = "related text";
|
||||
|
||||
[JsonPropertyName("reasoning")]
|
||||
public string Reasoning { get; set; } = "";
|
||||
}
|
||||
|
|
@ -18,9 +18,14 @@ public partial class AgentService : IAgentService
|
|||
_settings = settings;
|
||||
}
|
||||
|
||||
public string GetDataDir()
|
||||
{
|
||||
return Path.Combine(_settings.DataDir);
|
||||
}
|
||||
|
||||
public string GetAgentDataDir(string agentId)
|
||||
{
|
||||
var dir = Path.Combine(_settings.DataDir, agentId);
|
||||
var dir = Path.Combine(_settings.DataDir, "agents", agentId);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public static class BotSharpServiceCollectionExtensions
|
|||
|
||||
services.AddScoped<IConversationStorage, ConversationStorage>();
|
||||
services.AddScoped<IConversationService, ConversationService>();
|
||||
services.AddScoped<IConversationStateService, ConversationStateService>();
|
||||
|
||||
RegisterPlugins(services, config);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ using BotSharp.Abstraction.Conversations.Models;
|
|||
using BotSharp.Abstraction.Functions;
|
||||
using BotSharp.Abstraction.Knowledges.Models;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
public class ConversationService : IConversationService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IUserIdentity _user;
|
||||
private readonly ConversationSetting _settings;
|
||||
|
|
@ -15,12 +18,14 @@ public class ConversationService : IConversationService
|
|||
public ConversationService(IServiceProvider services,
|
||||
IUserIdentity user,
|
||||
ConversationSetting settings,
|
||||
IConversationStorage storage)
|
||||
IConversationStorage storage,
|
||||
ILogger<ConversationService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_user = user;
|
||||
_settings = settings;
|
||||
_storage = storage;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task DeleteConversation(string id)
|
||||
|
|
@ -62,7 +67,7 @@ public class ConversationService : IConversationService
|
|||
db.Add<IBotSharpTable>(record);
|
||||
});
|
||||
|
||||
_storage.InitStorage(sess.AgentId, record.Id);
|
||||
_storage.InitStorage(record.Id);
|
||||
|
||||
return record.ToConversation();
|
||||
}
|
||||
|
|
@ -71,9 +76,9 @@ public class ConversationService : IConversationService
|
|||
Func<RoleDialogModel, Task> onMessageReceived,
|
||||
Func<RoleDialogModel, Task> onFunctionExecuting)
|
||||
{
|
||||
_storage.Append(agentId, conversationId, lastDalog);
|
||||
_storage.Append(conversationId, lastDalog);
|
||||
|
||||
var wholeDialogs = GetDialogHistory(agentId, conversationId);
|
||||
var wholeDialogs = GetDialogHistory(conversationId);
|
||||
|
||||
var response = await SendMessage(agentId, conversationId, wholeDialogs, async msg =>
|
||||
{
|
||||
|
|
@ -90,7 +95,7 @@ public class ConversationService : IConversationService
|
|||
var result = msg.ExecutionResult.Replace("\r", " ").Replace("\n", " ");
|
||||
var content = $"{result}";
|
||||
// Console.WriteLine($"{msg.Role}: {content}");
|
||||
_storage.Append(agentId, conversationId, new RoleDialogModel(msg.Role, content)
|
||||
_storage.Append(conversationId, new RoleDialogModel(msg.Role, content)
|
||||
{
|
||||
FunctionName = msg.FunctionName,
|
||||
});
|
||||
|
|
@ -100,7 +105,7 @@ public class ConversationService : IConversationService
|
|||
{
|
||||
var content = msg.Content.Replace("\r", " ").Replace("\n", " ");
|
||||
// Console.WriteLine($"{msg.Role}: {content}");
|
||||
_storage.Append(agentId, conversationId, new RoleDialogModel(msg.Role, content));
|
||||
_storage.Append(conversationId, new RoleDialogModel(msg.Role, content));
|
||||
|
||||
await onMessageReceived(msg);
|
||||
}
|
||||
|
|
@ -111,9 +116,16 @@ public class ConversationService : IConversationService
|
|||
|
||||
public async Task<bool> SendMessage(string agentId, string conversationId, List<RoleDialogModel> wholeDialogs, Func<RoleDialogModel, Task> onMessageReceived)
|
||||
{
|
||||
var agent = await _services.GetRequiredService<IAgentService>().GetAgent(agentId);
|
||||
var agent = await _services.GetRequiredService<IAgentService>()
|
||||
.GetAgent(agentId);
|
||||
|
||||
var converation = await GetConversation(conversationId);
|
||||
|
||||
// load state
|
||||
var stateService = _services.GetRequiredService<IConversationStateService>();
|
||||
var state = stateService.Load(conversationId);
|
||||
state["agentId"] = agentId;
|
||||
|
||||
// Get relevant domain knowledge
|
||||
if (_settings.EnableKnowledgeBase)
|
||||
{
|
||||
|
|
@ -132,11 +144,13 @@ public class ConversationService : IConversationService
|
|||
// Before chat completion hook
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
await hook.SetAgent(agent)
|
||||
hook.SetAgent(agent)
|
||||
.SetConversation(converation)
|
||||
.SetDialogs(wholeDialogs)
|
||||
.SetChatCompletion(chatCompletion)
|
||||
.BeforeCompletion();
|
||||
.SetChatCompletion(chatCompletion);
|
||||
|
||||
await hook.OnStateLoaded(state);
|
||||
await hook.BeforeCompletion();
|
||||
}
|
||||
|
||||
var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg =>
|
||||
|
|
@ -148,6 +162,19 @@ public class ConversationService : IConversationService
|
|||
{
|
||||
await hook.OnFunctionExecuting(msg.FunctionName, msg.Content);
|
||||
}
|
||||
// Save states
|
||||
var jo = JsonSerializer.Deserialize<object>(msg.Content);
|
||||
if (jo is JsonElement root)
|
||||
{
|
||||
foreach (JsonProperty property in root.EnumerateObject())
|
||||
{
|
||||
string propertyName = property.Name;
|
||||
string propertyValue = property.Value.ToString();
|
||||
|
||||
_logger.LogInformation($"Set conversation state: {propertyName} - {propertyValue}");
|
||||
state[propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -174,9 +201,9 @@ public class ConversationService : IConversationService
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public List<RoleDialogModel> GetDialogHistory(string agentId, string conversationId, int lastCount = 20)
|
||||
public List<RoleDialogModel> GetDialogHistory(string conversationId, int lastCount = 20)
|
||||
{
|
||||
var dialogs = _storage.GetDialogs(agentId, conversationId);
|
||||
var dialogs = _storage.GetDialogs(conversationId);
|
||||
return dialogs.TakeLast(lastCount).ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
using BotSharp.Abstraction.Conversations.Models;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Conversations.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Maintain the conversation state
|
||||
/// </summary>
|
||||
public class ConversationStateService : IConversationStateService, IDisposable
|
||||
{
|
||||
private ConversationState _state;
|
||||
private IAgentService _agentService;
|
||||
private string _conversationId;
|
||||
private string _file;
|
||||
|
||||
public ConversationStateService(IAgentService agentService)
|
||||
{
|
||||
_agentService = agentService;
|
||||
}
|
||||
|
||||
public void SetState(string name, string value)
|
||||
{
|
||||
_state[name] = value;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
public ConversationState Load(string conversationId)
|
||||
{
|
||||
if (_state != null)
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
_state = new ConversationState();
|
||||
_conversationId = conversationId;
|
||||
|
||||
_file = GetStorageFile(_conversationId);
|
||||
|
||||
if (File.Exists(_file))
|
||||
{
|
||||
var dict = File.ReadAllLines(_file);
|
||||
foreach (var line in dict)
|
||||
{
|
||||
_state[line.Split(':')[0]] = line.Split(':')[1];
|
||||
}
|
||||
}
|
||||
|
||||
return _state;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var states = new List<string>();
|
||||
|
||||
foreach (var dic in _state)
|
||||
{
|
||||
states.Add($"{dic.Key}:{dic.Value}");
|
||||
}
|
||||
File.WriteAllLines(_file, states);
|
||||
}
|
||||
|
||||
private string GetStorageFile(string conversationId)
|
||||
{
|
||||
var dir = _agentService.GetDataDir();
|
||||
return Path.Combine(dir, "conversations", conversationId + ".state");
|
||||
}
|
||||
|
||||
public string GetState(string name)
|
||||
{
|
||||
if (!_state.ContainsKey(name))
|
||||
{
|
||||
_state[name] = "";
|
||||
}
|
||||
return _state[name];
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ public class ConversationStorage : IConversationStorage
|
|||
_agent = agent;
|
||||
}
|
||||
|
||||
public void Append(string agentId, string conversationId, RoleDialogModel dialog)
|
||||
public void Append(string conversationId, RoleDialogModel dialog)
|
||||
{
|
||||
var conversationFile = GetStorageFile(agentId, conversationId);
|
||||
var conversationFile = GetStorageFile(conversationId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{dialog.Role}|{dialog.CreatedAt}|{dialog.FunctionName}");
|
||||
sb.AppendLine($" - {dialog.Content}");
|
||||
|
|
@ -21,9 +21,9 @@ public class ConversationStorage : IConversationStorage
|
|||
File.AppendAllText(conversationFile, conversation);
|
||||
}
|
||||
|
||||
public List<RoleDialogModel> GetDialogs(string agentId, string conversationId)
|
||||
public List<RoleDialogModel> GetDialogs(string conversationId)
|
||||
{
|
||||
var conversationFile = GetStorageFile(agentId, conversationId);
|
||||
var conversationFile = GetStorageFile(conversationId);
|
||||
var dialogs = File.ReadAllLines(conversationFile);
|
||||
|
||||
var results = new List<RoleDialogModel>();
|
||||
|
|
@ -44,26 +44,28 @@ public class ConversationStorage : IConversationStorage
|
|||
return results;
|
||||
}
|
||||
|
||||
public void InitStorage(string agentId, string conversationId)
|
||||
public void InitStorage(string conversationId)
|
||||
{
|
||||
var dir = _agent.GetAgentDataDir(agentId);
|
||||
var dialogDir = Path.Combine(dir, "conversations");
|
||||
if (!Directory.Exists(dialogDir))
|
||||
var file = GetStorageFile(conversationId);
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
Directory.CreateDirectory(dialogDir);
|
||||
}
|
||||
|
||||
var conversationFile = Path.Combine(dialogDir, conversationId + ".txt");
|
||||
if (!File.Exists(conversationFile))
|
||||
{
|
||||
File.WriteAllLines(conversationFile, new string[0]);
|
||||
File.WriteAllLines(file, new string[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetStorageFile(string agentId, string conversationId)
|
||||
private string GetStorageFile(string conversationId)
|
||||
{
|
||||
var dir = _agent.GetAgentDataDir(agentId);
|
||||
var dialogDir = Path.Combine(dir, "conversations");
|
||||
return Path.Combine(dialogDir, conversationId + ".txt");
|
||||
var dir = GetConversationDataDir();
|
||||
return Path.Combine(dir, conversationId + ".txt");
|
||||
}
|
||||
|
||||
public string GetConversationDataDir()
|
||||
{
|
||||
var dir = Path.Combine(_agent.GetDataDir(), "conversations");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class KnowledgeController : ControllerBase, IApiAdapter
|
|||
}
|
||||
|
||||
[HttpGet("/knowledge/{agentId}")]
|
||||
public async Task<string> RetrieveKnowledge([FromRoute] string agentId, [FromQuery(Name = "q")] string question)
|
||||
public async Task<List<RetrievedResult>> RetrieveKnowledge([FromRoute] string agentId, [FromQuery(Name = "q")] string question)
|
||||
{
|
||||
return await _knowledgeService.GetAnswer(new KnowledgeRetrievalModel
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using BotSharp.Abstraction.Knowledges.Models;
|
||||
using BotSharp.Abstraction.MLTasks;
|
||||
using BotSharp.Abstraction.VectorStorage;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BotSharp.Core.Plugins.Knowledges.Services;
|
||||
|
||||
|
|
@ -51,10 +52,10 @@ public class KnowledgeService : IKnowledgeService
|
|||
var result = await GetVectorDb().Search(retrievalModel.AgentId, vector, limit: 10);
|
||||
|
||||
// Restore
|
||||
return string.Join("\n\n", result.Select((x, i) => $"{i + 1}: {x.Trim()}"));
|
||||
return string.Join("\n\n", result.Select((x, i) => $"### Paragraph {i + 1} ###\n{x.Trim()}"));
|
||||
}
|
||||
|
||||
public async Task<string> GetAnswer(KnowledgeRetrievalModel retrievalModel)
|
||||
public async Task<List<RetrievedResult>> GetAnswer(KnowledgeRetrievalModel retrievalModel)
|
||||
{
|
||||
// Restore
|
||||
var prompt = await GetKnowledges(retrievalModel);
|
||||
|
|
@ -62,13 +63,17 @@ public class KnowledgeService : IKnowledgeService
|
|||
var sb = new StringBuilder(prompt);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Answer question based on the given information above. Try to response in bullet points if necessary. Please keep your answers concise and free of irrelevant information.");
|
||||
sb.AppendLine($"Question: {retrievalModel.Question}");
|
||||
sb.AppendLine("Answer: ");
|
||||
sb.AppendLine("------");
|
||||
sb.AppendLine("Answer question based on the given information above. Keep your answers concise. Please response with paragraph number, cite sources and reasoning in JSON format, if multiple paragraphs are found, put them in a JSON array. make sure the paragraph number is real. If you don't know the answer just output empty.");
|
||||
sb.AppendLine("[" + JsonSerializer.Serialize(new RetrievedResult()) + "]");
|
||||
sb.AppendLine("------");
|
||||
sb.AppendLine($"QUESTION: \"{retrievalModel.Question}\"");
|
||||
sb.AppendLine("Which paragraphs are relevant in order to answer the above question?");
|
||||
sb.AppendLine("ANSWER: ");
|
||||
prompt = sb.ToString().Trim();
|
||||
|
||||
var completion = await GetTextCompletion().GetCompletion(prompt);
|
||||
return completion;
|
||||
return JsonSerializer.Deserialize<List<RetrievedResult>>(completion);
|
||||
}
|
||||
|
||||
public IVectorDb GetVectorDb()
|
||||
|
|
|
|||
|
|
@ -108,8 +108,7 @@ public class ChatCompletionProvider : IChatCompletion
|
|||
{
|
||||
return false;
|
||||
}
|
||||
Console.Write(message.FunctionCall.Name);
|
||||
Console.Write(message.FunctionCall.Arguments);
|
||||
_logger.LogInformation($"{message.FunctionCall.Name}: {message.FunctionCall.Arguments}");
|
||||
var funcContextIn = new RoleDialogModel(ChatRole.Function.ToString(), message.FunctionCall.Arguments)
|
||||
{
|
||||
FunctionName = message.FunctionCall.Name
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class TextCompletionProvider : ITextCompletion
|
|||
{
|
||||
text
|
||||
},
|
||||
Temperature = 1f,
|
||||
Temperature = 0.7f,
|
||||
MaxTokens = 256
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue