From 9b34fd13d5e9227c179eeafbba17caae543dd93d Mon Sep 17 00:00:00 2001 From: Danil Date: Fri, 27 Feb 2026 12:29:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AI/talk_handler.py | 97 +++++++++++++++++++++++++++++++++++++++------- config.py | 3 +- main.py | 12 ++++-- 3 files changed, 95 insertions(+), 17 deletions(-) diff --git a/AI/talk_handler.py b/AI/talk_handler.py index dc5bfa7..7355db7 100644 --- a/AI/talk_handler.py +++ b/AI/talk_handler.py @@ -1,6 +1,8 @@ import asyncio import os import logging +import sqlite3 +from pathlib import Path import aiohttp from aiogram.types import Message @@ -54,14 +56,77 @@ SYSTEM_PROMPT = ( "а дальше пересказывай своими словами, без длинных дословных цитат." ) -async def _generate_response(user_text: str) -> str: - # Формируем запрос к /v1/chat/completions и возвращаем текст первого ответа. +# Размер контекста — количество сообщений на чат +_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 с историей чата и возвращаем текст первого ответа. 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": [ - {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": user_text}, - ], + "messages": msgs, "max_tokens": 500, "temperature": 0.8, "top_p": 0.9, @@ -80,14 +145,19 @@ 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=message.chat.id, action="typing") + await message.bot.send_chat_action(chat_id=chat_id, action="typing") try: - response = await _generate_response(user_text) + response = await _generate_response(chat_id, user_text, user_name) 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. @@ -104,13 +174,11 @@ AUTOREPLY_SYSTEM_PROMPT = ( ) -async def generate_autoreply(text: str) -> str: +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": [ - {"role": "system", "content": AUTOREPLY_SYSTEM_PROMPT}, - {"role": "user", "content": text}, - ], + "messages": msgs, "max_tokens": 150, "temperature": 0.9, "top_p": 0.9, @@ -120,3 +188,6 @@ async def generate_autoreply(text: str) -> str: data = await resp.json() return data["choices"][0]["message"]["content"].strip() + +# Инициализация БД при импорте модуля +_init_db() diff --git a/config.py b/config.py index ae8f7b4..545b5a4 100644 --- a/config.py +++ b/config.py @@ -10,6 +10,7 @@ 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 @@ -79,7 +80,7 @@ KEYWORDS = {'крип', 'радиохэд', 'creep', 'radiohead'} # --- Авто-ответы ИИ --- AUTOREPLY_CHANCE = 0.10 AUTOREPLY_COOLDOWN = 120 -AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар'} +AUTOREPLY_TRIGGERS = {'бот', 'фраер', 'кент', 'братуха', 'пацаны', 'чё как', 'базар', 'ебать', 'пизда', 'хуй', 'бля', 'сука', 'говно', 'пидор', 'пиздабол', 'пиздатый', 'пиздатая', 'пиздатое', 'пиздатые', 'пиздато', 'пиздато', 'пиздато', 'пиздато', 'ваномас'} def get_hint_text(templates_str: str) -> str: return ( diff --git a/main.py b/main.py index fd67740..4364fa7 100644 --- a/main.py +++ b/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 +from AI.talk_handler import handle_talk, generate_autoreply, push_message from zparser import get_military_data import config @@ -569,6 +569,8 @@ async def handle_keywords(message: Message): 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): audio_path = config.BASE_DIR / "maket" / "Radiohead - Creep.mp3" @@ -579,6 +581,9 @@ async def handle_keywords(message: Message): ) 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) @@ -590,9 +595,10 @@ async def handle_keywords(message: Message): if should_reply: try: - await message.bot.send_chat_action(chat_id=message.chat.id, action="typing") - response = await generate_autoreply(message.text) + 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: -- 2.45.2