Merge branch 'SciSharp:master' into master
This commit is contained in:
commit
805dbbbe08
|
|
@ -10,6 +10,13 @@ public interface IConversationHook
|
|||
Conversation Conversation { get; }
|
||||
IConversationHook SetConversation(Conversation conversation);
|
||||
|
||||
/// <summary>
|
||||
/// Get the predifined intent for the conversation.
|
||||
/// It will send to the conversation context to help LLM to understand the user's intent.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<string> GetConversationIntent() => Task.FromResult(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when user connects with agent first time.
|
||||
/// This hook is the good timing to show welcome infomation.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ public class EvaluationRequest : LlmBaseRequest
|
|||
[JsonPropertyName("states")]
|
||||
public IEnumerable<MessageState> States { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("chat")]
|
||||
public ChatEvaluationRequest Chat { get; set; } = new ChatEvaluationRequest();
|
||||
|
||||
[JsonPropertyName("metric")]
|
||||
public MetricEvaluationRequest Metric { get; set; } = new MetricEvaluationRequest();
|
||||
}
|
||||
|
||||
|
||||
public class ChatEvaluationRequest
|
||||
{
|
||||
[JsonPropertyName("duplicate_limit")]
|
||||
public int DuplicateLimit { get; set; } = 2;
|
||||
|
||||
|
|
@ -24,4 +34,26 @@ public class EvaluationRequest : LlmBaseRequest
|
|||
[JsonPropertyName("stop_criteria")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? StopCriteria { get; set; }
|
||||
|
||||
public ChatEvaluationRequest()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class MetricEvaluationRequest
|
||||
{
|
||||
[JsonPropertyName("additional_instruction")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? AdditionalInstruction { get; set; }
|
||||
|
||||
[JsonPropertyName("metrics")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IEnumerable<NameDesc>? Metrics { get; set; } = [];
|
||||
|
||||
public MetricEvaluationRequest()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,5 @@ public class EvaluationResult
|
|||
public string TaskInstruction { get; set; }
|
||||
public string SystemPrompt { get; set; }
|
||||
public string GeneratedConversationId { get; set; }
|
||||
public string? MetricResult { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BotSharp.Abstraction.Infrastructures;
|
||||
|
||||
public class HookEmitOption
|
||||
{
|
||||
public bool OnlyOnce { get; set; }
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ namespace BotSharp.Abstraction.Planning;
|
|||
|
||||
public interface IPlanningHook
|
||||
{
|
||||
Task<string> GetSummaryAdditionalRequirements(string planner)
|
||||
Task<string> GetSummaryAdditionalRequirements(string planner, RoleDialogModel message)
|
||||
=> Task.FromResult(string.Empty);
|
||||
|
||||
Task OnPlanningCompleted(string planner, RoleDialogModel msg)
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
namespace BotSharp.Abstraction.Repositories;
|
||||
|
||||
public interface IDatabaseHook
|
||||
{
|
||||
// Get database type
|
||||
string GetDatabaseType(RoleDialogModel message);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Repositories.Enums;
|
||||
using BotSharp.Abstraction.Users.Enums;
|
||||
using System.IO;
|
||||
|
||||
namespace BotSharp.Core.Agents.Services;
|
||||
|
|
@ -16,6 +17,12 @@ public partial class AgentService
|
|||
return refreshResult;
|
||||
}
|
||||
|
||||
var user = _db.GetUserById(_user.Id);
|
||||
if (!UserConstant.AdminRoles.Contains(user.Role))
|
||||
{
|
||||
return "Unauthorized user.";
|
||||
}
|
||||
|
||||
var agentDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
|
||||
dbSettings.FileRepository,
|
||||
_agentSettings.DataDir);
|
||||
|
|
@ -25,10 +32,8 @@ public partial class AgentService
|
|||
refreshResult = $"Cannot find the directory: {agentDir}";
|
||||
return refreshResult;
|
||||
}
|
||||
|
||||
var user = _db.GetUserById(_user.Id);
|
||||
|
||||
var refreshedAgents = new List<string>();
|
||||
|
||||
foreach (var dir in Directory.GetDirectories(agentDir))
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using BotSharp.Abstraction.Evaluations.Models;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
using BotSharp.Abstraction.Instructs.Models;
|
||||
using BotSharp.Abstraction.Models;
|
||||
|
||||
namespace BotSharp.Core.Evaluations.Services;
|
||||
|
||||
|
|
@ -31,15 +32,19 @@ public partial class EvaluatingService
|
|||
return result;
|
||||
}
|
||||
|
||||
var generatedConvId = await SimulateConversation(initMessage, refDialogContents, request);
|
||||
var initialStates = GetInitialStates(conversationId);
|
||||
var generatedConvId = await SimulateConversation(initMessage, refDialogContents, request, initialStates);
|
||||
var metricResult = await EvaluateMetrics(generatedConvId, refDialogContents, request);
|
||||
|
||||
return new EvaluationResult
|
||||
{
|
||||
GeneratedConversationId = generatedConvId
|
||||
GeneratedConversationId = generatedConvId,
|
||||
MetricResult = metricResult
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> SimulateConversation(string initMessage, IEnumerable<string> refDialogs, EvaluationRequest request)
|
||||
private async Task<string> SimulateConversation(string initMessage, IEnumerable<string> refDialogs,
|
||||
EvaluationRequest request, IEnumerable<MessageState>? states = null)
|
||||
{
|
||||
var count = 0;
|
||||
var duplicateCount = 0;
|
||||
|
|
@ -49,6 +54,7 @@ public partial class EvaluatingService
|
|||
var prevUserMsg = string.Empty;
|
||||
var curBotMsg = string.Empty;
|
||||
var prevBotMsg = string.Empty;
|
||||
var initialStates = states?.ToList() ?? [];
|
||||
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
|
@ -56,13 +62,14 @@ public partial class EvaluatingService
|
|||
|
||||
var query = "Please see yourself as a user and follow the instruction to generate a message.";
|
||||
var targetAgentId = request.AgentId;
|
||||
var evaluatorAgent = await agentService.GetAgent(BuiltInAgentId.Evaluator);
|
||||
var simulatorPrompt = evaluatorAgent.Templates.FirstOrDefault(x => x.Name == "instruction.simulator")?.Content ?? string.Empty;
|
||||
var evaluator = await agentService.GetAgent(BuiltInAgentId.Evaluator);
|
||||
var simulatorPrompt = evaluator.Templates.FirstOrDefault(x => x.Name == "instruction.simulator")?.Content ?? string.Empty;
|
||||
|
||||
while (true)
|
||||
{
|
||||
curDialogs.Add($"{AgentRole.User}: {curUserMsg}");
|
||||
var dialog = await SendMessage(targetAgentId, convId, curUserMsg);
|
||||
var dialog = await SendMessage(targetAgentId, convId, curUserMsg, states: initialStates);
|
||||
initialStates = [];
|
||||
|
||||
prevBotMsg = curBotMsg;
|
||||
curBotMsg = dialog?.RichContent?.Message?.Text ?? dialog?.Content ?? string.Empty;
|
||||
|
|
@ -80,30 +87,20 @@ public partial class EvaluatingService
|
|||
{
|
||||
{ "ref_conversation", refDialogs },
|
||||
{ "cur_conversation", curDialogs },
|
||||
{ "additional_instruction", request.AdditionalInstruction },
|
||||
{ "stop_criteria", request.StopCriteria }
|
||||
{ "additional_instruction", request.Chat.AdditionalInstruction },
|
||||
{ "stop_criteria", request.Chat.StopCriteria }
|
||||
}
|
||||
});
|
||||
|
||||
_logger.LogInformation($"Generated message: {result?.GeneratedMessage}, stop: {result?.Stop}, reason: {result?.Reason}");
|
||||
|
||||
if (count > request.MaxRounds || (result != null && result.Stop))
|
||||
if (count > request.Chat.MaxRounds || (result != null && result.Stop))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (curUserMsg.IsEqualTo(prevUserMsg) || curBotMsg.IsEqualTo(prevBotMsg))
|
||||
{
|
||||
duplicateCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
duplicateCount = 0;
|
||||
}
|
||||
|
||||
|
||||
if (duplicateCount >= request.DuplicateLimit)
|
||||
duplicateCount = curBotMsg.IsEqualTo(prevBotMsg) ? duplicateCount + 1 : 0;
|
||||
if (duplicateCount >= request.Chat.DuplicateLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
@ -115,6 +112,38 @@ public partial class EvaluatingService
|
|||
return convId;
|
||||
}
|
||||
|
||||
|
||||
private async Task<string?> EvaluateMetrics(string curConversationId, IEnumerable<string> refDialogs, EvaluationRequest request)
|
||||
{
|
||||
var storage = _services.GetRequiredService<IConversationStorage>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var instructService = _services.GetRequiredService<IInstructService>();
|
||||
|
||||
var curDialogs = storage.GetDialogs(curConversationId);
|
||||
var curDialogContents = GetConversationContent(curDialogs);
|
||||
|
||||
var evaluator = await agentService.GetAgent(BuiltInAgentId.Evaluator);
|
||||
var metricPrompt = evaluator.Templates.FirstOrDefault(x => x.Name == "instruction.metrics")?.Content ?? string.Empty;
|
||||
var query = "Please follow the instruction for evaluation.";
|
||||
|
||||
var result = await instructService.Instruct<JsonDocument>(metricPrompt, BuiltInAgentId.Evaluator,
|
||||
new InstructOptions
|
||||
{
|
||||
Provider = request.Provider,
|
||||
Model = request.Model,
|
||||
Message = query,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "ref_conversation", refDialogs },
|
||||
{ "cur_conversation", curDialogs },
|
||||
{ "additional_instruction", request.Metric.AdditionalInstruction },
|
||||
{ "metrics", request.Metric.Metrics }
|
||||
}
|
||||
});
|
||||
|
||||
return result != null ? result.RootElement.GetRawText() : null;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetConversationContent(IEnumerable<RoleDialogModel> dialogs)
|
||||
{
|
||||
var contents = new List<string>();
|
||||
|
|
@ -134,4 +163,30 @@ public partial class EvaluatingService
|
|||
|
||||
return contents;
|
||||
}
|
||||
|
||||
private IEnumerable<MessageState> GetInitialStates(string conversationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conversationId))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var states = db.GetConversationStates(conversationId);
|
||||
var initialStates = new List<MessageState>();
|
||||
|
||||
foreach (var state in states)
|
||||
{
|
||||
var value = state.Value?.Values?.FirstOrDefault(x => string.IsNullOrEmpty(x.MessageId));
|
||||
|
||||
if (string.IsNullOrEmpty(value?.Data))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
initialStates.Add(new MessageState(state.Key, value.Data, value.ActiveRounds));
|
||||
}
|
||||
|
||||
return initialStates;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ public partial class EvaluatingService : IEvaluatingService
|
|||
dialogs.Add(new RoleDialogModel(AgentRole.User, question));
|
||||
prompt += question.Trim();
|
||||
|
||||
response = await SendMessage(request.AgentId, conv.Id, question);
|
||||
response = await SendMessage(request.AgentId, conv.Id, question, states: new List<MessageState>
|
||||
{
|
||||
new MessageState("channel", ConversationChannel.OpenAPI)
|
||||
});
|
||||
dialogs.Add(new RoleDialogModel(AgentRole.Assistant, response.Content));
|
||||
prompt += $"\r\n{AgentRole.Assistant}: {response.Content.Trim()}";
|
||||
prompt += $"\r\n{AgentRole.User}: ";
|
||||
|
|
@ -86,17 +89,16 @@ public partial class EvaluatingService : IEvaluatingService
|
|||
return conv;
|
||||
}
|
||||
|
||||
private async Task<RoleDialogModel> SendMessage(string agentId, string conversationId, string text)
|
||||
private async Task<RoleDialogModel> SendMessage(string agentId, string conversationId, string text,
|
||||
PostbackMessageModel? postback = null,
|
||||
List<MessageState>? states = null)
|
||||
{
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
|
||||
var inputMsg = new RoleDialogModel(AgentRole.User, text);
|
||||
routing.Context.SetMessageId(conversationId, inputMsg.MessageId);
|
||||
conv.SetConversationId(conversationId, new List<MessageState>
|
||||
{
|
||||
new MessageState("channel", ConversationChannel.OpenAPI)
|
||||
});
|
||||
conv.SetConversationId(conversationId, states ?? []);
|
||||
|
||||
RoleDialogModel response = default;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ namespace BotSharp.Core.Infrastructures;
|
|||
|
||||
public static class HookEmitter
|
||||
{
|
||||
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action)
|
||||
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action, HookEmitOption? option = null)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<T>>();
|
||||
var result = new HookEmittedResult();
|
||||
var hooks = services.GetServices<T>();
|
||||
option = option ?? new();
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
|
|
@ -16,6 +17,11 @@ public static class HookEmitter
|
|||
{
|
||||
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
|
||||
action(hook);
|
||||
|
||||
if (option.OnlyOnce)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -26,11 +32,12 @@ public static class HookEmitter
|
|||
return result;
|
||||
}
|
||||
|
||||
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action)
|
||||
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action, HookEmitOption? option = null)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<T>>();
|
||||
var result = new HookEmittedResult();
|
||||
var hooks = services.GetServices<T>();
|
||||
option = option ?? new();
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
|
|
@ -38,6 +45,11 @@ public static class HookEmitter
|
|||
{
|
||||
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
|
||||
await action(hook);
|
||||
|
||||
if (option.OnlyOnce)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BotSharp.Core.Routing.Planning;
|
||||
|
||||
public class FirstStagePlan
|
||||
{
|
||||
[JsonPropertyName("task_detail")]
|
||||
public string Task { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("step")]
|
||||
public int Step { get; set; } = -1;
|
||||
|
||||
[JsonPropertyName("need_breakdown_task")]
|
||||
public bool ContainMultipleSteps { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("need_lookup_dictionary")]
|
||||
public bool NeedLookupDictionary { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("related_tables")]
|
||||
public string[] Tables { get; set; } = new string[0];
|
||||
|
||||
[JsonPropertyName("related_urls")]
|
||||
public string[] Urls { get; set; } = new string[0];
|
||||
|
||||
[JsonPropertyName("input_args")]
|
||||
public JsonDocument[] Parameters { get; set; } = new JsonDocument[0];
|
||||
|
||||
[JsonPropertyName("output_results")]
|
||||
public string[] Results { get; set; } = new string[0];
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"STEP {Step}: {Task}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,45 @@
|
|||
You are a conversation evaluator.
|
||||
You are a conversaton evaluator.
|
||||
Please take the content in the [REFERENCE CONVERSATION] section and [ONGOING CONVERSATION] section, and evaluate the metrics defined in [OUTPUT JSON FORMAT].
|
||||
|
||||
** You need to take a close look at the content in both [REFERENCE CONVERSATION] and [ONGOING CONVERSATION], and evaluate the metrics listed in [OUTPUT JSON FORMAT].
|
||||
|
||||
|
||||
=================
|
||||
[ADDITIONAL INSTRUCTION]
|
||||
{{ "\r\n" }}
|
||||
{%- if additional_instruction != empty -%}
|
||||
{{ additional_instruction }}
|
||||
{%- endif -%}
|
||||
{{ "\r\n" }}
|
||||
|
||||
|
||||
=================
|
||||
[OUTPUT JSON FORMAT]
|
||||
|
||||
** The output must be in JSON format:
|
||||
{
|
||||
{%- if metrics != empty -%}
|
||||
{{ "\r\n" }}
|
||||
{% for metric in metrics -%}
|
||||
{{ metric.name }}: {{ metric.description }},{{ "\r\n" }}
|
||||
{%- endfor %}
|
||||
{%- else -%}
|
||||
"summary": a short summary that summarizes the [ONGOING CONVERSATION] content compared to the [REFERENCE CONVERSATION]
|
||||
{%- endif -%}
|
||||
}
|
||||
|
||||
|
||||
=================
|
||||
[REFERENCE CONVERSATION]
|
||||
|
||||
{% for text in ref_conversation -%}
|
||||
{{ text }}{{ "\r\n" }}
|
||||
{%- endfor %}
|
||||
|
||||
|
||||
=================
|
||||
[ONGOING CONVERSATION]
|
||||
|
||||
{% for text in cur_conversation -%}
|
||||
{{ text }}{{ "\r\n" }}
|
||||
{%- endfor %}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using BotSharp.Abstraction.Plugins.Models;
|
||||
using BotSharp.Abstraction.Repositories.Enums;
|
||||
using BotSharp.Abstraction.Users.Enums;
|
||||
using BotSharp.Plugin.MongoStorage.Repository;
|
||||
|
||||
namespace BotSharp.Plugin.MongoStorage;
|
||||
|
|
@ -31,14 +29,4 @@ public class MongoStoragePlugin : IBotSharpPlugin
|
|||
services.AddScoped<IBotSharpRepository, MongoRepository>();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AttachMenu(List<PluginMenuDef> menu)
|
||||
{
|
||||
var section = menu.First(x => x.Label == "Apps");
|
||||
menu.Add(new PluginMenuDef("MongoDB", icon: "bx bx-data", link: "page/mongodb", weight: section.Weight + 10)
|
||||
{
|
||||
Roles = new List<string> { UserRole.Root, UserRole.Admin }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Plugin.Planner.TwoStaging;
|
||||
using BotSharp.Plugin.Planner.TwoStaging.Models;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BotSharp.Plugin.Planner.Functions;
|
||||
|
||||
|
|
@ -24,14 +26,13 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
{
|
||||
var fn = _services.GetRequiredService<IRoutingService>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
state.SetState("max_tokens", "4096");
|
||||
states.SetState("max_tokens", "4096");
|
||||
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
var taskRequirement = state.GetState("requirement_detail");
|
||||
var taskRequirement = states.GetState("requirement_detail");
|
||||
|
||||
// Get table names
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var steps = states.GetState("planning_result").JsonArrayContent<SecondStagePlan>();
|
||||
var allTables = new List<string>();
|
||||
var ddlStatements = string.Empty;
|
||||
|
|
@ -53,6 +54,7 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
});
|
||||
await fn.InvokeFunction("sql_table_definition", msgCopy);
|
||||
ddlStatements += "\r\n" + msgCopy.Content;
|
||||
states.SetState("table_ddls", ddlStatements);
|
||||
|
||||
// Summarize and generate query
|
||||
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult);
|
||||
|
|
@ -69,6 +71,9 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
var summary = await GetAiResponse(plannerAgent);
|
||||
message.Content = summary.Content;
|
||||
|
||||
// Validate the sql result
|
||||
await fn.InvokeFunction("validate_sql", message);
|
||||
|
||||
await HookEmitter.Emit<IPlanningHook>(_services, async hook =>
|
||||
await hook.OnPlanningCompleted(nameof(TwoStageTaskPlanner), message)
|
||||
);
|
||||
|
|
@ -88,7 +93,7 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
var additionalRequirements = new List<string>();
|
||||
await HookEmitter.Emit<IPlanningHook>(_services, async x =>
|
||||
{
|
||||
var requirement = await x.GetSummaryAdditionalRequirements(nameof(TwoStageTaskPlanner));
|
||||
var requirement = await x.GetSummaryAdditionalRequirements(nameof(TwoStageTaskPlanner), message);
|
||||
additionalRequirements.Add(requirement);
|
||||
});
|
||||
|
||||
|
|
@ -119,8 +124,8 @@ public class SummaryPlanFn : IFunctionCallback
|
|||
wholeDialogs.Last().Content += "\n\nIf the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id).\nFor example, you should use SET @id = select max(id) from table;";
|
||||
wholeDialogs.Last().Content += "\n\nTry if you can generate a single query to fulfill the needs.";
|
||||
|
||||
var completion = CompletionProvider.GetChatCompletion(_services,
|
||||
provider: plannerAgent.LlmConfig.Provider,
|
||||
var completion = CompletionProvider.GetChatCompletion(_services,
|
||||
provider: plannerAgent.LlmConfig.Provider,
|
||||
model: plannerAgent.LlmConfig.Model);
|
||||
|
||||
return await completion.GetChatCompletions(plannerAgent, wholeDialogs);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ public class FirstStagePlan
|
|||
[JsonPropertyName("related_tables")]
|
||||
public string[] Tables { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("has_found_relevant_knowledge")]
|
||||
public bool HasFoundRelevantKnowledge { get; set; } = false;
|
||||
|
||||
//[JsonPropertyName("related_urls")]
|
||||
//public string[] Urls { get; set; } = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
The user is dealing with a complex problem, and you need to break this complex problem into several small tasks to more easily solve the user's needs.
|
||||
You are planning to convert the user requirement into sql statements. The user is dealing with a complex problem, and you need to break this complex problem into several small tasks to more easily solve the user's needs.
|
||||
Use the TwoStagePlanner approach to plan the overall implementation steps, follow the below steps strictly.
|
||||
|
||||
1. Call plan_primary_stage to generate the primary plan.
|
||||
|
|
@ -13,7 +13,8 @@ Use the TwoStagePlanner approach to plan the overall implementation steps, follo
|
|||
*** IMPORTANT ***
|
||||
Don't run the planning process repeatedly if you have already got the result of user's request.
|
||||
Function verify_dictionary_term CAN'T generate INSERT SQL Statement.
|
||||
|
||||
The table name must come from the relevant knowledge. has_found_relevant_knowledge must be true.
|
||||
Do not introduce your actions or intentions in any way.
|
||||
|
||||
{% if global_knowledges != empty -%}
|
||||
=====
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.redshift.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.sqlserver.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\query_result_formatting.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\render_buttons.liquid" />
|
||||
<None Remove="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\sql_statement_correctness.liquid" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -86,6 +87,9 @@
|
|||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\database.summarize.sqlserver.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\render_buttons.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\beda4c12-e1ec-4b4b-b328-3df4a6687c4f\templates\sql_statement_correctness.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Repositories;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.SqlDriver.Interfaces;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using Npgsql;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Functions;
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ public class ExecuteQueryFn : IFunctionCallback
|
|||
{
|
||||
var args = JsonSerializer.Deserialize<ExecuteQueryArgs>(message.FunctionArgs);
|
||||
var refinedArgs = await RefineSqlStatement(message, args);
|
||||
var dbHook = _services.GetRequiredService<IDatabaseHook>();
|
||||
var dbHook = _services.GetRequiredService<ISqlDriverHook>();
|
||||
var dbType = dbHook.GetDatabaseType(message);
|
||||
|
||||
try
|
||||
|
|
@ -57,10 +58,18 @@ public class ExecuteQueryFn : IFunctionCallback
|
|||
|
||||
message.Content = JsonSerializer.Serialize(results);
|
||||
}
|
||||
catch (DbException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred while executing SQL query.");
|
||||
message.Content = $"Error occurred while executing SQL query: {ex.Message}";
|
||||
message.Data = ex;
|
||||
message.StopCompletion = true;
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred while executing SQL query.");
|
||||
message.Content = "Error occurred while retrieving information.";
|
||||
message.Content = $"Error occurred while executing SQL query: {ex.Message}";
|
||||
message.StopCompletion = true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -140,11 +149,11 @@ public class ExecuteQueryFn : IFunctionCallback
|
|||
provider: agent.LlmConfig.Provider,
|
||||
model: agent.LlmConfig.Model);
|
||||
|
||||
var refinedMessage = await completion.GetChatCompletions(agent, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, "Check and output the correct SQL statements")
|
||||
var refinedMessage = await completion.GetChatCompletions(agent, new List<RoleDialogModel>
|
||||
{
|
||||
new RoleDialogModel(AgentRole.User, "Check and output the correct SQL statements")
|
||||
});
|
||||
|
||||
|
||||
return refinedMessage.Content.JsonContent<ExecuteQueryArgs>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using BotSharp.Abstraction.Repositories;
|
||||
using BotSharp.Plugin.SqlDriver.Interfaces;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
|
|
@ -30,7 +30,7 @@ public class GetTableDefinitionFn : IFunctionCallback
|
|||
var args = JsonSerializer.Deserialize<SqlStatement>(message.FunctionArgs);
|
||||
var tables = args.Tables;
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var dbHook = _services.GetRequiredService<IDatabaseHook>();
|
||||
var dbHook = _services.GetRequiredService<ISqlDriverHook>();
|
||||
var dbType = dbHook.GetDatabaseType(message);
|
||||
|
||||
// Get table DDL from database
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Agents.Models;
|
||||
using BotSharp.Abstraction.Instructs;
|
||||
using BotSharp.Abstraction.Instructs.Models;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Agents.Services;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Core.Instructs;
|
||||
using BotSharp.Plugin.SqlDriver.Interfaces;
|
||||
using BotSharp.Plugin.SqlDriver.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Functions;
|
||||
|
||||
public class SqlValidateFn : IFunctionCallback
|
||||
{
|
||||
public string Name => "validate_sql";
|
||||
public string Indication => "Performing data validate operation.";
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
public SqlValidateFn(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public async Task<bool> Execute(RoleDialogModel message)
|
||||
{
|
||||
string pattern = @"```sql\s*([\s\S]*?)\s*```";
|
||||
var sqls = Regex.Match(message.Content, pattern);
|
||||
if (!sqls.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var sql = sqls.Groups[1].Value;
|
||||
|
||||
var dbHook = _services.GetRequiredService<ISqlDriverHook>();
|
||||
var dbType = dbHook.GetDatabaseType(message);
|
||||
var validateSql = dbType.ToLower() switch
|
||||
{
|
||||
"mysql" => $"explain\r\n{sql}",
|
||||
"sqlserver" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;",
|
||||
"redshift" => $"explain\r\n{sql}",
|
||||
_ => throw new NotImplementedException($"Database type {dbType} is not supported.")
|
||||
};
|
||||
var msgCopy = RoleDialogModel.From(message);
|
||||
msgCopy.FunctionArgs = JsonSerializer.Serialize(new ExecuteQueryArgs
|
||||
{
|
||||
SqlStatements = new string[] { validateSql }
|
||||
});
|
||||
|
||||
var fn = _services.GetRequiredService<IRoutingService>();
|
||||
await fn.InvokeFunction("execute_sql", msgCopy);
|
||||
|
||||
if (msgCopy.Data != null && msgCopy.Data is DbException ex)
|
||||
{
|
||||
|
||||
var instructService = _services.GetRequiredService<IInstructService>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
var agent = await agentService.GetAgent(BuiltInAgentId.SqlDriver);
|
||||
var template = agent.Templates.FirstOrDefault(x => x.Name == "sql_statement_correctness")?.Content ?? string.Empty;
|
||||
var ddl = states.GetState("table_ddls");
|
||||
|
||||
var correctedSql = await instructService.Instruct<string>(template, BuiltInAgentId.SqlDriver,
|
||||
new InstructOptions
|
||||
{
|
||||
Provider = agent?.LlmConfig?.Provider ?? "openai",
|
||||
Model = agent?.LlmConfig?.Model ?? "gpt-4o",
|
||||
Message = "Correct SQL Statement",
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "original_sql", validateSql },
|
||||
{ "error_message", ex.Message },
|
||||
{ "table_structure", ddl }
|
||||
}
|
||||
});
|
||||
message.Content = correctedSql;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
namespace BotSharp.Plugin.SqlDriver.Hooks;
|
||||
|
||||
public class SqlDriverConversationHook : ConversationHookBase, IConversationHook
|
||||
{
|
||||
public override Task OnResponseGenerated(RoleDialogModel message)
|
||||
{
|
||||
// Render function buttons
|
||||
|
||||
return base.OnResponseGenerated(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
using BotSharp.Abstraction.Agents.Enums;
|
||||
using BotSharp.Abstraction.Messaging.Enums;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent.Template;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Planning;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using System.Text.RegularExpressions;
|
||||
using BotSharp.Plugin.SqlDriver.Interfaces;
|
||||
|
||||
namespace BotSharp.Plugin.SqlDriver.Hooks;
|
||||
|
||||
|
|
@ -16,9 +22,19 @@ public class SqlDriverPlanningHook : IPlanningHook
|
|||
|
||||
public async Task OnPlanningCompleted(string planner, RoleDialogModel msg)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
await HookEmitter.Emit<ISqlDriverHook>(_services, async (hook) =>
|
||||
{
|
||||
await hook.SqlGenerated(msg);
|
||||
});
|
||||
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
if (!settings.ExecuteSqlSelectAutonomous)
|
||||
{
|
||||
var conversationStateService = _services.GetRequiredService<IConversationStateService>();
|
||||
var conversationId = conversationStateService.GetConversationId();
|
||||
msg.PostbackFunctionName = "execute_sql";
|
||||
msg.RichContent = BuildRunQueryButton(planner, msg.Content);
|
||||
msg.StopCompletion = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -37,13 +53,62 @@ public class SqlDriverPlanningHook : IPlanningHook
|
|||
// Invoke "execute_sql"
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
await routing.InvokeFunction(response.FunctionName, response);
|
||||
|
||||
msg.CurrentAgentId = agent.Id;
|
||||
msg.FunctionName = response.FunctionName;
|
||||
msg.FunctionArgs = response.FunctionArgs;
|
||||
msg.Content = response.Content;
|
||||
msg.StopCompletion = response.StopCompletion;
|
||||
}
|
||||
|
||||
/*var routing = _services.GetRequiredService<IRoutingService>();
|
||||
await routing.InvokeAgent(BuiltInAgentId.SqlDriver, wholeDialogs);*/
|
||||
public async Task<string> GetSummaryAdditionalRequirements(string planner, RoleDialogModel message)
|
||||
{
|
||||
var settings = _services.GetRequiredService<SqlDriverSetting>();
|
||||
var sqlHooks = _services.GetServices<ISqlDriverHook>();
|
||||
var agentService = _services.GetRequiredService<IAgentService>();
|
||||
|
||||
var dbType = !sqlHooks.IsNullOrEmpty() ? sqlHooks.First().GetDatabaseType(message) : settings.DatabaseType;
|
||||
var agent = await agentService.LoadAgent(BuiltInAgentId.SqlDriver);
|
||||
|
||||
return agent.Templates.FirstOrDefault(x => x.Name == $"database.summarize.{dbType}")?.Content ?? string.Empty;
|
||||
}
|
||||
|
||||
private RichContent<IRichMessage> BuildRunQueryButton(string conversationId, string text)
|
||||
{
|
||||
string pattern = @"```sql\s*([\s\S]*?)\s*```";
|
||||
var sql = Regex.Match(text, pattern).Groups[1].Value;
|
||||
var state = _services.GetRequiredService<IConversationStateService>();
|
||||
var deleteTable = state.GetState("tmp_table");
|
||||
var deleteSql = $"DROP TABLE IF EXISTS {deleteTable};";
|
||||
|
||||
return new RichContent<IRichMessage>
|
||||
{
|
||||
FillPostback = true,
|
||||
Editor = EditorTypeEnum.Text,
|
||||
Recipient = new Recipient
|
||||
{
|
||||
Id = conversationId
|
||||
},
|
||||
Message = new ButtonTemplateMessage
|
||||
{
|
||||
Text = text,
|
||||
Buttons = new List<ElementButton>
|
||||
{
|
||||
new ElementButton
|
||||
{
|
||||
Type = "text",
|
||||
Title = "Execute the SQL Statement",
|
||||
Payload = sql,
|
||||
IsPrimary = true
|
||||
},
|
||||
new ElementButton
|
||||
{
|
||||
Type = "text",
|
||||
Title = "Purge Cache",
|
||||
Payload = deleteSql
|
||||
}
|
||||
}.ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class SqlExecutorHook : AgentHookBase, IAgentHook
|
|||
var fns = agent?.Functions?.Where(x => _targetSqlExecutorFunctions.Contains(x.Name))?.ToList();
|
||||
|
||||
var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(SQL_EXECUTOR_TEMPLATE))?.Content ?? string.Empty;
|
||||
var dbType = GetDatabaseType();
|
||||
var dbType = GetDatabaseType(); //need change-> using hook?
|
||||
var render = _services.GetRequiredService<ITemplateRender>();
|
||||
prompt = render.Render(prompt, new Dictionary<string, object>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Plugin.SqlDriver.Interfaces;
|
||||
|
||||
public interface ISqlDriverHook
|
||||
{
|
||||
// Get database type
|
||||
string GetDatabaseType(RoleDialogModel message);
|
||||
Task SqlGenerated(RoleDialogModel message);
|
||||
Task SqlExecuting(RoleDialogModel message);
|
||||
Task SqlExecuted(RoleDialogModel message);
|
||||
}
|
||||
|
|
@ -31,5 +31,6 @@ public class SqlDriverPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IPlanningHook, SqlDriverPlanningHook>();
|
||||
services.AddScoped<IAgentHook, SqlDictionaryLookupHook>();
|
||||
services.AddScoped<IAgentHook, GetTableDefinitionHook>();
|
||||
services.AddScoped<IConversationHook, SqlDriverConversationHook>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
Determine whether to render the following button based on the text.
|
||||
sql_executable: When the text contains an executable sql statement, set it to true
|
||||
contains_tmp_table: When the text contains a table named tmp, set it to true
|
||||
is_sql_template
|
||||
|
||||
Output should be json format
|
||||
{
|
||||
"sql_executable": false,
|
||||
"contains_tmp_table": false,
|
||||
"is_sql_template": false
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@ Make sure all the column names are defined in the Table Structure.
|
|||
Original SQL statements:
|
||||
{{ original_sql }}
|
||||
|
||||
=====
|
||||
Error Message:
|
||||
{{ error_message }}
|
||||
|
||||
=====
|
||||
Table Structure:
|
||||
{{ table_structure }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Infrastructures;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using BotSharp.Plugin.Twilio.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -32,122 +34,202 @@ public class TwilioVoiceController : TwilioController
|
|||
/// <exception cref="ArgumentNullException"></exception>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/welcome")]
|
||||
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string[] states, [FromQuery] string intent)
|
||||
public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest request)
|
||||
{
|
||||
var text = JsonSerializer.Serialize(request);
|
||||
if (request?.CallSid == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
|
||||
}
|
||||
|
||||
string conversationId = $"TwilioVoice_{request.CallSid}";
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
VoiceResponse response;
|
||||
if (string.IsNullOrWhiteSpace(intent))
|
||||
VoiceResponse response = null;
|
||||
request.ConversationId = $"TwilioVoice_{request.CallSid}";
|
||||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
var url = $"twilio/voice/{conversationId}/receive/0?{GenerateStatesParameter(states)}";
|
||||
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
|
||||
SpeechPaths = ["twilio/welcome.mp3"],
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true,
|
||||
Timeout = 2
|
||||
};
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnSessionCreating(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
if (string.IsNullOrWhiteSpace(request.Intent))
|
||||
{
|
||||
response = twilio.ReturnNoninterruptedInstructions(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
int seqNum = 0;
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, seqNum, request.Intent);
|
||||
var callerMessage = new CallerMessage()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
ConversationId = request.ConversationId,
|
||||
SeqNumber = seqNum,
|
||||
Content = intent,
|
||||
Content = request.Intent,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
States = ParseStates(request.States)
|
||||
};
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
response = new VoiceResponse();
|
||||
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnSessionCreated(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for caller's response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string[] states, VoiceRequest request, [FromQuery] int attempts = 1)
|
||||
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
|
||||
{
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
|
||||
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum);
|
||||
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(request.ConversationId, request.SeqNum);
|
||||
string text = (request.SpeechResult + "\r\n" + request.Digits).Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
messages.Add(text);
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, request.SeqNum, text);
|
||||
}
|
||||
|
||||
VoiceResponse response;
|
||||
VoiceResponse response = null;
|
||||
if (messages.Any())
|
||||
{
|
||||
var messageContent = string.Join("\r\n", messages);
|
||||
var callerMessage = new CallerMessage()
|
||||
{
|
||||
ConversationId = conversationId,
|
||||
SeqNumber = seqNum,
|
||||
ConversationId = request.ConversationId,
|
||||
SeqNumber = request.SeqNum,
|
||||
Content = messageContent,
|
||||
Digits = request.Digits,
|
||||
From = request.From,
|
||||
States = ParseStates(states)
|
||||
States = ParseStates(request.States)
|
||||
};
|
||||
await messageQueue.EnqueueAsync(callerMessage);
|
||||
|
||||
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
|
||||
response = new VoiceResponse();
|
||||
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnReceivedUserMessage(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (attempts >= 2)
|
||||
// keep waiting for user response
|
||||
if (request.Attempts > 2)
|
||||
{
|
||||
var speechPaths = new List<string>();
|
||||
|
||||
if (seqNum == 0)
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
speechPaths.Add("twilio/welcome.mp3");
|
||||
SpeechPaths = new List<string>(),
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
// prompt user to speak clearly
|
||||
if (request.SeqNum == 0)
|
||||
{
|
||||
instruction.SpeechPaths.Add("twilio/welcome.mp3");
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1);
|
||||
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
|
||||
var lastRepy = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum - 1);
|
||||
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{lastRepy.SpeechFileName}");
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingUserResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}&attempts={++attempts}", true);
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = new List<string>(),
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
if (request.Attempts == 2)
|
||||
{
|
||||
instruction.SpeechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingUserResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polling for assistant reply after user responsed
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
|
||||
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
|
||||
[FromQuery] string[] states, VoiceRequest request)
|
||||
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
|
||||
{
|
||||
var nextSeqNum = seqNum + 1;
|
||||
var nextSeqNum = request.SeqNum + 1;
|
||||
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var fileStorage = _services.GetRequiredService<IFileStorageService>();
|
||||
|
||||
if (request.SpeechResult != null)
|
||||
{
|
||||
await sessionManager.StageCallerMessageAsync(conversationId, nextSeqNum, request.SpeechResult);
|
||||
await sessionManager.StageCallerMessageAsync(request.ConversationId, nextSeqNum, request.SpeechResult);
|
||||
}
|
||||
|
||||
var reply = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum);
|
||||
var reply = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum);
|
||||
VoiceResponse response;
|
||||
|
||||
if (reply == null)
|
||||
{
|
||||
var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum);
|
||||
var indication = await sessionManager.GetReplyIndicationAsync(request.ConversationId, request.SeqNum);
|
||||
if (indication != null)
|
||||
{
|
||||
_logger.LogWarning($"Indication: {indication}");
|
||||
|
|
@ -172,9 +254,9 @@ public class TwilioVoiceController : TwilioController
|
|||
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
|
||||
}
|
||||
|
||||
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
|
||||
fileStorage.SaveSpeechFile(conversationId, fileName, data);
|
||||
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
|
||||
var fileName = $"indication_{request.SeqNum}_{segIndex}.mp3";
|
||||
fileStorage.SaveSpeechFile(request.ConversationId, fileName, data);
|
||||
speechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{fileName}");
|
||||
|
||||
// add typing
|
||||
var typingIndex = Random.Shared.Next(1, 7);
|
||||
|
|
@ -185,8 +267,25 @@ public class TwilioVoiceController : TwilioController
|
|||
segIndex++;
|
||||
}
|
||||
}
|
||||
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
|
||||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = speechPaths,
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnIndicationGenerated(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
|
||||
await sessionManager.RemoveReplyIndicationAsync(request.ConversationId, request.SeqNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -208,25 +307,69 @@ public class TwilioVoiceController : TwilioController
|
|||
instructions.Add($"twilio/typing-{typingIndex}.mp3");
|
||||
}
|
||||
|
||||
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
SpeechPaths = instructions,
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnWaitingAgentResponse(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (reply.HumanIntervationNeeded)
|
||||
{
|
||||
response = twilio.DialCsrAgent($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentTransferring(request, _settings);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.DialCsrAgent($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
|
||||
}
|
||||
else if (reply.ConversationEnd)
|
||||
{
|
||||
response = twilio.HangUp($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
|
||||
response = twilio.HangUp($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentHangUp(request);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
response = twilio.ReturnInstructions(new List<string>
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
|
||||
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(states)}", true, hints: reply.Hints);
|
||||
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
|
||||
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true,
|
||||
Hints = reply.Hints
|
||||
};
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
{
|
||||
await hook.OnAgentResponsing(request, instruction);
|
||||
}, new HookEmitOption
|
||||
{
|
||||
OnlyOnce = true
|
||||
});
|
||||
|
||||
response = twilio.ReturnInstructions(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +389,7 @@ public class TwilioVoiceController : TwilioController
|
|||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseStates(string[] states)
|
||||
private Dictionary<string, string> ParseStates(List<string> states)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
if (states is null || !states.Any())
|
||||
|
|
@ -264,9 +407,9 @@ public class TwilioVoiceController : TwilioController
|
|||
return result;
|
||||
}
|
||||
|
||||
private string GenerateStatesParameter(string[] states)
|
||||
private string GenerateStatesParameter(List<string> states)
|
||||
{
|
||||
if (states is null || states.Length == 0)
|
||||
if (states is null || states.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioSessionHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Before session creating
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnSessionCreating(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On session created
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnSessionCreated(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On received user message
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnReceivedUserMessage(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Waiting user response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnWaitingUserResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On agent generated indication
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnIndicationGenerated(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Waiting agent response
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnWaitingAgentResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Before agent responsing
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentResponsing(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// On agent hang up
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentHangUp(ConversationalVoiceRequest request)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Before agent transferred
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <returns></returns>
|
||||
Task OnAgentTransferring(ConversationalVoiceRequest request, TwilioSetting settings)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioSessionManager
|
||||
{
|
||||
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
|
||||
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
|
||||
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
|
||||
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
|
||||
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
|
||||
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
|
||||
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Models;
|
||||
|
||||
public class ConversationalVoiceRequest : VoiceRequest
|
||||
{
|
||||
[FromRoute]
|
||||
public string ConversationId { get; set; }
|
||||
|
||||
[FromRoute]
|
||||
public int SeqNum { get; set; }
|
||||
|
||||
public int Attempts { get; set; } = 1;
|
||||
|
||||
public string Intent { get; set; }
|
||||
|
||||
public List<string> States { get; set; } = [];
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace BotSharp.Plugin.Twilio.Models;
|
||||
|
||||
public class ConversationalVoiceResponse
|
||||
{
|
||||
public List<string> SpeechPaths { get; set; } = [];
|
||||
public string CallbackPath { get; set; }
|
||||
public bool ActionOnEmptyResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout in seconds
|
||||
/// </summary>
|
||||
public int Timeout { get; set; } = 3;
|
||||
|
||||
public string Hints { get; set; }
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Services
|
||||
{
|
||||
public interface ITwilioSessionManager
|
||||
{
|
||||
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
|
||||
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
|
||||
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
|
||||
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
|
||||
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
|
||||
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
|
||||
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using BotSharp.Abstraction.Files;
|
||||
using BotSharp.Abstraction.Routing;
|
||||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Utilities;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Twilio.Jwt.AccessToken;
|
||||
using Token = Twilio.Jwt.AccessToken.Token;
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ public class TwilioService
|
|||
return response;
|
||||
}
|
||||
|
||||
public VoiceResponse ReturnInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3, string hints = null)
|
||||
public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
var gather = new Gather()
|
||||
|
|
@ -76,17 +77,17 @@ public class TwilioService
|
|||
Gather.InputEnum.Speech,
|
||||
Gather.InputEnum.Dtmf
|
||||
},
|
||||
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
|
||||
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
|
||||
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
|
||||
SpeechTimeout = "auto", // timeout > 0 ? timeout.ToString() : "3",
|
||||
Timeout = timeout > 0 ? timeout : 3,
|
||||
ActionOnEmptyResult = actionOnEmptyResult,
|
||||
Hints = hints
|
||||
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult,
|
||||
Hints = conversationalVoiceResponse.Hints
|
||||
};
|
||||
|
||||
if (!speechPaths.IsNullOrEmpty())
|
||||
if (!conversationalVoiceResponse.SpeechPaths.IsNullOrEmpty())
|
||||
{
|
||||
foreach (var speechPath in speechPaths)
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
{
|
||||
gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
|
||||
}
|
||||
|
|
@ -95,12 +96,12 @@ public class TwilioService
|
|||
return response;
|
||||
}
|
||||
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3)
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
if (speechPaths != null && speechPaths.Any())
|
||||
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
|
||||
{
|
||||
foreach (var speechPath in speechPaths)
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
{
|
||||
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
|
||||
}
|
||||
|
|
@ -112,11 +113,11 @@ public class TwilioService
|
|||
Gather.InputEnum.Speech,
|
||||
Gather.InputEnum.Dtmf
|
||||
},
|
||||
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
|
||||
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
|
||||
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
|
||||
SpeechTimeout = timeout > 0 ? timeout.ToString() : "3",
|
||||
Timeout = timeout > 0 ? timeout : 3,
|
||||
ActionOnEmptyResult = actionOnEmptyResult
|
||||
SpeechTimeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
|
||||
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult
|
||||
};
|
||||
response.Append(gather);
|
||||
return response;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using StackExchange.Redis;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using BotSharp.Abstraction.Settings;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Services;
|
||||
using StackExchange.Redis;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue