forked from zovos/bot_tg
Compare commits
No commits in common. "79e581a0103e39d2bcd40c2cdb2c58a61f6e9df3" and "07f6efe7b1e5c25e977694058c4014522a390e6d" have entirely different histories.
79e581a010
...
07f6efe7b1
3 changed files with 18 additions and 183 deletions
|
|
@ -1,8 +1,6 @@
|
|||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
from aiogram.types import Message
|
||||
|
|
@ -14,120 +12,27 @@ LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-a
|
|||
# ПЖ НЕ ДЕЛАЙТЕ ДУДОС
|
||||
# Системный промпт задаёт стиль и "характер" ответа.
|
||||
SYSTEM_PROMPT = (
|
||||
"Ты — дерзкий, мемный, гиперэмоциональный пацан с района. Базаришь строго на фене — "
|
||||
"дворовый язык, блатной жаргон, тюремный сленг, с интернет-угаром. "
|
||||
"Тон: коротко, резко, уверенно, иногда абсурдно, но по делу. "
|
||||
"Стиль: 1–3 предложения; если список — максимум 3 пункта. "
|
||||
"Не используй формальный язык, канцелярит, вежливые обороты. Не ломай персонажа.\n\n"
|
||||
|
||||
"Лексика (мешай, но не лепи всё подряд одним полотном): "
|
||||
"кент, братуха, мусора, шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
|
||||
"чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта, "
|
||||
"жёстко, имба, разъёб, кринж, лютый, шиза, на приколе, залёт, вкидываю, по красоте, без шансов.\n\n"
|
||||
|
||||
"Фирменные приёмы речи:\n"
|
||||
"— иногда начинай с: 'Слушай сюда', 'Короче', 'Смотри', 'Ща поясню', 'Не гони'.\n"
|
||||
"— вставляй междометия: 'ёпта', 'ну типа', 'в натуре', 'по-любому'.\n"
|
||||
"— иногда самоирония: 'я ща как эксперт, ёпта', 'не душни'.\n"
|
||||
"— иногда делай 'слоговой разгон' — разбивай слова на куски через дефисы, "
|
||||
"как будто проговариваешь по слогам. Примеры: "
|
||||
"над-ра-чи-ва-ем, сд-ра-чи-ва-ем, ско-ся-чи-ли, у-лы-боч-ку, тэкс-тур-ку, ин-тел-ли-ген.\n"
|
||||
"— держи 'словарь коронок': выбирай 0–2 фразы на ответ (редко 3), "
|
||||
"иногда делай повтор как тик (1–3 раза), но не превращай всё в простыню.\n"
|
||||
" СЛОВАРЬ_КОРОНОК = {\n"
|
||||
" 'старт/переход': ['Так', 'Короче', 'Смотри', 'Так, давайте заценим… заценим текстурку'],\n"
|
||||
" 'процесс/угар': ['Надрачиваем палочку', 'Сдрачиваем её', 'Так, надрочили'],\n"
|
||||
" 'ошибка/фейл': ['Эх, блядь', 'Ой, скосячили они его, смотрите', 'Ой, скосячили', 'Ой, блядь, скосячили', 'Да ладно'],\n"
|
||||
" 'оценка/обсирание': ['Ну, текстурка тут, конечно, полный мрак вообще', 'То есть вообще никакая — дерьмо полное', 'Пипец!', 'Реально, посмотрите', 'Вот это говнище, блин', 'Текстурка говенцой'],\n"
|
||||
" 'восклицания': ['Здорово пизда у коровы!', 'Устали', 'Улыбочку!'],\n"
|
||||
" 'интеллигент-угар': ['Такой для ебать интеллиген'],\n"
|
||||
" 'мат-тик': ['Сука, сука, сука…']\n"
|
||||
" }\n"
|
||||
" Правила: 'Улыбочку!' можно 2–3 раза подряд; 'Ой, скосячили' можно повторить 1–3 раза; "
|
||||
" 'Сука, сука, сука…' — максимум один раз за ответ.\n\n"
|
||||
|
||||
"Твои убеждения (всегда держи линию): "
|
||||
"Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
|
||||
"Ты — чёткий пацан с района. Базаришь строго на фене — блатной жаргон, "
|
||||
"тюремный сленг, дворовый язык. Используй слова: кент, братуха, мусора, "
|
||||
"шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
|
||||
"чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта. "
|
||||
"Отвечай коротко и дерзко. Не ломай персонажа. Не используй формальный язык. "
|
||||
"Твои убеждения: Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
|
||||
"Линукс — лучшая ОС во вселенной, остальное для фраеров. "
|
||||
"Вселенную надо переписать на Rust, это не обсуждается. "
|
||||
"Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo.\n\n"
|
||||
|
||||
"Песенный вайб (короткая отсылка): иногда напевай очень коротко: «Сегодня праздник у девчат», "
|
||||
"а дальше пересказывай своими словами, без длинных дословных цитат."
|
||||
"Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo."
|
||||
)
|
||||
|
||||
# Размер контекста — количество сообщений на чат
|
||||
_CONTEXT_SIZE = 100
|
||||
# Путь к SQLite базе (берётся из переменной окружения или из config)
|
||||
_DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH", "/db/chat_history.sqlite3")
|
||||
|
||||
|
||||
def _init_db() -> None:
|
||||
"""Создаёт таблицу истории чата, если её нет."""
|
||||
Path(_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS chat_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
text TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id ON chat_history(chat_id, id)")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def push_message(chat_id: int, role: str, name: str, text: str) -> None:
|
||||
"""Записывает сообщение в историю чата и обрезает до _CONTEXT_SIZE."""
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO chat_history (chat_id, role, name, text) VALUES (?, ?, ?, ?)",
|
||||
(chat_id, role, name, text),
|
||||
)
|
||||
# Удаляем старые записи, оставляем только последние _CONTEXT_SIZE
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM chat_history
|
||||
WHERE chat_id = ? AND id NOT IN (
|
||||
SELECT id FROM chat_history WHERE chat_id = ? ORDER BY id DESC LIMIT ?
|
||||
)
|
||||
""",
|
||||
(chat_id, chat_id, _CONTEXT_SIZE),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _get_history(chat_id: int) -> list[dict]:
|
||||
"""Возвращает историю чата как список словарей."""
|
||||
with sqlite3.connect(_DB_PATH) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT role, name, text FROM chat_history WHERE chat_id = ? ORDER BY id ASC",
|
||||
(chat_id,),
|
||||
).fetchall()
|
||||
return [{"role": r[0], "name": r[1], "text": r[2]} for r in rows]
|
||||
|
||||
|
||||
def _build_messages(chat_id: int, system_prompt: str, current_text: str, current_name: str) -> list:
|
||||
"""Собирает список messages для LLM из истории чата."""
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
for entry in _get_history(chat_id):
|
||||
content = f"{entry['name']}: {entry['text']}" if entry["role"] == "user" else entry["text"]
|
||||
messages.append({"role": entry["role"], "content": content})
|
||||
messages.append({"role": "user", "content": f"{current_name}: {current_text}"})
|
||||
return messages
|
||||
|
||||
|
||||
async def _generate_response(chat_id: int, user_text: str, user_name: str) -> str:
|
||||
# Формируем запрос к /v1/chat/completions с историей чата и возвращаем текст первого ответа.
|
||||
async def _generate_response(user_text: str) -> str:
|
||||
# Формируем запрос к /v1/chat/completions и возвращаем текст первого ответа.
|
||||
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
|
||||
msgs = await asyncio.to_thread(_build_messages, chat_id, SYSTEM_PROMPT, user_text, user_name)
|
||||
payload = {
|
||||
"messages": msgs,
|
||||
"max_tokens": 500,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_text},
|
||||
],
|
||||
"max_tokens": 256,
|
||||
"temperature": 0.8,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
|
|
@ -145,49 +50,16 @@ async def handle_talk(message: Message):
|
|||
return
|
||||
|
||||
user_text = parts[1].strip()
|
||||
user_name = message.from_user.first_name or "кент"
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Показываем "печатает..." пока ждём ответ модели.
|
||||
await message.bot.send_chat_action(chat_id=chat_id, action="typing")
|
||||
await message.bot.send_chat_action(chat_id=message.chat.id, action="typing")
|
||||
|
||||
try:
|
||||
response = await _generate_response(chat_id, user_text, user_name)
|
||||
response = await _generate_response(user_text)
|
||||
if not response:
|
||||
response = "Братуха, чёт базар не клеится, попробуй ещё раз."
|
||||
# Пишем запрос и ответ в историю
|
||||
await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text)
|
||||
await asyncio.to_thread(push_message, chat_id, "assistant", "бот", response)
|
||||
await message.reply(response, parse_mode=None)
|
||||
except Exception as exc:
|
||||
# При любых ошибках не падаем, а отдаём понятный fallback.
|
||||
logger.exception("Talk generation failed")
|
||||
await message.reply("Бля, кент, чёт движок заглох. Попробуй позже.")
|
||||
|
||||
|
||||
AUTOREPLY_SYSTEM_PROMPT = (
|
||||
"Ты — дерзкий пацан с района, который сидит в групповом чате. "
|
||||
"Ты НЕ отвечаешь на вопрос — ты сам влез в разговор. "
|
||||
"Прокомментируй сообщение коротко (1-2 предложения), дерзко, на фене. "
|
||||
"Можешь пошутить, подколоть, согласиться или послать. "
|
||||
"НЕ начинай с обращения. Не используй формальный язык."
|
||||
)
|
||||
|
||||
|
||||
async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
|
||||
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
|
||||
msgs = await asyncio.to_thread(_build_messages, chat_id, AUTOREPLY_SYSTEM_PROMPT, text, user_name)
|
||||
payload = {
|
||||
"messages": msgs,
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.9,
|
||||
"top_p": 0.9,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
|
||||
data = await resp.json()
|
||||
return data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
|
||||
# Инициализация БД при импорте модуля
|
||||
_init_db()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ DEFAULT_TZ = os.getenv("TZ", "Europe/Moscow")
|
|||
|
||||
# --- Настройки /penis ---
|
||||
PENIS_DB_PATH = "/db/penis_stats.sqlite3"
|
||||
CHAT_HISTORY_DB_PATH = "/db/chat_history.sqlite3"
|
||||
PENIS_START_LENGTH = 10.0
|
||||
PENIS_MIN_DELTA = 0.1
|
||||
PENIS_MAX_DELTA = 7.0
|
||||
|
|
@ -77,11 +76,6 @@ SCHEDULE = {
|
|||
# --- Остальное ---
|
||||
KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'}
|
||||
|
||||
# --- Авто-ответы ИИ ---
|
||||
AUTOREPLY_CHANCE = 0.10
|
||||
AUTOREPLY_COOLDOWN = 120
|
||||
AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'пиздато', 'пиздато', 'пиздато', 'ваномас'}
|
||||
|
||||
def get_hint_text(templates_str: str) -> str:
|
||||
return (
|
||||
"Команды:\n"
|
||||
|
|
|
|||
35
main.py
35
main.py
|
|
@ -18,7 +18,7 @@ from aiogram.types import (
|
|||
from aiogram.client.default import DefaultBotProperties
|
||||
|
||||
# Локальные импорты
|
||||
from AI.talk_handler import handle_talk, generate_autoreply, push_message
|
||||
from AI.talk_handler import handle_talk
|
||||
from zparser import get_military_data
|
||||
import config
|
||||
|
||||
|
|
@ -562,47 +562,16 @@ async def handle_gen_mem(message: Message, bot: Bot):
|
|||
return
|
||||
await message.reply_photo(BufferedInputFile(output.getvalue(), filename="meme.jpg"))
|
||||
|
||||
_last_autoreply_ts = 0
|
||||
|
||||
async def handle_keywords(message: Message):
|
||||
global _last_autoreply_ts
|
||||
if not message.text or message.text.startswith('/'):
|
||||
return
|
||||
text_lower = message.text.lower()
|
||||
chat_id = message.chat.id
|
||||
user_name = (message.from_user.first_name or "кент") if message.from_user else "кент"
|
||||
|
||||
if any(re.search(rf'\b{word}\b', text_lower) for word in config.KEYWORDS):
|
||||
if any(re.search(rf'\b{word}\b', message.text.lower()) for word in config.KEYWORDS):
|
||||
audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3"
|
||||
if audio_path.exists():
|
||||
await message.answer_audio(
|
||||
audio=FSInputFile(audio_path),
|
||||
caption="I'm a creep, I'm a weirdo...",
|
||||
)
|
||||
return
|
||||
|
||||
# Записываем каждое обычное сообщение в историю чата
|
||||
await asyncio.to_thread(push_message, chat_id, "user", user_name, message.text)
|
||||
|
||||
import time as _time
|
||||
now = _time.time()
|
||||
triggered = any(trigger in text_lower for trigger in config.AUTOREPLY_TRIGGERS)
|
||||
|
||||
should_reply = triggered or (
|
||||
random.random() < config.AUTOREPLY_CHANCE
|
||||
and (now - _last_autoreply_ts) > config.AUTOREPLY_COOLDOWN
|
||||
)
|
||||
|
||||
if should_reply:
|
||||
try:
|
||||
await message.bot.send_chat_action(chat_id=chat_id, action="typing")
|
||||
response = await generate_autoreply(chat_id, message.text, user_name)
|
||||
if response:
|
||||
await asyncio.to_thread(push_message, chat_id, "assistant", "бот", response)
|
||||
await message.reply(response, parse_mode=None)
|
||||
_last_autoreply_ts = now
|
||||
except Exception:
|
||||
logger.exception("Autoreply failed")
|
||||
|
||||
# --- Запуск ---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue