Ne inşa ediyoruz
İki küçük bot, tek bir ortak beyin. llm.py adlı bir dosya API'ye dokunan her şeyi yönetir: kişilik, hız limiti ve yeniden deneme mantığı. Bir Discord botu ve bir Telegram botu yalnızca platform olaylarını bir mesaj listesine ve geriye çevirir. Daha sonra üçüncü bir platform eklerseniz yalnızca dönüştürücüyü yazmanız gerekir.
Bağımlılıkları pip install httpx discord.py python-telegram-bot ile yükleyin. API_KEY değerini hesap sayfanızdan, DISCORD_TOKEN ve/veya TELEGRAM_TOKEN değerlerini ise her platformun kendi bot yapılandırmasından dışa aktarın. Aşağıdaki her iki bot da Python 3.10 veya daha yeni bir sürüm gerektirir.
İstek formatı, çok dilli hızlı başlangıç'ta kapsanan düz OpenAI chat-completions şeklindedir. Burada bir SDK'ya ihtiyacınız yoktur.
Paylaşılan çekirdek: kişilik, hız kesici ve yeniden denemeler
Tehlikeli kısmı tek bir yere koyun. Aşağıdaki modül, istek zaman damgaları için kayan bir pencere tutar ve dakikada 240 çağrıya yaklaştığında bekler; bu, ana başına dakikada 300 çağrıya izin verilen sınıra yönelik bilinçli bir marjinaltır. Bir yarıkapı (semaphore), eşzamanlılığı dört ile sınırlar böylece yoğun bir kanal aynı anda yirmi istek tetikleyemez.
# 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."Durum işleme mekanizmasına bakın. Bir 429 veya 503 durumu 1, 2 ve ardından 4 saniye uyur ve tekrar dener. Bir 403 durumu içerik filtresinin tetiklendiğini gösterir, bu nedenle bot yeniden denemek yerine nötr bir satırla yanıt verir. Diğer tüm hatalar yükseltilir ve platform kitaplığınız bunları günlüğe kaydeder. Bir 402 görürseniz bakiyeniz boş veya deneme süresi dolmuştur; ön ödemeli bakiyeyi yükleyin.
Discord adaptörü
Discord botlarının, geliştirici portalında ve kodda mesaj-içeriği niyetinin açık olması gerekir; aksi takdirde msg.content boş gelir. Bot yalnızca bahsedildiğinde yanıt verir; bu da onu yoğun bir sunucudaki her mesajı okumaktan alıkoyar.
# 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"])Geçmiş, kanal kimliğine göre anahtarlanmış deque nesnesidir ve maxlen=10 ile ayarlanmıştır; bu nedenle her kanal kendi konuşmasını oluşturur ve eski dönüşler otomatik olarak kaybolur. Her kullanıcı satırının başına konuşanın görüntüleme adını eklemek, modelin paylaşılan bir odada insanları birbirinden ayırmasını sağlar. On mesaj muhafazakâr bir varsayılandır; bağlam penceresi 100.000 token kapasitesine sahiptir, bu nedenle önemli hale gelmeden önce bu değeri oldukça artırabilirsiniz.
Telegram adaptörü
python-telegram-bot v20 ve sonrası asenkroniktir ve bu durum ortak çekirdek ile doğal olarak uyumludur. chat_data, kitaplığın sizin için tuttuğu sohbet başına bir sözlüktür; bu nedenle geçmiş ve bekleme süresi durumu için ek depolama koduna gerek yoktur.
# 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() başlatmanın en basit yoludur ve genel bir URL gerektirmez. chat_data hafızada yaşar, bu nedenle bir yeniden başlatma onu temizler. Geçmişin yeniden başlatmalardan sağ çıkmasını istiyorsanız, deque içeriğini kendiniz bir dosyaya veya veritabanına kaydedin.
Yaş kısıtlaması ve NSFW kanal kuralı
Bu API yalnızca yetişkinler içindir ve üzerine inşa ettiğiniz her şey bu sorumluluğu miras alır. Yaş doğrulamasını bir sonradan düşünce olarak değil, bir özellik olarak ele alın.
- Discord: yalnızca yaş kısıtlaması işaretlenmiş sunucu kanallarında yanıt verin.
channel.is_nsfw()kontrolü ücretsizdir ve bot başka yerlerde, doğrudan mesajlar dahil reddeder. - Telegram: her oluşturmadan önce her sohbet için açık bir
/adultonayı gerektirir ve kuralı karşılama metninde belirtin. - Her ikisi: gençlere yönelik alanlarda botu sunmayın ve bir kullanıcının 18 yaşından küçük olduğunu öğrenir öğrenmez erişimi kaldırın.
Bir onay komutu bir kapıdır, yaş kanıtı değildir. Küçüklerle ilgili cinsel içerik, kurgu ve rol yapma dahil olmak üzere API tarafından her zaman 403 ile engellenir; botunuz bu yanıtı aşmaya çalışmamalıdır.
Çalıştırma ve kalabalık olmadan test etme
Her botu kendi terminalinde python discord_bot.py veya python telegram_bot.py ile başlatın. İlk çalıştırma için yalnızca kendinizi içeren özel bir test sunucusu veya sohbet oluşturun. Kısa bir kontrol listesinden geçerek neredeyse tüm başlangıç hatalarını yakalayacaksınız.
- Botu NSFW olmayan bir kanalda bahsederek onu reddetmesini onaylayın. Bu, kapının çalıştığını kanıtlar.
- İşaretli bir kanalda bahsedip hızlıca iki mesaj gönderin. İkincisi kum saati tepkisini almalıdır; bu, bekleme süresinin çalıştığını kanıtlar.
- Dört mesajlık bir konuşma tutun, ardından ilk mesaja başvurun. Bot hatırlıyorsa geçmiş doğru şekilde bağlanmıştır.
- API anahtarını geçici olarak yanlış bir değere ayarlayın ve hatanın sessizce kaybolmak yerine günlüklerinizde göründüğünü onaylayın.
- Uzun bir metin bloğu yapıştırın ve yanıtın platformun uzunluk sınırının altında birkaç mesaja bölündüğünü izleyin.
Yanıtlar genel hissediyorsa, sorun genellikle kişiliktir, kod değil. PERSONA'nin bir cümlesini değiştirin, yeniden başlatın ve karşılaştırın. Kişilik ortak modülde yaşadığından, tek bir düzenleme her iki botu da aynı anda değiştirir; bu da yapının ek dosya değerine sahip olmasının nedenidir.
Dağıtım için hazır olduğunuzda, her zaman açık bir işlem yöneticisi yeterlidir. Botlar platformlarıyla uzun süreli bir bağlantı tutar ve API'ye kısa çıkış HTTPS çağrıları yapar; bu nedenle giriş portlarına ve neredeyse hiç belleğe ihtiyaçları yoktur. Bir çökme topluluğunuzu sessiz bir botla konuşmaya bırakmasın diye bir yeniden başlatma politikası ekleyin.
Hız limitleri, geçiş boyutu ve maliyet
Botun yük altındaki davranışını üç düğme kontrol eder. Kullanıcı başına bekleme süresi, bir kişinin anahtarı tek başına kullanmasını engeller. Küresel pencere, dakikada 300 istek tavanını korur. Yarıkapı (semaphore) dalgalanmaları yumuşatır. Tek bir anahtar üzerinde birkaç bot işlemi çalıştırıyorsanız, bu tavanı paylaşırlar; bu nedenle her işlemin limit değerini buna göre düşürün.
Kalabalığı davet etmeden önce harcamayı tahmin edin. Varsayımlar: Her çağrı yaklaşık 1.200 girdi tokeni (kişilik ve on dönüş) gönderir ve 250 token döndürür. Bir çağrının maliyeti 1.200 x $0,25 / 1.000.000 + 250 x $1,00 / 1.000.000 = $0,0003 + $0,00025 = $0,00055'tir. Günde 2.000 yanıt üreten bir sunucu, bu varsayımlar altında günde yaklaşık $1,10 harcar. Sayılarınız farklı olacaktır, bu nedenle usage alanını bir hafta boyunca günlüğe kaydedin ve yeniden hesaplayın.
Kişiliği kendiniz ayarlamak için istemleme kılavuzuna bakın. Fiyatlar fiyatlandırma sayfasında listelenmiştir.
Botu paylaşmadan önce sağlamlaştırma
Tokenları kaynak kontrolünde tutmayın; bunları ortamdan aşağıda gösterildiği gibi okuyun. API'ye göndermeden önce mesaj uzunluğunu sınırlandırın, böylece tek bir büyük yapıştırma işlemi bakiyenizi tüketemez. Hataları günlüğe kaydedin ancak mesaj metnini değil, çünkü kullanıcılarınız sohbetlerinin gizli kalmasını makul olarak bekler. Bir kanalın kuyruğunu temizleyen /reset komutunu ekleyin; bu, bir konuşma saptığında en hızlı çözümdür. Son olarak, botun profilinde yetişkin kurgu yazdığını ve yanıtlarının oluşturulduğunu belirten görünür bir not ekleyin.
Bir tasarım seçeneği daha bir cümleye değer: botlar yanıtı akış (streaming) yerine tamamlanmasını bekler. Bir sohbet mesajını token token düzenlemek platform düzenleme limitleriyle doğrudan çarpışır ve 150 kelimeye yakın sınırlandırılmış yanıtlar için bekleme süresi kısadır. Daha sonra kendi web ön ucunuzda canlı yazma özelliğini isterseniz, taşıma işlemini kontrol ettiğiniz yerde stream: true'i etkinleştirin. Buna kadar, zaten gönderdiğiniz yazma göstergesi botun çalıştığını göstermenin en ucuz yoludur ve kodu, bir oturuşta okunabilecek ve herhangi birini davet etmeden önce denetlenebilecek kadar kısa tutar.