feat: интеллектуальная cron-система с AI-агентом

Основные изменения:
- CronJob теперь хранит prompt для ИИ вместо команды
- Добавлены поля: user_id, notify, log_results
- Задачи выполняются через AI-агент (автономный выбор инструмента)
- Планировщик проверяет задачи каждую минуту
- Уведомления отправляются в Telegram (если notify=True)
- Результаты сохраняются в cron_logs/ (если log_results=True)
- Добавлена команда /cron для управления задачами
- Обновлена БД и модель данных

Новые файлы:
- bot/services/cron_scheduler.py - планировщик задач
- CRON_SYSTEM.md - документация

Изменённые файлы:
- bot/tools/cron_tool.py - обновлён для работы с промптами
- bot/handlers/commands.py - добавлена cron_command
- bot.py - интеграция планировщика, регистрация команды
- .gitignore - исключение cron_logs/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-02-25 12:36:32 +08:00
parent bff74741a6
commit f559c83baa
6 changed files with 874 additions and 59 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ bot_config.json
# Logs
*.log
cron_logs/
# Cache
.cache/

219
CRON_SYSTEM.md Normal file
View File

@ -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> <prompt>
```
**Параметры:**
- `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 <id>
```
**Пример:**
```
/cron run 1
```
### `/cron remove` - Удалить задачу
```
/cron remove <id>
```
### `/cron toggle` - Включить/выключить задачу
```
/cron toggle <id>
```
## 🛠️ Инструменты ИИ-агента
При выполнении задачи ИИ-агент может использовать:
| Инструмент | Назначение | Триггеры |
|------------|------------|----------|
| `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-агентом*

63
bot.py
View File

@ -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))

View File

@ -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 <name> <schedule> <prompt> - добавить задачу
/cron run <id> - выполнить задачу немедленно
/cron remove <id> - удалить задачу
/cron toggle <id> - включить/выключить задачу
"""
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 <name> <schedule> <prompt>`"
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 <name> <schedule> <prompt> [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 <id>`")
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 <id>`")
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 <id>`")
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 <name> <schedule> <prompt>` - добавить задачу\n"
"• `/cron run <id>` - выполнить задачу\n"
"• `/cron remove <id>` - удалить задачу\n"
"• `/cron toggle <id>` - включить/выключить задачу",
parse_mode="Markdown"
)
except Exception as e:
logger.exception(f"Ошибка в команде /cron: {e}")
await update.message.reply_text(f"❌ Ошибка: {e}")

View File

@ -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

View File

@ -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: