Artificial Intelligence Blogs Posts
cancel
Showing results for 
Search instead for 
Did you mean: 
AndreasForster
Product and Topic Expert
Product and Topic Expert
17,502

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.

  • Is this tutorial production-ready? No
  • Does it provide inspiration and a head start for your own projects? I would like to think so
  • Can you learn how AI agents work and how to create and extend your own? Absolutely! You will see how to create an AI agent using the open source framework LangGraph on the BTP.

Table of contents

  • Background
  • Architecture 
  • Prerequisites
  • Develop an AI Agent in a Jupyter Notebook
  • Deploy the AI Agent as REST-API
  • Put a sandbox user interface on top

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.

BTP AI Agent video.gif

 

Background

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:

  • get_invoice_status(invoice_id)
  • get_email_address(name)
  • send_email(recipient_name, email_address, email_text)

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:

  • answer_SAP_question(text): Delegates questions about SAP to an FAQ assistant
  • get_text_from_link(link): Retrieves the text from a website. You will see later how this tool is useful in combination with the above FAQ assistant
  • get_lunch_menu(): Gets today's menu from the SAP canteen in Zurich, Switzerland, as example of website scraping. That website is in German only, but you can test whether the AI agent can translate the text to English, or whether this could be an additional tool for the AI agent.
  • get_live_tv_arte(): Obtain a description of what's currently shown on the French/German TV station ARTE, as an example of calling a REST-API. In case you are outside France or Germany you might not be able to see the live stream, but the description of what's on should be visible.

 

Architecture

The architecture of the AI agent can be very lean. At a minimum you just need:

  • SAP Generative AI Hub: To access Claude, the Large Language Model which is powering this AI agent
  • Cloud Foundry: The code of the actual AI agent is deployed as a REST-API on Cloud Foundry. This tutorial also includes a quick sandboxing user interface that is also deployed on Cloud Foundry.

Only in case you want to implement the FAQ assistant that answers questions about SAP, you also need:

  • SAP HANA Cloud: To store the texts and to create, store and compare the text's embeddings
  • Mistral-Large-Instruct: A Large Language Model, that is hosted by SAP for text processing and creation

architecture.png

 

Prerequisites

To follow the implementation hands-on, you require these components:

  • SAP Generative AI Hub with the model "claude-3.5-sonnet" deployed,
  • Python environment, ie Jupyter Notebooks in Miniconda,  for sandboxing and testing
  • Cloud Foundry on BTP if you want to deploy the application as REST-API

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

  • !pip install langchain_core
  • !pip install langgraph
  • !pip install "generative-ai-hub-sdk[all]"

Develop an AI Agent in a Jupyter Notebook

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:

  • Enter your own email addresses in the get_email_address() function
  • Enter the URL of your FAQ assistant in the answer_SAP_question() function. This is optional, you can also decide not to use this function.

 

Get invoice status
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   

 

Get email address
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   

 

Send email
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}"

 

Answer SAP question
### 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

 

Get text from link

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

 

Get lunch menu

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

 

Get current program on TV station ARTE

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]

 

The AI agent

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...

200 graph display.png

 

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.

  • The execution starts the HumanMessage. That's us.
  • The next row starting with AIMessage shows that the AI agent decided to call the get_invoice_status function, passing the invoice_id 42 as parameter.
  • The ToolMessage shows that the get_invoice_function returned the information that the invoice is paid.
  • The last row beginning with AIMessage again shows that the AI agent used that information and phrased a written response.

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

  • The tools and their descriptions
  • The system message
  • The actual request

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.

300 email.png

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.']]

 

Deploy the AI Agent as REST-API

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 

  1. btpaiagent.py ➔ holds the actual application logic
  2. manifest.yaml ➔ contains for instance the memory requirements and environment variables
  3. requirements.txt ➔ lists the Python packages that will be installed
  4. runtime.txt ➔ specifies the Python version (python-3.12.x)

Before pushing the application to Cloud Foundry you need to make the following adjustments

  1. In btpaiagent.py specify your own SMTP server credentials to send emails. For productive use you may want to store these more securely, ie in the SAP Credential Store. An example is in this blog
  2. In manifest.yml enter your credentials for the SAP Generative AI Hub.

Then push your application to Cloud Foundry.

cf push

And you should see the URL of your REST-API!

400 deployed.png

 

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.

500 lunch or tv.png

 

