feat(Tools): add WebFetchTool and update tool rendering logic

- Introduced WebFetchTool for fetching web content with specified prompts and URLs.
- Updated MessageTool to include WebFetch in the tool rendering options.
- Enhanced BashTool and TaskTool to improve display of input and output information.
- Refactored GenericTools for better parameter display and consistency across tools.
- Adjusted types to include WebFetchTool input and output definitions.
This commit is contained in:
MyPrototypeWhat 2025-09-22 23:47:43 +08:00
parent 91b4d806cd
commit 7631d9d730
10 changed files with 126 additions and 50 deletions

View File

@ -16,15 +16,16 @@ export function BashTool({ input, output }: { input: BashToolInputType; output?:
<ToolTitle
icon={<Terminal className="h-4 w-4" />}
label="Bash"
params={
<Code size="sm" className="text-sm">
{input.command}
</Code>
}
params={input.description}
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
/>
}
subtitle={
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis text-xs">
{input.command}
</Code>
}>
<div>{output}</div>
<div className="whitespace-pre-line">{output}</div>
</AccordionItem>
)
}

View File

@ -19,9 +19,9 @@ export function ToolTitle({
return (
<div className={`flex items-center gap-1 ${className}`}>
{icon}
{label && <span className="font-medium">{label}</span>}
{params && <span className="flex-shrink-0 text-muted-foreground text-sm">{params}</span>}
{stats && <span className="flex-shrink-0 text-muted-foreground">{stats}</span>}
{label && <span className="font-medium text-sm">{label}</span>}
{params && <span className="flex-shrink-0 text-muted-foreground text-xs">{params}</span>}
{stats && <span className="flex-shrink-0 text-muted-foreground text-xs">{stats}</span>}
</div>
)
}

View File

@ -1,7 +1,9 @@
import { AccordionItem } from '@heroui/react'
import { AccordionItem, ScrollShadow } from '@heroui/react'
import { FileText } from 'lucide-react'
import { SimpleFieldInputTool, StringOutputTool, ToolTitle } from './GenericTools'
import { ToolTitle } from './GenericTools'
import { AgentToolsType } from './types'
import ReactMarkdown from 'react-markdown'
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType } from './types'
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
@ -20,7 +22,7 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
return (
<AccordionItem
key="tool"
key={AgentToolsType.Read}
aria-label="Read Tool"
title={
<ToolTitle
@ -30,14 +32,11 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
stats={output && stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
/>
}>
<div>
<SimpleFieldInputTool input={input} label="File Path" fieldName="file_path" />
{output && (
<div>
<StringOutputTool output={output} label="File Content" />
</div>
)}
</div>
{output ? (
// <div className="h-full scroll-auto">
<ReactMarkdown>{output}</ReactMarkdown>
// </div>
) : null}
</AccordionItem>
)
}

View File

