2025-02-26 17:41:52 +00:00
|
|
|
using BotSharp.Abstraction.Conversations.Enums;
|
2025-02-03 04:02:36 +00:00
|
|
|
using BotSharp.Abstraction.Files.Utilities;
|
|
|
|
|
using BotSharp.Abstraction.Functions.Models;
|
2025-02-28 18:44:59 +00:00
|
|
|
using BotSharp.Abstraction.Options;
|
2025-02-03 04:02:36 +00:00
|
|
|
using BotSharp.Abstraction.Realtime.Models;
|
2025-02-28 18:44:59 +00:00
|
|
|
using BotSharp.Core.Infrastructures;
|
2025-02-07 17:18:47 +00:00
|
|
|
using BotSharp.Plugin.OpenAI.Models.Realtime;
|
2025-02-03 04:02:36 +00:00
|
|
|
using OpenAI.Chat;
|
2025-02-09 23:31:46 +00:00
|
|
|
using System.Net.WebSockets;
|
|
|
|
|
using System.Text;
|
2025-02-03 04:02:36 +00:00
|
|
|
using System.Text.Json;
|
2025-02-09 23:31:46 +00:00
|
|
|
using System.Threading;
|
2025-02-03 04:02:36 +00:00
|
|
|
|
|
|
|
|
namespace BotSharp.Plugin.OpenAI.Providers.Realtime;
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
/// <summary>
|
|
|
|
|
/// Reference to https://platform.openai.com/docs/api-reference/realtime-server-events
|
|
|
|
|
/// </summary>
|
2025-02-03 04:02:36 +00:00
|
|
|
public class RealTimeCompletionProvider : IRealTimeCompletion
|
|
|
|
|
{
|
|
|
|
|
public string Provider => "openai";
|
2025-02-07 22:40:57 +00:00
|
|
|
public string Model => _model;
|
2025-02-03 04:02:36 +00:00
|
|
|
|
|
|
|
|
protected readonly OpenAiSettings _settings;
|
|
|
|
|
protected readonly IServiceProvider _services;
|
|
|
|
|
protected readonly ILogger<RealTimeCompletionProvider> _logger;
|
|
|
|
|
|
|
|
|
|
protected string _model = "gpt-4o-mini-realtime-preview-2024-12-17";
|
2025-02-09 23:31:46 +00:00
|
|
|
private ClientWebSocket _webSocket;
|
2025-02-07 22:40:57 +00:00
|
|
|
|
2025-02-03 04:02:36 +00:00
|
|
|
public RealTimeCompletionProvider(
|
|
|
|
|
OpenAiSettings settings,
|
|
|
|
|
ILogger<RealTimeCompletionProvider> logger,
|
|
|
|
|
IServiceProvider services)
|
|
|
|
|
{
|
|
|
|
|
_settings = settings;
|
|
|
|
|
_logger = logger;
|
|
|
|
|
_services = services;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
public async Task Connect(RealtimeHubConnection conn,
|
|
|
|
|
Action onModelReady,
|
2025-03-06 09:03:30 +00:00
|
|
|
Action<string,string> onModelAudioDeltaReceived,
|
2025-02-09 23:31:46 +00:00
|
|
|
Action onModelAudioResponseDone,
|
|
|
|
|
Action<string> onAudioTranscriptDone,
|
2025-02-11 23:27:07 +00:00
|
|
|
Action<List<RoleDialogModel>> onModelResponseDone,
|
|
|
|
|
Action<string> onConversationItemCreated,
|
|
|
|
|
Action<RoleDialogModel> onInputAudioTranscriptionCompleted,
|
2025-02-09 23:31:46 +00:00
|
|
|
Action onUserInterrupted)
|
|
|
|
|
{
|
|
|
|
|
var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
|
|
|
|
var settings = settingsService.GetSetting(provider: "openai", conn.Model);
|
|
|
|
|
|
|
|
|
|
_webSocket = new ClientWebSocket();
|
|
|
|
|
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {settings.ApiKey}");
|
|
|
|
|
_webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");
|
|
|
|
|
|
|
|
|
|
await _webSocket.ConnectAsync(new Uri($"wss://api.openai.com/v1/realtime?model={conn.Model}"), CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
if (_webSocket.State == WebSocketState.Open)
|
|
|
|
|
{
|
|
|
|
|
// Receive a message
|
2025-02-11 23:27:07 +00:00
|
|
|
_ = ReceiveMessage(conn,
|
2025-03-05 22:57:58 +00:00
|
|
|
onModelReady,
|
2025-02-11 23:27:07 +00:00
|
|
|
onModelAudioDeltaReceived,
|
2025-02-09 23:31:46 +00:00
|
|
|
onModelAudioResponseDone,
|
|
|
|
|
onAudioTranscriptDone,
|
|
|
|
|
onModelResponseDone,
|
2025-02-11 23:27:07 +00:00
|
|
|
onConversationItemCreated,
|
|
|
|
|
onInputAudioTranscriptionCompleted,
|
2025-02-09 23:31:46 +00:00
|
|
|
onUserInterrupted);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task Disconnect()
|
|
|
|
|
{
|
2025-03-05 22:57:34 +00:00
|
|
|
if (_webSocket.State == WebSocketState.Open)
|
|
|
|
|
{
|
|
|
|
|
await _webSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task AppenAudioBuffer(string message)
|
|
|
|
|
{
|
|
|
|
|
var audioAppend = new
|
|
|
|
|
{
|
|
|
|
|
type = "input_audio_buffer.append",
|
|
|
|
|
audio = message
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await SendEventToModel(audioAppend);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 21:52:10 +00:00
|
|
|
public async Task TriggerModelInference(string? instructions = null)
|
|
|
|
|
{
|
|
|
|
|
// Triggering model inference
|
|
|
|
|
await SendEventToModel(new
|
|
|
|
|
{
|
|
|
|
|
type = "response.create",
|
|
|
|
|
response = new
|
|
|
|
|
{
|
|
|
|
|
instructions
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-01 00:19:44 +00:00
|
|
|
public async Task CancelModelResponse()
|
|
|
|
|
{
|
|
|
|
|
await SendEventToModel(new
|
|
|
|
|
{
|
|
|
|
|
type = "response.cancel"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RemoveConversationItem(string itemId)
|
|
|
|
|
{
|
|
|
|
|
await SendEventToModel(new
|
|
|
|
|
{
|
|
|
|
|
type = "conversation.item.delete",
|
|
|
|
|
item_id = itemId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-05 22:57:58 +00:00
|
|
|
private async Task ReceiveMessage(RealtimeHubConnection conn,
|
|
|
|
|
Action onModelReady,
|
2025-03-06 09:03:30 +00:00
|
|
|
Action<string,string> onModelAudioDeltaReceived,
|
2025-02-09 23:31:46 +00:00
|
|
|
Action onModelAudioResponseDone,
|
|
|
|
|
Action<string> onAudioTranscriptDone,
|
2025-02-11 23:27:07 +00:00
|
|
|
Action<List<RoleDialogModel>> onModelResponseDone,
|
|
|
|
|
Action<string> onConversationItemCreated,
|
|
|
|
|
Action<RoleDialogModel> onInputAudioTranscriptionCompleted,
|
2025-02-09 23:31:46 +00:00
|
|
|
Action onUserInterrupted)
|
|
|
|
|
{
|
2025-03-05 18:29:28 +00:00
|
|
|
var buffer = new byte[1024 * 16];
|
2025-02-09 23:31:46 +00:00
|
|
|
WebSocketReceiveResult result;
|
2025-03-03 21:30:06 +00:00
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
result = await _webSocket.ReceiveAsync(
|
|
|
|
|
new ArraySegment<byte>(buffer), CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
// Convert received data to text/audio (Twilio sends Base64-encoded audio)
|
|
|
|
|
string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
|
|
|
|
if (string.IsNullOrEmpty(receivedText))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
_logger.LogDebug($"{nameof(RealTimeCompletionProvider)} received: {receivedText}");
|
|
|
|
|
var response = JsonSerializer.Deserialize<ServerEventResponse>(receivedText);
|
|
|
|
|
|
|
|
|
|
if (response.Type == "error")
|
|
|
|
|
{
|
|
|
|
|
var error = JsonSerializer.Deserialize<ServerEventErrorResponse>(receivedText);
|
|
|
|
|
_logger.LogError($"Error: {error.Body.Message}");
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "session.created")
|
|
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
2025-03-05 22:57:58 +00:00
|
|
|
onModelReady();
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
else if (response.Type == "session.updated")
|
|
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
else if (response.Type == "response.audio_transcript.delta")
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "response.audio_transcript.done")
|
|
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
2025-02-09 23:31:46 +00:00
|
|
|
var data = JsonSerializer.Deserialize<ResponseAudioTranscript>(receivedText);
|
2025-03-03 21:30:06 +00:00
|
|
|
await Task.Delay(1000);
|
2025-02-09 23:31:46 +00:00
|
|
|
onAudioTranscriptDone(data.Transcript);
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "response.audio.delta")
|
|
|
|
|
{
|
|
|
|
|
var audio = JsonSerializer.Deserialize<ResponseAudioDelta>(receivedText);
|
2025-03-06 09:03:30 +00:00
|
|
|
if (audio?.Delta != null)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-03-03 21:30:06 +00:00
|
|
|
_logger.LogDebug($"{response.Type}: {receivedText}");
|
2025-03-06 09:03:30 +00:00
|
|
|
onModelAudioDeltaReceived(audio.Delta, audio.ItemId);
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "response.audio.done")
|
|
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
2025-02-09 23:31:46 +00:00
|
|
|
onModelAudioResponseDone();
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "response.done")
|
|
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
2025-02-11 23:27:07 +00:00
|
|
|
var messages = await OnResponsedDone(conn, receivedText);
|
|
|
|
|
onModelResponseDone(messages);
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "conversation.item.created")
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
|
|
|
|
onConversationItemCreated(receivedText);
|
|
|
|
|
}
|
|
|
|
|
else if (response.Type == "conversation.item.input_audio_transcription.completed")
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"{response.Type}: {receivedText}");
|
|
|
|
|
var message = await OnInputAudioTranscriptionCompleted(conn, receivedText);
|
|
|
|
|
onInputAudioTranscriptionCompleted(message);
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
else if (response.Type == "input_audio_buffer.speech_started")
|
|
|
|
|
{
|
2025-03-05 18:29:28 +00:00
|
|
|
// Handle user interuption
|
2025-03-06 18:01:43 +00:00
|
|
|
if (conn.MarkQueue.Count > 0 && conn.ResponseStartTimestamp != null)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-03-06 18:01:43 +00:00
|
|
|
var elapsedTime = conn.LatestMediaTimestamp - conn.ResponseStartTimestamp;
|
2025-03-05 18:29:28 +00:00
|
|
|
|
2025-03-06 09:03:30 +00:00
|
|
|
if (!string.IsNullOrEmpty(conn.LastAssistantItemId))
|
2025-03-03 21:30:06 +00:00
|
|
|
{
|
2025-03-05 18:29:28 +00:00
|
|
|
var truncateEvent = new
|
|
|
|
|
{
|
|
|
|
|
type = "conversation.item.truncate",
|
2025-03-06 09:03:30 +00:00
|
|
|
item_id = conn.LastAssistantItemId,
|
2025-03-05 18:29:28 +00:00
|
|
|
content_index = 0,
|
|
|
|
|
audio_end_ms = elapsedTime
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await SendEventToModel(truncateEvent);
|
|
|
|
|
}
|
2025-03-03 21:30:06 +00:00
|
|
|
|
|
|
|
|
onUserInterrupted();
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} while (!result.CloseStatus.HasValue);
|
|
|
|
|
|
|
|
|
|
await _webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task SendEventToModel(object message)
|
|
|
|
|
{
|
2025-02-26 17:41:52 +00:00
|
|
|
if (_webSocket.State != WebSocketState.Open)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
if (message is not string data)
|
|
|
|
|
{
|
2025-02-28 18:44:59 +00:00
|
|
|
data = JsonSerializer.Serialize(message, BotSharpOptions.defaultJsonOptions);
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buffer = Encoding.UTF8.GetBytes(data);
|
2025-02-26 17:41:52 +00:00
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-03 04:02:36 +00:00
|
|
|
public async Task<RealtimeSession> CreateSession(Agent agent, List<RoleDialogModel> conversations)
|
|
|
|
|
{
|
|
|
|
|
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
|
|
|
|
|
|
|
|
|
|
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
|
|
|
|
var chatClient = client.GetChatClient(_model);
|
|
|
|
|
var (prompt, messages, options) = PrepareOptions(agent, conversations);
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent.Description;
|
|
|
|
|
|
|
|
|
|
var args = new RealtimeSessionCreationRequest
|
2025-02-03 04:02:36 +00:00
|
|
|
{
|
2025-03-05 18:29:28 +00:00
|
|
|
Model = _model,
|
2025-02-09 23:31:46 +00:00
|
|
|
Instructions = instruction,
|
2025-02-03 04:02:36 +00:00
|
|
|
ToolChoice = "auto",
|
|
|
|
|
Tools = options.Tools.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
var fn = new FunctionDef
|
|
|
|
|
{
|
|
|
|
|
Name = x.FunctionName,
|
|
|
|
|
Description = x.FunctionDescription
|
|
|
|
|
};
|
|
|
|
|
fn.Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters);
|
|
|
|
|
return fn;
|
|
|
|
|
}).ToArray(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
2025-03-05 18:29:28 +00:00
|
|
|
var settings = settingsService.GetSetting(Provider, args.Model ?? _model);
|
2025-02-03 04:02:36 +00:00
|
|
|
|
|
|
|
|
var api = _services.GetRequiredService<IOpenAiRealtimeApi>();
|
|
|
|
|
var session = await api.GetSessionAsync(args, settings.ApiKey);
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-05 22:57:58 +00:00
|
|
|
public async Task UpdateSession(RealtimeHubConnection conn, bool turnDetection = true)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
|
|
|
|
var convService = _services.GetRequiredService<IConversationService>();
|
|
|
|
|
var conv = await convService.GetConversation(conn.ConversationId);
|
|
|
|
|
|
|
|
|
|
var agentService = _services.GetRequiredService<IAgentService>();
|
2025-02-28 18:44:59 +00:00
|
|
|
var agent = await agentService.LoadAgent(conn.CurrentAgentId);
|
2025-02-09 23:31:46 +00:00
|
|
|
|
|
|
|
|
var client = ProviderHelper.GetClient(Provider, _model, _services);
|
|
|
|
|
var chatClient = client.GetChatClient(_model);
|
|
|
|
|
var (prompt, messages, options) = PrepareOptions(agent, []);
|
|
|
|
|
|
|
|
|
|
var instruction = messages.FirstOrDefault()?.Content.FirstOrDefault()?.Text ?? agent.Description;
|
2025-02-28 18:44:59 +00:00
|
|
|
var functions = options.Tools.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
var fn = new FunctionDef
|
|
|
|
|
{
|
|
|
|
|
Name = x.FunctionName,
|
|
|
|
|
Description = x.FunctionDescription
|
|
|
|
|
};
|
|
|
|
|
fn.Parameters = JsonSerializer.Deserialize<FunctionParametersDef>(x.FunctionParameters);
|
|
|
|
|
return fn;
|
|
|
|
|
}).ToArray();
|
2025-02-09 23:31:46 +00:00
|
|
|
|
|
|
|
|
var sessionUpdate = new
|
|
|
|
|
{
|
|
|
|
|
type = "session.update",
|
|
|
|
|
session = new RealtimeSessionUpdateRequest
|
|
|
|
|
{
|
|
|
|
|
InputAudioFormat = "g711_ulaw",
|
|
|
|
|
OutputAudioFormat = "g711_ulaw",
|
2025-02-11 23:27:07 +00:00
|
|
|
InputAudioTranscription = new InputAudioTranscription
|
|
|
|
|
{
|
|
|
|
|
Model = "whisper-1",
|
|
|
|
|
},
|
2025-02-09 23:31:46 +00:00
|
|
|
Voice = "alloy",
|
|
|
|
|
Instructions = instruction,
|
|
|
|
|
ToolChoice = "auto",
|
2025-02-28 18:44:59 +00:00
|
|
|
Tools = functions,
|
2025-02-09 23:31:46 +00:00
|
|
|
Modalities = [ "text", "audio" ],
|
2025-03-06 09:03:30 +00:00
|
|
|
Temperature = Math.Max(options.Temperature ?? 0f, 0.6f),
|
2025-03-01 00:19:44 +00:00
|
|
|
MaxResponseOutputTokens = 512,
|
|
|
|
|
TurnDetection = new RealtimeSessionTurnDetection
|
|
|
|
|
{
|
2025-03-06 09:03:30 +00:00
|
|
|
Threshold = 0.8f,
|
2025-03-05 18:29:28 +00:00
|
|
|
PrefixPadding = 300,
|
2025-03-06 09:03:30 +00:00
|
|
|
SilenceDuration = 800
|
2025-03-01 00:19:44 +00:00
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-05 22:57:58 +00:00
|
|
|
if (!turnDetection)
|
|
|
|
|
{
|
|
|
|
|
sessionUpdate.session.TurnDetection = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-28 18:44:59 +00:00
|
|
|
await HookEmitter.Emit<IContentGeneratingHook>(_services, async hook =>
|
|
|
|
|
{
|
|
|
|
|
await hook.OnSessionUpdated(agent, instruction, functions);
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-11 23:27:07 +00:00
|
|
|
await SendEventToModel(sessionUpdate);
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-11 23:27:07 +00:00
|
|
|
public async Task InsertConversationItem(RoleDialogModel message)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-10 21:52:10 +00:00
|
|
|
if (message.Role == AgentRole.Function)
|
|
|
|
|
{
|
|
|
|
|
var functionConversationItem = new
|
|
|
|
|
{
|
|
|
|
|
type = "conversation.item.create",
|
|
|
|
|
item = new
|
|
|
|
|
{
|
|
|
|
|
call_id = message.ToolCallId,
|
|
|
|
|
type = "function_call_output",
|
|
|
|
|
output = message.Content
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-02-11 23:27:07 +00:00
|
|
|
|
|
|
|
|
await SendEventToModel(functionConversationItem);
|
2025-02-10 21:52:10 +00:00
|
|
|
}
|
2025-02-11 23:27:07 +00:00
|
|
|
else if (message.Role == AgentRole.Assistant)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-10 23:28:03 +00:00
|
|
|
var conversationItem = new
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-10 23:28:03 +00:00
|
|
|
type = "conversation.item.create",
|
|
|
|
|
item = new
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-10 23:28:03 +00:00
|
|
|
type = "message",
|
|
|
|
|
role = message.Role,
|
|
|
|
|
content = new object[]
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-10 23:28:03 +00:00
|
|
|
new
|
|
|
|
|
{
|
|
|
|
|
type = "text",
|
|
|
|
|
text = message.Content
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-10 23:28:03 +00:00
|
|
|
};
|
2025-02-09 23:31:46 +00:00
|
|
|
|
2025-02-11 23:27:07 +00:00
|
|
|
await SendEventToModel(conversationItem);
|
|
|
|
|
}
|
|
|
|
|
else if (message.Role == AgentRole.User)
|
|
|
|
|
{
|
|
|
|
|
var conversationItem = new
|
|
|
|
|
{
|
|
|
|
|
type = "conversation.item.create",
|
|
|
|
|
item = new
|
|
|
|
|
{
|
|
|
|
|
type = "message",
|
|
|
|
|
role = message.Role,
|
|
|
|
|
content = new object[]
|
|
|
|
|
{
|
|
|
|
|
new
|
|
|
|
|
{
|
|
|
|
|
type = "input_text",
|
|
|
|
|
text = message.Content
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await SendEventToModel(conversationItem);
|
2025-02-10 23:28:03 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new NotImplementedException("");
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-03 04:02:36 +00:00
|
|
|
protected (string, IEnumerable<ChatMessage>, ChatCompletionOptions) PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
|
|
|
|
|
{
|
|
|
|
|
var agentService = _services.GetRequiredService<IAgentService>();
|
|
|
|
|
var state = _services.GetRequiredService<IConversationStateService>();
|
|
|
|
|
var fileStorage = _services.GetRequiredService<IFileStorageService>();
|
|
|
|
|
var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
|
|
|
|
var settings = settingsService.GetSetting(Provider, _model);
|
|
|
|
|
var allowMultiModal = settings != null && settings.MultiModal;
|
|
|
|
|
|
|
|
|
|
var messages = new List<ChatMessage>();
|
|
|
|
|
|
|
|
|
|
var temperature = float.Parse(state.GetState("temperature", "0.0"));
|
2025-02-05 23:50:32 +00:00
|
|
|
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
|
|
|
|
|
? tokens
|
|
|
|
|
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
|
2025-02-03 04:02:36 +00:00
|
|
|
var options = new ChatCompletionOptions()
|
|
|
|
|
{
|
|
|
|
|
ToolChoice = ChatToolChoice.CreateAutoChoice(),
|
|
|
|
|
Temperature = temperature,
|
|
|
|
|
MaxOutputTokenCount = maxTokens
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
|
|
|
|
|
foreach (var function in functions)
|
|
|
|
|
{
|
|
|
|
|
if (!agentService.RenderFunction(agent, function)) continue;
|
|
|
|
|
|
|
|
|
|
var property = agentService.RenderFunctionProperty(agent, function);
|
|
|
|
|
|
|
|
|
|
options.Tools.Add(ChatTool.CreateFunctionTool(
|
|
|
|
|
functionName: function.Name,
|
|
|
|
|
functionDescription: function.Description,
|
|
|
|
|
functionParameters: BinaryData.FromObjectAsJson(property)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
var text = agentService.RenderedInstruction(agent);
|
|
|
|
|
messages.Add(new SystemChatMessage(text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(agent.Knowledges))
|
|
|
|
|
{
|
|
|
|
|
messages.Add(new SystemChatMessage(agent.Knowledges));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var samples = ProviderHelper.GetChatSamples(agent.Samples);
|
|
|
|
|
foreach (var sample in samples)
|
|
|
|
|
{
|
|
|
|
|
messages.Add(sample.Role == AgentRole.User ? new UserChatMessage(sample.Content) : new AssistantChatMessage(sample.Content));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var filteredMessages = conversations.Select(x => x).ToList();
|
|
|
|
|
var firstUserMsgIdx = filteredMessages.FindIndex(x => x.Role == AgentRole.User);
|
|
|
|
|
if (firstUserMsgIdx > 0)
|
|
|
|
|
{
|
|
|
|
|
filteredMessages = filteredMessages.Where((_, idx) => idx >= firstUserMsgIdx).ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var message in filteredMessages)
|
|
|
|
|
{
|
|
|
|
|
if (message.Role == AgentRole.Function)
|
|
|
|
|
{
|
|
|
|
|
messages.Add(new AssistantChatMessage(new List<ChatToolCall>
|
|
|
|
|
{
|
|
|
|
|
ChatToolCall.CreateFunctionToolCall(message.ToolCallId, message.FunctionName, BinaryData.FromString(message.FunctionArgs ?? string.Empty))
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
messages.Add(new ToolChatMessage(message.ToolCallId, message.Content));
|
|
|
|
|
}
|
|
|
|
|
else if (message.Role == AgentRole.User)
|
|
|
|
|
{
|
|
|
|
|
var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content;
|
|
|
|
|
var textPart = ChatMessageContentPart.CreateTextPart(text);
|
|
|
|
|
var contentParts = new List<ChatMessageContentPart> { textPart };
|
|
|
|
|
|
|
|
|
|
if (allowMultiModal && !message.Files.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
foreach (var file in message.Files)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(file.FileData))
|
|
|
|
|
{
|
|
|
|
|
var (contentType, bytes) = FileUtility.GetFileInfoFromData(file.FileData);
|
|
|
|
|
var contentPart = ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(bytes), contentType, ChatImageDetailLevel.Auto);
|
|
|
|
|
contentParts.Add(contentPart);
|
|
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
|
|
|
|
|
{
|
|
|
|
|
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
|
|
|
|
|
var bytes = fileStorage.GetFileBytes(file.FileStorageUrl);
|
|
|
|
|
var contentPart = ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(bytes), contentType, ChatImageDetailLevel.Auto);
|
|
|
|
|
contentParts.Add(contentPart);
|
|
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(file.FileUrl))
|
|
|
|
|
{
|
|
|
|
|
var uri = new Uri(file.FileUrl);
|
|
|
|
|
var contentPart = ChatMessageContentPart.CreateImagePart(uri, ChatImageDetailLevel.Auto);
|
|
|
|
|
contentParts.Add(contentPart);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
messages.Add(new UserChatMessage(contentParts) { ParticipantName = message.FunctionName });
|
|
|
|
|
}
|
|
|
|
|
else if (message.Role == AgentRole.Assistant)
|
|
|
|
|
{
|
|
|
|
|
messages.Add(new AssistantChatMessage(message.Content));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prompt = GetPrompt(messages, options);
|
|
|
|
|
return (prompt, messages, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetPrompt(IEnumerable<ChatMessage> messages, ChatCompletionOptions options)
|
|
|
|
|
{
|
|
|
|
|
var prompt = string.Empty;
|
|
|
|
|
|
|
|
|
|
if (!messages.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
// System instruction
|
|
|
|
|
var verbose = string.Join("\r\n", messages
|
|
|
|
|
.Select(x => x as SystemChatMessage)
|
|
|
|
|
.Where(x => x != null)
|
|
|
|
|
.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(x.ParticipantName))
|
|
|
|
|
{
|
|
|
|
|
// To display Agent name in log
|
|
|
|
|
return $"[{x.ParticipantName}]: {x.Content.FirstOrDefault()?.Text ?? string.Empty}";
|
|
|
|
|
}
|
|
|
|
|
return $"{AgentRole.System}: {x.Content.FirstOrDefault()?.Text ?? string.Empty}";
|
|
|
|
|
}));
|
|
|
|
|
prompt += $"{verbose}\r\n";
|
|
|
|
|
|
|
|
|
|
prompt += "\r\n[CONVERSATION]";
|
|
|
|
|
verbose = string.Join("\r\n", messages
|
|
|
|
|
.Where(x => x as SystemChatMessage == null)
|
|
|
|
|
.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
var fnMessage = x as ToolChatMessage;
|
|
|
|
|
if (fnMessage != null)
|
|
|
|
|
{
|
|
|
|
|
return $"{AgentRole.Function}: {fnMessage.Content.FirstOrDefault()?.Text ?? string.Empty}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var userMessage = x as UserChatMessage;
|
|
|
|
|
if (userMessage != null)
|
|
|
|
|
{
|
|
|
|
|
var content = x.Content.FirstOrDefault()?.Text ?? string.Empty;
|
|
|
|
|
return !string.IsNullOrEmpty(userMessage.ParticipantName) && userMessage.ParticipantName != "route_to_agent" ?
|
|
|
|
|
$"{userMessage.ParticipantName}: {content}" :
|
|
|
|
|
$"{AgentRole.User}: {content}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var assistMessage = x as AssistantChatMessage;
|
|
|
|
|
if (assistMessage != null)
|
|
|
|
|
{
|
|
|
|
|
var toolCall = assistMessage.ToolCalls?.FirstOrDefault();
|
|
|
|
|
return toolCall != null ?
|
|
|
|
|
$"{AgentRole.Assistant}: Call function {toolCall?.FunctionName}({toolCall?.FunctionArguments})" :
|
|
|
|
|
$"{AgentRole.Assistant}: {assistMessage.Content.FirstOrDefault()?.Text ?? string.Empty}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
}));
|
|
|
|
|
prompt += $"\r\n{verbose}\r\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!options.Tools.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
var functions = string.Join("\r\n", options.Tools.Select(fn =>
|
|
|
|
|
{
|
|
|
|
|
return $"\r\n{fn.FunctionName}: {fn.FunctionDescription}\r\n{fn.FunctionParameters}";
|
|
|
|
|
}));
|
|
|
|
|
prompt += $"\r\n[FUNCTIONS]{functions}\r\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return prompt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetModelName(string model)
|
|
|
|
|
{
|
|
|
|
|
_model = model;
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
|
|
|
|
|
public async Task<List<RoleDialogModel>> OnResponsedDone(RealtimeHubConnection conn, string response)
|
|
|
|
|
{
|
|
|
|
|
var outputs = new List<RoleDialogModel>();
|
|
|
|
|
|
|
|
|
|
var data = JsonSerializer.Deserialize<ResponseDone>(response).Body;
|
2025-03-01 00:19:44 +00:00
|
|
|
if (data.Status != "completed")
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
foreach (var output in data.Outputs)
|
|
|
|
|
{
|
|
|
|
|
if (output.Type == "function_call")
|
|
|
|
|
{
|
2025-02-11 23:27:07 +00:00
|
|
|
outputs.Add(new RoleDialogModel(output.Role, output.Arguments)
|
2025-02-09 23:31:46 +00:00
|
|
|
{
|
2025-02-28 18:44:59 +00:00
|
|
|
CurrentAgentId = conn.CurrentAgentId,
|
2025-02-09 23:31:46 +00:00
|
|
|
FunctionName = output.Name,
|
2025-02-10 21:52:10 +00:00
|
|
|
FunctionArgs = output.Arguments,
|
2025-02-26 17:41:52 +00:00
|
|
|
ToolCallId = output.CallId,
|
2025-03-01 00:19:44 +00:00
|
|
|
MessageId = output.Id,
|
2025-02-26 17:41:52 +00:00
|
|
|
MessageType = MessageTypeName.FunctionCall
|
2025-02-09 23:31:46 +00:00
|
|
|
});
|
|
|
|
|
}
|
2025-02-11 23:27:07 +00:00
|
|
|
else if (output.Type == "message")
|
|
|
|
|
{
|
|
|
|
|
var content = output.Content.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
outputs.Add(new RoleDialogModel(output.Role, content.Transcript)
|
|
|
|
|
{
|
2025-03-03 21:30:06 +00:00
|
|
|
CurrentAgentId = conn.CurrentAgentId,
|
|
|
|
|
MessageId = output.Id,
|
|
|
|
|
MessageType = MessageTypeName.Plain
|
2025-02-11 23:27:07 +00:00
|
|
|
});
|
|
|
|
|
}
|
2025-02-09 23:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-28 18:44:59 +00:00
|
|
|
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
|
|
|
|
|
// After chat completion hook
|
|
|
|
|
foreach (var hook in contentHooks)
|
|
|
|
|
{
|
|
|
|
|
await hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, "response.done")
|
|
|
|
|
{
|
|
|
|
|
CurrentAgentId = conn.CurrentAgentId
|
|
|
|
|
}, new TokenStatsModel
|
|
|
|
|
{
|
|
|
|
|
Provider = Provider,
|
|
|
|
|
Model = _model,
|
|
|
|
|
CompletionCount = data.Usage.OutputTokens,
|
|
|
|
|
PromptCount = data.Usage.InputTokens
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-09 23:31:46 +00:00
|
|
|
return outputs;
|
|
|
|
|
}
|
2025-02-11 23:27:07 +00:00
|
|
|
|
|
|
|
|
public async Task<RoleDialogModel> OnInputAudioTranscriptionCompleted(RealtimeHubConnection conn, string response)
|
|
|
|
|
{
|
|
|
|
|
var data = JsonSerializer.Deserialize<ResponseAudioTranscript>(response);
|
|
|
|
|
return new RoleDialogModel(AgentRole.User, data.Transcript)
|
|
|
|
|
{
|
2025-02-28 18:44:59 +00:00
|
|
|
CurrentAgentId = conn.CurrentAgentId
|
2025-02-11 23:27:07 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<RoleDialogModel> OnConversationItemCreated(RealtimeHubConnection conn, string response)
|
|
|
|
|
{
|
|
|
|
|
var item = JsonSerializer.Deserialize<ConversationItemCreated>(response).Item;
|
|
|
|
|
var message = new RoleDialogModel(item.Role, item.Content.FirstOrDefault()?.Transcript);
|
|
|
|
|
|
|
|
|
|
return message;
|
|
|
|
|
}
|
2025-02-03 04:02:36 +00:00
|
|
|
}
|