Merge branch 'SciSharp:master' into master

This commit is contained in:
Haiping 2024-11-13 02:07:47 +00:00 committed by GitHub
commit 805dbbbe08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 779 additions and 193 deletions

View file

@ -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.

View file

@ -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()
{
}
}

View file

@ -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; }
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Infrastructures;
public class HookEmitOption
{
public bool OnlyOnce { get; set; }
}

View file

@ -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)

View file

@ -1,7 +0,0 @@
namespace BotSharp.Abstraction.Repositories;
public interface IDatabaseHook
{
// Get database type
string GetDatabaseType(RoleDialogModel message);
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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)
{

View file

@ -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}";
}
}

View file

@ -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 %}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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; } = [];

View file

@ -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 -%}
=====

View file

@ -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>

View file

@ -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>();
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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()
}
};
}
}

View file

@ -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>
{

View file

@ -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);
}

View file

@ -31,5 +31,6 @@ public class SqlDriverPlugin : IBotSharpPlugin
services.AddScoped<IPlanningHook, SqlDriverPlanningHook>();
services.AddScoped<IAgentHook, SqlDictionaryLookupHook>();
services.AddScoped<IAgentHook, GetTableDefinitionHook>();
services.AddScoped<IConversationHook, SqlDriverConversationHook>();
}
}

View file

@ -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
}

View file

@ -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 }}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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; } = [];
}

View file

@ -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; }
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -1,3 +1,4 @@
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using StackExchange.Redis;
using Task = System.Threading.Tasks.Task;

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Services;
using StackExchange.Redis;