Building an Inspirational Quote App with LLMs

January 1, 2024

Project Context

Every Friday at the day job, my team and I have an informal catch up session. As part of that we would often share quotes from the book 1001 Quotations to inspire you before you die by Robert Arp, we would take turns picking a theme and a random page number to share the quote from. After a while, I thought to myself taht this could be something that could be automated with LLMs and the Streamlit app, and so I embarked on the journey to do just that.

App Overview

Project requirements

Please note due to the nature of the app, you need an API key from OpenAI and Pexels to run the app.

App Configuration

APIs and Dependencies

The first part was to load the required dependencies for the app & retrieve the OpenAI api from the secrets file.

Show the code
import streamlit as st
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
import os
import yaml
import requests

if 'OPENAI_API_KEY' not in st.session_state:
    st.session_state['OPENAI_API_KEY'] = st.secrets['openai']

With this in hand we have what we need to start building the app.

Core App Components

The main part of the application is the interaction between the user and the LLMs. The user will provide a theme and a decade, and the LLM will generate a quote based on those inputs. We define the three as follows, first the LLM, then the quote prompt and finally the image prompt:

Show the code
llm = ChatOpenAI(
    model=selected_model,
    temperature=temperature,
    openai_api_key=st.session_state['OPENAI_API_KEY']
)

quote_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a knowledgeable assistant that generates meaningful quotes. Never repeat a previously generated quote. Each quote should be unique and original for the given theme and era."),
    ("user", """Generate a {theme} from the {decade} era. The quote must be different from these previous quotes: {prev_quotes}
    Format the response as:
    Quote: [The quote]
    Author: [Author's name]
    Context: [2-3 sentences of context]""")
])

image_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are an assistant that generates image search queries based on quotes."),
    ("user", "Create a specific image search query for a {theme} quote. The query should be descriptive but avoid names. Format: only return the search query.")
])

We define a few dynamic components that are passed to the arguments above, namely the selected model, the user is able to pick from 4o and 4o-mini. The temperature, which is a float value between 0 and 1, determines how creative the model is in generating the quote. The next key part is the quote prompt, this are the instructions that the LLM will use to generate the quote. The user will provide a theme and a decade, which will feed into the {decade} and {theme} placeholders in the prompt. similarly the image prompt allows the LLM to generate a search query based on the theme of the quote, using the Pexels API. Next we need to create the functions to actually retrieve the quote and image. We do this below:

Show the code
def get_image_url(query):
    try:
        headers = {'Authorization': st.secrets['PEXELS_API_KEY']}
        response = requests.get(
            f"https://api.pexels.com/v1/search?query={query}&per_page=1",
            headers=headers
        )
        return response.json()['photos'][0]['src']['large']
    except:
        return None

def generate_quote():
    prev_quotes_str = ", ".join(st.session_state['previous_quotes'])
    quote_response = llm.invoke(
        quote_prompt.format(
            theme=themes[selected_theme],
            decade=selected_decade,
            prev_quotes=prev_quotes_str
        )
    )
    
    # Store the new quote
    quote_text = quote_response.content.split('\n')[0].split(':', 1)[1].strip()
    st.session_state['previous_quotes'].add(quote_text)
    
    image_query = llm.invoke(
        image_prompt.format(
            theme=themes[selected_theme]
        )
    )
    image_url = get_image_url(image_query.content)
    return quote_response.content, image_url

The get_image_url function is a simple function that retrieves an image from the Pexels API based on the query generated by the LLM. The generate_quote function is the main function that generates the quote and image. It first formats the prompt with the selected theme and decade, then retrieves the quote and image query from the LLM. The quote is stored in the session state to prevent duplicates, and the image is retrieved using the get_image_url function.

App Layout & User Interaction

The final part of the app is the layout and user interaction. As previously mentioned, the user is able to select a few parameters in the app, such as the model, temperature, theme and decade. This are made available from a side bar using the st.sidebar function, along with the page configuration, the theme options and the quote decade are defined below:

