Skip to content

Commit

Permalink
Merge pull request #5 from NeonGeckoCom/klat_personas_management
Browse files Browse the repository at this point in the history
Synchronization with Klat Personas
  • Loading branch information
NeonKirill authored Mar 22, 2024
2 parents effa61e + 0a48463 commit cb71730
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 37 deletions.
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

0 comments on commit cb71730

Please sign in to comment.