生成 AI は研究室の外へ飛び出し、今やビジネス現場の常識を塗り替えています。SAPは全速力でその波に乗っています。このブログシリーズでは、SAP AI Core の既定モデルを最速で呼び出し、実務で使える AI エージェントへ拡張する“秒速ハンズオン”をお届けします。
お知らせ
You can find the English version here.
本シリーズで学べること
SAP AI Core 上でカスタム AI Agent を “秒速” で動かす方法LangChain・Google 検索ツール・RAGを使った実装AI Agent を REST API 化し、SAPUI5/Fiori の UI に載せ、Cloud Foundryにデプロイする手順
学習時間
各章は 10–15 分 で読める&手を動かせるを予定しています。
️ 連載ロードマップ
Part 0 プロローグPart 1 環境構築: SAP AI CoreとAI LaunchpadPart 2 LangChain でチャットモデルを構築Part 3 Agentツール: Google 検索を統合Part 4 RAG 基礎 ①: HANA Cloud VectorEngineと埋め込み処理Part 5 RAG 基礎 ②: Retriever Toolの構築Part 6 AI Agent を REST API 化Part 7 SAPUI5 でチャットUI を構築 [現在のブログ]Part 8 CloudFoundry にデプロイ
注記
続編は順次公開予定です。
この記事がお役に立ったら、ぜひ Kudos を押していただけると励みになります。 「ここをもっと詳しく知りたい」「別のテーマも取り上げてほしい」など、ご要望があればお気軽にコメントください!
SAPUI5 でチャットUI を構築
1 | はじめに
本章では、前章までに構築した AI Agent を、SAP UI5/Fiori ベースのチャット UI から呼び出せるようにします。
SAP UI5 は SAP が提供する UI フレームワークであり、企業向けのモダンな Web アプリケーションを短時間で開発できる点が特徴です。特に SAP Business Application Studio(BAS) を利用すると、Fiori 用テンプレートからプロジェクト構成を自動生成できるため、ディレクトリや設定ファイルの手動作成が不要になります。
2 | 事前準備
BTP サブアカウントSAP AI Core インスタンスSAP AI LaunchpadのサブスクリプションPython 3.13環境 & pipVSCodeやBASなどのIDE
Trial 環境の注意
Trial の HANA Cloud インスタンスは 毎晩自動停止 されます。日をまたぐ作業の場合は翌日インスタンスを再起動してください
3 | Fiori アプリケーションの準備
SAP 系 UI 開発では、BAS のテンプレート機能の利用を強く推奨します。テンプレートから生成されるフォルダ構成と設定ファイルをベースにすることで、Fiori アプリ固有のマニフェスト(mta.yaml)やモジュール構成を素早く整備できます。
BAS で Create Dev Space をクリックし、テンプレートに SAP Fiori を選択します。併せて前章で作成した Python API をローカルで起動したいので、Additional SAP Extensions から Python Tools を選択します。
左上のハンバーガーメニュー → File > New Project From Template を選択します。
Template Selection は Basic を選択します。
ata Source and Service Selection では、UI に表示するデータは Python API から取得するため None を選択します。
Entity Selection で Entity name に ChatEntity と入力します。
Project Attributes を以下のように設定し、Add deployment configuration を Yes にします。
項目値Module Namemy-ai-agent-uiApplication TitleMy AI ChatDescriptionChat UI for AI Agent
Deployment Configuration では次のように設定します。
項目値TargetCloud FoundryDestinationNoneAdd Router ModuleAdd Application to Managed Application Router
まれに Target を CF にしても Router Module のオプションが表示されないことがあります。その場合は、一度 Target を ABAP に切り替えてから Cloud Foundry に戻すと正しく表示されます。
最後に、Python API の配置と動作確認を実施します。構成は次のようになります。
# フォルダ構成
PROJECTS
├── my-ai-agent-ui/
├── my-ai-agent-api/
├── main.py
├── requirements.txt
└── .env
my-ai-agent-api 配下で仮想環境を作成・有効化し、以下のコマンドでローカルサーバが起動するか確認します。
gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app –bind 0.0.0.0:${PORT:-8000}
これで、Fioriアプリケーションの構築準備が整いました!
4 | Fiori アプリケーションの改良
生成された Fiori プロジェクトをベースに、チャット UI が AI Agent と通信できるようにコードを調整していきましょう。
まずコントローラー(my-ai-agent-ui/webapp/controller/ChatEntity.controller.js)を以下のように書きます。
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”;
// 環境によってエンドポイントを切り替え
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”, {
/** 初期化 */
onInit() {
this.getView().setModel(new JSONModel({
busy: false,
txtInput: “”,
uploadedFiles: [],
messages: [{
role: “assistant”,
content: “こんにちは!私はチャットボットです。何かお手伝いできることはありますか?”,
hasThinkingProcess: false
}]
}), “ui”);
this.byId(“fileUploader”)?.setUploadUrl(`${ENDPOINT}/agent/upload`);
},
/** チャット履歴を初期化 */
onClearChatPress() {
this.getView().getModel(“ui”).setProperty(“/messages”, [{
role: “assistant”,
content: “こんにちは!私はチャットボットです。何かお手伝いできることはありますか?”,
hasThinkingProcess: false
}]);
MessageToast.show(“チャットをクリアしました”);
},
/** メッセージ送信 */
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(`エラー: ${err.message}`);
}finally{
ui.setProperty(“/busy”, false);
}
},
/** 思考プロセス表示切替 */
onToggleThinkingProcess(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(“ui”);
if (ctx) this._toggleFlag(ctx, “isExpanded”);
},
/** Observation 表示切替 */
onToggleObservation(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(“ui”);
if (ctx) this._toggleFlag(ctx, “isObservationExpanded”);
},
/** 任意フラグを反転 */
_toggleFlag(ctx, flag) {
const ui = this.getView().getModel(“ui”);
const path = ctx.getPath();
ui.setProperty(`${path}/${flag}`, !ui.getProperty(`${path}/${flag}`));
},
/** チャットを最下部へスクロール */
_scrollToBottom() {
setTimeout(() => {
const sc = this.byId(“chatScrollContainer”);
const items = sc?.getContent()[0].getItems();
if (items?.length) sc.scrollToElement(items[items.length – 1]);
}, 100);
},
/** AI チャット 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(`サーバーエラー (${res.status}): ${await res.text()}`);
return res.json();
}
});
});
次に View(my-ai-agent-ui/webapp/view/ChatEntity.view.xml)を以下のように変更します。
<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 チャット with Fiori”/>
</f:heading>
<f:actions>
<Button id=”clearChatButton”
text=”チャットをクリア”
icon=”sap-icon://refresh”
type=”Emphasized”
press=”onClearChatPress”/>
</f:actions>
</f:DynamicPageTitle>
</f:title>
<f:content>
<!– チャット UI メインエリア –>
<VBox id=”chatMainAreaBox” class=”sapUiNoContentPadding chatMainArea”>
<IconTabBar id=”iconTabBar”
class=”sapUiNoContentPadding”
stretchContentHeight=”true”
expanded=”true”
expandable=”false”>
<items>
<IconTabFilter id=”chatbotTab” text=”チャットボット” class=”sapUiNoContentPadding”>
<VBox id=”chatContainerBox” class=”chatContainer sapUiNoContentPadding”>
<!– チャットメッセージエリア –>
<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 コメント –>
<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>
<!– 思考プロセスアコーディオン –>
<HBox id=”thinkingToggleBox” visible=”{ui>hasThinkingProcess}”
class=”thinkingProcessContainer”>
<Button id=”thinkingToggleButton” icon=”{= ${ui>isExpanded} ? ‘sap-icon://collapse’ : ‘sap-icon://expand’ }”
text=”思考プロセスを{= ${ui>isExpanded} ? ‘隠す’ : ‘表示’ }”
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} ? ‘折りたたむ’ : ‘続きを読む’ }”
visible=”{ui>hasLongObservation}”
press=”onToggleObservation”
class=”observationToggleLink”/>
</VBox>
</HBox>
</VBox>
</VBox>
</VBox>
</VBox>
</VBox>
<!– user コメント –>
<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>
<!– 入力欄+送信ボタン –>
<HBox id=”chatInputAreaBox” class=”chatInputArea”>
<TextArea id=”chatInput” value=”{ui>/txtInput}”
width=”100%”
growing=”true”
placeholder=”メッセージを入力してください…”
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=”送信”
press=”onBtnChatbotSendPress”
busy=”{ui>/busy}”
busyIndicatorDelay=”0″/>
</HBox>
</VBox>
</IconTabFilter>
</items>
</IconTabBar>
</VBox>
</f:content>
</f:DynamicPage>
</mvc:View>
さらに、UI の詳細なデザイン設定として、スタイル(my-ai-agent-ui/webapp/css/style.css)を設定します。
/* チャットメインエリア */
.chatMainArea {
height: 100vh;
min-height: 300px;
}
/* チャットコンテナ */
.chatContainer {
display: flex;
flex-direction: column;
height: calc(90vh – 100px);
padding: 0;
}
/* チャットスクロールコンテナ */
.chatScrollContainer {
flex: 1;
min-height: 300px;
background-color: #f8f9fa;
border: none;
border-radius: 0;
padding: 0;
margin: 0;
}
/* チャットメッセージコンテナ */
.chatMessagesContainer {
padding: 1rem;
}
/* チャットメッセージ行 */
.chatMessageRow {
width: 100%;
margin-bottom: 0.5rem;
}
/* チャットメッセージラッパー */
.chatMessageWrapper {
margin-bottom: 1rem;
}
/* チャットコメント – アシスタント */
.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;
}
/* チャットコメント – ユーザー */
.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;
}
/* 思考プロセスコンテナ */
.thinkingProcessContainer {
margin-top: 0.5rem;
margin-left: 2.5rem;
}
/* 思考プロセストグルボタン */
.thinkingProcessToggle {
font-size: 0.875rem;
color: #0073e6;
}
.thinkingProcessToggle:hover {
background-color: rgba(0, 115, 230, 0.08) !important;
}
/* 思考プロセスコンテンツ */
.thinkingProcessContent {
margin-left: 2.5rem;
margin-top: 0.5rem;
max-width: 70%;
}
/* 思考ステップボックス */
.thinkingStepBox {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f5f7fa;
border: 1px solid #e4e7ea;
border-radius: 8px;
}
.thinkingStepBox:last-child {
margin-bottom: 0;
}
/* 思考ステップタイトル */
.thinkingStepTitle {
color: #0a6ed1;
margin-bottom: 0.75rem;
font-size: 1rem;
}
/* 思考ステップコンテンツ */
.thinkingStepContent {
padding-left: 0.5rem;
}
/* 思考ステップ項目 */
.thinkingStepItem {
margin-bottom: 0.5rem;
align-items: flex-start;
}
.thinkingStepItem:last-child {
margin-bottom: 0;
}
/* 思考ラベル */
.thinkingLabel {
min-width: 100px;
margin-right: 1rem;
color: #32363a;
font-size: 0.875rem;
}
/* 思考テキスト */
.thinkingText {
flex: 1;
font-size: 0.875rem;
color: #515456;
line-height: 1.5;
}
/* Observation続きを読むリンク */
.observationToggleLink {
margin-top: 0.25rem;
font-size: 0.875rem;
}
/* 入力エリア */
.chatInputArea {
background-color: #ffffff;
padding: 1rem;
border-top: 1px solid #e4e7ea;
align-items: flex-end;
flex-shrink: 0;
}
/* テキストエリアのスタイル調整 */
.chatTextArea .sapMTextAreaInner {
border: 2px solid #e4e7ea;
background-color: #ffffff;
}
.chatTextArea .sapMTextAreaInner:focus-within {
border-color: #0073e6;
}
/* 送信ボタン */
.chatSendButton {
min-height: 40px;
border-radius: 20px;
margin-left: 0.5rem;
}
/* アイコン調整 */
.chatIcon {
font-size: 1.2rem;
margin-top: 2px;
}
.chatBubbleAssistant .chatIcon {
margin-right: 0.5rem;
}
.chatBubbleUser .chatIcon {
margin-left: 0.5rem;
}
/* IconTabBar のパディング削除 */
.sapMITB {
padding: 0;
}
.sapMITBContent {
padding: 0;
background-color: transparent;
}
/* レスポンシブ対応 */
@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;
}
}
チャット UI から Python API へリクエストを転送するため、プロキシミドルウェアを追加します。まず依存関係をインストールしましょう。
npm install ui5-middleware-simpleproxy –save-dev
そして my-ai-agent-ui/package.json に次の設定を追記してください。
…
“main”: “webapp/index.html”,
“devDependencies”: {
(省略)
},
/* ここから
“ui5”: {
“dependencies”: [
“ui5-middleware-simpleproxy”
]
},
ここまで */
“scripts”: {
….
これでチャット UI からの /api リクエストがローカルの Python API へ転送されるようになります。
5 | Fiori アプリケーションのテスト
ローカル環境でアプリと API を同時に起動し、実際にチャット機能が動作するか確認しましょう。
まず、my-ai-agent-api ディレクトリで以下のコマンドを実行して Python API を起動します。
gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app –bind 0.0.0.0:${PORT:-8000}
また、別のターミナルを開き、my-ai-agent-ui のルートで次のコマンドを実行します。
npm start
ログに表示された URL(通常は http://localhost:8080)をブラウザで開き、チャット入力欄にメッセージを送ってみてください。AI エージェントから返答が表示されれば成功です!
チャット画面の チャットをクリア ボタンをクリックすると履歴がリセットされ、「チャットをクリアしました」と確認できます。新しい会話を問題なく始められるか確認しましょう!
6 | チャレンジ – テキストファイルアップロードボタンを追加しよう
テキストを追加でアップロードして、追加したドキュメントを踏まえた AI Agent とのやりとりを実現して見ましょう。なお、このチャレンジは前章のチャレンジを実施していないと実装できないので注意しましょう。
チャット UI のヘッダーに .txt ファイル専用のアップロードボタン を追加し、選択したファイルを Python API へ送信し、サーバ側でファイルをチャンク化して保存するようにします。途中経過をわかりやすくするため、アップロード中は BusyIndicator で画面をブロックするようにしましょう。
ChatEntity.controller.js で次の 3 点を追加します(パスや名前空間は環境に合わせてください):
冒頭の sap/ui/core/BusyIndicator インポートonFileChange ハンドラ – ファイル種別チェック & アップロード呼び出し_uploadFile メソッド – fetch + FormData で API /agent/upload へ POST/** ファイル選択時 */
async onFileChange(oEvent) {
const oFileUploader = oEvent.getSource();
const file = (oEvent.getParameter(“files”) || [])[0];
if (!file) return;
if (!file.name.endsWith(“.txt”)) {
MessageToast.show(“txt形式のファイルのみアップロード可能です。”);
oFileUploader.clear();
return;
}
await this._uploadFile(file);
oFileUploader.clear();
},
/** ファイルアップロード */
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(`”${filename}” をアップロードしました(${chunks_created} チャンク作成)`);
} catch (err) {
console.error(err);
MessageToast.show(`アップロードエラー: ${err.message}`);
} finally {
ui.setProperty(“/busy”, false);
BusyIndicator.hide();
}
}
次に、ヘッダーへ FileUploader と Clear ボタンを並べます。FileUploader は buttonOnly=”true” にして、アイコンボタンにするのがコツです。
<f:actions>
<u:FileUploader id=”fileUploader”
name=”file”
placeholder=”ファイルを選択…”
fileType=”txt”
change=”onFileChange”
buttonText=””
buttonOnly=”true”
icon=”sap-icon://upload”
class=”fileUploaderButton” />
<Button id=”clearChatButton”
text=”チャットをクリア”
icon=”sap-icon://refresh”
type=”Emphasized”
press=”onClearChatPress” />
</f:actions>
さらにヘッダー下部に “アップロード済ファイル一覧” を出すことで、どのファイルが何チャンクに分割されたかを視覚化できます。
<HBox id=”fileUploadAreaBox”
visible=”{= ${ui>/uploadedFiles}.length > 0}”>
<Text text=”追加でアップロードしたファイル:” />
<HBox items=”{ui>/uploadedFiles}”>
<ObjectStatus text=”{ui>name}” icon=”sap-icon://document-text” />
</HBox>
</HBox>
上級チャレンジ – リセットボタン
ファイルをアップロードしていくとベクトル DB がどんどん膨らみます。テスト用に データベースを一括リセット するボタンを追加してみましょう。
API 側: /agent/delete を実装(ヒント: Part4 における DB を初期化するコード)。Controller: onDeletePress を実装し、/agent/delete を呼び出して UI のファイル一覧も空にする。View: ヘッダーのアップロードボタンとクリアボタンの間に Delete ボタンを配置。
実装ヒント
UI Model の /uploadedFiles を空配列 [] にすると、ファイル一覧エリアが自動的に非表示になります。
7 | 次回予告
Part 8 CloudFoundry にデプロイ
Part 8 では、完成したアプリケーションを Cloud Foundry へデプロイします。UI は mta build → cf deploy、Python バックエンドは manifest.yaml を使って cf push ― 2 本立てで “秒速デプロイ” に挑戦します。お楽しみに!
免責事項
本ブログに記載された見解および意見はすべて私個人のものであり、私の個人的な立場で発信しています。SAP は本ブログの内容について一切の責任を負いません。
生成 AI は研究室の外へ飛び出し、今やビジネス現場の常識を塗り替えています。SAPは全速力でその波に乗っています。このブログシリーズでは、SAP AI Core の既定モデルを最速で呼び出し、実務で使える AI エージェントへ拡張する“秒速ハンズオン”をお届けします。お知らせYou can find the English version here. 本シリーズで学べることSAP AI Core 上でカスタム AI Agent を “秒速” で動かす方法LangChain・Google 検索ツール・RAGを使った実装AI Agent を REST API 化し、SAPUI5/Fiori の UI に載せ、Cloud Foundryにデプロイする手順学習時間各章は 10–15 分 で読める&手を動かせるを予定しています。
連載ロードマップPart 0 プロローグPart 1 環境構築: SAP AI CoreとAI LaunchpadPart 2 LangChain でチャットモデルを構築Part 3 Agentツール: Google 検索を統合Part 4 RAG 基礎 ①: HANA Cloud VectorEngineと埋め込み処理Part 5 RAG 基礎 ②: Retriever Toolの構築Part 6 AI Agent を REST API 化Part 7 SAPUI5 でチャットUI を構築 [現在のブログ]Part 8 CloudFoundry にデプロイ注記続編は順次公開予定です。この記事がお役に立ったら、ぜひ Kudos を押していただけると励みになります。 「ここをもっと詳しく知りたい」「別のテーマも取り上げてほしい」など、ご要望があればお気軽にコメントください!SAPUI5 でチャットUI を構築1 | はじめに本章では、前章までに構築した AI Agent を、SAP UI5/Fiori ベースのチャット UI から呼び出せるようにします。SAP UI5 は SAP が提供する UI フレームワークであり、企業向けのモダンな Web アプリケーションを短時間で開発できる点が特徴です。特に SAP Business Application Studio(BAS) を利用すると、Fiori 用テンプレートからプロジェクト構成を自動生成できるため、ディレクトリや設定ファイルの手動作成が不要になります。 2 | 事前準備BTP サブアカウントSAP AI Core インスタンスSAP AI LaunchpadのサブスクリプションPython 3.13環境 & pipVSCodeやBASなどのIDETrial 環境の注意Trial の HANA Cloud インスタンスは 毎晩自動停止 されます。日をまたぐ作業の場合は翌日インスタンスを再起動してください 3 | Fiori アプリケーションの準備SAP 系 UI 開発では、BAS のテンプレート機能の利用を強く推奨します。テンプレートから生成されるフォルダ構成と設定ファイルをベースにすることで、Fiori アプリ固有のマニフェスト(mta.yaml)やモジュール構成を素早く整備できます。 BAS で Create Dev Space をクリックし、テンプレートに SAP Fiori を選択します。併せて前章で作成した Python API をローカルで起動したいので、Additional SAP Extensions から Python Tools を選択します。左上のハンバーガーメニュー → File > New Project From Template を選択します。Template Selection は Basic を選択します。ata Source and Service Selection では、UI に表示するデータは Python API から取得するため None を選択します。Entity Selection で Entity name に ChatEntity と入力します。Project Attributes を以下のように設定し、Add deployment configuration を Yes にします。項目値Module Namemy-ai-agent-uiApplication TitleMy AI ChatDescriptionChat UI for AI AgentDeployment Configuration では次のように設定します。項目値TargetCloud FoundryDestinationNoneAdd Router ModuleAdd Application to Managed Application Routerまれに Target を CF にしても Router Module のオプションが表示されないことがあります。その場合は、一度 Target を ABAP に切り替えてから Cloud Foundry に戻すと正しく表示されます。 最後に、Python API の配置と動作確認を実施します。構成は次のようになります。# フォルダ構成
PROJECTS
├── my-ai-agent-ui/
├── my-ai-agent-api/
├── main.py
├── requirements.txt
└── .envmy-ai-agent-api 配下で仮想環境を作成・有効化し、以下のコマンドでローカルサーバが起動するか確認します。gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app –bind 0.0.0.0:${PORT:-8000}これで、Fioriアプリケーションの構築準備が整いました! 4 | Fiori アプリケーションの改良生成された Fiori プロジェクトをベースに、チャット UI が AI Agent と通信できるようにコードを調整していきましょう。まずコントローラー(my-ai-agent-ui/webapp/controller/ChatEntity.controller.js)を以下のように書きます。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”;
// 環境によってエンドポイントを切り替え
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”, {
/** 初期化 */
onInit() {
this.getView().setModel(new JSONModel({
busy: false,
txtInput: “”,
uploadedFiles: [],
messages: [{
role: “assistant”,
content: “こんにちは!私はチャットボットです。何かお手伝いできることはありますか?”,
hasThinkingProcess: false
}]
}), “ui”);
this.byId(“fileUploader”)?.setUploadUrl(`${ENDPOINT}/agent/upload`);
},
/** チャット履歴を初期化 */
onClearChatPress() {
this.getView().getModel(“ui”).setProperty(“/messages”, [{
role: “assistant”,
content: “こんにちは!私はチャットボットです。何かお手伝いできることはありますか?”,
hasThinkingProcess: false
}]);
MessageToast.show(“チャットをクリアしました”);
},
/** メッセージ送信 */
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(`エラー: ${err.message}`);
}finally{
ui.setProperty(“/busy”, false);
}
},
/** 思考プロセス表示切替 */
onToggleThinkingProcess(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(“ui”);
if (ctx) this._toggleFlag(ctx, “isExpanded”);
},
/** Observation 表示切替 */
onToggleObservation(oEvent) {
const ctx = oEvent.getSource()?.getBindingContext(“ui”);
if (ctx) this._toggleFlag(ctx, “isObservationExpanded”);
},
/** 任意フラグを反転 */
_toggleFlag(ctx, flag) {
const ui = this.getView().getModel(“ui”);
const path = ctx.getPath();
ui.setProperty(`${path}/${flag}`, !ui.getProperty(`${path}/${flag}`));
},
/** チャットを最下部へスクロール */
_scrollToBottom() {
setTimeout(() => {
const sc = this.byId(“chatScrollContainer”);
const items = sc?.getContent()[0].getItems();
if (items?.length) sc.scrollToElement(items[items.length – 1]);
}, 100);
},
/** AI チャット 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(`サーバーエラー (${res.status}): ${await res.text()}`);
return res.json();
}
});
}); 次に View(my-ai-agent-ui/webapp/view/ChatEntity.view.xml)を以下のように変更します。<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 チャット with Fiori”/>
</f:heading>
<f:actions>
<Button id=”clearChatButton”
text=”チャットをクリア”
icon=”sap-icon://refresh”
type=”Emphasized”
press=”onClearChatPress”/>
</f:actions>
</f:DynamicPageTitle>
</f:title>
<f:content>
<!– チャット UI メインエリア –>
<VBox id=”chatMainAreaBox” class=”sapUiNoContentPadding chatMainArea”>
<IconTabBar id=”iconTabBar”
class=”sapUiNoContentPadding”
stretchContentHeight=”true”
expanded=”true”
expandable=”false”>
<items>
<IconTabFilter id=”chatbotTab” text=”チャットボット” class=”sapUiNoContentPadding”>
<VBox id=”chatContainerBox” class=”chatContainer sapUiNoContentPadding”>
<!– チャットメッセージエリア –>
<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 コメント –>
<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>
<!– 思考プロセスアコーディオン –>
<HBox id=”thinkingToggleBox” visible=”{ui>hasThinkingProcess}”
class=”thinkingProcessContainer”>
<Button id=”thinkingToggleButton” icon=”{= ${ui>isExpanded} ? ‘sap-icon://collapse’ : ‘sap-icon://expand’ }”
text=”思考プロセスを{= ${ui>isExpanded} ? ‘隠す’ : ‘表示’ }”
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} ? ‘折りたたむ’ : ‘続きを読む’ }”
visible=”{ui>hasLongObservation}”
press=”onToggleObservation”
class=”observationToggleLink”/>
</VBox>
</HBox>
</VBox>
</VBox>
</VBox>
</VBox>
</VBox>
<!– user コメント –>
<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>
<!– 入力欄+送信ボタン –>
<HBox id=”chatInputAreaBox” class=”chatInputArea”>
<TextArea id=”chatInput” value=”{ui>/txtInput}”
width=”100%”
growing=”true”
placeholder=”メッセージを入力してください…”
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=”送信”
press=”onBtnChatbotSendPress”
busy=”{ui>/busy}”
busyIndicatorDelay=”0″/>
</HBox>
</VBox>
</IconTabFilter>
</items>
</IconTabBar>
</VBox>
</f:content>
</f:DynamicPage>
</mvc:View> さらに、UI の詳細なデザイン設定として、スタイル(my-ai-agent-ui/webapp/css/style.css)を設定します。/* チャットメインエリア */
.chatMainArea {
height: 100vh;
min-height: 300px;
}
/* チャットコンテナ */
.chatContainer {
display: flex;
flex-direction: column;
height: calc(90vh – 100px);
padding: 0;
}
/* チャットスクロールコンテナ */
.chatScrollContainer {
flex: 1;
min-height: 300px;
background-color: #f8f9fa;
border: none;
border-radius: 0;
padding: 0;
margin: 0;
}
/* チャットメッセージコンテナ */
.chatMessagesContainer {
padding: 1rem;
}
/* チャットメッセージ行 */
.chatMessageRow {
width: 100%;
margin-bottom: 0.5rem;
}
/* チャットメッセージラッパー */
.chatMessageWrapper {
margin-bottom: 1rem;
}
/* チャットコメント – アシスタント */
.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;
}
/* チャットコメント – ユーザー */
.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;
}
/* 思考プロセスコンテナ */
.thinkingProcessContainer {
margin-top: 0.5rem;
margin-left: 2.5rem;
}
/* 思考プロセストグルボタン */
.thinkingProcessToggle {
font-size: 0.875rem;
color: #0073e6;
}
.thinkingProcessToggle:hover {
background-color: rgba(0, 115, 230, 0.08) !important;
}
/* 思考プロセスコンテンツ */
.thinkingProcessContent {
margin-left: 2.5rem;
margin-top: 0.5rem;
max-width: 70%;
}
/* 思考ステップボックス */
.thinkingStepBox {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: #f5f7fa;
border: 1px solid #e4e7ea;
border-radius: 8px;
}
.thinkingStepBox:last-child {
margin-bottom: 0;
}
/* 思考ステップタイトル */
.thinkingStepTitle {
color: #0a6ed1;
margin-bottom: 0.75rem;
font-size: 1rem;
}
/* 思考ステップコンテンツ */
.thinkingStepContent {
padding-left: 0.5rem;
}
/* 思考ステップ項目 */
.thinkingStepItem {
margin-bottom: 0.5rem;
align-items: flex-start;
}
.thinkingStepItem:last-child {
margin-bottom: 0;
}
/* 思考ラベル */
.thinkingLabel {
min-width: 100px;
margin-right: 1rem;
color: #32363a;
font-size: 0.875rem;
}
/* 思考テキスト */
.thinkingText {
flex: 1;
font-size: 0.875rem;
color: #515456;
line-height: 1.5;
}
/* Observation続きを読むリンク */
.observationToggleLink {
margin-top: 0.25rem;
font-size: 0.875rem;
}
/* 入力エリア */
.chatInputArea {
background-color: #ffffff;
padding: 1rem;
border-top: 1px solid #e4e7ea;
align-items: flex-end;
flex-shrink: 0;
}
/* テキストエリアのスタイル調整 */
.chatTextArea .sapMTextAreaInner {
border: 2px solid #e4e7ea;
background-color: #ffffff;
}
.chatTextArea .sapMTextAreaInner:focus-within {
border-color: #0073e6;
}
/* 送信ボタン */
.chatSendButton {
min-height: 40px;
border-radius: 20px;
margin-left: 0.5rem;
}
/* アイコン調整 */
.chatIcon {
font-size: 1.2rem;
margin-top: 2px;
}
.chatBubbleAssistant .chatIcon {
margin-right: 0.5rem;
}
.chatBubbleUser .chatIcon {
margin-left: 0.5rem;
}
/* IconTabBar のパディング削除 */
.sapMITB {
padding: 0;
}
.sapMITBContent {
padding: 0;
background-color: transparent;
}
/* レスポンシブ対応 */
@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;
}
} チャット UI から Python API へリクエストを転送するため、プロキシミドルウェアを追加します。まず依存関係をインストールしましょう。npm install ui5-middleware-simpleproxy –save-dev そして my-ai-agent-ui/package.json に次の設定を追記してください。…
“main”: “webapp/index.html”,
“devDependencies”: {
(省略)
},
/* ここから
“ui5”: {
“dependencies”: [
“ui5-middleware-simpleproxy”
]
},
ここまで */
“scripts”: {
….これでチャット UI からの /api リクエストがローカルの Python API へ転送されるようになります。 5 | Fiori アプリケーションのテストローカル環境でアプリと API を同時に起動し、実際にチャット機能が動作するか確認しましょう。まず、my-ai-agent-api ディレクトリで以下のコマンドを実行して Python API を起動します。gunicorn -w 1 -k uvicorn.workers.UvicornWorker main:app –bind 0.0.0.0:${PORT:-8000}また、別のターミナルを開き、my-ai-agent-ui のルートで次のコマンドを実行します。npm start ログに表示された URL(通常は http://localhost:8080)をブラウザで開き、チャット入力欄にメッセージを送ってみてください。AI エージェントから返答が表示されれば成功です! チャット画面の チャットをクリア ボタンをクリックすると履歴がリセットされ、「チャットをクリアしました」と確認できます。新しい会話を問題なく始められるか確認しましょう! 6 | チャレンジ – テキストファイルアップロードボタンを追加しようテキストを追加でアップロードして、追加したドキュメントを踏まえた AI Agent とのやりとりを実現して見ましょう。なお、このチャレンジは前章のチャレンジを実施していないと実装できないので注意しましょう。チャット UI のヘッダーに .txt ファイル専用のアップロードボタン を追加し、選択したファイルを Python API へ送信し、サーバ側でファイルをチャンク化して保存するようにします。途中経過をわかりやすくするため、アップロード中は BusyIndicator で画面をブロックするようにしましょう。 ChatEntity.controller.js で次の 3 点を追加します(パスや名前空間は環境に合わせてください):冒頭の sap/ui/core/BusyIndicator インポートonFileChange ハンドラ – ファイル種別チェック & アップロード呼び出し_uploadFile メソッド – fetch + FormData で API /agent/upload へ POST/** ファイル選択時 */
async onFileChange(oEvent) {
const oFileUploader = oEvent.getSource();
const file = (oEvent.getParameter(“files”) || [])[0];
if (!file) return;
if (!file.name.endsWith(“.txt”)) {
MessageToast.show(“txt形式のファイルのみアップロード可能です。”);
oFileUploader.clear();
return;
}
await this._uploadFile(file);
oFileUploader.clear();
},
/** ファイルアップロード */
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(`”${filename}” をアップロードしました(${chunks_created} チャンク作成)`);
} catch (err) {
console.error(err);
MessageToast.show(`アップロードエラー: ${err.message}`);
} finally {
ui.setProperty(“/busy”, false);
BusyIndicator.hide();
}
} 次に、ヘッダーへ FileUploader と Clear ボタンを並べます。FileUploader は buttonOnly=”true” にして、アイコンボタンにするのがコツです。<f:actions>
<u:FileUploader id=”fileUploader”
name=”file”
placeholder=”ファイルを選択…”
fileType=”txt”
change=”onFileChange”
buttonText=””
buttonOnly=”true”
icon=”sap-icon://upload”
class=”fileUploaderButton” />
<Button id=”clearChatButton”
text=”チャットをクリア”
icon=”sap-icon://refresh”
type=”Emphasized”
press=”onClearChatPress” />
</f:actions> さらにヘッダー下部に “アップロード済ファイル一覧” を出すことで、どのファイルが何チャンクに分割されたかを視覚化できます。<HBox id=”fileUploadAreaBox”
visible=”{= ${ui>/uploadedFiles}.length > 0}”>
<Text text=”追加でアップロードしたファイル:” />
<HBox items=”{ui>/uploadedFiles}”>
<ObjectStatus text=”{ui>name}” icon=”sap-icon://document-text” />
</HBox>
</HBox> 上級チャレンジ – リセットボタンファイルをアップロードしていくとベクトル DB がどんどん膨らみます。テスト用に データベースを一括リセット するボタンを追加してみましょう。API 側: /agent/delete を実装(ヒント: Part4 における DB を初期化するコード)。Controller: onDeletePress を実装し、/agent/delete を呼び出して UI のファイル一覧も空にする。View: ヘッダーのアップロードボタンとクリアボタンの間に Delete ボタンを配置。実装ヒントUI Model の /uploadedFiles を空配列 [] にすると、ファイル一覧エリアが自動的に非表示になります。 7 | 次回予告Part 8 CloudFoundry にデプロイPart 8 では、完成したアプリケーションを Cloud Foundry へデプロイします。UI は mta build → cf deploy、Python バックエンドは manifest.yaml を使って cf push ― 2 本立てで “秒速デプロイ” に挑戦します。お楽しみに! 免責事項本ブログに記載された見解および意見はすべて私個人のものであり、私の個人的な立場で発信しています。SAP は本ブログの内容について一切の責任を負いません。 Read More Technology Blog Posts by SAP articles
#SAP
#SAPTechnologyblog