feat(ai): integrate chart planning support

This commit is contained in:
digua committed 2026-06-10 00:15:25 +08:00
1 parent 6f54dcc62b
commit 9ad012680d
19 files changed
+407 -25

No files matched your search

+2
View File
@@ -103,6 +103,8 @@ export function createCliRunAgentStream(
userMessage,
locale,
assistantAllowedTools,
enableAutoDetection: enableAutoSkill === true,
enableAnalyticalAutoDetection: enableAutoSkill === true,
})
const isChartCapability = chartRuntime.isChartCapability
const autoSkillAllowedTools =
+8
View File
@@ -19,6 +19,7 @@ import {
createCompressionLlmAdapter,
AgentEventHandler,
formatAIError,
getChartPlannerCapabilityForMessage,
initTokenizer,
type AgentStreamChunk,
type PiMessage,
@@ -175,6 +176,13 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
locale,
dataSnapshot,
availableTools: tools.map((tool) => tool.name),
availableCapabilities: [
getChartPlannerCapabilityForMessage({
userMessage,
locale,
availableTools: tools.map((tool) => tool.name),
}),
].filter((capability) => capability !== null),
skillSummary: skillDef?.name ?? (skillMenu ? 'auto_skill_menu' : undefined),
}
const routeStartedAt = Date.now()
+84
View File
@@ -316,6 +316,87 @@ describe('runChatTurn', () => {
])
})
it('persists render_chart results as chart content blocks for CLI replay', async () => {
const stdout = new MemoryWritable()
const aiChatManager = createAIChatManager()
const chart = {
version: 1,
spec: {
version: 1,
type: 'bar',
title: '季度消息量趋势',
encoding: { x: 'quarter', y: 'count' },
},
dataset: {
columns: [
{ name: 'quarter', type: 'category' },
{ name: 'count', type: 'integer' },
],
rows: [
{ quarter: '2025-Q4', count: 1607 },
{ quarter: '2026-Q1', count: 4284 },
],
},
data: {
labels: ['2025-Q4', '2026-Q1'],
values: [1607, 4284],
},
rowCount: 2,
} as const
const result = await runChatTurn(
{ sessionId: 'session-1', question: '分析季度消息量变化趋势', json: true, includeEvents: true },
{
dbManager: createDbManager(['session-1']),
pathProvider: {} as never,
aiChatManager,
stdout,
createRunAgentStream: () => async (_params, onEvent) => {
onEvent({
type: 'plan',
plan: {
type: 'plan',
version: 1,
status: 'created',
plan: {
version: 1,
title: '季度消息量趋势',
route: 'planned_execution',
intent: 'trend',
steps: [
{ goal: '生成趋势图', suggestedTools: ['get_schema', 'render_chart'], evidenceNeeded: '季度数据' },
],
successCriteria: ['展示趋势'],
},
},
})
onEvent({ type: 'tool_result', toolName: 'render_chart', toolResult: { details: { chart } } })
onEvent({ type: 'content', content: '2026-Q1 是峰值。' })
onEvent({
type: 'done',
isFinished: true,
usage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
})
},
}
)
assert.deepEqual(
result.contentBlocks?.map((block) => block.type),
['plan', 'chart', 'text']
)
assert.equal(result.contentBlocks?.[1]?.type, 'chart')
assert.deepEqual(result.contentBlocks?.[1]?.chart.dataset.rows, [])
assert.deepEqual(result.contentBlocks?.[1]?.chart.data, chart.data)
assert.deepEqual((aiChatManager as unknown as { __messages: unknown[] }).__messages.at(-1), {
aiChatId: 'ai_chat_1',
role: 'assistant',
content: '2026-Q1 是峰值。',
contentBlocks: result.contentBlocks,
tokenUsage: { promptTokens: 3, completionTokens: 5, totalTokens: 8 },
})
})
it('fails the turn and does not persist messages when the agent stream reports an error', async () => {
const stdout = new MemoryWritable()
const aiChatManager = createAIChatManager()
@@ -348,6 +429,7 @@ describe('runChatTurn', () => {
{ id: 'ai-chat-1', sessionId: 'session-1', assistantId: 'custom_assistant' },
])
let streamedAssistantId: string | undefined
let streamedEnableAutoSkill: boolean | undefined
await runChatTurn(
{ aiChatId: 'ai-chat-1', question: 'hello', json: true },
@@ -358,6 +440,7 @@ describe('runChatTurn', () => {
stdout,
createRunAgentStream: () => async (params, onEvent) => {
streamedAssistantId = params.assistantId
streamedEnableAutoSkill = params.enableAutoSkill
onEvent({ type: 'content', content: 'hi' })
onEvent({ type: 'done', isFinished: true })
},
@@ -365,6 +448,7 @@ describe('runChatTurn', () => {
)
assert.equal(streamedAssistantId, 'custom_assistant')
assert.equal(streamedEnableAutoSkill, true)
})
})
+45 -1
View File
@@ -8,7 +8,7 @@ import type {
PlanContentBlock,
TokenUsageData,
} from '@openchatlab/node-runtime'
import type { PathProvider } from '@openchatlab/core'
import type { ChartPayload, PathProvider } from '@openchatlab/core'
import { createCliRunAgentStream } from './agent-stream-runner'
export interface ChatCommandOptions {
@@ -76,6 +76,37 @@ function createAgentStreamError(error: unknown): Error {
return new Error('Agent stream failed')
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isChartPayload(value: unknown): value is ChartPayload {
return isRecord(value) && value.version === 1 && isRecord(value.spec) && isRecord(value.dataset)
}
function extractChartPayloads(toolResult: unknown): ChartPayload[] {
if (!isRecord(toolResult)) return []
const details = isRecord(toolResult.details) ? toolResult.details : toolResult
const charts: ChartPayload[] = []
if (isChartPayload(details.chart)) charts.push(details.chart)
if (Array.isArray(details.charts)) {
for (const chart of details.charts) {
if (isChartPayload(chart)) charts.push(chart)
}
}
return charts
}
function toPersistedChartPayload(chart: ChartPayload): ChartPayload {
return {
...chart,
dataset: {
...chart.dataset,
rows: [],
},
}
}
function resolveSingleSessionId(dbManager: DatabaseManager): string {
const sessionIds = dbManager.listSessionIds()
if (sessionIds.length === 1) return sessionIds[0]!
@@ -172,6 +203,14 @@ export async function runChatTurn(
}
}
const appendChartBlocks = (charts: ChartPayload[]) => {
if (charts.length === 0) return
// 中文注释:图表 block 需要持久化给 CLI 生成的 AI 对话回放;
// 原始 SQL 行数据可能较大,保存时只保留渲染数据和字段元信息。
contentBlocks.push(...charts.map((chart) => ({ type: 'chart' as const, chart: toPersistedChartPayload(chart) })))
hasReplayContentBlocks = true
}
await runAgentStream(
{
userMessage: options.question,
@@ -180,6 +219,7 @@ export async function runChatTurn(
assistantId: target.assistantId,
chatType: 'group',
locale: options.locale ?? 'zh-CN',
enableAutoSkill: true,
},
(chunk: AgentStreamChunk) => {
if (options.includeEvents) {
@@ -198,6 +238,10 @@ export async function runChatTurn(
appendThinkBlock(chunk.content ?? '', chunk.thinkTag, chunk.thinkDurationMs)
return
}
if (chunk.type === 'tool_result' && chunk.toolName === 'render_chart') {
appendChartBlocks(extractChartPayloads(chunk.toolResult))
return
}
if (chunk.type === 'content' && chunk.content) {
answer += chunk.content
appendTextBlock(chunk.content)