Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ class MyGroupsGet(OutputSchema):
description="Group ID of the app support team or None if no support is defined for this product"
),
] = None
chatbot: Annotated[
GroupGetBase | None,
Field(
description="Group ID of the support chatbot user or None if no chatbot is defined for this product"
),
] = None

model_config = ConfigDict(
json_schema_extra={
Expand Down Expand Up @@ -245,6 +251,12 @@ class MyGroupsGet(OutputSchema):
"description": "The support team of the application",
"thumbnail": "https://placekitten.com/15/15",
},
"chatbot": {
"gid": "6",
"label": "Chatbot User",
"description": "The chatbot user of the application",
"thumbnail": "https://placekitten.com/15/15",
},
}
}
)
Expand All @@ -255,6 +267,7 @@ def from_domain_model(
groups_by_type: GroupsByTypeTuple,
my_product_group: tuple[Group, AccessRightsDict] | None,
product_support_group: Group | None,
product_chatbot_primary_group: Group | None,
) -> Self:
assert groups_by_type.primary # nosec
assert groups_by_type.everyone # nosec
Expand All @@ -277,6 +290,13 @@ def from_domain_model(
if product_support_group
else None
),
chatbot=(
GroupGetBase.model_validate(
GroupGetBase.dump_basic_group_data(product_chatbot_primary_group)
)
if product_chatbot_primary_group
else None
),
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def from_domain_model(
my_product_group: tuple[Group, AccessRightsDict] | None,
my_preferences: AggregatedPreferences,
my_support_group: Group | None,
my_chatbot_user_group: Group | None,
profile_contact: MyProfileAddressGet | None = None,
) -> Self:
profile_data = remap_keys(
Expand All @@ -194,7 +195,10 @@ def from_domain_model(
return cls(
**profile_data,
groups=MyGroupsGet.from_domain_model(
my_groups_by_type, my_product_group, my_support_group
my_groups_by_type,
my_product_group,
my_support_group,
my_chatbot_user_group,
),
preferences=my_preferences,
contact=profile_contact,
Expand Down
4 changes: 4 additions & 0 deletions packages/models-library/src/models_library/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class ConversationType(StrAutoEnum):
auto() # Something like sticky note, can be located anywhere in the pipeline UI
)
SUPPORT = auto() # Support conversation
SUPPORT_CALL = auto() # Support call conversation

def is_support_type(self) -> bool:
return self in {ConversationType.SUPPORT, ConversationType.SUPPORT_CALL}


class ConversationMessageType(StrAutoEnum):
Expand Down
8 changes: 8 additions & 0 deletions packages/models-library/src/models_library/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
"type": "standard",
"thumbnail": None,
}
chatbot: JsonDict = {
"gid": 5,
"name": "Chatbot",
"description": "chatbot group",
"type": "primary",
"thumbnail": None,
}

schema.update(
{
Expand All @@ -86,6 +93,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
organization,
product,
support,
chatbot,
]
}
)
Expand Down
5 changes: 4 additions & 1 deletion packages/models-library/tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from pydantic import TypeAdapter


@pytest.mark.parametrize("with_chatbot_user_group", [True, False])
@pytest.mark.parametrize("with_support_group", [True, False])
@pytest.mark.parametrize("with_standard_groups", [True, False])
def test_adapter_from_model_to_schema(
with_support_group: bool, with_standard_groups: bool
with_support_group: bool, with_standard_groups: bool, with_chatbot_user_group: bool
):
my_profile = MyProfile.model_validate(MyProfile.model_json_schema()["example"])

Expand All @@ -31,6 +32,7 @@ def test_adapter_from_model_to_schema(
)

my_support_group = groups[4]
my_chatbot_user_group = groups[5]

my_preferences = {"foo": Preference(default_value=3, value=1)}

