Merge pull request #105 from hchen2020/master

Support liquid template.
This commit is contained in:
Haiping 2023-08-17 23:30:53 -05:00 committed by GitHub
commit 2d9c97219b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 232 additions and 256 deletions

View file

@ -80,6 +80,7 @@ The main documentation for the site is organized into the following sections:
:caption: Prompt Engineering
prompt/intro
prompt/template
.. _architecture-docs:

View file

@ -1 +1,3 @@
# Prompt Engineering
# Prompt Engineering
LLM uses prompt as input, and the model produces different outputs according to the input.

3
docs/prompt/template.md Normal file
View file

@ -0,0 +1,3 @@
# Template
We can define the prompt as a template, and the template can be changed according to variables, so that a instruction file can be used to generate a dynamic prompt.

View file

@ -1,46 +0,0 @@
using BotSharp.Abstraction.Conversations.Models;
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 void OnAgentLoaded(Agent agent)
{
}
public virtual bool OnAgentRouting(RoleDialogModel message, ref string id)
{
return true;
}
}

View file

@ -13,7 +13,7 @@ public interface IAgentHook
bool OnAgentLoading(ref string id);
bool OnInstructionLoaded(ref string instruction);
bool OnInstructionLoaded(string template, Dictionary<string, object> dict);
bool OnFunctionsLoaded(ref string functions);

View file

@ -7,4 +7,5 @@ public class AgentSettings
/// </summary>
public string RouterId { get; set; }
public string DataDir { get; set; }
public string TemplateFormat { get; set; }
}

View file

@ -5,6 +5,6 @@ namespace BotSharp.Abstraction.Conversations;
public interface IConversationStorage
{
void InitStorage(string conversationId);
void Append(string conversationId, RoleDialogModel dialog);
void Append(string conversationId, Agent agent, RoleDialogModel dialog);
List<RoleDialogModel> GetDialogs(string conversationId);
}

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Agents.Enums;
namespace BotSharp.Abstraction.Conversations.Models;
public class RoleDialogModel
@ -40,6 +42,13 @@ public class RoleDialogModel
public override string ToString()
{
return $"{Role}: {Content}";
if (Role == AgentRole.Function)
{
return $"{Role}: {FunctionName}";
}
else
{
return $"{Role}: {Content}";
}
}
}

View file

@ -4,7 +4,10 @@ namespace BotSharp.Abstraction.MLTasks;
public interface IChatCompletion
{
// string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
Task<bool> GetChatCompletionsAsync(Agent agent,
List<RoleDialogModel> conversations,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting);
Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived);
}

View file

@ -0,0 +1,76 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using Fluid;
using Microsoft.AspNetCore.Mvc;
namespace BotSharp.Core.Agents.Services;
public abstract class AgentHookBase : IAgentHook
{
protected Agent _agent;
public Agent Agent => _agent;
private static readonly FluidParser _parser = new FluidParser();
private readonly IServiceProvider _services;
public AgentHookBase(IServiceProvider services)
{
_services = services;
}
public void SetAget(Agent agent)
{
_agent = agent;
}
public virtual bool OnAgentLoading(ref string id)
{
return true;
}
public virtual bool OnInstructionLoaded(string template, Dictionary<string, object> dict)
{
if (_parser.TryParse(template, out var t, out var error))
{
PopulateStateTokens(dict);
var context = new TemplateContext(dict);
_agent.Instruction = t.Render(context);
return true;
}
else
{
return false;
}
}
private void PopulateStateTokens(Dictionary<string, object> dict)
{
var stateService = _services.GetRequiredService<IConversationStateService>();
var state = stateService.Load();
foreach (var t in state)
{
dict[t.Key] = t.Value;
}
}
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 void OnAgentLoaded(Agent agent)
{
}
public virtual bool OnAgentRouting(RoleDialogModel message, ref string id)
{
return true;
}
}

View file

