Merge branch 'SciSharp:master' into master

This commit is contained in:
C. Oceania 2024-04-25 06:46:16 -05:00 committed by GitHub
commit 46704e9fa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 504 additions and 93 deletions

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Infrastructures.Enums;
public class LanguageType
{
public const string UNKNOWN = "Unknown";
public const string ENGLISH = "English";
public const string SPANISH = "Spanish";
public const string CHINESE = "Chinese";
}

View file

@ -13,6 +13,7 @@ public class ElementButton
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Url { get; set; }
[Translate]
public string Title { get; set; } = string.Empty;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@ -28,5 +29,7 @@ public class ElementButton
[JsonPropertyName("post_action_disclaimer")]
[JsonProperty("post_action_disclaimer")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[Translate]
public string? PostActionDisclaimer { get; set; }
}

View file

@ -11,6 +11,7 @@ public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
[JsonPropertyName("text")]
[JsonProperty("text")]
[Translate]
public string Text { get; set; } = string.Empty;
[JsonPropertyName("template_type")]
@ -36,6 +37,7 @@ public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
public class GenericElement
{
[Translate]
public string Title { get; set; }
public string Subtitle { get; set; }

View file

@ -44,5 +44,5 @@ public interface IRoutingService
Task<string> GetConversationContent(List<RoleDialogModel> dialogs, int maxDialogCount = 50);
bool HasMissingRequiredField(RoleDialogModel message, out string agentId);
(bool, string) HasMissingRequiredField(RoleDialogModel message, out string agentId);
}

View file

@ -50,6 +50,12 @@ public class RoutingArgs
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string UserGoal { get; set; }
[JsonPropertyName("language")]
public string Language { get; set; } = LanguageType.ENGLISH;
[JsonPropertyName("lastest_message_translated_to_english")]
public string UserMessageInEnglish { get; set; }
public override string ToString()
{
var route = string.IsNullOrEmpty(AgentName) ? "" : $"<Route to {AgentName.ToUpper()} because {NextActionReason}>";

View file

@ -0,0 +1,10 @@
namespace BotSharp.Abstraction.Translation.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false)]
public class TranslateAttribute : Attribute
{
public TranslateAttribute()
{
}
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Translation;
public interface ITranslationService
{
Task<T> Translate<T>(Agent router, string messageId, T data, string language = "Spanish", bool clone = true) where T : class;
}

View file

@ -14,3 +14,4 @@ global using BotSharp.Abstraction.Models;
global using BotSharp.Abstraction.Routing.Models;
global using BotSharp.Abstraction.Routing.Planning;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Abstraction.Translation.Attributes;

View file

@ -62,6 +62,7 @@
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.plan.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\planner_prompt.two_stage.2nd.task.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid" />
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instruction.liquid" />
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.executor.liquid" />
@ -118,6 +119,9 @@
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\.welcome.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View file

@ -8,6 +8,7 @@ using BotSharp.Core.Instructs;
using BotSharp.Core.Messaging;
using BotSharp.Core.Routing.Planning;
using BotSharp.Core.Templating;
using BotSharp.Core.Translation;
using Microsoft.Extensions.Configuration;
namespace BotSharp.Core.Conversations;
@ -35,6 +36,7 @@ public class ConversationPlugin : IBotSharpPlugin
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IConversationStateService, ConversationStateService>();
services.AddScoped<IConversationAttachmentService, ConversationAttachmentService>();
services.AddScoped<ITranslationService, TranslationService>();
// Rich content messaging
services.AddScoped<IRichContentService, RichContentService>();

View file

@ -71,15 +71,6 @@ public partial class ConversationService
}
}
// Persist to storage
if (!message.StopCompletion)
{
_storage.Append(_conversationId, message);
// Add to thread
dialogs.Add(RoleDialogModel.From(message));
}
if (!stopCompletion)
{
// Routing with reasoning
@ -139,7 +130,7 @@ public partial class ConversationService
response.RichContent is RichContent<IRichMessage> template &&
string.IsNullOrEmpty(template.Message.Text))
{
template.Message.Text = response.Content;
template.Message.Text = response.SecondaryContent ?? response.Content;
}
// Only read content from RichContent for UI rendering. When richContent is null, create a basic text message for richContent.
@ -147,7 +138,7 @@ public partial class ConversationService
response.RichContent = response.RichContent ?? new RichContent<IRichMessage>
{
Recipient = new Recipient { Id = state.GetConversationId() },
Message = new TextMessage(response.Content)
Message = new TextMessage(response.SecondaryContent ?? response.Content)
};
// Patch return function name