Show the code
st.set_page_config(page_title="Daily Quote Generator", layout="wide")

themes = {
    "Inspiration & Motivation": "inspirational and motivational quotes that encourage personal growth",
    "Love & Relationships": "quotes about love, relationships, and human connection",
    "Philosophy & Wisdom": "philosophical quotes about life's deeper meanings",
    "Science & Innovation": "quotes about scientific discovery and innovation",
    "Art & Creativity": "quotes about artistic expression and creativity"
}

decades = [
    "Pre-1900s",
    "1900-1950",
    "1950-1980",
    "1980-2000",
    "2000-Present"
]

with st.sidebar:
    st.markdown("### 🎨 Customize Your Quote")
    selected_theme = st.selectbox("Choose a Theme", list(themes.keys()))
    selected_decade = st.selectbox("Choose an Era", decades)
    
    st.markdown("### ⚙️ Model Settings")
    selected_model = st.selectbox("Model",  ["gpt-4o-mini"])
    temperature = st.slider("Temperature", min_value=0.0, max_value=1.0, value=0.8, step=0.1,
                          help="Higher values make output more random, lower values more deterministic")

The app layout is set to wide to fit the whole screen. The themes and decades are defined as dictionaries and lists respectively, from which the user can select, to customise their generated quote. Next, we provide a custom theme to make the app abit more playful, by editing the button and the background color, as well as the colors of the sidebar settings. For this we use custom ccs:

Show the code
st.markdown("""
    <style>
    .stApp {
        background: linear-gradient(120deg, #FF9A8B 0%, #FF6A88 55%, #FF99AC 100%);
    }
    .stButton > button {
        background: linear-gradient(45deg, #2ecc71, #3498db);
        color: white;
        border: none;
        padding: 0.5rem 1rem;
        border-radius: 25px;
        font-size: 1.1rem;
        box-shadow: 0 4px 15px rgba(0,0,0,0.1);
        transition: transform 0.3s ease;
    }
    .stButton > button:hover {
        transform: translateY(-2px);
    }
    .quote-box {
        background: rgba(255, 255, 255, 0.95);
        padding: 2rem;
        border-radius: 15px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.1);
        margin: 1rem 0;
    }
    h1 {
        font-family: 'Segoe UI', sans-serif;
        color: white !important;
        text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
    }
    h3 {
        color: #2c3e50;
        font-size: 1.5rem !important;
    }
    .stSidebar {
        background-color: rgba(255, 255, 255, 0.1);
        backdrop-filter: blur(10px);
    }
    </style>
""", unsafe_allow_html=True)

Finally, we bring the core component of the app together using the st.columns functionality in Streamlit:

Show the code
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
    if st.button("Generate Quote", use_container_width=True):
        with st.spinner("✨ Creating your quote..."):
            try:
                quote_response, image_url = generate_quote()
                
                text_col, image_col = st.columns([3, 2])
                
                with text_col:
                    parts = quote_response.split('\n')
                    for part in parts:
                        if part.startswith("Quote:"):
                            st.markdown(f"### {part.split(':', 1)[1].strip()}")
                        elif part.startswith("Author:"):
                            st.markdown(f"*— {part.split(':', 1)[1].strip()}*")
                        elif part.startswith("Context:"):
                            st.markdown(f"\n{part.split(':', 1)[1].strip()}")
                
                with image_col:
                    if image_url:
                        st.markdown(
                            f"""
                            <div styl="display: flex; justify-content: center;">
                                <img sr="{image_url}" styl="max-width: 100%; height: auto;">
                            </div>
                            """,
                            unsafe_allow_html=True
                        )
                    else:
                        st.warning("Could not generate a relevant image.")
                
            except Exception as e:
                st.error(f"An error occurred: {str(e)}")

st.markdown("---")
st.markdown("<div styl='text-align: center;'>Powered by OpenAI and Pexels</div>", unsafe_allow_html=True)

That’s it! Don’t forget to check out the app!!