Merge from master into add_richCoontent Branch

This commit is contained in:
Yanan Wang 2023-10-27 16:21:52 -05:00
commit eb4e0b0187
42 changed files with 251 additions and 236 deletions

View file

@ -1,6 +1,6 @@
# Messaging Components
Conversations are a lot more than simple text messages when you are building a AI chatbot. In addition to text, the `BotSharp`` allows you to send rich-media, like audio, video, and images, and provides a set of structured messaging options in the form of message templates, quick replies, buttons and more. The UI rendering program can render components according to the returned data format.
Conversations are a lot more than simple text messages when you are building a AI chatbot. In addition to text, the `BotSharp` allows you to send rich-media, like audio, video, and images, and provides a set of structured messaging options in the form of message templates, quick replies, buttons and more. The UI rendering program can render components according to the returned data format.
## Text Messages
@ -75,7 +75,7 @@ Message templates are structured message formats used for various purposes to pr
...
}
]
}
}
}
}
}

View file

@ -1,36 +1,9 @@
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Conversations.Models;
public class IncomingMessageModel
public class IncomingMessageModel : MessageConfig
{
public string Text { get; set; } = string.Empty;
public virtual string Channel { get; set; } = string.Empty;
/// <summary>
/// Completion Provider
/// </summary>
[JsonPropertyName("provider")]
public virtual string? Provider { get; set; } = null;
/// <summary>
/// Model name
/// </summary>
[JsonPropertyName("model")]
public virtual string? Model { get; set; } = null;
/// <summary>
/// The sampling temperature to use that controls the apparent creativity of generated completions.
/// </summary>
public float Temperature { get; set; } = 0.5f;
/// <summary>
/// An alternative value to Temperature, called nucleus sampling, that causes
/// the model to consider the results of the tokens with probability mass.
/// </summary>
public float SamplingFactor { get; set; } = 0.5f;
/// <summary>
/// Conversation states from input
/// </summary>
public List<string> States { get; set; } = new List<string>();
public virtual string Channel { get; set; }
}

View file

@ -1,7 +1,12 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Conversations.Models;
public class RoleDialogModel
public class RoleDialogModel : ITrackableMessage
{
public string MessageId { get; set; }
/// <summary>
/// user, system, assistant, function
/// </summary>
@ -36,10 +41,17 @@ public class RoleDialogModel
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public bool StopCompletion { get; set; }
public FunctionCallFromLlm Instruction { get; set; }
private RoleDialogModel()
{
}
public RoleDialogModel(string role, string text)
{
Role = role;
Content = text;
MessageId = Guid.NewGuid().ToString();
}
public override string ToString()

View file

@ -4,6 +4,7 @@ public class FunctionDef
{
public string Name { get; set; }
public string Description { get; set; }
public string? Impact { get; set; }
public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef();
public override string ToString()

View file

@ -1,7 +1,10 @@
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Instructs.Models;
public class InstructResult
public class InstructResult : ITrackableMessage
{
public string MessageId { get; set; }
public string Text { get; set; }
public object Data { get; set; }
public ConversationState States { get; set; }

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Models;
/// <summary>
/// Define a message ID to extend message-level applications, such as model fees, token usage, and data collection
/// </summary>
public interface ITrackableMessage
{
string MessageId { get; set; }
}

View file

@ -0,0 +1,32 @@
namespace BotSharp.Abstraction.Models;
public class MessageConfig
{
/// <summary>
/// Completion Provider
/// </summary>
[JsonPropertyName("provider")]
public virtual string? Provider { get; set; } = null;
/// <summary>
/// Model name
/// </summary>
[JsonPropertyName("model")]
public virtual string? Model { get; set; } = null;
/// <summary>
/// The sampling temperature to use that controls the apparent creativity of generated completions.
/// </summary>
public float Temperature { get; set; } = 0.5f;
/// <summary>
/// An alternative value to Temperature, called nucleus sampling, that causes
/// the model to consider the results of the tokens with probability mass.
/// </summary>
public float SamplingFactor { get; set; } = 0.5f;
/// <summary>
/// Conversation states from input
/// </summary>
public List<string> States { get; set; } = new List<string>();
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
namespace BotSharp.Abstraction.Routing;
@ -19,5 +18,5 @@ public interface IRoutingHandler
void SetDialogs(List<RoleDialogModel> dialogs) { }
Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst);
Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message);
}

View file

@ -8,7 +8,7 @@ public interface IRoutingService
void ResetRecursiveCounter();
void RefreshDialogs();
Task<FunctionCallFromLlm> GetNextInstruction();
Task<RoleDialogModel> InvokeAgent(string agentId);
Task<RoleDialogModel> InstructLoop();
Task<RoleDialogModel> ExecuteOnce(Agent agent);
Task<bool> InvokeAgent(string agentId, RoleDialogModel message);
Task<bool> InstructLoop(RoleDialogModel message);
Task<bool> ExecuteOnce(Agent agent, RoleDialogModel message);
}

View file

@ -18,8 +18,11 @@ public class RoutingContext
/// </summary>
public string IntentName { get; set; }
/// <summary>
/// Agent that can handl user original goal.
/// </summary>
public string OriginAgentId
=> _stack.Last();
=> _stack.Where(x => x != _setting.RouterId).Last();
public string GetCurrentAgentId()
{

View file

@ -7,4 +7,5 @@ global using System.ComponentModel.DataAnnotations;
global using System.Text.Json.Serialization;
global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Models;

View file

@ -1,3 +1,4 @@
using System.Text.Json;
using System.Text.RegularExpressions;
namespace BotSharp.Abstraction.Utilities;
@ -27,4 +28,16 @@ public static class StringExtensions
{
return str1.Equals(str2, option);
}
public static string JsonContent(this string text)
{
var m = Regex.Match(text, @"\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!))\}");
return m.Success ? m.Value : "{}";
}
public static T? JsonContent<T>(this string text)
{
text = JsonContent(text);
return JsonSerializer.Deserialize<T>(text);
}
}

View file

@ -4,18 +4,14 @@ namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
#if !DEBUG
[MemoryCache(10 * 60)]
#endif
[MemoryCache(10 * 60, PerInstanceCache = true)]
public async Task<List<Agent>> GetAgents(bool? allowRouting = null)
{
var agents = _db.GetAgents(allowRouting: allowRouting);
return await Task.FromResult(agents);
}
#if !DEBUG
[MemoryCache(10 * 60)]
#endif
[MemoryCache(10 * 60, PerInstanceCache = true)]
public async Task<Agent> GetAgent(string id)
{
var profile = _db.GetAgent(id);

View file

@ -8,7 +8,7 @@ namespace BotSharp.Core.Conversations.Services;
public partial class ConversationService
{
public async Task<bool> SendMessage(string agentId,
RoleDialogModel incoming,
RoleDialogModel message,
Func<RoleDialogModel, Task> onMessageReceived,
Func<RoleDialogModel, Task> onFunctionExecuting,
Func<RoleDialogModel, Task> onFunctionExecuted)
@ -18,16 +18,16 @@ public partial class ConversationService
var agentService = _services.GetRequiredService<IAgentService>();
Agent agent = await agentService.LoadAgent(agentId);
var message = $"Received [{agent.Name}] {incoming.Role}: {incoming.Content}";
var content = $"Received [{agent.Name}] {message.Role}: {message.Content}";
#if DEBUG
Console.WriteLine(message, Color.OrangeRed);
Console.WriteLine(content, Color.OrangeRed);
#else
_logger.LogInformation(message);
_logger.LogInformation(content);
#endif
incoming.CurrentAgentId = agent.Id;
message.CurrentAgentId = agent.Id;
_storage.Append(_conversationId, incoming);
_storage.Append(_conversationId, message);
var hooks = _services.GetServices<IConversationHook>().ToList();
@ -37,13 +37,13 @@ public partial class ConversationService
hook.SetAgent(agent)
.SetConversation(conversation);
await hook.OnMessageReceived(incoming);
await hook.OnMessageReceived(message);
// Interrupted by hook
if (incoming.StopCompletion)
if (message.StopCompletion)
{
await onMessageReceived(incoming);
_storage.Append(_conversationId, incoming);
await onMessageReceived(message);
_storage.Append(_conversationId, message);
return true;
}
}
@ -52,11 +52,11 @@ public partial class ConversationService
var routing = _services.GetRequiredService<IRoutingService>();
var settings = _services.GetRequiredService<RoutingSettings>();
var response = agentId == settings.RouterId ?
await routing.InstructLoop() :
await routing.ExecuteOnce(agent);
var ret = agentId == settings.RouterId ?
await routing.InstructLoop(message) :
await routing.ExecuteOnce(agent, message);
await HandleAssistantMessage(response, onMessageReceived);
await HandleAssistantMessage(message, onMessageReceived);
var statistics = _services.GetRequiredService<ITokenStatistics>();
statistics.PrintStatistics();
@ -64,7 +64,7 @@ public partial class ConversationService
routing.ResetRecursiveCounter();
routing.RefreshDialogs();
return true;
return ret;
}
private async Task<Conversation> GetConversationRecord(string agentId)

View file

@ -54,8 +54,6 @@ public partial class ConversationService : IConversationService
public async Task<Conversation> NewConversation(Conversation sess)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var dbSettings = _services.GetRequiredService<BotSharpDatabaseSettings>();
var conversationSettings = _services.GetRequiredService<ConversationSetting>();
var user = db.GetUserByExternalId(_user.Id);
var foundUserId = user?.Id ?? string.Empty;
@ -80,11 +78,11 @@ public partial class ConversationService : IConversationService
throw new NotImplementedException();
}
public List<RoleDialogModel> GetDialogHistory(int lastCount = 20)
public List<RoleDialogModel> GetDialogHistory(int lastCount = 50)
{
var dialogs = _storage.GetDialogs(_conversationId);
return dialogs
.Where(x => x.CreatedAt > DateTime.UtcNow.AddHours(-8))
.Where(x => x.CreatedAt > DateTime.UtcNow.AddHours(-24))
.TakeLast(lastCount)
.ToList();
}

View file

@ -6,19 +6,13 @@ namespace BotSharp.Core.Conversations.Services;
public class ConversationStorage : IConversationStorage
{
private readonly BotSharpDatabaseSettings _dbSettings;
private readonly AgentSettings _agentSettings;
private readonly IServiceProvider _services;
private readonly IUserIdentity _user;
public ConversationStorage(
BotSharpDatabaseSettings dbSettings,
AgentSettings agentSettings,
IServiceProvider services,
IUserIdentity user)
IServiceProvider services)
{
_dbSettings = dbSettings;
_agentSettings = agentSettings;
_services = services;
_user = user;
}
public void Append(string conversationId, RoleDialogModel dialog)
@ -32,7 +26,7 @@ public class ConversationStorage : IConversationStorage
{
var args = dialog.FunctionArgs.Replace("\r", " ").Replace("\n", " ").Trim();
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{dialog.FunctionName}|{args}");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{dialog.MessageId}");
var content = dialog.Content;
content = content.Replace("\r", " ").Replace("\n", " ").Trim();
@ -44,9 +38,7 @@ public class ConversationStorage : IConversationStorage
}
else
{
var agentName = db.GetAgent(agentId)?.Name;
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{agentName}|");
sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{dialog.MessageId}");
var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim();
if (string.IsNullOrEmpty(content))
{
@ -73,15 +65,13 @@ public class ConversationStorage : IConversationStorage
var createdAt = DateTime.Parse(meta.Split('|')[0]);
var role = meta.Split('|')[1];
var currentAgentId = meta.Split('|')[2];
var funcName = meta.Split('|')[3];
var funcArgs= meta.Split('|')[4];
var messageId = meta.Split('|')[3];
var text = dialog.Substring(4);
results.Add(new RoleDialogModel(role, text)
{
CurrentAgentId = currentAgentId,
FunctionName = funcName,
FunctionArgs = funcArgs,
MessageId = messageId,
Content = text,
CreatedAt = createdAt
});

View file

@ -33,6 +33,7 @@ public partial class InstructService : IInstructService
{
return new InstructResult
{
MessageId = message.MessageId,
Text = message.Content
};
}
@ -42,6 +43,7 @@ public partial class InstructService : IInstructService
var result = await completer.GetCompletion(agent.Instruction);
var response = new InstructResult
{
MessageId = message.MessageId,
Text = result
};

View file

@ -5,7 +5,6 @@ using BotSharp.Abstraction.Users.Models;
using BotSharp.Abstraction.Agents.Models;
using MongoDB.Driver;
using BotSharp.Abstraction.Routing.Models;
using Amazon.Util;
namespace BotSharp.Core.Repository;
@ -373,13 +372,7 @@ public class FileRepository : IBotSharpRepository
var functionFile = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir,
agentId, "functions.json");
var functions = new List<string>();
foreach (var function in inputFunctions)
{
functions.Add(JsonSerializer.Serialize(function, _options));
}
var functionText = JsonSerializer.Serialize(functions, _options);
var functionText = JsonSerializer.Serialize(inputFunctions, _options);
File.WriteAllText(functionFile, functionText);
}
@ -493,9 +486,6 @@ public class FileRepository : IBotSharpRepository
return responses;
}
#if !DEBUG
[MemoryCache(10 * 60)]
#endif
public Agent? GetAgent(string agentId)
{
var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir);

View file

@ -66,14 +66,11 @@ public class RouteToAgentFn : IFunctionCallback
else
{
message.CurrentAgentId = targetAgent.Id;
message.Content = $"Routing to {args.AgentName}";
}
}
_context.Push(message.CurrentAgentId);
// Set default execution data
message.Data = JsonSerializer.Deserialize<JsonElement>(message.FunctionArgs);
return true;
}

View file

@ -26,18 +26,15 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase, IRoutingHan
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgents(inst.AgentName).FirstOrDefault();
var result = new RoleDialogModel(AgentRole.Function, inst.Question)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Arguments),
CurrentAgentId = record.Id
};
message.FunctionName = inst.Function;
message.CurrentAgentId = record.Id;
message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments);
return result;
return true;
}
}

View file

@ -24,14 +24,11 @@ public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
message.Content = inst.Response;
message.FunctionName = inst.Function;
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
@ -39,9 +36,9 @@ public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
foreach (var hook in hooks)
{
await hook.OnConversationEnding(result);
await hook.OnConversationEnding(message);
}
return result;
return true;
}
}

View file

@ -9,30 +9,24 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle
{
public string Name => "human_intervention_needed";
public string Description => "Reach out to a real human or customer representative.";
private readonly RoutingSettings _settings;
public string Description => "Reach out to human being, customer service or customer representative.";
public List<NameDesc> Parameters => new List<NameDesc>
{
new NameDesc("reason", "why need customer service representative (human being)"),
new NameDesc("reason", "why need customer service"),
new NameDesc("response", "response content to user")
};
public HumanInterventionNeededHandler(IServiceProvider services, ILogger<HumanInterventionNeededHandler> logger, RoutingSettings settings)
: base(services, logger, settings)
{
_settings = settings;
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
message.Role = AgentRole.Assistant;
message.Content = inst.Response;
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
@ -40,9 +34,9 @@ public class HumanInterventionNeededHandler : RoutingHandlerBase, IRoutingHandle
foreach (var hook in hooks)
{
await hook.OnHumanInterventionNeeded(result);
await hook.OnHumanInterventionNeeded(message);
}
return result;
return true;
}
}

View file

@ -24,14 +24,11 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase, IRouting
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.User, inst.Reason)
{
FunctionName = inst.Function,
StopCompletion = true
};
message.FunctionName = inst.Function;
message.StopCompletion = true;
return result;
return true;
}
}

View file

@ -24,15 +24,11 @@ public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst,
StopCompletion = true
};
return result;
message.Content = inst.Response;
message.StopCompletion = true;
message.Role = AgentRole.Assistant;
return true;
}
}

View file

@ -27,14 +27,12 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
// Retrieve information from specific agent
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgents(inst.AgentName).FirstOrDefault();
var response = await routing.InvokeAgent(record.Id);
inst.Response = response.Content;
var ret = await routing.InvokeAgent(record.Id, message);
/*_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question)
{
@ -45,6 +43,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
/*_dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer)
{
MessageId = inst.MessageId,
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments),
ExecutionResult = inst.Parameters.Answer,
@ -52,11 +51,11 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase, IRoutingH
CurrentAgentId = record.Id
});*/
_router.Instruction += $"\r\n{AgentRole.Function}: {response.Content}";
_router.Instruction += $"\r\n{AgentRole.Function}: {message.Content}";
// Got the response from agent, then send to reasoner again to make the decision
// inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?");
return null;
return true;
}
}

View file

@ -28,24 +28,15 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var context = _services.GetRequiredService<RoutingContext>();
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == inst.Function);
var message = new RoleDialogModel(AgentRole.Function, inst.Question)
{
FunctionName = inst.Function,
FunctionArgs = JsonSerializer.Serialize(inst),
CurrentAgentId = context.GetCurrentAgentId(),
};
message.FunctionArgs = JsonSerializer.Serialize(inst);
var ret = await function.Execute(message);
var result = await routing.InvokeAgent(context.GetCurrentAgentId());
// Keep last message data for debug
result.Data = result.Data ?? message.Data;
result.FunctionName = result.FunctionName ?? message.FunctionName;
return result;
ret = await routing.InvokeAgent(context.GetCurrentAgentId(), message);
return true;
}
}

View file

@ -23,24 +23,17 @@ public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
}
public async Task<RoleDialogModel> Handle(IRoutingService routing, FunctionCallFromLlm inst)
public async Task<bool> Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message)
{
var result = new RoleDialogModel(AgentRole.Assistant, inst.Response)
{
CurrentAgentId = _settings.RouterId,
FunctionName = inst.Function,
Data = inst
};
var hooks = _services.GetServices<IConversationHook>()
.OrderBy(x => x.Priority)
.ToList();
foreach (var hook in hooks)
{
await hook.OnCurrentTaskEnding(result);
await hook.OnCurrentTaskEnding(message);
}
return result;
return true;
}
}

View file

@ -56,15 +56,25 @@ public partial class RoutingService
model: _settings.Model);
int retryCount = 0;
var agentService = _services.GetRequiredService<IAgentService>();
var dialogs = Dialogs;
while (retryCount < 3)
{
try
{
var conversation = "";
foreach (var dialog in _dialogs.TakeLast(20))
foreach (var dialog in dialogs.TakeLast(50))
{
conversation += $"{dialog.Role}: {dialog.Content}\r\n";
var role = dialog.Role;
if (role != AgentRole.User)
{
var agent = await agentService.GetAgent(dialog.CurrentAgentId);
role = agent.Name;
}
conversation += $"{role}: {dialog.Content}\r\n";
}
content = $"{conversation}\r\n###\r\n{content}";
@ -73,9 +83,7 @@ public partial class RoutingService
new RoleDialogModel(AgentRole.User, content)
});
var pattern = @"\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!))\}";
response.Content = Regex.Match(response.Content, pattern).Value;
args = JsonSerializer.Deserialize<FunctionCallFromLlm>(response.Content);
args = response.Content.JsonContent<FunctionCallFromLlm>();
break;
}
catch (Exception ex)
@ -113,9 +121,6 @@ public partial class RoutingService
return args;
}
#if !DEBUG
[MemoryCache(10 * 60)]
#endif
private string GetNextStepPrompt()
{
var template = _routerInstance.Router.Templates.First(x => x.Name == "next_step_prompt").Content;

View file

@ -8,13 +8,13 @@ public partial class RoutingService
{
const int MAXIMUM_RECURSION_DEPTH = 3;
private int _currentRecursionDepth = 0;
public async Task<RoleDialogModel> InvokeAgent(string agentId)
public async Task<bool> InvokeAgent(string agentId, RoleDialogModel message)
{
_currentRecursionDepth++;
if (_currentRecursionDepth > MAXIMUM_RECURSION_DEPTH)
{
_logger.LogWarning($"Current recursive call depth greater than {MAXIMUM_RECURSION_DEPTH}, which will cause unexpected result.");
return Dialogs.Last();
return false;
}
var agentService = _services.GetRequiredService<IAgentService>();
@ -23,50 +23,56 @@ public partial class RoutingService
var settings = _services.GetRequiredService<ChatCompletionSetting>();
var chatCompletion = CompletionProvider.GetChatCompletion(_services, provider: settings.Provider, model: settings.Model);
RoleDialogModel response = chatCompletion.GetChatCompletions(agent, Dialogs);
message.Role = response.Role;
if (response.Role == AgentRole.Function)
{
return await InvokeFunction(agent, response);
message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs;
await InvokeFunction(agent, message);
}
else
{
return response;
message.Content = response.Content;
}
return true;
}
private async Task<RoleDialogModel> InvokeFunction(Agent agent, RoleDialogModel response)
private async Task<RoleDialogModel> InvokeFunction(Agent agent, RoleDialogModel message)
{
// execute function
// Save states
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(response.FunctionArgs));
SaveStateByArgs(JsonSerializer.Deserialize<JsonDocument>(message.FunctionArgs));
var conversationService = _services.GetRequiredService<IConversationService>();
// Call functions
await conversationService.CallFunctions(response);
await conversationService.CallFunctions(message);
Dialogs.Add(response);
Dialogs.Add(message);
// Pass execution result to LLM to get response
if (!response.StopCompletion)
if (!message.StopCompletion)
{
// Find response template
var templateService = _services.GetRequiredService<IResponseTemplateService>();
var responseTemplate = await templateService.RenderFunctionResponse(agent.Id, response);
var responseTemplate = await templateService.RenderFunctionResponse(agent.Id, message);
if (!string.IsNullOrEmpty(responseTemplate))
{
response.Role = AgentRole.Assistant;
response.Content = responseTemplate.Trim();
message.Role = AgentRole.Assistant;
message.Content = responseTemplate.Trim();
}
else
{
response = await InvokeAgent(response.CurrentAgentId);
await InvokeAgent(message.CurrentAgentId, message);
}
}
else
{
response.Role = AgentRole.Assistant;
message.Role = AgentRole.Assistant;
}
return response;
return message;
}
}

View file

@ -46,38 +46,29 @@ public partial class RoutingService : IRoutingService
_routerInstance = routerInstance;
}
public async Task<RoleDialogModel> ExecuteOnce(Agent agent)
public async Task<bool> ExecuteOnce(Agent agent, RoleDialogModel message)
{
var message = Dialogs.Last().Content;
var handlers = _services.GetServices<IRoutingHandler>();
var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent");
handler.SetDialogs(Dialogs);
var result = await handler.Handle(this, new FunctionCallFromLlm
{
Function = "route_to_agent",
Question = message,
Reason = message,
Question = message.Content,
Reason = message.Content,
AgentName = agent.Name
});
}, message);
return result;
}
public async Task<RoleDialogModel> InstructLoop()
public async Task<bool> InstructLoop(RoleDialogModel message)
{
_routerInstance.Load();
var router = _routerInstance.Router;
var result = new RoleDialogModel(AgentRole.Assistant, "Can you repeat your request again?")
{
CurrentAgentId = router.Id
};
var message = Dialogs.Last().Content;
var handlers = _services.GetServices<IRoutingHandler>();
int loopCount = 0;
@ -87,7 +78,8 @@ public partial class RoutingService : IRoutingService
loopCount++;
var inst = await GetNextInstruction();
inst.Question = inst.Question ?? message;
message.Instruction = inst;
inst.Question = message.Content;
var handler = handlers.FirstOrDefault(x => x.Name == inst.Function);
if (handler == null)
@ -98,12 +90,18 @@ public partial class RoutingService : IRoutingService
handler.SetRouter(router);
handler.SetDialogs(Dialogs);
result = await handler.Handle(this, inst);
message.FunctionName = inst.Function;
message.Role = AgentRole.Function;
message.FunctionArgs = inst.Arguments == null ? "{}" : JsonSerializer.Serialize(inst.Arguments);
await handler.Handle(this, inst, message);
inst.Response = message.Content;
stop = !_settings.EnableReasoning;
}
return result;
return true;
}
protected void SaveStateByArgs(JsonDocument args)

View file

@ -35,6 +35,10 @@ public class ResponseTemplateService : IResponseTemplateService
// Convert args and execute data to dictionary
var dict = new Dictionary<string, object>();
// Populate states
var state = _services.GetRequiredService<IConversationStateService>();
state.GetStates().Select(x => dict[x.Key] = x.Value).ToList();
if (message.FunctionArgs != null)
{
ExtractArgs(JsonSerializer.Deserialize<JsonDocument>(message.FunctionArgs), dict);

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.ApiAdapters;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Models;
using BotSharp.OpenAPI.ViewModels.Conversations;
namespace BotSharp.OpenAPI.Controllers;
@ -19,15 +20,17 @@ public class ConversationController : ControllerBase, IApiAdapter
}
[HttpPost("/conversation/{agentId}")]
public async Task<ConversationViewModel> NewConversation([FromRoute] string agentId)
public async Task<ConversationViewModel> NewConversation([FromRoute] string agentId, [FromBody] MessageConfig config)
{
var service = _services.GetRequiredService<IConversationService>();
var sess = new Conversation
var conv = new Conversation
{
AgentId = agentId
};
sess = await service.NewConversation(sess);
return ConversationViewModel.FromSession(sess);
conv = await service.NewConversation(conv);
config.States.ForEach(x => conv.States[x.Split('=')[0]] = x.Split('=')[1]);
return ConversationViewModel.FromSession(conv);
}
[HttpDelete("/conversation/{agentId}/{conversationId}")]
@ -50,13 +53,11 @@ public class ConversationController : ControllerBase, IApiAdapter
.SetState("sampling_factor", input.SamplingFactor);
var response = new MessageResponseModel();
var stackMsg = new List<RoleDialogModel>();
await conv.SendMessage(agentId,
new RoleDialogModel("user", input.Text),
var inputMsg = new RoleDialogModel("user", input.Text);
await conv.SendMessage(agentId, inputMsg,
async msg =>
{
stackMsg.Add(msg);
},
async fnExecuting =>
{
@ -64,15 +65,14 @@ public class ConversationController : ControllerBase, IApiAdapter
},
async fnExecuted =>
{
response.Function = fnExecuted.FunctionName;
response.Data = fnExecuted.Data;
response.RichContent = fnExecuted.RichContent;
});
response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content));
response.Data = response.Data ?? stackMsg.Last().Data;
response.Function = stackMsg.Last().FunctionName;
response.RichContent = response.RichContent ?? stackMsg.Last().RichContent;
response.MessageId = inputMsg.MessageId;
response.Text = inputMsg.Content;
response.Data = inputMsg.Data;
response.Function = inputMsg.FunctionName;
response.Instruction = inputMsg.Instruction;
return response;
}

View file

@ -1,9 +1,14 @@
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.OpenAPI.ViewModels.Conversations;
public class MessageResponseModel
public class MessageResponseModel : ITrackableMessage
{
public string MessageId { get; set; }
public string Text { get; set; }
public string Function { get; set; }
public object Data { get; set; }
public object? RichContent { get; set; }
public FunctionCallFromLlm Instruction { get; set; }
}

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Plugin.MongoStorage.Models;
[BsonIgnoreExtraElements]
public class AgentResponseMongoElement
{
public string Prefix { get; set; }

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models;
namespace BotSharp.Plugin.MongoStorage.Models;
[BsonIgnoreExtraElements]
public class AgentTemplateMongoElement
{
public string Name { get; set; }

View file

@ -3,10 +3,12 @@ using System.Text.Json;
namespace BotSharp.Plugin.MongoStorage.Models;
[BsonIgnoreExtraElements]
public class FunctionDefMongoElement
{
public string Name { get; set; }
public string Description { get; set; }
public string? Impact { get; set; }
public FunctionParametersDefMongoElement Parameters { get; set; } = new FunctionParametersDefMongoElement();
public FunctionDefMongoElement()
@ -20,6 +22,7 @@ public class FunctionDefMongoElement
{
Name = function.Name,
Description = function.Description,
Impact = function.Impact,
Parameters = new FunctionParametersDefMongoElement
{
Type = function.Parameters.Type,
@ -35,6 +38,7 @@ public class FunctionDefMongoElement
{
Name = mongoFunction.Name,
Description = mongoFunction.Description,
Impact = mongoFunction.Impact,
Parameters = new FunctionParametersDef
{
Type = mongoFunction.Parameters.Type,

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Routing.Models;
namespace BotSharp.Plugin.MongoStorage.Models;
[BsonIgnoreExtraElements]
public class RoutingRuleMongoElement
{
public string Field { get; set; }

View file

@ -10,4 +10,5 @@ global using BotSharp.Abstraction.Plugins;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using MongoDB.Bson;
global using MongoDB.Driver;
global using MongoDB.Driver;
global using MongoDB.Bson.Serialization.Attributes;

View file

@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>4fb8c9df-7975-4926-ba73-46c8ca440691</UserSecretsId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>

View file

@ -2,8 +2,8 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us
1. Read the [CONVERSATION] content.
2. Select a appropriate function from [FUNCTIONS].
3. Determine which agent is suitable to handle this conversation.
4. Re-think about the selected function or agent is the best choice.
5. For agent required arguments, leave it blank if user doesn't provide it.
4. Re-think on whether the function you chose matches the reason.
5. For agent required arguments, leave it as blank object if user doesn't provide it.
[FUNCTIONS]
{% for handler in routing_handlers %}

View file

@ -1 +1,4 @@
What is the next step based on the CONVERSATION? Response must be in appropriate JSON format.
What is the next step based on the CONVERSATION?
Response must be in appropriate JSON format.
Route to the Agent that last handled the conversation if necessary.
If user wants to speak to customer service, use function human_intervention_needed.

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Conversations.Models;
using System.Text.Json;
namespace BotSharp.Plugin.PizzaBot.Functions;
@ -14,6 +15,7 @@ public class GetPizzaPricesFn : IFunctionCallback
cheese_unit_price = 3.5,
margherita_unit_price = 3.8,
};
message.Content = JsonSerializer.Serialize(message.Data);
return true;
}
}