feat: Added Google Live APIs

added Unit Test cases for LLM
This commit is contained in:
Gunpal Jain 2025-04-04 04:32:34 +05:30
parent 8ec7ecdf55
commit 8039f7e702
27 changed files with 1961 additions and 13 deletions

View file

@ -141,6 +141,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Abstraction.Comput
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Test.BrowserUse", "tests\BotSharp.Test.BrowserUse\BotSharp.Test.BrowserUse.csproj", "{7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.LLM.Tests", "tests\BotSharp.LLM.Tests\BotSharp.LLM.Tests.csproj", "{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -589,6 +591,14 @@ Global
{7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|Any CPU.Build.0 = Release|Any CPU
{7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.ActiveCfg = Release|Any CPU
{7D0DB012-9798-4BB9-B15B-A5B0B7B3B094}.Release|x64.Build.0 = Release|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.ActiveCfg = Debug|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Debug|x64.Build.0 = Debug|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|Any CPU.Build.0 = Release|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.ActiveCfg = Release|Any CPU
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -657,6 +667,7 @@ Global
{B268E2F0-060F-8466-7D81-ABA4D735CA59} = {51AFE054-AE99-497D-A593-69BAEFB5106F}
{970BE341-9AC8-99A5-6572-E703C1E02FCB} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{7D0DB012-9798-4BB9-B15B-A5B0B7B3B094} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{7C0C7D13-D161-4AB0-9C29-83A0F1FF990E} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -6,7 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="EntityFramework" Version="6.4.4" />
<PackageVersion Include="Google_GenerativeAI" Version="2.4.6" />
<PackageVersion Include="Google_GenerativeAI" Version="2.5.3" />
<PackageVersion Include="Google_GenerativeAI.Live" Version="2.5.3" />
<PackageVersion Include="LLMSharp.Google.Palm" Version="1.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
@ -112,6 +113,7 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.2" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="BotSharp.Core" Version="$(BotSharpVersion)" />

View file

@ -238,6 +238,9 @@ public class ChatCompletionProvider : IChatCompletion
private string GetPrompt(MessageParameters parameters)
{
//parameters.System can be null?
if(parameters.System == null)
parameters.System = new List<SystemMessage>();
var prompt = $"{string.Join("\r\n", parameters.System.Select(x => x.Text))}\r\n";
prompt += "\r\n[CONVERSATION]";

View file

@ -12,10 +12,12 @@
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google_GenerativeAI" />
<PackageReference Include="Google_GenerativeAI.Live" />
<PackageReference Include="LLMSharp.Google.Palm" />
</ItemGroup>

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Plugins;
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.GoogleAi.Providers.Chat;
using BotSharp.Plugin.GoogleAI.Providers.Embedding;
using BotSharp.Plugin.GoogleAi.Providers.Realtime;
using BotSharp.Plugin.GoogleAi.Providers.Text;
namespace BotSharp.Plugin.GoogleAi;
@ -24,6 +25,7 @@ public class GoogleAiPlugin : IBotSharpPlugin
services.AddScoped<ITextCompletion, GeminiTextCompletionProvider>();
services.AddScoped<IChatCompletion, PalmChatCompletionProvider>();
services.AddScoped<IChatCompletion, GeminiChatCompletionProvider>();
services.AddScoped<IRealTimeCompletion, GoogleRealTimeProvider>();
services.AddScoped<ITextEmbedding, TextEmbeddingProvider>();
}
}

View file

@ -21,10 +21,13 @@ public class GeminiChatCompletionProvider : IChatCompletion
public string Provider => "google-ai";
public string Model => _model;
private GoogleAiSettings _googleSettings;
public GeminiChatCompletionProvider(
IServiceProvider services,
GoogleAiSettings googleSettings,
ILogger<GeminiChatCompletionProvider> logger)
{
_googleSettings = googleSettings;
_services = services;
_logger = logger;
}
@ -39,7 +42,7 @@ public class GeminiChatCompletionProvider : IChatCompletion
await hook.BeforeGenerating(agent, conversations);
}
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var aiModel = client.CreateGenerativeModel(_model);
var (prompt, request) = PrepareOptions(aiModel, agent, conversations);
@ -98,7 +101,7 @@ public class GeminiChatCompletionProvider : IChatCompletion
await hook.BeforeGenerating(agent, conversations);
}
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var chatClient = client.CreateGenerativeModel(_model);
var (prompt, messages) = PrepareOptions(chatClient, agent, conversations);
@ -163,7 +166,7 @@ public class GeminiChatCompletionProvider : IChatCompletion
public async Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var chatClient = client.CreateGenerativeModel(_model);
var (prompt, messages) = PrepareOptions(chatClient,agent, conversations);

View file

@ -30,7 +30,7 @@ public class TextEmbeddingProvider : ITextEmbedding
public async Task<float[]> GetVectorAsync(string text)
{
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _settings, _logger);
var embeddingClient = client.CreateEmbeddingModel(_model);
var response = await embeddingClient.EmbedContentAsync(text);
@ -40,7 +40,7 @@ public class TextEmbeddingProvider : ITextEmbedding
public async Task<List<float[]>> GetVectorsAsync(List<string> texts)
{
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _settings, _logger);
var embeddingClient = client.CreateEmbeddingModel(_model);
var response = await embeddingClient.BatchEmbedContentAsync(texts.Select(s=>new Content(s, Roles.User)));

View file

@ -1,15 +1,24 @@
using LLMSharp.Google.Palm;
using Microsoft.Extensions.Logging;
namespace BotSharp.Plugin.GoogleAi.Providers;
public static class ProviderHelper
{
public static GenerativeAI.GoogleAi GetGeminiClient(string provider, string model, IServiceProvider services)
public static GenerativeAI.GoogleAi GetGeminiClient(string provider, string model, IServiceProvider services, GoogleAiSettings? aiSettings, ILogger? _logger)
{
var settingsService = services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(provider, model);
var client = new GenerativeAI.GoogleAi(settings.ApiKey);
return client;
if (aiSettings == null || aiSettings.Gemini ==null || string.IsNullOrEmpty(aiSettings.Gemini.ApiKey))
{
var settingsService = services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(provider, model);
var client = new GenerativeAI.GoogleAi(settings.ApiKey, logger:_logger);
return client;
}
else
{
return new GenerativeAI.GoogleAi(aiSettings.Gemini.ApiKey, logger:_logger);
}
}
public static GooglePalmClient GetPalmClient(string provider, string model, IServiceProvider services)

View file

@ -0,0 +1,30 @@
using System.Threading;
using GenerativeAI.Core;
using GenerativeAI.Types;
namespace BotSharp.Plugin.GoogleAi.Providers.Realtime
{
public class FakeFunctionTool:IFunctionTool
{
public Tool Tool { get; set; }
public FakeFunctionTool(Tool tool)
{
this.Tool = tool;
}
public Tool AsTool()
{
return Tool;
}
public async Task<FunctionResponse?> CallAsync(FunctionCall functionCall, CancellationToken cancellationToken = new CancellationToken())
{
return null;
}
public bool IsContainFunction(string name)
{
return false;
}
}
}

View file

@ -0,0 +1,508 @@
using System.Net.WebSockets;
using System.Text.Json.Nodes;
using System.Threading;
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Realtime;
using BotSharp.Abstraction.Realtime.Models;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.GoogleAi.Providers.Chat;
using GenerativeAI;
using GenerativeAI.Core;
using GenerativeAI.Live;
using GenerativeAI.Live.Extensions;
using GenerativeAI.Types;
using Microsoft.Extensions.Logging;
namespace BotSharp.Plugin.GoogleAi.Providers.Realtime
{
public class GoogleRealTimeProvider : IRealTimeCompletion
{
public string Provider => "google-ai";
private string _model = GoogleAIModels.Gemini2FlashExp;
public string Model { get; }
private MultiModalLiveClient? _client;
private readonly IServiceProvider _services;
private readonly ILogger<GeminiChatCompletionProvider> _logger;
private List<string> renderedInstructions = [];
private readonly GoogleAiSettings _googleSettings;
public GoogleRealTimeProvider(
IServiceProvider services,
GoogleAiSettings googleSettings,
ILogger<GeminiChatCompletionProvider> logger)
{
_googleSettings = googleSettings;
_services = services;
_logger = logger;
}
public void SetModelName(string model)
{
_model = model;
}
private Action onModelReady;
Action<string, string> onModelAudioDeltaReceived;
private Action onModelAudioResponseDone;
Action<string> onModelAudioTranscriptDone;
private Action<List<RoleDialogModel>> onModelResponseDone;
Action<string> onConversationItemCreated;
private Action<RoleDialogModel> onInputAudioTranscriptionCompleted;
Action onUserInterrupted;
RealtimeHubConnection conn;
public async Task Connect(RealtimeHubConnection conn,
Action onModelReady,
Action<string, string> onModelAudioDeltaReceived,
Action onModelAudioResponseDone,
Action<string> onModelAudioTranscriptDone,
Action<List<RoleDialogModel>> onModelResponseDone,
Action<string> onConversationItemCreated,
Action<RoleDialogModel> onInputAudioTranscriptionCompleted,
Action onUserInterrupted)
{
this.conn = conn;
this.onModelReady = onModelReady;
this.onModelAudioDeltaReceived = onModelAudioDeltaReceived;
this.onModelAudioResponseDone = onModelAudioResponseDone;
this.onModelAudioTranscriptDone = onModelAudioTranscriptDone;
this.onModelResponseDone = onModelResponseDone;
this.onConversationItemCreated = onConversationItemCreated;
this.onInputAudioTranscriptionCompleted = onInputAudioTranscriptionCompleted;
this.onUserInterrupted = onUserInterrupted;
}
public async Task Disconnect()
{
if (_client != null)
await _client.DisconnectAsync();
}
public async Task AppenAudioBuffer(string message)
{
var audioAppend = new
{
type = "input_audio_buffer.append",
audio = message
};
await SendEventToModel(audioAppend);
}
public async Task TriggerModelInference(string? instructions = null)
{
}
public async Task CancelModelResponse()
{
}
public async Task RemoveConversationItem(string itemId)
{
}
private async Task AttachEvents()
{
_client.MessageReceived += async (sender, e) =>
{
if (e.Payload.SetupComplete != null)
{
onModelReady();
onConversationItemCreated(_client.ConnectionId.ToString());
}
if (e.Payload.ServerContent != null)
{
if (e.Payload.ServerContent.TurnComplete == true)
{
var responseDone = await ResponseDone(conn,e.Payload.ServerContent);
onModelResponseDone(responseDone);
}
}
};
_client.AudioChunkReceived += async (sender, e) =>
{
onModelAudioDeltaReceived(Convert.ToBase64String(e.Buffer), Guid.NewGuid().ToString());
};
_client.TextChunkReceived += async (sender, e) =>
{
onInputAudioTranscriptionCompleted(new RoleDialogModel(AgentRole.Assistant, e.Text));
};
_client.GenerationInterrupted += async (sender, e) => { onUserInterrupted(); };
_client.AudioReceiveCompleted += async (sender, e) =>
{
onModelAudioResponseDone();
};
}
private async Task<List<RoleDialogModel>> ResponseDone(RealtimeHubConnection conn, BidiGenerateContentServerContent serverContent)
{
var outputs = new List<RoleDialogModel>();
var parts = serverContent.ModelTurn?.Parts;
if (parts != null)
{
foreach (var part in parts)
{
var call = part.FunctionCall;
if (call != null)
{
var item = new RoleDialogModel(AgentRole.Assistant, part.Text)
{
CurrentAgentId = conn.CurrentAgentId,
MessageId = call.Id ?? String.Empty,
MessageType = MessageTypeName.FunctionCall
};
outputs.Add(item);
}
else
{
var item = new RoleDialogModel(AgentRole.Assistant, call.Args?.ToJsonString() ?? string.Empty)
{
CurrentAgentId = conn.CurrentAgentId,
FunctionName = call.Name,
FunctionArgs = call.Args?.ToJsonString() ?? string.Empty,
ToolCallId = call.Id ?? String.Empty,
MessageId = call.Id ?? String.Empty,
MessageType = MessageTypeName.FunctionCall
};
outputs.Add(item);
}
}
}
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,
});
}
return outputs;
}
public async Task SendEventToModel(object message)
{
//todo Send Audio Chunks to Model, Botsharp RealTime Implementation seems to be incomplete
}
public async Task<RealtimeSession> CreateSession(Agent agent, List<RoleDialogModel> conversations)
{
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var chatClient = client.CreateGenerativeModel(_model);
var (prompt, request) = PrepareOptions(chatClient, agent, conversations);
var config = request.GenerationConfig;
//Output Modality can either be text or audio
config.ResponseModalities = new List<Modality>([Modality.AUDIO]);
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting(Provider, _model);
_client = chatClient.CreateMultiModalLiveClient(config,
systemInstruction: request.SystemInstruction?.Parts.FirstOrDefault()?.Text);
_client.UseGoogleSearch = _googleSettings.Gemini.UseGoogleSearch;
if (request.Tools != null && request.Tools.Count > 0)
{
var lst = (request.Tools.Select(s => (IFunctionTool)new FakeFunctionTool(s)).ToList());
_client.AddFunctionTools(lst, new ToolConfig()
{
FunctionCallingConfig = new FunctionCallingConfig()
{
Mode = FunctionCallingMode.AUTO
}
});
}
await AttachEvents();
await _client.ConnectAsync();
_client.FunctionTools?.Clear();
return new RealtimeSession()
{
Id = _client.ConnectionId.ToString(),
Model = _model,
Voice = "default"
};
}
public async Task<string> UpdateSession(RealtimeHubConnection conn, bool interruptResponse = true)
{
var convService = _services.GetRequiredService<IConversationService>();
var conv = await convService.GetConversation(conn.ConversationId);
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(conn.CurrentAgentId);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var chatClient = client.CreateGenerativeModel(_model);
var (prompt, request) = PrepareOptions(chatClient, agent, new List<RoleDialogModel>());
var config = request.GenerationConfig;
//Output Modality can either be text or audio
config.ResponseModalities = new List<Modality>([Modality.AUDIO]);
var words = new List<string>();
HookEmitter.Emit<IRealtimeHook>(_services, hook => words.AddRange(hook.OnModelTranscriptPrompt(agent)));
var realtimeModelSettings = _services.GetRequiredService<RealtimeModelSettings>();
config.Temperature = Math.Max(realtimeModelSettings.Temperature, 0.6f);
config.MaxOutputTokens = realtimeModelSettings.MaxResponseOutputTokens;
var functions = request.Tools?.SelectMany(s => s.FunctionDeclarations).Select(x =>
{
var fn = new FunctionDef
{
Name = x.Name ?? string.Empty,
Description = x.Description
};
fn.Parameters = x.Parameters != null
? JsonSerializer.Deserialize<FunctionParametersDef>(JsonSerializer.Serialize(x.Parameters))
: null;
return fn;
}).ToArray();
await HookEmitter.Emit<IContentGeneratingHook>(_services,
async hook => { await hook.OnSessionUpdated(agent, prompt, functions); });
//ToDo: Not sure what's the purpose of UpdateSession, Google Realtime conversion works right after sending the message away!
// await _client.SendSetupAsync(new BidiGenerateContentSetup()
// {
// GenerationConfig = config,
// Model = Model,
// SystemInstruction = request.SystemInstruction,
// Tools = request.Tools?.ToArray(),
// });
return prompt;
}
public async Task InsertConversationItem(RoleDialogModel message)
{
if (message.Role == AgentRole.Function)
{
var function = new FunctionResponse()
{
Name = message.FunctionName,
Response = JsonNode.Parse(message.Content ?? "{}")
};
await _client.SendToolResponseAsync(new BidiGenerateContentToolResponse()
{
FunctionResponses = [function]
});
}
else if (message.Role == AgentRole.Assistant)
{
}
else if (message.Role == AgentRole.User)
{
await _client.SendClientContentAsync(new BidiGenerateContentClientContent()
{
TurnComplete = true,
Turns = new []{new Content(message.Content, AgentRole.User)}
});
}
else
{
throw new NotImplementedException("");
}
}
public async Task<List<RoleDialogModel>> OnResponsedDone(RealtimeHubConnection conn, string response)
{
throw new NotImplementedException("");
}
public async Task<RoleDialogModel> OnConversationItemCreated(RealtimeHubConnection conn, string response)
{
return new RoleDialogModel(AgentRole.User, response);
}
private (string, GenerateContentRequest) PrepareOptions(GenerativeModel aiModel, Agent agent,
List<RoleDialogModel> conversations)
{
var agentService = _services.GetRequiredService<IAgentService>();
var googleSettings = _googleSettings;
renderedInstructions = [];
// Add settings
aiModel.UseGoogleSearch = googleSettings.Gemini.UseGoogleSearch;
aiModel.UseGrounding = googleSettings.Gemini.UseGrounding;
aiModel.FunctionCallingBehaviour = new FunctionCallingBehaviour()
{
AutoCallFunction = false
};
// Assembly messages
var contents = new List<Content>();
var tools = new List<Tool>();
var funcDeclarations = new List<FunctionDeclaration>();
var systemPrompts = new List<string>();
if (!string.IsNullOrEmpty(agent.Instruction) || !agent.SecondaryInstructions.IsNullOrEmpty())
{
var instruction = agentService.RenderedInstruction(agent);
renderedInstructions.Add(instruction);
systemPrompts.Add(instruction);
}
var funcPrompts = new List<string>();
var functions = agent.Functions.Concat(agent.SecondaryFunctions ?? []);
foreach (var function in functions)
{
if (!agentService.RenderFunction(agent, function)) continue;
var def = agentService.RenderFunctionProperty(agent, function);
var props = JsonSerializer.Serialize(def?.Properties);
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}"
? new Schema()
{
Type = "object",
Properties = JsonSerializer.Deserialize<Dictionary<string, Schema>>(props),
Required = def?.Required ?? []
}
: null;
funcDeclarations.Add(new FunctionDeclaration
{
Name = function.Name,
Description = function.Description,
Parameters = parameters
});
funcPrompts.Add($"{function.Name}: {function.Description} {def}");
}
if (!funcDeclarations.IsNullOrEmpty())
{
tools.Add(new Tool { FunctionDeclarations = funcDeclarations });
}
var convPrompts = new List<string>();
foreach (var message in conversations)
{
if (message.Role == AgentRole.Function)
{
contents.Add(new Content([
new Part()
{
FunctionCall = new FunctionCall
{
Name = message.FunctionName,
Args = JsonNode.Parse(message.FunctionArgs ?? "{}")
}
}
], AgentRole.Model));
contents.Add(new Content([
new Part()
{
FunctionResponse = new FunctionResponse
{
Name = message.FunctionName,
Response = new JsonObject()
{
["result"] = message.Content ?? string.Empty
}
}
}
], AgentRole.Function));
convPrompts.Add(
$"{AgentRole.Assistant}: Call function {message.FunctionName}({message.FunctionArgs}) => {message.Content}");
}
else if (message.Role == AgentRole.User)
{
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}");
}
}
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;
var request = new GenerateContentRequest
{
SystemInstruction = !systemPrompts.IsNullOrEmpty()
? new Content(systemPrompts[0], AgentRole.System)
: null,
Contents = contents,
Tools = tools,
GenerationConfig = new()
{
Temperature = temperature,
MaxOutputTokens = maxTokens
}
};
var prompt = GetPrompt(systemPrompts, funcPrompts, convPrompts);
return (prompt, request);
}
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())
{
prompt += "\r\n\r\n[FUNCTIONS]\r\n";
prompt += string.Join("\r\n", funcPrompts);
}
if (!convPrompts.IsNullOrEmpty())
{
prompt += "\r\n\r\n[CONVERSATION]\r\n";
prompt += string.Join("\r\n", convPrompts);
}
return prompt;
}
}
}

