From 70eb303798613e948304d015aaf6313d11c79958 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 8 Jan 2025 18:36:40 -0800 Subject: [PATCH 01/53] Implement Klat SocketIO data models --- neon_data_models/models/api/__init__.py | 1 + neon_data_models/models/api/klat/__init__.py | 27 ++++ neon_data_models/models/api/klat/socketio.py | 133 +++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 neon_data_models/models/api/klat/__init__.py create mode 100644 neon_data_models/models/api/klat/socketio.py diff --git a/neon_data_models/models/api/__init__.py b/neon_data_models/models/api/__init__.py index e0e9c2bb..bbc589a1 100644 --- a/neon_data_models/models/api/__init__.py +++ b/neon_data_models/models/api/__init__.py @@ -29,3 +29,4 @@ from neon_data_models.models.api.llm import * from neon_data_models.models.api.mq import * from neon_data_models.models.api.messagebus import * +from neon_data_models.models.api.klat import * \ No newline at end of file diff --git a/neon_data_models/models/api/klat/__init__.py b/neon_data_models/models/api/klat/__init__.py new file mode 100644 index 00000000..2bd33fb0 --- /dev/null +++ b/neon_data_models/models/api/klat/__init__.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-2024 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 neon_data_models.models.api.klat.socketio import * diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py new file mode 100644 index 00000000..606c6a60 --- /dev/null +++ b/neon_data_models/models/api/klat/socketio.py @@ -0,0 +1,133 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2024 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, Dict, List, Literal + +from neon_data_models.models.api.llm import LLMPersona +from pydantic import Field + +from neon_data_models.enum import CcaiPromptStates +from neon_data_models.models.base import BaseModel + + +class GetSttRequest(BaseModel): + cid: str = Field(description="Conversation ID associated with the request") + sid: str = Field( + description="Client Session ID associated with the request") + message_id: str = Field( + description="Message (Shout) ID associated with the request") + audio_data: str = Field( + description="B64-encoded audio file object to transcribe") + + +class GetTtsRequest(BaseModel): + cid: str = Field(description="Conversation ID associated with the request") + sid: str = Field( + description="Client Session ID associated with the request") + message_id: str = Field( + description="Message (Shout) ID associated with the request") + text: str = Field(description="Text to generate audio for") + lang: str = Field(description="BCP-47 Language code associated with `text`") + + +class UserMessage(BaseModel): + cid: str = Field(description="Conversation ID associated with the message") + userID: str = Field(description="User ID associated with the message") + prompt_id: Optional[str] = Field( + default=None, description="Prompt ID this message is in response to") + promptState: CcaiPromptStates = Field( + default=None, + description="Associated CCAI state if `prompt_id` is defined") + source: str = Field(description="Username associated with the message") + messageText: str = Field( + description="Message content (input string or audio filename)") + repliedMessage: Optional[str] = Field( + default=None, description="Message ID this message is a reply to") + is_bot: Literal['0', '1'] = Field( + default='0', description="'1' if the message came from a bot, else '0'") + lang: str = Field(default='en', description="ISO 639-1 Language code") + attachments: List[str] = Field( + default=[], + description="List of string filenames attached to this message") + context: dict = Field(default={}, description="Optional arbitrary context") + test: bool = Field( + default=False, + description="True if this message is associated with testing") + isAudio: Literal['0', '1'] = Field( + default="0", + description="'1' if messageText represents encoded WAV audio") + messageTTS: Dict[str, Dict[Literal['male', 'female'], str]] = Field( + default={}, + description="TTS Audio formatted as {: {: " + "}}") + isAnnouncement: bool = Field( + default=False, + description="True if the message is a system announcement") + timeCreated: int = Field(description="Unix timestamp (epoch seconds)") + message_id: str = Field(description="UUID for this message") + bound_service: str = Field(default="", description="Service this message is targeting") + + class Config: + use_enum_values = True + validate_default = True + + +class PromptData(BaseModel): + # TODO: Determine and document descriptions and a model for `data` + data: dict = Field(description="Prompt data") + receiver: str = Field() + cid: str = Field(description="Conversation ID associated with the prompt") + request_id = Field() + + +class GetTranslationsRequest(BaseModel): + class CidTranslationRequest(BaseModel): + lang: str = Field(description="Translation target language") + source_lang: str = Field(description="Source language of shouts") + shouts: Dict[str, str] = Field( + default={}, description="Dict of shout ID to text in `lang`") + + request_id: str = Field(description="UUID for this request") + data: Dict[str, CidTranslationRequest] = Field( + description="Mapping of conversation ID to required translation") + + +class AuthExpired(BaseModel): + body: str = Field(description="HTTP error response body") + status: int = Field(description="HTTP response status code") + handler: str = Field(description="Name of the requested function") + + +class PersonasChanged(BaseModel): + personas: Dict[str, List[LLMPersona]] = ( + Field(description="Dict of LLM name to list of supported personas")) + update_time: int = Field( + description="Unix timestamp when the change occurred") + + +__all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, UserMessage.__name__, + PromptData.__name__, GetTranslationsRequest.__name__, + AuthExpired.__name__] \ No newline at end of file From acd6c8001ac16a73a90f13bdc73989fa4db5e0fa Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 10:05:14 -0800 Subject: [PATCH 02/53] Refactor model names to match associated SIO event names --- neon_data_models/models/api/klat/socketio.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 606c6a60..d861980a 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -53,7 +53,7 @@ class GetTtsRequest(BaseModel): lang: str = Field(description="BCP-47 Language code associated with `text`") -class UserMessage(BaseModel): +class NewMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") userID: str = Field(description="User ID associated with the message") prompt_id: Optional[str] = Field( @@ -103,7 +103,7 @@ class PromptData(BaseModel): request_id = Field() -class GetTranslationsRequest(BaseModel): +class RequestNeonTranslations(BaseModel): class CidTranslationRequest(BaseModel): lang: str = Field(description="Translation target language") source_lang: str = Field(description="Source language of shouts") @@ -121,13 +121,19 @@ class AuthExpired(BaseModel): handler: str = Field(description="Name of the requested function") -class PersonasChanged(BaseModel): +class ConfiguredPersonasChanged(BaseModel): personas: Dict[str, List[LLMPersona]] = ( Field(description="Dict of LLM name to list of supported personas")) update_time: int = Field( description="Unix timestamp when the change occurred") -__all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, UserMessage.__name__, - PromptData.__name__, GetTranslationsRequest.__name__, - AuthExpired.__name__] \ No newline at end of file +class BanSubmindFromConversation(BaseModel): + # This model is used to both ban and revoke a ban + cid: str = Field(description="Conversation ID to (un-)ban submind from") + nick: str = Field(description="Username of the submind to (un-)ban") + + +__all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, NewMessage.__name__, + PromptData.__name__, RequestNeonTranslations.__name__, + AuthExpired.__name__, BanSubmindFromConversation.__name__] \ No newline at end of file From c485cb06453c0fe6c1a9601ae0805bd858738bff Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 12:03:18 -0800 Subject: [PATCH 03/53] Annotate PromptData Define GetPromptData Socketio data structure --- neon_data_models/models/api/klat/socketio.py | 46 ++++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d861980a..d925b148 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -23,8 +23,8 @@ # 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, Dict, List, Literal +import uuid +from typing import Optional, Dict, List, Literal, Union from neon_data_models.models.api.llm import LLMPersona from pydantic import Field @@ -95,12 +95,41 @@ class Config: validate_default = True +class GetPromptData(BaseModel): + nick: str = Field(description="Nickname of user requesting prompt data") + cid: str = Field(description="Conversation ID associated with the prompt") + limit: int = Field( + default=5, + description="Maximum number of prompts to return if `prompt_id` " + "is unset") + prompt_id: Optional[str] = Field( + default=None, description="Optional prompt ID to get data for") + + class PromptData(BaseModel): - # TODO: Determine and document descriptions and a model for `data` - data: dict = Field(description="Prompt data") - receiver: str = Field() + class _PromptData(BaseModel): + id: str = Field(alias="_id", description="Unique ID for the prompt") + is_completed: Literal['0', '1'] = Field( + description="'1' if a response to the prompt has been determined") + proposed_responses: Dict[str, str] = Field( + default={}, + description="Dict of participant name to proposed response") + submind_opinions: Dict[str, str] = Field( + default={}, + description="Dict of participant name to opinion response") + votes: Dict[str, str] = Field( + default={}, + description="Dict of participant name to vote") + participating_subminds: List[str] = Field( + default=[], + description="List of subminds that participated in this prompt") + + data: Union[_PromptData, List[_PromptData]] = Field(description="Prompt data") + receiver: str = Field(description="Nickname of user requesting prompt data") cid: str = Field(description="Conversation ID associated with the prompt") - request_id = Field() + request_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="Unique ID of the request to identify the response") class RequestNeonTranslations(BaseModel): @@ -135,5 +164,6 @@ class BanSubmindFromConversation(BaseModel): __all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, NewMessage.__name__, - PromptData.__name__, RequestNeonTranslations.__name__, - AuthExpired.__name__, BanSubmindFromConversation.__name__] \ No newline at end of file + GetPromptData.__name__, PromptData.__name__, + RequestNeonTranslations.__name__, AuthExpired.__name__, + BanSubmindFromConversation.__name__] \ No newline at end of file From 2d95cb0b88b96657e2b33ec78cf7d414aed87d05 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 12:05:15 -0800 Subject: [PATCH 04/53] Annotate PromptData Define GetPromptData Socketio data structure --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d925b148..872c3c8b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -166,4 +166,4 @@ class BanSubmindFromConversation(BaseModel): __all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, NewMessage.__name__, GetPromptData.__name__, PromptData.__name__, RequestNeonTranslations.__name__, AuthExpired.__name__, - BanSubmindFromConversation.__name__] \ No newline at end of file + BanSubmindFromConversation.__name__] From fc8c658fc27fad309b28e5b6a69d6514a647e311 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 13:14:27 -0800 Subject: [PATCH 05/53] Add a validator to `NewMessage` to support deprecated `message_body` input referenced in Klat Observer --- neon_data_models/models/api/klat/socketio.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 872c3c8b..49f8edf5 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -27,7 +27,7 @@ from typing import Optional, Dict, List, Literal, Union from neon_data_models.models.api.llm import LLMPersona -from pydantic import Field +from pydantic import Field, model_validator from neon_data_models.enum import CcaiPromptStates from neon_data_models.models.base import BaseModel @@ -90,6 +90,15 @@ class NewMessage(BaseModel): message_id: str = Field(description="UUID for this message") bound_service: str = Field(default="", description="Service this message is targeting") + @model_validator(mode='before') + @classmethod + def validate_inputs(cls, values): + # Prefer the proper `messageText` key, but accept `message_body` for + # backwards-compat. + values['messageText'] = values.get('messageText') or \ + values.pop('message_body') + return values + class Config: use_enum_values = True validate_default = True From 0503321bd488351e6c246783904ce7cac7c67e4d Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 15:19:19 -0800 Subject: [PATCH 06/53] Annotate `NewMessage.promptState` as Optional --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 49f8edf5..178c506e 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -58,7 +58,7 @@ class NewMessage(BaseModel): userID: str = Field(description="User ID associated with the message") prompt_id: Optional[str] = Field( default=None, description="Prompt ID this message is in response to") - promptState: CcaiPromptStates = Field( + promptState: Optional[CcaiPromptStates] = Field( default=None, description="Associated CCAI state if `prompt_id` is defined") source: str = Field(description="Username associated with the message") From 9f07f529281d5e8503350342a9d414520700a66f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 17:00:26 -0800 Subject: [PATCH 07/53] Refactor `id` to `_id` instead of aliasing --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 178c506e..e7720d0b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -117,7 +117,7 @@ class GetPromptData(BaseModel): class PromptData(BaseModel): class _PromptData(BaseModel): - id: str = Field(alias="_id", description="Unique ID for the prompt") + _id: str = Field(description="Unique ID for the prompt") is_completed: Literal['0', '1'] = Field( description="'1' if a response to the prompt has been determined") proposed_responses: Dict[str, str] = Field( From 5e81cae4d7225969f91840c5316be947f9541a83 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 17:09:46 -0800 Subject: [PATCH 08/53] Override `_PromptData.model_dump` to dump by alias by default Add unit test case to validate PromptData serialization --- neon_data_models/models/api/klat/socketio.py | 6 +++- tests/models/api/test_klat.py | 37 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/models/api/test_klat.py diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index e7720d0b..1f17265b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -117,7 +117,7 @@ class GetPromptData(BaseModel): class PromptData(BaseModel): class _PromptData(BaseModel): - _id: str = Field(description="Unique ID for the prompt") + id: str = Field(alias="_id", description="Unique ID for the prompt") is_completed: Literal['0', '1'] = Field( description="'1' if a response to the prompt has been determined") proposed_responses: Dict[str, str] = Field( @@ -133,6 +133,10 @@ class _PromptData(BaseModel): default=[], description="List of subminds that participated in this prompt") + def model_dump(self, *args, **kwargs): + kwargs.setdefault('by_alias', True) + return BaseModel.model_dump(self, *args, **kwargs) + data: Union[_PromptData, List[_PromptData]] = Field(description="Prompt data") receiver: str = Field(description="Nickname of user requesting prompt data") cid: str = Field(description="Conversation ID associated with the prompt") diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py new file mode 100644 index 00000000..f74bea0e --- /dev/null +++ b/tests/models/api/test_klat.py @@ -0,0 +1,37 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2024 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 unittest import TestCase + + +class TestKlat(TestCase): + def test_prompt_data(self): + from neon_data_models.models.api.klat.socketio import PromptData + test_object = PromptData._PromptData(_id="test_id", + is_completed='1') + self.assertIsInstance(test_object, PromptData._PromptData) + self.assertEqual(test_object, + PromptData._PromptData(**test_object.model_dump())) From 852cb2a55945d2b8b302f6d0592e636ca2bd8395 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 9 Jan 2025 17:20:40 -0800 Subject: [PATCH 09/53] override serialization instead of model_dump and increase test coverage to resolve PromptData serialization bug --- neon_data_models/models/api/klat/socketio.py | 13 +++++++++---- tests/models/api/test_klat.py | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 1f17265b..d6f69a65 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -27,7 +27,7 @@ from typing import Optional, Dict, List, Literal, Union from neon_data_models.models.api.llm import LLMPersona -from pydantic import Field, model_validator +from pydantic import Field, model_validator, model_serializer from neon_data_models.enum import CcaiPromptStates from neon_data_models.models.base import BaseModel @@ -133,9 +133,14 @@ class _PromptData(BaseModel): default=[], description="List of subminds that participated in this prompt") - def model_dump(self, *args, **kwargs): - kwargs.setdefault('by_alias', True) - return BaseModel.model_dump(self, *args, **kwargs) + @model_serializer + def alias_serialize(self): + return {"_id": self.id, + "is_completed": self.is_completed, + "proposed_responses": self.proposed_responses, + "submind_opinions": self.submind_opinions, + "votes": self.votes, + "participating_subminds": self.participating_subminds} data: Union[_PromptData, List[_PromptData]] = Field(description="Prompt data") receiver: str = Field(description="Nickname of user requesting prompt data") diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py index f74bea0e..281575c6 100644 --- a/tests/models/api/test_klat.py +++ b/tests/models/api/test_klat.py @@ -35,3 +35,9 @@ def test_prompt_data(self): self.assertIsInstance(test_object, PromptData._PromptData) self.assertEqual(test_object, PromptData._PromptData(**test_object.model_dump())) + + valid_prompt_data = PromptData(data=test_object, + receiver="mock_username", + cid="mock_conversation") + self.assertEqual(valid_prompt_data, + PromptData(**valid_prompt_data.model_dump())) \ No newline at end of file From b6e52df22530a622fb3ce3d5b50f8f5b70163ac2 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 27 Mar 2025 15:26:55 -0700 Subject: [PATCH 10/53] Update `isAnnouncement`, `timeCreated`, and `messageID` to validate types observed on Klat SocketIO API --- neon_data_models/models/api/klat/socketio.py | 26 ++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d6f69a65..1f7976f9 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -1,6 +1,6 @@ # NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System # All trademark and other rights reserved by their respective owners -# Copyright 2008-2024 Neongecko.com Inc. +# Copyright 2008-2025 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: @@ -23,12 +23,14 @@ # 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 uuid -from typing import Optional, Dict, List, Literal, Union -from neon_data_models.models.api.llm import LLMPersona +from typing import Optional, Dict, List, Literal, Union +from datetime import datetime from pydantic import Field, model_validator, model_serializer +from neon_data_models.models.api.llm import LLMPersona from neon_data_models.enum import CcaiPromptStates from neon_data_models.models.base import BaseModel @@ -56,12 +58,13 @@ class GetTtsRequest(BaseModel): class NewMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") userID: str = Field(description="User ID associated with the message") - prompt_id: Optional[str] = Field( + userDisplayName: str = Field(description="Username of the sender") + promptID: Optional[str] = Field( default=None, description="Prompt ID this message is in response to") promptState: Optional[CcaiPromptStates] = Field( default=None, description="Associated CCAI state if `prompt_id` is defined") - source: str = Field(description="Username associated with the message") + source: str = Field(description="Service associated with the message") messageText: str = Field( description="Message content (input string or audio filename)") repliedMessage: Optional[str] = Field( @@ -83,11 +86,10 @@ class NewMessage(BaseModel): default={}, description="TTS Audio formatted as {: {: " "}}") - isAnnouncement: bool = Field( - default=False, - description="True if the message is a system announcement") - timeCreated: int = Field(description="Unix timestamp (epoch seconds)") - message_id: str = Field(description="UUID for this message") + isAnnouncement: Literal['0', '1'] = Field( + default='0', description="True if the message is a system announcement") + timeCreated: datetime = Field(description="Unix timestamp (epoch seconds)") + messageID: str = Field(description="UUID for this message") bound_service: str = Field(default="", description="Service this message is targeting") @model_validator(mode='before') @@ -97,6 +99,10 @@ def validate_inputs(cls, values): # backwards-compat. values['messageText'] = values.get('messageText') or \ values.pop('message_body') + values['is_bot'] = values.get('is_bot') or \ + values.pop('isBot', '0') + values['promptID'] = values.get('promptID') or \ + values.pop('prompt_id', None) return values class Config: From e86799a477336bc5d5a890ca5ab3b13475dd8e30 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 29 Sep 2025 17:32:47 -0700 Subject: [PATCH 11/53] Refactor after rebase on dev --- neon_data_models/models/api/klat/socketio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 1f7976f9..bee0ab8e 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -31,7 +31,7 @@ from pydantic import Field, model_validator, model_serializer from neon_data_models.models.api.llm import LLMPersona -from neon_data_models.enum import CcaiPromptStates +from neon_data_models.enum import CcaiState from neon_data_models.models.base import BaseModel @@ -61,7 +61,7 @@ class NewMessage(BaseModel): userDisplayName: str = Field(description="Username of the sender") promptID: Optional[str] = Field( default=None, description="Prompt ID this message is in response to") - promptState: Optional[CcaiPromptStates] = Field( + promptState: Optional[CcaiState] = Field( default=None, description="Associated CCAI state if `prompt_id` is defined") source: str = Field(description="Service associated with the message") From 91d9698895cd219f783cc2e18246145c174c7c4b Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 29 Sep 2025 17:36:06 -0700 Subject: [PATCH 12/53] Update based on pyklatchat-server current implementation --- neon_data_models/models/api/klat/socketio.py | 179 ++++++++++++------- 1 file changed, 111 insertions(+), 68 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index bee0ab8e..4954dc0e 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -38,71 +38,105 @@ class GetSttRequest(BaseModel): cid: str = Field(description="Conversation ID associated with the request") sid: str = Field( - description="Client Session ID associated with the request") + description="Client Session ID associated with the request" + ) + lang: str = Field( + description="BCP-47 Language code associated with audio", + default="en-us", + ) message_id: str = Field( - description="Message (Shout) ID associated with the request") + description="Message (Shout) ID associated with the request" + ) audio_data: str = Field( - description="B64-encoded audio file object to transcribe") + description="B64-encoded audio file object to transcribe" + ) class GetTtsRequest(BaseModel): cid: str = Field(description="Conversation ID associated with the request") sid: str = Field( - description="Client Session ID associated with the request") + description="Client Session ID associated with the request" + ) message_id: str = Field( - description="Message (Shout) ID associated with the request") + description="Message (Shout) ID associated with the request" + ) text: str = Field(description="Text to generate audio for") - lang: str = Field(description="BCP-47 Language code associated with `text`") + lang: str = Field( + description="BCP-47 Language code associated with `text`", + default="en-us", + ) + + @model_validator(mode="before") + @classmethod + def validate_inputs(cls, values): + if "message_text" in values: + values.setdefault("text", values.pop("message_text")) + return values class NewMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") userID: str = Field(description="User ID associated with the message") - userDisplayName: str = Field(description="Username of the sender") + userDisplayName: Optional[str] = Field( + description="Username of the sender", default=None + ) promptID: Optional[str] = Field( - default=None, description="Prompt ID this message is in response to") + default=None, description="Prompt ID this message is in response to" + ) promptState: Optional[CcaiState] = Field( default=None, - description="Associated CCAI state if `prompt_id` is defined") + description="Associated CCAI state if `prompt_id` is defined", + ) source: str = Field(description="Service associated with the message") messageText: str = Field( - description="Message content (input string or audio filename)") + description="Message content (input string or audio filename)" + ) repliedMessage: Optional[str] = Field( - default=None, description="Message ID this message is a reply to") - is_bot: Literal['0', '1'] = Field( - default='0', description="'1' if the message came from a bot, else '0'") - lang: str = Field(default='en', description="ISO 639-1 Language code") + default=None, description="Message ID this message is a reply to" + ) + is_bot: Literal["0", "1"] = Field( + default="0", description="'1' if the message came from a bot, else '0'" + ) + lang: str = Field(default="en", description="ISO 639-1 Language code") attachments: List[str] = Field( default=[], - description="List of string filenames attached to this message") + description="List of string filenames attached to this message", + ) context: dict = Field(default={}, description="Optional arbitrary context") test: bool = Field( default=False, - description="True if this message is associated with testing") - isAudio: Literal['0', '1'] = Field( + description="True if this message is associated with testing", + ) + isAudio: Literal["0", "1"] = Field( default="0", - description="'1' if messageText represents encoded WAV audio") - messageTTS: Dict[str, Dict[Literal['male', 'female'], str]] = Field( + description="'1' if messageText represents encoded WAV audio", + ) + messageTTS: Dict[str, Dict[Literal["male", "female"], str]] = Field( default={}, description="TTS Audio formatted as {: {: " - "}}") - isAnnouncement: Literal['0', '1'] = Field( - default='0', description="True if the message is a system announcement") + "}}", + ) + isAnnouncement: Literal["0", "1"] = Field( + default="0", description="True if the message is a system announcement" + ) timeCreated: datetime = Field(description="Unix timestamp (epoch seconds)") messageID: str = Field(description="UUID for this message") - bound_service: str = Field(default="", description="Service this message is targeting") + bound_service: str = Field( + default="", description="Service this message is targeting" + ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def validate_inputs(cls, values): # Prefer the proper `messageText` key, but accept `message_body` for # backwards-compat. - values['messageText'] = values.get('messageText') or \ - values.pop('message_body') - values['is_bot'] = values.get('is_bot') or \ - values.pop('isBot', '0') - values['promptID'] = values.get('promptID') or \ - values.pop('prompt_id', None) + values["messageText"] = values.get("messageText") or values.pop( + "message_body" + ) + values["is_bot"] = values.get("is_bot") or values.pop("isBot", "0") + values["promptID"] = values.get("promptID") or values.pop( + "prompt_id", None + ) return values class Config: @@ -116,56 +150,57 @@ class GetPromptData(BaseModel): limit: int = Field( default=5, description="Maximum number of prompts to return if `prompt_id` " - "is unset") + "is unset", + ) prompt_id: Optional[str] = Field( - default=None, description="Optional prompt ID to get data for") + default=None, description="Optional prompt ID to get data for" + ) class PromptData(BaseModel): class _PromptData(BaseModel): id: str = Field(alias="_id", description="Unique ID for the prompt") - is_completed: Literal['0', '1'] = Field( - description="'1' if a response to the prompt has been determined") + is_completed: Literal["0", "1"] = Field( + description="'1' if a response to the prompt has been determined" + ) proposed_responses: Dict[str, str] = Field( default={}, - description="Dict of participant name to proposed response") + description="Dict of participant name to proposed response", + ) submind_opinions: Dict[str, str] = Field( default={}, - description="Dict of participant name to opinion response") + description="Dict of participant name to opinion response", + ) votes: Dict[str, str] = Field( - default={}, - description="Dict of participant name to vote") + default={}, description="Dict of participant name to vote" + ) participating_subminds: List[str] = Field( default=[], - description="List of subminds that participated in this prompt") + description="List of subminds that participated in this prompt", + ) @model_serializer def alias_serialize(self): - return {"_id": self.id, - "is_completed": self.is_completed, - "proposed_responses": self.proposed_responses, - "submind_opinions": self.submind_opinions, - "votes": self.votes, - "participating_subminds": self.participating_subminds} - - data: Union[_PromptData, List[_PromptData]] = Field(description="Prompt data") - receiver: str = Field(description="Nickname of user requesting prompt data") + return { + "_id": self.id, + "is_completed": self.is_completed, + "proposed_responses": self.proposed_responses, + "submind_opinions": self.submind_opinions, + "votes": self.votes, + "participating_subminds": self.participating_subminds, + } + + data: Union[_PromptData, List[_PromptData]] = Field( + description="Prompt data" + ) + receiver: str = Field( + description="Nickname of user requesting prompt data" + ) cid: str = Field(description="Conversation ID associated with the prompt") request_id: str = Field( default_factory=lambda: str(uuid.uuid4()), - description="Unique ID of the request to identify the response") - - -class RequestNeonTranslations(BaseModel): - class CidTranslationRequest(BaseModel): - lang: str = Field(description="Translation target language") - source_lang: str = Field(description="Source language of shouts") - shouts: Dict[str, str] = Field( - default={}, description="Dict of shout ID to text in `lang`") - - request_id: str = Field(description="UUID for this request") - data: Dict[str, CidTranslationRequest] = Field( - description="Mapping of conversation ID to required translation") + description="Unique ID of the request to identify the response", + ) class AuthExpired(BaseModel): @@ -175,10 +210,12 @@ class AuthExpired(BaseModel): class ConfiguredPersonasChanged(BaseModel): - personas: Dict[str, List[LLMPersona]] = ( - Field(description="Dict of LLM name to list of supported personas")) + personas: Dict[str, List[LLMPersona]] = Field( + description="Dict of LLM name to list of supported personas" + ) update_time: int = Field( - description="Unix timestamp when the change occurred") + description="Unix timestamp when the change occurred" + ) class BanSubmindFromConversation(BaseModel): @@ -187,7 +224,13 @@ class BanSubmindFromConversation(BaseModel): nick: str = Field(description="Username of the submind to (un-)ban") -__all__ = [GetSttRequest.__name__, GetTtsRequest.__name__, NewMessage.__name__, - GetPromptData.__name__, PromptData.__name__, - RequestNeonTranslations.__name__, AuthExpired.__name__, - BanSubmindFromConversation.__name__] +__all__ = [ + GetSttRequest.__name__, + GetTtsRequest.__name__, + NewMessage.__name__, + GetPromptData.__name__, + PromptData.__name__, + RequestNeonTranslations.__name__, + AuthExpired.__name__, + BanSubmindFromConversation.__name__, +] From ebd67820f3eda2d7020a58616292d96bd275669b Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 29 Sep 2025 18:33:23 -0700 Subject: [PATCH 13/53] Implement SIO message models for everything in pyklatchat-server --- neon_data_models/models/api/klat/socketio.py | 219 +++++++++++++++++-- 1 file changed, 200 insertions(+), 19 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 4954dc0e..dafb47b9 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -26,7 +26,8 @@ import uuid -from typing import Optional, Dict, List, Literal, Union +from time import time +from typing import Optional, Dict, List, Literal, Union, Any from datetime import datetime from pydantic import Field, model_validator, model_serializer @@ -52,6 +53,35 @@ class GetSttRequest(BaseModel): ) +class GetSttResponse(BaseModel): + transcript: str = Field(description="Transcribed text") + lang: str = Field( + description="BCP-47 Language code associated with `transcript`", + default="en-us", + ) + message_id: str = Field( + description="Message (Shout) ID associated with the request" + ) + sid: str = Field( + description="Client Session ID associated with the request" + ) + cid: str = Field(description="Conversation ID associated with the request") + + @model_validator(mode="before") + @classmethod + def parse_context(cls, values): + """ + Parse out message_id, sid, and cid from context if not handled by the + Observer module. + """ + values.setdefault( + "message_id", values.get("context", {}).get("message_id") + ) + values.setdefault("sid", values.get("context", {}).get("sid")) + values.setdefault("cid", values.get("context", {}).get("cid")) + return values + + class GetTtsRequest(BaseModel): cid: str = Field(description="Conversation ID associated with the request") sid: str = Field( @@ -74,25 +104,92 @@ def validate_inputs(cls, values): return values +class GetTtsResponse(BaseModel): + audio_data: str = Field( + description="B64-encoded WAV audio file object generated from `text`" + ) + lang: str = Field( + description="BCP-47 Language code associated with `audio_data`", + default="en-us", + ) + gender: Literal["male", "female", "undefined"] = Field( + description="Gender associated with generated audio", + default="undefined", + ) + message_id: str = Field( + description="Message (Shout) ID associated with the request" + ) + sid: str = Field( + description="Client Session ID associated with the request" + ) + cid: str = Field(description="Conversation ID associated with the request") + + @model_validator(mode="before") + @classmethod + def parse_context(cls, values): + """ + Parse out message_id, sid, and cid from context if not handled by the + Observer module. + """ + values.setdefault( + "message_id", values.get("context", {}).get("message_id") + ) + values.setdefault("sid", values.get("context", {}).get("sid")) + values.setdefault("cid", values.get("context", {}).get("cid")) + return values + + def to_db_query(self) -> Dict[str, Any]: + return { + "shout_id": self.message_id, + "audio_data": self.audio_data, + "lang": self.lang, + "gender": self.gender, + } + + def to_incoming_tts(self) -> Dict[str, Any]: + return { + "cid": self.cid, + "message_id": self.message_id, + "audio_data": self.audio_data, + "lang": self.lang, + "gender": self.gender, + } + + +class NewPromptMessage(BaseModel): + cid: str = Field(description="Conversation ID associated with the prompt") + user_id: str = Field(description="User ID associated with the prompt", + alias="userID") + prompt_id: str = Field(description="Unique ID for the prompt", alias="promptID") + prompt_state: CcaiState = Field( + description="CCAI state for the prompt", default=CcaiState.IDLE, alias="promptState" + ) + context: Dict[str, Any] = Field( + description="Extra context for the prompt", default={} + ) + + class NewMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") - userID: str = Field(description="User ID associated with the message") - userDisplayName: Optional[str] = Field( - description="Username of the sender", default=None + user_id: str = Field(description="User ID associated with the message", alias="userID") + user_nick: Optional[str] = Field( + description="Username of the sender", default=None, alias="userDisplayName" ) - promptID: Optional[str] = Field( - default=None, description="Prompt ID this message is in response to" + prompt_id: Optional[str] = Field( + default=None, description="Prompt ID this message is in response to", alias="promptID" ) promptState: Optional[CcaiState] = Field( - default=None, + default=None, alias="promptState", description="Associated CCAI state if `prompt_id` is defined", ) source: str = Field(description="Service associated with the message") - messageText: str = Field( - description="Message content (input string or audio filename)" + message_body: str = Field( + description="Message content (input string or audio filename)", + alias="messageText" ) - repliedMessage: Optional[str] = Field( - default=None, description="Message ID this message is a reply to" + replied_message: Optional[str] = Field( + default=None, description="Message ID this message is a reply to", + alias="repliedMessage" ) is_bot: Literal["0", "1"] = Field( default="0", description="'1' if the message came from a bot, else '0'" @@ -107,20 +204,24 @@ class NewMessage(BaseModel): default=False, description="True if this message is associated with testing", ) - isAudio: Literal["0", "1"] = Field( + is_audio: Literal["0", "1"] = Field( default="0", description="'1' if messageText represents encoded WAV audio", + alias="isAudio", ) - messageTTS: Dict[str, Dict[Literal["male", "female"], str]] = Field( - default={}, + message_tts: Dict[str, Dict[Literal["male", "female"], str]] = Field( + default={}, alias="messageTTS", description="TTS Audio formatted as {: {: " "}}", ) - isAnnouncement: Literal["0", "1"] = Field( - default="0", description="True if the message is a system announcement" + is_announcement: Literal["0", "1"] = Field( + default="0", description="True if the message is a system announcement", + alias="isAnnouncement" ) - timeCreated: datetime = Field(description="Unix timestamp (epoch seconds)") - messageID: str = Field(description="UUID for this message") + time_created: datetime = Field(description="Unix timestamp (epoch seconds)", + alias="timeCreated") + message_id: str = Field(description="UUID for this message", + alias="messageID") bound_service: str = Field( default="", description="Service this message is targeting" ) @@ -143,6 +244,73 @@ class Config: use_enum_values = True validate_default = True + def to_db_query(self) -> Dict[str, Any]: + return { + "_id": self.message_id, + "cid": self.cid, + "user_id": self.user_id, + "prompt_id": self.prompt_id, + "message_text": self.message_body, + "message_lang": self.lang, + "attachments": self.attachments, + "replied_message": self.replied_message, + "is_audio": self.is_audio, + "is_announcement": self.is_announcement, + "is_bot": self.is_bot, + "translations": {}, + "created_on": int(self.time_created.timestamp()), + } + + def to_new_prompt_message(self) -> NewPromptMessage: + return NewPromptMessage( + cid=self.cid, + user_id=self.user_id, + prompt_id=self.prompt_id, + prompt_state=self.promptState, + context=self.context, + ) + + +class NewCcaiPrompt(BaseModel): + prompt_text: str = Field( + description="Text of the prompt" + ) # TODO: Check if formatted to remove prefix + cid: str = Field(description="Conversation ID associated with the prompt") + prompt_id: str = Field( + description="Unique ID for the prompt" + ) # TODO: Check for default value creation in Klat server + created_on: int = Field( + descrtion="Epoch seconds of prompt creation", + default_factory=lambda: int(time()), + ) + context: Dict[str, Any] = Field( + description="Extra context for the prompt", deprecated=True, default={} + ) + + def to_db_query(self) -> Dict[str, Any]: + return { + "_id": self.prompt_id, + "cid": self.cid, + "is_completed": "0", + "data": {"prompt_text": self.prompt_text}, + "created_on": self.created_on, + "context": self.context, + } + + +class CcaiPromptCompleted(BaseModel): + winner: str = Field(description="Winning response text") + prompt_id: str = Field(description="Unique ID for the prompt") + + @model_validator(mode="before") + @classmethod + def validate_inputs(cls, values): + values.setdefault( + "prompt_id", + values.get("context", {}).get("prompt", {}).get("prompt_id"), + ) + return values + class GetPromptData(BaseModel): nick: str = Field(description="Nickname of user requesting prompt data") @@ -156,6 +324,15 @@ class GetPromptData(BaseModel): default=None, description="Optional prompt ID to get data for" ) + def to_db_query(self) -> Dict[str, Any]: + assert self.prompt_id is not None, "prompt_id must be defined" + return { + "cid": self.cid, + "limit": self.limit, + "prompt_ids": [self.prompt_id], + "fetch_user_data": True, + } + class PromptData(BaseModel): class _PromptData(BaseModel): @@ -226,11 +403,15 @@ class BanSubmindFromConversation(BaseModel): __all__ = [ GetSttRequest.__name__, + GetSttResponse.__name__, GetTtsRequest.__name__, + GetTtsResponse.__name__, NewMessage.__name__, + NewPromptMessage.__name__, GetPromptData.__name__, + NewCcaiPrompt.__name__, + CcaiPromptCompleted.__name__, PromptData.__name__, - RequestNeonTranslations.__name__, AuthExpired.__name__, BanSubmindFromConversation.__name__, ] From f47efe1211e2da79c59180cc6619f5ca0b2f8ddd Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 13:00:48 -0700 Subject: [PATCH 14/53] Address issues noted in klat-server implementation and format file --- neon_data_models/models/api/klat/socketio.py | 79 ++++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index dafb47b9..7b8cd2c6 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -158,38 +158,51 @@ def to_incoming_tts(self) -> Dict[str, Any]: class NewPromptMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the prompt") - user_id: str = Field(description="User ID associated with the prompt", - alias="userID") - prompt_id: str = Field(description="Unique ID for the prompt", alias="promptID") + user_id: str = Field( + description="User ID associated with the prompt", alias="userID" + ) + prompt_id: str = Field( + description="Unique ID for the prompt", alias="promptID" + ) prompt_state: CcaiState = Field( - description="CCAI state for the prompt", default=CcaiState.IDLE, alias="promptState" + description="CCAI state for the prompt", + default=CcaiState.IDLE, + alias="promptState", ) context: Dict[str, Any] = Field( description="Extra context for the prompt", default={} ) -class NewMessage(BaseModel): +class UserMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") - user_id: str = Field(description="User ID associated with the message", alias="userID") + user_id: str = Field( + description="User ID associated with the message", alias="userID" + ) user_nick: Optional[str] = Field( - description="Username of the sender", default=None, alias="userDisplayName" + description="Username of the sender", + default=None, + alias="userDisplayName", ) prompt_id: Optional[str] = Field( - default=None, description="Prompt ID this message is in response to", alias="promptID" + default=None, + description="Prompt ID this message is in response to", + alias="promptID", ) promptState: Optional[CcaiState] = Field( - default=None, alias="promptState", + default=None, + alias="promptState", description="Associated CCAI state if `prompt_id` is defined", ) source: str = Field(description="Service associated with the message") message_body: str = Field( description="Message content (input string or audio filename)", - alias="messageText" + alias="messageText", ) replied_message: Optional[str] = Field( - default=None, description="Message ID this message is a reply to", - alias="repliedMessage" + default=None, + description="Message ID this message is a reply to", + alias="repliedMessage", ) is_bot: Literal["0", "1"] = Field( default="0", description="'1' if the message came from a bot, else '0'" @@ -210,18 +223,24 @@ class NewMessage(BaseModel): alias="isAudio", ) message_tts: Dict[str, Dict[Literal["male", "female"], str]] = Field( - default={}, alias="messageTTS", + default={}, + alias="messageTTS", description="TTS Audio formatted as {: {: " "}}", ) is_announcement: Literal["0", "1"] = Field( - default="0", description="True if the message is a system announcement", - alias="isAnnouncement" + default="0", + description="True if the message is a system announcement", + alias="isAnnouncement", + ) + time_created: datetime = Field( + description="Unix timestamp (epoch seconds)", alias="timeCreated" + ) + message_id: str = Field( + description="UUID for this message", + alias="messageID", + default_factory=lambda: uuid.uuid4().hex[:10], ) - time_created: datetime = Field(description="Unix timestamp (epoch seconds)", - alias="timeCreated") - message_id: str = Field(description="UUID for this message", - alias="messageID") bound_service: str = Field( default="", description="Service this message is targeting" ) @@ -231,10 +250,14 @@ class NewMessage(BaseModel): def validate_inputs(cls, values): # Prefer the proper `messageText` key, but accept `message_body` for # backwards-compat. - values["messageText"] = values.get("messageText") or values.pop( + values["messageText"] = values.get("messageText") or values.get( "message_body" ) - values["is_bot"] = values.get("is_bot") or values.pop("isBot", "0") + values["is_bot"] = ( + values.get("is_bot") + or values.get("isBot") + or values.pop("bot", "0") + ) values["promptID"] = values.get("promptID") or values.pop( "prompt_id", None ) @@ -301,6 +324,12 @@ def to_db_query(self) -> Dict[str, Any]: class CcaiPromptCompleted(BaseModel): winner: str = Field(description="Winning response text") prompt_id: str = Field(description="Unique ID for the prompt") + request_id: str = Field( + description="ID of the database transaction request" + ) + context: Dict[str, Any] = Field( + description="Extra context for the prompt", default={} + ) @model_validator(mode="before") @classmethod @@ -311,6 +340,12 @@ def validate_inputs(cls, values): ) return values + def to_db_query(self) -> Dict[str, Any]: + return { + "prompt_id": self.prompt_id, + "prompt_context": self.context, + } + class GetPromptData(BaseModel): nick: str = Field(description="Nickname of user requesting prompt data") @@ -406,7 +441,7 @@ class BanSubmindFromConversation(BaseModel): GetSttResponse.__name__, GetTtsRequest.__name__, GetTtsResponse.__name__, - NewMessage.__name__, + UserMessage.__name__, NewPromptMessage.__name__, GetPromptData.__name__, NewCcaiPrompt.__name__, From 3cff8ba63df51b1c45c575529ee76241379043b2 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 13:10:32 -0700 Subject: [PATCH 15/53] Fix missed refactor from `promptState` -> `prompt_state` --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 7b8cd2c6..d628c5c2 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -189,7 +189,7 @@ class UserMessage(BaseModel): description="Prompt ID this message is in response to", alias="promptID", ) - promptState: Optional[CcaiState] = Field( + prompt_state: Optional[CcaiState] = Field( default=None, alias="promptState", description="Associated CCAI state if `prompt_id` is defined", From 87049bc6b7b4ce79ff8a68216b921ad3f9ef74f0 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 13:14:55 -0700 Subject: [PATCH 16/53] Add backwards-compat. validation --- neon_data_models/models/api/klat/socketio.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d628c5c2..29d901ff 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -310,6 +310,15 @@ class NewCcaiPrompt(BaseModel): description="Extra context for the prompt", deprecated=True, default={} ) + @model_validator(mode="before") + @classmethod + def validate_inputs(cls, values): + # Handle an invalid input context as a valid empty dict + # for backwards compatibility + if values.get("context", {}) is None: + values["context"] = {} + return values + def to_db_query(self) -> Dict[str, Any]: return { "_id": self.prompt_id, From d499e599e5ab15e03404f9e2c8b67279391ed54a Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 13:22:18 -0700 Subject: [PATCH 17/53] Address observed errors in deployment --- neon_data_models/models/api/klat/socketio.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 29d901ff..14d03f23 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -289,7 +289,7 @@ def to_new_prompt_message(self) -> NewPromptMessage: cid=self.cid, user_id=self.user_id, prompt_id=self.prompt_id, - prompt_state=self.promptState, + prompt_state=self.prompt_state, context=self.context, ) @@ -331,10 +331,13 @@ def to_db_query(self) -> Dict[str, Any]: class CcaiPromptCompleted(BaseModel): - winner: str = Field(description="Winning response text") + winner: Optional[str] = Field( + default=None, + description="Winning response text or `None` in the event of an error", + ) prompt_id: str = Field(description="Unique ID for the prompt") - request_id: str = Field( - description="ID of the database transaction request" + request_id: Optional[str] = Field( + default=None, description="ID of the database transaction request" ) context: Dict[str, Any] = Field( description="Extra context for the prompt", default={} From 3a91d14b472fde0121d8ae3096391292e96644ba Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 14:16:56 -0700 Subject: [PATCH 18/53] Add backwards-compat. parsing to UserMessage --- neon_data_models/models/api/klat/socketio.py | 42 +++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 14d03f23..bc527cf9 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -242,9 +242,18 @@ class UserMessage(BaseModel): default_factory=lambda: uuid.uuid4().hex[:10], ) bound_service: str = Field( - default="", description="Service this message is targeting" + default="", description="Service this message is targeting", ) + # Below are observed as used, but purpose is unclear or deprecated + no_save: bool = Field(default=False, deprecated=True) + title: Optional[str] = Field(default=None, deprecated=True) + routing_key: Optional[str] = Field(default=None, deprecated=True) + bot_type: Optional[Any] = Field(default=None, deprecated=True) + omit_reply: bool = Field(default=False, deprecated=True) + to_discussion: Optional[Any] = Field(default=None, deprecated=True) + dom: Optional[str] = Field(default=None, deprecated=True) + @model_validator(mode="before") @classmethod def validate_inputs(cls, values): @@ -252,7 +261,7 @@ def validate_inputs(cls, values): # backwards-compat. values["messageText"] = values.get("messageText") or values.get( "message_body" - ) + ) or values.get("shout") or values.get("message_text") values["is_bot"] = ( values.get("is_bot") or values.get("isBot") @@ -261,6 +270,12 @@ def validate_inputs(cls, values): values["promptID"] = values.get("promptID") or values.pop( "prompt_id", None ) + + # Map some observed legacy keys + values["userDisplayName"] = values.get("userDisplayName") or values.get('nick') or values.get('username') + values["bound_service"] = values.get("bound_service") or values.get('service_name') + values["repliedMessage"] = values.get("repliedMessage") or values.get('responded_shout') + return values class Config: @@ -293,6 +308,29 @@ def to_new_prompt_message(self) -> NewPromptMessage: context=self.context, ) + def model_dump(self, **kwargs): + """ + Override model_dump to include SIO fields for backwards compatibility + """ + + # For backwards-compat with Klat Client, include aliased keys in + # serialization. In the future, this should be configurable and + # eventually removed. + by_alias = {} + if 'by_alias' not in kwargs: + by_alias = super().model_dump(by_alias=True, **kwargs) + + # Add parameters for backwards-compat. + by_alias['nick'] = self.user_nick + by_alias['message_text'] = self.message_body + by_alias['username'] = self.user_nick + by_alias['service_name'] = self.bound_service + by_alias['bot'] = self.is_bot + by_alias['shout'] = self.message_body + by_alias['time'] = int(self.time_created.timestamp()) + by_alias['responded_shout'] = self.replied_message + + return {**super().model_dump(**kwargs), **by_alias} class NewCcaiPrompt(BaseModel): prompt_text: str = Field( From 7dec28c93aeb5bfe46da21ad0d532c34de3b4c87 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 14:20:52 -0700 Subject: [PATCH 19/53] Fix `bound_service` default value handling --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index bc527cf9..f32edae8 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -273,7 +273,7 @@ def validate_inputs(cls, values): # Map some observed legacy keys values["userDisplayName"] = values.get("userDisplayName") or values.get('nick') or values.get('username') - values["bound_service"] = values.get("bound_service") or values.get('service_name') + values["bound_service"] = values.get("bound_service") or values.get('service_name') or "" values["repliedMessage"] = values.get("repliedMessage") or values.get('responded_shout') return values From a4839d1794dabd417e83ebff66622583bf742dae Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 14:30:06 -0700 Subject: [PATCH 20/53] Add backwards-compat serialization --- neon_data_models/models/api/klat/socketio.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index f32edae8..3100fda1 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -66,6 +66,7 @@ class GetSttResponse(BaseModel): description="Client Session ID associated with the request" ) cid: str = Field(description="Conversation ID associated with the request") + context: Dict[str, Any] = Field(default={}, description="Optional context") @model_validator(mode="before") @classmethod @@ -100,7 +101,7 @@ class GetTtsRequest(BaseModel): @classmethod def validate_inputs(cls, values): if "message_text" in values: - values.setdefault("text", values.pop("message_text")) + values.setdefault("text", values.get("message_text")) return values @@ -123,6 +124,7 @@ class GetTtsResponse(BaseModel): description="Client Session ID associated with the request" ) cid: str = Field(description="Conversation ID associated with the request") + context: Dict[str, Any] = Field(default={}, description="Optional context") @model_validator(mode="before") @classmethod @@ -173,6 +175,20 @@ class NewPromptMessage(BaseModel): description="Extra context for the prompt", default={} ) + def model_dump(self, **kwargs): + """ + Override model_dump to include SIO fields for backwards compatibility + """ + + # For backwards-compat with Klat Client, include aliased keys in + # serialization. In the future, this should be configurable and + # eventually removed. + by_alias = {} + if 'by_alias' not in kwargs: + by_alias = super().model_dump(by_alias=True, **kwargs) + + return {**super().model_dump(**kwargs), **by_alias} + class UserMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") @@ -328,6 +344,7 @@ def model_dump(self, **kwargs): by_alias['bot'] = self.is_bot by_alias['shout'] = self.message_body by_alias['time'] = int(self.time_created.timestamp()) + by_alias['created_on'] = by_alias['time'] by_alias['responded_shout'] = self.replied_message return {**super().model_dump(**kwargs), **by_alias} From b8e5175bf89a1aa2a226a0a668892714874b085c Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 14:44:14 -0700 Subject: [PATCH 21/53] Update CcaiPromptCompleted to extend UserMessage --- neon_data_models/models/api/klat/socketio.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 3100fda1..03db202d 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -385,18 +385,14 @@ def to_db_query(self) -> Dict[str, Any]: } -class CcaiPromptCompleted(BaseModel): +class CcaiPromptCompleted(UserMessage): winner: Optional[str] = Field( default=None, description="Winning response text or `None` in the event of an error", ) - prompt_id: str = Field(description="Unique ID for the prompt") request_id: Optional[str] = Field( default=None, description="ID of the database transaction request" ) - context: Dict[str, Any] = Field( - description="Extra context for the prompt", default={} - ) @model_validator(mode="before") @classmethod From 0e4e9f77f27d5c1465c0e8407aa23003e82b17e2 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 14:51:51 -0700 Subject: [PATCH 22/53] Troubleshooting CcaiPromptCompleted handling --- neon_data_models/models/api/klat/socketio.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 03db202d..2513b995 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -393,12 +393,18 @@ class CcaiPromptCompleted(UserMessage): request_id: Optional[str] = Field( default=None, description="ID of the database transaction request" ) + prompt_id: str = Field( + description="Prompt ID this message is in response to", + alias="promptID", + ) + sid: str = Field(description="Client Session ID associated with the request") + conversation_context: Dict[str, Any] = Field(description="Context of the conversation", default={}) @model_validator(mode="before") @classmethod def validate_inputs(cls, values): values.setdefault( - "prompt_id", + "promptID", values.get("context", {}).get("prompt", {}).get("prompt_id"), ) return values From 577ce522fbb24c3c117853097f2c6b27c2943252 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 15:08:23 -0700 Subject: [PATCH 23/53] Troubleshooting CcaiPromptCompleted parsing --- neon_data_models/models/api/klat/socketio.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 2513b995..c744781d 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -356,13 +356,13 @@ class NewCcaiPrompt(BaseModel): cid: str = Field(description="Conversation ID associated with the prompt") prompt_id: str = Field( description="Unique ID for the prompt" - ) # TODO: Check for default value creation in Klat server + ) created_on: int = Field( descrtion="Epoch seconds of prompt creation", default_factory=lambda: int(time()), ) context: Dict[str, Any] = Field( - description="Extra context for the prompt", deprecated=True, default={} + description="Extra context for the prompt", default={} ) @model_validator(mode="before") @@ -386,16 +386,15 @@ def to_db_query(self) -> Dict[str, Any]: class CcaiPromptCompleted(UserMessage): - winner: Optional[str] = Field( - default=None, - description="Winning response text or `None` in the event of an error", + winner: str = Field( + default="", + description="Winning response text; empty in the event of an error", ) request_id: Optional[str] = Field( default=None, description="ID of the database transaction request" ) prompt_id: str = Field( description="Prompt ID this message is in response to", - alias="promptID", ) sid: str = Field(description="Client Session ID associated with the request") conversation_context: Dict[str, Any] = Field(description="Context of the conversation", default={}) @@ -403,10 +402,13 @@ class CcaiPromptCompleted(UserMessage): @model_validator(mode="before") @classmethod def validate_inputs(cls, values): + values.setdefault("winner", values.get("context", {}).get("winner", "")) values.setdefault( - "promptID", + "prompt_id", values.get("context", {}).get("prompt", {}).get("prompt_id"), ) + # TODO: Below assertion for initial development; remove before merge + assert values["prompt_id"] != "", f"prompt_id must be defined: {values}" return values def to_db_query(self) -> Dict[str, Any]: From d91cd0fc807820a6e7c64841205b582573963a3d Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 15:18:34 -0700 Subject: [PATCH 24/53] More `CcaiPromptCompleted` `prompt_id` value troubleshooting --- neon_data_models/models/api/klat/socketio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index c744781d..d18c9122 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -403,6 +403,9 @@ class CcaiPromptCompleted(UserMessage): @classmethod def validate_inputs(cls, values): values.setdefault("winner", values.get("context", {}).get("winner", "")) + if values.get("prompt_id") in (None, ""): + # TODO: Figure out where this is set to an invalid value + values.pop("prompt_id") values.setdefault( "prompt_id", values.get("context", {}).get("prompt", {}).get("prompt_id"), From 953c6e7512c00c6c8d025fa975b2c2b64f9a35d4 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 15:58:46 -0700 Subject: [PATCH 25/53] Include backwards-compat. key in CcaiPromptCompleted message --- neon_data_models/models/api/klat/socketio.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d18c9122..caf45ef6 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -420,6 +420,15 @@ def to_db_query(self) -> Dict[str, Any]: "prompt_context": self.context, } + def model_dump(self, **kwargs): + """ + Override model_dump to include SIO fields for backwards compatibility + """ + + # For backwards-compat with Klat Client, include aliased keys + by_alias = {"promptID": self.prompt_id} + return {**super().model_dump(**kwargs), **by_alias} + class GetPromptData(BaseModel): nick: str = Field(description="Nickname of user requesting prompt data") From 14cfe7cfb66e57707517ba48f3384978610aeaac Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 17:46:19 -0700 Subject: [PATCH 26/53] Add default "source" value --- neon_data_models/models/api/klat/socketio.py | 77 +++++++++++++------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index caf45ef6..49c9618b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -180,11 +180,11 @@ def model_dump(self, **kwargs): Override model_dump to include SIO fields for backwards compatibility """ - # For backwards-compat with Klat Client, include aliased keys in + # For backwards-compat with Klat Client, include aliased keys in # serialization. In the future, this should be configurable and # eventually removed. by_alias = {} - if 'by_alias' not in kwargs: + if "by_alias" not in kwargs: by_alias = super().model_dump(by_alias=True, **kwargs) return {**super().model_dump(**kwargs), **by_alias} @@ -210,7 +210,9 @@ class UserMessage(BaseModel): alias="promptState", description="Associated CCAI state if `prompt_id` is defined", ) - source: str = Field(description="Service associated with the message") + source: str = Field( + default="unknown", description="Service associated with the message" + ) message_body: str = Field( description="Message content (input string or audio filename)", alias="messageText", @@ -258,7 +260,8 @@ class UserMessage(BaseModel): default_factory=lambda: uuid.uuid4().hex[:10], ) bound_service: str = Field( - default="", description="Service this message is targeting", + default="", + description="Service this message is targeting", ) # Below are observed as used, but purpose is unclear or deprecated @@ -275,9 +278,12 @@ class UserMessage(BaseModel): def validate_inputs(cls, values): # Prefer the proper `messageText` key, but accept `message_body` for # backwards-compat. - values["messageText"] = values.get("messageText") or values.get( - "message_body" - ) or values.get("shout") or values.get("message_text") + values["messageText"] = ( + values.get("messageText") + or values.get("message_body") + or values.get("shout") + or values.get("message_text") + ) values["is_bot"] = ( values.get("is_bot") or values.get("isBot") @@ -288,9 +294,17 @@ def validate_inputs(cls, values): ) # Map some observed legacy keys - values["userDisplayName"] = values.get("userDisplayName") or values.get('nick') or values.get('username') - values["bound_service"] = values.get("bound_service") or values.get('service_name') or "" - values["repliedMessage"] = values.get("repliedMessage") or values.get('responded_shout') + values["userDisplayName"] = ( + values.get("userDisplayName") + or values.get("nick") + or values.get("username") + ) + values["bound_service"] = ( + values.get("bound_service") or values.get("service_name") or "" + ) + values["repliedMessage"] = values.get("repliedMessage") or values.get( + "responded_shout" + ) return values @@ -329,34 +343,33 @@ def model_dump(self, **kwargs): Override model_dump to include SIO fields for backwards compatibility """ - # For backwards-compat with Klat Client, include aliased keys in + # For backwards-compat with Klat Client, include aliased keys in # serialization. In the future, this should be configurable and # eventually removed. by_alias = {} - if 'by_alias' not in kwargs: + if "by_alias" not in kwargs: by_alias = super().model_dump(by_alias=True, **kwargs) # Add parameters for backwards-compat. - by_alias['nick'] = self.user_nick - by_alias['message_text'] = self.message_body - by_alias['username'] = self.user_nick - by_alias['service_name'] = self.bound_service - by_alias['bot'] = self.is_bot - by_alias['shout'] = self.message_body - by_alias['time'] = int(self.time_created.timestamp()) - by_alias['created_on'] = by_alias['time'] - by_alias['responded_shout'] = self.replied_message + by_alias["nick"] = self.user_nick + by_alias["message_text"] = self.message_body + by_alias["username"] = self.user_nick + by_alias["service_name"] = self.bound_service + by_alias["bot"] = self.is_bot + by_alias["shout"] = self.message_body + by_alias["time"] = int(self.time_created.timestamp()) + by_alias["created_on"] = by_alias["time"] + by_alias["responded_shout"] = self.replied_message return {**super().model_dump(**kwargs), **by_alias} + class NewCcaiPrompt(BaseModel): prompt_text: str = Field( description="Text of the prompt" ) # TODO: Check if formatted to remove prefix cid: str = Field(description="Conversation ID associated with the prompt") - prompt_id: str = Field( - description="Unique ID for the prompt" - ) + prompt_id: str = Field(description="Unique ID for the prompt") created_on: int = Field( descrtion="Epoch seconds of prompt creation", default_factory=lambda: int(time()), @@ -396,13 +409,19 @@ class CcaiPromptCompleted(UserMessage): prompt_id: str = Field( description="Prompt ID this message is in response to", ) - sid: str = Field(description="Client Session ID associated with the request") - conversation_context: Dict[str, Any] = Field(description="Context of the conversation", default={}) + sid: str = Field( + description="Client Session ID associated with the request" + ) + conversation_context: Dict[str, Any] = Field( + description="Context of the conversation", default={} + ) @model_validator(mode="before") @classmethod def validate_inputs(cls, values): - values.setdefault("winner", values.get("context", {}).get("winner", "")) + values.setdefault( + "winner", values.get("context", {}).get("winner", "") + ) if values.get("prompt_id") in (None, ""): # TODO: Figure out where this is set to an invalid value values.pop("prompt_id") @@ -411,7 +430,9 @@ def validate_inputs(cls, values): values.get("context", {}).get("prompt", {}).get("prompt_id"), ) # TODO: Below assertion for initial development; remove before merge - assert values["prompt_id"] != "", f"prompt_id must be defined: {values}" + assert values["prompt_id"] != "", ( + f"prompt_id must be defined: {values}" + ) return values def to_db_query(self) -> Dict[str, Any]: From 6c016bd284081b04c36945ca6f7f11bf19c7b741 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 18:18:45 -0700 Subject: [PATCH 27/53] Add `sid` to `UserMessage` as observed in current deployment --- neon_data_models/models/api/klat/socketio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 49c9618b..d152cb7c 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -191,6 +191,7 @@ def model_dump(self, **kwargs): class UserMessage(BaseModel): + sid: str = Field(description="Client Session ID associated with the message") cid: str = Field(description="Conversation ID associated with the message") user_id: str = Field( description="User ID associated with the message", alias="userID" From 26ae0dfae54fb5d5efd01744c3438d50a9c594db Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 1 Oct 2025 18:39:40 -0700 Subject: [PATCH 28/53] Update `UserMessage` default values to match actual behavior --- neon_data_models/models/api/klat/socketio.py | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d152cb7c..7c1a4aa2 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -191,7 +191,9 @@ def model_dump(self, **kwargs): class UserMessage(BaseModel): - sid: str = Field(description="Client Session ID associated with the message") + sid: str = Field( + description="Client Session ID associated with the message" + ) cid: str = Field(description="Conversation ID associated with the message") user_id: str = Field( description="User ID associated with the message", alias="userID" @@ -247,10 +249,14 @@ class UserMessage(BaseModel): description="TTS Audio formatted as {: {: " "}}", ) - is_announcement: Literal["0", "1"] = Field( + is_announcement: bool = Field( + description="True if the message is a system announcement", + default=False, + ) + isAnnouncement: Literal["0", "1"] = Field( default="0", + deprecated=True, description="True if the message is a system announcement", - alias="isAnnouncement", ) time_created: datetime = Field( description="Unix timestamp (epoch seconds)", alias="timeCreated" @@ -267,11 +273,11 @@ class UserMessage(BaseModel): # Below are observed as used, but purpose is unclear or deprecated no_save: bool = Field(default=False, deprecated=True) - title: Optional[str] = Field(default=None, deprecated=True) + title: str = Field(default="", deprecated=True) routing_key: Optional[str] = Field(default=None, deprecated=True) bot_type: Optional[Any] = Field(default=None, deprecated=True) omit_reply: bool = Field(default=False, deprecated=True) - to_discussion: Optional[Any] = Field(default=None, deprecated=True) + to_discussion: bool = Field(default=False, deprecated=True) dom: Optional[str] = Field(default=None, deprecated=True) @model_validator(mode="before") @@ -352,13 +358,13 @@ def model_dump(self, **kwargs): by_alias = super().model_dump(by_alias=True, **kwargs) # Add parameters for backwards-compat. - by_alias["nick"] = self.user_nick + by_alias["nick"] = self.user_id by_alias["message_text"] = self.message_body - by_alias["username"] = self.user_nick + by_alias["username"] = None # Backwards-compat? by_alias["service_name"] = self.bound_service by_alias["bot"] = self.is_bot by_alias["shout"] = self.message_body - by_alias["time"] = int(self.time_created.timestamp()) + by_alias["time"] = self.time_created.timestamp() by_alias["created_on"] = by_alias["time"] by_alias["responded_shout"] = self.replied_message From c8e92ecebe6121b8de9cdaf5988c8cb455171fc7 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 12:40:40 -0700 Subject: [PATCH 29/53] Clean up SIO models to simplify alias handling --- neon_data_models/models/api/klat/socketio.py | 73 +++++++------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 7c1a4aa2..0d2e2448 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -29,7 +29,13 @@ from time import time from typing import Optional, Dict, List, Literal, Union, Any from datetime import datetime -from pydantic import Field, model_validator, model_serializer +from pydantic import ( + AliasChoices, + Field, + model_validator, + model_serializer, + ConfigDict, +) from neon_data_models.models.api.llm import LLMPersona from neon_data_models.enum import CcaiState @@ -175,6 +181,9 @@ class NewPromptMessage(BaseModel): description="Extra context for the prompt", default={} ) + # Allow creation by name and alias inputs for backwards-compat. + model_config = ConfigDict(validate_by_alias=True, validate_by_name=True) + def model_dump(self, **kwargs): """ Override model_dump to include SIO fields for backwards compatibility @@ -196,7 +205,8 @@ class UserMessage(BaseModel): ) cid: str = Field(description="Conversation ID associated with the message") user_id: str = Field( - description="User ID associated with the message", alias="userID" + description="User ID associated with the message", + alias=AliasChoices("userID", "nick"), ) user_nick: Optional[str] = Field( description="Username of the sender", @@ -218,15 +228,17 @@ class UserMessage(BaseModel): ) message_body: str = Field( description="Message content (input string or audio filename)", - alias="messageText", + alias=AliasChoices("messageText", "shout", "message_text"), ) replied_message: Optional[str] = Field( default=None, description="Message ID this message is a reply to", - alias="repliedMessage", + alias=AliasChoices("repliedMessage", "responded_shout"), ) is_bot: Literal["0", "1"] = Field( - default="0", description="'1' if the message came from a bot, else '0'" + default="0", + description="'1' if the message came from a bot, else '0'", + alias=AliasChoices("isBot", "bot"), ) lang: str = Field(default="en", description="ISO 639-1 Language code") attachments: List[str] = Field( @@ -259,7 +271,8 @@ class UserMessage(BaseModel): description="True if the message is a system announcement", ) time_created: datetime = Field( - description="Unix timestamp (epoch seconds)", alias="timeCreated" + description="Unix timestamp (epoch seconds)", + alias=AliasChoices("timeCreated", "time", "created_on") ) message_id: str = Field( description="UUID for this message", @@ -269,6 +282,7 @@ class UserMessage(BaseModel): bound_service: str = Field( default="", description="Service this message is targeting", + alias="service_name", ) # Below are observed as used, but purpose is unclear or deprecated @@ -280,44 +294,13 @@ class UserMessage(BaseModel): to_discussion: bool = Field(default=False, deprecated=True) dom: Optional[str] = Field(default=None, deprecated=True) - @model_validator(mode="before") - @classmethod - def validate_inputs(cls, values): - # Prefer the proper `messageText` key, but accept `message_body` for - # backwards-compat. - values["messageText"] = ( - values.get("messageText") - or values.get("message_body") - or values.get("shout") - or values.get("message_text") - ) - values["is_bot"] = ( - values.get("is_bot") - or values.get("isBot") - or values.pop("bot", "0") - ) - values["promptID"] = values.get("promptID") or values.pop( - "prompt_id", None - ) - - # Map some observed legacy keys - values["userDisplayName"] = ( - values.get("userDisplayName") - or values.get("nick") - or values.get("username") - ) - values["bound_service"] = ( - values.get("bound_service") or values.get("service_name") or "" - ) - values["repliedMessage"] = values.get("repliedMessage") or values.get( - "responded_shout" - ) - - return values class Config: use_enum_values = True validate_default = True + # Allow creation by name and alias inputs for backwards-compat. + validate_by_alias=True + validate_by_name=True def to_db_query(self) -> Dict[str, Any]: return { @@ -358,15 +341,7 @@ def model_dump(self, **kwargs): by_alias = super().model_dump(by_alias=True, **kwargs) # Add parameters for backwards-compat. - by_alias["nick"] = self.user_id - by_alias["message_text"] = self.message_body - by_alias["username"] = None # Backwards-compat? - by_alias["service_name"] = self.bound_service - by_alias["bot"] = self.is_bot - by_alias["shout"] = self.message_body - by_alias["time"] = self.time_created.timestamp() - by_alias["created_on"] = by_alias["time"] - by_alias["responded_shout"] = self.replied_message + # by_alias["username"] = None # Backwards-compat? return {**super().model_dump(**kwargs), **by_alias} From c86305a026ed7b37fed1ec0fb5301ab0ea5ded0e Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 12:48:31 -0700 Subject: [PATCH 30/53] Fix type annotation of `bound_service` to match current implementation --- neon_data_models/models/api/klat/socketio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 0d2e2448..2b3cf315 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -279,8 +279,8 @@ class UserMessage(BaseModel): alias="messageID", default_factory=lambda: uuid.uuid4().hex[:10], ) - bound_service: str = Field( - default="", + bound_service: Optional[str] = Field( + default=None, description="Service this message is targeting", alias="service_name", ) From 5c391f46361329ef1e711adf09e90fbdef86e65f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 13:25:27 -0700 Subject: [PATCH 31/53] Troubleshooting `is_bot` alias checks in alpha --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 2b3cf315..99707741 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -238,7 +238,7 @@ class UserMessage(BaseModel): is_bot: Literal["0", "1"] = Field( default="0", description="'1' if the message came from a bot, else '0'", - alias=AliasChoices("isBot", "bot"), + # alias=AliasChoices("isBot", "bot"), ) lang: str = Field(default="en", description="ISO 639-1 Language code") attachments: List[str] = Field( From e72473fc56db670f1721c81a54fe1f23874010f5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 13:47:28 -0700 Subject: [PATCH 32/53] Fix `NewPromptMessage` structure and docstrings --- neon_data_models/models/api/klat/socketio.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 99707741..6b9e308c 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -165,18 +165,25 @@ def to_incoming_tts(self) -> Dict[str, Any]: class NewPromptMessage(BaseModel): - cid: str = Field(description="Conversation ID associated with the prompt") + """ + Model representing a user message that relates to a CCAI prompt. + """ + cid: str = Field(description="Conversation ID associated with the prompt and this response") user_id: str = Field( - description="User ID associated with the prompt", alias="userID" + description="User ID associated with this message", alias="userID" ) prompt_id: str = Field( - description="Unique ID for the prompt", alias="promptID" + description="Unique ID for the prompt this message relates to", alias="promptID" ) prompt_state: CcaiState = Field( - description="CCAI state for the prompt", + description="CCAI state this response is associated with", default=CcaiState.IDLE, alias="promptState", ) + message_text: str = Field( + description="Submind response content", + alias="messageText", + ) context: Dict[str, Any] = Field( description="Extra context for the prompt", default={} ) From bea8abe9cb65c1c4c1fc275bbd3d5e0a34d2e5cc Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 14:08:45 -0700 Subject: [PATCH 33/53] Fix `NewPromptMessage` creation bug --- neon_data_models/models/api/klat/socketio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 6b9e308c..27c6350b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -333,6 +333,7 @@ def to_new_prompt_message(self) -> NewPromptMessage: prompt_id=self.prompt_id, prompt_state=self.prompt_state, context=self.context, + message_text=self.message_body ) def model_dump(self, **kwargs): From a0c04052e8969cb9c2a8f9a61df06459135946a5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 14:21:51 -0700 Subject: [PATCH 34/53] Troubleshooting Proctor `bot` flag handling --- neon_data_models/models/api/klat/socketio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 27c6350b..a79120f2 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -245,7 +245,7 @@ class UserMessage(BaseModel): is_bot: Literal["0", "1"] = Field( default="0", description="'1' if the message came from a bot, else '0'", - # alias=AliasChoices("isBot", "bot"), + alias=AliasChoices("bot"), ) lang: str = Field(default="en", description="ISO 639-1 Language code") attachments: List[str] = Field( From e03535e7705a4b917897e104f19db1ff25793d3d Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 14:33:10 -0700 Subject: [PATCH 35/53] Troubleshooting backwards-compat --- neon_data_models/models/api/klat/socketio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index a79120f2..f58071f7 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -207,7 +207,8 @@ def model_dump(self, **kwargs): class UserMessage(BaseModel): - sid: str = Field( + sid: Optional[str] = Field( + default=None, description="Client Session ID associated with the message" ) cid: str = Field(description="Conversation ID associated with the message") From 7abec7875f5bc851f7079fc4614f44e6a855b7ac Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 14:39:40 -0700 Subject: [PATCH 36/53] Update userID vs nick handling --- neon_data_models/models/api/klat/socketio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index f58071f7..a938b170 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -213,13 +213,13 @@ class UserMessage(BaseModel): ) cid: str = Field(description="Conversation ID associated with the message") user_id: str = Field( - description="User ID associated with the message", - alias=AliasChoices("userID", "nick"), + description="User ID associated with the message (nick plus unique string)", + alias=AliasChoices("userID"), ) user_nick: Optional[str] = Field( description="Username of the sender", default=None, - alias="userDisplayName", + alias=AliasChoices("nick", "userDisplayName"), ) prompt_id: Optional[str] = Field( default=None, From bab522a37032de2b898142dde40a876c2b8d2c1f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 15:28:06 -0700 Subject: [PATCH 37/53] Troubleshooting message serialization --- neon_data_models/models/api/klat/socketio.py | 36 +++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index a938b170..deb69307 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -169,8 +169,8 @@ class NewPromptMessage(BaseModel): Model representing a user message that relates to a CCAI prompt. """ cid: str = Field(description="Conversation ID associated with the prompt and this response") - user_id: str = Field( - description="User ID associated with this message", alias="userID" + user_id: str = Field( # TODO: Confirm what this string looks like + description="User UUID associated with this message", alias="userID" ) prompt_id: str = Field( description="Unique ID for the prompt this message relates to", alias="promptID" @@ -212,8 +212,9 @@ class UserMessage(BaseModel): description="Client Session ID associated with the message" ) cid: str = Field(description="Conversation ID associated with the message") - user_id: str = Field( - description="User ID associated with the message (nick plus unique string)", + user_id: Optional[str] = Field( + default=None, + description="Sometimes a username + string and sometimes a UUID associated with the user", alias=AliasChoices("userID"), ) user_nick: Optional[str] = Field( @@ -254,14 +255,9 @@ class UserMessage(BaseModel): description="List of string filenames attached to this message", ) context: dict = Field(default={}, description="Optional arbitrary context") - test: bool = Field( + is_audio: bool = Field( default=False, - description="True if this message is associated with testing", - ) - is_audio: Literal["0", "1"] = Field( - default="0", - description="'1' if messageText represents encoded WAV audio", - alias="isAudio", + description="True if message_body represents encoded WAV audio", ) message_tts: Dict[str, Dict[Literal["male", "female"], str]] = Field( default={}, @@ -273,11 +269,6 @@ class UserMessage(BaseModel): description="True if the message is a system announcement", default=False, ) - isAnnouncement: Literal["0", "1"] = Field( - default="0", - deprecated=True, - description="True if the message is a system announcement", - ) time_created: datetime = Field( description="Unix timestamp (epoch seconds)", alias=AliasChoices("timeCreated", "time", "created_on") @@ -301,7 +292,20 @@ class UserMessage(BaseModel): omit_reply: bool = Field(default=False, deprecated=True) to_discussion: bool = Field(default=False, deprecated=True) dom: Optional[str] = Field(default=None, deprecated=True) + test: bool = Field( + default=False, + description="True if this message is associated with testing", + deprecated=True + ) + @model_validator(mode="before") + @classmethod + def validate_inputs(cls, values): + if values.get("isAnnouncement") and "is_announcement" not in values: + values["is_announcement"] = values.get("isAnnouncement") == 1 + if values.get("isAudio") and "is_audio" not in values: + values["is_audio"] = values.get("isAudio") == 1 + return values class Config: use_enum_values = True From 08b96aed84c5a5ecd6212e589ecfe5b9f37cd093 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 15:48:05 -0700 Subject: [PATCH 38/53] Tighten up user_id validation and handling --- neon_data_models/models/api/klat/socketio.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index deb69307..fa21164d 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -214,7 +214,7 @@ class UserMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") user_id: Optional[str] = Field( default=None, - description="Sometimes a username + string and sometimes a UUID associated with the user", + description="UUID associated with the user", alias=AliasChoices("userID"), ) user_nick: Optional[str] = Field( @@ -307,6 +307,16 @@ def validate_inputs(cls, values): values["is_audio"] = values.get("isAudio") == 1 return values + @model_validator(mode="after") + def validate_fields(self): + # Client appears to send a UID as a nick + if self.user_id == self.user_nick: + raise ValueError(f"user_id should be a UUID, " + f"not nick ({self.user_nick})") + if self.user_nick is None: + self.user_nick = "guest" + return self + class Config: use_enum_values = True validate_default = True From 6d6e99e5cb993a34e9d033f289414d12c13564cb Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 16:35:46 -0700 Subject: [PATCH 39/53] Troubleshooting exceptions --- neon_data_models/models/api/klat/socketio.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index fa21164d..d2cbe968 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -440,6 +440,16 @@ def validate_inputs(cls, values): ) return values + @model_validator(mode="after") + def validate_fields(self): + # Client appears to send a UID as a nick + if self.user_id == self.user_nick: + # TODO: This is patching backwards-compat. + self.user_id = None + if self.user_nick is None: + self.user_nick = "guest" + return self + def to_db_query(self) -> Dict[str, Any]: return { "prompt_id": self.prompt_id, From fa3ca8c04f766927638a4d3090d4173503d7109c Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Thu, 2 Oct 2025 18:47:20 -0700 Subject: [PATCH 40/53] Refactor to be consistent with `user_id` and `user_uid` params --- neon_data_models/models/api/klat/socketio.py | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index d2cbe968..53bb5933 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -169,8 +169,12 @@ class NewPromptMessage(BaseModel): Model representing a user message that relates to a CCAI prompt. """ cid: str = Field(description="Conversation ID associated with the prompt and this response") - user_id: str = Field( # TODO: Confirm what this string looks like - description="User UUID associated with this message", alias="userID" + user_id: str = Field( + description="User ID (nick + suffix) associated with this message", + alias="userID" + ) + user_uid: str = Field( + description="User UUID associated with this message" ) prompt_id: str = Field( description="Unique ID for the prompt this message relates to", alias="promptID" @@ -214,9 +218,13 @@ class UserMessage(BaseModel): cid: str = Field(description="Conversation ID associated with the message") user_id: Optional[str] = Field( default=None, - description="UUID associated with the user", + description="User ID (nick + suffix) associated with the user", alias=AliasChoices("userID"), ) + user_uid: Optional[str] = Field( + default=None, + description="UUID associated with the user database entry" + ), user_nick: Optional[str] = Field( description="Username of the sender", default=None, @@ -311,8 +319,8 @@ def validate_inputs(cls, values): def validate_fields(self): # Client appears to send a UID as a nick if self.user_id == self.user_nick: - raise ValueError(f"user_id should be a UUID, " - f"not nick ({self.user_nick})") + raise ValueError(f"user_id should be a nick + suffix, " + f"not nick ({self.user_id})") if self.user_nick is None: self.user_nick = "guest" return self @@ -328,7 +336,7 @@ def to_db_query(self) -> Dict[str, Any]: return { "_id": self.message_id, "cid": self.cid, - "user_id": self.user_id, + "user_id": self.user_uid, "prompt_id": self.prompt_id, "message_text": self.message_body, "message_lang": self.lang, @@ -345,6 +353,7 @@ def to_new_prompt_message(self) -> NewPromptMessage: return NewPromptMessage( cid=self.cid, user_id=self.user_id, + user_uid=self.user_uid, prompt_id=self.prompt_id, prompt_state=self.prompt_state, context=self.context, @@ -362,10 +371,6 @@ def model_dump(self, **kwargs): by_alias = {} if "by_alias" not in kwargs: by_alias = super().model_dump(by_alias=True, **kwargs) - - # Add parameters for backwards-compat. - # by_alias["username"] = None # Backwards-compat? - return {**super().model_dump(**kwargs), **by_alias} From 20a21bb3adc50bf05fb195327fc2ca75f013f710 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 10:06:27 -0700 Subject: [PATCH 41/53] Troubleshooting --- neon_data_models/models/api/klat/socketio.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 53bb5933..fcd4c1e9 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -313,6 +313,12 @@ def validate_inputs(cls, values): values["is_announcement"] = values.get("isAnnouncement") == 1 if values.get("isAudio") and "is_audio" not in values: values["is_audio"] = values.get("isAudio") == 1 + if values.get("userDisplayName") and values.get("nick") and values['nick'].startswith(values['userDisplayName']): + # Patch old behavior and ensure `user_id` is nick + suffix + values['userID'] = values.pop('nick') + # TODO: Below troubleshooting observed incomprehensible error + if isinstance(values.get("user_uid"), tuple): + raise ValueError(f"user_uid cannot be a tuple: {values['user_uid']}") return values @model_validator(mode="after") From a033e1fef5c3f757aa6eb425f8cdc2e6535de5a7 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 10:10:56 -0700 Subject: [PATCH 42/53] More troubleshooting user_uid --- neon_data_models/models/api/klat/socketio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index fcd4c1e9..af479afb 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -318,7 +318,8 @@ def validate_inputs(cls, values): values['userID'] = values.pop('nick') # TODO: Below troubleshooting observed incomprehensible error if isinstance(values.get("user_uid"), tuple): - raise ValueError(f"user_uid cannot be a tuple: {values['user_uid']}") + print(f"user_uid={values['user_uid']}") + values['user_uid'] = None return values @model_validator(mode="after") From 7ea1440e36a77e286bafac242e11038948267bc5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 10:32:15 -0700 Subject: [PATCH 43/53] More user_uid troubleshooting --- neon_data_models/models/api/klat/socketio.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index af479afb..9cce7b86 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -316,14 +316,12 @@ def validate_inputs(cls, values): if values.get("userDisplayName") and values.get("nick") and values['nick'].startswith(values['userDisplayName']): # Patch old behavior and ensure `user_id` is nick + suffix values['userID'] = values.pop('nick') - # TODO: Below troubleshooting observed incomprehensible error - if isinstance(values.get("user_uid"), tuple): - print(f"user_uid={values['user_uid']}") - values['user_uid'] = None + import logging + logging.info(f"UserMessage init with values: {values}") return values @model_validator(mode="after") - def validate_fields(self): + def validate_user_params(self): # Client appears to send a UID as a nick if self.user_id == self.user_nick: raise ValueError(f"user_id should be a nick + suffix, " From dff86b312dac7ecae628a45cb901e7bafbd052bb Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 10:56:21 -0700 Subject: [PATCH 44/53] Implement minimal UserMessage test case and resolve raised exception --- neon_data_models/models/api/klat/socketio.py | 9 ++------- tests/models/api/test_klat.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 9cce7b86..fedd98c6 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -221,10 +221,7 @@ class UserMessage(BaseModel): description="User ID (nick + suffix) associated with the user", alias=AliasChoices("userID"), ) - user_uid: Optional[str] = Field( - default=None, - description="UUID associated with the user database entry" - ), + user_uid: Optional[str] = Field(default=None, description="User UUID") user_nick: Optional[str] = Field( description="Username of the sender", default=None, @@ -316,14 +313,12 @@ def validate_inputs(cls, values): if values.get("userDisplayName") and values.get("nick") and values['nick'].startswith(values['userDisplayName']): # Patch old behavior and ensure `user_id` is nick + suffix values['userID'] = values.pop('nick') - import logging - logging.info(f"UserMessage init with values: {values}") return values @model_validator(mode="after") def validate_user_params(self): # Client appears to send a UID as a nick - if self.user_id == self.user_nick: + if self.user_id == self.user_nick and self.user_id is not None: raise ValueError(f"user_id should be a nick + suffix, " f"not nick ({self.user_id})") if self.user_nick is None: diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py index 281575c6..32af98bb 100644 --- a/tests/models/api/test_klat.py +++ b/tests/models/api/test_klat.py @@ -25,6 +25,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from unittest import TestCase +from time import time class TestKlat(TestCase): @@ -40,4 +41,14 @@ def test_prompt_data(self): receiver="mock_username", cid="mock_conversation") self.assertEqual(valid_prompt_data, - PromptData(**valid_prompt_data.model_dump())) \ No newline at end of file + PromptData(**valid_prompt_data.model_dump())) + + def test_user_message(self): + from neon_data_models.models.api.klat.socketio import UserMessage + test_object = UserMessage(message_text="test message", + user="test_user", + time_created=time(), + cid="test_cid") + self.assertIsInstance(test_object, UserMessage) + self.assertEqual(test_object, + UserMessage(**test_object.model_dump())) From d3485b4c248bb8d2475f60db26fd504460d6d8c7 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 13:41:49 -0700 Subject: [PATCH 45/53] Troubleshooting backwards-compat --- neon_data_models/models/api/klat/socketio.py | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index fedd98c6..bfc00e02 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -211,8 +211,7 @@ def model_dump(self, **kwargs): class UserMessage(BaseModel): - sid: Optional[str] = Field( - default=None, + sid: str = Field( description="Client Session ID associated with the message" ) cid: str = Field(description="Conversation ID associated with the message") @@ -222,10 +221,11 @@ class UserMessage(BaseModel): alias=AliasChoices("userID"), ) user_uid: Optional[str] = Field(default=None, description="User UUID") - user_nick: Optional[str] = Field( + username: Optional[str] = Field( description="Username of the sender", default=None, - alias=AliasChoices("nick", "userDisplayName"), + alias="userDisplayName" + # alias=AliasChoices("nick", "userDisplayName", "user_nick"), ) prompt_id: Optional[str] = Field( default=None, @@ -242,7 +242,8 @@ class UserMessage(BaseModel): ) message_body: str = Field( description="Message content (input string or audio filename)", - alias=AliasChoices("messageText", "shout", "message_text"), + alias="messageText" + #AliasChoices("messageText", "shout", "message_text"), ) replied_message: Optional[str] = Field( default=None, @@ -276,7 +277,8 @@ class UserMessage(BaseModel): ) time_created: datetime = Field( description="Unix timestamp (epoch seconds)", - alias=AliasChoices("timeCreated", "time", "created_on") + alias="timeCreated" + # AliasChoices("timeCreated", "time", "created_on") ) message_id: str = Field( description="UUID for this message", @@ -318,11 +320,12 @@ def validate_inputs(cls, values): @model_validator(mode="after") def validate_user_params(self): # Client appears to send a UID as a nick - if self.user_id == self.user_nick and self.user_id is not None: + if self.user_id == self.username and self.user_id is not None: raise ValueError(f"user_id should be a nick + suffix, " f"not nick ({self.user_id})") - if self.user_nick is None: - self.user_nick = "guest" + if self.username is None: + # TODO: Is "guest" fallback necessary? + self.username = self.user_id.split('-')[0] or "guest" return self class Config: @@ -371,6 +374,7 @@ def model_dump(self, **kwargs): by_alias = {} if "by_alias" not in kwargs: by_alias = super().model_dump(by_alias=True, **kwargs) + # TODO: This doesn't appear to include all aliases? return {**super().model_dump(**kwargs), **by_alias} @@ -448,11 +452,11 @@ def validate_inputs(cls, values): @model_validator(mode="after") def validate_fields(self): # Client appears to send a UID as a nick - if self.user_id == self.user_nick: + if self.user_id == self.username: # TODO: This is patching backwards-compat. self.user_id = None - if self.user_nick is None: - self.user_nick = "guest" + if self.username is None: + self.username = "guest" return self def to_db_query(self) -> Dict[str, Any]: From 9fd2a2d899d0958ce04d0e94458b00548056b2f5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 3 Oct 2025 16:16:51 -0700 Subject: [PATCH 46/53] Clean up changes --- neon_data_models/models/api/klat/socketio.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index bfc00e02..ef1ca5b0 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -218,14 +218,13 @@ class UserMessage(BaseModel): user_id: Optional[str] = Field( default=None, description="User ID (nick + suffix) associated with the user", - alias=AliasChoices("userID"), + alias="userID", ) user_uid: Optional[str] = Field(default=None, description="User UUID") username: Optional[str] = Field( description="Username of the sender", default=None, alias="userDisplayName" - # alias=AliasChoices("nick", "userDisplayName", "user_nick"), ) prompt_id: Optional[str] = Field( default=None, @@ -243,17 +242,16 @@ class UserMessage(BaseModel): message_body: str = Field( description="Message content (input string or audio filename)", alias="messageText" - #AliasChoices("messageText", "shout", "message_text"), ) replied_message: Optional[str] = Field( default=None, description="Message ID this message is a reply to", - alias=AliasChoices("repliedMessage", "responded_shout"), + alias="repliedMessage" ) is_bot: Literal["0", "1"] = Field( default="0", description="'1' if the message came from a bot, else '0'", - alias=AliasChoices("bot"), + alias="bot", ) lang: str = Field(default="en", description="ISO 639-1 Language code") attachments: List[str] = Field( @@ -278,7 +276,6 @@ class UserMessage(BaseModel): time_created: datetime = Field( description="Unix timestamp (epoch seconds)", alias="timeCreated" - # AliasChoices("timeCreated", "time", "created_on") ) message_id: str = Field( description="UUID for this message", From a0400c11830d251dbed54e0e5ccd9fb911448e37 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 6 Oct 2025 12:48:07 -0700 Subject: [PATCH 47/53] Update NewCcaiPrompt to support returning completed prompts --- neon_data_models/models/api/klat/socketio.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index ef1ca5b0..094e826b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -388,6 +388,24 @@ class NewCcaiPrompt(BaseModel): context: Dict[str, Any] = Field( description="Extra context for the prompt", default={} ) + # Completed prompts sent to the client use below fields + winner: Optional[str] = Field(default=None, description="Winning response User ID") + participating_subminds: List[str] = Field( + default=[], + description="List of subminds by User ID that participated in this prompt", + ) + proposed_responses: Dict[str, str] = Field( + default={}, + description="Dict of participating submind User ID to proposed response", + ) + votes: Dict[str, str] = Field( + default={}, + description="Dict of participating submind User ID to vote", + ) + submind_discussion_history: List[Dict[str, str]] = Field( + default=[], + description="List of discussoion round dicts of submind User ID to opinion response", + ) @model_validator(mode="before") @classmethod @@ -396,6 +414,8 @@ def validate_inputs(cls, values): # for backwards compatibility if values.get("context", {}) is None: values["context"] = {} + if values.get("submind_opinions"): + values["submind_discussion_history"] = [values.pop("submind_opinions")] return values def to_db_query(self) -> Dict[str, Any]: From d7e6a4a2c335453539aede7f78abfe2e8b592c64 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 6 Oct 2025 16:45:16 -0700 Subject: [PATCH 48/53] Clean up changes --- neon_data_models/models/api/klat/socketio.py | 76 +++++--------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 094e826b..e6f56493 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -30,7 +30,6 @@ from typing import Optional, Dict, List, Literal, Union, Any from datetime import datetime from pydantic import ( - AliasChoices, Field, model_validator, model_serializer, @@ -45,18 +44,13 @@ class GetSttRequest(BaseModel): cid: str = Field(description="Conversation ID associated with the request") sid: str = Field( - description="Client Session ID associated with the request" + description="Shout ID associated with the request", + alias="message_id" ) lang: str = Field( description="BCP-47 Language code associated with audio", default="en-us", ) - message_id: str = Field( - description="Message (Shout) ID associated with the request" - ) - audio_data: str = Field( - description="B64-encoded audio file object to transcribe" - ) class GetSttResponse(BaseModel): @@ -65,11 +59,9 @@ class GetSttResponse(BaseModel): description="BCP-47 Language code associated with `transcript`", default="en-us", ) - message_id: str = Field( - description="Message (Shout) ID associated with the request" - ) sid: str = Field( - description="Client Session ID associated with the request" + description="Shout ID associated with the request", + alias="message_id" ) cid: str = Field(description="Conversation ID associated with the request") context: Dict[str, Any] = Field(default={}, description="Optional context") @@ -81,9 +73,6 @@ def parse_context(cls, values): Parse out message_id, sid, and cid from context if not handled by the Observer module. """ - values.setdefault( - "message_id", values.get("context", {}).get("message_id") - ) values.setdefault("sid", values.get("context", {}).get("sid")) values.setdefault("cid", values.get("context", {}).get("cid")) return values @@ -92,12 +81,9 @@ def parse_context(cls, values): class GetTtsRequest(BaseModel): cid: str = Field(description="Conversation ID associated with the request") sid: str = Field( - description="Client Session ID associated with the request" - ) - message_id: str = Field( - description="Message (Shout) ID associated with the request" + description="Shout ID associated with the request", + alias="message_id" ) - text: str = Field(description="Text to generate audio for") lang: str = Field( description="BCP-47 Language code associated with `text`", default="en-us", @@ -123,11 +109,9 @@ class GetTtsResponse(BaseModel): description="Gender associated with generated audio", default="undefined", ) - message_id: str = Field( - description="Message (Shout) ID associated with the request" - ) sid: str = Field( - description="Client Session ID associated with the request" + description="Shout ID associated with the request", + alias="message_id" ) cid: str = Field(description="Conversation ID associated with the request") context: Dict[str, Any] = Field(default={}, description="Optional context") @@ -136,19 +120,16 @@ class GetTtsResponse(BaseModel): @classmethod def parse_context(cls, values): """ - Parse out message_id, sid, and cid from context if not handled by the + Parse out sid, and cid from context if not handled by the Observer module. """ - values.setdefault( - "message_id", values.get("context", {}).get("message_id") - ) values.setdefault("sid", values.get("context", {}).get("sid")) values.setdefault("cid", values.get("context", {}).get("cid")) return values def to_db_query(self) -> Dict[str, Any]: return { - "shout_id": self.message_id, + "shout_id": self.sid, "audio_data": self.audio_data, "lang": self.lang, "gender": self.gender, @@ -157,7 +138,7 @@ def to_db_query(self) -> Dict[str, Any]: def to_incoming_tts(self) -> Dict[str, Any]: return { "cid": self.cid, - "message_id": self.message_id, + "sid": self.sid, "audio_data": self.audio_data, "lang": self.lang, "gender": self.gender, @@ -212,7 +193,7 @@ def model_dump(self, **kwargs): class UserMessage(BaseModel): sid: str = Field( - description="Client Session ID associated with the message" + description="Shout ID associated with the message" ) cid: str = Field(description="Conversation ID associated with the message") user_id: Optional[str] = Field( @@ -281,7 +262,7 @@ class UserMessage(BaseModel): description="UUID for this message", alias="messageID", default_factory=lambda: uuid.uuid4().hex[:10], - ) + ) # TODO: This is probably `sid` with a different name bound_service: Optional[str] = Field( default=None, description="Service this message is targeting", @@ -371,14 +352,13 @@ def model_dump(self, **kwargs): by_alias = {} if "by_alias" not in kwargs: by_alias = super().model_dump(by_alias=True, **kwargs) - # TODO: This doesn't appear to include all aliases? return {**super().model_dump(**kwargs), **by_alias} class NewCcaiPrompt(BaseModel): prompt_text: str = Field( - description="Text of the prompt" - ) # TODO: Check if formatted to remove prefix + description="Text of the prompt, without the '!PROMPT: ' prefix" + ) cid: str = Field(description="Conversation ID associated with the prompt") prompt_id: str = Field(description="Unique ID for the prompt") created_on: int = Field( @@ -441,7 +421,7 @@ class CcaiPromptCompleted(UserMessage): description="Prompt ID this message is in response to", ) sid: str = Field( - description="Client Session ID associated with the request" + description="Shout ID associated with the request" ) conversation_context: Dict[str, Any] = Field( description="Context of the conversation", default={} @@ -460,7 +440,6 @@ def validate_inputs(cls, values): "prompt_id", values.get("context", {}).get("prompt", {}).get("prompt_id"), ) - # TODO: Below assertion for initial development; remove before merge assert values["prompt_id"] != "", ( f"prompt_id must be defined: {values}" ) @@ -560,27 +539,6 @@ def alias_serialize(self): ) -class AuthExpired(BaseModel): - body: str = Field(description="HTTP error response body") - status: int = Field(description="HTTP response status code") - handler: str = Field(description="Name of the requested function") - - -class ConfiguredPersonasChanged(BaseModel): - personas: Dict[str, List[LLMPersona]] = Field( - description="Dict of LLM name to list of supported personas" - ) - update_time: int = Field( - description="Unix timestamp when the change occurred" - ) - - -class BanSubmindFromConversation(BaseModel): - # This model is used to both ban and revoke a ban - cid: str = Field(description="Conversation ID to (un-)ban submind from") - nick: str = Field(description="Username of the submind to (un-)ban") - - __all__ = [ GetSttRequest.__name__, GetSttResponse.__name__, @@ -592,6 +550,4 @@ class BanSubmindFromConversation(BaseModel): NewCcaiPrompt.__name__, CcaiPromptCompleted.__name__, PromptData.__name__, - AuthExpired.__name__, - BanSubmindFromConversation.__name__, ] From 0037a0a475f757f6dc309dc9aca6b100c3a2a138 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 6 Oct 2025 17:13:06 -0700 Subject: [PATCH 49/53] Add `user_uid` to STT/TTS requests to match currently emitted events --- neon_data_models/models/api/klat/socketio.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index e6f56493..965ea7e6 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -36,7 +36,6 @@ ConfigDict, ) -from neon_data_models.models.api.llm import LLMPersona from neon_data_models.enum import CcaiState from neon_data_models.models.base import BaseModel @@ -47,6 +46,11 @@ class GetSttRequest(BaseModel): description="Shout ID associated with the request", alias="message_id" ) + user_uid: str = Field( + description="User UUID associated with the request", + alias="user_id", + deprecated=True + ) lang: str = Field( description="BCP-47 Language code associated with audio", default="en-us", @@ -84,6 +88,11 @@ class GetTtsRequest(BaseModel): description="Shout ID associated with the request", alias="message_id" ) + user_uid: str = Field( + description="User UUID associated with the request", + alias="user_id", + deprecated=True + ) lang: str = Field( description="BCP-47 Language code associated with `text`", default="en-us", From c41bcd2b835093b7e385b4db6ac0697a493e033b Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 6 Oct 2025 17:16:14 -0700 Subject: [PATCH 50/53] Clean up `PromptData` to use bool instead of strings to represent a bool --- neon_data_models/models/api/klat/socketio.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 965ea7e6..3753dd06 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -505,8 +505,8 @@ def to_db_query(self) -> Dict[str, Any]: class PromptData(BaseModel): class _PromptData(BaseModel): id: str = Field(alias="_id", description="Unique ID for the prompt") - is_completed: Literal["0", "1"] = Field( - description="'1' if a response to the prompt has been determined" + is_completed: bool = Field( + description="true if a response to the prompt has been determined" ) proposed_responses: Dict[str, str] = Field( default={}, @@ -524,11 +524,19 @@ class _PromptData(BaseModel): description="List of subminds that participated in this prompt", ) + @model_validator(mode="before") + @classmethod + def normalize_completed(cls, values): + if "is_completed" in values and isinstance(values["is_completed"], str): + values["is_completed"] = values["is_completed"] == "1" + return values + @model_serializer def alias_serialize(self): + # Alias to match existing MongoDB schema return { "_id": self.id, - "is_completed": self.is_completed, + "is_completed": "1" if self.is_completed else "0", "proposed_responses": self.proposed_responses, "submind_opinions": self.submind_opinions, "votes": self.votes, From cb7fc5e3782ce234187ee59cf6f1c2cb3b9cf479 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 7 Oct 2025 11:49:33 -0700 Subject: [PATCH 51/53] Enforce `username` or `user_id` must be defined in `UserMessage` and Implement Unit Tests --- neon_data_models/models/api/klat/socketio.py | 10 +- tests/models/api/test_klat.py | 521 ++++++++++++++++++- 2 files changed, 504 insertions(+), 27 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 3753dd06..9bbabf08 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -98,13 +98,6 @@ class GetTtsRequest(BaseModel): default="en-us", ) - @model_validator(mode="before") - @classmethod - def validate_inputs(cls, values): - if "message_text" in values: - values.setdefault("text", values.get("message_text")) - return values - class GetTtsResponse(BaseModel): audio_data: str = Field( @@ -311,7 +304,8 @@ def validate_user_params(self): raise ValueError(f"user_id should be a nick + suffix, " f"not nick ({self.user_id})") if self.username is None: - # TODO: Is "guest" fallback necessary? + if self.user_id is None: + raise ValueError("Either user_id or username must be defined") self.username = self.user_id.split('-')[0] or "guest" return self diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py index 32af98bb..6c43be37 100644 --- a/tests/models/api/test_klat.py +++ b/tests/models/api/test_klat.py @@ -24,31 +24,514 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import base64 from unittest import TestCase +from datetime import datetime from time import time +from pydantic import ValidationError + +from neon_data_models.enum import CcaiState class TestKlat(TestCase): - def test_prompt_data(self): - from neon_data_models.models.api.klat.socketio import PromptData - test_object = PromptData._PromptData(_id="test_id", - is_completed='1') - self.assertIsInstance(test_object, PromptData._PromptData) - self.assertEqual(test_object, - PromptData._PromptData(**test_object.model_dump())) - - valid_prompt_data = PromptData(data=test_object, - receiver="mock_username", - cid="mock_conversation") - self.assertEqual(valid_prompt_data, - PromptData(**valid_prompt_data.model_dump())) + def test_get_stt_request(self): + """Test GetSttRequest model creation and validation""" + from neon_data_models.models.api.klat.socketio import GetSttRequest + + # Test with required fields + test_object = GetSttRequest( + cid="test_conversation_id", + sid="test_shout_id", + user_uid="test_user_id" + ) + self.assertIsInstance(test_object, GetSttRequest) + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.user_uid, "test_user_id") + self.assertEqual(test_object.lang, "en-us") # Default value + + # Test with alias parameter + test_object = GetSttRequest( + cid="test_conversation_id", + message_id="test_message_id", + user_id="test_user_uid" + ) + self.assertEqual(test_object.sid, "test_message_id") + self.assertEqual(test_object.user_uid, "test_user_uid") + + # Test serialization/deserialization + dumped = test_object.model_dump() + recreated = GetSttRequest(**dumped) + self.assertEqual(test_object, recreated) + + def test_get_stt_response(self): + """Test GetSttResponse model creation and validation""" + from neon_data_models.models.api.klat.socketio import GetSttResponse + + # Test with required fields + test_object = GetSttResponse( + transcript="Hello world", + sid="test_shout_id", + cid="test_conversation_id" + ) + self.assertIsInstance(test_object, GetSttResponse) + self.assertEqual(test_object.transcript, "Hello world") + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.lang, "en-us") # Default value + self.assertEqual(test_object.context, {}) # Default value + + # Test with context extraction + context = {"sid": "context_sid", "cid": "context_cid", "extra": "data"} + test_object = GetSttResponse( + transcript="Hello world", + context=context + ) + self.assertEqual(test_object.sid, "context_sid") + self.assertEqual(test_object.cid, "context_cid") + self.assertEqual(test_object.context["extra"], "data") + + # Test serialization/deserialization + dumped = test_object.model_dump() + recreated = GetSttResponse(**dumped) + self.assertEqual(test_object, recreated) + + def test_get_tts_request(self): + """Test GetTtsRequest model creation and validation""" + from neon_data_models.models.api.klat.socketio import GetTtsRequest + + # Test with required fields + test_object = GetTtsRequest( + cid="test_conversation_id", + sid="test_shout_id", + user_uid="test_user_uid", + ) + self.assertIsInstance(test_object, GetTtsRequest) + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.user_uid, "test_user_uid") + self.assertEqual(test_object.lang, "en-us") # Default value + + # Test with alias parameters + test_object = GetTtsRequest( + cid="test_conversation_id", + message_id="test_message_id", + user_id="test_user_uid", + ) + self.assertEqual(test_object.sid, "test_message_id") + self.assertEqual(test_object.user_uid, "test_user_uid") + + # Test serialization/deserialization + dumped = test_object.model_dump() + recreated = GetTtsRequest(**dumped) + self.assertEqual(test_object, recreated) + + def test_get_tts_response(self): + """Test GetTtsResponse model creation and validation""" + from neon_data_models.models.api.klat.socketio import GetTtsResponse + + # Mock base64 audio data + mock_audio = base64.b64encode(b"mock audio data").decode() + + # Test with required fields + test_object = GetTtsResponse( + audio_data=mock_audio, + sid="test_shout_id", + cid="test_conversation_id" + ) + self.assertIsInstance(test_object, GetTtsResponse) + self.assertEqual(test_object.audio_data, mock_audio) + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.lang, "en-us") # Default value + self.assertEqual(test_object.gender, "undefined") # Default value + self.assertEqual(test_object.context, {}) # Default value + + # Test with context extraction + context = {"sid": "context_sid", "cid": "context_cid"} + test_object = GetTtsResponse( + audio_data=mock_audio, + context=context + ) + self.assertEqual(test_object.sid, "context_sid") + self.assertEqual(test_object.cid, "context_cid") + + # Test to_db_query method + db_query = test_object.to_db_query() + self.assertEqual(db_query["shout_id"], test_object.sid) + self.assertEqual(db_query["audio_data"], test_object.audio_data) + self.assertEqual(db_query["lang"], test_object.lang) + self.assertEqual(db_query["gender"], test_object.gender) + + # Test to_incoming_tts method + incoming_tts = test_object.to_incoming_tts() + self.assertEqual(incoming_tts["cid"], test_object.cid) + self.assertEqual(incoming_tts["sid"], test_object.sid) + self.assertEqual(incoming_tts["audio_data"], test_object.audio_data) + self.assertEqual(incoming_tts["lang"], test_object.lang) + self.assertEqual(incoming_tts["gender"], test_object.gender) + + # Test serialization/deserialization + dumped = test_object.model_dump() + recreated = GetTtsResponse(**dumped) + self.assertEqual(test_object, recreated) + def test_new_prompt_message(self): + """Test NewPromptMessage model creation and validation""" + from neon_data_models.models.api.klat.socketio import NewPromptMessage + + # Test with required fields + test_object = NewPromptMessage( + cid="test_conversation_id", + user_id="test_user-suffix", + user_uid="test_user_uuid", + prompt_id="test_prompt_id", + message_text="Hello world" + ) + self.assertIsInstance(test_object, NewPromptMessage) + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.user_id, "test_user-suffix") + self.assertEqual(test_object.user_uid, "test_user_uuid") + self.assertEqual(test_object.prompt_id, "test_prompt_id") + self.assertEqual(test_object.message_text, "Hello world") + self.assertEqual(test_object.prompt_state, CcaiState.IDLE) # Default value + self.assertEqual(test_object.context, {}) # Default value + + # Test with alias parameters + test_object = NewPromptMessage( + cid="test_conversation_id", + userID="test_user-suffix", + user_uid="test_user_uuid", + promptID="test_prompt_id", + messageText="Hello world", + promptState=CcaiState.RESP + ) + self.assertEqual(test_object.user_id, "test_user-suffix") + self.assertEqual(test_object.prompt_id, "test_prompt_id") + self.assertEqual(test_object.message_text, "Hello world") + self.assertEqual(test_object.prompt_state, CcaiState.RESP) + + # Test model_dump with aliases + dumped = test_object.model_dump() + self.assertIn("user_id", dumped) + self.assertIn("userID", dumped) # Should include alias + self.assertEqual(dumped["user_id"], dumped["userID"]) + self.assertIn("prompt_id", dumped) + self.assertIn("promptID", dumped) # Should include alias + self.assertEqual(dumped["prompt_id"], dumped["promptID"]) + + # Test serialization/deserialization + recreated = NewPromptMessage(**dumped) + self.assertEqual(test_object, recreated) + def test_user_message(self): + """Test UserMessage model creation and validation""" from neon_data_models.models.api.klat.socketio import UserMessage - test_object = UserMessage(message_text="test message", - user="test_user", - time_created=time(), - cid="test_cid") + + # Test with required fields + current_time = datetime.now() + test_object = UserMessage( + sid="test_shout_id", + cid="test_conversation_id", + message_body="Hello world", + time_created=current_time, + user_id="guest-sid", + ) self.assertIsInstance(test_object, UserMessage) - self.assertEqual(test_object, - UserMessage(**test_object.model_dump())) + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.message_body, "Hello world") + self.assertEqual(test_object.time_created, current_time) + self.assertEqual(test_object.user_id, "guest-sid") + self.assertEqual(test_object.username, "guest") # Parsed from user_id + self.assertEqual(test_object.source, "unknown") # Default value + self.assertEqual(test_object.is_bot, "0") # Default value + self.assertEqual(test_object.lang, "en") # Default value + + # Test with alias parameters and username derivation + test_object = UserMessage( + sid="test_shout_id", + cid="test_conversation_id", + userID="test_user-suffix", + messageText="Hello world", + time_created=current_time + ) + self.assertEqual(test_object.user_id, "test_user-suffix") + self.assertEqual(test_object.message_body, "Hello world") + # Username should be derived from user_id + self.assertEqual(test_object.username, "test_user") + + # Test with validation + try: + UserMessage( + sid="test_shout_id", + cid="test_conversation_id", + user_id="same_name", + userDisplayName="same_name", # Should raise error when same as user_id + message_body="Hello world", + time_created=current_time + ) + self.fail("Should have raised ValueError") + except ValueError as e: + self.assertIn("user_id should be a nick + suffix", str(e)) + + # Test isAnnouncement and isAudio aliases + test_object = UserMessage( + sid="test_shout_id", + cid="test_conversation_id", + message_body="Hello world", + time_created=current_time, + isAnnouncement=1, + isAudio=1, + username="test_user" + ) + self.assertTrue(test_object.is_announcement) + self.assertTrue(test_object.is_audio) + self.assertEqual(test_object.username, "test_user") + self.assertIsNone(test_object.user_id) # Derived from username + + # Test to_db_query method + db_query = test_object.to_db_query() + self.assertEqual(db_query["_id"], test_object.message_id) + self.assertEqual(db_query["cid"], test_object.cid) + self.assertEqual(db_query["message_text"], test_object.message_body) + self.assertEqual(db_query["is_audio"], test_object.is_audio) + self.assertEqual(db_query["is_announcement"], test_object.is_announcement) + self.assertEqual(db_query["created_on"], int(current_time.timestamp())) + + # Test to_new_prompt_message conversion + test_object = UserMessage( + sid="test_shout_id", + cid="test_conversation_id", + userID="test_user-suffix", + user_uid="test_uuid", + prompt_id="test_prompt_id", + prompt_state=CcaiState.VOTE, + message_body="Hello world", + time_created=current_time, + ) + prompt_message = test_object.to_new_prompt_message() + self.assertEqual(prompt_message.cid, test_object.cid) + self.assertEqual(prompt_message.user_id, test_object.user_id) + self.assertEqual(prompt_message.user_uid, test_object.user_uid) + self.assertEqual(prompt_message.prompt_id, test_object.prompt_id) + self.assertEqual(prompt_message.prompt_state, test_object.prompt_state) + self.assertEqual(prompt_message.message_text, test_object.message_body) + + def test_new_ccai_prompt(self): + """Test NewCcaiPrompt model creation and validation""" + from neon_data_models.models.api.klat.socketio import NewCcaiPrompt + + # Test with required fields + test_object = NewCcaiPrompt( + prompt_text="What is the meaning of life?", + cid="test_conversation_id", + prompt_id="test_prompt_id" + ) + self.assertIsInstance(test_object, NewCcaiPrompt) + self.assertEqual(test_object.prompt_text, "What is the meaning of life?") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.prompt_id, "test_prompt_id") + self.assertIsNotNone(test_object.created_on) + self.assertEqual(test_object.context, {}) # Default value + self.assertIsNone(test_object.winner) # Default value + self.assertEqual(test_object.participating_subminds, []) # Default value + self.assertEqual(test_object.proposed_responses, {}) # Default value + self.assertEqual(test_object.votes, {}) # Default value + self.assertEqual(test_object.submind_discussion_history, []) # Default value + + # Test with None context (should convert to empty dict) + test_object = NewCcaiPrompt( + prompt_text="What is the meaning of life?", + cid="test_conversation_id", + prompt_id="test_prompt_id", + context=None + ) + self.assertEqual(test_object.context, {}) + + # Test submind_opinions conversion to submind_discussion_history + submind_opinions = {"submind1": "opinion1", "submind2": "opinion2"} + test_object = NewCcaiPrompt( + prompt_text="What is the meaning of life?", + cid="test_conversation_id", + prompt_id="test_prompt_id", + submind_opinions=submind_opinions + ) + self.assertEqual(len(test_object.submind_discussion_history), 1) + self.assertEqual(test_object.submind_discussion_history[0], submind_opinions) + + # Test to_db_query method + db_query = test_object.to_db_query() + self.assertEqual(db_query["_id"], test_object.prompt_id) + self.assertEqual(db_query["cid"], test_object.cid) + self.assertEqual(db_query["is_completed"], "0") + self.assertEqual(db_query["data"]["prompt_text"], test_object.prompt_text) + self.assertEqual(db_query["created_on"], test_object.created_on) + self.assertEqual(db_query["context"], test_object.context) + + def test_ccai_prompt_completed(self): + """Test CcaiPromptCompleted model creation and validation""" + from neon_data_models.models.api.klat.socketio import CcaiPromptCompleted + + # Prepare a context with required fields + current_time = datetime.now() + context = { + "winner": "The meaning of life is 42", + "prompt": {"prompt_id": "test_prompt_id"} + } + + # Test with required fields + test_object = CcaiPromptCompleted( + sid="test_shout_id", + cid="test_conversation_id", + message_body="The meaning of life is 42", + time_created=current_time, + prompt_id="test_prompt_id", + user_id="proctor-sid", + context=context + ) + self.assertIsInstance(test_object, CcaiPromptCompleted) + self.assertEqual(test_object.sid, "test_shout_id") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.message_body, "The meaning of life is 42") + self.assertEqual(test_object.time_created, current_time) + self.assertEqual(test_object.prompt_id, "test_prompt_id") + self.assertEqual(test_object.winner, "The meaning of life is 42") + self.assertEqual(test_object.user_id, "proctor-sid") + self.assertEqual(test_object.username, "proctor") # Parsed from user_id + + # Test with username validation + with self.assertRaises(ValidationError): + test_object = CcaiPromptCompleted( + sid="test_shout_id", + cid="test_conversation_id", + user_id="same_name", + username="same_name", + message_body="Hello world", + time_created=current_time, + prompt_id="test_prompt_id" + ) + + # Test missing prompt_id handling + with self.assertRaises(ValidationError): + CcaiPromptCompleted( + sid="test_shout_id", + cid="test_conversation_id", + message_body="Hello world", + time_created=current_time, + prompt_id="" # Empty string should raise assertion + ) + + # Test to_db_query method + db_query = test_object.to_db_query() + self.assertEqual(db_query["prompt_id"], test_object.prompt_id) + self.assertEqual(db_query["prompt_context"], test_object.context) + + # Test model_dump with aliases + dumped = test_object.model_dump() + self.assertIn("prompt_id", dumped) + self.assertIn("promptID", dumped) # Should include alias + self.assertEqual(dumped["prompt_id"], dumped["promptID"]) + + def test_get_prompt_data(self): + """Test GetPromptData model creation and validation""" + from neon_data_models.models.api.klat.socketio import GetPromptData + + # Test with required fields + test_object = GetPromptData( + nick="test_user", + cid="test_conversation_id", + prompt_id="test_prompt_id" + ) + self.assertIsInstance(test_object, GetPromptData) + self.assertEqual(test_object.nick, "test_user") + self.assertEqual(test_object.cid, "test_conversation_id") + self.assertEqual(test_object.prompt_id, "test_prompt_id") + self.assertEqual(test_object.limit, 5) # Default value + + # Test with custom limit + test_object = GetPromptData( + nick="test_user", + cid="test_conversation_id", + prompt_id="test_prompt_id", + limit=10 + ) + self.assertEqual(test_object.limit, 10) + + # Test to_db_query method + db_query = test_object.to_db_query() + self.assertEqual(db_query["cid"], test_object.cid) + self.assertEqual(db_query["limit"], test_object.limit) + self.assertEqual(db_query["prompt_ids"], [test_object.prompt_id]) + self.assertTrue(db_query["fetch_user_data"]) + + # Test with missing prompt_id + test_object = GetPromptData( + nick="test_user", + cid="test_conversation_id" + ) + self.assertIsNone(test_object.prompt_id) + # to_db_query should raise assertion error when prompt_id is None + with self.assertRaises(AssertionError): + test_object.to_db_query() + + def test_prompt_data(self): + """Test PromptData model creation and validation""" + from neon_data_models.models.api.klat.socketio import PromptData + + # Test _PromptData nested class + test_inner_object = PromptData._PromptData( + _id="test_id", + is_completed='1' # String value should be converted to boolean + ) + self.assertIsInstance(test_inner_object, PromptData._PromptData) + self.assertEqual(test_inner_object.id, "test_id") + self.assertTrue(test_inner_object.is_completed) + self.assertEqual(test_inner_object.proposed_responses, {}) # Default value + self.assertEqual(test_inner_object.submind_opinions, {}) # Default value + self.assertEqual(test_inner_object.votes, {}) # Default value + self.assertEqual(test_inner_object.participating_subminds, []) # Default value + + # Test serialization of _PromptData + serialized = test_inner_object.model_dump() + self.assertEqual(serialized["_id"], "test_id") + self.assertTrue(serialized["is_completed"]) + + # Test with custom serializer to match MongoDB schema + serialized = test_inner_object.alias_serialize() + self.assertEqual(serialized["_id"], "test_id") + self.assertEqual(serialized["is_completed"], "1") # Should be string "1" + + # Test full PromptData with single _PromptData object + valid_prompt_data = PromptData( + data=test_inner_object, + receiver="test_user", + cid="test_conversation_id" + ) + self.assertIsInstance(valid_prompt_data, PromptData) + self.assertEqual(valid_prompt_data.data, test_inner_object) + self.assertEqual(valid_prompt_data.receiver, "test_user") + self.assertEqual(valid_prompt_data.cid, "test_conversation_id") + self.assertIsNotNone(valid_prompt_data.request_id) # Should have auto-generated UUID + + # Test full PromptData with list of _PromptData objects + test_inner_object2 = PromptData._PromptData( + _id="test_id2", + is_completed='0' + ) + valid_prompt_data = PromptData( + data=[test_inner_object, test_inner_object2], + receiver="test_user", + cid="test_conversation_id" + ) + self.assertIsInstance(valid_prompt_data.data, list) + self.assertEqual(len(valid_prompt_data.data), 2) + self.assertEqual(valid_prompt_data.data[0], test_inner_object) + self.assertEqual(valid_prompt_data.data[1], test_inner_object2) + + # Test serialization/deserialization + dumped = valid_prompt_data.model_dump() + recreated = PromptData(**dumped) + self.assertEqual(valid_prompt_data.receiver, recreated.receiver) + self.assertEqual(valid_prompt_data.cid, recreated.cid) + self.assertEqual(valid_prompt_data.request_id, recreated.request_id) From 44485ef8869f6ecac6c2c72159f722e66f2a2322 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 7 Oct 2025 12:54:17 -0700 Subject: [PATCH 52/53] Remove unused CcaiPromptCompleted.request_id --- neon_data_models/models/api/klat/socketio.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 9bbabf08..90bf175b 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -417,9 +417,6 @@ class CcaiPromptCompleted(UserMessage): default="", description="Winning response text; empty in the event of an error", ) - request_id: Optional[str] = Field( - default=None, description="ID of the database transaction request" - ) prompt_id: str = Field( description="Prompt ID this message is in response to", ) From e63912249c6f70acb58f1e9c4cf2c2eec05c2aaf Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Tue, 7 Oct 2025 12:54:29 -0700 Subject: [PATCH 53/53] Refactor test case to specify "SocketIO" --- tests/models/api/test_klat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py index 6c43be37..67b8792f 100644 --- a/tests/models/api/test_klat.py +++ b/tests/models/api/test_klat.py @@ -27,13 +27,12 @@ import base64 from unittest import TestCase from datetime import datetime -from time import time from pydantic import ValidationError from neon_data_models.enum import CcaiState -class TestKlat(TestCase): +class TestKlatSocketIO(TestCase): def test_get_stt_request(self): """Test GetSttRequest model creation and validation""" from neon_data_models.models.api.klat.socketio import GetSttRequest