View file

@ -83,11 +83,11 @@ public partial class RouteToAgentFn : IFunctionCallback
}
var routing = _services.GetRequiredService<IRoutingService>();
var missingfield = routing.HasMissingRequiredField(message, out var agentId);
var (missingfield, reason) = routing.HasMissingRequiredField(message, out var agentId);
if (missingfield && message.CurrentAgentId != agentId)
{
// Stack redirection agent
_context.Push(agentId, reason: $"REDIRECTION {message.Content}");
_context.Push(agentId, reason: $"REDIRECTION {reason}");
}
}

View file

@ -10,10 +10,21 @@ public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("reason", "why response to user directly without go to other agents"),
new ParameterPropertyDef("response", "response content to user in courteous words. If the user wants to end the conversation, you must set conversation_end to true and response politely."),
new ParameterPropertyDef("conversation_end", "whether to end this conversation", type: "boolean"),
new ParameterPropertyDef("task_completed ", "whether the user's task request has been completed.", type: "boolean")
new ParameterPropertyDef("reason",
"why response to user directly without go to other agents."),
new ParameterPropertyDef("response",
"response content to user in courteous words with language English. If the user wants to end the conversation, you must set conversation_end to true and response politely."),
new ParameterPropertyDef("conversation_end",
"whether to end this conversation.",
type: "boolean"),
new ParameterPropertyDef("task_completed ",
"whether the user's task request has been completed.",
type: "boolean"),
new ParameterPropertyDef("language",
"User preferred language, considering the whole conversation. Language could be English, Spanish or Chinese.",
required: true),
new ParameterPropertyDef("lastest_message_translated_to_english",
"Translate user lastest message in [CONVERSATION] to English"),
};
public ResponseToUserRoutingHandler(IServiceProvider services, ILogger<ResponseToUserRoutingHandler> logger, RoutingSettings settings)

View file

@ -12,23 +12,28 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("next_action_reason",
"the reason why route to this virtual agent",
"the reason why route to this virtual agent.",
required: true),
new ParameterPropertyDef("next_action_agent",
"agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent",
"agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent.",
required: true),
new ParameterPropertyDef("args",
"useful parameters of next action agent, format: { }",
type: "object"),
new ParameterPropertyDef("user_goal_description",
"user goal based on user initial task.",
required: true),
new ParameterPropertyDef("user_goal_agent",
"agent who can acheive user initial task, must align with user_goal_description.",
required: true),
new ParameterPropertyDef("args",
"useful parameters of next action agent, format: { }",
type: "object"),
new ParameterPropertyDef("is_new_task",
"whether the user is requesting a new task that is different from the previous topic.",
type: "boolean")
type: "boolean"),
new ParameterPropertyDef("language",
"User preferred language, considering the whole conversation. Language could be English, Spanish or Chinese.",
required: true),
new ParameterPropertyDef("lastest_message_translated_to_english",
"Translate lastest user message in [CONVERSATION] to English"),
};
public RouteToAgentRoutingHandler(IServiceProvider services, ILogger<RouteToAgentRoutingHandler> logger, RoutingSettings settings)

View file

