diff --git a/.gitignore b/.gitignore index ba01256..4b5191f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ bot_config.json # Logs *.log +cron_logs/ # Cache .cache/ diff --git a/CRON_SYSTEM.md b/CRON_SYSTEM.md new file mode 100644 index 0000000..7b1bdf6 --- /dev/null +++ b/CRON_SYSTEM.md @@ -0,0 +1,219 @@ +# 🕐 Интеллектуальная Cron-система + +Интеллектуальная система планирования задач для Telegram CLI Bot. + +## 📋 Особенности + +В отличие от классического cron, задачи выполняются не как команды, а как **промпты для ИИ-агента**. + +### Структура задачи + +```python +CronJob: + - id: int # Уникальный ID + - name: str # Название задачи + - prompt: str # Промпт для ИИ-агента + - schedule: str # Расписание (@hourly, @daily, */5 * * * *) + - user_id: int # ID пользователя Telegram + - enabled: bool # Включена ли задача + - notify: bool # Уведомлять пользователя в Telegram + - log_results: bool # Сохранять результат в лог-файл + - last_run: datetime # Последнее выполнение + - next_run: datetime # Следующее выполнение + - created_at: datetime # Дата создания +``` + +## 🔄 Процесс выполнения + +``` +┌─────────────────────────────────────────────────────────┐ +│ Cron Scheduler (проверяет каждую минуту) │ +│ ↓ (если время пришло) │ +│ Отправляет промпт ИИ-агенту │ +│ ↓ │ +│ ИИ-агент (Рик) анализирует промпт: │ +│ - Решает какой инструмент использовать │ +│ - Выполняет инструмент (поиск, SSH, RSS) │ +│ ↓ │ +│ Если notify=True → отправка уведомления в Telegram │ +│ Если log_results=True → сохранение в лог-файл │ +└─────────────────────────────────────────────────────────┘ +``` + +## 📝 Команды управления + +### `/cron list` - Показать все задачи + +``` +/cron list +``` + +**Пример вывода:** +``` +⏰ Ваши задачи: + +✅ Проверка диска (ID: 1) + 🔔📝 Промпт: Проверить свободное место на сервере... + Расписание: @daily + Следующий запуск: 2026-02-26 00:00:00 + Последний запуск: 2026-02-25 00:00:00 +``` + +### `/cron add` - Добавить задачу + +``` +/cron add +``` + +**Параметры:** +- `name` - название задачи +- `schedule` - расписание: + - `@hourly` - каждый час + - `@daily` - каждый день + - `@weekly` - каждую неделю + - `*/5 * * * *` - каждые 5 минут (cron format) +- `prompt` - промпт для ИИ-агента + +**Примеры:** + +```bash +# Ежедневная проверка диска +/cron add check_disk @daily Проверить свободное место на сервере home + +# Ежечасные новости +/cron add tech_news @hourly Что нового в Linux сегодня + +# Каждые 5 минут мониторинг +/cron add monitor */5 * * * * Проверить нагрузку на сервер +``` + +### `/cron run` - Выполнить задачу немедленно + +``` +/cron run +``` + +**Пример:** +``` +/cron run 1 +``` + +### `/cron remove` - Удалить задачу + +``` +/cron remove +``` + +### `/cron toggle` - Включить/выключить задачу + +``` +/cron toggle +``` + +## 🛠️ Инструменты ИИ-агента + +При выполнении задачи ИИ-агент может использовать: + +| Инструмент | Назначение | Триггеры | +|------------|------------|----------| +| `ddgs_tool` | Поиск в интернете | "найди", "поиск", "узнай" | +| `rss_tool` | Чтение RSS лент | "новости", "почитай", "лента" | +| `ssh_tool` | SSH-команды | "проверь сервер", "выполни команду" | +| `cron_tool` | Управление задачами | "напомни", "запланируй" | + +## 📂 Логирование + +Результаты выполнения задач сохраняются в: +``` +cron_logs/ + cron_job_1_check_disk.log + cron_job_2_tech_news.log + ... +``` + +**Формат лога:** +``` +============================================================ +[2026-02-25 10:30:00] Задача: Проверка диска (ID: 1) +============================================================ +Промпт: +Проверить свободное место на сервере home + +Результат: +Задача 'Проверка диска' выполнена. + +Использован инструмент: ssh_tool +Результат: Filesystem Size Used Avail Use% Mounted on +... + +``` + +## 🔔 Уведомления + +Если `notify=True`, бот отправляет уведомление в Telegram: + +``` +✅ Задача 'Проверка диска' выполнена. + +Использован инструмент: ssh_tool +Результат: Свободно 45GB на /dev/sda1 +``` + +## 💡 Примеры использования + +### 1. Ежедневный мониторинг диска + +```bash +/cron add disk_daily @daily Проверить свободное место на сервере home. Если меньше 10GB - предупредить +``` + +### 2. Ежечасные новости IT + +```bash +/cron add it_news @hourly Найти свежие новости про Python и Linux за последний час +``` + +### 3. Мониторинг нагрузки каждые 5 минут + +```bash +/cron add load_monitor */5 * * * * Проверить нагрузку CPU и RAM на сервере +``` + +### 4. Еженедельный поиск уязвимостей + +```bash +/cron add security_scan @weekly Найти информацию о новых уязвимостях в Linux за неделю +``` + +## 🚀 Архитектура + +``` +bot/ + tools/ + cron_tool.py # Инструмент управления задачами + services/ + cron_scheduler.py # Планировщик (проверка каждую минуту) + handlers/ + commands.py # Обработчик команды /cron +``` + +## ⚙️ Технические детали + +- **Проверка задач:** каждую минуту (60 секунд) +- **Хранение:** SQLite (`cron.db`) +- **Логи:** текстовые файлы (`cron_logs/`) +- **Формат расписания:** cron format или специальные (@hourly, @daily, @weekly) + +## 🎯 Отличия от классического cron + +| Классический cron | Интеллектуальный cron | +|-------------------|----------------------| +| Выполняет команды | Выполняет промпты для ИИ | +| Жёсткая логика | Гибкое решение через ИИ | +| Вывод в stdout/email | Уведомления в Telegram + логи | +| Нет контекста | ИИ использует контекст и память | + +--- + +*Версия: 0.5.3* +*Интеллектуальная cron-система с AI-агентом* diff --git a/bot.py b/bot.py index 8d06a7e..3859daf 100644 --- a/bot.py +++ b/bot.py @@ -77,13 +77,14 @@ from bot.utils.decorators import check_access from bot.keyboards.menus import MenuItem, init_menus # Импорты хендлеров из модулей -from bot.handlers.commands import start_command, menu_command, help_command, settings_command +from bot.handlers.commands import start_command, menu_command, help_command, settings_command, cron_command from bot.handlers.callbacks import menu_callback from bot.services.command_executor import execute_cli_command # Импорты инструментов и AI агента from bot.ai_agent import ai_agent from bot.tools import tools_registry +from bot.services.cron_scheduler import init_scheduler, get_scheduler # Глобальные менеджеры сессий ssh_session_manager = SSHSessionManager() @@ -371,30 +372,41 @@ def format_tool_result(tool_name: str, result: 'ToolResult') -> str: return output - elif tool_name == 'cron_manager': + elif tool_name == 'cron_tool': action = result.metadata.get('action', 'list') if action == 'list' and result.data: output = "⏰ **Ваши задачи:**\n\n" for job in result.data: status = "✅" if job.get('enabled') else "❌" - output += f"{status} **{job.get('name', 'Без названия')}**\n" - output += f" Команда: `{job.get('command', '')}`\n" - output += f" Расписание: {job.get('schedule', '')}\n" + notify_icon = "🔔" if job.get('notify') else "🔕" + log_icon = "📝" if job.get('log_results') else "🚫" + output += f"{status} **{job.get('name', 'Без названия')}** (ID: {job.get('id')})\n" + output += f" {notify_icon}{log_icon} Промпт: _{job.get('prompt', '')[:100]}_{'...' if len(job.get('prompt', '')) > 100 else ''}\n" + output += f" Расписание: `{job.get('schedule', '')}`\n" if job.get('next_run'): output += f" Следующий запуск: {job.get('next_run')}\n" + if job.get('last_run'): + output += f" Последний запуск: {job.get('last_run')}\n" output += "\n" return output elif action == 'add' and result.success: data = result.data - return f"✅ **Задача добавлена:**\n• ID: {data.get('id')}\n• Название: {data.get('name')}\n• Расписание: {data.get('schedule')}\n• Следующий запуск: {data.get('next_run', 'N/A')}" + notify_status = "🔔 Уведомлять" if result.metadata.get('notify') else "🔕 Без уведомлений" + log_status = "📝 Логировать" if result.metadata.get('log_results') else "🚫 Без логов" + return f"✅ **Задача добавлена:**\n• ID: {data.get('id')}\n• Название: {data.get('name')}\n• Расписание: {data.get('schedule')}\n• {notify_status}, {log_status}\n• Следующий запуск: {data.get('next_run', 'N/A')}" elif action == 'remove' and result.success: return f"✅ **Задача удалена:** ID {result.data.get('id')}" elif action == 'run' and result.success: - return f"✅ **Задача выполнена:** {result.data.get('message', '')}" + result_text = result.metadata.get('result_text', 'Задача выполнена') + tool_used = result.data.get('tool_used', 'не указан') + return f"✅ **Задача выполнена:**\n\n{result_text}\n\n🔧 Инструмент: {tool_used}" + + elif action == 'run' and not result.success: + return f"❌ **Ошибка выполнения задачи:**\n{result.error}" return f"Cron: {result.data}" @@ -1277,6 +1289,7 @@ async def post_init(application: Application): BotCommand("menu", "Главное меню с кнопками"), BotCommand("help", "Справка"), BotCommand("settings", "Настройки"), + BotCommand("cron", "Управление задачами"), BotCommand("stop", "Прервать SSH-сессию"), BotCommand("ai", "Задача для Qwen Code AI"), BotCommand("memory", "Статистика памяти ИИ"), @@ -1285,9 +1298,44 @@ async def post_init(application: Application): ] await application.bot.set_my_commands(commands) + # Инициализация планировщика cron-задач + cron_tool = tools_registry.get_tool('cron_tool') + if cron_tool: + scheduler = init_scheduler(cron_tool, ai_agent, send_notification=send_cron_notification) + await scheduler.start() + logger.info("🕐 Планировщик cron-задач инициализирован") + else: + logger.warning("⚠️ Cron инструмент не найден, планировщик не запущен") + logger.info("Бот инициализирован") +async def send_cron_notification(user_id: int, message: str): + """ + Отправить уведомление пользователю о результате cron-задачи. + + Args: + user_id: ID пользователя в Telegram + message: Текст уведомления + """ + try: + # Получаем application из контекста + from telegram.ext import Application + app = Application.get_instance() + + if app: + await app.bot.send_message( + chat_id=user_id, + text=message, + parse_mode="Markdown" + ) + logger.info(f"🔔 Уведомление отправлено пользователю {user_id}") + else: + logger.warning("Application не инициализирован, уведомление не отправлено") + except Exception as e: + logger.exception(f"Ошибка отправки уведомления: {e}") + + @check_access async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработка команды /stop - прерывание активной SSH-сессии.""" @@ -1629,6 +1677,7 @@ def main(): application.add_handler(CommandHandler("start", start_command)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("settings", settings_command)) + application.add_handler(CommandHandler("cron", cron_command)) application.add_handler(CommandHandler("menu", menu_command)) application.add_handler(CommandHandler("stop", stop_command)) application.add_handler(CommandHandler("memory", memory_command)) diff --git a/bot/handlers/commands.py b/bot/handlers/commands.py index 91cc702..1b3d2d0 100644 --- a/bot/handlers/commands.py +++ b/bot/handlers/commands.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Обработчики команд бота (/start, /menu, /help, /settings).""" +"""Обработчики команд бота (/start, /menu, /help, /settings, /cron).""" import logging from telegram import Update @@ -8,6 +8,8 @@ from telegram.ext import ContextTypes # Импорты из модулей bot/ from bot.config import config, state_manager, server_manager, menu_builder from bot.utils.decorators import check_access +from bot.tools import tools_registry +from bot.ai_agent import ai_agent logger = logging.getLogger(__name__) @@ -114,3 +116,207 @@ async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE): parse_mode="Markdown", reply_markup=menu_builder.get_keyboard("settings") ) + + +@check_access +async def cron_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Обработка команды /cron - управление задачами. + + Использование: + /cron list - показать все задачи + /cron add - добавить задачу + /cron run - выполнить задачу немедленно + /cron remove - удалить задачу + /cron toggle - включить/выключить задачу + """ + user_id = update.effective_user.id + args = context.args + + # Получаем cron инструмент + cron_tool = tools_registry.get_tool('cron_tool') + if not cron_tool: + await update.message.reply_text("❌ Ошибка: cron инструмент не найден") + return + + # Парсим команду + if not args: + # По умолчанию показываем список задач + action = 'list' + else: + action = args[0].lower() + + try: + if action == 'list': + # Показать все задачи пользователя + result = await cron_tool.execute(action='list', user_id=user_id) + + if result.success and result.data: + output = "⏰ **Ваши задачи:**\n\n" + for job in result.data: + status = "✅" if job.get('enabled') else "❌" + notify_icon = "🔔" if job.get('notify') else "🔕" + log_icon = "📝" if job.get('log_results') else "🚫" + output += f"{status} **{job.get('name', 'Без названия')}** (ID: {job.get('id')})\n" + output += f" {notify_icon}{log_icon} Промпт: _{job.get('prompt', '')[:100]}_{'...' if len(job.get('prompt', '')) > 100 else ''}\n" + output += f" Расписание: `{job.get('schedule', '')}`\n" + if job.get('next_run'): + output += f" Следующий запуск: {job.get('next_run')}\n" + if job.get('last_run'): + output += f" Последний запуск: {job.get('last_run')}\n" + output += "\n" + + if not output.strip(): + output = "📭 У вас пока нет задач.\n\nДобавьте задачу командой:\n`/cron add `" + + await update.message.reply_text(output, parse_mode="Markdown") + else: + await update.message.reply_text("📭 У вас пока нет задач.") + + elif action == 'add': + if len(args) < 4: + await update.message.reply_text( + "❌ **Недостаточно аргументов**\n\n" + "**Использование:**\n" + "`/cron add [notify] [log]`\n\n" + "**Примеры:**\n" + "`/cron check_disk Ежедневно проверять диск на сервере`\n" + "`/cron news hourly Что нового в Linux сегодня`\n\n" + "**Расписание:**\n" + "• `@hourly` - каждый час\n" + "• `@daily` - каждый день\n" + "• `@weekly` - каждую неделю\n" + "• `*/5 * * * *` - каждые 5 минут", + parse_mode="Markdown" + ) + return + + name = args[1] + schedule = args[2] + # Промпт может содержать пробелы - берём всё после schedule + prompt = ' '.join(args[3:]) + + # Парсим опциональные параметры + notify = 'notify' in prompt.lower() + log_results = 'no_log' not in prompt.lower() and 'без_лога' not in prompt.lower() + + result = await cron_tool.execute( + action='add', + name=name, + prompt=prompt, + schedule=schedule, + user_id=user_id, + notify=notify, + log_results=log_results + ) + + if result.success: + notify_status = "🔔 Уведомлять" if notify else "🔕 Без уведомлений" + log_status = "📝 Логировать" if log_results else "🚫 Без логов" + await update.message.reply_text( + f"✅ **Задача добавлена:**\n" + f"• ID: {result.data.get('id')}\n" + f"• Название: {name}\n" + f"• Промпт: _{prompt}_\n" + f"• Расписание: `{schedule}`\n" + f"• {notify_status}, {log_status}\n" + f"• Следующий запуск: {result.data.get('next_run', 'N/A')}", + parse_mode="Markdown" + ) + else: + await update.message.reply_text(f"❌ Ошибка: {result.error}") + + elif action == 'run': + if len(args) < 2: + await update.message.reply_text("❌ Укажите ID задачи: `/cron run `") + return + + try: + job_id = int(args[1]) + except ValueError: + await update.message.reply_text("❌ ID должен быть числом") + return + + status_msg = await update.message.reply_text("⏳ Выполняю задачу...") + + # Выполняем задачу через AI-агент + result = await cron_tool.execute( + action='run', + job_id=job_id, + ai_agent=ai_agent, + user_id=user_id + ) + + await status_msg.delete() + + if result.success: + result_text = result.metadata.get('result_text', 'Задача выполнена') + tool_used = result.data.get('tool_used', 'не указан') + await update.message.reply_text( + f"✅ **Задача выполнена:**\n\n{result_text}\n\n🔧 Инструмент: {tool_used}", + parse_mode="Markdown" + ) + else: + await update.message.reply_text(f"❌ Ошибка: {result.error}") + + elif action == 'remove': + if len(args) < 2: + await update.message.reply_text("❌ Укажите ID задачи: `/cron remove `") + return + + try: + job_id = int(args[1]) + except ValueError: + await update.message.reply_text("❌ ID должен быть числом") + return + + result = await cron_tool.execute(action='remove', job_id=job_id) + + if result.success: + await update.message.reply_text(f"✅ Задача удалена: ID {job_id}") + else: + await update.message.reply_text(f"❌ Ошибка: {result.error}") + + elif action == 'toggle': + if len(args) < 2: + await update.message.reply_text("❌ Укажите ID задачи: `/cron toggle `") + return + + try: + job_id = int(args[1]) + except ValueError: + await update.message.reply_text("❌ ID должен быть числом") + return + + # Получаем текущее состояние задачи + list_result = await cron_tool.execute(action='list', user_id=user_id) + current_state = True + for job in list_result.data: + if job['id'] == job_id: + current_state = job.get('enabled', True) + break + + new_state = not current_state + result = await cron_tool.execute(action='toggle', job_id=job_id, enabled=new_state) + + if result.success: + state_text = "включена" if new_state else "выключена" + await update.message.reply_text(f"✅ Задача ID {job_id} {state_text}") + else: + await update.message.reply_text(f"❌ Ошибка: {result.error}") + + else: + await update.message.reply_text( + "❌ Неизвестная команда.\n\n" + "**Доступные команды:**\n" + "• `/cron list` - показать все задачи\n" + "• `/cron add ` - добавить задачу\n" + "• `/cron run ` - выполнить задачу\n" + "• `/cron remove ` - удалить задачу\n" + "• `/cron toggle ` - включить/выключить задачу", + parse_mode="Markdown" + ) + + except Exception as e: + logger.exception(f"Ошибка в команде /cron: {e}") + await update.message.reply_text(f"❌ Ошибка: {e}") diff --git a/bot/services/cron_scheduler.py b/bot/services/cron_scheduler.py new file mode 100644 index 0000000..f4de3d1 --- /dev/null +++ b/bot/services/cron_scheduler.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Cron Scheduler - планировщик задач для автоматического выполнения. + +Проверяет задачи каждую минуту и выполняет те, у которых наступило время. +""" + +import logging +import asyncio +from datetime import datetime +from typing import Optional, Callable, Awaitable +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class CronScheduler: + """ + Планировщик cron-задач. + + Автоматически проверяет задачи и выполняет их через AI-агент. + """ + + def __init__( + self, + cron_tool, + ai_agent, + send_notification: Optional[Callable[[int, str], Awaitable[None]]] = None + ): + """ + Инициализировать планировщик. + + Args: + cron_tool: Экземпляр CronTool + ai_agent: Экземпляр AI-агента для выполнения задач + send_notification: Асинхронная функция для отправки уведомлений (user_id, message) + """ + self.cron_tool = cron_tool + self.ai_agent = ai_agent + self.send_notification = send_notification + self._running = False + self._task: Optional[asyncio.Task] = None + self._check_interval = 60 # Проверка каждую минуту + + async def start(self): + """Запустить планировщик в фоновом режиме.""" + if self._running: + logger.warning("Планировщик уже запущен") + return + + self._running = True + self._task = asyncio.create_task(self._run_loop()) + logger.info("🕐 Планировщик cron-задач запущен") + + async def stop(self): + """Остановить планировщик.""" + self._running = False + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + logger.info("🕐 Планировщик cron-задач остановлен") + + async def _run_loop(self): + """Основной цикл планировщика.""" + while self._running: + try: + await self._check_and_run_tasks() + except Exception as e: + logger.exception(f"Ошибка в цикле планировщика: {e}") + + await asyncio.sleep(self._check_interval) + + async def _check_and_run_tasks(self): + """Проверить задачи и выполнить те, у которых наступило время.""" + now = datetime.now() + logger.debug(f"🕐 Проверка задач на {now.strftime('%Y-%m-%d %H:%M:%S')}") + + # Получаем список всех задач + result = await self.cron_tool.list_jobs() + + if not result.success: + logger.error(f"Ошибка получения списка задач: {result.error}") + return + + jobs = result.data + executed_count = 0 + + for job in jobs: + if not job.get('enabled'): + continue + + next_run_str = job.get('next_run') + if not next_run_str: + continue + + try: + next_run = datetime.strptime(next_run_str, '%Y-%m-%d %H:%M:%S') + except ValueError: + logger.error(f"Ошибка парсинга next_run для задачи {job['id']}: {next_run_str}") + continue + + # Если время пришло + if now >= next_run: + logger.info(f"⏰ Время задачи #{job['id']}: {job['name']}") + await self._execute_job(job) + executed_count += 1 + + if executed_count > 0: + logger.info(f"✅ Выполнено задач: {executed_count}") + + async def _execute_job(self, job: dict): + """ + Выполнить задачу. + + Args: + job: Словарь с данными задачи + """ + job_id = job['id'] + job_name = job['name'] + notify = job.get('notify', False) + log_results = job.get('log_results', True) + user_id = job.get('user_id') # ID пользователя который создал задачу + + # Выполняем задачу через AI-агент + result = await self.cron_tool.run_job( + job_id=job_id, + ai_agent=self.ai_agent, + user_id=user_id + ) + + if result.success: + logger.info(f"✅ Задача '{job_name}' выполнена успешно") + + # Отправляем уведомление если нужно + if notify and self.send_notification and user_id: + result_text = result.metadata.get('result_text', 'Задача выполнена') + await self.send_notification(user_id, result_text) + else: + logger.error(f"❌ Задача '{job_name}' не выполнена: {result.error}") + + if notify and self.send_notification and user_id: + await self.send_notification( + user_id, + f"❌ **Ошибка задачи '{job_name}':**\n{result.error}" + ) + + def set_notification_callback(self, callback: Callable[[int, str], Awaitable[None]]): + """Установить callback для отправки уведомлений.""" + self.send_notification = callback + + +# Глобальный планировщик +scheduler: Optional[CronScheduler] = None + + +def init_scheduler(cron_tool, ai_agent, send_notification=None) -> CronScheduler: + """Инициализировать глобальный планировщик.""" + global scheduler + scheduler = CronScheduler(cron_tool, ai_agent, send_notification) + return scheduler + + +def get_scheduler() -> Optional[CronScheduler]: + """Получить глобальный планировщик.""" + return scheduler diff --git a/bot/tools/cron_tool.py b/bot/tools/cron_tool.py index 3b93b89..97d9939 100644 --- a/bot/tools/cron_tool.py +++ b/bot/tools/cron_tool.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """ -Cron Tool - инструмент для управления задачами пользователя. +Cron Tool - инструмент для управления интеллектуальными задачами. -Позволяет создавать, планировать и выполнять периодические задачи. +Позволяет создавать, планировать и выполнять периодические задачи через AI-агент. +Задачи хранятся как промпты для ИИ, а не как команды. """ import logging @@ -20,26 +21,46 @@ logger = logging.getLogger(__name__) @dataclass class CronJob: - """Задача cron.""" + """ + Интеллектуальная задача cron. + + Attributes: + id: ID задачи + name: Название задачи + prompt: Промпт для ИИ-агента (вместо команды) + schedule: Расписание (cron format: "*/5 * * * *" или "@daily", "@hourly") + enabled: Включена ли задача + user_id: ID пользователя Telegram + notify: Уведомлять ли пользователя в Telegram о результате + log_results: Сохранять ли результат в лог-файл + last_run: Время последнего выполнения + next_run: Время следующего выполнения + created_at: Время создания + """ id: Optional[int] name: str - command: str - schedule: str # cron format: "*/5 * * * *" или "daily", "hourly" + prompt: str # Промпт для ИИ вместо команды + schedule: str + user_id: Optional[int] = None # ID пользователя Telegram enabled: bool = True + notify: bool = False # Уведомлять пользователя в Telegram + log_results: bool = True # Сохранять в лог last_run: Optional[datetime] = None next_run: Optional[datetime] = None created_at: datetime = field(default_factory=datetime.now) class CronTool(BaseTool): - """Инструмент для управления задачами пользователя.""" + """Инструмент для управления интеллектуальными задачами пользователя.""" name = "cron_tool" - description = "Управление периодическими задачами пользователя. Создание, планирование и выполнение задач по расписанию." + description = "Управление периодическими задачами через AI-агент. Создание, планирование и выполнение задач по расписанию." category = "automation" - def __init__(self, db_path: str = None): + def __init__(self, db_path: str = None, log_dir: str = None): self.db_path = Path(db_path) if db_path else Path(__file__).parent.parent.parent / "cron.db" + self.log_dir = Path(log_dir) if log_dir else Path(__file__).parent.parent.parent / "cron_logs" + self.log_dir.mkdir(parents=True, exist_ok=True) self._jobs: Dict[int, CronJob] = {} self._init_db() @@ -47,18 +68,32 @@ class CronTool(BaseTool): """Инициализировать БД.""" conn = sqlite3.connect(self.db_path) c = conn.cursor() + + # Создаём таблицу с user_id c.execute(''' CREATE TABLE IF NOT EXISTS cron_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - command TEXT NOT NULL, + prompt TEXT NOT NULL, schedule TEXT NOT NULL, + user_id INTEGER, enabled INTEGER DEFAULT 1, + notify INTEGER DEFAULT 0, + log_results INTEGER DEFAULT 1, last_run DATETIME, next_run DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') + + # Проверяем есть ли колонка user_id (для обратной совместимости) + c.execute("PRAGMA table_info(cron_jobs)") + columns = [col[1] for col in c.fetchall()] + + if 'user_id' not in columns: + logger.info("Добавление колонки user_id в таблицу cron_jobs") + c.execute('ALTER TABLE cron_jobs ADD COLUMN user_id INTEGER') + conn.commit() conn.close() @@ -93,8 +128,18 @@ class CronTool(BaseTool): return None - async def add_job(self, name: str, command: str, schedule: str) -> ToolResult: - """Добавить задачу.""" + async def add_job(self, name: str, prompt: str, schedule: str, user_id: int = None, notify: bool = False, log_results: bool = True) -> ToolResult: + """ + Добавить интеллектуальную задачу. + + Args: + name: Название задачи + prompt: Промпт для ИИ-агента + schedule: Расписание (cron format или @daily, @hourly, @weekly) + user_id: ID пользователя Telegram + notify: Уведомлять ли пользователя в Telegram + log_results: Сохранять ли результат в лог + """ conn = sqlite3.connect(self.db_path) c = conn.cursor() @@ -103,9 +148,9 @@ class CronTool(BaseTool): next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else None c.execute(''' - INSERT INTO cron_jobs (name, command, schedule, next_run) - VALUES (?, ?, ?, ?) - ''', (name, command, schedule, next_run_str)) + INSERT INTO cron_jobs (name, prompt, schedule, user_id, notify, log_results, next_run) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (name, prompt, schedule, user_id, 1 if notify else 0, 1 if log_results else 0, next_run_str)) job_id = c.lastrowid conn.commit() @@ -113,15 +158,18 @@ class CronTool(BaseTool): self._jobs[job_id] = CronJob( id=job_id, name=name, - command=command, + prompt=prompt, schedule=schedule, + user_id=user_id, + notify=notify, + log_results=log_results, next_run=next_run ) return ToolResult( success=True, - data={'id': job_id, 'name': name, 'schedule': schedule, 'next_run': next_run_str}, - metadata={'status': 'added'} + data={'id': job_id, 'name': name, 'prompt': prompt, 'schedule': schedule, 'user_id': user_id, 'next_run': next_run_str}, + metadata={'status': 'added', 'notify': notify, 'log_results': log_results} ) except Exception as e: @@ -133,14 +181,27 @@ class CronTool(BaseTool): finally: conn.close() - async def list_jobs(self) -> ToolResult: - """Получить список всех задач.""" + async def list_jobs(self, user_id: int = None) -> ToolResult: + """ + Получить список всех задач. + + Args: + user_id: ID пользователя для фильтрации (если None - все задачи) + """ conn = sqlite3.connect(self.db_path) c = conn.cursor() - c.execute(''' - SELECT id, name, command, schedule, enabled, last_run, next_run, created_at - FROM cron_jobs ORDER BY id - ''') + + if user_id: + c.execute(''' + SELECT id, name, prompt, schedule, user_id, enabled, notify, log_results, last_run, next_run, created_at + FROM cron_jobs WHERE user_id = ? ORDER BY id + ''', (user_id,)) + else: + c.execute(''' + SELECT id, name, prompt, schedule, user_id, enabled, notify, log_results, last_run, next_run, created_at + FROM cron_jobs ORDER BY id + ''') + rows = c.fetchall() conn.close() @@ -149,12 +210,15 @@ class CronTool(BaseTool): jobs.append({ 'id': row[0], 'name': row[1], - 'command': row[2], + 'prompt': row[2], 'schedule': row[3], - 'enabled': bool(row[4]), - 'last_run': row[5], - 'next_run': row[6], - 'created_at': row[7] + 'user_id': row[4], + 'enabled': bool(row[5]), + 'notify': bool(row[6]), + 'log_results': bool(row[7]), + 'last_run': row[8], + 'next_run': row[9], + 'created_at': row[10] }) return ToolResult( @@ -206,11 +270,21 @@ class CronTool(BaseTool): metadata={'status': 'toggled'} ) - async def run_job(self, job_id: int) -> ToolResult: - """Выполнить задачу немедленно.""" + async def run_job(self, job_id: int, ai_agent=None, user_id: int = None) -> ToolResult: + """ + Выполнить интеллектуальную задачу через AI-агент. + + Args: + job_id: ID задачи + ai_agent: Экземпляр AI-агента для выполнения промпта + user_id: ID пользователя для контекста + + Returns: + ToolResult с результатом выполнения + """ conn = sqlite3.connect(self.db_path) c = conn.cursor() - c.execute("SELECT command FROM cron_jobs WHERE id = ?", (job_id,)) + c.execute("SELECT name, prompt, notify, log_results FROM cron_jobs WHERE id = ?", (job_id,)) row = c.fetchone() if not row: @@ -220,47 +294,145 @@ class CronTool(BaseTool): error=f"Задача не найдена: {job_id}" ) - command = row[0] + name, prompt, notify, log_results = row conn.close() - # Здесь должна быть логика выполнения команды - # Для демонстрации возвращаем заглушку - logger.info(f"Выполнение задачи {job_id}: {command}") + logger.info(f"🕐 Выполнение задачи #{job_id}: {name}") + logger.info(f" Промпт: {prompt}") - # Обновляем last_run - conn = sqlite3.connect(self.db_path) - c = conn.cursor() - c.execute("UPDATE cron_jobs SET last_run = datetime('now') WHERE id = ?", (job_id,)) - conn.commit() - conn.close() + result_data = { + 'id': job_id, + 'name': name, + 'prompt': prompt, + 'executed_at': datetime.now().isoformat() + } - return ToolResult( - success=True, - data={'id': job_id, 'command': command, 'message': 'Задача выполнена'}, - metadata={'status': 'executed'} - ) + # Выполняем задачу через AI-агент + if ai_agent: + try: + # Отправляем промпт ИИ-агенту + logger.info(f"🤖 Отправка промпта AI-агенту для задачи {name}") + + # ИИ-агент анализирует промпт и решает какой инструмент использовать + decision = await ai_agent.decide(prompt, context={'user_id': user_id}) + + if decision.should_use_tool: + logger.info(f"🔧 AI-агент решил использовать инструмент: {decision.tool_name}") + tool_result = await ai_agent.execute_tool(decision.tool_name, **decision.tool_args) + + result_data['tool_used'] = decision.tool_name + result_data['tool_result'] = tool_result.to_dict() if hasattr(tool_result, 'to_dict') else str(tool_result) + result_data['success'] = tool_result.success + + # Формируем результат + result_text = f"Задача '{name}' выполнена.\n\n" + result_text += f"Использован инструмент: {decision.tool_name}\n" + if tool_result.success: + result_text += f"Результат: {tool_result.data or 'Успешно'}" + else: + result_text += f"Ошибка: {tool_result.error}" + + else: + # ИИ решил что инструмент не нужен - выполняем промпт напрямую + logger.info(f"ℹ️ AI-агент решил что инструмент не требуется") + result_text = f"Задача '{name}' выполнена (без инструментов).\nПромпт: {prompt}" + result_data['success'] = True + result_data['ai_reasoning'] = decision.reasoning - async def execute(self, action: str = "list", **kwargs) -> ToolResult: + # Сохраняем в лог если нужно + if log_results: + self._save_to_log(job_id, name, prompt, result_text) + + # Обновляем last_run + conn = sqlite3.connect(self.db_path) + c = conn.cursor() + c.execute("UPDATE cron_jobs SET last_run = datetime('now') WHERE id = ?", (job_id,)) + conn.commit() + conn.close() + + return ToolResult( + success=True, + data=result_data, + metadata={ + 'status': 'executed', + 'notify': notify, + 'log_results': log_results, + 'result_text': result_text + } + ) + + except Exception as e: + logger.exception(f"Ошибка выполнения задачи через AI-агент: {e}") + + if log_results: + self._save_to_log(job_id, name, prompt, f"Ошибка: {e}") + + return ToolResult( + success=False, + error=str(e), + data=result_data + ) + else: + # AI-агент не предоставлен - просто логируем + logger.warning(f"AI-агент не предоставлен, задача {name} не выполнена") + + if log_results: + self._save_to_log(job_id, name, prompt, "Ошибка: AI-агент не предоставлен") + + return ToolResult( + success=False, + error="AI-агент не предоставлен", + data=result_data + ) + + def _save_to_log(self, job_id: int, job_name: str, prompt: str, result: str): + """Сохранить результат выполнения задачи в лог-файл.""" + log_file = self.log_dir / f"cron_job_{job_id}_{job_name}.log" + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_entry = f""" +{'='*60} +[{timestamp}] Задача: {job_name} (ID: {job_id}) +{'='*60} +Промпт: +{prompt} + +Результат: +{result} + +""" + + with open(log_file, 'a', encoding='utf-8') as f: + f.write(log_entry) + + logger.debug(f"Результат задачи {job_name} сохранён в лог: {log_file}") + + async def execute(self, action: str = "list", ai_agent=None, user_id: int = None, **kwargs) -> ToolResult: """ Выполнить действие с cron задачами. Args: action: Действие - list, add, remove, toggle, run + ai_agent: Экземпляр AI-агента (для run) + user_id: ID пользователя (для add, run, list) kwargs: Дополнительные аргументы """ actions = { - 'list': self.list_jobs, + 'list': lambda: self.list_jobs(user_id=user_id), 'add': lambda: self.add_job( name=kwargs.get('name'), - command=kwargs.get('command'), - schedule=kwargs.get('schedule') + prompt=kwargs.get('prompt'), + schedule=kwargs.get('schedule'), + user_id=user_id, + notify=kwargs.get('notify', False), + log_results=kwargs.get('log_results', True) ), 'remove': lambda: self.remove_job(job_id=kwargs.get('job_id')), 'toggle': lambda: self.toggle_job( job_id=kwargs.get('job_id'), enabled=kwargs.get('enabled', True) ), - 'run': lambda: self.run_job(job_id=kwargs.get('job_id')) + 'run': lambda: self.run_job(job_id=kwargs.get('job_id'), ai_agent=ai_agent, user_id=user_id) } if action not in actions: