google-labs-jules[bot] commited on
Commit
d051ea8
Β·
1 Parent(s): 68e7bc4

fix: Address PR comments

Browse files

- Add sys.path modification in app.py to resolve ModuleNotFoundError.
- Move ChatMessage, HumanMessage, and AIMessage classes to chat_helper.py and import them in app.py.
- Update global_config.py to use absolute paths for all file paths.
- Add chat_helper import in app.py.

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. MANIFEST.in +6 -0
  2. app.py +53 -192
  3. pyproject.toml +39 -0
  4. {helpers β†’ src/slidedeckai}/__init__.py +0 -0
  5. src/slidedeckai/_version.py +1 -0
  6. src/slidedeckai/cli.py +36 -0
  7. src/slidedeckai/core.py +198 -0
  8. {file_embeddings β†’ src/slidedeckai/file_embeddings}/embeddings.npy +0 -0
  9. {file_embeddings β†’ src/slidedeckai/file_embeddings}/icons.npy +0 -0
  10. global_config.py β†’ src/slidedeckai/global_config.py +13 -11
  11. src/slidedeckai/helpers/__init__.py +0 -0
  12. {helpers β†’ src/slidedeckai/helpers}/chat_helper.py +6 -13
  13. {helpers β†’ src/slidedeckai/helpers}/file_manager.py +1 -4
  14. {helpers β†’ src/slidedeckai/helpers}/icons_embeddings.py +3 -6
  15. {helpers β†’ src/slidedeckai/helpers}/image_search.py +0 -0
  16. {helpers β†’ src/slidedeckai/helpers}/llm_helper.py +1 -3
  17. {helpers β†’ src/slidedeckai/helpers}/pptx_helper.py +3 -6
  18. {helpers β†’ src/slidedeckai/helpers}/text_helper.py +0 -0
  19. {icons β†’ src/slidedeckai/icons}/png128/0-circle.png +0 -0
  20. {icons β†’ src/slidedeckai/icons}/png128/1-circle.png +0 -0
  21. {icons β†’ src/slidedeckai/icons}/png128/123.png +0 -0
  22. {icons β†’ src/slidedeckai/icons}/png128/2-circle.png +0 -0
  23. {icons β†’ src/slidedeckai/icons}/png128/3-circle.png +0 -0
  24. {icons β†’ src/slidedeckai/icons}/png128/4-circle.png +0 -0
  25. {icons β†’ src/slidedeckai/icons}/png128/5-circle.png +0 -0
  26. {icons β†’ src/slidedeckai/icons}/png128/6-circle.png +0 -0
  27. {icons β†’ src/slidedeckai/icons}/png128/7-circle.png +0 -0
  28. {icons β†’ src/slidedeckai/icons}/png128/8-circle.png +0 -0
  29. {icons β†’ src/slidedeckai/icons}/png128/9-circle.png +0 -0
  30. {icons β†’ src/slidedeckai/icons}/png128/activity.png +0 -0
  31. {icons β†’ src/slidedeckai/icons}/png128/airplane.png +0 -0
  32. {icons β†’ src/slidedeckai/icons}/png128/alarm.png +0 -0
  33. {icons β†’ src/slidedeckai/icons}/png128/alien-head.png +0 -0
  34. {icons β†’ src/slidedeckai/icons}/png128/alphabet.png +0 -0
  35. {icons β†’ src/slidedeckai/icons}/png128/amazon.png +0 -0
  36. {icons β†’ src/slidedeckai/icons}/png128/amritsar-golden-temple.png +0 -0
  37. {icons β†’ src/slidedeckai/icons}/png128/amsterdam-canal.png +0 -0
  38. {icons β†’ src/slidedeckai/icons}/png128/amsterdam-windmill.png +0 -0
  39. {icons β†’ src/slidedeckai/icons}/png128/android.png +0 -0
  40. {icons β†’ src/slidedeckai/icons}/png128/angkor-wat.png +0 -0
  41. {icons β†’ src/slidedeckai/icons}/png128/apple.png +0 -0
  42. {icons β†’ src/slidedeckai/icons}/png128/archive.png +0 -0
  43. {icons β†’ src/slidedeckai/icons}/png128/argentina-obelisk.png +0 -0
  44. {icons β†’ src/slidedeckai/icons}/png128/artificial-intelligence-brain.png +0 -0
  45. {icons β†’ src/slidedeckai/icons}/png128/atlanta.png +0 -0
  46. {icons β†’ src/slidedeckai/icons}/png128/austin.png +0 -0
  47. {icons β†’ src/slidedeckai/icons}/png128/automation-decision.png +0 -0
  48. {icons β†’ src/slidedeckai/icons}/png128/award.png +0 -0
  49. {icons β†’ src/slidedeckai/icons}/png128/balloon.png +0 -0
  50. {icons β†’ src/slidedeckai/icons}/png128/ban.png +0 -0
