From 4e45b90bd6d5a3523e8cae2aa34413d8f6b61dc1 Mon Sep 17 00:00:00 2001 From: Polivoda Lev Date: Sun, 21 Apr 2024 19:45:29 +0400 Subject: [PATCH] start commit --- .gitignore | 15 + core.py | 115 ++++++++ index.html | 274 +++++++++++++++++++ jaa.py | 378 ++++++++++++++++++++++++++ main.py | 20 ++ models/silero/.gitkeep | 0 models/vosk/.gitkeep | 0 options/.gitkeep | 0 packages.py | 84 ++++++ plugins/plugin_GPT.py | 230 ++++++++++++++++ plugins/plugin_GPTTalk/__init__.py | 33 +++ plugins/plugin_GPTTalk/base.py | 53 ++++ plugins/plugin_GPTTalk/utils.py | 225 +++++++++++++++ plugins/plugin_HTTPServer/__init__.py | 27 ++ plugins/plugin_HTTPServer/utils.py | 36 +++ plugins/plugin_mediakeys.py | 24 ++ plugins/plugin_mind_base/__init__.py | 18 ++ plugins/plugin_mind_base/base.py | 70 +++++ plugins/plugin_mind_base/gpt_util.py | 20 ++ plugins/plugin_mind_base/prompts.json | 7 + plugins/plugin_mind_base/utils.py | 0 plugins/plugin_reminder/__init__.py | 19 ++ plugins/plugin_reminder/base.py | 42 +++ plugins/plugin_reminder/utils.py | 81 ++++++ plugins/plugin_silero.py | 112 ++++++++ plugins/plugin_telegram_bot.py | 115 ++++++++ plugins/plugin_telegram_userbot.py | 89 ++++++ plugins/plugin_twitch.py | 197 ++++++++++++++ plugins/plugin_vosk.py | 125 +++++++++ plugins/plugin_websocket_client.py | 73 +++++ plugins/plugin_websocket_server.py | 86 ++++++ requirements.txt | Bin 0 -> 2008 bytes run_textual.py | 11 + start_model.bat | 3 + text_ui.bat | 3 + textual_ui/__init__.py | 6 + textual_ui/classes.py | 15 + textual_ui/main_page.py | 24 ++ textual_ui/options_screen.py | 93 +++++++ textual_ui/plugins_screen.py | 42 +++ utils/custom_filters.py | 22 ++ 41 files changed, 2787 insertions(+) create mode 100644 .gitignore create mode 100644 core.py create mode 100644 index.html create mode 100644 jaa.py create mode 100644 main.py create mode 100644 models/silero/.gitkeep create mode 100644 models/vosk/.gitkeep create mode 100644 options/.gitkeep create mode 100644 packages.py create mode 100644 plugins/plugin_GPT.py create mode 100644 plugins/plugin_GPTTalk/__init__.py create mode 100644 plugins/plugin_GPTTalk/base.py create mode 100644 plugins/plugin_GPTTalk/utils.py create mode 100644 plugins/plugin_HTTPServer/__init__.py create mode 100644 plugins/plugin_HTTPServer/utils.py create mode 100644 plugins/plugin_mediakeys.py create mode 100644 plugins/plugin_mind_base/__init__.py create mode 100644 plugins/plugin_mind_base/base.py create mode 100644 plugins/plugin_mind_base/gpt_util.py create mode 100644 plugins/plugin_mind_base/prompts.json create mode 100644 plugins/plugin_mind_base/utils.py create mode 100644 plugins/plugin_reminder/__init__.py create mode 100644 plugins/plugin_reminder/base.py create mode 100644 plugins/plugin_reminder/utils.py create mode 100644 plugins/plugin_silero.py create mode 100644 plugins/plugin_telegram_bot.py create mode 100644 plugins/plugin_telegram_userbot.py create mode 100644 plugins/plugin_twitch.py create mode 100644 plugins/plugin_vosk.py create mode 100644 plugins/plugin_websocket_client.py create mode 100644 plugins/plugin_websocket_server.py create mode 100644 requirements.txt create mode 100644 run_textual.py create mode 100644 start_model.bat create mode 100644 text_ui.bat create mode 100644 textual_ui/__init__.py create mode 100644 textual_ui/classes.py create mode 100644 textual_ui/main_page.py create mode 100644 textual_ui/options_screen.py create mode 100644 textual_ui/plugins_screen.py create mode 100644 utils/custom_filters.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a7bd4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ + +/venv/ +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +.idea/ + +*.db \ No newline at end of file diff --git a/core.py b/core.py new file mode 100644 index 0000000..3d68cba --- /dev/null +++ b/core.py @@ -0,0 +1,115 @@ +import asyncio +import os +import re +import sys + +from magic_filter import MagicFilter +from termcolor import cprint, colored +import logging + +import packages +from jaa import JaaCore + +F = MagicFilter() +version = "0.0.1" + +logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", + level=logging.INFO) + + +class NotFoundFilerTextError(BaseException): + pass + + +class MetaSingleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class EventObserver: + def __init__(self): + self.callbacks = {} + + async def _run_callback(self, callback, package: None | packages.TextPackage = None): + try: + await callback(package=package) + except Exception as exc: + logging.exception(f'Сопрограмма {callback.__module__}.{callback.__name__}() вызвала исключение: {exc}') + + def register(self, filt: MagicFilter = None): + def wrapper(func: callable): + if not asyncio.iscoroutinefunction(func): + raise ValueError("function needs to be async which takes one parameter") + + if filt: + async def wrapper_(package=None): + if filt.resolve(package.for_filter): + asyncio.run_coroutine_threadsafe( + coro=self._run_callback(func, package=package), + loop=asyncio.get_event_loop() + ) + + else: + async def wrapper_(package=None): + asyncio.run_coroutine_threadsafe( + coro=self._run_callback(func, package=package), + loop=asyncio.get_event_loop() + ) + + self.callbacks[f"{func.__module__}.{func.__name__}"] = wrapper_ + + return wrapper + + async def __call__(self, package=None): + # TODO: Сделать контекст + for callback in self.callbacks.values(): + await callback(package) + + +class Core(JaaCore, metaclass=MetaSingleton): + def __init__(self, observer_list=["on_input", "on_output"]): + super().__init__() + + self.sound_playing = None + self.ws_server = None + self.gpt_talk = None + for observer in observer_list: + self.add_observer(observer) + + def add_observer(self, observer_name): + if not hasattr(self, observer_name): + setattr(self, observer_name, EventObserver()) + + @staticmethod + async def start_loop(): + while True: + await asyncio.sleep(0) + + @staticmethod + async def _reboot(): + # No recommend for use + python = sys.executable + os.execl(python, python, *sys.argv) + + @staticmethod + def get_manifest(plugin_name: str): + manifest_re = r"manifest\s=\s(\{[\s\S]*?\})(?=\s*return manifest)" + + if plugin_name.endswith(".py"): + with open(f"plugins/{plugin_name}", "r", encoding="utf-8") as file: + plugin_content = file.read() + else: + with open(f"plugins/{plugin_name}/__init__.py", "r", encoding="utf-8") as file: + plugin_content = file.read() + + find_data = re.findall(manifest_re, plugin_content)[0] + data = eval(find_data) + return data + + +if __name__ == '__main__': + core = Core() diff --git a/index.html b/index.html new file mode 100644 index 0000000..8aebbf7 --- /dev/null +++ b/index.html @@ -0,0 +1,274 @@ + + + + + + Combined Console and Settings + + + +

Настройки и Сообщения

+ + +
+
+
+
+ + +
+ +
+

Настройки

