From 83dcdd927733188dc09e4a0fcf0c40cacc4eee53 Mon Sep 17 00:00:00 2001 From: AllfatherHatt Date: Mon, 16 Mar 2026 12:50:53 +0100 Subject: [PATCH] Attempt to adding live feedback --- reginaldCog/reginald.py | 88 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/reginaldCog/reginald.py b/reginaldCog/reginald.py index 34bbd05..71e891f 100644 --- a/reginaldCog/reginald.py +++ b/reginaldCog/reginald.py @@ -2,6 +2,8 @@ import asyncio import datetime import json import random +from contextlib import suppress +from typing import Awaitable, Callable import discord import openai @@ -23,6 +25,8 @@ CALLABLE_FUNCTIONS = { DEFAULT_MODEL = "gpt-5-mini-2025-08-07" DEFAULT_MAX_COMPLETION_TOKENS = 2000 +TYPING_HEARTBEAT_INTERVAL_SECONDS = 8 +STATUS_UPDATE_MIN_INTERVAL_SECONDS = 1.5 class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): @@ -79,6 +83,52 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): "descend into foolishness. You are, at all times, a gentleman of wit and integrity." ) + def get_thinking_status_message(self) -> str: + return random.choice( + [ + "_Reginald is considering your request..._", + "_Reginald is consulting the estate archives..._", + "_Reginald is preparing a proper response..._", + ] + ) + + def get_tool_status_message(self, tool_name: str) -> str: + tool_statuses = { + "time_now": "Reginald is consulting the house clocks...", + "get_current_weather": "Reginald is consulting the weather office...", + "get_weather_forecast": "Reginald is reviewing the forecast ledgers...", + } + return tool_statuses.get(tool_name, "Reginald is consulting an external source...") + + async def typing_heartbeat(self, channel: discord.TextChannel): + while True: + with suppress(discord.HTTPException): + await channel.trigger_typing() + await asyncio.sleep(TYPING_HEARTBEAT_INTERVAL_SECONDS) + + def make_status_updater( + self, status_message: discord.Message + ) -> Callable[[str, bool], Awaitable[None]]: + last_content = "" + last_update_at = 0.0 + + async def update_status(content: str, force: bool = False): + nonlocal last_content, last_update_at + + if not content: + return + + now = asyncio.get_running_loop().time() + if not force and (content == last_content or now - last_update_at < STATUS_UPDATE_MIN_INTERVAL_SECONDS): + return + + with suppress(discord.HTTPException): + await status_message.edit(content=f"_{content}_") + last_content = content + last_update_at = now + + return update_status + @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot or not message.guild: @@ -159,7 +209,23 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): ] formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"}) - response_text = await self.generate_response(api_key, formatted_messages) + status_message = None + status_update = None + with suppress(discord.HTTPException): + status_message = await message.channel.send(self.get_thinking_status_message()) + status_update = self.make_status_updater(status_message) + + typing_task = asyncio.create_task(self.typing_heartbeat(message.channel)) + try: + response_text = await self.generate_response( + api_key, + formatted_messages, + status_update=status_update, + ) + finally: + typing_task.cancel() + with suppress(asyncio.CancelledError): + await typing_task memory.append({"user": user_name, "content": prompt}) memory.append({"user": "Reginald", "content": response_text}) @@ -186,6 +252,9 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): mid_memory[channel_id] = mid_term_summaries[-self.summary_retention_limit :] await self.send_split_message(message.channel, response_text) + if status_message is not None: + with suppress(discord.HTTPException): + await status_message.delete() def should_reginald_interject(self, message_content: str) -> bool: direct_invocation = {"reginald,"} @@ -215,8 +284,18 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): return json.dumps(result, default=str) @debug - async def generate_response(self, api_key: str, messages: list[dict]) -> str: + async def generate_response( + self, + api_key: str, + messages: list[dict], + status_update: Callable[[str, bool], Awaitable[None]] | None = None, + ) -> str: model = await self.config.openai_model() or DEFAULT_MODEL + + async def maybe_update_status(content: str, force: bool = False): + if status_update is not None: + await status_update(content, force) + try: client = openai.AsyncOpenAI(api_key=api_key) completion_args = { @@ -229,6 +308,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): "tools": TOOLS, "tool_choice": "auto", } + await maybe_update_status("Reginald is thinking...", force=True) response = await client.chat.completions.create(**completion_args) assistant_message = response.choices[0].message @@ -244,6 +324,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): if tool_calls: for tool_call in tool_calls: + await maybe_update_status(self.get_tool_status_message(tool_call.function.name), force=True) tool_result = await self._execute_tool_call(tool_call) messages.append( { @@ -254,6 +335,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): ) completion_args["messages"] = messages + await maybe_update_status("Reginald is composing a polished response...", force=True) response = await client.chat.completions.create(**completion_args) if response.choices and response.choices[0].message and response.choices[0].message.content: @@ -264,10 +346,12 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog): print("DEBUG: OpenAI response was empty or malformed:", response) response_text = "No response received from AI." + await maybe_update_status("Reginald is delivering his reply...", force=True) return response_text except OpenAIError as error: error_message = f"OpenAI Error: {error}" + await maybe_update_status("Reginald has encountered an unfortunate complication.", force=True) reginald_responses = [ f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n{error_message}", f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n{error_message}", -- 2.47.2