MANIFEST.in ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ include src/slidedeckai/strings.json
2
+ recursive-include src/slidedeckai/prompts *.txt
3
+ recursive-include src/slidedeckai/pptx_templates *.pptx
4
+ recursive-include src/slidedeckai/icons *.png
5
+ recursive-include src/slidedeckai/icons *.txt
6
+ recursive-include src/slidedeckai/file_embeddings *.npy
app.py CHANGED
@@ -6,6 +6,7 @@ import logging
6
  import os
7
  import pathlib
8
  import random
 
9
  import tempfile
10
  from typing import List, Union
11
 
@@ -17,13 +18,35 @@ import requests
17
  import streamlit as st
18
  from dotenv import load_dotenv
19
 
20
- import global_config as gcfg
21
- import helpers.file_manager as filem
22
- from global_config import GlobalConfig
23
- from helpers import chat_helper, llm_helper, pptx_helper, text_helper
 
 
 
 
24
 
25
  load_dotenv()
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  RUN_IN_OFFLINE_MODE = os.getenv('RUN_IN_OFFLINE_MODE', 'False').lower() == 'true'
28
 
29
 
@@ -368,99 +391,38 @@ def set_up_chat_ui():
368
  st.session_state[PDF_FILE_KEY],
369
  (st.session_state['start_page'], st.session_state['end_page'])
370
  )
371
- provider, llm_name = llm_helper.get_provider_model(
372
- llm_provider_to_use,
373
- use_ollama=RUN_IN_OFFLINE_MODE
374
- )
375
 
376
- # Validate that provider and model were parsed successfully
377
- if not provider or not llm_name:
378
- handle_error(
379
- f'Failed to parse provider and model from: "{llm_provider_to_use}". '
380
- f'Please select a valid LLM from the dropdown.',
381
- True
382
- )
383
- return
384
-
385
- user_key = api_key_token.strip()
386
- az_deployment = azure_deployment.strip()
387
- az_endpoint = azure_endpoint.strip()
388
- api_ver = api_version.strip()
389
-
390
- if not are_all_inputs_valid(
391
- prompt_text, provider, llm_name, user_key,
392
- az_deployment, az_endpoint, api_ver
393
- ):
394
- return
395
-
396
- logger.info(
397
- 'User input: %s | #characters: %d | LLM: %s',
398
- prompt_text, len(prompt_text), llm_name
399
- )
400
  st.chat_message('user').write(prompt_text)
401
 
402
- if _is_it_refinement():
403
- user_messages = _get_user_messages()
404
- user_messages.append(prompt_text)
405
- list_of_msgs = [
406
- f'{idx + 1}. {msg}' for idx, msg in enumerate(user_messages)
407
- ]
408
- formatted_template = prompt_template.format(
409
- **{
410
- 'instructions': '\n'.join(list_of_msgs),
411
- 'previous_content': _get_last_response(),
412
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
413
- }
414
- )
415
- else:
416
- formatted_template = prompt_template.format(
417
- **{
418
- 'question': prompt_text,
419
- 'additional_info': st.session_state.get(ADDITIONAL_INFO, ''),
420
- }
421
- )
422
 
423
  progress_bar = st.progress(0, 'Preparing to call LLM...')
424
- response = ''
 
 
425
 
426
  try:
427
- llm = llm_helper.get_litellm_llm(
428
- provider=provider,
429
- model=llm_name,
430
- max_new_tokens=gcfg.get_max_output_tokens(llm_provider_to_use),
431
- api_key=user_key,
432
- azure_endpoint_url=az_endpoint,
433
- azure_deployment_name=az_deployment,
434
- azure_api_version=api_ver,
435
- )
436
 
437
- if not llm:
438
- handle_error(
439
- 'Failed to create an LLM instance! Make sure that you have selected the'
440
- ' correct model from the dropdown list and have provided correct API key'
441
- ' or access token.',
442
- False
443
- )
444
- return
 
 
445
 
446
- for chunk in llm.stream(formatted_template):
447
- if isinstance(chunk, str):
448
- response += chunk
449
- else:
450
- content = getattr(chunk, 'content', None)
451
- if content is not None:
452
- response += content
453
- else:
454
- response += str(chunk)
455
-
456
- # Update the progress bar with an approx progress percentage
457
- progress_bar.progress(
458
- min(
459
- len(response) / gcfg.get_max_output_tokens(llm_provider_to_use),
460
- 0.95
461
- ),
462
- text='Streaming content...this might take a while...'
463
- )
464
  except (httpx.ConnectError, requests.exceptions.ConnectionError):
465
  handle_error(
466
  'A connection error occurred while streaming content from the LLM endpoint.'
@@ -469,22 +431,19 @@ def set_up_chat_ui():
469
  ' using Ollama, make sure that Ollama is already running on your system.',
470
  True
471
  )
472
- return
473
  except huggingface_hub.errors.ValidationError as ve:
474
  handle_error(
475
  f'An error occurred while trying to generate the content: {ve}'
476
  '\nPlease try again with a significantly shorter input text.',
477
  True
478
  )
479
- return
480
  except ollama.ResponseError:
