2025-05-14 23:14:01 +00:00
|
|
|
using System.Threading;
|
2025-05-13 16:45:19 +00:00
|
|
|
using BotSharp.Abstraction.Realtime.Models.Session;
|
|
|
|
|
using BotSharp.Core.Session;
|
2025-05-13 20:43:08 +00:00
|
|
|
using BotSharp.Plugin.GoogleAI.Models.Realtime;
|
2025-04-03 23:02:34 +00:00
|
|
|
using GenerativeAI;
|
|
|
|
|
using GenerativeAI.Types;
|
2025-05-13 16:45:19 +00:00
|
|
|
using GenerativeAI.Types.Converters;
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
namespace BotSharp.Plugin.GoogleAi.Providers.Realtime;
|
|
|
|
|
|
|
|
|
|
public class GoogleRealTimeProvider : IRealTimeCompletion
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
public string Provider => "google-ai";
|
|
|
|
|
public string Model => _model;
|
2025-04-25 14:58:04 +00:00
|
|
|
|
|
|
|
|
private string _model = GoogleAIModels.Gemini2FlashExp;
|
2025-05-14 23:14:01 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
private readonly IServiceProvider _services;
|
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
private List<string> renderedInstructions = [];
|
|
|
|
|
|
2025-05-13 16:45:19 +00:00
|
|
|
private LlmRealtimeSession _session;
|
2025-04-05 20:22:06 +00:00
|
|
|
private readonly GoogleAiSettings _settings;
|
|
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
private const string DEFAULT_MIME_TYPE = "audio/pcm;rate=16000";
|
|
|
|
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
|
|
|
|
{
|
|
|
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
|
|
|
PropertyNameCaseInsensitive = true,
|
|
|
|
|
Converters = { new JsonStringEnumConverter(), new DateOnlyJsonConverter(), new TimeOnlyJsonConverter() },
|
|
|
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
|
|
|
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
private RealtimeTranscriptionResponse _inputStream = new();
|
|
|
|
|
private RealtimeTranscriptionResponse _outputStream = new();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private RealtimeHubConnection _conn;
|
|
|
|
|
private Func<Task> _onModelReady;
|
|
|
|
|
private Func<string, string, Task> _onModelAudioDeltaReceived;
|
|
|
|
|
private Func<Task> _onModelAudioResponseDone;
|
|
|
|
|
private Func<string, Task> _onModelAudioTranscriptDone;
|
|
|
|
|
private Func<List<RoleDialogModel>, Task> _onModelResponseDone;
|
|
|
|
|
private Func<string, Task> _onConversationItemCreated;
|
|
|
|
|
private Func<RoleDialogModel, Task> _onInputAudioTranscriptionDone;
|
|
|
|
|
private Func<Task> _onInterruptionDetected;
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public GoogleRealTimeProvider(
|
|
|
|
|
IServiceProvider services,
|
|
|
|
|
GoogleAiSettings settings,
|
|
|
|
|
ILogger<GoogleRealTimeProvider> logger)
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
_settings = settings;
|
|
|
|
|
_services = services;
|
|
|
|
|
_logger = logger;
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public void SetModelName(string model)
|
|
|
|
|
{
|
|
|
|
|
_model = model;
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
public async Task Connect(
|
|
|
|
|
RealtimeHubConnection conn,
|
|
|
|
|
Func<Task> onModelReady,
|
|
|
|
|
Func<string, string, Task> onModelAudioDeltaReceived,
|
|
|
|
|
Func<Task> onModelAudioResponseDone,
|
|
|
|
|
Func<string, Task> onModelAudioTranscriptDone,
|
|
|
|
|
Func<List<RoleDialogModel>, Task> onModelResponseDone,
|
|
|
|
|
Func<string, Task> onConversationItemCreated,
|
|
|
|
|
Func<RoleDialogModel, Task> onInputAudioTranscriptionDone,
|
|
|
|
|
Func<Task> onInterruptionDetected)
|
2025-04-05 20:22:06 +00:00
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
_conn = conn;
|
|
|
|
|
_onModelReady = onModelReady;
|
|
|
|
|
_onModelAudioDeltaReceived = onModelAudioDeltaReceived;
|
|
|
|
|
_onModelAudioResponseDone = onModelAudioResponseDone;
|
|
|
|
|
_onModelAudioTranscriptDone = onModelAudioTranscriptDone;
|
|
|
|
|
_onModelResponseDone = onModelResponseDone;
|
|
|
|
|
_onConversationItemCreated = onConversationItemCreated;
|
|
|
|
|
_onInputAudioTranscriptionDone = onInputAudioTranscriptionDone;
|
|
|
|
|
_onInterruptionDetected = onInterruptionDetected;
|
|
|
|
|
|
2025-05-13 16:45:19 +00:00
|
|
|
var settingsService = _services.GetRequiredService<ILlmProviderService>();
|
2025-04-08 01:16:47 +00:00
|
|
|
var realtimeModelSettings = _services.GetRequiredService<RealtimeModelSettings>();
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-04-08 01:16:47 +00:00
|
|
|
_model = realtimeModelSettings.Model;
|
2025-05-13 16:45:19 +00:00
|
|
|
var modelSettings = settingsService.GetSetting(Provider, _model);
|
|
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
Reset();
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream = new();
|
|
|
|
|
_outputStream = new();
|
2025-05-13 20:43:08 +00:00
|
|
|
_session = new LlmRealtimeSession(_services, new ChatSessionOptions
|
|
|
|
|
{
|
|
|
|
|
JsonOptions = _jsonOptions
|
|
|
|
|
});
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-05-13 22:33:23 +00:00
|
|
|
var uri = BuildWebsocketUri(modelSettings.ApiKey, "v1beta");
|
2025-05-14 23:14:01 +00:00
|
|
|
await _session.ConnectAsync(uri: uri, cancellationToken: CancellationToken.None);
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
await onModelReady();
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
_ = ReceiveMessage();
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
private async Task ReceiveMessage()
|
2025-05-13 16:45:19 +00:00
|
|
|
{
|
|
|
|
|
await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None))
|
|
|
|
|
{
|
|
|
|
|
var receivedText = update?.RawResponse;
|
|
|
|
|
if (string.IsNullOrEmpty(receivedText))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var response = JsonSerializer.Deserialize<RealtimeServerResponse>(receivedText, _jsonOptions);
|
|
|
|
|
if (response == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.SetupComplete != null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Session setup completed.");
|
|
|
|
|
}
|
2025-05-15 04:49:36 +00:00
|
|
|
else if (response.SessionResumptionUpdate != null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Session resumption update => New handle: {response.SessionResumptionUpdate.NewHandle}, Resumable: {response.SessionResumptionUpdate.Resumable}");
|
2025-05-15 20:46:42 +00:00
|
|
|
_conn.PrevSessionId = response.SessionResumptionUpdate?.NewHandle;
|
2025-05-15 04:49:36 +00:00
|
|
|
}
|
2025-05-14 23:14:01 +00:00
|
|
|
else if (response.ToolCall != null && !response.ToolCall.FunctionCalls.IsNullOrEmpty())
|
|
|
|
|
{
|
2025-05-15 04:49:36 +00:00
|
|
|
var functionCall = response.ToolCall.FunctionCalls!.First();
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation($"Tool call received: {functionCall.Name}({functionCall.Args?.ToJsonString(_jsonOptions) ?? string.Empty}).");
|
2025-05-14 23:14:01 +00:00
|
|
|
|
|
|
|
|
if (functionCall != null)
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
var messages = OnFunctionCall(_conn, functionCall);
|
|
|
|
|
await _onModelResponseDone(messages);
|
2025-05-14 23:14:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-13 20:43:08 +00:00
|
|
|
else if (response.ServerContent != null)
|
|
|
|
|
{
|
2025-05-13 22:33:23 +00:00
|
|
|
if (response.ServerContent.InputTranscription?.Text != null)
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream.Collect(response.ServerContent.InputTranscription.Text);
|
2025-05-13 22:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.ServerContent.OutputTranscription?.Text != null)
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
_outputStream.Collect(response.ServerContent.OutputTranscription.Text);
|
2025-05-13 22:33:23 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
if (response.ServerContent.ModelTurn != null)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Model audio delta received.");
|
2025-05-13 22:33:23 +00:00
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
// Handle input transcription
|
2025-05-15 20:46:42 +00:00
|
|
|
var inputTranscription = _inputStream.GetText();
|
2025-05-13 22:33:23 +00:00
|
|
|
if (!string.IsNullOrEmpty(inputTranscription))
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
var message = OnUserAudioTranscriptionCompleted(_conn, inputTranscription);
|
|
|
|
|
await _onInputAudioTranscriptionDone(message);
|
2025-05-13 22:33:23 +00:00
|
|
|
}
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream.Clear();
|
2025-05-13 22:33:23 +00:00
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
var parts = response.ServerContent.ModelTurn.Parts;
|
2025-05-13 20:43:08 +00:00
|
|
|
if (!parts.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
foreach (var part in parts)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(part.InlineData?.Data))
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
await _onModelAudioDeltaReceived(part.InlineData.Data, string.Empty);
|
2025-05-13 20:43:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (response.ServerContent.GenerationComplete == true)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Model generation completed.");
|
|
|
|
|
}
|
|
|
|
|
else if (response.ServerContent.TurnComplete == true)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation($"Model turn completed.");
|
2025-05-13 22:33:23 +00:00
|
|
|
|
2025-05-15 04:49:36 +00:00
|
|
|
// Handle output transcription
|
2025-05-15 20:46:42 +00:00
|
|
|
var outputTranscription = _outputStream.GetText();
|
2025-05-13 22:33:23 +00:00
|
|
|
if (!string.IsNullOrEmpty(outputTranscription))
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
var messages = await OnResponseDone(_conn, outputTranscription, response.UsageMetaData);
|
|
|
|
|
await _onModelResponseDone(messages);
|
2025-05-13 22:33:23 +00:00
|
|
|
}
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream.Clear();
|
|
|
|
|
_outputStream.Clear();
|
2025-05-13 20:43:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2025-05-13 22:33:23 +00:00
|
|
|
_logger.LogError(ex, $"Error when deserializing server response. {ex.Message}");
|
|
|
|
|
break;
|
2025-05-13 20:43:08 +00:00
|
|
|
}
|
2025-05-13 16:45:19 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream.Dispose();
|
|
|
|
|
_outputStream.Dispose();
|
2025-05-13 16:45:19 +00:00
|
|
|
_session.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task Disconnect()
|
|
|
|
|
{
|
2025-05-13 20:43:08 +00:00
|
|
|
if (_session != null)
|
2025-05-13 16:45:19 +00:00
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
_inputStream?.Dispose();
|
|
|
|
|
_outputStream?.Dispose();
|
2025-05-14 23:14:01 +00:00
|
|
|
await _session.DisconnectAsync();
|
2025-05-15 20:46:42 +00:00
|
|
|
_session.Dispose();
|
2025-05-13 16:45:19 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task AppenAudioBuffer(string message)
|
|
|
|
|
{
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
RealtimeInput = new()
|
|
|
|
|
{
|
|
|
|
|
MediaChunks = [new() { Data = message, MimeType = DEFAULT_MIME_TYPE }]
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-07 04:15:27 +00:00
|
|
|
public async Task AppenAudioBuffer(ArraySegment<byte> data, int length)
|
|
|
|
|
{
|
|
|
|
|
var buffer = data.AsSpan(0, length).ToArray();
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
RealtimeInput = new()
|
|
|
|
|
{
|
|
|
|
|
MediaChunks = [new() { Data = Convert.ToBase64String(buffer), MimeType = DEFAULT_MIME_TYPE }]
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-07 04:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task TriggerModelInference(string? instructions = null)
|
|
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
if (string.IsNullOrWhiteSpace(instructions)) return;
|
2025-05-13 20:43:08 +00:00
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
var content = new Content(instructions, AgentRole.User);
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
ClientContent = new()
|
|
|
|
|
{
|
2025-05-15 04:49:36 +00:00
|
|
|
Turns = [content],
|
2025-05-13 20:43:08 +00:00
|
|
|
TurnComplete = true
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task CancelModelResponse()
|
|
|
|
|
{
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task RemoveConversationItem(string itemId)
|
|
|
|
|
{
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
public async Task SendEventToModel(object message)
|
|
|
|
|
{
|
2025-05-13 20:43:08 +00:00
|
|
|
if (_session == null) return;
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
await _session.SendEventToModelAsync(message);
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-28 22:23:25 +00:00
|
|
|
public async Task<string> UpdateSession(RealtimeHubConnection conn, bool isInit = false)
|
2025-04-05 20:22:06 +00:00
|
|
|
{
|
2025-05-15 20:46:42 +00:00
|
|
|
if (!isInit)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var agentService = _services.GetRequiredService<IAgentService>();
|
2025-05-15 04:49:36 +00:00
|
|
|
var realtimeSetting = _services.GetRequiredService<RealtimeModelSettings>();
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var agent = await agentService.LoadAgent(conn.CurrentAgentId);
|
2025-05-13 16:45:19 +00:00
|
|
|
var (prompt, request) = PrepareOptions(agent, []);
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
var config = request.GenerationConfig ?? new();
|
2025-04-05 20:22:06 +00:00
|
|
|
if (config != null)
|
|
|
|
|
{
|
2025-05-13 22:33:23 +00:00
|
|
|
//Output Modality can either be text or audio
|
2025-05-13 16:45:19 +00:00
|
|
|
config.ResponseModalities = [Modality.AUDIO];
|
2025-04-04 01:18:14 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var words = new List<string>();
|
2025-05-15 07:25:08 +00:00
|
|
|
HookEmitter.Emit<IRealtimeHook>(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)), agent.Id);
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-15 04:49:36 +00:00
|
|
|
config.Temperature = Math.Max(realtimeSetting.Temperature, 0.6f);
|
|
|
|
|
config.MaxOutputTokens = realtimeSetting.MaxResponseOutputTokens;
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
|
|
|
|
|
var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x =>
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
var fn = new FunctionDef
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Name = x.Name ?? string.Empty,
|
|
|
|
|
Description = x.Description ?? string.Empty,
|
2025-05-13 16:45:19 +00:00
|
|
|
Parameters = x.Parameters != null
|
|
|
|
|
? JsonSerializer.Deserialize<FunctionParametersDef>(JsonSerializer.Serialize(x.Parameters))
|
|
|
|
|
: null
|
2025-04-05 20:22:06 +00:00
|
|
|
};
|
|
|
|
|
return fn;
|
|
|
|
|
}).ToArray();
|
|
|
|
|
|
|
|
|
|
await HookEmitter.Emit<IContentGeneratingHook>(_services,
|
2025-05-15 07:25:08 +00:00
|
|
|
async hook => { await hook.OnSessionUpdated(agent, prompt, functions, isInit); }, agent.Id);
|
2025-04-05 20:22:06 +00:00
|
|
|
|
|
|
|
|
if (_settings.Gemini.UseGoogleSearch)
|
|
|
|
|
{
|
2025-05-13 16:45:19 +00:00
|
|
|
request.Tools ??= [];
|
2025-04-05 20:22:06 +00:00
|
|
|
request.Tools.Add(new Tool()
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
GoogleSearch = new GoogleSearchTool()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-15 20:46:42 +00:00
|
|
|
|
|
|
|
|
var payload = new RealtimeClientPayload
|
2025-05-13 20:43:08 +00:00
|
|
|
{
|
2025-05-13 22:33:23 +00:00
|
|
|
Setup = new RealtimeGenerateContentSetup()
|
2025-05-13 20:43:08 +00:00
|
|
|
{
|
|
|
|
|
GenerationConfig = config,
|
|
|
|
|
Model = Model.ToModelId(),
|
|
|
|
|
SystemInstruction = request.SystemInstruction,
|
2025-05-14 23:14:01 +00:00
|
|
|
Tools = request.Tools?.ToArray(),
|
|
|
|
|
InputAudioTranscription = realtimeSetting.InputAudioTranscribe ? new() : null,
|
2025-05-15 04:49:36 +00:00
|
|
|
OutputAudioTranscription = realtimeSetting.InputAudioTranscribe ? new() : null,
|
|
|
|
|
SessionResumption = new()
|
2025-05-15 20:46:42 +00:00
|
|
|
{
|
|
|
|
|
Handle = _conn.PrevSessionId
|
|
|
|
|
}
|
2025-05-13 20:43:08 +00:00
|
|
|
}
|
2025-05-15 20:46:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"Setup payload: {JsonSerializer.Serialize(payload, _jsonOptions)}");
|
|
|
|
|
await SendEventToModel(payload);
|
2025-05-13 20:43:08 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
return prompt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task InsertConversationItem(RoleDialogModel message)
|
|
|
|
|
{
|
|
|
|
|
if (message.Role == AgentRole.Function)
|
|
|
|
|
{
|
|
|
|
|
var function = new FunctionResponse()
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-05-15 04:49:36 +00:00
|
|
|
Id = message.ToolCallId,
|
2025-04-05 20:22:06 +00:00
|
|
|
Name = message.FunctionName ?? string.Empty,
|
2025-05-14 23:14:01 +00:00
|
|
|
Response = new JsonObject()
|
|
|
|
|
{
|
|
|
|
|
["result"] = message.Content ?? string.Empty
|
|
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
};
|
|
|
|
|
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
ToolResponse = new()
|
|
|
|
|
{
|
|
|
|
|
FunctionResponses = [function]
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
else if (message.Role == AgentRole.Assistant)
|
|
|
|
|
{
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
ClientContent = new()
|
|
|
|
|
{
|
|
|
|
|
Turns = [new Content(message.Content, AgentRole.Model)],
|
|
|
|
|
TurnComplete = true
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
|
|
|
|
else if (message.Role == AgentRole.User)
|
|
|
|
|
{
|
2025-05-13 20:43:08 +00:00
|
|
|
await SendEventToModel(new BidiClientPayload
|
|
|
|
|
{
|
|
|
|
|
ClientContent = new()
|
|
|
|
|
{
|
|
|
|
|
Turns = [new Content(message.Content, AgentRole.User)],
|
|
|
|
|
TurnComplete = true
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
|
|
|
|
else
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-05-13 22:33:23 +00:00
|
|
|
throw new NotImplementedException($"Unrecognized role {message.Role}.");
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
#region Private methods
|
|
|
|
|
private List<RoleDialogModel> OnFunctionCall(RealtimeHubConnection conn, RealtimeFunctionCall functionCall)
|
2025-04-05 20:22:06 +00:00
|
|
|
{
|
2025-05-14 23:14:01 +00:00
|
|
|
var outputs = new List<RoleDialogModel>
|
|
|
|
|
{
|
|
|
|
|
new(AgentRole.Assistant, string.Empty)
|
|
|
|
|
{
|
|
|
|
|
CurrentAgentId = conn.CurrentAgentId,
|
|
|
|
|
FunctionName = functionCall.Name,
|
|
|
|
|
FunctionArgs = functionCall.Args?.ToJsonString(_jsonOptions),
|
|
|
|
|
ToolCallId = functionCall.Id,
|
|
|
|
|
MessageType = MessageTypeName.FunctionCall
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return outputs;
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
|
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
private async Task<List<RoleDialogModel>> OnResponseDone(RealtimeHubConnection conn, string text, RealtimeUsageMetaData? usage)
|
2025-04-05 20:22:06 +00:00
|
|
|
{
|
2025-05-14 23:14:01 +00:00
|
|
|
var outputs = new List<RoleDialogModel>
|
|
|
|
|
{
|
|
|
|
|
new(AgentRole.Assistant, text)
|
|
|
|
|
{
|
|
|
|
|
CurrentAgentId = conn.CurrentAgentId,
|
|
|
|
|
MessageId = Guid.NewGuid().ToString(),
|
|
|
|
|
MessageType = MessageTypeName.Plain
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (usage != null)
|
|
|
|
|
{
|
|
|
|
|
var contentHooks = _services.GetServices<IContentGeneratingHook>();
|
|
|
|
|
foreach (var hook in contentHooks)
|
|
|
|
|
{
|
|
|
|
|
await hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, text)
|
|
|
|
|
{
|
|
|
|
|
CurrentAgentId = conn.CurrentAgentId
|
|
|
|
|
},
|
|
|
|
|
new TokenStatsModel
|
|
|
|
|
{
|
|
|
|
|
Provider = Provider,
|
|
|
|
|
Model = _model,
|
|
|
|
|
Prompt = text,
|
|
|
|
|
TextInputTokens = usage.PromptTokensDetails?.FirstOrDefault(x => x.Modality == Modality.TEXT.ToString())?.TokenCount ?? 0,
|
|
|
|
|
AudioInputTokens = usage.PromptTokensDetails?.FirstOrDefault(x => x.Modality == Modality.AUDIO.ToString())?.TokenCount ?? 0,
|
|
|
|
|
TextOutputTokens = usage.ResponseTokensDetails?.FirstOrDefault(x => x.Modality == Modality.TEXT.ToString())?.TokenCount ?? 0,
|
|
|
|
|
AudioOutputTokens = usage.ResponseTokensDetails?.FirstOrDefault(x => x.Modality == Modality.AUDIO.ToString())?.TokenCount ?? 0
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return outputs;
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
|
2025-05-13 16:45:19 +00:00
|
|
|
private (string, GenerateContentRequest) PrepareOptions(Agent agent,
|
2025-04-05 20:22:06 +00:00
|
|
|
List<RoleDialogModel> conversations)
|
|
|
|
|
{
|
|
|
|
|
var agentService = _services.GetRequiredService<IAgentService>();
|
|
|
|
|
var googleSettings = _settings;
|
|
|
|
|
renderedInstructions = [];
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
// Assembly messages
|
|
|
|
|
var contents = new List<Content>();
|
|
|
|
|
var tools = new List<Tool>();
|
|
|
|
|
var funcDeclarations = new List<FunctionDeclaration>();
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var systemPrompts = new List<string>();
|
|
|
|
|
if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
var instruction = agentService.RenderedInstruction(agent);
|
|
|
|
|
renderedInstructions.Add(instruction);
|
|
|
|
|
systemPrompts.Add(instruction);
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var funcPrompts = new List<string>();
|
|
|
|
|
var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
|
|
|
|
|
foreach (var function in functions)
|
|
|
|
|
{
|
|
|
|
|
if (!agentService.RenderFunction(agent, function)) continue;
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var def = agentService.RenderFunctionProperty(agent, function);
|
|
|
|
|
var props = JsonSerializer.Serialize(def?.Properties);
|
|
|
|
|
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}"
|
|
|
|
|
? new Schema()
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Type = "object",
|
|
|
|
|
Properties = JsonSerializer.Deserialize<Dictionary<string, Schema>>(props),
|
|
|
|
|
Required = def?.Required ?? []
|
|
|
|
|
}
|
|
|
|
|
: null;
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
funcDeclarations.Add(new FunctionDeclaration
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Name = function.Name,
|
|
|
|
|
Description = function.Description,
|
|
|
|
|
Parameters = parameters
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
funcPrompts.Add($"{function.Name}: {function.Description} {def}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!funcDeclarations.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
tools.Add(new Tool { FunctionDeclarations = funcDeclarations });
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var convPrompts = new List<string>();
|
|
|
|
|
foreach (var message in conversations)
|
|
|
|
|
{
|
|
|
|
|
if (message.Role == AgentRole.Function)
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
contents.Add(new Content([
|
|
|
|
|
new Part()
|
|
|
|
|
{
|
|
|
|
|
FunctionCall = new FunctionCall
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Name = message.FunctionName,
|
|
|
|
|
Args = JsonNode.Parse(message.FunctionArgs ?? "{}")
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
|
|
|
|
], AgentRole.Model));
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
contents.Add(new Content([
|
|
|
|
|
new Part()
|
|
|
|
|
{
|
|
|
|
|
FunctionResponse = new FunctionResponse
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Name = message.FunctionName ?? string.Empty,
|
|
|
|
|
Response = new JsonObject()
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
["result"] = message.Content ?? string.Empty
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
}
|
|
|
|
|
], AgentRole.Function));
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
convPrompts.Add(
|
|
|
|
|
$"{AgentRole.Assistant}: Call function {message.FunctionName}({message.FunctionArgs}) => {message.Content}");
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
else if (message.Role == AgentRole.User)
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
var text = !string.IsNullOrWhiteSpace(message.Payload) ? message.Payload : message.Content;
|
|
|
|
|
contents.Add(new Content(text, AgentRole.User));
|
|
|
|
|
convPrompts.Add($"{AgentRole.User}: {text}");
|
|
|
|
|
}
|
|
|
|
|
else if (message.Role == AgentRole.Assistant)
|
|
|
|
|
{
|
|
|
|
|
contents.Add(new Content(message.Content, AgentRole.Model));
|
|
|
|
|
convPrompts.Add($"{AgentRole.Assistant}: {message.Content}");
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
|
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var state = _services.GetRequiredService<IConversationStateService>();
|
|
|
|
|
var temperature = float.Parse(state.GetState("temperature", "0.0"));
|
|
|
|
|
var maxTokens = int.TryParse(state.GetState("max_tokens"), out var tokens)
|
|
|
|
|
? tokens
|
|
|
|
|
: agent.LlmConfig?.MaxOutputTokens ?? LlmConstant.DEFAULT_MAX_OUTPUT_TOKEN;
|
2025-05-13 16:45:19 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var request = new GenerateContentRequest
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
SystemInstruction = !systemPrompts.IsNullOrEmpty()
|
|
|
|
|
? new Content(systemPrompts[0], AgentRole.System)
|
|
|
|
|
: null,
|
|
|
|
|
Contents = contents,
|
|
|
|
|
Tools = tools,
|
|
|
|
|
GenerationConfig = new()
|
2025-04-03 23:02:34 +00:00
|
|
|
{
|
2025-04-05 20:22:06 +00:00
|
|
|
Temperature = temperature,
|
|
|
|
|
MaxOutputTokens = maxTokens
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
};
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
var prompt = GetPrompt(systemPrompts, funcPrompts, convPrompts);
|
|
|
|
|
return (prompt, request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetPrompt(IEnumerable<string> systemPrompts, IEnumerable<string> funcPrompts,
|
|
|
|
|
IEnumerable<string> convPrompts)
|
|
|
|
|
{
|
|
|
|
|
string prompt = string.Join("\r\n\r\n", systemPrompts);
|
|
|
|
|
|
|
|
|
|
if (!funcPrompts.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
prompt += "\r\n\r\n[FUNCTIONS]\r\n";
|
|
|
|
|
prompt += string.Join("\r\n", funcPrompts);
|
|
|
|
|
}
|
2025-04-03 23:02:34 +00:00
|
|
|
|
2025-04-05 20:22:06 +00:00
|
|
|
if (!convPrompts.IsNullOrEmpty())
|
|
|
|
|
{
|
|
|
|
|
prompt += "\r\n\r\n[CONVERSATION]\r\n";
|
|
|
|
|
prompt += string.Join("\r\n", convPrompts);
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-04-05 20:22:06 +00:00
|
|
|
|
|
|
|
|
return prompt;
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|
2025-05-13 22:33:23 +00:00
|
|
|
|
|
|
|
|
|
2025-05-14 23:14:01 +00:00
|
|
|
private RoleDialogModel OnUserAudioTranscriptionCompleted(RealtimeHubConnection conn, string text)
|
2025-05-13 22:33:23 +00:00
|
|
|
{
|
|
|
|
|
return new RoleDialogModel(AgentRole.User, text)
|
|
|
|
|
{
|
|
|
|
|
CurrentAgentId = conn.CurrentAgentId
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Uri BuildWebsocketUri(string apiKey, string version = "v1alpha")
|
|
|
|
|
{
|
|
|
|
|
return new Uri($"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.{version}.GenerativeService.BidiGenerateContent?key={apiKey}");
|
|
|
|
|
}
|
2025-05-15 20:46:42 +00:00
|
|
|
|
|
|
|
|
private void Reset()
|
|
|
|
|
{
|
|
|
|
|
_inputStream?.Clear();
|
|
|
|
|
_outputStream?.Clear();
|
|
|
|
|
|
|
|
|
|
_inputStream?.Dispose();
|
|
|
|
|
_outputStream?.Dispose();
|
|
|
|
|
_session?.Dispose();
|
|
|
|
|
}
|
2025-05-14 23:14:01 +00:00
|
|
|
#endregion
|
2025-04-03 23:02:34 +00:00
|
|
|
}
|