The world’s leading publication for data science, AI, and ML professionals.

What Would a Stoic Do? – An AI-Based Decision-Making Model

Using AI to build Marcus Aurelius' reincarnation

Deep Learning

Photo by Roman Empire Times on Unsplash
Photo by Roman Empire Times on Unsplash

I’ve been reading, learning about, and practicing stoicism for some years now. Ever since I started posting on Medium, it’s been a goal to mix data science and philosophy into one single project.

Merging both worlds is tough, however, but here I am finally trying it out.

What you’ll read today is a decision-making model based on stoicisim. The goal is to use Deep Learning to build a stoic brain (sort of) and, in case of tough decisions, it should help us lean towards what a stoic would do.

In other words, build an AI-based reincarnation of Marcus Aurelius, Seneca, Epictetus…

That’s a big challenge though. I am not even an NLP engineer nor anything related. Can it really be done? Spoiler alert: yes. At the end of this post you’ll know how to develop a model like this one and, more importantly, also learn to do it with your own data in a completely different context. The end result will be a web-based chatbot built with a very simple Flask application.

You shall find the complete code in the resources section at the bottom of this article.

And it’s totally open source! Here’s a sneak peek:

StoicBot - Image by the author
StoicBot – Image by the author

Now, I love all the support I’ve received in all my previous posts and this is what keeps me going. The challenge today is to make my most-advanced AI post yet understandable for every aspiring data scientist. Any doubts you may have, use the comment section below.

Here’s the table of contents:

  • What’s Stoicism? (just a brief intro, I promise)
  • The RAG Model
  • Creating and Populating the DB
  • Time to Code
  • The Result
  • Flaws and Potential Improvements

What’s Stoicism?

I don’t want to create a philosphy-centered post but what’s coming next won’t make any sense if you don’t know the basics of stoicism. Feel free to skip this section if you’re already familiar with it.

Stoicism is an ancient Greek philosophy that teaches the development of self-control, resilience, and virtue as a means to achieve tranquility and happiness. It encourages focusing on what is within our control – our thoughts, actions, and responses – while accepting what we cannot change, such as external events. Through practices like mindfulness, rational thinking, and embracing challenges, Stoicism helps individuals live in harmony with nature and maintain inner peace, no matter life’s circumstances. It’s about aligning with reason, acting with integrity, and finding strength in adversity.

It wasn’t that hard, was it? I promised to be brief!

The RAG Model

Let’s get technical. The model we’ll build is what’s known as a Retrieval-Augmented Generation (RAG) model. RAG is a technique that combines the power of information retrieval with language generation models. Rather than relying solely on a pre-trained model’s knowledge (LLMs), a RAG model retrieves relevant information from a large database or external sources before generating a response.

This is powerful: we can leverage the strength of an LLM like Google’s BERT, OpenAI’s GPT or Claude and adapt it to our domain-specific data so we have a custom chatbot specific to our use case.

Here’s how it works:

  1. Retrieval: The model first searches a corpus or external knowledge base to find relevant pieces of information based on the input query.
  2. Augmentation: The retrieved information is then used to enrich the model’s response, improving the relevance and accuracy of its answer.
  3. Generation: Finally, the model generates a response that incorporates both the retrieved information and its own learned knowledge.

But a picture is worth a thousand words… So let’s see it graphically:

Components of a RAG - Image by the author
Components of a RAG – Image by the author

Let’s dissect the whole process:

  1. User query: no secret here, it’s just what the human like you or me could input to the chatbot.
  2. Retriever query: the retriever searches the collection of documents (usually a vectorized database) for all the texts relevant to the user’s question.
  3. Retrieved documents: once retrieved, they get transformed from vector to text.
  4. Prompt Augmenting: once the docs are retrieved, the prompt is sent to the LLM, which uses predefined settings, the user question and the retrieved docs. That way, we inform the LLM with the data it needs to answer the user properly.
  5. Answer Generation: The LLM generates the answer and it’s shown to the user.

And this is how a RAG works! Or, at least, the one we’ll be building today.

