...
返回参数名 | 返回值说明 |
retCode | 接口调用是否成功;0 - 表示成功;负数为错误码 |
5.
...
页面集成
SmartBi NLA提供三种页面集成的方式 。
5.1
...
页面集成方式一
...
名称 |
【Smartbi V11 中的自然语言对话界面设计】
上图是Smartbi V11版本中的自然语言对话界面,主要是基于一个对话式聊天的方式实现的自然语言查询过程。期望用户在与“AI模型”在对话的过程中获取到用户期望的分析数据。并可以将分析结果插入到仪表盘中,实现分析结果的固化和分享。
本次教程,我们没有必要做这么复杂的设计,我们以一个简单的设计,来说明Smartbi NLA 二次开发的API是如何使用的。
【Demo开发样例-界面设计】
5.2 组件设计
在界面设计中,我们需要将界面按照功能区域,拆分成不同的组件。
【界面组件拆分】
- 组件设计
- 标题栏与输入框比较简单,不需要单独做展示组件
- 查询条件组件
- 结果区组件:
- 表格显示组件(交叉表、清单表)
- 图形显示组件(柱图、线图、饼图、散点图)
- 交互式问答组件(选择模型、选择实体)
5.3 NLA接口调用流程
graph TD
A[自然语言查询] -->|login| B(登录Smartbi)
B -->|loginIfSmartbiLogged| C(登录NLA)
C -->|query| D{执行查询}
D -->|无歧义| E[展示查询结果]
D -->|不确定的模型| F[选择模型]
D -->|不确定的实体| G[选择实体]
F --> |manualChangeTheme|D
G --> |rhetoricalQuestionResultInput|D
【NLA接入流程】
如果是在Smartbi扩展包内实现NLA调用,可以省略登录Smartbi的过程。
6. Demo代码编写
6.1 demo使用开发框架说明
本次demo,为了更好的说明第三方集成情况。采用完全模拟第三方平台的开发模式。demo应用使用的开发框架如下:
- Java框架 -- Springboot 2
- 前端框架 -- Vue2 、elementui、 jquery、Echarts
- SmartbiV11 -- 非常重要,本次开发对接的API是基于V11版本提供的
6.2 代码结构说明
【代码结构】
@startuml
前端HTML -> JS_API: 调用前端接口
JS_API --> APIController: 调用服务端接口
APIController --> SmartbiAPI: 调用Java远程接口
SmartbiAPI --> Smartbi_Server: 调用Smartbi接口
@enduml
【接口调用时序】
6.2.1 主要代码文件说明
...
文件
...
说明
...
后端Java代码
...
SmartbiAPI.java
...
Smartbi api 接口
...
APIController.java
...
演示程序,提供给前端的接口,实现内容与SmartbiAPI.java对应
...
前端JS
...
api.js
...
前端API接口,与APIController.java 对应
...
components/result-view.js
...
查询结果展示路由组件
...
components/query-tip.js
...
查询条件展示组件
...
components/select-entity.js
...
选择实体展示组件
...
components/result/chart-view.js
...
图形显示组件
...
components/result/table-view.js
...
表格显示组件
...
components/result/text-view.js
...
文字显示组件
...
components/result/select-view.js
...
模糊实体选择组件
...
前端HTML
...
index.html
...
页面文件
6.3 登录
【登录部分】
...
值 | |
请求地址 | |
参数说明 | question:问句,可以选填 userName:用户名 password:密码 |
请求URL:http://smartbi-nla-server:port/smartbi/vision/AIChatView2.html?userName=admin&password=admin
5.2 页面集成方式二
名称 | 值 |
请求地址 | |
参数说明 | userName:用户名 password: 密码 surl :/smartbi/smartbix/#/sdk |
5.3 页面集成方式三
嵌入式集成(iframe嵌入需注意可能会有跨域问题)
<script> window.aiChatbotConfig = { // 配置AIChat的版本号 version: 1, // 配置嵌入的域名 iframeSrc: "http://localhost:3000", // btn: { // 按钮图片可以自定义配置 // backgroundImg: 'http://xxx.png' // } }; </script> <script type="text/javascript" src="http://localhost:3000/smartbi/vision/external.js"></script> |
嵌入后页面展示:
6. Demo代码编写
6.1 demo使用开发框架说明
本次demo,为了更好的说明第三方集成情况。采用完全模拟第三方平台的开发模式。demo应用使用的开发框架如下:
- Java框架 -- Springboot 2
- 前端框架 -- Vue2 、elementui、 jquery、Echarts
- SmartbiV11 -- 非常重要,本次开发对接的API是基于V11版本提供的
6.2 代码结构说明
【代码结构】
@startuml
前端HTML -> JS_API: 调用前端接口
JS_API --> APIController: 调用服务端接口
APIController --> SmartbiAPI: 调用Java远程接口
SmartbiAPI --> Smartbi_Server: 调用Smartbi接口
@enduml
【接口调用时序】
6.2.1 主要代码文件说明
文件 | 说明 | |
后端Java代码 | SmartbiAPI.java | Smartbi api 接口 |
APIController.java | 演示程序,提供给前端的接口,实现内容与SmartbiAPI.java对应 | |
前端JS | api.js | 前端API接口,与APIController.java 对应 |
components/result-view.js | 查询结果展示路由组件 | |
components/query-tip.js | 查询条件展示组件 | |
components/select-entity.js | 选择实体展示组件 | |
components/result/chart-view.js | 图形显示组件 | |
components/result/table-view.js | 表格显示组件 | |
components/result/text-view.js | 文字显示组件 | |
components/result/select-view.js | 模糊实体选择组件 | |
前端HTML | index.html | 页面文件 |
6.3 登录
【登录部分】
<el-form :inline="true" :model="form1" class="demo-form-inline">
<el-form-item label="账号:">
<el-input v-model="form1.user" placeholder="账号"></el-input>
</el-form-item>
<el-form-item label="密码:">
<el-input v-model="form1.password" placeholder="密码" show-password></el-input>
</el-form-item>
<el-form-item label="Smartbi地址:">
<el-input v-model="form1.server" placeholder="Smartbi地址" style="width:350px"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login" :disabled="loginOk">登录</el-button>
</el-form-item>
</el-form>
...
var tooltipValueFormatter = function(value) {
let valueStr = String(value);
let isFloat = valueStr.indexOf(".") + 1 > 0;
if (isFloat && typeof(value) == "number") {
return value.toFixed(2);
} else if (isFloat && typeof(value) == "string") {
return Number(value).toFixed(2);
}
return value;
}
Vue.component('chart-view', {
name: 'ChartView',
template: '\
<div class="container">\
<div style="height: 270px; width: 100%; background-color: white;" ref="echartContainerId"></div>\
</div>\
',
props: ["resultContent"],
data() {
return {
myChart: null,
chartType: null,
option: null,
onceShowMaxItem: 7
}
},
watch: {
//queryResult
resultContent: {
handler: function (newValue, oldValue) {
let that = this;
setTimeout(function () {
that.showResult(that.resultContent.portletType, that.resultContent.html);
}, 100);
},
immediate: true
}
},
computed: {
isEmptyData() {
//option不存在不是空数据
if (!this.option) {
return false;
}
let series = this.option.series;
return !series || (series && series.length === 0) || (!series[0].data || (series[0].data && series[0].data.length === 0))
}
},
methods: {
showResult(chartType, echartsOption) {
this.chartType = chartType;
this.option = JSON.parse(echartsOption);
if (this.isEmptyData) {
this.$emit("showEmptyQueryResultView");
return;
}
//该方法可以不需要,主要是对后端返回的option进行加工
this.processOption();
this.$nextTick(() => {
this.myChart = echarts.init(this.$refs.echartContainerId);
this.myChart.setOption(this.option);
});
},
processOption() {
let grid = {
left: '2%',
right: '2%',
bottom: '2%',
top: '7%',
containLabel: true,
show: 'true',
borderWidth: '0'
};
if (this.chartType === "ECHARTS_BAR" || this.chartType === "ECHARTS_LINE" || this.chartType === "ECHARTS_COMBINATION__DUAL") {
let onceShowMaxItem = this.onceShowMaxItem; //一次最多显示多少根柱子,后面运算时用到, 3-7有效,太多没试过,小于3时不太准确
let dataNum = this.option.xAxis.data.length;
let processedEnd = 100;
if (dataNum > onceShowMaxItem) {
processedEnd = 100 * onceShowMaxItem / dataNum;
}
let dataZoom = [
{
// start, end是控制echarts图形的缩放比例,%,通过计算end, 控制最多显示多少根柱子(多少单位的X轴长度)
type: 'inside',
xAxisIndex: [0],
start: 0, end: processedEnd,
//end: 100 / this.option.xAxis.data.length * 6,
zoomOnMouseWheel: false,
zoomLock: true,
},
];
//设置图形可以左右滚动
this.option["dataZoom"] = dataZoom;
//给第一个柱子或线设置显示数值,所有柱子都设置会很挤
//this.option["series"][0]["label"] = label;
//解决Y轴刻度被遮挡
this.option["grid"] = grid;
}
if (this.chartType === "ECHARTS_BAR__HORIZONTAL") {
//解决Y轴刻度被遮挡
this.option["grid"] = grid;
}
if (this.chartType === "ECHARTS_PIE") {
let label = {
normal: {
position: "inside"//此处将展示的文字在内部展示
}
};
//解决饼图标签显示不全
this.option["series"][0]["label"] = label;
}
if (!!this.option.tooltip) {
this.option.tooltip["confine"] = true;
this.option.tooltip["valueFormatter"] = tooltipValueFormatter;
} else {
let tooltip = {
confine: true,
valueFormatter: tooltipValueFormatter
};
this.option["tooltip"] = tooltip;
}
}
},
destroyed() {
if (!!this.myChart) {
this.myChart.dispose();
}
this.myChart = null;
}
});
6.6 交互式问答
交互式问答的内容,主要是在query方法返回code=50的时候会出现。交互式问答主要有2种情况:
- 选择模糊的数据集(数据模型、业务主题)
- 选择模糊的维度、指标、成员
交互式问答,返回的模糊内容,主要在query方法返回的result.interActionItems属性中。详细可以参考“query接口”
6.6.1 选择模糊数据集
【query方法返回result.interActionItems】
<div v-else-if="interactionItem.rhetoricalQuestionType == 'sim_table'">
<div class="bot-answer-message" ref="chatRecord">{{ interactionItem.desc }}</div>
<el-radio-group v-model="userSelected[0]" style="margin-top: 10px">
<div v-for="(item, index) in alternativesItems" :key="index">
<el-radio :label="index">{{ item }}</el-radio>
</div>
</el-radio-group>
<div>
<el-button class="btn" @click="handleClickConfirmSimTable" :disabled="userSelected.length == 0">确定</el-button>
</div>
</div>
【选择模糊数据集界面代码】
【选择模糊数据集界面效果】
doChooseSimTable(userChoseThemeId) {
API
.manualChangeTheme(
this.result.result.question,
userChoseThemeId,
this.result.result.uuid,
(ret) => {
this.setResult(ret);
}
);
}
【选定数据集接口】
manualChangeTheme接口的详细定义,请参考对应API部分说明。
6.6.2 选择模糊维度、指标、成员
【query方法返回result.interActionItems】
<div v-if="interactionItem.rhetoricalQuestionType == 'fuzzy_entity'">
<select-entity
:desc="interactionItem.desc"
:fuzzyEntityName="interactionItem.fuzzyObjects[0]"
:alternativeList="interactionItem.alternatives[0]"
@userSelectFuzzyEntity="handleUserSelectFuzzyEntity"></select-entity>
</div>
<div>
<div style="color: #3370FF; font-size: 14px">【{{ fuzzyEntityName }}】</div>
<el-checkbox-group v-model="userSelected" style="margin-top: 10px">
<div v-for="(item, index) in alternativesItems" :key="index">
<el-checkbox :label="item">{{ item }}</el-checkbox>
</div>
</el-checkbox-group>
...
</div>
【选择模糊维度、指标、成员界面代码】
【选择模糊维度、指标、成员界面效果】
/**
* 交互式问答:选择维度、指标或者成员
* @param interactionItem
*/
doRhetoricalQuestionResultInput(interactionItem) {
var intentionParams = this.result.result.intentionParams? this.result.result.intentionParams: "";
API
.rhetoricalQuestionResultInput(
this.result.result.question,
this.result.result.themeId,
this.result.result.uuid,
JSON.stringify(interactionItem),
JSON.stringify(intentionParams),
(ret) => {
this.setResult(ret);
}
);
},
【选定维度、指标、成员接口】
rhetoricalQuestionResultInput接口的详细定义,请参考对应API部分说明。
{
"queryIntention": "nl2sql",
"rhetoricalQuestionType": "fuzzy_entity",
"desc": "发现不确定的实体,请从以下列表进行选择并确认。",
"fuzzyObjects": [
"传祺"
],
"alternatives": [
[
"成员:【9主流热销私家车/品牌/传祺】",
"成员:【9主流热销私家车/车型/传祺GA4】"
]
],
"alternativesRealValue": [
[
{
"agg": "",
"type": "dimension_where",
"operator": "==",
"path": "9主流热销私家车/品牌",
"synonym": "",
"sorted": "",
"show_content": "成员:【9主流热销私家车/品牌/传祺】=【传祺】",
"name": "品牌",
"member": [
"传祺"
],
"id": "AUGMENTED_DATASET_FIELD.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column5",
"keyword": "传祺",
"identify_type": "select_fuzzy_entity",
"value": "",
"time_level": ""
},
{
"agg": "",
"type": "dimension_where",
"operator": "==",
"path": "9主流热销私家车/车型",
"synonym": "",
"sorted": "",
"show_content": "成员:【9主流热销私家车/车型/传祺GA4】=【传祺】",
"name": "车型",
"member": [
"传祺GA4"
],
"id": "AUGMENTED_DATASET_FIELD.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column6",
"keyword": "传祺",
"identify_type": "",
"value": "",
"time_level": ""
}
]
],
//来源alternativesRealValue中选定的内容
"userSelected": [
[
{
"agg": "",
"type": "dimension_where",
"operator": "==",
"path": "9主流热销私家车/品牌",
"synonym": "",
"sorted": "",
"show_content": "成员:【9主流热销私家车/品牌/传祺】=【传祺】",
"name": "品牌",
"member": [
"传祺"
],
"id": "AUGMENTED_DATASET_FIELD.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column5",
"keyword": "传祺",
"identify_type": "select_fuzzy_entity",
"value": "",
"time_level": ""
}
]
],
"otherParams": "{\"entity\":[{\"agg\":\"\",\"id\":\"AUGMENTED_DATASET_LEVEL.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column9-column9_Year-LEVEL-1655372258460\",\"identify_type\":\"auto\",\"keyword\":\"2022年\",\"member\":[\"2022\"],\"name\":\"年\",\"operator\":\"==\",\"path\":\"日期_时间维度/年\",\"sorted\":\"\",\"synonym\":\"\",\"time_level\":\"year\",\"type\":\"dimension_where\",\"value\":\"\"},{\"agg\":\"\",\"id\":\"AUGMENTED_DATASET_FIELD.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column6\",\"identify_type\":\"auto\",\"keyword\":\"车型\",\"member\":[],\"name\":\"车型\",\"operator\":\"\",\"path\":\"9主流热销私家车/车型\",\"sorted\":\"\",\"synonym\":\"\",\"time_level\":\"\",\"type\":\"dimension\",\"value\":\"\"},{\"agg\":\"sum\",\"id\":\"AUGMENTED_DATASET_MEASURE.I8a8aa3ed017bc404c404760a017bc40bc0b90004.Field-demo2019-null-null-car_selling_fact-column10_1686644374005\",\"identify_type\":\"auto\",\"keyword\":\"销售量\",\"member\":[],\"name\":\"销售量\",\"operator\":\"\",\"path\":\"9主流热销私家车/销售量\",\"sorted\":\"\",\"synonym\":\"\",\"time_level\":\"\",\"type\":\"measure\",\"value\":\"\"}],\"id\":\"I8a8aa3ed017bc404c404760a017bc40bc0b90004\",\"ner_result\":\"[{\\\"label\\\":\\\"Member\\\",\\\"idx\\\":[2,3],\\\"entity\\\":\\\"传祺\\\"},{\\\"label\\\":\\\"Dimension\\\",\\\"idx\\\":[5,6],\\\"entity\\\":\\\"车型\\\"},{\\\"label\\\":\\\"Measure\\\",\\\"idx\\\":[8,10],\\\"entity\\\":\\\"销售量\\\"}]\",\"query_type\":\"nl2sql_query\",\"uncertain_entity\":{\"传祺\":[[\"传祺\",\"品牌\",\"m\"],[\"传祺GA4\",\"车型\",\"m\"]]},\"uncertain_entity_index\":{\"传祺\":[6,7]},\"unrecognized_word\":{},\"word_cut\":[\"2022年\",\"传祺\",\"各\",\"车型\",\"的\",\"销售量\"]}"
}
...
this.option["dataZoom"] = dataZoom;
//给第一个柱子或线设置显示数值,所有柱子都设置会很挤
//this.option["series"][0]["label"] = label;
//解决Y轴刻度被遮挡
this.option["grid"] = grid;
}
if (this.chartType === "ECHARTS_BAR__HORIZONTAL") {
//解决Y轴刻度被遮挡
this.option["grid"] = grid;
}
if (this.chartType === "ECHARTS_PIE") {
let label = {
normal: {
position: "inside"//此处将展示的文字在内部展示
}
};
//解决饼图标签显示不全
this.option["series"][0]["label"] = label;
}
if (!!this.option.tooltip) {
this.option.tooltip["confine"] = true;
this.option.tooltip["valueFormatter"] = tooltipValueFormatter;
} else {
let tooltip = {
confine: true,
valueFormatter: tooltipValueFormatter
};
this.option["tooltip"] = tooltip;
}
}
},
destroyed() {
if (!!this.myChart) {
this.myChart.dispose();
}
this.myChart = null;
}
});