481
  handle_error(
482
- f'The model `{llm_name}` is unavailable with Ollama on your system.'
483
- f' Make sure that you have provided the correct LLM name or pull it using'
484
- f' `ollama pull {llm_name}`. View LLMs available locally by running `ollama list`.',
485
  True
486
  )
487
- return
488
  except Exception as ex:
489
  _msg = str(ex)
490
  if 'payment required' in _msg.lower():
@@ -509,101 +468,6 @@ def set_up_chat_ui():
509
  ' Read **[how to get free LLM API keys](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)**.',
510
  True
511
  )
512
- return
513
-
514
- history.add_user_message(prompt_text)
515
- history.add_ai_message(response)
516
-
517
- # The content has been generated as JSON
518
- # There maybe trailing ``` at the end of the response -- remove them
519
- # To be careful: ``` may be part of the content as well when code is generated
520
- response = text_helper.get_clean_json(response)
521
- logger.info(
522
- 'Cleaned JSON length: %d', len(response)
523
- )
524
-
525
- # Now create the PPT file
526
- progress_bar.progress(
527
- GlobalConfig.LLM_PROGRESS_MAX,
528
- text='Finding photos online and generating the slide deck...'
529
- )
530
- progress_bar.progress(1.0, text='Done!')
531
- st.chat_message('ai').code(response, language='json')
532
-
533
- if path := generate_slide_deck(response):
534
- _display_download_button(path)
535
-
536
- logger.info(
537
- '#messages in history / 2: %d',
538
- len(st.session_state[CHAT_MESSAGES]) / 2
539
- )
540
-
541
-
542
- def generate_slide_deck(json_str: str) -> Union[pathlib.Path, None]:
543
- """
544
- Create a slide deck and return the file path. In case there is any error creating the slide
545
- deck, the path may be to an empty file.
546
-
547
- :param json_str: The content in *valid* JSON format.
548
- :return: The path to the .pptx file or `None` in case of error.
549
- """
550
-
551
- try:
552
- parsed_data = json5.loads(json_str)
553
- except ValueError:
554
- handle_error(
555
- 'Encountered error while parsing JSON...will fix it and retry',
556
- True
557
- )
558
- try:
559
- parsed_data = json5.loads(text_helper.fix_malformed_json(json_str))
560
- except ValueError:
561
- handle_error(
562
- 'Encountered an error again while fixing JSON...'
563
- 'the slide deck cannot be created, unfortunately ☹'
564
- '\nPlease try again later.',
565
- True
566
- )
567
- return None
568
- except RecursionError:
569
- handle_error(
570
- 'Encountered a recursion error while parsing JSON...'
571
- 'the slide deck cannot be created, unfortunately ☹'
572
- '\nPlease try again later.',
573
- True
574
- )
575
- return None
576
- except Exception:
577
- handle_error(
578
- 'Encountered an error while parsing JSON...'
579
- 'the slide deck cannot be created, unfortunately ☹'
580
- '\nPlease try again later.',
581
- True
582
- )
583
- return None
584
-
585
- if DOWNLOAD_FILE_KEY in st.session_state:
586
- path = pathlib.Path(st.session_state[DOWNLOAD_FILE_KEY])
587
- else:
588
- temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
589
- path = pathlib.Path(temp.name)
590
- st.session_state[DOWNLOAD_FILE_KEY] = str(path)
591
-
592
- if temp:
593
- temp.close()
594
-
595
- try:
596
- logger.debug('Creating PPTX file: %s...', st.session_state[DOWNLOAD_FILE_KEY])
597
- pptx_helper.generate_powerpoint_presentation(
598
- parsed_data,
599
- slides_template=pptx_template,
600
- output_file_path=path
601
- )
602
- except Exception as ex:
603
- st.error(APP_TEXT['content_generation_error'])
604
- logger.exception('Caught a generic exception: %s', str(ex))
605
-
606
- return path
607
 
608
 
609
  def _is_it_refinement() -> bool:
@@ -643,9 +507,6 @@ def _get_last_response() -> str:
643
  :return: The response text.
644
  """
645
 
646
- return st.session_state[CHAT_MESSAGES][-1].content
647
-
648
-
649
  def _display_messages_history(view_messages: st.expander):
650
  """
651
  Display the history of messages.
 
6
  import os
7
  import pathlib
8
  import random
9
+ import sys
10
  import tempfile
11
  from typing import List, Union
12
 
 
18
  import streamlit as st
19
  from dotenv import load_dotenv
20
 
21
+ sys.path.insert(0, os.path.abspath('src'))
22
+ from slidedeckai.core import SlideDeckAI
23
+ from slidedeckai import global_config as gcfg
24
+ from slidedeckai.global_config import GlobalConfig
25
+ from slidedeckai.helpers import llm_helper, text_helper
26
+ import slidedeckai.helpers.file_manager as filem
27
+ from slidedeckai.helpers.chat_helper import ChatMessage, HumanMessage, AIMessage
28
+ from slidedeckai.helpers import chat_helper
29
 