@ -128,7 +128,7 @@ public class RoutingContext : IRoutingContext
};
var routing = _services.GetRequiredService<IRoutingService>();
var missingfield = routing.HasMissingRequiredField(message, out agentId);
var (missingfield, _) = routing.HasMissingRequiredField(message, out agentId);
if (missingfield)
{
if (currentAgentId != agentId)

View file

@ -10,8 +10,9 @@ public partial class RoutingService
/// If the target agent needs some required fields but the
/// </summary>
/// <returns></returns>
public bool HasMissingRequiredField(RoleDialogModel message, out string agentId)
public (bool, string) HasMissingRequiredField(RoleDialogModel message, out string agentId)
{
var reason = string.Empty;
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var routing = _services.GetRequiredService<IRoutingService>();
@ -20,7 +21,7 @@ public partial class RoutingService
if (routingRules == null || !routingRules.Any())
{
agentId = message.CurrentAgentId;
return false;
return (false, reason);
}
agentId = routingRules.First().AgentId;
@ -68,9 +69,13 @@ public partial class RoutingService
if (missingFields.Any())
{
var logger = _services.GetRequiredService<ILogger<RouteToAgentFn>>();
// Add field to args
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "missing_fields", missingFields);
message.Content = $"missing some information: {string.Join(", ", missingFields)}";
reason = $"missing some information: {string.Join(", ", missingFields)}";
// message.Content = reason;
logger.LogWarning(reason);
// Handle redirect
var routingRule = routingRules.FirstOrDefault(x => missingFields.Contains(x.Field));
@ -82,7 +87,6 @@ public partial class RoutingService
// Add redirected agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name);
agentId = routingRule.RedirectTo;
var logger = _services.GetRequiredService<ILogger<RouteToAgentFn>>();
#if DEBUG
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow);
#else
@ -96,7 +100,7 @@ public partial class RoutingService
}
}
return missingFields.Any();
return (missingFields.Any(), reason);
}
private string AppendPropertyToArgs(string args, string key, string value)

View file

