Merge pull request #94 from hchen2020/master

Tracking conversation state.
This commit is contained in:
Haiping 2023-08-09 20:09:03 -05:00 committed by GitHub
commit f982a021a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 212 additions and 49 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
namespace BotSharp.Abstraction.Conversations.Models;
public class ConversationState : Dictionary<string, string>
{
}

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ public static class BotSharpServiceCollectionExtensions
services.AddScoped<IConversationStorage, ConversationStorage>();
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IConversationStateService, ConversationStateService>();
RegisterPlugins(services, config);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ public class TextCompletionProvider : ITextCompletion
{
text
},
Temperature = 1f,
Temperature = 0.7f,
MaxTokens = 256
};