Put a sandbox user interface on top

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.

  1. btpassistant.py ➔ holds the actual application logic and user interface
  2. manifest.yaml ➔ contains for instance the memory requirements and environment variables
  3. requirements.txt ➔ lists the Python packages that will be installed
  4. runtime.txt ➔ specifies the Python version (python-3.12.x)

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 push

500 deployed ui.png

And 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!

BTP AI Agent peter.png

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!

13 Comments
Cocquerel
Active Contributor

Great blog, thanks for sharing.
Looking at the coding, it looks like you did not provide any description for the tools and no instruction on how to use them (input and output parameters) except via code comments. Does that mean the langgraph library is providing the code comments to the AI Agent via system message?

AndreasForster
Product and Topic Expert
Product and Topic Expert

Thanks for bringing that up @Cocquerel. I forgot to mention that in the text and will add it soon have added this now.

The comments inside the individual tools/functions are not just for us humans. 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. Here the comments from the get_invoice_status() as example. 

"""Returns an invoice's status, ie whether it has been paid or not.

Args:
invoice_id: The invoice id
"""

 

Super interesting example of agent value add, congratulations @AndreasForster  with SAP Hosted AI / Mistral

Cocquerel
Active Contributor
0 Kudos

In the case some tools I want to use are web services developed using CAP Node.js running on BTP Foundry, I'm supposed to create python wrapper on top or can I use the Node.js version langgraph library. In this second case, is it working via code comment to provide the description of the tools. Is it possible to mix python and node.js tools in one AI Agent ?

CanAbdulla
Advisor
Advisor
0 Kudos

Thanks for the nice blog post Andreas!

AndreasForster
Product and Topic Expert
Product and Topic Expert

Hello @Cocquerel I am not sure whether Python and Node.js can be mixed in the same application. But if Node.js provides a REST-API then this could be made available here by wrapping it in a Python function. Maybe the whole AI agent could also be created in Node.js actually. In addition to the LangGraph.js you posted, we also have SAP Cloud SDK for AI on pnpm, which can use LLMs on the Gen AI Hub. 

Vitaliy-R
Developer Advocate
Developer Advocate
0 Kudos

@Cocquerel, could you elaborate a bit on what parts you would like to do via JS and what via Python?

AndriiRzhaksyns
Advisor
Advisor
0 Kudos

Good topic. Good Work!

Cocquerel
Active Contributor
0 Kudos

@Vitaliy-R Most of blogs and tutorials related to Generative AI are Python based. I am personally also more comfortable with the Python language than with Node.js, which I find more complex.
In other side, I would like to use SAPUI5 for the frontend and CDS to expose data from SAP Backends. So, the way to go would be CAP Node.js app.
So, if I would found a clear and documented Agentic AI scenario template fully based on CAP Node.js using JS GenAI SDK and LangGraph.js , I would certainly go for it. But I never saw such example until now. That's why, I was asking on the possibility to mix python and node.js .

prasad_gvk2
Explorer
0 Kudos

Hi Andreas,

Thanks for sharing details for very good visibility on creating custom agent. I have a very basic question from this example as What is the exact role of SAP generative AI(Claude LLM) in this scenario. If custom agent wants to execute as individual agent, still Generative AI/LLM  required here? Please clarify to understand the process in the holistic way.

Thanks

Prasad. 

    

AndreasForster
Product and Topic Expert
Product and Topic Expert
0 Kudos

Thank you @prasad_gvk2, The Large Language Model is very instrumental here. It's not just as obvious as in a chatbot.


The LLM for instance takes the user request "Has invoice xyz been paid" and assesses, which of the tools/functions to which it has access, can help answer that request. For this assessment it is using the comments that are within these functions, ie

"""Returns an invoice's status, ie whether it has been paid or not.

Args:
invoice_id: The invoice id
"""

The LLM also produces the Python code, that calls this function, ie with the invoice number from the incoming text

carsten_hahn
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi Andreas,

do you know whether we are also planning to provide an SAP MCP service to make the development of AI Agents easier?

I found in the Web that our partner k2view is already working on that:

https://www.k2view.com/blog/sap-mcp/#Why-do-sap-and-mcp-matter-for-ai-agents

Thx

Carsten

AndreasForster
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi @carsten_hahn , for developing in Python I find this super easy.
For a more UI driven environment to create custom skills and agents, keep an I on the upcoming Joule Studio
https://www.sap.com/products/artificial-intelligence/joule-studio.html
https://discovery-center.cloud.sap/ai-feature/e93aa292-e7f4-449d-9586-f1a8510d5ab6/