diff --git a/neon_llm_core/chatbot.py b/neon_llm_core/chatbot.py index 72499d3..e8884d9 100644 --- a/neon_llm_core/chatbot.py +++ b/neon_llm_core/chatbot.py @@ -29,7 +29,7 @@ from neon_mq_connector.utils.client_utils import send_mq_request from ovos_utils.log import LOG -from neon_llm_core.config import LLMMQConfig +from neon_llm_core.utils.config import LLMMQConfig class LLMBot(ChatBot): @@ -37,7 +37,7 @@ class LLMBot(ChatBot): def __init__(self, *args, **kwargs): ChatBot.__init__(self, *args, **kwargs) self.bot_type = "submind" - self.base_llm = kwargs.get("llm_name") # chat_gpt, fastchat, etc. + self.base_llm = kwargs.get("llm_name") # chatgpt, fastchat, etc. self.persona = kwargs.get("persona") self.mq_queue_config = self.get_llm_mq_config(self.base_llm) LOG.info(f'Initialised config for llm={self.base_llm}|' diff --git a/neon_llm_core/rmq.py b/neon_llm_core/rmq.py index c042542..7d6ef9a 100644 --- a/neon_llm_core/rmq.py +++ b/neon_llm_core/rmq.py @@ -31,8 +31,10 @@ from neon_mq_connector.utils.rabbit_utils import create_mq_callback from ovos_utils.log import LOG -from neon_llm_core.config import load_config +from neon_llm_core.utils.config import load_config from neon_llm_core.llm import NeonLLM +from neon_llm_core.utils.constants import LLM_VHOST +from neon_llm_core.utils.personas.provider import PersonasProvider class NeonLLMMQConnector(MQConnector, ABC): @@ -45,30 +47,13 @@ def __init__(self): self.ovos_config = load_config() mq_config = self.ovos_config.get("MQ", dict()) super().__init__(config=mq_config, service_name=self.service_name) - self.vhost = "/llm" + self.vhost = LLM_VHOST self.register_consumers() self._model = None self._bots = list() - - if self.ovos_config.get("llm_bots", {}).get(self.name): - from neon_llm_core.chatbot import LLMBot - LOG.info(f"Chatbot(s) configured for: {self.name}") - for persona in self.ovos_config['llm_bots'][self.name]: - # Spawn a service for each persona to support @user requests - if not persona.get('enabled', True): - LOG.warning(f"Persona disabled: {persona['name']}") - continue - # Get a configured username to use for LLM submind connections - if mq_config.get("users", {}).get("neon_llm_submind"): - self.ovos_config["MQ"]["users"][persona['name']] = \ - mq_config['users']['neon_llm_submind'] - bot = LLMBot(llm_name=self.name, service_name=persona['name'], - persona=persona, config=self.ovos_config, - vhost="/chatbots") - bot.run() - LOG.info(f"Started chatbot: {bot.service_name}") - self._bots.append(bot) + self._personas_provider = PersonasProvider(service_name=self.name, + ovos_config=self.ovos_config) def register_consumers(self): for idx in range(self.model_config.get("num_parallel_processes", 1)): @@ -242,3 +227,15 @@ def compose_opinion_prompt(respondent_nick: str, question: str, @param answer: respondent's response to the question """ pass + + def run(self, run_consumers: bool = True, run_sync: bool = True, + run_observer: bool = True, **kwargs): + super().run(run_consumers=run_consumers, + run_sync=run_sync, + run_observer=run_observer, + **kwargs) + self._personas_provider.start_sync() + + def stop(self): + super().stop() + self._personas_provider.stop_sync() diff --git a/neon_llm_core/utils/__init__.py b/neon_llm_core/utils/__init__.py new file mode 100644 index 0000000..250e4cd --- /dev/null +++ b/neon_llm_core/utils/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 NeonGecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/neon_llm_core/config.py b/neon_llm_core/utils/config.py similarity index 66% rename from neon_llm_core/config.py rename to neon_llm_core/utils/config.py index a4c2e51..286c795 100644 --- a/neon_llm_core/config.py +++ b/neon_llm_core/utils/config.py @@ -25,35 +25,56 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import json +import os from dataclasses import dataclass from os.path import join, dirname, isfile +from typing import Union + +from neon_utils.log_utils import init_log from ovos_utils.log import LOG from ovos_config.config import Configuration +from neon_llm_core.utils.constants import LLM_VHOST +import neon_mq_connector.utils.client_utils as mq_connector_client_utils -def load_config() -> dict: - """ - Load and return a configuration object, - """ - legacy_config_path = "/app/app/config.json" + +def load_legacy_config() -> Union[dict, None]: + legacy_config_path = os.getenv("NEON_LLM_LEGACY_CONFIG", "/app/app/config.json") if isfile(legacy_config_path): - LOG.warning(f"Deprecated configuration found at {legacy_config_path}") with open(legacy_config_path) as f: config = json.load(f) + init_log(config=config) + mq_connector_client_utils._default_mq_config = config.get("MQ") return config - config = Configuration() - if not config: - LOG.warning(f"No configuration found! falling back to defaults") - default_config_path = join(dirname(__file__), "default_config.json") - with open(default_config_path) as f: - config = json.load(f) + + +load_ovos_config = Configuration + + +def load_default_config() -> Union[dict, None]: + LOG.warning(f"No configuration found! falling back to defaults") + default_config_path = join(dirname(__file__), "default_config.json") + with open(default_config_path) as f: + config = json.load(f) return config +def load_config() -> Union[dict, None]: + """ + Load and return a configuration object, + """ + configs_loading_order = (load_legacy_config, load_ovos_config, load_default_config,) + for config_loader in configs_loading_order: + config = config_loader() + if config: + LOG.info(f'Applied configs from loader={config_loader.__name__}()') + return config + + @dataclass class LLMMQConfig: ask_response_queue: str ask_appraiser_queue: str ask_discusser_queue: str - vhost: str = '/llm' + vhost: str = LLM_VHOST diff --git a/neon_llm_core/utils/constants.py b/neon_llm_core/utils/constants.py new file mode 100644 index 0000000..7acff7a --- /dev/null +++ b/neon_llm_core/utils/constants.py @@ -0,0 +1,27 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 NeonGecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +LLM_VHOST = '/llm' diff --git a/neon_llm_core/utils/personas/__init__.py b/neon_llm_core/utils/personas/__init__.py new file mode 100644 index 0000000..250e4cd --- /dev/null +++ b/neon_llm_core/utils/personas/__init__.py @@ -0,0 +1,25 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 NeonGecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/neon_llm_core/utils/personas/models.py b/neon_llm_core/utils/personas/models.py new file mode 100644 index 0000000..0d5e1d8 --- /dev/null +++ b/neon_llm_core/utils/personas/models.py @@ -0,0 +1,43 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 NeonGecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import Optional + +from pydantic import BaseModel, computed_field + + +class PersonaModel(BaseModel): + name: str + description: str + enabled: bool = True + user_id: Optional[str] = None + + @computed_field + @property + def id(self) -> str: + persona_id = self.name + if self.user_id: + persona_id += f"_{self.user_id}" + return persona_id diff --git a/neon_llm_core/utils/personas/provider.py b/neon_llm_core/utils/personas/provider.py new file mode 100644 index 0000000..aa81702 --- /dev/null +++ b/neon_llm_core/utils/personas/provider.py @@ -0,0 +1,99 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2021 NeonGecko.com Inc. +# BSD-3 +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import os +from time import time + +from neon_mq_connector.utils import RepeatingTimer +from neon_mq_connector.utils.client_utils import send_mq_request +from ovos_utils.log import LOG + +from neon_llm_core.utils.constants import LLM_VHOST +from neon_llm_core.utils.personas.models import PersonaModel +from neon_llm_core.utils.personas.state import PersonaHandlersState + + +class PersonasProvider: + + PERSONA_STATE_TTL = int(os.getenv("PERSONA_STATE_TTL", 15 * 60)) + PERSONA_SYNC_INTERVAL = int(os.getenv("PERSONA_SYNC_INTERVAL", 5 * 60)) + GET_CONFIGURED_PERSONAS_QUEUE = "get_configured_personas" + + def __init__(self, service_name: str, ovos_config: dict): + self.service_name = service_name + self._persona_handlers_state = PersonaHandlersState(service_name=service_name, + ovos_config=ovos_config) + self._personas = [] # list of personas available for given service + self._persona_last_sync = 0 + self._persona_sync_thread = None + + @property + def persona_sync_thread(self): + """Creates new synchronization thread which fetches Klat personas""" + if not (isinstance(self._persona_sync_thread, RepeatingTimer) and + self._persona_sync_thread.is_alive()): + self._persona_sync_thread = RepeatingTimer(self.PERSONA_SYNC_INTERVAL, + self._fetch_persona_config) + self._persona_sync_thread.daemon = True + return self._persona_sync_thread + + @property + def personas(self): + return self._personas + + @personas.setter + def personas(self, data): + now = int(time()) + LOG.debug(f'Setting personas={data}') + if data and isinstance(data, list): + self._personas = data + self._persona_last_sync = now + self._persona_handlers_state.clean_up_personas(ignore_items=self._personas) + elif now - self._persona_last_sync > self.PERSONA_STATE_TTL: + LOG.warning(f'Persona state TTL expired, resetting personas config') + self._personas = [] + self._persona_handlers_state.init_default_handlers() + + def _fetch_persona_config(self): + response = send_mq_request(vhost=LLM_VHOST, + request_data={"service_name": self.service_name}, + target_queue=PersonasProvider.GET_CONFIGURED_PERSONAS_QUEUE) + response_data = response.get('items', []) + personas = [] + for item in response_data: + item.setdefault('name', item.pop('persona_name', None)) + persona = PersonaModel.parse_obj(obj=item) + self._persona_handlers_state.add_persona_handler(persona=persona) + personas.append(persona) + self.personas = personas + + def start_sync(self): + self._fetch_persona_config() + self.persona_sync_thread.start() + + def stop_sync(self): + if self._persona_sync_thread: + self._persona_sync_thread.cancel() + self._persona_sync_thread = None diff --git a/neon_llm_core/utils/personas/state.py b/neon_llm_core/utils/personas/state.py new file mode 100644 index 0000000..94be76d --- /dev/null +++ b/neon_llm_core/utils/personas/state.py @@ -0,0 +1,88 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import time +from typing import Dict, Union, List + +from ovos_utils import LOG + +from neon_llm_core.chatbot import LLMBot +from neon_llm_core.utils.personas.models import PersonaModel + + +class PersonaHandlersState: + + def __init__(self, service_name: str, ovos_config: dict): + self._created_items: Dict[str, LLMBot] = {} + self.service_name = service_name + self.ovos_config = ovos_config + self.mq_config = ovos_config.get('MQ', {}) + self.init_default_handlers() + + def init_default_handlers(self): + self._created_items = {} + if self.ovos_config.get("llm_bots", {}).get(self.service_name): + LOG.info(f"Chatbot(s) configured for: {self.service_name}") + for persona in self.ovos_config['llm_bots'][self.service_name]: + self.add_persona_handler(persona=PersonaModel.parse_obj(obj=persona)) + + def add_persona_handler(self, persona: PersonaModel) -> Union[LLMBot, None]: + persona_dict = persona.model_dump() + if persona.id in list(self._created_items): + if self._created_items[persona.id].persona != persona_dict: + LOG.warning(f"Overriding already existing persona: '{persona.id}' with new data={persona}") + try: + self._created_items[persona.id].stop() + # time to gracefully stop the submind + time.sleep(0.5) + except Exception as ex: + LOG.warning(f'Failed to gracefully stop {persona.id}, ex={str(ex)}') + else: + LOG.debug('Persona config provided is identical to existing, skipping') + return self._created_items[persona.id] + if not persona.enabled: + LOG.warning(f"Persona disabled: '{persona.id}'") + return + # Get a configured username to use for LLM submind connections + persona_id = f"{persona.id}_{self.service_name}" + self.ovos_config["MQ"]["users"][persona_id] = self.mq_config['users']['neon_llm_submind'] + bot = LLMBot(llm_name=self.service_name, service_name=persona_id, + persona=persona_dict, config=self.ovos_config, + vhost="/chatbots") + bot.run() + LOG.info(f"Started chatbot: {bot.service_name}") + self._created_items[persona.id] = bot + return bot + + def clean_up_personas(self, ignore_items: List[PersonaModel] = None): + connected_personas = set(self._created_items) + ignored_persona_ids = set(persona.id for persona in ignore_items or []) + personas_to_remove = connected_personas - ignored_persona_ids + for persona_id in personas_to_remove: + LOG.info(f'Removing persona_id = {persona_id}') + self._created_items.pop(persona_id, None) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b9f3251..4662454 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,5 @@ # networking neon-mq-connector>=0.7.1a4 ovos-utils~=0.0.32 -ovos-config~=0.0.10 \ No newline at end of file +ovos-config~=0.0.10 +pydantic==2.6.3 \ No newline at end of file