fix: resolve all call-non-callable ty diagnostics across codebase

Replace hasattr() duck-typing with isinstance() checks for DiscordAdapter
in gateway/run.py, add TypedDict for IMAGEGEN_BACKENDS in tools_config.py,
properly type fal_client getattr'd callables in image_generation_tool.py,
fix dict[str, object] → Callable annotation in approval.py, use
isinstance(BaseModel) in web_tools.py, capture _message_handler to local
in base.py, rename shadowed list_distributions parameter in batch_runner.py,
and remove dead queue_message branch.
This commit is contained in:
alt-glitch 2026-04-21 16:00:37 +05:30
parent ca2b6a529e
commit f8d2365795
7 changed files with 61 additions and 47 deletions

View file

@ -1828,8 +1828,11 @@ class BasePlatformAdapter(ABC):
try: try:
await self._run_processing_hook("on_processing_start", event) await self._run_processing_hook("on_processing_start", event)
# Call the handler (this can take a while with tool calls) handler = self._message_handler
response = await self._message_handler(event) if handler is None:
return
response = await handler(event)
# Send response if any. A None/empty response is normal when # Send response if any. A None/empty response is normal when
# streaming already delivered the text (already_sent=True) or # streaming already delivered the text (already_sent=True) or

View file

@ -4358,9 +4358,10 @@ class GatewayRunner:
# is speaking, without needing a separate tool call. # is speaking, without needing a separate tool call.
# ----------------------------------------------------------------- # -----------------------------------------------------------------
if source.platform == Platform.DISCORD: if source.platform == Platform.DISCORD:
from gateway.platforms.discord import DiscordAdapter
adapter = self.adapters.get(Platform.DISCORD) adapter = self.adapters.get(Platform.DISCORD)
guild_id = self._get_guild_id(event) 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) vc_context = adapter.get_voice_channel_context(guild_id)
if vc_context: if vc_context:
context_prompt += f"\n\n{vc_context}" context_prompt += f"\n\n{vc_context}"
@ -5946,9 +5947,10 @@ class GatewayRunner:
"all": "TTS (voice reply to all messages)", "all": "TTS (voice reply to all messages)",
} }
# Append voice channel info if connected # Append voice channel info if connected
from gateway.platforms.discord import DiscordAdapter
adapter = self.adapters.get(event.source.platform) adapter = self.adapters.get(event.source.platform)
guild_id = self._get_guild_id(event) 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) info = adapter.get_voice_channel_info(guild_id)
if info: if info:
lines = [ lines = [
@ -5979,8 +5981,9 @@ class GatewayRunner:
async def _handle_voice_channel_join(self, event: MessageEvent) -> str: async def _handle_voice_channel_join(self, event: MessageEvent) -> str:
"""Join the user's current Discord voice channel.""" """Join the user's current Discord voice channel."""
from gateway.platforms.discord import DiscordAdapter
adapter = self.adapters.get(event.source.platform) 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." return "Voice channels are not supported on this platform."
guild_id = self._get_guild_id(event) guild_id = self._get_guild_id(event)
@ -5995,10 +5998,8 @@ class GatewayRunner:
# Wire callbacks BEFORE join so voice input arriving immediately # Wire callbacks BEFORE join so voice input arriving immediately
# after connection is not lost. # after connection is not lost.
if hasattr(adapter, "_voice_input_callback"): adapter._voice_input_callback = self._handle_voice_channel_input
adapter._voice_input_callback = self._handle_voice_channel_input adapter._on_voice_disconnect = self._handle_voice_timeout_cleanup
if hasattr(adapter, "_on_voice_disconnect"):
adapter._on_voice_disconnect = self._handle_voice_timeout_cleanup
try: try:
success = await adapter.join_voice_channel(voice_channel) success = await adapter.join_voice_channel(voice_channel)
@ -6015,8 +6016,7 @@ class GatewayRunner:
if success: if success:
adapter._voice_text_channels[guild_id] = int(event.source.chat_id) 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._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "all"
self._save_voice_modes() self._save_voice_modes()
self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=False) self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=False)
@ -6030,13 +6030,14 @@ class GatewayRunner:
async def _handle_voice_channel_leave(self, event: MessageEvent) -> str: async def _handle_voice_channel_leave(self, event: MessageEvent) -> str:
"""Leave the Discord voice channel.""" """Leave the Discord voice channel."""
from gateway.platforms.discord import DiscordAdapter
adapter = self.adapters.get(event.source.platform) adapter = self.adapters.get(event.source.platform)
guild_id = self._get_guild_id(event) 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." 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." return "Not in a voice channel."
try: try:
@ -6047,8 +6048,7 @@ class GatewayRunner:
self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "off" self._voice_mode[self._voice_key(event.source.platform, event.source.chat_id)] = "off"
self._save_voice_modes() self._save_voice_modes()
self._set_adapter_auto_tts_disabled(adapter, event.source.chat_id, disabled=True) 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." return "Left voice channel."
def _handle_voice_timeout_cleanup(self, chat_id: str) -> None: def _handle_voice_timeout_cleanup(self, chat_id: str) -> None:
@ -6208,13 +6208,13 @@ class GatewayRunner:
adapter = self.adapters.get(event.source.platform) adapter = self.adapters.get(event.source.platform)
# If connected to a voice channel, play there instead of sending a file # 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) guild_id = self._get_guild_id(event)
if (guild_id if (guild_id
and hasattr(adapter, "play_in_voice_channel") and isinstance(adapter, DiscordAdapter)
and hasattr(adapter, "is_in_voice_channel")
and adapter.is_in_voice_channel(guild_id)): and adapter.is_in_voice_channel(guild_id)):
await adapter.play_in_voice_channel(guild_id, actual_path) await adapter.play_in_voice_channel(guild_id, actual_path)
elif adapter and hasattr(adapter, "send_voice"): elif adapter:
send_kwargs: Dict[str, Any] = { send_kwargs: Dict[str, Any] = {
"chat_id": event.source.chat_id, "chat_id": event.source.chat_id,
"audio_path": actual_path, "audio_path": actual_path,
@ -10468,8 +10468,6 @@ class GatewayRunner:
adapter = self.adapters.get(source.platform) adapter = self.adapters.get(source.platform)
if adapter and pending_event: if adapter and pending_event:
merge_pending_message_event(adapter._pending_messages, session_key, 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} return result_holder[0] or {"final_response": response, "messages": history}
was_interrupted = result.get("interrupted") was_interrupted = result.get("interrupted")

View file

@ -13,7 +13,7 @@ import json as _json
import logging import logging
import sys import sys
from pathlib import Path 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 ( from hermes_cli.config import (
@ -970,13 +970,19 @@ def _detect_active_provider_index(providers: list, config: dict) -> int:
# right catalog at picker time. # 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.""" """Lazy-load the FAL model catalog from the tool module."""
from tools.image_generation_tool import FAL_MODELS, DEFAULT_MODEL from tools.image_generation_tool import FAL_MODELS, DEFAULT_MODEL
return FAL_MODELS, DEFAULT_MODEL return FAL_MODELS, DEFAULT_MODEL
IMAGEGEN_BACKENDS = { IMAGEGEN_BACKENDS: Dict[str, _ImagegenBackend] = {
"fal": { "fal": {
"display": "FAL.ai", "display": "FAL.ai",
"config_key": "image_gen", "config_key": "image_gen",

View file

@ -1130,7 +1130,7 @@ def main(
num_workers: int = 4, num_workers: int = 4,
resume: bool = False, resume: bool = False,
verbose: bool = False, verbose: bool = False,
list_distributions: bool = False, show_distributions: bool = False,
ephemeral_system_prompt: str = None, ephemeral_system_prompt: str = None,
log_prefix_chars: int = 100, log_prefix_chars: int = 100,
providers_allowed: str = None, providers_allowed: str = None,
@ -1158,7 +1158,7 @@ def main(
num_workers (int): Number of parallel worker processes (default: 4) num_workers (int): Number of parallel worker processes (default: 4)
resume (bool): Resume from checkpoint if run was interrupted (default: False) resume (bool): Resume from checkpoint if run was interrupted (default: False)
verbose (bool): Enable verbose logging (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) 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) 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") 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 --prefill_messages_file=configs/prefill_opus.json
# List available distributions # List available distributions
python batch_runner.py --list_distributions python batch_runner.py --show_distributions
""" """
# Handle list distributions # Handle list distributions
if list_distributions: if show_distributions:
from toolset_distributions import print_distribution_info from toolset_distributions import print_distribution_info
print("📊 Available Toolset Distributions") print("📊 Available Toolset Distributions")

View file

@ -16,7 +16,7 @@ import sys
import threading import threading
import time import time
import unicodedata import unicodedata
from typing import Optional from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -228,10 +228,10 @@ class _ApprovalEntry:
_gateway_queues: dict[str, list] = {} # session_key → [_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. """Register a per-session callback for sending approval requests to the user.
The callback signature is ``cb(approval_data: dict) -> None`` where The callback signature is ``cb(approval_data: dict) -> None`` where

View file

@ -26,10 +26,11 @@ import os
import datetime import datetime
import threading import threading
import uuid import uuid
from typing import Any, Dict, Optional, Union from typing import Any, Callable, Dict, Optional, Type, Union
from urllib.parse import urlencode from urllib.parse import urlencode
import fal_client import fal_client
import httpx
from tools.debug_helpers import DebugSession from tools.debug_helpers import DebugSession
from tools.managed_tool_gateway import resolve_managed_tool_gateway from tools.managed_tool_gateway import resolve_managed_tool_gateway
@ -312,21 +313,27 @@ class _ManagedFalSyncClient:
self._queue_url_format = _normalize_fal_queue_url_format(queue_run_origin) self._queue_url_format = _normalize_fal_queue_url_format(queue_run_origin)
self._sync_client = sync_client_class(key=key) 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") 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") 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") 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( def submit(
self, self,
application: str, application: str,

View file

@ -1720,8 +1720,8 @@ async def web_crawl_tool(
metadata = {} metadata = {}
# Extract data from the item # Extract data from the item
if hasattr(item, 'model_dump'): from pydantic import BaseModel
# Pydantic model - use model_dump to get dict if isinstance(item, BaseModel):
item_dict = item.model_dump() item_dict = item.model_dump()
content_markdown = item_dict.get('markdown') content_markdown = item_dict.get('markdown')
content_html = item_dict.get('html') content_html = item_dict.get('html')
@ -1730,15 +1730,15 @@ async def web_crawl_tool(
# Regular object with attributes # Regular object with attributes
content_markdown = getattr(item, 'markdown', None) content_markdown = getattr(item, 'markdown', None)
content_html = getattr(item, 'html', None) content_html = getattr(item, 'html', None)
# Handle metadata - convert to dict if it's an object # Handle metadata - convert to dict if it's an object
metadata_obj = getattr(item, 'metadata', {}) metadata_obj = getattr(item, 'metadata', {})
if hasattr(metadata_obj, 'model_dump'): if isinstance(metadata_obj, BaseModel):
metadata = metadata_obj.model_dump() metadata = metadata_obj.model_dump()
elif hasattr(metadata_obj, '__dict__'):
metadata = metadata_obj.__dict__
elif isinstance(metadata_obj, dict): elif isinstance(metadata_obj, dict):
metadata = metadata_obj metadata = metadata_obj
elif hasattr(metadata_obj, '__dict__'):
metadata = metadata_obj.__dict__
else: else:
metadata = {} metadata = {}
elif isinstance(item, dict): elif isinstance(item, dict):