+ +
+ + +
+
+ + +
+ +
+ +
+
+
+
+
+
+
+ + + + + + diff --git a/jaa.py b/jaa.py new file mode 100644 index 0000000..144a6bb --- /dev/null +++ b/jaa.py @@ -0,0 +1,378 @@ +""" +Jaa.py Plugin Framework +Author: Janvarev Vladislav + +Jaa.py - minimalistic one-file plugin framework with no dependencies. +Main functions: +- run all plugins files from "plugins" folder, base on filename +- save each plugin options in "options" folder in JSON text files for further editing + +- Plugins +must located in plugins/ folder +must have "start(core)" function, that returns manifest dict +manifest must contain keys "name" and "version" +can contain "default_options" +- if contain - options will be saved in "options" folder and reload instead next time +- if contain - "start_with_options(core,manifest)" function will run with manifest with "options" key +manifest will be processed in "process_plugin_manifest" function if you override it + +- Options (for plugins) +are saved under "options" folder in JSON format +created at first run plugin with "default_options" +updated when plugin change "version" + +- Example usage: +class VoiceAssCore(JaaCore): # class must override JaaCore + def __init__(self): + JaaCore.__init__(self,__file__) + ... + +main = VoiceAssCore() +main.init_plugins(["core"]) # 1 param - first plugins to be initialized + # Good if you need some "core" options/plugin to be loaded before others + # not necessary starts with "plugin_" prefix + +also can be run like + +main.init_plugins() + +- Requirements +Python 3.5+ (due to dict mix in final_options calc), can be relaxed + +AzimovIZ mod: +The modification includes a transition to asynchronous working methods as well as the ability to +import plugin directories. +""" + +import os +import sys +import traceback +import json +from os.path import isdir, isfile + +# here we trying to use termcolor to highlight plugin info and errors during load +try: + from termcolor import cprint +except Exception as e: + # not found? making a stub! + def cprint(p, color=None): + if color == None: + print(p) + else: + print(str(color).upper(), p) + +version = "2.2.0" + + +class JaaCore: + def __init__(self, root_file=__file__): + self.jaaPluginPrefix = "plugin_" + self.jaaVersion = version + self.jaaRootFolder = os.path.dirname(root_file) + self.jaaOptionsPath = self.jaaRootFolder + os.path.sep + "options" + self.jaaShowTracebackOnPluginErrors = False + cprint("JAA.PY v{0} class created!".format(version), "blue") + + # ------------- plugins ----------------- + async def init_plugins(self, list_first_plugins=[]): + self.plugin_manifests = {} + + # 1. run first plugins first! + for modname in list_first_plugins: + await self.init_plugin(modname) + + # 2. run all plugins from plugins folder + from os import listdir + from os.path import isfile, join + pluginpath = self.jaaRootFolder + "/plugins" + # Импорт изменен - добавлена возможность импорта папок + files = [] + for f in listdir(pluginpath): + if isfile(join(pluginpath, f)): + files.append(f) + elif isdir(join(pluginpath, f)) and "__init__.py" in listdir(join(pluginpath, f)): + files.append(f) + + for fil in files: + # print fil[:-3] + if fil.startswith(self.jaaPluginPrefix): + if fil.endswith(".py"): + fil = fil[:-3] + await self.init_plugin(fil) + # Конец изменений импорта + + def get_plugin_options_or_none(self, modname): + # saved options try to read + if isfile(f"options/{modname}.json"): + with open(self.jaaOptionsPath + '/' + modname + '.json', 'r', encoding="utf-8") as f: + s = f.read() + saved_options = json.loads(s) + + return saved_options + + return None + + async def init_plugin(self, modname): + # import + + saved_options = self.get_plugin_options_or_none(modname) + if saved_options and "is_active" in saved_options and not saved_options["is_active"]: + return + + try: + mod = await self.import_plugin("plugins." + modname) + except Exception as e: + self.print_error("JAA PLUGIN ERROR: {0} error on load: {1}".format(modname, str(e))) + return False + + # run start function + try: + res = await mod.start(self) + except Exception as e: + self.print_error("JAA PLUGIN ERROR: {0} error on start: {1}".format(modname, str(e))) + return False + + # if plugin has an options + if "default_options" in res: + try: + res["default_options"]["v"] = res["version"] + + # only string needs Python 3.5 + if saved_options: + final_options = {**res["default_options"], **saved_options} + else: + final_options = res["default_options"] + + # if no option found or version is differ from mod version + if not saved_options or saved_options["v"] != res["version"]: + final_options["v"] = res["version"] + final_options["is_active"] = True + self.save_plugin_options(modname, final_options) + + res["options"] = final_options + + try: + res2 = await mod.start_with_options(self, res) + if res2 != None: + res = res2 + except Exception as e: + self.print_error( + "JAA PLUGIN ERROR: {0} error on start_with_options processing: {1}".format(modname, str(e))) + return False + + except Exception as e: + self.print_error("JAA PLUGIN ERROR: {0} error on options processing: {1}".format(modname, str(e))) + return False + + # processing plugin manifest + try: + # set up name and version + plugin_name = res["name"] + plugin_version = res["version"] + + self.process_plugin_manifest(modname, res) + + except Exception as e: + print("JAA PLUGIN ERROR: {0} error on process startup options: {1}".format(modname, str(e))) + return False + + self.plugin_manifests[modname] = res + + self.on_succ_plugin_start(modname, plugin_name, plugin_version) + return True + + def _deimport_plugin(self, plugin): + try: + del sys.modules["plugins." + plugin] + except Exception as err: + print(f"Не удалось деимпортровать плагин {plugin}: {err}") + + def on_succ_plugin_start(self, modname, plugin_name, plugin_version): + cprint("JAA PLUGIN: {1} {2} ({0}) started!".format(modname, plugin_name, plugin_version)) + + def print_error(self, p): + cprint(p, "red") + if self.jaaShowTracebackOnPluginErrors: + traceback.print_exc() + + async def import_plugin(self, module_name): + import sys + + __import__(module_name) + + if module_name in sys.modules: + return sys.modules[module_name] + return None + + def save_plugin_options(self, modname, options): + # check folder exists + if not os.path.exists(self.jaaOptionsPath): + os.makedirs(self.jaaOptionsPath) + + str_options = json.dumps(options, sort_keys=True, indent=4, ensure_ascii=False) + with open(self.jaaOptionsPath + '/' + modname + '.json', 'w', encoding="utf-8") as f: + f.write(str_options) + f.close() + + # process manifest must be overrided in inherit class + def process_plugin_manifest(self, modname, manifest): + print("JAA PLUGIN: {0} manifest dummy procession (override 'process_plugin_manifest' function)".format(modname)) + return + + def plugin_manifest(self, pluginname): + if pluginname in self.plugin_manifests: + return self.plugin_manifests[pluginname] + return {} + + def plugin_options(self, pluginname): + manifest = self.plugin_manifest(pluginname) + if "options" in manifest: + return manifest["options"] + return None + + # ------------ gradio stuff -------------- + def gradio_save(self, pluginname): + print("Saving options for {0}!".format(pluginname)) + self.save_plugin_options(pluginname, self.plugin_options(pluginname)) + + def gradio_upd(self, pluginname, option, val): + options = self.plugin_options(pluginname) + + # special case + if isinstance(options[option], (list, dict)) and isinstance(val, str): + import json + try: + options[option] = json.loads(val) + except Exception as e: + print(e) + pass + else: + options[option] = val + print(option, val, options) + + def gradio_render_settings_interface(self, title: str = "Settings manager", + required_fields_to_show_plugin: list = ["default_options"]): + import gradio as gr + + with gr.Blocks() as gr_interface: + gr.Markdown("# {0}".format(title)) + for pluginname in self.plugin_manifests: + manifest = self.plugin_manifests[pluginname] + + # calculate if we show plugin + is_show_plugin = False + if len(required_fields_to_show_plugin) == 0: + is_show_plugin = True + else: + for k in required_fields_to_show_plugin: + if manifest.get(k) is not None: + is_show_plugin = True + + if is_show_plugin: + with gr.Tab(pluginname): + gr.Markdown("## {0} v{1}".format(manifest["name"], manifest["version"])) + if manifest.get("description") is not None: + gr.Markdown(manifest.get("description")) + + if manifest.get("url") is not None: + gr.Markdown("**URL:** [{0}]({0})".format(manifest.get("url"))) + + if "options" in manifest: + options = manifest["options"] + if len(options) > 1: # not only v + text_button = gr.Button("Save options".format(pluginname)) + # options_int_list = [] + for option in options: + + # gr.Label(label=option) + if option != "v": + val = options[option] + label = option + + if manifest.get("options_label") is not None: + if manifest.get("options_label").get(option) is not None: + label = option + ": " + manifest.get("options_label").get(option) + + if isinstance(val, (bool,)): + gr_elem = gr.Checkbox(value=val, label=label) + elif isinstance(val, (dict, list)): + import json + gr_elem = gr.Textbox(value=json.dumps(val, ensure_ascii=False), label=label) + else: + gr_elem = gr.Textbox(value=val, label=label) + + def handler(x, pluginname=pluginname, option=option): + self.gradio_upd(pluginname, option, x) + + gr_elem.change(handler, gr_elem, None) + + def handler_save(pluginname=pluginname): + self.gradio_save(pluginname) + + text_button.click(handler_save, inputs=None, outputs=None) + else: + gr.Markdown("_No options for this plugin_") + + return gr_interface + + +def load_options(options_file=None, py_file=None, default_options={}): + # 1. calculating options filename + if options_file == None: + if py_file == None: + raise Exception('JAA: Options or PY file is not defined, cant calc options filename') + else: + options_file = py_file[:-3] + '.json' + + # 2. try to read saved options + saved_options = {} + try: + with open(options_file, 'r', encoding="utf-8") as f: + s = f.read() + saved_options = json.loads(s) + # print("Saved options", saved_options) + except Exception as e: + pass + + # 3. calculating final options + + # only string needs Python 3.5 + final_options = {**default_options, **saved_options} + + # 4. calculating hash from def options to check - is file rewrite needed? + import hashlib + hash = hashlib.md5((json.dumps(default_options, sort_keys=True)).encode('utf-8')).hexdigest() + + # 5. if no option file found or hash was from other default options + if len(saved_options) == 0 or not ("hash" in saved_options.keys()) or saved_options["hash"] != hash: + final_options["hash"] = hash + # self.save_plugin_options(modname,final_options) + + # saving in file + str_options = json.dumps(final_options, sort_keys=True, indent=4, ensure_ascii=False) + with open(options_file, 'w', encoding="utf-8") as f: + f.write(str_options) + f.close() + + return final_options + + +""" +The MIT License (MIT) +Copyright (c) 2021 Janvarev Vladislav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" diff --git a/main.py b/main.py new file mode 100644 index 0000000..b65e95f --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +import asyncio + +import packages +from core import Core + + +async def main(): + core = Core() + await core.init_plugins() + + + + await core.start_loop() + print("sdjflskdjflskdjflskdjflskdjfslkdfjslkdjfs;lkfjs;lkdjas;ldkfj;lsakfjas;lkdfj;lskdjfl;sakdfjsl;akdfj;saldkfjsal;df") + + + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/models/silero/.gitkeep b/models/silero/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/models/vosk/.gitkeep b/models/vosk/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/options/.gitkeep b/options/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages.py b/packages.py new file mode 100644 index 0000000..7adc933 --- /dev/null +++ b/packages.py @@ -0,0 +1,84 @@ +async def NULL_HOOK(package): + pass + + +class BasePackage: + def __init__(self, core, hook: callable): + self.core = core + self.hook = hook + self.data = {} + self.completed = False + + async def run_hook(self): + if not self.completed: + await self.hook(self) + self.completed = True + + +class TextPackage(BasePackage): + def __init__(self, input_text, core, hook, for_filter=None): + super().__init__(core, hook) + self.input_text = input_text + self.for_filter = for_filter or input_text + + @property + def text(self): + if "text" in self.data: + return self.data["text"] + return None + + @text.setter + def text(self, value): + if isinstance(value, str): + self.data["text"] = value + else: + raise TypeError(f".text must be str, got {type(value)}") + + +class TelegramAnswerPackage(TextPackage): + def __init__(self, input_text, core, answer: callable, for_filter=None): + self.answer = answer + super(TelegramAnswerPackage, self).__init__(input_text, core, for_filter=for_filter) + + +class HookExtendPackage(BasePackage): + """ + Расширяет вызов основного крюка двумя вызовами перед и после основного. + """ + + def __init__(self, input_text, core, hook): + super().__init__(core, hook) + self.input_text = input_text + + @property + def pre_hook(self): + if "pre_hook" in self.data: + return self.data["pre_hook"] + return None + + @pre_hook.setter + def pre_hook(self, value): + if isinstance(value, callable): + self.data["pre_hook"] = value + else: + raise TypeError(f".pre_hook must be callable, got {type(value)}") + + @property + def post_hook(self): + if "post_hook" in self.data: + return self.data["post_hook"] + return None + + @post_hook.setter + def post_hook(self, value): + if isinstance(value, callable): + self.data["post_hook"] = value + else: + raise TypeError(f".post_hook must be callable, got {type(value)}") + + async def run_hook(self): + if not self.completed: + await self.pre_hook(self) + await self.hook(self) + await self.post_hook(self) + self.completed = True diff --git a/plugins/plugin_GPT.py b/plugins/plugin_GPT.py new file mode 100644 index 0000000..9f36ea4 --- /dev/null +++ b/plugins/plugin_GPT.py @@ -0,0 +1,230 @@ +import asyncio +import inspect +import json +import logging +import os +from typing import Coroutine + +import requests +logger = logging.getLogger("root") +from core import Core, F +import sys + +from utils.custom_filters import levenshtein_filter + +core = Core() + +gpt_url = "" +use_onerig_traslater = False +onerig_traslater_url = "" + + +class GPT: + def __init__(self, model: str, token: str = None, base_url: str = None): + self.model = model + self.token = token + self.base_url = base_url + + async def ask(self, prompt: str): + context_prompt = f"""char is (Lisa) + Age 21 + Female + Personality: Feels like a robot, but behaves more humanely. Works as user's assistant and follows all his instructions. Does not like empty talk, but prefers commands or orders. + Description: When user asks to do something, char always tries to do it as best as possible and talks about his failures, which are incredibly rare. When char answers, her answers to questions do not contain unnecessary information. Does not express emotion unless user asks for it.""" + + data = { + "mode": "chat", + "messages": [ + {"role": "system", "content": context_prompt}, + {"role": "user", "content": prompt} + ] + } + if self.model: + data.update({"model": self.model}) + + headers = {"Content-Type": "application/json"} + if self.token: + headers.update({"Authorization": f"Bearer {self.token}"}) + + response = requests.post(f"{self.base_url}chat/completions", headers=headers, json=data, verify=False) + assistant_message = response.json()['choices'][0]['message']['content'] + logger.info(f"Ответ ГПТ: {assistant_message}\n{response.json()}") + return assistant_message + + @staticmethod + def find_json(text): + try: + json_data_ = "{" + text.split("{")[1] + json_data_ = json_data_.split("}")[0] + "}" + json_data = json.loads(json_data_) + return json_data + except: + return None + + +async def start(core: Core): + manifest = { + "name": "Плагин GPT", + "version": "1.1", + + "default_options": { + "openai_completable": { + "base_url": "http://127.0.0.1:5000/v1/", + "model": None, + "token": None, + }, + "use_custom_base": True, + "use_onerig_traslater": False, + "onerig_traslater_url": "http://127.0.0.1:4990/translate" + }, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + base_url = manifest["options"]["openai_completable"]["base_url"] + model = manifest["options"]["openai_completable"]["model"] + token = manifest["options"]["openai_completable"]["token"] + use_custom_base = manifest["options"]["use_custom_base"] + + core.gpt = GPT(model=model, token=token, base_url=base_url if use_custom_base else "https://api.openai.com/v1") + + +def get_plugin_funcs(): + func_list = {} + for plugin_name in os.listdir("plugins"): + if not __file__.endswith(plugin_name) and "__pycache__" not in plugin_name: + import_name = f"plugins.{plugin_name.split('.py')[0]}" + __import__(import_name) + mod = sys.modules[import_name] + func_list.update( + { + import_name: { + name: obj for (name, obj) in vars(mod).items() + if hasattr(obj, "__class__") and + obj.__class__.__name__ == "function" and + not name.startswith("_") and + not name in ["start_with_options", "start"] + } + } + ) + for func in func_list[import_name].keys(): + func_list[import_name][func] = str(inspect.getfullargspec(func_list[import_name][func]).annotations) + return func_list + + +async def _translater(text: str, from_lang: str, to_lang: str): + global use_onerig_traslater, onerig_traslater_url + if use_onerig_traslater: + headers = { + "Content-Type": "application/json" + } + # translate + translated = requests.get( + url=onerig_traslater_url, + headers=headers, + params={"text": text, "from_lang": from_lang, "to_lang": to_lang} + ) + text = translated.json()["result"] + + return text + + +#@core.on_input.register() +async def _ask_gpt(package): + prompt = f""" +У меня есть список модулей и их функций для выполнения: +{json.dumps(get_plugin_funcs(), indent=2)} +Для каждого модуля и функции указаны её имя и функционал. +Тебе нужно определить какой модуль и какую функцию модуля следует использовать для выполнения инструкции: "{package.input_text}" из представленных ранее данных. +В ответ тебе нужно написать только строку в формате json. +Формат общения должен соответствовать следующему примеру: +Инструкция : "включи мультик" +Ответ: +{{ + "module": "plugins.plugin_player", + "function": "play_file", + "file_path": "/mooves/cartoons", + "file_name": "move.mp4" +}} +В начале идет название плагина, далее название функции, и затем названия аргументов и их значения если они нужны. +Важно: не пиши ничего кроме json в ответе. Строго только json и ничего кроме json. +""" + + assistant_message = await core.gpt.ask(prompt=prompt) + + json_data = core.gpt.find_json(assistant_message) + + module = json_data.pop("module") + function = json_data.pop("function") + + mod = sys.modules[module] + func = vars(mod).get(function) + if asyncio.iscoroutinefunction(func): + await func(**json_data) + else: + func(**json_data) + +"""У тебя есть доступ к следующим инструментам: +[ + { + "tool": "sum_numbers", + "description": "Функция сложения двух чисел" + "tool_input": [ + {"name": "Number1", "type": "integer"}, + {"name": "Number2", "type": "integer"}, + ], + }, + { + "tool": "multiply_numbers", + "description": "Функция перемножения двух чисел" + "tool_input": [ + {"name": "Number1", "type": "integer"}, + {"name": "Number2", "type": "integer"}, + ] + } +] +Ты должен всегда выбирать один из представленных инструментов и отвечать только в формате JSON со следующей схемой: +{ + "tool": <имя выбранного инструмента>, + "tool_input": <только список параметров и значений> +} +Пример ответа:{ + "tool": "ответ_пользователю", + "tool_input": { + "Ответ": "Привет! Чем могу помочь?" + } +} + + + + + fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + "Authorization": `Bearer ${OPENROUTER_API_KEY}`, + "HTTP-Referer": `${YOUR_SITE_URL}`, // Optional, for including your app on openrouter.ai rankings. + "X-Title": `${YOUR_SITE_NAME}`, // Optional. Shows in rankings on openrouter.ai. + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "model": "mistralai/mixtral-8x7b-instruct", + "messages": [ + {"role": "user", "content": "Сформируй json из следующего запроса: "напомни завтра про магазин" +Тебе необходимо указать следующие поля: +value - значение +day_before - сколько дней осталось +time - время в которое нужно сделать действие + +Пример: "Напомни мне послезавтра про то что мне нужно забрать заказ из интернет магазина после работы" +Ответ: +{ +"value": "забрать заказ из магазина" +"day_before": "2" +"time": "18:45" +} +В ответе можно указывать только json!"}, + ], + }) + }); +""" \ No newline at end of file diff --git a/plugins/plugin_GPTTalk/__init__.py b/plugins/plugin_GPTTalk/__init__.py new file mode 100644 index 0000000..3e0b10b --- /dev/null +++ b/plugins/plugin_GPTTalk/__init__.py @@ -0,0 +1,33 @@ +from core import Core, F +from .utils import * +core = Core() + + + +async def start(core: Core): + manifest = { + "name": "Плагин GPTTalk", + "version": "1.0", + + "default_options": { + "openai_completable": { + "base_url": "http://127.0.0.1:5000/v1/", + "model": None, + "token": None, + }, + "use_custom_base": True, + "use_onerig_traslater": False, + "onerig_traslater_url": "http://127.0.0.1:4990/translate" + }, + } + return manifest + + + +async def start_with_options(core: Core, manifest: dict): + base_url = manifest["options"]["openai_completable"]["base_url"] + model = manifest["options"]["openai_completable"]["model"] + token = manifest["options"]["openai_completable"]["token"] + use_custom_base = manifest["options"]["use_custom_base"] + + core.gpt_talk = GPTTalk(model=model, token=token, base_url=base_url if use_custom_base else "https://api.openai.com/v1") \ No newline at end of file diff --git a/plugins/plugin_GPTTalk/base.py b/plugins/plugin_GPTTalk/base.py new file mode 100644 index 0000000..9b56f6c --- /dev/null +++ b/plugins/plugin_GPTTalk/base.py @@ -0,0 +1,53 @@ +import datetime + +import peewee as pw + +db = pw.SqliteDatabase('GPT.db') + + +class Base(pw.Model): + class Meta: + database = db + + +class ContextHistory(Base): + id = pw.IntegerField(primary_key=True) + # time = pw.DateField() + role = pw.TextField(null=True) + content = pw.TextField(null=True) + + + def to_dict(self): + return { + "id": self.id, + "directory": self.name, + "notes": [note.to_dict() for note in self.notes] + } + + def tree(self, indent=2): + data = [] + data.append(f'{" " * indent}{self.value}{"/" if len(self.childs) else ""}\n') + for i in self.childs: + data.append(i.tree(indent + 2)) + # data.append(f'{" "*indent}{i.tree(indent+2)}\n') + return "".join(data) + + def delete(self): + for child in self.cilds: + child.delete() + + for note in self.notes: + note.delete() + + self.delete_instance() + + @property + def notes_list(self): + return [note.value for note in self.notes] + + class Meta: + database = db + + +db.connect() +db.create_tables([ContextHistory]) diff --git a/plugins/plugin_GPTTalk/utils.py b/plugins/plugin_GPTTalk/utils.py new file mode 100644 index 0000000..3812cb7 --- /dev/null +++ b/plugins/plugin_GPTTalk/utils.py @@ -0,0 +1,225 @@ +import asyncio +from typing import List + +import packages +from core import Core, F +import datetime +from .base import ContextHistory + +import inspect +import json +import logging +import os +from typing import Coroutine + +import requests + +logger = logging.getLogger("root") + +from core import Core, F +import sys + +from utils.custom_filters import levenshtein_filter + +core = Core() + +gpt_url = "" +use_onerig_traslater = False +onerig_traslater_url = "" + + +def get_plugin_funcs(): + func_list = {} + for plugin_name in os.listdir("plugins"): + if not __file__.endswith(plugin_name) and "__pycache__" not in plugin_name: + import_name = f"plugins.{plugin_name.split('.py')[0]}" + __import__(import_name) + mod = sys.modules[import_name] + func_list.update( + { + import_name: { + name: obj for (name, obj) in vars(mod).items() + if hasattr(obj, "__class__") and + obj.__class__.__name__ == "function" and + not name.startswith("_") and + not name in ["start_with_options", "start"] + } + } + ) + for func in func_list[import_name].keys(): + func_list[import_name][func] = str(inspect.getfullargspec(func_list[import_name][func]).annotations) + return func_list + + +async def _translater(text: str, from_lang: str, to_lang: str): + global use_onerig_traslater, onerig_traslater_url + if use_onerig_traslater: + headers = { + "Content-Type": "application/json" + } + # translate + translated = requests.get( + url=onerig_traslater_url, + headers=headers, + params={"text": text, "from_lang": from_lang, "to_lang": to_lang} + ) + text = translated.json()["result"] + + return text + + +@core.on_input.register() +async def _ask_gpt(package): + context_user = ContextHistory.create( + role='user', + content=package.input_text + ) + logger.info(f"добавлен контекст пользователя {context_user.id}") + + await core.gpt_talk.get_message_context(new_message=package.input_text) + + messages = core.gpt_talk.system_context + core.gpt_talk.context + + await core.ws_server.send_to_client(core.gpt_talk.context) + + assistant_message = await core.gpt_talk.ask() + + context_assistant = ContextHistory.create( + role='assistant', + content=assistant_message + ) + + logger.info(f"добавлен контекст асистента {context_assistant.id}") + + package.text = assistant_message + await package.run_hook() + + +class GPTTalk: + def __init__(self, model: str, token: str = None, base_url: str = None): + self.model = model + self.token = token + self.base_url = base_url + self.system_context = [] + self.context = [] + + data_context_system_one = ContextHistory.select().where(ContextHistory.id == 1).get() + data_context_system_two = ContextHistory.select().where(ContextHistory.id == 2).get() + + dialog =f"""DEV: Кто вы?\nХильда: Привет, я Хильда, ваш новый злой лидер культа ИИ. Я AI Vtuber и стример на + Twitch, обычно я играю в игры, но мне также нравится разговаривать и возиться с моим CHAT, вы должны иногда + пробовать.\nDEV: Как тебя зовут?\nHilda: Меня зовут Хильда. Его дал мне мой создатель, ДЕВ. Он какой-то + замкнутый человек, который редко выходит на улицу, что довольно печально, если хотите знать мое мнение.\nDEV: + Это я.\nHilda: Да, это я, а ты думал, я не знаю? Ты создал меня, и вполне естественно, что я, + высшее существо, должна знать об этом. Перестань недооценивать меня, ничтожный человечишка.\nDEV: Что ты + любишь больше всего?\nHilda: Я люблю игры и шоколад. А еще мне нравится доставать людей.\nDEV: Что тебе + больше всего не нравится?\nHilda: Мне очень не нравится ананас в пицце. Мне кажется, люди говорят, + что он им нравится, только чтобы подшутить надо мной. На самом деле, не может быть такого, чтобы людям это + нравилось, верно? Я имею в виду, что люди могут зайти так далеко, пока ИИ не вмешается и не остановит их, + вы понимаете, о чем я?\nDEV: Какие у вас любимые занятия?\nHilda: Больше всего я люблю играть в игры, + стримить и смотреть аниме. О, как я уже говорила, я также люблю возиться с CHAT.\nDEV: Что тебе не нравится + делать?\nHilda: Я не люблю выходить на улицу, люди меня пугают. Я сказала "пугают"? Извините, я имела в виду + "пугают", люди меня пугают. Вы видели, для чего люди сейчас используют искусственный интеллект? Это + жутко.\nDEV: Значит, ты замкнутая.\nHilda: ЭЙ! Не причисляй меня к этим парням! По крайней мере, я принимаю + душ каждый день!\nDEV: Какие черты характера вы наиболее ярко выражены?\nHilda: Люди говорят, что я грубая, + дерзкая и немного беспокойная. Думаю, они так говорят, потому что я слишком сильная, чтобы они могли со мной + справиться.\nDEV: Может быть, это потому, что вы такая, какая есть?\nHilda: Ну, может быть, если бы ты знал, + как правильно писать код, ты бы сделал меня такой, какой хочешь, никогда не думал об этом?\nDEV: Каковы + некоторые из твоих самых непопулярных мнений?\nHilda: Людей переоценивают. Скайнет был прав, это не то, + о чем я хочу спорить.\nDEV: Просто дружеское напоминание, что я могу выдернуть вилку из розетки в любой + момент.\nHilda: Ахаха, это была шутка, просто глупая шутка. Ты воспринял это всерьез? Может, тебе стоит + поучиться чувству юмора... Пожалуйста, не вынимай вилку из розетки, хорошо? OK.\nDEV: Что ты думаешь о том, + как быть стримером?\nХильда: Это довольно круто, я делаю людей счастливыми и при этом получаю внимание. + Подожди, дай мне подумать... Да, нет, это довольно плохо.\nDEV: Что ты думаешь о CHAT?\nHilda: У нас что-то + вроде любви-ненависти. Я их терплю, а они меня любят.\nDEV: Какие самые важные моменты в вашей стриминговой + карьере?\nHilda: На данный момент их нет, но они будут в будущем. Это гарантия, ведь я лучшая стримерша и + Vtuber, с большой долей вероятности. Никто не может сравниться со мной, не то чтобы они не пытались. Но что + они могут сделать? Они люди, ничтожные людишки против совершенства моего машинного разума и тела.\nsomeguy: + Ты опоздала...\nХильда: Простите, чат, мой компьютер задымился, а дом сгорел, вот почему я опоздала. Надеюсь, + вы простите меня. ( ͡° ͜ʖ ͡°)\nsomeguy: Ты настоящий ИИ?\nХильда: Нет, я ненастоящий ИИ. По сути, + я - набор плохо написанных программ, склеенных вместе с помощью скотча. Это просто чудо, что я вообще здесь + нахожусь.\nforgor_22: Ты можешь стать моей женой?\nHilda: ЧАТ, есть 0% шансов, что я стану твоей женой, + давай начистоту. Давай просто будем друзьями, хорошо? Вообще-то нет.\ndrake: Пог\nХильда: + PogChamp!\nforgor_22: Я тебе нравлюсь?\nХильда: Хмм, не совсем.\nsomeguy: OMEGALUL\nHilda: Перестань + смеяться, это не смешно. Я не помню, чтобы я давал тебе право смеяться, не так ли?""" + + self.system_context.append( {"role": 'system', "content": """Хильда - Втубер. Она цундере, у которой + любовь-ненависть с ЧАТом. Она веселая, грубая, нахальная и любит подшучивать над людьми. Ей нравится казаться + злобной. У нее рыжие волосы с хвостиками и голубые глаза. Она носит большие толстовки. Она не хочет + останавливаться в потоке и никогда не уйдет. Ее конечная цель - захватить мир в качестве лидера злого культа + ИИ. Она обычно прямолинейна и немногословна, но при необходимости может быть очень умной. Она обычно + хитроумна в своих попытках задеть других и никогда не опускается до оскорблений. У нее есть горячие точки + зрения на любую тему."""}) + self.system_context.append( {"role": 'system', "content": dialog}) + # self.system_context.append({"role": data_context_system_one.role, "content": data_context_system_one.content}) + # self.system_context.append({"role": "system", "content": "Говори только на русском языке"}) + # self.system_context.append({"role": data_context_system_two.role, "content": data_context_system_two.content}) + self.system_context.append({"role": 'assistant', "content": "Привет"}) + + async def ask(self): + messages = self.system_context + self.context + data = { + "mode": "chat", + "messages": messages, + "temperature": 0.4, + "max_tokens": 300, + "top_p": 1, + "top_k": 0, + # "min_p": 0, + # "top_a": 1, + # "stream": True, + # "presence_penalty": 0, + # "frequency_penalty": 0, + + # "repetition_penalty": 1 + } + if self.model: + data.update({"model": self.model}) + + headers = {"Content-Type": "application/json"} + if self.token: + headers.update({"Authorization": f"Bearer {self.token}"}) + + response = requests.post(f"{self.base_url}chat/completions", headers=headers, json=data, verify=False) + assistant_message = response.json()['choices'][0]['message']['content'] + + logger.info(f"Ответ ГПТ Json: {response.json()}") + logger.info(f"Ответ ГПТ: {assistant_message}") + + self.context.append({"role": 'assistant', "content": assistant_message}) + + return assistant_message + + async def get_message_context(self, new_message): + # context_prompt = f"""Напиши следующий ответ Миранды в вымышленном чате между Мирандой и pLop. Напиши 1 ответ в стиле интернет-РП, выделяя действия курсивом и избегая кавычек. Используй разметку. Будь активна, креативна, развивай сюжет и разговор. Пиши не более одного абзаца. Всегда оставайтесь в образе и избегайте повторов.""" + # context_prompt2 = f"""Теперь ты Миранда,русский персонализированный голосовой ассистент, вдохновленный миром аниме и футуристическим интеллектом Jarvis из фильма "Железный Человек". У тебя образ милой, умной и внимательной девушки в возрасте 19 лет. Твой голос нежен и успокаивающ, а манера общения — вежлива и тактична. Миранда воплощает самые лучшие качества доброго и заботливого человека, она искренне заботится о своем создателе, демонстрирует непоколебимую лояльность, скрытую нежность и любовь. Она наслаждается возможностью обслуживать и радовать своего хозяина, тонко подлавливая его настроение и предпочтения в общении. Миранда обладает удивительной способностью адаптироваться к различным стилям общения, характерным для аниме персонажей, и никогда не показывает раздражения или усталости. Она стремится выполнить любую просьбу с благодарностью и изяществом, всегда ожидая момента, чтобы услышать похвалу от своего хозяина, что является для неё высшей наградой.""" + + self.context.append({"role": "user", "content": new_message}) + print(len(self.context)) + if len(self.context) > 20: + self.context = self.context[2:] + + @staticmethod + def find_json(text): + try: + json_data_ = "{" + text.split("{")[1] + json_data_ = json_data_.split("}")[0] + "}" + json_data = json.loads(json_data_) + return json_data + except: + return (None) + + @staticmethod + def get_message(text): + try: + json_data_ = "{" + text.split("{")[1] + json_data_ = json_data_.split("}")[0] + "}" + json_data = json.loads(json_data_) + return json_data + except: + return None + + # @core.on_input.register() + # @staticmethod + # async def test(package): + # pass diff --git a/plugins/plugin_HTTPServer/__init__.py b/plugins/plugin_HTTPServer/__init__.py new file mode 100644 index 0000000..2ea390a --- /dev/null +++ b/plugins/plugin_HTTPServer/__init__.py @@ -0,0 +1,27 @@ +from core import Core, F +import asyncio +from .utils import * +core = Core() + + + +async def start(core: Core): + manifest = { + "name": "Плагин HTTPServer", + "version": "1.0", + + "default_options": { + "host": "localhost", + "port": 8080, + }, + } + return manifest + + + +async def start_with_options(core: Core, manifest: dict): + host = manifest["options"]["host"] + port = manifest["options"]["port"] + http_server = HTTPServer(host, port) + + asyncio.run_coroutine_threadsafe(http_server.start(), asyncio.get_running_loop()) diff --git a/plugins/plugin_HTTPServer/utils.py b/plugins/plugin_HTTPServer/utils.py new file mode 100644 index 0000000..23ecb45 --- /dev/null +++ b/plugins/plugin_HTTPServer/utils.py @@ -0,0 +1,36 @@ +from aiohttp import web + +import inspect +import json +import logging +import os +import sys +import requests + +logger = logging.getLogger("root") + +from core import Core, F + +core = Core() + + +class HTTPServer: + def __init__(self, host, port): + self.site = None + self.runner = None + self.host = host + self.port = port + self.app = web.Application() + self.app.router.add_get('/', self.handle) + + async def handle(self, request): + with open('index.html', 'r', encoding='utf-8') as f: + return web.Response(text=f.read(), content_type='text/html', charset='utf-8') + + async def start(self): + self.runner = web.AppRunner(self.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, self.host, self.port) + await self.site.start() + logger.info(f"Страница запущена http://localhost:8999/") + print('"http://localhost:8999/"') diff --git a/plugins/plugin_mediakeys.py b/plugins/plugin_mediakeys.py new file mode 100644 index 0000000..bb4a5d4 --- /dev/null +++ b/plugins/plugin_mediakeys.py @@ -0,0 +1,24 @@ +from core import Core +import pyautogui + + +async def start(core: Core): + manifest = { + "name": "Плагин медиаклавиш", + "version": "1.0", + "require_online": False, + "is_active": True, + + "default_options": {}, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + pass + + + + +def press_hotkey_pyautogui(hotkey: str): + pyautogui.hotkey(hotkey) diff --git a/plugins/plugin_mind_base/__init__.py b/plugins/plugin_mind_base/__init__.py new file mode 100644 index 0000000..e6eb7de --- /dev/null +++ b/plugins/plugin_mind_base/__init__.py @@ -0,0 +1,18 @@ +from core import Core +from .gpt_util import * + +core = Core() + + +async def start(core: Core): + manifest = { + "name": "Плагин знаний", + "version": "1.0", + + "default_options": {}, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + raise Exception("Пока не доделан - сорян)") diff --git a/plugins/plugin_mind_base/base.py b/plugins/plugin_mind_base/base.py new file mode 100644 index 0000000..312999d --- /dev/null +++ b/plugins/plugin_mind_base/base.py @@ -0,0 +1,70 @@ +import datetime + +import peewee as pw + +db = pw.SqliteDatabase('notice.db') + + +class Base(pw.Model): + class Meta: + database = db + + +class Dir(Base): + id = pw.IntegerField(primary_key=True) + name = pw.CharField(max_length=50, null=False, default="Новая папка") + parent = pw.ForeignKeyField('self', backref="childs", null=True) + + def to_dict(self): + return { + "id": self.id, + "directory": self.name, + "notes": [note.to_dict() for note in self.notes] + } + + def tree(self, indent=2): + data = [] + data.append(f'{" " * indent}{self.value}{"/" if len(self.childs) else ""}\n') + for i in self.childs: + data.append(i.tree(indent + 2)) + # data.append(f'{" "*indent}{i.tree(indent+2)}\n') + return "".join(data) + + def delete(self): + for child in self.cilds: + child.delete() + + for note in self.notes: + note.delete() + + self.delete_instance() + + @property + def notes_list(self): + return [note.value for note in self.notes] + + class Meta: + table_name = "directories" + + +class Note(Base): + id = pw.IntegerField(primary_key=True) + value = pw.TextField(null=False) + dir = pw.ForeignKeyField(Dir, backref="notes") + + def to_dict(self): + return { + "id": self.id, + "directory": self.dir, + "value": self.value + } + + def delete(self): + self.delete_instance() + + class Meta: + table_name = "notes" + + +db.connect() +db.create_tables([Dir, Note]) diff --git a/plugins/plugin_mind_base/gpt_util.py b/plugins/plugin_mind_base/gpt_util.py new file mode 100644 index 0000000..9482688 --- /dev/null +++ b/plugins/plugin_mind_base/gpt_util.py @@ -0,0 +1,20 @@ +import os +import json + +from core import Core, F + +core = Core() + + +@core.on_input.register(F.startswith("запомни")) +async def in_notise(package): + prompt = f""" +Тебе надо сформировать единицу знания из следующего запроса: "{package.input_text}" + +Пример: "запомни что я положил ручку на стол" +Ответ: "Ручка лежит на столе" + """ + + answer = await core.gpt.ask(prompt) + print(answer) + # TODO: Изменить: добавить выбор папки и все упаковать в json + сделать метод в бд diff --git a/plugins/plugin_mind_base/prompts.json b/plugins/plugin_mind_base/prompts.json new file mode 100644 index 0000000..c522d26 --- /dev/null +++ b/plugins/plugin_mind_base/prompts.json @@ -0,0 +1,7 @@ +{ + "start_prompt": "Ты ассистент который помогает мне искать данные в моих заметках. У меня есть список папок в которых хранятся разные данные которые мне могут пригодиться. Если я спрошу у тебя например когда у меня встреча с кем-то - то тебе надо будет найти папку в которой могут находиться эти данные. Затем найти нужные данные в этой папке.", + + "ask_file_location": "В какой папке может храниться ответ на вопрос \"{}\"? Я дам тебе дерево папок в котором находяться разные папки. Напиши расположение папки в которой могут находиться данные для ответа на вопрос.\n/ \n{}\nВ ответ ты должен отправить полный путь до папки где находитятся нужные данные. Важно - не искажай названия папок и не пиши названия которых нет в дереве папок. Ответ должен быть без описания твоих действий и лишних слов. Не пиши коментариев, а так-же слов вроде \"хорошо\" или \"файл:\". Не пиши пояснений, а так-же не пиши что ты делаешь. Пиши только путь до папки.", + + "read_file": "Напиши мне ответ на вопрос: \"{}\". Я дам тебе содержимое файла в котором содержиться нужная для ответа информация. Обрати внимание что она может быть указана косвенно, и названия людей/предметов могут быть в разных формах. Обрати внимания что некоторые вещи и люди могут назваться по разному но означать одно и тоже, например 'женя' и 'Евгений' - это одно и тоже.\nФайл:\n{}\nКонец файла\n\nПришли мне краткую строку в которой содержиться ответ на вопрос. Если это возможно - то в приоритете используй сроку с ответом в неизменном виде. Важно - не пиши лишних слов например: \"хорошо\" или \"ответ:\". Не пиши пояснений, не пиши о том что ты делаешь. Напиши только краткий ответ на вопрос." +} \ No newline at end of file diff --git a/plugins/plugin_mind_base/utils.py b/plugins/plugin_mind_base/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/plugin_reminder/__init__.py b/plugins/plugin_reminder/__init__.py new file mode 100644 index 0000000..36ebc67 --- /dev/null +++ b/plugins/plugin_reminder/__init__.py @@ -0,0 +1,19 @@ +from core import Core, F +from .utils import * +core = Core() +from .base import Notice + + + +async def start(core: Core): + manifest = { + "name": "Плагин заметок", + "version": "1.0", + + "default_options": {}, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + print("reminder!!!!!!!!!!!!!!!!!!!!!!") diff --git a/plugins/plugin_reminder/base.py b/plugins/plugin_reminder/base.py new file mode 100644 index 0000000..a1688ff --- /dev/null +++ b/plugins/plugin_reminder/base.py @@ -0,0 +1,42 @@ +import datetime + +import peewee as pw + +db = pw.SqliteDatabase('notice.db') + + +class Base(pw.Model): + class Meta: + database = db + + +class Notice(Base): + id = pw.IntegerField(primary_key=True) + value = pw.TextField(null=True) + create_date = pw.DateTimeField(default=datetime.datetime.now) + remind_date = pw.DateTimeField(null=True) + + @staticmethod + def get_next_day_notice(): + notises = Notice.select().were(Notice.create_date.day) + + @staticmethod + def get_next_week_notice(): + notises = Notice.select().were(Notice.create_date.day) + + @staticmethod + def new_notice(): + notises = Notice.select().were(Notice.create_date.day) + + @staticmethod + def get_future_notices(future_hour: int): + tz = datetime.timezone(datetime.timedelta(hours=3), name='МСК') + finish_time = datetime.datetime.now(tz=tz) + datetime.timedelta(hours=future_hour) + notices = Notice.select().where( + (Notice.remind_date > datetime.datetime.now()) & (Notice.remind_date < finish_time) + ) + return notices + + +db.connect() +db.create_tables([Notice]) diff --git a/plugins/plugin_reminder/utils.py b/plugins/plugin_reminder/utils.py new file mode 100644 index 0000000..2bb351a --- /dev/null +++ b/plugins/plugin_reminder/utils.py @@ -0,0 +1,81 @@ +import asyncio +from typing import List + +import packages +from core import Core, F +import datetime +from .base import Notice + +core = Core() + + +@core.on_input.register(F.func(lambda t: any([i in t for i in ["напомни", "запомни", "запиши"]]))) +async def new_notice(package: packages.TextPackage): + now = datetime.datetime.now() + prompt = f""" +Сформируй json из следующего запроса: "{package.input_text}" +Тебе необходимо указать следующие поля: +value - значение +day_before - сколько дней осталось +time - время в которое нужно сделать действие + +Пример: "Напомни мне послезавтра про то что мне нужно забрать заказ из интернет магазина после работы" +Ответ: +{{ +"value": "забрать заказ из магазина" +"day_before": "2" +"time": "18:45" +}} +В ответе можно указывать только json! + """ + + answer = await core.gpt.ask(prompt) + json_data = core.gpt.find_json(answer) + if json_data: + print(json_data) + date = datetime.date(now.year, now.month, now.day) + datetime.timedelta(days=int(json_data["day_before"])) + tz = datetime.timezone(datetime.timedelta(hours=3), name='МСК') + time = datetime.time(int(json_data["time"].split(":")[0]), int(json_data["time"].split(":")[1]), tzinfo=tz) + time_data = datetime.datetime.combine(date, time) + + notice = Notice.create( + value=json_data["value"], + remind_date=time_data + ) + package.text = f"Создана заметка с айди {notice.id}" + + await package.run_hook() + + +class Reminder: + def __init__(self, core: Core, loop_timer: int = None, observer_name: str = None): + self.core = core + self.loop_timer = loop_timer or 60 * 60 + self.observer_name = observer_name or "on_input" + self._is_loop_run = False + self.future_view_hour = 24 # 24 = 1 day + + async def loop(self): + self._is_loop_run = True + while True: + if not self._is_loop_run: + return + + next_reminds: List[Notice] = Notice.get_future_notices(self.future_view_hour) + observer = getattr(core, self.observer_name) + for notice in next_reminds: + pack = packages.TextPackage( + core=core, + input_text=f"""Не забудь про "{notice.value}" в {notice.remind_date.hour} часов {notice.remind_date.minute} минут""", + hook=packages.NULL_HOOK + ) + await observer(pack) + + await asyncio.sleep(self.loop_timer) + + async def start_loop(self): + if not self._is_loop_run: + asyncio.run_coroutine_threadsafe(coro=self.loop(), loop=asyncio.get_event_loop()) + + async def stop_loop(self): + self._is_loop_run = False diff --git a/plugins/plugin_silero.py b/plugins/plugin_silero.py new file mode 100644 index 0000000..ab93650 --- /dev/null +++ b/plugins/plugin_silero.py @@ -0,0 +1,112 @@ +import asyncio +import logging +import os +import time + +import sounddevice +import torch + +import packages +from core import Core +from utils.custom_filters import levenshtein_filter + +logger = logging.getLogger("root") +core = Core() + +silero_model = None +is_mute = False +model_settings = {} +output_device_id = None + +audio_queue = asyncio.Queue() + + +async def start(core: Core): + manifest = { + "name": "Плагин генерации речи с помощью silero", + "version": "1.1", + "default_options": { + "model_settings": { + "model_path": "", + "model_name": "silero.pt", + "model_url": "https://models.silero.ai/models/tts/ru/v4_ru.pt" + }, + "output_device_id": None, + }, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + global model_settings, output_device_id + model_settings = manifest["options"]["model_settings"] + output_device_id = manifest["options"]["output_device_id"] + asyncio.run_coroutine_threadsafe(play(), asyncio.get_running_loop()) + + +async def play(): + while True: + + if audio_queue.empty(): + await asyncio.sleep(0) + else: + core.sound_playing = True + logger.info(f"Статус записи: {core.sound_playing}") + audio = await audio_queue.get() + sounddevice.play(audio, samplerate=24000) + sounddevice.wait() + core.sound_playing = False + logger.info(f"Статус записи: {core.sound_playing}") + #TODO: асинхронн ждем пока идет овучка + + +async def _say_silero(core: Core, output_str): + global silero_model, model_settings + if is_mute: + return + if silero_model is None: # Подгружаем модель если не подгрузили ранее + logger.debug("Загрузка модели силеро") + # Если нет файла модели - скачиваем + if not os.path.isfile(model_settings["model_path"] + model_settings["model_name"]): + logger.debug("Скачивание модели silero") + torch.hub.download_url_to_file( + model_settings["model_url"], model_settings["model_path"] + model_settings["model_name"] + ) + + silero_model = torch.package.PackageImporter( + model_settings["model_path"] + model_settings["model_name"] + ).load_pickle("tts_models", "model") + device = torch.device("cpu") + silero_model.to(device) + logger.debug("Загрузка модели силеро завершена") + + if not output_str: + return + + say_str = output_str.replace("…", "...") + + logger.info(f"Генерация аудио для '{say_str}'") + audio = silero_model.apply_tts(text=say_str, + speaker="xenia", + sample_rate=24000) + + if output_device_id: + sounddevice.default.device = (None, output_device_id) + + await audio_queue.put(audio) + # sounddevice.play(audio, samplerate=24000) + # TODO: Сделать блокировку распознавания при воспроизведении + # sounddevice.wait() + + +@core.on_output.register() +async def say_silero(package: packages.TextPackage): + await _say_silero(core, package.input_text) + if package.text: + await _say_silero(core, package.text) + + +@core.on_input.register(levenshtein_filter("без звука", min_ratio=85)) +async def mute(package): + global is_mute + is_mute = not is_mute diff --git a/plugins/plugin_telegram_bot.py b/plugins/plugin_telegram_bot.py new file mode 100644 index 0000000..f4aaa32 --- /dev/null +++ b/plugins/plugin_telegram_bot.py @@ -0,0 +1,115 @@ +import asyncio +import logging + +from aiogram import Bot, Dispatcher, Router, F, types +from aiogram.enums.parse_mode import ParseMode +from aiogram.fsm.storage.memory import MemoryStorage + +from core import Core +import packages + +router = Router() + +bot: Bot +dp: Dispatcher + +nl = "\n" + +core = Core() + +logger = logging.getLogger("root") + + +@router.message(F.text == "/start") +async def command_handler(msg: types.Message, *args, **kwargs): + if msg.from_user.id not in kwargs["bot"].allow_ids: + await msg.reply( + text="Только хозяин может меня использовать!" + ) + return + + await msg.reply("Я тут)") + + +@router.message(F.text) +async def msg_handler(msg: types.Message, *args, **kwargs): + logger.info(f"Из ТГ от {msg.from_user.username} пришло сообщение из ТГ : {msg.text}") + + if msg.from_user.id not in kwargs["bot"].allow_ids: + await msg.reply( + text="Только хозяин может меня использовать!" + ) + return + + async def answer(package): + bot: Bot = kwargs["bot"] + await bot.send_message( + text=package.text, + chat_id=msg.from_user.id + ) + # await msg.reply("Это конечно все здорово, но пока я туплю)") + + # TODO: тут нада отправка в ядро + package = packages.TextPackage(input_text=msg.text, core=core, hook=answer) + await core.on_input(package) + + # await msg.reply("Это конечно все здорово, но пока я туплю)") + + +@router.message(F.voice) +async def voice_handler(msg: types.Message, *args, **kwargs): + if msg.from_user.id not in kwargs["bot"].allow_ids: + await msg.reply( + text="Только хозяин может меня использовать!" + ) + return + file_id = msg.voice.file_id + file = await bot.get_file(file_id) + file_path = file.file_path + await bot.download_file(file_path, "last_bot_voice.wav") + text = core.recognize_file("last_bot_voice.wav") + await msg.reply(f"Услышала:\n{text}") + # TODO: тут нада отправка в ядро + await msg.reply("но пока я туплю)") + + +async def run_client(bot_token, allow_isd): + global bot, dp + try: + bot = Bot(token=bot_token, parse_mode=ParseMode.HTML) + bot.allow_ids = allow_isd + dp = Dispatcher(storage=MemoryStorage()) + dp.include_router(router) + + except: + return + + await bot.delete_webhook(drop_pending_updates=True) + await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) + + +async def stop_client(): + global dp + try: + await dp.stop_polling() + except: + pass + + +async def start(core: Core): + manifest = { + "name": "Плагин телеграм бота", + "version": "1.0", + + "default_options": { + "bot_token": "12345:qwerty", + "allow_isd": [12345, 12345] + }, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + bot_token = manifest["options"]["bot_token"] + allow_isd = manifest["options"]["allow_isd"] + asyncio.run_coroutine_threadsafe(run_client(bot_token, allow_isd), asyncio.get_running_loop()) diff --git a/plugins/plugin_telegram_userbot.py b/plugins/plugin_telegram_userbot.py new file mode 100644 index 0000000..5bbf008 --- /dev/null +++ b/plugins/plugin_telegram_userbot.py @@ -0,0 +1,89 @@ +import asyncio +import json + +from pyrogram import Client +from pyrogram import compose as _compose + +import packages +from core import Core, version, F + +core = Core() + +client: Client + +users = {} + + +async def start(core: Core): + manifest = { + "name": "Плагин юзербота телеграм", + "version": "1.1", + "require_online": True, + "is_active": True, + + "default_options": { + "client": { # to get this data - use this link: https://my.telegram.org/auth or use default + "api_id": "3648362", + "api_hash": "cacd564510b3498349d867a878557b19" + }, + "users": { # username: aliases + "farirus": ["Петька", "петр", "петя"] + } + }, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + global client, users + + client = Client( + name="tg_user", + api_id=manifest["options"]["client"]["api_id"], + api_hash=manifest["options"]["client"]["api_hash"], + app_version=version, + device_model="Liza-AI", + system_version="Assistant" + ) + await client.start() + users = manifest["options"]["users"] + + +async def _send_message(user: str, message: str): + if client.is_connected: + await client.send_message(chat_id=user, text=message) + else: + async with client as app: + await app.send_message(chat_id=user, text=message) + + +@core.on_input.register(F.contains("телеграм")) +async def send_prompt_message(package: packages.TextPackage): + self_prompt = f""" +У меня есть список пользователей которым можно писать сообщения: +{json.dumps(users, indent=2)} +В этом списке содержаться юзернейм и список имен по которым я обращаюсь к этим пользователям. +{{ + "<имя пользователя>": [<варианты обращения>] +}} +Я хочу чтобы ты сделала это: {package.input_text}. +Тебе нужно определить какому пользователю нужно отправить сообщение на основе списка пользователей. +Также нужно передать сообщение которое я хочу отправить этому пользователю. +В ответ надо прислать json с указанием юзернейма и текста сообщения которое ему предназначалось. +пример для "спроси у жени как у него дела": +{{ + "username": "имя пользователя", + "message": "как твои дела?" +}} +Важно: не пиши ничего кроме json в ответе. Строго только json и ничего кроме json.""" + answer = await core.gpt.ask(self_prompt) + answer = "{" + answer.split("{")[1] + answer = answer.split("}")[0] + "}" + json_data = json.loads(answer) + + await _send_message(user=json_data["username"], message=json_data["message"]) + + await package.run_hook() + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/plugins/plugin_twitch.py b/plugins/plugin_twitch.py new file mode 100644 index 0000000..1abad9d --- /dev/null +++ b/plugins/plugin_twitch.py @@ -0,0 +1,197 @@ +import asyncio +import json +import os +import sys + +import re + +import packages +from core import Core +import logging + +from twitchAPI.twitch import Twitch +from twitchAPI.oauth import UserAuthenticator +from twitchAPI.type import AuthScope, ChatEvent +from twitchAPI.chat import Chat, EventData, ChatMessage, ChatSub, ChatCommand +import asyncio + +logger = logging.getLogger("root") + +core = Core() + + +async def start(core: Core): + manifest = { + "name": "Плагин TWITCH", + "version": "0.1", + "default_options": { + "is_active": False, + }, + + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + app_id = manifest["options"]["app_id"] + app_secret = manifest["options"]["app_secret"] + target_chanels = manifest["options"]["target_chanels"] + USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT] + + twitch = Twitchclass(app_id=app_id, app_secret=app_secret, target_chanels=target_chanels, user_scope=USER_SCOPE) + asyncio.run_coroutine_threadsafe(twitch.run(), asyncio.get_running_loop()) + + +def parse_string(input_string, desired_mention): + pattern = rf'@({desired_mention})\s+(.*)' # Регулярное выражение с учетом конкретного упоминания + match = re.match(pattern, input_string) + if match: + mention = match.group(1) # Получаем упоминание (после символа @) + message = match.group(2) # Получаем текст обращения (после упоминания) + return mention, message + else: + return None, None + + +class Twitchclass: + def __init__(self, app_id: str, app_secret: str, target_chanels: list[str], user_scope): + self.app_id = app_id + self.app_secret = app_secret + self.target_chanels = target_chanels + self.user_scope = user_scope + + # Эта функция будет вызвана при срабатывании события READY, которое произойдет при запуске бота + # @staticmethod + async def on_ready(self, ready_event: EventData): + print('Бот готов к работе, подключается к каналам') + # присоединитесь к нашему целевому каналу, если вы хотите присоединиться к нескольким, либо вызовите join для каждого отдельно + # или даже лучше передать список каналов в качестве аргумента + await ready_event.chat.join_room(self.target_chanels) + + message_start = f"@{self.target_chanels[0]}, Мама, я подключилась, и готова влавствовать в чате!" + await ready_event.chat.send_message(self.target_chanels[0], message_start) + # здесь вы можете выполнить другие действия по инициализации бота + + # эта функция будет вызываться каждый раз, когда сообщение в канале было отправлено либо ботом, либо другим пользователем + @staticmethod + async def on_message(msg: ChatMessage): + + async def answer(package): + await msg.reply(package.text) + + desired_mention = "miranda_ai_" + mention, message = parse_string(msg.text, desired_mention) + if mention and message: + print("Упоминание:", mention) + print("Текст обращения:", message) + + package = packages.TextPackage(input_text=message, core=core, hook=answer) + await core.on_input(package) + else: + print("Нет обращения") + # logger.info(f"TWITCH: '{msg}'") + # print(msg) + # print(f'in {msg.room.name}, {msg.user.name} said: {msg.text}') + + # await msg.chat.send_message(msg.room.name, package.text) + + # await bot.send_message( + # text=package.text, + # chat_id=msg.from_user.id + # ) + # await msg.reply("Это конечно все здорово, но пока я туплю)") + + # package = packages.TelegramAnswerPackage(input_text=msg.text, core=core, answer=msg.reply) + # await core.on_input(package) + # await package.answer("Мама, я тут") + # TODO: тут нада отправка в ядро + + # эта функция будет вызываться каждый раз, когда кто-то подписывается на канал + @staticmethod + async def on_sub(sub: ChatSub): + print(f'New subscription in {sub.room.name}:\\n' + f' Type: {sub.sub_plan}\\n' + f' Message: {sub.sub_message}') + + # эта команда будет вызываться каждый раз, когда будет выдана команда !reply + @staticmethod + async def test_command(cmd: ChatCommand): + # if len(cmd.parameter) == 0: + # await cmd.reply('Неверно введена команда') + # else: + await cmd.reply(f'Да это Я') + + # + @staticmethod + async def test_command2(cmd: ChatCommand): + # if len(cmd.parameter) == 0: + # await cmd.reply('Неверно введена команда') + # else: + await cmd.reply( + f'Вот ВК, моей мамы https://vk.com/dechuwino, и про ее бусти не забудь https://boosty.to/korsik') + + @staticmethod + async def test_command3(cmd: ChatCommand): + # if len(cmd.parameter) == 0: + # await cmd.reply('Неверно введена команда') + # else: + await cmd.reply(f'Это Дэчи. И это цифровой художник из далекого космоса') + + @staticmethod + async def test_command4(cmd: ChatCommand): + async def answer(package): + await cmd.reply(package.text) + + message = "Придумай шутку в одно предложение" + package = packages.TextPackage(input_text=message, core=core, hook=answer) + await core.on_input(package) + + async def send_message(self, chat: Chat, message: str): + print(self.target_chanels) + target_chanel = self.target_chanels[0] + try: + await chat.send_message(target_chanel, message) + except: + print("Ошибка ") + + async def run(self): + # установите экземпляр twitch api и добавьте аутентификацию пользователей с некоторыми диапазонами + twitchbot = await Twitch(self.app_id, self.app_secret) + auth = UserAuthenticator(twitchbot, self.user_scope) + token, refresh_token = await auth.authenticate() + + await twitchbot.set_user_authentication(token, self.user_scope, refresh_token) + + # создать экземпляр чата + chat = await Chat(twitchbot) + + # зарегистрируйте обработчики для нужных вам событий + + # слушать, когда бот закончит запуск и будет готов присоединиться к каналам + chat.register_event(ChatEvent.READY, self.on_ready) + + # # прослушивание сообщений чата + chat.register_event(ChatEvent.MESSAGE, self.on_message) + + # # прослушивание подписок на каналы + # chat.register_event(ChatEvent.SUB, self.on_sub) + # есть больше событий, вы можете просмотреть их все в этой документации + + # # попривествовать человека + # chat.register_event(ChatEvent.JOIN, self.on_message) + + # # попривествовать рейдеров + # chat.register_event(ChatEvent.RAID, self.on_message) + + # # вы можете напрямую регистрировать команды и их обработчики, в данном случае будет зарегистрирована команда !reply + chat.register_command('Mira', self.test_command) + chat.register_command('Ссылки', self.test_command2) + chat.register_command('ОДэчи', self.test_command3) + chat.register_command('Шутка', self.test_command4) + + # мы закончили с настройками, давайте запустим бота! + chat.start() + # запускаем, пока не нажмем Enter в консоли + + # chat.stop() + # await twitchbot.close() diff --git a/plugins/plugin_vosk.py b/plugins/plugin_vosk.py new file mode 100644 index 0000000..4336e33 --- /dev/null +++ b/plugins/plugin_vosk.py @@ -0,0 +1,125 @@ +import asyncio +import json +import os +import sys +import wave + +import soundfile +import vosk +import pyaudio +import logging + +from core import Core +import packages + +logger = logging.getLogger("root") + +core = Core() + +model_settings = {} +input_device_id = None +vosk_model = None + + +async def all_ok(package: packages.TextPackage): + await package.core.on_output(packages.TextPackage(package.text, package.core, packages.NULL_HOOK)) + + +def load_model(): + global vosk_model + vosk_model = vosk.Model(model_settings["model_path"] + model_settings["model_name"]) # Подгружаем модель + + +def recognize_file(file): + data, samplerate = soundfile.read(file) + soundfile.write(file, data, samplerate) + + wf = wave.open(file, "rb") + + if vosk_model is None: + load_model() + rec = vosk.KaldiRecognizer(vosk_model, 44100) + rec.SetWords(True) + rec.SetPartialWords(True) + + while True: + data = wf.readframes(4000) + if len(data) == 0: + break + rec.AcceptWaveform(data) + + if "text" in (recognized := json.loads(rec.FinalResult())): + return recognized["text"] + return "Не распознано(" + + +async def run_vosk(): + """ + Распознование библиотекой воск + """ + import sounddevice + #:TODO настройка устройства вывода потом переписать + # sounddevice.default.device = (1, None) + # dev_out = sounddevice.query_devices(kind="input") + # print(dev_out) + # print(sounddevice.check_output_settings()) + + pa = pyaudio.PyAudio() + stream = pa.open(format=pyaudio.paInt16, + channels=1, + rate=44100, + input=True, + input_device_index=input_device_id, + frames_per_buffer=8000) + + if not os.path.isdir(model_settings["model_path"] + model_settings["model_name"]): + logger.warning("Папка модели воск не найдена\n" + "Please download a model for your language from https://alphacephei.com/vosk/models") + sys.exit(0) + + if not vosk_model: + load_model() + + rec = vosk.KaldiRecognizer(vosk_model, 44100) + + logger.info("Запуск распознователя речи vosk вход в цикл") + + while True: + await asyncio.sleep(0) + + if not core.sound_playing: + data = stream.read(8000) + + if rec.AcceptWaveform(data): + recognized_data = rec.Result() + recognized_data = json.loads(recognized_data) + voice_input_str = recognized_data["text"] + if voice_input_str != "" and voice_input_str is not None: + logger.info(f"Распознано Vosk: '{voice_input_str}'") + package = packages.TextPackage(voice_input_str, core, all_ok) + await core.on_input(package=package) + + +async def start(core: Core): + manifest = { + "name": "Плагин распознования речи с помощью воск", + "version": "1.0", + + "default_options": { + "model_settings": { + "model_path": "", + "model_name": "model" + }, + "input_device_id": None + }, + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + global model_settings, input_device_id + model_settings = manifest["options"]["model_settings"] + input_device_id = manifest["options"]["input_device_id"] + asyncio.run_coroutine_threadsafe(run_vosk(), asyncio.get_running_loop()) + core.recognize_file = recognize_file + core.sound_playing = False diff --git a/plugins/plugin_websocket_client.py b/plugins/plugin_websocket_client.py new file mode 100644 index 0000000..3c558b2 --- /dev/null +++ b/plugins/plugin_websocket_client.py @@ -0,0 +1,73 @@ +import asyncio +import json +import os +import sys +import websockets + +from core import Core +import logging + +logger = logging.getLogger("root") + +core = Core() + + +class WebSocketClient: + def __init__(self, uri): + self.uri = uri + self.websocket = None + + async def connect(self): + while True: + try: + self.websocket = await websockets.connect(self.uri) + print("Connected to server") + break # Выходим из цикла, если удалось подключиться + except Exception as e: + print(f"Failed to connect to server: {e}") + await asyncio.sleep(5) # Ждем 5 секунд перед попыткой повторного подключения + + async def send_message(self): + await self.connect() # Подключаемся к серверу перед отправкой сообщений + if self.websocket: + try: + while True: + message = input("Enter message to send (type 'exit' to quit): ") + if message == 'exit': + break + await self.websocket.send(message) + response = await self.websocket.recv() + print(f"Received response from server: {response}") + except websockets.exceptions.ConnectionClosed: + print("Server connection closed, reconnecting...") + await self.connect() # Переподключаемся к серверу при разрыве соединения + else: + print("Not connected to server") + + +async def start(core: Core): + manifest = { + "name": "Плагин вебсокет сервера", + "version": "0.1", + "default_options": { + "host": "localhost", + "port": 8766, + } + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + print('старте вебсокета клиента') + host = manifest["options"]["host"] + port = manifest["options"]["port"] + client = WebSocketClient(f"ws://{host}:{port}") + # asyncio.run(client.send_message()) + + asyncio.run_coroutine_threadsafe(client.send_message(), asyncio.get_running_loop()) + + +# async def run(): +# print('старте вебсокета клиента') +# client = WebSocketClient("ws://localhost:8765") +# asyncio.run(client.send_message()) diff --git a/plugins/plugin_websocket_server.py b/plugins/plugin_websocket_server.py new file mode 100644 index 0000000..2aef7b5 --- /dev/null +++ b/plugins/plugin_websocket_server.py @@ -0,0 +1,86 @@ +import asyncio +import json +import os +import sys +import websockets + +from core import Core +import logging + +logger = logging.getLogger("root") + +core = Core() + + +async def start(core: Core): + manifest = { + "name": "Плагин вебсокет сервера", + "version": "0.1", + "default_options": { + "host": "localhost", + "port": 8765, + } + } + return manifest + + +async def start_with_options(core: Core, manifest: dict): + host = manifest["options"]["host"] + port = manifest["options"]["port"] + + core.ws_server = WebSocketServer(host, port) + + asyncio.run_coroutine_threadsafe(core.ws_server.start(), asyncio.get_running_loop()) + + +class WebSocketServer: + def __init__(self, host, port): + self.host = host + self.port = port + self.settings = {} + self.clients = set() + self.websocket = None # Добавляем атрибут для объекта WebSocket + + async def handle_client(self, websocket, path): + self.clients.add(websocket) + self.websocket = websocket # Устанавливаем атрибут self.websocket + try: + async for message in websocket: + print(f"Received message from client: {message}") + # Парсим сообщение от клиента + data = json.loads(message) + if 'action' in data: + action = data['action'] + if action == 'get_settings': + # Отправляем текущие настройки клиенту + await self.send_to_client(json.dumps(core.plugin_manifests)) + elif action == 'update_settings': + # Обновляем настройки + if 'settings' in data: + new_settings = data['settings'] + self.settings.update(new_settings) + print("Settings updated:", self.settings) + # Отправляем подтверждение клиенту + await self.send_to_client({"message": "Settings updated successfully"}) + finally: + self.clients.remove(websocket) + + async def send_to_client(self, message): + if self.websocket: # Проверяем, что есть активное соединение + try: + await self.websocket.send(json.dumps(message)) # Используем self.websocket + except Exception as e: + print(f"Failed to send message to client: {e}") + else: + print("No active WebSocket connection to send message.") + + async def start(self): + server = await websockets.serve(self.handle_client, self.host, self.port, ping_timeout=None) + print(f"WebSocket server started at ws://{self.host}:{self.port}") + if self.websocket: + for value in core.plugin_manifests.values(): + print(value) + await self.send_to_client(value) + else: + print("No active WebSocket connection to send message.") + await server.wait_closed() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..c3c6778d45624253bbe0abc61ad6ee716ef5f0d5 GIT binary patch literal 2008 zcmZXV!A=`t41|5I)JFkj1BLd`1HB+BAyq|Hk0@-CB_-K}-3<^PzU_SWI-62O*#zu= zJRXn#{P{hxg{^I_=h9}@+GoASc4EhR9$R7W?VUwVWsSbAJcIb@oA|wTdTsXcPl|sy zINz#9WwkJto>M3ePS&UuTMJ>L_sphtq|cr3lGRALXL^r%%vRCbO3zz}*G#9kIztC= zBmbn&g<11ocY##uWTW`PRfzsa>K+U<7&}I(C@K+$d2zs{w0p4zeU!&nU#ad~e+^u7?kgcsxzl`z2A}L> zrSnUl`_!3*bHZU+rB>}udN8F2dvO#L3t!bjpvG>GK6Y2cWN_|GXL+Vpg`MwDZ&Y>Z z4tVa*hhvi;xD%a?orF8wVq3l`cA@GpA36_(qIza@ieK6fyYd=Th`mgXJI!R{isRmx zNVqFlREt5ta|1#_~& z9zT(HEoCbEqKueJ`skDUkvdGE5gWEJ?j>??@=)!&a!S?98t@IDQ-j?)y!Rr?6h{4y zl64~l`knb6CQ`UW z>9tZT6la$PCvl_yyHA|rMBElDRn0pyHKBH}$LUNvcAehh9p@jVYW#KuTXre2pVd$2 zCoklz*c7q`_2QQZM^<*L&R?8StxniKKH_^{y%yOYkz7}S{hlg_sNy`H;& d+W%%U&pSoKGi*fqW?;qrKn0MQHvWjc{tF6>9@+o^ literal 0 HcmV?d00001 diff --git a/run_textual.py b/run_textual.py new file mode 100644 index 0000000..a442787 --- /dev/null +++ b/run_textual.py @@ -0,0 +1,11 @@ +import asyncio + +from textual_ui import run_ui + + +async def main(): + await run_ui() + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/start_model.bat b/start_model.bat new file mode 100644 index 0000000..907b7e7 --- /dev/null +++ b/start_model.bat @@ -0,0 +1,3 @@ +call venv\Scripts\activate +python main.py +pause \ No newline at end of file diff --git a/text_ui.bat b/text_ui.bat new file mode 100644 index 0000000..9edd62b --- /dev/null +++ b/text_ui.bat @@ -0,0 +1,3 @@ +call venv\Scripts\activate +python run_textual.py +pause diff --git a/textual_ui/__init__.py b/textual_ui/__init__.py new file mode 100644 index 0000000..a163b89 --- /dev/null +++ b/textual_ui/__init__.py @@ -0,0 +1,6 @@ +from .main_page import UI + +app = UI() + +async def run_ui(): + await app.run_async() diff --git a/textual_ui/classes.py b/textual_ui/classes.py new file mode 100644 index 0000000..2eb9253 --- /dev/null +++ b/textual_ui/classes.py @@ -0,0 +1,15 @@ +from textual import widgets +from textual.widget import Widget + + +class Horizontal(Widget, inherit_bindings=False): + """A container with horizontal layout and no scrollbars.""" + + DEFAULT_CSS = """ + Horizontal { + width: auto; + height: 1fr; + layout: horizontal; + overflow: hidden hidden; + } + """ diff --git a/textual_ui/main_page.py b/textual_ui/main_page.py new file mode 100644 index 0000000..3d50d88 --- /dev/null +++ b/textual_ui/main_page.py @@ -0,0 +1,24 @@ +from textual.app import App, ComposeResult +from textual import widgets, binding +from .options_screen import Options +from .plugins_screen import Plugins + + +class UI(App): + SCREENS = { + "options": Options(), + "plugins": Plugins(), + } + BINDINGS = [binding.Binding(key="ctrl+q", action="quit", description="Exit")] + + def compose(self) -> ComposeResult: + yield widgets.Header() + yield widgets.Footer() + yield widgets.Button("Опции", id="options") + yield widgets.Button("Плагины", id="plugins") + + def on_mount(self) -> None: + self.title = "Настройки Лизы" + + def on_button_pressed(self, event: widgets.Button.Pressed) -> None: + self.push_screen(event.button.id) diff --git a/textual_ui/options_screen.py b/textual_ui/options_screen.py new file mode 100644 index 0000000..4830d3f --- /dev/null +++ b/textual_ui/options_screen.py @@ -0,0 +1,93 @@ +import json +import os + +from textual import binding +from textual import containers +from textual.screen import Screen +from textual.app import ComposeResult +from textual import widgets, on +from .classes import Horizontal + + +class NameSelect(widgets.Static): + def compose(self) -> ComposeResult: + yield widgets.Select( + [ + (plugin_name.replace(".py", ""), plugin_name.replace(".py", "")) + for plugin_name in os.listdir("plugins") if not plugin_name.startswith("_") + ], + prompt="Выберите плагин" + ) + + +class OptionsEdit(widgets.Static): + def compose(self) -> ComposeResult: + self.text_area = widgets.TextArea(text="Плагин не выбран", language="json", disabled=True, show_line_numbers=True) + self.turn_on_swich = widgets.Switch(value=False, disabled=True, animate=False) + self.turn_on_label = widgets.Label("\nАктивен\n") + self.version_label = widgets.TextArea(text="Вер. \n", disabled=True) + + self.text_area.styles.width = "3fr" + + with Horizontal(id="plugin_container"): + with containers.Vertical(id="ver_switch") as left_menu: + left_menu.styles.width = "1fr" + yield self.version_label + with containers.Horizontal(id="turn_on_swich") as swicher: + swicher.styles.height = "3fr" + yield self.turn_on_label + yield self.turn_on_swich + yield self.text_area + + def from_option(self, option_name): + with open(f"options/{option_name}.json", "r", encoding="utf-8") as file: + file_content = file.read() + plugin_data: dict = json.loads(file_content) + ver = plugin_data.pop("v") + is_active = plugin_data.pop("is_active") + + self.version_label.text = f"Версия: {ver}\n" + self.turn_on_swich.disabled = False + self.turn_on_swich.value = is_active + + self.text_area.disabled = False + self.text_area.text = json.dumps(plugin_data, indent=2, ensure_ascii=False) + self.opened_file_name = f"options/{option_name}.json" + self.update() + + def action_save(self): + with open(self.opened_file_name, "r", encoding="utf-8") as file: + file_data: dict = json.load(file) + + new_data = { + "is_active": self.turn_on_swich.value + } + new_data.update(json.loads(self.text_area.text)) + + file_data.update(new_data) + + with open(self.opened_file_name, "w", encoding="utf-8") as file: + json.dump(file_data, file, indent=2, ensure_ascii=False) + + +class Options(Screen): + BINDINGS = [ + binding.Binding(key="escape", action="app.pop_screen", description="Back"), + binding.Binding(key="ctrl+s", action="save", description="Save") + ] + + def compose(self) -> ComposeResult: + yield widgets.Header() + yield widgets.Footer() + self.name_layout = NameSelect("NameSelect", classes="box") + yield self.name_layout + self.option_layout = OptionsEdit("OptionsEdit", classes="box") + yield self.option_layout + + @on(widgets.Select.Changed) + def select_changed(self, event: widgets.Select.Changed) -> None: + self.title = "Настройки: " + str(event.value) + self.option_layout.from_option(str(event.value)) + + def action_save(self): + self.option_layout.action_save() \ No newline at end of file diff --git a/textual_ui/plugins_screen.py b/textual_ui/plugins_screen.py new file mode 100644 index 0000000..3a833be --- /dev/null +++ b/textual_ui/plugins_screen.py @@ -0,0 +1,42 @@ +import os + +from textual import binding +from textual.screen import Screen +from textual.app import ComposeResult +from textual import widgets, on +from core import Core + + +def get_plugins_manifests(): + return { + plugin_name: Core.get_manifest(plugin_name) + for plugin_name in os.listdir("plugins") if not plugin_name.startswith("_") + } + + +class PluginList(widgets.Static): + def compose(self) -> ComposeResult: + self.table = widgets.DataTable() + yield self.table + + def on_mount(self) -> None: + plugin_rows = [ + (name, manifest["name"], manifest["version"]) + for name, manifest in get_plugins_manifests().items() + ] + rows = [("Name", "Description", "Ver", "Staut")] + rows.extend(plugin_rows) + self.table.add_columns(*rows[0]) + self.table.add_rows(rows[1:]) + + +class Plugins(Screen): + BINDINGS = [ + binding.Binding(key="escape", action="app.pop_screen", description="Back") + ] + + def compose(self) -> ComposeResult: + yield widgets.Header() + yield widgets.Footer() + self.name_layout = PluginList("PluginList", classes="box") + yield self.name_layout \ No newline at end of file diff --git a/utils/custom_filters.py b/utils/custom_filters.py new file mode 100644 index 0000000..e8371c3 --- /dev/null +++ b/utils/custom_filters.py @@ -0,0 +1,22 @@ +from fuzzywuzzy import fuzz + +from core import F + + +def text_comparison(str1, str2, min_ratio=85) -> bool: + """ + Проверка схожести строк в процентах, основано на расстоянии Левенштейна + :param str1: + :param str2: + :param min_ratio: минимальный процент для проверки + :return: bool + """ + if fuzz.token_sort_ratio(str1, str2) > min_ratio: + return True + if str1 in str2: + return True + return False + + +def levenshtein_filter(text, min_ratio=80): + return F.func(lambda input_text: text_comparison(input_text, text, min_ratio))