11import functools
22import logging
33from collections .abc import AsyncIterator
4- from typing import Final
4+ from typing import Final , Literal , NamedTuple
55
66from aiohttp import web
77from models_library .basic_types import IDStr
88from models_library .conversations import ConversationMessageType , ConversationUserType
9+ from models_library .groups import GroupID
910from models_library .rabbitmq_messages import WebserverChatbotRabbitMessage
1011from models_library .rest_ordering import OrderBy , OrderDirection
1112from pydantic import TypeAdapter
12- from servicelib .logging_utils import log_context
13+ from servicelib .logging_utils import log_catch , log_context , log_decorator
1314from servicelib .rabbitmq import RabbitMQClient
1415
1516from ..conversations import conversations_service
1617from ..conversations .errors import ConversationErrorNotFoundError
18+ from ..groups .api import list_group_members
1719from ..products import products_service
1820from ..rabbitmq import get_rabbitmq_client
21+ from ..users import users_service
22+ from ._client import Message
1923from .chatbot_service import get_chatbot_rest_client
2024
2125_logger = logging .getLogger (__name__ )
2832_CHATBOT_PROCESS_MESSAGE_TTL_IN_MS = 2 * 60 * 60 * 1000 # 2 hours
2933
3034
35+ class _Role (NamedTuple ):
36+ role : Literal ["user" , "assistant" , "developer" ]
37+ name : str | None = None
38+
39+
40+ _SUPPORT_ROLE_NAME : Final [str ] = "support-team-member"
41+
42+ _CHATBOT_INSTRUCTION_MESSAGE : Final [
43+ str
44+ ] = """
45+ This conversation takes place in the context of the {product} product. Only answer questions related to this product.
46+ The user '{support_role_name}' is a support team member and is assisting users of the {product} product
47+ with their inquiries. Help the user by providing answers to their questions. Make your answers concise and to the point.
48+ Address users by their name. Be friendly and accommodating.
49+ """
50+
51+
52+ async def _get_role (
53+ * ,
54+ app : web .Application ,
55+ message_gid : GroupID ,
56+ chatbot_primary_gid : GroupID ,
57+ support_group_primary_gids : set [GroupID ],
58+ ) -> _Role :
59+ if message_gid == chatbot_primary_gid :
60+ return _Role (role = "assistant" )
61+ if message_gid in support_group_primary_gids :
62+ return _Role (role = "user" , name = _SUPPORT_ROLE_NAME )
63+ user_id = await users_service .get_user_id_from_gid (app = app , primary_gid = message_gid )
64+ user_full_name = await users_service .get_user_fullname (app = app , user_id = user_id )
65+ return _Role (role = "user" , name = user_full_name ["first_name" ])
66+
67+
68+ @log_decorator (_logger , logging .DEBUG )
3169async def _process_chatbot_trigger_message (app : web .Application , data : bytes ) -> bool :
3270 rabbit_message = TypeAdapter (WebserverChatbotRabbitMessage ).validate_json (data )
3371 assert app # nosec
3472
35- with log_context (
36- _logger ,
37- logging .DEBUG ,
38- msg = f"Processing chatbot trigger message for conversation ID { rabbit_message .conversation .conversation_id } " ,
39- ):
40- _product_name = rabbit_message .conversation .product_name
41- _product = products_service .get_product (app , product_name = _product_name )
73+ with log_catch (logger = _logger , reraise = False ):
74+ product_name = rabbit_message .conversation .product_name
75+ product = products_service .get_product (app , product_name = product_name )
4276
43- if _product .support_chatbot_user_id is None :
77+ if product .support_chatbot_user_id is None :
4478 _logger .error (
4579 "Product %s does not have support_chatbot_user_id configured, cannot process chatbot message. (This should not happen)" ,
46- _product_name ,
80+ product_name ,
4781 )
4882 return True # return true to avoid re-processing
83+ support_group_primary_gids = set ()
84+ if product .support_standard_group_id is not None :
85+ support_group_primary_gids = {
86+ elm .primary_gid
87+ for elm in await list_group_members (
88+ app , product .support_standard_group_id
89+ )
90+ }
91+
92+ chatbot_primary_gid = await users_service .get_user_primary_group_id (
93+ app = app , user_id = product .support_chatbot_user_id
94+ )
4995
5096 # Get last 20 messages for the conversation ID
5197 with log_context (
5298 _logger ,
5399 logging .DEBUG ,
54100 msg = f"Listed messages for conversation ID { rabbit_message .conversation .conversation_id } " ,
55101 ):
56- _ , messages = await conversations_service .list_messages_for_conversation (
57- app = app ,
58- conversation_id = rabbit_message .conversation .conversation_id ,
59- offset = 0 ,
60- limit = 20 ,
61- order_by = OrderBy (field = IDStr ("created" ), direction = OrderDirection .DESC ),
102+ _ , messages_in_db = (
103+ await conversations_service .list_messages_for_conversation (
104+ app = app ,
105+ conversation_id = rabbit_message .conversation .conversation_id ,
106+ offset = 0 ,
107+ limit = 20 ,
108+ order_by = OrderBy (
109+ field = IDStr ("created" ), direction = OrderDirection .DESC
110+ ),
111+ )
62112 )
63113
64- _question_for_chatbot = ""
65- for inx , msg in enumerate (messages ):
66- if inx == 0 :
67- # Make last message stand out as the question
68- _question_for_chatbot += (
69- "User last message: \n "
70- f"{ msg .content .strip ()} \n \n "
71- "Previous messages in the conversation: \n "
72- )
73- else :
74- _question_for_chatbot += f"{ msg .content .strip ()} \n "
114+ messages = []
115+ for msg in messages_in_db :
116+ role = await _get_role (
117+ app = app ,
118+ message_gid = msg .user_group_id ,
119+ chatbot_primary_gid = chatbot_primary_gid ,
120+ support_group_primary_gids = support_group_primary_gids ,
121+ )
122+ messages .append (
123+ Message (role = role .role , name = role .name , content = msg .content )
124+ )
125+ context_message = Message (
126+ role = "developer" ,
127+ content = _CHATBOT_INSTRUCTION_MESSAGE .format (
128+ product = product_name ,
129+ support_role_name = _SUPPORT_ROLE_NAME ,
130+ ),
131+ )
132+ messages .append (context_message )
75133
76134 # Talk to the chatbot service
77-
78135 with log_context (
79136 _logger ,
80137 logging .DEBUG ,
81138 msg = f"Asking question from chatbot conversation ID { rabbit_message .conversation .conversation_id } " ,
82139 ):
83140 chatbot_client = get_chatbot_rest_client (app )
84- chat_response = await chatbot_client .ask_question ( _question_for_chatbot )
141+ response_message = await chatbot_client .send ( messages )
85142
86143 try :
87144 with log_context (
@@ -92,10 +149,10 @@ async def _process_chatbot_trigger_message(app: web.Application, data: bytes) ->
92149 await conversations_service .create_support_message (
93150 app = app ,
94151 product_name = rabbit_message .conversation .product_name ,
95- user_id = _product .support_chatbot_user_id ,
152+ user_id = product .support_chatbot_user_id ,
96153 conversation_user_type = ConversationUserType .CHATBOT_USER ,
97154 conversation = rabbit_message .conversation ,
98- content = chat_response . answer ,
155+ content = response_message . content ,
99156 type_ = ConversationMessageType .MESSAGE ,
100157 )
101158 except ConversationErrorNotFoundError :
0 commit comments