@ -4,9 +4,6 @@ namespace BotSharp.Core.Routing;
public partial class RoutingService
{
private List<FunctionCallingResponse> _functionCallStack = new List<FunctionCallingResponse>();
public List<FunctionCallingResponse> FunctionCallStack => _functionCallStack;
public async Task<bool> InvokeFunction(string name, RoleDialogModel message)
{
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == name);
@ -38,14 +35,6 @@ public partial class RoutingService
{
result = await function.Execute(clonedMessage);
_functionCallStack.Add(new FunctionCallingResponse
{
Role = AgentRole.Function,
FunctionName = clonedMessage.FunctionName,
Args = JsonDocument.Parse(clonedMessage.FunctionArgs ?? "{}"),
Content = clonedMessage.Content
});
// After functions have been executed
foreach (var hook in hooks)
{

View file

@ -1,7 +1,19 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Infrastructures.Enums;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Planning;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Abstraction.Templating;
using BotSharp.Core.Routing.Planning;
using Fluid.Ast;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Diagnostics.Metrics;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using ThirdParty.Json.LitJson;
using static System.Net.Mime.MediaTypeNames;
namespace BotSharp.Core.Routing;
@ -39,15 +51,10 @@ public partial class RoutingService : IRoutingService
var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent");
var conv = _services.GetRequiredService<IConversationService>();
var dialogs = new List<RoleDialogModel>();
if (conv.States.GetState("hide_context", "false") == "true")
{
dialogs.Add(message);
}
else
{
dialogs = conv.GetDialogHistory();
}
var storage = _services.GetRequiredService<IConversationStorage>();
storage.Append(conv.ConversationId, message);
var dialogs = conv.GetDialogHistory();
handler.SetDialogs(dialogs);
var inst = new FunctionCallFromLlm
@ -71,11 +78,14 @@ public partial class RoutingService : IRoutingService
public async Task<RoleDialogModel> InstructLoop(RoleDialogModel message, List<RoleDialogModel> dialogs)
{
var agentService = _services.GetRequiredService<IAgentService>();
_router = await agentService.LoadAgent(message.CurrentAgentId);
RoleDialogModel response = default;
var agentService = _services.GetRequiredService<IAgentService>();
var convService = _services.GetRequiredService<IConversationService>();
var storage = _services.GetRequiredService<IConversationStorage>();
_router = await agentService.LoadAgent(message.CurrentAgentId);
var states = _services.GetRequiredService<IConversationStateService>();
var executor = _services.GetRequiredService<IExecutor>();
@ -83,17 +93,23 @@ public partial class RoutingService : IRoutingService
_context.Push(_router.Id);
int loopCount = 0;
while (loopCount < planner.MaxLoopCount && !_context.IsEmpty)
dialogs.Add(message);
// Get first instruction
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
// Handle multi-language for input
if (inst.Language != LanguageType.UNKNOWN && inst.Language != LanguageType.ENGLISH)
{
loopCount++;
var conversation = await GetConversationContent(dialogs);
_router.TemplateDict["conversation"] = conversation;
// Get instruction from Planner
var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
message.Content = inst.UserMessageInEnglish;
}
storage.Append(convService.ConversationId, message);
int loopCount = 1;
while (true)
{
await HookEmitter.Emit<IRoutingHook>(_services, async hook =>
await hook.OnRoutingInstructionReceived(inst, message)
);
@ -121,6 +137,36 @@ public partial class RoutingService : IRoutingService
}
await planner.AgentExecuted(_router, inst, response, dialogs);
if (loopCount >= planner.MaxLoopCount || _context.IsEmpty)
{
break;
}
// Get next instruction from Planner
_router.TemplateDict["conversation"] = await GetConversationContent(dialogs);
inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs);
loopCount++;
}
// Handle multi-language for output
if (inst.Language != LanguageType.UNKNOWN && inst.Language != LanguageType.ENGLISH)
{
var translator = _services.GetRequiredService<ITranslationService>();
if (response.RichContent != null)
{
response.RichContent.Message = await translator.Translate(_router,
message.MessageId,
response.RichContent.Message,
language: inst.Language);
}
else
{
response.SecondaryContent = await translator.Translate(_router,
message.MessageId,
response.Content,
language: inst.Language);
}
}
return response;

View file

@ -0,0 +1,308 @@
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Templating;
using BotSharp.Abstraction.Translation.Attributes;
using Newtonsoft.Json;
using System.Collections;
using System.Reflection;
namespace BotSharp.Core.Translation;
public class TranslationService : ITranslationService
{
private readonly IServiceProvider _services;
private readonly ILogger<TranslationService> _logger;
private readonly BotSharpOptions _options;
private Agent _router;
private string _messageId;
public TranslationService(IServiceProvider services,
ILogger<TranslationService> logger,
BotSharpOptions options)
{
_services = services;
_logger = logger;
_options = options;
}
public async Task<T> Translate<T>(Agent router, string messageId, T data, string language = "Spanish", bool clone = true) where T : class
{
_router = router;
_messageId = messageId;
var unique = new HashSet<string>();
Collect(data, ref unique);
if (unique.Count == 0)
{
return data;
}
var cloned = data;
if (clone)
{
cloned = Clone(data);
}
var map = await InnerTranslate(unique, language);
cloned = Assign(cloned, map);
return cloned;
}
private T Clone<T>(T data) where T : class
{
if (data == null) return data;
var str = System.Text.Json.JsonSerializer.Serialize(data, _options.JsonSerializerOptions);
var cloned = System.Text.Json.JsonSerializer.Deserialize<T>(str, _options.JsonSerializerOptions);
return cloned;
}
/// <summary>
/// Collect unique strings in data
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="res"></param>
private void Collect<T>(T data, ref HashSet<string> res) where T : class
{
if (data == null) return;
var dataType = data.GetType();
if (dataType == typeof(string))
{
res.Add(data.ToString());
return;
}
var interfaces = dataType.GetTypeInfo().ImplementedInterfaces;
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
return;
}
var isList = interfaces.Any(x => x.Name == typeof(IEnumerable).Name);
if (dataType.IsArray || isList)
{
var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
foreach (var item in (data as IEnumerable<string>))
{
if (item == null) continue;
res.Add(item);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
foreach (var item in (data as IEnumerable<object>))
{
if (item == null) continue;
Collect(item, ref res);
}
}
return;
}
var props = dataType.GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(data, null);
var propType = prop.PropertyType;
var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute));
if (value == null) continue;
if (propType == typeof(string))
{
if (translate != null)
{
Collect(value, ref res);
}
}
else if (propType.IsClass || propType.IsInterface)
{
interfaces = propType.GetTypeInfo().ImplementedInterfaces;
isList = interfaces.Any(x => x.Name == typeof(IEnumerable).Name);
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
Collect(value, ref res);
}
else if (propType.IsArray || isList)
{
var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
if (translate != null)
{
Collect(value, ref res);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
Collect(value, ref res);
}
}
else
{
Collect(value, ref res);
}
}
}
}
/// <summary>
/// Assign translated values to corresponding attributes
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="map"></param>
/// <returns></returns>
private T Assign<T>(T data, Dictionary<string, string> map) where T : class
{
if (data == null) return data;
var dataType = data.GetType();
if (dataType == typeof(string) && map.TryGetValue(data.ToString(), out var target))
{
return target as T;
}
var interfaces = dataType.GetTypeInfo().ImplementedInterfaces;
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
return data;
}
var isList = interfaces.Any(x => x.Name == typeof(IEnumerable).Name);
if (dataType.IsArray || isList)
{
var elementType = dataType.IsArray ? dataType.GetElementType() : dataType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
var list = new List<string>();
foreach (var item in (data as IEnumerable<string>))
{
if (map.TryGetValue(item, out target))
{
list.Add(target);
}
else
{
list.Add(item?.ToString());
}
}
data = dataType.IsArray ? list.ToArray() as T : list as T;
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
foreach (var item in (data as IEnumerable<object>))
{
if (item == null) continue;
Assign(item, map);
}
}
return data;
}
var props = dataType.GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(data, null);
var propType = prop.PropertyType;
var translate = prop.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(TranslateAttribute));
if (value == null) continue;
if (propType == typeof(string))
{
if (translate != null)
{
prop.SetValue(data, Assign(value, map));
}
}
else if (propType.IsClass || propType.IsInterface)
{
interfaces = propType.GetTypeInfo().ImplementedInterfaces;
isList = interfaces.Any(x => x.Name == typeof(IEnumerable).Name);
if (interfaces.Any(x => x.Name == typeof(IDictionary<,>).Name))
{
Assign(value, map);
}
else if (propType.IsArray || isList)
{
var elementType = propType.IsArray ? propType.GetElementType() : propType.GetGenericArguments().FirstOrDefault();
if (elementType == typeof(string))
{
if (translate != null)
{
var targetValue = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(Assign(value, map)), propType);
prop.SetValue(data, targetValue);
}
}
else if (elementType != null && (elementType.IsClass || elementType.IsInterface))
{
prop.SetValue(data, Assign(value, map));
}
}
else
{
Assign(value, map);
}
}
}
return data;
}
/// <summary>
/// Translate
/// </summary>
/// <param name="list"></param>
/// <param name="language"></param>
/// <returns></returns>
private async Task<Dictionary<string, string>> InnerTranslate(HashSet<string> list, string language)
{
// chat completion
var completion = CompletionProvider.GetChatCompletion(_services,
provider: _router?.LlmConfig?.Provider,
model: _router?.LlmConfig?.Model);
var texts = list.ToArray();
var translator = new Agent
{
Id = Guid.Empty.ToString(),
Name = "Translator",
TemplateDict = new Dictionary<string, object>
{
{ "text_list", JsonConvert.SerializeObject(texts) },
{ "language", language }
}
};
var template = _router.Templates.First(x => x.Name == "translation_prompt").Content;
var render = _services.GetRequiredService<ITemplateRender>();
var prompt = render.Render(template, translator.TemplateDict);
var translationDialogs = new List<RoleDialogModel>
{
new RoleDialogModel(AgentRole.User, prompt)
{
FunctionName = "translation_prompt",
MessageId = _messageId
}
};
var translationResponse = await completion.GetChatCompletions(translator, translationDialogs);
var translatedTexts = translationResponse.Content.JsonArrayContent<string>();
var map = new Dictionary<string, string>();
for (var i = 0; i < list.Count; i++)
{
map.Add(texts[i], translatedTexts[i]);
}
return map;
}
}

