1+ import streamlit as st
2+ from snowflake .core import Root # requires snowflake>=0.8.0
3+ from snowflake .snowpark .context import get_active_session
4+ import time
5+
6+ MODELS = [
7+ "mistral-large" ,
8+ "snowflake-arctic" ,
9+ "llama3-70b" ,
10+ "llama3-8b" ,
11+ ]
12+
13+ def init_messages ():
14+ """
15+ Initialize the session state for chat messages. If the session state indicates that the
16+ conversation should be cleared or if the "messages" key is not in the session state,
17+ initialize it as an empty list.
18+ """
19+ if st .session_state .clear_conversation or "messages" not in st .session_state :
20+ st .session_state .messages = []
21+
22+ def init_service_metadata ():
23+ """
24+ Initialize the session state for cortex search service metadata. Query the available
25+ cortex search services from the Snowflake session and store their names and search
26+ columns in the session state.
27+ """
28+ if "service_metadata" not in st .session_state :
29+ services = session .sql ("SHOW CORTEX SEARCH SERVICES IN DATABASE MOVIES;" ).collect ()
30+ service_metadata = []
31+ if services :
32+ for s in services :
33+ svc_name = s ["name" ]
34+ svc_search_col = session .sql (
35+ f"DESC CORTEX SEARCH SERVICE MOVIES.DATA.{ svc_name } ;"
36+ ).collect ()[0 ]["search_column" ]
37+ service_metadata .append (
38+ {"name" : svc_name , "search_column" : svc_search_col }
39+ )
40+
41+ st .session_state .service_metadata = service_metadata
42+
43+ def init_config_options ():
44+ """
45+ Initialize the configuration options in the Streamlit sidebar. Allow the user to select
46+ a cortex search service, clear the conversation, toggle debug mode, and toggle the use of
47+ chat history. Also provide advanced options to select a model, the number of context chunks,
48+ and the number of chat messages to use in the chat history.
49+ """
50+ st .sidebar .selectbox (
51+ "Select cortex search service:" ,
52+ [s ["name" ] for s in st .session_state .service_metadata ],
53+ key = "selected_cortex_search_service" ,
54+ )
55+
56+ st .sidebar .button ("Clear conversation" , key = "clear_conversation" )
57+ st .sidebar .toggle ("Debug" , key = "debug" , value = False )
58+ st .sidebar .toggle ("Use chat history" , key = "use_chat_history" , value = True )
59+
60+ with st .sidebar .expander ("Advanced options" ):
61+ st .selectbox ("Select model:" , MODELS , key = "model_name" )
62+ st .number_input (
63+ "Select number of context chunks" ,
64+ value = 5 ,
65+ key = "num_retrieved_chunks" ,
66+ min_value = 1 ,
67+ max_value = 10 ,
68+ )
69+ st .number_input (
70+ "Select number of messages to use in chat history" ,
71+ value = 5 ,
72+ key = "num_chat_messages" ,
73+ min_value = 1 ,
74+ max_value = 10 ,
75+ )
76+
77+ st .sidebar .expander ("Session State" ).write (st .session_state )
78+
79+ def query_cortex_search_service (query ):
80+ """
81+ Query the selected cortex search service with the given query and retrieve context documents.
82+ Display the retrieved context documents in the sidebar if debug mode is enabled. Return the
83+ context documents as a string.
84+
85+ Args:
86+ query (str): The query to search the cortex search service with.
87+
88+ Returns:
89+ str: The concatenated string of context documents.
90+ """
91+ db , schema = 'movies' , 'data'
92+
93+ cortex_search_service = (
94+ root .databases [db ]
95+ .schemas [schema ]
96+ .cortex_search_services [st .session_state .selected_cortex_search_service ]
97+ )
98+
99+ context_documents = cortex_search_service .search (
100+ query , columns = [], limit = st .session_state .num_retrieved_chunks
101+ )
102+ results = context_documents .results
103+
104+ service_metadata = st .session_state .service_metadata
105+ search_col = [s ["search_column" ] for s in service_metadata
106+ if s ["name" ] == st .session_state .selected_cortex_search_service ][0 ]
107+
108+ context_str = ""
109+ for i , r in enumerate (results ):
110+ context_str += f"Context document { i + 1 } : { r [search_col ]} \n " + "\n "
111+
112+ if st .session_state .debug :
113+ st .sidebar .text_area ("Context documents" , context_str , height = 500 )
114+
115+ return context_str
116+
117+ def get_chat_history ():
118+ """
119+ Retrieve the chat history from the session state limited to the number of messages specified
120+ by the user in the sidebar options.
121+
122+ Returns:
123+ list: The list of chat messages from the session state.
124+ """
125+ start_index = max (
126+ 0 , len (st .session_state .messages ) - st .session_state .num_chat_messages
127+ )
128+ return st .session_state .messages [start_index : len (st .session_state .messages ) - 1 ]
129+
130+ def complete (model , prompt ):
131+ """
132+ Generate a completion for the given prompt using the specified model.
133+
134+ Args:
135+ model (str): The name of the model to use for completion.
136+ prompt (str): The prompt to generate a completion for.
137+
138+ Returns:
139+ str: The generated completion.
140+ """
141+ return session .sql ("SELECT snowflake.cortex.complete(?,?)" , (model , prompt )).collect ()[0 ][0 ]
142+
143+ def make_chat_history_summary (chat_history , question ):
144+ """
145+ Generate a summary of the chat history combined with the current question to extend the query
146+ context. Use the language model to generate this summary.
147+
148+ Args:
149+ chat_history (str): The chat history to include in the summary.
150+ question (str): The current user question to extend with the chat history.
151+
152+ Returns:
153+ str: The generated summary of the chat history and question.
154+ """
155+ prompt = f"""
156+ [INST]
157+ Based on the chat history below and the question, generate a query that extend the question
158+ with the chat history provided. The query should be in natural language.
159+ Answer with only the query. Do not add any explanation.
160+
161+ <chat_history>
162+ { chat_history }
163+ </chat_history>
164+ <question>
165+ { question }
166+ </question>
167+ [/INST]
168+ """
169+
170+ summary = complete (st .session_state .model_name , prompt )
171+
172+ if st .session_state .debug :
173+ st .sidebar .text_area (
174+ "Chat history summary" , summary .replace ("$" , "\$" ), height = 150
175+ )
176+
177+ return summary
178+
179+ def create_prompt (user_question ):
180+ """
181+ Create a prompt for the language model by combining the user question with context retrieved
182+ from the cortex search service and chat history (if enabled). Format the prompt according to
183+ the expected input format of the model.
184+
185+ Args:
186+ user_question (str): The user's question to generate a prompt for.
187+
188+ Returns:
189+ str: The generated prompt for the language model.
190+ """
191+ if st .session_state .use_chat_history :
192+ chat_history = get_chat_history ()
193+ if chat_history != []:
194+ question_summary = make_chat_history_summary (chat_history , user_question )
195+ prompt_context = query_cortex_search_service (question_summary )
196+ else :
197+ prompt_context = query_cortex_search_service (user_question )
198+ else :
199+ prompt_context = query_cortex_search_service (user_question )
200+ chat_history = ""
201+
202+ prompt = f"""
203+ [INST]
204+ You are a helpful Movie Recommendation chatbot with RAG capabilities. When a user asks you a question about movie recommendations you will recommend movies that are similar to the one they ask about or are in the same genre
205+ . Use that context with the user's chat history provided in the between <chat_history> and </chat_history> tags
206+ to provide a summary that addresses the user's question. Ensure the answer is coherent, concise,
207+ and directly relevant to the user's question.
208+
209+ If the user asks a generic question which cannot be answered with the given context or chat_history,
210+ just say "I don't know the answer to that question.
211+
212+ Don't saying things like "according to the provided context".
213+
214+ <chat_history>
215+ { chat_history }
216+ </chat_history>
217+ <context>
218+ { prompt_context }
219+ </context>
220+ <question>
221+ { user_question }
222+ </question>
223+ [/INST]
224+ Answer:
225+ """
226+ return prompt
227+
228+ def init_chunking ():
229+ st .markdown ("We have not detected a Cortex Service or a chunked table. Please click below to set this up" )
230+ if st .button ('Prepare Service' ):
231+ try :
232+ with st .spinner ("Preparing service... please wait." ):
233+ session .sql ("BEGIN \
234+ call CORTEX_APP_INSTANCE.CORE.TABLE_CHUNKER();\
235+ call CORTEX_APP_INSTANCE.CORE.CREATE_CORTEX_SEARCH();\
236+ END" ).collect ()
237+
238+ st .success ("Table chunked and Cortex Service created. Reloading..." )
239+
240+ # Reload service metadata
241+ st .session_state .pop ("service_metadata" , None ) # Clear old metadata if exists
242+
243+ # Re-fetch service metadata
244+ init_service_metadata ()
245+
246+ if st .session_state .service_metadata and st .session_state .service_metadata :
247+ st .session_state .selected_cortex_search_service = st .session_state .service_metadata [0 ]["name" ]
248+
249+
250+ # Rerun the app cleanly
251+ st .rerun ()
252+ except Exception as e :
253+ st .error (f"An error occurred: { e } " )
254+
255+ def ensure_service_ready ():
256+ """
257+ Ensure that a Cortex Search Service exists and is selected.
258+ If no service exists, create one and rerun the app.
259+ """
260+ init_service_metadata ()
261+
262+ # If no service exists yet, prepare one
263+ if "service_metadata" not in st .session_state or not st .session_state .service_metadata :
264+ init_chunking ()
265+ st .stop ()
266+
267+ # If no selection made yet, select the first service
268+ if "selected_cortex_search_service" not in st .session_state :
269+ st .session_state .selected_cortex_search_service = st .session_state .service_metadata [0 ]["name" ]
270+
271+
272+ def main ():
273+ st .title (f":movie_camera: Snowflake CineBot" )
274+
275+ ensure_service_ready ()
276+ init_config_options ()
277+ init_messages ()
278+
279+ # If the underlying service has not been created, disable the chat and display config
280+ disable_chat = (
281+ "service_metadata" not in st .session_state
282+ or len (st .session_state .service_metadata ) == 0
283+ )
284+
285+ if disable_chat :
286+ init_chunking ()
287+
288+ icons = {"assistant" : "❄️" , "user" : "👤" }
289+
290+ # Display chat messages from history on app rerun
291+ for message in st .session_state .messages :
292+ with st .chat_message (message ["role" ], avatar = icons [message ["role" ]]):
293+ st .markdown (message ["content" ])
294+
295+ if question := st .chat_input ("Ask a question..." , disabled = disable_chat ):
296+ # Add user message to chat history
297+ st .session_state .messages .append ({"role" : "user" , "content" : question })
298+ # Display user message in chat message container
299+ with st .chat_message ("user" , avatar = icons ["user" ]):
300+ st .markdown (question .replace ("$" , "\$" ))
301+
302+ # Display assistant response in chat message container
303+ with st .chat_message ("assistant" , avatar = icons ["assistant" ]):
304+ message_placeholder = st .empty ()
305+ question = question .replace ("'" , "" )
306+ with st .spinner ("Thinking..." ):
307+ generated_response = complete (
308+ st .session_state .model_name , create_prompt (question )
309+ )
310+ message_placeholder .markdown (generated_response )
311+
312+ st .session_state .messages .append (
313+ {"role" : "assistant" , "content" : generated_response }
314+ )
315+
316+ if __name__ == "__main__" :
317+ session = get_active_session ()
318+ root = Root (session )
319+ main ()
0 commit comments