Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63d03aead | ||
|
|
fffedeccbd | ||
|
|
b7bf9494aa | ||
|
|
dbba4533c3 | ||
|
|
e7c0d8485d | ||
|
|
b5d292e6ca | ||
|
|
e19a3b4378 | ||
|
|
5013fffa59 | ||
|
|
69cedfc114 | ||
|
|
d1b6085ee8 | ||
|
|
78e9cc11a1 | ||
|
|
18137c054b | ||
|
|
40c1f9dcb4 | ||
|
|
83dcdd9277 |
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"convertable": true,
|
||||||
|
"units": {
|
||||||
|
"kilogram": {
|
||||||
|
"factor": 1,
|
||||||
|
"aliases": [
|
||||||
|
"kg",
|
||||||
|
"kgs",
|
||||||
|
"kilogram",
|
||||||
|
"kilograms",
|
||||||
|
"kilogramme",
|
||||||
|
"kilogrammes",
|
||||||
|
"kg.",
|
||||||
|
"kgs."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pound": {
|
||||||
|
"factor": 0.45,
|
||||||
|
"aliases": [
|
||||||
|
"lb",
|
||||||
|
"lb.",
|
||||||
|
"lbs",
|
||||||
|
"lbs.",
|
||||||
|
"pound",
|
||||||
|
"pounds"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tonne": {
|
||||||
|
"factor": 1000,
|
||||||
|
"aliases": [
|
||||||
|
"t",
|
||||||
|
"t.",
|
||||||
|
"tonne",
|
||||||
|
"tonnes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ class FitnessCog(commands.Cog):
|
|||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
default_threads = "1457402451530350727,1328363409648910356"
|
default_threads = "1457402451530350727,1328363409648910356,1533433244186443827"
|
||||||
threads_raw = os.getenv("THREADS_ID", default_threads)
|
threads_raw = os.getenv("THREADS_ID", default_threads)
|
||||||
|
|
||||||
self.threads_id: list[int] = []
|
self.threads_id: list[int] = []
|
||||||
@@ -117,7 +117,7 @@ class FitnessCog(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
unit = parts[2] if len(parts) > 2 else "m"
|
unit = parts[2] if len(parts) > 2 else None
|
||||||
|
|
||||||
timestamp = int(message.created_at.timestamp())
|
timestamp = int(message.created_at.timestamp())
|
||||||
username = message.author.name
|
username = message.author.name
|
||||||
|
|||||||
+97
-4
@@ -2,6 +2,8 @@ import asyncio
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
from contextlib import suppress
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import openai
|
import openai
|
||||||
@@ -23,6 +25,7 @@ CALLABLE_FUNCTIONS = {
|
|||||||
|
|
||||||
DEFAULT_MODEL = "gpt-5-mini-2025-08-07"
|
DEFAULT_MODEL = "gpt-5-mini-2025-08-07"
|
||||||
DEFAULT_MAX_COMPLETION_TOKENS = 2000
|
DEFAULT_MAX_COMPLETION_TOKENS = 2000
|
||||||
|
STATUS_UPDATE_MIN_INTERVAL_SECONDS = 1.5
|
||||||
|
|
||||||
|
|
||||||
class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
||||||
@@ -79,6 +82,46 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
"descend into foolishness. You are, at all times, a gentleman of wit and integrity."
|
"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...")
|
||||||
|
|
||||||
|
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()
|
@commands.Cog.listener()
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
if message.author.bot or not message.guild:
|
if message.author.bot or not message.guild:
|
||||||
@@ -159,8 +202,43 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
]
|
]
|
||||||
formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"})
|
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)
|
||||||
|
|
||||||
|
response_text = None
|
||||||
|
if hasattr(message.channel, "typing"):
|
||||||
|
try:
|
||||||
|
async with message.channel.typing():
|
||||||
|
response_text = await self.generate_response(
|
||||||
|
api_key,
|
||||||
|
formatted_messages,
|
||||||
|
status_update=status_update,
|
||||||
|
)
|
||||||
|
except (discord.HTTPException, AttributeError):
|
||||||
|
# Fall back to normal processing if typing indicator isn't available.
|
||||||
|
response_text = await self.generate_response(
|
||||||
|
api_key,
|
||||||
|
formatted_messages,
|
||||||
|
status_update=status_update,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response_text = await self.generate_response(
|
||||||
|
api_key,
|
||||||
|
formatted_messages,
|
||||||
|
status_update=status_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.send_split_message(message.channel, response_text)
|
||||||
|
finally:
|
||||||
|
if status_message is not None:
|
||||||
|
with suppress(discord.HTTPException):
|
||||||
|
await status_message.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
memory.append({"user": user_name, "content": prompt})
|
memory.append({"user": user_name, "content": prompt})
|
||||||
memory.append({"user": "Reginald", "content": response_text})
|
memory.append({"user": "Reginald", "content": response_text})
|
||||||
|
|
||||||
@@ -184,8 +262,8 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
).mid_term_memory() as mid_memory:
|
).mid_term_memory() as mid_memory:
|
||||||
short_memory[channel_id] = memory
|
short_memory[channel_id] = memory
|
||||||
mid_memory[channel_id] = mid_term_summaries[-self.summary_retention_limit :]
|
mid_memory[channel_id] = mid_term_summaries[-self.summary_retention_limit :]
|
||||||
|
except Exception as error:
|
||||||
await self.send_split_message(message.channel, response_text)
|
print(f"DEBUG: Memory persistence failed after response delivery: {error}")
|
||||||
|
|
||||||
def should_reginald_interject(self, message_content: str) -> bool:
|
def should_reginald_interject(self, message_content: str) -> bool:
|
||||||
direct_invocation = {"reginald,"}
|
direct_invocation = {"reginald,"}
|
||||||
@@ -215,8 +293,18 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
return json.dumps(result, default=str)
|
return json.dumps(result, default=str)
|
||||||
|
|
||||||
@debug
|
@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
|
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:
|
try:
|
||||||
client = openai.AsyncOpenAI(api_key=api_key)
|
client = openai.AsyncOpenAI(api_key=api_key)
|
||||||
completion_args = {
|
completion_args = {
|
||||||
@@ -229,6 +317,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
"tools": TOOLS,
|
"tools": TOOLS,
|
||||||
"tool_choice": "auto",
|
"tool_choice": "auto",
|
||||||
}
|
}
|
||||||
|
await maybe_update_status("Reginald is thinking...", force=True)
|
||||||
response = await client.chat.completions.create(**completion_args)
|
response = await client.chat.completions.create(**completion_args)
|
||||||
|
|
||||||
assistant_message = response.choices[0].message
|
assistant_message = response.choices[0].message
|
||||||
@@ -244,6 +333,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
|
|
||||||
if tool_calls:
|
if tool_calls:
|
||||||
for tool_call in 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)
|
tool_result = await self._execute_tool_call(tool_call)
|
||||||
messages.append(
|
messages.append(
|
||||||
{
|
{
|
||||||
@@ -254,6 +344,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
completion_args["messages"] = messages
|
completion_args["messages"] = messages
|
||||||
|
await maybe_update_status("Reginald is composing a polished response...", force=True)
|
||||||
response = await client.chat.completions.create(**completion_args)
|
response = await client.chat.completions.create(**completion_args)
|
||||||
|
|
||||||
if response.choices and response.choices[0].message and response.choices[0].message.content:
|
if response.choices and response.choices[0].message and response.choices[0].message.content:
|
||||||
@@ -264,10 +355,12 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
|
|||||||
print("DEBUG: OpenAI response was empty or malformed:", response)
|
print("DEBUG: OpenAI response was empty or malformed:", response)
|
||||||
response_text = "No response received from AI."
|
response_text = "No response received from AI."
|
||||||
|
|
||||||
|
await maybe_update_status("Reginald is delivering his reply...", force=True)
|
||||||
return response_text
|
return response_text
|
||||||
|
|
||||||
except OpenAIError as error:
|
except OpenAIError as error:
|
||||||
error_message = f"OpenAI Error: {error}"
|
error_message = f"OpenAI Error: {error}"
|
||||||
|
await maybe_update_status("Reginald has encountered an unfortunate complication.", force=True)
|
||||||
reginald_responses = [
|
reginald_responses = [
|
||||||
f"Regrettably, I must inform you that I have encountered a bureaucratic obstruction:\n\n{error_message}",
|
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}",
|
f"It would seem that a most unfortunate technical hiccup has befallen my faculties:\n\n{error_message}",
|
||||||
|
|||||||
Reference in New Issue
Block a user