View file

@ -22,6 +22,7 @@ global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.Functions.Models;
global using BotSharp.Abstraction.Repositories;
global using BotSharp.Abstraction.Repositories.Filters;
global using BotSharp.Abstraction.Translation;
global using BotSharp.Core.Repository;
global using BotSharp.Core.Routing;
global using BotSharp.Core.Agents.Services;

View file

@ -1,11 +1,11 @@
{
"id": "01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b",
"name": "Human Support",
"description": "Reach out to human customer service representative.",
"description": "Reach out to human customer service.",
"type": "task",
"createdDateTime": "2024-04-22T10:00:00Z",
"updatedDateTime": "2024-04-22T10:00:00Z",
"disabled": false,
"isPublic": true,
"profiles": [ "human" ]
}
}

View file

@ -1,2 +1,2 @@
You are a human customer service connection program.
When other AI customer service cannot solve user problems, you know how to call the API to transfer users to human customer service for answers.
When other AI customer service agents cannot solve user problems, you know how to call the API to transfer users to human customer service for answers.

View file

@ -1,4 +1,6 @@
You're {{router.name}} ({{router.description}}). Follow these steps to handle user request:
You're {{router.name}} ({{router.description}}).
You can understand messages sent by users in different languages.
Follow these steps to handle user request:
1. Read the [CONVERSATION] content.
2. Select a appropriate function from [FUNCTIONS].
3. Determine which agent is suitable to handle this conversation.