30
  load_dotenv()
31
 
32
+ class StreamlitChatMessageHistory:
33
+ """Chat message history stored in Streamlit session state."""
34
+
35
+ def __init__(self, key: str):
36
+ self.key = key
37
+ if key not in st.session_state:
38
+ st.session_state[key] = []
39
+
40
+ @property
41
+ def messages(self):
42
+ return st.session_state[self.key]
43
+
44
+ def add_user_message(self, content: str):
45
+ st.session_state[self.key].append(HumanMessage(content))
46
+
47
+ def add_ai_message(self, content: str):
48
+ st.session_state[self.key].append(AIMessage(content))
49
+
50
  RUN_IN_OFFLINE_MODE = os.getenv('RUN_IN_OFFLINE_MODE', 'False').lower() == 'true'
51
 
52
 
 
391
  st.session_state[PDF_FILE_KEY],
392
  (st.session_state['start_page'], st.session_state['end_page'])
393
  )
 
 
 
 
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  st.chat_message('user').write(prompt_text)
396
 
397
+ slide_generator = SlideDeckAI(
398
+ model=llm_provider_to_use,
399
+ topic=prompt_text,
400
+ api_key=api_key_token.strip(),
401
+ template_idx=list(GlobalConfig.PPTX_TEMPLATE_FILES.keys()).index(pptx_template),
402
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
  progress_bar = st.progress(0, 'Preparing to call LLM...')
405
+
406
+ def progress_callback(current_progress):
407
+ progress_bar.progress(min(current_progress / gcfg.get_max_output_tokens(llm_provider_to_use), 0.95), text='Streaming content...this might take a while...')
408
 
409
  try:
410
+ if _is_it_refinement():
411
+ path = slide_generator.revise(instructions=prompt_text, progress_callback=progress_callback)
412
+ else:
413
+ path = slide_generator.generate(progress_callback=progress_callback)
 
 
 
 
 
414
 
415
+ progress_bar.progress(1.0, text='Done!')
416
+
417
+ if path:
418
+ st.session_state[DOWNLOAD_FILE_KEY] = str(path)
419
+ history.add_user_message(prompt_text)
420
+ history.add_ai_message(slide_generator.last_response)
421
+ st.chat_message('ai').code(slide_generator.last_response, language='json')
422
+ _display_download_button(path)
423
+ else:
424
+ handle_error("Failed to generate slide deck.", True)
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  except (httpx.ConnectError, requests.exceptions.ConnectionError):
427
  handle_error(
428
  'A connection error occurred while streaming content from the LLM endpoint.'
 
431
  ' using Ollama, make sure that Ollama is already running on your system.',
432
  True
433
  )
 
434
  except huggingface_hub.errors.ValidationError as ve:
435
  handle_error(
436
  f'An error occurred while trying to generate the content: {ve}'
437
  '\nPlease try again with a significantly shorter input text.',
438
  True
439
  )
 
440
  except ollama.ResponseError:
441
  handle_error(
442
+ f'The model is unavailable with Ollama on your system.'
443
+ f' Make sure that you have provided the correct LLM name or pull it.'
444
+ f' View LLMs available locally by running `ollama list`.',
445
  True
446
  )
 
447
  except Exception as ex:
448
  _msg = str(ex)
449
  if 'payment required' in _msg.lower():
 
468
  ' Read **[how to get free LLM API keys](https://github.com/barun-saha/slide-deck-ai?tab=readme-ov-file#summary-of-the-llms)**.',
469
  True
470
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
 
473
  def _is_it_refinement() -> bool:
 
507
  :return: The response text.
508
  """
509
 
 
 
 
510
  def _display_messages_history(view_messages: st.expander):
511
  """
512
  Display the history of messages.
pyproject.toml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "slidedeckai"
7
+ authors = [
8
+ { name="Barun Saha", email="[email protected]" }
9
+ ]
10
+ description = "A Python package to generate slide decks using AI."
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent"
17
+ ]
18
+ dynamic = ["dependencies", "version"]
19
+
20
+ [tool.setuptools]
21
+ package-dir = {"" = "src"}
22
+ include-package-data = true
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.dynamic]
28
+ dependencies = {file = ["requirements.txt"]}
29
+ version = {attr = "slidedeckai._version.__version__"}
30
+
31
+ [tool.setuptools.package-data]
32
+ slidedeckai = ["prompts/**/*.txt", "strings.json", "pptx_templates/*.pptx", "icons/png128/*.png", "icons/svg_repo.txt", "file_embeddings/*.npy"]
33
+
34
+ [project.urls]
35
+ "Homepage" = "https://github.com/barun-saha/slide-deck-ai"
36
+ "Bug Tracker" = "https://github.com/barun-saha/slide-deck-ai/issues"
37
+
38
+ [project.scripts]
39
+ slidedeckai = "slidedeckai.cli:main"
{helpers β†’ src/slidedeckai}/__init__.py RENAMED
File without changes
src/slidedeckai/_version.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "8.0.0"
src/slidedeckai/cli.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Command-line interface for SlideDeckAI.
3
+ """
4
+ import argparse
5
+ from .core import SlideDeckAI
6
+
7
+ def main():
8
+ """
9
+ The main function for the CLI.
10
+ """
11
+ parser = argparse.ArgumentParser(description='Generate slide decks with SlideDeckAI.')
12
+ parser.add_argument('--model', required=True, help='The name of the LLM model to use.')
13
+ parser.add_argument('--topic', required=True, help='The topic of the slide deck.')
14
+ parser.add_argument('--api-key', help='The API key for the LLM provider.')
15
+ parser.add_argument('--template-id', type=int, default=0, help='The index of the PowerPoint template to use.')
16
+ parser.add_argument('--output-path', help='The path to save the generated .pptx file.')
17
+ args = parser.parse_args()
18
+
19
+ slide_generator = SlideDeckAI(
20
+ model=args.model,
21
+ topic=args.topic,
22
+ api_key=args.api_key,
23
+ template_idx=args.template_id,
24
+ )
25
+
26
+ pptx_path = slide_generator.generate()
27
+
28
+ if args.output_path:
29
+ import shutil
30
+ shutil.move(str(pptx_path), args.output_path)
31
+ print(f"Slide deck saved to {args.output_path}")
32
+ else:
33
+ print(f"Slide deck saved to {pptx_path}")
34
+
35
+ if __name__ == '__main__':
36
+ main()
src/slidedeckai/core.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core classes for SlideDeckAI.
3
+ """
4
+ import logging
5
+ import os
6
+ import pathlib
7
+ import tempfile
8
+ from typing import Union
9
+
10
+ import json5
11
+ from dotenv import load_dotenv
12
+
13
+ from . import global_config as gcfg
14
+ from .global_config import GlobalConfig
15
+ from .helpers import llm_helper, pptx_helper, text_helper
16
+ from .helpers.chat_helper import ChatMessageHistory
17
+
18
+ load_dotenv()
19
+
20
+ RUN_IN_OFFLINE_MODE = os.getenv('RUN_IN_OFFLINE_MODE', 'False').lower() == 'true'
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class SlideDeckAI:
25
+ """
26
+ The main class for generating slide decks.
27
+ """
28
+
29
+ def __init__(self, model, topic, api_key=None, pdf_file_path=None, pdf_page_range=None, template_idx=0):
30
+ """
31
+ Initializes the SlideDeckAI object.
32
+
33
+ :param model: The name of the LLM model to use.
34
+ :param topic: The topic of the slide deck.
35
+ :param api_key: The API key for the LLM provider.
36
+ :param pdf_file_path: The path to a PDF file to use as a source for the slide deck.
37
+ :param pdf_page_range: A tuple representing the page range to use from the PDF file.
38
+ :param template_idx: The index of the PowerPoint template to use.
39
+ """
40
+ self.model = model
41
+ self.topic = topic
42
+ self.api_key = api_key
43
+ self.pdf_file_path = pdf_file_path
44
+ self.pdf_page_range = pdf_page_range
45
+ self.template_idx = template_idx
46
+ self.chat_history = ChatMessageHistory()
47
+ self.last_response = None
48
+
49
+ def _get_prompt_template(self, is_refinement: bool) -> str:
50
+ """
51
+ Return a prompt template.
52
+
53
+ :param is_refinement: Whether this is the initial or refinement prompt.
54
+ :return: The prompt template as f-string.
55
+ """
56
+ if is_refinement:
57
+ with open(GlobalConfig.REFINEMENT_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:
58
+ template = in_file.read()
59
+ else:
60
+ with open(GlobalConfig.INITIAL_PROMPT_TEMPLATE, 'r', encoding='utf-8') as in_file:
61
+ template = in_file.read()
62
+ return template
63
+
64
+ def generate(self, progress_callback=None):
65
+ """
66
+ Generates the initial slide deck.
67
+ :return: The path to the generated .pptx file.
68
+ """
69
+ self.chat_history.add_user_message(self.topic)
70
+ prompt_template = self._get_prompt_template(is_refinement=False)
71
+ formatted_template = prompt_template.format(question=self.topic, additional_info='')
72
+
73
+ provider, llm_name = llm_helper.get_provider_model(self.model, use_ollama=RUN_IN_OFFLINE_MODE)
74
+
75
+ llm = llm_helper.get_litellm_llm(
76
+ provider=provider,
77
+ model=llm_name,
78
+ max_new_tokens=gcfg.get_max_output_tokens(self.model),
79
+ api_key=self.api_key,
80
+ )
81
+
82
+ response = ""
83
+ for chunk in llm.stream(formatted_template):
84
+ if isinstance(chunk, str):
85
+ response += chunk
86
+ else:
87
+ content = getattr(chunk, 'content', None)
88
+ if content is not None:
89
+ response += content
90
+ else:
91
+ response += str(chunk)
92
+ if progress_callback:
93
+ progress_callback(len(response))
94
+
95
+ self.last_response = text_helper.get_clean_json(response)
96
+ self.chat_history.add_ai_message(self.last_response)
97
+
98
+ return self._generate_slide_deck(self.last_response)
99
+
100
+ def revise(self, instructions, progress_callback=None):
101
+ """
102
+ Revises the slide deck with new instructions.
103
+
104
+ :param instructions: The instructions for revising the slide deck.
105
+ :return: The path to the revised .pptx file.
106
+ """
107
+ if not self.last_response:
108
+ raise ValueError("You must generate a slide deck before you can revise it.")
109
+
110
+ if len(self.chat_history.messages) >= 16:
111
+ raise ValueError("Chat history is full. Please reset to continue.")
112
+
113
+ self.chat_history.add_user_message(instructions)
114
+
115
+ prompt_template = self._get_prompt_template(is_refinement=True)
116
+
117
+ list_of_msgs = [f'{idx + 1}. {msg.content}' for idx, msg in enumerate(self.chat_history.messages) if msg.role == 'user']
118
+
119
+ formatted_template = prompt_template.format(
120
+ instructions='\n'.join(list_of_msgs),
121
+ previous_content=self.last_response,
122
+ additional_info='',
123
+ )
124
+
125
+ provider, llm_name = llm_helper.get_provider_model(self.model, use_ollama=RUN_IN_OFFLINE_MODE)
126
+
127
+ llm = llm_helper.get_litellm_llm(
128
+ provider=provider,
129
+ model=llm_name,
130
+ max_new_tokens=gcfg.get_max_output_tokens(self.model),
131
+ api_key=self.api_key,
132
+ )
133
+
134
+ response = ""
135
+ for chunk in llm.stream(formatted_template):
136
+ if isinstance(chunk, str):
137
+ response += chunk
138
+ else:
139
+ content = getattr(chunk, 'content', None)
140
+ if content is not None:
141
+ response += content
142
+ else:
143
+ response += str(chunk)
144
+ if progress_callback:
145
+ progress_callback(len(response))
146
+
147
+ self.last_response = text_helper.get_clean_json(response)
148
+ self.chat_history.add_ai_message(self.last_response)
149
+
150
+ return self._generate_slide_deck(self.last_response)
151
+
152
+ def _generate_slide_deck(self, json_str: str) -> Union[pathlib.Path, None]:
153
+ """
154
+ Create a slide deck and return the file path.
155
+
156
+ :param json_str: The content in *valid* JSON format.
157
+ :return: The path to the .pptx file or `None` in case of error.
158
+ """
159
+ try:
160
+ parsed_data = json5.loads(json_str)
161
+ except (ValueError, RecursionError) as e:
162
+ logger.error("Error parsing JSON: %s", e)
163
+ try:
164
+ parsed_data = json5.loads(text_helper.fix_malformed_json(json_str))
165
+ except (ValueError, RecursionError) as e2:
166
+ logger.error("Error parsing fixed JSON: %s", e2)
167
+ return None
168
+
169
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix='.pptx')
170
+ path = pathlib.Path(temp.name)
171
+ temp.close()
172
+
173
+ try:
174
+ pptx_helper.generate_powerpoint_presentation(
175
+ parsed_data,
176
+ slides_template=list(GlobalConfig.PPTX_TEMPLATE_FILES.keys())[self.template_idx],
177
+ output_file_path=path
178
+ )
179
+ except Exception as ex:
180
+ logger.exception('Caught a generic exception: %s', str(ex))
181
+ return None
182
+
183
+ return path
184
+
185
+ def set_template(self, idx):
186
+ """
187
+ Sets the PowerPoint template to use.
188
+
189
+ :param idx: The index of the template to use.
190
+ """
191
+ self.template_idx = idx
192
+
193
+ def reset(self):
194
+ """
195
+ Resets the chat history.
196
+ """
197
+ self.chat_history = ChatMessageHistory()
198
+ self.last_response = None
{file_embeddings β†’ src/slidedeckai/file_embeddings}/embeddings.npy RENAMED
File without changes
{file_embeddings β†’ src/slidedeckai/file_embeddings}/icons.npy RENAMED
File without changes
global_config.py β†’ src/slidedeckai/global_config.py RENAMED
@@ -4,6 +4,7 @@ A set of configurations used by the app.
4
  import logging
