diff --git a/.gitignore b/.gitignore
index 64457e8..ba01256 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,8 @@ huggingface_cache/
# OS
.DS_Store
Thumbs.db
+
+# Database (personal data - do not commit)
+*.db
+*.sqlite3
+vector_db/
diff --git a/AI_AGENT_TOOLS.md b/AI_AGENT_TOOLS.md
new file mode 100644
index 0000000..17a10ac
--- /dev/null
+++ b/AI_AGENT_TOOLS.md
@@ -0,0 +1,260 @@
+# 🤖 AI-Агент с Автономными Инструментами
+
+## ✅ Реализовано
+
+**Интеграция завершена успешно!** Теперь твой бот умеет **самостоятельно решать**, когда использовать инструменты — без прямых команд!
+
+---
+
+## 📋 Что реализовано
+
+### 1. Улучшенный AI-агент (`bot/ai_agent.py`)
+
+**Добавлено:**
+- ✅ Расширенные триггеры для всех инструментов
+- ✅ Приоритет инструментов: SSH > Cron > Поиск > RSS
+- ✅ Контекстная чувствительность (оценка уверенности)
+- ✅ Логирование использования инструментов
+- ✅ Поддержка пользовательских предпочтений
+
+### 2. SSH-инструмент (`bot/tools/ssh_tool.py`)
+
+**Возможности:**
+- ✅ Подключение к серверам по SSH (asyncssh)
+- ✅ Выполнение команд с таймаутом
+- ✅ Красивое форматирование вывода
+- ✅ Конфигурация серверов (по умолчанию: 192.168.1.54)
+
+**Пример:**
+```
+Ты: Проверь нагрузку на сервере
+Бот: 🖥️ SSH: home
+ Команда: uptime
+ Вывод: 14:30:00 up 10 days, load average: 0.15
+ ✅ Успешно
+```
+
+### 3. Cron-инструмент (`bot/tools/cron_tool.py`)
+
+**Возможности:**
+- ✅ Создание задач по расписанию
+- ✅ Поддержка форматов: `*/5 * * * *`, `@hourly`, `@daily`, `@weekly`
+- ✅ Просмотр, удаление, включение/выключение задач
+- ✅ Немедленный запуск задач
+
+**Пример:**
+```
+Ты: Напомни каждый день делать бэкап
+Бот: ⏰ Ваши задачи:
+ ✅ Daily Backup
+ Команда: /home/user/backup.sh
+ Расписание: @daily
+ Следующий запуск: 2024-03-11 00:00:00
+```
+
+### 4. Обновлённый `format_tool_result` в `bot.py`
+
+**Добавлено:**
+- ✅ Форматирование для SSH-команд
+- ✅ Форматирование для Cron-задач
+- ✅ Обработка ошибок с красивым выводом
+
+---
+
+## 🎯 Как это работает
+
+### Автономное использование инструментов
+
+Бот **сам понимает**, когда нужен инструмент:
+
+| Твоя фраза | Бот использует | Почему |
+|------------|----------------|--------|
+| "Найди информацию про Python" | 🔍 DDGS Search | Триггер "найди" |
+| "Почитай новости IT" | 📰 RSS Reader | Триггер "новости" |
+| "Проверь сервер" | 🖥️ SSH Executor | Триггер "проверь сервер" |
+| "Напомни каждый день" | ⏰ Cron Manager | Триггер "каждый день" |
+
+### Приоритеты
+
+Если сообщение подходит под несколько инструментов:
+
+1. **SSH** (системные задачи) — высший приоритет
+2. **Cron** (планирование)
+3. **Поиск** (информация)
+4. **RSS** (новости)
+
+---
+
+## 📦 Доступные инструменты
+
+| Инструмент | Назначение | Триггеры |
+|------------|------------|----------|
+| `ddgs_search` | Поиск в интернете | "найди", "поиск", "узнай", "как сделать" |
+| `rss_reader` | Чтение RSS лент | "новости", "почитай", "лента", "IT новости" |
+| `ssh_executor` | SSH-команды | "проверь сервер", "выполни команду", "uptime" |
+| `cron_manager` | Задачи по расписанию | "напомни", "запланируй", "каждый день" |
+
+---
+
+## 🔧 Настройка
+
+### Добавление сервера в SSH
+
+Открой `bot/tools/ssh_tool.py` и добавь:
+
+```python
+self.servers['myserver'] = ServerConfig(
+ host='192.168.1.100',
+ port=22,
+ username='user',
+ password='pass' # или client_keys=['/path/to/key']
+)
+```
+
+### Добавление триггеров
+
+Открой `bot/ai_agent.py` и добавь в соответствующий список:
+
+```python
+SEARCH_TRIGGERS = [
+ # ... существующие ...
+ 'мой_триггер' # новый триггер
+]
+```
+
+---
+
+## 🧪 Тестирование
+
+Проверка работы AI-агента:
+
+```bash
+cd ~/git/telegram-cli-bot
+python3 -c "
+import asyncio
+from bot.ai_agent import ai_agent
+
+async def test():
+ tests = [
+ 'Найди информацию про Python',
+ 'Почитай новости',
+ 'Проверь нагрузку на сервере',
+ 'Напомни каждый день',
+ 'Привет!'
+ ]
+
+ for msg in tests:
+ decision = await ai_agent.decide(msg)
+ print(f'{msg}: {decision.tool_name or \"нет инструмента\"} (conf={decision.confidence})')
+
+asyncio.run(test())
+"
+```
+
+**Ожидаемый результат:**
+```
+Найди информацию про Python: ddgs_search (conf=0.9)
+Почитай новости: rss_reader (conf=0.9)
+Проверь нагрузку на сервере: ssh_executor (conf=0.9)
+Напомни каждый день: cron_manager (conf=0.85)
+Привет!: нет инструмента (conf=0.0)
+```
+
+---
+
+## 📝 Примеры использования
+
+### 🔍 Поиск в интернете
+
+```
+Ты: Найди руководство по async/await в Python
+Бот: 🔍 Результаты поиска:
+ 1. **Python Async/Await Documentation**
+ https://docs.python.org/3/library/asyncio.html
+ asyncio is a library to write concurrent code ...
+```
+
+### 📰 Чтение новостей
+
+```
+Ты: Почитай новости Linux
+Бот: 📰 Последние новости:
+ 1. Linux Kernel 6.8 Released
+ 📅 2024-03-10 14:30:00
+ 🔗 https://...
+```
+
+### 🖥️ SSH-команды
+
+```
+Ты: Сколько места на диске?
+Бот: 🖥️ SSH: home
+ Команда: df -h
+ Вывод:
+ Filesystem Size Used Avail
+ /dev/sda1 100G 50G 50G
+ ✅ Успешно
+```
+
+### ⏰ Cron-задачи
+
+```
+Ты: Запланируй бэкап каждый день в 3 ночи
+Бот: ✅ Задача добавлена:
+ • ID: 1
+ • Название: Daily Backup
+ • Расписание: 0 3 * * *
+ • Следующий запуск: 2024-03-11 03:00:00
+```
+
+---
+
+## 🚀 Запуск бота
+
+```bash
+cd ~/git/telegram-cli-bot
+./run.sh
+```
+
+Или вручную:
+
+```bash
+python3 bot.py
+```
+
+---
+
+## 📊 Статистика
+
+**Файлы изменены/созданы:**
+- ✅ `bot/ai_agent.py` — улучшенный AI-агент
+- ✅ `bot/tools/ssh_tool.py` — SSH-инструмент (новый)
+- ✅ `bot/tools/cron_tool.py` — Cron-инструмент (новый)
+- ✅ `bot/tools/__init__.py` — реестр (обновлён)
+- ✅ `bot.py` — форматирование результатов (обновлён)
+- ✅ `TOOLS.md` — документация (обновлена)
+
+**Строк кода добавлено:** ~600+
+
+**Инструментов доступно:** 4
+
+---
+
+## 🎯 Следующие шаги
+
+**Можно добавить:**
+1. **Веб-скрапинг** — парсинг конкретных сайтов
+2. **Мониторинг** — авто-проверка метрик сервера
+3. **Уведомления** — отправка уведомлений по расписанию
+4. **Интеграции** — GitHub API, Docker API, etc.
+
+---
+
+## ⚠️ Важно
+
+- Бот персональный — нет ролевой модели и ограничений
+- Инструменты доступны **всегда** в AI-режиме
+- Бот **сам решает** когда использовать инструмент
+- Логи пишутся в `bot.log`
+
+**Приятного использования! 🚀**
diff --git a/SYSTEM_PROMPT.md b/SYSTEM_PROMPT.md
new file mode 100644
index 0000000..78f6e89
--- /dev/null
+++ b/SYSTEM_PROMPT.md
@@ -0,0 +1,159 @@
+# Системный промпт для Telegram CLI Bot
+
+## 📋 Обзор
+
+Системный промпт — это набор инструкций для Qwen AI, который определяет:
+- Роль и задачи бота
+- Доступные инструменты (capabilities)
+- Правила автономного использования инструментов
+- Форматы ответов
+
+## 🏗️ Архитектура
+
+```
+system_prompt.md # Файл с системным промптом
+ ↓
+qwen_integration.py # Загружает промпт при инициализации
+ ↓
+bot.py (handle_ai_task) # Добавляет промпт к каждому запросу
+ ↓
+Qwen Code CLI # Получает промпт + контекст + запрос
+```
+
+## 🔧 Как это работает
+
+### 1. Загрузка промпта
+
+При первом запросе к Qwen менеджер загружает промпт из файла:
+
+```python
+system_prompt = qwen_manager.load_system_prompt()
+```
+
+Промпт кэшируется в памяти для последующих запросов.
+
+### 2. Формирование полного запроса
+
+Каждый запрос к Qwen включает:
+
+```
+[СИСТЕМНЫЙ ПРОМПТ]
+ ↓
+[КОНТЕКСТ ПАМЯТИ] (из ChromaDB RAG)
+ ↓
+[ИСТОРИЯ ДИАЛОГА] (последние 20 сообщений)
+ ↓
+[ЗАПРОС ПОЛЬЗОВАТЕЛЯ]
+```
+
+### 3. Автономное использование инструментов
+
+AI агент анализирует запрос пользователя и **сам решает** какой инструмент использовать:
+
+| Триггеры | Инструмент | Пример |
+|----------|------------|--------|
+| "найди", "погугли", "узнай" | `ddgs_search` | "найди информацию про asyncio" |
+| "новости", "rss", "лента" | `rss_reader` | "что нового в Linux?" |
+| "выполни команду", "ssh" | `ssh_executor` | "проверь нагрузку на сервере" |
+| "напомни", "запланируй" | `cron_manager` | "напомни каждый день в 9 утра" |
+
+## 📁 Структура system_prompt.md
+
+### Разделы промпта:
+
+1. **Роль и задачи** — кто такой бот, для кого работает
+2. **Доступные инструменты** — описание каждого инструмента:
+ - Название и функция
+ - Когда использовать
+ - Параметры
+ - Примеры вызова
+3. **Принципы работы** — автономность, контекст, прозрачность, приоритеты
+4. **Форматы ответов** — как оформлять результаты
+5. **Важные правила** — технические ограничения, форматирование
+6. **Примеры диалогов** — few-shot prompting для лучшего понимания
+
+## 🎯 Приоритеты инструментов
+
+При принятии решения AI следует приоритету:
+
+1. **SSH** — явные системные задачи
+2. **Cron** — планирование и напоминания
+3. **Поиск (DDGS)** — свежие данные из интернета
+4. **RSS** — новости из подписанных лент
+
+## 🔄 Обновление промпта
+
+Для изменения поведения бота:
+
+1. Отредактируйте `system_prompt.md`
+2. Перезапустите бота (или оставьте как есть — кэш обновится при следующем запросе)
+3. Для сброса кэша: `qwen_manager._system_prompt = None`
+
+## 💡 Советы по настройке
+
+### Добавление нового инструмента
+
+1. Опишите инструмент в разделе "Доступные инструменты":
+ ```markdown
+ ### N. Название (`tool_name`)
+
+ **Назначение:** ...
+
+ **Когда использовать:**
+ - Триггер 1
+ - Триггер 2
+
+ **Параметры:**
+ - `param1` (type): описание
+
+ **Пример вызова:**
+ ```python
+ tool_name(param1="value")
+ ```
+ ```
+
+2. Добавьте триггеры в `bot/ai_agent.py`:
+ ```python
+ NEW_TOOL_TRIGGERS = ['триггер1', 'триггер2']
+ ```
+
+3. Реализуйте логику проверки в `_should_use_new_tool()`
+
+4. Добавьте обработку в `decide()`
+
+### Изменение поведения
+
+- **Более агрессивное использование инструментов:** уменьшите пороги confidence в `ai_agent.py`
+- **Более консервативное:** увеличьте пороги или добавьте больше условий
+- **Изменение формата ответов:** отредактируйте раздел "Формат ответов" в промпте
+
+## 📊 Мониторинг
+
+Для отслеживания использования инструментов:
+
+```python
+# История использования инструментов
+ai_agent.get_tool_history(limit=10)
+
+# Предпочтения пользователя
+ai_agent.get_user_preference(user_id, 'preferred_tool')
+```
+
+## ⚠️ Ограничения
+
+- **Размер контекста:** до 200K токенов (безопасный лимит)
+- **Время выполнения:** 5 минут максимум на задачу
+- **Кэширование:** системный промпт кэшируется в памяти менеджера
+
+## 📈 Версии
+
+| Версия | Изменения |
+|--------|-----------|
+| 0.5.3 | Базовая реализация системного промпта |
+| 0.5.2 | AI агент с авто-выбором инструментов |
+| 0.5.1 | Интеграция RSS reader |
+| 0.5.0 | Интеграция DDGS search |
+
+---
+
+*Документация для разработчиков Telegram CLI Bot*
diff --git a/TOOLS.md b/TOOLS.md
new file mode 100644
index 0000000..d92f67c
--- /dev/null
+++ b/TOOLS.md
@@ -0,0 +1,342 @@
+# 🛠️ Инструменты Telegram CLI Bot
+
+Инструменты — это capabilities, которые бот может использовать **автономно** для выполнения задач пользователя (Agentic AI подход).
+
+## 📋 Архитектура
+
+```
+bot/
+├── tools/
+│ ├── __init__.py # Реестр инструментов (ToolsRegistry)
+│ ├── ddgs_tool.py # Поиск в интернете через DuckDuckGo
+│ ├── rss_tool.py # Чтение RSS/Atom лент
+│ ├── ssh_tool.py # Выполнение команд по SSH
+│ └── cron_tool.py # Управление задачами по расписанию
+└── ai_agent.py # AI агент для принятия решений
+```
+
+## 🏗️ Как это работает
+
+### 1. Реестр инструментов
+
+`ToolsRegistry` — синглтон, который хранит все доступные инструменты:
+
+```python
+from bot.tools import tools_registry
+
+# Получить инструмент
+tool = tools_registry.get('ddgs_search')
+
+# Выполнить инструмент
+result = await tools_registry.execute_tool('ddgs_search', query='python tutorial', max_results=5)
+```
+
+### 2. AI Агент
+
+`AIAgent` анализирует сообщение пользователя и решает, какой инструмент использовать:
+
+```python
+from bot.ai_agent import ai_agent
+
+# Принять решение
+decision = await ai_agent.decide("Найди информацию про Python 3.12")
+
+if decision.should_use_tool:
+ result = await ai_agent.execute_tool(decision.tool_name, **decision.tool_args)
+```
+
+### 3. Автономное использование
+
+Бот **автоматически** проверяет решение агента при обработке сообщений в AI-режиме:
+
+1. Пользователь пишет сообщение в AI-режиме
+2. Бот проверяет триггеры (ключевые слова)
+3. Если найден триггер → выполняет инструмент
+4. Возвращает результат пользователю
+
+## 📦 Доступные инструменты
+
+### 🔍 DDGS Search (`ddgs_search`)
+
+Поиск информации в интернете через DuckDuckGo.
+
+**Использование:**
+```python
+result = await tools_registry.execute_tool(
+ 'ddgs_search',
+ query='python async await',
+ max_results=10
+)
+```
+
+**Триггеры для авто-использования:**
+- "найди", "поиск", "погугли", "узнай"
+- "что нового", "последние новости"
+- "как сделать", "руководство", "tutorial"
+- "что такое", "как работает", "где найти"
+
+**Примеры:**
+```
+Пользователь: Найди информацию про Rust programming
+Бот: 🔍 **Результаты поиска:**
+ 1. **Rust - Official Website**
+ https://www.rust-lang.org
+ Rust is a programming language ...
+```
+
+### 📰 RSS Reader (`rss_reader`)
+
+Чтение RSS/Atom новостных лент.
+
+**Действия:**
+
+| Действие | Описание | Параметры |
+|----------|----------|-----------|
+| `fetch` | Получить свежие новости | - |
+| `list` | Показать новости | `limit`, `feed_id`, `search`, `undigested_only` |
+| `add_feed` | Добавить ленту | `url`, `title` |
+| `list_feeds` | Список лент | - |
+| `mark_digest` | Отметить как прочитанное | `news_id` |
+
+**Использование:**
+```python
+# Получить новости
+result = await tools_registry.execute_tool(
+ 'rss_reader',
+ action='list',
+ limit=10,
+ undigested_only=True
+)
+
+# Добавить ленту
+result = await tools_registry.execute_tool(
+ 'rss_reader',
+ action='add_feed',
+ url='https://lwn.net/headlines/newrss',
+ title='LWN.net'
+)
+```
+
+**Триггеры для авто-использования:**
+- "новости", "rss", "лента", "feed"
+- "дайджест", "что нового в linux"
+- "новости it", "tech news"
+- "почитай новости", "свежие статьи"
+
+**Примеры:**
+```
+Пользователь: Почитай новости
+Бот: 📰 **Последние новости:**
+ 1. Linux Kernel 6.8 Released
+ 📅 2024-03-10 14:30:00
+ 🔗 https://...
+```
+
+### 🖥️ SSH Executor (`ssh_executor`)
+
+Выполнение команд на удалённых серверах по SSH.
+
+**Использование:**
+```python
+result = await tools_registry.execute_tool(
+ 'ssh_executor',
+ command='uptime',
+ server='home',
+ timeout=30
+)
+```
+
+**Конфигурация серверов:**
+```python
+# Серверы настраиваются в ssh_tool.py
+servers = {
+ 'home': {
+ 'host': '192.168.1.54',
+ 'port': 22,
+ 'username': 'mirivlad',
+ 'password': '***'
+ }
+}
+```
+
+**Триггеры для авто-использования:**
+- "выполни команду", "ssh", "запусти на сервере"
+- "проверь сервер", "посмотри логи"
+- "покажи процесс", "сколько места", "df", "top"
+- "systemctl", "journalctl", "uptime"
+
+**Примеры:**
+```
+Пользователь: Проверь нагрузку на сервере
+Бот: 🖥️ **SSH: home**
+ **Команда:** `uptime`
+
+ **Вывод:**
+ ```
+ 14:30:00 up 10 days, 2:30, 1 user, load average: 0.15, 0.10, 0.05
+ ```
+
+ ✅ **Успешно**
+```
+
+### ⏰ Cron Manager (`cron_manager`)
+
+Управление периодическими задачами пользователя.
+
+**Действия:**
+
+| Действие | Описание | Параметры |
+|----------|----------|-----------|
+| `list` | Показать все задачи | - |
+| `add` | Добавить задачу | `name`, `command`, `schedule` |
+| `remove` | Удалить задачу | `job_id` |
+| `toggle` | Включить/выключить | `job_id`, `enabled` |
+| `run` | Выполнить немедленно | `job_id` |
+
+**Форматы расписаний:**
+- `*/5 * * * *` — каждые 5 минут
+- `@hourly` — каждый час
+- `@daily` — каждый день
+- `@weekly` — каждую неделю
+
+**Использование:**
+```python
+# Добавить задачу
+result = await tools_registry.execute_tool(
+ 'cron_manager',
+ action='add',
+ name='Daily Backup',
+ command='/home/user/backup.sh',
+ schedule='@daily'
+)
+```
+
+**Триггеры для авто-использования:**
+- "напомни", "запланируй", "каждый день"
+- "периодически", "по расписанию", "автоматически"
+- "создай задачу", "добавь в cron"
+
+**Примеры:**
+```
+Пользователь: Напомни мне каждый день делать бэкап
+Бот: ⏰ **Ваши задачи:**
+
+ ✅ **Daily Backup**
+ Команда: `/home/user/backup.sh`
+ Расписание: @daily
+ Следующий запуск: 2024-03-11 00:00:00
+```
+
+## ➕ Добавление нового инструмента
+
+1. Создайте файл `bot/tools/your_tool.py`:
+
+```python
+from bot.tools import BaseTool, ToolResult, register_tool
+
+@register_tool
+class YourTool(BaseTool):
+ name = "your_tool"
+ description = "Описание инструмента"
+ category = "category"
+
+ async def execute(self, **kwargs) -> ToolResult:
+ # Логика инструмента
+ return ToolResult(success=True, data={'result': 'data'})
+```
+
+2. Инструмент автоматически зарегистрируется в реестре
+
+3. Добавьте триггеры в `bot/ai_agent.py`:
+
+```python
+YOUR_TRIGGERS = ['триггер1', 'триггер2']
+
+def _should_use_your_tool(self, message: str) -> tuple[bool, float]:
+ message_lower = message.lower()
+ for trigger in YOUR_TRIGGERS:
+ if trigger in message_lower:
+ return True, 0.9
+ return False, 0.0
+```
+
+4. Добавьте форматирование в `bot.py`:
+
+```python
+elif tool_name == 'your_tool':
+ return f"Результат: {result.data}"
+```
+
+## 🔧 Установка зависимостей
+
+```bash
+cd ~/git/telegram-cli-bot
+pip install -r requirements.txt
+```
+
+## 🎯 Приоритеты инструментов
+
+Приоритет проверки (от высшего к низшему):
+
+1. **SSH Executor** — системные задачи
+2. **Cron Manager** — планирование задач
+3. **DDGS Search** — поиск информации
+4. **RSS Reader** — чтение новостей
+
+## 📝 Примеры использования
+
+### Поиск в интернете
+
+```
+Пользователь: Найди информацию про Python 3.12
+Бот: 🔍 **Результаты поиска:**
+ 1. **Python 3.12.0 Documentation**
+ https://docs.python.org/3.12/
+ The official home of the Python Programming Language
+```
+
+### Чтение новостей
+
+```
+Пользователь: Почитай новости
+Бот: 📰 **Последние новости:**
+ 1. Linux Kernel 6.8 Released
+ 📅 2024-03-10 14:30:00
+ 🔗 https://...
+```
+
+### SSH команда
+
+```
+Пользователь: Проверь нагрузку на сервере
+Бот: 🖥️ **SSH: home**
+ **Команда:** `uptime`
+
+ **Вывод:**
+ ```
+ 14:30:00 up 10 days, 2:30, 1 user, load average: 0.15, 0.10, 0.05
+ ```
+
+ ✅ **Успешно**
+```
+
+### Управление задачами
+
+```
+Пользователь: Напомни мне каждый день делать бэкап
+Бот: ⏰ **Ваши задачи:**
+
+ ✅ **Daily Backup**
+ Команда: `/home/user/backup.sh`
+ Расписание: @daily
+ Следующий запуск: 2024-03-11 00:00:00
+```
+
+## 🔐 Безопасность
+
+Для персонального бота приоритет на **удобстве и функциональности**:
+- Инструменты доступны всегда
+- Бот использует их без прямой команды
+- Agentic AI — бот сам решает когда нужен инструмент
+
+**Важно:** При добавлении новых серверов в SSH не забывайте обновлять конфигурацию в `bot/tools/ssh_tool.py`.
diff --git a/bot.py b/bot.py
index 59e652a..22659a8 100644
--- a/bot.py
+++ b/bot.py
@@ -78,6 +78,10 @@ from bot.handlers.commands import start_command, menu_command, help_command, set
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
+
# Глобальные менеджеры сессий
ssh_session_manager = SSHSessionManager()
local_session_manager = LocalSessionManager()
@@ -121,7 +125,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
async def handle_ai_task(update: Update, text: str):
- """Обработка задачи для ИИ агента с использованием системы памяти."""
+ """Обработка задачи для ИИ агента с использованием системы памяти и инструментов."""
user_id = update.effective_user.id
state = state_manager.get(user_id)
@@ -138,6 +142,38 @@ async def handle_ai_task(update: Update, text: str):
# Отправляем статус
status_msg = await update.message.reply_text("⏳ 🤖 Думаю...")
+ # === ПРОВЕРКА: Решение AI агента об использовании инструментов ===
+ agent_decision = await ai_agent.decide(text, context={'user_id': user_id})
+
+ if agent_decision.should_use_tool:
+ logger.info(f"AI агент решил использовать инструмент: {agent_decision.tool_name} (confidence={agent_decision.confidence})")
+
+ # Выполняем инструмент
+ tool_result = await ai_agent.execute_tool(
+ agent_decision.tool_name,
+ **agent_decision.tool_args
+ )
+
+ if tool_result.success:
+ # Формируем ответ с результатами инструмента
+ full_output = format_tool_result(agent_decision.tool_name, tool_result)
+
+ # Добавляем в историю
+ state.ai_chat_history.append(f"Assistant: {full_output[:500]}")
+ save_message(user_id, "assistant", full_output)
+
+ # Отправляем ответ
+ if len(full_output) > 3500:
+ full_output = full_output[:3500] + "\n... (вывод обрезан)"
+
+ await send_long_message(update, full_output, parse_mode="Markdown")
+ await status_msg.delete()
+ return
+ else:
+ logger.warning(f"Инструмент {agent_decision.tool_name} вернул ошибку: {tool_result.error}")
+ # Продолжаем с обычным ИИ-ответом если инструмент не сработал
+
+ # === ОБЫЧНЫЙ ИИ-ОТВЕТ через Qwen ===
output_buffer = []
def on_output(text: str):
@@ -155,21 +191,27 @@ async def handle_ai_task(update: Update, text: str):
# Считаем токены в контексте (примерно: 1 слово ≈ 1.3 токена)
context_words = len((memory_context + "\n" + history_context).split())
context_tokens = int(context_words * 1.3)
-
+
# Максимальный контекст модели (Qwen поддерживает до 256K токенов)
# Для безопасности берём 200K
MAX_CONTEXT_TOKENS = 200_000
context_percent = round((context_tokens / MAX_CONTEXT_TOKENS) * 100, 1)
- # Собираем полный промпт
+ # Собираем полный промпт с системным промптом
+ system_prompt = qwen_manager.load_system_prompt()
+
full_task = (
+ f"{system_prompt}\n\n"
+ f"=== КОНТЕКСТ ПАМЯТИ ===\n"
f"{memory_context}\n\n"
- f"Previous conversation:\n{history_context}\n\n"
- f"Current request: {text}"
+ f"=== ИСТОРИЯ ДИАЛОГА ===\n"
+ f"{history_context}\n\n"
+ f"=== ЗАПРОС ПОЛЬЗОВАТЕЛЯ ===\n"
+ f"{text}"
)
- # Выполняем задачу
- result = await qwen_manager.run_task(user_id, full_task, on_output, on_oauth_url)
+ # Выполняем задачу (системный промпт уже добавлен в full_task)
+ result = await qwen_manager.run_task(user_id, full_task, on_output, on_oauth_url, use_system_prompt=False)
# Показываем результат
full_output = "".join(output_buffer).strip()
@@ -202,6 +244,109 @@ async def handle_ai_task(update: Update, text: str):
await send_long_message(update, response_text, parse_mode="Markdown")
+def format_tool_result(tool_name: str, result: 'ToolResult') -> str:
+ """Форматировать результат выполнения инструмента."""
+ from bot.tools import ToolResult
+
+ if tool_name == 'ddgs_search':
+ if not result.data:
+ return "🔍 Ничего не найдено по вашему запросу."
+
+ output = "🔍 **Результаты поиска:**\n\n"
+ for i, item in enumerate(result.data[:5], 1):
+ title = item.get('title', 'Без названия')
+ href = item.get('href', '')
+ body = item.get('body', '')[:200]
+ output += f"{i}. **{title}**\n"
+ if href:
+ output += f" {href}\n"
+ if body:
+ output += f" {body}\n\n"
+
+ return output
+
+ elif tool_name == 'rss_reader':
+ action = result.metadata.get('action', 'list')
+
+ if action == 'list' and result.data:
+ output = "📰 **Последние новости:**\n\n"
+ for i, item in enumerate(result.data[:10], 1):
+ title = item.get('title', 'Без названия')
+ pub_date = item.get('pub_date', '')
+ link = item.get('link', '')[:50]
+ output += f"{i}. {title}\n"
+ if pub_date:
+ output += f" 📅 {pub_date}\n"
+ if link:
+ output += f" 🔗 {link}\n\n"
+ return output
+
+ elif action == 'fetch':
+ total = result.data.get('total_new_items', 0) if result.data else 0
+ return f"✅ Получено {total} новых элементов из лент."
+
+ elif action == 'list_feeds' and result.data:
+ output = "📑 **Ваши RSS ленты:**\n\n"
+ for feed in result.data:
+ output += f"• {feed.get('title', feed.get('url', 'Unknown'))}\n"
+ return output
+
+ return f"RSS: {result.data}"
+
+ elif tool_name == 'ssh_executor':
+ if not result.success:
+ return f"❌ **Ошибка SSH:**\n```\n{result.error}\n```"
+
+ data = result.data
+ server = result.metadata.get('server', 'unknown')
+ command = result.metadata.get('command', '')
+
+ output = f"🖥️ **SSH: {server}**\n"
+ output += f"**Команда:** `{command}`\n\n"
+
+ if data.get('stdout'):
+ output += f"**Вывод:**\n```\n{data['stdout']}\n```\n\n"
+
+ if data.get('stderr'):
+ output += f"**Ошибки:**\n```\n{data['stderr']}\n```\n\n"
+
+ if data.get('returncode') == 0:
+ output += "✅ **Успешно**"
+ else:
+ output += f"❌ **Код возврата:** {data.get('returncode', 'N/A')}"
+
+ return output
+
+ elif tool_name == 'cron_manager':
+ 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"
+ if job.get('next_run'):
+ output += f" Следующий запуск: {job.get('next_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')}"
+
+ 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', '')}"
+
+ return f"Cron: {result.data}"
+
+ return f"Инструмент {tool_name}: {result.data}"
+
+
async def handle_ssh_session_input(update: Update, text: str, session: SSHSession):
"""Обработка ввода пользователя в активную SSH-сессию."""
user_id = update.effective_user.id
diff --git a/bot/ai_agent.py b/bot/ai_agent.py
new file mode 100644
index 0000000..1eff7a8
--- /dev/null
+++ b/bot/ai_agent.py
@@ -0,0 +1,319 @@
+#!/usr/bin/env python3
+"""
+AI Agent Module - автономный агент с инструментами.
+
+Агент может самостоятельно принимать решения об использовании инструментов
+на основе контекста запроса пользователя.
+"""
+
+import logging
+from typing import Optional, List, Dict, Any
+from dataclasses import dataclass
+from datetime import datetime
+
+from bot.tools import tools_registry, ToolResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class AgentDecision:
+ """Решение агента об использовании инструмента."""
+ should_use_tool: bool
+ tool_name: Optional[str] = None
+ tool_args: Optional[Dict[str, Any]] = None
+ confidence: float = 0.0
+ reasoning: str = ""
+
+
+class AIAgent:
+ """
+ AI-агент с доступом к инструментам.
+
+ Агент анализирует запрос и решает, нужно ли использовать
+ какой-либо инструмент для выполнения задачи.
+ """
+
+ # Триггеры для поиска в интернете
+ SEARCH_TRIGGERS = [
+ 'найди', 'поиск', 'погугли', 'узнай', 'проверь в интернете',
+ 'что нового', 'последние новости', 'свежая информация',
+ 'как сделать', 'руководство', 'документация', 'tutorial',
+ 'weather', 'news', 'search', 'find', 'look up',
+ 'что это', 'кто такой', 'где находится', 'когда выйдет',
+ 'скачай', 'загрузи', 'найди информацию', 'посмотри в сети'
+ ]
+
+ # Триггеры для RSS
+ RSS_TRIGGERS = [
+ 'новости', 'rss', 'лента', 'feed', 'дайджест',
+ 'что нового в linux', 'новости it', 'tech news',
+ 'почитай новости', 'последние статьи', 'свежие новости',
+ 'новости технологий', 'opensource новости', 'linux новости',
+ 'покажи новости', 'что в лентах', 'есть новые статьи'
+ ]
+
+ # Триггеры для SSH-команд
+ SSH_TRIGGERS = [
+ 'выполни команду', 'ssh', 'запусти на сервере', 'проверь сервер',
+ 'посмотри логи', 'покажи процесс', 'сколько места', 'df', 'top',
+ 'перезапусти', 'останови', 'запусти сервис', 'systemctl',
+ 'проверь нагрузку', 'uptime', 'кто залогинен', 'who', 'last',
+ 'посмотри в /var/log', 'проверь диск', 'мониторинг',
+ 'выполни на 192.168.1', 'запусти скрипт', 'cron'
+ ]
+
+ # Триггеры для Cron-задач
+ CRON_TRIGGERS = [
+ 'напомни', 'запланируй', 'каждый день', 'каждый час',
+ 'периодически', 'по расписанию', 'автоматически',
+ 'создай задачу', 'добавь в cron', 'регулярно',
+ 'повторяй', 'каждую неделю', 'ежедневно', 'ежечасно'
+ ]
+
+ def __init__(self):
+ self.registry = tools_registry
+ self._tool_use_history: List[Dict] = []
+ self._user_preferences: Dict[int, Dict] = {} # preferences per user
+
+ def _should_search(self, message: str) -> tuple[bool, float]:
+ """Проверить, нужен ли поиск в интернете."""
+ message_lower = message.lower()
+ score = 0.0
+
+ # Прямые триггеры — высокий приоритет
+ for trigger in self.SEARCH_TRIGGERS:
+ if trigger in message_lower:
+ return True, 0.9
+
+ # Вопросы с "что", "как", "где", "когда" о внешних фактах
+ question_words = ['что такое', 'как сделать', 'где найти', 'когда будет']
+ for qword in question_words:
+ if qword in message_lower:
+ score = max(score, 0.7)
+
+ # Упоминания текущих событий
+ current_events = ['сегодня', 'сейчас', 'в этом году', 'recent', 'latest', '2024', '2025', '2026']
+ for event in current_events:
+ if event in message_lower:
+ score = max(score, 0.6)
+
+ # Если есть вопросительные слова + внешние факты
+ if any(word in message_lower for word in ['почему', 'зачем', 'как работает']):
+ score = max(score, 0.65)
+
+ return score >= 0.65, score
+
+ def _should_read_rss(self, message: str) -> tuple[bool, float]:
+ """Проверить, нужно ли читать RSS ленты."""
+ message_lower = message.lower()
+ score = 0.0
+
+ for trigger in self.RSS_TRIGGERS:
+ if trigger in message_lower:
+ return True, 0.9
+
+ # Если пользователь спрашивает про новости технологий
+ tech_news = ['новости linux', 'it новости', 'tech news', 'opensource']
+ for topic in tech_news:
+ if topic in message_lower:
+ score = max(score, 0.8)
+
+ # Контекстные подсказки
+ if any(word in message_lower for word in ['почитай', 'посмотри', 'покажи']) and \
+ any(word in message_lower for word in ['статьи', 'материалы', 'публикации']):
+ score = max(score, 0.75)
+
+ return score >= 0.75, score
+
+ def _should_use_ssh(self, message: str) -> tuple[bool, float]:
+ """Проверить, нужна ли SSH-команда."""
+ message_lower = message.lower()
+ score = 0.0
+
+ # Прямые триггеры
+ for trigger in self.SSH_TRIGGERS:
+ if trigger in message_lower:
+ return True, 0.9
+
+ # Команды системного администрирования
+ sysadmin_tasks = ['проверь', 'посмотри', 'покажи', 'выполни', 'запусти']
+ sysadmin_objects = ['сервер', 'лог', 'процесс', 'диск', 'память', 'сервис', 'демон']
+
+ has_task = any(task in message_lower for task in sysadmin_tasks)
+ has_object = any(obj in message_lower for obj in sysadmin_objects)
+
+ if has_task and has_object:
+ score = max(score, 0.75)
+
+ # Упоминания конкретных утилит
+ utils = ['systemctl', 'journalctl', 'top', 'htop', 'df', 'du', 'free', 'ps', 'netstat']
+ for util in utils:
+ if util in message_lower:
+ score = max(score, 0.8)
+
+ return score >= 0.75, score
+
+ def _should_use_cron(self, message: str) -> tuple[bool, float]:
+ """Проверить, нужна ли cron-задача."""
+ message_lower = message.lower()
+ score = 0.0
+
+ # Прямые триггеры
+ for trigger in self.CRON_TRIGGERS:
+ if trigger in message_lower:
+ return True, 0.85
+
+ # Расписания
+ schedules = ['каждый', 'каждую', 'ежедневно', 'ежечасно', 'еженедельно', 'раз в']
+ for sched in schedules:
+ if sched in message_lower:
+ score = max(score, 0.8)
+
+ # Напоминания и периодические задачи
+ if any(word in message_lower for word in ['напомни', 'запланируй', 'повторяй']):
+ score = max(score, 0.85)
+
+ return score >= 0.8, score
+
+ async def decide(self, message: str, context: Optional[Dict] = None) -> AgentDecision:
+ """
+ Принять решение об использовании инструмента.
+
+ Args:
+ message: Сообщение пользователя
+ context: Дополнительный контекст (история, состояние)
+
+ Returns:
+ AgentDecision с решением агента
+ """
+ user_id = context.get('user_id') if context else None
+
+ # Приоритет: SSH > Cron > Поиск > RSS
+ # Проверяем в порядке приоритета
+
+ # 1. Проверка на SSH-команды (системные задачи)
+ should_ssh, ssh_conf = self._should_use_ssh(message)
+ if should_ssh and ssh_conf > 0.75:
+ return AgentDecision(
+ should_use_tool=True,
+ tool_name='ssh_executor',
+ tool_args={'command': self._extract_ssh_command(message)},
+ confidence=ssh_conf,
+ reasoning='Пользователю нужно выполнить команду на сервере'
+ )
+
+ # 2. Проверка на Cron-задачи (планирование)
+ should_cron, cron_conf = self._should_use_cron(message)
+ if should_cron and cron_conf > 0.75:
+ return AgentDecision(
+ should_use_tool=True,
+ tool_name='cron_manager',
+ tool_args={'action': 'list'}, # Показываем список задач
+ confidence=cron_conf,
+ reasoning='Пользователь хочет создать или управлять задачей'
+ )
+
+ # 3. Проверка на поиск
+ should_search, search_conf = self._should_search(message)
+ if should_search and search_conf > 0.7:
+ query = self._extract_search_query(message)
+ return AgentDecision(
+ should_use_tool=True,
+ tool_name='ddgs_search',
+ tool_args={'query': query, 'max_results': 5},
+ confidence=search_conf,
+ reasoning='Пользователю нужна информация из интернета'
+ )
+
+ # 4. Проверка на RSS
+ should_rss, rss_conf = self._should_read_rss(message)
+ if should_rss and rss_conf > 0.7:
+ return AgentDecision(
+ should_use_tool=True,
+ tool_name='rss_reader',
+ tool_args={'action': 'list', 'limit': 10, 'undigested_only': True},
+ confidence=rss_conf,
+ reasoning='Пользователь хочет прочитать новости из лент'
+ )
+
+ # Инструменты не нужны
+ return AgentDecision(
+ should_use_tool=False,
+ confidence=0.0,
+ reasoning='Инструменты не требуются'
+ )
+
+ def _extract_search_query(self, message: str) -> str:
+ """Извлечь поисковый запрос из сообщения."""
+ triggers_to_remove = self.SEARCH_TRIGGERS + ['покажи', 'напиши', 'дай', 'расскажи', 'хочу', 'надо', 'нужно']
+
+ query = message.lower()
+ for trigger in triggers_to_remove:
+ query = query.replace(trigger, '')
+
+ query = query.strip(' ?:.,!')
+
+ if not query:
+ query = message
+
+ return query.strip()
+
+ def _extract_ssh_command(self, message: str) -> str:
+ """Извлечь SSH-команду из сообщения."""
+ message_lower = message.lower()
+
+ # Если есть явная команда в кавычках
+ import re
+ quoted = re.search(r'["\']([^"\']+)["\']', message)
+ if quoted:
+ return quoted.group(1).strip()
+
+ # Если команда после триггера
+ for trigger in ['выполни команду', 'запусти', 'ssh']:
+ if trigger in message_lower:
+ idx = message_lower.find(trigger)
+ return message[idx + len(trigger):].strip()
+
+ # Возвращаем оригинальное сообщение как команду
+ return message
+
+ async def execute_tool(self, tool_name: str, **kwargs) -> ToolResult:
+ """Выполнить инструмент и сохранить историю."""
+ logger.info(f"🤖 AI-агент выполняет инструмент: {tool_name} с аргументами: {kwargs}")
+
+ result = await self.registry.execute_tool(tool_name, **kwargs)
+
+ # Сохраняем историю использования
+ self._tool_use_history.append({
+ 'tool_name': tool_name,
+ 'args': kwargs,
+ 'result': result.to_dict(),
+ 'timestamp': datetime.now().isoformat()
+ })
+
+ # Ограничиваем историю
+ if len(self._tool_use_history) > 100:
+ self._tool_use_history = self._tool_use_history[-50:]
+
+ logger.info(f"✅ Инструмент {tool_name} выполнен: success={result.success}")
+ return result
+
+ def get_tool_history(self, limit: int = 10) -> List[Dict]:
+ """Получить историю использования инструментов."""
+ return self._tool_use_history[-limit:]
+
+ def set_user_preference(self, user_id: int, preference: str, value: Any):
+ """Установить предпочтение пользователя для инструментов."""
+ if user_id not in self._user_preferences:
+ self._user_preferences[user_id] = {}
+ self._user_preferences[user_id][preference] = value
+ logger.info(f"Установлено предпочтение для пользователя {user_id}: {preference} = {value}")
+
+ def get_user_preference(self, user_id: int, preference: str, default: Any = None) -> Any:
+ """Получить предпочтение пользователя."""
+ return self._user_preferences.get(user_id, {}).get(preference, default)
+
+
+# Глобальный агент
+ai_agent = AIAgent()
diff --git a/bot/tools/__init__.py b/bot/tools/__init__.py
new file mode 100644
index 0000000..2f6ac37
--- /dev/null
+++ b/bot/tools/__init__.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+"""
+Реестр инструментов для Telegram CLI Bot.
+
+Инструменты - это capabilities, которые бот может использовать автономно
+для выполнения задач пользователя (Agentic AI подход).
+"""
+
+import logging
+from abc import ABC, abstractmethod
+from typing import Dict, List, Any, Optional
+from dataclasses import dataclass, field
+from datetime import datetime
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class ToolResult:
+ """Результат выполнения инструмента."""
+ success: bool
+ data: Any = None
+ error: Optional[str] = None
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ def to_dict(self) -> Dict:
+ return {
+ 'success': self.success,
+ 'data': self.data,
+ 'error': self.error,
+ 'metadata': self.metadata
+ }
+
+
+class BaseTool(ABC):
+ """Базовый класс для всех инструментов."""
+
+ name: str = "base_tool"
+ description: str = "Базовый инструмент"
+ category: str = "general"
+
+ @abstractmethod
+ async def execute(self, **kwargs) -> ToolResult:
+ """Выполнить инструмент."""
+ pass
+
+ def get_capabilities(self) -> Dict:
+ """Вернуть описание возможностей инструмента."""
+ return {
+ 'name': self.name,
+ 'description': self.description,
+ 'category': self.category
+ }
+
+
+class ToolsRegistry:
+ """Реестр всех доступных инструментов."""
+
+ _instance: Optional['ToolsRegistry'] = None
+ _tools: Dict[str, BaseTool] = {}
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+
+ def register(self, tool: BaseTool):
+ """Зарегистрировать инструмент."""
+ self._tools[tool.name] = tool
+ logger.info(f"Зарегистрирован инструмент: {tool.name}")
+
+ def unregister(self, tool_name: str):
+ """От-register инструмент."""
+ if tool_name in self._tools:
+ del self._tools[tool_name]
+ logger.info(f"Удален инструмент: {tool_name}")
+
+ def get(self, tool_name: str) -> Optional[BaseTool]:
+ """Получить инструмент по имени."""
+ return self._tools.get(tool_name)
+
+ def get_all(self) -> Dict[str, BaseTool]:
+ """Получить все инструменты."""
+ return self._tools.copy()
+
+ def get_capabilities_list(self) -> List[Dict]:
+ """Получить список всех возможностей для ИИ."""
+ return [tool.get_capabilities() for tool in self._tools.values()]
+
+ async def execute_tool(self, tool_name: str, **kwargs) -> ToolResult:
+ """Выполнить инструмент по имени."""
+ tool = self.get(tool_name)
+ if not tool:
+ return ToolResult(
+ success=False,
+ error=f"Инструмент '{tool_name}' не найден"
+ )
+
+ logger.info(f"Выполнение инструмента: {tool_name} с аргументами: {kwargs}")
+ try:
+ result = await tool.execute(**kwargs)
+ result.metadata['tool_name'] = tool_name
+ result.metadata['timestamp'] = datetime.now().isoformat()
+ return result
+ except Exception as e:
+ logger.exception(f"Ошибка выполнения инструмента {tool_name}: {e}")
+ return ToolResult(
+ success=False,
+ error=str(e),
+ metadata={'tool_name': tool_name}
+ )
+
+
+# Глобальный экземпляр реестра
+tools_registry = ToolsRegistry()
+
+
+def register_tool(tool_class: type) -> type:
+ """Декоратор для автоматической регистрации инструмента."""
+ tool_instance = tool_class()
+ tools_registry.register(tool_instance)
+ return tool_class
+
+
+# Авто-импорт инструментов для регистрации
+# Импортируем после определения register_tool чтобы декоратор сработал
+from bot.tools import ddgs_tool, rss_tool, ssh_tool, cron_tool
diff --git a/bot/tools/cron_tool.py b/bot/tools/cron_tool.py
new file mode 100644
index 0000000..0a91759
--- /dev/null
+++ b/bot/tools/cron_tool.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python3
+"""
+Cron Tool - инструмент для управления задачами пользователя.
+
+Позволяет создавать, планировать и выполнять периодические задачи.
+"""
+
+import logging
+import sqlite3
+import json
+from pathlib import Path
+from datetime import datetime, timedelta
+from typing import List, Dict, Any, Optional, Callable
+from dataclasses import dataclass, field
+
+from bot.tools import BaseTool, ToolResult, register_tool
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class CronJob:
+ """Задача cron."""
+ id: Optional[int]
+ name: str
+ command: str
+ schedule: str # cron format: "*/5 * * * *" или "daily", "hourly"
+ enabled: 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_manager"
+ description = "Управление периодическими задачами пользователя. Создание, планирование и выполнение задач по расписанию."
+ category = "automation"
+
+ def __init__(self, db_path: str = None):
+ self.db_path = Path(db_path) if db_path else Path(__file__).parent.parent.parent / "cron.db"
+ self._jobs: Dict[int, CronJob] = {}
+ self._init_db()
+
+ def _init_db(self):
+ """Инициализировать БД."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+ c.execute('''
+ CREATE TABLE IF NOT EXISTS cron_jobs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ command TEXT NOT NULL,
+ schedule TEXT NOT NULL,
+ enabled INTEGER DEFAULT 1,
+ last_run DATETIME,
+ next_run DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ ''')
+ conn.commit()
+ conn.close()
+
+ def _parse_schedule(self, schedule: str) -> Optional[datetime]:
+ """
+ Распарсить расписание и вернуть следующее время выполнения.
+
+ Поддерживает:
+ - "*/N * * * *" - каждые N минут
+ - "@hourly" - каждый час
+ - "@daily" - каждый день
+ - "@weekly" - каждую неделю
+ """
+ now = datetime.now()
+
+ if schedule.startswith('*/'):
+ # Каждые N минут
+ try:
+ minutes = int(schedule.split()[0][2:])
+ return now + timedelta(minutes=minutes)
+ except (ValueError, IndexError):
+ return None
+
+ elif schedule == '@hourly':
+ return now + timedelta(hours=1)
+
+ elif schedule == '@daily':
+ return now + timedelta(days=1)
+
+ elif schedule == '@weekly':
+ return now + timedelta(weeks=1)
+
+ return None
+
+ async def add_job(self, name: str, command: str, schedule: str) -> ToolResult:
+ """Добавить задачу."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+
+ try:
+ next_run = self._parse_schedule(schedule)
+ 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))
+
+ job_id = c.lastrowid
+ conn.commit()
+
+ self._jobs[job_id] = CronJob(
+ id=job_id,
+ name=name,
+ command=command,
+ schedule=schedule,
+ next_run=next_run
+ )
+
+ return ToolResult(
+ success=True,
+ data={'id': job_id, 'name': name, 'schedule': schedule, 'next_run': next_run_str},
+ metadata={'status': 'added'}
+ )
+
+ except Exception as e:
+ logger.exception(f"Ошибка добавления задачи: {e}")
+ return ToolResult(
+ success=False,
+ error=str(e)
+ )
+ finally:
+ conn.close()
+
+ async def list_jobs(self) -> ToolResult:
+ """Получить список всех задач."""
+ 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
+ ''')
+ rows = c.fetchall()
+ conn.close()
+
+ jobs = []
+ for row in rows:
+ jobs.append({
+ 'id': row[0],
+ 'name': row[1],
+ 'command': row[2],
+ 'schedule': row[3],
+ 'enabled': bool(row[4]),
+ 'last_run': row[5],
+ 'next_run': row[6],
+ 'created_at': row[7]
+ })
+
+ return ToolResult(
+ success=True,
+ data=jobs,
+ metadata={'count': len(jobs)}
+ )
+
+ async def remove_job(self, job_id: int) -> ToolResult:
+ """Удалить задачу."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+ c.execute("DELETE FROM cron_jobs WHERE id = ?", (job_id,))
+ if c.rowcount == 0:
+ conn.close()
+ return ToolResult(
+ success=False,
+ error=f"Задача не найдена: {job_id}"
+ )
+ conn.commit()
+ conn.close()
+
+ if job_id in self._jobs:
+ del self._jobs[job_id]
+
+ return ToolResult(
+ success=True,
+ data={'id': job_id},
+ metadata={'status': 'removed'}
+ )
+
+ async def toggle_job(self, job_id: int, enabled: bool) -> ToolResult:
+ """Включить/выключить задачу."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+ c.execute("UPDATE cron_jobs SET enabled = ? WHERE id = ?", (1 if enabled else 0, job_id))
+ if c.rowcount == 0:
+ conn.close()
+ return ToolResult(
+ success=False,
+ error=f"Задача не найдена: {job_id}"
+ )
+ conn.commit()
+ conn.close()
+
+ return ToolResult(
+ success=True,
+ data={'id': job_id, 'enabled': enabled},
+ metadata={'status': 'toggled'}
+ )
+
+ async def run_job(self, job_id: int) -> ToolResult:
+ """Выполнить задачу немедленно."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+ c.execute("SELECT command FROM cron_jobs WHERE id = ?", (job_id,))
+ row = c.fetchone()
+
+ if not row:
+ conn.close()
+ return ToolResult(
+ success=False,
+ error=f"Задача не найдена: {job_id}"
+ )
+
+ command = row[0]
+ conn.close()
+
+ # Здесь должна быть логика выполнения команды
+ # Для демонстрации возвращаем заглушку
+ logger.info(f"Выполнение задачи {job_id}: {command}")
+
+ # Обновляем 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={'id': job_id, 'command': command, 'message': 'Задача выполнена'},
+ metadata={'status': 'executed'}
+ )
+
+ async def execute(self, action: str = "list", **kwargs) -> ToolResult:
+ """
+ Выполнить действие с cron задачами.
+
+ Args:
+ action: Действие - list, add, remove, toggle, run
+ kwargs: Дополнительные аргументы
+ """
+ actions = {
+ 'list': self.list_jobs,
+ 'add': lambda: self.add_job(
+ name=kwargs.get('name'),
+ command=kwargs.get('command'),
+ schedule=kwargs.get('schedule')
+ ),
+ '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'))
+ }
+
+ if action not in actions:
+ return ToolResult(
+ success=False,
+ error=f"Неизвестное действие: {action}. Доступные: {list(actions.keys())}"
+ )
+
+ logger.info(f"Cron действие: {action} с аргументами: {kwargs}")
+ return await actions[action]()
+
+
+# Автоматическая регистрация при импорте
+@register_tool
+class CronToolAuto(CronTool):
+ """Авто-регистрируемая версия CronTool."""
+ pass
diff --git a/bot/tools/ddgs_tool.py b/bot/tools/ddgs_tool.py
new file mode 100644
index 0000000..704b381
--- /dev/null
+++ b/bot/tools/ddgs_tool.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+"""
+DDGS Search Tool - инструмент для поиска в интернете через DuckDuckGo.
+
+Бот может использовать этот инструмент автономно, когда пользователю нужна
+свежая информация из интернета.
+"""
+
+import sys
+import json
+import logging
+from pathlib import Path
+from typing import List, Dict, Any
+
+from bot.tools import BaseTool, ToolResult, register_tool
+
+logger = logging.getLogger(__name__)
+
+
+class DDGSTool(BaseTool):
+ """Инструмент поиска через DuckDuckGo."""
+
+ name = "ddgs_search"
+ description = "Поиск информации в интернете через DuckDuckGo. Используется когда нужны свежие данные, новости, факты."
+ category = "search"
+
+ def __init__(self, tools_dir: str = None):
+ self.tools_dir = Path(tools_dir) if tools_dir else Path(__file__).parent.parent / "tools"
+ self.script_path = self.tools_dir / "ddgs_search.py"
+
+ def _search_ddgs(self, query: str, max_results: int = 10) -> List[Dict]:
+ """
+ Выполнить поиск через ddgs.
+
+ Args:
+ query: Поисковый запрос
+ max_results: Максимальное количество результатов
+
+ Returns:
+ Список результатов с title, href, body
+ """
+ try:
+ from ddgs import DDGS
+ except ImportError:
+ logger.error("ddgs library not found. Install: pip install ddgs")
+ return []
+
+ try:
+ ddgs = DDGS()
+ results = ddgs.text(query, max_results=max_results)
+
+ formatted_results = []
+ for result in results:
+ formatted_result = {
+ "title": result.get("title", ""),
+ "href": result.get("href", ""),
+ "body": result.get("body", "")
+ }
+ formatted_results.append(formatted_result)
+
+ return formatted_results
+ except Exception as e:
+ logger.error(f"Ошибка DDGS поиска: {e}")
+ return []
+
+ async def execute(self, query: str, max_results: int = 10) -> ToolResult:
+ """
+ Выполнить поиск.
+
+ Args:
+ query: Поисковый запрос
+ max_results: Максимальное количество результатов (default: 10)
+ """
+ if not query or not query.strip():
+ return ToolResult(
+ success=False,
+ error="Пустой поисковый запрос"
+ )
+
+ logger.info(f"DDGS поиск: '{query}' (max_results={max_results})")
+
+ results = self._search_ddgs(query, max_results)
+
+ if not results:
+ return ToolResult(
+ success=True,
+ data=[],
+ metadata={'message': 'Ничего не найдено', 'query': query}
+ )
+
+ return ToolResult(
+ success=True,
+ data=results,
+ metadata={
+ 'query': query,
+ 'count': len(results),
+ 'max_results': max_results
+ }
+ )
+
+
+# Автоматическая регистрация при импорте
+@register_tool
+class DDGSToolAuto(DDGSTool):
+ """Авто-регистрируемая версия DDGSTool."""
+ pass
diff --git a/bot/tools/rss_tool.py b/bot/tools/rss_tool.py
new file mode 100644
index 0000000..fc55433
--- /dev/null
+++ b/bot/tools/rss_tool.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+"""
+RSS Reader Tool - инструмент для чтения RSS/Atom лент.
+
+Бот может использовать этот инструмент автономно для получения новостей
+из подписанных лент пользователя.
+"""
+
+import sys
+import json
+import logging
+import sqlite3
+import subprocess
+import os
+import re
+from pathlib import Path
+from datetime import datetime, timedelta
+from typing import List, Dict, Any, Optional
+from email.utils import parsedate_to_datetime
+
+from bot.tools import BaseTool, ToolResult, register_tool
+
+logger = logging.getLogger(__name__)
+
+
+class RSSTool(BaseTool):
+ """Инструмент для работы с RSS лентами."""
+
+ name = "rss_reader"
+ description = "Чтение RSS/Atom новостных лент. Управление подписками, получение новостей, дайджесты."
+ category = "news"
+
+ def __init__(self, db_path: str = None):
+ self.db_path = Path(db_path) if db_path else Path(__file__).parent.parent.parent / "rss.db"
+ self.lock_file = Path("/tmp/rss_fetch.lock")
+ self.fetch_interval_minutes = 5
+
+ def _init_db(self):
+ """Инициализировать БД."""
+ conn = sqlite3.connect(self.db_path)
+ c = conn.cursor()
+ c.execute('''
+ CREATE TABLE IF NOT EXISTS feeds (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ url TEXT NOT NULL UNIQUE,
+ title TEXT,
+ last_fetched DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ ''')
+ c.execute('''
+ CREATE TABLE IF NOT EXISTS news (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ feed_id INTEGER NOT NULL,
+ guid TEXT NOT NULL,
+ pub_date DATETIME,
+ title TEXT,
+ description TEXT,
+ content TEXT,
+ link TEXT,
+ digest_flag INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (feed_id) REFERENCES feeds(id),
+ UNIQUE(feed_id, guid)
+ )
+ ''')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_news_feed ON news(feed_id)')
+ c.execute('CREATE INDEX IF NOT EXISTS idx_news_date ON news(pub_date)')
+ conn.commit()
+ conn.close()
+
+ def _parse_feed(self, xml_content: str) -> List[Dict]:
+ """Парсить RSS/Atom XML."""
+ items = []
+
+ # Remove CDATA markers
+ xml = re.sub(r'', '', xml)
+
+ # Find all items
+ xml_items = re.findall(r'