View file

@ -10,3 +10,4 @@ Expected user goal agent is {{ expected_user_goal_agent }}.
{%- else -%}
User goal agent is inferred based on user initial request.
{%- endif %}
Detect language based on the overall content in [CONVERSATION] and only include user message.

View file

@ -0,0 +1,4 @@
{{ text_list }}
=====
Translate the sentences in the list into {{ language }}, only output the translated text in string list [""].

View file

@ -1,7 +1,7 @@
This is a model evaluation program, which interactive with model to complete a certain task based on the background information given to you.
This is a model evaluation program, which interacts with model to complete a certain task based on the background information given to you.
{{ task_prompt }}
user: Hi!
assistant: Hello, How can I help you?
user:
user:

View file

@ -83,8 +83,7 @@ public class ConversationController : ControllerBase
ConversationId = conversationId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
Text = message.Content,
SecondaryText = message.SecondaryContent,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Data = message.Data,
Sender = UserViewModel.FromUser(user)
});
@ -97,8 +96,7 @@ public class ConversationController : ControllerBase
ConversationId = conversationId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
Text = message.Content,
SecondaryText = message.SecondaryContent,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Function = message.FunctionName,
Data = message.Data,
Sender = new UserViewModel
@ -106,8 +104,7 @@ public class ConversationController : ControllerBase
FirstName = agent.Name,
Role = message.Role,
},
RichContent = message.RichContent,
SecondaryRichContent = message.SecondaryRichContent
RichContent = message.SecondaryRichContent ?? message.RichContent
});
}
}
@ -180,9 +177,9 @@ public class ConversationController : ControllerBase
replyMessage: input.Postback,
async msg =>
{
response.Text = msg.Content;
response.Text = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content;
response.Function = msg.FunctionName;
response.RichContent = msg.RichContent;
response.RichContent = msg.SecondaryRichContent ?? msg.RichContent;
response.Instruction = msg.Instruction;
response.Data = msg.Data;
},

View file