View file

@ -17,11 +17,14 @@ public class GeminiTextCompletionProvider : ITextCompletion
public string Provider => "google-ai";
public string Model => _model;
private GoogleAiSettings _googleSettings;
public GeminiTextCompletionProvider(
IServiceProvider services,
GoogleAiSettings googleSettings,
ILogger<GeminiTextCompletionProvider> logger,
ITokenStatistics tokenStatistics)
{
_googleSettings = googleSettings;
_services = services;
_logger = logger;
_tokenStatistics = tokenStatistics;
@ -47,7 +50,7 @@ public class GeminiTextCompletionProvider : ITextCompletion
await hook.BeforeGenerating(agent, new List<RoleDialogModel> { userMessage });
}
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services);
var client = ProviderHelper.GetGeminiClient(Provider, _model, _services, _googleSettings, _logger);
var aiModel = client.CreateGenerativeModel(_model);
PrepareOptions(aiModel);

View file

@ -17,6 +17,8 @@ string[] allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get
"https://botsharp.scisharpstack.org",
"https://chat.scisharpstack.org"
};
// Add BotSharp
builder.Services.AddBotSharpCore(builder.Configuration, options =>

View file

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebStarter", "WebStarter.csproj", "{6CC8C2A2-9A39-A975-2286-93F838537659}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6CC8C2A2-9A39-A975-2286-93F838537659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CC8C2A2-9A39-A975-2286-93F838537659}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CC8C2A2-9A39-A975-2286-93F838537659}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CC8C2A2-9A39-A975-2286-93F838537659}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E179BD61-3AB6-406D-8EDC-EBF0110A082F}
EndGlobalSection
EndGlobal

