mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +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.
|
"""Check if Feishu/Lark dependencies are available.
|
||||||
|
|
||||||
Lazy-installs lark-oapi via ``tools.lazy_deps.ensure("platform.feishu")``
|
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:
|
if FEISHU_AVAILABLE:
|
||||||
return True
|
return True
|
||||||
try:
|
|
||||||
from tools.lazy_deps import ensure as _lazy_ensure
|
def _import():
|
||||||
_lazy_ensure("platform.feishu", prompt=False)
|
import lark_oapi as lark
|
||||||
except Exception:
|
from lark_oapi.api.application.v6 import GetApplicationRequest
|
||||||
return False
|
from lark_oapi.api.im.v1 import (
|
||||||
try:
|
CreateFileRequest, CreateFileRequestBody,
|
||||||
import lark_oapi # noqa: F401
|
CreateImageRequest, CreateImageRequestBody,
|
||||||
except ImportError:
|
CreateMessageRequest, CreateMessageRequestBody,
|
||||||
return False
|
GetChatRequest, GetMessageRequest, GetMessageResourceRequest,
|
||||||
FEISHU_AVAILABLE = True
|
P2ImMessageMessageReadV1,
|
||||||
return True
|
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):
|
class FeishuAdapter(BasePlatformAdapter):
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ def check_matrix_requirements() -> bool:
|
||||||
"""Return True if the Matrix adapter can be used.
|
"""Return True if the Matrix adapter can be used.
|
||||||
|
|
||||||
Lazy-installs mautrix via ``tools.lazy_deps.ensure("platform.matrix")``
|
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", "")
|
token = os.getenv("MATRIX_ACCESS_TOKEN", "")
|
||||||
password = os.getenv("MATRIX_PASSWORD", "")
|
password = os.getenv("MATRIX_PASSWORD", "")
|
||||||
|
|
@ -242,11 +242,27 @@ def check_matrix_requirements() -> bool:
|
||||||
try:
|
try:
|
||||||
import mautrix # noqa: F401
|
import mautrix # noqa: F401
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
def _import():
|
||||||
from tools.lazy_deps import ensure as _lazy_ensure
|
from mautrix.types import (
|
||||||
_lazy_ensure("platform.matrix", prompt=False)
|
ContentURI, EventID, EventType, PaginationDirection,
|
||||||
import mautrix # noqa: F401, F811
|
PresenceState, RoomCreatePreset, RoomID, SyncToken,
|
||||||
except Exception:
|
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(
|
logger.warning(
|
||||||
"Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'"
|
"Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -76,27 +76,26 @@ def check_slack_requirements() -> bool:
|
||||||
"""Check if Slack dependencies are available.
|
"""Check if Slack dependencies are available.
|
||||||
|
|
||||||
Lazy-installs slack-bolt/slack-sdk via ``tools.lazy_deps.ensure("platform.slack")``
|
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:
|
if SLACK_AVAILABLE:
|
||||||
return True
|
return True
|
||||||
try:
|
|
||||||
from tools.lazy_deps import ensure as _lazy_ensure
|
def _import():
|
||||||
_lazy_ensure("platform.slack", prompt=False)
|
from slack_bolt.async_app import AsyncApp
|
||||||
except Exception:
|
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
||||||
return False
|
from slack_sdk.web.async_client import AsyncWebClient
|
||||||
try:
|
import aiohttp
|
||||||
from slack_bolt.async_app import AsyncApp as _App
|
return {
|
||||||
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler as _Handler
|
"AsyncApp": AsyncApp,
|
||||||
from slack_sdk.web.async_client import AsyncWebClient as _Client
|
"AsyncSocketModeHandler": AsyncSocketModeHandler,
|
||||||
except ImportError:
|
"AsyncWebClient": AsyncWebClient,
|
||||||
return False
|
"aiohttp": aiohttp,
|
||||||
AsyncApp = _App
|
"SLACK_AVAILABLE": True,
|
||||||
AsyncSocketModeHandler = _Handler
|
}
|
||||||
AsyncWebClient = _Client
|
|
||||||
SLACK_AVAILABLE = True
|
from tools.lazy_deps import ensure_and_bind
|
||||||
return True
|
return ensure_and_bind("platform.slack", _import, globals(), prompt=False)
|
||||||
|
|
||||||
|
|
||||||
def _extract_text_from_slack_blocks(blocks: list) -> str:
|
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"]
|
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"]
|
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
|
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"]
|
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"]
|
cli = ["simple-term-menu==1.6.6"]
|
||||||
tts-premium = ["elevenlabs==1.59.0"]
|
tts-premium = ["elevenlabs==1.59.0"]
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -440,3 +440,56 @@ def feature_install_command(feature: str) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
specs = LAZY_DEPS[feature]
|
specs = LAZY_DEPS[feature]
|
||||||
return "uv pip install " + " ".join(repr(s) for s in specs)
|
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" },
|
{ name = "wandb" },
|
||||||
]
|
]
|
||||||
slack = [
|
slack = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
{ name = "slack-bolt" },
|
{ name = "slack-bolt" },
|
||||||
{ name = "slack-sdk" },
|
{ name = "slack-sdk" },
|
||||||
]
|
]
|
||||||
|
|
@ -2149,6 +2150,7 @@ requires-dist = [
|
||||||
{ name = "agent-client-protocol", marker = "extra == 'acp'", specifier = "==0.9.0" },
|
{ name = "agent-client-protocol", marker = "extra == 'acp'", specifier = "==0.9.0" },
|
||||||
{ name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.3" },
|
{ name = "aiohttp", marker = "extra == 'homeassistant'", specifier = "==3.13.3" },
|
||||||
{ name = "aiohttp", marker = "extra == 'messaging'", 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", marker = "extra == 'sms'", specifier = "==3.13.3" },
|
||||||
{ name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" },
|
{ name = "aiohttp-socks", marker = "extra == 'matrix'", specifier = "==0.11.0" },
|
||||||
{ name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },
|
{ name = "aiosqlite", marker = "extra == 'matrix'", specifier = "==0.22.1" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue