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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ public GeneralLLMServiceTests() {
_redisMock.Object,
_dbContext,
_loggerMock.Object,
_ollamaServiceMock.Object,
_openAIServiceMock.Object,
_geminiServiceMock.Object,
anthropicServiceMock.Object,
_factoryMock.Object);
}

Expand Down
19 changes: 11 additions & 8 deletions TelegramSearchBot.LLM.Test/Service/AI/LLM/LLMFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using StackExchange.Redis;
Expand Down Expand Up @@ -55,15 +56,17 @@ public LLMFactoryTests() {
var responsesService = new OpenAIResponsesService(
_dbContext, responsesLogger.Object, messageExtensionServiceMock.Object, httpClientFactoryMock.Object);

var services = new ServiceCollection()
.AddSingleton(_openAIServiceMock.Object)
.AddSingleton(_ollamaServiceMock.Object)
.AddSingleton(_geminiServiceMock.Object)
.AddSingleton(anthropicServiceMock.Object)
.AddSingleton(responsesService)
.BuildServiceProvider();

_factory = new LLMFactory(
_redisMock.Object,
_dbContext,
_loggerMock.Object,
_ollamaServiceMock.Object,
_openAIServiceMock.Object,
_geminiServiceMock.Object,
anthropicServiceMock.Object,
responsesService);
services,
_loggerMock.Object);
}

[Fact]
Expand Down
11 changes: 11 additions & 0 deletions TelegramSearchBot.LLM/Interface/AI/LLM/IBotIdentityProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;

namespace TelegramSearchBot.Interface.AI.LLM {
public interface IBotIdentityProvider {
Task<BotIdentity> GetIdentityAsync(CancellationToken cancellationToken = default);
void SetIdentity(long botUserId, string botName);
}

public sealed record BotIdentity(long UserId, string UserName);
}
11 changes: 11 additions & 0 deletions TelegramSearchBot.LLM/Interface/AI/LLM/IGroupLlmSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;

#nullable enable