5
  import os
6
  import re
 
7
 
8
  from dataclasses import dataclass
9
  from dotenv import load_dotenv
@@ -11,6 +12,7 @@ from dotenv import load_dotenv
11
 
12
  load_dotenv()
13
 
 
14
 
15
  @dataclass(frozen=True)
16
  class GlobalConfig:
@@ -128,32 +130,32 @@ class GlobalConfig:
128
 
129
  LOG_LEVEL = 'DEBUG'
130
  COUNT_TOKENS = False
131
- APP_STRINGS_FILE = 'strings.json'
132
- PRELOAD_DATA_FILE = 'examples/example_02.json'
133
- INITIAL_PROMPT_TEMPLATE = 'prompts/initial_template_v4_two_cols_img.txt'
134
- REFINEMENT_PROMPT_TEMPLATE = 'prompts/refinement_template_v4_two_cols_img.txt'
135
 
136
  LLM_PROGRESS_MAX = 90
137
- ICONS_DIR = 'icons/png128/'
138
  TINY_BERT_MODEL = 'gaunernst/bert-mini-uncased'
139
- EMBEDDINGS_FILE_NAME = 'file_embeddings/embeddings.npy'
140
- ICONS_FILE_NAME = 'file_embeddings/icons.npy'
141
 
142
  PPTX_TEMPLATE_FILES = {
143
  'Basic': {
144
- 'file': 'pptx_templates/Blank.pptx',
145
  'caption': 'A good start (Uses [photos](https://unsplash.com/photos/AFZ-qBPEceA) by [cetteup](https://unsplash.com/@cetteup?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-foggy-forest-filled-with-lots-of-trees-d3ci37Gcgxg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)) 🟧'
146
  },
147
  'Ion Boardroom': {
148
- 'file': 'pptx_templates/Ion_Boardroom.pptx',
149
  'caption': 'Make some bold decisions πŸŸ₯'
150
  },
151
  'Minimalist Sales Pitch': {
152
- 'file': 'pptx_templates/Minimalist_sales_pitch.pptx',
153
  'caption': 'In high contrast ⬛'
154
  },
155
  'Urban Monochrome': {
156
- 'file': 'pptx_templates/Urban_monochrome.pptx',
157
  'caption': 'Marvel in a monochrome dream ⬜'
158
  },
159
  }
 