@ -2,6 +2,8 @@ import { AccordionItem } from '@heroui/react'
import { Bot } from 'lucide-react'
import { ToolTitle } from './GenericTools'
import Markdown from 'react-markdown'
import type { TaskToolInput as TaskToolInputType, TaskToolOutput as TaskToolOutputType } from './types'
export function TaskTool({ input, output }: { input: TaskToolInputType; output?: TaskToolOutputType }) {
@ -12,8 +14,7 @@ export function TaskTool({ input, output }: { input: TaskToolInputType; output?:
title={<ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />}>
{output?.map((item) => (
<div key={item.type}>
<div>Type: {item.type}</div>
<div>{item.text}</div>
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
</div>
))}
</AccordionItem>

View File

@ -1,16 +1,47 @@
import { AccordionItem } from '@heroui/react'
import { ListTodo } from 'lucide-react'
import { AccordionItem, Chip, Card, CardBody } from '@heroui/react'
import { ListTodo, CheckCircle, Clock, Circle } from 'lucide-react'
import { ToolTitle } from './GenericTools'
import type {
TodoWriteToolInput as TodoWriteToolInputType,
TodoWriteToolOutput as TodoWriteToolOutputType
TodoWriteToolOutput as TodoWriteToolOutputType,
TodoItem
} from './types'
import { AgentToolsType } from './types'
const getStatusConfig = (status: TodoItem['status']) => {
switch (status) {
case 'completed':
return {
color: 'success' as const,
icon: <CheckCircle className="h-3 w-3" />,
label: '已完成'
}
case 'in_progress':
return {
color: 'primary' as const,
icon: <Clock className="h-3 w-3" />,
label: '进行中'
}
case 'pending':
return {
color: 'default' as const,
icon: <Circle className="h-3 w-3" />,
label: '待处理'
}
default:
return {
color: 'default' as const,
icon: <Circle className="h-3 w-3" />,
label: '待处理'
}
}
}
export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType; output?: TodoWriteToolOutputType }) {
return (
<AccordionItem
key="tool"
key={AgentToolsType.TodoWrite}
aria-label="Todo Write Tool"
title={
<ToolTitle
@ -19,16 +50,34 @@ export function TodoWriteTool({ input, output }: { input: TodoWriteToolInputType
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
/>
}>
<div>
{input.todos.map((todo, index) => (
<div key={index}>
<div>
<span>{todo.status}</span>
{todo.activeForm && <span>{todo.activeForm}</span>}
</div>
<div>{todo.content}</div>
</div>
))}
<div className="space-y-3">
{input.todos.map((todo, index) => {
const statusConfig = getStatusConfig(todo.status)
return (
<Card key={index} className="shadow-sm">
<CardBody>
<div className="flex items-start gap-3">
<Chip
color={statusConfig.color}
variant="flat"
size="sm"
startContent={statusConfig.icon}
className="flex-shrink-0">
{statusConfig.label}
</Chip>
<div className="min-w-0 flex-1">
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
{todo.status === 'completed' ? <s>{todo.content}</s> : todo.content}
</div>
{todo.status === 'in_progress' && (
<div className="mt-1 text-default-400 text-xs">{todo.activeForm}</div>
)}
</div>
</div>
</CardBody>
</Card>
)
})}
</div>
{output}
</AccordionItem>

View File

@ -0,0 +1,17 @@
import { AccordionItem } from '@heroui/react'
import { Globe } from 'lucide-react'
import { ToolTitle } from './GenericTools'
import type { WebFetchToolInput, WebFetchToolOutput } from './types'
export function WebFetchTool({ input, output }: { input: WebFetchToolInput; output?: WebFetchToolOutput }) {
return (
<AccordionItem
key="tool"
aria-label="Web Fetch Tool"
title={<ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />}
subtitle={input.prompt}>
{output}
</AccordionItem>
)
}

View File

@ -20,10 +20,7 @@ export function WebSearchTool({ input, output }: { input: WebSearchToolInput; ou
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
/>
}>
<div>
<SimpleFieldInputTool input={input} label="Web Search Query" fieldName="query" />
{output}
</div>
{output}
</AccordionItem>
)
}

View File

@ -16,6 +16,7 @@ import { TodoWriteTool } from './TodoWriteTool'
import { AgentToolsType, ToolInput, ToolOutput } from './types'
import { WebSearchTool } from './WebSearchTool'
import { WriteTool } from './WriteTool'
import { WebFetchTool } from './WebFetchTool'
const logger = loggerService.withContext('MessageAgentTools')
@ -29,7 +30,8 @@ export const toolRenderers = {
[AgentToolsType.TodoWrite]: TodoWriteTool,
[AgentToolsType.WebSearch]: WebSearchTool,
[AgentToolsType.Grep]: GrepTool,
[AgentToolsType.Write]: WriteTool
[AgentToolsType.Write]: WriteTool,
[AgentToolsType.WebFetch]: WebFetchTool
} as const
// 类型守卫函数
@ -40,18 +42,18 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools
// 统一的渲染函数
function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) {
const Renderer = toolRenderers[toolName]
if (!Renderer) {
logger.error('Unknown tool type', { toolName })
return <div>Unknown tool type: {toolName}</div>
}
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'
}}>
'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 h-fit max-h-96 scroll-auto'
}}
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>

View File

@ -7,7 +7,8 @@ export enum AgentToolsType {
TodoWrite = 'TodoWrite',
WebSearch = 'WebSearch',
Grep = 'Grep',
Write = 'Write'
Write = 'Write',
WebFetch = 'WebFetch'
}
// Read 工具的类型定义
@ -52,7 +53,7 @@ export type GlobToolOutput = string
// TodoWrite 工具的类型定义
export interface TodoItem {
content: string
status: string
status: 'completed' | 'in_progress' | 'pending'
activeForm?: string
}
@ -66,9 +67,15 @@ export type TodoWriteToolOutput = string
export interface WebSearchToolInput {
query: string
}
export type WebSearchToolOutput = string
// WebFetch 工具的类型定义
export type WebFetchToolInput = {
prompt: string
url: string
}
export type WebFetchToolOutput = string
// Grep 工具的类型定义
export interface GrepToolInput {
pattern: string
@ -93,6 +100,7 @@ export type ToolInput =
| GlobToolInput
| TodoWriteToolInput
| WebSearchToolInput
| WebFetchToolInput
| GrepToolInput
| WriteToolInput
export type ToolOutput =
@ -104,6 +112,7 @@ export type ToolOutput =
| TodoWriteToolOutput
| WebSearchToolOutput
| GrepToolOutput
| WebFetchToolOutput
| WriteToolOutput
// 工具渲染器接口
export interface ToolRenderer {

View File

@ -34,6 +34,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
case 'WebSearch':
case 'Grep':
case 'Write':
case 'WebFetch':
return <MessageAgentTools toolResponse={toolResponse} />
default:
return null