Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private async Task<RoleDialogModel> InnerGetChatCompletions(Agent agent, List<Ro
}
else if (reason == ChatFinishReason.Length)
{
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{text}");
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{DescribeContent(text)}, ToolCalls: {DescribeToolCalls(ToLlmToolCalls(value.ToolCalls))}");

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

msg = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length")
{
Expand Down Expand Up @@ -321,6 +321,13 @@ private async Task<RoleDialogModel> InnerGetChatCompletionsStreamingAsync(Agent
}
else if (choice.FinishReason.HasValue)
{
if (choice.FinishReason == ChatFinishReason.Length)
{
// What the model wrote before the limit is replaced by the notice below, so this is the only record of it
_logger.LogWarning($"Action: {nameof(InnerGetChatCompletionsStreamingAsync)}, Reason: {choice.FinishReason}, Agent: {agent.Name}, " +
$"MaxOutputTokens: {options.MaxOutputTokenCount}, Content:{DescribeContent(textStream.GetText())}, ToolCalls: {DescribeToolCalls(ReconstructToolCalls(toolCalls))}");
}

var text = choice.FinishReason == ChatFinishReason.Length ? "Model reached the maximum number of tokens allowed."
: choice.FinishReason == ChatFinishReason.ContentFilter ? "Content is omitted due to content filter rule."
: choice.FinishReason.Value.ToString();
Expand Down Expand Up @@ -759,6 +766,20 @@ private static List<LlmToolCall> ToLlmToolCalls(IEnumerable<ChatToolCall>? toolC
.Select(x => new LlmToolCall(x.Id, x.FunctionName, x.FunctionArguments?.ToString()))
.ToList();

/// <summary>
/// The tool calls of a reply cut off by the output limit: each name and how long its arguments got. The arguments
/// themselves are left out, since they can hold the user's personal data.
/// </summary>
internal static string DescribeToolCalls(List<LlmToolCall> calls)
=> string.Join(", ", calls.Select(x => $"{x.FunctionName}({x.FunctionArgs?.Length ?? 0} chars)"));

/// <summary>
/// What a reply cut off by the output limit said: its length and only its start, enough to tell what the model was
/// writing without logging the whole reply.
/// </summary>
internal static string DescribeContent(string? text)
=> string.IsNullOrEmpty(text) ? "(none)" : $"{text.Length} chars: {(text.Length > 200 ? text[..200] + "..." : text)}";

/// <summary>
/// Rebuilds the tool calls a streaming reply asked for.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
{
response = twilio.ReturnBidirectionalMediaStreamsInstructions(instruction, agent);
}
// A call that arrives with an intent already has its first message queued for the Gather flow
else if (string.IsNullOrWhiteSpace(request.Intent) && _settings.ConversationRelay.IsEnabledFor(request.AgentId))
{
// ConversationRelay speaks text only: a greeting a session hook wrote (e.g. with the caller's name) is
// said as it is, otherwise the prompts the session hooks queued become the greeting
var greeting = !string.IsNullOrWhiteSpace(instruction.Text)
? instruction.Text
: string.Join(" ", instruction.SpeechPaths
.Select(twilio.GetPromptText)
.Where(text => !string.IsNullOrWhiteSpace(text)));
if (greeting.Length == 0 && instruction.SpeechPaths.Count > 0)
{
_logger.LogWarning($"No prompt text for the welcome speech of agent {request.AgentId}; " +
"starting ConversationRelay without a greeting.");
}
response = twilio.ReturnConversationRelay(request.AgentId, request.ConversationId, greeting);
}
else
{
if (string.IsNullOrWhiteSpace(request.Intent))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ public class AgentTurnCallbacks
/// Called when the model starts writing a new response, e.g. after a tool call
/// </summary>
public Func<Task>? OnStreamStarted { get; init; }

/// <summary>
/// Called when a response the model wrote ends in a tool call: what it said before the call is complete,
/// and the tool may take a while before anything else is said
/// </summary>
public Func<Task>? OnStreamEndedWithToolCall { get; init; }
}
Loading
Loading