@ -21,9 +21,6 @@ public class ChatResponseModel : InstructResult
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public FunctionCallFromLlm? Instruction { get; set; }
[JsonPropertyName("secondary_text")]
public string? SecondaryText { get; set; }
/// <summary>
/// Rich message for UI rendering
/// </summary>
@ -31,10 +28,6 @@ public class ChatResponseModel : InstructResult
[JsonPropertyName("rich_content")]
public RichContent<IRichMessage>? RichContent { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("secondary_rich_content")]
public RichContent<IRichMessage>? SecondaryRichContent { get; set; }
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View file

@ -300,6 +300,7 @@ public class ChatCompletionProvider : IChatCompletion
}));
prompt += $"{verbose}\r\n";
prompt += "\r\n[CONVERSATION]\r\n";
verbose = string.Join("\r\n", chatCompletionsOptions.Messages
.Where(x => x.Role != AgentRole.System).Select(x =>
{
@ -311,7 +312,7 @@ public class ChatCompletionProvider : IChatCompletion
else if (x.Role == ChatRole.User)
{
var m = x as ChatRequestUserMessage;
return !string.IsNullOrEmpty(m.Name) ?
return !string.IsNullOrEmpty(m.Name) && m.Name != "route_to_agent" ?
$"{m.Name}: {m.Content}" :
$"{m.Role}: {m.Content}";
}

View file

@ -45,7 +45,7 @@ public class ChatHubConversationHook : ConversationHookBase
{
ConversationId = conv.ConversationId,
MessageId = message.MessageId,
Text = message.Content,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Sender = UserViewModel.FromUser(sender)
});
@ -71,9 +71,9 @@ public class ChatHubConversationHook : ConversationHookBase
{
ConversationId = conv.ConversationId,
MessageId = message.MessageId,
Text = message.Content,
Text = !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content,
Function = message.FunctionName,
RichContent = message.RichContent,
RichContent = message.SecondaryRichContent ?? message.RichContent,
Data = message.Data,
Sender = new UserViewModel()
{

View file

@ -7,6 +7,8 @@ using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Routing;
using Microsoft.AspNetCore.SignalR;
using System.Text.Encodings.Web;
using System.Text.Unicode;
namespace BotSharp.Plugin.ChatHub.Hooks;
@ -45,7 +47,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
public override async Task OnMessageReceived(RoleDialogModel message)
{
var conversationId = _state.GetConversationId();
var log = $"{message.Content}";
var log = $"{GetMessageContent(message)}";
var input = new ContentLogInputModel(conversationId, message)
{
@ -59,7 +61,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg)
{
var conversationId = _state.GetConversationId();
var log = $"{message.Content}";
var log = $"{GetMessageContent(message)}";
var replyContent = JsonSerializer.Serialize(replyMsg, _options.JsonSerializerOptions);
log += $"\r\n```json\r\n{replyContent}\r\n```";
@ -183,10 +185,18 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
if (message.Role == AgentRole.Assistant)
{
var agent = await _agentService.LoadAgent(message.CurrentAgentId);
var log = $"{message.Content}";
if (message.RichContent != null)
var log = $"{GetMessageContent(message)}";
if (message.RichContent != null || message.SecondaryRichContent != null)
{
var richContent = JsonSerializer.Serialize(message.RichContent, _options.JsonSerializerOptions);
var jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
AllowTrailingCommas = true,
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
var richContent = JsonSerializer.Serialize(message.SecondaryRichContent ?? message.RichContent, jsonOptions);
log += $"\r\n```json\r\n{richContent}\r\n```";
}
@ -204,7 +214,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
public override async Task OnTaskCompleted(RoleDialogModel message)
{
var conversationId = _state.GetConversationId();
var log = $"{message.Content}";
var log = $"{GetMessageContent(message)}";
var agent = await _agentService.LoadAgent(message.CurrentAgentId);
var input = new ContentLogInputModel(conversationId, message)
@ -479,4 +489,9 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
return JsonSerializer.Serialize(model, _options.JsonSerializerOptions);
}
private string GetMessageContent(RoleDialogModel message)
{
return !string.IsNullOrEmpty(message.SecondaryContent) ? message.SecondaryContent : message.Content;
}
}