refactor(tools): streamline tool selection and enhance unknown tool handling

- Introduced a new utility function to determine if a tool is an agent tool, simplifying the tool selection logic in MessageTool.
- Refactored MessageAgentTools to improve rendering logic and added an UnknownToolRenderer for better handling of unrecognized tools.
- Updated BashOutputTool to remove unnecessary Card components, enhancing layout consistency.
- Improved overall code clarity and maintainability by reducing redundancy and adhering to existing patterns.
This commit is contained in:
MyPrototypeWhat 2025-09-28 19:00:35 +08:00
parent 965d7d3008
commit e95219f2ec
4 changed files with 170 additions and 95 deletions

View File

@ -1,4 +1,4 @@
import { AccordionItem, Card, CardBody, Chip, Code } from '@heroui/react'
import { AccordionItem, Chip, Code } from '@heroui/react'
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
import { useMemo } from 'react'
@ -132,57 +132,43 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
{/* Standard Output */}
{parsedOutput.stdout && (
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
<CardBody className="p-3">
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
{parsedOutput.stdout}
</pre>
</CardBody>
</Card>
<div>
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
{parsedOutput.stdout}
</pre>
</div>
)}
{/* Standard Error */}
{parsedOutput.stderr && (
<Card
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
shadow="none">
<CardBody className="p-3">
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.stderr}
</pre>
</CardBody>
</Card>
<div className="border border-danger-200">
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.stderr}
</pre>
</div>
)}
{/* Tool Use Error */}
{parsedOutput.tool_use_error && (
<Card
className="border border-danger-200 bg-danger-50/30 dark:border-danger-800 dark:bg-danger-900/10"
shadow="none">
<CardBody className="p-3">
<div className="mb-2 flex items-center gap-2">
<XCircle className="h-4 w-4 text-danger" />
<span className="font-medium text-danger-600 text-xs">Error:</span>
</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.tool_use_error}
</pre>
</CardBody>
</Card>
<div className="border border-danger-200">
<div className="mb-2 flex items-center gap-2">
<XCircle className="h-4 w-4 text-danger" />
<span className="font-medium text-danger-600 text-xs">Error:</span>
</div>
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
{parsedOutput.tool_use_error}
</pre>
</div>
)}
</>
) : (
// 原始输出(如果解析失败或非 XML 格式)
output && (
<Card className="bg-default-50 dark:bg-default-900/20" shadow="none">
<CardBody className="p-3">
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
{output}
</pre>
</CardBody>
</Card>
<div>
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
</div>
)
)}
</AccordionItem>

View File

@ -0,0 +1,88 @@
import { AccordionItem } from '@heroui/react'
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { Wrench } from 'lucide-react'
import { useEffect, useState } from 'react'
import { ToolTitle } from './GenericTools'
interface UnknownToolProps {
toolName: string
input?: unknown
output?: unknown
}
export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToolProps) {
const { highlightCode } = useCodeStyle()
const [inputHtml, setInputHtml] = useState<string>('')
const [outputHtml, setOutputHtml] = useState<string>('')
useEffect(() => {
if (input !== undefined) {
const inputStr = JSON.stringify(input, null, 2)
highlightCode(inputStr, 'json').then(setInputHtml)
}
}, [input, highlightCode])
useEffect(() => {
if (output !== undefined) {
const outputStr = JSON.stringify(output, null, 2)
highlightCode(outputStr, 'json').then(setOutputHtml)
}
}, [output, highlightCode])
const getToolDisplayName = (name: string) => {
if (name.startsWith('mcp__')) {
const parts = name.substring(5).split('__')
if (parts.length >= 2) {
return `${parts[0]}:${parts.slice(1).join(':')}`
}
}
return name
}
const getToolDescription = () => {
if (toolName.startsWith('mcp__')) {
return 'MCP Server Tool'
}
return 'Tool'
}
return (
<AccordionItem
key="unknown-tool"
aria-label={toolName}
title={
<ToolTitle
icon={<Wrench className="h-4 w-4" />}
label={getToolDisplayName(toolName)}
params={getToolDescription()}
/>
}>
<div className="space-y-3">
{input !== undefined && (
<div>
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Input:</div>
<div
className="overflow-x-auto rounded bg-gray-50 dark:bg-gray-900"
dangerouslySetInnerHTML={{ __html: inputHtml }}
/>
</div>
)}
{output !== undefined && (
<div>
<div className="mb-1 font-semibold text-foreground-600 text-xs dark:text-foreground-400">Output:</div>
<div
className="rounded bg-gray-50 dark:bg-gray-900 [&>*]:whitespace-pre-line"
dangerouslySetInnerHTML={{ __html: outputHtml }}
/>
</div>
)}
{input === undefined && output === undefined && (
<div className="text-foreground-500 text-xs">No data available for this tool</div>
)}
</div>
</AccordionItem>
)
}

