Change default self-update backup directory from /a0/tmp to /root and add lazy aiogram dependency loading for Telegram plugin

- Update default backup path from /a0/tmp/self-update-backups to /root/update-backups in self_update_manager.py, helpers/self_update.py, and documentation
- Move aiogram from global requirements.txt to plugin-local requirements for _telegram_integration
- Add ensure_dependencies() helper that installs aiogram on-demand via uv pip install
- Add has_aiogram() check to avoid
This commit is contained in:
frdel 2026-03-26 10:20:35 +01:00
parent 68ad5aca46
commit 247c8d845f
15 changed files with 135 additions and 35 deletions

View file

@ -614,7 +614,7 @@ def execute_pending_update(
if bool(request_data.get("backup_usr", True)): if bool(request_data.get("backup_usr", True)):
backup_destination = create_usr_backup( backup_destination = create_usr_backup(
repo_dir=REPO_DIR, 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")), backup_name=str(request_data.get("backup_name", "agent-zero-usr-backup.zip")),
conflict_policy=str(request_data.get("backup_conflict_policy", "rename")), conflict_policy=str(request_data.get("backup_conflict_policy", "rename")),
logger=logger, logger=logger,

View file

@ -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 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` - The default file name format is `usr-YYYYMMDD-HHMMSS.zip`
- Conflict handling supports rename, overwrite, or fail-before-restart - Conflict handling supports rename, overwrite, or fail-before-restart

View file

@ -108,8 +108,7 @@ def get_log_text() -> str:
def get_default_backup_dir(repo_dir: str | Path | None = None) -> Path: def get_default_backup_dir(repo_dir: str | Path | None = None) -> Path:
repository = get_repo_dir(repo_dir) return Path("/root/update-backups")
return repository / "tmp" / "self-update-backups"
def get_repo_dir(repo_dir: str | Path | None = None) -> Path: def get_repo_dir(repo_dir: str | Path | None = None) -> Path:

View file

@ -8,6 +8,9 @@ This plugin connects one or more Telegram bots to Agent Zero. Each bot runs inde
## Main Behavior ## 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 <current interpreter> -r plugins/_telegram_integration/requirements.txt`.
- **Bot lifecycle** - **Bot lifecycle**
- Managed by a `job_loop` extension that starts, restarts, or stops bots whenever plugin settings change. - Managed by a `job_loop` extension that starts, restarts, or stops bots whenever plugin settings change.
- Supports both long-polling and webhook delivery modes. - Supports both long-polling and webhook delivery modes.

View file

@ -1,5 +1,6 @@
from helpers.api import ApiHandler, Request from helpers.api import ApiHandler, Request
from helpers.errors import format_error from helpers.errors import format_error
from plugins._telegram_integration.helpers.dependencies import ensure_dependencies
class TestConnection(ApiHandler): class TestConnection(ApiHandler):
@ -18,6 +19,7 @@ class TestConnection(ApiHandler):
return {"success": False, "results": results} return {"success": False, "results": results}
try: try:
ensure_dependencies()
from plugins._telegram_integration.helpers.bot_manager import test_token from plugins._telegram_integration.helpers.bot_manager import test_token
ok, message = await test_token(token) ok, message = await test_token(token)
results.append({ results.append({

View file

@ -1,5 +1,6 @@
from helpers.api import ApiHandler, Request, Response from helpers.api import ApiHandler, Request, Response
from helpers.print_style import PrintStyle from helpers.print_style import PrintStyle
from plugins._telegram_integration.helpers.dependencies import ensure_dependencies
class TelegramWebhook(ApiHandler): class TelegramWebhook(ApiHandler):
@ -18,6 +19,7 @@ class TelegramWebhook(ApiHandler):
return ["POST"] return ["POST"]
async def process(self, input: dict, request: Request) -> dict | Response: async def process(self, input: dict, request: Request) -> dict | Response:
ensure_dependencies()
from aiogram.types import Update from aiogram.types import Update
from plugins._telegram_integration.helpers.bot_manager import get_bot from plugins._telegram_integration.helpers.bot_manager import get_bot

View file

@ -5,6 +5,7 @@ from helpers.extension import Extension
from helpers.errors import format_error from helpers.errors import format_error
from helpers.print_style import PrintStyle from helpers.print_style import PrintStyle
from helpers import plugins from helpers import plugins
from plugins._telegram_integration.helpers.dependencies import ensure_dependencies, has_aiogram
PLUGIN_NAME: str = "_telegram_integration" PLUGIN_NAME: str = "_telegram_integration"
@ -13,6 +14,19 @@ PLUGIN_NAME: str = "_telegram_integration"
class TelegramBotManager(Extension): class TelegramBotManager(Extension):
async def execute(self, **kwargs: Any) -> None: 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 ( from plugins._telegram_integration.helpers.bot_manager import (
get_all_bots, get_all_bots,
create_bot, create_bot,
@ -32,12 +46,6 @@ class TelegramBotManager(Extension):
cleanup_old_attachments() 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() running = get_all_bots()
# Stop bots that are no longer enabled # Stop bots that are no longer enabled

View file

@ -2,10 +2,14 @@ from helpers.extension import Extension
from helpers.print_style import PrintStyle from helpers.print_style import PrintStyle
from helpers.errors import format_error from helpers.errors import format_error
from agent import AgentContext, LoopData, UserMessage from agent import AgentContext, LoopData, UserMessage
from plugins._telegram_integration.helpers.handler import ( from plugins._telegram_integration.helpers.constants import (
CTX_TG_BOT, CTX_TG_ATTACHMENTS, CTX_TG_KEYBOARD, CTX_TG_BOT,
CTX_TG_TYPING_STOP, CTX_TG_REPLY_TO, 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 MAX_SEND_RETRIES: int = 2
CTX_SEND_FAILURES: str = "_telegram_send_failures" CTX_SEND_FAILURES: str = "_telegram_send_failures"
@ -46,6 +50,7 @@ class TelegramAutoReply(Extension):
attachments: list[str], attachments: list[str],
keyboard: list[list[dict]] | None, keyboard: list[list[dict]] | None,
): ):
ensure_dependencies()
from plugins._telegram_integration.helpers.handler import send_telegram_reply from plugins._telegram_integration.helpers.handler import send_telegram_reply
error = await send_telegram_reply( error = await send_telegram_reply(

View file

@ -1,6 +1,6 @@
from helpers.extension import Extension from helpers.extension import Extension
from agent import LoopData 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): class TelegramContextPrompt(Extension):

View file

@ -1,10 +1,11 @@
from helpers.extension import Extension from helpers.extension import Extension
from helpers.tool import Response from helpers.tool import Response
from plugins._telegram_integration.helpers.handler import ( from plugins._telegram_integration.helpers.constants import (
CTX_TG_BOT, CTX_TG_BOT,
CTX_TG_ATTACHMENTS, CTX_TG_ATTACHMENTS,
CTX_TG_KEYBOARD, CTX_TG_KEYBOARD,
) )
from plugins._telegram_integration.helpers.dependencies import ensure_dependencies
class TelegramResponseIntercept(Extension): class TelegramResponseIntercept(Extension):
@ -40,6 +41,7 @@ class TelegramResponseIntercept(Extension):
await self._send_inline(context, tool, response) await self._send_inline(context, tool, response)
async def _send_inline(self, context, tool, response: Response): async def _send_inline(self, context, tool, response: Response):
ensure_dependencies()
from plugins._telegram_integration.helpers.handler import send_telegram_reply from plugins._telegram_integration.helpers.handler import send_telegram_reply
agent = self.agent agent = self.agent

View file

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

View file

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

View file

@ -21,24 +21,20 @@ from initialize import initialize_agent
from plugins._telegram_integration.helpers import telegram_client as tc from plugins._telegram_integration.helpers import telegram_client as tc
from plugins._telegram_integration.helpers.bot_manager import get_bot from plugins._telegram_integration.helpers.bot_manager import get_bot
from plugins._telegram_integration.helpers.constants import (
PLUGIN_NAME,
PLUGIN_NAME = "_telegram_integration" DOWNLOAD_FOLDER,
DOWNLOAD_FOLDER = "usr/uploads" STATE_FILE,
STATE_FILE = "usr/plugins/_telegram_integration/state.json" CTX_TG_BOT,
CTX_TG_BOT_CFG,
# Context data keys CTX_TG_CHAT_ID,
CTX_TG_BOT = "telegram_bot" CTX_TG_USER_ID,
CTX_TG_BOT_CFG = "telegram_bot_cfg" CTX_TG_USERNAME,
CTX_TG_CHAT_ID = "telegram_chat_id" CTX_TG_TYPING_STOP,
CTX_TG_USER_ID = "telegram_user_id" CTX_TG_REPLY_TO,
CTX_TG_USERNAME = "telegram_username" CTX_TG_ATTACHMENTS,
CTX_TG_TYPING_STOP = "_telegram_typing_stop" CTX_TG_KEYBOARD,
CTX_TG_REPLY_TO = "_telegram_reply_to_message_id" )
# Transient
CTX_TG_ATTACHMENTS = "_telegram_response_attachments"
CTX_TG_KEYBOARD = "_telegram_response_keyboard"
# Chat mapping: (bot_name, tg_user_id) → AgentContext ID # Chat mapping: (bot_name, tg_user_id) → AgentContext ID
@ -590,4 +586,3 @@ def _inherit_model_override(ctx: AgentContext):
) )
if source: if source:
ctx.set_data("chat_model_override", source.get_data("chat_model_override")) ctx.set_data("chat_model_override", source.get_data("chat_model_override"))

View file

@ -0,0 +1 @@
aiogram>=3.15.0

View file

@ -48,7 +48,6 @@ html2text>=2024.2.26
beautifulsoup4>=4.12.3 beautifulsoup4>=4.12.3
boto3>=1.35.0 boto3>=1.35.0
exchangelib>=5.4.3 exchangelib>=5.4.3
aiogram>=3.15.0
pywinpty==3.0.2; sys_platform == "win32" pywinpty==3.0.2; sys_platform == "win32"
python-socketio>=5.14.2 python-socketio>=5.14.2
uvicorn>=0.38.0 uvicorn>=0.38.0