Merge pull request #101 from hchen2020/master

Add Agent hook.
This commit is contained in:
Haiping 2023-08-14 12:02:37 -05:00 committed by GitHub
commit 893acaa493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 73 deletions

View file

@ -0,0 +1,40 @@
namespace BotSharp.Abstraction.Agents;
public abstract class AgentHookBase : IAgentHook
{
protected Agent _agent;
public Agent Agent => _agent;
public void SetAget(Agent agent)
{
_agent = agent;
}
public virtual bool OnAgentLoading(ref string id)
{
return true;
}
public virtual bool OnInstructionLoaded(ref string instruction)
{
_agent.Instruction = instruction;
return true;
}
public virtual bool OnFunctionsLoaded(ref string functions)
{
_agent.Functions = functions;
return true;
}
public virtual bool OnSamplesLoaded(ref string samples)
{
_agent.Samples = samples;
return true;
}
public virtual Agent OnAgentLoaded()
{
return _agent;
}
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Agents.Enums;
public class AgentRole
{
public const string System = "system";
public const string Assistant = "assistant";
public const string User = "user";
public const string Function = "function";
}

View file

@ -0,0 +1,28 @@
namespace BotSharp.Abstraction.Agents;
public interface IAgentHook
{
Agent Agent { get; }
void SetAget(Agent agent);
/// <summary>
/// Triggered before loading, you can change the returned id to switch agent.
/// </summary>
/// <param name="id">Agent Id</param>
/// <returns></returns>
bool OnAgentLoading(ref string id);
bool OnInstructionLoaded(ref string instruction);
bool OnFunctionsLoaded(ref string functions);
bool OnSamplesLoaded(ref string samples);
/// <summary>
/// Triggered when agent is loaded completely.
/// </summary>
/// <param name="agent"></param>
/// <returns></returns>
Agent OnAgentLoaded();
}

View file

@ -7,6 +7,14 @@ public interface IAgentService
{
Task<Agent> CreateAgent(Agent agent);
Task<List<Agent>> GetAgents();
/// <summary>
/// Load agent configurations and triggher hooks
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<Agent> LoadAgent(string id);
Task<Agent> GetAgent(string id);
Task<bool> DeleteAgent(string id);
Task UpdateAgent(Agent agent);

View file

@ -1,9 +1,8 @@
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
namespace BotSharp.Abstraction.Conversations;
public abstract class ConversationCompletionHookBase : IConversationCompletionHook
public abstract class ConversationHookBase : IConversationHook
{
protected Agent _agent;
public Agent Agent => _agent;
@ -14,34 +13,24 @@ public abstract class ConversationCompletionHookBase : IConversationCompletionHo
protected List<RoleDialogModel> _dialogs;
public List<RoleDialogModel> Dialogs => _dialogs;
protected IChatCompletion _chatCompletion;
public IChatCompletion ChatCompletion => _chatCompletion;
public IConversationCompletionHook SetAgent(Agent agent)
public IConversationHook SetAgent(Agent agent)
{
_agent = agent;
return this;
}
public IConversationCompletionHook SetConversation(Conversation conversation)
public IConversationHook SetConversation(Conversation conversation)
{
_conversation = conversation;
return this;
}
public IConversationCompletionHook SetDialogs(List<RoleDialogModel> dialogs)
public virtual Task OnStateLoaded(ConversationState state)
{
_dialogs = dialogs;
return this;
return Task.CompletedTask;
}
public IConversationCompletionHook SetChatCompletion(IChatCompletion chatCompletion)
{
_chatCompletion = chatCompletion;
return this;
}
public virtual Task OnStateLoaded(ConversationState state, Action<Agent>? onAgentSwitched = null)
public virtual Task OnStateChanged(string name, string preValue, string currentValue)
{
return Task.CompletedTask;
}
@ -51,7 +40,12 @@ public abstract class ConversationCompletionHookBase : IConversationCompletionHo
return Task.CompletedTask;
}
public virtual Task OnFunctionExecuting(string name, string args)
public virtual Task OnFunctionExecuting(RoleDialogModel message)
{
return Task.CompletedTask;
}
public virtual Task OnFunctionExecuted(RoleDialogModel message)
{
return Task.CompletedTask;
}
@ -60,4 +54,10 @@ public abstract class ConversationCompletionHookBase : IConversationCompletionHo
{
return Task.CompletedTask;
}
public virtual Task OnDialogsLoaded(List<RoleDialogModel> dialogs)
{
_dialogs = dialogs;
return Task.CompletedTask;
}
}

View file

@ -1,24 +0,0 @@
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
namespace BotSharp.Abstraction.Conversations;
public interface IConversationCompletionHook
{
Agent Agent { get; }
IConversationCompletionHook SetAgent(Agent agent);
Conversation Conversation { get; }
IConversationCompletionHook SetConversation(Conversation conversation);
List<RoleDialogModel> Dialogs { get; }
IConversationCompletionHook SetDialogs(List<RoleDialogModel> dialogs);
IChatCompletion ChatCompletion { get; }
IConversationCompletionHook SetChatCompletion(IChatCompletion chatCompletion);
Task OnStateLoaded(ConversationState state, Action<Agent>? onAgentSwitched = null);
Task BeforeCompletion();
Task OnFunctionExecuting(string name, string args);
Task AfterCompletion(RoleDialogModel message);
}

View file

@ -0,0 +1,29 @@
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
namespace BotSharp.Abstraction.Conversations;
public interface IConversationHook
{
Agent Agent { get; }
IConversationHook SetAgent(Agent agent);
Conversation Conversation { get; }
IConversationHook SetConversation(Conversation conversation);
List<RoleDialogModel> Dialogs { get; }
/// <summary>
/// Triggered when dialog history is loaded
/// </summary>
/// <param name="dialogs"></param>
/// <returns></returns>
Task OnDialogsLoaded(List<RoleDialogModel> dialogs);
Task OnStateLoaded(ConversationState state);
Task OnStateChanged(string name, string preValue, string currentValue);
Task BeforeCompletion();
Task OnFunctionExecuting(RoleDialogModel message);
Task OnFunctionExecuted(RoleDialogModel message);
Task AfterCompletion(RoleDialogModel message);
}

View file

@ -7,7 +7,9 @@ namespace BotSharp.Abstraction.Conversations;
/// </summary>
public interface IConversationStateService
{
ConversationState Load(string conversationId);
void SetConversation(string conversationId);
ConversationState Load();
string GetState(string name);
void SetState(string name, string value);
void Save();
}

View file

@ -19,6 +19,17 @@ public class RoleDialogModel
/// </summary>
public string? ExecutionResult { get; set; }
/// <summary>
/// When function callback has been executed, system will pass result to LLM again,
/// Set this property to True to stop calling LLM.
/// </summary>
public bool StopSubsequentInteraction { get;set; }
/// <summary>
/// Channel name
/// </summary>
public string Channel { get; set; }
public RoleDialogModel(string role, string text)
{
Role = role;

View file

@ -0,0 +1,47 @@
using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
public async Task<Agent> LoadAgent(string id)
{
var hooks = _services.GetServices<IAgentHook>();
// Before agent is loaded.
foreach (var hook in hooks)
{
hook.OnAgentLoading(ref id);
}
var agent = await GetAgent(id);
// After agent is loaded
foreach (var hook in hooks)
{
hook.SetAget(agent);
if (!string.IsNullOrEmpty(agent.Instruction))
{
var instruction = agent.Instruction;
hook.OnInstructionLoaded(ref instruction);
}
if (!string.IsNullOrEmpty(agent.Functions))
{
var functions = agent.Functions;
hook.OnFunctionsLoaded(ref functions);
}
if (!string.IsNullOrEmpty(agent.Samples))
{
var samples = agent.Samples;
hook.OnSamplesLoaded(ref samples);
}
hook.OnAgentLoaded();
}
return agent;
}
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Functions;
@ -116,10 +117,7 @@ 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 converation = await GetConversation(conversationId);
// Create conversation if this conversation not exists
@ -133,13 +131,18 @@ public class ConversationService : IConversationService
converation = await NewConversation(sess);
}
// load state
// conversation state
var stateService = _services.GetRequiredService<IConversationStateService>();
var state = stateService.Load(conversationId);
state["agentId"] = agentId;
stateService.SetConversation(conversationId);
stateService.Load();
stateService.SetState("agentId", agentId);
// load agent
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(agentId);
// Get relevant domain knowledge
if (_settings.EnableKnowledgeBase)
/*if (_settings.EnableKnowledgeBase)
{
var knowledge = _services.GetRequiredService<IKnowledgeService>();
agent.Knowledges = await knowledge.GetKnowledges(new KnowledgeRetrievalModel
@ -147,21 +150,19 @@ public class ConversationService : IConversationService
AgentId = agentId,
Question = string.Join("\n", wholeDialogs.Select(x => x.Content))
});
}
}*/
var chatCompletion = GetChatCompletion();
var hooks = _services.GetServices<IConversationCompletionHook>().ToList();
var hooks = _services.GetServices<IConversationHook>().ToList();
// Before chat completion hook
foreach (var hook in hooks)
{
hook.SetAgent(agent)
.SetConversation(converation)
.SetDialogs(wholeDialogs)
.SetChatCompletion(chatCompletion);
.SetConversation(converation);
await hook.OnStateLoaded(state, onAgentSwitched: x => agent = x);
await hook.OnDialogsLoaded(wholeDialogs);
await hook.BeforeCompletion();
}
@ -172,7 +173,7 @@ public class ConversationService : IConversationService
// Before executing functions
foreach (var hook in hooks)
{
await hook.OnFunctionExecuting(msg.FunctionName, msg.Content);
await hook.OnFunctionExecuting(msg);
}
// Save states
var jo = JsonSerializer.Deserialize<object>(msg.Content);
@ -180,11 +181,7 @@ public class ConversationService : IConversationService
{
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;
stateService.SetState(property.Name, property.Value.ToString());
}
}
}
@ -197,6 +194,15 @@ public class ConversationService : IConversationService
}
}
await onMessageReceived(msg);
if (msg.Role == AgentRole.Function)
{
// After functions have been executed
foreach (var hook in hooks)
{
await hook.OnFunctionExecuted(msg);
}
}
});
return result;

