diff --git a/docker/run/fs/exe/self_update_manager.py b/docker/run/fs/exe/self_update_manager.py index bbe4caa85..94745c26c 100644 --- a/docker/run/fs/exe/self_update_manager.py +++ b/docker/run/fs/exe/self_update_manager.py @@ -614,7 +614,7 @@ def execute_pending_update( if bool(request_data.get("backup_usr", True)): backup_destination = create_usr_backup( repo_dir=REPO_DIR, - backup_path=str(request_data.get("backup_path", "/a0/tmp/self-update-backups")), + backup_path=str(request_data.get("backup_path", "/root/update-backups")), backup_name=str(request_data.get("backup_name", "agent-zero-usr-backup.zip")), conflict_policy=str(request_data.get("backup_conflict_policy", "rename")), logger=logger, diff --git a/docs/guides/self-update.md b/docs/guides/self-update.md index d84e82f3b..289f2bcaf 100644 --- a/docs/guides/self-update.md +++ b/docs/guides/self-update.md @@ -27,7 +27,7 @@ Because these files live in `/exe`, you can recover from an older downgraded `/a The updater can create a zip backup of `/a0/usr` before replacing repository files. -- The default backup directory is `/a0/tmp/self-update-backups` +- The default backup directory is `/root/update-backups` - The default file name format is `usr-YYYYMMDD-HHMMSS.zip` - Conflict handling supports rename, overwrite, or fail-before-restart diff --git a/helpers/self_update.py b/helpers/self_update.py index 74c9a296c..b29e841c5 100644 --- a/helpers/self_update.py +++ b/helpers/self_update.py @@ -108,8 +108,7 @@ def get_log_text() -> str: def get_default_backup_dir(repo_dir: str | Path | None = None) -> Path: - repository = get_repo_dir(repo_dir) - return repository / "tmp" / "self-update-backups" + return Path("/root/update-backups") def get_repo_dir(repo_dir: str | Path | None = None) -> Path: diff --git a/plugins/_telegram_integration/README.md b/plugins/_telegram_integration/README.md index 1cb3ad67c..c83d6d3b3 100644 --- a/plugins/_telegram_integration/README.md +++ b/plugins/_telegram_integration/README.md @@ -8,6 +8,9 @@ This plugin connects one or more Telegram bots to Agent Zero. Each bot runs inde ## Main Behavior +- **Dependency management** + - Keeps `aiogram` in the plugin-local `requirements.txt` instead of the global root requirements. + - Active code paths call `helpers/dependencies.py::ensure_dependencies()` to install `aiogram` into the framework runtime on first use via `uv pip install --python -r plugins/_telegram_integration/requirements.txt`. - **Bot lifecycle** - Managed by a `job_loop` extension that starts, restarts, or stops bots whenever plugin settings change. - Supports both long-polling and webhook delivery modes. diff --git a/plugins/_telegram_integration/api/test_connection.py b/plugins/_telegram_integration/api/test_connection.py index 271c734f5..bfd95427c 100644 --- a/plugins/_telegram_integration/api/test_connection.py +++ b/plugins/_telegram_integration/api/test_connection.py @@ -1,5 +1,6 @@ from helpers.api import ApiHandler, Request from helpers.errors import format_error +from plugins._telegram_integration.helpers.dependencies import ensure_dependencies class TestConnection(ApiHandler): @@ -18,6 +19,7 @@ class TestConnection(ApiHandler): return {"success": False, "results": results} try: + ensure_dependencies() from plugins._telegram_integration.helpers.bot_manager import test_token ok, message = await test_token(token) results.append({ diff --git a/plugins/_telegram_integration/api/webhook.py b/plugins/_telegram_integration/api/webhook.py index 9056ae054..049be486b 100644 --- a/plugins/_telegram_integration/api/webhook.py +++ b/plugins/_telegram_integration/api/webhook.py @@ -1,5 +1,6 @@ from helpers.api import ApiHandler, Request, Response from helpers.print_style import PrintStyle +from plugins._telegram_integration.helpers.dependencies import ensure_dependencies class TelegramWebhook(ApiHandler): @@ -18,6 +19,7 @@ class TelegramWebhook(ApiHandler): return ["POST"] async def process(self, input: dict, request: Request) -> dict | Response: + ensure_dependencies() from aiogram.types import Update from plugins._telegram_integration.helpers.bot_manager import get_bot diff --git a/plugins/_telegram_integration/extensions/python/job_loop/_10_telegram_bot.py b/plugins/_telegram_integration/extensions/python/job_loop/_10_telegram_bot.py index e5d055ab3..ad2f9493d 100644 --- a/plugins/_telegram_integration/extensions/python/job_loop/_10_telegram_bot.py +++ b/plugins/_telegram_integration/extensions/python/job_loop/_10_telegram_bot.py @@ -5,6 +5,7 @@ from helpers.extension import Extension from helpers.errors import format_error from helpers.print_style import PrintStyle from helpers import plugins +from plugins._telegram_integration.helpers.dependencies import ensure_dependencies, has_aiogram PLUGIN_NAME: str = "_telegram_integration" @@ -13,6 +14,19 @@ PLUGIN_NAME: str = "_telegram_integration" class TelegramBotManager(Extension): async def execute(self, **kwargs: Any) -> None: + config = plugins.get_plugin_config(PLUGIN_NAME) or {} + bots_cfg = config.get("bots", []) + enabled_names = { + b["name"] for b in bots_cfg if b.get("enabled") and b.get("name") and b.get("token") + } + + # Avoid installing aiogram on idle ticks when Telegram is not configured. + if not enabled_names and not has_aiogram(): + return + + if enabled_names: + ensure_dependencies() + from plugins._telegram_integration.helpers.bot_manager import ( get_all_bots, create_bot, @@ -32,12 +46,6 @@ class TelegramBotManager(Extension): cleanup_old_attachments() - config = plugins.get_plugin_config(PLUGIN_NAME) or {} - bots_cfg = config.get("bots", []) - enabled_names = { - b["name"] for b in bots_cfg if b.get("enabled") and b.get("name") and b.get("token") - } - running = get_all_bots() # Stop bots that are no longer enabled diff --git a/plugins/_telegram_integration/extensions/python/process_chain_end/_55_telegram_reply.py b/plugins/_telegram_integration/extensions/python/process_chain_end/_55_telegram_reply.py index 11de2b57c..d0730a6dc 100644 --- a/plugins/_telegram_integration/extensions/python/process_chain_end/_55_telegram_reply.py +++ b/plugins/_telegram_integration/extensions/python/process_chain_end/_55_telegram_reply.py @@ -2,10 +2,14 @@ from helpers.extension import Extension from helpers.print_style import PrintStyle from helpers.errors import format_error from agent import AgentContext, LoopData, UserMessage -from plugins._telegram_integration.helpers.handler import ( - CTX_TG_BOT, CTX_TG_ATTACHMENTS, CTX_TG_KEYBOARD, - CTX_TG_TYPING_STOP, CTX_TG_REPLY_TO, +from plugins._telegram_integration.helpers.constants import ( + CTX_TG_BOT, + CTX_TG_ATTACHMENTS, + CTX_TG_KEYBOARD, + CTX_TG_TYPING_STOP, + CTX_TG_REPLY_TO, ) +from plugins._telegram_integration.helpers.dependencies import ensure_dependencies MAX_SEND_RETRIES: int = 2 CTX_SEND_FAILURES: str = "_telegram_send_failures" @@ -46,6 +50,7 @@ class TelegramAutoReply(Extension): attachments: list[str], keyboard: list[list[dict]] | None, ): + ensure_dependencies() from plugins._telegram_integration.helpers.handler import send_telegram_reply error = await send_telegram_reply( diff --git a/plugins/_telegram_integration/extensions/python/system_prompt/_20_telegram_context.py b/plugins/_telegram_integration/extensions/python/system_prompt/_20_telegram_context.py index cac462245..bf580a43c 100644 --- a/plugins/_telegram_integration/extensions/python/system_prompt/_20_telegram_context.py +++ b/plugins/_telegram_integration/extensions/python/system_prompt/_20_telegram_context.py @@ -1,6 +1,6 @@ from helpers.extension import Extension from agent import LoopData -from plugins._telegram_integration.helpers.handler import CTX_TG_BOT, CTX_TG_BOT_CFG +from plugins._telegram_integration.helpers.constants import CTX_TG_BOT, CTX_TG_BOT_CFG class TelegramContextPrompt(Extension): diff --git a/plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py b/plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py index 8b8885d60..14968c623 100644 --- a/plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py +++ b/plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py @@ -1,10 +1,11 @@ from helpers.extension import Extension from helpers.tool import Response -from plugins._telegram_integration.helpers.handler import ( +from plugins._telegram_integration.helpers.constants import ( CTX_TG_BOT, CTX_TG_ATTACHMENTS, CTX_TG_KEYBOARD, ) +from plugins._telegram_integration.helpers.dependencies import ensure_dependencies class TelegramResponseIntercept(Extension): @@ -40,6 +41,7 @@ class TelegramResponseIntercept(Extension): await self._send_inline(context, tool, response) async def _send_inline(self, context, tool, response: Response): + ensure_dependencies() from plugins._telegram_integration.helpers.handler import send_telegram_reply agent = self.agent diff --git a/plugins/_telegram_integration/helpers/constants.py b/plugins/_telegram_integration/helpers/constants.py new file mode 100644 index 000000000..830e41547 --- /dev/null +++ b/plugins/_telegram_integration/helpers/constants.py @@ -0,0 +1,16 @@ +PLUGIN_NAME = "_telegram_integration" +DOWNLOAD_FOLDER = "usr/uploads" +STATE_FILE = "usr/plugins/_telegram_integration/state.json" + +# Context data keys +CTX_TG_BOT = "telegram_bot" +CTX_TG_BOT_CFG = "telegram_bot_cfg" +CTX_TG_CHAT_ID = "telegram_chat_id" +CTX_TG_USER_ID = "telegram_user_id" +CTX_TG_USERNAME = "telegram_username" +CTX_TG_TYPING_STOP = "_telegram_typing_stop" +CTX_TG_REPLY_TO = "_telegram_reply_to_message_id" + +# Transient +CTX_TG_ATTACHMENTS = "_telegram_response_attachments" +CTX_TG_KEYBOARD = "_telegram_response_keyboard" diff --git a/plugins/_telegram_integration/helpers/dependencies.py b/plugins/_telegram_integration/helpers/dependencies.py new file mode 100644 index 000000000..fcfab19a8 --- /dev/null +++ b/plugins/_telegram_integration/helpers/dependencies.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import importlib +import importlib.util +import shutil +import subprocess +import sys +import threading +from pathlib import Path + +from helpers.errors import format_error +from helpers.print_style import PrintStyle + + +_LOCK = threading.Lock() +_CHECKED = False +_PLUGIN_DIR = Path(__file__).resolve().parents[1] +_REQUIREMENTS_FILE = _PLUGIN_DIR / "requirements.txt" + + +def has_aiogram() -> bool: + return importlib.util.find_spec("aiogram") is not None + + +def ensure_dependencies() -> None: + global _CHECKED + + if _CHECKED and has_aiogram(): + return + + with _LOCK: + if _CHECKED and has_aiogram(): + return + if has_aiogram(): + _CHECKED = True + return + + _install_aiogram() + importlib.invalidate_caches() + + if not has_aiogram(): + raise RuntimeError("Telegram dependency 'aiogram' is still unavailable after installation") + + _CHECKED = True + + +def _install_aiogram() -> None: + uv = shutil.which("uv") + if not uv: + raise RuntimeError("Telegram plugin requires 'uv' to install aiogram automatically") + if not _REQUIREMENTS_FILE.is_file(): + raise RuntimeError(f"Telegram plugin requirements file not found: {_REQUIREMENTS_FILE}") + + cmd = [ + uv, + "pip", + "install", + "--python", + sys.executable, + "-r", + str(_REQUIREMENTS_FILE), + ] + + PrintStyle.info("Telegram: aiogram not found, installing plugin dependency") + try: + subprocess.check_call(cmd, cwd=str(_PLUGIN_DIR)) + except Exception as e: + raise RuntimeError(f"Failed to install Telegram dependency 'aiogram': {format_error(e)}") from e diff --git a/plugins/_telegram_integration/helpers/handler.py b/plugins/_telegram_integration/helpers/handler.py index 5ea7ec6e8..444e54c2b 100644 --- a/plugins/_telegram_integration/helpers/handler.py +++ b/plugins/_telegram_integration/helpers/handler.py @@ -21,24 +21,20 @@ from initialize import initialize_agent from plugins._telegram_integration.helpers import telegram_client as tc from plugins._telegram_integration.helpers.bot_manager import get_bot - - -PLUGIN_NAME = "_telegram_integration" -DOWNLOAD_FOLDER = "usr/uploads" -STATE_FILE = "usr/plugins/_telegram_integration/state.json" - -# Context data keys -CTX_TG_BOT = "telegram_bot" -CTX_TG_BOT_CFG = "telegram_bot_cfg" -CTX_TG_CHAT_ID = "telegram_chat_id" -CTX_TG_USER_ID = "telegram_user_id" -CTX_TG_USERNAME = "telegram_username" -CTX_TG_TYPING_STOP = "_telegram_typing_stop" -CTX_TG_REPLY_TO = "_telegram_reply_to_message_id" - -# Transient -CTX_TG_ATTACHMENTS = "_telegram_response_attachments" -CTX_TG_KEYBOARD = "_telegram_response_keyboard" +from plugins._telegram_integration.helpers.constants import ( + PLUGIN_NAME, + DOWNLOAD_FOLDER, + STATE_FILE, + CTX_TG_BOT, + CTX_TG_BOT_CFG, + CTX_TG_CHAT_ID, + CTX_TG_USER_ID, + CTX_TG_USERNAME, + CTX_TG_TYPING_STOP, + CTX_TG_REPLY_TO, + CTX_TG_ATTACHMENTS, + CTX_TG_KEYBOARD, +) # Chat mapping: (bot_name, tg_user_id) → AgentContext ID @@ -590,4 +586,3 @@ def _inherit_model_override(ctx: AgentContext): ) if source: ctx.set_data("chat_model_override", source.get_data("chat_model_override")) - diff --git a/plugins/_telegram_integration/requirements.txt b/plugins/_telegram_integration/requirements.txt new file mode 100644 index 000000000..9d09e7007 --- /dev/null +++ b/plugins/_telegram_integration/requirements.txt @@ -0,0 +1 @@ +aiogram>=3.15.0 diff --git a/requirements.txt b/requirements.txt index 10c0d78da..189b6f4ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,7 +48,6 @@ html2text>=2024.2.26 beautifulsoup4>=4.12.3 boto3>=1.35.0 exchangelib>=5.4.3 -aiogram>=3.15.0 pywinpty==3.0.2; sys_platform == "win32" python-socketio>=5.14.2 uvicorn>=0.38.0