From c886547bec90591e299c1fc9c184624b2d423e21 Mon Sep 17 00:00:00 2001 From: Tom Gotsman Date: Wed, 6 Mar 2024 17:31:22 -0800 Subject: [PATCH 1/5] get app working with assistants api --- webui/webui/components/chat.py | 7 +++- webui/webui/components/navbar.py | 45 +++++++++++++--------- webui/webui/state.py | 64 ++++++++++++++++++-------------- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/webui/webui/components/chat.py b/webui/webui/components/chat.py index 0586c47..28c3575 100644 --- a/webui/webui/components/chat.py +++ b/webui/webui/components/chat.py @@ -4,7 +4,12 @@ from webui.state import QA, State -message_style = dict(display="inline-block", padding="1em", border_radius="8px", max_width=["30em", "30em", "50em", "50em", "50em", "50em"]) +message_style = dict( + display="inline-block", + padding="1em", + border_radius="8px", + max_width=["30em", "30em", "50em", "50em", "50em", "50em"], +) def message(qa: QA) -> rx.Component: diff --git a/webui/webui/components/navbar.py b/webui/webui/components/navbar.py index e1fe77d..6b45f63 100644 --- a/webui/webui/components/navbar.py +++ b/webui/webui/components/navbar.py @@ -1,28 +1,34 @@ import reflex as rx from webui.state import State + def sidebar_chat(chat: str) -> rx.Component: """A sidebar chat item. Args: chat: The chat item. """ - return rx.drawer.close(rx.hstack( - rx.button( - chat, on_click=lambda: State.set_chat(chat), width="80%", variant="surface" - ), - rx.button( - rx.icon( - tag="trash", - on_click=State.delete_chat, - stroke_width=1, + return rx.drawer.close( + rx.hstack( + rx.button( + chat, + on_click=lambda: State.set_chat(chat), + width="80%", + variant="surface", ), - width="20%", - variant="surface", - color_scheme="red", - ), - width="100%", - )) + rx.button( + rx.icon( + tag="trash", + on_click=State.delete_chat, + stroke_width=1, + ), + width="20%", + variant="surface", + color_scheme="red", + ), + width="100%", + ) + ) def sidebar(trigger) -> rx.Component: @@ -85,9 +91,12 @@ def navbar(): rx.heading("Reflex Chat"), rx.desktop_only( rx.badge( - State.current_chat, - rx.tooltip(rx.icon("info", size=14), content="The current selected chat."), - variant="soft" + State.current_chat, + rx.tooltip( + rx.icon("info", size=14), + content="The current selected chat.", + ), + variant="soft", ) ), align_items="center", diff --git a/webui/webui/state.py b/webui/webui/state.py index 3959d58..069b63c 100644 --- a/webui/webui/state.py +++ b/webui/webui/state.py @@ -3,11 +3,16 @@ from openai import OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) +assistant_id = os.getenv("ASSISTANT_ID") # Checking if the API key is set properly if not os.getenv("OPENAI_API_KEY"): raise Exception("Please set OPENAI_API_KEY environment variable.") +# Checking if the assistant key is set properly +if not os.getenv("ASSISTANT_ID"): + raise Exception("Please set ASSISTANT_ID environment variable.") + class QA(rx.Base): """A question and answer pair.""" @@ -97,38 +102,41 @@ async def openai_process_question(self, question: str): self.processing = True yield - # Build the messages. - messages = [ - {"role": "system", "content": "You are a friendly chatbot named Reflex. Respond in markdown."} - ] + thread = client.beta.threads.create() for qa in self.chats[self.current_chat]: - messages.append({"role": "user", "content": qa.question}) - messages.append({"role": "assistant", "content": qa.answer}) + message_user = client.beta.threads.messages.create( + thread_id=thread.id, + role="user", + content=qa.question, + ) + + run = client.beta.threads.runs.create( + thread_id=thread.id, assistant_id=assistant_id + ) - # Remove the last mock answer. - messages = messages[:-1] + # Periodically retrieve the Run to check status and see if it has completed + while run.status != "completed": + keep_retrieving_run = client.beta.threads.runs.retrieve( + thread_id=thread.id, run_id=run.id + ) - # Start a new session to answer the question. - session = client.chat.completions.create( - model=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), - messages=messages, - stream=True, - ) + if keep_retrieving_run.status == "completed": + break - # Stream the results, yielding after every word. - for item in session: - if hasattr(item.choices[0].delta, "content"): - answer_text = item.choices[0].delta.content - # Ensure answer_text is not None before concatenation - if answer_text is not None: - self.chats[self.current_chat][-1].answer += answer_text - else: - # Handle the case where answer_text is None, perhaps log it or assign a default value - # For example, assigning an empty string if answer_text is None - answer_text = "" - self.chats[self.current_chat][-1].answer += answer_text - self.chats = self.chats - yield + # Retrieve messages added by the Assistant to the thread + all_messages = client.beta.threads.messages.list(thread_id=thread.id) + answer_text = all_messages.data[0].content[0].text.value + + if answer_text is not None: + self.chats[self.current_chat][-1].answer += answer_text + else: + # Handle the case where answer_text is None, perhaps log it or assign a default value + # For example, assigning an empty string if answer_text is None + answer_text = "" + self.chats[self.current_chat][-1].answer += answer_text + + self.chats = self.chats + yield # Toggle the processing flag. self.processing = False From 6ae6611afb11665913c85efbb2604229c8f9d5c6 Mon Sep 17 00:00:00 2001 From: Tom Gotsman Date: Wed, 6 Mar 2024 17:45:59 -0800 Subject: [PATCH 2/5] add custom brand name --- webui/webui/components/navbar.py | 4 ++-- webui/webui/webui.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webui/webui/components/navbar.py b/webui/webui/components/navbar.py index 6b45f63..68cdd42 100644 --- a/webui/webui/components/navbar.py +++ b/webui/webui/components/navbar.py @@ -87,8 +87,8 @@ def navbar(): return rx.box( rx.hstack( rx.hstack( - rx.avatar(fallback="RC", variant="solid"), - rx.heading("Reflex Chat"), + rx.avatar(fallback="CC", variant="solid"), + rx.heading("Coca Cola"), rx.desktop_only( rx.badge( State.current_chat, diff --git a/webui/webui/webui.py b/webui/webui/webui.py index 7a8dcbf..c8385db 100644 --- a/webui/webui/webui.py +++ b/webui/webui/webui.py @@ -22,7 +22,7 @@ def index() -> rx.Component: app = rx.App( theme=rx.theme( appearance="dark", - accent_color="violet", + accent_color="red", ), ) app.add_page(index) From 61c0cbfd5df31e39c8562a8ef070340acbcd8dcd Mon Sep 17 00:00:00 2001 From: Tom Gotsman Date: Fri, 8 Mar 2024 18:34:21 -0800 Subject: [PATCH 3/5] update to take into account more than 1 message --- webui/webui/state.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/webui/webui/state.py b/webui/webui/state.py index 069b63c..0de69b9 100644 --- a/webui/webui/state.py +++ b/webui/webui/state.py @@ -2,7 +2,15 @@ import reflex as rx from openai import OpenAI -client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) +_client = None + +def get_openai_client(): + global _client + if _client is None: + _client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + + return _client + assistant_id = os.getenv("ASSISTANT_ID") # Checking if the API key is set properly @@ -101,22 +109,22 @@ async def openai_process_question(self, question: str): # Clear the input and start the processing. self.processing = True yield - - thread = client.beta.threads.create() + + thread = get_openai_client().beta.threads.create() for qa in self.chats[self.current_chat]: - message_user = client.beta.threads.messages.create( + message_user = get_openai_client().beta.threads.messages.create( thread_id=thread.id, role="user", content=qa.question, ) - run = client.beta.threads.runs.create( + run = get_openai_client().beta.threads.runs.create( thread_id=thread.id, assistant_id=assistant_id ) # Periodically retrieve the Run to check status and see if it has completed while run.status != "completed": - keep_retrieving_run = client.beta.threads.runs.retrieve( + keep_retrieving_run = get_openai_client().beta.threads.runs.retrieve( thread_id=thread.id, run_id=run.id ) @@ -124,7 +132,7 @@ async def openai_process_question(self, question: str): break # Retrieve messages added by the Assistant to the thread - all_messages = client.beta.threads.messages.list(thread_id=thread.id) + all_messages = get_openai_client().beta.threads.messages.list(thread_id=thread.id) answer_text = all_messages.data[0].content[0].text.value if answer_text is not None: From 7cda03df65e5ccca9e1c620ecc6bdbdcaebb80bd Mon Sep 17 00:00:00 2001 From: Tom Gotsman Date: Wed, 13 Mar 2024 11:21:49 -0700 Subject: [PATCH 4/5] makingsure to take account of previous messages and dealing with openai failure case --- webui/webui/components/chat.py | 5 +++- webui/webui/state.py | 42 +++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/webui/webui/components/chat.py b/webui/webui/components/chat.py index 28c3575..7596f0e 100644 --- a/webui/webui/components/chat.py +++ b/webui/webui/components/chat.py @@ -49,7 +49,10 @@ def message(qa: QA) -> rx.Component: def chat() -> rx.Component: """List all the messages in a single conversation.""" return rx.vstack( - rx.box(rx.foreach(State.chats[State.current_chat], message), width="100%"), + rx.box( + rx.foreach(State.chats[State.current_chat]["messages"], message), + width="100%", + ), py="8", flex="1", width="100%", diff --git a/webui/webui/state.py b/webui/webui/state.py index 0de69b9..8e00e06 100644 --- a/webui/webui/state.py +++ b/webui/webui/state.py @@ -4,6 +4,7 @@ _client = None + def get_openai_client(): global _client if _client is None: @@ -11,6 +12,7 @@ def get_openai_client(): return _client + assistant_id = os.getenv("ASSISTANT_ID") # Checking if the API key is set properly @@ -30,7 +32,7 @@ class QA(rx.Base): DEFAULT_CHATS = { - "Intros": [], + "Intros": {"id": "", "messages": []}, } @@ -38,7 +40,7 @@ class State(rx.State): """The app state.""" # A dict from the chat name to the list of questions and answers. - chats: dict[str, list[QA]] = DEFAULT_CHATS + chats: dict[str, dict[str, list[QA]]] = DEFAULT_CHATS # The current chat name. current_chat = "Intros" @@ -104,20 +106,26 @@ async def openai_process_question(self, question: str): # Add the question to the list of questions. qa = QA(question=question, answer="") - self.chats[self.current_chat].append(qa) + self.chats[self.current_chat]["messages"].append(qa) # Clear the input and start the processing. self.processing = True yield - - thread = get_openai_client().beta.threads.create() - for qa in self.chats[self.current_chat]: - message_user = get_openai_client().beta.threads.messages.create( - thread_id=thread.id, - role="user", - content=qa.question, + + if self.chats[self.current_chat]["id"] == "": + thread = get_openai_client().beta.threads.create() + self.chats[self.current_chat]["id"] = thread.id + else: + thread = get_openai_client().beta.threads.retrieve( + thread_id=self.chats[self.current_chat]["id"] ) + get_openai_client().beta.threads.messages.create( + thread_id=thread.id, + role="user", + content=qa.question, + ) + run = get_openai_client().beta.threads.runs.create( thread_id=thread.id, assistant_id=assistant_id ) @@ -131,17 +139,25 @@ async def openai_process_question(self, question: str): if keep_retrieving_run.status == "completed": break + if keep_retrieving_run.status == "failed": + self.processing = False + yield rx.window_alert("OpenAI Request Failed!.") + return + # Retrieve messages added by the Assistant to the thread - all_messages = get_openai_client().beta.threads.messages.list(thread_id=thread.id) + all_messages = get_openai_client().beta.threads.messages.list( + thread_id=thread.id + ) + answer_text = all_messages.data[0].content[0].text.value if answer_text is not None: - self.chats[self.current_chat][-1].answer += answer_text + self.chats[self.current_chat]["messages"][-1].answer += answer_text else: # Handle the case where answer_text is None, perhaps log it or assign a default value # For example, assigning an empty string if answer_text is None answer_text = "" - self.chats[self.current_chat][-1].answer += answer_text + self.chats[self.current_chat]["messages"][-1].answer += answer_text self.chats = self.chats yield From 7c5af5c006480a640dd459d127649c665276de75 Mon Sep 17 00:00:00 2001 From: Tom Gotsman Date: Thu, 14 Mar 2024 15:21:01 -0700 Subject: [PATCH 5/5] added password protection --- webui/requirements.txt | 4 +-- webui/webui/components/navbar.py | 18 ++++++------ webui/webui/layout.py | 49 ++++++++++++++++++++++++++++++++ webui/webui/state.py | 16 +++++++++-- webui/webui/webui.py | 44 +++++++++++++++++++++++++--- 5 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 webui/webui/layout.py diff --git a/webui/requirements.txt b/webui/requirements.txt index bc57de6..0f52043 100644 --- a/webui/requirements.txt +++ b/webui/requirements.txt @@ -1,3 +1,3 @@ -reflex>=0.4.0 -openai>=1.12.0 +reflex>=0.4.4 +openai>=1.13.3 diff --git a/webui/webui/components/navbar.py b/webui/webui/components/navbar.py index 68cdd42..0868043 100644 --- a/webui/webui/components/navbar.py +++ b/webui/webui/components/navbar.py @@ -112,15 +112,15 @@ def navbar(): background_color=rx.color("mauve", 6), ) ), - rx.desktop_only( - rx.button( - rx.icon( - tag="sliders-horizontal", - color=rx.color("mauve", 12), - ), - background_color=rx.color("mauve", 6), - ) - ), + # rx.desktop_only( + # rx.button( + # rx.icon( + # tag="sliders-horizontal", + # color=rx.color("mauve", 12), + # ), + # background_color=rx.color("mauve", 6), + # ) + # ), align_items="center", ), justify_content="space-between", diff --git a/webui/webui/layout.py b/webui/webui/layout.py new file mode 100644 index 0000000..44296f3 --- /dev/null +++ b/webui/webui/layout.py @@ -0,0 +1,49 @@ +import reflex as rx + + +def container(*children, **props): + """A fixed container based on a 960px grid.""" + # Enable override of default props. + props = ( + dict( + width="100%", + max_width="960px", + background=rx.color("mauve", 1), + height="100%", + px="9", + margin="0 auto", + position="relative", + ) + | props + ) + return rx.stack(*children, **props) + + +def auth_layout(*args): + """The shared layout for the login and sign up pages.""" + return rx.box( + container( + rx.vstack( + rx.heading("Welcome to your EY Interactive Chatbot!", size="8"), + rx.heading("Enter your password to get started.", size="5"), + align="center", + spacing="4", + ), + *args, + border_top_radius="10px", + box_shadow="0 4px 60px 0 rgba(0, 0, 0, 0.08), 0 4px 16px 0 rgba(0, 0, 0, 0.08)", + display="flex", + flex_direction="column", + align_items="center", + padding_top="52px", + padding_bottom="24px", + padding_x="24px", + spacing="4", + ), + height="100vh", + padding_x="50px", + padding_y="50px", + background="url(bg.svg)", + background_repeat="no-repeat", + background_size="cover", + ) diff --git a/webui/webui/state.py b/webui/webui/state.py index 8e00e06..141b676 100644 --- a/webui/webui/state.py +++ b/webui/webui/state.py @@ -15,6 +15,8 @@ def get_openai_client(): assistant_id = os.getenv("ASSISTANT_ID") +PASSWORD = os.getenv("PASSWORD") + # Checking if the API key is set properly if not os.getenv("OPENAI_API_KEY"): raise Exception("Please set OPENAI_API_KEY environment variable.") @@ -39,6 +41,10 @@ class QA(rx.Base): class State(rx.State): """The app state.""" + password: str = "" + + correct_password: bool = False + # A dict from the chat name to the list of questions and answers. chats: dict[str, dict[str, list[QA]]] = DEFAULT_CHATS @@ -54,11 +60,17 @@ class State(rx.State): # The name of the new chat. new_chat_name: str = "" + def check_password(self): + if PASSWORD == self.password: + self.correct_password = True + else: + return rx.window_alert("Invalid password.") + def create_chat(self): """Create a new chat.""" # Add the new chat to the list of chats. self.current_chat = self.new_chat_name - self.chats[self.new_chat_name] = [] + self.chats[self.new_chat_name] = {"id": "", "messages": []} def delete_chat(self): """Delete the current chat.""" @@ -141,7 +153,7 @@ async def openai_process_question(self, question: str): if keep_retrieving_run.status == "failed": self.processing = False - yield rx.window_alert("OpenAI Request Failed!.") + yield rx.window_alert("OpenAI Request Failed! Try asking again.") return # Retrieve messages added by the Assistant to the thread diff --git a/webui/webui/webui.py b/webui/webui/webui.py index c8385db..5c8fa8a 100644 --- a/webui/webui/webui.py +++ b/webui/webui/webui.py @@ -1,12 +1,39 @@ """The main Chat app.""" +import os import reflex as rx from webui.components import chat, navbar +from webui.state import State +from webui.layout import auth_layout -def index() -> rx.Component: - """The main app.""" - return rx.chakra.vstack( +def login() -> rx.Component: + return auth_layout( + rx.box( + rx.vstack( + rx.input( + type="password", + placeholder="Password", + on_blur=State.set_password, + size="3", + ), + rx.button( + "Log in", on_click=State.check_password, size="3", + ), + align="center", + spacing="4", + ), + background=rx.color("mauve", 1), + border="1px solid #eaeaea", + padding="16px", + width="400px", + border_radius="8px", + ), + ) + + +def chatapp() -> rx.Component: + return rx.vstack( navbar(), chat.chat(), chat.action_bar(), @@ -18,10 +45,19 @@ def index() -> rx.Component: ) +def index() -> rx.Component: + """The main app.""" + if not os.getenv("PASSWORD"): + return chatapp() + + else: + return rx.cond(State.correct_password, chatapp(), login()) + + # Add state and page to the app. app = rx.App( theme=rx.theme( - appearance="dark", + appearance="light", accent_color="red", ), )