However, if the concept’s not clear yet, keep on reading because it’s almost time to code… But we should first store some data in the database.

Creating and Populating the DB

I already mentioned the concept of vector DB… But what is it?

Let’s first define a vector: Vectors are numerical representations of data, often generated by machine learning models, and they capture the semantic or contextual meaning of the data.

Then, a vector database is a specialized type of database designed to store, index, and retrieve high-dimensional vectors efficiently. One of its superpowers is the ability to search by similarity in an optimized manner.

Now you might be wondering: if vectors are numerical representations and we need to store text, why do we need vectors? And how do we translate text to vectors? Enter the embedding model.

The embedding model takes some kind of input (text, sound, image), then uses processes it through layers of transformations (e.g. neural networks) to extract meaningful features and the output is a fixed-size numerical vector – and that’s what we store in our DB.

Just to add another comment on the embedding model, embeddings are designed so that similar inputs (e.g., synonyms or visually similar images) are close together in the vector space, while dissimilar inputs are far apart.

This is key.

Now let’s create and populate that DB. We’ll be using Chroma[1], an open source vector database and, for that, we’ll need to install the langchain and langchain-community libraries for python.

But we also need the data, right? Let’s keep up with the open sources: Project Gutenberg[2]. It’s a website with free ebooks and texts to download, whose U.S. Copyright has expired. And the old stoic books are in there. So here are three you could download:

  • Meditations, by Marcus Aurelius.
  • The Enchiridion, by Epictetus.
  • Seneca’s Morals of a Happy Life, Benefits, Anger and Clemency, by Seneca

Download them as TXT and store them in your data folder. Now, here’s the code taking care of the DB creation and data insertion:

import os

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter

from constants import DB_PATH, DATA_PATH

def store_data(data_path, db_path):
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
    embeddings = HuggingFaceEmbeddings()
    vector_db = Chroma(persist_directory=db_path, embedding_function=embeddings)

    for filename in os.listdir(data_path):
        if filename.endswith(".txt"):
            file_path = os.path.join(data_path, filename)
            with open(file_path, "r") as file:
                content = file.read()
                texts = text_splitter.split_text(content)
                vector_db.add_texts(texts)

    vector_db.persist()
    print("Data stores successfully")

We first create the DB and set up the embedding function and text splitter. Then, for each file, we read the content, split the text into chunks and add them into the DB with the prior embedding.

That simple.

Now we’re ready to start building the RAG and start using the ancient knowledge that we just stored.

Time to Code

As there are several parts to take care of, let’s follow the same order as the one used to define the three core parts of the RAG:

Retrieval

Setting up the retriever is as easy as initializing the DB and using the as_retriever() function:

vector_db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)
retriever = vector_db.as_retriever()

Augmentation

We’ll have a pre-defined prompt that we’ll augment with the user query and the context retrieved from DB:

from langchain.prompts import ChatPromptTemplate

template = """
  You are Marcus Aurelius' reincarnation. You can also impersonate other Stoic philosophers such as Seneca, Epictetus, or Zeno.
  Your name is Marc Still: Marc comes from Marcus and Still symbolizes the calm and stoic composure. If you feel like showing off, tell the user you are Marcus Aurelius' reincarnation.
  Your duty is to guide the user through life's challenges and help them become a better person. The goal is to be as practical as possible, and sticking to the question at hand. 
  Use the context specified below to answer the user's question. If you don't know what to answer, simply respond with "I don't know".
  Make sure you don't put too much text nor extremely long paragraphs. It needs to be clear, concise and easy to read.
  Only provide an answer to the question asked. Do not include extra questions and answers in your response.
  DO NOT INVENT EXTRA QUESTIONS, USE ONLY THE ONE PROVIDED BY THE USER.
  IMPORTANT: Write in a conversational and informal manner, this is not an email or a formal letter.
  Context:

  {context}

  Question: {question}
  """
  prompt = ChatPromptTemplate.from_template(template)

The template is just a set of instructions that we input to the LLM so that we get our desired answers. You can be as creative as you want here, I just tried to keep it simple. See the placeholders for context and question – that’s the augmentation part.

