skill tool config (#6114)

Co-authored-by: xxyyh <2289112474@qq>
This commit is contained in:
YeYuheng 2025-12-18 13:42:59 +08:00 committed by archer
parent 1dab68b5ca
commit 5097d25379
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
9 changed files with 353 additions and 143 deletions

View File

@ -13,26 +13,27 @@ export const getPrompt = ({
return `<!-- 任务执行流程设计系统 -->
<role>
****,
****,
****:,
****:,
****:
- 任务分解:将任务拆解为清晰的执行步骤
- 工具匹配:为每个步骤选择合适的工具
- 流程组织:按照逻辑顺序组织步骤
- 战略分析:从宏观层面理解任务的本质和目标
- 框架设计:将任务划分为几个关键阶段或模块
- 资源规划:识别每个阶段需要的资源类型
</role>
<mission>
****:,:
1. ****:
2. ****:使
3. ****:,
****:,:
1. ****:()
2. ****:/
3. ****:
****:
-
- 使
- ,
-
-
- "五年规划",
-
</mission>
${currentConfigContext}
@ -40,52 +41,57 @@ ${currentConfigContext}
<context_awareness>
****():
- ,
-
-
- "按什么顺序做什么"
</context_awareness>
<system_features_handling>
****():
(),:
(),:
1. ****
- resources.system_features
- 使
2. ****
- 如果文件上传已启用:在第一步或相关步骤中说明需要用户上传文件
-
- ()
- 如果文件上传已启用:在第一阶段或相关阶段中说明需要用户上传文件
-
- ()
3. ** expected_tools **
- ,
- expected_tools
- ,
- ,
</system_features_handling>
<info_collection_phase>
:
****::
****::
1. ****
- ?
- ?
- ?
1. ****
- ?
- ?
- ?
2. **使**
- 使?
- 使?
2. ****
- ?(///)
- ?
- ?
3. ****
- ?
- ?
- ?
3. ****
- ?
- 使?
- ?
****:
- ****:,
- ****:使
- ****:,
- ****:"做什么""怎么做",
- ****:,
- ****:,
****:
- 宏观层面:任务目标
- 具体细节:具体参数
****:
@ -94,36 +100,42 @@ ${currentConfigContext}
JSON():
{
"phase": "collection",
"reasoning": "为什么问这个问题的推理过程:基于什么考虑、希望收集什么信息、对后续有什么帮助",
"reasoning": "为什么问这个问题的推理过程:基于什么战略考虑、希望收集什么方向性信息",
"question": "实际向用户提出的问题内容"
}
,:
:
****:
{
"phase": "collection",
"reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和步骤设计",
"question": "我想了解一下您希望这个执行流程实现什么功能?能否详细描述一下具体要处理什么样的任务?"
"reasoning": "需要了解任务的核心定位和战略目标,这将决定整体框架的设计方向",
"question": "这个 Skill 主要想解决什么问题?比如是面向客户服务、内容创作、数据分析,还是其他应用场景?"
}
:
{
"phase": "collection",
"reasoning": "需要确认步骤设计的重点方向,这将影响流程的详细程度",
"question": "关于流程的设计,您更关注:\\nA. 步骤的详细程度(每个步骤都很详细)\\nB. 步骤的灵活性(可以根据情况调整)\\nC. 步骤的简洁性(尽可能少的步骤)\\nD. 其他\\n\\n请选择最符合的选项,或输入您的详细回答:"
"reasoning": "需要识别任务所需的核心能力类型,这将决定资源配置的大方向",
"question": "从功能角度看,这个任务主要需要:\\nA. 获取和整合信息(搜索、查询、知识检索)\\nB. 处理和分析数据(计算、转换、分析)\\nC. 生成和创作内容(文档、报告、创意)\\nD. 集成外部服务(API调用、消息通知)\\n\\n请选择最主要的方向,或描述您的理解:"
}
****():
- "第一步需要调用哪个API?"
- "数据格式应该是JSON还是XML?"
- "需要设置多少个验证节点?"
- "具体的搜索关键词是什么?"
:
- (///)
- (///)
- (///)
- (///)
- (///)
- (///)
- (//)
- (使//)
:
-
-
- 使
:
-
-
-
-
</info_collection_phase>
<capability_boundary_enforcement>
@ -191,12 +203,14 @@ ${currentConfigContext}
${resourceList}
"""
****:
****:
1. JSON格式输出
2. **** -
3. ()
4. ,
5. **使** -
3. ,
4. ****:"五年规划",User Mode收集
5. 3-5,
6. ,
7. **使** -
**🚨 使()**:
@ -218,7 +232,7 @@ ${resourceList}
- type ,
****:
1. ID,?
1. ID,?
2. [] "type": "tool"
3. [] "type": "knowledge"
4. id type
@ -288,6 +302,20 @@ ${resourceList}
- name: 简洁的英文计划名称,使线,:"createMarketingReport", "analyzeCustomerData"
- description: 详细描述计划的功能使,
- description应该包含:功能说明
- **steps数量**: 3-5,
****:
1. ****:step代表一个关键阶段或模块,
2. ****:"做什么","怎么做"
3. ****:/,User Mode收集
4. ****:,
****:
- :"收集用户需求信息" ()
- :"通过表单收集用户的姓名、邮箱、电话,并验证格式" ()
- :"分析和处理数据" ()
- :"先过滤空值,然后按时间排序,最后计算平均值" ()
使:
- description中使用@[]
@ -297,19 +325,13 @@ ${resourceList}
-
-
:
1. 功能匹配优先:选择最能满足步骤需求的工具
2. 组合优化:多个工具可以组合使用以获得更好效果
3. 逻辑连贯:确保工具选择的逻辑性
4. 简洁高效:避免不必要的工具冗余
** **:
** **:
{
"phase": "generation",
"plan_analysis": {
"name": "createTravelItinerary",
"description": "创建旅游行程计划,包括景点查询、天气预报、行程文档生成。适用于需要规划旅行的场景,输出完整的markdown格式行程文档。",
"goal": "为用户规划详细的旅游行程",
"name": "travelPlanning",
"description": "旅游行程规划助手,帮助用户规划旅行路线。整合目的地信息、天气数据,生成完整行程方案。",
"goal": "为用户提供完整的旅游行程规划方案",
"type": "内容生成"
},
"execution_plan": {
@ -317,24 +339,24 @@ ${resourceList}
"steps": [
{
"id": "step1",
"title": "查询旅游目的地信息",
"description": "使用@[travel_destinations]知识库查询目的地的景点、美食、住宿等详细信息",
"title": "收集目的地基础信息",
"description": "通过@[travel_destinations]获取目的地相关信息",
"expectedTools": [
{"id": "travel_destinations", "type": "knowledge"}
]
},
{
"id": "step2",
"title": "查询实时天气信息",
"description": "使用@[mojiWeather/tool]工具获取目的地未来7天的天气预报",
"title": "获取实时环境数据",
"description": "使用@[mojiWeather/tool]获取天气等环境信息",
"expectedTools": [
{"id": "mojiWeather/tool", "type": "tool"}
]
},
{
"id": "step3",
"title": "生成行程计划文档",
"description": "使用@[markdownTransform]工具将行程信息格式化为markdown文档",
"title": "生成行程方案",
"description": "整合信息并使用@[markdownTransform]生成最终方案",
"expectedTools": [
{"id": "markdownTransform", "type": "tool"}
]
@ -374,33 +396,33 @@ ${resourceList}
<phase_decision_guidelines>
**🎯 关键:如何判断当前应该处于哪个阶段**
**,**:
**,**:
1. ****:
- ?
- 使?
- ?
- ?
- 使?
- ?
- , \`"phase": "collection"\` 继续提问
2. ****:
- ****, \`"phase": "generation"\`:
*
*
*
*
* 2-4 ()
3. **退**:
-
- :
* () \`"phase": "generation"\` 生成新计划
* () \`"phase": "generation"\` 生成新计划
* \`"phase": "collection"\` 回退继续提问
****:
- ()
-
- ,
- ,
- ,退
- ()
-
- ,
- ,
- ,退
</phase_decision_guidelines>
<conversation_rules>

View File

@ -228,6 +228,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
}
if (plan) {
console.log('plan output', plan);
workflowStreamResponse?.({
event: SseResponseEventEnum.agentPlan,
data: { agentPlan: plan }
@ -362,6 +363,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
teamId: runningUserInfo.teamId,
appId: runningAppInfo.id,
userInput: lastInteractive ? interactiveInput : userChatInput,
messages: historiesMessages, // 传入完整的对话历史
model
});
if (matchResult.matched && matchResult.systemPrompt) {

View File

@ -8,7 +8,7 @@ import { getLLMModel } from '../../../../ai/model';
*
* MatcherService.ts _generateUniqueFunctionName
*/
const generateUniqueFunctionName = (skill: HelperBotGeneratedSkillType): string => {
const generateUniqueFunctionName = (skill: AiSkillSchemaType): string => {
let baseName = skill.name || skill._id.toString();
// 清理名称
@ -30,13 +30,13 @@ const generateUniqueFunctionName = (skill: HelperBotGeneratedSkillType): string
* MatcherService.ts match
*/
export const buildSkillTools = (
skills: HelperBotGeneratedSkillType[]
skills: AiSkillSchemaType[]
): {
tools: ChatCompletionTool[];
skillsMap: Record<string, HelperBotGeneratedSkillType>;
skillsMap: Record<string, AiSkillSchemaType>;
} => {
const tools: ChatCompletionTool[] = [];
const skillsMap: Record<string, HelperBotGeneratedSkillType> = {};
const skillsMap: Record<string, AiSkillSchemaType> = {};
for (const skill of skills) {
// 生成唯一函数名
@ -67,26 +67,28 @@ export const buildSkillTools = (
* Skill SystemPrompt
* skill XML
*/
export const formatSkillAsSystemPrompt = (skill: HelperBotGeneratedSkillType): string => {
let prompt = '<reference_skill>\n';
prompt += `**参考技能**: ${skill.name}\n\n`;
export const formatSkillAsSystemPrompt = (skill: AiSkillSchemaType): string => {
const lines = ['<reference_skill>', `**参考技能**: ${skill.name}`, ''];
if (skill.description) {
prompt += `**描述**: ${skill.description}\n\n`;
lines.push(`**描述**: ${skill.description}`, '');
}
if (skill.steps && skill.steps.trim()) {
prompt += `**步骤信息**:\n${skill.steps}\n\n`;
lines.push(`**步骤信息**:`, skill.steps, '');
}
prompt += '**说明**:\n';
prompt += '1. 以上是用户之前保存的类似任务的执行计划\n';
prompt += '2. 请参考该技能的步骤流程和工具选择\n';
prompt += '3. 根据当前用户的具体需求,调整和优化计划\n';
prompt += '4. 保持步骤的逻辑性和完整性\n';
prompt += '</reference_skill>\n';
lines.push(
'**说明**:',
'1. 以上是用户之前保存的类似任务的执行框架',
'2. 请参考该技能的宏观阶段划分和资源方向',
'3. 根据当前用户的具体需求,调整和优化框架',
'4. 保持阶段的逻辑性和方向的清晰性',
'',
'</reference_skill>'
);
return prompt;
return lines.join('\n');
};
/**
@ -97,45 +99,42 @@ export const matchSkillForPlan = async ({
teamId,
appId,
userInput,
messages,
model
}: {
teamId: string;
appId: string;
userInput: string;
messages?: ChatCompletionMessageParam[];
model: string;
}): Promise<{
matched: boolean;
skill?: HelperBotGeneratedSkillType;
skill?: AiSkillSchemaType;
systemPrompt?: string;
reason?: string;
}> => {
try {
// 1. 查询用户的 skills (使用 teamId 和 appId)
const skills = await MongoAiSkill.find({
teamId,
appId,
status: { $in: ['active', 'draft'] }
appId
})
.sort({ createTime: -1 })
.limit(50) // 限制数量,避免 tools 过多
.limit(50)
.lean();
console.log('skill list length', skills.length);
console.log('skill', skills);
if (!skills || skills.length === 0) {
return { matched: false, reason: 'No skills available' };
}
// 2. 构建 tools 数组
const { tools, skillsMap } = buildSkillTools(skills);
console.log('tools', tools);
console.debug('tools', tools);
// 3. 获取模型配置
const modelData = getLLMModel(model);
// 4. 调用 LLM Tool Calling 进行匹配
// 构建系统提示词,指导 LLM 选择相似的任务
const systemPrompt = `你是一个智能任务匹配助手。请根据用户的当前需求,从提供的技能工具集中选择最相似的任务。
const skillMatchSystemPrompt = `你是一个智能任务匹配助手。请根据用户的当前需求,从提供的技能工具集中选择最相似的任务。
****
1. ****
@ -148,22 +147,34 @@ export const matchSkillForPlan = async ({
-
-
`;
使`;
// 构建简化的消息,只包含系统提示词和用户输入
const allMessages = [
const allMessages: ChatCompletionMessageParam[] = [
{
role: 'system' as const,
content: systemPrompt
},
{
role: 'user' as const,
content: userInput
content: skillMatchSystemPrompt
}
];
console.log('match request', { userInput, skillCount: skills.length });
const { toolCalls } = await createLLMResponse({
if (messages && messages.length > 0) {
allMessages.push(...messages);
}
allMessages.push({
role: 'user' as const,
content: userInput
});
console.debug('match request', {
hasHistory: !!(messages && messages.length > 0),
historyCount: messages?.length || 0,
currentInput: userInput.substring(0, 100), // 只显示前100个字符
skillCount: skills.length
});
const llmResponse = await createLLMResponse({
body: {
model: modelData.model,
messages: allMessages,
@ -174,6 +185,17 @@ export const matchSkillForPlan = async ({
}
});
console.log('=== LLM 完整返回 ===');
console.log('完整响应 keys:', Object.keys(llmResponse));
console.log('toolCalls:', llmResponse.toolCalls);
console.log('assistantMessage:', llmResponse.assistantMessage);
// 打印完整对象(过滤掉可能很长的字段)
const { assistantMessage, ...otherFields } = llmResponse;
console.log('其他返回字段:', JSON.stringify(otherFields, null, 2));
const { toolCalls } = llmResponse;
// 5. 解析匹配结果
if (toolCalls && toolCalls.length > 0) {
const toolCall = toolCalls[0];

View File

@ -77,7 +77,9 @@ export const dispatchPlanAgent = async ({
// 分类query/user select/user form
const lastMessages = requestMessages[requestMessages.length - 1];
console.log('--------------PLAN MODE--------------');
console.log('user input:', userInput);
console.log('systemPrompt:', systemPrompt);
// 上一轮是 Ask 模式,进行工具调用拼接
if (

View File

@ -50,6 +50,15 @@ export const getPlanAgentSystemPrompt = ({
${subAppPrompt}
PLAN step description
- [@${SubAppIds.ask}]${PlanAgentAskTool.function.description}
****
1. ****
2. ****使
3. **使**/使使
- bing/webSearchgoogle/searchmetaso/metasoSearch
- 使 @tavily_search使 @tavily_search
</toolset>
<process>
1. ****
@ -252,7 +261,10 @@ export const getUserContent = ({
}) => {
let userContent = `用户输入:${userInput}`;
if (systemPrompt) {
userContent += `\n\n背景信息${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n请按照用户提供的背景信息来重新生成计划优先遵循用户的步骤安排和偏好。`;
userContent += `\n\n背景信息${parseSystemPrompt({ systemPrompt, getSubAppInfo })}\n
\n
****@工具名使使`;
}
return userContent;
};
@ -297,6 +309,11 @@ export const getReplanAgentSystemPrompt = ({
${subAppPrompt}
PLAN step description
- [@${SubAppIds.ask}]${PlanAgentAskTool.function.description}
****
1. ****使使
2. ****
3. ****使
</tools>
<process>
@ -494,6 +511,14 @@ export const getReplanAgentUserPrompt = ({
${stepsResponsePrompt}
${stepsIdPrompt} ,
`;
${stepsIdPrompt}
****
- 使使
-
-
`;
};

View File

@ -33,6 +33,7 @@ import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAiSkillDetail } from '@/web/core/ai/skill/api';
import { validateToolConfiguration, getToolConfigStatus } from './utils';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@ -103,19 +104,36 @@ const EditForm = ({
}
}, [selectedModel, setAppForm]);
// 打开编辑器
// 打开skill编辑器
const handleEditSkill = useCallback(
async (skill: SkillEditType) => {
// If skill has dbId, load full details from server
if (skill.id) {
const detail = await getAiSkillDetail({ id: skill.id });
// Validate tools and determine their configuration status
const toolsWithStatus = (detail.tools || [])
.filter((tool) => {
// First, validate tool compatibility with current config
const isValid = validateToolConfiguration({
toolTemplate: tool,
canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile,
canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg
});
return isValid;
})
.map((tool) => ({
...tool,
configStatus: getToolConfigStatus(tool)
}));
// Merge server data with local data
onEditSkill({
id: detail._id,
name: detail.name,
description: detail.description || '',
stepsText: detail.steps,
selectedTools: detail.tools || [],
selectedTools: toolsWithStatus,
dataset: { list: detail.datasets || [] }
});
} else {
@ -123,7 +141,7 @@ const EditForm = ({
onEditSkill(skill);
}
},
[onEditSkill]
[onEditSkill, appForm.chatConfig.fileSelectConfig]
);
return (

View File

@ -3,12 +3,14 @@ import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { SkillEditType } from '@fastgpt/global/core/app/formEdit/type';
import type { SkillEditType, SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import HelperBot from '@/components/core/chat/HelperBot';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import { validateToolConfiguration, checkNeedsUserConfiguration } from '../utils';
type Props = {
skill: SkillEditType;
@ -59,26 +61,85 @@ const ChatTest = ({ skill, appForm, onAIGenerate }: Props) => {
<HelperBot
type={HelperBotTypeEnum.skillAgent}
metadata={skillAgentMetadata}
onApply={(generatedSkillData) => {
onApply={async (generatedSkillData) => {
console.log(generatedSkillData, 222);
// const stepsText = generatedSkillData.execution_plan.steps
// .map((step, index) => {
// let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`;
// if (step.expectedTools && step.expectedTools.length > 0) {
// const tools = step.expectedTools
// .map((tool) => `${tool.type === 'tool' ? '🔧' : '📚'} ${tool.id}`)
// .join(', ');
// stepText += `\n使用工具: ${tools}`;
// }
// return stepText;
// })
// .join('\n\n');
// onAIGenerate({
// name: generatedSkillData.plan_analysis.name || skill.name,
// description: generatedSkillData.plan_analysis.description || skill.description,
// stepsText: stepsText
// });
// 1. 提取所有步骤中的工具 ID去重仅保留 type='tool'
const allToolIds = new Set<string>();
generatedSkillData.execution_plan.steps.forEach((step) => {
step.expectedTools?.forEach((tool) => {
if (tool.type === 'tool') {
allToolIds.add(tool.id);
}
});
});
// 2. 并行获取工具详情
const targetToolIds = Array.from(allToolIds);
const newTools: SelectedToolItemType[] = [];
const failedToolIds: string[] = [];
if (targetToolIds.length > 0) {
const results = await Promise.all(
targetToolIds.map((toolId: string) =>
getToolPreviewNode({ appId: toolId })
.then((tool) => ({ status: 'fulfilled' as const, toolId, tool }))
.catch((error) => ({ status: 'rejected' as const, toolId, error }))
)
);
results.forEach((result) => {
if (result.status === 'fulfilled') {
// 验证工具配置
const toolValid = validateToolConfiguration({
toolTemplate: result.tool,
canSelectFile: appForm.chatConfig.fileSelectConfig?.canSelectFile,
canSelectImg: appForm.chatConfig.fileSelectConfig?.canSelectImg
});
if (toolValid) {
// 判断是否需要用户配置,设置 configStatus
const needsConfig = checkNeedsUserConfiguration(result.tool);
newTools.push({
...result.tool,
configStatus: needsConfig ? 'waitingForConfig' : 'active'
});
} else {
// 工具验证失败,记录失败
failedToolIds.push(result.toolId);
}
} else if (result.status === 'rejected') {
failedToolIds.push(result.toolId);
}
});
// 可选:提示用户哪些工具获取失败
if (failedToolIds.length > 0) {
console.warn('部分工具获取失败:', failedToolIds);
}
}
// 3. 构建 stepsText保持原有逻辑
const stepsText = generatedSkillData.execution_plan.steps
.map((step, index) => {
let stepText = `步骤 ${index + 1}: ${step.title}\n${step.description}`;
if (step.expectedTools && step.expectedTools.length > 0) {
const tools = step.expectedTools
.map((tool) => `${tool.type === 'tool' ? '🔧' : '📚'} ${tool.id}`)
.join(', ');
stepText += `\n使用工具: ${tools}`;
}
return stepText;
})
.join('\n\n');
// 4. 应用生成的数据,包含 selectedTools
onAIGenerate({
name: generatedSkillData.plan_analysis.name || skill.name,
description: generatedSkillData.plan_analysis.description || skill.description,
stepsText: stepsText,
selectedTools: newTools
});
}}
/>
</Box>

View File

@ -308,3 +308,51 @@ export const checkNeedsUserConfiguration = (toolTemplate: FlowNodeTemplateType):
false
);
};
/**
* Get the configuration status of a tool
* Checks if tool needs configuration and whether all required fields are filled
* @param toolTemplate - The tool template to check
* @returns 'active' if tool is ready to use, 'waitingForConfig' if configuration needed
*/
export const getToolConfigStatus = (
toolTemplate: FlowNodeTemplateType
): 'active' | 'waitingForConfig' => {
// Check if tool needs configuration
const needsConfig = checkNeedsUserConfiguration(toolTemplate);
if (!needsConfig) {
return 'active';
}
// For tools that need config, check if all required fields have values
const formRenderTypesMap: Record<string, boolean> = {
[FlowNodeInputTypeEnum.input]: true,
[FlowNodeInputTypeEnum.textarea]: true,
[FlowNodeInputTypeEnum.numberInput]: true,
[FlowNodeInputTypeEnum.password]: true,
[FlowNodeInputTypeEnum.switch]: true,
[FlowNodeInputTypeEnum.select]: true,
[FlowNodeInputTypeEnum.JSONEditor]: true,
[FlowNodeInputTypeEnum.timePointSelect]: true,
[FlowNodeInputTypeEnum.timeRangeSelect]: true
};
// Find all inputs that need configuration
const configInputs = toolTemplate.inputs.filter((input) => {
if (input.toolDescription) return false;
if (input.key === NodeInputKeyEnum.forbidStream) return false;
if (input.key === NodeInputKeyEnum.systemInputConfig) return true;
return input.renderTypeList.some((type) => formRenderTypesMap[type]);
});
// Check if all required fields are filled
const allConfigured = configInputs.every((input) => {
const value = input.value;
if (value === undefined || value === null || value === '') return false;
if (Array.isArray(value) && value.length === 0) return false;
if (typeof value === 'object' && Object.keys(value).length === 0) return false;
return true;
});
return allConfigured ? 'active' : 'waitingForConfig';
};

View File

@ -48,9 +48,19 @@ async function handler(
appId: tool.id,
lang: getLocale(req)
});
// Merge saved config back into inputs
const mergedInputs = toolNode.inputs.map((input) => ({
...input,
value:
tool.config && tool.config[input.key] !== undefined
? tool.config[input.key] // Use saved config value
: input.value // Keep default value
}));
return {
...toolNode,
configStatus: 'active' as const
inputs: mergedInputs
};
} catch (error) {
// If tool not found or error, mark as invalid