namespace TelegramSearchBot.Interface.AI.LLM {
public interface IGroupLlmSettingsService {
Task<string?> GetModelAsync(long chatId, CancellationToken cancellationToken = default);
Task<(string Previous, string Current)> SetModelAsync(long chatId, string modelName, CancellationToken cancellationToken = default);
}
}
40 changes: 34 additions & 6 deletions TelegramSearchBot.LLM/Service/AI/LLM/AnthropicService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@ public class AnthropicService : IService, ILLMService {
private readonly DataDbContext _dbContext;
private readonly IMessageExtensionService _messageExtensionService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IBotIdentityProvider _botIdentityProvider;
private string _fallbackBotName = string.Empty;

public static string _botName;
public string BotName {
get => _botName;
set => _botName = value;
get => GetBotNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
set {
if (_botIdentityProvider != null) {
_botIdentityProvider.SetIdentity(Env.BotId, value);
} else {
_fallbackBotName = value ?? string.Empty;
}
}
}

private static readonly string[] _anthropicModels = {
Expand All @@ -53,14 +60,33 @@ public AnthropicService(
DataDbContext context,
ILogger<AnthropicService> logger,
IMessageExtensionService messageExtensionService,
IHttpClientFactory httpClientFactory) {
IHttpClientFactory httpClientFactory)
: this(context, logger, messageExtensionService, httpClientFactory, null) {
}

public AnthropicService(
DataDbContext context,
ILogger<AnthropicService> logger,
IMessageExtensionService messageExtensionService,
IHttpClientFactory httpClientFactory,
IBotIdentityProvider botIdentityProvider) {
_logger = logger;
_dbContext = context;
_messageExtensionService = messageExtensionService;
_httpClientFactory = httpClientFactory;
_botIdentityProvider = botIdentityProvider;
_logger.LogInformation("AnthropicService instance created. McpToolHelper should be initialized at application startup.");
}

private async Task<string> GetBotNameAsync() {
if (_botIdentityProvider == null) {
return _fallbackBotName;
}

var identity = await _botIdentityProvider.GetIdentityAsync();
return identity.UserName ?? string.Empty;
}

private AnthropicClient CreateClient(LLMChannel channel) {
var options = new Anthropic.Core.ClientOptions {
ApiKey = channel.ApiKey,
Expand Down Expand Up @@ -496,7 +522,8 @@ private async IAsyncEnumerable<string> ExecWithNativeToolCallingAsync(
List<OpenAI.Chat.ChatTool> nativeTools,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) {

string systemPrompt = McpToolHelper.FormatSystemPromptForNativeToolCalling(BotName, ChatId);
var botName = await GetBotNameAsync();
string systemPrompt = McpToolHelper.FormatSystemPromptForNativeToolCalling(botName, ChatId);
bool supportsVision = await CheckVisionSupport(modelName, channel.Id);
var (_, providerHistory) = await GetChatHistory(ChatId, systemPrompt, message, supportsVision);

Expand Down Expand Up @@ -663,7 +690,8 @@ private async IAsyncEnumerable<string> ExecWithXmlToolCallingAsync(
LlmExecutionContext executionContext,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) {

string systemPrompt = McpToolHelper.FormatSystemPrompt(BotName, ChatId);
var botName = await GetBotNameAsync();
string systemPrompt = McpToolHelper.FormatSystemPrompt(botName, ChatId);
bool supportsVision = await CheckVisionSupport(modelName, channel.Id);
var (_, providerHistory) = await GetChatHistory(ChatId, systemPrompt, message, supportsVision);

Expand Down
29 changes: 29 additions & 0 deletions TelegramSearchBot.LLM/Service/AI/LLM/BotIdentityProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using TelegramSearchBot.Attributes;
using TelegramSearchBot.Common;
using TelegramSearchBot.Interface;
using TelegramSearchBot.Interface.AI.LLM;

namespace TelegramSearchBot.Service.AI.LLM {
[Injectable(ServiceLifetime.Singleton)]
public class BotIdentityProvider : IService, IBotIdentityProvider {
private readonly object _lock = new();
private BotIdentity _identity = new(0, string.Empty);

public string ServiceName => nameof(BotIdentityProvider);

public Task<BotIdentity> GetIdentityAsync(CancellationToken cancellationToken = default) {
lock (_lock) {
return Task.FromResult(_identity);
}
}

public void SetIdentity(long botUserId, string botName) {
lock (_lock) {
_identity = new BotIdentity(botUserId, botName ?? string.Empty);
Env.BotId = botUserId;
}
}
}
}
36 changes: 33 additions & 3 deletions TelegramSearchBot.LLM/Service/AI/LLM/GeminiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,47 @@ public class GeminiService : ILLMService, IService {
private readonly DataDbContext _dbContext;
private readonly Dictionary<long, ChatSession> _chatSessions = new();
private readonly IHttpClientFactory _httpClientFactory;
public string BotName { get; set; }
private readonly IBotIdentityProvider _botIdentityProvider;
private string _fallbackBotName = string.Empty;
public string BotName {
get => GetBotNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
set {
if (_botIdentityProvider != null) {
_botIdentityProvider.SetIdentity(Env.BotId, value);
} else {
_fallbackBotName = value ?? string.Empty;
}
}
}

public GeminiService(
DataDbContext context,
ILogger<GeminiService> logger,
IHttpClientFactory httpClientFactory) {
IHttpClientFactory httpClientFactory)
: this(context, logger, httpClientFactory, null) {
}

public GeminiService(
DataDbContext context,
ILogger<GeminiService> logger,
IHttpClientFactory httpClientFactory,
IBotIdentityProvider botIdentityProvider) {
_logger = logger;
_dbContext = context;
_httpClientFactory = httpClientFactory;
_botIdentityProvider = botIdentityProvider;
_logger.LogInformation("GeminiService instance created");
}

private async Task<string> GetBotNameAsync() {
if (_botIdentityProvider == null) {
return _fallbackBotName;
}

var identity = await _botIdentityProvider.GetIdentityAsync();
return identity.UserName ?? string.Empty;
}

private void AddMessageToHistory(List<GenerativeAI.Types.Content> chatHistory, long fromUserId, string content) {
AddMessageToHistory(chatHistory, fromUserId, content, null);
}
Expand Down Expand Up @@ -369,7 +398,8 @@ public async IAsyncEnumerable<string> ExecAsync(
var currentMessageBuilder = new StringBuilder();
// Track history for snapshot
var trackedHistory = new List<SerializedChatMessage>();
trackedHistory.Add(new SerializedChatMessage { Role = "system", Content = McpToolHelper.FormatSystemPrompt(null, ChatId) });
var botName = await GetBotNameAsync();
trackedHistory.Add(new SerializedChatMessage { Role = "system", Content = McpToolHelper.FormatSystemPrompt(botName, ChatId) });

for (int cycle = 0; cycle < maxToolCycles; cycle++) {
if (cancellationToken.IsCancellationRequested) throw new TaskCanceledException();
Expand Down
14 changes: 0 additions & 14 deletions TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
public class GeneralLLMService : IService, IGeneralLLMService {
protected IConnectionMultiplexer connectionMultiplexer { get; set; }
private readonly DataDbContext _dbContext;
private readonly OpenAIService _openAIService;
private readonly OllamaService _ollamaService;
private readonly GeminiService _geminiService;
private readonly AnthropicService _anthropicService;
private readonly ILogger<GeneralLLMService> _logger;
private readonly ILLMFactory _LLMFactory;

Expand All @@ -40,21 +36,11 @@
IConnectionMultiplexer connectionMultiplexer,
DataDbContext dbContext,
ILogger<GeneralLLMService> logger,
OllamaService ollamaService,
OpenAIService openAIService,
GeminiService geminiService,
AnthropicService anthropicService,
ILLMFactory _LLMFactory
) {
this.connectionMultiplexer = connectionMultiplexer;
_dbContext = dbContext;
_logger = logger;

// Initialize services with default values
_openAIService = openAIService;
_ollamaService = ollamaService;
_geminiService = geminiService;
_anthropicService = anthropicService;
this._LLMFactory = _LLMFactory;
}
public async Task<List<LLMChannel>> GetChannelsAsync(string modelName) {
Expand Down Expand Up @@ -106,7 +92,7 @@
yield return e;
}
}
public async IAsyncEnumerable<string> ExecAsync(

Check warning on line 95 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Async-iterator 'GeneralLLMService.ExecAsync(Message, long, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed

Check warning on line 95 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Async-iterator 'GeneralLLMService.ExecAsync(Message, long, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
Model.Data.Message message,
long ChatId,
string modelName,
Expand Down Expand Up @@ -253,13 +239,13 @@
_logger.LogWarning($"未能获取 {modelName} 模型的图片分析结果");
return $"Error:未能获取 {modelName} 模型的图片分析结果";
}
public async IAsyncEnumerable<string> AnalyzeImageAsync(string PhotoPath, long ChatId, string modelName, ILLMService service, LLMChannel channel, CancellationToken cancellationToken = default) {

Check warning on line 242 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Async-iterator 'GeneralLLMService.AnalyzeImageAsync(string, long, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed

Check warning on line 242 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Async-iterator 'GeneralLLMService.AnalyzeImageAsync(string, long, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
await foreach (var result in AnalyzeImageAsync(PhotoPath, ChatId, modelName, service, channel, DefaultAltPhotoPrompt, cancellationToken)) {
yield return result;
}
}

public async IAsyncEnumerable<string> AnalyzeImageAsync(string PhotoPath, long ChatId, string modelName, ILLMService service, LLMChannel channel, string prompt, CancellationToken cancellationToken = default) {

Check warning on line 248 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Async-iterator 'GeneralLLMService.AnalyzeImageAsync(string, long, string, ILLMService, LLMChannel, string, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed

Check warning on line 248 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Async-iterator 'GeneralLLMService.AnalyzeImageAsync(string, long, string, ILLMService, LLMChannel, string, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
prompt = string.IsNullOrWhiteSpace(prompt) ? DefaultAltPhotoPrompt : prompt;
yield return await service.AnalyzeImageAsync(PhotoPath, modelName, channel, prompt);
yield break;
Expand Down Expand Up @@ -295,7 +281,7 @@
_logger.LogWarning($"未能获取 {modelName} 模型的嵌入向量");
return Array.Empty<float>();
}
public async IAsyncEnumerable<float[]> GenerateEmbeddingsAsync(string message, string modelName, ILLMService service, LLMChannel channel, CancellationToken cancellationToken = default) {

Check warning on line 284 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Async-iterator 'GeneralLLMService.GenerateEmbeddingsAsync(string, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed

Check warning on line 284 in TelegramSearchBot.LLM/Service/AI/LLM/GeneralLLMService.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Async-iterator 'GeneralLLMService.GenerateEmbeddingsAsync(string, string, ILLMService, LLMChannel, CancellationToken)' has one or more parameters of type 'CancellationToken' but none of them is decorated with the 'EnumeratorCancellation' attribute, so the cancellation token parameter from the generated 'IAsyncEnumerable<>.GetAsyncEnumerator' will be unconsumed
yield return await service.GenerateEmbeddingsAsync(message, modelName, channel);
yield break;
}
Expand Down
71 changes: 71 additions & 0 deletions TelegramSearchBot.LLM/Service/AI/LLM/GroupLlmSettingsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TelegramSearchBot.Attributes;
using TelegramSearchBot.Interface;
using TelegramSearchBot.Interface.AI.LLM;
using TelegramSearchBot.Model;
using TelegramSearchBot.Model.Data;

#nullable enable

namespace TelegramSearchBot.Service.AI.LLM {
[Injectable(ServiceLifetime.Scoped)]
public class GroupLlmSettingsService : IService, IGroupLlmSettingsService {
private readonly DataDbContext _dbContext;

public GroupLlmSettingsService(DataDbContext dbContext) {
_dbContext = dbContext;
}

public string ServiceName => nameof(GroupLlmSettingsService);

public async Task<string?> GetModelAsync(long chatId, CancellationToken cancellationToken = default) {
var settings = await _dbContext.GroupSettings.AsNoTracking()
.FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
return settings == null ? null : settings.LLMModelName;
}

public async Task<(string Previous, string Current)> SetModelAsync(long chatId, string modelName, CancellationToken cancellationToken = default) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
var normalizedModelName = modelName?.Trim();
if (string.IsNullOrWhiteSpace(normalizedModelName)) {
throw new ArgumentException("Model name cannot be empty.", nameof(modelName));
}

modelName = normalizedModelName;
var settings = await _dbContext.GroupSettings
.FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
var previous = settings == null ? null : settings.LLMModelName;
GroupSettings? newSettings = null;

if (settings is null) {
newSettings = new GroupSettings {
GroupId = chatId,
LLMModelName = modelName
};
await _dbContext.GroupSettings.AddAsync(newSettings, cancellationToken);
} else {
settings.LLMModelName = modelName;
}

try {
await _dbContext.SaveChangesAsync(cancellationToken);
} catch (DbUpdateException) when (newSettings != null) {
_dbContext.Entry(newSettings).State = EntityState.Detached;
settings = await _dbContext.GroupSettings
.FirstOrDefaultAsync(s => s.GroupId == chatId, cancellationToken);
if (settings is null) {
throw;
}

previous = settings.LLMModelName;
settings.LLMModelName = modelName;
await _dbContext.SaveChangesAsync(cancellationToken);
}

return (previous ?? "Default", modelName);
}
}
}
Loading
Loading