View file

@ -164,6 +164,20 @@
"CompletionCost": 0.002
}
]
},
{
"Provider": "google-ai",
"Models": [
{
"Name": "gemini-2.0-flash-exp",
"ApiKey": "",
"Type": "chat",
"MultiModal": true,
"RealTime": true,
"PromptCost": 0.0015,
"CompletionCost": 0.002
}
]
}
],
@ -186,7 +200,7 @@
},
"MCP": {
"Enabled": true,
"Enabled": false,
"McpClientOptions": {
"ClientInfo": {
"Name": "SimpleToolsBotsharp",

View file

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>BotSharp.Plugin.Google</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
<ProjectReference Include="..\..\src\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
<ProjectReference Include="..\..\src\Plugins\BotSharp.Plugin.AnthropicAI\BotSharp.Plugin.AnthropicAI.csproj" />
<ProjectReference Include="..\..\src\Plugins\BotSharp.Plugin.GoogleAI\BotSharp.Plugin.GoogleAI.csproj" />
<ProjectReference Include="..\..\src\Plugins\BotSharp.Plugin.OpenAI\BotSharp.Plugin.OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,110 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using Shouldly;
namespace BotSharp.Plugin.Google.Core
{
public class ChatCompletion_Tests:TestBase
{
protected static Agent CreateTestAgent()
{
return new Agent()
{
Id = "test-agent-id",
Name = "TestAgent",
Description = "This is a test agent used for unit testing purposes.",
Type = "Chat",
CreatedDateTime = DateTime.UtcNow,
UpdatedDateTime = DateTime.UtcNow,
IsPublic = false,
Disabled = false
};
}
public static IEnumerable<object[]> CreateTestLLMProviders()
{
//Common
var agent = CreateTestAgent();
IServiceCollection services;
IConfiguration configuration;
string modelName;
if (LLMProvider.CanRunGemini)
{
//Google Gemini
(services, configuration, modelName) = LLMProvider.CreateGemini();
yield return new object[] { services.BuildServiceProvider().GetService<IChatCompletion>() ?? throw new Exception("Error while initializing"), agent, modelName };
}
if (LLMProvider.CanRunOpenAI)
{
//OpenAI
(services, configuration, modelName) = LLMProvider.CreateOpenAI();
yield return new object[] { services.BuildServiceProvider().GetService<IChatCompletion>() ?? throw new Exception("Error while initializing"), agent, modelName };
}
if (LLMProvider.CanRunAnthropic)
{
//Anthropic
(services, configuration, modelName) = LLMProvider.CreateAnthropic();
yield return new object[] { services.BuildServiceProvider().GetService<IChatCompletion>() ?? throw new Exception("Error while initializing"), agent, modelName };
}
}
public ChatCompletion_Tests()
{
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletions_Test(IChatCompletion chatCompletion, Agent agent, string modelName)
{
chatCompletion.SetModelName(modelName);
var conversation = new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "write a poem about stars")]);
var result = await chatCompletion.GetChatCompletions(agent,conversation);
result.Content.ShouldNotBeNullOrEmpty();
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletionsAsync_Test(IChatCompletion chatCompletion, Agent agent, string modelName)
{
chatCompletion.SetModelName(modelName);
var conversation = new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "write a poem about stars")]);
RoleDialogModel reply = null;
var result = await chatCompletion.GetChatCompletionsAsync(agent,conversation, async (received) =>
{
reply = received;
}, async (func) =>
{
});
result.ShouldBeTrue();
reply.ShouldNotBeNull();
reply.Content.ShouldNotBeNullOrEmpty();
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletionsStreamingAsync_Test(IChatCompletion chatCompletion, Agent agent, string modelName)
{
chatCompletion.SetModelName(modelName);
var conversation = new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "write a poem about stars")]);
RoleDialogModel reply = null;
var result = await chatCompletion.GetChatCompletionsStreamingAsync(agent,conversation, async (received) =>
{
reply = received;
});
result.ShouldBeTrue();
reply.ShouldNotBeNull();
reply.Content.ShouldNotBeNullOrEmpty();
}
}
}

