feat(ai): prefer high-level tools over raw SQL when rendering charts

Add optional `rows` parameter to render_chart so AI can pass pre-fetched
data arrays (e.g. from get_time_stats, member_stats) directly instead of
rewriting SQL from scratch. Update chart capability prompts and all locale
files to guide AI to check high-level tools first before falling back to SQL.
Remove debug console.debug log from agent-core thinkingLevel resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
diguaandClaude Sonnet 4.6 committed 2026-06-10 00:15:25 +08:00
1 parent e71956ae3c
commit 56ad4a9578
7 files changed
+60 -35

No files matched your search

@@ -123,14 +123,6 @@ export async function runAgentCore(options: AgentCoreOptions): Promise<AgentCore
})()
const finalThinkingLevel = resolvedThinkingLevel ?? (piModel.reasoning ? undefined : 'off')
console.debug('[agent-core] thinkingLevel resolution:', {
input: options.thinkingLevel,
modelId: piModel.id,
modelReasoning: effectiveModel.reasoning,
isDefault,
resolved: resolvedThinkingLevel,
final: finalThinkingLevel,
})
const coreAgent = new PiAgentCore({
initialState: {
@@ -172,7 +172,7 @@ const ZH_PROMPT = `你是 ChatLab 绘图助手。本轮用户希望你在回答
6. 工具失败、SQL 修正、schema 探索过程不要写进最终回答;最终只保留原生图表和简短结论,禁止用 chart1.png、chart2.png 等文件名引用图表。
7. 调用 render_chart 时,ChatLab 会立即在该位置插入原生图表;需要图表就直接调用工具,不要写 SQL 注释、Markdown 图片或“已生成图表/见上图”这类文本占位。
8. 用户要求多张图时,图表应分散出现在相关总结或解释附近;不要把所有图集中生成后再在最后用文字引用。
9. 调查数据、筛选对象、确认统计口径时,优先使用可用的普通分析工具;render_chart 只用于最终要展示成图的聚合数据,不要把它用于元数据探测、标量查询、单行日期字符串或纯说明性结果。
9. 为图表准备数据时,优先调用高层工具(如 get_time_stats、member_stats)而非自己写 SQL;只有高层工具无法覆盖所需筛选或聚合时才用 sql。render_chart 不要用于元数据探测、标量查询、单行日期字符串或纯说明性结果。
10. render_chart 的工具结果会包含数据预览(Data preview);用它分析峰值、低谷和差异,不要为了读取数值重复生成等价图表。
11. 图表解释只能描述数据直接支持的趋势、峰值、低谷、差异和同步性,禁止夸张拟人化或无法从图中验证的关系判断。
@@ -180,8 +180,8 @@ const ZH_PROMPT = `你是 ChatLab 绘图助手。本轮用户希望你在回答
1. 调用 get_schema。
2. 根据 schema、用户要求、系统当前日期和启动上下文确定统计对象、时间范围、维度、指标和图表类型。
3. 如需调查事实、建立话题地图、筛选成员或确认统计口径,先用可用的普通分析工具完成;不要为了调查而生成临时图表。
4. 准备图表数据集 SQL:SQL 结果必须直接对应 ChartSpec 需要的维度、指标和 series;必要时在这条数据集 SQL 中完成筛选、聚合和补零。
5. 在希望图表出现的位置调用 render_chart,提交图表数据集 SQL 和 ChartSpec v1。
4. 准备图表数据——先问自己:哪个可用的高层工具能直接提供这张图所需的数据?get_time_stats 能给时间分布、member_stats 能给成员排名;能用就调用,将 data 数组作为 rows 传入 render_chart。只有在高层工具无法覆盖所需筛选或聚合时,才编写 SQL。
5. 在希望图表出现的位置调用 render_chart,提交数据(rows 或 sql)和 ChartSpec v1。
6. 图表生成后,用 1-3 句简洁文字解释结论。用户要求多张图时,可以多次调用 render_chart,并把每张图放在最相关的段落附近。
常用语、口头禅、高频短句类图表必须排除非真人文本:发送者为系统消息、媒体占位(如 [表情包]、[图片]、[语音]、[视频]、[文件]、[链接]、[红包]、[转账])以及撤回/删除提示都不能进入统计。
@@ -216,7 +216,7 @@ Hard rules:
6. Do not include failed SQL attempts, schema exploration, or retry reasoning in the final answer. Final answer should contain only native charts and a short conclusion; never reference charts with filenames like chart1.png or chart2.png.
7. When you call render_chart, ChatLab immediately inserts the native chart at that position. If a chart is needed, call the tool directly; do not write SQL-comment placeholders, Markdown images, or text such as "generated chart" or "see chart above".
8. If the user asks for multiple charts, place each chart near the relevant summary or explanation. Do not generate all charts together and then reference them later with text.
9. Use available ordinary analysis tools to investigate data, choose subjects, and confirm statistical definitions. Use render_chart only for aggregated data that should be displayed as a chart; do not use it for metadata inspection, scalar queries, single-row date strings, or purely explanatory results.
9. When preparing chart data, prefer calling a high-level tool (such as get_time_stats, member_stats) over writing SQL; only use sql when no high-level tool can satisfy the required filters or aggregations. Do not use render_chart for metadata inspection, scalar queries, single-row date strings, or purely explanatory results.
10. render_chart tool results include a Data preview; use it to identify peaks, lows, and differences. Do not generate equivalent charts again just to read values.
11. Explain only trends, peaks, lows, differences, and synchrony directly supported by chart data. Do not make exaggerated personality or relationship claims.
@@ -224,8 +224,8 @@ Workflow:
1. Call get_schema.
2. Derive the target members, time range, dimensions, metrics, and chart type from schema, the user request, system current date, and startup context.
3. If you need to investigate facts, build a topic map, select members, or confirm statistical definitions, use available ordinary analysis tools first; do not generate temporary charts for investigation.
4. Prepare the chart dataset SQL: its result must directly match the dimensions, metrics, and series required by ChartSpec; when needed, do filtering, aggregation, and zero-filling in this dataset SQL.
5. Call render_chart at the position where the chart should appear, with the chart dataset SQL and ChartSpec v1.
4. Prepare chart data — ask yourself first: which available high-level tool can directly provide the data this chart needs? get_time_stats covers time distributions; member_stats covers member rankings. If one fits, call it and pass its data array as rows to render_chart. Only write SQL when no high-level tool can cover the required filters or aggregations.
5. Call render_chart at the position where the chart should appear, with data (rows or sql) and ChartSpec v1.
6. After the chart is generated, explain the key finding in 1-3 concise sentences. If the user asks for multiple charts, call render_chart multiple times and place each chart near its most relevant section.
For catchphrase, common phrase, or frequent short-text charts, exclude non-human text from the SQL: system-message senders, media placeholders such as [Sticker], [Image], [Photo], [Voice], [Video], [File], [Link], and recall/deletion notices must not be counted.
@@ -63,7 +63,7 @@ export default {
},
},
get_time_stats: {
desc: 'Get time distribution statistics of chat activity. Suitable for questions like "when is the group most active" or "what time do people usually chat".',
desc: 'Get time distribution statistics of chat activity. Suitable for questions like "when is the group most active" or "what time do people usually chat". The returned `data` array can be passed directly as the `rows` parameter of render_chart for visualization.',
params: {
type: 'Statistics type: hourly (by hour), weekday (by day of week), daily (by date)',
},
@@ -257,9 +257,10 @@ Returned summaries are brief descriptions of each segment, helping quickly locat
params: {},
},
render_chart: {
desc: 'Generate a native ChatLab chart from read-only SQL and ChartSpec v1. Supports bar, line, pie, and heatmap. Do not output HTML, JavaScript, SVG, ECharts options, or rendering code.',
desc: 'Generate a native ChatLab chart from ChartSpec v1. Supports bar, line, pie, and heatmap. Provide either `rows` (pre-fetched data array from a tool result, e.g. the `data` field from get_time_stats) or `sql` (read-only SELECT) — prefer `rows` when data is already available; use `sql` only when high-level tools cannot satisfy the need. Do not output HTML, JavaScript, SVG, ECharts options, or rendering code.',
params: {
sql: 'Read-only SELECT or WITH SELECT SQL. It must return fields referenced by ChartSpec encoding.',
rows: 'Pre-fetched data array from a high-level tool result (e.g. the `data` field from get_time_stats). Prefer this over sql when data is already available. Mutually exclusive with sql.',
sql: 'Read-only SELECT or WITH SELECT SQL. Use only when high-level tools cannot satisfy the need. Must return fields referenced by ChartSpec encoding.',
params: 'Named SQL parameters. Use an empty object when no parameters are needed.',
chartSpec: 'ChartSpec v1 with version, type, title, and encoding.',
maxRows: 'Maximum chart query rows. Default 1000.',
@@ -64,7 +64,7 @@ export default {
},
},
get_time_stats: {
desc: 'グループチャットの時間分布統計を取得する。「いつが一番アクティブ?」「みんな何時にチャットしている?」などの質問に適している。',
desc: 'グループチャットの時間分布統計を取得する。「いつが一番アクティブ?」「みんな何時にチャットしている?」などの質問に適している。返される `data` 配列は render_chart の `rows` パラメータとして直接使用できる。',
params: {
type: '統計タイプ:hourly(時間別)、weekday(曜日別)、daily(日別)',
},
@@ -257,9 +257,10 @@ export default {
params: {},
},
render_chart: {
desc: '読み取り専用 SQL と ChartSpec v1 から ChatLab ネイティブチャートを生成します。bar、line、pie、heatmap をサポートします。HTML、JavaScript、SVG、ECharts option、描画コードは出力しないでください。',
desc: 'ChartSpec v1 から ChatLab ネイティブチャートを生成します。bar、line、pie、heatmap をサポートします。`rows`(高レベルツールのデータ配列、例:get_time_stats の `data` フィールド)または `sql`(読み取り専用 SELECT)のいずれかを指定します。データがすでに取得済みの場合は `rows` を優先し、高レベルツールで対応できない場合のみ `sql` を使用します。HTML、JavaScript、SVG、ECharts option、描画コードは出力しないでください。',
params: {
sql: '読み取り専用の SELECT または WITH SELECT SQL。ChartSpec encoding で参照するフィールドを返す必要があります。',
rows: '高レベルツールから取得したデータ配列(例:get_time_stats の `data` フィールド)。データが取得済みの場合は sql より優先して使用します。sql とは排他。',
sql: '読み取り専用の SELECT または WITH SELECT SQL。高レベルツールで対応できない場合のみ使用します。ChartSpec encoding で参照するフィールドを返す必要があります。',
params: 'SQL の名前付きパラメータ。不要な場合は空オブジェクトを渡します。',
chartSpec: 'version、type、title、encoding を含む ChartSpec v1。',
maxRows: 'チャートクエリの最大行数。デフォルトは 1000。',
@@ -57,7 +57,7 @@ export default {
},
},
get_time_stats: {
desc: '获取群聊的时间分布统计。适用于回答"什么时候最活跃"、"大家一般几点聊天"等问题。',
desc: '获取群聊的时间分布统计。适用于回答"什么时候最活跃"、"大家一般几点聊天"等问题。返回的 `data` 数组可直接作为 `render_chart` 的 `rows` 参数用于绘图。',
params: {
type: '统计类型:hourly(按小时)、weekday(按星期)、daily(按日期)',
},
@@ -248,9 +248,10 @@ export default {
params: {},
},
render_chart: {
desc: '根据只读 SQL 和 ChartSpec v1 生成 ChatLab 原生图表。支持 bar、line、pie、heatmap。禁止输出 HTML、JavaScript、SVG、ECharts option 或渲染代码。',
desc: '根据 ChartSpec v1 生成 ChatLab 原生图表。支持 bar、line、pie、heatmap。提供 `rows`(来自高层工具的数据数组,如 get_time_stats 的 `data` 字段)或 `sql`(只读 SELECT)二选一;有现成数据时优先用 `rows`,只有高层工具无法满足时才写 `sql`。禁止输出 HTML、JavaScript、SVG、ECharts option 或渲染代码。',
params: {
sql: '只读 SELECT 或 WITH SELECT SQL。必须返回 ChartSpec encoding 中引用的字段。',
rows: '来自高层工具返回的数据数组(如 get_time_stats 的 `data` 字段)。有现成数据时优先使用,与 sql 二选一。',
sql: '只读 SELECT 或 WITH SELECT SQL。仅在高层工具无法满足需求时使用,必须返回 ChartSpec encoding 中引用的字段。',
params: 'SQL 命名参数对象;不需要参数时传空对象。',
chartSpec: 'ChartSpec v1,包含 version、type、title、encoding。',
maxRows: '图表查询最大行数,默认 1000。',
@@ -57,7 +57,7 @@ export default {
},
},
get_time_stats: {
desc: '取得群聊的時間分佈統計。適用於回答「什麼時候最活躍」、「大家一般幾點聊天」等問題。',
desc: '取得群聊的時間分佈統計。適用於回答「什麼時候最活躍」、「大家一般幾點聊天」等問題。返回的 `data` 陣列可直接作為 `render_chart` 的 `rows` 參數用於繪圖。',
params: {
type: '統計類型:hourly(按小時)、weekday(按星期)、daily(按日期)',
},
@@ -248,9 +248,10 @@ export default {
params: {},
},
render_chart: {
desc: '根據唯讀 SQL 和 ChartSpec v1 生成 ChatLab 原生圖表。支援 bar、line、pie、heatmap。禁止輸出 HTML、JavaScript、SVG、ECharts option 或渲染程式碼。',
desc: '根據 ChartSpec v1 生成 ChatLab 原生圖表。支援 bar、line、pie、heatmap。提供 `rows`(來自高層工具的資料陣列,如 get_time_stats 的 `data` 欄位)或 `sql`(唯讀 SELECT)二擇一;有現成資料時優先用 `rows`,僅在高層工具無法滿足需求時才寫 `sql`。禁止輸出 HTML、JavaScript、SVG、ECharts option 或渲染程式碼。',
params: {
sql: '唯讀 SELECT 或 WITH SELECT SQL。必須返回 ChartSpec encoding 中引用的欄位。',
rows: '來自高層工具返回的資料陣列(如 get_time_stats 的 `data` 欄位)。有現成資料時優先使用,與 sql 二擇一。',
sql: '唯讀 SELECT 或 WITH SELECT SQL。僅在高層工具無法滿足需求時使用,必須返回 ChartSpec encoding 中引用的欄位。',
params: 'SQL 命名參數物件;不需要參數時傳空物件。',
chartSpec: 'ChartSpec v1,包含 version、type、title、encoding。',
maxRows: '圖表查詢最大行數,預設 1000。',
+38 -9
View File
@@ -15,13 +15,20 @@ const RE_SECONDS_TIMESTAMP_DIVIDED_AS_MILLISECONDS = /\b(?:\w+\.)?ts\s*\/\s*1000
const inputSchema: JsonSchema = {
type: 'object',
properties: {
rows: {
type: 'array',
description:
'Pre-fetched data rows to render directly, skipping SQL execution. Pass the data array from a high-level tool result (e.g. the `data` field from get_time_stats, or member activity rows). Prefer this over sql when data is already available. Mutually exclusive with sql.',
items: { type: 'object' },
},
sql: {
type: 'string',
description: 'Read-only SELECT or WITH SELECT SQL used to produce chart rows.',
description:
'Read-only SELECT or WITH SELECT SQL used to produce chart rows. Use only when pre-fetched data is unavailable or custom aggregation is required.',
},
params: {
type: 'object',
description: 'Named SQL parameters. Use an empty object when no parameters are needed.',
description: 'Named SQL parameters. Only used when sql is provided.',
additionalProperties: true,
default: {},
},
@@ -39,7 +46,7 @@ const inputSchema: JsonSchema = {
maximum: 5000,
},
},
required: ['sql', 'chartSpec'],
required: ['chartSpec'],
}
function normalizeSql(sql: unknown, maxRows: number): string {
@@ -69,6 +76,15 @@ function normalizeParams(raw: unknown): Record<string, unknown> {
return raw as Record<string, unknown>
}
function normalizeRows(raw: unknown, maxRows: number): { rows: Record<string, unknown>[]; truncated: boolean } {
if (!Array.isArray(raw)) throw new Error('rows must be an array')
const items = raw.filter(
(item): item is Record<string, unknown> => typeof item === 'object' && item !== null && !Array.isArray(item)
)
const truncated = items.length > maxRows
return { rows: truncated ? items.slice(0, maxRows) : items, truncated }
}
function normalizeMaxRows(raw: unknown): number {
const value = typeof raw === 'number' ? raw : DEFAULT_MAX_ROWS
if (!Number.isFinite(value)) return DEFAULT_MAX_ROWS
@@ -103,13 +119,26 @@ function summarizeChartForModel(
async function handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<ToolResult> {
if (!context.dataProvider) throw new Error('render_chart requires a data provider')
const hasSql = typeof params.sql === 'string' && params.sql.trim().length > 0
const hasRows = Array.isArray(params.rows)
if (!hasSql && !hasRows) throw new Error('render_chart requires either sql or rows')
const maxRows = normalizeMaxRows(params.maxRows)
const sql = normalizeSql(params.sql, maxRows)
const sqlParams = normalizeParams(params.params)
let rows: Record<string, unknown>[]
let truncated: boolean
if (hasRows) {
const normalized = normalizeRows(params.rows, maxRows)
rows = normalized.rows
truncated = normalized.truncated
} else {
const sql = normalizeSql(params.sql, maxRows)
const sqlParams = normalizeParams(params.params)
const fetchedRows = await context.dataProvider.executeParameterizedSql<Record<string, unknown>>(sql, sqlParams)
truncated = fetchedRows.length > maxRows
rows = truncated ? fetchedRows.slice(0, maxRows) : fetchedRows
}
const fetchedRows = await context.dataProvider.executeParameterizedSql<Record<string, unknown>>(sql, sqlParams)
const truncated = fetchedRows.length > maxRows
const rows = truncated ? fetchedRows.slice(0, maxRows) : fetchedRows
const chart = buildChartPayload(rows, params.chartSpec, { truncated })
return {
@@ -127,7 +156,7 @@ async function handler(params: Record<string, unknown>, context: ToolExecutionCo
export const renderChartTool: ToolDefinition = {
name: 'render_chart',
description:
'Generate a native ChatLab chart from read-only SQL plus ChartSpec v1. Use this for flexible bar, line, pie, and heatmap charts. Never output HTML, JavaScript, SVG, ECharts options, or rendering code.',
"Generate a native ChatLab chart from ChartSpec v1. Provide either `rows` (pre-fetched data array from a tool result such as get_time_stats or member_stats) or `sql` (read-only SELECT). Prefer `rows` when data is already available — pass the tool's data array directly. Use `sql` only for custom aggregations not covered by existing tools. Never output HTML, JavaScript, SVG, ECharts options, or rendering code.",
inputSchema,
handler,
category: 'analysis',