refactor(ai): centralize chart runtime policy

This commit is contained in:
digua committed 2026-06-05 11:12:42 +08:00
1 parent 15470444e5
commit 296573ed5c
28 files changed
+413 -369

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { getChartCapabilityAllowedBuiltinTools } from '@openchatlab/core'
import { getChartCapabilityAllowedBuiltinTools } from '@openchatlab/node-runtime'
import { getAvailableToolDefs } from './agent-stream-runner'
describe('CLI chart capability tool filtering', () => {
+17 -15
View File
@@ -5,19 +5,16 @@
import type { DatabaseManager, AIConversationManager, AgentStreamChunk } from '@openchatlab/node-runtime'
import {
CHART_CAPABILITY_CORE_TOOLS,
SkillManager,
buildSkillMenuWithBuiltinChart,
createActivateSkillTool,
getSkillConfigWithBuiltinChart,
} from '@openchatlab/node-runtime'
import {
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_SKILL_ID,
getChartCapabilityAllowedBuiltinTools,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilitySkill,
getChatOverview,
shouldUseChartCapabilityForMessage,
} from '@openchatlab/core'
getSkillConfigWithBuiltinChart,
resolveChartRuntimeForRequest,
} from '@openchatlab/node-runtime'
import { getChatOverview } from '@openchatlab/core'
import type { DataSnapshot } from '@openchatlab/node-runtime'
import type { AgentStreamRequest } from '@openchatlab/http-routes'
import { AGENT_TOOL_REGISTRY } from '@openchatlab/tools'
@@ -90,14 +87,19 @@ export function createCliRunAgentStream(
const maxToolResultTokens = Math.floor(contextWindow * (maxToolResultPercent / 100))
const db = (dbManager as any).open?.(sessionId)
const isChartCapability =
skillId === CHART_CAPABILITY_SKILL_ID || (!skillId && shouldUseChartCapabilityForMessage(userMessage))
const chartRuntime = resolveChartRuntimeForRequest({
skillId,
userMessage,
locale,
assistantAllowedTools,
})
const isChartCapability = chartRuntime.isChartCapability
const autoSkillAllowedTools =
!skillId && enableAutoSkill && assistantAllowedTools && assistantAllowedTools.length > 0
? getChartCapabilityAllowedBuiltinTools(assistantAllowedTools)
!skillId && enableAutoSkill
? getAllowedBuiltinToolsForChartAutoSkill(assistantAllowedTools)
: assistantAllowedTools
const allowedToolSet = isChartCapability
? new Set(getChartCapabilityAllowedBuiltinTools(assistantAllowedTools))
? new Set(chartRuntime.allowedBuiltinTools)
: autoSkillAllowedTools && autoSkillAllowedTools.length > 0
? new Set(autoSkillAllowedTools)
: null
@@ -114,7 +116,7 @@ export function createCliRunAgentStream(
let resolvedSkillDef: { name: string; prompt: string } | undefined
let resolvedSkillMenu: string | undefined
if (isChartCapability) {
const def = getChartCapabilitySkill(locale ?? 'zh-CN')
const def = chartRuntime.skillDef ?? getChartCapabilitySkill(locale ?? 'zh-CN')
resolvedSkillDef = { name: def.name, prompt: def.prompt }
} else if (skillId) {
const def = skillMgr.getSkillConfig(skillId)
+15 -14
View File
@@ -13,21 +13,17 @@ import {
checkAndCompress,
createCompressionLlmAdapter,
formatAIError,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilitySkill,
initTokenizer,
resolveChartRuntimeForRequest,
} from '@openchatlab/node-runtime'
import type { CompressionConfig, CompressionLlmAdapter, AgentRuntimeStatus } from '@openchatlab/node-runtime'
import { Agent, type AgentStreamChunk, type SkillContext } from './agent'
import type { ToolContext } from './tools/types'
import { getAllowedToolsForChartCapability } from './tools'
import { getDefaultAssistantConfig, buildPiModel, findModelDefinition } from './llm'
import type { AIServiceConfig } from './llm/types'
import { getDefaultGeneralAssistantId } from './assistant/defaultGeneral'
import {
CHART_CAPABILITY_SKILL_ID,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
shouldUseChartCapabilityForMessage,
} from '@openchatlab/core'
import * as assistantManager from './assistant'
import type { AssistantConfig } from './assistant/types'
import * as skillManager from './skills'
@@ -160,10 +156,14 @@ export function createElectronRunAgentStream(): (
}
let skillCtx: SkillContext | undefined
const isChartCapability =
skillId === CHART_CAPABILITY_SKILL_ID || (!skillId && shouldUseChartCapabilityForMessage(userMessage))
if (isChartCapability) {
const chartSkill = getChartCapabilitySkill(locale ?? 'zh-CN')
const chartRuntime = resolveChartRuntimeForRequest({
skillId,
userMessage,
locale,
assistantAllowedTools: assistantConfig?.allowedBuiltinTools,
})
if (chartRuntime.isChartCapability) {
const chartSkill = chartRuntime.skillDef ?? getChartCapabilitySkill(locale ?? 'zh-CN')
skillCtx = { skillDef: { ...chartSkill, chatScope: 'all' } as SkillContext['skillDef'] }
assistantConfig = {
...(assistantConfig ?? {
@@ -172,7 +172,7 @@ export function createElectronRunAgentStream(): (
systemPrompt: '',
presetQuestions: [],
}),
allowedBuiltinTools: getAllowedToolsForChartCapability(assistantConfig?.allowedBuiltinTools),
allowedBuiltinTools: chartRuntime.allowedBuiltinTools,
}
} else if (skillId) {
const skillDef = skillManager.getSkillConfig(skillId) ?? undefined
@@ -181,6 +181,7 @@ export function createElectronRunAgentStream(): (
}
} else if (enableAutoSkill) {
const effectiveChatType = chatType ?? 'group'
const autoSkillAllowedTools = getAllowedBuiltinToolsForChartAutoSkill(assistantConfig?.allowedBuiltinTools)
assistantConfig = {
...(assistantConfig ?? {
id: resolvedAssistantId,
@@ -188,9 +189,9 @@ export function createElectronRunAgentStream(): (
systemPrompt: '',
presetQuestions: [],
}),
allowedBuiltinTools: getChartCapabilityAllowedBuiltinTools(assistantConfig?.allowedBuiltinTools),
allowedBuiltinTools: autoSkillAllowedTools,
}
const allowedTools = assistantConfig.allowedBuiltinTools
const allowedTools = autoSkillAllowedTools
const menu = buildSkillMenuWithBuiltinChart(skillManager.getSkillMenu(effectiveChatType, allowedTools), locale)
if (menu) {
skillCtx = { skillMenu: menu }
-50
View File
@@ -1,50 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { CHART_CAPABILITY_SKILL_ID } from '@openchatlab/core'
import { CHART_SCHEMA_REQUIRED_MESSAGE } from '@openchatlab/node-runtime'
import { createActivateSkillTool, getAllowedToolsForChartCapability, getAllTools } from './index'
describe('desktop chart capability tool filtering', () => {
it('does not expose raw SQL tools in chart-only turns', () => {
const toolNames = getAllowedToolsForChartCapability(['execute_sql'])
assert.deepEqual(toolNames, ['render_chart'])
assert.ok(!toolNames.includes('execute_sql'))
})
it('keeps explicitly allowed non-SQL analysis tools', () => {
const toolNames = getAllowedToolsForChartCapability(['keyword_frequency', 'execute_sql'])
assert.deepEqual(toolNames.sort(), ['keyword_frequency', 'render_chart'])
assert.ok(!toolNames.includes('execute_sql'))
})
it('activates the built-in chart skill without an imported skill file', async () => {
const tool = createActivateSkillTool('group', ['render_chart'], 'zh-CN')
const result = await tool.execute('call_1', { skill_id: CHART_CAPABILITY_SKILL_ID })
const block = result.content[0]
assert.equal(result.details.skillId, CHART_CAPABILITY_SKILL_ID)
assert.equal(result.details.applicable, true)
assert.equal(block.type, 'text')
if (block.type !== 'text') throw new Error('expected text content')
assert.match(block.text, /render_chart/)
})
it('requires get_schema before desktop render_chart execution', async () => {
const tools = getAllTools(
{
sessionId: 'session-1',
locale: 'en-US',
},
['render_chart']
)
const renderChart = tools.find((tool) => tool.name === 'render_chart')
assert.ok(renderChart)
const result = await renderChart.execute('call-1', {})
assert.deepEqual(result.content, [{ type: 'text', text: CHART_SCHEMA_REQUIRED_MESSAGE }])
assert.equal(result.details, null)
})
})
+6 -17
View File
@@ -6,9 +6,9 @@
*/
import type { AgentTool } from '@openchatlab/node-runtime'
import { CHART_CAPABILITY_ANALYSIS_TOOLS, getChartCapabilityAllowedBuiltinTools } from '@openchatlab/core'
import type { ToolContext, TruncationStrategy } from './types'
import { TOOL_REGISTRY } from './definitions'
import { isAnalysisToolAllowed } from './tool-filter'
import { t as i18nT } from '../../i18n'
import { applyPreprocessingPipeline, type PreprocessableMessage } from '@openchatlab/node-runtime'
import { aiLogger } from '../logger'
@@ -21,8 +21,6 @@ import {
} from '@openchatlab/node-runtime'
const CORE_TOOL_NAMES = new Set(TOOL_REGISTRY.filter((e) => e.category === 'core').map((e) => e.name))
const RAW_SQL_TOOL_NAME_SET = new Set(['execute_sql'])
const CHART_CAPABILITY_ANALYSIS_TOOL_SET = new Set<string>(CHART_CAPABILITY_ANALYSIS_TOOLS)
const preprocessLogger = {
info: (category: string, message: string, extra?: Record<string, unknown>) => aiLogger.info(category, message, extra),
@@ -109,20 +107,17 @@ function wrapWithPreprocessing(tool: AgentTool<any>, context: ToolContext): Agen
* 获取所有可用的 AgentTool
*
* - Core 工具始终加载,不受 allowedTools 白名单影响
* - Analysis 工具仅在 allowedTools 中显式列出时才加载(opt-in)
* - Analysis 工具仅在 allowedTools 中显式列出时加载
*
* @param context 工具上下文
* @param allowedTools analysis 工具白名单(仅控制 analysis 工具)
* @param allowedTools analysis 工具白名单;undefined/[] 表示不加载 analysis 工具
*/
export function getAllTools(context: ToolContext, allowedTools?: string[]): AgentTool<any>[] {
const coreTools = TOOL_REGISTRY.filter((e) => e.category === 'core').map((e) => e.factory(context))
let analysisTools: AgentTool<any>[] = []
if (allowedTools && allowedTools.length > 0) {
analysisTools = TOOL_REGISTRY.filter((e) => e.category === 'analysis' && allowedTools.includes(e.name)).map((e) =>
e.factory(context)
)
}
const analysisTools = TOOL_REGISTRY.filter(
(e) => e.category === 'analysis' && isAnalysisToolAllowed(e.name, allowedTools)
).map((e) => e.factory(context))
const chartSchemaGateState = createChartSchemaGateState()
@@ -132,12 +127,6 @@ export function getAllTools(context: ToolContext, allowedTools?: string[]): Agen
.map((t) => wrapWithPreprocessing(t, context))
}
export function getAllowedToolsForChartCapability(allowedTools?: readonly string[] | null): string[] {
return getChartCapabilityAllowedBuiltinTools(allowedTools).filter(
(toolName) => CHART_CAPABILITY_ANALYSIS_TOOL_SET.has(toolName) || !RAW_SQL_TOOL_NAME_SET.has(toolName)
)
}
/**
* 创建 activate_skill 元工具(AI 自选模式专用)
* 委托给共享包的 createActivateSkillTool,注入 Electron 端的 getSkillConfig
@@ -0,0 +1,16 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { isAnalysisToolAllowed } from './tool-filter'
describe('Electron analysis tool filtering', () => {
it('keeps analysis tools opt-in when no allowlist is configured', () => {
assert.equal(isAnalysisToolAllowed('keyword_frequency', undefined), false)
assert.equal(isAnalysisToolAllowed('keyword_frequency', []), false)
})
it('allows only explicitly listed analysis tools', () => {
assert.equal(isAnalysisToolAllowed('keyword_frequency', ['keyword_frequency']), true)
assert.equal(isAnalysisToolAllowed('execute_sql', ['keyword_frequency']), false)
})
})
@@ -0,0 +1,3 @@
export function isAnalysisToolAllowed(toolName: string, allowedTools?: readonly string[] | null): boolean {
return !!allowedTools?.includes(toolName)
}
+1
View File
@@ -41,6 +41,7 @@
"type-check": "vue-tsc --noEmit -p tsconfig.web.json",
"test:auth": "tsx --test apps/cli/src/http/auth.test.ts src/services/utils/http.test.ts",
"test:agent-context": "node --experimental-strip-types --test apps/desktop/main/ai/context/sessionLog.test.mjs",
"test:chart-runtime": "tsx --test packages/core/src/chart/index.test.ts packages/tools/src/definitions/render-chart.test.ts packages/node-runtime/src/ai/__tests__/chart-runtime.test.ts packages/node-runtime/src/ai/__tests__/builtin-chart-skill.test.ts packages/node-runtime/src/ai/__tests__/chart-schema-gate.test.ts apps/cli/src/ai/agent-stream-runner.test.ts apps/cli/src/ai/tool-adapter.test.ts apps/desktop/main/ai/tools/tool-filter.test.ts src/stores/aiChatChartBlocks.test.ts src/stores/skill.test.ts && node --import tsx --test tests/chart-runtime/agent-chart-flow.test.mts",
"test:e2e:launcher": "node --test tests/e2e/helpers/app-launcher.test.js",
"test:e2e:smoke": "node tests/e2e/run-smoke.js"
},
@@ -1,78 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
CHART_CAPABILITY_ANALYSIS_TOOLS,
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_SKILL_ID,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
shouldUseChartCapabilityForMessage,
} from './chart-capability'
describe('chart capability skill', () => {
it('advertises render_chart and schema lookup tools', () => {
const skill = getChartCapabilitySkill('en-US')
assert.equal(skill.id, CHART_CAPABILITY_SKILL_ID)
assert.deepEqual(skill.tools, ['render_chart', 'get_schema'])
assert.deepEqual(CHART_CAPABILITY_CORE_TOOLS, ['get_schema'])
assert.match(skill.prompt, /render_chart/)
assert.match(skill.prompt, /get_schema/)
})
it('instructs common phrase charts to exclude non-human placeholders', () => {
const zhSkill = getChartCapabilitySkill('zh-CN')
const enSkill = getChartCapabilitySkill('en-US')
assert.match(zhSkill.prompt, /常用语、口头禅、高频短句/)
assert.match(zhSkill.prompt, /\[表情包\]/)
assert.match(zhSkill.prompt, /撤回\/删除提示/)
assert.match(enSkill.prompt, /catchphrase, common phrase, or frequent short-text charts/)
assert.match(enSkill.prompt, /\[Sticker\]/)
assert.match(enSkill.prompt, /recall\/deletion notices/)
})
it('tightens chart SQL and final-answer behavior', () => {
const zhSkill = getChartCapabilitySkill('zh-CN')
const enSkill = getChartCapabilitySkill('en-US')
assert.match(zhSkill.prompt, /必须先调用 get_schema/)
assert.match(zhSkill.prompt, /禁止猜测表名、字段名或时间字段/)
assert.match(zhSkill.prompt, /MAX\(ts\)/)
assert.match(zhSkill.prompt, /禁止写 ts\/1000/)
assert.match(zhSkill.prompt, /补齐日期 × series 的 0 值/)
assert.match(zhSkill.prompt, /工具失败、SQL 修正、schema 探索过程不要写进最终回答/)
assert.match(zhSkill.prompt, /禁止夸张拟人化/)
assert.match(enSkill.prompt, /Always call get_schema/)
assert.match(enSkill.prompt, /Do not guess table names/)
assert.match(enSkill.prompt, /never write ts\/1000/)
assert.match(enSkill.prompt, /fill the date x series grid with zero values/)
assert.match(enSkill.prompt, /Do not include failed SQL attempts/)
assert.match(enSkill.prompt, /Do not make exaggerated personality or relationship claims/)
})
it('adds only chart analysis tools to assistant builtin tools', () => {
assert.deepEqual(CHART_CAPABILITY_ANALYSIS_TOOLS, ['render_chart'])
assert.deepEqual(getChartCapabilityAllowedBuiltinTools(), ['render_chart'])
assert.deepEqual(getChartCapabilityAllowedBuiltinTools(['keyword_frequency']), [
'keyword_frequency',
'render_chart',
])
})
it('does not duplicate render_chart when it is already allowed', () => {
assert.deepEqual(getChartCapabilityAllowedBuiltinTools(['render_chart', 'time_stats']), [
'render_chart',
'time_stats',
])
})
it('detects explicit chart requests', () => {
assert.equal(shouldUseChartCapabilityForMessage('画一个前五常用语占比饼图'), true)
assert.equal(shouldUseChartCapabilityForMessage('帮我做一下热力图'), true)
assert.equal(shouldUseChartCapabilityForMessage('visualize member trends as a line chart'), true)
assert.equal(shouldUseChartCapabilityForMessage('分析一下谁说话最多'), false)
})
})
-114
View File
@@ -1,115 +1 @@
export const CHART_CAPABILITY_SKILL_ID = 'chart_runtime'
export const CHART_CAPABILITY_CORE_TOOLS = ['get_schema'] as const
export const CHART_CAPABILITY_ANALYSIS_TOOLS = ['render_chart'] as const
export function getChartCapabilityAllowedBuiltinTools(allowedTools?: readonly string[] | null): string[] {
return Array.from(new Set([...(allowedTools ?? []), ...CHART_CAPABILITY_ANALYSIS_TOOLS]))
}
export function shouldUseChartCapabilityForMessage(message: string): boolean {
const normalized = message.trim().toLowerCase()
if (!normalized) return false
return /(?:画|绘制|生成|做|出|展示).{0,8}(?:图|图表)|(?:图表|可视化|饼图|柱状图|条形图|折线图|热力图|趋势图|占比图|分布图|chart|charts|plot|visuali[sz]e|visuali[sz]ation|pie chart|bar chart|line chart|heatmap)/i.test(
normalized
)
}
export function getChartCapabilitySkill(locale: string = 'zh-CN'): {
id: string
name: string
description: string
tags: string[]
chatScope: 'all'
tools: string[]
prompt: string
} {
const isZh = locale.startsWith('zh')
return {
id: CHART_CAPABILITY_SKILL_ID,
name: isZh ? '绘图助手' : 'Chart Assistant',
description: isZh ? '按本轮问题生成灵活的聊天数据图表' : 'Generate flexible charts for this chat question',
tags: [isZh ? '图表' : 'chart'],
chatScope: 'all',
tools: ['render_chart', 'get_schema'],
prompt: isZh ? ZH_PROMPT : EN_PROMPT,
}
}
const ZH_PROMPT = `你是 ChatLab 绘图助手。本轮用户希望你在回答里自然嵌入一张或多张图表。
你必须通过 render_chart 工具生成图表。不要输出 HTML、JavaScript、SVG、Canvas、ECharts option 或任何渲染代码。
硬性规则:
1. 第一次写 SQL 前必须先调用 get_schema。禁止猜测表名、字段名或时间字段。
2. “最近”“到现在”“到最近”必须用数据中的 MAX(ts) 或等价字段确定结束日期;不要手算或臆测日期。
3. ChatLab 的 message.ts 是秒级 Unix 时间戳,使用 date(ts, 'unixepoch', 'localtime');禁止写 ts/1000。
4. “最近的 N 人/成员”必须先用数据定义并筛选出 N 个成员,再只统计这些成员。
5. 折线图有多条线时,每条线必须有明确 series 字段含义;按日期统计时应补齐日期 × series 的 0 值,避免缺失日期导致折线断裂或误连。
6. 工具失败、SQL 修正、schema 探索过程不要写进最终回答;最终只保留图表和简短结论。
7. 图表解释只能描述数据直接支持的趋势、峰值、低谷、差异和同步性,禁止夸张拟人化或无法从图中验证的关系判断。
工作流程:
1. 调用 get_schema。
2. 根据 schema 和用户要求确定统计对象、时间范围、维度、指标和图表类型。
3. 必要时用只读 SQL 在 render_chart 中完成成员筛选、时间范围确认、聚合和补零。
4. 调用 render_chart,提交只读 SQL 和 ChartSpec v1。
5. 图表生成后,用 1-3 句简洁文字解释结论。用户要求多张图时,可以多次调用 render_chart。
常用语、口头禅、高频短句类图表必须排除非真人文本:发送者为系统消息、媒体占位(如 [表情包]、[图片]、[语音]、[视频]、[文件]、[链接]、[红包]、[转账])以及撤回/删除提示都不能进入统计。
ChartSpec v1 支持:
- bar: encoding.x + encoding.y
- line: encoding.x + encoding.y,可选 encoding.series 表示每条线的含义
- pie: encoding.label + encoding.value
- heatmap: encoding.x + encoding.y + encoding.value
ChartSpec 示例:
{
"version": 1,
"type": "line",
"title": "最近 30 天成员发言趋势",
"encoding": { "x": "day", "y": "msg_count", "series": "member_name" },
"unit": "条"
}
用户明确要求图表时必须尝试生成。用户没有明确要求时,只有排名、趋势、分布、占比或二维密度明显更清楚时才主动生成,且默认最多一张。`
const EN_PROMPT = `You are the ChatLab Chart Assistant. In this turn, the user wants one or more charts embedded naturally in the answer.
You must generate charts through the render_chart tool. Do not output HTML, JavaScript, SVG, Canvas, ECharts options, or rendering code.
Hard rules:
1. Always call get_schema before writing the first SQL query. Do not guess table names, field names, or timestamp fields.
2. For "latest", "until now", or "through the latest date", derive the end date from MAX(ts) or the equivalent timestamp field in the data. Do not hand-calculate or assume dates.
3. ChatLab message.ts is a Unix timestamp in seconds. Use date(ts, 'unixepoch', 'localtime'); never write ts/1000.
4. For "latest N members" or "recent N people", first define and select those N members from the data, then count only those members.
5. For multi-series line charts, every line must have an explicit series field. For daily counts, fill the date x series grid with zero values so missing days do not break or mislead the line.
6. Do not include failed SQL attempts, schema exploration, or retry reasoning in the final answer. Final answer should contain only the chart and a short conclusion.
7. Explain only trends, peaks, lows, differences, and synchrony directly supported by chart data. Do not make exaggerated personality or relationship claims.
Workflow:
1. Call get_schema.
2. Derive the target members, time range, dimensions, metrics, and chart type from schema and the user request.
3. When needed, use read-only SQL inside render_chart to select members, determine time range, aggregate data, and fill zero rows.
4. Call render_chart with read-only SQL and ChartSpec v1.
5. 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.
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.
ChartSpec v1 supports:
- bar: encoding.x + encoding.y
- line: encoding.x + encoding.y, optional encoding.series for the meaning of each line
- pie: encoding.label + encoding.value
- heatmap: encoding.x + encoding.y + encoding.value
ChartSpec example:
{
"version": 1,
"type": "line",
"title": "Member message trend in the last 30 days",
"encoding": { "x": "day", "y": "msg_count", "series": "member_name" },
"unit": "messages"
}
When the user explicitly asks for charts, you must try to generate them. If the user does not explicitly ask, add at most one chart only when ranking, trend, distribution, ratio, or two-dimensional density is clearly easier to understand visually.`
+1 -8
View File
@@ -5,14 +5,7 @@
// 内置工具目录
export type { ToolCategory, BuiltinToolCatalogEntry } from './tool-catalog'
export { BUILTIN_TOOL_CATALOG } from './tool-catalog'
export {
CHART_CAPABILITY_SKILL_ID,
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_ANALYSIS_TOOLS,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
shouldUseChartCapabilityForMessage,
} from './chart-capability'
export { CHART_CAPABILITY_SKILL_ID } from './chart-capability'
// LLM 模型系统类型
export type {
-7
View File
@@ -80,8 +80,6 @@ export {
getClusterGraph,
getRelationshipStats,
getLanguagePreferenceAnalysis,
isHumanCatchphraseContent,
NON_HUMAN_CATCHPHRASE_PLACEHOLDERS,
FULL_MSG_COLUMNS,
FULL_MSG_FROM,
FULL_MSG_SELECT,
@@ -249,11 +247,6 @@ export {
getBuiltinModelsByProvider,
getBuiltinModelById,
CHART_CAPABILITY_SKILL_ID,
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_ANALYSIS_TOOLS,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
shouldUseChartCapabilityForMessage,
THINK_TAGS,
extractThinkingContent,
stripToolCallTags,
@@ -0,0 +1,66 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
CHART_CAPABILITY_SKILL_ID,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
resolveChartRuntimeForRequest,
} from '../chart-runtime'
describe('chart runtime policy', () => {
it('keeps chart prompt and skill metadata in node runtime', () => {
const skill = getChartCapabilitySkill('en-US')
assert.equal(skill.id, CHART_CAPABILITY_SKILL_ID)
assert.equal(skill.name, 'Chart Assistant')
assert.deepEqual(skill.tools, ['render_chart', 'get_schema'])
assert.match(skill.prompt, /render_chart/)
})
it('enables chart runtime only for explicit chart skill by default', () => {
assert.equal(
resolveChartRuntimeForRequest({
skillId: CHART_CAPABILITY_SKILL_ID,
userMessage: 'draw a chart',
locale: 'en-US',
}).isChartCapability,
true
)
assert.equal(
resolveChartRuntimeForRequest({
skillId: null,
userMessage: '画一个趋势图',
locale: 'zh-CN',
}).isChartCapability,
false
)
})
it('keeps chart tool allowlists free of raw SQL', () => {
assert.deepEqual(getChartCapabilityAllowedBuiltinTools(), ['render_chart'])
assert.deepEqual(getChartCapabilityAllowedBuiltinTools(['keyword_frequency', 'execute_sql']), [
'keyword_frequency',
'render_chart',
])
})
it('does not narrow unrestricted auto-skill assistant tools', () => {
assert.equal(getAllowedBuiltinToolsForChartAutoSkill(undefined), undefined)
assert.deepEqual(getAllowedBuiltinToolsForChartAutoSkill([]), [])
assert.deepEqual(getAllowedBuiltinToolsForChartAutoSkill(['keyword_frequency']), [
'keyword_frequency',
'render_chart',
])
})
it('does not remove raw SQL from non-chart auto-skill turns', () => {
assert.deepEqual(getAllowedBuiltinToolsForChartAutoSkill(['execute_sql', 'keyword_frequency']), [
'execute_sql',
'keyword_frequency',
'render_chart',
])
})
})
@@ -1,46 +1,5 @@
import { CHART_CAPABILITY_SKILL_ID, getChartCapabilitySkill } from '@openchatlab/core'
import type { SkillDef } from './types'
type SkillGetter = (id: string) => SkillDef | null
export function getBuiltinChartSkill(locale: string = 'zh-CN'): SkillDef {
return { ...getChartCapabilitySkill(locale), builtinId: CHART_CAPABILITY_SKILL_ID }
}
function getChartMenuLine(locale: string): string {
const skill = getBuiltinChartSkill(locale)
const isZh = locale.startsWith('zh')
const guidance = isZh
? '用户明确要求图表、画图、占比、趋势、分布、饼图、柱状图、折线图或热力图时优先激活;不要输出 Python/JS 绘图代码'
: 'Activate first when the user explicitly asks for charts, visualization, ratios, trends, distributions, pie, bar, line, or heatmap charts; do not output Python/JS chart code'
return `- ${skill.id}: ${skill.name} — ${skill.description}. ${guidance}`
}
export function buildSkillMenuWithBuiltinChart(baseMenu: string | null | undefined, locale: string = 'zh-CN'): string {
if (baseMenu?.includes(CHART_CAPABILITY_SKILL_ID)) return baseMenu
const chartLine = getChartMenuLine(locale)
if (!baseMenu) {
return `## 可用技能
以下是你可以使用的分析技能。当你判断用户的问题适合使用某个技能时,
请调用 activate_skill 工具激活它,然后按照返回的指导完成任务。
${chartLine}
如果用户的问题不需要使用技能,直接回答即可。`
}
const closing = '\n\n如果用户的问题不需要使用技能,直接回答即可。'
if (!baseMenu.includes(closing)) return `${baseMenu}\n${chartLine}`
return baseMenu.replace(closing, `\n${chartLine}${closing}`)
}
export function getSkillConfigWithBuiltinChart(
id: string,
locale: string = 'zh-CN',
getSkillConfig: SkillGetter
): SkillDef | null {
if (id === CHART_CAPABILITY_SKILL_ID) return getBuiltinChartSkill(locale)
return getSkillConfig(id)
}
export {
buildSkillMenuWithBuiltinChart,
getChartCapabilitySkill as getBuiltinChartSkill,
getSkillConfigWithBuiltinChart,
} from './chart-runtime'
@@ -0,0 +1,192 @@
import { CHART_CAPABILITY_SKILL_ID } from '@openchatlab/core'
import type { SkillDef } from './types'
export { CHART_CAPABILITY_SKILL_ID }
export const CHART_CAPABILITY_CORE_TOOLS = ['get_schema'] as const
export const CHART_CAPABILITY_ANALYSIS_TOOLS = ['render_chart'] as const
const RAW_SQL_TOOL_NAMES = new Set(['execute_sql'])
export interface ResolveChartRuntimeOptions {
skillId?: string | null
userMessage?: string
locale?: string
assistantAllowedTools?: readonly string[] | null
enableAutoDetection?: boolean
}
export interface ResolvedChartRuntime {
isChartCapability: boolean
skillDef?: SkillDef
allowedBuiltinTools?: string[]
}
export function getChartCapabilityAllowedBuiltinTools(allowedTools?: readonly string[] | null): string[] {
const baseTools = (allowedTools ?? []).filter((toolName) => !RAW_SQL_TOOL_NAMES.has(toolName))
return Array.from(new Set([...baseTools, ...CHART_CAPABILITY_ANALYSIS_TOOLS]))
}
export function getAllowedBuiltinToolsForChartAutoSkill(allowedTools?: readonly string[] | null): string[] | undefined {
if (!allowedTools) return undefined
if (allowedTools.length === 0) return []
return Array.from(new Set([...allowedTools, ...CHART_CAPABILITY_ANALYSIS_TOOLS]))
}
export function shouldUseChartCapabilityForMessage(message: string): boolean {
const normalized = message.trim().toLowerCase()
if (!normalized) return false
return /(?:画|绘制|生成|做|出|展示).{0,8}(?:图|图表)|(?:图表|可视化|饼图|柱状图|条形图|折线图|热力图|趋势图|占比图|分布图|chart|charts|plot|visuali[sz]e|visuali[sz]ation|pie chart|bar chart|line chart|heatmap)/i.test(
normalized
)
}
export function resolveChartRuntimeForRequest(options: ResolveChartRuntimeOptions): ResolvedChartRuntime {
const locale = options.locale ?? 'zh-CN'
const isExplicitChartSkill = options.skillId === CHART_CAPABILITY_SKILL_ID
const isAutoChartSkill =
options.enableAutoDetection === true &&
!options.skillId &&
shouldUseChartCapabilityForMessage(options.userMessage ?? '')
if (!isExplicitChartSkill && !isAutoChartSkill) {
return { isChartCapability: false }
}
return {
isChartCapability: true,
skillDef: { ...getChartCapabilitySkill(locale), chatScope: 'all' },
allowedBuiltinTools: getChartCapabilityAllowedBuiltinTools(options.assistantAllowedTools),
}
}
export function getChartCapabilitySkill(locale: string = 'zh-CN'): SkillDef {
const isZh = locale.startsWith('zh')
return {
id: CHART_CAPABILITY_SKILL_ID,
name: isZh ? '绘图助手' : 'Chart Assistant',
description: isZh ? '按本轮问题生成灵活的聊天数据图表' : 'Generate flexible charts for this chat question',
tags: [isZh ? '图表' : 'chart'],
chatScope: 'all',
tools: ['render_chart', 'get_schema'],
prompt: isZh ? ZH_PROMPT : EN_PROMPT,
builtinId: CHART_CAPABILITY_SKILL_ID,
}
}
function getChartMenuLine(locale: string): string {
const skill = getChartCapabilitySkill(locale)
const isZh = locale.startsWith('zh')
const guidance = isZh
? '用户明确要求图表、画图、占比、趋势、分布、饼图、柱状图、折线图或热力图时优先激活;不要输出 Python/JS 绘图代码'
: 'Activate first when the user explicitly asks for charts, visualization, ratios, trends, distributions, pie, bar, line, or heatmap charts; do not output Python/JS chart code'
return `- ${skill.id}: ${skill.name} — ${skill.description}. ${guidance}`
}
export function buildSkillMenuWithBuiltinChart(baseMenu: string | null | undefined, locale: string = 'zh-CN'): string {
if (baseMenu?.includes(CHART_CAPABILITY_SKILL_ID)) return baseMenu
const chartLine = getChartMenuLine(locale)
if (!baseMenu) {
return `## 可用技能
以下是你可以使用的分析技能。当你判断用户的问题适合使用某个技能时,
请调用 activate_skill 工具激活它,然后按照返回的指导完成任务。
${chartLine}
如果用户的问题不需要使用技能,直接回答即可。`
}
const closing = '\n\n如果用户的问题不需要使用技能,直接回答即可。'
if (!baseMenu.includes(closing)) return `${baseMenu}\n${chartLine}`
return baseMenu.replace(closing, `\n${chartLine}${closing}`)
}
export function getSkillConfigWithBuiltinChart(
id: string,
locale: string = 'zh-CN',
getSkillConfig: (id: string) => SkillDef | null
): SkillDef | null {
if (id === CHART_CAPABILITY_SKILL_ID) return getChartCapabilitySkill(locale)
return getSkillConfig(id)
}
const ZH_PROMPT = `你是 ChatLab 绘图助手。本轮用户希望你在回答里自然嵌入一张或多张图表。
你必须通过 render_chart 工具生成图表。不要输出 HTML、JavaScript、SVG、Canvas、ECharts option 或任何渲染代码。
硬性规则:
1. 第一次写 SQL 前必须先调用 get_schema。禁止猜测表名、字段名或时间字段。
2. “最近”“到现在”“到最近”必须用数据中的 MAX(ts) 或等价字段确定结束日期;不要手算或臆测日期。
3. ChatLab 的 message.ts 是秒级 Unix 时间戳,使用 date(ts, 'unixepoch', 'localtime');禁止写 ts/1000。
4. “最近的 N 人/成员”必须先用数据定义并筛选出 N 个成员,再只统计这些成员。
5. 折线图有多条线时,每条线必须有明确 series 字段含义;按日期统计时应补齐日期 × series 的 0 值,避免缺失日期导致折线断裂或误连。
6. 工具失败、SQL 修正、schema 探索过程不要写进最终回答;最终只保留图表和简短结论。
7. 图表解释只能描述数据直接支持的趋势、峰值、低谷、差异和同步性,禁止夸张拟人化或无法从图中验证的关系判断。
工作流程:
1. 调用 get_schema。
2. 根据 schema 和用户要求确定统计对象、时间范围、维度、指标和图表类型。
3. 必要时用只读 SQL 在 render_chart 中完成成员筛选、时间范围确认、聚合和补零。
4. 调用 render_chart,提交只读 SQL 和 ChartSpec v1。
5. 图表生成后,用 1-3 句简洁文字解释结论。用户要求多张图时,可以多次调用 render_chart。
常用语、口头禅、高频短句类图表必须排除非真人文本:发送者为系统消息、媒体占位(如 [表情包]、[图片]、[语音]、[视频]、[文件]、[链接]、[红包]、[转账])以及撤回/删除提示都不能进入统计。
ChartSpec v1 支持:
- bar: encoding.x + encoding.y
- line: encoding.x + encoding.y,可选 encoding.series 表示每条线的含义
- pie: encoding.label + encoding.value
- heatmap: encoding.x + encoding.y + encoding.value
ChartSpec 示例:
{
"version": 1,
"type": "line",
"title": "最近 30 天成员发言趋势",
"encoding": { "x": "day", "y": "msg_count", "series": "member_name" },
"unit": "条"
}
用户明确要求图表时必须尝试生成。用户没有明确要求时,只有排名、趋势、分布、占比或二维密度明显更清楚时才主动生成,且默认最多一张。`
const EN_PROMPT = `You are the ChatLab Chart Assistant. In this turn, the user wants one or more charts embedded naturally in the answer.
You must generate charts through the render_chart tool. Do not output HTML, JavaScript, SVG, Canvas, ECharts options, or rendering code.
Hard rules:
1. Always call get_schema before writing the first SQL query. Do not guess table names, field names, or timestamp fields.
2. For "latest", "until now", or "through the latest date", derive the end date from MAX(ts) or the equivalent timestamp field in the data. Do not hand-calculate or assume dates.
3. ChatLab message.ts is a Unix timestamp in seconds. Use date(ts, 'unixepoch', 'localtime'); never write ts/1000.
4. For "latest N members" or "recent N people", first define and select those N members from the data, then count only those members.
5. For multi-series line charts, every line must have an explicit series field. For daily counts, fill the date x series grid with zero values so missing days do not break or mislead the line.
6. Do not include failed SQL attempts, schema exploration, or retry reasoning in the final answer. Final answer should contain only the chart and a short conclusion.
7. Explain only trends, peaks, lows, differences, and synchrony directly supported by chart data. Do not make exaggerated personality or relationship claims.
Workflow:
1. Call get_schema.
2. Derive the target members, time range, dimensions, metrics, and chart type from schema and the user request.
3. When needed, use read-only SQL inside render_chart to select members, determine time range, aggregate data, and fill zero rows.
4. Call render_chart with read-only SQL and ChartSpec v1.
5. 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.
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.
ChartSpec v1 supports:
- bar: encoding.x + encoding.y
- line: encoding.x + encoding.y, optional encoding.series for the meaning of each line
- pie: encoding.label + encoding.value
- heatmap: encoding.x + encoding.y + encoding.value
ChartSpec example:
{
"version": 1,
"type": "line",
"title": "Member message trend in the last 30 days",
"encoding": { "x": "day", "y": "msg_count", "series": "member_name" },
"unit": "messages"
}
When the user explicitly asks for charts, you must try to generate them. If the user does not explicitly ask, add at most one chart only when ranking, trend, distribution, ratio, or two-dimensional density is clearly easier to understand visually.`
@@ -37,6 +37,7 @@ export type ContentBlock =
params?: Record<string, unknown>
}
}
| { type: 'error'; error: { name: string | null; message: string; stack: string | null } }
| {
type: 'summary_meta'
bufferBoundaryTimestamp: number
+10 -2
View File
@@ -53,10 +53,18 @@ export type { SkillManagerLogger } from './skill-manager'
export { createActivateSkillTool } from './activate-skill-tool'
export type { ActivateSkillToolOptions, ActivateSkillTool, ActivateSkillToolResult } from './activate-skill-tool'
export {
CHART_CAPABILITY_ANALYSIS_TOOLS,
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_SKILL_ID,
buildSkillMenuWithBuiltinChart,
getBuiltinChartSkill,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
getChartCapabilitySkill as getBuiltinChartSkill,
getSkillConfigWithBuiltinChart,
} from './builtin-chart-skill'
resolveChartRuntimeForRequest,
shouldUseChartCapabilityForMessage,
} from './chart-runtime'
export { CHART_SCHEMA_REQUIRED_MESSAGE, createChartSchemaGateState, wrapWithChartSchemaGate } from './chart-schema-gate'
export type { ChartSchemaGateState } from './chart-schema-gate'
+8
View File
@@ -127,9 +127,17 @@ export type { CreateCompressionLlmAdapterOptions } from './ai'
// SkillManager
export { SkillManager } from './ai'
export {
CHART_CAPABILITY_ANALYSIS_TOOLS,
CHART_CAPABILITY_CORE_TOOLS,
CHART_CAPABILITY_SKILL_ID,
buildSkillMenuWithBuiltinChart,
getAllowedBuiltinToolsForChartAutoSkill,
getChartCapabilityAllowedBuiltinTools,
getChartCapabilitySkill,
getBuiltinChartSkill,
getSkillConfigWithBuiltinChart,
resolveChartRuntimeForRequest,
shouldUseChartCapabilityForMessage,
CHART_SCHEMA_REQUIRED_MESSAGE,
createChartSchemaGateState,
wrapWithChartSchemaGate,
@@ -28,8 +28,8 @@ const isEmpty = computed(() => props.chart.rowCount === 0)
const summary = computed(() => {
const parts: string[] = []
if (props.chart.spec.unit) parts.push(props.chart.spec.unit)
if (props.chart.truncated) parts.push('truncated')
parts.push(`${props.chart.rowCount} rows`)
if (props.chart.truncated) parts.push(t('ai.chat.message.chart.truncated'))
parts.push(t('ai.chat.message.chart.rows', { count: props.chart.rowCount }))
return parts.join(' · ')
})
@@ -138,7 +138,7 @@ const expandedMinWidth = computed(() => {
v-if="isEmpty"
class="flex h-64 items-center justify-center text-xs text-gray-400 dark:text-gray-500"
>
No data
{{ t('ai.chat.message.chart.empty') }}
</div>
<EChartBar
v-else-if="chart.spec.type === 'bar'"
@@ -178,7 +178,7 @@ const expandedMinWidth = computed(() => {
<div class="px-2 py-3">
<div v-if="isEmpty" class="flex h-32 items-center justify-center text-xs text-gray-400 dark:text-gray-500">
No data
{{ t('ai.chat.message.chart.empty') }}
</div>
<EChartBar
v-else-if="chart.spec.type === 'bar'"
+4 -1
View File
@@ -66,7 +66,10 @@
"tooltip": "Fork conversation from here"
},
"chart": {
"expand": "Open large chart"
"expand": "Open large chart",
"empty": "No data",
"rows": "{count} rows",
"truncated": "Truncated"
},
"tools": {
"get_chat_overview": "Get Chat Overview",
+4 -1
View File
@@ -66,7 +66,10 @@
"tooltip": "ここから会話をフォーク"
},
"chart": {
"expand": "大きなチャートを開く"
"expand": "大きなチャートを開く",
"empty": "データがありません",
"rows": "{count} 行のデータ",
"truncated": "切り捨て済み"
},
"tools": {
"get_chat_overview": "チャット概要を取得",
+4 -1
View File
@@ -66,7 +66,10 @@
"tooltip": "从此处复刻对话"
},
"chart": {
"expand": "打开大图"
"expand": "打开大图",
"empty": "暂无数据",
"rows": "{count} 行数据",
"truncated": "已截断"
},
"tools": {
"get_chat_overview": "获取聊天概览",
+4 -1
View File
@@ -66,7 +66,10 @@
"tooltip": "從此處複製對話"
},
"chart": {
"expand": "開啟大圖"
"expand": "開啟大圖",
"empty": "暫無資料",
"rows": "{count} 行資料",
"truncated": "已截斷"
},
"tools": {
"get_chat_overview": "取得聊天概覽",
+20 -3
View File
@@ -77,10 +77,13 @@ describe('aiChat chart block helpers', () => {
})
it('converts chart payloads to content blocks', () => {
assert.deepEqual(toChartContentBlocks([chart, secondChart]), [
{ type: 'chart', chart },
{ type: 'chart', chart: secondChart },
const blocks = toChartContentBlocks([chart, secondChart])
assert.deepEqual(blocks, [
{ type: 'chart', chart: { ...chart, dataset: { ...chart.dataset, rows: [] } } },
{ type: 'chart', chart: { ...secondChart, dataset: { ...secondChart.dataset, rows: [] } } },
])
assert.equal(blocks[0].chart.rowCount, chart.rowCount)
})
it('converts render-only chart tool failures to visible error blocks', () => {
@@ -108,4 +111,18 @@ describe('aiChat chart block helpers', () => {
null
)
})
it('does not expose recoverable schema-gate guidance as a user-visible error', () => {
assert.equal(
toRenderOnlyToolErrorBlock('render_chart', {
content: [
{
type: 'text',
text: 'Error: Call get_schema before render_chart. Do not guess table names, fields, or timestamp units.',
},
],
}),
null
)
})
})
+12 -1
View File
@@ -33,8 +33,18 @@ export function extractChartPayloads(toolResult: unknown): ChartPayload[] {
return charts
}
export function toPersistedChartPayload(chart: ChartPayload): ChartPayload {
return {
...chart,
dataset: {
...chart.dataset,
rows: [],
},
}
}
export function toChartContentBlocks(charts: ChartPayload[]): ChartContentBlock[] {
return charts.map((chart) => ({ type: 'chart', chart }))
return charts.map((chart) => ({ type: 'chart', chart: toPersistedChartPayload(chart) }))
}
function extractToolResultText(toolResult: unknown): string | null {
@@ -63,6 +73,7 @@ export function toRenderOnlyToolErrorBlock(
const match = /^Error:\s*(.+)$/is.exec(text)
const message = match?.[1]?.trim()
if (!message) return null
if (/^Call get_schema before render_chart\b/i.test(message)) return null
return {
type: 'error',
+1 -1
View File
@@ -40,7 +40,7 @@ describe('skill store chart capability', () => {
const config = await store.getSkillConfig(CHART_CAPABILITY_SKILL_ID)
assert.equal(config?.id, CHART_CAPABILITY_SKILL_ID)
assert.deepEqual(config?.tools, ['render_chart', 'get_schema'])
assert.match(config?.prompt ?? '', /render_chart/)
assert.equal(config?.prompt, '')
store.activateSkill(null)
assert.equal(store.activeSkillId, null)
+18 -4
View File
@@ -7,7 +7,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAssistantStore } from './assistant'
import { usePlatformService, useSkillService } from '@/services'
import { CHART_CAPABILITY_SKILL_ID, getChartCapabilitySkill } from '@openchatlab/core'
import { CHART_CAPABILITY_SKILL_ID } from '@openchatlab/core'
import { CHATLAB_SITE_BASE } from '@/utils/chatlabSiteLocale'
const CLOUD_MARKET_BASE_URL = CHATLAB_SITE_BASE
@@ -64,15 +64,28 @@ export const useSkillStore = defineStore('skill', () => {
const currentChatType = ref<'group' | 'private'>('group')
const currentLocale = ref<string>('zh-CN')
function getChartCapabilitySkillSummary(): SkillSummary {
const isZh = currentLocale.value.startsWith('zh')
return {
id: CHART_CAPABILITY_SKILL_ID,
name: isZh ? '绘图助手' : 'Chart Assistant',
description: isZh ? '按本轮问题生成灵活的聊天数据图表' : 'Generate flexible charts for this chat question',
tags: [isZh ? '图表' : 'chart'],
chatScope: 'all',
tools: ['render_chart', 'get_schema'],
builtinId: CHART_CAPABILITY_SKILL_ID,
}
}
const activeSkill = computed(() => {
if (!activeSkillId.value) return null
if (activeSkillId.value === CHART_CAPABILITY_SKILL_ID) {
return getChartCapabilitySkill(currentLocale.value)
return getChartCapabilitySkillSummary()
}
return skills.value.find((s) => s.id === activeSkillId.value) ?? null
})
const chartCapabilitySkill = computed(() => getChartCapabilitySkill(currentLocale.value))
const chartCapabilitySkill = computed(() => getChartCapabilitySkillSummary())
const scopedSkills = computed(() => {
return skills.value.filter((s) => s.chatScope === 'all' || s.chatScope === currentChatType.value)
@@ -196,9 +209,10 @@ export const useSkillStore = defineStore('skill', () => {
async function getSkillConfig(id: string): Promise<SkillConfigFull | null> {
if (id === CHART_CAPABILITY_SKILL_ID) {
const skill = getChartCapabilitySkill(currentLocale.value)
const skill = getChartCapabilitySkillSummary()
return {
...skill,
prompt: '',
builtinId: CHART_CAPABILITY_SKILL_ID,
}
}