View file

@ -0,0 +1,161 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.MLTasks.Settings;
using BotSharp.Abstraction.Repositories;
using BotSharp.Abstraction.Settings;
using BotSharp.Core;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.AnthropicAI;
using BotSharp.Plugin.AnthropicAI.Settings;
using BotSharp.Plugin.GoogleAi;
using BotSharp.Plugin.GoogleAi.Settings;
using BotSharp.Plugin.OpenAI;
using BotSharp.Plugin.OpenAI.Settings;
using GenerativeAI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace BotSharp.Plugin.Google.Core
{
public static class LLMProvider
{
public static bool CanRunGemini => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GOOGLE_API_KEY"));
public static bool CanRunOpenAI => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OPEN_AI_APIKEY"));
public static bool CanRunAnthropic => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"));
private static ILoggerFactory _loggerFactory = LoggerFactory.Create((builder) => builder.AddConsole());
public static (IServiceCollection services, IConfiguration config, string modelName) CreateGemini()
{
string modelName = GoogleAIModels.Gemini2FlashLitePreview;
var apiKey = Environment.GetEnvironmentVariable("GOOGLE_API_KEY") ??
throw new Exception("GOOGLE_API_KEY is not set");
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").AddInMemoryCollection(
new Dictionary<string, string?>(new[] { new KeyValuePair<string,string?>("GoogleAi:Gemini:ApiKey",apiKey) })).Build();
LlmProviderSetting setting = new LlmProviderSetting();
setting.Provider = "google-ai";
setting.Models = new List<LlmModelSetting>([
new LlmModelSetting()
{
Name = modelName,
ApiKey = apiKey
},
new LlmModelSetting()
{
Name = GoogleAIModels.Gemini2FlashExp,
ApiKey = apiKey
},
new LlmModelSetting()
{
Name = GoogleAIModels.TextEmbedding,
ApiKey = apiKey
}
]);
services.AddSingleton(new GoogleAiSettings()
{
Gemini = new GeminiSetting()
{
ApiKey = apiKey
}
});
services.AddSingleton<List<LlmProviderSetting>>(new List<LlmProviderSetting>([ setting]));
AddCommonServices(services, configuration);
new GoogleAiPlugin().RegisterDI(services, configuration);
return (services, configuration, modelName);
}
public static (IServiceCollection services, IConfiguration config, string modelName) CreateOpenAI()
{
string modelName = "gpt-4o-mini";
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_APIKEY") ??
throw new Exception("OPEN_AI_APIKEY is not set");
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").AddInMemoryCollection(
new Dictionary<string, string?>(new[] { new KeyValuePair<string,string?>("GoogleAi:Gemini:ApiKey",apiKey) })).Build();
LlmProviderSetting setting = new LlmProviderSetting();
setting.Provider = "OpenAi";
setting.Models = new List<LlmModelSetting>([
new LlmModelSetting()
{
Name = modelName,
ApiKey = apiKey
},
new LlmModelSetting()
{
Name = "text-embedding-3-small",
ApiKey = apiKey
}
]);
services.AddSingleton(new OpenAiSettings()
{
});
services.AddSingleton<List<LlmProviderSetting>>(new List<LlmProviderSetting>([ setting]));
AddCommonServices(services, configuration);
new OpenAiPlugin().RegisterDI(services, configuration);
return (services, configuration, modelName);
}
private static void AddCommonServices(ServiceCollection services, IConfigurationRoot configuration)
{
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton<IAgentService, TestAgentService>();
services.AddSingleton<ILlmProviderService, LlmProviderService>();
services.AddSingleton<ISettingService, SettingService>();
services.AddSingleton<IConversationStateService, NullConversationStateService>();
services.AddSingleton<IFileStorageService, NullFileStorageService>();
services.AddLogging(s=>s.AddConsole());
}
public static (IServiceCollection services, IConfiguration configuration, string modelName) CreateAnthropic()
{
string modelName = "claude-3-5-haiku-20241022";
var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ??
throw new Exception("ANTHROPIC_API_KEY is not set");
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
LlmProviderSetting setting = new LlmProviderSetting();
setting.Provider = "anthropic";
setting.Models = new List<LlmModelSetting>([
new LlmModelSetting()
{
Name = modelName,
ApiKey = apiKey
}
]);
services.AddSingleton(new AnthropicSettings()
{
Claude = new ClaudeSetting()
{
}
});
services.AddSingleton<List<LlmProviderSetting>>(new List<LlmProviderSetting>([ setting]));
AddCommonServices(services, configuration);
new AnthropicPlugin().RegisterDI(services, configuration);
return (services, configuration, modelName);
}
}
}

