SAP is going full speed with AI agents on Joule. Indeed, it's incredible how such agents can address complex requests by orchestrating a set of generic tools they are given access to.
If you have a need to build your own custom AI agent on BTP, then this tutorial will hopefully be a good start.
Table of contents
You find all code of this tutorial in this repository.
And thanks go to @CanAbdulla for his early feedback on the blog before it went live.
Let's take a common business requirement as task for creating a custom BTP AI agent.
Send an email to Peter and let him know whether invoice XYZ been paid.
In the ideal world you would leverage SAP standard functionality, ie Joule. If that's not possible for whatever reason, we can use BTP to make our own agent. To make it easy to implement this example, the AI agent is not actually connecting to a live ERP system. Instead the response of the invoice status will be faked. However, the AI agent doesn't know that the invoice status is made up and it should not be too tricky to connect the agent to a SAP system.
Instead of hardcoding the workflow of getting the invoice status and sending the email, we provide the AI agent with specific tools (here Python functions) that it can decide on it's own how to use them and in which sequence. These function can take one or more parameters as input and return a response themselves:
An AI agent shines best when these tools can be used for a number of different purposes. Hence the AI agent is also given these additional tools for you to be creative with:
The architecture of the AI agent can be very lean. At a minimum you just need:
Only in case you want to implement the FAQ assistant that answers questions about SAP, you also need:
To follow the implementation hands-on, you require these components:
Optionally: For the AI assistant to be able to answer questions about SAP, you also need to implement the tutorial Creating an FAQ assistant as tool for a BTP AI agent.
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. The blog "Machine Learning with SAP HANA Cloud" that was co-written with @YannickSchaper gives an overview of setting up a miniconda environment.
You need to install these packages in your Notebook environment
All code for the BTP AI agent can be downloaded from this repository. First things first. In the file credentials.json enter your own logon details for SAP AI Core and your own SMTP email server. Now run the code from 010 BTP AI agent.ipynb. Load these credentials and apply them.
import json, os
with open('./credentials.json', 'r') as creds:
credentials = json.load(creds)
os.environ["AICORE_CLIENT_ID"] = credentials["SAP_AI_CORE"]["AICORE_CLIENT_ID"]
os.environ["AICORE_CLIENT_SECRET"] = credentials["SAP_AI_CORE"]["AICORE_CLIENT_SECRET"]
os.environ["AICORE_AUTH_URL"] = credentials["SAP_AI_CORE"]["AICORE_AUTH_URL"]
os.environ["AICORE_RESOURCE_GROUP"] = credentials["SAP_AI_CORE"]["AICORE_RESOURCE_GROUP"]
os.environ["AICORE_BASE_URL"] = credentials["SAP_AI_CORE"]["AICORE_BASE_URL"]
smtp_server = credentials["SMTP"]["SMTP_SERVER"]
smtp_port = credentials["SMTP"]["SMTP_PORT"]
smtp_user = credentials["SMTP"]["SMTP_USER"]
smtp_password = credentials["SMTP"]["SMTP_PASSWORD"]Now we start with the AI agent. The following functions will become tools for the agent. Each function starts with a comment of what the function does and which input parameters it requires. Those comments are useful for us humans, but they are also picked up by the agent to understand what each function does, which input it requires and what it returns. That's already sufficient for the agent to decide whether and how to use each tool/function.
To make the code work you will just need to make these adjustments:
import random
def get_invoice_status(invoice_id: str) -> str:
"""Returns an invoice's status, ie whether it has been paid or not.
Args:
invoice_id: The invoice id
"""
# This function mocks retrieving the invoice status from a live system
# See SAP's API documentation for the real / live API that can provide this information from your system, ie the InvoiceClearingStatus on
# https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/19d48293097f4a2589433856b034dfa5/cb3caf09bd6749c59f0765981032b74e.html?locale=en-US
options = ['Paid', 'Overdue', 'Unpaid, not due yet']
invoice_status = random.choice(options)
response = f"The status of invoice {invoice_id} is: {invoice_status}."
return response
def get_email_address(name: str) -> str:
"""Returns the person's email address
Args:
name: The name of the person whose email address is returned
"""
# This function mocks retrieving an email address from a live system
dict = {}
dict['Ewald'] = 'enteryourownemail@yourcompany.com.xyz'
dict['Stefan'] = 'enteryourownemail@yourcompany.com.xyz'
dict['Fabian'] = 'enteryourownemail@yourcompany.com.xyz'
if name in dict.keys():
response = dict[name]
else:
response = dict['Ewald']
return response
import smtplib, ssl
from email.mime.text import MIMEText
def send_email(recipient_name: str, email_address: str, email_text: str) -> str:
"""Sends emails. Returns a status update.
Args:
recipient_name: The name of the email recipient
email_address: The recipient's email address
email_text: The email's text that will be send
"""
# This function uses SMTP credentials to send an email directly from Python
# For productive use you may want to leverage a BTP component, for instance the Alert Notification service
# https://help.sap.com/docs/alert-notification/sap-alert-notification-for-sap-btp/email-action-type?locale=en-US
# Prepare email content
subject = 'Email from your SAP BTP AI Agent'
content = email_text
msg = MIMEText(content, 'plain')
msg['Subject'] = subject
msg['From'] = smtp_user
# Send the email
context = ssl.create_default_context()
with smtplib.SMTP_SSL(smtp_server, smtp_port, context=context) as server:
server.login(smtp_user, smtp_password)
server.sendmail(from_addr=smtp_user, to_addrs=email_address, msg=msg.as_string())
return f"I sent the email to {recipient_name} ({email_address}): \n{email_text}"
### TODO Requires the implementation of the FAQ assistant as described in:
### https://community.sap.com/t5/artificial-intelligence-and-machine-learning-blogs/hands-on-tutorial-creating-an-faq-assistant-as-tool-for-a-btp-ai-agent/ba-p/14027300
import requests
def answer_SAP_question(text: str) -> str:
"""Responds to questions about the company SAP
Args:
text: The question about SAP
"""
# Forward the incoming question to the AI assistant's REST-API
backend_api = "https://ENTERYOURURLFROMTHEAIASSISTANT/"
user_input = text
paylod = {'user_request': user_input}
headers = {'Accept' : 'application/json', 'Content-Type' : 'application/json'}
r = requests.get(backend_api, json=paylod, headers=headers, verify=False)
response = r.json()
# Obtain the response and response log
faq_response = response['faq_response']
faq_response_log = response['faq_response_log']
# Return the response
return faq_response
To attempt to retrieve the actual answer if the FAQ assistant's answer point to website.
import requests
from bs4 import BeautifulSoup
def get_text_from_link(link: str) -> str:
"""Returns the text of the given website link
Args:
link: The link of the webiste whose text is to be returned
"""
# Header to get automated access to SAP's website content
headers = {
'referer': 'https://www.scrapingcourse.com/ecommerce/',
'accept-language': 'en-US,en;q=0.9',
'content-type': 'application/json',
'accept-encoding': 'gzip, deflate, br',
'sec-ch-device-memory': '8',
'sec-ch-ua': '"Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"',
'sec-ch-ua-platform': "Windows",
'sec-ch-ua-platform-version': '"10.0.0"',
'sec-ch-viewport-width': '792'
}
# Obtain the website content
response = requests.get(link, headers=headers)
# Simple parsing to get the website's text
soup = BeautifulSoup(response.content, 'html.parser')
link_text = soup.body.get_text()
link_text = link_text.replace('\n', '')
link_text = link_text.replace('Javascript must be enabled for the correct page display', '')
return link_text
An example for webscraping in an AI agent tool. Update 15 April 2025: The canteen's website changed and the following scraping logic doesn't work anymore. The agent will not throw an error, but it cannot retrieve today's lunch menu anymore.
import requests
from datetime import datetime
def get_lunch_menu() -> str:
"""Returns today's menu of the SAP canteen in Zurich
"""
# Scrape the canteen's website
response = requests.get("https://circle.sv-restaurant.ch/de/menuplan/chreis-14/")
soup = BeautifulSoup(response.content, 'html.parser')
# Get current day of week
dt = datetime.now()
weekday_current = dt.weekday()
# If called on weekend, use Monday instead
if weekday_current < 5:
weekday_menu = weekday_current
else:
weekday_menu = 0
# Get date for which menu will be returned
dates_raw = soup.find_all(class_='date')
dates = []
for day in dates_raw:
dates.append(day.text)
date = dates[0] # Past dates are removed from the restaurant page
# Get menus for that date
menus = []
menus_raw = dates_raw = soup.find_all(id='menu-plan-tab' + str(weekday_menu))
menus_all_raw = soup.find(id='menu-plan-tab1')
menus_all = menus_all_raw.find_all(class_='menu-title')
for menu in menus_all:
if menu.text not in ['Lunch auf der Terrasse']:
menus.append(menu.text)
# Prepare the response with the above information
menu_flowtext = ''
for i in range(len(menus)):
menu_flowtext += " " + str(i+1) + ") " + menus[i]
menu_flowtext = menu_flowtext.lstrip()
response = f"On {weekday_menu}, the {date}, Chreis 14 serves {menu_flowtext}."
return response
An example for calling a REST-API from an AI agent tool.
import requests
def get_live_tv_arte() -> str:
"""Returns what is currently shown on the TV stastion ARTE
"""
response = requests.get('https://api.arte.tv/api/player/v2/config/de/LIVE')
data = response.json()
title = data['data']['attributes']['metadata']['title']
description = data['data']['attributes']['metadata']['description']
return title + ': ' + description
Now collect the names of these tool functions in a list. This list will make those functions accessible to the AI agent later on. In case you haven't implemented the AI assistant REST-API you need to remove "answer_SAP_question" from the list.
tools = [get_invoice_status, get_email_address, send_email, answer_SAP_question, get_text_from_link, get_lunch_menu, get_live_tv_arte]
We are ready for the best part, let's create the AI agent. Get the imports out of the way.
from langchain_core.messages import SystemMessage
from langgraph.graph import START, StateGraph, MessagesState
from langgraph.prebuilt import tools_condition, ToolNode
from gen_ai_hub.proxy.langchain.init_models import init_llm
import urllib3
urllib3.disable_warnings()
Initialise the Large Language Model and provide it with the list of tools / functions.
llm = init_llm('anthropic--claude-3.5-sonnet', max_tokens=300)
llm_with_tools = llm.bind_tools(tools)
Configure the AI agent's behaviour with a system message.
sys_msg = SystemMessage(content="You are a helfpul assistant tasked with answering questions about different topics. Your name is 'SAP BTP AI Agent'. Keep your answers short. After giving a response, do not ask for additional requests. Instead of referring to a link on your response call the function get_text_from_link to get the information from a given link yourself. Only use information that is provided to you by the different tools you are given. When sending email include a greeting and a salutation.")
Define the Assistant node, which leverages the Large Language model, the bound tools and the system message as basis of the AI agent.
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
Build and compile the graph, which defines the AI agent's capabilities. For full flexibility all tools are connected directly to the initial assistant node. Alternatively tools could be specifically assigned and dedicated as subcomponents to other tools.
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant",
# If the latest message (result) from assistant is a tool call -> tools_condition routes to Tools
# If the latest message (result) from assistant is not a tool call -> tools_condition routes to END
tools_condition,
)
builder.add_edge("tools", "assistant")
graph = builder.compile()
The Graph is built and can be visualised. There isn't much too see, since there is a Start node, followed by the assistant, to which all tools are connected. And there is an end. After all the code it's just good to have a visual...
Now test the AI agent! Start easy with something simple. Has a specific invoice been paid?
user_input = "Has invoice 42 been paid?"
agent_outcome = graph.invoke({"messages": [("user", user_input)]})
print(agent_outcome['messages'][-1].to_json()['kwargs']['content'])
And the answer in this case is:
"Yes, invoice 42 has been paid. The status of the invoice is confirmed as "Paid" according to our records."
How did the agent come to that conclusion? Pull the most important information from the AI agent's log.
messages_extract = []
for msg in agent_outcome['messages']:
msg_actor = type(msg).__name__
msg_text = msg.content
if msg_actor == 'AIMessage':
if msg_text == '':
msg_text = msg.tool_calls
if msg_actor == 'ToolMessage':
msg_actor = msg_actor + ' (' + msg.name + ')'
messages_extract.append([msg_actor, msg_text])
print(str(messages_extract).replace('],', '],\n'))[['HumanMessage', 'Has invoice 42 been paid?'],
['AIMessage', [{'name': 'get_invoice_status', 'args': {'invoice_id': '42'}, 'id': 'toolu_bdrk_01S8LnoAEMaXo2e7VStSkdA5', 'type': 'tool_call'}]],
['ToolMessage (get_invoice_status)', 'The status of invoice 42 is: Paid.'],
['AIMessage', 'Yes, invoice 42 has been paid. The status of the invoice is confirmed as "Paid" according to our records.']]
The color coding helps follow the logic.
There was no need for us to program the execution logic. The AI agent was able to work this out on its own, based on the different information that was provided
Let's make it more complicated and ask to send someone the status of an invoice by email.
user_input = "Email Andreas the status of invoice 43"
agent_outcome = graph.invoke({"messages": [("user", user_input)]})
print(agent_outcome['messages'][-1].to_json()['kwargs']['content'])AI agent: I've sent an email to Andreas with the status of invoice 43. The invoice is currently unpaid but not due yet. The email has been sent to MASKED_EMAIL with the details of the invoice status.
MASKED_EMAIL does indeed mask a real valid email. Just protecting everyone's privacy. And indeed, this email was sent.
What were the detailed steps the AI agent went through for this?
[['HumanMessage', 'Email Andreas the status of invoice 43'],
['AIMessage', [{'name': 'get_invoice_status', 'args': {'invoice_id': '43'}, 'id': 'toolu_bdrk_013xPo3kEDLNujfEjnNc3vZ7', 'type': 'tool_call'}]],
['ToolMessage (get_invoice_status)', 'The status of invoice 43 is: Unpaid, not due yet.'],
['AIMessage', [{'name': 'get_email_address', 'args': {'name': 'Andreas'}, 'id': 'toolu_bdrk_013CsFB4ipU4FKtUuJpn9bDo', 'type': 'tool_call'}]],
['ToolMessage (get_email_address)', 'MASKED_EMAIL'],
['AIMessage', [{'name': 'send_email', 'args': {'email_address': 'MASKED_EMAIL', 'email_text': "Dear Andreas,\n\nI hope this email finds you well. I wanted to inform you about the status of invoice 43.\n\nThe current status of invoice 43 is: Unpaid, not due yet.\n\nIf you have any questions or need further information, please don't hesitate to ask.\n\nBest regards,\nSAP BTP AI Agent", 'recipient_name': 'Andreas'}, 'id': 'toolu_bdrk_011uCXBEjqCPHpvdX6oj7MWK', 'type': 'tool_call'}]],
['ToolMessage (send_email)', "I sent the email to Andreas (MASKED_EMAIL): \nDear Andreas,\n\nI hope this email finds you well. I wanted to inform you about the status of invoice 43.\n\nThe current status of invoice 43 is: Unpaid, not due yet.\n\nIf you have any questions or need further information, please don't hesitate to ask.\n\nBest regards,\nSAP BTP AI Agent"],
['AIMessage', "I've sent an email to Andreas with the status of invoice 43. The invoice is currently unpaid but not due yet. The email has been sent to MASKED_EMAIL with the details of the invoice status."]]
Here the agent determined the invoice status as before. Then realised that it needs an email address to then send the email. I think that's pretty cool!
Let's test with AI agent with another task. When asking the AI assistant, that is powering the answer_SAP_question() function, for the boss of SAP, the answer will point you to link on SAP's website. Now we want to go further and ask the AI agent the same question.
user_input = "Who is the boss at SAP?"
agent_outcome = graph.invoke({"messages": [("user", user_input)]})
print(agent_outcome['messages'][-1].to_json()['kwargs']['content'])AI Agent: Based on the information from the SAP Executive Board page, the current CEO or boss of SAP is Christian Klein. He holds the position of Chief Executive Officer (CEO) of SAP SE. The Executive Board, which is the governing body of SAP SE, is led by Christian Klein as the CEO.
That's so much better. And it's not a fact that was trained into the Large Language Model. The log shows how the AI agent got the pointer to the website from the AI assistant (get_SAP_answer) and how it's used its skill to obtain text from a website.
[['HumanMessage', 'Who is the boss at SAP?'],
['AIMessage', [{'name': 'answer_SAP_question', 'args': {'text': 'Who is the current CEO or boss of SAP?'}, 'id': 'toolu_bdrk_01QLVPpJRP2UfeqgkL5vBdv8', 'type': 'tool_call'}]],
['ToolMessage (answer_SAP_question)', "To find the current CEO or boss of SAP, you should visit the SAP Executive Board page at https://www.sap.com/investors/en/governance/executive-board.html. This page will provide the most up-to-date information on the company's leadership."],
['AIMessage', [{'name': 'get_text_from_link', 'args': {'link': 'https://www.sap.com/investors/en/governance/executive-board.html'}, 'id': 'toolu_bdrk_01K1mJtaZvDwswZK1FWsTTix', 'type': 'tool_call'}]],
['ToolMessage (get_text_from_link)', "Skip to ContentContact UsService Request Investor RelationsCorporate GovernanceExecutive Board/Corporate GovernanceExecutive Board LeadershipSAP SE Executive BoardThe Executive Board is the governing body of SAP SE. Subject to the requirements of stock corporation law, the Executive Board is committed to SAP's interests and bound by SAP company policy. It provides the Supervisory Board with regular, prompt, and comprehensive reports about all essential issues of business, corporate strategy, and potential risks.Christian KleinChief Executive Officer (CEO)Learn more about Christian Klein\xa0Muhammad AlamSAP Product & EngineeringLearn more about Muhammad Alam\xa0Dominik AsamChief Financial OfficerLearn more about Dominik Asam\xa0Thomas SaueressigCustomer Services & DeliveryLearn more about Thomas Saueressig\xa0Sebastian SteinhaeuserChief Operating OfficerLearn more about Sebastian Steinhaeuser\xa0Gina Vargiu-BreuerChief People Officer and Labor DirectorLearn more about Gina Vargiu-Breuer\xa0Learn more about SAP SE Extended BoardLearn more about SAP SE Supervisory BoardQuick LinksSAP Trust CenterFind a solutionIndustriesFind a partnerTrials and demosFind servicesTrendingSAP SapphireSAP Business SuiteSAP Business Data CloudSAP Business AISustainabilityPartner ecosystemAbout SAPCompany InformationWorldwide DirectoryInvestor RelationsCareersNews and pressEventsCustomer StoriesNewsletterSite InformationPrivacyTerms of UseLegal DisclosureCopyrightTrademarkSitemapCookie statementDo not share/sell my personal informationBack to top"],
['AIMessage', 'Based on the information from the SAP Executive Board page, the current CEO or boss of SAP is Christian Klein. He holds the position of Chief Executive Officer (CEO) of SAP SE. The Executive Board, which is the governing body of SAP SE, is led by Christian Klein as the CEO.']]
Now deploy the AI assistant as REST-API so that it can be integrated into other applications. Here we deploy a REST-API on Cloud Foundry, but you can use any environment of your choice. In case you are new to Cloud Foundry, then Steps 1 and 2 of this Learning Journey get you started. The application that we deploy here consists of 4 files. The full code is available in this repository.
Before pushing the application to Cloud Foundry you need to make the following adjustments
Then push your application to Cloud Foundry.
cf pushAnd you should see the URL of your REST-API!
Now an application can call that BTP AI agent. Let's test this from Python with a not so serious request. Let's use the AI agents functionality of getting the lunch menu and the TV program. Should I go for lunch or watch tv?
user_input = "Should I go for lunch or watch tv?"
backend_api = "REPLACEWITHYOURURL"
paylod = {'user_input': user_input}
headers = {'Accept' : 'application/json', 'Content-Type' : 'application/json'}
r = requests.get(backend_api, json=paylod, headers=headers, verify=False)
response = r.json()
btpaiagent_response = response['btpaiagent_response']
btpaiagent_response_log = response['btpaiagent_response_log']
print(btpaiagent_response)And It's giving a good overview, especially when taking into account that both the lunch menu and the TV program were retrieved in German.
To make all this more usable, here is a simple sandboxing user interface. For a productive project, you may want to use UI5 or SAP Build. This is also a Python application, now using the streamlit package for the user interface, deployed on Cloud Foundry. The full code is available in this repository.
Before pushing the application to Cloud Foundry you need to add the url of your own AI agent in btpassistant.py (the backend_api variable). Then push as usual to Cloud Foundry.
cf pushAnd this is the fruit of all the work. With a few basic tools the AI agent was able to let Peter know of the status of a specific invoice!
If you have fun with BTP AI agent concept, feel free to post here any tools / functions into the comments.
Or if you want to continue with another example, then @CanAbdulla has a hands-on tutorial for you to create an AI Agent with human-in-the-loop control.
Happy agenting!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 5 | |
| 4 | |
| 4 | |
| 4 | |
| 2 | |
| 2 | |
| 1 | |
| 1 | |
| 1 | |
| 1 |