@ -20,8 +20,8 @@ public class AgentRouter : IAgentRouting
public async Task<Agent> LoadCurrentAgent()
{
// Load current agent from state
var stateService = _services.GetRequiredService<IConversationStateService>();
var currentAgentId = stateService.GetState("agentId");
var state = _services.GetRequiredService<IConversationStateService>();
var currentAgentId = state.GetState("agentId");
if (string.IsNullOrEmpty(currentAgentId))
{
currentAgentId = _settings.RouterId;
@ -30,7 +30,7 @@ public class AgentRouter : IAgentRouting
var agent = await agentService.LoadAgent(currentAgentId);
// Set agent and trigger state changed
stateService.SetState("agentId", currentAgentId);
state.SetState("agentId", currentAgentId);
return agent;
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Agents.Models;
using Microsoft.Extensions.Logging;
using System.IO;
namespace BotSharp.Core.Agents.Services;
@ -26,7 +25,7 @@ public partial class AgentService
var profile = query.FirstOrDefault();
var dir = GetAgentDataDir(id);
var instructionFile = Path.Combine(dir, "instruction.txt");
var instructionFile = Path.Combine(dir, $"instruction.{_settings.TemplateFormat}");
if (File.Exists(instructionFile))
{
profile.Instruction = File.ReadAllText(instructionFile);
@ -36,7 +35,7 @@ public partial class AgentService
_logger.LogError($"Can't find instruction file from {instructionFile}");
}
var samplesFile = Path.Combine(dir, "samples.txt");
var samplesFile = Path.Combine(dir, $"samples.{_settings.TemplateFormat}");
if (File.Exists(samplesFile))
{
profile.Samples = File.ReadAllText(samplesFile);

View file

@ -23,8 +23,7 @@ public partial class AgentService
if (!string.IsNullOrEmpty(agent.Instruction))
{
var instruction = agent.Instruction;
hook.OnInstructionLoaded(ref instruction);
hook.OnInstructionLoaded(agent.Instruction, new Dictionary<string, object>());
}
if (!string.IsNullOrEmpty(agent.Functions))

View file

@ -75,6 +75,7 @@
<ItemGroup>
<PackageReference Include="Colorful.Console" Version="1.2.15" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="6.2.1" />
<PackageReference Include="Fluid.Core" Version="2.4.0" />
<PackageReference Include="LLamaSharp" Version="0.4.2-preview" />
<PackageReference Include="PdfPig" Version="0.1.8" />
<PackageReference Include="TensorFlow.Keras" Version="0.11.2" />

View file

@ -47,14 +47,16 @@ public class ConversationController : ControllerBase, IApiAdapter
var conv = _services.GetRequiredService<IConversationService>();
var response = new MessageResponseModel();
var stackMsg = new List<RoleDialogModel>();
await conv.SendMessage(agentId, conversationId,
new RoleDialogModel("user", input.Text),
async msg =>
response.Text = msg.Content,
stackMsg.Add(msg),
async fn
=> await Task.CompletedTask);
response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content));
return response;
}
}

View file