Expand All @@ -40,4 +42,5 @@ def test_adapter_from_model_to_schema(
my_product_group,
my_preferences,
my_support_group if with_support_group else None,
my_chatbot_user_group if with_chatbot_user_group else None,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Add SUPPORT_CALL conversationt ype
Revision ID: 5756d9282a0a
Revises: ff13501db935
Create Date: 2025-10-21 13:40:20.182151+00:00
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "5756d9282a0a"
down_revision = "ff13501db935"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("products", "base_url", existing_type=sa.VARCHAR(), nullable=False)

# op.execute("ALTER TYPE conversationtype ADDmake VALUE 'SUPPORT_CALL'")

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("products", "base_url", existing_type=sa.VARCHAR(), nullable=True)
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ConversationType(enum.Enum):
PROJECT_STATIC = "PROJECT_STATIC" # Static conversation for the project
PROJECT_ANNOTATION = "PROJECT_ANNOTATION" # Something like sticky note, can be located anywhere in the pipeline UI
SUPPORT = "SUPPORT" # Support conversation
SUPPORT_CALL = "SUPPORT_CALL" # Support call conversation


conversations = sa.Table(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ def _get_group_id(con: sa.engine.Connection) -> int:
return result.first()[0]

def _insert_product(con: sa.engine.Connection, group_id: int, name: str) -> int:
product_config = {"name": name, "group_id": group_id, "host_regex": ""}
product_config = {
"name": name,
"group_id": group_id,
"host_regex": "",
"base_url": "http://localhost",
}
con.execute(products.insert().values(product_config))

def _insert_groups_extra_properties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10520,6 +10520,7 @@ components:
- PROJECT_STATIC
- PROJECT_ANNOTATION
- SUPPORT
- SUPPORT_CALL
title: ConversationType
CountryInfoDict:
properties:
Expand Down Expand Up @@ -13818,6 +13819,12 @@ components:
- type: 'null'
description: Group ID of the app support team or None if no support is defined
for this product
chatbot:
anyOf:
- $ref: '#/components/schemas/GroupGetBase'
- type: 'null'
description: Group ID of the support chatbot user or None if no chatbot
is defined for this product
type: object
required:
- me
Expand All @@ -13832,6 +13839,11 @@ components:
description: Open to all users
gid: 1
label: All
chatbot:
description: The chatbot user of the application
gid: '6'
label: Chatbot User
thumbnail: https://placekitten.com/15/15
me:
accessRights:
delete: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def _process_chatbot_trigger_message(app: web.Application, data: bytes) ->
limit=20,
order_by=OrderBy(field=IDStr("created"), direction=OrderDirection.DESC),
)

_question_for_chatbot = ""
for inx, msg in enumerate(messages):
if inx == 0:
Expand Down Expand Up @@ -77,7 +78,7 @@ async def _process_chatbot_trigger_message(app: web.Application, data: bytes) ->
type_=ConversationMessageType.MESSAGE,
)
except ConversationErrorNotFoundError:
_logger.debug(
_logger.warning(
"Can not create a support message as conversation %s was not found",
rabbit_message.conversation.conversation_id,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
ConversationMessageID,
ConversationMessagePatchDB,
ConversationMessageType,
ConversationType,
)
from models_library.rest_pagination import (
Page,
Expand Down Expand Up @@ -72,7 +71,7 @@ async def create_conversation_message(request: web.Request):
_conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if _conversation.type != ConversationType.SUPPORT:
if _conversation.type.is_support_type() is False:
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
Expand Down Expand Up @@ -116,7 +115,7 @@ async def list_conversation_messages(request: web.Request):
_conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if _conversation.type != ConversationType.SUPPORT:
if _conversation.type.is_support_type() is False:
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
Expand Down Expand Up @@ -170,7 +169,7 @@ async def get_conversation_message(request: web.Request):
_conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if _conversation.type != ConversationType.SUPPORT:
if _conversation.type.is_support_type() is False:
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
Expand Down Expand Up @@ -208,7 +207,7 @@ async def update_conversation_message(request: web.Request):
_conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if _conversation.type != ConversationType.SUPPORT:
if _conversation.type.is_support_type() is False:
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
Expand Down Expand Up @@ -248,7 +247,7 @@ async def delete_conversation_message(request: web.Request):
_conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if _conversation.type != ConversationType.SUPPORT:
if _conversation.type.is_support_type() is False:
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class _ListConversationsQueryParams(PageQueryParameters):
@field_validator("type")
@classmethod
def validate_type(cls, value):
if value is not None and value != ConversationType.SUPPORT:
if value is not None and value.is_support_type() is False:
msg = "Only support type conversations are allowed"
raise ValueError(msg)
return value
Expand All @@ -70,7 +70,7 @@ async def create_conversation(request: web.Request):
req_ctx = AuthenticatedRequestContext.model_validate(request)
body_params = await parse_request_body_as(_ConversationsCreateBodyParams, request)
# Ensure only support conversations are allowed
if body_params.type != ConversationType.SUPPORT:
if body_params.type.is_support_type() is False:
raise_unsupported_type(body_params.type)

_extra_context = body_params.extra_context or {}
Expand Down Expand Up @@ -101,7 +101,7 @@ async def list_conversations(request: web.Request):
query_params = parse_request_query_parameters_as(
_ListConversationsQueryParams, request
)
if query_params.type != ConversationType.SUPPORT:
if query_params.type.is_support_type() is False:
raise_unsupported_type(query_params.type)

total, conversations = (
Expand Down Expand Up @@ -146,7 +146,7 @@ async def get_conversation(request: web.Request):
conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if conversation.type != ConversationType.SUPPORT:
if conversation.type.is_support_type() is False:
raise_unsupported_type(conversation.type)

conversation, _ = await _conversation_service.get_support_conversation_for_user(
Expand Down Expand Up @@ -175,7 +175,7 @@ async def update_conversation(request: web.Request):
conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if conversation.type != ConversationType.SUPPORT:
if conversation.type.is_support_type() is False:
raise_unsupported_type(conversation.type)

await _conversation_service.get_support_conversation_for_user(
Expand Down Expand Up @@ -210,7 +210,7 @@ async def delete_conversation(request: web.Request):
conversation = await _conversation_service.get_conversation(
request.app, conversation_id=path_params.conversation_id
)
if conversation.type != ConversationType.SUPPORT:
if conversation.type.is_support_type() is False:
raise_unsupported_type(conversation.type)

# Only support conversation creator can delete conversation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ConversationMessagePatchDB,
ConversationMessageType,
ConversationPatchDB,
ConversationType,
ConversationUserType,
)
from models_library.products import ProductName
Expand Down Expand Up @@ -218,7 +219,7 @@ async def _trigger_chatbot_processing(
conversation=conversation,
last_message_id=last_message_id,
)
_logger.debug(
_logger.info(
"Publishing chatbot processing message with conversation id %s and last message id %s.",
conversation.conversation_id,
last_message_id,
Expand Down Expand Up @@ -266,7 +267,7 @@ async def create_support_message(
return message

if is_first_message or conversation.fogbugz_case_id is None:
_logger.debug(
_logger.info(
"Support settings available, this is first message, creating FogBugz case for Conversation ID: %s",
conversation.conversation_id,
)
Expand Down Expand Up @@ -298,7 +299,7 @@ async def create_support_message(
)
else:
assert not is_first_message # nosec
_logger.debug(
_logger.info(
"Support settings available, but this is NOT the first message, so we need to reopen a FogBugz case. Conversation ID: %s",
conversation.conversation_id,
)
Expand Down Expand Up @@ -329,7 +330,8 @@ async def create_support_message(

if (
product.support_chatbot_user_id
and conversation_user_type == ConversationUserType.CHATBOT_USER
and conversation.type == ConversationType.SUPPORT
and conversation_user_type == ConversationUserType.REGULAR_USER
):
# If enabled, ask Chatbot to analyze the message history and respond
await _trigger_chatbot_processing(
Expand Down
Loading
Loading