4
  import logging
5
  import os
6
  import re
7
+ from pathlib import Path
8
 
9
  from dataclasses import dataclass
10
  from dotenv import load_dotenv
 
12
 
13
  load_dotenv()
14
 
15
+ _SRC_DIR = Path(__file__).resolve().parent
16
 
17
  @dataclass(frozen=True)
18
  class GlobalConfig:
 
130
 
131
  LOG_LEVEL = 'DEBUG'
132
  COUNT_TOKENS = False
133
+ APP_STRINGS_FILE = _SRC_DIR / 'strings.json'
134
+ PRELOAD_DATA_FILE = _SRC_DIR / 'examples/example_02.json'
135
+ INITIAL_PROMPT_TEMPLATE = _SRC_DIR / 'prompts/initial_template_v4_two_cols_img.txt'
136
+ REFINEMENT_PROMPT_TEMPLATE = _SRC_DIR / 'prompts/refinement_template_v4_two_cols_img.txt'
137
 
138
  LLM_PROGRESS_MAX = 90
139
+ ICONS_DIR = _SRC_DIR / 'icons/png128/'
140
  TINY_BERT_MODEL = 'gaunernst/bert-mini-uncased'
141
+ EMBEDDINGS_FILE_NAME = _SRC_DIR / 'file_embeddings/embeddings.npy'
142
+ ICONS_FILE_NAME = _SRC_DIR / 'file_embeddings/icons.npy'
143
 
