🚀 SAP AI Core Agent QuickLaunch Series 🚀 – Part 7: Building the Chat UI with SAPUI5

Estimated read time 29 min read

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} &amp;&amp; ${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} &amp;&amp; ${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

You May Also Like

More From Author