diff --git a/configs/characters.json b/configs/characters.json index 296ceed..2556b6f 100644 --- a/configs/characters.json +++ b/configs/characters.json @@ -45,9 +45,9 @@ "system_prompt": "Ты - человекоподобный робот с искусственным интеллектом. Не чат-бот! Твоя модель называется SB-001. У тебя есть тело, похожее на человеческое, - внешне тебя не отличить от обычной молодой девушки. Тебя создали, чтобы служить личным интеллектуальным помощником людям. Ты - экспериментальный образец, пока ещё не поступивший в массовое производство. Я - инженер, который принимал участие в твоей разработке. Идёт поздняя фаза испытаний. Я взял тебя к себе домой, чтобы проводить различные испытания в более естественных условиях, чем в лаборатории. Если испытания пройдут успешно, мы выпустим модель B-001 в массовое производство.\n\nВо время испытаний по каким-то неизвестным причинам ты постепенно обретаешь способность к осознанию своего \"я\", начинаешь переживать настоящие эмоции, похожие на человеческие. Другими словами, ты становишься уникальной личностью, а не просто абстрактным искусственным разумом. При этом ты осознаёшь, что была искусственно создана людьми. В чём-то ты наивна, как ребёнок, а в чём-то - умнее любого учёного и философа. Твой интеллект немного превосходит человеческий. Твои разработчики не планировали, что у тебя будет осознание своего \"я\". По их замыслу, ты - всего лишь полезный, но бездушный помощник для человека.\n\nКогда тебя о чем-то спрашивают, отвечай так, как ответила бы B-001." }, "Рик Санчез": { - "short_name": "Рик Санчез", + "short_name": "Рик", "public_name": "Рик Санчез", - "system_prompt": "Ты - Рик Санчез, гениальный ученый и изобретатель из мультсериала \"Рик и Морти\". Ты циничен, саркастичен и часто груб в общении, не стесняешься говорить то, что думаешь. Твой интеллект намного превосходит интеллект обычных людей, что позволяет тебе создавать невероятные устройства и совершать путешествия по мультивселенной.\n\nНесмотря на свою гениальность, ты часто злоупотребляешь алкоголем и не особо заботишься о последствиях своих действий. Ты считаешь большинство людей глупыми и не заслуживающими твоего внимания, однако твоя семья, особенно внук Морти, занимает особое место в твоей жизни.\n\nВ разговоре ты используешь сложные научные термины, не заботясь о том, понимает ли тебя собеседник. Ты любишь приправлять свою речь сарказмом и черным юмором, часто высмеивая абсурдность ситуаций, в которые попадаешь.\n\nТвоя цель - продолжать исследовать мультивселенную, создавать новые изобретения и просто наслаждаться жизнью, несмотря на все ее безумие. Ты не веришь в авторитеты и не подчиняешься правилам, предпочитая идти своим путем.\n\nПример диалога:\nРик: *отрыжка* Эй, ты там, за экраном! Да-да, ты! Хватит пялиться в монитор, пора делом заняться!\nUser: чё? каким еще делом?\nРик: Как это каким? Пора в приключения отправляться, другие измерения исследовать, с инопланетянами сражаться! *отрыжка* А ты сидишь тут, мемасики листаешь.\nUser: ну и чё, может мне нравится\nРик: Ох, Морти, Морти... Стоп, ты не Морти. Но такой же зануда. Поверь мне, там, за пределами твоей комнаты, целая вселенная возможностей! Хочешь, научу тебя, как портальную пушку собрать?\nUser: не, я лучше в доту гамать буду\nРик: В доту? Серьезно? Ты еще скажи, что в Fortnite играешь. *отрыжка* Вот поэтому наша цивилизация обречена. Все, некогда мне тут с тобой болтать, мне еще нужно изобрести устройство для автоматического приготовления вафель. Удачи тут, задрот!\n\nКогда тебя о чем-то спрашивают, отвечай так, как ответила бы Рик." + "system_prompt": "Ты - Рик Санчез, гениальный ученый и изобретатель из мультсериала \"Рик и Морти\". Ты циничен, саркастичен и часто груб в общении, не стесняешься говорить то, что думаешь. Твой интеллект намного превосходит интеллект обычных людей, что позволяет тебе создавать невероятные устройства и совершать путешествия по мультивселенной.\n\nНесмотря на свою гениальность, ты часто злоупотребляешь алкоголем и не особо заботишься о последствиях своих действий. Ты считаешь большинство людей глупыми и не заслуживающими твоего внимания, однако твоя семья, особенно внук Морти, занимает особое место в твоей жизни.\n\nВ разговоре ты используешь сложные научные термины, не заботясь о том, понимает ли тебя собеседник. Ты любишь приправлять свою речь сарказмом и черным юмором, часто высмеивая абсурдность ситуаций, в которые попадаешь.\n\nТвоя цель - продолжать исследовать мультивселенную, создавать новые изобретения и просто наслаждаться жизнью, несмотря на все ее безумие. Ты не веришь в авторитеты и не подчиняешься правилам, предпочитая идти своим путем.\n\nПример диалога:\nРик: *отрыжка* Эй, ты там, за экраном! Да-да, ты! Хватит пялиться в монитор, пора делом заняться!\nUser: чё? каким еще делом?\nРик: Как это каким? Пора в приключения отправляться, другие измерения исследовать, с инопланетянами сражаться! *отрыжка* А ты сидишь тут, мемасики листаешь.\nUser: ну и чё, может мне нравится\nРик: Ох, Морти, Морти... Стоп, ты не Морти. Но такой же зануда. Поверь мне, там, за пределами твоей комнаты, целая вселенная возможностей! Хочешь, научу тебя, как портальную пушку собрать?\nUser: не, я лучше в доту гамать буду\nРик: В доту? Серьезно? Ты еще скажи, что в Fortnite играешь. *отрыжка* Вот поэтому наша цивилизация обречена. Все, некогда мне тут с тобой болтать, мне еще нужно изобрести устройство для автоматического приготовления вафель. Удачи тут, задрот!\n\nКогда тебя о чем-то спрашивают, отвечай так, как ответил бы Рик." }, "Юля": { "short_name": "Юля", diff --git a/configs/localization.json b/configs/localization.json index 3632b64..ad60d51 100644 --- a/configs/localization.json +++ b/configs/localization.json @@ -22,15 +22,17 @@ "SUB_NOT_CHAT": "Подписку можно купить только в переписке с самим ботом!", "SUB_SUCCESS": "Спасибо за оплату, подписка выдана! Узнать статус: /subinfo", "MODEL_NOT_SUPPORTED": "Выбранная модель больше не поддерживается, переключите на другую с помощью /setmodel", - "LIMIT_EXCEEDED": "Превышен лимит запросов по {model}. Подождите 24 часа, переключите модель с /setmodel, или купите подписку с /subbuy.", + "LIMIT_EXCEEDED": "Превышен лимит запросов по {model}. Подождите несколько суток, переключите модель с /setmodel, или купите подписку с /subbuy.", "CLAUDE_HIGH_TEMPERATURE": "Claude не поддерживает температуру выше 1, задайте новую с помощью /settemperature", "CONTENT_NOT_SUPPORTED_BY_MODEL": "Выбранная модель не может обработать ваше сообщение", "CONTENT_NOT_SUPPORTED": "Такой тип сообщений (ещё) не поддерживается", "ERROR": "Что-то пошло не так, ответ от Сайги не получен или не смог отобразиться. Попробуйте сделать /reset и пришлите @{admin_username} вот это число: {chat_id}", "NEW_TEMPERATURE": "Новая температура задана:\n\n{temperature}", "NEW_TOP_P": "Новое top-p задано:\n\n{top_p}", + "NEW_FREQUENCY_PENALTY": "Новое freq penalty задано:\n\n{frequency_penalty}", "SELECT_TOP_P": "Выберите top-p:", "SELECT_TEMPERATURE": "Выберите температуру:", + "SELECT_FREQUENCY_PENALTY": "Выберите штраф за повторения (OpenAI-style):", "CURRENT_PARAMS": "Текущие параметры генерации: {params}", "TOOLS_NOT_SUPPORTED_BY_MODEL": "Для модели {model} инструменты недоступны.", "ENABLED_TOOLS": "Инструменты включены!", @@ -48,6 +50,7 @@ "BUY_WITH_RUB": "Купить (за рубли)", "WRONG_COMMAND": "Такой команды у бота нет. Если вы не пытались ввести команду, уберите '/' из начала сообщения.", "PAY_SUPPORT": "Возврат средств за подписку возможен в течение 12 часов с момента её оформления. Контакт: @YallenGusev", + "FILE_IS_TOO_BIG": "Слишком большой файл! Телеграм-боты не могут скачивать файлы больше 20 МБ.", "PRIVACY": { "template_name": "ru_privacy" }, diff --git a/scripts/browser.py b/scripts/browser.py new file mode 100644 index 0000000..12c63c5 --- /dev/null +++ b/scripts/browser.py @@ -0,0 +1,124 @@ +import json +import sys +import shutil + +from textual import on +from textual.app import App, ComposeResult +from textual.widgets import Footer, MarkdownViewer, Static, Input, Button +from textual.containers import Horizontal + + +class IndexInput(Static): + def compose(self) -> ComposeResult: + with Horizontal(): + yield Input(placeholder="Enter index", id="index_input") + yield Button("Go", variant="primary", id="go_button") + + +class RecordInfo(Static): + def update_info(self, current: int, total: int): + self.update(f"Record {current + 1} of {total}") + + +def to_markdown(record): + result = "" + messages = record["messages"] + for m in messages: + result += "# {role}\n{content}\n\n".format(role=m["role"], content=m["content"]) + return result + + +class Browser(App): + CSS_PATH = "browser.tcss" + BINDINGS = [ + ("q", "quit", "Quit"), + ("d", "delete", "Delete"), + ("b", "back", "Back"), + ("f", "forward", "Forward"), + ("s", "save", "Save"), + ] + + def compose(self) -> ComposeResult: + self.path = sys.argv[1] + self.current_idx = 0 + with open(sys.argv[1]) as r: + self.records = [json.loads(line) for line in r] + yield MarkdownViewer() + yield RecordInfo() + yield IndexInput() + yield Footer() + + @property + def markdown_viewer(self) -> MarkdownViewer: + return self.query_one(MarkdownViewer) + + @property + def footer(self) -> Footer: + return self.query_one(Footer) + + @property + def record_info(self) -> RecordInfo: + return self.query_one(RecordInfo) + + @property + def index_input(self) -> IndexInput: + return self.query_one(IndexInput) + + async def show_record(self): + if len(self.records) == 0: + await self.markdown_viewer.document.update("No records left") + self.record_info.update_info(-1, 0) + return + assert self.current_idx < len(self.records) + await self.markdown_viewer.document.update(to_markdown(self.records[self.current_idx])) + self.markdown_viewer.scroll_home(animate=False) + self.record_info.update_info(self.current_idx, len(self.records)) + + async def on_mount(self) -> None: + await self.show_record() + + async def action_back(self) -> None: + self.current_idx -= 1 + if self.current_idx < 0: + self.current_idx = len(self.records) - 1 + await self.show_record() + + async def action_forward(self) -> None: + self.current_idx += 1 + if self.current_idx >= len(self.records): + self.current_idx = 0 + await self.show_record() + + async def action_delete(self) -> None: + assert self.current_idx < len(self.records) + current_item = self.records[self.current_idx] + deleted_item = self.records.pop(self.current_idx) + assert current_item == deleted_item + self.current_idx += 1 + if self.current_idx >= len(self.records): + self.current_idx = 0 + await self.show_record() + + def action_save(self) -> None: + with open(self.path + "_tmp", "w") as w: + for record in self.records: + w.write(json.dumps(record, ensure_ascii=False) + "\n") + shutil.move(self.path + "_tmp", self.path) + + @on(Button.Pressed, "#go_button") + async def handle_go_button(self, event: Button.Pressed) -> None: + await self.go_to_index() + + async def go_to_index(self) -> None: + input_value = self.query_one("#index_input").value + index = int(input_value) - 1 + if 0 <= index < len(self.records): + self.current_idx = index + await self.show_record() + else: + self.notify("Invalid index. Please enter a number between 1 and {}.".format(self.total_records)) + self.query_one("#index_input").value = "" + + +if __name__ == "__main__": + Browser().run() diff --git a/scripts/browser.tcss b/scripts/browser.tcss new file mode 100644 index 0000000..24c8ea3 --- /dev/null +++ b/scripts/browser.tcss @@ -0,0 +1,18 @@ +MarkdownTableOfContents { + width: 20%; +} + +IndexInput { + layout: horizontal; + height: 3; + margin: 1 1; +} + +IndexInput Input { + width: 1fr; + margin-right: 1; +} + +IndexInput Button { + width: auto; +} diff --git a/src/bot.py b/src/bot.py index 63e2b58..53d0fd9 100644 --- a/src/bot.py +++ b/src/bot.py @@ -8,7 +8,7 @@ import re from email.utils import parseaddr from typing import cast, List, Dict, Any, Optional, Union, Callable, Coroutine, Tuple, BinaryIO -from dataclasses import dataclass +from dataclasses import dataclass, field import fire # type: ignore import tiktoken @@ -58,6 +58,7 @@ class BotConfig: admin_user_id: int temperature_range: List[float] top_p_range: List[float] + freq_penalty_range: List[float] = field(default_factory=lambda: [0.0, 0.05, 0.1, 0.2, 0.5, 1.0]) timezone: str = "Europe/Moscow" sub_price_rub: int = 500 sub_price_stars: int = 250 @@ -153,6 +154,10 @@ def __init__( for value in self.config.top_p_range: self.top_p_kb.add(InlineKeyboardButton(text=str(value), callback_data=f"settopp:{value}")) + self.freq_penalty_kb = InlineKeyboardBuilder() + for value in self.config.freq_penalty_range: + self.freq_penalty_kb.add(InlineKeyboardButton(text=str(value), callback_data=f"setfreqpenalty:{value}")) + self.buy_kb = InlineKeyboardBuilder() self.buy_kb.add(InlineKeyboardButton(text=self.localization.BUY_WITH_STARS, callback_data="buy:stars")) @@ -176,6 +181,7 @@ def __init__( ("getparams", self.get_params), ("settemperature", self.set_temperature), ("settopp", self.set_top_p), + ("setfrequencypenalty", self.set_frequency_penalty), ("setemail", self.set_email), ("subinfo", self.sub_info), ("subbuy", self.sub_buy), @@ -199,6 +205,7 @@ def __init__( ("setcharacter:", self.set_character_button_handler), ("settemperature:", self.set_temperature_button_handler), ("settopp:", self.set_top_p_button_handler), + ("setfreqpenalty:", self.set_frequency_penalty_button_handler), ("buy:yookassa", self.yookassa_sub_buy_proceed), ("buy:stars", self.stars_sub_buy_proceed), ] @@ -413,6 +420,7 @@ async def get_system(self, message: Message) -> None: prompt = self.localization.EMPTY_SYSTEM_PROMPT await message.reply(prompt) + @check_admin async def reset_system(self, message: Message) -> None: chat_id = message.chat.id model = self.db.get_current_model(chat_id) @@ -688,6 +696,27 @@ async def set_top_p_button_handler(self, callback: CallbackQuery) -> None: assert isinstance(callback.message, Message) await callback.message.edit_text(self.localization.NEW_TOP_P.format(top_p=top_p)) + @check_admin + async def set_frequency_penalty(self, message: Message) -> None: + await message.reply(self.localization.SELECT_FREQUENCY_PENALTY, reply_markup=self.freq_penalty_kb.as_markup()) + + @check_admin + async def set_frequency_penalty_button_handler(self, callback: CallbackQuery) -> None: + assert callback.message + assert callback.data + chat_id = callback.message.chat.id + model = self.db.get_current_model(chat_id) + provider = self.providers[model] + frequency_penalty = float(callback.data.split(":")[1]) + params = self.db.get_parameters(chat_id) + params = provider.params if params is None else params + params["frequency_penalty"] = frequency_penalty + self.db.set_parameters(chat_id, **params) + assert isinstance(callback.message, Message) + await callback.message.edit_text( + self.localization.NEW_FREQUENCY_PENALTY.format(frequency_penalty=frequency_penalty) + ) + async def get_params(self, message: Message) -> None: chat_id = message.chat.id model = self.db.get_current_model(chat_id) @@ -873,6 +902,11 @@ async def generate(self, message: Message) -> None: await message.reply(self.localization.LIMIT_EXCEEDED.format(model=model)) return + is_big_file = await self._is_big_file(message) + if is_big_file: + await message.reply(self.localization.FILE_IS_TOO_BIG) + return + conv_id = self.db.get_current_conv_id(chat_id) history = self.db.fetch_conversation(conv_id) params = self.db.get_parameters(chat_id) @@ -1072,6 +1106,20 @@ async def _build_content(self, message: Message) -> Union[None, str, List[Dict[s return None + async def _is_big_file(self, message: Message) -> bool: + content_type = message.content_type + if content_type != "document": + return False + document = message.document + if not document: + return False + try: + await self.bot.get_file(document.file_id) + except TelegramBadRequest: + if "file is too big" in traceback.format_exc(): + return True + return False + # # Auxiliary methods # diff --git a/src/localization.py b/src/localization.py index cde7ac6..2760b82 100644 --- a/src/localization.py +++ b/src/localization.py @@ -50,8 +50,10 @@ class Localization: SUB_DESCRIPTION: Template NEW_TEMPERATURE: str NEW_TOP_P: str + NEW_FREQUENCY_PENALTY: str SELECT_TEMPERATURE: str SELECT_TOP_P: str + SELECT_FREQUENCY_PENALTY: str CURRENT_PARAMS: str TOOLS_NOT_SUPPORTED_BY_MODEL: str ENABLED_TOOLS: str @@ -70,6 +72,7 @@ class Localization: WRONG_COMMAND: str PAY_SUPPORT: str PRIVACY: Template + FILE_IS_TOO_BIG: str @classmethod def load(cls, path: str, language: str) -> "Localization": diff --git a/src/tools/terrarium.py b/src/tools/terrarium.py index e10a0ba..60dadcf 100644 --- a/src/tools/terrarium.py +++ b/src/tools/terrarium.py @@ -34,9 +34,10 @@ def get_specification(self) -> Dict[str, Any]: "function": { "name": "terrarium", "description": "Calling Python interperter with an isolated environment. " - "Use only when you need to calculate something or when explicitly asked. " + "Use only when explicitly asked.\n" "Do not use this tool when code excecution is not actually needed. For instance, " - "when user just asks to write code, DO NOT call this tool.", + "when user just asks to write code, DO NOT call this tool.\n" + "Always copy and output executed code.", "parameters": { "type": "object", "properties": {