View File

@ -19,10 +19,10 @@ import { SearchTool } from './SearchTool'
import { TaskTool } from './TaskTool'
import { TodoWriteTool } from './TodoWriteTool'
import { AgentToolsType, ToolInput, ToolOutput } from './types'
import { UnknownToolRenderer } from './UnknownToolRenderer'
import { WebFetchTool } from './WebFetchTool'
import { WebSearchTool } from './WebSearchTool'
import { WriteTool } from './WriteTool'
const logger = loggerService.withContext('MessageAgentTools')
// 创建工具渲染器映射,这样就实现了完全的类型安全
@ -54,20 +54,24 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?:
const Renderer = toolRenderers[toolName]
return (
<Accordion
className="w-max max-w-full"
itemClasses={{
trigger:
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
indicator: 'flex-shrink-0',
subtitle: 'text-xs',
content:
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
}}
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
{/* <Renderer input={input as any} output={output as any} /> */}
{Renderer({ input: input as any, output: output as any })}
</Accordion>
<div className="w-max max-w-full rounded-md bg-foreground-100 py-1 transition-all duration-300 ease-in-out dark:bg-foreground-100">
<Accordion
className="w-max max-w-full"
itemClasses={{
trigger:
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
indicator: 'flex-shrink-0',
subtitle: 'text-xs',
content:
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll',
base: 'space-y-1'
}}
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
{Renderer
? Renderer({ input: input as any, output: output as any })
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })}
</Accordion>
</div>
)
}
@ -80,17 +84,5 @@ export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolRe
response
})
// 使用类型守卫确保类型安全
if (!isValidAgentToolsType(tool?.name)) {
logger.warn('Invalid tool name received', { toolName: tool?.name })
return (
<div className="rounded-lg bg-red-50 p-3 dark:bg-red-900/20">
<div className="text-red-600 text-sm dark:text-red-400">Invalid tool name: {tool?.name}</div>
</div>
)
}
const toolName = tool.name
return renderToolContent(toolName, args as ToolInput, response as ToolOutput)
return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput)
}

View File

@ -10,39 +10,48 @@ interface Props {
block: ToolMessageBlock
}
const prefix = 'builtin_'
const agentPrefix = 'mcp__'
const agentTools = [
'Read',
'Task',
'Bash',
'Search',
'Glob',
'TodoWrite',
'WebSearch',
'Grep',
'Write',
'WebFetch',
'Edit',
'MultiEdit',
'BashOutput',
'NotebookEdit',
'ExitPlanMode'
]
const isAgentTool = (toolName: string) => {
if (agentTools.includes(toolName) || toolName.startsWith(agentPrefix)) {
return true
}
return false
}
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
let toolName = toolResponse.tool.name
if (toolName.startsWith(prefix)) {
toolName = toolName.slice(prefix.length)
}
switch (toolName) {
case 'web_search':
case 'web_search_preview':
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
case 'knowledge_search':
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
case 'memory_search':
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
case 'Read':
case 'Task':
case 'Bash':
case 'Search':
case 'Glob':
case 'TodoWrite':
case 'WebSearch':
case 'Grep':
case 'Write':
case 'WebFetch':
case 'Edit':
case 'MultiEdit':
case 'BashOutput':
case 'NotebookEdit':
case 'ExitPlanMode':
return <MessageAgentTools toolResponse={toolResponse} />
default:
return null
switch (toolName) {
case 'web_search':
case 'web_search_preview':
return <MessageWebSearchToolTitle toolResponse={toolResponse} />
case 'knowledge_search':
return <MessageKnowledgeSearchToolTitle toolResponse={toolResponse} />
case 'memory_search':
return <MessageMemorySearchToolTitle toolResponse={toolResponse} />
default:
return null
}
} else if (isAgentTool(toolName)) {
return <MessageAgentTools toolResponse={toolResponse} />
}
}