-
Notifications
You must be signed in to change notification settings - Fork 2
feat: implement LLM todo tool for Telegram group chats #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
| using TelegramSearchBot.Service.Tools; | ||
| using TelegramSearchBot.Model; | ||
| using Xunit; | ||
| using Moq; | ||
| using TelegramSearchBot.Interface; | ||
|
|
||
| namespace TelegramSearchBot.Test.Service.Tools | ||
| { | ||
| public class TodoToolServiceTests | ||
| { | ||
| private readonly Mock<ISendMessageService> _mockSendMessageService; | ||
| private readonly TodoToolService _todoToolService; | ||
|
|
||
| public TodoToolServiceTests() | ||
| { | ||
| _mockSendMessageService = new Mock<ISendMessageService>(); | ||
| _todoToolService = new TodoToolService(_mockSendMessageService.Object, null); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SendTodoToGroup_ValidParameters_Success() | ||
| { | ||
| // Arrange | ||
| long chatId = -123456789; // Group chat ID | ||
| string title = "Test Todo"; | ||
| string description = "This is a test todo item"; | ||
| string priority = "high"; | ||
| string dueDate = "2025-12-31"; | ||
|
|
||
| // Act | ||
| var result = await _todoToolService.SendTodoToGroup(chatId, title, description, priority, dueDate); | ||
|
|
||
| // Assert | ||
| Assert.Contains("✅", result); | ||
| Assert.Contains(title, result); | ||
| Assert.Contains(chatId.ToString(), result); | ||
|
|
||
| // Verify SendMessage was called | ||
| _mockSendMessageService.Verify(x => x.SendMessage(It.Is<string>(s => s.Contains(title)), chatId), Times.Once); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SendTodoToGroup_PrivateChatId_LogsWarning() | ||
| { | ||
| // Arrange | ||
| long chatId = 123456789; // Private chat ID (positive) | ||
| string title = "Test Todo"; | ||
| string description = "This is a test todo item"; | ||
|
|
||
| // Act | ||
| var result = await _todoToolService.SendTodoToGroup(chatId, title, description); | ||
|
|
||
| // Assert | ||
| Assert.Contains("✅", result); // Should still work but with warning | ||
| _mockSendMessageService.Verify(x => x.SendMessage(It.Is<string>(s => s.Contains(title)), chatId), Times.Once); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SendTodoToGroup_InvalidPriority_DefaultsToMedium() | ||
| { | ||
| // Arrange | ||
| long chatId = -123456789; | ||
| string title = "Test Todo"; | ||
| string description = "This is a test todo item"; | ||
| string invalidPriority = "invalid"; | ||
|
|
||
| // Act | ||
| var result = await _todoToolService.SendTodoToGroup(chatId, title, description, invalidPriority); | ||
|
|
||
| // Assert | ||
| Assert.Contains("✅", result); | ||
| _mockSendMessageService.Verify(x => x.SendMessage(It.Is<string>(s => s.Contains("MEDIUM")), chatId), Times.Once); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SendQuickTodo_ValidParameters_Success() | ||
| { | ||
| // Arrange | ||
| long chatId = -123456789; | ||
| string message = "Quick todo message"; | ||
|
|
||
| // Act | ||
| var result = await _todoToolService.SendQuickTodo(chatId, message); | ||
|
|
||
| // Assert | ||
| Assert.Contains("✅", result); | ||
| Assert.Contains(chatId.ToString(), result); | ||
|
|
||
| // Verify SendMessage was called with formatted message | ||
| _mockSendMessageService.Verify(x => x.SendMessage(It.Is<string>(s => s.Contains("📋") && s.Contains("TODO")), chatId), Times.Once); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SendTodoToGroup_SendMessageFails_ReturnsErrorMessage() | ||
| { | ||
| // Arrange | ||
| long chatId = -123456789; | ||
| string title = "Test Todo"; | ||
| string description = "This is a test todo item"; | ||
|
|
||
| _mockSendMessageService | ||
| .Setup(x => x.SendMessage(It.IsAny<string>(), chatId)) | ||
| .ThrowsAsync(new Exception("Telegram API error")); | ||
|
|
||
| // Act | ||
| var result = await _todoToolService.SendTodoToGroup(chatId, title, description); | ||
|
|
||
| // Assert | ||
| Assert.Contains("❌", result); | ||
| Assert.Contains("Failed to send todo", result); | ||
| Assert.Contains("Telegram API error", result); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||
| using System; | ||||||
| using System.Threading.Tasks; | ||||||
| using Microsoft.Extensions.Logging; | ||||||
| using TelegramSearchBot.Attributes; | ||||||
| using TelegramSearchBot.Interface; | ||||||
| using TelegramSearchBot.Model; | ||||||
|
|
||||||
| namespace TelegramSearchBot.Service.Tools | ||||||
| { | ||||||
| /// <summary> | ||||||
| /// LLM tool for sending todo messages to group chats | ||||||
| /// </summary> | ||||||
| [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient)] | ||||||
| public class TodoToolService | ||||||
| { | ||||||
| private readonly ISendMessageService _sendMessageService; | ||||||
| private readonly ILogger<TodoToolService> _logger; | ||||||
|
|
||||||
| public TodoToolService(ISendMessageService sendMessageService, ILogger<TodoToolService> logger) | ||||||
| { | ||||||
| _sendMessageService = sendMessageService; | ||||||
| _logger = logger; | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Send a todo message to a group chat | ||||||
| /// </summary> | ||||||
| /// <param name="chatId">Target group chat ID</param> | ||||||
| /// <param name="title">Todo title/subject</param> | ||||||
| /// <param name="description">Detailed description of the todo</param> | ||||||
| /// <param name="priority">Priority level (low, medium, high, urgent)</param> | ||||||
| /// <param name="dueDate">Optional due date for the todo</param> | ||||||
| /// <param name="toolContext">Tool execution context</param> | ||||||
| /// <returns>Confirmation message</returns> | ||||||
| [McpTool("Send a todo message to a group chat with structured information including title, description, priority, and optional due date.")] | ||||||
| public async Task<string> SendTodoToGroup( | ||||||
| [McpParameter("Target group chat ID to send the todo message to")] | ||||||
| long chatId, | ||||||
|
|
||||||
| [McpParameter("Title or subject of the todo item")] | ||||||
| string title, | ||||||
|
|
||||||
| [McpParameter("Detailed description of the todo item")] | ||||||
| string description, | ||||||
|
|
||||||
| [McpParameter("Priority level (low, medium, high, urgent)", IsRequired = false)] | ||||||
| string priority = "medium", | ||||||
|
|
||||||
| [McpParameter("Optional due date for the todo item (format: YYYY-MM-DD or YYYY-MM-DD HH:MM)", IsRequired = false)] | ||||||
| string dueDate = null, | ||||||
|
|
||||||
| ToolContext toolContext = null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| // Validate chat ID is a group chat (negative chat IDs are groups/channels) | ||||||
| if (chatId > 0) | ||||||
| { | ||||||
| _logger.LogWarning("TodoToolService: Chat ID {ChatId} appears to be a private chat, not a group", chatId); | ||||||
| } | ||||||
|
|
||||||
| // Validate priority | ||||||
| priority = priority?.ToLowerInvariant() ?? "medium"; | ||||||
| if (!new[] { "low", "medium", "high", "urgent" }.Contains(priority)) | ||||||
|
Check failure on line 64 in TelegramSearchBot/Service/Tools/TodoToolService.cs
|
||||||
| { | ||||||
| priority = "medium"; | ||||||
| } | ||||||
|
|
||||||
| // Build todo message | ||||||
| var message = BuildTodoMessage(title, description, priority, dueDate); | ||||||
|
|
||||||
| // Send message to group | ||||||
| await _sendMessageService.SendMessage(message, chatId); | ||||||
|
|
||||||
| _logger.LogInformation("TodoToolService: Successfully sent todo to chat {ChatId}: {Title}", chatId, title); | ||||||
|
|
||||||
| return $"✅ Todo item '{title}' has been sent to group {chatId} with {priority} priority."; | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "TodoToolService: Failed to send todo to chat {ChatId}: {Title}", chatId, title); | ||||||
| return $"❌ Failed to send todo item '{title}' to group {chatId}: {ex.Message}"; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Send a quick todo message with minimal parameters | ||||||
| /// </summary> | ||||||
| /// <param name="chatId">Target group chat ID</param> | ||||||
| /// <param name="message">Todo message content</param> | ||||||
| /// <param name="toolContext">Tool execution context</param> | ||||||
| /// <returns>Confirmation message</returns> | ||||||
| [McpTool("Send a quick todo message to a group chat with minimal formatting")] | ||||||
| public async Task<string> SendQuickTodo( | ||||||
| [McpParameter("Target group chat ID to send the todo message to")] | ||||||
| long chatId, | ||||||
|
|
||||||
| [McpParameter("Quick todo message content")] | ||||||
| string message, | ||||||
|
|
||||||
| ToolContext toolContext = null) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| // Validate chat ID is a group chat (negative chat IDs are groups/channels) | ||||||
| if (chatId > 0) | ||||||
| { | ||||||
| _logger.LogWarning("TodoToolService: Chat ID {ChatId} appears to be a private chat, not a group", chatId); | ||||||
| } | ||||||
|
|
||||||
| // Build quick todo message | ||||||
| var formattedMessage = $"📋 **TODO**\n\n{message}"; | ||||||
|
|
||||||
| // Send message to group | ||||||
| await _sendMessageService.SendMessage(formattedMessage, chatId); | ||||||
|
|
||||||
| _logger.LogInformation("TodoToolService: Successfully sent quick todo to chat {ChatId}", chatId); | ||||||
|
|
||||||
| return $"✅ Quick todo has been sent to group {chatId}."; | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| _logger.LogError(ex, "TodoToolService: Failed to send quick todo to chat {ChatId}", chatId); | ||||||
| return $"❌ Failed to send quick todo to group {chatId}: {ex.Message}"; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private string BuildTodoMessage(string title, string description, string priority, string dueDate) | ||||||
| { | ||||||
| var priorityEmojis = new Dictionary<string, string> | ||||||
|
Check failure on line 130 in TelegramSearchBot/Service/Tools/TodoToolService.cs
|
||||||
| { | ||||||
| { "low", "🟢" }, | ||||||
| { "medium", "🟡" }, | ||||||
| { "high", "🟠" }, | ||||||
| { "urgent", "🔴" } | ||||||
| }; | ||||||
|
|
||||||
| var priorityEmoji = priorityEmojis.TryGetValue(priority, out var emoji) ? emoji : "🟡"; | ||||||
|
|
||||||
| var message = $"📋 **TODO: {title}** {priorityEmoji}\n\n"; | ||||||
| message += $"**Priority:** {priority.ToUpperInvariant()}\n\n"; | ||||||
| message += $"**Description:**\n{description}\n"; | ||||||
|
|
||||||
| if (!string.IsNullOrEmpty(dueDate)) | ||||||
| { | ||||||
| message += $"\n**Due Date:** {dueDate}\n"; | ||||||
| } | ||||||
|
|
||||||
| message += $"\n*Created: {DateTimeOffset.Now:yyyy-MM-dd HH:mm})*"; | ||||||
|
||||||
| message += $"\n*Created: {DateTimeOffset.Now:yyyy-MM-dd HH:mm})*"; | |
| message += $"\n*Created: {DateTimeOffset.Now:yyyy-MM-dd HH:mm}*"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
using System.Linq;directive. TheContainsmethod on arrays requires the System.Linq namespace to be imported.