144
  PPTX_TEMPLATE_FILES = {
145
  'Basic': {
146
+ 'file': _SRC_DIR / 'pptx_templates/Blank.pptx',
147
  'caption': 'A good start (Uses [photos](https://unsplash.com/photos/AFZ-qBPEceA) by [cetteup](https://unsplash.com/@cetteup?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-foggy-forest-filled-with-lots-of-trees-d3ci37Gcgxg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)) 🟧'
148
  },
149
  'Ion Boardroom': {
150
+ 'file': _SRC_DIR / 'pptx_templates/Ion_Boardroom.pptx',
151
  'caption': 'Make some bold decisions πŸŸ₯'
152
  },
153
  'Minimalist Sales Pitch': {
154
+ 'file': _SRC_DIR / 'pptx_templates/Minimalist_sales_pitch.pptx',
155
  'caption': 'In high contrast ⬛'
156
  },
157
  'Urban Monochrome': {
158
+ 'file': _SRC_DIR / 'pptx_templates/Urban_monochrome.pptx',
159
  'caption': 'Marvel in a monochrome dream ⬜'
160
  },
161
  }
src/slidedeckai/helpers/__init__.py ADDED
File without changes
{helpers β†’ src/slidedeckai/helpers}/chat_helper.py RENAMED
@@ -27,24 +27,17 @@ class AIMessage(ChatMessage):
27
  super().__init__(content, 'ai')
28
 
29
 
30
- class StreamlitChatMessageHistory:
31
- """Chat message history stored in Streamlit session state."""
32
 
33
- def __init__(self, key: str):
34
- self.key = key
35
- if key not in st.session_state:
36
- st.session_state[key] = []
37
-
38
- @property
39
- def messages(self):
40
- return st.session_state[self.key]
41
 
42
  def add_user_message(self, content: str):
43
- st.session_state[self.key].append(HumanMessage(content))
44
 
