Skip to content

Commit b4f3629

Browse files
authored
Merge pull request #1461 from yuyixg/features/inbound-relay-watchdog
Twilio: inbound calls on ConversationRelay and a response watchdog
2 parents 75e5a5f + 5c73dcc commit b4f3629

9 files changed

Lines changed: 356 additions & 53 deletions

File tree

‎src/Plugins/BotSharp.Plugin.OpenAI/Providers/Chat/ChatCompletionProvider.Chat.cs‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private async Task<RoleDialogModel> InnerGetChatCompletions(Agent agent, List<Ro
5656
}
5757
else if (reason == ChatFinishReason.Length)
5858
{
59-
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{text}");
59+
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{DescribeContent(text)}, ToolCalls: {DescribeToolCalls(ToLlmToolCalls(value.ToolCalls))}");
6060

6161
responseMessage = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length")
6262
{
@@ -176,7 +176,7 @@ private async Task<bool> InnerGetChatCompletionsAsync(Agent agent,
176176
}
177177
else if (reason == ChatFinishReason.Length)
178178
{
179-
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletionsAsync)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{text}");
179+
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletionsAsync)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{DescribeContent(text)}, ToolCalls: {DescribeToolCalls(ToLlmToolCalls(value.ToolCalls))}");
180180

181181
msg = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length")
182182
{
@@ -321,6 +321,13 @@ private async Task<RoleDialogModel> InnerGetChatCompletionsStreamingAsync(Agent
321321
}
322322
else if (choice.FinishReason.HasValue)
323323
{
324+
if (choice.FinishReason == ChatFinishReason.Length)
325+
{
326+
// What the model wrote before the limit is replaced by the notice below, so this is the only record of it
327+
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletionsStreamingAsync)}, Reason: {choice.FinishReason}, Agent: {agent.Name}, " +
328+
$"MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{DescribeContent(textStream.GetText())}, ToolCalls: {DescribeToolCalls(ReconstructToolCalls(toolCalls))}");
329+
}
330+
324331
var text = choice.FinishReason == ChatFinishReason.Length ? "Model reached the maximum number of tokens allowed."
325332
: choice.FinishReason == ChatFinishReason.ContentFilter ? "Content is omitted due to content filter rule."
326333
: choice.FinishReason.Value.ToString();
@@ -759,6 +766,20 @@ private static List<LlmToolCall> ToLlmToolCalls(IEnumerable<ChatToolCall>? toolC
759766
.Select(x => new LlmToolCall(x.Id, x.FunctionName, x.FunctionArguments?.ToString()))
760767
.ToList();
761768

769+
/// <summary>
770+
/// The tool calls of a reply cut off by the output limit: each name and how long its arguments got. The arguments
771+
/// themselves are left out, since they can hold the user's personal data.
772+
/// </summary>
773+
internal static string DescribeToolCalls(List<LlmToolCall> calls)
774+
=> string.Join(", ", calls.Select(x => $"{x.FunctionName}({x.FunctionArgs?.Length ?? 0} chars)"));
775+
776+
/// <summary>
777+
/// What a reply cut off by the output limit said: its length and only its start, enough to tell what the model was
778+
/// writing without logging the whole reply.
779+
/// </summary>
780+
internal static string DescribeContent(string? text)
781+
=> string.IsNullOrEmpty(text) ? "(none)" : $"{text.Length} chars: {(text.Length > 200 ? text[..200] + "..." : text)}";
782+
762783
/// <summary>
763784
/// Rebuilds the tool calls a streaming reply asked for.
764785
/// </summary>

‎src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
9393
{
9494
response = twilio.ReturnBidirectionalMediaStreamsInstructions(instruction, agent);
9595
}
96+
// A call that arrives with an intent already has its first message queued for the Gather flow
97+
else if (string.IsNullOrWhiteSpace(request.Intent) && _settings.ConversationRelay.IsEnabledFor(request.AgentId))
98+
{
99+
// ConversationRelay speaks text only: a greeting a session hook wrote (e.g. with the caller's name) is
100+
// said as it is, otherwise the prompts the session hooks queued become the greeting
101+
var greeting = !string.IsNullOrWhiteSpace(instruction.Text)
102+
? instruction.Text
103+
: string.Join(" ", instruction.SpeechPaths
104+
.Select(twilio.GetPromptText)
105+
.Where(text => !string.IsNullOrWhiteSpace(text)));
106+
if (greeting.Length == 0 && instruction.SpeechPaths.Count > 0)
107+
{
108+
_logger.LogWarning($"No prompt text for the welcome speech of agent {request.AgentId}; " +
109+
"starting ConversationRelay without a greeting.");
110+
}
111+
response = twilio.ReturnConversationRelay(request.AgentId, request.ConversationId, greeting);
112+
}
96113
else
97114
{
98115
if (string.IsNullOrWhiteSpace(request.Intent))

‎src/Plugins/BotSharp.Plugin.Twilio/Models/AgentTurnCallbacks.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@ public class AgentTurnCallbacks
2121
/// Called when the model starts writing a new response, e.g. after a tool call
2222
/// </summary>
2323
public Func<Task>? OnStreamStarted { get; init; }
24+
25+
/// <summary>
26+
/// Called when a response the model wrote ends in a tool call: what it said before the call is complete,
27+
/// and the tool may take a while before anything else is said
28+
/// </summary>
29+
public Func<Task>? OnStreamEndedWithToolCall { get; init; }
2430
}

0 commit comments

Comments
 (0)