148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
"""Conversation service: run a capped bot-vs-bot conversation (talkforme)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from vibe_bot import llm_client
|
|
from vibe_bot.config import CHAT_MODEL, MAX_COMPLETION_TOKENS
|
|
from vibe_bot.prompts import RESPONSE_LENGTH_HINT
|
|
from vibe_bot.textutil import split_message
|
|
|
|
if TYPE_CHECKING:
|
|
from discord.ext.commands import Bot, Context
|
|
|
|
from vibe_bot.database import CustomBotManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Input size bound: reject oversized topics before they reach the LLM.
|
|
MAX_TOPIC_LENGTH = 500
|
|
|
|
# Hard cap on the number of replies a single !talkforme invocation produces.
|
|
TALK_LIMIT = 20
|
|
|
|
|
|
def flip_counter(counter: int) -> int:
|
|
"""Flip between 0 and 1 (the two conversing bots)."""
|
|
return 1 if counter == 0 else 0
|
|
|
|
|
|
class ConversationService:
|
|
"""Runs a two-bot conversation about a topic, chunking each reply."""
|
|
|
|
def __init__(self, manager: CustomBotManager) -> None:
|
|
self._manager = manager
|
|
|
|
async def run(
|
|
self,
|
|
ctx: Context[Bot],
|
|
bot1: str,
|
|
bot2: str,
|
|
limit: str,
|
|
topic: str,
|
|
) -> None:
|
|
"""Have ``bot1`` and ``bot2`` talk about ``topic`` for up to ``limit`` replies.
|
|
|
|
Args:
|
|
ctx: The Discord command context.
|
|
bot1: Name of the first custom bot.
|
|
bot2: Name of the second custom bot.
|
|
limit: Requested number of replies (string; parsed to an int).
|
|
topic: The conversation topic.
|
|
|
|
"""
|
|
if len(topic) > MAX_TOPIC_LENGTH:
|
|
logger.warning(
|
|
"Talkforme topic too long from user %s: length=%d",
|
|
ctx.author.id,
|
|
len(topic),
|
|
)
|
|
await ctx.send(f"Topic too long. Max {MAX_TOPIC_LENGTH} characters.")
|
|
return
|
|
|
|
bot1_info = await asyncio.to_thread(self._manager.get_custom_bot, bot1)
|
|
if not bot1_info:
|
|
await ctx.send(f"{bot1} is not a real bot...")
|
|
return
|
|
bot1_prompt = bot1_info[1]
|
|
|
|
bot2_info = await asyncio.to_thread(self._manager.get_custom_bot, bot2)
|
|
if not bot2_info:
|
|
await ctx.send(f"{bot2} is not a real bot...")
|
|
return
|
|
bot2_prompt = bot2_info[1]
|
|
|
|
try:
|
|
message_limit = int(limit)
|
|
except ValueError:
|
|
await ctx.send("Message limit must be an integer.")
|
|
return
|
|
|
|
effective_limit = min(message_limit, TALK_LIMIT)
|
|
await ctx.send(
|
|
f"{bot1} is going to talk to {bot2} "
|
|
f'about "{topic[:50]}" for {effective_limit} replies.',
|
|
)
|
|
|
|
bot_list = [(bot1, bot1_prompt), (bot2, bot2_prompt)]
|
|
|
|
async def send_chunked(text: str) -> None:
|
|
"""Send text in 1000-char chunks to stay under Discord's limit."""
|
|
for chunk in split_message(text, 1000):
|
|
await ctx.send(chunk)
|
|
|
|
message_counter = 0
|
|
bot_counter = 0
|
|
current_bot = bot_list[bot_counter]
|
|
prompt_histories: list[list[dict[str, str]]] = [
|
|
[{"role": "user", "content": topic}],
|
|
[{"role": "assistant", "content": topic}],
|
|
]
|
|
|
|
first_bot_response = await llm_client.chat_completion_with_history(
|
|
system_prompt=(
|
|
current_bot[1] + f"\n{RESPONSE_LENGTH_HINT} "
|
|
f"You are talking to {current_bot[flip_counter(bot_counter)][0]}"
|
|
),
|
|
prompts=prompt_histories[bot_counter],
|
|
model=CHAT_MODEL,
|
|
max_tokens=MAX_COMPLETION_TOKENS,
|
|
)
|
|
await ctx.send(f"## {current_bot[0]}")
|
|
await send_chunked(first_bot_response)
|
|
prompt_histories[0].append({"role": "assistant", "content": first_bot_response})
|
|
prompt_histories[1].append({"role": "user", "content": first_bot_response})
|
|
|
|
bot_counter = flip_counter(counter=bot_counter)
|
|
|
|
while message_counter < effective_limit:
|
|
current_bot = bot_list[bot_counter]
|
|
logger.debug("Current bot is %s", current_bot[0])
|
|
bot_response = await llm_client.chat_completion_with_history(
|
|
system_prompt=(
|
|
current_bot[1] + f"\n{RESPONSE_LENGTH_HINT} "
|
|
f"You are talking to {current_bot[flip_counter(bot_counter)][0]}"
|
|
),
|
|
prompts=prompt_histories[bot_counter],
|
|
model=CHAT_MODEL,
|
|
max_tokens=MAX_COMPLETION_TOKENS,
|
|
)
|
|
message_counter += 1
|
|
prompt_histories[bot_counter].append(
|
|
{"role": "assistant", "content": bot_response},
|
|
)
|
|
prompt_histories[flip_counter(bot_counter)].append(
|
|
{"role": "user", "content": bot_response},
|
|
)
|
|
await ctx.send(f"## {current_bot[0]}")
|
|
await send_chunked(bot_response)
|
|
bot_counter = flip_counter(counter=bot_counter)
|
|
logger.debug(
|
|
"Message counter is %d/%d",
|
|
message_counter,
|
|
effective_limit,
|
|
)
|