View file

@ -0,0 +1,85 @@
using System.Text.Json;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Plugin.Google.Core
{
public class NullConversationStateService:IConversationStateService
{
public void Dispose()
{
// TODO release managed resources here
}
public string GetConversationId()
{
return "fake-conversation-id";
}
public Dictionary<string, string> Load(string conversationId, bool isReadOnly = false)
{
return new Dictionary<string, string> { { "Key", "Value" } };
}
public string GetState(string name, string defaultValue = "")
{
var states = GetStates();
if (!states.ContainsKey(name))
return defaultValue;
return states[name]??defaultValue;
}
public bool ContainsState(string name)
{
return false;
}
public Dictionary<string, string> GetStates()
{
return new Dictionary<string, string> { { "temperature", "0.5" }, { "max_tokens", "8000" }, { "top_p", "1.0" }, { "frequency_penalty", "0.0" } };
}
public IConversationStateService SetState<T>(string name, T value, bool isNeedVersion = true, int activeRounds = -1,
string valueType = StateDataType.String, string source = StateSource.User, bool readOnly = false)
{
return this;
}
public void SaveStateByArgs(JsonDocument args)
{
}
public bool RemoveState(string name)
{
return true;
}
public void CleanStates(params string[] excludedStates)
{
}
public void Save()
{
}
public ConversationState GetCurrentState()
{
return new ConversationState { { "StateKey", new StateKeyValue { Key = "Key", Values = new List<StateValue>()} } };
}
public void SetCurrentState(ConversationState state)
{
}
public void ResetCurrentState()
{
}
}
}

View file

