diff --git a/.github/workflows/build_nano_assets.yml b/.github/workflows/build_nano_assets.yml deleted file mode 100644 index cbfb935..0000000 --- a/.github/workflows/build_nano_assets.yml +++ /dev/null @@ -1,38 +0,0 @@ -# This workflow will generate a distribution and upload it to PyPI - -name: Build Nano Assets -on: - workflow_call: - -jobs: - build_and_publish: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install Setuptools - run: | - python -m pip install -U pip setuptools - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Install JS Beautifier - run: | - python -m pip install jsbeautifier==1.15.1 - - name: Build Nano JS - working-directory: ./ - run: python ./scripts/file_merger.py --working_dir ./chat_client/static --weighted_dirs 1=['js'] --weighted_files 0=['nano_builder.js'] --skip_files meta.js klatchatNano.js --save_to ./js/klatchatNano.js --beautify 1 - - name: Build Nano CSS - working-directory: ./ - run: python ./scripts/file_merger.py --working_dir ./chat_client/static --weighted_dirs 1=['css'] --skip_files sidebar.css klatchatNano.css --save_to ./css/klatchatNano.css --beautify 0 - - name: Push Built Files to VCS - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Built Nano Assets diff --git a/.gitignore b/.gitignore index 10c7883..51b69e2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,6 @@ sandbox.py # Generated Migration Files passed_migrations -# Nano generated files -*Nano - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/chat_client/static/js/chat_utils.js b/chat_client/static/js/chat_utils.js index d331796..b9f07dd 100644 --- a/chat_client/static/js/chat_utils.js +++ b/chat_client/static/js/chat_utils.js @@ -229,7 +229,7 @@ const sendMessage = async (inputElem, cid, repliedMessageId=null, isAudio='0', i /** * Builds new conversation HTML from provided data and attaches it to the list of displayed conversations - * @param conversationData: JS Object containing conversation data of type: + * @param conversationData - JS Object containing conversation data of type: * { * '_id': 'id of conversation', * 'conversation_name': 'title of the conversation', @@ -243,13 +243,12 @@ const sendMessage = async (inputElem, cid, repliedMessageId=null, isAudio='0', i * 'created_on': 'creation time of the message' * }, ... (num of user messages returned)] * } - * @param conversationParentID: ID of conversation parent - * @param remember: to store this conversation into localStorage (defaults to true) - * @param skin: Conversation skin to build - * + * @param skin - Conversation skin to build + * @param remember - to store this conversation into localStorage (defaults to true) + * @param conversationParentID - ID of conversation parent * @return id of the built conversation */ -async function buildConversation(conversationData={}, skin = CONVERSATION_SKINS.BASE, remember=true,conversationParentID = 'conversationsBody'){ +async function buildConversation(conversationData, skin, remember=true,conversationParentID = 'conversationsBody'){ const idField = '_id'; const cid = conversationData[idField]; if (!cid){ @@ -257,7 +256,7 @@ async function buildConversation(conversationData={}, skin = CONVERSATION_SKINS. return -1; } if(remember){ - await addNewCID(cid, skin); + await addNewConversationToAlignmentStore(cid, skin); } const newConversationHTML = await buildConversationHTML(conversationData, skin); const conversationsBody = document.getElementById(conversationParentID); @@ -369,10 +368,13 @@ async function buildConversation(conversationData={}, skin = CONVERSATION_SKINS. * @param alertParent: parent of error alert (optional) * @returns {Promise<{}>} promise resolving conversation data returned */ -async function getConversationDataByInput(input="", skin=CONVERSATION_SKINS.BASE, oldestMessageTS=null, maxResults=20, alertParent=null){ +async function getConversationDataByInput(input="", forceSkin=null, oldestMessageTS=null, maxResults=20, alertParent=null){ let conversationData = {}; if(input){ - let query_url = `chat_api/search/${input.toString()}?limit_chat_history=${maxResults}&skin=${skin}`; + let query_url = `chat_api/search/${input.toString()}?limit_chat_history=${maxResults}`; + if (forceSkin){ + query_url += `&skin=${forceSkin}` + } if(oldestMessageTS){ query_url += `&creation_time_from=${oldestMessageTS}`; } @@ -434,7 +436,7 @@ const getMinifySettingsTable = () => { * @param cid: conversation id to add * @param skin: conversation skin to add */ -async function addNewCID(cid, skin){ +async function addNewConversationToAlignmentStore(cid, skin){ return await getChatAlignmentTable().put({'cid': cid, 'skin': skin, 'added_on': getCurrentTimestamp()}, [cid]); } @@ -504,15 +506,16 @@ const chatAlignmentRestoredEvent = new CustomEvent("chatAlignmentRestored", { "d async function restoreChatAlignment(keyName=conversationAlignmentKey){ let cachedItems = await retrieveItemsLayout(); if (cachedItems.length === 0){ - cachedItems = [{'cid': '1', 'added_on': getCurrentTimestamp(), 'skin': CONVERSATION_SKINS.BASE}] - await addNewCID('1', CONVERSATION_SKINS.BASE); + cachedItems = [{'cid': '1', 'added_on': getCurrentTimestamp()}] } for (const item of cachedItems) { - await getConversationDataByInput(item.cid, item.skin).then(async conversationData=>{ + await getConversationDataByInput(item.cid).then(async conversationData=>{ if(conversationData && Object.keys(conversationData).length > 0) { - await buildConversation(conversationData, item.skin, false); + await buildConversation(conversationData, conversationData.skin, true); }else{ - if (item.cid !== '1') { + if (item.cid === '1'){ + displayAlert(document.getElementById('conversationsBody'), 'Default Conversation is missing!', 'danger', 'noRestoreConversationAlert'); + }else{ displayAlert(document.getElementById('conversationsBody'), 'No matching conversation found', 'danger', 'noRestoreConversationAlert', {'type': alertBehaviors.AUTO_EXPIRE}); } await removeConversation(item.cid); @@ -652,10 +655,10 @@ function setChatState(cid, state='active', state_msg = ''){ * @param alertParentID: id of the element to display alert in * @param conversationParentID: parent Node ID of the conversation */ -async function displayConversation(searchStr, skin=CONVERSATION_SKINS.BASE, alertParentID = null, conversationParentID='conversationsBody'){ +async function displayConversation(searchStr, forceSkin=null, alertParentID = null, conversationParentID='conversationsBody'){ if (searchStr !== "") { const alertParent = document.getElementById(alertParentID); - await getConversationDataByInput(searchStr, skin, null, 20, alertParent).then(async conversationData => { + await getConversationDataByInput(searchStr, forceSkin, null, 20, alertParent).then(async conversationData => { let responseOk = false; if (!conversationData || Object.keys(conversationData).length === 0){ displayAlert( @@ -669,6 +672,7 @@ async function displayConversation(searchStr, skin=CONVERSATION_SKINS.BASE, aler else if (isDisplayed(conversationData['_id'])) { displayAlert(alertParent, 'Chat is already displayed', 'danger'); } else { + const skin = conversationData.skin await buildConversation(conversationData, skin, true, conversationParentID); if (skin === CONVERSATION_SKINS.BASE) { for (const inputType of ['incoming', 'outcoming']) { @@ -707,7 +711,7 @@ async function createNewConversation(conversationName, isPrivate=false, conversa const responseJson = await response.json(); let responseOk = false; if (response.ok) { - await buildConversation(responseJson); + await buildConversation(responseJson, CONVERSATION_SKINS.BASE); responseOk = true; } else { displayAlert('newConversationModalBody', @@ -730,7 +734,7 @@ document.addEventListener('DOMContentLoaded', (e)=>{ }); addBySearch.addEventListener('click', async (e) => { e.preventDefault(); - displayConversation(conversationSearchInput.value, CONVERSATION_SKINS.BASE, 'importConversationModalBody').then(responseOk=> { + displayConversation(conversationSearchInput.value, null, 'importConversationModalBody').then(responseOk=> { conversationSearchInput.value = ""; if(responseOk) { importConversationModal.modal('hide'); diff --git a/chat_client/static/js/message_utils.js b/chat_client/static/js/message_utils.js index 3d62199..321b1c9 100644 --- a/chat_client/static/js/message_utils.js +++ b/chat_client/static/js/message_utils.js @@ -160,7 +160,7 @@ function getFirstMessageFromCID(firstChild){ * @param cid: target conversation id * @param skin: target conversation skin */ -async function addOldMessages(cid, skin=CONVERSATION_SKINS.BASE) { +async function addOldMessages(cid, skin) { const messageContainer = getMessageListContainer( cid ); if (messageContainer.children.length > 0) { for (let i = 0; i < messageContainer.children.length; i++) { @@ -183,7 +183,7 @@ async function addOldMessages(cid, skin=CONVERSATION_SKINS.BASE) { console.debug( `!!message_id=${message["message_id"]} is already displayed` ) } } - await initMessages( conversationData, skin ); + await initMessages( conversationData ); } } ).then( _ => { firstMessageItem.scrollIntoView( {behavior: "smooth"} ); @@ -234,9 +234,8 @@ const getUserMessages = (conversationData, forceType='plain') => { /** * Initializes listener for loading old message on scrolling conversation box * @param conversationData: Conversation Data object to fetch - * @param skin: conversation skin to apply */ -function initLoadOldMessages(conversationData, skin) { +function initLoadOldMessages(conversationData) { const cid = conversationData['_id']; const messageList = getMessageListContainer(cid); const messageListParent = messageList.parentElement; @@ -248,7 +247,7 @@ function initLoadOldMessages(conversationData, skin) { !conversationState[cid]['all_messages_displayed'] && conversationState[cid]['scrollY'] === 0) { setChatState(cid, 'updating', 'Loading messages...') - await addOldMessages(cid, skin); + await addOldMessages(cid, conversationData.skin); for(const inputType of ['incoming', 'outcoming']){ await requestTranslation(cid, null, null, inputType); } @@ -335,14 +334,13 @@ async function initPagination(conversationData) { * 'created_on': 'creation time of the message' * }, ... (num of user messages returned)] * } - * @param skin - target conversation skin to consider */ -async function initMessages(conversationData, skin = CONVERSATION_SKINS.BASE){ +async function initMessages(conversationData){ initProfileDisplay(conversationData); attachReplies(conversationData); addAttachments(conversationData); addCommunicationChannelTransformCallback(conversationData); - initLoadOldMessages(conversationData, skin); + initLoadOldMessages(conversationData); await initPagination(conversationData); } diff --git a/chat_client/static/nano_builder.js b/chat_client/static/nano_builder.js index 10356ad..77fc62f 100644 --- a/chat_client/static/nano_builder.js +++ b/chat_client/static/nano_builder.js @@ -70,7 +70,7 @@ class NanoBuilder { const chatData = options['CHAT_DATA']; const nanoChatsLoaded = new CustomEvent('nanoChatsLoaded') Array.from(chatData).forEach(async chat => { - await displayConversation(chat['CID'], CONVERSATION_SKINS.BASE, chat['PARENT_ID'], chat['PARENT_ID']) + await displayConversation(chat['CID'], null, chat['PARENT_ID'], chat['PARENT_ID']) }); console.log('all chats loaded') document.dispatchEvent(nanoChatsLoaded); diff --git a/chat_server/blueprints/chat.py b/chat_server/blueprints/chat.py index f831d7a..7d2fc61 100644 --- a/chat_server/blueprints/chat.py +++ b/chat_server/blueprints/chat.py @@ -33,9 +33,11 @@ from fastapi import APIRouter, Form, Depends from fastapi.responses import JSONResponse +from chat_server.constants.conversations import ConversationSkins from chat_server.server_utils.api_dependencies.validators.users import ( get_authorized_user, ) +from chat_server.server_utils.cache_utils import SubmindsState from chat_server.server_utils.conversation_utils import build_message_json from chat_server.server_utils.api_dependencies.extractors import CurrentUserData from chat_server.server_utils.api_dependencies.models import GetConversationModel @@ -125,9 +127,21 @@ async def get_matching_conversation( else: query_filter = None + if not model.skin: + is_proctored_conversation = SubmindsState.is_proctored_conversation( + cid=conversation_data["_id"] + ) + skin = ( + ConversationSkins.PROMPTS + if is_proctored_conversation + else ConversationSkins.BASE + ) + else: + skin = model.skin + message_data = ( fetch_message_data( - skin=model.skin, + skin=skin, conversation_data=conversation_data, limit=model.limit_chat_history, creation_time_filter=query_filter, @@ -138,6 +152,7 @@ async def get_matching_conversation( build_message_json(raw_message=message_data[i], skin=model.skin) for i in range(len(message_data)) ] + conversation_data["skin"] = skin return conversation_data diff --git a/chat_server/server_utils/api_dependencies/models/chats.py b/chat_server/server_utils/api_dependencies/models/chats.py index 11783d2..04df3a1 100644 --- a/chat_server/server_utils/api_dependencies/models/chats.py +++ b/chat_server/server_utils/api_dependencies/models/chats.py @@ -38,6 +38,4 @@ class GetConversationModel(BaseModel): search_str: str = Field(Path(), examples=["1"]) limit_chat_history: int | None = Field(Query(default=100), examples=[100]) creation_time_from: str | None = Field(Query(default=None), examples=[int(time())]) - skin: str = Field( - Query(default=ConversationSkins.BASE), examples=[ConversationSkins.BASE] - ) + skin: str | None = Field(Query(default=None), examples=[ConversationSkins.BASE]) diff --git a/chat_server/server_utils/cache_utils.py b/chat_server/server_utils/cache_utils.py index 7acff20..0ab5545 100644 --- a/chat_server/server_utils/cache_utils.py +++ b/chat_server/server_utils/cache_utils.py @@ -27,6 +27,10 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import Type +from cachetools import Cache + + +# TODO: consider storing cached values in Redis (Kirill) class CacheFactory: @@ -35,7 +39,7 @@ class CacheFactory: __active_caches = {} @classmethod - def get(cls, name: str, cache_type: Type = None, **kwargs): + def get(cls, name: str, cache_type: Type[Cache] = None, **kwargs) -> Cache: """ Get cache instance based on name and type @@ -50,3 +54,32 @@ def get(cls, name: str, cache_type: Type = None, **kwargs): else: raise KeyError(f"Missing cache instance under {name}") return cls.__active_caches[name] + + +class SubmindsState: + items = {} + + @classmethod + def update(cls, data: dict): + cls.items["proctored_conversations"] = cls._get_proctored_conversations(data) + + @classmethod + def _get_proctored_conversations(cls, data): + proctored_conversations = [] + for cid, subminds in data.get("subminds_per_cid", {}).items(): + for submind in subminds: + if ( + cls._is_proctor(submind["submind_id"]) + and submind["status"] != "banned" + ): + proctored_conversations.append(cid) + break + return proctored_conversations + + @classmethod + def _is_proctor(cls, submind_id: str) -> bool: + return submind_id.startswith("proctor") + + @classmethod + def is_proctored_conversation(cls, cid: str) -> bool: + return cid in cls.items.get("proctored_conversations", []) diff --git a/chat_server/sio/handlers/user_message.py b/chat_server/sio/handlers/user_message.py index 684be44..63a494a 100644 --- a/chat_server/sio/handlers/user_message.py +++ b/chat_server/sio/handlers/user_message.py @@ -33,9 +33,9 @@ from utils.database_utils.mongo_utils.queries.wrapper import MongoDocumentsAPI from utils.logging_utils import LOG from ..server import sio -from ..utils import emit_error, login_required +from ..utils import emit_error from ...server_config import server_config -from ...server_utils.enums import UserRoles +from ...server_utils.cache_utils import SubmindsState from ...services.popularity_counter import PopularityCounter @@ -176,16 +176,13 @@ async def user_message(sid, data): @sio.event -@login_required(min_required_role=UserRoles.ADMIN) -async def broadcast(sid, data): - """Forwards received broadcast message from client""" - msg_type = data.pop("msg_type", None) - msg_receivers = data.pop("to", None) - if msg_type: +async def subminds_state_received(sid, data): + """Handles received subminds state""" + if data: + SubmindsState.update(data=data) await sio.emit( - msg_type, + "subminds_state_updated", data=data, - to=msg_receivers, ) else: - LOG.error(f'data={data} skipped - no "msg_type" provided') + LOG.error(f"Empty data skipped for subminds_state_received") diff --git a/services/klatchat_observer/controller.py b/services/klatchat_observer/controller.py index 42ace1b..cefc4aa 100644 --- a/services/klatchat_observer/controller.py +++ b/services/klatchat_observer/controller.py @@ -331,7 +331,9 @@ def connect_sio(self): """ Method for establishing connection with Socket IO server """ - self._sio = socketio.Client() + self._sio = socketio.Client( + request_timeout=15, reconnection_delay=0.5, reconnection_delay_max=2 + ) self._sio.connect( url=self.sio_url, namespaces=["/"], @@ -769,8 +771,7 @@ def on_tts_response(self, body: dict): def on_subminds_state(self, body: dict): """Handles receiving subminds state message""" LOG.debug(f"Received submind state: {body}") - body["msg_type"] = "subminds_state" - self.sio.emit("broadcast", data=body) + self.sio.emit("subminds_state_received", data=body) @create_mq_callback() def on_get_configured_personas(self, body: dict): diff --git a/utils/database_utils/mongo_utils/queries/constants.py b/utils/database_utils/mongo_utils/queries/constants.py index 95ae07b..983d093 100644 --- a/utils/database_utils/mongo_utils/queries/constants.py +++ b/utils/database_utils/mongo_utils/queries/constants.py @@ -45,10 +45,3 @@ class UserPatterns(Enum): "avatar": "neon.webp", } GUEST_NANO = {"first_name": "Nano", "last_name": "Guest", "tokens": []} - - -class ConversationSkins: - """List of supported conversation skins""" - - BASE = "base" - PROMPTS = "prompts" diff --git a/utils/database_utils/mongo_utils/queries/mongo_queries.py b/utils/database_utils/mongo_utils/queries/mongo_queries.py index 2bb27c4..738d1b8 100644 --- a/utils/database_utils/mongo_utils/queries/mongo_queries.py +++ b/utils/database_utils/mongo_utils/queries/mongo_queries.py @@ -28,8 +28,9 @@ from time import time from typing import List, Tuple +from chat_server.constants.conversations import ConversationSkins from ..structures import MongoFilter -from .constants import UserPatterns, ConversationSkins +from .constants import UserPatterns from .wrapper import MongoDocumentsAPI from utils.logging_utils import LOG