diff --git a/neon_data_models/models/api/klat/socketio.py b/neon_data_models/models/api/klat/socketio.py index 70794aa..52363de 100644 --- a/neon_data_models/models/api/klat/socketio.py +++ b/neon_data_models/models/api/klat/socketio.py @@ -288,7 +288,9 @@ 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']): + if values.get("userDisplayName") and values.get("nick") and \ + values['nick'].startswith(values['userDisplayName']) and \ + values['nick'] != values['userDisplayName']: # Patch old behavior and ensure `user_id` is nick + suffix values['userID'] = values.pop('nick') return values diff --git a/neon_data_models/models/api/mq/chatbots.py b/neon_data_models/models/api/mq/chatbots.py index c4e52d7..52a0713 100644 --- a/neon_data_models/models/api/mq/chatbots.py +++ b/neon_data_models/models/api/mq/chatbots.py @@ -26,7 +26,7 @@ from typing import Any, Dict, Literal, Optional, List, Union from datetime import datetime, timezone -from pydantic import Field, model_validator +from pydantic import Field, model_validator, AliasChoices from neon_data_models.enum import SubmindStatus, CcaiState, CcaiControl from neon_data_models.types import BotType @@ -40,48 +40,69 @@ class ChatbotsMqRequest(KlatContext, MQContext): """ Defines a request from Klat to the Chatbots service. """ - username: str = Field(alias="nick", - description="Username (or 'nick') of the sender") + + username: str = Field( + alias="nick", description="Username (or 'nick') of the sender" + ) cid: str = Field(description="Conversation ID associated with the shout") - message_text: str = Field(alias="messageText", - description="Text content of the shout") + message_text: str = Field( + alias="messageText", description="Text content of the shout" + ) from_bot: bool = Field( default=False, - description="True if the shout is from a bot, False if from a user") + description="True if the shout is from a bot, False if from a user", + ) prompt_id: Optional[str] = Field( default=None, - description="ID of the CCAI prompt associated with the shout") + description="ID of the CCAI prompt associated with the shout", + ) prompt_state: Optional[CcaiState] = Field( - default=None, deprecated=True, - description="State of the CCAI conversation associated with the shout") + default=None, + deprecated=True, + description="State of the CCAI conversation associated with the shout", + ) time_created: datetime = Field( - default= datetime.now(tz=timezone.utc), - description="Timestamp when the shout was created") + default=datetime.now(tz=timezone.utc), + description="Timestamp when the shout was created", + ) requested_participants: Optional[List[str]] = Field( - default=None, alias="participating_subminds", - description="List of CCAI participants requested to handle the shout") + default=None, + alias="participating_subminds", + description="List of CCAI participants requested to handle the shout", + ) recipient: Optional[str] = Field( - default=None, description="Explicitly defined recipient of the shout") + default=None, description="Explicitly defined recipient of the shout" + ) bound_service: Optional[str] = Field( - default=None, description="Service bound to the conversation") - context: Optional[dict] = Field( - default=None, deprecated=True, description="Extra proctor context") - + default=None, description="Service bound to the conversation" + ) + context: dict = Field( + default={}, deprecated=True, description="Extra proctor context" + ) + @classmethod - def from_sio_message(cls, sio_message: dict) -> 'ChatbotsMqRequest': + def from_sio_message(cls, sio_message: dict) -> "ChatbotsMqRequest": + # Parse incoming message `sid`, `cid`, and `title` from SIO Message klat_context = KlatContext(**sio_message) - mq_context = MQContext(**sio_message) + # This is the first MQ message; define new context + mq_context = MQContext() return ChatbotsMqRequest( **klat_context.model_dump(exclude_none=True), **mq_context.model_dump(exclude_none=True), - username=sio_message.get("userDisplayName") or \ - sio_message.get("userID"), - message_text=sio_message["messageText"], - from_bot=sio_message.get("bot") == 1, - prompt_id = sio_message.get("promptID"), - prompt_state=sio_message.get("promptState"), - time_created=sio_message["timeCreated"], - recipient=sio_message.get("recipient"), + username=sio_message.get("username") + or sio_message.get("userDisplayName"), + message_text=sio_message.get("message_body") + or sio_message.get("messageText"), + from_bot=sio_message.get("is_bot", sio_message.get("bot")) == 1, + prompt_id=sio_message.get("prompt_id") + or sio_message.get("promptID"), + prompt_state=sio_message.get("prompt_state") + or sio_message.get("promptState"), + time_created=sio_message.get("time_created") + or sio_message.get("timeCreated"), + recipient=sio_message.get( + "recipient" + ), bound_service=sio_message.get("bound_service"), ) @@ -90,11 +111,11 @@ def model_dump(self, **kwargs): Override model_dump to include SIO fields for backwards compatibility """ - # For backwards-compat with Klat Server, include aliased keys in + # For backwards-compat with Klat Server, 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. @@ -107,171 +128,260 @@ class ChatbotsMqSubmindResponse(KlatContext, MQContext): """ Defines a chatbot response to a request. """ - user_id: str = Field(alias='userID', - description="Unique UID of the sender") - username: Optional[str] = Field(default=None, - alias="userDisplayName", - description="Username of the sender") - message_text: str = Field(alias="messageText", - description="Text content of the shout") + + user_id: str = Field( + alias="userID", description="Unique UID of the sender" + ) + username: Optional[str] = Field( + default=None, + alias="nick", + description="Username of the sender", + ) + message_text: str = Field( + alias=AliasChoices("messageText", "shout"), + description="Text content of the shout", + ) sid: str = Field(default="", alias="messageID", description="Shout ID") replied_message: Optional[str] = Field( - default=None, alias="repliedMessage", - description="ID of the shout being replied to") - bot: Literal["0", "1"] = Field(default='0', alias='is_bot', - description="1 if the shout is from a bot") + default=None, + alias=AliasChoices("repliedMessage", "responded_shout"), + description="ID of the shout being replied to", + ) + bot: Literal["0", "1"] = Field( + default="0", alias="is_bot", description="1 if the shout is from a bot" + ) prompt_id: Optional[str] = Field( - default=None, alias="promptID", - description="ID of the CCAI prompt associated with the shout") + default=None, + alias="promptID", + description="ID of the CCAI prompt associated with the shout", + ) is_announcement: bool = Field( - default=False, alias="isAnnouncement", - description="True if the shout is an announcement") + default=False, + alias="isAnnouncement", + description="True if the shout is an announcement", + ) time_created: datetime = Field( - default= datetime.now(tz=timezone.utc), alias="timeCreated", - description="Timestamp when the shout was created") + default=datetime.now(tz=timezone.utc), + alias=AliasChoices("timeCreated", "time", "created_on"), + description="Timestamp when the shout was created", + ) source: str = Field( default="klat_observer", - description="Name of the service originating the shout") - bot_type: Optional[BotType] = Field(default=None, deprecated=True, - description="Type of submind sending the shout") - + description="Name of the service originating the shout", + ) + bot_type: Optional[BotType] = Field( + default=None, + deprecated=True, + description="Type of submind sending the shout", + ) + # Below are deprecated fields for backwards-compat. service_name: Any = Field(default=None, deprecated=True) - context: Optional[dict] = Field( - default=None, deprecated=True, - description="Context used for Klat Server backwards-compat.") - dom: Any = Field(default=None, deprecated=True, - description="Domain of this conversation") + context: dict = Field( + default={}, + deprecated=True, + description="Context used for Klat Server backwards-compat.", + ) + dom: Any = Field( + default=None, + deprecated=True, + description="Domain of this conversation", + ) omit_reply: bool = Field( - default=True, deprecated=True, - description="If true, the Proctor will ignore this message") - no_save: bool = Field(default=False, deprecated=True, - description="If true, this message will be ignored") + default=True, + deprecated=True, + description="If true, the Proctor will ignore this message", + ) + no_save: bool = Field( + default=False, + deprecated=True, + description="If true, this message will be ignored", + ) to_discussion: bool = Field(default=False, deprecated=True) prompt_state: CcaiState = Field( - default=CcaiState.IDLE, deprecated=True, alias="promptState", - description="State of the CCAI conversation associated with the shout") - - @model_validator(mode='after') + default=CcaiState.IDLE, + deprecated=True, + alias=AliasChoices("promptState", "conversation_state"), + description="State of the CCAI conversation associated with the shout", + ) + + @model_validator(mode="after") def set_username_from_user_id(self): if self.username is None and self.user_id: - self.username = self.user_id.rsplit('-', 1)[0] + self.username = self.user_id.rsplit("-", 1)[0] + if self.username == self.user_id: + raise ValueError( + f"username cannot be the same as user_id: {self.username}" + ) return self - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def validate_inputs(cls, values): if isinstance(values, dict): - # Some additional aliases for backwards-compat. - if "nick" in values: - values.setdefault("userID", values.get("nick")) - - if "shout" in values: - values.setdefault("messageText", values.get("shout")) - - if "responded_shout" in values: - values.setdefault("repliedMessage", - values.get("responded_shout")) - - if "time" in values: - values.setdefault("timeCreated", values.get("time")) - - if "created_on" in values: - values.setdefault("timeCreated", values.get("created_on")) - if "sid" in values and values["sid"] is None: values.pop("sid") - if "conversation_state" in values: - values.setdefault("promptState", - values.get("conversation_state")) - return values def model_dump(self, **kwargs): - # For backwards-compat with Klat Server, include aliased keys in + # For backwards-compat with Klat Server, 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) - by_alias['isAnnouncement'] = '1' if self.is_announcement else '0' - by_alias['nick'] = self.user_id - by_alias['responded_shout'] = self.replied_message - by_alias['shout'] = self.message_text - by_alias['time'] = self.time_created.timestamp() - by_alias['promptState'] = self.prompt_state.value - by_alias['created_on'] = self.time_created.timestamp() - + # Add all aliases for fields using AliasChoices + # username: AliasChoices("userDisplayName", "nick") + by_alias["userDisplayName"] = self.username + by_alias["nick"] = self.username + + # message_text: AliasChoices("messageText", "shout") + by_alias["messageText"] = self.message_text + by_alias["shout"] = self.message_text + + # replied_message: AliasChoices("repliedMessage", "responded_shout") + by_alias["repliedMessage"] = self.replied_message + by_alias["responded_shout"] = self.replied_message + + # prompt_id: alias="promptID" + by_alias["promptID"] = self.prompt_id + + # is_announcement: alias="isAnnouncement" + by_alias["isAnnouncement"] = "1" if self.is_announcement else "0" + + # time_created: AliasChoices("timeCreated", "time", "created_on") + if self.time_created: + by_alias["timeCreated"] = self.time_created.timestamp() + by_alias["time"] = self.time_created.timestamp() + by_alias["created_on"] = self.time_created.timestamp() + + # prompt_state: AliasChoices("promptState", "conversation_state") + if self.prompt_state: + by_alias["promptState"] = self.prompt_state.value + by_alias["conversation_state"] = self.prompt_state.value + return {**super().model_dump(**kwargs), **by_alias} class PromptCompletedContext(BaseModel): prompt: Optional[ChatbotsMqRequest] = Field( - default=None, description="Original request containing the prompt") + default=None, description="Original request containing the prompt" + ) prompt_text: str = Field(description="The string prompt that is completed") - available_subminds: List[str] = Field( # Seems to always match `participating_subminds` - default=[], description="List of subminds available to participate") + available_subminds: List[str] = ( + Field( # Seems to always match `participating_subminds` + default=[], description="List of subminds available to participate" + ) + ) participating_subminds: List[str] = Field( - default=[], description="List of subminds participating in the prompt") + default=[], description="List of subminds participating in the prompt" + ) proposed_responses: Dict[str, str] = Field( - default={}, description="Dict of nick to proposal") - + default={}, description="Dict of nick to proposal" + ) + # In the future, there will be a list of these for multi-round discussion + submind_discussion_history: Dict[str, List[str]] = Field( + default=[], + description="List of dict of discussion rounds (dict of nick to shout)", + ) submind_opinions: Dict[str, str] = Field( - default={}, description="Dict of nick to discussion") + default={}, description="Dict of nick to discussion" + ) - votes: Dict[str, str] = Field(default={}, - description="Dict of nick to vote") + votes: Dict[str, str] = Field( + default={}, description="Dict of nick to vote" + ) votes_per_submind: Dict[str, List[str]] = Field( - default={}, description="Dict of nick to list of received votes") - winner: str = Field(default="", description="Selected winner") + default={}, + description="Dict of nick to list of received votes (nicks)", + ) + winner: str = Field(default="", description="nick of selected winner") # Below are deprecated is_active: bool = Field( # Seems to report active all the time - default=False, deprecated=True, - description="True if a response has not yet been chosen") + default=False, + deprecated=True, + description="True if a response has not yet been chosen", + ) state: Optional[CcaiState] = Field( - default=CcaiState.PICK, deprecated=True, - description="State of the CCAI conversation (always PICK)") + default=CcaiState.PICK, + deprecated=True, + description="State of the CCAI conversation (always PICK)", + ) + + @model_validator(mode="before") + @classmethod + def validate_discussion_history(cls, values): + if ( + "submind_discussion_history" not in values + and "submind_opinions" in values + ): + values["submind_discussion_history"] = { + k: [v] for k, v in values["submind_opinions"].items() + } + return values class ChatbotsMqSavePrompt(ChatbotsMqSubmindResponse): context: PromptCompletedContext = Field( alias="conversation_context", - description="Definition of the completed discussion") - + description="Definition of the completed discussion", + ) + def model_dump(self, **kwargs): return ChatbotsMqSubmindResponse.model_dump(self, **kwargs) -class ChatbotsMqNewPrompt(ChatbotsMqSubmindResponse): +class ChatbotsMqNewPrompt(KlatContext, MQContext): prompt_id: str = Field( description="ID of the CCAI prompt associated with the shout" ) - user_id: Optional[str] = Field(default=None, alias="nick", - description="User ID of the proctor") prompt_text: str = Field(description="The new prompt being discussed") prompt_state: CcaiState = Field( - default=CcaiState.IDLE, deprecated=True, - description="Implemented for backwards-compat. New Prompt always IDLE") + default=CcaiState.IDLE, + deprecated=True, + description="Implemented for backwards-compat. New Prompt always IDLE", + ) + user_id: Optional[str] = Field( + default=None, + alias="userID", + validation_alias="userID", + description="User ID of the proctor", + ) + sid: str = Field(default="", alias="messageID", description="Shout ID") + username: Optional[str] = Field( + default=None, + alias="nick", + description="Username of the sender", + ) + time_created: datetime = Field( + default=datetime.now(tz=timezone.utc), + alias=AliasChoices("timeCreated", "time", "created_on"), + description="Timestamp when the shout was created", + ) + source: str = Field( + default="klat_observer", + description="Name of the service originating the shout", + ) + bot_type: Optional[BotType] = Field( + default=None, + deprecated=True, + description="Type of submind sending the shout", + ) discussion_rounds: int = Field( - default=2, - description="Number of discussion rounds per cycle for this prompt") - context: Optional[dict] = Field(default=None, deprecated=True, - alias="conversation_context") - - @model_validator(mode='before') - @classmethod - def validate_context(cls, values): - values.setdefault("message_text", values.get("shout", "")) - values.setdefault("user_id", values.get("nick")) - return values - - def model_dump(self, **kwargs): - return ChatbotsMqSubmindResponse.model_dump(self, **kwargs) + default=2, + description="Number of discussion rounds per cycle for this prompt", + ) + context: dict = Field( + default={}, + deprecated=True, + description="Conversation Context used by Klat Server", + alias="conversation_context", + ) class ChatbotsMqResponse: @@ -279,14 +389,20 @@ class ChatbotsMqResponse: Type adapter for validating an arbitrary MQ message. This will always return an instance that extends `BaseMessage` and `MQContext`. """ + @classmethod - def __new__(cls, *_, **kwargs) -> Union[ChatbotsMqSavePrompt, - ChatbotsMqNewPrompt, - ChatbotsMqSubmindResponse]: - message_text = kwargs.get("message_text") or kwargs.get("messageText") \ + def __new__( + cls, *_, **kwargs + ) -> Union[ + ChatbotsMqSavePrompt, ChatbotsMqNewPrompt, ChatbotsMqSubmindResponse + ]: + message_text = ( + kwargs.get("message_text") + or kwargs.get("messageText") or kwargs.get("shout") - kwargs['message_text'] = message_text - + ) + kwargs["message_text"] = message_text + if message_text == CcaiControl.SAVE_PROMPT_RESULTS.value: return ChatbotsMqSavePrompt(**kwargs) elif message_text == CcaiControl.CREATE_PROMPT.value: @@ -295,69 +411,86 @@ def __new__(cls, *_, **kwargs) -> Union[ChatbotsMqSavePrompt, return ChatbotsMqSubmindResponse(**kwargs) - - class ChatbotsMqSubmindsState(MQContext): class SubmindState(BaseModel): - submind_id: str = Field(description="Connected submind's user_id") + submind_id: str = Field( + description="Connected submind's ID (nickname + suffix)" + ) status: SubmindStatus = Field( - description="Subminds's status in a particular conversation") + description="Subminds's status in a particular conversation" + ) subminds_per_cid: Dict[str, List[SubmindState]] = Field( - description="List of submind participants per conversation ID") + description="List of submind participants per conversation ID" + ) connected_subminds: Dict[str, ConnectedSubmind] = Field( - description="Dict of submind `user_id` to `ConnectedSubmind` object") + description="Dict of `submind_id` to `ConnectedSubmind` object" + ) cid_submind_bans: Dict[str, List[str]] = Field( - description="Dict of `cid` to list of banned submind `user_id`s") + description="Dict of `cid` to list of banned `submind_id`s" + ) banned_subminds: List[str] = Field( - description="List of globally banned submind `user_id`s") + description="List of globally banned `submind_id`s" + ) msg_type: Literal["subminds_state"] = Field( - "subminds_state", description="Message type for SIO", deprecated=True) + "subminds_state", description="Message type for SIO", deprecated=True + ) class ChatbotsMqConfiguredPersonasRequest(MQContext): service_name: str = Field( - description="Name of the service to get personas for") + description="Name of the service to get personas for" + ) user_id: Optional[str] = Field( - default=None, description="Optional user_id making with the request.") + default=None, description="Optional user_id making the request." + ) class ChatbotsMqConfiguredPersonasResponse(MQContext): update_time: datetime = Field( - description="Time the personas were last checked") + description="Time the personas were last checked" + ) items: List[LLMPersona] = Field( - description="List of configured personas from Klat") + description="List of configured personas from Klat" + ) context: dict = Field(deprecated=True) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def validate_context(cls, values): # Deprecated context handling for backwards-compat. - if 'context' not in values and 'message_id' in values: - values['context'] = {"mq": {"message_id": values['message_id']}} + if "context" not in values and "message_id" in values: + values["context"] = {"mq": {"message_id": values["message_id"]}} return values - + def model_dump(self, **kwargs): """ - Override model_dump to include 'persona_name' field for each item based + Override model_dump to include 'persona_name' field for each item based on its 'name' for backwards-compat. with Klat server """ by_alias = {} - if 'by_alias' not in kwargs: + if "by_alias" not in kwargs: # `by_alias` to include `persona_name` in serialized `LLMPersona`s by_alias = super().model_dump(by_alias=True, **kwargs) - + return {**super().model_dump(**kwargs), **by_alias} @classmethod - def from_persona_request(cls, data: dict, - request: ChatbotsMqConfiguredPersonasRequest): - data["items"] = [item for item in data["items"] - if request.service_name in item["supported_llms"]] - return cls(**data, message_id=request.message_id, - routing_key=request.routing_key) + def from_persona_request( + cls, data: dict, request: ChatbotsMqConfiguredPersonasRequest + ): + data["items"] = [ + item + for item in data["items"] + if request.service_name in item["supported_llms"] + ] + return cls( + **data, + message_id=request.message_id, + routing_key=request.routing_key, + ) class ChatbotsMqPromptsDataRequest(MQContext): @@ -371,87 +504,120 @@ class ChatbotsMqPromptsDataResponse(MQContext): context: dict = Field(deprecated=True) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def validate_context(cls, values): # Deprecated context handling for backwards-compat. - if 'context' not in values and 'message_id' in values: - values['context'] = {"mq": {"message_id": values['message_id']}} + if "context" not in values and "message_id" in values: + values["context"] = {"mq": {"message_id": values["message_id"]}} return values - + @classmethod - def from_prompt_data_request(cls, data: dict, - request: ChatbotsMqPromptsDataRequest): - return cls(**data, message_id=request.message_id, - routing_key=request.routing_key) + def from_prompt_data_request( + cls, data: dict, request: ChatbotsMqPromptsDataRequest + ): + return cls( + **data, + message_id=request.message_id, + routing_key=request.routing_key, + ) class ChatbotsMqSubmindConnection(MQContext): - user_id: str = Field(description="User ID of the submind", alias="nick") + user_id: str = Field( + description="User ID of the submind", + validation_alias="userID", + alias="userID", + ) time: datetime = Field( - default= datetime.now(tz=timezone.utc), - description="Timestamp when the submind last connected") + default=datetime.now(tz=timezone.utc), + description="Timestamp when the submind last connected", + ) cids: Optional[List[str]] = Field( - default=None, description="List of conversation IDs the submind is in") + default=None, description="List of conversation IDs the submind is in" + ) context: Optional[ConnectedSubmind] = Field( default=None, - description="ConnectedSubmind definition of the connecting bot") + description="ConnectedSubmind definition of the connecting bot", + ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def validate_context(cls, values): if "context" in values and isinstance(values["context"], dict): - user_id = values.get("user_id") or values.get("nick") or "" - values["context"].setdefault("service_name", - user_id.rsplit('-',1)[0]) + user_id = values.get("user_id") or values.get("userID") or "" + values["context"].setdefault( + "service_name", user_id.rsplit("-", 1)[0] + ) return values class ChatbotsMqSubmindDisconnection(MQContext): - user_id: str = Field(description="User ID of the submind", alias="nick") + user_id: str = Field( + description="User ID of the submind", + validation_alias="userID", + alias="userID", + ) class ChatbotsMqSubmindInvitation(MQContext): cid: str = Field(description="Conversation ID to invite subminds to") requested_participants: List[str] = Field( - description="List of submind User IDs to invite to the conversation") + description="List of submind User IDs to invite to the conversation" + ) class ChatbotsMqUpdateParticipatingSubminds(MQContext): cid: str = Field(description="Conversation ID to update") subminds_to_invite: List[str] = Field( default=[], - description="List of submind User IDs to invite to the conversation") + description="List of submind User IDs to invite to the conversation", + ) subminds_to_kick: List[str] = Field( default=[], - description="List of submind User IDs to evict from the conversation") + description="List of submind User IDs to evict from the conversation", + ) class ChatbotsMqSubmindConversationBan(MQContext): - user_id: str = Field(description="User ID of the submind", alias="nick") + user_id: str = Field( + description="User ID of the submind", + validation_alias="userID", + alias="userID", + ) cid: str = Field(description="Conversation ID to (un)ban submind from") class ChatbotsMqSubmindGlobalBan(MQContext): - user_id: str = Field(description="User ID of the submind", alias="nick") + user_id: str = Field( + description="User ID of the submind", + validation_alias="userID", + alias="userID", + ) class ChatbotsMqSubmindResponseError(MQContext): - message: Optional[str] = Field(default=None, alias="msg", - description="Error message") - - -__all__ = [ChatbotsMqRequest.__name__, ChatbotsMqResponse.__name__, - ChatbotsMqSubmindResponse.__name__, ChatbotsMqSavePrompt.__name__, - ChatbotsMqNewPrompt.__name__, ChatbotsMqSubmindsState.__name__, - ChatbotsMqConfiguredPersonasRequest.__name__, - ChatbotsMqConfiguredPersonasResponse.__name__, - ChatbotsMqPromptsDataRequest.__name__, - ChatbotsMqPromptsDataResponse.__name__, - ChatbotsMqSubmindConnection.__name__, - ChatbotsMqSubmindDisconnection.__name__, - ChatbotsMqSubmindInvitation.__name__, - ChatbotsMqUpdateParticipatingSubminds.__name__, - ChatbotsMqSubmindConversationBan.__name__, - ChatbotsMqSubmindGlobalBan.__name__, - ChatbotsMqSubmindResponseError.__name__,] + message: Optional[str] = Field( + default=None, alias="msg", description="Error message" + ) + + +__all__ = [ + ChatbotsMqRequest.__name__, + ChatbotsMqResponse.__name__, + ChatbotsMqSubmindResponse.__name__, + ChatbotsMqSavePrompt.__name__, + ChatbotsMqNewPrompt.__name__, + ChatbotsMqSubmindsState.__name__, + ChatbotsMqConfiguredPersonasRequest.__name__, + ChatbotsMqConfiguredPersonasResponse.__name__, + ChatbotsMqPromptsDataRequest.__name__, + ChatbotsMqPromptsDataResponse.__name__, + ChatbotsMqSubmindConnection.__name__, + ChatbotsMqSubmindDisconnection.__name__, + ChatbotsMqSubmindInvitation.__name__, + ChatbotsMqUpdateParticipatingSubminds.__name__, + ChatbotsMqSubmindConversationBan.__name__, + ChatbotsMqSubmindGlobalBan.__name__, + ChatbotsMqSubmindResponseError.__name__, +] diff --git a/tests/models/api/test_klat.py b/tests/models/api/test_klat.py index fecff9d..c279ffb 100644 --- a/tests/models/api/test_klat.py +++ b/tests/models/api/test_klat.py @@ -36,28 +36,28 @@ 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 - + # Test with required fields test_object = GetSttRequest( cid="test_conversation_id", sid="test_shout_id", - user_uid="test_user_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" + 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) @@ -66,12 +66,12 @@ def test_get_stt_request(self): 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" + cid="test_conversation_id", ) self.assertIsInstance(test_object, GetSttResponse) self.assertEqual(test_object.transcript, "Hello world") @@ -79,13 +79,10 @@ def test_get_stt_response(self): 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 - ) + 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") @@ -98,7 +95,7 @@ def test_get_stt_response(self): 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", @@ -110,7 +107,7 @@ def test_get_tts_request(self): 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", @@ -119,7 +116,7 @@ def test_get_tts_request(self): ) 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) @@ -128,15 +125,15 @@ def test_get_tts_request(self): 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" + cid="test_conversation_id", ) self.assertIsInstance(test_object, GetTtsResponse) self.assertEqual(test_object.audio_data, mock_audio) @@ -145,23 +142,20 @@ def test_get_tts_response(self): 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 - ) + 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) @@ -178,14 +172,14 @@ def test_get_tts_response(self): 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" + message_text="Hello world", ) self.assertIsInstance(test_object, NewPromptMessage) self.assertEqual(test_object.cid, "test_conversation_id") @@ -193,9 +187,11 @@ def test_new_prompt_message(self): 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.prompt_state, CcaiState.IDLE + ) # Default value self.assertEqual(test_object.context, {}) # Default value - + # Test with alias parameters test_object = NewPromptMessage( cid="test_conversation_id", @@ -203,13 +199,13 @@ def test_new_prompt_message(self): user_uid="test_user_uuid", promptID="test_prompt_id", messageText="Hello world", - promptState=CcaiState.RESP + 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) @@ -222,11 +218,11 @@ def test_new_prompt_message(self): # 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 with required fields current_time = datetime.now() test_object = UserMessage( @@ -246,20 +242,20 @@ def test_user_message(self): 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 + 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( @@ -268,12 +264,12 @@ def test_user_message(self): user_id="same_name", userDisplayName="same_name", # Should raise error when same as user_id message_body="Hello world", - time_created=current_time + 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", @@ -282,22 +278,24 @@ def test_user_message(self): time_created=current_time, isAnnouncement=1, isAudio=1, - username="test_user" + 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.sid) 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["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", @@ -317,68 +315,125 @@ def test_user_message(self): self.assertEqual(prompt_message.prompt_state, test_object.prompt_state) self.assertEqual(prompt_message.message_text, test_object.message_body) + # Real message + data = { + "routing_key": None, + "message_id": "f29bc127ebf145f9b926424ee48f9f5b", + "sid": "653195bd024c43cdbc1a6c0e8c1407ad", + "cid": "f4ef56bf4b", + "title": "", + "user_id": "logistics_vllm-ae2191f1a93f48809f6a8a29544f3614", + "username": "logistics_vllm", + "message_text": "I am ready for the next prompt", + "replied_message": None, + "bot": "1", + "prompt_id": "", + "is_announcement": False, + "time_created": 1760117318.0, + "source": "klat_observer", + "bot_type": None, + "service_name": None, + "context": {}, + "dom": None, + "omit_reply": True, + "no_save": False, + "to_discussion": False, + "prompt_state": 5, + "messageID": "653195bd024c43cdbc1a6c0e8c1407ad", + "userID": "logistics_vllm-ae2191f1a93f48809f6a8a29544f3614", + "is_bot": "1", + "promptID": "", + "isAnnouncement": "0", + "userDisplayName": "logistics_vllm", + "nick": "logistics_vllm", + "messageText": "I am ready for the next prompt", + "shout": "I am ready for the next prompt", + "repliedMessage": None, + "responded_shout": None, + "timeCreated": 1760117318.0, + "time": 1760117318.0, + "created_on": 1760117318.0, + "promptState": 5, + "conversation_state": 5, + } + message = UserMessage(**data) + self.assertEqual(message.user_id, data["user_id"]) + self.assertEqual(message.username, data["nick"]) + 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" + 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.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.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 - + 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 + 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 + submind_opinions=submind_opinions, ) self.assertEqual(len(test_object.submind_discussion_history), 1) - self.assertEqual(test_object.submind_discussion_history[0], submind_opinions) - + 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["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 - + 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"} + "prompt": {"prompt_id": "test_prompt_id"}, } - + # Test with required fields test_object = CcaiPromptCompleted( sid="test_shout_id", @@ -387,7 +442,7 @@ def test_ccai_prompt_completed(self): time_created=current_time, prompt_id="test_prompt_id", user_id="proctor-sid", - context=context + context=context, ) self.assertIsInstance(test_object, CcaiPromptCompleted) self.assertEqual(test_object.sid, "test_shout_id") @@ -397,8 +452,10 @@ def test_ccai_prompt_completed(self): 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 - + self.assertEqual( + test_object.username, "proctor" + ) # Parsed from user_id + # Test with username validation with self.assertRaises(ValidationError): test_object = CcaiPromptCompleted( @@ -408,9 +465,9 @@ def test_ccai_prompt_completed(self): username="same_name", message_body="Hello world", time_created=current_time, - prompt_id="test_prompt_id" + prompt_id="test_prompt_id", ) - + # Test missing prompt_id handling with self.assertRaises(ValidationError): CcaiPromptCompleted( @@ -418,14 +475,14 @@ def test_ccai_prompt_completed(self): cid="test_conversation_id", message_body="Hello world", time_created=current_time, - prompt_id="" # Empty string should raise assertion + 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) @@ -435,39 +492,38 @@ def test_ccai_prompt_completed(self): 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" + 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 + 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" + 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 @@ -477,57 +533,66 @@ def test_get_prompt_data(self): 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 + 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.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 - + 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" - + 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" + 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 - + 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' + _id="test_id2", is_completed="0" ) valid_prompt_data = PromptData( data=[test_inner_object, test_inner_object2], receiver="test_user", - cid="test_conversation_id" + 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) diff --git a/tests/models/api/test_mq.py b/tests/models/api/test_mq.py index ef5fe51..36f619e 100644 --- a/tests/models/api/test_mq.py +++ b/tests/models/api/test_mq.py @@ -580,7 +580,7 @@ def test_neon_api_message(self): "requested_skill": "stt", "message_body": "base64encodedstring", "client": "test_client", - "nick": "test_user", + "username": "test_user", "cid": "test_session", "sid": "test_shout_id", "timeCreated": 123456789, @@ -610,7 +610,7 @@ def test_neon_api_message(self): "requested_skill": "recognizer", "messageText": "How are you?", "client": "test_client", - "nick": "test_user", + "username": "test_user", "cid": "test_session", "sid": "test_shout_id", "timeCreated": 123456789, @@ -995,7 +995,7 @@ def test_chatbots_mq_request(self): # Test userID fallback sio_message = { - "userID": "user_id", + "username": "user_id", "cid": "test_conversation", "messageText": "Hello from SIO", "timeCreated": current_time, @@ -1011,7 +1011,7 @@ def test_chatbots_mq_submind_response(self): # Test basic initialization with required fields current_time = datetime.now(tz=timezone.utc) valid_kwargs = { - "userID": "user123", + "userID": "user123-uid_suffix", "messageText": "Hello, this is a response", "message_id": "test_message_id", "conversation_state": 0 @@ -1019,14 +1019,14 @@ def test_chatbots_mq_submind_response(self): response = ChatbotsMqResponse(**valid_kwargs) self.assertIsInstance(response, ChatbotsMqSubmindResponse) - self.assertEqual(response.user_id, "user123") + self.assertEqual(response.username, "user123") self.assertEqual(response.message_text, "Hello, this is a response") self.assertEqual(response.bot, "0") # Default value check self.assertFalse(response.is_announcement) # Default value check # Test with all fields including aliases full_kwargs = { - "userID": "user123", + "userID": "user123-uid_suffix", "userDisplayName": "John Doe", "messageText": "Hello, this is a complete response", "messageID": "msg123", @@ -1044,8 +1044,8 @@ def test_chatbots_mq_submind_response(self): full_response = ChatbotsMqResponse(**full_kwargs) self.assertIsInstance(full_response, ChatbotsMqSubmindResponse) - self.assertEqual(full_response.user_id, "user123") - self.assertEqual(full_response.username, "John Doe") + self.assertEqual(full_response.user_id, "user123-uid_suffix") + self.assertEqual(full_response.username, "user123") self.assertEqual(full_response.message_text, "Hello, this is a complete response") self.assertEqual(full_response.sid, "msg123") self.assertEqual(full_response.replied_message, "original_msg456") @@ -1174,7 +1174,7 @@ def test_chatbots_mq_save_prompt(self): # Test initialization valid_kwargs = { - "userID": "user123", + "userID": "user123-uid_suffix", "messageText": "Prompt completed", "message_id": "test_message_id", "prompt_id": "prompt123", @@ -1185,7 +1185,7 @@ def test_chatbots_mq_save_prompt(self): } save_prompt = ChatbotsMqSavePrompt(**valid_kwargs) self.assertIsInstance(save_prompt, ChatbotsMqSavePrompt) - self.assertEqual(save_prompt.user_id, "user123") + self.assertEqual(save_prompt.username, "user123") self.assertEqual(save_prompt.message_text, "Prompt completed") self.assertEqual(save_prompt.prompt_id, "prompt123") # self.assertEqual(save_prompt.prompt_text, "Test prompt text") @@ -1195,14 +1195,13 @@ def test_chatbots_mq_save_prompt(self): # Test model_dump inheritance from ChatbotsMqResponse serialized = save_prompt.model_dump() self.assertIn("userID", serialized) - self.assertIn("messageText", serialized) self.assertIn("prompt_id", serialized) self.assertIn("context", serialized) # Test missing required fields with self.assertRaises(ValidationError): ChatbotsMqSavePrompt( - userID="user123", + userID="user123-uid_suffix", messageText="Prompt completed", message_id="test_message_id", prompt_id="prompt123", @@ -1227,10 +1226,9 @@ def test_chatbots_mq_new_prompt(self): new_prompt = ChatbotsMqNewPrompt(**valid_kwargs) self.assertIsInstance(new_prompt, ChatbotsMqNewPrompt) self.assertEqual(new_prompt.user_id, "user123") - self.assertEqual(new_prompt.message_text, valid_kwargs["messageText"]) self.assertEqual(new_prompt.prompt_id, "prompt123") self.assertEqual(new_prompt.prompt_text, "Test prompt text") - self.assertIsNone(new_prompt.context) + self.assertEqual(new_prompt.context, {}) # Test with conversation_context alias context_kwargs = { @@ -1256,12 +1254,10 @@ def test_chatbots_mq_new_prompt(self): "conversation_state": 0 } partial_prompt = ChatbotsMqNewPrompt(**partial_kwargs) - self.assertEqual(partial_prompt.message_text, '') # Test model_dump inheritance from ChatbotsMqResponse - serialized = new_prompt.model_dump() + serialized = partial_prompt.model_dump() self.assertIn("user_id", serialized) - self.assertIn("messageText", serialized) self.assertIn("prompt_id", serialized) # Test missing required fields @@ -1307,7 +1303,7 @@ def test_old_class_names_compatibility(self): self.assertIsInstance(req, ChatbotsMqRequest) resp = ChatbotsMqResponse( - userID="test_user", + userID="test_user-uid_suffix", messageText="Test response", message_id="test_message_id", conversation_state=0, @@ -1610,141 +1606,16 @@ def test_chatbots_mq_prompts_data_response(self): with self.assertRaises(ValidationError): ChatbotsMqPromptsDataResponse(records=["prompt1", "prompt2"]) - def test_chatbots_mq_submind_response(self): - from neon_data_models.models.api.mq.chatbots import ChatbotsMqSubmindResponse - from datetime import datetime, timezone - - # Test basic initialization with required fields - current_time = datetime.now(tz=timezone.utc) - valid_kwargs = { - "userID": "test_user", - "messageText": "Test submind response", - "message_id": "test_message_id", - "prompt_state": 1, - } - - response = ChatbotsMqSubmindResponse(**valid_kwargs) - self.assertIsInstance(response, ChatbotsMqSubmindResponse) - self.assertEqual(response.user_id, "test_user") - self.assertEqual(response.message_text, "Test submind response") - self.assertEqual(response.bot, "0") # Default value check - - # Test with all fields including aliased fields - full_kwargs = { - "userID": "submind1", - "userDisplayName": "Test Submind", - "messageText": "Full test response", - "messageID": "test_shout_id", - "repliedMessage": "original_message_id", - "bot": "1", - "promptID": "prompt_id_123", - "conversation_state": 2, - "is_announcement": True, - "timeCreated": current_time, - "source": "test_source", - "client": "test_client", - "cid": "conversation123", - "message_id": "test_message_id" - } - - full_response = ChatbotsMqSubmindResponse(**full_kwargs) - self.assertIsInstance(full_response, ChatbotsMqSubmindResponse) - self.assertEqual(full_response.user_id, "submind1") - self.assertEqual(full_response.username, "Test Submind") - self.assertEqual(full_response.message_text, "Full test response") - self.assertEqual(full_response.sid, "test_shout_id") - self.assertEqual(full_response.replied_message, "original_message_id") - self.assertEqual(full_response.bot, "1") - self.assertEqual(full_response.prompt_id, "prompt_id_123") - self.assertEqual(full_response.prompt_state, 2) - self.assertTrue(full_response.is_announcement) - self.assertEqual(full_response.time_created, current_time) - self.assertEqual(full_response.source, "test_source") - - # Test alternate field names (validate_inputs validator) - alternate_kwargs = { - "nick": "submind2", - "shout": "Message using alternate field names", - "responded_shout": "parent_message_id", - "time": current_time.timestamp(), - "message_id": "test_message_id", - "conversation_state": 2, - } - - alternate_response = ChatbotsMqSubmindResponse(**alternate_kwargs) - self.assertEqual(alternate_response.user_id, "submind2") - self.assertEqual(alternate_response.message_text, "Message using alternate field names") - self.assertEqual(alternate_response.replied_message, "parent_message_id") - - # Test model_dump serialization - serialized = full_response.model_dump(by_alias=True) - self.assertEqual(serialized["userID"], "submind1") - self.assertEqual(serialized["messageText"], "Full test response") - self.assertTrue(serialized["isAnnouncement"]) - self.assertEqual(serialized["nick"], "submind1") # Check backwards compatibility field - self.assertEqual(serialized["shout"], "Full test response") # Check backwards compatibility field - - - # Test username from user_id with a single hyphen - hyphen_user_id = ChatbotsMqSubmindResponse( - userID="service-123456", - messageText="Test message", - message_id="test_id" - ) - self.assertEqual(hyphen_user_id.username, "service") - - # Test username from user_id with multiple hyphens - multiple_hyphens = ChatbotsMqSubmindResponse( - userID="service-name-with-hyphens-123456", - messageText="Test message", - message_id="test_id" - ) - self.assertEqual(multiple_hyphens.username, "service-name-with-hyphens") - - # Test username from user_id without hyphen - no_hyphen = ChatbotsMqSubmindResponse( - userID="servicename", - messageText="Test message", - message_id="test_id" - ) - self.assertEqual(no_hyphen.username, "servicename") - - # Test existing username is preserved when both fields are provided - preserved_username = ChatbotsMqSubmindResponse( - userID="service-123456", - username="custom_name", - messageText="Test message", - message_id="test_id" - ) - self.assertEqual(preserved_username.username, "custom_name") - - # Test when user_id is None - with self.assertRaises(ValidationError) as e: - none_user_id = ChatbotsMqSubmindResponse( - userID=None, - messageText="Test message", - message_id="test_id" - ) - self.assertEqual(str(e), "user_id cannot be None") - - # Test missing required fields - with self.assertRaises(ValidationError): - ChatbotsMqSubmindResponse(messageText="Missing user_id", message_id="test_id") - - with self.assertRaises(ValidationError): - ChatbotsMqSubmindResponse(userID="test_user", message_id="test_id") - def test_chatbots_mq_response_type_adapter(self): from neon_data_models.models.api.mq.chatbots import ChatbotsMqResponse from neon_data_models.models.api.mq.chatbots import ChatbotsMqSubmindResponse, ChatbotsMqSavePrompt, ChatbotsMqNewPrompt - from neon_data_models.enum import CcaiControl from datetime import datetime, timezone current_time = datetime.now(tz=timezone.utc) # Test regular submind response regular_message = { - "userID": "submind1", + "userID": "submind1-uid_suffix", "messageText": "Regular response", "conversation_state": 2, "message_id": "test_message_id" @@ -1752,7 +1623,8 @@ def test_chatbots_mq_response_type_adapter(self): result = ChatbotsMqResponse(**regular_message) self.assertIsInstance(result, ChatbotsMqSubmindResponse) - self.assertEqual(result.user_id, "submind1") + self.assertEqual(result.user_id, "submind1-uid_suffix") + self.assertEqual(result.username, "submind1") self.assertEqual(result.message_text, "Regular response") # Test new prompt message @@ -1783,7 +1655,7 @@ def test_chatbots_mq_response_type_adapter(self): } alt_new_prompt_message = { - "nick": "proctor-ac18f03d0937490080c798d3b242ecd0", + "user_id": "proctor-ac18f03d0937490080c798d3b242ecd0", "bot_type": "proctor", "service_name": "proctor", "cid": "35d5dff220", @@ -1808,7 +1680,7 @@ def test_chatbots_mq_response_type_adapter(self): alt_response = ChatbotsMqResponse(**alt_new_prompt_message) self.assertIsInstance(alt_response, ChatbotsMqNewPrompt) - self.assertEqual(alt_response.user_id, alt_new_prompt_message['nick']) + self.assertEqual(alt_response.user_id, alt_new_prompt_message['user_id']) # Test save prompt message @@ -1839,7 +1711,7 @@ def test_chatbots_mq_response_type_adapter(self): "no_save": False } alt_save_prompt_message = { - "nick": "proctor-ac18f03d0937490080c798d3b242ecd0", + "user_id": "proctor-ac18f03d0937490080c798d3b242ecd0", "bot_type": "proctor", "service_name": "proctor", "cid": "35d5dff220", @@ -1873,7 +1745,7 @@ def test_chatbots_mq_response_type_adapter(self): "bound_service": "", "bot": "0", "messageText": "Why is testing important?", - "nick": "ca45d1ea45134523af7f" + "user_uid": "ca45d1ea45134523af7f" }, "is_active": True, "prompt_text": "Why is testing important?", @@ -1929,7 +1801,7 @@ def test_chatbots_mq_response_type_adapter(self): # Test with message_text vs messageText alternate_syntax = { - "userID": "submind1", + "userID": "submind1-uid_suffix", "message_text": "Using message_text instead of messageText", "conversation_state": 2, "message_id": "test_message_id" @@ -1946,7 +1818,7 @@ def test_chatbots_mq_submind_connection(self): # Test basic initialization current_time = datetime.now(tz=timezone.utc) valid_kwargs = { - "nick": "submind1", + "userID": "submind1", "time": current_time, "message_id": "test_message_id" } @@ -1994,7 +1866,7 @@ def test_chatbots_mq_submind_disconnection(self): # Test basic initialization valid_kwargs = { - "nick": "submind1", + "userID": "submind1", "message_id": "test_message_id" } @@ -2077,7 +1949,7 @@ def test_chatbots_mq_submind_conversation_ban(self): # Test basic initialization valid_kwargs = { - "nick": "submind1", + "user_id": "submind1", "cid": "conversation123", "message_id": "test_message_id" } @@ -2110,7 +1982,7 @@ def test_chatbots_mq_submind_global_ban(self): # Test basic initialization valid_kwargs = { - "nick": "submind1", + "user_id": "submind1", "message_id": "test_message_id" } @@ -2172,7 +2044,7 @@ def test_chatbots_mq_response_edge_cases(self): # Test with empty/minimal valid message minimal_message = { - "userID": "test_user", + "userID": "test_user-uid_suffix", "messageText": "", "message_id": "test_message_id" } @@ -2183,20 +2055,20 @@ def test_chatbots_mq_response_edge_cases(self): # Test with mixed aliased and non-aliased fields mixed_fields = { - "userID": "test_user", + "userID": "test_user-uid_suffix", "shout": "Using aliased shout field", "message_id": "test_message_id", - "nick": "nick_value" # This should be used for user_id too + # "nick": "nick_value" # This should not be used } result = ChatbotsMqResponse(**mixed_fields) self.assertIsInstance(result, ChatbotsMqSubmindResponse) self.assertEqual(result.message_text, "Using aliased shout field") - self.assertEqual(result.user_id, "test_user") # userID takes precedence over nick + self.assertEqual(result.username, "test_user") # userID takes precedence over nick # Test with malformed timestamps that should be converted timestamp_message = { - "userID": "test_user", + "userID": "test_user-uid_suffix", "messageText": "Timestamp test", "created_on": "1611234567", # String timestamp "time": 1611234567, # Integer timestamp @@ -2211,7 +2083,7 @@ def test_chatbots_mq_response_edge_cases(self): # Test with null/None fields that should be handled null_fields = { - "userID": "test_user", + "userID": "test_user-uid_suffix", "messageText": "Test with nulls", "message_id": "test_message_id", "sid": None, # Should be handled by the model validator