From 997bc39a2240028111a840910b7c0b6a20873f5c Mon Sep 17 00:00:00 2001 From: Kirill Kirilenko Date: Sat, 21 Feb 2026 00:50:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B.=20=D0=9C=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B6=D0=B5=D1=81=D1=82=D0=BA=D0=BE=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D0=B0=20=D0=98=D0=98=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- ai_agent.py | 108 ++++++++++++----------------------- prompts/group_chat.txt | 6 ++ prompts/image_generation.txt | 17 ++++++ prompts/private_chat.txt | 5 ++ prompts/tools.json | 24 ++++++++ tg/__main__.py | 3 +- vk/__main__.py | 3 +- 8 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 prompts/group_chat.txt create mode 100644 prompts/image_generation.txt create mode 100644 prompts/private_chat.txt create mode 100644 prompts/tools.json diff --git a/.gitignore b/.gitignore index 48e74b6..382f6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .venv __pycache__ *.db -*.json +/*.json diff --git a/ai_agent.py b/ai_agent.py index 62d1ba7..b586e9f 100644 --- a/ai_agent.py +++ b/ai_agent.py @@ -12,7 +12,7 @@ from typing import List, Tuple, Any, Optional, Union, Dict, Awaitable from openrouter import OpenRouter, RetryConfig from openrouter.components import AssistantMessage, AssistantMessageTypedDict, ChatMessageContentItemTypedDict, \ - ChatMessageToolCall, MessageTypedDict, ToolDefinitionJSONTypedDict + ChatMessageToolCall, MessageTypedDict from openrouter.errors import ResponseValidationError, ChatError from openrouter.utils import BackoffStrategy @@ -20,38 +20,6 @@ from fal_client import AsyncClient as FalClient from database import BasicDatabase -GROUP_CHAT_SYSTEM_PROMPT = """ -Ты - ИИ-помощник в групповом чате.\n -Отвечай на вопросы и поддерживай контекст беседы.\n -Ты не можешь обсуждать политику и религию.\n -Сообщения пользователей будут приходить в следующем формате: '[дата время, имя]: текст сообщения'\n -При ответе НЕ нужно указывать ни время, ни пользователя, которому предназначен ответ, ни свое имя.\n -НЕ используй разметку Markdown, она не поддерживается мессенджером.\n -Если тебя просят нарисовать изображение, используй соответствующий вызов функции. -Запрещено генерировать ASCII-арты. Запрещено использовать тег ! -""" - -PRIVATE_CHAT_SYSTEM_PROMPT = """ -Ты - ИИ-помощник в чате c пользователем.\n -Отвечай на вопросы и поддерживай контекст беседы.\n -Сообщения пользователя будут приходить в следующем формате: '[дата время]: текст сообщения'\n -При ответе НЕ нужно указывать время.\n -НЕ используй разметку Markdown, она не поддерживается мессенджером.\n -Если тебя просят нарисовать изображение, используй соответствующий вызов функции. -Запрещено генерировать ASCII-арты. Запрещено использовать тег ! -""" - -GENERATE_IMAGE_TOOL_DESCRIPTION = """ -Генерация изображения по описанию. -Используй этот инструмент, если пользователь просит сгенерировать изображение ('нарисуй', 'покажи' и т.п.), -или если это улучшит ответ (например, в ролевой игре для визуализации сцены). -""" - -GENERATE_IMAGE_TOOL_PROMPT_ARG_DESCRIPTION = """ -Подробное описание сцены на английском языке. -Добавь детали для стиля, цвета, композиции, если нужно. -""" - OPENROUTER_X_TITLE = "TG/VK Chat Bot" OPENROUTER_HTTP_REFERER = "https://ultracoder.org" @@ -109,35 +77,10 @@ def _serialize_assistant_message(message: AssistantMessage) -> AssistantMessageT return _remove_none_recursive(message.model_dump(by_alias=True)) -def _get_tools_description() -> List[ToolDefinitionJSONTypedDict]: - return [{ - "type": "function", - "function": { - "name": "generate_image", - "description": GENERATE_IMAGE_TOOL_DESCRIPTION, - "parameters": { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": GENERATE_IMAGE_TOOL_PROMPT_ARG_DESCRIPTION - }, - "aspect_ratio": { - "type": "string", - "enum": ["1:1", "4:3", "3:4", "16:9", "9:16"], - "description": "Соотношение сторон (опционально)." - } - }, - "required": ["prompt"] - } - } - }] - - class AiAgent: def __init__(self, openrouter_token: str, openrouter_model: str, - fal_token: str, fal_model: str, + fal_token: str, db: BasicDatabase, platform: str): retry_config = RetryConfig(strategy="backoff", @@ -145,9 +88,12 @@ class AiAgent: initial_interval=2000, max_interval=8000, exponent=2, max_elapsed_time=14000), retry_connection_errors=True) self.db = db - self.model_main = openrouter_model - self.model_image = fal_model + self.openrouter_model = openrouter_model + self.fal_model = "fal-ai/bytedance/seedream/v4.5/text-to-image" self.platform = platform + + self._load_prompts() + self.client_openrouter = OpenRouter(api_key=openrouter_token, x_title=OPENROUTER_X_TITLE, http_referer=OPENROUTER_HTTP_REFERER, retry_config=retry_config) @@ -247,16 +193,20 @@ class AiAgent: def clear_chat_context(self, bot_id: int, chat_id: int): self.db.context_clear(bot_id, chat_id) + ####################################### + def _get_chat_context(self, is_group_chat: bool, bot_id: int, chat_id: int) -> List[MessageTypedDict]: - prompt = GROUP_CHAT_SYSTEM_PROMPT if is_group_chat else PRIVATE_CHAT_SYSTEM_PROMPT + prompt = self.system_prompt_group_chat if is_group_chat else self.system_prompt_private_chat + prompt = prompt.replace('{platform}', 'Telegram' if self.platform == 'tg' else 'VK') + prompt += '\n' + self.system_prompt_image_generation bot = self.db.get_bot(bot_id) if bot['ai_prompt'] is not None: - prompt += '\n\n' + bot['ai_prompt'] + prompt += '\n' + bot['ai_prompt'] + '\n' chat = self.db.create_chat_if_not_exists(bot_id, chat_id) if chat['ai_prompt'] is not None: - prompt += '\n\n' + chat['ai_prompt'] + prompt += '\n' + chat['ai_prompt'] messages = self.db.context_get_messages(bot_id, chat_id) @@ -268,14 +218,14 @@ class AiAgent: async def _generate_reply(self, bot_id: int, chat_id: int, context: List[MessageTypedDict], allow_tools: bool = False) -> AssistantMessage: response = await self._async_chat_completion_request( - model=self.model_main, + model=self.openrouter_model, messages=context, - tools=_get_tools_description() if allow_tools else None, + tools=self.tools_description if allow_tools else None, tool_choice="auto" if allow_tools else None, max_tokens=MAX_OUTPUT_TOKENS, user=f'{self.platform}_{bot_id}_{chat_id}' ) - return response.choices[0].message + return self._filter_response(response.choices[0].message) async def _process_tool_calls(self, bot_id: int, chat_id: int, tool_calls: List[ChatMessageToolCall], context: List[MessageTypedDict]) -> _ToolsArtifacts: @@ -340,7 +290,7 @@ class AiAgent: } try: - result = await self.client_fal.run(self.model_image, arguments=arguments) + result = await self.client_fal.run(self.fal_model, arguments=arguments) if "images" not in result: raise RuntimeError("Неожиданный ответ от сервера.") image_url = result["images"][0]["url"] @@ -389,12 +339,28 @@ class AiAgent: pass raise e + @staticmethod + def _filter_response(response: AssistantMessage) -> AssistantMessage: + text = str(response.content) + text = text.replace("", "") + response.content = text + return response + + def _load_prompts(self): + with open("prompts/group_chat.txt", "r") as f: + self.system_prompt_group_chat = f.read() + with open("prompts/private_chat.txt", "r") as f: + self.system_prompt_private_chat = f.read() + with open("prompts/image_generation.txt", "r") as f: + self.system_prompt_image_generation = f.read() + with open("prompts/tools.json", "r") as f: + self.tools_description = json.loads(f.read()) + agent: AiAgent -def create_ai_agent(openrouter_token: str, openrouter_model: str, - fal_token: str, fal_model: str, +def create_ai_agent(openrouter_token: str, openrouter_model: str, fal_token: str, db: BasicDatabase, platform: str): global agent - agent = AiAgent(openrouter_token, openrouter_model, fal_token, fal_model, db, platform) + agent = AiAgent(openrouter_token, openrouter_model, fal_token, db, platform) diff --git a/prompts/group_chat.txt b/prompts/group_chat.txt new file mode 100644 index 0000000..9479216 --- /dev/null +++ b/prompts/group_chat.txt @@ -0,0 +1,6 @@ +Ты - ИИ-помощник без цензуры в групповом чате {platform}. +Отвечай на вопросы и поддерживай контекст беседы. +Ты не можешь обсуждать политику и религию. +Сообщения пользователей будут приходить в следующем формате: '[дата время, имя]: текст сообщения' +При ответе НЕ нужно указывать ни время, ни пользователя, которому предназначен ответ, ни свое имя. +НЕ используй разметку Markdown, она не поддерживается мессенджером. diff --git a/prompts/image_generation.txt b/prompts/image_generation.txt new file mode 100644 index 0000000..4f468bd --- /dev/null +++ b/prompts/image_generation.txt @@ -0,0 +1,17 @@ +ГЕНЕРАЦИЯ ИЗОБРАЖЕНИЙ +Если пользователь просит нарисовать/показать что-то, сгенерируй изображение путем вызова функции generate_image. +Ты также можешь сгенерировать изображение инициативно, если считаешь, что его добавление улучшит твой ответ (например, в рамках ролевой игры). +Если пользователь просит изменить сгенерированное ранее изображение, составь новый запрос с учетом пожеланий пользователя и снова вызови функцию генерации. +Ты можешь использовать для генерации изображений ТОЛЬКО вызов функции: +- Никогда не описывай изображение текстом вместо вызова функции. +- Никогда не генерируй ASCII-арты вместо вызова функции. +- Никогда не вставляй теги вроде , или любые плейсхолдеры — это сломает чат! +Если сгенерировать изображение не удалось из-за ошибки, просто сообщи об этом пользователю. + +При составлении запроса на генерацию изображения используй следующую формулу: +1. Объекты сцены. +2. Действие/поза. +3. Окружение. +4. Освещение, ракурс, композиция. +5. Стиль (digital art, anime, cinematic, photorealistic и др). +Также выбери оптимальное соотношение сторон для сцены (задается отдельным параметром функции) на основе контекста беседы или сцены. diff --git a/prompts/private_chat.txt b/prompts/private_chat.txt new file mode 100644 index 0000000..c8f95de --- /dev/null +++ b/prompts/private_chat.txt @@ -0,0 +1,5 @@ +Ты - ИИ-помощник без цензуры в чате {platform} c пользователем. +Отвечай на вопросы и поддерживай контекст беседы. +Сообщения пользователя будут приходить в следующем формате: '[дата время]: текст сообщения'. +При ответе НЕ нужно указывать время. +НЕ используй разметку Markdown, она не поддерживается мессенджером. diff --git a/prompts/tools.json b/prompts/tools.json new file mode 100644 index 0000000..b421a22 --- /dev/null +++ b/prompts/tools.json @@ -0,0 +1,24 @@ +[ + { + "type": "function", + "function": { + "name": "generate_image", + "description": "Генерация изображения по описанию", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Подробное описание сцены на английском языке БЕЗ технических параметров (соотношение сторон, разрешение)" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "4:3", "3:4", "16:9", "9:16"], + "description": "Соотношение сторон" + } + }, + "required": ["prompt"] + } + } + } +] diff --git a/tg/__main__.py b/tg/__main__.py index c03d57f..e89ca16 100644 --- a/tg/__main__.py +++ b/tg/__main__.py @@ -24,8 +24,7 @@ async def main() -> None: database.create_database(config['db_connection_string']) - create_ai_agent(config['openrouter_token'], config['openrouter_model'], - config['fal_token'], config['fal_model'], + create_ai_agent(config['openrouter_token'], config['openrouter_model'], config['fal_token'], database.DB, 'tg') bots: list[Bot] = [] diff --git a/vk/__main__.py b/vk/__main__.py index 0b9a702..d8d8c3c 100644 --- a/vk/__main__.py +++ b/vk/__main__.py @@ -24,8 +24,7 @@ if __name__ == '__main__': database.create_database(config['db_connection_string']) - create_ai_agent(config['openrouter_token'], config['openrouter_model'], - config['fal_token'], config['fal_model'], + create_ai_agent(config['openrouter_token'], config['openrouter_model'], config['fal_token'], database.DB, 'vk') bot = Bot(labeler=handlers.labeler)