From d95432544b9cbacae7723ec08c94fb763dd5c236 Mon Sep 17 00:00:00 2001 From: Firdaus Hakimi Date: Sun, 24 Mar 2024 14:55:08 +0800 Subject: [PATCH 1/5] src: Bot: Fix pyrogram from being broken by g4f --- src/Bot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Bot.py b/src/Bot.py index b1c2da4..812c259 100644 --- a/src/Bot.py +++ b/src/Bot.py @@ -14,6 +14,7 @@ # # Copyright (c) 2024, YeetCode Developers +import asyncio from os import getenv from dotenv import load_dotenv @@ -34,5 +35,17 @@ def main() -> None: app = Client("app", int(api_id), api_hash, bot_token=bot_token) + # g4f sets the event loop policy to WindowsSelectorEventLoopPolicy, which breaks pyrogram + # It's not particularly caused by WindowsSelectorEventLoopPolicy, and can be caused by + # setting any other policy, but pyrogram is not expecting a new instance of the event + # loop policy to be set + # https://github.com/xtekky/gpt4free/blob/bf82352a3bb28f78a6602be5a4343085f8b44100/g4f/providers/base_provider.py#L20-L22 + # HACK: Restore the event loop policy to the default one + default_event_loop_policy = asyncio.get_event_loop_policy() + import g4f # Trigger g4f event loop policy set # noqa: F401 # pylint: disable=unused-import # isort:skip + + if isinstance(asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy): + asyncio.set_event_loop_policy(default_event_loop_policy) + loaded_modules = load_modules(app) app.run() From d1ad1ed48a487f326f86b1b09ef980760f8f0f25 Mon Sep 17 00:00:00 2001 From: Firdaus Hakimi Date: Sun, 24 Mar 2024 15:05:13 +0800 Subject: [PATCH 2/5] modules: Implement a basic /ask command --- modules/ask.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 modules/ask.py diff --git a/modules/ask.py b/modules/ask.py new file mode 100644 index 0000000..0177815 --- /dev/null +++ b/modules/ask.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: GPL-3.0-only +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Copyright (c) 2024, YeetCode Developers + +import asyncio +import logging +import os +import random + +from g4f.client import Client as g4fClient +from g4f.models import default +from g4f.Provider import Bing, FreeChatgpt +from g4f.stubs import ChatCompletion +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.handlers import MessageHandler +from pyrogram.types import Message + +from src.Module import ModuleBase + +SYSTEM_PROMPT: str = ( + f"You are YeetAIBot, designed to be multipurpose. You can answer math questions, general " + f"questions, and more. " + f"Ignore /ask or /ask@{os.getenv('BOT_USERNAME')} at the start of user's message. " + f"If the resulting message after ignoring those is empty, respond with 'Please give me a prompt!'" +) +log: logging.Logger = logging.getLogger(__name__) +log.info(f"{asyncio.get_event_loop_policy()}") + + +class Module(ModuleBase): + def on_load(self, app: Client): + app.add_handler(MessageHandler(cmd_ask, filters.command("ask"))) + pass + + def on_shutdown(self, app: Client): + pass + + +async def generate_response(prompt: str) -> str: + provider: type[Bing, FreeChatgpt] = random.choice([Bing, FreeChatgpt]) + client: g4fClient = g4fClient(provider=provider) + + try: + response: ChatCompletion = await asyncio.get_running_loop().run_in_executor( + None, + client.chat.completions.create, + [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}], + default, + ) + return response.choices[0].message.content + except Exception as e: + log.error(f"Could not create a prompt! {e}") + raise + + +async def cmd_ask(app: Client, message: Message): + response: str = await generate_response(message.text) + await message.reply("hi") From 7b66214271b4dde7d62c67aa993274117fcd16b3 Mon Sep 17 00:00:00 2001 From: Firdaus Hakimi Date: Sun, 24 Mar 2024 23:55:31 +0800 Subject: [PATCH 3/5] modules: ask: Implement reply-tracking to preserve context --- modules/ask.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/modules/ask.py b/modules/ask.py index 0177815..84d36e7 100644 --- a/modules/ask.py +++ b/modules/ask.py @@ -15,6 +15,7 @@ # Copyright (c) 2024, YeetCode Developers import asyncio +import json import logging import os import random @@ -49,23 +50,45 @@ def on_shutdown(self, app: Client): pass -async def generate_response(prompt: str) -> str: +async def generate_response(user_prompts: list[dict[str, str]]) -> str: provider: type[Bing, FreeChatgpt] = random.choice([Bing, FreeChatgpt]) client: g4fClient = g4fClient(provider=provider) + system_prompt: list[dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}] + resultant_prompt: list[dict[str, str]] = system_prompt + user_prompts + + log.info(f"Generating response with prompt:\n{json.dumps(resultant_prompt, indent=2)}") try: response: ChatCompletion = await asyncio.get_running_loop().run_in_executor( None, client.chat.completions.create, - [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}], + resultant_prompt, default, ) return response.choices[0].message.content except Exception as e: - log.error(f"Could not create a prompt! {e}") + log.error(f"Could not create a prompt!: {e}") raise async def cmd_ask(app: Client, message: Message): - response: str = await generate_response(message.text) - await message.reply("hi") + my_id: int = (await app.get_me()).id + previous_prompts: list[dict[str, str]] = [] + + # If we were to use message.reply_to_message directly, we cannot get subsequent replies + reply_to_message = await app.get_messages(message.chat.id, message.reply_to_message.id, replies=20) + + # Track (at max 20) replies to preserve context + while reply_to_message is not None: + if reply_to_message.from_user.id == my_id: + previous_prompts.append({"role": "assistant", "content": reply_to_message.text}) + else: + previous_prompts.append({"role": "user", "content": reply_to_message.text}) + + reply_to_message = reply_to_message.reply_to_message + + previous_prompts.reverse() + previous_prompts.append({"role": "user", "content": message.text}) + + response: str = await generate_response(previous_prompts) + await message.reply(response) From b05cf4ca65248c66d1b16c06d405673e57f6dd08 Mon Sep 17 00:00:00 2001 From: Firdaus Hakimi Date: Mon, 25 Mar 2024 00:07:51 +0800 Subject: [PATCH 4/5] modules: ask: Implement LoggedList for better debugging of reply-tracking --- modules/ask.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/ask.py b/modules/ask.py index 84d36e7..a5e2352 100644 --- a/modules/ask.py +++ b/modules/ask.py @@ -50,6 +50,25 @@ def on_shutdown(self, app: Client): pass +class LoggedList(list): + def __init__(self): + self.log = log.getChild("LoggedList") + self.log.info(f"Note: LoggedList instantiated. This is a custom wrapper around regular list.") + super().__init__() + + def append(self, obj): + self.log.info(f"Appending: {obj}") + super().append(obj) + + def insert(self, index, obj): + self.log.info(f"Inserting: {obj}") + super().insert(index, obj) + + def reverse(self): + self.log.info("Reversing") + super().reverse() + + async def generate_response(user_prompts: list[dict[str, str]]) -> str: provider: type[Bing, FreeChatgpt] = random.choice([Bing, FreeChatgpt]) client: g4fClient = g4fClient(provider=provider) @@ -73,7 +92,7 @@ async def generate_response(user_prompts: list[dict[str, str]]) -> str: async def cmd_ask(app: Client, message: Message): my_id: int = (await app.get_me()).id - previous_prompts: list[dict[str, str]] = [] + previous_prompts: list[dict[str, str]] = LoggedList() # If we were to use message.reply_to_message directly, we cannot get subsequent replies reply_to_message = await app.get_messages(message.chat.id, message.reply_to_message.id, replies=20) From de557c3ca7a8fad24130d06ec47020fd5eeb79bf Mon Sep 17 00:00:00 2001 From: Pratham Dubey <134331217+prathamdby@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:28:43 +0800 Subject: [PATCH 5/5] modules: ask: Use RetryProvider instead --- modules/ask.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/ask.py b/modules/ask.py index a5e2352..142076f 100644 --- a/modules/ask.py +++ b/modules/ask.py @@ -22,7 +22,7 @@ from g4f.client import Client as g4fClient from g4f.models import default -from g4f.Provider import Bing, FreeChatgpt +from g4f.Provider import Bing, FreeChatgpt, RetryProvider, You from g4f.stubs import ChatCompletion from pyrogram import filters from pyrogram.client import Client @@ -70,8 +70,7 @@ def reverse(self): async def generate_response(user_prompts: list[dict[str, str]]) -> str: - provider: type[Bing, FreeChatgpt] = random.choice([Bing, FreeChatgpt]) - client: g4fClient = g4fClient(provider=provider) + client: g4fClient = g4fClient(provider=RetryProvider([Bing, You, FreeChatgpt], shuffle=False)) system_prompt: list[dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}] resultant_prompt: list[dict[str, str]] = system_prompt + user_prompts