feat: Implement user timeout functionality and enhance conversation context with user IDs

This commit is contained in:
Slipstream 2025-05-17 12:17:40 -06:00
parent 9c2bb929f7
commit 7eb0180dce
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD

View File

@ -24,6 +24,45 @@ class TetoCog(commands.Cog):
self._api_endpoint = "https://openrouter.ai/api/v1/chat/completions" # Default endpoint
self._ai_model = "google/gemini-2.5-flash-preview" # Default model
self.tools = [
{
"type": "function",
"function": {
"name": "timeout_user",
"description": "Times out a user in the Discord server.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "The ID of the user to time out."
},
"duration_seconds": {
"type": "integer",
"description": "The duration of the timeout in seconds."
}
},
"required": ["user_id", "duration_seconds"]
}
}
}
]
async def timeout_user(self, user_id: str, duration_seconds: int):
"""Times out a user."""
try:
guild = self.bot.get_guild(1234567890) # Replace with actual guild ID or find dynamically
if not guild:
return f"Error: Could not find the guild."
member = guild.get_member(int(user_id))
if not member:
return f"Error: Could not find user with ID {user_id} in the guild."
await member.timeout(discord.utils.utcnow() + discord.timedelta(seconds=duration_seconds))
return f"Successfully timed out user {member.display_name} for {duration_seconds} seconds."
except Exception as e:
return f"Error timing out user: {e}"
async def _teto_reply_ai_with_messages(self, messages, system_mode="reply"):
"""
Use OpenRouter AI to generate a Kasane Teto-style response.
@ -51,6 +90,56 @@ class TetoCog(commands.Cog):
"When expressing emotions, prefer using text-based emoticons like :) and ;) as well as Japanese-style emoticons like >~<, ^~^, and OwO over emojis. \n"
"Reply to the user in a short, conversational manner, staying in character."
)
# Collect unique user IDs from the conversation history
user_ids_in_convo = set()
for message_entry in messages:
if message_entry["role"] == "user":
# Assuming user messages in history have a way to identify the author,
# this might need adjustment based on how history is stored.
# For now, let's assume the message object itself is available or can be retrieved.
# This part needs refinement based on actual message history structure.
# As a placeholder, let's assume we can get user ID from the message object.
# In the on_message listener, we have the message object, but here we only have the history.
# A better approach is to store user ID with the message in history.
# For now, let's try to extract user IDs from the text content if available
if isinstance(message_entry["content"], list):
for item in message_entry["content"]:
if item["type"] == "text":
# Simple regex to find potential user IDs in the text
id_matches = re.findall(r"\(ID: (\d+)\)", item["text"])
for user_id in id_matches:
user_ids_in_convo.add(user_id)
elif isinstance(message_entry["content"], str):
id_matches = re.findall(r"\(ID: (\d+)\)", message_entry["content"])
for user_id in id_matches:
user_ids_in_convo.add(user_id)
# Add list of users from conversation to the system prompt
if user_ids_in_convo:
user_list_text = "Users in this conversation:\n"
for user_id in user_ids_in_convo:
try:
# Attempt to fetch member to get display name
guild = self.bot.get_guild(1234567890) # Replace with actual guild ID or find dynamically
if guild:
member = guild.get_member(int(user_id))
if member:
user_list_text += f"- {member.display_name} (ID: {user_id})\n"
else:
user_list_text += f"- Unknown User (ID: {user_id})\n"
else:
user_list_text += f"- Unknown User (ID: {user_id}) (Could not find guild)\n"
except ValueError:
user_list_text += f"- Invalid User ID format: {user_id}\n"
system_prompt += f"\n\n{user_list_text}"
else:
system_prompt += "\n\nNo specific users identified in the conversation history."
payload = {
"model": self._ai_model,
"messages": [{"role": "system", "content": system_prompt}] + messages,
@ -66,7 +155,75 @@ class TetoCog(commands.Cog):
data = await resp.json()
if "choices" not in data or not data["choices"]:
raise RuntimeError(f"OpenRouter API returned unexpected response format: {data}")
return data["choices"][0]["message"]["content"]
ai_response_message = data["choices"][0]["message"]
# Handle tool calls
if "tool_calls" in ai_response_message and ai_response_message["tool_calls"]:
tool_calls = ai_response_message["tool_calls"]
tool_responses = []
for tool_call in tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = tool_call["function"]["arguments"] # This is a string
try:
tool_args_dict = json.loads(tool_args)
if tool_name == "timeout_user":
user_id = tool_args_dict.get("user_id")
duration_seconds = tool_args_dict.get("duration_seconds")
if user_id and duration_seconds is not None:
tool_result = await self.timeout_user(user_id, duration_seconds)
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": tool_name,
"content": tool_result,
})
else:
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": tool_name,
"content": f"Error: Missing user_id or duration_seconds for timeout_user tool.",
})
else:
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": tool_name,
"content": f"Error: Unknown tool {tool_name}.",
})
except json.JSONDecodeError:
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": tool_name,
"content": f"Error: Invalid JSON arguments for tool {tool_name}: {tool_args}",
})
except Exception as e:
tool_responses.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": tool_name,
"content": f"Error executing tool {tool_name}: {e}",
})
# Append tool responses and call AI again
messages.extend(tool_responses)
payload["messages"] = [{"role": "system", "content": system_prompt}] + messages
async with aiohttp.ClientSession() as session_followup:
async with session_followup.post(url, headers=headers, json=payload) as resp_followup:
if resp_followup.status != 200:
text = await resp_followup.text()
raise RuntimeError(f"OpenRouter API returned error status {resp_followup.status} during tool response follow-up: {text[:500]}")
data_followup = await resp_followup.json()
if "choices" not in data_followup or not data_followup["choices"]:
raise RuntimeError(f"OpenRouter API returned unexpected response format during tool response follow-up: {data_followup}")
return data_followup["choices"][0]["message"]["content"]
else:
# No tool calls, return the AI's text response
return ai_response_message["content"]
else:
text = await resp.text()
raise RuntimeError(f"OpenRouter API returned non-JSON response (status {resp.status}): {text[:500]}")
@ -132,6 +289,14 @@ class TetoCog(commands.Cog):
# Only keep track of actual AI interactions in memory
if trigger:
user_content = []
# Include user mentions with IDs for the AI
mentioned_users_info = []
for mention in message.mentions:
if isinstance(mention, discord.Member):
mentioned_users_info.append(f"{mention.display_name} (ID: {mention.id})")
if mentioned_users_info:
user_content.append({"type": "text", "text": "Mentioned users: " + ", ".join(mentioned_users_info)})
if message.content:
user_content.append({"type": "text", "text": message.content})
@ -182,7 +347,16 @@ class TetoCog(commands.Cog):
log.info("[TETO DEBUG] Message triggered AI but contained no supported content (text, image, sticker, emoji).")
return # Don't send empty messages to the AI
convo.append({"role": "user", "content": user_content})
# Prepend user's display name to the message content
formatted_user_content = []
author_display_name = message.author.display_name
for item in user_content:
if item["type"] == "text":
formatted_user_content.append({"type": "text", "text": f"{author_display_name}: {item['text']}"})
else:
formatted_user_content.append(item) # Keep other types as they are
convo.append({"role": "user", "content": formatted_user_content})
try:
async with channel.typing():