mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(gateway): complete lazy-install rebind for slack/feishu/matrix + add ensure_and_bind helper (#25038)
Fixes #25028. The lazy-install hooks added in #25014 installed packages correctly but failed to rebind module-level globals after install: - Slack: missing aiohttp rebind → NameError on file uploads - Feishu: none of the ~25 lark_oapi symbols rebound → TypeError on adapter instantiation - Matrix: mautrix.types enums stayed as stubs → mismatched values at runtime Introduces tools.lazy_deps.ensure_and_bind() — a DRY helper that combines ensure() + importer-callable + globals().update(). This eliminates the error-prone pattern of manually listing every global that needs updating after lazy-install. Each platform adapter now defines a single _import() function returning all bindings. Also fixes: pyproject.toml [slack] extra was missing aiohttp (needed by slack-bolt's async path).
This commit is contained in:
parent
52521c937a
commit
d898e0eb7f
6 changed files with 149 additions and 39 deletions
|
|
@ -1346,22 +1346,62 @@ def check_feishu_requirements() -> bool:
|
|||
"""Check if Feishu/Lark dependencies are available.
|
||||
|
||||
Lazy-installs lark-oapi via ``tools.lazy_deps.ensure("platform.feishu")``
|
||||
on first call if not present.
|
||||
on first call if not present. Rebinds all module-level globals on success.
|
||||
"""
|
||||
global FEISHU_AVAILABLE
|
||||
if FEISHU_AVAILABLE:
|
||||
return True
|
||||
try:
|
||||
from tools.lazy_deps import ensure as _lazy_ensure
|
||||
_lazy_ensure("platform.feishu", prompt=False)
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
import lark_oapi # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
FEISHU_AVAILABLE = True
|
||||
return True
|
||||
|
||||
def _import():
|
||||
import lark_oapi as lark
|
||||
from lark_oapi.api.application.v6 import GetApplicationRequest
|
||||
from lark_oapi.api.im.v1 import (
|
||||
CreateFileRequest, CreateFileRequestBody,
|
||||
CreateImageRequest, CreateImageRequestBody,
|
||||
CreateMessageRequest, CreateMessageRequestBody,
|
||||
GetChatRequest, GetMessageRequest, GetMessageResourceRequest,
|
||||
P2ImMessageMessageReadV1,
|
||||
ReplyMessageRequest, ReplyMessageRequestBody,
|
||||
UpdateMessageRequest, UpdateMessageRequestBody,
|
||||
)
|
||||
from lark_oapi.core import AccessTokenType, HttpMethod
|
||||
from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN
|
||||
from lark_oapi.core.model import BaseRequest
|
||||
from lark_oapi.event.callback.model.p2_card_action_trigger import (
|
||||
CallBackCard, P2CardActionTriggerResponse,
|
||||
)
|
||||
from lark_oapi.event.dispatcher_handler import EventDispatcherHandler
|
||||
from lark_oapi.ws import Client as FeishuWSClient
|
||||
return {
|
||||
"lark": lark,
|
||||
"GetApplicationRequest": GetApplicationRequest,
|
||||
"CreateFileRequest": CreateFileRequest,
|
||||
"CreateFileRequestBody": CreateFileRequestBody,
|
||||
"CreateImageRequest": CreateImageRequest,
|
||||
"CreateImageRequestBody": CreateImageRequestBody,
|
||||
"CreateMessageRequest": CreateMessageRequest,
|
||||
"CreateMessageRequestBody": CreateMessageRequestBody,
|
||||
"GetChatRequest": GetChatRequest,
|
||||
"GetMessageRequest": GetMessageRequest,
|
||||
"GetMessageResourceRequest": GetMessageResourceRequest,
|
||||
"P2ImMessageMessageReadV1": P2ImMessageMessageReadV1,
|
||||
"ReplyMessageRequest": ReplyMessageRequest,
|
||||
"ReplyMessageRequestBody": ReplyMessageRequestBody,
|
||||
"UpdateMessageRequest": UpdateMessageRequest,
|
||||
"UpdateMessageRequestBody": UpdateMessageRequestBody,
|
||||
"AccessTokenType": AccessTokenType,
|
||||
"HttpMethod": HttpMethod,
|
||||
"FEISHU_DOMAIN": FEISHU_DOMAIN,
|
||||
"LARK_DOMAIN": LARK_DOMAIN,
|
||||
"BaseRequest": BaseRequest,
|
||||
"CallBackCard": CallBackCard,
|
||||
"P2CardActionTriggerResponse": P2CardActionTriggerResponse,
|
||||
"EventDispatcherHandler": EventDispatcherHandler,
|
||||
"FeishuWSClient": FeishuWSClient,
|
||||
"FEISHU_AVAILABLE": True,
|
||||
}
|
||||
|
||||
from tools.lazy_deps import ensure_and_bind
|
||||
return ensure_and_bind("platform.feishu", _import, globals(), prompt=False)
|
||||
|
||||
|
||||
class FeishuAdapter(BasePlatformAdapter):
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ def check_matrix_requirements() -> bool:
|
|||
"""Return True if the Matrix adapter can be used.
|
||||
|
||||
Lazy-installs mautrix via ``tools.lazy_deps.ensure("platform.matrix")``
|
||||
on first call if not present.
|
||||
on first call if not present. Rebinds all module-level type globals on success.
|
||||
"""
|
||||
token = os.getenv("MATRIX_ACCESS_TOKEN", "")
|
||||
password = os.getenv("MATRIX_PASSWORD", "")
|
||||
|
|
@ -242,11 +242,27 @@ def check_matrix_requirements() -> bool:
|
|||
try:
|
||||
import mautrix # noqa: F401
|
||||
except ImportError:
|
||||
try:
|
||||
from tools.lazy_deps import ensure as _lazy_ensure
|
||||
_lazy_ensure("platform.matrix", prompt=False)
|
||||
import mautrix # noqa: F401, F811
|
||||
except Exception:
|
||||
def _import():
|
||||
from mautrix.types import (
|
||||
ContentURI, EventID, EventType, PaginationDirection,
|
||||
PresenceState, RoomCreatePreset, RoomID, SyncToken,
|
||||
TrustState, UserID,
|
||||
)
|
||||
return {
|
||||
"ContentURI": ContentURI,
|
||||
"EventID": EventID,
|
||||
"EventType": EventType,
|
||||
"PaginationDirection": PaginationDirection,
|
||||
"PresenceState": PresenceState,
|
||||
"RoomCreatePreset": RoomCreatePreset,
|
||||
"RoomID": RoomID,
|
||||
"SyncToken": SyncToken,
|
||||
"TrustState": TrustState,
|
||||
"UserID": UserID,
|
||||
}
|
||||
|
||||
from tools.lazy_deps import ensure_and_bind
|
||||
if not ensure_and_bind("platform.matrix", _import, globals(), prompt=False):
|
||||
logger.warning(
|
||||
"Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -76,27 +76,26 @@ def check_slack_requirements() -> bool:
|
|||
"""Check if Slack dependencies are available.
|
||||
|
||||
Lazy-installs slack-bolt/slack-sdk via ``tools.lazy_deps.ensure("platform.slack")``
|
||||
on first call if not present.
|
||||
on first call if not present. Rebinds all module-level globals on success.
|
||||
"""
|
||||
global SLACK_AVAILABLE, AsyncApp, AsyncSocketModeHandler, AsyncWebClient
|
||||
if SLACK_AVAILABLE:
|
||||
return True
|
||||
try:
|
||||
from tools.lazy_deps import ensure as _lazy_ensure
|
||||
_lazy_ensure("platform.slack", prompt=False)
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
from slack_bolt.async_app import AsyncApp as _App
|
||||
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler as _Handler
|
||||
from slack_sdk.web.async_client import AsyncWebClient as _Client
|
||||
except ImportError:
|
||||
return False
|
||||
AsyncApp = _App
|
||||
AsyncSocketModeHandler = _Handler
|
||||
AsyncWebClient = _Client
|
||||
SLACK_AVAILABLE = True
|
||||
return True
|
||||
|
||||
def _import():
|
||||
from slack_bolt.async_app import AsyncApp
|
||||
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
import aiohttp
|
||||
return {
|
||||
"AsyncApp": AsyncApp,
|
||||
"AsyncSocketModeHandler": AsyncSocketModeHandler,
|
||||
"AsyncWebClient": AsyncWebClient,
|
||||
"aiohttp": aiohttp,
|
||||
"SLACK_AVAILABLE": True,
|
||||
}
|
||||
|
||||
from tools.lazy_deps import ensure_and_bind
|
||||
return ensure_and_bind("platform.slack", _import, globals(), prompt=False)
|
||||
|
||||
|
||||
def _extract_text_from_slack_blocks(blocks: list) -> str:
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ hindsight = ["hindsight-client==0.6.1"]
|
|||
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-xdist==3.8.0", "pytest-split==0.11.0", "mcp==1.26.0", "ty==0.0.21", "ruff==0.15.10"]
|
||||
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"]
|
||||
cron = [] # croniter is now a core dependency; this extra kept for back-compat
|
||||
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1"]
|
||||
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"]
|
||||
matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
|
||||
cli = ["simple-term-menu==1.6.6"]
|
||||
tts-premium = ["elevenlabs==1.59.0"]
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import subprocess
|
|||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -440,3 +440,56 @@ def feature_install_command(feature: str) -> Optional[str]:
|
|||
return None
|
||||
specs = LAZY_DEPS[feature]
|
||||
return "uv pip install " + " ".join(repr(s) for s in specs)
|
||||
|
||||
|
||||
def ensure_and_bind(
|
||||
feature: str,
|
||||
importer: Callable[[], dict[str, Any]],
|
||||
target_globals: dict,
|
||||
*,
|
||||
prompt: bool = False,
|
||||
) -> bool:
|
||||
"""Ensure a feature is installed, then rebind names into the caller's globals.
|
||||
|
||||
Combines :func:`ensure` with a post-install import step that rebinds
|
||||
module-level names. This eliminates the error-prone pattern of manually
|
||||
listing every global that needs updating after lazy-install.
|
||||
|
||||
``importer`` is a zero-arg callable that returns a dict of
|
||||
``{name: value}`` for all symbols the caller needs rebound. It is called
|
||||
only after :func:`ensure` succeeds (or if the packages are already
|
||||
installed).
|
||||
|
||||
Returns True on success, False if deps couldn't be installed or imported.
|
||||
|
||||
Example usage in a platform adapter::
|
||||
|
||||
def check_slack_requirements() -> bool:
|
||||
if SLACK_AVAILABLE:
|
||||
return True
|
||||
def _import():
|
||||
from slack_bolt.async_app import AsyncApp
|
||||
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
import aiohttp
|
||||
return {
|
||||
"AsyncApp": AsyncApp,
|
||||
"AsyncSocketModeHandler": AsyncSocketModeHandler,
|
||||
"AsyncWebClient": AsyncWebClient,
|
||||
"aiohttp": aiohttp,
|
||||
"SLACK_AVAILABLE": True,
|
||||
}
|
||||
return ensure_and_bind("platform.slack", _import, globals(), prompt=False)
|
||||
"""
|
||||
try:
|
||||
ensure(feature, prompt=prompt)
|
||||
except (FeatureUnavailable, Exception):
|
||||
return False
|
||||
|
||||
try:
|
||||
bindings = importer()
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
target_globals.update(bindings)
|
||||
return True
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -2092,6 +2092,7 @@ rl = [
|
|||
{ name = "wandb" },
|
||||
]
|
||||
slack = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "slack-bolt" },
|
||||
{ name = "slack-sdk" },
|
||||
]
|
||||
|
|
@ -2149,6 +2150,7 @@ requires-dist = [
|
|||
{ name = "agent-client-protocol", marker = "extra == 'acp'", specifier = "==0.9.0" },
|
||||
{ name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.3" },
|
||||
{ name = "aiohttp", marker = "extra == 'messaging'", specifier = "==3.13.3" },
|
||||
{ name = "aiohttp", marker = "extra == 'slack'", specifier = "==3.13.3" },
|
||||
{ name = "aiohttp", marker = "extra == 'sms'", specifier = "==3.13.3" },
|
||||
{ name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" },
|
||||
{ name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue