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)
}