O que estamos construindo
Dois bots pequenos, um cérebro compartilhado. Um arquivo chamado llm.py cuida de tudo que toca a API: a persona, o limitador de requisições, a lógica de novas tentativas. Um bot do Discord e um bot do Telegram então não fazem nada além de traduzir eventos da plataforma em uma lista de mensagens e vice-versa. Adicione uma terceira plataforma mais tarde e você só escreve o adaptador.
Instale as dependências com pip install httpx discord.py python-telegram-bot. Exporte API_KEY da sua página de conta, além de DISCORD_TOKEN e/ou TELEGRAM_TOKEN da configuração de bot de cada plataforma. Ambos os bots abaixo assumem Python 3.10 ou superior.
O formato da requisição é o formato simples de chat-completions da OpenAI, coberto em o guia rápido multi-idioma. Nada aqui precisa de um SDK.
O núcleo compartilhado: persona, limitador e tentativas
Coloque a parte perigosa em um único lugar. O módulo abaixo mantém uma janela deslizante de carimbos de data/hora das requisições e aguarda quando se aproxima de 240 chamadas por minuto, uma margem deliberada abaixo dos 300 por minuto permitidos por chave. Um semáforo limita a concorrência a quatro para que um canal ocupado não dispare vinte requisições de uma vez.
# llm.py - shared by both bots
import asyncio
import os
import time
from collections import deque
import httpx
URL = "https://api.unrestrictedaiapi.com/v1/chat/completions"
PERSONA = "You are Pip, a sardonic bar-room storyteller. Keep replies under 150 words."
_stamps = deque() # timestamps of recent calls
_gate = asyncio.Semaphore(4) # at most 4 requests in flight
_client = httpx.AsyncClient(timeout=60.0)
async def _wait_for_slot(limit=240, window=60.0):
# Stay below the 300 requests/minute per-key limit with headroom.
while True:
now = time.monotonic()
while _stamps and now - _stamps[0] > window:
_stamps.popleft()
if len(_stamps) < limit:
_stamps.append(now)
return
await asyncio.sleep(window - (now - _stamps[0]) + 0.05)
async def chat(history):
"""history: list of {"role","content"} dicts, oldest first."""
messages = [{"role": "system", "content": PERSONA}] + list(history)
async with _gate:
for attempt in range(3):
await _wait_for_slot()
r = await _client.post(
URL,
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "uncensored", "messages": messages, "max_tokens": 400},
)
if r.status_code in (429, 503):
await asyncio.sleep(2 ** attempt)
continue
if r.status_code == 403:
return "I can't continue with that."
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "The service is busy. Try again in a moment."Observe o tratamento de status. Um 429 ou 503 aguarda por 1, 2 e depois 4 segundos e tenta novamente. Um 403 significa que o filtro de conteúdo foi acionado, então o bot responde com uma linha neutra em vez de tentar novamente. Qualquer outro erro levanta uma exceção, que sua biblioteca de plataforma registrará. Se você ver um 402, seu saldo está vazio ou o teste acabou; recarregue o crédito pré-pago.
O adaptador Discord
Bots do Discord precisam que a intenção de conteúdo de mensagem seja ativada no portal do desenvolvedor e também no código, ou msg.content chega vazio. O bot responde apenas quando mencionado, o que evita que ele leia todas as mensagens em um servidor ocupado.
# discord_bot.py
import os
import time
from collections import defaultdict, deque
import discord
from llm import chat
intents = discord.Intents.default()
intents.message_content = True # also enable it in the developer portal
bot = discord.Client(intents=intents)
HISTORY_TURNS = 10 # user+assistant messages kept per channel
history = defaultdict(lambda: deque(maxlen=HISTORY_TURNS))
last_use = {} # user id -> last request time
COOLDOWN = 5.0 # seconds between requests per user
def chunks(text, size=1900):
return [text[i:i + size] for i in range(0, len(text), size)] or [""]
@bot.event
async def on_message(msg: discord.Message):
if msg.author.bot or bot.user not in msg.mentions:
return
# Age gate: guild text channels must be flagged NSFW. No DMs.
if not isinstance(msg.channel, discord.TextChannel) or not msg.channel.is_nsfw():
await msg.reply("I only chat in channels marked age-restricted (NSFW).")
return
now = time.monotonic()
if now - last_use.get(msg.author.id, 0) < COOLDOWN:
await msg.add_reaction("\u23f3")
return
last_use[msg.author.id] = now
text = msg.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not text:
return
h = history[msg.channel.id]
h.append({"role": "user", "content": f"{msg.author.display_name}: {text}"})
async with msg.channel.typing():
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for part in chunks(answer):
await msg.channel.send(part)
bot.run(os.environ["DISCORD_TOKEN"])O histórico é um deque com maxlen=10 indexado por id do canal, então cada canal é sua própria conversa e as mensagens antigas saem automaticamente. Prefixar cada linha do usuário com o nome de exibição do locutor permite que o modelo distinga as pessoas em uma sala compartilhada. Dez mensagens são um padrão conservador; a janela de contexto tem 100.000 tokens, então você pode aumentar isso muito antes que faça diferença.
O adaptador Telegram
python-telegram-bot v20 e posteriores é assíncrono, o que combina naturalmente com o núcleo compartilhado. chat_data é um dicionário por conversa que a biblioteca mantém para você, então histórico e estado de resfriamento não precisam de código de armazenamento extra.
# telegram_bot.py (python-telegram-bot v20+)
import os
import time
from collections import deque
from telegram import Update
from telegram.ext import (ApplicationBuilder, CommandHandler, ContextTypes,
MessageHandler, filters)
from llm import chat
async def adult(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.chat_data["adult"] = True
await update.message.reply_text("Noted. Say something.")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"This bot produces adult fiction. Send /adult only if you are 18 or older."
)
async def talk(update: Update, context: ContextTypes.DEFAULT_TYPE):
cd = context.chat_data
if not cd.get("adult"):
await update.message.reply_text("Send /adult to confirm you are 18+.")
return
if time.monotonic() - cd.get("last", 0) < 4:
return # per-chat cooldown
cd["last"] = time.monotonic()
h = cd.setdefault("history", deque(maxlen=10))
h.append({"role": "user", "content": update.message.text})
await context.bot.send_chat_action(update.effective_chat.id, "typing")
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for i in range(0, len(answer), 4000): # Telegram limit is 4096 chars
await update.message.reply_text(answer[i:i + 4000])
app = ApplicationBuilder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("adult", adult))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, talk))
app.run_polling()run_polling() é a maneira mais simples de iniciar e não precisa de URL público. Lembre-se de que chat_data vive na memória, então uma reinicialização o limpa. Se precisar que o histórico sobreviva a reinicializações, armazene os conteúdos do deque você mesmo em um arquivo ou banco de dados.
Restrição de idade e regra do canal NSFW
Esta API é apenas para adultos, e qualquer coisa que você construir nela herda essa responsabilidade. Trate a restrição de idade como um recurso, não como um pensamento tardio.
- Discord: responda apenas em canais de guilda marcados como restritos por idade. A verificação
channel.is_nsfw()não custa nada e o bot se recusa em outros lugares, incluindo mensagens diretas. - Telegram: exija uma confirmação explícita
/adultpor chat antes de qualquer geração e indique a regra no texto de boas-vindas. - Ambos: não ofereça o bot em espaços voltados para jovens e remova o acesso prontamente se souber que um usuário tem menos de 18 anos.
Um comando de confirmação é uma porta, não prova de idade. Conteúdo sexual envolvendo menores é sempre bloqueado pela API com um 403, incluindo ficção e roleplay, e seu bot nunca deve tentar contornar essa resposta.
Executando e testando sem multidão
Inicie cada bot em seu próprio terminal com python discord_bot.py ou python telegram_bot.py. Para a primeira execução, crie um servidor de teste privado ou um chat apenas com você. Percorra uma lista de verificação curta e você pegará quase todos os bugs de iniciante.
- Mencione o bot em um canal não NSFW e confirme que ele se recusa. Isso prova que a barreira funciona.
- Mencione-o em um canal sinalizado e envie duas mensagens rapidamente. A segunda deve gerar a reação de ampulheta, o que prova que o atraso funciona.
- Mantenha uma conversa de quatro mensagens e, em seguida, refira-se à primeira mensagem. Se o bot lembrar, o histórico está conectado corretamente.
- Defina temporariamente a chave de API para um valor errado e confirme que a falha aparece nos seus logs em vez de desaparecer silenciosamente.
- Cole um bloco longo de texto e veja a resposta dividida em várias mensagens dentro do limite de comprimento da plataforma.
Se as respostas parecerem genéricas, a persona geralmente é o problema, não o código. Altere uma frase de PERSONA, reinicie e compare. Como a persona vive no módulo compartilhado, uma edição altera ambos os bots de uma vez, o que é exatamente o motivo pelo qual a estrutura vale o arquivo extra.
Quando estiver pronto para implantar, qualquer gerenciador de processos sempre ativo serve. Os bots mantêm uma conexão de longa duração com sua plataforma e fazem chamadas HTTPS de saída curtas para a API, então não precisam de portas de entrada e quase nenhuma memória. Adicione uma política de reinicialização para que uma falha não deixe sua comunidade conversando com um bot silencioso.
Limites de requisições, tamanho do histórico e custo
Três controles ajustam como o bot se comporta sob carga. O tempo de espera por usuário impede que uma pessoa monopolize a chave. A janela global protege o teto de 300 requisições por minuto. O semáforo suaviza picos. Se você executar vários processos de bot em uma chave, eles compartilham esse teto, então reduza o limit de cada processo de acordo.
Estime o gasto antes de convidar uma multidão. Suposições: cada chamada envia cerca de 1.200 tokens de entrada (persona mais dez voltas) e retorna 250 tokens. Uma chamada custa 1.200 x $0,25 / 1.000.000 + 250 x $1,00 / 1.000.000 = $0,0003 + $0,00025 = $0,00055. Um servidor produzindo 2.000 respostas por dia gastaria cerca de $1,10 por dia sob essas suposições. Seus números serão diferentes, então registre o campo usage por uma semana e recalcule.
Para ajustar a própria persona, leia o guia de prompts. Os preços estão listados na página de preços.
Endurecimento antes de compartilhar o bot
Mantenha os tokens fora do controle de versão; leia-os do ambiente conforme mostrado. Limite o comprimento da mensagem antes de enviá-la à API para que uma colagem enorme não consuma seu saldo. Registre erros, mas não o texto das mensagens, já que seus usuários razoavelmente esperam que seus chats permaneçam privados. Adicione um comando /reset que limpa o deque de um canal, que é o ajuste mais rápido quando uma conversa sai do trilho. Finalmente, coloque uma nota visível no perfil do bot dizendo que ele escreve ficção adulta e que suas respostas são geradas.
Mais uma escolha de design merece uma frase: os bots aguardam a resposta completa em vez de fazer streaming dela. Editar uma mensagem de chat token por token esbarra diretamente nos limites de edição da plataforma e, para respostas limitadas a cerca de 150 palavras, a espera é curta. Se você quiser digitação ao vivo mais tarde em sua própria interface web, ative stream: true lá, onde você controla o transporte. Até lá, o indicador de digitação que você já envia é a maneira mais barata de mostrar que o bot está funcionando e mantém o código curto o suficiente para ser lido de uma vez e auditado antes de convidar alguém.