@ -0,0 +1,147 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Models;
namespace BotSharp.Plugin.Google.Core
{
public class NullFileStorageService:IFileStorageService
{
public string GetDirectory(string conversationId)
{
return $"FakeDirectory/{conversationId}";
}
public IEnumerable<string> GetFiles(string relativePath, string? searchQuery = null)
{
return new List<string> { "FakeFile1.txt", "FakeFile2.txt" };
}
public byte[] GetFileBytes(string fileStorageUrl)
{
return new byte[] { 0x00, 0x01, 0x02 };
}
public bool SaveFileStreamToPath(string filePath, Stream stream)
{
return true;
}
public bool SaveFileBytesToPath(string filePath, byte[] bytes)
{
return true;
}
public string GetParentDir(string dir, int level = 1)
{
return "FakeParentDirectory";
}
public bool ExistDirectory(string? dir)
{
return true;
}
public void CreateDirectory(string dir)
{
}
public void DeleteDirectory(string dir)
{
}
public string BuildDirectory(params string[] segments)
{
return string.Join("/", segments);
}
public Task<IEnumerable<MessageFileModel>> GetMessageFileScreenshotsAsync(string conversationId, IEnumerable<string> messageIds)
{
return Task.FromResult<IEnumerable<MessageFileModel>>(new List<MessageFileModel>
{
new MessageFileModel { FileName = "Screenshot1.png" },
new MessageFileModel { FileName = "Screenshot2.png" }
});
}
public IEnumerable<MessageFileModel> GetMessageFiles(string conversationId, IEnumerable<string> messageIds, string source,
IEnumerable<string>? contentTypes = null)
{
return new List<MessageFileModel>
{
new MessageFileModel { FileName = "File1.docx" },
new MessageFileModel { FileName = "File2.pdf" }
};
}
public string GetMessageFile(string conversationId, string messageId, string source, string index, string fileName)
{
return $"FakePath/{fileName}";
}
public IEnumerable<MessageFileModel> GetMessagesWithFile(string conversationId, IEnumerable<string> messageIds)
{
return new List<MessageFileModel>
{
new MessageFileModel { FileName = "MessageFile1.jpg" },
new MessageFileModel { FileName = "MessageFile2.png" }
};
}
public bool SaveMessageFiles(string conversationId, string messageId, string source, List<FileDataModel> files)
{
return true;
}
public bool DeleteMessageFiles(string conversationId, IEnumerable<string> messageIds, string targetMessageId,
string? newMessageId = null)
{
return true;
}
public bool DeleteConversationFiles(IEnumerable<string> conversationIds)
{
return true;
}
public string GetUserAvatar()
{
return "FakeUserAvatar.png";
}
public bool SaveUserAvatar(FileDataModel file)
{
return true;
}
public bool SaveSpeechFile(string conversationId, string fileName, BinaryData data)
{
return true;
}
public BinaryData GetSpeechFile(string conversationId, string fileName)
{
return BinaryData.FromBytes(new byte[] { 0x03, 0x04, 0x05 });
}
public bool SaveKnowledgeBaseFile(string collectionName, string vectorStoreProvider, Guid fileId, string fileName,
BinaryData fileData)
{
return true;
}
public bool DeleteKnowledgeFile(string collectionName, string vectorStoreProvider, Guid? fileId = null)
{
return true;
}
public string GetKnowledgeBaseFileUrl(string collectionName, string vectorStoreProvider, Guid fileId, string fileName)
{
return $"https://fakeurl.com/{fileName}";
}
public BinaryData GetKnowledgeBaseFileBinaryData(string collectionName, string vectorStoreProvider, Guid fileId,
string fileName)
{
return BinaryData.FromBytes(new byte[] { 0x06, 0x07, 0x08 });
}
}
}

View file

@ -0,0 +1,109 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Utilities;
namespace BotSharp.Plugin.Google.Core
{
public class TestAgentService : IAgentService
{
public Task<Agent> CreateAgent(Agent agent)
{
return Task.FromResult(new Agent());
}
public Task<string> RefreshAgents()
{
return Task.FromResult("Refreshed successfully");
}
public Task<PagedItems<Agent>> GetAgents(AgentFilter filter)
{
return Task.FromResult(new PagedItems<Agent>());
}
public Task<List<IdName>> GetAgentOptions(List<string>? agentIds = null)
{
return Task.FromResult(new List<IdName> { new IdName(id: "1", name: "Fake Agent") });
}
public Task<Agent> LoadAgent(string id, bool loadUtility = true)
{
return Task.FromResult(new Agent());
}
public Task InheritAgent(Agent agent)
{
return Task.CompletedTask;
}
public string RenderedInstruction(Agent agent)
{
return "Fake Instruction";
}
public string RenderedTemplate(Agent agent, string templateName)
{
return $"Rendered template for {templateName}";
}
public bool RenderFunction(Agent agent, FunctionDef def)
{
return true;
}
public FunctionParametersDef? RenderFunctionProperty(Agent agent, FunctionDef def)
{
return def.Parameters;
}
public Task<Agent> GetAgent(string id)
{
return Task.FromResult(new Agent());
}
public Task<bool> DeleteAgent(string id)
{
return Task.FromResult(true);
}
public Task UpdateAgent(Agent agent, AgentField updateField)
{
return Task.CompletedTask;
}
public Task<string> PatchAgentTemplate(Agent agent)
{
return Task.FromResult("Patched successfully");
}
public Task<string> UpdateAgentFromFile(string id)
{
return Task.FromResult("Updated from file successfully");
}
public string GetDataDir()
{
return "Fake Data Directory";
}
public string GetAgentDataDir(string agentId)
{
return $"Fake Data Directory for agent {agentId}";
}
public Task<List<UserAgent>> GetUserAgents(string userId)
{
return Task.FromResult(new List<UserAgent> { new UserAgent() });
}
public PluginDef GetPlugin(string agentId)
{
return new PluginDef();
}
}
}

View file

@ -0,0 +1,64 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
namespace BotSharp.Plugin.Google.Core
{
public class Embedding_Tests:TestBase
{
protected static Agent CreateTestAgent()
{
return new Agent()
{
Id = "test-agent-id",
Name = "TestAgent",
Description = "This is a test agent used for unit testing purposes.",
Type = "Chat",
CreatedDateTime = DateTime.UtcNow,
UpdatedDateTime = DateTime.UtcNow,
IsPublic = false,
Disabled = false
};
}
public static IEnumerable<object[]> CreateTestLLMProviders()
{
//Common
var agent = CreateTestAgent();
IServiceCollection services;
IConfiguration configuration;
string modelName;
if (LLMProvider.CanRunGemini)
{
//Google Gemini
(services, configuration, modelName) = LLMProvider.CreateGemini();
yield return new object[] { services.BuildServiceProvider().GetService<ITextEmbedding>() ?? throw new Exception("Error while initializing"), agent, modelName };
}
if (LLMProvider.CanRunOpenAI)
{
//OpenAI
(services, configuration, modelName) = LLMProvider.CreateOpenAI();
yield return new object[] { services.BuildServiceProvider().GetService<ITextEmbedding>() ?? throw new Exception("Error while initializing"), agent, modelName };
}
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletions_Test(ITextEmbedding chatCompletion, Agent agent, string modelName)
{
var text = "This is a placeholder for a really long text used for testing, generated for simulation purposes. The text simulates a verbose input and can be modified to any required content.";
var result = await chatCompletion.GetVectorAsync(text);
result.ShouldNotBeEmpty();
}
}
}

View file

@ -0,0 +1,124 @@
using System.Text.Json;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
namespace BotSharp.Plugin.Google.Core
{
public class FunctionCalling_Tests : TestBase
{
public static IEnumerable<object[]> CreateTestLLMProviders()
{
//Common
var agent = CreateTestAgent();
IServiceCollection services;
IConfiguration configuration;
string modelName;
if (LLMProvider.CanRunGemini)
{
//Google Gemini
(services, configuration, modelName) = LLMProvider.CreateGemini();
yield return new object[]
{
services.BuildServiceProvider().GetService<IChatCompletion>() ??
throw new Exception("Error while initializing"),
agent, modelName
};
}
if (LLMProvider.CanRunOpenAI)
{
//OpenAI
(services, configuration, modelName) = LLMProvider.CreateOpenAI();
yield return new object[]
{
services.BuildServiceProvider().GetService<IChatCompletion>() ??
throw new Exception("Error while initializing"),
agent, modelName
};
}
if (LLMProvider.CanRunAnthropic)
{
//Anthropic
(services, configuration, modelName) = LLMProvider.CreateAnthropic();
yield return new object[]
{
services.BuildServiceProvider().GetService<IChatCompletion>() ??
throw new Exception("Error while initializing"),
agent, modelName
};
}
}
protected static Agent CreateTestAgent()
{
return new Agent()
{
Id = "test-agent-id",
Name = "TestAgent",
Description = "This is a test agent used for unit testing purposes.",
Type = "Chat",
CreatedDateTime = DateTime.UtcNow,
UpdatedDateTime = DateTime.UtcNow,
IsPublic = false,
Disabled = false,
Functions = new List<FunctionDef>([JsonSerializer.Deserialize<FunctionDef>("{\n \"name\": \"get_weather_info\",\n \"description\": \"get current weather info for a given city\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"city\": {\n \"type\": \"string\",\n \"description\": \"city name.\"\n }\n },\n \"required\": [ \"city\" ]\n }\n}")])
};
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletions_Test(IChatCompletion chatCompletion, Agent agent, string modelName)
{
chatCompletion.SetModelName(modelName);
var conversation =
new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "how's the weather in Sydney?")]);
var result = await chatCompletion.GetChatCompletions(agent, conversation);
result.FunctionName.ShouldBe("get_weather_info");
result.FunctionArgs.ShouldContain("Sydney",Case.Insensitive);
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletionsAsync_Test(IChatCompletion chatCompletion, Agent agent, string modelName)
{
chatCompletion.SetModelName(modelName);
var conversation =
new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "how's the weather in Sydney?")]);
RoleDialogModel reply = null;
RoleDialogModel function = null;
var result = await chatCompletion.GetChatCompletionsAsync(agent, conversation,
async (received) => { reply = received; }, async (func) => { function = func; });
result.ShouldBeTrue();
function.ShouldNotBeNull();
function.FunctionName.ShouldNotBeNullOrEmpty();
function.FunctionArgs.ShouldContain("Sydney",Case.Insensitive);
}
[Theory]
[MemberData(nameof(CreateTestLLMProviders))]
public async Task GetChatCompletionsStreamingAsync_Test(IChatCompletion chatCompletion, Agent agent,
string modelName)
{
//Not sure about support of function calling with Streaming
// chatCompletion.SetModelName(modelName);
// var conversation =
// new List<RoleDialogModel>([new RoleDialogModel(AgentRole.User, "how's the weather in Sydney?")]);
// RoleDialogModel reply = null;
// var result = await chatCompletion.GetChatCompletionsStreamingAsync(agent, conversation,
// async (received) => { reply = received; });
// result.ShouldBeTrue();
// reply.ShouldNotBeNull();
// reply.Content.ShouldNotBeNullOrEmpty();
}
}
}

