Merge pull request 'Attempt to adding live feedback' (#21) from 9month-revision-update2 into master
Reviewed-on: #21
This commit is contained in:
commit
40c1f9dcb4
@ -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}",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user