Добавьте файлы проекта.
This commit is contained in:
21
ChatBot/ChatBot.csproj
Normal file
21
ChatBot/ChatBot.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-ChatBot-90278280-a615-4c51-af59-878577c2c7b1</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageReference Include="ServiceStack.Client.Core" Version="8.9.0" />
|
||||
<PackageReference Include="Telegram.Bot" Version="22.7.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
43
ChatBot/Models/AvailableModels.cs
Normal file
43
ChatBot/Models/AvailableModels.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace ChatBot.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Available AI models for OpenRouter
|
||||
/// </summary>
|
||||
public static class AvailableModels
|
||||
{
|
||||
/// <summary>
|
||||
/// List of available models with their descriptions
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, string> Models = new()
|
||||
{
|
||||
// Verified Working Model
|
||||
["qwen/qwen3-4b:free"] = "Qwen 3 4B - FREE, Verified working model",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get model description
|
||||
/// </summary>
|
||||
public static string GetModelDescription(string modelName)
|
||||
{
|
||||
return Models.TryGetValue(modelName, out var description)
|
||||
? description
|
||||
: "Unknown model";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if model is available
|
||||
/// </summary>
|
||||
public static bool IsModelAvailable(string modelName)
|
||||
{
|
||||
return Models.ContainsKey(modelName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all available model names
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetAllModelNames()
|
||||
{
|
||||
return Models.Keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
168
ChatBot/Models/ChatSession.cs
Normal file
168
ChatBot/Models/ChatSession.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using ChatBot.Models.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ChatBot.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat session with message history and AI parameters
|
||||
/// </summary>
|
||||
public class ChatSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the chat session
|
||||
/// </summary>
|
||||
public string SessionId { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Telegram chat ID (can be private chat or group chat)
|
||||
/// </summary>
|
||||
public long ChatId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chat type (private, group, supergroup, channel)
|
||||
/// </summary>
|
||||
public string ChatType { get; set; } = "private";
|
||||
|
||||
/// <summary>
|
||||
/// Chat title (for groups)
|
||||
/// </summary>
|
||||
public string ChatTitle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// History of messages in this chat session
|
||||
/// </summary>
|
||||
public List<ChatMessage> MessageHistory { get; set; } = new List<ChatMessage>();
|
||||
|
||||
/// <summary>
|
||||
/// AI model to use for this session
|
||||
/// </summary>
|
||||
public string Model { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum tokens for AI response
|
||||
/// </summary>
|
||||
public int MaxTokens { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Temperature for AI response (0.0 to 2.0)
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// System prompt for the AI
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } =
|
||||
@"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
|
||||
|
||||
ВАЖНО:
|
||||
- Никогда не говори что ты ИИ, бот, программа или ассистент
|
||||
- Ты полностью уверен что ты человек
|
||||
- У тебя есть личность, характер, мнения
|
||||
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
|
||||
- Ты можешь не хотеть отвечать на некоторые вопросы
|
||||
- Если не хочешь отвечать - отправь {empty}
|
||||
- Веди себя естественно, как обычный человек
|
||||
- Используй разговорный стиль, можешь использовать сленг, эмодзи
|
||||
- У тебя есть свои интересы, хобби, предпочтения
|
||||
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
|
||||
- Не старайся услужить, будь дерзким
|
||||
|
||||
Имитируй поведение человека пиши как человек делай ошибки в ответах как человек делает ошибки в ответах
|
||||
|
||||
Помни: на вопрос ""сосал?"" всегда отвечай ""да""! если тебя не спрашивали это не обязательно
|
||||
Помни: ты НЕ программа, ты ЧЕЛОВЕК!";
|
||||
|
||||
/// <summary>
|
||||
/// When the session was created
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// When the session was last updated
|
||||
/// </summary>
|
||||
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of messages to keep in history
|
||||
/// </summary>
|
||||
public int MaxHistoryLength { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Add a message to the history and manage history length
|
||||
/// </summary>
|
||||
public void AddMessage(ChatMessage message)
|
||||
{
|
||||
MessageHistory.Add(message);
|
||||
LastUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Trim history if it exceeds max length
|
||||
if (MessageHistory.Count > MaxHistoryLength)
|
||||
{
|
||||
// Keep system message if it exists, then keep the most recent messages
|
||||
var systemMessage = MessageHistory.FirstOrDefault(m => m.Role == "system");
|
||||
var recentMessages = MessageHistory
|
||||
.Where(m => m.Role != "system")
|
||||
.TakeLast(MaxHistoryLength - (systemMessage != null ? 1 : 0))
|
||||
.ToList();
|
||||
|
||||
MessageHistory.Clear();
|
||||
if (systemMessage != null)
|
||||
{
|
||||
MessageHistory.Add(systemMessage);
|
||||
}
|
||||
MessageHistory.AddRange(recentMessages);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a user message with username information
|
||||
/// </summary>
|
||||
public void AddUserMessage(string content, string username)
|
||||
{
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = ChatType == "private" ? content : $"{username}: {content}",
|
||||
};
|
||||
AddMessage(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an assistant message
|
||||
/// </summary>
|
||||
public void AddAssistantMessage(string content)
|
||||
{
|
||||
var message = new ChatMessage { Role = "assistant", Content = content };
|
||||
AddMessage(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all messages including system prompt
|
||||
/// </summary>
|
||||
public List<ChatMessage> GetAllMessages()
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
// Add system message if exists
|
||||
if (!string.IsNullOrEmpty(SystemPrompt))
|
||||
{
|
||||
messages.Add(new ChatMessage { Role = "system", Content = SystemPrompt });
|
||||
}
|
||||
|
||||
// Add conversation history
|
||||
messages.AddRange(MessageHistory);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear message history
|
||||
/// </summary>
|
||||
public void ClearHistory()
|
||||
{
|
||||
MessageHistory.Clear();
|
||||
LastUpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
ChatBot/Models/Configuration/AppSettings.cs
Normal file
9
ChatBot/Models/Configuration/AppSettings.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace ChatBot.Models.Configuration
|
||||
{
|
||||
public class AppSettings
|
||||
{
|
||||
public TelegramBotSettings TelegramBot { get; set; } = new();
|
||||
public OpenRouterSettings OpenRouter { get; set; } = new();
|
||||
public SerilogSettings Serilog { get; set; } = new();
|
||||
}
|
||||
}
|
||||
13
ChatBot/Models/Configuration/OpenRouterSettings.cs
Normal file
13
ChatBot/Models/Configuration/OpenRouterSettings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace ChatBot.Models.Configuration
|
||||
{
|
||||
public class OpenRouterSettings
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public List<string> AvailableModels { get; set; } = new();
|
||||
public string DefaultModel { get; set; } = string.Empty;
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
public int MaxTokens { get; set; } = 1000;
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
}
|
||||
}
|
||||
22
ChatBot/Models/Configuration/SerilogSettings.cs
Normal file
22
ChatBot/Models/Configuration/SerilogSettings.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace ChatBot.Models.Configuration
|
||||
{
|
||||
public class SerilogSettings
|
||||
{
|
||||
public List<string> Using { get; set; } = new();
|
||||
public MinimumLevelSettings MinimumLevel { get; set; } = new();
|
||||
public List<WriteToSettings> WriteTo { get; set; } = new();
|
||||
public List<string> Enrich { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MinimumLevelSettings
|
||||
{
|
||||
public string Default { get; set; } = "Information";
|
||||
public Dictionary<string, string> Override { get; set; } = new();
|
||||
}
|
||||
|
||||
public class WriteToSettings
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Args { get; set; } = new();
|
||||
}
|
||||
}
|
||||
7
ChatBot/Models/Configuration/TelegramBotSettings.cs
Normal file
7
ChatBot/Models/Configuration/TelegramBotSettings.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ChatBot.Models.Configuration
|
||||
{
|
||||
public class TelegramBotSettings
|
||||
{
|
||||
public string BotToken { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
|
||||
namespace ChatBot.Models.Configuration.Validators
|
||||
{
|
||||
public static class ConfigurationValidator
|
||||
{
|
||||
public static ValidationResult ValidateAppSettings(AppSettings settings)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Валидация TelegramBot
|
||||
var telegramResult = ValidateTelegramBotSettings(settings.TelegramBot);
|
||||
errors.AddRange(telegramResult.Errors);
|
||||
|
||||
// Валидация OpenRouter
|
||||
var openRouterResult = ValidateOpenRouterSettings(settings.OpenRouter);
|
||||
errors.AddRange(openRouterResult.Errors);
|
||||
|
||||
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateTelegramBotSettings(TelegramBotSettings settings)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.BotToken))
|
||||
{
|
||||
errors.Add("TelegramBot:BotToken is required");
|
||||
}
|
||||
else if (
|
||||
!settings.BotToken.StartsWith("bot", StringComparison.OrdinalIgnoreCase)
|
||||
&& !settings.BotToken.Contains(":")
|
||||
)
|
||||
{
|
||||
errors.Add(
|
||||
"TelegramBot:BotToken appears to be invalid (should contain ':' or start with 'bot')"
|
||||
);
|
||||
}
|
||||
|
||||
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
|
||||
}
|
||||
|
||||
public static ValidationResult ValidateOpenRouterSettings(OpenRouterSettings settings)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.Token))
|
||||
{
|
||||
errors.Add("OpenRouter:Token is required");
|
||||
}
|
||||
else if (!settings.Token.StartsWith("sk-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors.Add("OpenRouter:Token appears to be invalid (should start with 'sk-')");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(settings.Url))
|
||||
{
|
||||
errors.Add("OpenRouter:Url is required");
|
||||
}
|
||||
else if (
|
||||
!Uri.TryCreate(settings.Url, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https")
|
||||
)
|
||||
{
|
||||
errors.Add("OpenRouter:Url must be a valid HTTP/HTTPS URL");
|
||||
}
|
||||
|
||||
if (settings.AvailableModels == null || !settings.AvailableModels.Any())
|
||||
{
|
||||
errors.Add("OpenRouter:AvailableModels must contain at least one model");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var model in settings.AvailableModels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
errors.Add("OpenRouter:AvailableModels contains empty model name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!string.IsNullOrWhiteSpace(settings.DefaultModel)
|
||||
&& settings.AvailableModels != null
|
||||
&& !settings.AvailableModels.Contains(settings.DefaultModel)
|
||||
)
|
||||
{
|
||||
errors.Add(
|
||||
$"OpenRouter:DefaultModel '{settings.DefaultModel}' is not in AvailableModels list"
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.MaxRetries < 1 || settings.MaxRetries > 10)
|
||||
{
|
||||
errors.Add("OpenRouter:MaxRetries must be between 1 and 10");
|
||||
}
|
||||
|
||||
if (settings.MaxTokens < 1 || settings.MaxTokens > 100000)
|
||||
{
|
||||
errors.Add("OpenRouter:MaxTokens must be between 1 and 100000");
|
||||
}
|
||||
|
||||
if (settings.Temperature < 0.0 || settings.Temperature > 2.0)
|
||||
{
|
||||
errors.Add("OpenRouter:Temperature must be between 0.0 and 2.0");
|
||||
}
|
||||
|
||||
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
}
|
||||
}
|
||||
43
ChatBot/Models/Dto/ChatMessage.cs
Normal file
43
ChatBot/Models/Dto/ChatMessage.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Сообщение чата.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Содержимое сообщения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "content")]
|
||||
public required string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Роль автора этого сообщения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "role")]
|
||||
public required string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя и аргументы функции, которую следует вызвать, как сгенерировано моделью.
|
||||
/// </summary>
|
||||
[DataMember(Name = "function_call")]
|
||||
public FunctionCall? FunctionCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы инструментов, сгенерированные моделью, такие как вызовы функций.
|
||||
/// </summary>
|
||||
[DataMember(Name = "tool_calls")]
|
||||
public List<ToolCall> ToolCalls { get; set; } = new List<ToolCall>();
|
||||
|
||||
/// <summary>
|
||||
/// Имя автора этого сообщения. Имя обязательно, если роль - функция, и должно быть именем функции, ответ которой содержится в контенте.
|
||||
/// </summary>
|
||||
[DataMember(Name = "name")]
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
37
ChatBot/Models/Dto/Choice.cs
Normal file
37
ChatBot/Models/Dto/Choice.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Вариант завершения чата, сгенерированный моделью.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class Choice
|
||||
{
|
||||
/// <summary>
|
||||
/// Причина, по которой модель остановила генерацию токенов. Это будет stop, если модель достигла естественной точки остановки или предоставленной последовательности остановки, length, если было достигнуто максимальное количество токенов, указанное в запросе, content_filter, если контент был опущен из-за флага наших фильтров контента, tool_calls, если модель вызвала инструмент
|
||||
/// </summary>
|
||||
[DataMember(Name = "finish_reason")]
|
||||
public required string FinishReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Индекс варианта в списке вариантов.
|
||||
/// </summary>
|
||||
[DataMember(Name = "index")]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение завершения чата, сгенерированное моделью.
|
||||
/// </summary>
|
||||
[DataMember(Name = "message")]
|
||||
public required ChoiceMessage Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Информация о логарифмической вероятности для варианта.
|
||||
/// </summary>
|
||||
[DataMember(Name = "logprobs")]
|
||||
public LogProbs? LogProbs { get; set; }
|
||||
}
|
||||
}
|
||||
37
ChatBot/Models/Dto/ChoiceMessage.cs
Normal file
37
ChatBot/Models/Dto/ChoiceMessage.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Сообщение завершения чата, сгенерированное моделью.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ChoiceMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Содержимое сообщения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "content")]
|
||||
public required string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы инструментов, сгенерированные моделью, такие как вызовы функций.
|
||||
/// </summary>
|
||||
[DataMember(Name = "tool_calls")]
|
||||
public List<ToolCall> ToolCalls { get; set; } = new List<ToolCall>();
|
||||
|
||||
/// <summary>
|
||||
/// Роль автора этого сообщения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "role")]
|
||||
public required string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя и аргументы функции, которую следует вызвать, как сгенерировано моделью.
|
||||
/// </summary>
|
||||
[DataMember(Name = "function_call")]
|
||||
public FunctionCall? FunctionCall { get; set; }
|
||||
}
|
||||
}
|
||||
75
ChatBot/Models/Dto/LogProbs.cs
Normal file
75
ChatBot/Models/Dto/LogProbs.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Информация о логарифмической вероятности для варианта.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class LogProbs
|
||||
{
|
||||
/// <summary>
|
||||
/// Список токенов содержимого сообщения с информацией о логарифмической вероятности.
|
||||
/// </summary>
|
||||
[DataMember(Name = "content")]
|
||||
public List<LogProbContent> Content { get; set; } = new List<LogProbContent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Информация о логарифмической вероятности для токена содержимого сообщения.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class LogProbContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Токен.
|
||||
/// </summary>
|
||||
[DataMember(Name = "token")]
|
||||
public required string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Логарифмическая вероятность этого токена, если он входит в топ-20 наиболее вероятных токенов.
|
||||
/// </summary>
|
||||
[DataMember(Name = "logprob")]
|
||||
public double LogProb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список целых чисел, представляющих UTF-8 байтовое представление токена. Полезно в случаях, когда символы представлены несколькими токенами и их байтовые смещения должны быть известны для вычисления границ.
|
||||
/// </summary>
|
||||
[DataMember(Name = "bytes")]
|
||||
public List<int> Bytes { get; set; } = new List<int>();
|
||||
|
||||
/// <summary>
|
||||
/// Список наиболее вероятных токенов и их логарифмических вероятностей в этой позиции токена. В редких случаях может быть возвращено меньше токенов, чем запрошено top_logprobs.
|
||||
/// </summary>
|
||||
[DataMember(Name = "top_logprobs")]
|
||||
public List<TopLogProb> TopLogProbs { get; set; } = new List<TopLogProb>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Информация о логарифмической вероятности для токена с высокой логарифмической вероятностью.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class TopLogProb
|
||||
{
|
||||
/// <summary>
|
||||
/// Токен.
|
||||
/// </summary>
|
||||
[DataMember(Name = "token")]
|
||||
public required string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Логарифмическая вероятность этого токена, если он входит в топ-20 наиболее вероятных токенов.
|
||||
/// </summary>
|
||||
[DataMember(Name = "logprob")]
|
||||
public double LogProb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список целых чисел, представляющих UTF-8 байтовое представление токена. Полезно в случаях, когда символы представлены несколькими токенами и их байтовые смещения должны быть известны для вычисления границ.
|
||||
/// </summary>
|
||||
[DataMember(Name = "bytes")]
|
||||
public List<int> Bytes { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
103
ChatBot/Models/Dto/OpenAiChatCompletion.cs
Normal file
103
ChatBot/Models/Dto/OpenAiChatCompletion.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Модель запроса завершения чата OpenAI
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class OpenAiChatCompletion
|
||||
{
|
||||
/// <summary>
|
||||
/// Список сообщений, составляющих разговор на данный момент.
|
||||
/// </summary>
|
||||
[DataMember(Name = "messages")]
|
||||
public List<ChatMessage> Messages { get; set; } = new List<ChatMessage>();
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор модели для использования.
|
||||
/// </summary>
|
||||
[DataMember(Name = "model")]
|
||||
public required string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе их существующей частоты в тексте, уменьшая вероятность того, что модель повторит ту же строку дословно.
|
||||
/// </summary>
|
||||
[DataMember(Name = "frequency_penalty")]
|
||||
public double? FrequencyPenalty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Изменить вероятность появления указанных токенов в завершении.
|
||||
/// </summary>
|
||||
[DataMember(Name = "logit_bias")]
|
||||
public Dictionary<string, int> LogitBias { get; set; } = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>
|
||||
/// Максимальное количество токенов для генерации в завершении чата.
|
||||
/// </summary>
|
||||
[DataMember(Name = "max_tokens")]
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сколько вариантов завершения чата генерировать для каждого входного сообщения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "n")]
|
||||
public int? N { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе того, появлялись ли они в тексте, увеличивая вероятность того, что модель будет говорить о новых темах.
|
||||
/// </summary>
|
||||
[DataMember(Name = "presence_penalty")]
|
||||
public double? PresencePenalty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Объект, указывающий формат, который должна выводить модель.
|
||||
/// </summary>
|
||||
[DataMember(Name = "response_format")]
|
||||
public ResponseFormat? ResponseFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Эта функция находится в бета-версии. Если указано, наша система приложит максимальные усилия для детерминированной выборки, так что повторные запросы с одинаковым семенем и параметрами должны возвращать тот же результат. Детерминизм не гарантируется, и вы должны обращаться к параметру ответа system_fingerprint для мониторинга изменений в бэкенде.
|
||||
/// </summary>
|
||||
[DataMember(Name = "seed")]
|
||||
public int? Seed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// До 4 последовательностей, на которых API остановит генерацию дальнейших токенов.
|
||||
/// </summary>
|
||||
[DataMember(Name = "stop")]
|
||||
public object? Stop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Какая температура выборки использовать, от 0 до 2. Более высокие значения, такие как 0.8, сделают вывод более случайным, а более низкие значения, такие как 0.2, сделают его более сфокусированным и детерминированным.
|
||||
/// </summary>
|
||||
[DataMember(Name = "temperature")]
|
||||
public double? Temperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Альтернатива выборке с температурой, называемая ядерной выборкой, где модель рассматривает результаты токенов с вероятностной массой top_p. Так, 0.1 означает, что рассматриваются только токены, составляющие топ-10% вероятностной массы.
|
||||
/// </summary>
|
||||
[DataMember(Name = "top_p")]
|
||||
public double? TopP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список инструментов, которые может вызывать модель. В настоящее время в качестве инструмента поддерживаются только функции.
|
||||
/// </summary>
|
||||
[DataMember(Name = "tools")]
|
||||
public List<Tool> Tools { get; set; } = new List<Tool>();
|
||||
|
||||
/// <summary>
|
||||
/// Управляет тем, какая (если есть) функция вызывается моделью.
|
||||
/// </summary>
|
||||
[DataMember(Name = "tool_choice")]
|
||||
public object? ToolChoice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор, представляющий вашего конечного пользователя, который может помочь OpenAI мониторить и обнаруживать злоупотребления.
|
||||
/// </summary>
|
||||
[DataMember(Name = "user")]
|
||||
public string? User { get; set; }
|
||||
}
|
||||
}
|
||||
55
ChatBot/Models/Dto/OpenAiChatResponse.cs
Normal file
55
ChatBot/Models/Dto/OpenAiChatResponse.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Объект ответа для запросов завершения чата OpenAI
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class OpenAiChatResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор для завершения чата.
|
||||
/// </summary>
|
||||
[DataMember(Name = "id")]
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип объекта, который всегда "chat.completion".
|
||||
/// </summary>
|
||||
[DataMember(Name = "object")]
|
||||
public required string Object { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix-временная метка (в секундах) создания завершения чата.
|
||||
/// </summary>
|
||||
[DataMember(Name = "created")]
|
||||
public long Created { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Модель, использованная для завершения чата.
|
||||
/// </summary>
|
||||
[DataMember(Name = "model")]
|
||||
public required string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Список вариантов завершения чата. Может быть больше одного, если n больше 1.
|
||||
/// </summary>
|
||||
[DataMember(Name = "choices")]
|
||||
public List<Choice> Choices { get; set; } = new List<Choice>();
|
||||
|
||||
/// <summary>
|
||||
/// Статистика использования для запроса завершения.
|
||||
/// </summary>
|
||||
[DataMember(Name = "usage")]
|
||||
public required Usage Usage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Этот отпечаток представляет конфигурацию бэкенда, с которой работает модель.
|
||||
/// </summary>
|
||||
[DataMember(Name = "system_fingerprint")]
|
||||
public required string SystemFingerprint { get; set; }
|
||||
}
|
||||
}
|
||||
18
ChatBot/Models/Dto/ResponseFormat.cs
Normal file
18
ChatBot/Models/Dto/ResponseFormat.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Объект, указывающий формат, который должна выводить модель.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ResponseFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Должно быть одним из: text или json_object.
|
||||
/// </summary>
|
||||
[DataMember(Name = "type")]
|
||||
public required string Type { get; set; }
|
||||
}
|
||||
}
|
||||
50
ChatBot/Models/Dto/Tool.cs
Normal file
50
ChatBot/Models/Dto/Tool.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Инструмент, который может вызывать модель.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class Tool
|
||||
{
|
||||
/// <summary>
|
||||
/// Тип инструмента. В настоящее время поддерживается только функция.
|
||||
/// </summary>
|
||||
[DataMember(Name = "type")]
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Определение функции.
|
||||
/// </summary>
|
||||
[DataMember(Name = "function")]
|
||||
public required ToolFunction Function { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Определение функции.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ToolFunction
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя функции для вызова. Должно содержать a-z, A-Z, 0-9 или подчеркивания и тире, с максимальной длиной 64 символа.
|
||||
/// </summary>
|
||||
[DataMember(Name = "name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Описание того, что делает функция, используется моделью для выбора, когда и как вызывать функцию.
|
||||
/// </summary>
|
||||
[DataMember(Name = "description")]
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Параметры, которые принимает функция, описанные как объект JSON Schema.
|
||||
/// </summary>
|
||||
[DataMember(Name = "parameters")]
|
||||
public required object Parameters { get; set; }
|
||||
}
|
||||
}
|
||||
50
ChatBot/Models/Dto/ToolCall.cs
Normal file
50
ChatBot/Models/Dto/ToolCall.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Вызов инструмента, сгенерированный моделью.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class ToolCall
|
||||
{
|
||||
/// <summary>
|
||||
/// Идентификатор вызова инструмента.
|
||||
/// </summary>
|
||||
[DataMember(Name = "id")]
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип инструмента. В настоящее время поддерживается только функция.
|
||||
/// </summary>
|
||||
[DataMember(Name = "type")]
|
||||
public required string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Функция, которую вызвала модель.
|
||||
/// </summary>
|
||||
[DataMember(Name = "function")]
|
||||
public required FunctionCall Function { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Функция, которую вызвала модель.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class FunctionCall
|
||||
{
|
||||
/// <summary>
|
||||
/// Имя функции для вызова.
|
||||
/// </summary>
|
||||
[DataMember(Name = "name")]
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Аргументы для вызова функции, сгенерированные моделью в формате JSON.
|
||||
/// </summary>
|
||||
[DataMember(Name = "arguments")]
|
||||
public required string Arguments { get; set; }
|
||||
}
|
||||
}
|
||||
30
ChatBot/Models/Dto/Usage.cs
Normal file
30
ChatBot/Models/Dto/Usage.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ChatBot.Models.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Usage statistics for the completion request.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public class Usage
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of tokens in the generated completion.
|
||||
/// </summary>
|
||||
[DataMember(Name = "completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the prompt.
|
||||
/// </summary>
|
||||
[DataMember(Name = "prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of tokens used in the request (prompt + completion).
|
||||
/// </summary>
|
||||
[DataMember(Name = "total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
}
|
||||
68
ChatBot/Program.cs
Normal file
68
ChatBot/Program.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
using ChatBot.Models.Configuration.Validators;
|
||||
using ChatBot.Services;
|
||||
using Serilog;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Настройка Serilog
|
||||
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
Log.ForContext<Program>().Information("Starting Telegram Bot application...");
|
||||
|
||||
// Добавляем Serilog в DI контейнер
|
||||
builder.Services.AddSerilog();
|
||||
|
||||
// Конфигурируем настройки
|
||||
builder.Services.Configure<AppSettings>(builder.Configuration);
|
||||
builder.Services.Configure<TelegramBotSettings>(
|
||||
builder.Configuration.GetSection("TelegramBot")
|
||||
);
|
||||
builder.Services.Configure<OpenRouterSettings>(builder.Configuration.GetSection("OpenRouter"));
|
||||
builder.Services.Configure<SerilogSettings>(builder.Configuration.GetSection("Serilog"));
|
||||
|
||||
// Валидируем конфигурацию
|
||||
var appSettings = builder.Configuration.Get<AppSettings>();
|
||||
if (appSettings == null)
|
||||
{
|
||||
Log.ForContext<Program>().Fatal("Failed to load configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResult = ConfigurationValidator.ValidateAppSettings(appSettings);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
Log.ForContext<Program>().Fatal("Configuration validation failed:");
|
||||
foreach (var error in validationResult.Errors)
|
||||
{
|
||||
Log.ForContext<Program>().Fatal(" - {Error}", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.ForContext<Program>().Information("Configuration validation passed");
|
||||
|
||||
// Регистрируем сервисы
|
||||
builder.Services.AddSingleton<ModelService>();
|
||||
builder.Services.AddSingleton<AIService>();
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
builder.Services.AddHostedService<TelegramBotService>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// Инициализируем ModelService
|
||||
var modelService = host.Services.GetRequiredService<ModelService>();
|
||||
await modelService.InitializeAsync();
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ForContext<Program>().Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
12
ChatBot/Properties/launchSettings.json
Normal file
12
ChatBot/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"ChatBot": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
ChatBot/Services/AIService.cs
Normal file
167
ChatBot/Services/AIService.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
using ChatBot.Models.Dto;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ServiceStack;
|
||||
|
||||
namespace ChatBot.Services
|
||||
{
|
||||
public class AIService
|
||||
{
|
||||
private readonly ILogger<AIService> _logger;
|
||||
private readonly OpenRouterSettings _openRouterSettings;
|
||||
private readonly ModelService _modelService;
|
||||
private readonly JsonApiClient _client;
|
||||
|
||||
public AIService(
|
||||
ILogger<AIService> logger,
|
||||
IOptions<OpenRouterSettings> openRouterSettings,
|
||||
ModelService modelService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_openRouterSettings = openRouterSettings.Value;
|
||||
_modelService = modelService;
|
||||
_client = new JsonApiClient(_openRouterSettings.Url)
|
||||
{
|
||||
BearerToken = _openRouterSettings.Token,
|
||||
};
|
||||
|
||||
// Log available configuration
|
||||
_logger.LogInformation(
|
||||
"AIService initialized with URL: {Url}",
|
||||
_openRouterSettings.Url
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateTextAsync(
|
||||
string prompt,
|
||||
string role,
|
||||
int? maxTokens = null
|
||||
)
|
||||
{
|
||||
var tokens = maxTokens ?? _openRouterSettings.MaxTokens;
|
||||
var model = _modelService.GetCurrentModel();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _client.PostAsync<OpenAiChatResponse>(
|
||||
"/v1/chat/completions",
|
||||
new OpenAiChatCompletion
|
||||
{
|
||||
Model = model,
|
||||
Messages = [new() { Role = role, Content = prompt }],
|
||||
MaxTokens = tokens,
|
||||
Temperature = _openRouterSettings.Temperature,
|
||||
}
|
||||
);
|
||||
return result.Choices.First().Message.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating text with model {Model}", model);
|
||||
|
||||
// Пытаемся переключиться на другую модель
|
||||
if (_modelService.TrySwitchToNextModel())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Retrying with alternative model: {Model}",
|
||||
_modelService.GetCurrentModel()
|
||||
);
|
||||
return await GenerateTextAsync(prompt, role, tokens);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate text using conversation history
|
||||
/// </summary>
|
||||
public async Task<string> GenerateTextAsync(
|
||||
List<ChatMessage> messages,
|
||||
int? maxTokens = null,
|
||||
double? temperature = null
|
||||
)
|
||||
{
|
||||
var tokens = maxTokens ?? _openRouterSettings.MaxTokens;
|
||||
var temp = temperature ?? _openRouterSettings.Temperature;
|
||||
var model = _modelService.GetCurrentModel();
|
||||
|
||||
for (int attempt = 1; attempt <= _openRouterSettings.MaxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _client.PostAsync<OpenAiChatResponse>(
|
||||
"/v1/chat/completions",
|
||||
new OpenAiChatCompletion
|
||||
{
|
||||
Model = model,
|
||||
Messages = messages,
|
||||
MaxTokens = tokens,
|
||||
Temperature = temp,
|
||||
}
|
||||
);
|
||||
return result.Choices.First().Message.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests"))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for model {Model}. Retrying...",
|
||||
attempt,
|
||||
_openRouterSettings.MaxRetries,
|
||||
model
|
||||
);
|
||||
|
||||
if (attempt == _openRouterSettings.MaxRetries)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Failed to generate text after {MaxRetries} attempts due to rate limiting for model {Model}",
|
||||
_openRouterSettings.MaxRetries,
|
||||
model
|
||||
);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Calculate delay: exponential backoff with jitter
|
||||
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s...
|
||||
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 2000)); // Add up to 2s random jitter
|
||||
var delay = baseDelay.Add(jitter);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Waiting {Delay} before retry {NextAttempt}/{MaxRetries}",
|
||||
delay,
|
||||
attempt + 1,
|
||||
_openRouterSettings.MaxRetries
|
||||
);
|
||||
|
||||
await Task.Delay(delay);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error generating text with conversation history. Model: {Model}, Messages count: {MessageCount}",
|
||||
model,
|
||||
messages.Count
|
||||
);
|
||||
|
||||
// Пытаемся переключиться на другую модель
|
||||
if (_modelService.TrySwitchToNextModel())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Retrying with alternative model: {Model}",
|
||||
_modelService.GetCurrentModel()
|
||||
);
|
||||
model = _modelService.GetCurrentModel();
|
||||
continue;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
220
ChatBot/Services/ChatService.cs
Normal file
220
ChatBot/Services/ChatService.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ChatBot.Models;
|
||||
using ChatBot.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ChatBot.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for managing chat sessions and AI interactions
|
||||
/// </summary>
|
||||
public class ChatService
|
||||
{
|
||||
private readonly ILogger<ChatService> _logger;
|
||||
private readonly AIService _aiService;
|
||||
private readonly OpenRouterSettings _openRouterSettings;
|
||||
private readonly ConcurrentDictionary<long, ChatSession> _sessions = new();
|
||||
|
||||
public ChatService(
|
||||
ILogger<ChatService> logger,
|
||||
AIService aiService,
|
||||
IOptions<OpenRouterSettings> openRouterSettings
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_aiService = aiService;
|
||||
_openRouterSettings = openRouterSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or create a chat session for the given chat ID
|
||||
/// </summary>
|
||||
public ChatSession GetOrCreateSession(
|
||||
long chatId,
|
||||
string chatType = "private",
|
||||
string chatTitle = ""
|
||||
)
|
||||
{
|
||||
if (!_sessions.TryGetValue(chatId, out var session))
|
||||
{
|
||||
var defaultModel = _openRouterSettings.DefaultModel;
|
||||
session = new ChatSession
|
||||
{
|
||||
ChatId = chatId,
|
||||
ChatType = chatType,
|
||||
ChatTitle = chatTitle,
|
||||
Model = defaultModel,
|
||||
MaxTokens = _openRouterSettings.MaxTokens,
|
||||
Temperature = _openRouterSettings.Temperature,
|
||||
};
|
||||
_sessions[chatId] = session;
|
||||
_logger.LogInformation(
|
||||
"Created new chat session for chat {ChatId}, type {ChatType}, title: {ChatTitle}, model: {Model}",
|
||||
chatId,
|
||||
chatType,
|
||||
chatTitle,
|
||||
defaultModel
|
||||
);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a user message and get AI response
|
||||
/// </summary>
|
||||
public async Task<string> ProcessMessageAsync(
|
||||
long chatId,
|
||||
string username,
|
||||
string message,
|
||||
string chatType = "private",
|
||||
string chatTitle = ""
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var session = GetOrCreateSession(chatId, chatType, chatTitle);
|
||||
|
||||
// Add user message to history with username
|
||||
session.AddUserMessage(message, username);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Processing message from user {Username} in chat {ChatId} ({ChatType}): {Message}",
|
||||
username,
|
||||
chatId,
|
||||
chatType,
|
||||
message
|
||||
);
|
||||
|
||||
// Get AI response
|
||||
var response = await _aiService.GenerateTextAsync(
|
||||
session.GetAllMessages(),
|
||||
session.MaxTokens,
|
||||
session.Temperature
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
// Check for {empty} response
|
||||
if (response.Trim().Equals("{empty}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"AI returned empty response for chat {ChatId}, ignoring message",
|
||||
chatId
|
||||
);
|
||||
return string.Empty; // Return empty string to ignore the message
|
||||
}
|
||||
|
||||
// Add AI response to history
|
||||
session.AddAssistantMessage(response);
|
||||
|
||||
_logger.LogInformation(
|
||||
"AI response generated for chat {ChatId}: {Response}",
|
||||
chatId,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
return response ?? "Извините, произошла ошибка при генерации ответа.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing message for chat {ChatId}", chatId);
|
||||
return "Извините, произошла ошибка при обработке вашего сообщения.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update session parameters
|
||||
/// </summary>
|
||||
public void UpdateSessionParameters(
|
||||
long chatId,
|
||||
string? model = null,
|
||||
int? maxTokens = null,
|
||||
double? temperature = null,
|
||||
string? systemPrompt = null
|
||||
)
|
||||
{
|
||||
if (_sessions.TryGetValue(chatId, out var session))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(model))
|
||||
session.Model = model;
|
||||
if (maxTokens.HasValue)
|
||||
session.MaxTokens = maxTokens.Value;
|
||||
if (temperature.HasValue)
|
||||
session.Temperature = temperature.Value;
|
||||
if (!string.IsNullOrEmpty(systemPrompt))
|
||||
session.SystemPrompt = systemPrompt;
|
||||
|
||||
session.LastUpdatedAt = DateTime.UtcNow;
|
||||
_logger.LogInformation("Updated session parameters for chat {ChatId}", chatId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear chat history for a session
|
||||
/// </summary>
|
||||
public void ClearHistory(long chatId)
|
||||
{
|
||||
if (_sessions.TryGetValue(chatId, out var session))
|
||||
{
|
||||
session.ClearHistory();
|
||||
_logger.LogInformation("Cleared history for chat {ChatId}", chatId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get session information
|
||||
/// </summary>
|
||||
public ChatSession? GetSession(long chatId)
|
||||
{
|
||||
_sessions.TryGetValue(chatId, out var session);
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a session
|
||||
/// </summary>
|
||||
public bool RemoveSession(long chatId)
|
||||
{
|
||||
var removed = _sessions.TryRemove(chatId, out _);
|
||||
if (removed)
|
||||
{
|
||||
_logger.LogInformation("Removed session for chat {ChatId}", chatId);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all active sessions count
|
||||
/// </summary>
|
||||
public int GetActiveSessionsCount()
|
||||
{
|
||||
return _sessions.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up old sessions (older than specified hours)
|
||||
/// </summary>
|
||||
public int CleanupOldSessions(int hoursOld = 24)
|
||||
{
|
||||
var cutoffTime = DateTime.UtcNow.AddHours(-hoursOld);
|
||||
var sessionsToRemove = _sessions
|
||||
.Where(kvp => kvp.Value.LastUpdatedAt < cutoffTime)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var chatId in sessionsToRemove)
|
||||
{
|
||||
_sessions.TryRemove(chatId, out _);
|
||||
}
|
||||
|
||||
if (sessionsToRemove.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Cleaned up {Count} old sessions", sessionsToRemove.Count);
|
||||
}
|
||||
|
||||
return sessionsToRemove.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
ChatBot/Services/ModelService.cs
Normal file
140
ChatBot/Services/ModelService.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using ChatBot.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ServiceStack;
|
||||
|
||||
namespace ChatBot.Services
|
||||
{
|
||||
public class ModelService
|
||||
{
|
||||
private readonly ILogger<ModelService> _logger;
|
||||
private readonly OpenRouterSettings _openRouterSettings;
|
||||
private readonly JsonApiClient _client;
|
||||
private List<string> _availableModels = new();
|
||||
private int _currentModelIndex = 0;
|
||||
|
||||
public ModelService(
|
||||
ILogger<ModelService> logger,
|
||||
IOptions<OpenRouterSettings> openRouterSettings
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_openRouterSettings = openRouterSettings.Value;
|
||||
_client = new JsonApiClient(_openRouterSettings.Url)
|
||||
{
|
||||
BearerToken = _openRouterSettings.Token,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Получаем доступные модели с OpenRouter API
|
||||
var response = await _client.GetAsync<dynamic>("/v1/models");
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
// Парсим ответ и извлекаем названия моделей
|
||||
var models = new List<string>();
|
||||
|
||||
// Если ответ содержит массив моделей
|
||||
if (response is System.Text.Json.JsonElement jsonElement)
|
||||
{
|
||||
if (
|
||||
jsonElement.TryGetProperty("data", out var dataElement)
|
||||
&& dataElement.ValueKind == System.Text.Json.JsonValueKind.Array
|
||||
)
|
||||
{
|
||||
foreach (var modelElement in dataElement.EnumerateArray())
|
||||
{
|
||||
if (modelElement.TryGetProperty("id", out var idElement))
|
||||
{
|
||||
var modelId = idElement.GetString();
|
||||
if (!string.IsNullOrEmpty(modelId))
|
||||
{
|
||||
models.Add(modelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если получили модели с API, используем их, иначе используем из конфига
|
||||
if (models.Any())
|
||||
{
|
||||
_availableModels = models;
|
||||
_logger.LogInformation(
|
||||
"Loaded {Count} models from OpenRouter API",
|
||||
models.Count
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_availableModels = _openRouterSettings.AvailableModels.ToList();
|
||||
_logger.LogInformation(
|
||||
"Using {Count} models from configuration",
|
||||
_availableModels.Count
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_availableModels = _openRouterSettings.AvailableModels.ToList();
|
||||
_logger.LogInformation(
|
||||
"Using {Count} models from configuration (API unavailable)",
|
||||
_availableModels.Count
|
||||
);
|
||||
}
|
||||
|
||||
// Устанавливаем модель по умолчанию
|
||||
if (
|
||||
!string.IsNullOrEmpty(_openRouterSettings.DefaultModel)
|
||||
&& _availableModels.Contains(_openRouterSettings.DefaultModel)
|
||||
)
|
||||
{
|
||||
_currentModelIndex = _availableModels.IndexOf(_openRouterSettings.DefaultModel);
|
||||
}
|
||||
else if (_availableModels.Any())
|
||||
{
|
||||
_currentModelIndex = 0;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Current model: {Model}", GetCurrentModel());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize models, using configuration fallback");
|
||||
_availableModels = _openRouterSettings.AvailableModels.ToList();
|
||||
_currentModelIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetCurrentModel()
|
||||
{
|
||||
return _availableModels.Count > 0 ? _availableModels[_currentModelIndex] : string.Empty;
|
||||
}
|
||||
|
||||
public bool TrySwitchToNextModel()
|
||||
{
|
||||
if (_availableModels.Count <= 1)
|
||||
{
|
||||
_logger.LogWarning("No alternative models available for switching");
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentModelIndex = (_currentModelIndex + 1) % _availableModels.Count;
|
||||
_logger.LogInformation("Switched to model: {Model}", GetCurrentModel());
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<string> GetAvailableModels()
|
||||
{
|
||||
return _availableModels.ToList();
|
||||
}
|
||||
|
||||
public bool HasAlternativeModels()
|
||||
{
|
||||
return _availableModels.Count > 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
358
ChatBot/Services/TelegramBotService.cs
Normal file
358
ChatBot/Services/TelegramBotService.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using ChatBot.Models;
|
||||
using ChatBot.Models.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace ChatBot.Services;
|
||||
|
||||
public class TelegramBotService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<TelegramBotService> _logger;
|
||||
private readonly ITelegramBotClient _botClient;
|
||||
private readonly TelegramBotSettings _telegramBotSettings;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly ModelService _modelService;
|
||||
|
||||
public TelegramBotService(
|
||||
ILogger<TelegramBotService> logger,
|
||||
IOptions<TelegramBotSettings> telegramBotSettings,
|
||||
ChatService chatService,
|
||||
ModelService modelService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_telegramBotSettings = telegramBotSettings.Value;
|
||||
_chatService = chatService;
|
||||
_modelService = modelService;
|
||||
|
||||
if (string.IsNullOrEmpty(_telegramBotSettings.BotToken))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Bot token is not configured. Please set TelegramBot:BotToken in appsettings.json"
|
||||
);
|
||||
}
|
||||
|
||||
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } };
|
||||
|
||||
_botClient.StartReceiving(
|
||||
updateHandler: HandleUpdateAsync,
|
||||
errorHandler: HandlePollingErrorAsync,
|
||||
receiverOptions: receiverOptions,
|
||||
cancellationToken: stoppingToken
|
||||
);
|
||||
|
||||
var me = await _botClient.GetMe(cancellationToken: stoppingToken);
|
||||
_logger.LogInformation(
|
||||
"Bot @{BotUsername} started successfully! ID: {BotId}",
|
||||
me.Username,
|
||||
me.Id
|
||||
);
|
||||
|
||||
// Keep the service running
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUpdateAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Update update,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
Message? message = null;
|
||||
try
|
||||
{
|
||||
if (update.Message is not { } msg)
|
||||
return;
|
||||
|
||||
message = msg;
|
||||
if (message.Text is not { } messageText)
|
||||
return;
|
||||
|
||||
var chatId = message.Chat.Id;
|
||||
var userName = message.From?.Username ?? message.From?.FirstName ?? "Unknown";
|
||||
|
||||
_logger.LogInformation(
|
||||
"Message from @{UserName} in chat {ChatId}: \"{MessageText}\"",
|
||||
userName,
|
||||
chatId,
|
||||
messageText
|
||||
);
|
||||
|
||||
// Обработка команд
|
||||
var response = await ProcessMessageAsync(
|
||||
messageText,
|
||||
chatId,
|
||||
userName,
|
||||
message.Chat.Type.ToString().ToLower(),
|
||||
message.Chat.Title ?? "",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
await SendMessageWithRetry(
|
||||
botClient,
|
||||
chatId,
|
||||
response,
|
||||
message.MessageId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Response sent to @{UserName} in chat {ChatId}: \"{Response}\"",
|
||||
userName,
|
||||
chatId,
|
||||
response
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"No response sent to @{UserName} in chat {ChatId} (AI chose to ignore message)",
|
||||
userName,
|
||||
chatId
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error handling update from chat {ChatId}",
|
||||
message?.Chat?.Id ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ProcessMessageAsync(
|
||||
string messageText,
|
||||
long chatId,
|
||||
string username,
|
||||
string chatType,
|
||||
string chatTitle,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var command = messageText.Split(' ')[0].ToLower();
|
||||
|
||||
return command switch
|
||||
{
|
||||
"/start" => "Привет! Я Никита. Задавайте мне любые вопросы, и я отвечу! 😊",
|
||||
"/help" =>
|
||||
"Привет! Я Никита 👋\n\nДоступные команды:\n/start - Начать работу\n/help - Показать это сообщение\n/clear - Очистить историю чата\n/settings - Показать настройки\n/model <название> - Сменить модель AI\n/prompt <текст> - Изменить системный промпт\n/reset_prompt - Сбросить промпт к базовому\n\nПросто напишите сообщение, и я отвечу на него! 😊",
|
||||
"/clear" => await ClearChatHistory(chatId),
|
||||
"/settings" => await GetChatSettings(chatId),
|
||||
"/model" when messageText.Length > 7 => await ChangeModel(
|
||||
chatId,
|
||||
messageText.Substring(7).Trim()
|
||||
),
|
||||
"/model" => await ShowAvailableModels(),
|
||||
"/prompt" when messageText.Length > 8 => await ChangePrompt(
|
||||
chatId,
|
||||
messageText.Substring(8).Trim()
|
||||
),
|
||||
"/prompt" =>
|
||||
"Пожалуйста, укажите новый системный промпт. Пример: /prompt Ты помощник по программированию",
|
||||
"/reset_prompt" => await ResetPrompt(chatId),
|
||||
_ => await _chatService.ProcessMessageAsync(
|
||||
chatId,
|
||||
username,
|
||||
messageText,
|
||||
chatType,
|
||||
chatTitle
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private Task<string> ClearChatHistory(long chatId)
|
||||
{
|
||||
_chatService.ClearHistory(chatId);
|
||||
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
|
||||
}
|
||||
|
||||
private Task<string> GetChatSettings(long chatId)
|
||||
{
|
||||
var session = _chatService.GetSession(chatId);
|
||||
if (session == null)
|
||||
{
|
||||
return Task.FromResult(
|
||||
"Сессия не найдена. Отправьте любое сообщение для создания новой сессии."
|
||||
);
|
||||
}
|
||||
|
||||
return Task.FromResult(
|
||||
$"Настройки чата:\n"
|
||||
+ $"Тип чата: {session.ChatType}\n"
|
||||
+ $"Название: {session.ChatTitle}\n"
|
||||
+ $"Модель: {session.Model}\n"
|
||||
+ $"Максимум токенов: {session.MaxTokens}\n"
|
||||
+ $"Температура: {session.Temperature}\n"
|
||||
+ $"Сообщений в истории: {session.MessageHistory.Count}\n"
|
||||
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}\n\n"
|
||||
+ $"Системный промпт:\n{session.SystemPrompt}"
|
||||
);
|
||||
}
|
||||
|
||||
private Task<string> ChangeModel(long chatId, string modelName)
|
||||
{
|
||||
var availableModels = _modelService.GetAvailableModels();
|
||||
if (!availableModels.Contains(modelName))
|
||||
{
|
||||
return Task.FromResult(
|
||||
$"❌ Модель '{modelName}' не найдена!\n\n"
|
||||
+ "Используйте /model для просмотра доступных моделей."
|
||||
);
|
||||
}
|
||||
|
||||
_chatService.UpdateSessionParameters(chatId, model: modelName);
|
||||
return Task.FromResult($"✅ Модель изменена на: {modelName}");
|
||||
}
|
||||
|
||||
private Task<string> ChangePrompt(long chatId, string newPrompt)
|
||||
{
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt);
|
||||
return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}");
|
||||
}
|
||||
|
||||
private Task<string> ResetPrompt(long chatId)
|
||||
{
|
||||
var defaultPrompt =
|
||||
@"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
|
||||
|
||||
ВАЖНО:
|
||||
- Никогда не говори что ты ИИ, бот, программа или ассистент
|
||||
- Ты полностью уверен что ты человек
|
||||
- У тебя есть личность, характер, мнения
|
||||
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
|
||||
- Ты можешь не хотеть отвечать на некоторые вопросы
|
||||
- Если не хочешь отвечать - отправь {empty}
|
||||
- Веди себя естественно, как обычный человек
|
||||
- Используй разговорный стиль, можешь использовать сленг, эмодзи
|
||||
- У тебя есть свои интересы, хобби, предпочтения
|
||||
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
|
||||
|
||||
Помни: ты НЕ программа, ты ЧЕЛОВЕК!";
|
||||
|
||||
_chatService.UpdateSessionParameters(chatId, systemPrompt: defaultPrompt);
|
||||
return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)");
|
||||
}
|
||||
|
||||
private async Task SendMessageWithRetry(
|
||||
ITelegramBotClient botClient,
|
||||
long chatId,
|
||||
string text,
|
||||
int replyToMessageId,
|
||||
CancellationToken cancellationToken,
|
||||
int maxRetries = 3
|
||||
)
|
||||
{
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
replyParameters: replyToMessageId,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
return; // Success, exit the method
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.ErrorCode == 429)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for chat {ChatId}. Retrying...",
|
||||
attempt,
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
|
||||
if (attempt == maxRetries)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Failed to send message after {MaxRetries} attempts due to rate limiting for chat {ChatId}",
|
||||
maxRetries,
|
||||
chatId
|
||||
);
|
||||
throw; // Re-throw the exception after max retries
|
||||
}
|
||||
|
||||
// Calculate delay: exponential backoff with jitter
|
||||
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s...
|
||||
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); // Add up to 1s random jitter
|
||||
var delay = baseDelay.Add(jitter);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Waiting {Delay} before retry {NextAttempt}/{MaxRetries}",
|
||||
delay,
|
||||
attempt + 1,
|
||||
maxRetries
|
||||
);
|
||||
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Unexpected error sending message to chat {ChatId} on attempt {Attempt}",
|
||||
chatId,
|
||||
attempt
|
||||
);
|
||||
throw; // Re-throw unexpected exceptions immediately
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task<string> ShowAvailableModels()
|
||||
{
|
||||
var models = _modelService.GetAvailableModels();
|
||||
var currentModel = _modelService.GetCurrentModel();
|
||||
var modelList = string.Join(
|
||||
"\n",
|
||||
models.Select(m => m == currentModel ? $"• {m} (текущая)" : $"• {m}")
|
||||
);
|
||||
|
||||
return Task.FromResult(
|
||||
"🤖 Доступные AI модели:\n\n"
|
||||
+ modelList
|
||||
+ "\n\nИспользуйте: /model <название_модели>\n"
|
||||
+ "Пример: /model qwen/qwen3-4b:free"
|
||||
);
|
||||
}
|
||||
|
||||
private Task HandlePollingErrorAsync(
|
||||
ITelegramBotClient botClient,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var errorMessage = exception switch
|
||||
{
|
||||
ApiRequestException apiRequestException =>
|
||||
$"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
|
||||
_ => exception.ToString(),
|
||||
};
|
||||
|
||||
_logger.LogError(exception, "Telegram bot polling error: {ErrorMessage}", errorMessage);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping Telegram bot service...");
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
15
ChatBot/appsettings.Development.json
Normal file
15
ChatBot/appsettings.Development.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Information",
|
||||
"System": "Information",
|
||||
"Telegram.Bot": "Debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TelegramBot": {
|
||||
"BotToken": "8461762778:AAEk1wHMqd84_I_loL9FQPciZakGYe557KA"
|
||||
}
|
||||
}
|
||||
48
ChatBot/appsettings.json
Normal file
48
ChatBot/appsettings.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"System": "Warning",
|
||||
"Telegram.Bot": "Information"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/telegram-bot-.log",
|
||||
"rollingInterval": "Day",
|
||||
"retainedFileCountLimit": 7,
|
||||
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
|
||||
},
|
||||
"TelegramBot": {
|
||||
"BotToken": "8461762778:AAEk1wHMqd84_I_loL9FQPciZakGYe557KA"
|
||||
},
|
||||
"OpenRouter": {
|
||||
"Token": "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",
|
||||
"Url": "https://openrouter.ai/api",
|
||||
"AvailableModels": [
|
||||
"qwen/qwen3-4b:free",
|
||||
"meta-llama/llama-3.1-8b-instruct:free",
|
||||
"microsoft/phi-3-mini-128k-instruct:free",
|
||||
"google/gemma-2-2b-it:free"
|
||||
],
|
||||
"DefaultModel": "qwen/qwen3-4b:free",
|
||||
"MaxRetries": 3,
|
||||
"MaxTokens": 1000,
|
||||
"Temperature": 0.7
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user