feat(discord): persistent typing indicator for DMs
Based on PR #2427 by @oxngon (core feature extracted, reformatting and unrelated changes dropped). Discord's TYPING_START gateway event is unreliable for bot DMs. This adds a background typing loop that hits POST /channels/{id}/typing every 8 seconds (indicator lasts ~10s) until the response is sent. - send_typing() starts a per-channel background loop (idempotent) - stop_typing() cancels it (called after _run_agent returns) - Base adapter gets stop_typing() as a no-op default - Per-channel tracking via _typing_tasks dict prevents duplicates
This commit is contained in:
parent
ffa8b562e9
commit
ab3cbfc99d
3 changed files with 68 additions and 8 deletions
|
|
@ -505,6 +505,14 @@ class BasePlatformAdapter(ABC):
|
|||
"""
|
||||
pass
|
||||
|
||||
async def stop_typing(self, chat_id: str) -> None:
|
||||
"""Stop a persistent typing indicator (if the platform uses one).
|
||||
|
||||
Override in subclasses that start background typing loops.
|
||||
Default is a no-op for platforms with one-shot typing indicators.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def send_image(
|
||||
self,
|
||||
chat_id: str,
|
||||
|
|
|
|||
|
|
@ -439,6 +439,9 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
# in those threads don't require @mention. Persisted to disk so the
|
||||
# set survives gateway restarts.
|
||||
self._bot_participated_threads: set = self._load_participated_threads()
|
||||
# Persistent typing indicator loops per channel (DMs don't reliably
|
||||
# show the standard typing gateway event for bots)
|
||||
self._typing_tasks: Dict[str, asyncio.Task] = {}
|
||||
# Cap to prevent unbounded growth (Discord threads get archived).
|
||||
self._MAX_TRACKED_THREADS = 500
|
||||
|
||||
|
|
@ -1239,14 +1242,48 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
|
||||
|
||||
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
||||
"""Send typing indicator."""
|
||||
if self._client:
|
||||
"""Start a persistent typing indicator for a channel.
|
||||
|
||||
Discord's TYPING_START gateway event is unreliable in DMs for bots.
|
||||
Instead, start a background loop that hits the typing endpoint every
|
||||
8 seconds (typing indicator lasts ~10s). The loop is cancelled when
|
||||
stop_typing() is called (after the response is sent).
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
# Don't start a duplicate loop
|
||||
if chat_id in self._typing_tasks:
|
||||
return
|
||||
|
||||
async def _typing_loop() -> None:
|
||||
try:
|
||||
channel = self._client.get_channel(int(chat_id))
|
||||
if channel:
|
||||
await channel.typing()
|
||||
except Exception:
|
||||
pass # Ignore typing indicator failures
|
||||
while True:
|
||||
try:
|
||||
route = discord.http.Route(
|
||||
"POST", "/channels/{channel_id}/typing",
|
||||
channel_id=chat_id,
|
||||
)
|
||||
await self._client.http.request(route)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as e:
|
||||
logger.debug("Discord typing indicator failed for %s: %s", chat_id, e)
|
||||
return
|
||||
await asyncio.sleep(8)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self._typing_tasks[chat_id] = asyncio.create_task(_typing_loop())
|
||||
|
||||
async def stop_typing(self, chat_id: str) -> None:
|
||||
"""Stop the persistent typing indicator for a channel."""
|
||||
task = self._typing_tasks.pop(chat_id, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
|
||||
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
||||
"""Get information about a Discord channel."""
|
||||
|
|
|
|||
|
|
@ -2126,6 +2126,14 @@ class GatewayRunner:
|
|||
session_key=session_key
|
||||
)
|
||||
|
||||
# Stop persistent typing indicator now that the agent is done
|
||||
try:
|
||||
_typing_adapter = self.adapters.get(source.platform)
|
||||
if _typing_adapter and hasattr(_typing_adapter, "stop_typing"):
|
||||
await _typing_adapter.stop_typing(source.chat_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
response = agent_result.get("final_response") or ""
|
||||
agent_messages = agent_result.get("messages", [])
|
||||
|
||||
|
|
@ -2325,6 +2333,13 @@ class GatewayRunner:
|
|||
return response
|
||||
|
||||
except Exception as e:
|
||||
# Stop typing indicator on error too
|
||||
try:
|
||||
_err_adapter = self.adapters.get(source.platform)
|
||||
if _err_adapter and hasattr(_err_adapter, "stop_typing"):
|
||||
await _err_adapter.stop_typing(source.chat_id)
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("Agent error in session %s", session_key)
|
||||
error_type = type(e).__name__
|
||||
error_detail = str(e)[:300] if str(e) else "no details available"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue