diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 1273bd64f7..4f9193188a 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -1833,8 +1833,11 @@ class BasePlatformAdapter(ABC): try: await self._run_processing_hook("on_processing_start", event) - # Call the handler (this can take a while with tool calls) - response = await self._message_handler(event) + handler = self._message_handler + if handler is None: + return + + response = await handler(event) # Send response if any. A None/empty response is normal when # streaming already delivered the text (already_sent=True) or diff --git a/gateway/run.py b/gateway/run.py index cf0b1c309f..4956930a76 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4431,9 +4431,10 @@ class GatewayRunner: # is speaking, without needing a separate tool call. # ----------------------------------------------------------------- if source.platform == Platform.DISCORD: + from gateway.platforms.discord import DiscordAdapter adapter = self.adapters.get(Platform.DISCORD) guild_id = self._get_guild_id(event) - if guild_id and adapter and hasattr(adapter, "get_voice_channel_context"): + if guild_id and isinstance(adapter, DiscordAdapter): vc_context = adapter.get_voice_channel_context(guild_id) if vc_context: context_prompt += f"\n\n{vc_context}" @@ -6026,9 +6027,10 @@ class GatewayRunner: "all": "TTS (voice reply to all messages)", } # Append voice channel info if connected + from gateway.platforms.discord import DiscordAdapter adapter = self.adapters.get(event.source.platform) guild_id = self._get_guild_id(event) - if guild_id and hasattr(adapter, "get_voice_channel_info"): + if guild_id and isinstance(adapter, DiscordAdapter): info = adapter.get_voice_channel_info(guild_id) if info: lines = [ @@ -6059,8 +6061,9 @@ class GatewayRunner: async def _handle_voice_channel_join(self, event: MessageEvent) -> str: """Join the user's current Discord voice channel.""" + from gateway.platforms.discord import DiscordAdapter adapter = self.adapters.get(event.source.platform) - if not hasattr(adapter, "join_voice_channel"): + if not isinstance(adapter, DiscordAdapter): return "Voice channels are not supported on this platform." guild_id = self._get_guild_id(event) @@ -6075,10 +6078,8 @@ class GatewayRunner: # Wire callbacks BEFORE join so voice input arriving immediately # after connection is not lost. - if hasattr(adapter, "_voice_input_callback"): - adapter._voice_input_callback = self._handle_voice_channel_input - if hasattr(adapter, "_on_voice_disconnect"): - adapter._on_voice_disconnect = self._handle_voice_timeout_cleanup + adapter._voice_input_callback = self._handle_voice_channel_input + adapter._on_voice_disconnect = self._handle_voice_timeout_cleanup try: success = await adapter.join_voice_channel(voice_channel) @@ -6095,8 +6096,7 @@ class GatewayRunner: if success: adapter._voice_text_channels[guild_id] = int(event.source.chat_id) - if hasattr(adapter, "_voice_sources"): - adapter._voice_sources[guild_id] = event.source.to_dict() + adapter._voice_sources[guild_id] = event.source.to_dict() self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "all" self._save_voice_modes() self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=False) @@ -6110,13 +6110,14 @@ class GatewayRunner: async def _handle_voice_channel_leave(self, event: MessageEvent) -> str: """Leave the Discord voice channel.""" + from gateway.platforms.discord import DiscordAdapter adapter = self.adapters.get(event.source.platform) guild_id = self._get_guild_id(event) - if not guild_id or not hasattr(adapter, "leave_voice_channel"): + if not guild_id or not isinstance(adapter, DiscordAdapter): return "Not in a voice channel." - if not hasattr(adapter, "is_in_voice_channel") or not adapter.is_in_voice_channel(guild_id): + if not adapter.is_in_voice_channel(guild_id): return "Not in a voice channel." try: @@ -6127,8 +6128,7 @@ class GatewayRunner: self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "off" self._save_voice_modes() self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=True) - if hasattr(adapter, "_voice_input_callback"): - adapter._voice_input_callback = None + adapter._voice_input_callback = None return "Left voice channel." def _handle_voice_timeout_cleanup(self, chat_id: str) -> None: @@ -6288,13 +6288,13 @@ class GatewayRunner: adapter = self.adapters.get(event.source.platform) # If connected to a voice channel, play there instead of sending a file + from gateway.platforms.discord import DiscordAdapter guild_id = self._get_guild_id(event) if (guild_id - and hasattr(adapter, "play_in_voice_channel") - and hasattr(adapter, "is_in_voice_channel") + and isinstance(adapter, DiscordAdapter) and adapter.is_in_voice_channel(guild_id)): await adapter.play_in_voice_channel(guild_id, actual_path) - elif adapter and hasattr(adapter, "send_voice"): + elif adapter: send_kwargs: Dict[str, Any] = { "chat_id": event.source.chat_id, "audio_path": actual_path, @@ -10627,8 +10627,6 @@ class GatewayRunner: adapter = self.adapters.get(source.platform) if adapter and pending_event: merge_pending_message_event(adapter._pending_messages, session_key, pending_event) - elif adapter and hasattr(adapter, 'queue_message'): - adapter.queue_message(session_key, pending) return result_holder[0] or {"final_response": response, "messages": history} was_interrupted = result.get("interrupted") diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index e89f961781..9d66ed98bf 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -13,7 +13,7 @@ import json as _json import logging import sys from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypedDict from hermes_cli.config import ( @@ -1098,13 +1098,19 @@ def _detect_active_provider_index(providers: list, config: dict) -> int: # right catalog at picker time. -def _fal_model_catalog(): +class _ImagegenBackend(TypedDict): + display: str + config_key: str + catalog_fn: Callable[[], Tuple[Dict[str, Dict[str, Any]], str]] + + +def _fal_model_catalog() -> Tuple[Dict[str, Dict[str, Any]], str]: """Lazy-load the FAL model catalog from the tool module.""" from tools.image_generation_tool import FAL_MODELS, DEFAULT_MODEL return FAL_MODELS, DEFAULT_MODEL -IMAGEGEN_BACKENDS = { +IMAGEGEN_BACKENDS: Dict[str, _ImagegenBackend] = { "fal": { "display": "FAL.ai", "config_key": "image_gen", diff --git a/scripts/batch_runner.py b/scripts/batch_runner.py index 1cc3a12d2a..05c7934e51 100644 --- a/scripts/batch_runner.py +++ b/scripts/batch_runner.py @@ -1130,7 +1130,7 @@ def main( num_workers: int = 4, resume: bool = False, verbose: bool = False, - list_distributions: bool = False, + show_distributions: bool = False, ephemeral_system_prompt: str = None, log_prefix_chars: int = 100, providers_allowed: str = None, @@ -1158,7 +1158,7 @@ def main( num_workers (int): Number of parallel worker processes (default: 4) resume (bool): Resume from checkpoint if run was interrupted (default: False) verbose (bool): Enable verbose logging (default: False) - list_distributions (bool): List available toolset distributions and exit + show_distributions (bool): List available toolset distributions and exit ephemeral_system_prompt (str): System prompt used during agent execution but NOT saved to trajectories (optional) log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses (default: 20) providers_allowed (str): Comma-separated list of OpenRouter providers to allow (e.g. "anthropic,openai") @@ -1190,10 +1190,10 @@ def main( --prefill_messages_file=configs/prefill_opus.json # List available distributions - python batch_runner.py --list_distributions + python batch_runner.py --show_distributions """ # Handle list distributions - if list_distributions: + if show_distributions: from toolset_distributions import print_distribution_info print("📊 Available Toolset Distributions") diff --git a/tools/approval.py b/tools/approval.py index fc344bd77b..913371b01b 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -16,7 +16,7 @@ import sys import threading import time import unicodedata -from typing import Optional +from typing import Any, Callable, Dict, Optional logger = logging.getLogger(__name__) @@ -228,10 +228,10 @@ class _ApprovalEntry: _gateway_queues: dict[str, list] = {} # session_key → [_ApprovalEntry, …] -_gateway_notify_cbs: dict[str, object] = {} # session_key → callable(approval_data) +_gateway_notify_cbs: Dict[str, Callable[[Dict[str, Any]], None]] = {} -def register_gateway_notify(session_key: str, cb) -> None: +def register_gateway_notify(session_key: str, cb: Callable[[Dict[str, Any]], None]) -> None: """Register a per-session callback for sending approval requests to the user. The callback signature is ``cb(approval_data: dict) -> None`` where diff --git a/tools/image_generation_tool.py b/tools/image_generation_tool.py index ac37449783..c65340eb2f 100644 --- a/tools/image_generation_tool.py +++ b/tools/image_generation_tool.py @@ -26,10 +26,11 @@ import os import datetime import threading import uuid -from typing import Any, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Type, Union from urllib.parse import urlencode import fal_client +import httpx from tools.debug_helpers import DebugSession from tools.managed_tool_gateway import resolve_managed_tool_gateway @@ -348,21 +349,27 @@ class _ManagedFalSyncClient: self._queue_url_format = _normalize_fal_queue_url_format(queue_run_origin) self._sync_client = sync_client_class(key=key) - self._http_client = getattr(self._sync_client, "_client", None) - self._maybe_retry_request = getattr(client_module, "_maybe_retry_request", None) - self._raise_for_status = getattr(client_module, "_raise_for_status", None) - self._request_handle_class = getattr(client_module, "SyncRequestHandle", None) - self._add_hint_header = getattr(client_module, "add_hint_header", None) - self._add_priority_header = getattr(client_module, "add_priority_header", None) - self._add_timeout_header = getattr(client_module, "add_timeout_header", None) - if self._http_client is None: + http_client: Optional[httpx.Client] = getattr(self._sync_client, "_client", None) + maybe_retry: Optional[Callable[..., httpx.Response]] = getattr(client_module, "_maybe_retry_request", None) + raise_for_status: Optional[Callable[[httpx.Response], None]] = getattr(client_module, "_raise_for_status", None) + request_handle_class: Optional[Type[Any]] = getattr(client_module, "SyncRequestHandle", None) + + if http_client is None: raise RuntimeError("fal_client.SyncClient._client is required for managed FAL gateway mode") - if self._maybe_retry_request is None or self._raise_for_status is None: + if maybe_retry is None or raise_for_status is None: raise RuntimeError("fal_client.client request helpers are required for managed FAL gateway mode") - if self._request_handle_class is None: + if request_handle_class is None: raise RuntimeError("fal_client.client.SyncRequestHandle is required for managed FAL gateway mode") + self._http_client: httpx.Client = http_client + self._maybe_retry_request: Callable[..., httpx.Response] = maybe_retry + self._raise_for_status: Callable[[httpx.Response], None] = raise_for_status + self._request_handle_class: Type[Any] = request_handle_class + self._add_hint_header: Optional[Callable[..., Any]] = getattr(client_module, "add_hint_header", None) + self._add_priority_header: Optional[Callable[..., Any]] = getattr(client_module, "add_priority_header", None) + self._add_timeout_header: Optional[Callable[..., Any]] = getattr(client_module, "add_timeout_header", None) + def submit( self, application: str, diff --git a/tools/web_tools.py b/tools/web_tools.py index 9e5d878da0..8219c59027 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -1720,8 +1720,8 @@ async def web_crawl_tool( metadata = {} # Extract data from the item - if hasattr(item, 'model_dump'): - # Pydantic model - use model_dump to get dict + from pydantic import BaseModel + if isinstance(item, BaseModel): item_dict = item.model_dump() content_markdown = item_dict.get('markdown') content_html = item_dict.get('html') @@ -1730,15 +1730,15 @@ async def web_crawl_tool( # Regular object with attributes content_markdown = getattr(item, 'markdown', None) content_html = getattr(item, 'html', None) - + # Handle metadata - convert to dict if it's an object metadata_obj = getattr(item, 'metadata', {}) - if hasattr(metadata_obj, 'model_dump'): + if isinstance(metadata_obj, BaseModel): metadata = metadata_obj.model_dump() - elif hasattr(metadata_obj, '__dict__'): - metadata = metadata_obj.__dict__ elif isinstance(metadata_obj, dict): metadata = metadata_obj + elif hasattr(metadata_obj, '__dict__'): + metadata = metadata_obj.__dict__ else: metadata = {} elif isinstance(item, dict):