@ -73,15 +73,10 @@ public class ConversationService : IConversationService
}
public async Task<bool> SendMessage(string agentId, string conversationId,
RoleDialogModel lastDalog,
RoleDialogModel lastDialog,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
lastDalog.CurrentAgentId = agentId;
_storage.Append(conversationId, lastDalog);
var wholeDialogs = GetDialogHistory(conversationId);
var converation = await GetConversation(conversationId);
// Create conversation if this conversation not exists
@ -103,6 +98,11 @@ public class ConversationService : IConversationService
var router = _services.GetRequiredService<IAgentRouting>();
var agent = await router.LoadCurrentAgent();
lastDialog.CurrentAgentId = agent.Id;
_storage.Append(conversationId, agent, lastDialog);
var wholeDialogs = GetDialogHistory(conversationId);
// Get relevant domain knowledge
/*if (_settings.EnableKnowledgeBase)
{
@ -127,100 +127,81 @@ public class ConversationService : IConversationService
}
var chatCompletion = GetChatCompletion();
var result = await GetChatCompletionsAsyncRecursively(chatCompletion,
conversationId,
agent,
wholeDialogs,
onMessageReceived,
onFunctionExecuting);
return result;
}
private async Task<bool> GetChatCompletionsAsyncRecursively(IChatCompletion chatCompletion,
string conversationId,
Agent agent,
List<RoleDialogModel> wholeDialogs,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg =>
{
await HandleMessage(conversationId, agent, msg, onMessageReceived, onFunctionExecuting);
await HandleAssistantMessage(msg, onMessageReceived);
if (msg.NeedReloadAgent)
// Add to dialog history
_storage.Append(conversationId, agent, msg);
}, async fn =>
{
var preAgentId = agent.Id;
await HandleFunctionMessage(fn, onFunctionExecuting);
// Agent has been transferred
if (fn.CurrentAgentId != preAgentId)
{
await HandleMessageIfAgentReloaded(conversationId, agent, msg, wholeDialogs, onMessageReceived, onFunctionExecuting);
var agentService = _services.GetRequiredService<IAgentService>();
agent = await agentService.LoadAgent(fn.CurrentAgentId);
// Set state to make next conversation will go to this agent directly
var state = _services.GetRequiredService<IConversationStateService>();
state.SetState("agentId", fn.CurrentAgentId);
}
fn.Content = fn.ExecutionResult;
// Add to dialog history
_storage.Append(conversationId, agent, fn);
// After function is executed, pass the result to LLM to get a natural response
wholeDialogs.Add(fn);
await GetChatCompletionsAsyncRecursively(chatCompletion, conversationId, agent, wholeDialogs, onMessageReceived, onFunctionExecuting);
});
return result;
}
private async Task HandleMessage(string conversationId, Agent agent, RoleDialogModel msg,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
private async Task HandleAssistantMessage(RoleDialogModel msg, Func<RoleDialogModel, Task> onMessageReceived)
{
if (msg.Role == "function")
var hooks = _services.GetServices<IConversationHook>().ToList();
// After chat completion hook
foreach (var hook in hooks)
{
// Save states
SaveStateByArgs(msg.FunctionArgs);
// Call functions
await onFunctionExecuting(msg);
await CallFunctions(msg);
// Add to dialog history
if (msg.ExecutionResult != null)
{
if (msg.NeedReloadAgent)
{
_logger.LogInformation($"Skipped append dialog log: {msg.FunctionName}\n{msg.FunctionArgs}\n{msg.ExecutionResult}");
return;
}
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content)
{
CurrentAgentId = agent.Id,
FunctionName = msg.FunctionName,
FunctionArgs = msg.FunctionArgs,
ExecutionResult = msg.ExecutionResult
});
}
await hook.AfterCompletion(msg);
}
else
{
// Add to dialog history
_storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content)
{
CurrentAgentId = agent.Id
});
var hooks = _services.GetServices<IConversationHook>().ToList();
// After chat completion hook
foreach (var hook in hooks)
{
await hook.AfterCompletion(msg);
}
await onMessageReceived(msg);
}
await onMessageReceived(msg);
}
private async Task HandleMessageIfAgentReloaded(string conversationId, Agent agent,
RoleDialogModel msg,
List<RoleDialogModel> wholeDialogs,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
private async Task HandleFunctionMessage(RoleDialogModel msg, Func<RoleDialogModel, Task> onFunctionExecuting)
{
var state = _services.GetRequiredService<IConversationStateService>();
var currentAgentId = state.GetState("agentId");
// Save states
SaveStateByArgs(msg.FunctionArgs);
// Send to LLM to get final response when agent is switched.
var conv = _services.GetRequiredService<IConversationService>();
var chatCompletion = conv.GetChatCompletion();
var agentService = _services.GetRequiredService<IAgentService>();
var newAgent = await agentService.LoadAgent(currentAgentId);
await chatCompletion.GetChatCompletionsAsync(newAgent, wholeDialogs, async newMsg =>
{
if (newMsg.Role == AgentRole.Function)
{
await HandleMessage(conversationId, agent, newMsg, onMessageReceived, onFunctionExecuting);
}
else
{
msg.StopPropagate = true;
await onMessageReceived(newMsg);
_storage.Append(conversationId, new RoleDialogModel(newMsg.Role, newMsg.Content)
{
CurrentAgentId = agent.Id
});
}
});
// Call functions
await onFunctionExecuting(msg);
await CallFunctions(msg);
}
private void SaveStateByArgs(string args)

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using System.IO;
@ -12,7 +13,7 @@ public class ConversationStorage : IConversationStorage
_dbSettings = dbSettings;
}
public void Append(string conversationId, RoleDialogModel dialog)
public void Append(string conversationId, Agent agent, RoleDialogModel dialog)
{
var conversationFile = GetStorageFile(conversationId);
var sb = new StringBuilder();
@ -21,7 +22,7 @@ public class ConversationStorage : IConversationStorage
{
var args = dialog.FunctionArgs.Replace("\r", " ").Replace("\n", " ").Trim();
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}|{dialog.FunctionName}|{args}");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}|{dialog.FunctionName}|{args}");
var content = dialog.ExecutionResult.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
@ -32,7 +33,7 @@ public class ConversationStorage : IConversationStorage
}
else if (dialog.Role == AgentRole.Assistant)
{
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|||");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}||");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{
@ -42,7 +43,7 @@ public class ConversationStorage : IConversationStorage
}
else
{
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}||");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}||");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{

View file

@ -19,7 +19,10 @@ public class ChatCompletionProvider : IChatCompletion
throw new NotImplementedException();
}
public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
public async Task<bool> GetChatCompletionsAsync(Agent agent,
List<RoleDialogModel> conversations,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
var content = string.Join("\n", conversations.Select(x => $"{x.Role}: {x.Content.Replace("user:", "User:")}")).Trim();
content += "\nBob: ";

View file

@ -1,5 +1,6 @@
using Azure;
using Azure.AI.OpenAI;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Functions.Models;
@ -31,36 +32,6 @@ public class ChatCompletionProvider : IChatCompletion
return client;
}
/*public string GetChatCompletions(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
var client = GetClient();
var chatCompletionsOptions = PrepareOptions(agent, conversations);
var response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
var choice = response.Value.Choices[0];
var message = choice.Message;
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
response = HandleFunctionCall(message,
onMessageReceived,
chatCompletionsOptions).Result;
}
choice = response.Value.Choices[0];
message = choice.Message;
_logger.LogInformation(message.Content);
if (!string.IsNullOrEmpty(message.Content))
{
onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content))
.Wait();
}
return message.Content.Trim();
}*/
public List<RoleDialogModel> GetChatSamples(string sampleText)
{
var samples = new List<RoleDialogModel>();
@ -107,7 +78,10 @@ public class ChatCompletionProvider : IChatCompletion
return functions;
}
public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
public async Task<bool> GetChatCompletionsAsync(Agent agent,
List<RoleDialogModel> conversations,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting)
{
var client = GetClient();
var chatCompletionsOptions = PrepareOptions(agent, conversations);
@ -118,24 +92,29 @@ public class ChatCompletionProvider : IChatCompletion
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
response = await HandleFunctionCall(agent,
message,
onMessageReceived,
chatCompletionsOptions);
}
_logger.LogInformation($"[{agent.Name}]: {message.FunctionCall.Name} => {message.FunctionCall.Arguments}");
if (response != null)
{
choice = response.Value.Choices[0];
message = choice.Message;
_logger.LogInformation(message.Content);
if (!string.IsNullOrEmpty(message.Content))
var funcContextIn = new RoleDialogModel(AgentRole.Function, message.Content)
{
var msgByLlm = new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content);
await onMessageReceived(msgByLlm);
}
CurrentAgentId = agent.Id,
FunctionName = message.FunctionCall.Name,
FunctionArgs = message.FunctionCall.Arguments
};
// Execute functions
await onFunctionExecuting(funcContextIn);
}
else
{
_logger.LogInformation($"[{agent.Name}] {message.Role}: {message.Content}");
var msg = new RoleDialogModel(AgentRole.Assistant, message.Content)
{
CurrentAgentId= agent.Id
};
// Text response received
await onMessageReceived(msg);
}
return true;
@ -185,56 +164,6 @@ public class ChatCompletionProvider : IChatCompletion
return true;
}
private async Task<Response<ChatCompletions>> HandleFunctionCall(Agent agent,
ChatMessage message,
Func<RoleDialogModel, Task> onMessageReceived,
ChatCompletionsOptions chatCompletionsOptions)
{
Response<ChatCompletions> response = default;
if (message.FunctionCall == null || message.FunctionCall.Arguments == null)
{
return response;
}
_logger.LogInformation($"{message.FunctionCall.Name}: {message.FunctionCall.Arguments}");
var funcContextIn = new RoleDialogModel(ChatRole.Function.ToString(), message.Content)
{
CurrentAgentId = agent.Id,
FunctionName = message.FunctionCall.Name,
FunctionArgs = message.FunctionCall.Arguments
};
// Execute functions
await onMessageReceived(funcContextIn);
if (funcContextIn.StopPropagate)
{
return response;
}
if (funcContextIn.IsConversationEnd)
{
await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), funcContextIn.Content)
{
IsConversationEnd = true
});
return response;
}
// After function is executed, pass the result to LLM
if (funcContextIn.ExecutionResult != null)
{
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, funcContextIn.ExecutionResult)
{
Name = funcContextIn.FunctionName
});
var client = GetClient();
response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions);
}
return response;
}
private ChatCompletionsOptions PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
{

View file

@ -26,26 +26,18 @@ public class fastTextEmbeddingProvider : ITextEmbedding
public fastTextEmbeddingProvider(fastTextSetting settings)
{
_settings = settings;
_fastText = new FastTextWrapper();
if (!File.Exists(settings.ModelPath))
{
throw new FileNotFoundException($"Can't load pre-trained word vectors from {settings.ModelPath}.\n Try to download from https://fasttext.cc/docs/en/english-vectors.html.");
}
}
public float[] GetVector(string text)
{
if (!_fastText.IsModelReady())
{
_fastText.LoadModel(_settings.ModelPath);
}
LoadModel();
return _fastText.GetSentenceVector(text);
}
public List<float[]> GetVectors(List<string> texts)
{
LoadModel();
var vectors = new List<float[]>();
for (int i = 0; i < texts.Count; i++)
{
@ -53,4 +45,22 @@ public class fastTextEmbeddingProvider : ITextEmbedding
}
return vectors;
}
private void LoadModel()
{
if (_fastText == null)
{
if (!File.Exists(_settings.ModelPath))
{
throw new FileNotFoundException($"Can't load pre-trained word vectors from {_settings.ModelPath}.\n Try to download from https://fasttext.cc/docs/en/english-vectors.html.");
}
_fastText = new FastTextWrapper();
if (!_fastText.IsModelReady())
{
_fastText.LoadModel(_settings.ModelPath);
}
}
}
}

View file

@ -14,7 +14,9 @@
},
"Agent": {
"DataDir": "agents"
"RouterId": "",
"DataDir": "agents",
"TemplateFormat": "liquid"
},
"Conversation": {