Добавьте файлы проекта.
This commit is contained in:
25
ChatBot.sln
Normal file
25
ChatBot.sln
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.14.36414.22 d17.14
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatBot", "ChatBot\ChatBot.csproj", "{DDE6533C-2CEF-4E89-BDAD-3347B2B1A4F5}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{DDE6533C-2CEF-4E89-BDAD-3347B2B1A4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DDE6533C-2CEF-4E89-BDAD-3347B2B1A4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DDE6533C-2CEF-4E89-BDAD-3347B2B1A4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DDE6533C-2CEF-4E89-BDAD-3347B2B1A4F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {D68168A0-EF53-40E2-B9C1-813A465A55A0}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Telegram Bot
|
||||||
|
|
||||||
|
Простой Telegram бот, написанный на C# с использованием .NET 9 и Telegram.Bot библиотеки.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- Обработка текстовых сообщений
|
||||||
|
- Базовые команды: `/start`, `/help`, `/echo`
|
||||||
|
- Логирование всех операций
|
||||||
|
- Асинхронная обработка сообщений
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
1. **Создайте бота в Telegram:**
|
||||||
|
- Найдите @BotFather в Telegram
|
||||||
|
- Отправьте команду `/newbot`
|
||||||
|
- Следуйте инструкциям для создания бота
|
||||||
|
- Сохраните полученный токен
|
||||||
|
|
||||||
|
2. **Настройте конфигурацию:**
|
||||||
|
- Откройте файл `ChatBot/appsettings.json`
|
||||||
|
- Замените `YOUR_BOT_TOKEN_HERE` на токен вашего бота
|
||||||
|
- Для разработки также обновите `appsettings.Development.json`
|
||||||
|
|
||||||
|
3. **Запустите приложение:**
|
||||||
|
```bash
|
||||||
|
cd ChatBot
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Команды бота
|
||||||
|
|
||||||
|
- `/start` - Начать работу с ботом
|
||||||
|
- `/help` - Показать список доступных команд
|
||||||
|
- `/echo <текст>` - Повторить указанный текст
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
ChatBot/
|
||||||
|
├── Services/
|
||||||
|
│ └── TelegramBotService.cs # Основной сервис бота
|
||||||
|
├── Program.cs # Точка входа приложения
|
||||||
|
├── appsettings.json # Конфигурация
|
||||||
|
└── ChatBot.csproj # Файл проекта
|
||||||
|
```
|
||||||
|
|
||||||
|
## Разработка
|
||||||
|
|
||||||
|
Для добавления новых команд отредактируйте метод `ProcessMessageAsync` в файле `TelegramBotService.cs`.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- .NET 9.0
|
||||||
|
- Действующий токен Telegram бота
|
||||||
Reference in New Issue
Block a user