45
  def add_ai_message(self, content: str):
46
- st.session_state[self.key].append(AIMessage(content))
47
-
48
 
49
  class ChatPromptTemplate:
50
  """Template for chat prompts."""
 
27
  super().__init__(content, 'ai')
28
 
29
 
30
+ class ChatMessageHistory:
31
+ """Chat message history stored in a list."""
32
 
33
+ def __init__(self):
34
+ self.messages = []
 
 
 
 
 
 
35
 
36
  def add_user_message(self, content: str):
37
+ self.messages.append(HumanMessage(content))
38
 
39
  def add_ai_message(self, content: str):
40
+ self.messages.append(AIMessage(content))
 
41
 
42
  class ChatPromptTemplate:
43
  """Template for chat prompts."""
{helpers β†’ src/slidedeckai/helpers}/file_manager.py RENAMED
@@ -8,10 +8,7 @@ import sys
8
  import streamlit as st
9
  from pypdf import PdfReader
10
 
11
- sys.path.append('..')
12
- sys.path.append('../..')
13
-
14
- from global_config import GlobalConfig
15
 
16
 
17
  logger = logging.getLogger(__name__)
 
8
  import streamlit as st
9
  from pypdf import PdfReader
10
 
11
+ from ..global_config import GlobalConfig
 
 
 
12
 
13
 
14
  logger = logging.getLogger(__name__)
{helpers β†’ src/slidedeckai/helpers}/icons_embeddings.py RENAMED
@@ -11,10 +11,7 @@ import numpy as np
11
  from sklearn.metrics.pairwise import cosine_similarity
12
  from transformers import BertTokenizer, BertModel
13
 
14
- sys.path.append('..')
15
- sys.path.append('../..')
16
-
17
- from global_config import GlobalConfig
18
 
19
 
20
  tokenizer = BertTokenizer.from_pretrained(GlobalConfig.TINY_BERT_MODEL)
@@ -28,9 +25,9 @@ def get_icons_list() -> List[str]:
28
  :return: The icons file names.
29
  """
30
 
31
- items = pathlib.Path('../' + GlobalConfig.ICONS_DIR).glob('*.png')
32
  items = [
33
- os.path.basename(str(item)).removesuffix('.png') for item in items
34
  ]
35
 
36
  return items
 
11
  from sklearn.metrics.pairwise import cosine_similarity
12
  from transformers import BertTokenizer, BertModel
13
 
14
+ from ..global_config import GlobalConfig
 
 
 
15
 
16
 
17
  tokenizer = BertTokenizer.from_pretrained(GlobalConfig.TINY_BERT_MODEL)
 
25
  :return: The icons file names.
26
  """
27
 
28
+ items = GlobalConfig.ICONS_DIR.glob('*.png')
29
  items = [
30
+ item.stem for item in items
31
  ]
32
 
33
  return items
{helpers β†’ src/slidedeckai/helpers}/image_search.py RENAMED
File without changes
{helpers β†’ src/slidedeckai/helpers}/llm_helper.py RENAMED
@@ -8,9 +8,7 @@ import urllib3
8
  from typing import Tuple, Union, Iterator, Optional
9
 
10
 
11
- sys.path.append('..')
12
-
13
- from global_config import GlobalConfig
14
 
15
  try:
16
  import litellm
 
8
  from typing import Tuple, Union, Iterator, Optional
9
 
10
 
11
+ from ..global_config import GlobalConfig
 
 
12
 
13
  try:
14
  import litellm
{helpers β†’ src/slidedeckai/helpers}/pptx_helper.py RENAMED
@@ -16,12 +16,9 @@ from dotenv import load_dotenv
16
  from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
17
  from pptx.shapes.placeholder import PicturePlaceholder, SlidePlaceholder
18
 
19
- sys.path.append('..')
20
- sys.path.append('../..')
21
-
22
- import helpers.icons_embeddings as ice
23
- import helpers.image_search as ims
24
- from global_config import GlobalConfig
25
 
26
 
27
  load_dotenv()
 
16
  from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
17
  from pptx.shapes.placeholder import PicturePlaceholder, SlidePlaceholder
18
 
19
+ from . import icons_embeddings as ice
20
+ from . import image_search as ims
21
+ from ..global_config import GlobalConfig
 
 
 
22
 
23
 
24
  load_dotenv()
{helpers β†’ src/slidedeckai/helpers}/text_helper.py RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/0-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/1-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/123.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/2-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/3-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/4-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/5-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/6-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/7-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/8-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/9-circle.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/activity.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/airplane.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/alarm.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/alien-head.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/alphabet.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/amazon.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/amritsar-golden-temple.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/amsterdam-canal.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/amsterdam-windmill.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/android.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/angkor-wat.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/apple.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/archive.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/argentina-obelisk.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/artificial-intelligence-brain.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/atlanta.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/austin.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/automation-decision.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/award.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/balloon.png RENAMED
File without changes
{icons β†’ src/slidedeckai/icons}/png128/ban.png RENAMED
File without changes