复杂AI工具的设计
本节内容可以参考文件"CaseStudy\LLMCaller.Table"的窗口"数据分析专家"。
打开这个对话框之后,你可以按顺序向AI提交下面的问题:
你会发现它不仅能听懂你的问题,还能自主完成各种数据统计和分析,就像一个经验丰富的数据分析师。
这个对话框之所以能解答上述问题,是因为我们在上一节的基础上,增加了一个用于分组统计的AI工具,这个工具是基于Foxtable的GroupTableBuilder类设计的。
GroupTableBuilder是一个非常强大的类,针对这个类设计工具,是有有一定难度的。
如果你能针对这个GroupTableBuilder设计一个能用且好用的工具,那么你在工具设计方面的能力就算合格了。
提示:
1、GroupTableBuilder是Foxtable的基础统计工具,CrosssTableBuilder是基于GroupTableBuilder的结果进行二次加工的,所以基本上针对GroupTableBuilder定义一个工具,就能随心所欲地让AI进行数据分析了。
2、但是GroupTableBuilder的开销比较大,简单的计算还是Compute方法比较轻便,所以保留了上一节设计的"compute_table"方法,让AI决定选择哪个工具。
3、数据统计分析的基础是表、列以及列的数据类型,所以我们还设计了两个辅助工具,分别用于获取所有的表以及指定的表的结构。
4、实际开发的时候,不要像示例文件这样,将前端页面、工具定义、工具执行的代码全部写在窗口的AfterLoad事件,建议分布在不同的自定义函数中。
设计步骤
1、修改窗口的AfterLoad事件代码,增加一个工具定义:
With
caller.AddTool("group_statistics",
"对指定数据表进行分组统计,使用前应该先获取表结构(同一个表只需获取一次,不用重复获取),按一个或多个列分组,对一个或多个数值列进行求和、平均、计数、最大、最小等汇总计算。返回统计结果数据(最多返回前1000行)。当用户需要按组汇总数据时使用此工具。对于日期列,可按年、按季度、按月、按周、按天分组(参考group_columns参数的说明)。如果有多个分组,可以考虑用交叉表呈现结果,看起来方便一些")
.AddParameter("table_name",
"string",
"要统计的数据表名称,必须是当前项目中已存在的表名",
True)
.AddParameter("group_columns",
"array",
"分组列列表,每个元素为列名;非日期列的格式为'原列名|新列名',新列名可选,例如['产品|Product','客户'];对于日期列,格式必须严格按照'列名|日期分组颗粒度|新列名',日期分组颗粒度可为'Year'、'Quarter'、'Month'、'Week'、'Day'、'None',默认按月(Month),例如:['日期|Year|年',
'日期|Month|月']",
True,
"string")
.AddParameter("aggregates",
"array",
"统计列列表,每项格式为'列名|聚合方式|新列名',新列名和聚合方式可选,聚合方式可为:Sum、Avg、Count、Max、Min,
默认为Sum。例如:['数量|Sum|总数量',
'分数|Avg','金额']",
True,
"string")
.AddParameter("filter",
"string",
"可选筛选条件,格式同compute_table的filter。",
False)
.AddParameter("same_period_growth",
"boolean",
"是否生成同比增长率(与上一年同期对比)。仅当分组中包含按年分组的日期列,例如:'日期|Year',以及一个按季度、月或周分组的日期列,例如:'日期|Quarter'、'日期|Month'、'日期|Week',才有意义",
False)
.AddParameter("circle_growth",
"boolean",
"是否生成环比增长率(与上一个周期对比)。通常与按月、按季度或按周分组配合使用,例如:'日期|Quarter'、'日期|Month'、'日期|Week'",
False)
.AddParameter("grand_proportion",
"boolean",
"是否生成总占比(占全部数据的百分比)。",
False)
.AddParameter("group_proportion",
"boolean",
"是否生成分组占比(占当前分组内数据的百分比)。需要至少两个分组列时才有意义。",
False)
End
With
2、修改窗口的AfterLoad事件代码,给执行工具的回调函数增加一个分支:
Case
"group_statistics"
'分组统计
Dim
tableName
As
String
= targs("table_name").ToString()
'获取表名
If
Not
DataTables.Contains(tableName)
Then
Return
"{""error"":""表
'"
& tableName &
"'
不存在""}"
End
If
Dim
groupCols
As
JArray = targs("group_columns")
'分组列
Dim
aggCols
As
JArray = targs("aggregates")
'统计列
Dim
dt
As
DataTable = DataTables(tableName)
Dim
gb
As
New
GroupTableBuilder("临时统计表",
dt)
For
Each
gcol
As
String
In
groupCols
'处理分组列
Dim
parts()
As
String
= gcol.Split("|"c)
Dim
colName
As
String
= parts(0)
'列名
If
dt.DataCols(colName).IsDate
Then
'如果日期列,根据工具定义:日期列格式严格按照'列名|日期分组颗粒度|新列名'
Dim
groupMode
As
DateGroupEnum = DateGroupEnum.Month
'默认按月分组
If
parts.Length > 1
Then
[Enum].TryParse(Of
DateGroupEnum)(parts(1),
True,
groupMode)
'出于健壮考虑,还是判断一下
Dim
aliasName
As
String
=
If(parts.Length
> 2, parts(2),
"")
'新列名,新列名为空会忽略
gb.Groups.AddDef(colName, groupMode, aliasName)
Else
'非日期列
If
parts.Length = 1
Then
gb.Groups.AddDef(colName)
ElseIf
parts.Length > 1
Then
gb.Groups.AddDef(colName, parts(1))
'指定了新列名
End
If
End
If
Next
For
Each
aggItem
As
String
In
aggCols
'处理统计列
Dim
parts()
As
String
= aggItem.Split("|"c)
Dim
colName
As
String
= parts(0)
'列名
Dim
aggMethod
As
String
=
If(parts.Length
> 1, parts(1).ToLower(),
"sum")
'统计类型
Dim
aliasName
As
String
=
If(parts.Length
> 2, parts(2),
"")
'新列名,新列名为空会忽略
Dim
aggType
As
AggregateEnum = AggregateEnum.Sum
If
aggMethod >
""
Then
[Enum].TryParse(Of
AggregateEnum)(aggMethod,
True,
aggType)
End
If
gb.Totals.AddDef(colName, aggType, aliasName)
Next
If
targs.ContainsKey("filter")
Then
gb.Filter = targs("filter").ToString()
If
targs.ContainsKey("same_period_growth")
Then
gb.SamePeriodGrowth = targs("same_period_growth").ToObject(Of
Boolean)()
If
targs.ContainsKey("circle_growth")
Then
gb.CircleGrowth = targs("circle_growth").ToObject(Of
Boolean)()
If
targs.ContainsKey("grand_proportion")
Then
gb.GrandProportion = targs("grand_proportion").ToObject(Of
Boolean)()
If
targs.ContainsKey("group_proportion")
Then
gb.GroupProportion = targs("group_proportion").ToObject(Of
Boolean)()
Dim
resultTable
As
DataTable = gb.Build(True)
Dim
resultJson
As
New
JObject
'
列名数组
Dim
colNames
As
New
JArray
For
Each
dc
As
DataCol
In
resultTable.DataCols
colNames.Add(dc.Caption)
'用的列标题代替列名称,因为统计表的列名可能不具备实际意义
Next
resultJson("columns")
= colNames
'行数据(最多给AI返回1000行)
Dim
rows
As
New
JArray
Dim
maxRows
As
Integer
= Math.Min(resultTable.dataRows.Count, 1000)
For
r
As
Integer
= 0
To
maxRows - 1
Dim
dr
As
DataRow = resultTable.DataRows(r)
Dim
rowArray
As
New
JArray
For
Each
dc
As
DataCol
In
resultTable.DataCols
If
dr.IsNull(dc)
Then
rowArray.Add(Nothing)
Else
rowArray.Add(dr(dc.Name))
End
If
Next
rows.Add(rowArray)
Next
resultJson("data")
= rows
Return
resultJson.ToString()