BotSharp/src/Plugins/BotSharp.Plugin.GoogleAI/Providers/Chat/GeminiChatCompletionProvider.cs

387 lines
14 KiB
C#
Raw Normal View History

2025-05-26 05:23:31 +00:00
using BotSharp.Abstraction.Files;
2025-09-12 22:17:03 +00:00
using BotSharp.Abstraction.Files.Models;
2025-05-26 05:23:31 +00:00
using BotSharp.Abstraction.Files.Utilities;
2025-05-16 01:19:55 +00:00
using BotSharp.Abstraction.Hooks;
using GenerativeAI;
using GenerativeAI.Core;
using GenerativeAI.Types;
2025-09-12 22:17:03 +00:00
using Google.Ai.Generativelanguage.V1Beta2;
2024-12-23 02:12:35 +00:00
namespace BotSharp.Plugin.GoogleAi.Providers.Chat;
public class GeminiChatCompletionProvider : IChatCompletion
{
private readonly IServiceProvider _services;
private readonly ILogger<GeminiChatCompletionProvider> _logger;
2025-03-05 23:22:46 +00:00
private List<string> renderedInstructions = [];
2024-12-23 02:12:35 +00:00
private string _model;
2024-12-26 15:55:59 +00:00
public string Provider => "google-ai";
2025-03-05 23:22:46 +00:00
public string Model => _model;
2024-12-23 02:12:35 +00:00
private GoogleAiSettings _settings;
2024-12-23 02:12:35 +00:00
public GeminiChatCompletionProvider(
IServiceProvider services,
GoogleAiSettings googleSettings,
2024-12-23 02:12:35 +00:00
ILogger<GeminiChatCompletionProvider> logger)
{
_settings = googleSettings;
2024-12-23 02:12:35 +00:00
_services = services;
_logger = logger;
}
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
{
2025-05-16 01:19:55 +00:00
var contentHooks = _services.GetHooks<IContentGeneratingHook>(agent.Id);
2024-12-23 02:12:35 +00:00
// Before chat completion hook
foreach (var hook in contentHooks)
{
await hook.BeforeGenerating(agent, conversations);
}
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
2025-05-26 05:23:31 +00:00
var aiModel = client.CreateGenerativeModel(_model.ToModelId());
2024-12-23 02:12:35 +00:00
var (prompt, request) = PrepareOptions(aiModel, agent, conversations);
var response = await aiModel.GenerateContentAsync(request);
var candidate = response.Candidates?.First();
var part = candidate?.Content?.Parts?.FirstOrDefault();
2024-12-23 02:12:35 +00:00
var text = part?.Text ?? string.Empty;
RoleDialogModel responseMessage;
2025-04-03 01:47:32 +00:00
if (response.GetFunction() != null)
2024-12-23 02:12:35 +00:00
{
responseMessage = new RoleDialogModel(AgentRole.Function, text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
ToolCallId = part.FunctionCall.Name,
FunctionName = part.FunctionCall.Name,
FunctionArgs = part.FunctionCall.Args?.ToJsonString(),
2025-03-05 23:22:46 +00:00
RenderedInstruction = string.Join("\r\n", renderedInstructions)
2024-12-23 02:12:35 +00:00
};
}
else
{
responseMessage = new RoleDialogModel(AgentRole.Assistant, text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
2025-03-05 23:22:46 +00:00
RenderedInstruction = string.Join("\r\n", renderedInstructions)
2024-12-23 02:12:35 +00:00
};
}
// After chat completion hook
foreach (var hook in contentHooks)
{
await hook.AfterGenerated(responseMessage, new TokenStatsModel
{
Prompt = prompt,
Provider = Provider,
2025-04-03 01:47:32 +00:00
Model = _model,
2025-04-21 16:19:56 +00:00
TextInputTokens = response?.UsageMetadata?.PromptTokenCount ?? 0,
TextOutputTokens = response?.UsageMetadata?.CandidatesTokenCount ?? 0
2024-12-23 02:12:35 +00:00
});
}
return responseMessage;
}
public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting)
2024-12-23 02:12:35 +00:00
{
2025-05-16 01:19:55 +00:00
var hooks = _services.GetHooks<IContentGeneratingHook>(agent.Id);
// Before chat completion hook
foreach (var hook in hooks)
{
await hook.BeforeGenerating(agent, conversations);
}
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
2025-05-26 05:28:50 +00:00
var chatClient = client.CreateGenerativeModel(_model.ToModelId());
2025-04-03 01:47:32 +00:00
var (prompt, messages) = PrepareOptions(chatClient, agent, conversations);
var response = await chatClient.GenerateContentAsync(messages);
var candidate = response.Candidates?.First();
var part = candidate?.Content?.Parts?.FirstOrDefault();
var text = part?.Text ?? string.Empty;
var msg = new RoleDialogModel(AgentRole.Assistant, text)
{
CurrentAgentId = agent.Id,
RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
// After chat completion hook
foreach (var hook in hooks)
{
await hook.AfterGenerated(msg, new TokenStatsModel
{
Prompt = prompt,
Provider = Provider,
Model = _model,
2025-04-21 16:19:56 +00:00
TextInputTokens = response?.UsageMetadata?.PromptTokenCount ?? 0,
TextOutputTokens = response?.UsageMetadata?.CandidatesTokenCount ?? 0
});
}
2025-04-03 01:47:32 +00:00
if (response.GetFunction() != null)
{
var toolCall = response.GetFunction();
_logger.LogInformation($"[{agent.Name}]: {toolCall?.Name}({toolCall?.Args?.ToJsonString()})");
var funcContextIn = new RoleDialogModel(AgentRole.Function, text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
ToolCallId = toolCall?.Id,
FunctionName = toolCall?.Name,
FunctionArgs = toolCall?.Args?.ToJsonString(),
RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
// Somethings LLM will generate a function name with agent name.
if (!string.IsNullOrEmpty(funcContextIn.FunctionName))
{
funcContextIn.FunctionName = funcContextIn.FunctionName.Split('.').Last();
}
// Execute functions
await onFunctionExecuting(funcContextIn);
}
else
{
// Text response received
await onMessageReceived(msg);
}
return true;
2024-12-23 02:12:35 +00:00
}
2025-06-27 18:49:44 +00:00
public Task<RoleDialogModel> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations)
2024-12-23 02:12:35 +00:00
{
2025-06-27 18:49:44 +00:00
throw new NotImplementedException();
2024-12-23 02:12:35 +00:00
}
public void SetModelName(string model)
{
_model = model;
}
private (string, GenerateContentRequest) PrepareOptions(GenerativeModel aiModel, Agent agent, List<RoleDialogModel> conversations)
{
var agentService = _services.GetRequiredService<IAgentService>();
var googleSettings = _services.GetRequiredService<GoogleAiSettings>();
2025-05-26 05:23:31 +00:00
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
var allowMultiModal = settings != null && settings.MultiModal;
2025-03-05 23:22:46 +00:00
renderedInstructions = [];
2024-12-23 02:12:35 +00:00
// Add settings
aiModel.UseGoogleSearch = googleSettings.Gemini.UseGoogleSearch;
aiModel.UseGrounding = googleSettings.Gemini.UseGrounding;
aiModel.FunctionCallingBehaviour = new FunctionCallingBehaviour()
{
AutoCallFunction = false
};
2025-04-03 01:47:32 +00:00
2025-08-14 15:47:07 +00:00
// Assemble messages
2024-12-23 02:12:35 +00:00
var contents = new List<Content>();
var tools = new List<Tool>();
var funcDeclarations = new List<FunctionDeclaration>();
2024-12-23 05:04:09 +00:00
var systemPrompts = new List<string>();
2025-08-14 15:47:07 +00:00
var funcPrompts = new List<string>();
// Prepare instruction and functions
2025-09-25 20:31:54 +00:00
var renderData = agentService.CollectRenderData(agent);
var (instruction, functions) = agentService.PrepareInstructionAndFunctions(agent, renderData);
2025-08-14 15:47:07 +00:00
if (!string.IsNullOrWhiteSpace(instruction))
2024-12-23 02:12:35 +00:00
{
2025-03-05 23:22:46 +00:00
renderedInstructions.Add(instruction);
2024-12-23 05:04:09 +00:00
systemPrompts.Add(instruction);
2024-12-23 02:12:35 +00:00
}
2024-12-26 06:18:02 +00:00
foreach (var function in functions)
2024-12-23 02:12:35 +00:00
{
2025-09-25 20:31:54 +00:00
if (!agentService.RenderFunction(agent, function, renderData))
{
continue;
}
2024-12-23 02:12:35 +00:00
2025-09-25 20:31:54 +00:00
var def = agentService.RenderFunctionProperty(agent, function, renderData);
2025-01-15 21:46:03 +00:00
var props = JsonSerializer.Serialize(def?.Properties);
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}" ? new Schema()
{
Type = "object",
2025-04-03 01:47:32 +00:00
Properties = JsonSerializer.Deserialize<Dictionary<string, Schema>>(props),
2025-01-15 21:46:03 +00:00
Required = def?.Required ?? []
} : null;
2024-12-23 02:12:35 +00:00
funcDeclarations.Add(new FunctionDeclaration
{
Name = function.Name,
Description = function.Description,
2025-01-15 21:46:03 +00:00
Parameters = parameters
2024-12-23 02:12:35 +00:00
});
2024-12-23 05:04:09 +00:00
funcPrompts.Add($"{function.Name}: {function.Description} {def}");
2024-12-23 02:12:35 +00:00
}
if (!funcDeclarations.IsNullOrEmpty())
{
tools.Add(new Tool { FunctionDeclarations = funcDeclarations });
}
2024-12-23 05:04:09 +00:00
var convPrompts = new List<string>();
2024-12-23 02:12:35 +00:00
foreach (var message in conversations)
{
if (message.Role == AgentRole.Function)
{
2025-04-03 01:47:32 +00:00
contents.Add(new Content([
new Part()
{
FunctionCall = new FunctionCall
2024-12-23 02:12:35 +00:00
{
2025-05-16 15:49:09 +00:00
Id = message.ToolCallId,
2025-04-03 01:47:32 +00:00
Name = message.FunctionName,
Args = JsonNode.Parse(message.FunctionArgs ?? "{}")
}
}
], AgentRole.Model));
contents.Add(new Content([
new Part()
{
FunctionResponse = new FunctionResponse
{
2025-05-16 15:49:09 +00:00
Id = message.ToolCallId,
2025-04-03 01:47:32 +00:00
Name = message.FunctionName,
Response = new JsonObject()
{
2025-09-30 16:16:53 +00:00
["result"] = message.RoleContent ?? string.Empty
}
2024-12-23 02:12:35 +00:00
}
2025-04-03 01:47:32 +00:00
}
], AgentRole.Function));
2024-12-23 02:12:35 +00:00
2025-09-30 16:16:53 +00:00
convPrompts.Add($"{AgentRole.Assistant}: Call function {message.FunctionName}({message.FunctionArgs}) => {message.RoleContent}");
2024-12-23 02:12:35 +00:00
}
else if (message.Role == AgentRole.User)
{
2025-09-30 16:16:53 +00:00
var text = message.RoleContent;
2025-05-26 05:23:31 +00:00
var contentParts = new List<Part> { new() { Text = text } };
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
2025-09-12 22:17:03 +00:00
CollectMessageContentParts(contentParts, message.Files);
2025-05-26 05:23:31 +00:00
}
contents.Add(new Content(contentParts, AgentRole.User));
2024-12-23 05:04:09 +00:00
convPrompts.Add($"{AgentRole.User}: {text}");
2024-12-23 02:12:35 +00:00
}
else if (message.Role == AgentRole.Assistant)
{
2025-09-30 16:16:53 +00:00
var text = message.RoleContent;
2025-09-12 22:17:03 +00:00
var contentParts = new List<Part> { new() { Text = text } };
if (allowMultiModal && !message.Files.IsNullOrEmpty())
{
CollectMessageContentParts(contentParts, message.Files);
}
contents.Add(new Content(contentParts, AgentRole.Model));
2025-09-30 16:16:53 +00:00
convPrompts.Add($"{AgentRole.Assistant}: {text}");
2024-12-23 02:12:35 +00:00
}
}
2025-02-05 23:50:32 +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;
2024-12-23 02:12:35 +00:00
var request = new GenerateContentRequest
{
2025-04-03 01:47:32 +00:00
SystemInstruction = !systemPrompts.IsNullOrEmpty() ? new Content(systemPrompts[0], AgentRole.System) : null,
2024-12-23 02:12:35 +00:00
Contents = contents,
2025-02-05 23:50:32 +00:00
Tools = tools,
GenerationConfig = new()
{
Temperature = temperature,
MaxOutputTokens = maxTokens
}
2024-12-23 02:12:35 +00:00
};
2024-12-23 05:04:09 +00:00
var prompt = GetPrompt(systemPrompts, funcPrompts, convPrompts);
2024-12-23 02:12:35 +00:00
return (prompt, request);
}
2024-12-23 05:04:09 +00:00
2025-09-12 22:17:03 +00:00
private void CollectMessageContentParts(List<Part> contentParts, List<BotSharpFile> files)
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();
foreach (var file in files)
{
if (!string.IsNullOrEmpty(file.FileData))
{
var (contentType, binary) = FileUtility.GetFileInfoFromData(file.FileData);
contentParts.Add(new Part()
{
InlineData = new()
{
MimeType = contentType.IfNullOrEmptyAs(file.ContentType),
Data = Convert.ToBase64String(binary.ToArray())
}
});
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var binary = fileStorage.GetFileBytes(file.FileStorageUrl);
contentParts.Add(new Part()
{
InlineData = new()
{
MimeType = contentType.IfNullOrEmptyAs(file.ContentType),
Data = Convert.ToBase64String(binary.ToArray())
}
});
}
else if (!string.IsNullOrEmpty(file.FileUrl))
{
contentParts.Add(new Part()
{
FileData = new()
{
FileUri = file.FileUrl
}
});
}
}
}
2024-12-23 05:04:09 +00:00
private string GetPrompt(IEnumerable<string> systemPrompts, IEnumerable<string> funcPrompts, IEnumerable<string> convPrompts)
{
var prompt = string.Empty;
prompt = string.Join("\r\n\r\n", systemPrompts);
if (!funcPrompts.IsNullOrEmpty())
{
2024-12-23 05:06:52 +00:00
prompt += "\r\n\r\n[FUNCTIONS]\r\n";
2024-12-23 05:04:09 +00:00
prompt += string.Join("\r\n", funcPrompts);
}
if (!convPrompts.IsNullOrEmpty())
{
2024-12-23 05:06:52 +00:00
prompt += "\r\n\r\n[CONVERSATION]\r\n";
2024-12-23 05:04:09 +00:00
prompt += string.Join("\r\n", convPrompts);
}
return prompt;
}
2024-12-23 02:12:35 +00:00
}