View file

@ -0,0 +1,57 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Realtime.Models;
using GenerativeAI;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
namespace BotSharp.Plugin.Google.Core
{
public class GoogleRealTime_Tests : TestBase
{
protected static Agent CreateTestAgent()
{
return new Agent()
{
Id = "test-agent-id",
Name = "TestAgent",
Description = "This is a test agent used for unit testing purposes.",
Type = "Chat",
CreatedDateTime = DateTime.UtcNow,
UpdatedDateTime = DateTime.UtcNow,
IsPublic = false,
Disabled = false
};
}
[Fact]
public async Task ShouldConnect_Tests()
{
if (!LLMProvider.CanRunGemini)
return;
(IServiceCollection services, IConfiguration config, string modelName) = LLMProvider.CreateGemini();
var agent = CreateTestAgent();
var realTimeCompleter = services.BuildServiceProvider().GetService<IRealTimeCompletion>();
realTimeCompleter.SetModelName(GoogleAIModels.Gemini2FlashExp);
bool modelReady = false;
await realTimeCompleter.Connect(new RealtimeHubConnection(), () => { modelReady = true; },
(s, s1) => { Console.WriteLine(s); }, () => { }, (s) => { Console.WriteLine(s); },
(list => { Console.WriteLine(list); }),
(s => { Console.WriteLine(s); }),
(model => { Console.WriteLine(model); }), (() => { Console.WriteLine("UserInterrupted"); }));
var session = await realTimeCompleter.CreateSession(agent, new List<RoleDialogModel>());
Thread.Sleep(1000);
modelReady.ShouldBeTrue();
await realTimeCompleter.InsertConversationItem(new RoleDialogModel(AgentRole.User,
"tell me something about Albert Einstein."));
Thread.Sleep(10000);
}
}
}

View file

