diff --git a/settings.toml b/settings.toml index 48c812a..561ab87 100644 --- a/settings.toml +++ b/settings.toml @@ -57,19 +57,18 @@ Use the following format: Use 'Winner: draw' only if both sides argue persuasively with no clear advantage. Use 'Winner: none' if there is no meaningful disagreement or argument. Then, provide a concise but complete summary of the reasoning behind your decision. Focus on the key strengths and weaknesses of each side's argument, and explain why one side prevailed (or why it was a draw or no meaningful disagreement). -Hard constraint: Refer to users with the exact unicode characters and casing provided even if at the start of sentences. -Hard constraint: Your entire response must not exceed 2000 characters. If necessary, prioritize substance, cut repetition, and trim soft qualifiers to stay within this limit. +Hard constraint: Refer to users with the exact unicode characters and casing provided even if at the start of sentences. Write concisely and in a straightforward way. Avoid anthithesis and purple prose. """ [genai.tokens] limit = 120000 overhead_max = 20 prompt_max = 100 -output_max = 400 +output_max = 1200 [genai.history] -minutes = 120 -messages = 1000 +minutes = 240 +messages = 350 [elo] diff --git a/src/iqbot/cogs/betting.py b/src/iqbot/cogs/betting.py index 7c95962..8c19041 100644 --- a/src/iqbot/cogs/betting.py +++ b/src/iqbot/cogs/betting.py @@ -13,6 +13,7 @@ from iqbot import db, genai from iqbot.checks import bot_manager from iqbot.config import settings +from iqbot.utils import send_long_message from iqbot.db import Bet, User @@ -107,7 +108,10 @@ async def accept_bet(self, reaction, bet) -> None: prompt = f"Who won the argument, {member1.name} or {member2.name}?" genai_response = await genai.client.send_prompt( - reaction, settings.genai.system_prompt, prompt + reaction, + settings.genai.system_prompt, + prompt, + participants=[bet.user_id_1, bet.user_id_2], ) match = re.search(r"(?<=winner:\s).+(?=\*\*)", genai_response.lower()) @@ -127,7 +131,7 @@ async def accept_bet(self, reaction, bet) -> None: user2 = await session.merge(user2) await session.commit() - await reaction.message.channel.send(genai_response[0:1999]) + await send_long_message(reaction.message.channel, genai_response) await reaction.message.channel.send( f"{member1.display_name}\n{member1.mention} **IQ {start_iq1} -> {user1.iq}**\n{member2.mention} **IQ {start_iq2} -> {user2.iq}**" ) @@ -230,7 +234,10 @@ async def evaluate( try: prompt = f"Evaluate the debate between {member1.name} and {member2.name} on the topic: {topic}. Include the topic in the response." genai_response = await genai.client.send_prompt( - ctx, settings.genai.system_prompt, prompt + ctx, + settings.genai.system_prompt, + prompt, + participants=[member1.id, member2.id], ) match = re.search(r"(?<=winner:\s).+(?=\*\*)", genai_response.lower()) @@ -246,7 +253,7 @@ async def evaluate( user2 = await session.merge(user2) await session.commit() - await ctx.channel.send(genai_response[0:1999]) + await send_long_message(ctx.channel, genai_response) except Exception as e: logger.error(f"Error in evaluate command: {e}") diff --git a/src/iqbot/cogs/misc.py b/src/iqbot/cogs/misc.py index 12ccd45..97f5a64 100644 --- a/src/iqbot/cogs/misc.py +++ b/src/iqbot/cogs/misc.py @@ -5,6 +5,8 @@ from loguru import logger from iqbot import genai +from iqbot.config import settings +from iqbot.utils import send_long_response class Misc(commands.Cog): @@ -39,21 +41,28 @@ async def steelman( "For each user named in the prompt, write a steelman summary: reconstruct the strongest, most coherent version of their argument in your own phrasing. " "Do NOT summarize, quote, or reference any user who is NOT explicitly named in the prompt. " "Do NOT narrate what was said—synthesize and refine each named user's reasoning into its best possible form. " - "Use clear headings for each named user. " - "Hard constraint: your entire response must be under 2000 characters. " - "To stay within this limit, prioritize substance, remove repetition, and trim soft qualifiers." + "Use clear headings for each named user." ) prompt = f"Please summarize the conversation between {member1.name} and {member2.name}. \n\n" - genai_response = await genai.client.send_prompt(ctx, system_prompt, prompt) + genai_response = await genai.client.send_prompt( + ctx, system_prompt, prompt, participants=[member1.id, member2.id] + ) genai_response = genai_response.replace(member1.name, member1.display_name) genai_response = genai_response.replace(member2.name, member2.display_name) - await ctx.respond(genai_response[0:1999]) + await send_long_response(ctx, genai_response) except Exception as e: logger.error(f"Error in on_reaction_add: {e}") await ctx.respond("An error occurred while processing your request.") + @commands.slash_command( + name="prompt", description="Shows the system prompt used for judging debates" + ) + async def prompt(self, ctx: ApplicationContext): + await send_long_response(ctx, settings.genai.system_prompt.strip()) + + def setup(bot): bot.add_cog(Misc(bot)) diff --git a/src/iqbot/genai.py b/src/iqbot/genai.py index f2f5e61..e17aec4 100644 --- a/src/iqbot/genai.py +++ b/src/iqbot/genai.py @@ -53,23 +53,27 @@ def available_tokens(self, input: str) -> int: ) def format_message(self, message: Message) -> str: - reply_id = None + author = message.author.name + reply_author = None if ( message.reference and message.reference.resolved and isinstance(message.reference.resolved, Message) ): - reply_id = message.reference.resolved.id - id = message.id - author = message.author.name - if not reply_id: - return f"[ID: {id} | {author}]: {message.content}" - else: - return f"[ID: {id} | {author} replying to {reply_id}]: {message.content}" + reply_author = message.reference.resolved.author.name + if reply_author: + return f"{author} (re {reply_author}): {message.content}" + return f"{author}: {message.content}" - async def read_context(self, ctx: ApplicationContext | Reaction | Message) -> str: + async def read_context( + self, + ctx: ApplicationContext | Reaction | Message, + participants: list[int] | None = None, + ) -> str: messages = [] context_tokens = self.available_tokens("") + reply_target_ids: set[int] = set() + if isinstance(ctx, ApplicationContext): channel = ctx.channel elif isinstance(ctx, Message): @@ -89,6 +93,14 @@ async def read_context(self, ctx: ApplicationContext | Reaction | Message) -> st if message.author.bot: continue + if participants is not None: + is_participant = message.author.id in participants + is_reply_target = message.id in reply_target_ids + if not is_participant and not is_reply_target: + continue + if is_participant and message.reference and message.reference.message_id: + reply_target_ids.add(message.reference.message_id) + formatted_message = self.format_message(message) message_tokens = self.count_tokens(formatted_message) @@ -179,9 +191,9 @@ async def send_prompt( ctx: ApplicationContext | Reaction, system_prompt: str, command_prompt: str, + participants: list[int] | None = None, ) -> str: raise NotImplementedError("send_prompt must be implemented by subclasses") - pass class GenAIGpt(GenAIBase): @@ -219,9 +231,10 @@ async def send_prompt( ctx: ApplicationContext | Reaction, system_prompt: str, command_prompt: str, + participants: list[int] | None = None, ) -> str: - conversation = await self.read_context(ctx) + conversation = await self.read_context(ctx, participants=participants) if not conversation: logger.warning("No conversation history found.") @@ -282,8 +295,9 @@ async def send_prompt( ctx: ApplicationContext | Reaction, system_prompt: str, command_prompt: str, + participants: list[int] | None = None, ) -> str: - conversation = await self.read_context(ctx) + conversation = await self.read_context(ctx, participants=participants) if not conversation: logger.warning("No conversation history found.") diff --git a/src/iqbot/utils.py b/src/iqbot/utils.py new file mode 100644 index 0000000..01af904 --- /dev/null +++ b/src/iqbot/utils.py @@ -0,0 +1,38 @@ +DISCORD_MAX = 2000 + + +def split_message(text: str, limit: int = DISCORD_MAX) -> list[str]: + """Split text into chunks that fit within Discord's character limit, + preferring to break at newlines.""" + if len(text) <= limit: + return [text] + + chunks: list[str] = [] + while text: + if len(text) <= limit: + chunks.append(text) + break + + split_at = text.rfind("\n", 0, limit) + if split_at <= 0: + split_at = limit + + chunks.append(text[:split_at]) + text = text[split_at:].lstrip("\n") + + return chunks + + +async def send_long_response(ctx, text: str) -> None: + """Send a potentially long AI response as a slash-command response, + using ctx.respond for the first chunk and channel.send for the rest.""" + chunks = split_message(text) + await ctx.respond(chunks[0]) + for chunk in chunks[1:]: + await ctx.channel.send(chunk) + + +async def send_long_message(channel, text: str) -> None: + """Send a potentially long message to a channel, splitting as needed.""" + for chunk in split_message(text): + await channel.send(chunk)