devuxious/PowerPoint-AI
8
1"""2Streamlit app containing the UI and the application logic.3"""4import datetime5import logging6import pathlib7import random8import sys9import tempfile10from typing import List, Union11 12import huggingface_hub13import json514import requests15import streamlit as st16from langchain_community.chat_message_histories import StreamlitChatMessageHistory17from langchain_core.messages import HumanMessage18from langchain_core.prompts import ChatPromptTemplate19 20sys.path.append('..')21sys.path.append('../..')22 23import helpers.icons_embeddings as ice24from global_config import GlobalConfig25from helpers import llm_helper, pptx_helper, text_helper26 27 28@st.cache_data29def _load_strings() -> dict:30 """31 Load various strings to be displayed in the app.32 :return: The dictionary of strings.33 """34 35 with open(GlobalConfig.APP_STRINGS_FILE, 'r', encoding='utf-8') as in_file:36 return json5.loads(in_file.read())37 38 39@st.cache_data40def _get_prompt_template(is_refinement: bool) -> str:41 """42 Return a prompt template.43 44 :param is_refinement: Whether this is the initial or refinement prompt.45 :return: The prompt template as f-string.46 """47 48 if is_refinement:49 with open(GlobalConfig.REFINEMENT_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:50 template = in_file.read()51 else:52 with open(GlobalConfig.INITIAL_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:53 template = in_file.read()54 55 return template56 57 58@st.cache_resource59def _get_llm():60 """61 Get an LLM instance.62 63 :return: The LLM.64 """65 66 return llm_helper.get_hf_endpoint()67 68 69@st.cache_data70def _get_icons_list() -> List[str]:71 """72 Get a list of available icons names without the dir name and file extension.73 74 :return: A llist of the icons.75 """76 77 return ice.get_icons_list()78 79st.set_page_config(page_title="Maiden | PowerPoint AI")80APP_TEXT = _load_strings()81 82# Session variables83CHAT_MESSAGES = 'chat_messages'84DOWNLOAD_FILE_KEY = 'download_file_name'85IS_IT_REFINEMENT = 'is_it_refinement'86 87 88logger = logging.getLogger(__name__)89 90texts = list(GlobalConfig.PPTX_TEMPLATE_FILES.keys())91captions = [GlobalConfig.PPTX_TEMPLATE_FILES[x]['caption'] for x in texts]92pptx_template = st.sidebar.radio(93 'Select a presentation template:',94 texts,95 captions=captions,96 horizontal=True97)98 99 100def build_ui():101 """102 Display the input elements for content generation.103 """104 105 st.title(APP_TEXT['app_name'])106 st.subheader(APP_TEXT['caption'])107 108 with st.expander('Usage Policies and Limitations'):109 st.text(APP_TEXT['tos'] + '\n\n' + APP_TEXT['tos2'])110 111 set_up_chat_ui()112 113 114def set_up_chat_ui():115 """116 Prepare the chat interface and related functionality.117 """118 119 st.chat_message('ai').write(120 random.choice(APP_TEXT['ai_greetings'])121 )122 123 history = StreamlitChatMessageHistory(key=CHAT_MESSAGES)124 125 if _is_it_refinement():126 template = _get_prompt_template(is_refinement=True)127 else:128 template = _get_prompt_template(is_refinement=False)129 130 prompt_template = ChatPromptTemplate.from_template(template)131 132 # Since Streamlit app reloads at every interaction, display the chat history133 # from the save session state134 for msg in history.messages:135 msg_type = msg.type136 if msg_type == 'user':137 st.chat_message(msg_type).write(msg.content)138 else:139 st.chat_message(msg_type).code(msg.content, language='json')140 141 if prompt := st.chat_input(142 placeholder=APP_TEXT['chat_placeholder'],143 max_chars=GlobalConfig.LLM_MODEL_MAX_INPUT_LENGTH144 ):145 if not text_helper.is_valid_prompt(prompt):146 st.error(147 'Not enough information provided!'148 ' Please be a little more descriptive and type a few words'149 ' with a few characters :)'150 )151 return152 153 logger.info('User input: %s | #characters: %d', prompt, len(prompt))154 st.chat_message('user').write(prompt)155 156 user_messages = _get_user_messages()157 user_messages.append(prompt)158 list_of_msgs = [159 f'{idx + 1}. {msg}' for idx, msg in enumerate(user_messages)160 ]161 list_of_msgs = '\n'.join(list_of_msgs)162 163 if _is_it_refinement():164 formatted_template = prompt_template.format(165 **{166 'instructions': list_of_msgs,167 'previous_content': _get_last_response(),168 'icons_list': '\n'.join(_get_icons_list())169 }170 )171 else:172 formatted_template = prompt_template.format(173 **{174 'question': prompt,175 'icons_list': '\n'.join(_get_icons_list())176 }177 )178 179 progress_bar = st.progress(0, 'Preparing to call LLM...')180 response = ''181 182 try:183 for chunk in _get_llm().stream(formatted_template):184 response += chunk185 186 # Update the progress bar187 progress_percentage = min(188 len(response) / GlobalConfig.LLM_MODEL_MAX_OUTPUT_LENGTH, 0.95189 )190 progress_bar.progress(191 progress_percentage,192 text='Streaming content...this might take a while...'193 )194 except requests.exceptions.ConnectionError:195 msg = (196 'A connection error occurred while streaming content from the LLM endpoint.'197 ' Unfortunately, the slide deck cannot be generated. Please try again later.'198 )199 logger.error(msg)200 st.error(msg)201 return202 except huggingface_hub.errors.ValidationError as ve:203 msg = (204 f'An error occurred while trying to generate the content: {ve}'205 '\nPlease try again with a significantly shorter input text.'206 )207 logger.error(msg)208 st.error(msg)209 return210 except Exception as ex:211 msg = (212 f'An unexpected error occurred while generating the content: {ex}'213 '\nPlease try again later, possibly with different inputs.'214 )215 logger.error(msg)216 st.error(msg)217 return218 219 history.add_user_message(prompt)220 history.add_ai_message(response)221 222 # The content has been generated as JSON223 # There maybe trailing ``` at the end of the response -- remove them224 # To be careful: ``` may be part of the content as well when code is generated225 response_cleaned = text_helper.get_clean_json(response)226 227 logger.info(228 'Cleaned JSON response:: original length: %d | cleaned length: %d',229 len(response), len(response_cleaned)230 )231 # logger.debug('Cleaned JSON: %s', response_cleaned)232 233 # Now create the PPT file234 progress_bar.progress(235 GlobalConfig.LLM_PROGRESS_MAX,236 text='Finding photos online and generating the slide deck...'237 )238 path = generate_slide_deck(response_cleaned)239 progress_bar.progress(1.0, text='Done!')240 241 st.chat_message('ai').code(response, language='json')242 243 if path:244 _display_download_button(path)245 246 logger.info(247 '#messages in history / 2: %d',248 len(st.session_state[CHAT_MESSAGES]) / 2249 )250 251 252def generate_slide_deck(json_str: str) -> Union[pathlib.Path, None]:253 """254 Create a slide deck and return the file path. In case there is any error creating the slide255 deck, the path may be to an empty file.256 257 :param json_str: The content in *valid* JSON format.258 :return: The path to the .pptx file or `None` in case of error.259 """260 261 try:262 parsed_data = json5.loads(json_str)263 except ValueError:264 st.error(265 'Encountered error while parsing JSON...will fix it and retry'266 )267 logger.error(268 'Caught ValueError: trying again after repairing JSON...'269 )270 try:271 parsed_data = json5.loads(text_helper.fix_malformed_json(json_str))272 except ValueError:273 st.error(274 'Encountered an error again while fixing JSON...'275 'the slide deck cannot be created, unfortunately ☹'276 '\nPlease try again later.'277 )278 logger.error(279 'Caught ValueError: failed to repair JSON!'280 )281 282 return None283 except RecursionError:284 st.error(285 'Encountered an error while parsing JSON...'286 'the slide deck cannot be created, unfortunately ☹'287 '\nPlease try again later.'288 )289 logger.error('Caught RecursionError while parsing JSON. Cannot generate the slide deck!')290 291 return None292 except Exception:293 st.error(294 'Encountered an error while parsing JSON...'295 'the slide deck cannot be created, unfortunately ☹'296 '\nPlease try again later.'297 )298 logger.error(299 'Caught ValueError: failed to parse JSON!'300 )301 302 return None303 304 if DOWNLOAD_FILE_KEY in st.session_state:305 path = pathlib.Path(st.session_state[DOWNLOAD_FILE_KEY])306 else:307 temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')308 path = pathlib.Path(temp.name)309 st.session_state[DOWNLOAD_FILE_KEY] = str(path)310 311 if temp:312 temp.close()313 314 try:315 logger.debug('Creating PPTX file: %s...', st.session_state[DOWNLOAD_FILE_KEY])316 pptx_helper.generate_powerpoint_presentation(317 parsed_data,318 slides_template=pptx_template,319 output_file_path=path320 )321 except Exception as ex:322 st.error(APP_TEXT['content_generation_error'])323 logger.error('Caught a generic exception: %s', str(ex))324 325 return path326 327 328def _is_it_refinement() -> bool:329 """330 Whether it is the initial prompt or a refinement.331 332 :return: True if it is the initial prompt; False otherwise.333 """334 335 if IS_IT_REFINEMENT in st.session_state:336 return True337 338 if len(st.session_state[CHAT_MESSAGES]) >= 2:339 # Prepare for the next call340 st.session_state[IS_IT_REFINEMENT] = True341 return True342 343 return False344 345 346def _get_user_messages() -> List[str]:347 """348 Get a list of user messages submitted until now from the session state.349 350 :return: The list of user messages.351 """352 353 return [354 msg.content for msg in st.session_state[CHAT_MESSAGES] if isinstance(msg, HumanMessage)355 ]356 357 358def _get_last_response() -> str:359 """360 Get the last response generated by AI.361 362 :return: The response text.363 """364 365 return st.session_state[CHAT_MESSAGES][-1].content366 367 368def _display_messages_history(view_messages: st.expander):369 """370 Display the history of messages.371 372 :param view_messages: The list of AI and Human messages.373 """374 375 with view_messages:376 view_messages.json(st.session_state[CHAT_MESSAGES])377 378 379def _display_download_button(file_path: pathlib.Path):380 """381 Display a download button to download a slide deck.382 383 :param file_path: The path of the .pptx file.384 """385 386 with open(file_path, 'rb') as download_file:387 st.download_button(388 'Download PPTX file ⬇️',389 data=download_file,390 file_name='Presentation.pptx',391 key=datetime.datetime.now()392 )393 394 395def main():396 """397 Trigger application run.398 """399 400 build_ui()401 402 403if __name__ == '__main__':404 main()405 