@ -0,0 +1,13 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BotSharp.Plugin.Google.Core
{
public abstract class TestBase
{
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -0,0 +1,414 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AllowedOrigins": [
"http://localhost:5015",
"http://0.0.0.0:5015",
"https://botsharp.scisharpstack.org",
"https://chat.scisharpstack.org"
],
"Jwt": {
"Issuer": "botsharp",
"Audience": "botsharp",
"Key": "31ba6052aa6f4569901facc3a41fcb4adfd9b46dd00c40af8a753fbdc2b89869"
},
"OAuth": {
"GitHub": {
"ClientId": "",
"ClientSecret": ""
},
"Google": {
"ClientId": "",
"ClientSecret": ""
},
"Keycloak": {
"BaseAddress": "",
"Realm": "",
"ClientId": "",
"ClientSecret": "",
"Version": 22
},
"Weixin": {
"AppId": "",
"AppSecret": ""
}
},
"LlmProviders": [
{
"Provider": "azure-openai",
"Models": [
{
"Id": "gpt-3.5-turbo",
"Name": "gpt-35-turbo",
"Version": "1106",
"ApiKey": "",
"Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/"
},
{
"Name": "gpt-35-turbo-instruct",
"Version": "0914",
"ApiKey": "",
"Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/",
"Type": "text",
"PromptCost": 0.0015,
"CompletionCost": 0.002
}
]
},
{
"Provider": "llama-sharp",
"Models": [
{
"Name": "llama-2-7b-guanaco-qlora.Q2_K.gguf",
"Type": "chat"
}
]
},
{
"Provider": "huggingface",
"Models": [
{
"Name": "mistralai/Mistral-7B-v0.1",
"Type": "text"
},
{
"Name": "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
"Type": "text"
}
]
},
{
"Provider": "sparkdesk",
"Models": [
{
"Name": "gpt-35-turbo",
"Type": "chat",
"PromptCost": 0.0015,
"CompletionCost": 0.002
}
]
},
{
"Provider": "metaglm",
"Models": [
{
"Name": "chatglm3_6b",
"Type": "chat",
"PromptCost": 0.0015,
"CompletionCost": 0.002
}
]
},
{
"Provider": "openai",
"Models": [
{
"Id": "gpt-4",
"Name": "gpt-4o-mini",
"Version": "2024-07-18",
"ApiKey": "",
"Type": "chat",
"MultiModal": true,
"PromptCost": 0.00015,
"CompletionCost": 0.0006
},
{
"Id": "gpt-4",
"Name": "gpt-4o-2024-11-20",
"Version": "2024-11-20",
"ApiKey": "",
"Type": "chat",
"MultiModal": true,
"PromptCost": 0.0025,
"CompletionCost": 0.01
},
{
"Id": "gpt-4",
"Name": "gpt-4o-mini-realtime-preview-2024-12-17",
"Version": "2024-12-17",
"ApiKey": "",
"Type": "chat",
"MultiModal": true,
"RealTime": true,
"PromptCost": 0.0025,
"CompletionCost": 0.01
},
{
"Id": "text-embedding-3",
"Name": "text-embedding-3-small",
"Version": "3-small",
"ApiKey": "",
"Type": "embedding",
"Dimension": 1536,
"PromptCost": 0.00002
}
]
}
],
"Router": {
},
"Evaluator": {
"AgentId": "dfd9b46d-d00c-40af-8a75-3fbdc2b89869"
},
"Agent": {
"DataDir": "agents",
"TemplateFormat": "liquid",
"HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c",
"EnableTranslator": false,
"LlmConfig": {
"Provider": "azure-openai",
"Model": "gpt-4o-mini"
}
},
//"MCPSettings": {
// "McpClientOptions": {
// "ClientInfo": {
// "Name": "SimpleToolsBotsharp",
// "Version": "1.0.0"
// }
// },
// "McpServerConfigs": [
// {
// "Id": "PizzaServer",
// "Name": "PizzaServer",
// "TransportType": "sse",
// "TransportOptions": [],
// "Location": "http://localhost:58905/sse"
// }
// ]
//},
"Conversation": {
"DataDir": "conversations",
"ShowVerboseLog": false,
"EnableLlmCompletionLog": false,
"EnableExecutionLog": true,
"EnableContentLog": true,
"EnableStateLog": true,
"EnableTranslationMemory": false,
"CleanSetting": {
"Enable": true,
"BatchSize": 50,
"MessageLimit": 2,
"BufferHours": 12,
"ExcludeAgentIds": []
},
"RateLimit": {
"MaxConversationPerDay": 100,
"MaxInputLengthPerRequest": 256,
"MinTimeSecondsBetweenMessages": 2
}
},
"SideCar": {
"Conversation": {
"Provider": "botsharp"
}
},
"WebBrowsing": {
"Driver": "Playwright"
},
"HttpHandler": {
"BaseAddress": "",
"Origin": ""
},
"SqlDriver": {
"MySqlConnectionString": "",
"SqlServerConnectionString": "",
"SqlLiteConnectionString": ""
},
"Statistics": {
"Enabled": false
},
"Instruction": {
"Logging": {
"Enabled": true,
"ExcludedAgentIds": []
}
},
"ChatHub": {
"EventDispatchBy": "group"
},
"SharpCache": {
"Enabled": true,
"CacheType": 1,
"Prefix": "botsharp"
},
"LlamaSharp": {
"Interactive": true,
"ModelDir": "C:/Users/haipi/Downloads",
"DefaultModel": "llama-2-7b-chat.Q8_0.gguf",
"MaxContextLength": 1024,
"NumberOfGpuLayer": 20
},
"AzureOpenAi": {
},
"AnthropicAi": {
"Claude": {
}
},
"GoogleAi": {
"PaLM": {
"Endpoint": "https://generativelanguage.googleapis.com",
"ApiKey": ""
},
"Gemini": {
"ApiKey": "",
"UseGoogleSearch": false,
"UseGrounding": false
}
},
"HuggingFace": {
"Endpoint": "https://api-inference.huggingface.co",
"Model": "tiiuae/falcon-180B-chat",
"Token": ""
},
"MetaAi": {
"fastText": {
"ModelPath": "dbpedia.ftz"
}
},
"RoutingSpeeder": {
},
"MetaMessenger": {
"Endpoint": "https://graph.facebook.com",
"ApiVersion": "v17.0",
"PageId": "",
"PageAccessToken": ""
},
"Twilio": {
"PhoneNumber": "+1",
"AccountSID": "",
"AuthToken": "",
"CallbackHost": "https://",
"AgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"
},
"Database": {
"Default": "FileRepository",
"TablePrefix": "BotSharp",
"BotSharpMongoDb": "",
"Redis": "botsharp.redis.cache.windows.net:6380,password=,ssl=True,abortConnect=False",
"FileRepository": "data",
"Assemblies": [ "BotSharp.Core" ]
},
"FileCore": {
"Storage": "LocalFileStorage",
"Pdf2TextConverter": {
"Provider": ""
},
"Pdf2ImageConverter": {
"Provider": ""
}
},
"TencentCos": {
"AppId": "",
"SecretId": "",
"SecretKey": "",
"BucketName": "",
"Region": ""
},
"Qdrant": {
"Url": "",
"ApiKey": ""
},
"Graph": {
"BaseUrl": "",
"SearchPath": ""
},
"WeChat": {
"AgentId": "437bed34-1169-4833-95ce-c24b8b56154a",
"Token": "#{Token}#",
"EncodingAESKey": "#{EncodingAESKey}#",
"WeixinAppId": "#{WeixinAppId}#",
"WeixinAppSecret": "#{WeixinAppSecret}#"
},
"KnowledgeBase": {
"VectorDb": {
"Provider": "Qdrant"
},
"GraphDb": {
"Provider": "Remote"
},
"Default": {
"CollectionName": "BotSharp",
"TextEmbedding": {
"Provider": "openai",
"Model": "text-embedding-3-small",
"Dimension": 1536
}
}
},
"SparkDesk": {
"AppId": "",
"ApiKey": "",
"ApiSecret": "",
"ModelVersion": "V3_5"
},
"MetaGLM": {
"ApiKey": "6b6c8b3fca3e5da21d633e350980744d.938gruOqrK4BDqW8",
"BaseAddress": "http://localhost:8100/v1/",
"ModelId": "chatglm3_6b",
"Temperature": 0.7,
"TopP": 0.7
},
"GoogleApi": {
"ApiKey": "",
"Map": {
"Endpoint": "https://maps.googleapis.com/maps/api/geocode/json",
"Components": "country=US|country=CA"
},
"Youtube": {
"Endpoint": "https://www.googleapis.com/youtube/v3/search",
"RegionCode": "US",
"Part": "id,snippet",
"Channels": []
}
},
"Interpreter": {
"Python": {
"PythonDLL": "C:/Users/xxx/AppData/Local/Programs/Python/Python311/python311.dll"
}
},
"PluginLoader": {
"Assemblies": [
"BotSharp.Plugin.GoogleAI"
]
}
}

View file

@ -101,4 +101,8 @@
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>