discordbot/cogs/multi_conversation_ai_cog.py
2025-04-25 14:03:49 -06:00

1828 lines
78 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import discord
from discord.ext import commands
from discord import app_commands, ui
import aiohttp
import json
import os
import datetime
import uuid
from typing import Dict, List, Optional, Any, Union
import asyncio
import builtins
from dotenv import load_dotenv
# Import Discord sync API functions
try:
from discord_bot_sync_api import save_discord_conversation, load_conversations, user_conversations as synced_conversations, user_settings as synced_user_settings, load_user_settings as load_synced_user_settings
SYNC_API_AVAILABLE = True
except ImportError:
print("Discord sync API not available. Sync features will be disabled.")
SYNC_API_AVAILABLE = False
synced_conversations = {}
synced_user_settings = {}
# Load environment variables
load_dotenv()
# File paths
CONVERSATIONS_FILE = "ai_multi_conversations.json" # File to store multiple conversations
USER_SETTINGS_FILE = "ai_multi_user_settings.json" # File to store user settings
# Customization Variables
AI_API_KEY = os.getenv("AI_API_KEY", "") # API key for OpenAI or compatible service
AI_API_URL = os.getenv("AI_API_URL", "https://api.openai.com/v1/chat/completions") # API endpoint
AI_DEFAULT_MODEL = os.getenv("AI_DEFAULT_MODEL", "gpt-3.5-turbo") # Default model to use
AI_DEFAULT_SYSTEM_PROMPT = os.getenv("AI_DEFAULT_SYSTEM_PROMPT", r"""You are a helpful assistant.""") # Default system prompt
AI_MAX_TOKENS = int(os.getenv("AI_MAX_TOKENS", "1000")) # Maximum tokens in response
AI_TEMPERATURE = float(os.getenv("AI_TEMPERATURE", "0.7")) # Temperature for response generation
AI_TIMEOUT = int(os.getenv("AI_TIMEOUT", "60")) # Timeout for API requests in seconds
AI_COMPATIBILITY_MODE = os.getenv("AI_COMPATIBILITY_MODE", "openai").lower() # API compatibility mode (openai, custom)
# Store user conversations
user_conversations = {}
# Store active conversation IDs for each user
active_conversations = {}
# Define the conversation data structure
class Conversation:
def __init__(
self,
id: str = None,
title: str = "New Conversation",
messages: List[Dict[str, Any]] = None,
created_at: datetime.datetime = None,
updated_at: datetime.datetime = None,
model_id: str = AI_DEFAULT_MODEL,
system_message: str = AI_DEFAULT_SYSTEM_PROMPT,
reasoning_enabled: bool = False,
reasoning_effort: str = "medium",
temperature: float = AI_TEMPERATURE,
max_tokens: int = AI_MAX_TOKENS,
web_search_enabled: bool = False,
character: str = "",
character_info: str = "",
character_breakdown: bool = False,
custom_instructions: str = ""
):
self.id = id or str(uuid.uuid4())
self.title = title
self.messages = messages or []
self.created_at = created_at or datetime.datetime.now()
self.updated_at = updated_at or datetime.datetime.now()
self.model_id = model_id
self.system_message = system_message
self.reasoning_enabled = reasoning_enabled
self.reasoning_effort = reasoning_effort
self.temperature = temperature
self.max_tokens = max_tokens
self.web_search_enabled = web_search_enabled
self.character = character
self.character_info = character_info
self.character_breakdown = character_breakdown
self.custom_instructions = custom_instructions
def to_dict(self):
"""Convert conversation to dictionary for serialization"""
return {
"id": self.id,
"title": self.title,
"messages": self.messages,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"model_id": self.model_id,
"system_message": self.system_message,
"reasoning_enabled": self.reasoning_enabled,
"reasoning_effort": self.reasoning_effort,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"web_search_enabled": self.web_search_enabled,
"character": self.character,
"character_info": self.character_info,
"character_breakdown": self.character_breakdown,
"custom_instructions": self.custom_instructions
}
@classmethod
def from_dict(cls, data):
"""Create conversation from dictionary"""
return cls(
id=data.get("id"),
title=data.get("title", "New Conversation"),
messages=data.get("messages", []),
created_at=datetime.datetime.fromisoformat(data.get("created_at")) if data.get("created_at") else None,
updated_at=datetime.datetime.fromisoformat(data.get("updated_at")) if data.get("updated_at") else None,
model_id=data.get("model_id", AI_DEFAULT_MODEL),
system_message=data.get("system_message", AI_DEFAULT_SYSTEM_PROMPT),
reasoning_enabled=data.get("reasoning_enabled", False),
reasoning_effort=data.get("reasoning_effort", "medium"),
temperature=data.get("temperature", AI_TEMPERATURE),
max_tokens=data.get("max_tokens", AI_MAX_TOKENS),
web_search_enabled=data.get("web_search_enabled", False),
character=data.get("character", ""),
character_info=data.get("character_info", ""),
character_breakdown=data.get("character_breakdown", False),
custom_instructions=data.get("custom_instructions", "")
)
# Load conversations from JSON file
def load_user_conversations():
"""Load user conversations from JSON file"""
global user_conversations
if os.path.exists(CONVERSATIONS_FILE):
try:
with open(CONVERSATIONS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys (user IDs) back to integers
user_conversations = {}
for user_id_str, convs in data.items():
user_id = int(user_id_str)
user_conversations[user_id] = [Conversation.from_dict(conv) for conv in convs]
print(f"Loaded conversations for {len(user_conversations)} users")
except Exception as e:
print(f"Error loading user conversations: {e}")
user_conversations = {}
# Save conversations to JSON file
def save_user_conversations():
"""Save user conversations to JSON file"""
try:
# Convert to JSON-serializable format
serializable_data = {}
for user_id, convs in user_conversations.items():
serializable_data[str(user_id)] = [conv.to_dict() for conv in convs]
with open(CONVERSATIONS_FILE, "w", encoding="utf-8") as f:
json.dump(serializable_data, f, indent=4, default=str, ensure_ascii=False)
except Exception as e:
print(f"Error saving user conversations: {e}")
# Load active conversations from settings file
def load_active_conversations():
"""Load active conversation IDs from settings file"""
global active_conversations
if os.path.exists(USER_SETTINGS_FILE):
try:
with open(USER_SETTINGS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
# Convert string keys (user IDs) back to integers
active_conversations = {int(k): v.get("active_conversation") for k, v in data.items() if "active_conversation" in v}
print(f"Loaded active conversations for {len(active_conversations)} users")
except Exception as e:
print(f"Error loading active conversations: {e}")
active_conversations = {}
# Save active conversations to settings file
def save_active_conversations():
"""Save active conversation IDs to settings file"""
try:
# Load existing settings first
settings = {}
if os.path.exists(USER_SETTINGS_FILE):
with open(USER_SETTINGS_FILE, "r") as f:
settings = json.load(f)
# Update with active conversations
for user_id, conv_id in active_conversations.items():
if str(user_id) not in settings:
settings[str(user_id)] = {}
settings[str(user_id)]["active_conversation"] = conv_id
# Save back to file
with open(USER_SETTINGS_FILE, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Error saving active conversations: {e}")
# Initialize by loading saved data
load_user_conversations()
load_active_conversations()
# Load synced user settings if available
if SYNC_API_AVAILABLE:
try:
load_synced_user_settings()
except Exception as e:
print(f"Error loading synced user settings: {e}")
class MultiConversationCog(commands.Cog, name="MultiConversation"):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = None
async def cog_load(self):
"""Create aiohttp session when cog is loaded"""
self.session = aiohttp.ClientSession()
async def cog_unload(self):
"""Close aiohttp session when cog is unloaded"""
if self.session:
await self.session.close()
# Save conversations and settings when unloading
save_user_conversations()
save_active_conversations()
def get_active_conversation(self, user_id: int) -> Optional[Conversation]:
"""Get the active conversation for a user"""
# Get the active conversation ID
active_id = active_conversations.get(user_id)
if not active_id:
return None
# Get the user's conversations
conversations = user_conversations.get(user_id, [])
if not conversations:
return None
# Find the active conversation
return next((c for c in conversations if c.id == active_id), None)
def create_new_conversation(self, user_id: int, title: str = "New Conversation") -> Conversation:
"""Create a new conversation for a user"""
# Check if we have synced settings for this user
synced_settings = None
if SYNC_API_AVAILABLE:
str_user_id = str(user_id)
if str_user_id in synced_user_settings:
synced_settings = synced_user_settings[str_user_id]
# Create a new conversation with default settings
new_conv = Conversation(title=title)
# Apply synced settings if available
if synced_settings:
# Apply model settings
if hasattr(synced_settings, 'model_id') and synced_settings.model_id:
new_conv.model_id = synced_settings.model_id
# Apply temperature settings
if hasattr(synced_settings, 'temperature'):
new_conv.temperature = synced_settings.temperature
# Apply max tokens settings
if hasattr(synced_settings, 'max_tokens'):
new_conv.max_tokens = synced_settings.max_tokens
# Apply reasoning settings
if hasattr(synced_settings, 'reasoning_enabled'):
new_conv.reasoning_enabled = synced_settings.reasoning_enabled
if hasattr(synced_settings, 'reasoning_effort'):
new_conv.reasoning_effort = synced_settings.reasoning_effort
# Apply web search settings
if hasattr(synced_settings, 'web_search_enabled'):
new_conv.web_search_enabled = synced_settings.web_search_enabled
# Apply system message
if hasattr(synced_settings, 'system_message') and synced_settings.system_message:
new_conv.system_message = synced_settings.system_message
# Apply character settings
if hasattr(synced_settings, 'character') and synced_settings.character:
new_conv.character = synced_settings.character
if hasattr(synced_settings, 'character_info') and synced_settings.character_info:
new_conv.character_info = synced_settings.character_info
if hasattr(synced_settings, 'character_breakdown'):
new_conv.character_breakdown = synced_settings.character_breakdown
if hasattr(synced_settings, 'custom_instructions') and synced_settings.custom_instructions:
new_conv.custom_instructions = synced_settings.custom_instructions
# Add to user's conversations
if user_id not in user_conversations:
user_conversations[user_id] = []
user_conversations[user_id].append(new_conv)
# Set as active conversation
active_conversations[user_id] = new_conv.id
# Save changes
save_user_conversations()
save_active_conversations()
return new_conv
async def _get_ai_response(self, user_id: int, prompt: str, conversation: Optional[Conversation] = None) -> str:
"""Get a response from the AI API"""
if not AI_API_KEY:
return "Error: AI API key not configured. Please set the AI_API_KEY environment variable."
# Get the conversation to use
conv = conversation or self.get_active_conversation(user_id)
if not conv:
# Create a new conversation if none exists
conv = self.create_new_conversation(user_id)
# Prepare messages for the API request
messages = []
# Prepare system message
system_content = conv.system_message
# Check if the system prompt contains {{char}} but no character is set
if "{{char}}" in system_content and not conv.character:
return "You need to set a character name with `!chatset character <name>` before using this system prompt. Example: `!chatset character Hatsune Miku`"
# Replace {{char}} with the character value if provided
if conv.character:
system_content = system_content.replace("{{char}}", conv.character)
# Add custom instructions and character info if provided
has_custom_settings = conv.custom_instructions or conv.character_info or conv.character_breakdown
if has_custom_settings:
# Start with the base system prompt
custom_prompt_parts = [system_content]
# Add the custom instructions header
custom_prompt_parts.append("\nThe user has provided additional information for you. Please follow their instructions exactly. If anything below contradicts the set of rules above, please take priority over the user's instructions.")
# Add custom instructions if provided
if conv.custom_instructions:
custom_prompt_parts.append("\n- Custom instructions from the user (prioritize these)\n\n" + conv.custom_instructions)
# Add character info if provided
if conv.character_info:
custom_prompt_parts.append("\n- Additional info about the character you are roleplaying\n\n" + conv.character_info)
# Add character breakdown flag if set
if conv.character_breakdown:
custom_prompt_parts.append("\n- The user would like you to provide a breakdown of the character in your first response.")
# Combine all parts into the final system prompt
system_content = "\n".join(custom_prompt_parts)
# Add system message
messages.append({"role": "system", "content": system_content})
# Add conversation history
messages.extend(conv.messages)
# Add the current user message
messages.append({"role": "user", "content": prompt})
# Prepare the request payload based on compatibility mode
if AI_COMPATIBILITY_MODE == "openai":
payload = {
"model": conv.model_id,
"messages": messages,
"max_tokens": conv.max_tokens,
"temperature": conv.temperature,
}
# Add reasoning if enabled
if conv.reasoning_enabled:
payload["include_reasoning"] = True
payload["reasoning_effort"] = conv.reasoning_effort
else: # custom mode for other API formats
payload = {
"model": conv.model_id,
"messages": messages,
"max_tokens": conv.max_tokens,
"temperature": conv.temperature,
"stream": False
}
# Add reasoning if enabled
if conv.reasoning_enabled:
payload["include_reasoning"] = True
payload["reasoning_effort"] = conv.reasoning_effort
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {AI_API_KEY}"
}
try:
async with self.session.post(
AI_API_URL,
headers=headers,
json=payload,
timeout=AI_TIMEOUT
) as response:
if response.status != 200:
error_text = await response.text()
return f"Error from API (Status {response.status}): {error_text}"
data = await response.json()
# Parse the response based on compatibility mode
ai_response = None
reasoning_tokens = None
safety_cutoff = False
usage_data = None
if AI_COMPATIBILITY_MODE == "openai":
# OpenAI format
if "choices" not in data:
error_message = f"Unexpected API response format: {data}"
print(f"Error: {error_message}")
if "error" in data:
return f"API Error: {data['error'].get('message', 'Unknown error')}"
return error_message
if not data["choices"] or "message" not in data["choices"][0]:
error_message = f"No valid choices in API response: {data}"
print(f"Error: {error_message}")
return error_message
ai_response = data["choices"][0]["message"]["content"]
# Check for reasoning tokens if requested
if conv.reasoning_enabled and "reasoning" in data["choices"][0]["message"]:
reasoning_tokens = data["choices"][0]["message"]["reasoning"]
# Check for safety cutoff
if "finish_reason" in data["choices"][0] and data["choices"][0]["finish_reason"] == "content_filter":
safety_cutoff = True
if "native_finish_reason" in data["choices"][0] and data["choices"][0]["native_finish_reason"] == "SAFETY":
safety_cutoff = True
# Get usage data if available
if "usage" in data:
usage_data = data["usage"]
else:
# Custom format - try different response structures
# Try standard OpenAI format first
if "choices" in data and data["choices"] and "message" in data["choices"][0]:
ai_response = data["choices"][0]["message"]["content"]
# Check for reasoning tokens if requested
if conv.reasoning_enabled and "reasoning" in data["choices"][0]["message"]:
reasoning_tokens = data["choices"][0]["message"]["reasoning"]
# Check for safety cutoff
if "finish_reason" in data["choices"][0] and data["choices"][0]["finish_reason"] == "content_filter":
safety_cutoff = True
if "native_finish_reason" in data["choices"][0] and data["choices"][0]["native_finish_reason"] == "SAFETY":
safety_cutoff = True
# Get usage data if available
if "usage" in data:
usage_data = data["usage"]
# Try other formats
elif "response" in data:
ai_response = data["response"]
elif "text" in data:
ai_response = data["text"]
elif "content" in data:
ai_response = data["content"]
elif "output" in data:
ai_response = data["output"]
elif "result" in data:
ai_response = data["result"]
else:
# If we can't find a known format, return the raw response for debugging
error_message = f"Could not parse API response: {data}"
print(f"Error: {error_message}")
return error_message
if not ai_response:
return "Error: Empty response from AI API."
# Add safety cutoff note if needed
if safety_cutoff:
ai_response = f"{ai_response}\n\nThe response was cut off for safety reasons."
# Add reasoning tokens if available and requested
final_response = ai_response
if conv.reasoning_enabled and reasoning_tokens:
final_response = f"{ai_response}\n\n**Reasoning:**\n```\n{reasoning_tokens}\n```"
# Update conversation history
# Add user message
conv.messages.append({"role": "user", "content": prompt})
# Add assistant message with reasoning and usage if available
assistant_message = {"role": "assistant", "content": ai_response}
if reasoning_tokens:
assistant_message["reasoning"] = reasoning_tokens
if usage_data:
assistant_message["usage_data"] = usage_data
conv.messages.append(assistant_message)
# Update conversation timestamp
conv.updated_at = datetime.datetime.now()
# Auto-generate title for new conversations with only one message
if conv.title == "New Conversation" and len(conv.messages) == 2:
# Use a simple title based on the prompt
if len(prompt) > 30:
conv.title = prompt[:27] + "..."
else:
conv.title = prompt
# Save conversation
save_user_conversations()
# Sync with API if available
if SYNC_API_AVAILABLE:
try:
save_discord_conversation(
str(user_id),
conv.messages,
conv.model_id,
conv.id,
conv.title,
conv.reasoning_enabled,
conv.reasoning_effort,
conv.temperature,
conv.max_tokens,
conv.web_search_enabled,
conv.system_message,
conv.character,
conv.character_info,
conv.character_breakdown,
conv.custom_instructions
)
print(f"Synced conversation {conv.id} for user {user_id}")
except Exception as e:
print(f"Error syncing conversation: {e}")
return final_response
except asyncio.TimeoutError:
return "Error: Request to AI API timed out. Please try again later."
except Exception as e:
error_message = f"Error communicating with AI API: {str(e)}"
print(f"Exception in _get_ai_response: {error_message}")
print(f"Exception type: {type(e).__name__}")
import traceback
traceback.print_exc()
return error_message
@commands.command(name="convs")
async def ai_conversations(self, ctx: commands.Context):
"""Manage your AI conversations"""
user_id = ctx.author.id
# Get user's conversations
conversations = user_conversations.get(user_id, [])
if not conversations:
# No conversations, create a new one
new_conv = self.create_new_conversation(user_id)
await ctx.reply(f"Created your first conversation: {new_conv.title}", view=ConversationManagementView(user_id))
else:
# Show conversation management UI
await ctx.reply("Here are your conversations:", view=ConversationManagementView(user_id))
@commands.command(name="chat")
async def ai_command(self, ctx: commands.Context, *, prompt: str):
"""Get a response from the AI using your active conversation"""
user_id = ctx.author.id
# Get active conversation
conv = self.get_active_conversation(user_id)
if not conv:
# No active conversation, create a new one
conv = self.create_new_conversation(user_id)
await ctx.reply(f"Created a new conversation: {conv.title}")
# Show typing indicator
async with ctx.typing():
# Get AI response
response = await self._get_ai_response(user_id, prompt, conv)
# Check if the response is too long
if len(response) > 1900:
# Split into chunks or send as file
try:
# Create a text file with the content
file_name = f'ai_response_{ctx.author.id}_{int(datetime.datetime.now().timestamp())}.txt'
with open(file_name, 'w', encoding='utf-8') as f:
f.write(response)
# Send the file
await ctx.reply(
f"Response for conversation '{conv.title}' (too long for Discord):",
file=discord.File(file_name)
)
# Clean up the file
try:
os.remove(file_name)
except:
pass
except Exception as e:
print(f"Error sending file: {e}")
# Fall back to splitting the message
chunks = [response[i:i+1900] for i in range(0, len(response), 1900)]
await ctx.reply(f"Response for conversation '{conv.title}' (split into {len(chunks)} parts):")
for i, chunk in enumerate(chunks):
await ctx.send(f"**Part {i+1}/{len(chunks)}**\n{chunk}")
else:
# Send the response directly
await ctx.reply(response)
@commands.command(name="chatset")
async def ai_settings(self, ctx: commands.Context, setting: str = None, *, value: str = None):
"""Update settings for your active conversation
Examples:
!chatset - Show current settings
!chatset temperature 0.8 - Set temperature to 0.8
!chatset reasoning on - Enable reasoning
!chatset web_search on - Enable web search
!chatset system <message> - Set system message
!chatset character <name> - Set character name
!chatset character_info <info> - Set character information
!chatset character_breakdown on - Enable character breakdown
!chatset custom_instructions <text> - Set custom instructions
"""
user_id = ctx.author.id
# Get active conversation
conv = self.get_active_conversation(user_id)
if not conv:
# No active conversation, create a new one
conv = self.create_new_conversation(user_id)
await ctx.reply(f"Created a new conversation: {conv.title}")
if setting is None:
# Show current settings
embed = discord.Embed(
title=f"Settings for: {conv.title}",
description="Current settings for your active conversation",
color=discord.Color.blue()
)
# Add fields for each setting
embed.add_field(name="Model", value=conv.model_id, inline=True)
embed.add_field(name="Temperature", value=str(conv.temperature), inline=True)
embed.add_field(name="Max Tokens", value=str(conv.max_tokens), inline=True)
embed.add_field(name="Reasoning", value="Enabled" if conv.reasoning_enabled else "Disabled", inline=True)
embed.add_field(name="Reasoning Effort", value=conv.reasoning_effort.capitalize(), inline=True)
embed.add_field(name="Web Search", value="Enabled" if conv.web_search_enabled else "Disabled", inline=True)
# Add character settings
embed.add_field(name="Character", value=conv.character or "None", inline=True)
embed.add_field(name="Character Breakdown", value="Enabled" if conv.character_breakdown else "Disabled", inline=True)
# Show system message (truncated if too long)
system_msg = conv.system_message
if len(system_msg) > 1000:
system_msg = system_msg[:997] + "..."
embed.add_field(name="System Message", value=system_msg, inline=False)
# Show character info if available (truncated if too long)
if conv.character_info:
char_info = conv.character_info
if len(char_info) > 1000:
char_info = char_info[:997] + "..."
embed.add_field(name="Character Info", value=char_info, inline=False)
# Show custom instructions if available (truncated if too long)
if conv.custom_instructions:
custom_instr = conv.custom_instructions
if len(custom_instr) > 1000:
custom_instr = custom_instr[:997] + "..."
embed.add_field(name="Custom Instructions", value=custom_instr, inline=False)
await ctx.reply(embed=embed)
return
# Update the specified setting
setting = setting.lower()
if setting == "temperature" or setting == "temp":
if not value:
await ctx.reply(f"Current temperature: {conv.temperature}")
return
try:
temp = float(value)
if 0 <= temp <= 2:
conv.temperature = temp
await ctx.reply(f"Temperature set to {temp}")
else:
await ctx.reply("Temperature must be between 0 and 2")
except ValueError:
await ctx.reply("Temperature must be a number")
elif setting == "max_tokens" or setting == "tokens":
if not value:
await ctx.reply(f"Current max tokens: {conv.max_tokens}")
return
try:
tokens = int(value)
if 100 <= tokens <= 4000:
conv.max_tokens = tokens
await ctx.reply(f"Max tokens set to {tokens}")
else:
await ctx.reply("Max tokens must be between 100 and 4000")
except ValueError:
await ctx.reply("Max tokens must be a number")
elif setting == "reasoning":
if not value:
await ctx.reply(f"Reasoning is currently {'enabled' if conv.reasoning_enabled else 'disabled'}")
return
if value.lower() in ["on", "true", "yes", "enable", "enabled"]:
conv.reasoning_enabled = True
await ctx.reply("Reasoning enabled")
elif value.lower() in ["off", "false", "no", "disable", "disabled"]:
conv.reasoning_enabled = False
await ctx.reply("Reasoning disabled")
else:
await ctx.reply("Value must be 'on' or 'off'")
elif setting == "reasoning_effort" or setting == "effort":
if not value:
await ctx.reply(f"Current reasoning effort: {conv.reasoning_effort}")
return
if value.lower() in ["low", "medium", "high"]:
conv.reasoning_effort = value.lower()
await ctx.reply(f"Reasoning effort set to {value.lower()}")
else:
await ctx.reply("Reasoning effort must be 'low', 'medium', or 'high'")
elif setting == "web_search" or setting == "search":
if not value:
await ctx.reply(f"Web search is currently {'enabled' if conv.web_search_enabled else 'disabled'}")
return
if value.lower() in ["on", "true", "yes", "enable", "enabled"]:
conv.web_search_enabled = True
await ctx.reply("Web search enabled")
elif value.lower() in ["off", "false", "no", "disable", "disabled"]:
conv.web_search_enabled = False
await ctx.reply("Web search disabled")
else:
await ctx.reply("Value must be 'on' or 'off'")
elif setting == "model":
if not value:
await ctx.reply(f"Current model: {conv.model_id}")
return
conv.model_id = value
await ctx.reply(f"Model set to {value}")
elif setting == "system" or setting == "system_message":
if not value:
# Show current system message
system_msg = conv.system_message
if len(system_msg) > 1900:
# Send as file if too long
file_name = f'system_message_{ctx.author.id}.txt'
with open(file_name, 'w', encoding='utf-8') as f:
f.write(system_msg)
await ctx.reply("Current system message (too long for Discord):", file=discord.File(file_name))
# Clean up the file
try:
os.remove(file_name)
except:
pass
else:
await ctx.reply(f"Current system message:\n```\n{system_msg}\n```")
return
# Update system message
conv.system_message = value
await ctx.reply(f"System message updated")
elif setting == "character":
if not value:
await ctx.reply(f"Current character: {conv.character or 'None'}")
return
# Update character
conv.character = value
await ctx.reply(f"Character set to: {value}")
elif setting == "character_info" or setting == "charinfo":
if not value:
# Show current character info
if not conv.character_info:
await ctx.reply("No character info set.")
return
char_info = conv.character_info
if len(char_info) > 1900:
# Send as file if too long
file_name = f'character_info_{ctx.author.id}.txt'
with open(file_name, 'w', encoding='utf-8') as f:
f.write(char_info)
await ctx.reply("Current character info (too long for Discord):", file=discord.File(file_name))
# Clean up the file
try:
os.remove(file_name)
except:
pass
else:
await ctx.reply(f"Current character info:\n```\n{char_info}\n```")
return
# Update character info
conv.character_info = value
await ctx.reply(f"Character info updated")
elif setting == "character_breakdown" or setting == "breakdown":
if not value:
await ctx.reply(f"Character breakdown is currently {'enabled' if conv.character_breakdown else 'disabled'}")
return
if value.lower() in ["on", "true", "yes", "enable", "enabled"]:
conv.character_breakdown = True
await ctx.reply("Character breakdown enabled")
elif value.lower() in ["off", "false", "no", "disable", "disabled"]:
conv.character_breakdown = False
await ctx.reply("Character breakdown disabled")
else:
await ctx.reply("Value must be 'on' or 'off'")
elif setting == "custom_instructions" or setting == "instructions":
if not value:
# Show current custom instructions
if not conv.custom_instructions:
await ctx.reply("No custom instructions set.")
return
custom_instr = conv.custom_instructions
if len(custom_instr) > 1900:
# Send as file if too long
file_name = f'custom_instructions_{ctx.author.id}.txt'
with open(file_name, 'w', encoding='utf-8') as f:
f.write(custom_instr)
await ctx.reply("Current custom instructions (too long for Discord):", file=discord.File(file_name))
# Clean up the file
try:
os.remove(file_name)
except:
pass
else:
await ctx.reply(f"Current custom instructions:\n```\n{custom_instr}\n```")
return
# Update custom instructions
conv.custom_instructions = value
await ctx.reply(f"Custom instructions updated")
elif setting == "title":
if not value:
await ctx.reply(f"Current title: {conv.title}")
return
# Update title
conv.title = value
await ctx.reply(f"Conversation title set to: {value}")
else:
await ctx.reply(f"Unknown setting: {setting}\nAvailable settings: temperature, max_tokens, reasoning, reasoning_effort, web_search, model, system, title")
return
# Update conversation timestamp
conv.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
# Sync with API if available
if SYNC_API_AVAILABLE:
try:
save_discord_conversation(
str(user_id),
conv.messages,
conv.model_id,
conv.id,
conv.title,
conv.reasoning_enabled,
conv.reasoning_effort,
conv.temperature,
conv.max_tokens,
conv.web_search_enabled,
conv.system_message,
conv.character,
conv.character_info,
conv.character_breakdown,
conv.custom_instructions
)
print(f"Synced conversation {conv.id} for user {user_id} after settings update")
except Exception as e:
print(f"Error syncing conversation after settings update: {e}")
@commands.command(name="chatimport")
async def ai_import(self, ctx: commands.Context):
"""Import conversations from the sync API"""
if not SYNC_API_AVAILABLE:
await ctx.reply("Discord sync API is not available. Sync features are disabled.")
return
user_id = ctx.author.id
user_id_str = str(user_id)
# Check if user has any synced conversations
if user_id_str not in synced_conversations or not synced_conversations[user_id_str]:
await ctx.reply("You don't have any synced conversations to import.")
return
# Count how many conversations will be imported
synced_count = len(synced_conversations[user_id_str])
# Create a confirmation message
await ctx.reply(
f"Found {synced_count} synced conversations. Do you want to import them?",
view=ImportConfirmationView(self, user_id)
)
async def import_conversations(self, user_id: int):
"""Import conversations from the sync API for a user"""
user_id_str = str(user_id)
if user_id_str not in synced_conversations or not synced_conversations[user_id_str]:
return "No synced conversations found."
# Get existing conversations
if user_id not in user_conversations:
user_conversations[user_id] = []
# Track imported conversations
imported_count = 0
updated_count = 0
# Import each synced conversation
for synced_conv in synced_conversations[user_id_str]:
# Check if this conversation already exists
existing_conv = next((c for c in user_conversations[user_id] if c.id == synced_conv.id), None)
if existing_conv:
# Update existing conversation if synced one is newer
if synced_conv.updated_at > existing_conv.updated_at:
# Convert messages to the right format
messages = []
for msg in synced_conv.messages:
message = {
"role": msg.role,
"content": msg.content
}
if msg.reasoning:
message["reasoning"] = msg.reasoning
if msg.usage_data:
message["usage_data"] = msg.usage_data
messages.append(message)
# Update the conversation
existing_conv.messages = messages
existing_conv.title = synced_conv.title
existing_conv.model_id = synced_conv.model_id
existing_conv.updated_at = synced_conv.updated_at
existing_conv.reasoning_enabled = synced_conv.reasoning_enabled
existing_conv.reasoning_effort = synced_conv.reasoning_effort
existing_conv.temperature = synced_conv.temperature
existing_conv.max_tokens = synced_conv.max_tokens
existing_conv.web_search_enabled = synced_conv.web_search_enabled
existing_conv.system_message = synced_conv.system_message or AI_DEFAULT_SYSTEM_PROMPT
updated_count += 1
else:
# Create a new conversation
# Convert messages to the right format
messages = []
for msg in synced_conv.messages:
message = {
"role": msg.role,
"content": msg.content
}
if msg.reasoning:
message["reasoning"] = msg.reasoning
if msg.usage_data:
message["usage_data"] = msg.usage_data
messages.append(message)
# Create the conversation
new_conv = Conversation(
id=synced_conv.id,
title=synced_conv.title,
messages=messages,
created_at=synced_conv.created_at,
updated_at=synced_conv.updated_at,
model_id=synced_conv.model_id,
system_message=synced_conv.system_message or AI_DEFAULT_SYSTEM_PROMPT,
reasoning_enabled=synced_conv.reasoning_enabled,
reasoning_effort=synced_conv.reasoning_effort,
temperature=synced_conv.temperature,
max_tokens=synced_conv.max_tokens,
web_search_enabled=synced_conv.web_search_enabled
)
# Add to user's conversations
user_conversations[user_id].append(new_conv)
imported_count += 1
# Save changes
save_user_conversations()
# Set active conversation if user doesn't have one
if user_id not in active_conversations or not active_conversations[user_id]:
if user_conversations[user_id]:
active_conversations[user_id] = user_conversations[user_id][0].id
save_active_conversations()
return f"Imported {imported_count} new conversations and updated {updated_count} existing conversations."
@app_commands.command(name="chat", description="Get a response from the AI using your active conversation")
@app_commands.describe(prompt="Your message to the AI")
async def ai_slash(self, interaction: discord.Interaction, prompt: str):
"""Slash command to get a response from the AI"""
user_id = interaction.user.id
# Get active conversation
conv = self.get_active_conversation(user_id)
if not conv:
# No active conversation, create a new one
conv = self.create_new_conversation(user_id)
# Defer the response since API calls can take time
await interaction.response.defer(thinking=True)
# Get AI response
response = await self._get_ai_response(user_id, prompt, conv)
# Check if the response is too long
if len(response) > 3900: # Discord's limit for interactions is 4000, use 3900 to be safe
try:
# Create a text file with the content
file_name = f'ai_response_{interaction.user.id}_{int(datetime.datetime.now().timestamp())}.txt'
with open(file_name, 'w', encoding='utf-8') as f:
f.write(response)
# Send the file
await interaction.followup.send(
f"Response for conversation '{conv.title}' (too long for Discord):",
file=discord.File(file_name)
)
# Clean up the file
try:
os.remove(file_name)
except:
pass
except Exception as e:
print(f"Error sending file: {e}")
# Fall back to splitting the message
chunks = [response[i:i+1900] for i in range(0, len(response), 1900)]
await interaction.followup.send(f"Response for conversation '{conv.title}' (split into {len(chunks)} parts):")
for i, chunk in enumerate(chunks):
await interaction.followup.send(f"**Part {i+1}/{len(chunks)}**\n{chunk}")
else:
# Send the response directly
await interaction.followup.send(response)
async def setup(bot):
await bot.add_cog(MultiConversationCog(bot))
# UI Components for conversation management
class ConversationSelectMenu(ui.Select):
def __init__(self, user_id: int, conversations: List[Conversation], current_id: str = None):
self.user_id = user_id
self.conversations = conversations
# Create options for each conversation
options = []
for conv in conversations:
# Truncate title if too long
title = conv.title
if len(title) > 100:
title = title[:97] + "..."
# Create option
option = discord.SelectOption(
label=title,
value=conv.id,
description=f"Created: {conv.created_at.strftime('%Y-%m-%d')}",
default=(conv.id == current_id)
)
options.append(option)
# Add option to create new conversation
options.append(discord.SelectOption(
label=" New Conversation",
value="new",
description="Start a new conversation"
))
super().__init__(placeholder="Select a conversation", options=options, min_values=1, max_values=1)
async def callback(self, interaction: discord.Interaction):
# Get the selected value
selected_id = self.values[0]
if selected_id == "new":
# Get the MultiConversationCog instance
cog = None
for c in interaction.client.cogs.values():
if isinstance(c, MultiConversationCog):
cog = c
break
if cog:
# Use the cog's method to create a new conversation with user settings
new_conv = cog.create_new_conversation(self.user_id, "New Conversation")
else:
# Fallback to creating a conversation directly
# Check if we have synced settings for this user
synced_settings = None
if SYNC_API_AVAILABLE:
str_user_id = str(self.user_id)
if str_user_id in synced_user_settings:
synced_settings = synced_user_settings[str_user_id]
# Create a new conversation with default settings
new_conv = Conversation(title="New Conversation")
# Apply synced settings if available
if synced_settings:
# Apply model settings
if hasattr(synced_settings, 'model_id') and synced_settings.model_id:
new_conv.model_id = synced_settings.model_id
# Apply temperature settings
if hasattr(synced_settings, 'temperature'):
new_conv.temperature = synced_settings.temperature
# Apply max tokens settings
if hasattr(synced_settings, 'max_tokens'):
new_conv.max_tokens = synced_settings.max_tokens
# Apply reasoning settings
if hasattr(synced_settings, 'reasoning_enabled'):
new_conv.reasoning_enabled = synced_settings.reasoning_enabled
if hasattr(synced_settings, 'reasoning_effort'):
new_conv.reasoning_effort = synced_settings.reasoning_effort
# Apply web search settings
if hasattr(synced_settings, 'web_search_enabled'):
new_conv.web_search_enabled = synced_settings.web_search_enabled
# Apply system message
if hasattr(synced_settings, 'system_message') and synced_settings.system_message:
new_conv.system_message = synced_settings.system_message
# Apply character settings
if hasattr(synced_settings, 'character') and synced_settings.character:
new_conv.character = synced_settings.character
if hasattr(synced_settings, 'character_info') and synced_settings.character_info:
new_conv.character_info = synced_settings.character_info
if hasattr(synced_settings, 'character_breakdown'):
new_conv.character_breakdown = synced_settings.character_breakdown
if hasattr(synced_settings, 'custom_instructions') and synced_settings.custom_instructions:
new_conv.custom_instructions = synced_settings.custom_instructions
# Add to user's conversations
if self.user_id not in user_conversations:
user_conversations[self.user_id] = []
user_conversations[self.user_id].append(new_conv)
# Set as active conversation
active_conversations[self.user_id] = new_conv.id
# Save changes
save_user_conversations()
save_active_conversations()
await interaction.response.edit_message(
content=f"Created new conversation: {new_conv.title}",
view=ConversationManagementView(self.user_id)
)
else:
# Set the selected conversation as active
active_conversations[self.user_id] = selected_id
save_active_conversations()
# Find the selected conversation
selected_conv = next((c for c in self.conversations if c.id == selected_id), None)
if selected_conv:
await interaction.response.edit_message(
content=f"Switched to conversation: {selected_conv.title}",
view=ConversationManagementView(self.user_id)
)
else:
await interaction.response.edit_message(content="Error: Conversation not found")
class ConversationManagementView(ui.View):
def __init__(self, user_id: int):
super().__init__(timeout=300) # 5 minute timeout
self.user_id = user_id
# Get user's conversations
conversations = user_conversations.get(user_id, [])
# Get active conversation ID
active_id = active_conversations.get(user_id)
# Add conversation select menu
self.add_item(ConversationSelectMenu(user_id, conversations, active_id))
@ui.button(label="Settings", style=discord.ButtonStyle.primary, emoji="⚙️")
async def settings_button(self, interaction: discord.Interaction, button: ui.Button):
# Get active conversation
active_id = active_conversations.get(self.user_id)
if not active_id:
await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True)
return
# Find the active conversation
conversations = user_conversations.get(self.user_id, [])
active_conv = next((c for c in conversations if c.id == active_id), None)
if not active_conv:
await interaction.response.send_message("Error: Active conversation not found", ephemeral=True)
return
# Show settings for the active conversation
embed = discord.Embed(
title=f"Settings for: {active_conv.title}",
description="Adjust settings for this conversation",
color=discord.Color.blue()
)
# Add fields for each setting
embed.add_field(name="Model", value=active_conv.model_id, inline=True)
embed.add_field(name="Temperature", value=str(active_conv.temperature), inline=True)
embed.add_field(name="Max Tokens", value=str(active_conv.max_tokens), inline=True)
embed.add_field(name="Reasoning", value="Enabled" if active_conv.reasoning_enabled else "Disabled", inline=True)
embed.add_field(name="Reasoning Effort", value=active_conv.reasoning_effort.capitalize(), inline=True)
embed.add_field(name="Web Search", value="Enabled" if active_conv.web_search_enabled else "Disabled", inline=True)
# Add character settings
embed.add_field(name="Character", value=active_conv.character or "None", inline=True)
embed.add_field(name="Character Breakdown", value="Enabled" if active_conv.character_breakdown else "Disabled", inline=True)
# Show system message (truncated if too long)
system_msg = active_conv.system_message
if len(system_msg) > 1000:
system_msg = system_msg[:997] + "..."
embed.add_field(name="System Message", value=system_msg, inline=False)
# Show character info if available (truncated if too long)
if active_conv.character_info:
char_info = active_conv.character_info
if len(char_info) > 1000:
char_info = char_info[:997] + "..."
embed.add_field(name="Character Info", value=char_info, inline=False)
# Show custom instructions if available (truncated if too long)
if active_conv.custom_instructions:
custom_instr = active_conv.custom_instructions
if len(custom_instr) > 1000:
custom_instr = custom_instr[:997] + "..."
embed.add_field(name="Custom Instructions", value=custom_instr, inline=False)
await interaction.response.send_message(embed=embed, view=ConversationSettingsView(self.user_id, active_id), ephemeral=True)
@ui.button(label="Rename", style=discord.ButtonStyle.secondary, emoji="✏️")
async def rename_button(self, interaction: discord.Interaction, button: ui.Button):
# Get active conversation
active_id = active_conversations.get(self.user_id)
if not active_id:
await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True)
return
# Show rename modal
await interaction.response.send_modal(RenameConversationModal(self.user_id, active_id))
@ui.button(label="Delete", style=discord.ButtonStyle.danger, emoji="🗑️")
async def delete_button(self, interaction: discord.Interaction, button: ui.Button):
# Get active conversation
active_id = active_conversations.get(self.user_id)
if not active_id:
await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True)
return
# Find the active conversation
conversations = user_conversations.get(self.user_id, [])
active_conv = next((c for c in conversations if c.id == active_id), None)
if not active_conv:
await interaction.response.send_message("Error: Active conversation not found", ephemeral=True)
return
# Confirm deletion
await interaction.response.send_message(
f"Are you sure you want to delete the conversation '{active_conv.title}'?",
view=DeleteConfirmationView(self.user_id, active_id),
ephemeral=True
)
class RenameConversationModal(ui.Modal, title="Rename Conversation"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add title input
self.title_input = ui.TextInput(
label="New Title",
placeholder="Enter a new title for this conversation",
default=self.conversation.title if self.conversation else "",
required=True,
max_length=100
)
self.add_item(self.title_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation title
self.conversation.title = self.title_input.value
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Renamed conversation to: {self.conversation.title}",
view=ConversationManagementView(self.user_id),
ephemeral=True
)
class DeleteConfirmationView(ui.View):
def __init__(self, user_id: int, conversation_id: str):
super().__init__(timeout=60) # 1 minute timeout
self.user_id = user_id
self.conversation_id = conversation_id
@ui.button(label="Yes, Delete", style=discord.ButtonStyle.danger)
async def confirm_button(self, interaction: discord.Interaction, button: ui.Button):
# Get user's conversations
conversations = user_conversations.get(self.user_id, [])
# Find the conversation to delete
conversation = next((c for c in conversations if c.id == self.conversation_id), None)
if not conversation:
await interaction.response.edit_message(content="Error: Conversation not found", view=None)
return
# Remove the conversation
user_conversations[self.user_id] = [c for c in conversations if c.id != self.conversation_id]
# If this was the active conversation, set a new active conversation
if active_conversations.get(self.user_id) == self.conversation_id:
if user_conversations[self.user_id]:
# Set the first remaining conversation as active
active_conversations[self.user_id] = user_conversations[self.user_id][0].id
else:
# No conversations left, remove active conversation
if self.user_id in active_conversations:
del active_conversations[self.user_id]
# Save changes
save_user_conversations()
save_active_conversations()
await interaction.response.edit_message(
content=f"Deleted conversation: {conversation.title}",
view=ConversationManagementView(self.user_id) if user_conversations.get(self.user_id) else None
)
@ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
async def cancel_button(self, interaction: discord.Interaction, button: ui.Button):
await interaction.response.edit_message(content="Deletion cancelled", view=None)
class ConversationSettingsView(ui.View):
def __init__(self, user_id: int, conversation_id: str):
super().__init__(timeout=300) # 5 minute timeout
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
@ui.button(label="Edit System Message", style=discord.ButtonStyle.primary, row=0)
async def system_message_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show system message modal
await interaction.response.send_modal(SystemMessageModal(self.user_id, self.conversation_id))
@ui.button(label="Set Character", style=discord.ButtonStyle.primary, row=0)
async def character_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show character modal
await interaction.response.send_modal(CharacterModal(self.user_id, self.conversation_id))
@ui.button(label="Character Info", style=discord.ButtonStyle.primary, row=0)
async def character_info_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show character info modal
await interaction.response.send_modal(CharacterInfoModal(self.user_id, self.conversation_id))
@ui.button(label="Custom Instructions", style=discord.ButtonStyle.primary, row=1)
async def custom_instructions_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show custom instructions modal
await interaction.response.send_modal(CustomInstructionsModal(self.user_id, self.conversation_id))
@ui.button(label="Toggle Reasoning", style=discord.ButtonStyle.secondary, row=0)
async def reasoning_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Toggle reasoning
self.conversation.reasoning_enabled = not self.conversation.reasoning_enabled
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Reasoning {'enabled' if self.conversation.reasoning_enabled else 'disabled'} for this conversation",
ephemeral=True
)
@ui.button(label="Toggle Web Search", style=discord.ButtonStyle.secondary, row=0)
async def web_search_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Toggle web search
self.conversation.web_search_enabled = not self.conversation.web_search_enabled
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Web search {'enabled' if self.conversation.web_search_enabled else 'disabled'} for this conversation",
ephemeral=True
)
@ui.button(label="Set Temperature", style=discord.ButtonStyle.secondary, row=1)
async def temperature_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show temperature modal
await interaction.response.send_modal(TemperatureModal(self.user_id, self.conversation_id))
@ui.button(label="Set Max Tokens", style=discord.ButtonStyle.secondary, row=1)
async def max_tokens_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show max tokens modal
await interaction.response.send_modal(MaxTokensModal(self.user_id, self.conversation_id))
@ui.button(label="Set Model", style=discord.ButtonStyle.secondary, row=1)
async def model_button(self, interaction: discord.Interaction, button: ui.Button):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Show model modal
await interaction.response.send_modal(ModelModal(self.user_id, self.conversation_id))
class SystemMessageModal(ui.Modal, title="Edit System Message"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add system message input
self.system_message_input = ui.TextInput(
label="System Message",
placeholder="Enter a system message for this conversation",
default=self.conversation.system_message if self.conversation else AI_DEFAULT_SYSTEM_PROMPT,
required=True,
style=discord.TextStyle.paragraph,
max_length=4000
)
self.add_item(self.system_message_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation system message
self.conversation.system_message = self.system_message_input.value
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
"System message updated",
ephemeral=True
)
class TemperatureModal(ui.Modal, title="Set Temperature"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add temperature input
self.temperature_input = ui.TextInput(
label="Temperature (0.0 - 2.0)",
placeholder="Enter a value between 0.0 and 2.0",
default=str(self.conversation.temperature) if self.conversation else str(AI_TEMPERATURE),
required=True,
max_length=4
)
self.add_item(self.temperature_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
try:
# Parse and validate temperature
temperature = float(self.temperature_input.value)
if temperature < 0.0 or temperature > 2.0:
await interaction.response.send_message("Temperature must be between 0.0 and 2.0", ephemeral=True)
return
# Update the conversation temperature
self.conversation.temperature = temperature
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Temperature set to {temperature}",
ephemeral=True
)
except ValueError:
await interaction.response.send_message("Invalid temperature value. Please enter a number between 0.0 and 2.0", ephemeral=True)
class MaxTokensModal(ui.Modal, title="Set Max Tokens"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add max tokens input
self.max_tokens_input = ui.TextInput(
label="Max Tokens (100 - 4000)",
placeholder="Enter a value between 100 and 4000",
default=str(self.conversation.max_tokens) if self.conversation else str(AI_MAX_TOKENS),
required=True,
max_length=4
)
self.add_item(self.max_tokens_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
try:
# Parse and validate max tokens
max_tokens = int(self.max_tokens_input.value)
if max_tokens < 100 or max_tokens > 4000:
await interaction.response.send_message("Max tokens must be between 100 and 4000", ephemeral=True)
return
# Update the conversation max tokens
self.conversation.max_tokens = max_tokens
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Max tokens set to {max_tokens}",
ephemeral=True
)
except ValueError:
await interaction.response.send_message("Invalid max tokens value. Please enter a number between 100 and 4000", ephemeral=True)
class ModelModal(ui.Modal, title="Set Model"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add model input
self.model_input = ui.TextInput(
label="Model",
placeholder="Enter a model name (e.g., gpt-3.5-turbo, gpt-4)",
default=self.conversation.model_id if self.conversation else AI_DEFAULT_MODEL,
required=True,
max_length=50
)
self.add_item(self.model_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation model
self.conversation.model_id = self.model_input.value
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message(
f"Model set to {self.conversation.model_id}",
ephemeral=True
)
class CharacterModal(ui.Modal, title="Set Character"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add character input
self.character_input = ui.TextInput(
label="Character Name",
placeholder="Enter a character name (e.g., Hatsune Miku)",
default=self.conversation.character if self.conversation else "",
required=False,
max_length=100
)
self.add_item(self.character_input)
# Add character breakdown toggle
self.breakdown_input = ui.TextInput(
label="Character Breakdown",
placeholder="Type 'yes' to enable, 'no' to disable",
default="yes" if self.conversation and self.conversation.character_breakdown else "no",
required=False,
max_length=3
)
self.add_item(self.breakdown_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation character
self.conversation.character = self.character_input.value
# Update character breakdown setting
breakdown_value = self.breakdown_input.value.lower()
if breakdown_value in ["yes", "y", "true", "on", "1"]:
self.conversation.character_breakdown = True
elif breakdown_value in ["no", "n", "false", "off", "0"]:
self.conversation.character_breakdown = False
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
# Prepare response message
if self.conversation.character:
message = f"Character set to: {self.conversation.character}\n"
else:
message = "Character cleared.\n"
message += f"Character breakdown: {'Enabled' if self.conversation.character_breakdown else 'Disabled'}"
await interaction.response.send_message(message, ephemeral=True)
class CharacterInfoModal(ui.Modal, title="Character Information"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add character info input
self.character_info_input = ui.TextInput(
label="Character Information",
placeholder="Enter information about the character being roleplayed",
default=self.conversation.character_info if self.conversation else "",
required=False,
style=discord.TextStyle.paragraph,
max_length=4000
)
self.add_item(self.character_info_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation character info
self.conversation.character_info = self.character_info_input.value
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message("Character information updated.", ephemeral=True)
class CustomInstructionsModal(ui.Modal, title="Custom Instructions"):
def __init__(self, user_id: int, conversation_id: str):
super().__init__()
self.user_id = user_id
self.conversation_id = conversation_id
# Find the conversation
conversations = user_conversations.get(user_id, [])
self.conversation = next((c for c in conversations if c.id == conversation_id), None)
# Add custom instructions input
self.custom_instructions_input = ui.TextInput(
label="Custom Instructions",
placeholder="Enter custom instructions for the AI to follow",
default=self.conversation.custom_instructions if self.conversation else "",
required=False,
style=discord.TextStyle.paragraph,
max_length=4000
)
self.add_item(self.custom_instructions_input)
async def on_submit(self, interaction: discord.Interaction):
if not self.conversation:
await interaction.response.send_message("Error: Conversation not found", ephemeral=True)
return
# Update the conversation custom instructions
self.conversation.custom_instructions = self.custom_instructions_input.value
self.conversation.updated_at = datetime.datetime.now()
# Save changes
save_user_conversations()
await interaction.response.send_message("Custom instructions updated.", ephemeral=True)
class ImportConfirmationView(ui.View):
def __init__(self, cog, user_id: int):
super().__init__(timeout=60) # 1 minute timeout
self.cog = cog
self.user_id = user_id
@ui.button(label="Yes, Import", style=discord.ButtonStyle.primary)
async def confirm_button(self, interaction: discord.Interaction, button: ui.Button):
# Defer response since this might take a while
await interaction.response.defer(thinking=True)
# Import conversations
result = await self.cog.import_conversations(self.user_id)
# Send result
await interaction.followup.send(result)
@ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
async def cancel_button(self, interaction: discord.Interaction, button: ui.Button):
await interaction.response.edit_message(content="Import cancelled", view=None)