Generative AI has leapt from research papers to daily business realityâ and SAP is surfing that wave at full speed. In this handsâon series, Iâll show you how to spin up a custom AI agent on SAP AI Core in minutes, then grow it into a productionâready assetâwithout drowning in theory.
Notice
æ„æŹèȘçăŻăăĄăă§ăă
What Youâll Learn in This Series
How to Run a Custom AI Agent on SAP AI Core in SecondsImplementation Using LangChain, Google Search Tool, and RAGSteps to Convert the AI Agent into a REST API, Integrate It into an SAPUI5/Fiori UI, and Deploy to Cloud Foundry
Time Commitment
Each part is designed to be completed in 10â15 minutes .
ïž Series Roadmap
Part 0 ProloguePart 1 Env Setup: SAP AICore & AI LaunchpadPart 2 Building a Chat Model with LangChainPart 3 Agent Tools: Integrating Google SearchPart4 RAG Basicsâ HANA Cloud VectorEngine & EmbeddingPart 5 RAG Basics âĄ: Building Retriever ToolPart 6: Converting the AI Agent into a REST APIPart 7: Building the Chat UI with SAPUI5 [current blog]Part 8: Deploying to Cloud Foundry
Note
Subsequent blogs will be published soon.
If you enjoyed this post, please give it a kudos! Your support really motivates me. Also, if thereâs anything youâd like to know more about, feel free to leave a comment!
Building the Chat UI with SAPUI5
1 | Introduction
In this chapter, weâll make it possible to invoke the AI agent you built in the previous chapters from a chat UI based on SAP UI5/Fiori.
SAP UI5 is a UI framework provided by SAP that lets you develop modern, enterprise-grade web applications in very little time. In particular, by using SAP Business Application Studio (BAS), you can auto-generate your projectâs structure from Fiori templatesâso you donât have to create directories or configuration files by hand.
2 | Prerequisites
BTP sub-accountSAP AI Core instanceSAP AI LaunchPad SubscriptionPython 3.13 and pipVSCode, BAS or any IDE
Note for the Trial Environment
The HANA Cloud instance in the Trial enviroment automatically shuts down every night. If your work spans past midnight, please restart the instance the following day.
3 | Preparing the Fiori Application
In SAP UI development, we strongly recommend leveraging BASâs template feature. By basing your project on the folder structure and configuration files it generates, you can quickly set up your Fiori appâs manifest (mta.yaml) and module structure without manual effort.
In BAS, click Create Dev Space and select the SAP Fiori template. Since weâll also want to run the Python API you created in the previous chapter locally, choose Python Tools under Additional SAP Extensions.
Click the hamburger menu in the upper-left corner and choose File > New Project From Template.
For Template Selection, choose Basic.â
In Data Source and Service Selection, select None, since the UI will fetch its data from the Python API.
Under Entity Selection, enter ChatEntity for the Entity name.
Configure Project Attributes as follows, and set Add deployment configuration to Yes:
ItemValueModule Namemy-ai-agent-uiApplication TitleMy AI ChatDescription Chat UI for AI Agent
In Deployment Configuration, set the options as directed by the template:
ItemValueTargetCloud FoundryDestination NoneAdd Router Module Add Application to Managed Application Router
If you donât see the Router Module option after setting Target to CF, switch Target to ABAP once, then back to Cloud Foundryâthe Router Module option will appear correctly.
Finally, deploy the Python API and verify that itâs running. The structure will look like this:
PROJECTS
âââ my-ai-agent-ui/
âââ my-ai-agent-api/
âââ main.py
âââ requirements.txt
âââ .env
In the my-ai-agent-api folder, create and activate a virtual environment, then verify that the local server starts by running:
gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app âbind 0.0.0.0:${PORT:-8000}
With that, your Fiori application setup is complete and ready to go!
4 | Refine the Fiori Application
With that, your Fiori application setuUsing the generated Fiori project as a foundation, letâs adjust the code so that the chat UI can communicate with the AI Agent.
First, implement the controller (my-ai-agent-ui/webapp/controller/ChatEntity.controller.js) as follows:
sap.ui.define([
âsap/ui/core/mvc/Controllerâ,
âsap/ui/model/json/JSONModelâ,
âsap/m/MessageToastâ,
âsap/ui/core/BusyIndicatorâ
], function (Controller, JSONModel, MessageToast, BusyIndicator) {
âuse strictâ;
// Switch endpoint based on environment
const ENDPOINT = (
[âlocalhostâ, âapplicationstudioâ].some(h => window.location.hostname.includes(h)) ||
window.location.port === â8080â
) ? ââ : âhttps://my-ai-agent-api-relaxed-raven-ie.cfapps.us10-001.hana.ondemand.comâ;
return Controller.extend(âmyaiagentui.controller.ChatEntityâ, {
/** Initialization */
onInit() {
this.getView().setModel(new JSONModel({
busy: false,
txtInput: ââ,
uploadedFiles: [],
messages: [{
role: âassistantâ,
content: âHello! Iâm a chatbot. Is there anything I can help you with?â,
hasThinkingProcess: false
}]
}), âuiâ);
this.byId(âfileUploaderâ)?.setUploadUrl(`${ENDPOINT}/agent/upload`);
},
/** Clear chat history */
onClearChatPress() {
this.getView().getModel(âuiâ).setProperty(â/messagesâ, [{
role: âassistantâ,
content: âHello! Iâm a chatbot. Is there anything I can help you with?â,
hasThinkingProcess: false
}]);
MessageToast.show(âChat clearedâ);
},
/** Send message */
async onBtnChatbotSendPress() {
const ui = this.getView().getModel(âuiâ);
const input = (ui.getProperty(â/txtInputâ) || ââ).trim();
if (!input) return;
const msgs = ui.getProperty(â/messagesâ);
msgs.push({ role: âuserâ, content: input });
ui.setProperty(â/messagesâ, msgs);
ui.setProperty(â/txtInputâ, ââ);
ui.setProperty(â/busyâ, true);
try {
const { output, intermediate_steps = [] } = await this._apiChatCompletion(input);
msgs.push({
role: âassistantâ,
content: output,
hasThinkingProcess: Boolean(intermediate_steps.length),
thinkingProcess: intermediate_steps.map((s, i) => ({
âŠs,
stepIndex: i + 1,
observationTruncated: s.observation?.slice(0, 100) + (s.observation?.length > 100 ? ââŠâ : ââ),
observationFull: s.observation,
isObservationExpanded: false,
hasLongObservation: (s.observation?.length || 0) > 100
})),
isExpanded: false
});
ui.setProperty(â/messagesâ, msgs);
this._scrollToBottom();
} catch (err) {
console.error(err);
MessageToast.show(`Error: ${err.message}`);
} finally {
ui.setProperty(â/busyâ, false);
}
},
/** Toggle thinking process display */
onToggleThinkingProcess(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(âuiâ);
if (ctx) this._toggleFlag(ctx, âisExpandedâ);
},
/** Toggle observation display */
onToggleObservation(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(âuiâ);
if (ctx) this._toggleFlag(ctx, âisObservationExpandedâ);
},
/** Toggle a boolean flag */
_toggleFlag(ctx, flag) {
const ui = this.getView().getModel(âuiâ);
const path = ctx.getPath();
ui.setProperty(`${path}/${flag}`, !ui.getProperty(`${path}/${flag}`));
},
/** Scroll chat to bottom */
_scrollToBottom() {
setTimeout(() => {
const sc = this.byId(âchatScrollContainerâ);
const items = sc?.getContent()[0].getItems();
if (items?.length) sc.scrollToElement(items[items.length â 1]);
}, 100);
},
/** Call AI chat API */
async _apiChatCompletion(query) {
const res = await fetch(`${ENDPOINT}/agent/chat`, {
method: âPOSTâ,
headers: { âContent-Typeâ: âapplication/jsonâ },
body: JSON.stringify({ query })
});
if (!res.ok) throw new Error(`Server error (${res.status}): ${await res.text()}`);
return res.json();
}
});
});
Next, update the View file (my-ai-agent-ui/webapp/view/ChatEntity.view.xml) as follows:
<mvc:View controllerName=âmyaiagentui.controller.ChatEntityâ
xmlns:core=âsap.ui.coreâ
xmlns:mvc=âsap.ui.core.mvcâ
xmlns:f=âsap.fâ
xmlns:u=âsap.ui.unifiedâ
displayBlock=âtrueâ
xmlns=âsap.mâ>
<f:DynamicPage id=âmainDynamicPageâ stickySubheaderProvider=âiconTabBarâ class=âsapUiNoContentPaddingâ>
<f:title>
<f:DynamicPageTitle id=âmainDynamicTitleâ>
<f:heading>
<Title id=âpageTitleâ text=âAI Chat with Fioriâ/>
</f:heading>
<f:actions>
<Button id=âclearChatButtonâ
text=âClear Chatâ
icon=âsap-icon://refreshâ
type=âEmphasizedâ
press=âonClearChatPressâ/>
</f:actions>
</f:DynamicPageTitle>
</f:title>
<f:content>
<!â Main chat UI area â>
<VBox id=âchatMainAreaBoxâ class=âsapUiNoContentPadding chatMainAreaâ>
<IconTabBar id=âiconTabBarâ
class=âsapUiNoContentPaddingâ
stretchContentHeight=âtrueâ
expanded=âtrueâ
expandable=âfalseâ>
<items>
<IconTabFilter id=âchatbotTabâ text=âChatbotâ class=âsapUiNoContentPaddingâ>
<VBox id=âchatContainerBoxâ class=âchatContainer sapUiNoContentPaddingâ>
<!â Chat messages area â>
<ScrollContainer id=âchatScrollContainerâ
height=â100%â
width=â100%â
vertical=âtrueâ
focusable=âtrueâ
class=âchatScrollContainerâ>
<VBox id=âchatMessagesBoxâ items=â{ui>/messages}â class=âchatMessagesContainerâ>
<VBox id=âmessageItemBoxâ>
<!â Assistant message â>
<VBox id=âassistantMessageVBoxâ visible=â{= ${ui>role} === âassistantâ}â
class=âchatMessageWrapperâ>
<HBox id=âassistantMessageHBoxâ direction=âRowâ class=âchatMessageRowâ>
<HBox id=âassistantMessageHBox1âł backgroundDesign=âSolidâ
class=âchatBubbleAssistantâ>
<core:Icon id=âassistantMessageIconâ decorative=âtrueâ
src=âsap-icon://aiâ
class=âchatIconâ/>
<FormattedText id=âassistantMessageFormattedTextâ htmlText=â{ui>content}â/>
</HBox>
</HBox>
<!â Thinking process accordion â>
<HBox id=âthinkingToggleBoxâ visible=â{ui>hasThinkingProcess}â
class=âthinkingProcessContainerâ>
<Button id=âthinkingToggleButtonâ icon=â{= ${ui>isExpanded} ? âsap-icon://collapseâ : âsap-icon://expandâ }â
text=âToggle Thinking Processâ
type=âTransparentâ
press=âonToggleThinkingProcessâ
class=âthinkingProcessToggleâ/>
</HBox>
<VBox id=âthinkingVBoxâ visible=â{= ${ui>isExpanded} && ${ui>hasThinkingProcess} }â
class=âthinkingProcessContentâ>
<VBox id=âthinkingVBox1âł items=â{ui>thinkingProcess}â>
<VBox id=âthinkingStepBoxâ class=âthinkingStepBoxâ>
<Title id=âthinkingStepTitleâ
text=â{= âStep â + ${ui>stepIndex} }â
level=âH5âł
class=âthinkingStepTitleâ/>
<VBox id=âthinkingStepContentâ class=âthinkingStepContentâ>
<HBox id=âthinkingStepThoughtâ class=âthinkingStepItemâ>
<Label id=âThoughtLabelâ text=âThought:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âThoughtTextâ text=â{ui>thought}â wrapping=âtrueâ class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepActionâ class=âthinkingStepItemâ>
<Label id=âActionLabelâ text=âAction:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âActionTextâ text=â{ui>action}â class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepInputâ class=âthinkingStepItemâ>
<Label id=âInputLabelâ text=âInput:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âInputTextâ text=â{ui>action_input}â class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepObservationâ class=âthinkingStepItemâ>
<Label id=âObservationLabelâ text=âObservation:â design=âBoldâ class=âthinkingLabelâ/>
<VBox id=âObservationVBoxâ class=âthinkingTextâ>
<Text id=âObservationTextâ text=â{= ${ui>isObservationExpanded} ? ${ui>observationFull} : ${ui>observationTruncated} }â
wrapping=âtrueâ/>
<Link id=âObservationLinkâ
text=â{= ${ui>isObservationExpanded} ? âCollapseâ : âRead moreâ }â
visible=â{ui>hasLongObservation}â
press=âonToggleObservationâ
class=âobservationToggleLinkâ/>
</VBox>
</HBox>
</VBox>
</VBox>
</VBox>
</VBox>
</VBox>
<!â User message â>
<HBox id=âUserCommentâ
visible=â{= ${ui>role} === âuserâ}â
direction=âRowReverseâ
class=âchatMessageRowâ>
<HBox id=âUserCommentBoxâ
direction=âRowReverseâ
backgroundDesign=âSolidâ
class=âchatBubbleUser sapThemeBrand-asBackgroundColorâ>
<core:Icon id=âUserCommentIconâ
decorative=âtrueâ
src=âsap-icon://customerâ
class=âchatIcon sapThemeTextInvertedâ/>
<Text id=âUserCommentTextâ class=âsapThemeTextInvertedâ text=â{ui>content}â/>
</HBox>
</HBox>
</VBox>
</VBox>
</ScrollContainer>
<!â Input area + send button â>
<HBox id=âchatInputAreaBoxâ class=âchatInputAreaâ>
<TextArea id=âchatInputâ value=â{ui>/txtInput}â
width=â100%â
growing=âtrueâ
placeholder=âType your messageâŠâ
editable=â{= !${ui>/busy}}â
busyIndicatorDelay=â0âł
class=âchatTextAreaâ>
<layoutData>
<FlexItemData id=âInputDataâ growFactor=â1âł/>
</layoutData>
</TextArea>
<Button id=âsendButtonâ class=âchatSendButtonâ
type=âEmphasizedâ
icon=âsap-icon://paper-planeâ
text=âSendâ
press=âonBtnChatbotSendPressâ
busy=â{ui>/busy}â
busyIndicatorDelay=â0âł/>
</HBox>
</VBox>
</IconTabFilter>
</items>
</IconTabBar>
</VBox>
</f:content>
</f:DynamicPage>
</mvc:View>
Additionally, for detailed UI design configuration, set up the styles in my-ai-agent-ui/webapp/css/style.css:
/* Chat main area */
.chatMainArea {
height: 100vh;
min-height: 300px;
}
/* Chat container */
.chatContainer {
display: flex;
flex-direction: column;
height: calc(90vh â 100px);
padding: 0;
}
/* Chat scroll container */
.chatScrollContainer {
flex: 1;
min-height: 300px;
background-color: #f8f9fa;
border: none;
border-radius: 0;
padding: 0;
margin: 0;
}
/* Chat messages container */
.chatMessagesContainer {
padding: 1rem;
}
/* Chat message row */
.chatMessageRow {
width: 100%;
margin-bottom: 0.5rem;
}
/* Chat message wrapper */
.chatMessageWrapper {
margin-bottom: 1rem;
}
/* Assistant chat bubble */
.chatBubbleAssistant {
max-width: 70%;
min-width: 100px;
background-color: #ffffff;
border: 1px solid #d0d7de;
border-radius: 18px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
align-items: flex-start;
}
.chatBubbleAssistant .sapMFormattedText {
word-wrap: break-word;
line-height: 1.4;
}
/* User chat bubble */
.chatBubbleUser {
max-width: 70%;
min-width: 100px;
border-radius: 18px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
align-items: flex-start;
}
.chatBubbleUser .sapMText {
word-wrap: break-word;
line-height: 1.4;
}
/* Thinking process container */
.thinkingProcessContainer {
margin-top: 0.5rem;
margin-left: 2.5rem;
}
/* Thinking process toggle button */
.thinkingProcessToggle {
font-size: 0.875rem;
color: #0073e6;
}
.thinkingProcessToggle:hover {
background-color: rgba(0, 115, 230, 0.08) !important;
}
/* Thinking process content */
.thinkingProcessContent {
margin-left: 2.5rem;
margin-top: 0.5rem;
max-width: 70%;
}
/* Individual thinking step box */
.thinkingStepBox {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f5f7fa;
border: 1px solid #e4e7ea;
border-radius: 8px;
}
.thinkingStepBox:last-child {
margin-bottom: 0;
}
/* Thinking step title */
.thinkingStepTitle {
color: #0a6ed1;
margin-bottom: 0.75rem;
font-size: 1rem;
}
/* Thinking step content wrapper */
.thinkingStepContent {
padding-left: 0.5rem;
}
/* Thinking step item */
.thinkingStepItem {
margin-bottom: 0.5rem;
align-items: flex-start;
}
.thinkingStepItem:last-child {
margin-bottom: 0;
}
/* Label within thinking process */
.thinkingLabel {
min-width: 100px;
margin-right: 1rem;
color: #32363a;
font-size: 0.875rem;
}
/* Text within thinking process */
.thinkingText {
flex: 1;
font-size: 0.875rem;
color: #515456;
line-height: 1.5;
}
/* âRead moreâ link for observations */
.observationToggleLink {
margin-top: 0.25rem;
font-size: 0.875rem;
}
/* Input area at bottom */
.chatInputArea {
background-color: #ffffff;
padding: 1rem;
border-top: 1px solid #e4e7ea;
align-items: flex-end;
flex-shrink: 0;
}
/* TextArea styling */
.chatTextArea .sapMTextAreaInner {
border: 2px solid #e4e7ea;
background-color: #ffffff;
}
.chatTextArea .sapMTextAreaInner:focus-within {
border-color: #0073e6;
}
/* Send button styling */
.chatSendButton {
min-height: 40px;
border-radius: 20px;
margin-left: 0.5rem;
}
/* Icon adjustments */
.chatIcon {
font-size: 1.2rem;
margin-top: 2px;
}
.chatBubbleAssistant .chatIcon {
margin-right: 0.5rem;
}
.chatBubbleUser .chatIcon {
margin-left: 0.5rem;
}
/* Remove padding from IconTabBar */
.sapMITB {
padding: 0;
}
.sapMITBContent {
padding: 0;
background-color: transparent;
}
/* Responsive tweaks */
@media (max-width: 768px) {
.chatBubbleAssistant,
.chatBubbleUser {
max-width: 85%;
}
.chatContainer {
height: calc(100vh â 80px);
}
.chatScrollContainer {
min-height: 300px;
}
.chatSendButton .sapMBtnInner .sapMBtnContent {
font-size: 0;
}
.chatSendButton .sapMBtnIcon {
margin-right: 0;
}
.thinkingProcessContent {
max-width: 85%;
}
.thinkingLabel {
min-width: 80px;
margin-right: 0.5rem;
}
}
To forward requests from the chat UI to the Python API, weâll add a proxy middleware. First, install the dependency:
npm install ui5-middleware-simpleproxy âsave-dev
And then, in my-ai-agent-ui/package.json, add the following section:
âŠ
âmainâ: âwebapp/index.htmlâ,
âdevDependenciesâ: {
// (your existing devDependencies)
},
âui5â: {
âdependenciesâ: [
âui5-middleware-simpleproxyâ
]
},
âscriptsâ: {
// (your existing scripts)
}
âŠ
And with this in place, any API requests from the chat UI will be forwarded to your local Python API.
5 | Test the Fiori Application
Run both the app and the API locally to confirm that the chat feature works as expected.
First, start the Python API in the my-ai-agent-api directory:
gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app âbind 0.0.0.0:${PORT:-8000}
Then, open a new terminal, navigate to the my-ai-agent-ui root folder, and run:
npm start
Finally, open the URL shown in the console (typically http://localhost:8080) in your browser, type a message into the chat input field, and send it. If you see a response from the AI agent, your setup is working correctly!
â
When you click the Clear Chat button on the chat screen, the history will be reset and youâll see the confirmation âChat cleared.â Verify that you can start a new conversation without any issues!
6 | Challenge â Add a Text-File Upload Button
Letâs try uploading extra text and interacting with the AI Agent using the newly added documents. Be aware that you wonât be able to implement this challenge unless youâve already completed the previous chapterâs challenge.
Add a dedicated .txt file upload button to the header of the chat UI, send the selected file to the Python API, and have the server chunk and store the file. To make the process clearer for users, block the screen with a BusyIndicator while the upload is in progress.
Add the following three items to ChatEntity.controller.js (adjust the paths and namespaces as needed for your environment):
Import sap/ui/core/BusyIndicator at the toponFileChange handler â performs file-type validation and triggers the upload_uploadFile method â uses fetch with FormData to POST to the /agent/upload API/** When a file is selected */
async onFileChange(oEvent) {
const oFileUploader = oEvent.getSource();
const file = (oEvent.getParameter(âfilesâ) || [])[0];
if (!file) return;
if (!file.name.endsWith(â.txtâ)) {
MessageToast.show(âOnly .txt files can be uploaded.â);
oFileUploader.clear();
return;
}
await this._uploadFile(file);
oFileUploader.clear();
},
/** File upload */
async _uploadFile(file) {
const ui = this.getView().getModel(âuiâ);
ui.setProperty(â/busyâ, true);
BusyIndicator.show(0);
try {
const fd = new FormData();
fd.append(âfileâ, file);
const res = await fetch(`${ENDPOINT}/agent/upload`, {
method: âPOSTâ,
body: fd
});
if (!res.ok) throw new Error(await res.text());
const { filename, chunks_created } = await res.json();
const files = ui.getProperty(â/uploadedFilesâ) || [];
ui.setProperty(â/uploadedFilesâ, [
âŠfiles,
{
name: filename,
chunks: chunks_created,
uploadedAt: new Date()
}
]);
MessageToast.show(`Uploaded â${filename}â (${chunks_created} chunks created)`);
} catch (err) {
console.error(err);
MessageToast.show(`Upload error: ${err.message}`);
} finally {
ui.setProperty(â/busyâ, false);
BusyIndicator.hide();
}
}
Next, place the FileUploader and Clear buttons side-by-side in the header. The key point is to set buttonOnly=âtrueâ on the FileUploader so it appears as an icon button.
<f:actions>
<u:FileUploader id=âfileUploaderâ
name=âfileâ
placeholder=âSelect a fileâŠâ
fileType=âtxtâ
change=âonFileChangeâ
buttonText=ââ
buttonOnly=âtrueâ
icon=âsap-icon://uploadâ
class=âfileUploaderButtonâ />
<Button id=âclearChatButtonâ
text=âClear Chatâ
icon=âsap-icon://refreshâ
type=âEmphasizedâ
press=âonClearChatPressâ />
</f:actions>
Finally, display an âUploaded Filesâ list beneath the header so users can quickly see how many chunks each file was split into.
<HBox id=âfileUploadAreaBoxâ
visible=â{= ${ui>/uploadedFiles}.length > 0}â>
<Text text=âAdditional uploaded files:â />
<HBox items=â{ui>/uploadedFiles}â>
<ObjectStatus text=â{ui>name}â icon=âsap-icon://document-textâ />
</HBox>
</HBox>
Advanced Challenge â Reset Button
When you keep uploading files, the vector DB grows larger and larger. For testing purposes, letâs add a button that resets the entire database at once.
API: Implement /agent/delete (hint â see the DB-initialization code from Part 4).Controller: Implement onDeletePress, call /agent/delete, and clear the UIâs file list.View: Place a Delete button between the upload and clear-chat buttons in the header.
Implementation tip
If you set the UI modelâs /uploadedFiles to an empty array [ ] , the uploaded-files area automatically disappears.
7 | Next Up
Part 8 Deploying to Cloud Foundry
In Part 8 weâll deploy the completed application to Cloud Foundry. The UI will go through mta build â cf deploy, while the Python backend will use manifest.yaml with cf pushâa two-track approach aimed at âblazing-fast deployment.â Stay tuned!
Disclaimer
All the views and opinions in the blog are my own and is made in my personal capacity and that SAP shall not be responsible or liable for any of the contents published in this blog.
â Generative AI has leapt from research papers to daily business realityâ and SAP is surfing that wave at full speed. In this handsâon series, Iâll show you how to spin up a custom AI agent on SAP AI Core in minutes, then grow it into a productionâready assetâwithout drowning in theory.Noticeæ„æŹèȘçăŻăăĄăă§ăă What Youâll Learn in This SeriesHow to Run a Custom AI Agent on SAP AI Core in SecondsImplementation Using LangChain, Google Search Tool, and RAGSteps to Convert the AI Agent into a REST API, Integrate It into an SAPUI5/Fiori UI, and Deploy to Cloud FoundryTime CommitmentEach part is designed to be completed in 10â15 minutes .
ïž Series RoadmapPart 0 ProloguePart 1 Env Setup: SAP AICore & AI LaunchpadPart 2 Building a Chat Model with LangChainPart 3 Agent Tools: Integrating Google SearchPart4 RAG Basicsâ HANA Cloud VectorEngine & EmbeddingPart 5 RAG Basics âĄ: Building Retriever ToolPart 6: Converting the AI Agent into a REST APIPart 7: Building the Chat UI with SAPUI5 [current blog]Part 8: Deploying to Cloud FoundryNoteSubsequent blogs will be published soon.If you enjoyed this post, please give it a kudos! Your support really motivates me. Also, if thereâs anything youâd like to know more about, feel free to leave a comment!Building the Chat UI with SAPUI51 | IntroductionIn this chapter, weâll make it possible to invoke the AI agent you built in the previous chapters from a chat UI based on SAP UI5/Fiori. SAP UI5 is a UI framework provided by SAP that lets you develop modern, enterprise-grade web applications in very little time. In particular, by using SAP Business Application Studio (BAS), you can auto-generate your projectâs structure from Fiori templatesâso you donât have to create directories or configuration files by hand. 2 | PrerequisitesBTP sub-accountSAP AI Core instanceSAP AI LaunchPad SubscriptionPython 3.13 and pipVSCode, BAS or any IDENote for the Trial EnvironmentThe HANA Cloud instance in the Trial enviroment automatically shuts down every night. If your work spans past midnight, please restart the instance the following day. 3 | Preparing the Fiori ApplicationIn SAP UI development, we strongly recommend leveraging BASâs template feature. By basing your project on the folder structure and configuration files it generates, you can quickly set up your Fiori appâs manifest (mta.yaml) and module structure without manual effort. In BAS, click Create Dev Space and select the SAP Fiori template. Since weâll also want to run the Python API you created in the previous chapter locally, choose Python Tools under Additional SAP Extensions.Click the hamburger menu in the upper-left corner and choose File > New Project From Template.For Template Selection, choose Basic.âIn Data Source and Service Selection, select None, since the UI will fetch its data from the Python API.Under Entity Selection, enter ChatEntity for the Entity name.Configure Project Attributes as follows, and set Add deployment configuration to Yes:ItemValueModule Namemy-ai-agent-uiApplication TitleMy AI ChatDescription Chat UI for AI AgentIn Deployment Configuration, set the options as directed by the template:ItemValueTargetCloud FoundryDestination NoneAdd Router Module Add Application to Managed Application RouterIf you donât see the Router Module option after setting Target to CF, switch Target to ABAP once, then back to Cloud Foundryâthe Router Module option will appear correctly. Finally, deploy the Python API and verify that itâs running. The structure will look like this:PROJECTS
âââ my-ai-agent-ui/
âââ my-ai-agent-api/
âââ main.py
âââ requirements.txt
âââ .envIn the my-ai-agent-api folder, create and activate a virtual environment, then verify that the local server starts by running:gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app âbind 0.0.0.0:${PORT:-8000}With that, your Fiori application setup is complete and ready to go! 4 | Refine the Fiori ApplicationWith that, your Fiori application setuUsing the generated Fiori project as a foundation, letâs adjust the code so that the chat UI can communicate with the AI Agent.First, implement the controller (my-ai-agent-ui/webapp/controller/ChatEntity.controller.js) as follows:sap.ui.define([
âsap/ui/core/mvc/Controllerâ,
âsap/ui/model/json/JSONModelâ,
âsap/m/MessageToastâ,
âsap/ui/core/BusyIndicatorâ
], function (Controller, JSONModel, MessageToast, BusyIndicator) {
âuse strictâ;
// Switch endpoint based on environment
const ENDPOINT = (
[âlocalhostâ, âapplicationstudioâ].some(h => window.location.hostname.includes(h)) ||
window.location.port === â8080â
) ? ââ : âhttps://my-ai-agent-api-relaxed-raven-ie.cfapps.us10-001.hana.ondemand.comâ;
return Controller.extend(âmyaiagentui.controller.ChatEntityâ, {
/** Initialization */
onInit() {
this.getView().setModel(new JSONModel({
busy: false,
txtInput: ââ,
uploadedFiles: [],
messages: [{
role: âassistantâ,
content: âHello! Iâm a chatbot. Is there anything I can help you with?â,
hasThinkingProcess: false
}]
}), âuiâ);
this.byId(âfileUploaderâ)?.setUploadUrl(`${ENDPOINT}/agent/upload`);
},
/** Clear chat history */
onClearChatPress() {
this.getView().getModel(âuiâ).setProperty(â/messagesâ, [{
role: âassistantâ,
content: âHello! Iâm a chatbot. Is there anything I can help you with?â,
hasThinkingProcess: false
}]);
MessageToast.show(âChat clearedâ);
},
/** Send message */
async onBtnChatbotSendPress() {
const ui = this.getView().getModel(âuiâ);
const input = (ui.getProperty(â/txtInputâ) || ââ).trim();
if (!input) return;
const msgs = ui.getProperty(â/messagesâ);
msgs.push({ role: âuserâ, content: input });
ui.setProperty(â/messagesâ, msgs);
ui.setProperty(â/txtInputâ, ââ);
ui.setProperty(â/busyâ, true);
try {
const { output, intermediate_steps = [] } = await this._apiChatCompletion(input);
msgs.push({
role: âassistantâ,
content: output,
hasThinkingProcess: Boolean(intermediate_steps.length),
thinkingProcess: intermediate_steps.map((s, i) => ({
âŠs,
stepIndex: i + 1,
observationTruncated: s.observation?.slice(0, 100) + (s.observation?.length > 100 ? ââŠâ : ââ),
observationFull: s.observation,
isObservationExpanded: false,
hasLongObservation: (s.observation?.length || 0) > 100
})),
isExpanded: false
});
ui.setProperty(â/messagesâ, msgs);
this._scrollToBottom();
} catch (err) {
console.error(err);
MessageToast.show(`Error: ${err.message}`);
} finally {
ui.setProperty(â/busyâ, false);
}
},
/** Toggle thinking process display */
onToggleThinkingProcess(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(âuiâ);
if (ctx) this._toggleFlag(ctx, âisExpandedâ);
},
/** Toggle observation display */
onToggleObservation(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(âuiâ);
if (ctx) this._toggleFlag(ctx, âisObservationExpandedâ);
},
/** Toggle a boolean flag */
_toggleFlag(ctx, flag) {
const ui = this.getView().getModel(âuiâ);
const path = ctx.getPath();
ui.setProperty(`${path}/${flag}`, !ui.getProperty(`${path}/${flag}`));
},
/** Scroll chat to bottom */
_scrollToBottom() {
setTimeout(() => {
const sc = this.byId(âchatScrollContainerâ);
const items = sc?.getContent()[0].getItems();
if (items?.length) sc.scrollToElement(items[items.length â 1]);
}, 100);
},
/** Call AI chat API */
async _apiChatCompletion(query) {
const res = await fetch(`${ENDPOINT}/agent/chat`, {
method: âPOSTâ,
headers: { âContent-Typeâ: âapplication/jsonâ },
body: JSON.stringify({ query })
});
if (!res.ok) throw new Error(`Server error (${res.status}): ${await res.text()}`);
return res.json();
}
});
}); Next, update the View file (my-ai-agent-ui/webapp/view/ChatEntity.view.xml) as follows:<mvc:View controllerName=âmyaiagentui.controller.ChatEntityâ
xmlns:core=âsap.ui.coreâ
xmlns:mvc=âsap.ui.core.mvcâ
xmlns:f=âsap.fâ
xmlns:u=âsap.ui.unifiedâ
displayBlock=âtrueâ
xmlns=âsap.mâ>
<f:DynamicPage id=âmainDynamicPageâ stickySubheaderProvider=âiconTabBarâ class=âsapUiNoContentPaddingâ>
<f:title>
<f:DynamicPageTitle id=âmainDynamicTitleâ>
<f:heading>
<Title id=âpageTitleâ text=âAI Chat with Fioriâ/>
</f:heading>
<f:actions>
<Button id=âclearChatButtonâ
text=âClear Chatâ
icon=âsap-icon://refreshâ
type=âEmphasizedâ
press=âonClearChatPressâ/>
</f:actions>
</f:DynamicPageTitle>
</f:title>
<f:content>
<!â Main chat UI area â>
<VBox id=âchatMainAreaBoxâ class=âsapUiNoContentPadding chatMainAreaâ>
<IconTabBar id=âiconTabBarâ
class=âsapUiNoContentPaddingâ
stretchContentHeight=âtrueâ
expanded=âtrueâ
expandable=âfalseâ>
<items>
<IconTabFilter id=âchatbotTabâ text=âChatbotâ class=âsapUiNoContentPaddingâ>
<VBox id=âchatContainerBoxâ class=âchatContainer sapUiNoContentPaddingâ>
<!â Chat messages area â>
<ScrollContainer id=âchatScrollContainerâ
height=â100%â
width=â100%â
vertical=âtrueâ
focusable=âtrueâ
class=âchatScrollContainerâ>
<VBox id=âchatMessagesBoxâ items=â{ui>/messages}â class=âchatMessagesContainerâ>
<VBox id=âmessageItemBoxâ>
<!â Assistant message â>
<VBox id=âassistantMessageVBoxâ visible=â{= ${ui>role} === âassistantâ}â
class=âchatMessageWrapperâ>
<HBox id=âassistantMessageHBoxâ direction=âRowâ class=âchatMessageRowâ>
<HBox id=âassistantMessageHBox1âł backgroundDesign=âSolidâ
class=âchatBubbleAssistantâ>
<core:Icon id=âassistantMessageIconâ decorative=âtrueâ
src=âsap-icon://aiâ
class=âchatIconâ/>
<FormattedText id=âassistantMessageFormattedTextâ htmlText=â{ui>content}â/>
</HBox>
</HBox>
<!â Thinking process accordion â>
<HBox id=âthinkingToggleBoxâ visible=â{ui>hasThinkingProcess}â
class=âthinkingProcessContainerâ>
<Button id=âthinkingToggleButtonâ icon=â{= ${ui>isExpanded} ? âsap-icon://collapseâ : âsap-icon://expandâ }â
text=âToggle Thinking Processâ
type=âTransparentâ
press=âonToggleThinkingProcessâ
class=âthinkingProcessToggleâ/>
</HBox>
<VBox id=âthinkingVBoxâ visible=â{= ${ui>isExpanded} && ${ui>hasThinkingProcess} }â
class=âthinkingProcessContentâ>
<VBox id=âthinkingVBox1âł items=â{ui>thinkingProcess}â>
<VBox id=âthinkingStepBoxâ class=âthinkingStepBoxâ>
<Title id=âthinkingStepTitleâ
text=â{= âStep â + ${ui>stepIndex} }â
level=âH5âł
class=âthinkingStepTitleâ/>
<VBox id=âthinkingStepContentâ class=âthinkingStepContentâ>
<HBox id=âthinkingStepThoughtâ class=âthinkingStepItemâ>
<Label id=âThoughtLabelâ text=âThought:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âThoughtTextâ text=â{ui>thought}â wrapping=âtrueâ class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepActionâ class=âthinkingStepItemâ>
<Label id=âActionLabelâ text=âAction:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âActionTextâ text=â{ui>action}â class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepInputâ class=âthinkingStepItemâ>
<Label id=âInputLabelâ text=âInput:â design=âBoldâ class=âthinkingLabelâ/>
<Text id=âInputTextâ text=â{ui>action_input}â class=âthinkingTextâ/>
</HBox>
<HBox id=âthinkingStepObservationâ class=âthinkingStepItemâ>
<Label id=âObservationLabelâ text=âObservation:â design=âBoldâ class=âthinkingLabelâ/>
<VBox id=âObservationVBoxâ class=âthinkingTextâ>
<Text id=âObservationTextâ text=â{= ${ui>isObservationExpanded} ? ${ui>observationFull} : ${ui>observationTruncated} }â
wrapping=âtrueâ/>
<Link id=âObservationLinkâ
text=â{= ${ui>isObservationExpanded} ? âCollapseâ : âRead moreâ }â
visible=â{ui>hasLongObservation}â
press=âonToggleObservationâ
class=âobservationToggleLinkâ/>
</VBox>
</HBox>
</VBox>
</VBox>
</VBox>
</VBox>
</VBox>
<!â User message â>
<HBox id=âUserCommentâ
visible=â{= ${ui>role} === âuserâ}â
direction=âRowReverseâ
class=âchatMessageRowâ>
<HBox id=âUserCommentBoxâ
direction=âRowReverseâ
backgroundDesign=âSolidâ
class=âchatBubbleUser sapThemeBrand-asBackgroundColorâ>
<core:Icon id=âUserCommentIconâ
decorative=âtrueâ
src=âsap-icon://customerâ
class=âchatIcon sapThemeTextInvertedâ/>
<Text id=âUserCommentTextâ class=âsapThemeTextInvertedâ text=â{ui>content}â/>
</HBox>
</HBox>
</VBox>
</VBox>
</ScrollContainer>
<!â Input area + send button â>
<HBox id=âchatInputAreaBoxâ class=âchatInputAreaâ>
<TextArea id=âchatInputâ value=â{ui>/txtInput}â
width=â100%â
growing=âtrueâ
placeholder=âType your messageâŠâ
editable=â{= !${ui>/busy}}â
busyIndicatorDelay=â0âł
class=âchatTextAreaâ>
<layoutData>
<FlexItemData id=âInputDataâ growFactor=â1âł/>
</layoutData>
</TextArea>
<Button id=âsendButtonâ class=âchatSendButtonâ
type=âEmphasizedâ
icon=âsap-icon://paper-planeâ
text=âSendâ
press=âonBtnChatbotSendPressâ
busy=â{ui>/busy}â
busyIndicatorDelay=â0âł/>
</HBox>
</VBox>
</IconTabFilter>
</items>
</IconTabBar>
</VBox>
</f:content>
</f:DynamicPage>
</mvc:View> Additionally, for detailed UI design configuration, set up the styles in my-ai-agent-ui/webapp/css/style.css:/* Chat main area */
.chatMainArea {
height: 100vh;
min-height: 300px;
}
/* Chat container */
.chatContainer {
display: flex;
flex-direction: column;
height: calc(90vh â 100px);
padding: 0;
}
/* Chat scroll container */
.chatScrollContainer {
flex: 1;
min-height: 300px;
background-color: #f8f9fa;
border: none;
border-radius: 0;
padding: 0;
margin: 0;
}
/* Chat messages container */
.chatMessagesContainer {
padding: 1rem;
}
/* Chat message row */
.chatMessageRow {
width: 100%;
margin-bottom: 0.5rem;
}
/* Chat message wrapper */
.chatMessageWrapper {
margin-bottom: 1rem;
}
/* Assistant chat bubble */
.chatBubbleAssistant {
max-width: 70%;
min-width: 100px;
background-color: #ffffff;
border: 1px solid #d0d7de;
border-radius: 18px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
align-items: flex-start;
}
.chatBubbleAssistant .sapMFormattedText {
word-wrap: break-word;
line-height: 1.4;
}
/* User chat bubble */
.chatBubbleUser {
max-width: 70%;
min-width: 100px;
border-radius: 18px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
align-items: flex-start;
}
.chatBubbleUser .sapMText {
word-wrap: break-word;
line-height: 1.4;
}
/* Thinking process container */
.thinkingProcessContainer {
margin-top: 0.5rem;
margin-left: 2.5rem;
}
/* Thinking process toggle button */
.thinkingProcessToggle {
font-size: 0.875rem;
color: #0073e6;
}
.thinkingProcessToggle:hover {
background-color: rgba(0, 115, 230, 0.08) !important;
}
/* Thinking process content */
.thinkingProcessContent {
margin-left: 2.5rem;
margin-top: 0.5rem;
max-width: 70%;
}
/* Individual thinking step box */
.thinkingStepBox {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f5f7fa;
border: 1px solid #e4e7ea;
border-radius: 8px;
}
.thinkingStepBox:last-child {
margin-bottom: 0;
}
/* Thinking step title */
.thinkingStepTitle {
color: #0a6ed1;
margin-bottom: 0.75rem;
font-size: 1rem;
}
/* Thinking step content wrapper */
.thinkingStepContent {
padding-left: 0.5rem;
}
/* Thinking step item */
.thinkingStepItem {
margin-bottom: 0.5rem;
align-items: flex-start;
}
.thinkingStepItem:last-child {
margin-bottom: 0;
}
/* Label within thinking process */
.thinkingLabel {
min-width: 100px;
margin-right: 1rem;
color: #32363a;
font-size: 0.875rem;
}
/* Text within thinking process */
.thinkingText {
flex: 1;
font-size: 0.875rem;
color: #515456;
line-height: 1.5;
}
/* âRead moreâ link for observations */
.observationToggleLink {
margin-top: 0.25rem;
font-size: 0.875rem;
}
/* Input area at bottom */
.chatInputArea {
background-color: #ffffff;
padding: 1rem;
border-top: 1px solid #e4e7ea;
align-items: flex-end;
flex-shrink: 0;
}
/* TextArea styling */
.chatTextArea .sapMTextAreaInner {
border: 2px solid #e4e7ea;
background-color: #ffffff;
}
.chatTextArea .sapMTextAreaInner:focus-within {
border-color: #0073e6;
}
/* Send button styling */
.chatSendButton {
min-height: 40px;
border-radius: 20px;
margin-left: 0.5rem;
}
/* Icon adjustments */
.chatIcon {
font-size: 1.2rem;
margin-top: 2px;
}
.chatBubbleAssistant .chatIcon {
margin-right: 0.5rem;
}
.chatBubbleUser .chatIcon {
margin-left: 0.5rem;
}
/* Remove padding from IconTabBar */
.sapMITB {
padding: 0;
}
.sapMITBContent {
padding: 0;
background-color: transparent;
}
/* Responsive tweaks */
@media (max-width: 768px) {
.chatBubbleAssistant,
.chatBubbleUser {
max-width: 85%;
}
.chatContainer {
height: calc(100vh â 80px);
}
.chatScrollContainer {
min-height: 300px;
}
.chatSendButton .sapMBtnInner .sapMBtnContent {
font-size: 0;
}
.chatSendButton .sapMBtnIcon {
margin-right: 0;
}
.thinkingProcessContent {
max-width: 85%;
}
.thinkingLabel {
min-width: 80px;
margin-right: 0.5rem;
}
} To forward requests from the chat UI to the Python API, weâll add a proxy middleware. First, install the dependency:npm install ui5-middleware-simpleproxy âsave-dev And then, in my-ai-agent-ui/package.json, add the following section: âŠ
âmainâ: âwebapp/index.htmlâ,
âdevDependenciesâ: {
// (your existing devDependencies)
},
âui5â: {
âdependenciesâ: [
âui5-middleware-simpleproxyâ
]
},
âscriptsâ: {
// (your existing scripts)
}
âŠAnd with this in place, any API requests from the chat UI will be forwarded to your local Python API. 5 | Test the Fiori ApplicationRun both the app and the API locally to confirm that the chat feature works as expected.First, start the Python API in the my-ai-agent-api directory:gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app âbind 0.0.0.0:${PORT:-8000}Then, open a new terminal, navigate to the my-ai-agent-ui root folder, and run:npm start Finally, open the URL shown in the console (typically http://localhost:8080) in your browser, type a message into the chat input field, and send it. If you see a response from the AI agent, your setup is working correctly!âWhen you click the Clear Chat button on the chat screen, the history will be reset and youâll see the confirmation âChat cleared.â Verify that you can start a new conversation without any issues! 6 | Challenge â Add a Text-File Upload ButtonLetâs try uploading extra text and interacting with the AI Agent using the newly added documents. Be aware that you wonât be able to implement this challenge unless youâve already completed the previous chapterâs challenge.Add a dedicated .txt file upload button to the header of the chat UI, send the selected file to the Python API, and have the server chunk and store the file. To make the process clearer for users, block the screen with a BusyIndicator while the upload is in progress. Add the following three items to ChatEntity.controller.js (adjust the paths and namespaces as needed for your environment):Import sap/ui/core/BusyIndicator at the toponFileChange handler â performs file-type validation and triggers the upload_uploadFile method â uses fetch with FormData to POST to the /agent/upload API/** When a file is selected */
async onFileChange(oEvent) {
const oFileUploader = oEvent.getSource();
const file = (oEvent.getParameter(âfilesâ) || [])[0];
if (!file) return;
if (!file.name.endsWith(â.txtâ)) {
MessageToast.show(âOnly .txt files can be uploaded.â);
oFileUploader.clear();
return;
}
await this._uploadFile(file);
oFileUploader.clear();
},
/** File upload */
async _uploadFile(file) {
const ui = this.getView().getModel(âuiâ);
ui.setProperty(â/busyâ, true);
BusyIndicator.show(0);
try {
const fd = new FormData();
fd.append(âfileâ, file);
const res = await fetch(`${ENDPOINT}/agent/upload`, {
method: âPOSTâ,
body: fd
});
if (!res.ok) throw new Error(await res.text());
const { filename, chunks_created } = await res.json();
const files = ui.getProperty(â/uploadedFilesâ) || [];
ui.setProperty(â/uploadedFilesâ, [
âŠfiles,
{
name: filename,
chunks: chunks_created,
uploadedAt: new Date()
}
]);
MessageToast.show(`Uploaded â${filename}â (${chunks_created} chunks created)`);
} catch (err) {
console.error(err);
MessageToast.show(`Upload error: ${err.message}`);
} finally {
ui.setProperty(â/busyâ, false);
BusyIndicator.hide();
}
} Next, place the FileUploader and Clear buttons side-by-side in the header. The key point is to set buttonOnly=âtrueâ on the FileUploader so it appears as an icon button.<f:actions>
<u:FileUploader id=âfileUploaderâ
name=âfileâ
placeholder=âSelect a fileâŠâ
fileType=âtxtâ
change=âonFileChangeâ
buttonText=ââ
buttonOnly=âtrueâ
icon=âsap-icon://uploadâ
class=âfileUploaderButtonâ />
<Button id=âclearChatButtonâ
text=âClear Chatâ
icon=âsap-icon://refreshâ
type=âEmphasizedâ
press=âonClearChatPressâ />
</f:actions> Finally, display an âUploaded Filesâ list beneath the header so users can quickly see how many chunks each file was split into.<HBox id=âfileUploadAreaBoxâ
visible=â{= ${ui>/uploadedFiles}.length > 0}â>
<Text text=âAdditional uploaded files:â />
<HBox items=â{ui>/uploadedFiles}â>
<ObjectStatus text=â{ui>name}â icon=âsap-icon://document-textâ />
</HBox>
</HBox> Advanced Challenge â Reset ButtonWhen you keep uploading files, the vector DB grows larger and larger. For testing purposes, letâs add a button that resets the entire database at once.API: Implement /agent/delete (hint â see the DB-initialization code from Part 4).Controller: Implement onDeletePress, call /agent/delete, and clear the UIâs file list.View: Place a Delete button between the upload and clear-chat buttons in the header.Implementation tipIf you set the UI modelâs /uploadedFiles to an empty array [ ] , the uploaded-files area automatically disappears. 7 | Next UpPart 8 Deploying to Cloud FoundryIn Part 8 weâll deploy the completed application to Cloud Foundry. The UI will go through mta build â cf deploy, while the Python backend will use manifest.yaml with cf pushâa two-track approach aimed at âblazing-fast deployment.â Stay tuned! DisclaimerAll the views and opinions in the blog are my own and is made in my personal capacity and that SAP shall not be responsible or liable for any of the contents published in this blog. Read More Technology Blog Posts by SAP articles
#SAP
#SAPTechnologyblog