mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
refactor(ai): centralize chart runtime policy
This commit is contained in:
1 parent
15470444e5
commit
296573ed5c
28 files changed
+413
-369
No files matched your search
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
Reference in new issue
Block a user