Добавьте файлы проекта.

This commit is contained in:
Leonid Pershin
2025-10-15 18:25:26 +03:00
parent 017102970c
commit 51a8157803
28 changed files with 2007 additions and 0 deletions

21
ChatBot/ChatBot.csproj Normal file
View 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>

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View File

@@ -0,0 +1,7 @@
namespace ChatBot.Models.Configuration
{
public class TelegramBotSettings
{
public string BotToken { get; set; } = string.Empty;
}
}

View File

@@ -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();
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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>();
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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
View 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();
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"ChatBot": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}

View 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
View 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
}
}