If you have a collection of FAQs that you want to be easily accessible for your business users, then a chatbot might be the answer. This blog explains how to create such a (non-hallucinating) chatbot on SAP's Business Technology Platform by leveraging the Generative AI Hub and SAP HANA Cloud's vector engine.
Table of contents
Currently the blogging framework doesn't allow for hyperlinks to areas in the same document, hence one cannot jump from the above Table of Contents to the relevant Chapters. For now, please just scroll down.
A collection of Frequently Asked Questions can be a great help to deal with common requests for specific information. However, the longer the list, the harder it can be to find the one piece of information one is looking for. Having to scroll through a long list can be tedious, and a simple text search might miss the one item you are looking for.
Hence a chatbot can be very useful, especially if it can deal with user questions, that are phrased differently to the curated list of Questions and Answers.
Maybe you are rolling out new software to your users (S/4?) and want to help them along finding their feet in the new system through an FAQ chatbot. Or you have a list of FAQs for any other purpose, whether intended for internal colleagues or external contacts such as customers. A chatbot that leverages the list of FAQs could help the users along.
In this blog you see how such a chatbot can be created, based on a fairly short list of FAQs about SAP and working at SAP. Whilst this example is quite simplified, the same overall approach has been working well for a customer with a list of 200+ FAQs. Maybe you find some inspiration in this blog for your own project.
All code that should be needed can be downloaded from this GitHub Repository "Creating an FAQ Chatbot on BTP". As always, please bear in mind, that any code shared here comes without support or guarantee.
Before looking at any code, let's first get an understanding of how the chatbot works at high level.
The architecture and process flows are based on the requirement, that the chatbot must not hallucinate. As exciting as Large Language Models are, when producing text they are producing text that seems likely to them, but the text might be simply made up and incorrect.
To ensure that the chatbot can become a trusted advisor, hallucinations have to be avoided. We achieve this, by not producing any new text at all. Instead, we use the Large Language Model to understand the user's request, find the predefined Question from the existing FAQ that best matches that request, and it is easy for the chatbot to return the predefined Answer that belongs to the chosen Question.
The overall process flow for an incoming question from a user is:
Administrators can upload Questions and Answers with this simple process flow:
Questions and Answers are stored in two separate tables. This allows for 1:n relationships between Answers and Question. This means, for each predefined Answer 1 or more Questions can be associated. This will be useful when improving and adjusting the bot to different terminology from the different users. After all, there are many ways to phrase the same question.
The overall underlying Architecture is:
So to follow the implementation hands-on, you require these components:
This blog assumes that you already have some familiarity with Python and Jupyter Notebooks. However, this project could also be a starting point to become familiar with those components. Personally, I like Miniconda to create a local Python environment and local Jupyter Notebooks. @YannickSchaper gives a great overview how to get started with our Python package hana_ml. That package allows Data Scientists to work from Python with data that remains in SAP HANA Cloud (or SAP Datasphere). It can even trigger Machine Learning in SAP HANA Cloud, but we will use it here mostly to upload data, enrich the data and to trigger the vector engine.
We will use a fairly short list of FAQs as basis for the Chatbot. These are just a few examples taken from the FAQs about SAP overall (ie history and sustainability) and Jobs @ SAP. Kudos to who knows by heart what the abbreviation "SAP" actually stand for... 😀 For all others there is the FAQ and our little custom chatbot.
The Questions and Answers for our chatbot are saved in two separate Excel files. This allows for specifying multiple Questions that belong to the same single Answer. Remember, all files and code used in this blog can be downloaded from this repository. You should just have to enter your own logon credentials for SAP HANA Cloud and the Generative AI Hub into the file credentials.json. The code to upload the FAQs is in 010 Upload Questions and Answers.ipynb.
Uploading the data to SAP HANA Cloud is easy with the hana_ml Python package. Establish a connection from Python to SAP HANA Cloud.
import hana_ml.dataframe as dataframe
conn = dataframe.ConnectionContext(
address = SAP_HANA_CLOUD_ADDRESS,
port = SAP_HANA_CLOUD_PORT,
user = SAP_HANA_CLOUD_USER,
password = SAP_HANA_CLOUD_PASSWORD,
)
conn.connection.isconnected()
Load the questions for into a Pandas DataFrame.
#!pip install openpyxl
import pandas as pd
df_data= pd.read_excel ('FAQ_QUESTIONS.xlsx')
df_data.head(5)
And upload it to SAP HANA Cloud. The table FAQ_QUESTIONS will be created automatically. Note how the column "QUESTION_VECTOR" will be created of type "REAL_VECTOR". This was added with version 2024.02 (QRC 1/2024). For now the column is empty. The vectors will be created and saved later.
import hana_ml.dataframe as dataframe
df_remote = dataframe.create_dataframe_from_pandas(connection_context=conn,
pandas_df=df_data,
table_name='FAQ_QUESTIONS',
force=True,
replace=False,
table_structure = {'QUESTION_VECTOR': 'REAL_VECTOR(1536)'})
The data is uploaded. You can have a quick look at a few rows. AID is the ID that identifies an answer. QID is the ID of an individual question that belongs to the answer. This composite index allows for multiple questions that can be responded to with the same answer.
df_remote.head(5).collect()
And follow the same steps to upload the Answers into table FAQ_ANSWERS.
df_data= pd.read_excel ('FAQ_ANSWERS.xlsx')
df_remote = dataframe.create_dataframe_from_pandas(connection_context=conn,
pandas_df=df_data,
table_name='FAQ_ANSWERS',
force=True,
replace=False)
df_remote.head(5).collect()
The questions are uploaded to SAP HANA Cloud, but so far only as text. Now we need to fill the QUESTION_VECTOR column with the vectorised/embedded version of the text. We use the Generative AI Hub to create those embeddings. You find the code of this section also in 020 Create embeddings of new Questions.ipynb.
SAP's Python package to work with the Generative AI Hub is called generative-ai-hub-sdk. Store the logon credentials of the Generative AI Hub in environment variables. You find these values in a Service Key of SAP AI Core.
import os
os.environ["AICORE_CLIENT_ID"] = "YOUR clientid"
os.environ["AICORE_CLIENT_SECRET"] = "YOUR clientsecret"
os.environ["AICORE_AUTH_URL"] = "YOUR url"
os.environ["AICORE_RESOURCE_GROUP"] = "your resource group, ie: default"
os.environ["AICORE_BASE_URL"] = "YOUR AI_API_URL"
Specify the embeddings model we want to use on the Generative AI Hub. This model must have been deployed there already.
from gen_ai_hub.proxy.langchain.openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings(proxy_model_name='text-embedding-ada-002')
Now identify which rows in the FAQ_QUESTIONS table are missing the embeddings. Keep the AID and QID columns of those rows in a local Pandas DataFrame.
df_remote_toprocess = conn.sql('''SELECT "AID", "QID", "QUESTION" FROM FAQ_QUESTIONS WHERE QUESTION_VECTOR IS NULL ORDER BY "AID", "QID" ''')
Iterate through that list of questions. For each question obtain the embedding from the Generative AI Hub and store it in the QUESTION_VECTOR column of the FAQ_QUESTIONS table.
import time
dbapi_cursor = conn.connection.cursor()
rowids_toprocess = df_remote_toprocess.select("AID", "QID", "QUESTION").collect()
for index, row_toprocess in rowids_toprocess.iterrows():
my_embedding = embedding.embed_documents(row_toprocess['QUESTION'])
my_embedding_str = str(my_embedding[0])
my_aid = row_toprocess['AID']
my_qid = row_toprocess['QID']
print(str(my_aid) + '-' + str(my_qid) + ': ' + str(my_embedding_str[:100]))
dbapi_cursor.execute(f"""UPDATE "FAQ_QUESTIONS" SET "QUESTION_VECTOR" = TO_REAL_VECTOR('{my_embedding_str}')
WHERE "AID" = {my_aid} AND "QID" = {my_qid};""")
All questions should now have the text vectorised.
df_remote = conn.table('FAQ_QUESTIONS').sort(['AID', 'QID'])
df_remote.head(5).collect()
Now let's play through a scenario of a user asking a question, which the application is trying to answer. This section's code is also in 030 Ask a Question.ipynb.
user_question = 'What is the meaning of the letters SAP?'
Vectorise the question through the Generative AI Hub, so that SAP HANA Cloud can compare it with the vectors already stored in the FAQ_QUESTIONS table. This identifies the most similar questions in the system. Notice how the similarity to many questions is calculated as the perfect match (similarity = 1). The embedding model text-embedding-ada-002 transforms a number of these short sentences into identical vectors.
from gen_ai_hub.proxy.langchain.openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings(proxy_model_name='text-embedding-ada-002')
user_question_embedding = embedding.embed_documents((user_question))
user_question_embedding_str = str(user_question_embedding[0])
sql = f'''SELECT TOP 200 "AID", "QID", "QUESTION", COSINE_SIMILARITY("QUESTION_VECTOR", TO_REAL_VECTOR('{user_question_embedding_str}')) AS SIMILARITY
FROM FAQ_QUESTIONS ORDER BY "SIMILARITY" DESC, "AID", "QID" '''
df_remote = conn.sql(sql)
df_remote.head(20).collect()
Take the most promising sentences and format the collection into a string, which will become part of the prompt that is sent to GPT. We will see that each question is preceded by an ID, which is a combination of the QID and AID. This new ID will help us to retrieve the corresponding data from the tables in SAP HANA Cloud.
top_n = max(df_remote.filter('SIMILARITY > 0.95').count(), 10)
df_data = df_remote.head(top_n).select('AID', 'QID', 'QUESTION').collect()
df_data['ROWID'] = df_data['AID'].astype(str) + '-' + df_data['QID'].astype(str) + ': '
df_data = df_data[['ROWID', 'QUESTION']]
candiates_str = df_data.to_string(header=False,
index=False,
index_names=False)
Prepare the full prompt, by specifying the task.
llm_prompt = f'''
Task: which of the following candidate questions is closest to this one?
{user_question_upper}
Only return the ID of the selected question, not the question itself
-----------------------------------
Candidate questions. Each question starts with the ID, followed by a :, followed by the question
{candiates_str}
'''
print(llm_prompt)
Specify which Large Language Model should be used on the Generative AI Hub (this model needs to be deployed there) and send the prompt off.
AI_CORE_MODEL_NAME = 'gpt-4-32k'
from gen_ai_hub.proxy.native.openai import chat
messages = [{"role": "system", "content": llm_prompt}
]
kwargs = dict(model_name=AI_CORE_MODEL_NAME, messages=messages)
response = chat.completions.create(**kwargs)
llm_response = response.choices[0].message.content
llm_response
The model responds '1001-1', which refers to the first Question 1 of Answer 1001. And indeed, that's the correct match. Now get the answer to that question from SAP HANA Cloud.
aid = qid = None
if len(llm_response.split('-')) == 2:
aid, qid = llm_response.split('-')
# From HANA Cloud get the question from the FAQ that matches the user request best
df_remote = conn.table('FAQ_QUESTIONS').filter(f''' "AID" = '{aid}' AND "QID" = '{qid}' ''').select('QUESTION')
matching_question = df_remote.head(5).collect().iloc[0,0]
# From HANA Cloud get the predefined answer of the above question from the FAQ
df_remote = conn.table('FAQ_ANSWERS').filter(f''' "AID" = '{aid}' ''').select('ANSWER')
matching_answer = df_remote.head(5).collect().iloc[0,0]
else:
matching_answer = "I don't seem to have an answer for that."
print(f'The user question was: {user_question}\nThe selected questoin from the FAQ is: {matching_question}\nWith the answer: {matching_answer}')
The mystery of what the letters SAP stand for has been solved.
We are happy with the core functionality and want to deploy this as a chatbot. You can choose from a number of components to create that User Interface, ie SAP Build Apps or UI5. My colleagues @BojanDobranovic and @botazhat actually already created such a Chatbot UI using SAP Build Apps. For this quick prototype I created a simple application with Python package streamlit, which I deployed on Cloud Foundry on the Business Technology Platform.
You can download that Cloud Foundry logic from the repository. Just make sure to enter your credentials for SAP HANA Cloud and the Generative AI Hub in the file faqbot.py. For productive use you can store these credentials also more elegantly as user-defined variables in Cloud Foundry, as shown in this blog. Also be aware that you may want to secure the Cloud Foundry URL. Otherwise your chatbot might be open to anyone on the Internet.
This blog has an example of deploying Python code on Cloud Foundry, in case you haven't tried this yet. You can then deploy it with this command.
cf7 push faqbot
The chatbot might not understand the request of a user if very different terminology, compared to the question stored in SAP HANA Cloud, is used. In this case, you can add that differently phrased question as a new entry to the FAQ_QUESTIONS table.
Try for example this question: "A Applications and P Products, but what about the S?". In my tests the selected question from the FAQ was "Do SAP employees participate in the company's success?", which is clearly a wrong match. To improve the chatbot's understanding, just add this additional question as new row to the FAQ_QUESTIONS.xlsx file. For that row you have to set AID to 1001 (to refer to the existing answer) with QID set to 2 (as this is the 2nd question for the same answer).
Then run the code from notebook 040 Add additional Questions and Answers.ipynb to upload only the new question (uploading all rows from the Excel file would drop the existing QUESTION_VECTORs).
import pandas as pd
df_q_local = pd.read_excel ('FAQ_QUESTIONS.xlsx')
# Download existing questions from SAP HANA Cloud
df_q_fromhana = conn.table('FAQ_QUESTIONS').drop('QUESTION_VECTOR').collect()
# Compare local data with data from SAP HANA Cloud to identify which questions are new
df_all = df_q_local.merge(df_q_fromhana, on=['AID', 'QUESTION', 'QID'],
how='left', indicator=True)
df_new = df_all[df_all['_merge'] == 'left_only']
df_new = df_new.drop('_merge', axis=1)
# Append new questions to existing SAP HANA Cloud table
import hana_ml.dataframe as dataframe
df_remote = dataframe.create_dataframe_from_pandas(connection_context=conn,
pandas_df=df_new,
table_name='FAQ_QUESTIONS',
force=False,
replace=False,
append=True
)
With the new row uploaded, you just need to create the QUESTION_VECTOR. Run the notebook 020 Create embeddings of new Questions.ipynb as before to create and save the new vector and the chatbot should now be able to understand this new question.
Similarly, you can of course add completely new Answers and Questions, just make sure to use the same AID in both tables (FAQ_QUESTIONS and FAQ_ANSWERS).
For Enterprise readiness you should also consider securing the Chatbot appropriately.
Our chatbot is now a user friendly user interface for a possibly long list of FAQs. However, you can go beyond this core functionality, for instance with
If you deploy such an FAQ chatbot, I would love to hear from you of course!
Happy chatboting
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.