Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronization with Klat Personas #5

Merged
merged 20 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions neon_llm_core/chatbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@
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):

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}|'
Expand Down
39 changes: 18 additions & 21 deletions neon_llm_core/rmq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)):
Expand Down Expand Up @@ -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()
25 changes: 25 additions & 0 deletions neon_llm_core/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 34 additions & 13 deletions neon_llm_core/config.py → neon_llm_core/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions neon_llm_core/utils/constants.py
Original file line number Diff line number Diff line change
@@ -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'
25 changes: 25 additions & 0 deletions neon_llm_core/utils/personas/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 43 additions & 0 deletions neon_llm_core/utils/personas/models.py
Original file line number Diff line number Diff line change
@@ -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
99 changes: 99 additions & 0 deletions neon_llm_core/utils/personas/provider.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading