From 101adcdc1b63ccd9a5928885fea86d653a6f1a7c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 20 Dec 2023 20:58:07 +0100 Subject: [PATCH 01/17] dirty --- ragna/deploy/_ui/central_view.py | 358 +++++++++++++++++-------------- 1 file changed, 194 insertions(+), 164 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 76fe6cf9..cd84cb11 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -1,4 +1,4 @@ -import uuid +from __future__ import annotations import panel as pn import param @@ -213,6 +213,45 @@ def chat_entry_value_renderer(cls, txt, role): ) +class NewChatMessage(pn.chat.ChatMessage): + def __init__( + self, show_reaction_icons=False, show_user=False, show_copy_icon=False, **kwargs + ): + super().__init__( + show_reaction_icons=show_reaction_icons, + show_user=show_user, + show_copy_icon=show_copy_icon, + **kwargs, + ) + + @classmethod + def from_system(cls, content: str): + return cls(object=content, user="system", avatar="imgs/ragna_logo.svg") + + @classmethod + def from_user(cls, content: str, *, user: str) -> NewChatMessage: + pass + + @staticmethod + def _assistant_avatar(assistant: str) -> str: + if assistant.startswith("Ragna"): + return "imgs/ragna_logo.svg" + elif assistant.startswith("OpenAI"): + model = assistant.split("/", 1)[-1] + if model.startswith("gpt-3"): + return pn.chat.message.GPT_3_LOGO + elif model.startswith("gpt-4"): + return pn.chat.message.GPT_4_LOGO + + return "" + + @classmethod + def from_assistant(cls, content: str, *, assistant: str) -> NewChatMessage: + return cls( + object=content, user="assistant", avatar=cls._assistant_avatar(assistant) + ) + + class RagnaChatInterface(pn.chat.ChatInterface): def __init__(self, *objects, **params): super().__init__(*objects, **params) @@ -236,7 +275,7 @@ def _update_placeholder(self): class CentralView(pn.viewable.Viewer): current_chat = param.ClassSelector(class_=dict, default=None) - trigger_scroll_to_latest = param.Integer(default=0) + # trigger_scroll_to_latest = param.Integer(default=0) def __init__(self, api_wrapper, **params): super().__init__(**params) @@ -366,37 +405,29 @@ async def chat_callback( self.current_chat["messages"].append(answer) - yield { - "user": "Ragna", - "avatar": "🤖", - "value": answer["content"], - } - except Exception as e: - print(e) - - yield { - "user": "Ragna", - "avatar": RagnaChatMessage.get_avatar("system", None), - "value": "Sorry, something went wrong. If this problem persists, please contact your administrator.", - } - - def get_chat_messages(self): - chat_entries = [] - - if self.current_chat is not None: - assistant = self.current_chat["metadata"]["assistant"] - # FIXME: user needs to be dynamic based on the username that was logged in with - username = "User" - - for m in self.current_chat["messages"]: - chat_entry = RagnaChatMessage( - m, - username if m["role"] == "user" else assistant, - on_click_source_info_callback=self.on_click_source_info_wrapper, - ) - chat_entries.append(chat_entry) + yield NewChatMessage.from_assistant(answer["content"], assistant=user) + except Exception: + yield NewChatMessage.from_system( + "Sorry, something went wrong. If this problem persists, please contact your administrator." + ) - return chat_entries + # def get_chat_messages(self): + # chat_entries = [] + # + # if self.current_chat is not None: + # assistant = self.current_chat["metadata"]["assistant"] + # # FIXME: user needs to be dynamic based on the username that was logged in with + # username = "User" + # + # for m in self.current_chat["messages"]: + # chat_entry = RagnaChatMessage( + # m, + # username if m["role"] == "user" else assistant, + # on_click_source_info_callback=self.on_click_source_info_wrapper, + # ) + # chat_entries.append(chat_entry) + # + # return chat_entries @pn.depends("current_chat") def chat_interface(self): @@ -405,7 +436,7 @@ def chat_interface(self): chat_interface = RagnaChatInterface( callback=self.chat_callback, - callback_user="Ragna", + callback_user=self.current_chat["metadata"]["assistant"], show_rerun=False, show_undo=False, show_clear=False, @@ -413,140 +444,139 @@ def chat_interface(self): view_latest=True, sizing_mode="stretch_width", auto_send_types=[], - widgets=[ - pn.widgets.TextInput( - placeholder="Ask Ragna...", - stylesheets=[ - """:host input[type="text"] { - border:none !important; - box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2); - padding: 10px 10px 10px 15px; - } - - :host input[type="text"]:focus { - box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3); - } - - """ - ], - ) - ], - renderers=[ - lambda txt: RagnaChatMessage.chat_entry_value_renderer(txt, role=None) - ], - message_params={ - "show_reaction_icons": False, - "show_user": False, - "show_copy_icon": False, - "show_timestamp": False, - # the proper avatar for the assistant is not when replacing the default ChatMessage objects - # with RagnaChatMessage objects. - "avatar_lookup": lambda user: "👤" if user == "User" else None, - }, - ) - - chat_interface._card.stylesheets += [ - """ - - :host { - border:none !important; - } - - .chat-feed-log { - padding-right: 18%; - margin-left: 18% ; - padding-top:25px !important; - - } - - .chat-interface-input-container { - margin-left:19%; - margin-right:20%; - margin-bottom: 20px; - } - - - """ - ] - - """ - By default, each new message is a ChatMessage object. - But for new messages from the AI, we want to have a RagnaChatMessage, that contains the msg data, the sources, etc. - I haven't found a better way than to watch for the `objects` param of chat_interface, - and replace the ChatMessage objects with RagnaChatMessage object. - We do it only for the new messages from the rag, not for the existing messages, neither for the messages from the user. - """ - - def messages_changed(event): - if len(chat_interface.objects) != len(self.current_chat["messages"]): - return - - assistant = self.current_chat["metadata"]["assistant"] - # FIXME: user needs to be dynamic based on the username that was logged in with - username = "User" - - needs_refresh = False - for i in range(len(chat_interface.objects)): - msg = chat_interface.objects[i] - - if not isinstance(msg, RagnaChatMessage) and msg.user != "User": - chat_interface.objects[i] = RagnaChatMessage( - self.current_chat["messages"][i], - username - if self.current_chat["messages"][i]["role"] == "user" - else assistant, - on_click_source_info_callback=self.on_click_source_info_wrapper, - ) - msg = chat_interface.objects[i] - needs_refresh = True - - if needs_refresh: - chat_interface._chat_log.param.trigger("objects") - - chat_interface.param.watch( - messages_changed, - ["objects"], - ) - - # Here, we build a list of RagnaChatMessages from the existing messages of this chat, - # and set them as the content of the chat interface - chat_interface.objects = self.get_chat_messages() - - # Now that setting all the objects is done, we can watch the change of objects, - # ie new messages being appended to the chat. When that happens, - # make sure we scroll to the latest msg. - chat_interface.param.watch( - lambda event: self.param.trigger("trigger_scroll_to_latest"), - ["objects"], + # widgets=[ + # pn.widgets.TextInput( + # placeholder="Ask Ragna...", + # stylesheets=[ + # """:host input[type="text"] { + # border:none !important; + # box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2); + # padding: 10px 10px 10px 15px; + # } + # + # :host input[type="text"]:focus { + # box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3); + # } + # + # """ + # ], + # ) + # ], + # renderers=[ + # lambda txt: RagnaChatMessage.chat_entry_value_renderer(txt, role=None) + # ], + # message_params={ + # "show_reaction_icons": False, + # "show_user": False, + # "show_copy_icon": False, + # "show_timestamp": True, + # }, ) + # + # chat_interface._card.stylesheets += [ + # """ + # + # :host { + # border:none !important; + # } + # + # .chat-feed-log { + # padding-right: 18%; + # margin-left: 18% ; + # padding-top:25px !important; + # + # } + # + # .chat-interface-input-container { + # margin-left:19%; + # margin-right:20%; + # margin-bottom: 20px; + # } + # + # + # """ + # ] + # + # """ + # By default, each new message is a ChatMessage object. + # But for new messages from the AI, we want to have a RagnaChatMessage, that contains the msg data, the sources, etc. + # I haven't found a better way than to watch for the `objects` param of chat_interface, + # and replace the ChatMessage objects with RagnaChatMessage object. + # We do it only for the new messages from the rag, not for the existing messages, neither for the messages from the user. + # """ + # + # def messages_changed(event): + # if len(chat_interface.objects) != len(self.current_chat["messages"]): + # return + # + # assistant = self.current_chat["metadata"]["assistant"] + # # FIXME: user needs to be dynamic based on the username that was logged in with + # username = "User" + # + # needs_refresh = False + # for i in range(len(chat_interface.objects)): + # msg = chat_interface.objects[i] + # + # if not isinstance(msg, RagnaChatMessage) and msg.user != "User": + # chat_interface.objects[i] = RagnaChatMessage( + # self.current_chat["messages"][i], + # username + # if self.current_chat["messages"][i]["role"] == "user" + # else assistant, + # on_click_source_info_callback=self.on_click_source_info_wrapper, + # ) + # msg = chat_interface.objects[i] + # needs_refresh = True + # + # if needs_refresh: + # chat_interface._chat_log.param.trigger("objects") + # + # chat_interface.param.watch( + # messages_changed, + # ["objects"], + # ) + # + # # Here, we build a list of RagnaChatMessages from the existing messages of this chat, + # # and set them as the content of the chat interface + # chat_interface.objects = self.get_chat_messages() + # + # # Now that setting all the objects is done, we can watch the change of objects, + # # ie new messages being appended to the chat. When that happens, + # # make sure we scroll to the latest msg. + # chat_interface.param.watch( + # lambda event: self.param.trigger("trigger_scroll_to_latest"), + # ["objects"], + # ) return chat_interface - @pn.depends("current_chat", "trigger_scroll_to_latest") - def scroll_to_latest_fix(self): - """ - This snippet needs to be re-rendered many times so the scroll-to-latest happens: - - each time the current chat changes, hence the pn.depends on current_chat - - each time a message is appended to the chat, hence the pn.depends on trigger_scroll_to_latest. - trigger_scroll_to_latest is triggered in the chat_interface method, when chat_interface.objects changes. - - Twist : the HTML script node needs to have a different ID each time it is rendered, - otherwise the browser doesn't re-render it / doesn't execute the JS part. - Hence the random ID. - """ - - random_id = str(uuid.uuid4()) - - return pn.pane.HTML( - """""".replace("{{RANDOM_ID}}", random_id) - ) + # @pn.depends("current_chat", "trigger_scroll_to_latest") + # def scroll_to_latest_fix(self): + # """ + # This snippet needs to be re-rendered many times so the scroll-to-latest happens: + # - each time the current chat changes, hence the pn.depends on current_chat + # - each time a message is appended to the chat, hence the pn.depends on trigger_scroll_to_latest. + # trigger_scroll_to_latest is triggered in the chat_interface method, when chat_interface.objects changes. + # + # Twist : the HTML script node needs to have a different ID each time it is rendered, + # otherwise the browser doesn't re-render it / doesn't execute the JS part. + # Hence the random ID. + # """ + # + # random_id = str(uuid.uuid4()) + # + # return pn.pane.HTML( + # """""".replace( + # "{{RANDOM_ID}}", random_id + # ) + # ) @pn.depends("current_chat") def header(self): @@ -657,7 +687,7 @@ def __panel__(self): self.main_column = pn.Column( self.header, self.chat_interface, - self.scroll_to_latest_fix, + # self.scroll_to_latest_fix, sizing_mode="stretch_width", stylesheets=[ """ :host { From 716373b2e6d51c9a74c8065d2cf1414b87115981 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 21 Dec 2023 11:19:02 +0100 Subject: [PATCH 02/17] partially working --- ragna/deploy/_ui/central_view.py | 530 +++++++++++++------------------ ragna/deploy/_ui/styles.py | 8 +- 2 files changed, 227 insertions(+), 311 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index cd84cb11..8e1dee39 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable, Literal, Optional + import panel as pn import param from panel.reactive import ReactiveHTML @@ -72,8 +74,10 @@ } """ +# subclass pn.chat.icon.ChatCopyIcon + -class RagnaChatCopyIcon(ReactiveHTML): +class CopyToClipboardButton(ReactiveHTML): title = param.String(default=None, doc="The title of the button ") value = param.String(default=None, doc="The text to copy to the clipboard.") @@ -117,160 +121,122 @@ class RagnaChatCopyIcon(ReactiveHTML): class RagnaChatMessage(pn.chat.ChatMessage): - msg_data = param.Dict(default={}) + role: str = param.Selector(objects=["system", "user", "assistant"]) + sources = param.List(allow_None=True) on_click_source_info_callback = param.Callable(default=None) - def __init__(self, msg_data, user, on_click_source_info_callback=None, **kwargs): - self.role = msg_data["role"] - - params = { - "msg_data": msg_data, - # user is the name of the assistant (eg 'Ragna/DemoAssistant') - # or the name of the user, depending on the role - "user": user, - "on_click_source_info_callback": on_click_source_info_callback, - "object": msg_data["content"], - "renderers": [ - lambda txt: RagnaChatMessage.chat_entry_value_renderer( - txt, role=self.role - ) - ], - "show_timestamp": False, - "show_reaction_icons": False, - "show_copy_icon": False, - "show_user": False, - } - - params["avatar"] = RagnaChatMessage.get_avatar(self.role, user) - - super().__init__(**(params | kwargs)) + def __init__( + self, + content: str, + *, + role: Literal["system", "user", "assistant"], + user: str, + sources: Optional[list[dict]] = None, + on_click_source_info_callback: Optional[Callable] = None, + ): + super().__init__( + object=content, + role=role, + user=user, + sources=sources, + on_click_source_info_callback=on_click_source_info_callback, + show_reaction_icons=False, + show_user=False, + show_copy_icon=False, + renderers=[self._render], + ) - self.update_css_classes() - self.chat_copy_icon.visible = False + if self.sources: + self._update_object_pane() - if self.role == "assistant": - source_info_button = pn.widgets.Button( - name="Source Info", - icon="info-circle", - stylesheets=[ - ui.CHAT_INTERFACE_CUSTOM_BUTTON, - ], + def _update_object_pane(self, event=None): + super()._update_object_pane(event) + if self.sources: + self._object_panel = self._center_row[0] = pn.Column( + self._object_panel, self._copy_and_source_view_buttons() ) - source_info_button.on_click(self.trigger_on_click_source_info_callback) - - copy_button = RagnaChatCopyIcon( + def _copy_and_source_view_buttons(self) -> pn.Row: + return pn.Row( + CopyToClipboardButton( value=self.object, title="Copy", stylesheets=[ ui.CHAT_INTERFACE_CUSTOM_BUTTON, ], - ) - - self._composite[1].append(pn.Row(copy_button, source_info_button, height=0)) - - def trigger_on_click_source_info_callback(self, event): - if self.on_click_source_info_callback is not None: - self.on_click_source_info_callback(event, self) - - def update_css_classes(self): - role = self.msg_data["role"] if "role" in self.msg_data else None - self.css_classes = ["chat-entry", f"chat-entry-{role}"] - - @classmethod - def get_avatar(cls, role, user) -> str: - if role == "system": - return "imgs/ragna_logo.svg" - elif role == "user": - # FIXME: user needs to be dynamic based on the username that was logged in with - return "👤" - elif role == "assistant": - # FIXME: This needs to represent the assistant somehow - if user == "Ragna/DemoAssistant": - return "imgs/ragna_logo.svg" - elif user.startswith("OpenAI/gpt-3.5"): - return pn.chat.message.GPT_3_LOGO - elif user == "OpenAI/gpt-4": - return pn.chat.message.GPT_4_LOGO - - return "🤖" - - # should never happen - return "?" - - @classmethod - def chat_entry_value_renderer(cls, txt, role): - markdown_css_classes = [] - if role is not None: - markdown_css_classes = [ - f"chat-entry-{role}", - ] - - return pn.pane.Markdown( - txt, - css_classes=markdown_css_classes, - stylesheets=[markdown_table_stylesheet], - ) - - -class NewChatMessage(pn.chat.ChatMessage): - def __init__( - self, show_reaction_icons=False, show_user=False, show_copy_icon=False, **kwargs - ): - super().__init__( - show_reaction_icons=show_reaction_icons, - show_user=show_user, - show_copy_icon=show_copy_icon, - **kwargs, + ), + pn.widgets.Button( + name="Source Info", + icon="info-circle", + stylesheets=[ + ui.CHAT_INTERFACE_CUSTOM_BUTTON, + ], + on_click=lambda event: self.on_click_source_info_callback( + event, self.sources + ), + ), + height=0, ) - @classmethod - def from_system(cls, content: str): - return cls(object=content, user="system", avatar="imgs/ragna_logo.svg") + def avatar_lookup(self, user: str) -> str: + if self.role == "system": + return "imgs/ragna_logo.svg" + elif self.role == "user": + return user[0].upper() - @classmethod - def from_user(cls, content: str, *, user: str) -> NewChatMessage: - pass + try: + organization, model = user.split("/") + except ValueError: + organization = None + model = user - @staticmethod - def _assistant_avatar(assistant: str) -> str: - if assistant.startswith("Ragna"): + if organization == "Ragna": return "imgs/ragna_logo.svg" - elif assistant.startswith("OpenAI"): - model = assistant.split("/", 1)[-1] + elif organization == "OpenAI": if model.startswith("gpt-3"): - return pn.chat.message.GPT_3_LOGO + return "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/1024px-ChatGPT_logo.svg.png?20230318122128" elif model.startswith("gpt-4"): - return pn.chat.message.GPT_4_LOGO - - return "" + return "https://upload.wikimedia.org/wikipedia/commons/a/a4/GPT-4.png" + elif organization == "Anthropic": + return "https://upload.wikimedia.org/wikipedia/commons/1/14/Anthropic.png" + else: + return model[0].upper() - @classmethod - def from_assistant(cls, content: str, *, assistant: str) -> NewChatMessage: - return cls( - object=content, user="assistant", avatar=cls._assistant_avatar(assistant) + def _render(self, content: str) -> pn.viewable.Viewable: + return pn.pane.Markdown( + content, + css_classes=["chat-message", f"chat-message-{self.role}"], + stylesheets=[markdown_table_stylesheet], ) class RagnaChatInterface(pn.chat.ChatInterface): - def __init__(self, *objects, **params): - super().__init__(*objects, **params) - - @param.depends("placeholder_text", watch=True, on_init=True) - def _update_placeholder(self): - loading_avatar = RagnaChatMessage.get_avatar("system", None) - - self._placeholder = RagnaChatMessage( - { - "role": "system", - "content": ui.message_loading_indicator, - }, - user=" ", - show_timestamp=False, - avatar=loading_avatar, - reaction_icons={}, - show_copy_icon=False, - ) + # FIXME: we need this! + # @param.depends("placeholder_text", watch=True, on_init=True) + # def _update_placeholder(self): + # loading_avatar = RagnaChatMessage.get_avatar("system", None) + # + # self._placeholder = NewChatMessage( + # { + # "role": "system", + # "content": ui.message_loading_indicator, + # }, + # user=" ", + # show_timestamp=False, + # avatar=loading_avatar, + # reaction_icons={}, + # show_copy_icon=False, + # ) + + def _build_message(self, *args, **kwargs) -> RagnaChatMessage | None: + message = super()._build_message(*args, **kwargs) + if message is None: + return None + + # We only ever hit this function for user inputs, since we control the + # generation of the system and assistant messages manually. Thus, we can + # unconditionally create a user message here. + return RagnaChatMessage(message.object, role="user", user=self.user) class CentralView(pn.viewable.Viewer): @@ -280,6 +246,8 @@ class CentralView(pn.viewable.Viewer): def __init__(self, api_wrapper, **params): super().__init__(**params) + # FIXME: make this dynamic from the login + self.user = "RagnaUser" self.api_wrapper = api_wrapper self.chat_info_button = pn.widgets.Button( # The name will be filled at runtime in self.header @@ -292,22 +260,20 @@ def __init__(self, api_wrapper, **params): self.on_click_chat_info = None def on_click_chat_info_wrapper(self, event): - if self.on_click_chat_info is not None: - pills = "".join( - [ - f"""
{d['name']}
""" - for d in self.current_chat["metadata"]["documents"] - ] - ) + if self.on_click_chat_info is None: + return - grid_height = len(self.current_chat["metadata"]["documents"]) // 3 + pills = "".join( + [ + f"""
{d['name']}
""" + for d in self.current_chat["metadata"]["documents"] + ] + ) - advanced_config_md = "\n".join( - f"- **{key.replace('_', ' ').title()}**: {value}" - for key, value in self.current_chat["metadata"]["params"].items() - ) + grid_height = len(self.current_chat["metadata"]["documents"]) // 3 - markdown = [ + markdown = "\n".join( + [ "To change configurations, start a new chat.\n", "**Uploaded Files**", f"
{pills}

\n\n", @@ -318,117 +284,105 @@ def on_click_chat_info_wrapper(self, event): "**Assistant**", f"""{self.current_chat['metadata']['assistant']}\n""", "**Advanced configuration**", - advanced_config_md, + *[ + f"- **{key.replace('_', ' ').title()}**: {value}" + for key, value in self.current_chat["metadata"]["params"].items() + ], ] + ) - markdown = "\n".join(markdown) - - self.on_click_chat_info( - event, - "Chat Config", - [ - pn.pane.Markdown( - markdown, - dedent=True, - # debug - # pn.pane.Markdown(f"Chat ID: {self.current_chat['id']}"), - stylesheets=ui.stylesheets( - (":host", {"width": "100%"}), - ( - ".pills_list", - { - # "background-color": "gold", - "display": "grid", - "grid-auto-flow": "row", - "row-gap": "10px", - "grid-template": f"repeat({grid_height}, 1fr) / repeat(3, 1fr)", - "max-height": "200px", - "overflow": "scroll", - }, - ), - ( - ".chat_document_pill", - { - "background-color": "rgb(241,241,241)", - "margin-left": "5px", - "margin-right": "5px", - "padding": "5px 15px", - "border-radius": "10px", - "color": "var(--accent-color)", - "width": "fit-content", - "grid-column": "span 1", - }, - ), - ("ul", {"list-style-type": "none"}), + self.on_click_chat_info( + event, + "Chat Info", + [ + pn.pane.Markdown( + markdown, + dedent=True, + stylesheets=ui.stylesheets( + (":host", {"width": "100%"}), + ( + ".pills_list", + { + # "background-color": "gold", + "display": "grid", + "grid-auto-flow": "row", + "row-gap": "10px", + "grid-template": f"repeat({grid_height}, 1fr) / repeat(3, 1fr)", + "max-height": "200px", + "overflow": "scroll", + }, ), + ( + ".chat_document_pill", + { + "background-color": "rgb(241,241,241)", + "margin-left": "5px", + "margin-right": "5px", + "padding": "5px 15px", + "border-radius": "10px", + "color": "var(--accent-color)", + "width": "fit-content", + "grid-column": "span 1", + }, + ), + ("ul", {"list-style-type": "none"}), ), - ], - ) - - def on_click_source_info_wrapper(self, event, msg): - if self.on_click_chat_info is not None: - markdown = "This response was generated using the following data from the uploaded files:
\n" + ), + ], + ) - for i in range(len(msg.msg_data["sources"])): - source = msg.msg_data["sources"][i] + def on_click_source_info_wrapper(self, event, sources): + if self.on_click_chat_info is None: + return - location = "" - if source["location"] != "": - location = f": {source['location']}" - markdown += ( - f"""{(i+1)}. **{source['document']['name']}** {location}\n""" - ) - markdown += "----\n" - - self.on_click_chat_info( - event, - "Source Info", - [ - pn.pane.Markdown( - markdown, - dedent=True, - stylesheets=[""" hr { width: 94%; height:1px; } """], - ), - ], - ) + markdown = [ + "This response was generated using the following data from the uploaded files:
" + ] + for rank, source in enumerate(sources, 1): + location = source["location"] + if location: + location = f": {location}" + markdown.append(f"{rank}. **{source['document']['name']}**{location}") + markdown.append("----") + + self.on_click_chat_info( + event, + "Source Info", + [ + pn.pane.Markdown( + "\n".join(markdown), + dedent=True, + stylesheets=[""" hr { width: 94%; height:1px; } """], + ), + ], + ) def set_current_chat(self, chat): self.current_chat = chat async def chat_callback( - self, contents: str, user: str, instance: pn.chat.ChatInterface + self, content: str, user: str, instance: pn.chat.ChatInterface ): - self.current_chat["messages"].append({"role": "user", "content": contents}) - try: - answer = await self.api_wrapper.answer(self.current_chat["id"], contents) - - self.current_chat["messages"].append(answer) - - yield NewChatMessage.from_assistant(answer["content"], assistant=user) + answer = await self.api_wrapper.answer(self.current_chat["id"], content) + + yield RagnaChatMessage( + answer["content"], + role="assistant", + user=self.current_chat["metadata"]["assistant"], + sources=answer["sources"], + on_click_source_info_callback=self.on_click_source_info_wrapper, + ) except Exception: - yield NewChatMessage.from_system( - "Sorry, something went wrong. If this problem persists, please contact your administrator." + yield RagnaChatMessage( + ( + "Sorry, something went wrong. " + "If this problem persists, please contact your administrator." + ), + role="system", + user="system", ) - # def get_chat_messages(self): - # chat_entries = [] - # - # if self.current_chat is not None: - # assistant = self.current_chat["metadata"]["assistant"] - # # FIXME: user needs to be dynamic based on the username that was logged in with - # username = "User" - # - # for m in self.current_chat["messages"]: - # chat_entry = RagnaChatMessage( - # m, - # username if m["role"] == "user" else assistant, - # on_click_source_info_callback=self.on_click_source_info_wrapper, - # ) - # chat_entries.append(chat_entry) - # - # return chat_entries - @pn.depends("current_chat") def chat_interface(self): if self.current_chat is None: @@ -436,13 +390,15 @@ def chat_interface(self): chat_interface = RagnaChatInterface( callback=self.chat_callback, - callback_user=self.current_chat["metadata"]["assistant"], + user=self.user, show_rerun=False, show_undo=False, show_clear=False, show_button_name=False, view_latest=True, sizing_mode="stretch_width", + # TODO: @panel hitting enter to send a message is fine, but clicking + # somewhere else should not send the message. auto_send_types=[], # widgets=[ # pn.widgets.TextInput( @@ -462,74 +418,32 @@ def chat_interface(self): # ], # ) # ], - # renderers=[ - # lambda txt: RagnaChatMessage.chat_entry_value_renderer(txt, role=None) - # ], - # message_params={ - # "show_reaction_icons": False, - # "show_user": False, - # "show_copy_icon": False, - # "show_timestamp": True, - # }, ) - # - # chat_interface._card.stylesheets += [ - # """ - # - # :host { - # border:none !important; - # } - # - # .chat-feed-log { - # padding-right: 18%; - # margin-left: 18% ; - # padding-top:25px !important; - # - # } - # - # .chat-interface-input-container { - # margin-left:19%; - # margin-right:20%; - # margin-bottom: 20px; - # } - # - # - # """ - # ] - # - # """ - # By default, each new message is a ChatMessage object. - # But for new messages from the AI, we want to have a RagnaChatMessage, that contains the msg data, the sources, etc. - # I haven't found a better way than to watch for the `objects` param of chat_interface, - # and replace the ChatMessage objects with RagnaChatMessage object. - # We do it only for the new messages from the rag, not for the existing messages, neither for the messages from the user. - # """ - # - # def messages_changed(event): - # if len(chat_interface.objects) != len(self.current_chat["messages"]): - # return - # - # assistant = self.current_chat["metadata"]["assistant"] - # # FIXME: user needs to be dynamic based on the username that was logged in with - # username = "User" - # - # needs_refresh = False - # for i in range(len(chat_interface.objects)): - # msg = chat_interface.objects[i] - # - # if not isinstance(msg, RagnaChatMessage) and msg.user != "User": - # chat_interface.objects[i] = RagnaChatMessage( - # self.current_chat["messages"][i], - # username - # if self.current_chat["messages"][i]["role"] == "user" - # else assistant, - # on_click_source_info_callback=self.on_click_source_info_wrapper, - # ) - # msg = chat_interface.objects[i] - # needs_refresh = True - # - # if needs_refresh: - # chat_interface._chat_log.param.trigger("objects") + + # TODO: @panel ChatFeed has a card_params parameter, but this isn't used + # anywhere. I assume we should be able to use it here. + chat_interface._card.stylesheets.extend( + ui.stylesheets( + (":host", {"border": "none !important"}), + ( + ".chat-feed-log", + { + "padding-right": "18%", + "margin-left": "18%", + "padding-top": "25px !important", + }, + ), + ( + ".chat-interface-input-container", + { + "margin-left": "19%", + "margin-right": "20%", + "margin-bottom": "20px", + }, + ), + ) + ) + # # chat_interface.param.watch( # messages_changed, diff --git a/ragna/deploy/_ui/styles.py b/ragna/deploy/_ui/styles.py index 42774ab9..c2455c50 100644 --- a/ragna/deploy/_ui/styles.py +++ b/ragna/deploy/_ui/styles.py @@ -1,7 +1,7 @@ """ UI Helpers """ -from typing import Optional +from typing import Iterable, Optional, Union import panel as pn @@ -15,14 +15,16 @@ def divider(): """ -def stylesheets(*class_selectors: tuple[str, dict[str, str]]) -> Optional[list[str]]: +def stylesheets( + *class_selectors: tuple[Union[str, Iterable[str]], dict[str, str]] +) -> Optional[list[str]]: if not class_selectors: return None return [ "\n".join( [ - f"{selector} {{", + f"{selector if isinstance(selector, str) else ', '.join(selector)} {{", *[ f" {property}: {value};" for property, value in declarations.items() From 3aa73930eabfaf3155299beb07341963456a6a9f Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 29 Dec 2023 11:02:04 +0100 Subject: [PATCH 03/17] put pack text input placeholder --- ragna/deploy/_ui/central_view.py | 39 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 8e1dee39..d5a90818 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -400,24 +400,27 @@ def chat_interface(self): # TODO: @panel hitting enter to send a message is fine, but clicking # somewhere else should not send the message. auto_send_types=[], - # widgets=[ - # pn.widgets.TextInput( - # placeholder="Ask Ragna...", - # stylesheets=[ - # """:host input[type="text"] { - # border:none !important; - # box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.2); - # padding: 10px 10px 10px 15px; - # } - # - # :host input[type="text"]:focus { - # box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3); - # } - # - # """ - # ], - # ) - # ], + widgets=[ + pn.widgets.TextInput( + placeholder="Ask a question about the documents", + stylesheets=ui.stylesheets( + ( + ":host input[type='text']", + { + "border": "none !important", + "box-shadow": "0px 0px 6px 0px rgba(0, 0, 0, 0.2)", + "padding": "10px 10px 10px 15px", + }, + ), + ( + ":host input[type='text']:focus", + { + "box-shadow": "0px 0px 8px 0px rgba(0, 0, 0, 0.3)", + }, + ), + ), + ) + ], ) # TODO: @panel ChatFeed has a card_params parameter, but this isn't used From 60b0a5cdaaa924dd35e83dca8034c1d6ba7c7fca Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Fri, 29 Dec 2023 11:37:44 +0100 Subject: [PATCH 04/17] placeholder dirty --- ragna/deploy/_ui/central_view.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index d5a90818..72bd26a2 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -123,7 +123,7 @@ class CopyToClipboardButton(ReactiveHTML): class RagnaChatMessage(pn.chat.ChatMessage): role: str = param.Selector(objects=["system", "user", "assistant"]) sources = param.List(allow_None=True) - on_click_source_info_callback = param.Callable(default=None) + on_click_source_info_callback = param.Callable(allow_None=True) def __init__( self, @@ -133,6 +133,7 @@ def __init__( user: str, sources: Optional[list[dict]] = None, on_click_source_info_callback: Optional[Callable] = None, + show_timestamp=True, ): super().__init__( object=content, @@ -140,6 +141,7 @@ def __init__( user=user, sources=sources, on_click_source_info_callback=on_click_source_info_callback, + show_timestamp=show_timestamp, show_reaction_icons=False, show_user=False, show_copy_icon=False, @@ -211,22 +213,14 @@ def _render(self, content: str) -> pn.viewable.Viewable: class RagnaChatInterface(pn.chat.ChatInterface): - # FIXME: we need this! - # @param.depends("placeholder_text", watch=True, on_init=True) - # def _update_placeholder(self): - # loading_avatar = RagnaChatMessage.get_avatar("system", None) - # - # self._placeholder = NewChatMessage( - # { - # "role": "system", - # "content": ui.message_loading_indicator, - # }, - # user=" ", - # show_timestamp=False, - # avatar=loading_avatar, - # reaction_icons={}, - # show_copy_icon=False, - # ) + @param.depends("placeholder_text", watch=True, on_init=True) + def _update_placeholder(self): + self._placeholder = RagnaChatMessage( + ui.message_loading_indicator, + role="system", + user="system", + show_timestamp=False, + ) def _build_message(self, *args, **kwargs) -> RagnaChatMessage | None: message = super()._build_message(*args, **kwargs) @@ -363,6 +357,9 @@ def set_current_chat(self, chat): async def chat_callback( self, content: str, user: str, instance: pn.chat.ChatInterface ): + import asyncio + + await asyncio.sleep(10) try: answer = await self.api_wrapper.answer(self.current_chat["id"], content) From a1ec862434f1357e2b0b685bca42c01727b03566 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Tue, 2 Jan 2024 16:35:06 +0100 Subject: [PATCH 05/17] dirty --- ragna/deploy/_ui/central_view.py | 49 ++++++++++++++++++++++---------- scripts/add_chats.py | 9 ++---- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 72bd26a2..f2c814f1 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -10,7 +10,7 @@ # TODO : move all the CSS rules in a dedicated file -chat_entry_stylesheets = [ +chat_message_stylesheets = [ """ :host .right, :host .center, :host .chat-entry { width:100% !important; @@ -28,7 +28,7 @@ } """, """ - :host .chat-entry-user { + :host .chat-message { background-color: rgba(243, 243, 243); border: 1px solid rgb(238, 238, 238); margin-bottom: 20px; @@ -36,7 +36,7 @@ """, # The padding bottom is used to give some space for the copy and source info buttons """ - :host .chat-entry-ragna, :host .chat-entry-system, :host .chat-entry-assistant{ + :host .chat-message { background-color: white; border: 1px solid rgb(234, 234, 234); padding-bottom: 30px; @@ -61,9 +61,6 @@ } """, ] -pn.chat.ChatMessage._stylesheets = ( - pn.chat.ChatMessage._stylesheets + chat_entry_stylesheets -) markdown_table_stylesheet = """ @@ -74,8 +71,6 @@ } """ -# subclass pn.chat.icon.ChatCopyIcon - class CopyToClipboardButton(ReactiveHTML): title = param.String(default=None, doc="The title of the button ") @@ -133,6 +128,7 @@ def __init__( user: str, sources: Optional[list[dict]] = None, on_click_source_info_callback: Optional[Callable] = None, + timestamp=None, show_timestamp=True, ): super().__init__( @@ -141,12 +137,15 @@ def __init__( user=user, sources=sources, on_click_source_info_callback=on_click_source_info_callback, + timestamp=timestamp, show_timestamp=show_timestamp, show_reaction_icons=False, show_user=False, show_copy_icon=False, + css_classes=["chat-message", f"chat-message-{role}"], renderers=[self._render], ) + self._stylesheets.extend(chat_message_stylesheets) if self.sources: self._update_object_pane() @@ -189,7 +188,7 @@ def avatar_lookup(self, user: str) -> str: try: organization, model = user.split("/") except ValueError: - organization = None + organization = "" model = user if organization == "Ragna": @@ -213,12 +212,14 @@ def _render(self, content: str) -> pn.viewable.Viewable: class RagnaChatInterface(pn.chat.ChatInterface): + get_user_from_role = param.Callable(allow_None=True) + @param.depends("placeholder_text", watch=True, on_init=True) def _update_placeholder(self): self._placeholder = RagnaChatMessage( ui.message_loading_indicator, role="system", - user="system", + user=self.get_user_from_role("system"), show_timestamp=False, ) @@ -354,19 +355,26 @@ def on_click_source_info_wrapper(self, event, sources): def set_current_chat(self, chat): self.current_chat = chat + def get_user_from_role(self, role: Literal["system", "user", "assistant"]) -> str: + if role == "system": + return "Ragna" + elif role == "user": + return self.user + elif role == "assistant": + return self.current_chat["metadata"]["assistant"] + else: + raise RuntimeError + async def chat_callback( self, content: str, user: str, instance: pn.chat.ChatInterface ): - import asyncio - - await asyncio.sleep(10) try: answer = await self.api_wrapper.answer(self.current_chat["id"], content) yield RagnaChatMessage( answer["content"], role="assistant", - user=self.current_chat["metadata"]["assistant"], + user=self.get_user_from_role("assistant"), sources=answer["sources"], on_click_source_info_callback=self.on_click_source_info_wrapper, ) @@ -377,7 +385,7 @@ async def chat_callback( "If this problem persists, please contact your administrator." ), role="system", - user="system", + user=self.get_user_from_role("system"), ) @pn.depends("current_chat") @@ -386,8 +394,19 @@ def chat_interface(self): return chat_interface = RagnaChatInterface( + *[ + RagnaChatMessage( + message["content"], + role=message["role"], + user=self.get_user_from_role(message["role"]), + sources=message["sources"], + timestamp=message["timestamp"], + ) + for message in self.current_chat["messages"] + ], callback=self.chat_callback, user=self.user, + get_user_from_role=self.get_user_from_role, show_rerun=False, show_undo=False, show_clear=False, diff --git a/scripts/add_chats.py b/scripts/add_chats.py index c180d8a4..0ed44bcc 100644 --- a/scripts/add_chats.py +++ b/scripts/add_chats.py @@ -1,11 +1,8 @@ import datetime import json -import os import httpx -from ragna.core._utils import default_user - def main(): client = httpx.Client(base_url="http://127.0.0.1:31476") @@ -13,15 +10,13 @@ def main(): ## authentication - username = default_user() + username = "foo" token = ( client.post( "/token", data={ "username": username, - "password": os.environ.get( - "AI_PROXY_DEMO_AUTHENTICATION_PASSWORD", username - ), + "password": username, }, ) .raise_for_status() From dbd00fdf8b0b202d34cc1a8709730ff91d6a557d Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 3 Jan 2024 10:29:27 +0100 Subject: [PATCH 06/17] dirty --- ragna/deploy/_ui/central_view.py | 25 +++++++++++++++---------- ragna/deploy/_ui/styles.py | 3 --- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index f2c814f1..9f39e140 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -12,7 +12,7 @@ chat_message_stylesheets = [ """ - :host .right, :host .center, :host .chat-entry { + :host .right, :host .center { width:100% !important; } """, @@ -36,7 +36,7 @@ """, # The padding bottom is used to give some space for the copy and source info buttons """ - :host .chat-message { + :host .message-content-assistant { background-color: white; border: 1px solid rgb(234, 234, 234); padding-bottom: 30px; @@ -55,11 +55,6 @@ min-height: unset !important; } """, - """ - :host .right { - - } - """, ] markdown_table_stylesheet = """ @@ -138,7 +133,9 @@ def __init__( sources=sources, on_click_source_info_callback=on_click_source_info_callback, timestamp=timestamp, - show_timestamp=show_timestamp, + # FIXME: + show_timestamp=False, + # show_timestamp=show_timestamp, show_reaction_icons=False, show_user=False, show_copy_icon=False, @@ -146,6 +143,9 @@ def __init__( renderers=[self._render], ) self._stylesheets.extend(chat_message_stylesheets) + # import pprint + # + # pprint.pprint(self._stylesheets) if self.sources: self._update_object_pane() @@ -206,11 +206,16 @@ def avatar_lookup(self, user: str) -> str: def _render(self, content: str) -> pn.viewable.Viewable: return pn.pane.Markdown( content, - css_classes=["chat-message", f"chat-message-{self.role}"], - stylesheets=[markdown_table_stylesheet], + css_classes=["message-content", f"message-content-{self.role}"], + stylesheets=[markdown_table_stylesheet, *chat_message_stylesheets], ) +# pn.chat.ChatMessage._stylesheets = ( +# pn.chat.ChatMessage._stylesheets + chat_message_stylesheets +# ) + + class RagnaChatInterface(pn.chat.ChatInterface): get_user_from_role = param.Callable(allow_None=True) diff --git a/ragna/deploy/_ui/styles.py b/ragna/deploy/_ui/styles.py index c2455c50..95300819 100644 --- a/ragna/deploy/_ui/styles.py +++ b/ragna/deploy/_ui/styles.py @@ -116,9 +116,6 @@ def stylesheets( color: gray; } -:host { - transform: translate(14px, -56px); -} .bk-btn { border-radius: 0; From 944be8545863b6aa17d60595accea72b63a9fd23 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 3 Jan 2024 12:08:33 +0100 Subject: [PATCH 07/17] almost done? --- ragna/deploy/_ui/central_view.py | 132 +++++++++---------------------- ragna/deploy/_ui/styles.py | 6 -- 2 files changed, 37 insertions(+), 101 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 9f39e140..2ecfd035 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -10,12 +10,18 @@ # TODO : move all the CSS rules in a dedicated file -chat_message_stylesheets = [ +message_stylesheets = [ """ :host .right, :host .center { width:100% !important; } """, + """ + :host .left { + height: unset !important; + min-height: unset !important; + } + """, """ :host div.bk-panel-models-layout-Column:not(.left) { width:100% !important; @@ -27,45 +33,14 @@ box-shadow: unset; } """, - """ - :host .chat-message { - background-color: rgba(243, 243, 243); - border: 1px solid rgb(238, 238, 238); - margin-bottom: 20px; - } - """, - # The padding bottom is used to give some space for the copy and source info buttons - """ - :host .message-content-assistant { - background-color: white; - border: 1px solid rgb(234, 234, 234); - padding-bottom: 30px; - margin-bottom: 20px; - } - """, """ :host .avatar { margin-top:0px; box-shadow: unset; } """, - """ - :host .left { - height: unset !important; - min-height: unset !important; - } - """, ] -markdown_table_stylesheet = """ - - /* Better rendering of the markdown tables */ - table { - margin-top:10px; - margin-bottom:10px; - } - """ - class CopyToClipboardButton(ReactiveHTML): title = param.String(default=None, doc="The title of the button ") @@ -133,19 +108,14 @@ def __init__( sources=sources, on_click_source_info_callback=on_click_source_info_callback, timestamp=timestamp, - # FIXME: - show_timestamp=False, - # show_timestamp=show_timestamp, + show_timestamp=show_timestamp, show_reaction_icons=False, show_user=False, show_copy_icon=False, - css_classes=["chat-message", f"chat-message-{role}"], + css_classes=[f"message-{role}"], renderers=[self._render], ) - self._stylesheets.extend(chat_message_stylesheets) - # import pprint - # - # pprint.pprint(self._stylesheets) + self._stylesheets.extend(message_stylesheets) if self.sources: self._update_object_pane() @@ -176,7 +146,6 @@ def _copy_and_source_view_buttons(self) -> pn.Row: event, self.sources ), ), - height=0, ) def avatar_lookup(self, user: str) -> str: @@ -203,19 +172,38 @@ def avatar_lookup(self, user: str) -> str: else: return model[0].upper() - def _render(self, content: str) -> pn.viewable.Viewable: + def _render(self, content: str) -> pn.pane.Markdown: return pn.pane.Markdown( content, css_classes=["message-content", f"message-content-{self.role}"], - stylesheets=[markdown_table_stylesheet, *chat_message_stylesheets], + stylesheets=ui.stylesheets( + ( + "table", + { + "margin-top": "10px", + "margin-bottom": "10px", + }, + ), + ( + ( + ":host .message-content-system", + ":host .message-content-assistant", + ), + { + "background": "none", + "border": "lightgray", + "border-style": "solid", + "border-width": "thin", + }, + ), + ( + ":host .message-content-assistant", + {"background": "lightgray"}, + ), + ), ) -# pn.chat.ChatMessage._stylesheets = ( -# pn.chat.ChatMessage._stylesheets + chat_message_stylesheets -# ) - - class RagnaChatInterface(pn.chat.ChatInterface): get_user_from_role = param.Callable(allow_None=True) @@ -247,7 +235,7 @@ def __init__(self, api_wrapper, **params): super().__init__(**params) # FIXME: make this dynamic from the login - self.user = "RagnaUser" + self.user = "User" self.api_wrapper = api_wrapper self.chat_info_button = pn.widgets.Button( # The name will be filled at runtime in self.header @@ -468,54 +456,8 @@ def chat_interface(self): ) ) - # - # chat_interface.param.watch( - # messages_changed, - # ["objects"], - # ) - # - # # Here, we build a list of RagnaChatMessages from the existing messages of this chat, - # # and set them as the content of the chat interface - # chat_interface.objects = self.get_chat_messages() - # - # # Now that setting all the objects is done, we can watch the change of objects, - # # ie new messages being appended to the chat. When that happens, - # # make sure we scroll to the latest msg. - # chat_interface.param.watch( - # lambda event: self.param.trigger("trigger_scroll_to_latest"), - # ["objects"], - # ) - return chat_interface - # @pn.depends("current_chat", "trigger_scroll_to_latest") - # def scroll_to_latest_fix(self): - # """ - # This snippet needs to be re-rendered many times so the scroll-to-latest happens: - # - each time the current chat changes, hence the pn.depends on current_chat - # - each time a message is appended to the chat, hence the pn.depends on trigger_scroll_to_latest. - # trigger_scroll_to_latest is triggered in the chat_interface method, when chat_interface.objects changes. - # - # Twist : the HTML script node needs to have a different ID each time it is rendered, - # otherwise the browser doesn't re-render it / doesn't execute the JS part. - # Hence the random ID. - # """ - # - # random_id = str(uuid.uuid4()) - # - # return pn.pane.HTML( - # """""".replace( - # "{{RANDOM_ID}}", random_id - # ) - # ) - @pn.depends("current_chat") def header(self): if self.current_chat is None: diff --git a/ragna/deploy/_ui/styles.py b/ragna/deploy/_ui/styles.py index 95300819..d8359aaa 100644 --- a/ragna/deploy/_ui/styles.py +++ b/ragna/deploy/_ui/styles.py @@ -129,12 +129,6 @@ def stylesheets( ) -SS_MULTI_SELECT_STYLE = """ -option:hover, option:checked, option:focus { - color:white !important; -} -""" - SS_LABEL_STYLE = """ :host { margin-top:20px; From 9730daab1677c561e62cfcdbca924c6ea30bd750 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 3 Jan 2024 12:47:26 +0100 Subject: [PATCH 08/17] revert user avatar --- ragna/deploy/_ui/central_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 2ecfd035..2c30c273 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -152,7 +152,7 @@ def avatar_lookup(self, user: str) -> str: if self.role == "system": return "imgs/ragna_logo.svg" elif self.role == "user": - return user[0].upper() + return "👤" try: organization, model = user.split("/") @@ -235,7 +235,7 @@ def __init__(self, api_wrapper, **params): super().__init__(**params) # FIXME: make this dynamic from the login - self.user = "User" + self.user = "" self.api_wrapper = api_wrapper self.chat_info_button = pn.widgets.Button( # The name will be filled at runtime in self.header From 23a06bb328e87d016a96e07661321370bbc73dc7 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 3 Jan 2024 13:11:14 +0100 Subject: [PATCH 09/17] revert unrelated --- scripts/add_chats.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/add_chats.py b/scripts/add_chats.py index 0ed44bcc..c180d8a4 100644 --- a/scripts/add_chats.py +++ b/scripts/add_chats.py @@ -1,8 +1,11 @@ import datetime import json +import os import httpx +from ragna.core._utils import default_user + def main(): client = httpx.Client(base_url="http://127.0.0.1:31476") @@ -10,13 +13,15 @@ def main(): ## authentication - username = "foo" + username = default_user() token = ( client.post( "/token", data={ "username": username, - "password": username, + "password": os.environ.get( + "AI_PROXY_DEMO_AUTHENTICATION_PASSWORD", username + ), }, ) .raise_for_status() From 0106d3409badbaeb3787076bbe4d78b6f10326f6 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Wed, 3 Jan 2024 13:15:38 +0100 Subject: [PATCH 10/17] mypy --- ragna/deploy/_ui/central_view.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 2c30c273..139b1701 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Literal, Optional +from typing import Callable, Literal, Optional, cast import panel as pn import param @@ -169,8 +169,8 @@ def avatar_lookup(self, user: str) -> str: return "https://upload.wikimedia.org/wikipedia/commons/a/a4/GPT-4.png" elif organization == "Anthropic": return "https://upload.wikimedia.org/wikipedia/commons/1/14/Anthropic.png" - else: - return model[0].upper() + + return model[0].upper() def _render(self, content: str) -> pn.pane.Markdown: return pn.pane.Markdown( @@ -352,9 +352,9 @@ def get_user_from_role(self, role: Literal["system", "user", "assistant"]) -> st if role == "system": return "Ragna" elif role == "user": - return self.user + return cast(str, self.user) elif role == "assistant": - return self.current_chat["metadata"]["assistant"] + return cast(str, self.current_chat["metadata"]["assistant"]) else: raise RuntimeError @@ -554,20 +554,9 @@ def set_loading(self, is_loading): self.main_column.loading = is_loading def __panel__(self): - """ - The ChatInterface.view_latest option doesn't seem to work. - So to scroll to the latest message, we use some JS trick. - - There might be a more elegant solution than running this after a timeout of 200ms, - but without it, the $$$ function isn't available yet. - And even if I add the $$$ here, the fix itself doesn't work and the chat doesn't scroll - to the bottom. - """ - self.main_column = pn.Column( self.header, self.chat_interface, - # self.scroll_to_latest_fix, sizing_mode="stretch_width", stylesheets=[ """ :host { From b53901fd2941e68011660f7598fb28bd5d1709c3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Simonard Date: Thu, 4 Jan 2024 08:27:55 +0100 Subject: [PATCH 11/17] :host is a pseudoclass --- ragna/deploy/_ui/central_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 139b1701..bde45509 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -186,8 +186,8 @@ def _render(self, content: str) -> pn.pane.Markdown: ), ( ( - ":host .message-content-system", - ":host .message-content-assistant", + ":host(.message-content-system)", + ":host(.message-content-assistant)", ), { "background": "none", @@ -197,7 +197,7 @@ def _render(self, content: str) -> pn.pane.Markdown: }, ), ( - ":host .message-content-assistant", + ":host(.message-content-assistant)", {"background": "lightgray"}, ), ), From b3f0850cbd51815229053dd8b25464f0ebfe609d Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 4 Jan 2024 10:39:48 +0100 Subject: [PATCH 12/17] fix styling --- ragna/deploy/_ui/central_view.py | 59 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index bde45509..c6b89171 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -31,6 +31,8 @@ :host .message { width: calc(100% - 15px); box-shadow: unset; + font-size: unset; + background-color: unset; } """, """ @@ -89,6 +91,7 @@ class RagnaChatMessage(pn.chat.ChatMessage): role: str = param.Selector(objects=["system", "user", "assistant"]) sources = param.List(allow_None=True) on_click_source_info_callback = param.Callable(allow_None=True) + _content_style_declarations = param.Dict(constant=True) def __init__( self, @@ -114,6 +117,17 @@ def __init__( show_copy_icon=False, css_classes=[f"message-{role}"], renderers=[self._render], + _content_style_declarations={ + "background-color": "rgb(243, 243, 243) !important" + } + if role == "user" + else { + "background-color": "none", + "border": "rgb(234, 234, 234)", + "border-style": "solid", + "border-width": "1.2px", + "border-radius": "5px", + }, ) self._stylesheets.extend(message_stylesheets) @@ -123,8 +137,15 @@ def __init__( def _update_object_pane(self, event=None): super()._update_object_pane(event) if self.sources: + assert self.role == "assistant" + css_class = "message-content-assistant-with-buttons" self._object_panel = self._center_row[0] = pn.Column( - self._object_panel, self._copy_and_source_view_buttons() + self._object_panel, + self._copy_and_source_view_buttons(), + css_classes=[css_class], + stylesheets=ui.stylesheets( + (f":host(.{css_class})", self._content_style_declarations) + ), ) def _copy_and_source_view_buttons(self) -> pn.Row: @@ -173,34 +194,20 @@ def avatar_lookup(self, user: str) -> str: return model[0].upper() def _render(self, content: str) -> pn.pane.Markdown: + class_selectors = [ + ( + "table", + {"margin-top": "10px", "margin-bottom": "10px"}, + ) + ] + if self.role != "assistant": + class_selectors.append( + (":host(.message-content)", self._content_style_declarations) + ) return pn.pane.Markdown( content, css_classes=["message-content", f"message-content-{self.role}"], - stylesheets=ui.stylesheets( - ( - "table", - { - "margin-top": "10px", - "margin-bottom": "10px", - }, - ), - ( - ( - ":host(.message-content-system)", - ":host(.message-content-assistant)", - ), - { - "background": "none", - "border": "lightgray", - "border-style": "solid", - "border-width": "thin", - }, - ), - ( - ":host(.message-content-assistant)", - {"background": "lightgray"}, - ), - ), + stylesheets=ui.stylesheets(*class_selectors), ) From 8883f419698e8e53047b5bc09a904aefe3e611bf Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 4 Jan 2024 10:43:27 +0100 Subject: [PATCH 13/17] add comment --- ragna/deploy/_ui/central_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index c6b89171..0e61f5ff 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -201,6 +201,8 @@ def _render(self, content: str) -> pn.pane.Markdown: ) ] if self.role != "assistant": + # The styling for the assistant messages is applied self._update_object_pane + # since it needs to apply to the content as well as the buttons. class_selectors.append( (":host(.message-content)", self._content_style_declarations) ) From 423ccdcc5305db3b2b54eae2dc4ebef000bb44bb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Simonard Date: Thu, 4 Jan 2024 18:43:08 +0100 Subject: [PATCH 14/17] Fixing assistant messages style --- ragna/deploy/_ui/central_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 0e61f5ff..d28bf07b 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -142,7 +142,7 @@ def _update_object_pane(self, event=None): self._object_panel = self._center_row[0] = pn.Column( self._object_panel, self._copy_and_source_view_buttons(), - css_classes=[css_class], + css_classes=["message", css_class], stylesheets=ui.stylesheets( (f":host(.{css_class})", self._content_style_declarations) ), From 56a7a39d7fa693993711d4bad1bd338c66c83f7e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Simonard Date: Thu, 4 Jan 2024 19:41:39 +0100 Subject: [PATCH 15/17] fix UI glitch in header for narrow width window --- ragna/deploy/_ui/central_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index d28bf07b..7e68e883 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -549,6 +549,7 @@ def header(self): width: 100% !important; margin:0px; height:54px; + overflow:hidden; } :host div { From 76fc1ed0c4133b71381d33f2463362e53acd79aa Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 4 Jan 2024 23:37:30 +0100 Subject: [PATCH 16/17] improve comments --- ragna/deploy/_ui/central_view.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index 7e68e883..c849338f 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -415,8 +415,10 @@ def chat_interface(self): show_button_name=False, view_latest=True, sizing_mode="stretch_width", - # TODO: @panel hitting enter to send a message is fine, but clicking - # somewhere else should not send the message. + # TODO: Remove the parameter when + # https://github.com/holoviz/panel/issues/6115 is merged and released. We + # currently need it to avoid sending a message when the text input is + # de-focussed. But this also means we can't hit enter to send. auto_send_types=[], widgets=[ pn.widgets.TextInput( @@ -441,8 +443,8 @@ def chat_interface(self): ], ) - # TODO: @panel ChatFeed has a card_params parameter, but this isn't used - # anywhere. I assume we should be able to use it here. + # TODO: Pass as regular parameters when + # https://github.com/holoviz/panel/pull/6154 is merged and released. chat_interface._card.stylesheets.extend( ui.stylesheets( (":host", {"border": "none !important"}), From 0d6a8ee37d54d3eb4207fcd70c9cb9b15b06e9f5 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 4 Jan 2024 23:39:33 +0100 Subject: [PATCH 17/17] cleanup --- ragna/deploy/_ui/central_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ragna/deploy/_ui/central_view.py b/ragna/deploy/_ui/central_view.py index c849338f..aa0810cb 100644 --- a/ragna/deploy/_ui/central_view.py +++ b/ragna/deploy/_ui/central_view.py @@ -238,7 +238,6 @@ def _build_message(self, *args, **kwargs) -> RagnaChatMessage | None: class CentralView(pn.viewable.Viewer): current_chat = param.ClassSelector(class_=dict, default=None) - # trigger_scroll_to_latest = param.Integer(default=0) def __init__(self, api_wrapper, **params): super().__init__(**params)