View file

@ -1,4 +1,6 @@
using BotSharp.Abstraction.Conversations.Models;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Logging;
using System.IO;
namespace BotSharp.Core.Conversations.Services;
@ -8,27 +10,44 @@ namespace BotSharp.Core.Conversations.Services;
/// </summary>
public class ConversationStateService : IConversationStateService, IDisposable
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
private ConversationState _state;
private MyDatabaseSettings _dbSettings;
private string _conversationId;
private string _file;
public ConversationStateService(MyDatabaseSettings dbSettings)
public ConversationStateService(ILogger<ConversationStateService> logger,
IServiceProvider services,
MyDatabaseSettings dbSettings)
{
_logger = logger;
_services = services;
_dbSettings = dbSettings;
}
public void SetState(string name, string value)
{
_state[name] = value;
var hooks = _services.GetServices<IConversationHook>();
string preValue = _state.ContainsKey(name) ? _state[name] : "";
if (!_state.ContainsKey(name) || _state[name] != value)
{
var currentValue = value;
_state[name] = currentValue;
_logger.LogInformation($"Set state: {name} - {value}");
foreach (var hook in hooks)
{
hook.OnStateChanged(name, preValue, currentValue).Wait();
}
}
}
public void Dispose()
public void SetConversation(string conversationId)
{
Save();
_conversationId = conversationId;
}
public ConversationState Load(string conversationId)
public ConversationState Load()
{
if (_state != null)
{
@ -36,7 +55,6 @@ public class ConversationStateService : IConversationStateService, IDisposable
}
_state = new ConversationState();
_conversationId = conversationId;
_file = GetStorageFile(_conversationId);
@ -45,10 +63,17 @@ public class ConversationStateService : IConversationStateService, IDisposable
var dict = File.ReadAllLines(_file);
foreach (var line in dict)
{
_state[line.Split(':')[0]] = line.Split(':')[1];
_state[line.Split('=')[0]] = line.Split('=')[1];
}
}
_logger.LogInformation($"Loaded state {_conversationId}");
var hooks = _services.GetServices<IConversationHook>();
foreach (var hook in hooks)
{
hook.OnStateLoaded(_state).Wait();
}
return _state;
}
@ -58,9 +83,10 @@ public class ConversationStateService : IConversationStateService, IDisposable
foreach (var dic in _state)
{
states.Add($"{dic.Key}:{dic.Value}");
states.Add($"{dic.Key}={dic.Value}");
}
File.WriteAllLines(_file, states);
_logger.LogInformation($"Saved state {_conversationId}");
}
private string GetStorageFile(string conversationId)
@ -81,4 +107,9 @@ public class ConversationStateService : IConversationStateService, IDisposable
}
return _state[name];
}
public void Dispose()
{
Save();
}
}

View file

@ -117,6 +117,13 @@ public class ChatCompletionProvider : IChatCompletion
// Execute functions
await onMessageReceived(funcContextIn);
if (funcContextIn.StopSubsequentInteraction)
{
// Emit a fake message that should be populated by whom set StopSubsequentInteraction as True.
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), ""));
return true;
}
// After function is executed, pass the result to LLM
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, funcContextIn.ExecutionResult)
{

View file

@ -15,6 +15,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Refit;
using BotSharp.Abstraction.Agents.Enums;
namespace BotSharp.Plugin.MetaMessenger.Controllers;
@ -94,8 +95,15 @@ public class WebhookController : ControllerBase
});
// Go to LLM
var result = await conv.SendMessage(agentId, senderId, new RoleDialogModel("user", input), async msg =>
var result = await conv.SendMessage(agentId, senderId, new RoleDialogModel("user", input)
{
Channel = "messenger"
}, async msg =>
{
if (msg.Role == AgentRole.Function)
{
}
content = msg.Content;
}, async fn =>
{