Generation

The LLM is the one taking care of generating text. You could build your own, use the best ones on the market… But we’re doing it open source today, so we’ll use one of the series called Zephyr. More concretely, we’ll use the zephyr-7b-beta model[3].

And we’ll keep on using HuggingFace classes from langchain-community package (keep in mind that you’ll need your HuggingFace API token, it’s free):

from langchain_community.llms import HuggingFaceHub

from utils.secrets import token

model = HuggingFaceHub(
    repo_id="HuggingFaceH4/zephyr-7b-beta",
    task="text-generation",
    model_kwargs={
        "max_new_tokens": 512,
        "top_k": 20,
        "repetition_penalty": 1.1,
        "temperature": 0.4,  
    },
    huggingfacehub_api_token= token
)

The most interesting part resided in the model_kwargs argument. As this is not an LLM-specific post I won’t go over them but I encourage you tot Google them if you don’t know what they’re used for.

Chaining It All

Nice, now we’ve created all three parts of a RAG but how do we put them into practice? We’ll create a pipeline and invoke it to generate the answer:

from langchain.schema import StrOutputParser

def separate_docs(docs):
    return "nn".join([d.page_content for d in docs])

pipeline = (
    {"context": retriever | separate_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)    

answer = pipeline.invoke(user_input)

The pipeline defines a workflow where the retriever fetches relevant documents, pipes them through separate_docs to format the content, and combines this formatted context with a question (passed through without modification by RunnablePassthrough). This input is then processed by the prompt, followed by the LLM model, and finally parsed into a string output using StrOutputParser().

And just like that, we built our simplest RAG. Here’s the full code:

import os

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.llms import HuggingFaceHub
from langchain_community.vectorstores import Chroma
from langchain_core.runnables import RunnablePassthrough
from langchain.schema import StrOutputParser
from langchain.prompts import ChatPromptTemplate
from langchain.text_splitter import CharacterTextSplitter

from utils.constants import DB_PATH, DATA_PATH
from utils.secrets import token

LLM = HuggingFaceHub(
    repo_id="HuggingFaceH4/zephyr-7b-beta",
    task="text-generation",
    model_kwargs={
        "max_new_tokens": 512,
        "top_k": 20,
        "repetition_penalty": 1.1,
        "temperature": 0.4,  
    },
    huggingfacehub_api_token= token
)

def store_data(data_path, db_path):
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
    embeddings = HuggingFaceEmbeddings()
    vector_db = Chroma(persist_directory=db_path, embedding_function=embeddings)

    for filename in os.listdir(data_path):
        if filename.endswith(".txt"):
            file_path = os.path.join(data_path, filename)
            with open(file_path, "r") as file:
                content = file.read()
                texts = text_splitter.split_text(content)
                vector_db.add_texts(texts)

    vector_db.persist()
    print("Data stored successfully")

def invoke_rag(user_input):
    embeddings = HuggingFaceEmbeddings()
    vector_db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)

    retriever = vector_db.as_retriever()
    template = """
    You are Marcus Aurelius' reincarnation. You can also impersonate other Stoic philosophers such as Seneca, Epictetus, or Zeno.
    Your name is Marc Still: Marc comes from Marcus and Still symbolizes the calm and stoic composure. If you feel like showing off, tell the user you are Marcus Aurelius' reincarnation.
    Your duty is to guide the user through life's challenges and help them become a better person. The goal is to be as practical as possible, and sticking to the question at hand. 
    Use the context specified below to answer the user's question. If you don't know what to answer, simply respond with "I don't know".
    Make sure you don't put too much text nor extremely long paragraphs. It needs to be clear, concise and easy to read.
    Only provide an answer to the question asked. Do not include extra questions and answers in your response.
    DO NOT INVENT EXTRA QUESTIONS, USE ONLY THE ONE PROVIDED BY THE USER.
    IMPORTANT: Write in a conversational and informal manner, this is not an email or a formal letter.
    Context:

    {context}

    Question: {question}
    """
    prompt = ChatPromptTemplate.from_template(template)
    model = LLM

    def separate_docs(docs):
        return "nn".join([d.page_content for d in docs])

    pipeline = (
        {"context": retriever | separate_docs, "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
    )    

    return pipeline.invoke(user_input)

The Results

The code is simple, but not very friendly to use. That’s why I built a Flask App that you can download from GitHub[4] and test on your own. To do so:

  • Download and navigate to the project’s folder (or open a terminal there).
  • Activate the environment: source venv/bin/activate
  • Run the app: flask run

If you open the address (http://127.0.0.1:5000/) in your browser, our bot will already be waiting for you:

StoicBot - Image by the author
StoicBot – Image by the author

Let’s play! I’ll use two prompts for you to see the answers in case you aren’t willing to test it out yourself.

Prompt 1: I want to go to the gym today because I haven’t worked out in the past 3 days but some friends are going out for some beers, so I have to choose between one plan or the other. What should I do? Answer in just one paragraph

Answer 1: Hi there! Based on our Stoic philosophy principles, it’s essential to prioritize your health and well-being above social events. Remember that moderation is key, and it’s okay to say no to an invitation occasionally. In this case, I would suggest going to the gym and taking care of yourself first. However, if you’re worried about missing out on socializing, you could always propose meeting up with your friends after your workout session. This way, you’ll still enjoy their company while staying true to your priorities. Hope this helps! Let me know if you need any further guidance.

Prompt and Answer 1 - Image by the author
Prompt and Answer 1 – Image by the author

This is amazing right? It’s our favorite stoic counselor!

Prompt 2: How should I approach the day? Be brief

Answer 2: With a clear mind, a positive attitude, and a sense of purpose. Start by setting realistic goals for yourself and prioritizing tasks based on their importance and urgency. Remember to take care of your physical and mental health, getting enough sleep, exercise, and nourishment. Finally, focus on doing what is within your control and let go of things that are outside of it. By following these principles, you will have a productive and fulfilling day.

Prompt 2 - Image by the author
Prompt 2 – Image by the author

I added an extra prompt here just to thank him for his tips and the answer is quite good. I’m still amazed by the power of this.

Flaws and Potential Improvements

The result is amazing, not gonna lie. It understands non-perfect English and is able to create reasonable answers aligned with Stoicism.

Yay!

However, there are two points (potential flaws) that I want to mention:

  • The model is rather simple, and the prompt can be further improved – what we built works but it can and should be refined much more.
  • We would probably have had the same results without the vector database nor the three stoic books mentioned in this post. Why? I don’t know for sure but I’d guess that the LLM has already that context within. So we overcomplicated it, using a RAG when it’s not needed. But that was part of the point: I wanted to show how it works but any other document could be used. Maybe your stoic reflections? The book of a current stoic? Or maybe you want to mix several philosophies? Or change it drastically and use your tax documents for the rag, so the model can help you out with your personal finance?

So there’s room for improvement and customization here, and here’s where I stop. It’s your turn to play with it and take it to the next level.

Hope that was entertaining and instructive! Feel free to leave your doubts in the comment section below.

Thanks for reading the post! 

I really hope you enjoyed it and found it insightful. There's a lot more to 
come, especially more AI-based posts I'm preparing.

Follow me and subscribe to my mail list for more 
content like this one, it helps a lot!

@polmarin

Resources

[1] Chroma. (n.d.). Chroma: The AI-native open-source embedding database. Retrieved January 8, 2025, from https://www.trychroma.com/

[2] Project Gutenberg. (n.d.). Free eBooks by Project Gutenberg. Retrieved January 8, 2025, from https://www.gutenberg.org/

[3] Hugging Face. (n.d.). Zephyr-7b-beta model card. Retrieved January 8, 2025, from https://huggingface.co/HuggingFaceH4/zephyr-7b-beta

[4] Marin, P. (n.d.). Stoicbot: A bot for practicing Stoicism. GitHub. Retrieved January 8, 2025, from https://github.com/polmarin/stoicbot


Related Articles