mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-22 17:00:14 +08:00
Feat/add skill tool (#11051)
* feat: add SkillTool component and integrate into agent tools - Introduced SkillTool component for rendering skill-related functionality. - Updated MessageAgentTools to include SkillTool in the tool renderers. - Enhanced MessageTool to recognize 'Skill' as a valid agent tool type. - Modified handleUserMessage to conditionally handle text blocks based on skill inclusion. - Added SkillToolInput and SkillToolOutput types for better type safety. * feat: implement command tag filtering in message handling - Added filterCommandTags function to remove command-* tags from text content, ensuring internal command messages do not appear in the user-facing UI. - Updated handleUserMessage to utilize the new filtering logic, enhancing the handling of text blocks and improving user experience by preventing unwanted command messages from being displayed. * refactor: rename tool prefix constants for clarity - Updated variable names for tool prefixes in MessageTool and SkillTool components to enhance code readability. - Changed `prefix` to `builtinToolsPrefix` and `agentPrefix` to `agentMcpToolsPrefix` for better understanding of their purpose.
This commit is contained in:
parent
68e0d8b0f1
commit
b6dcf2f5fa
@ -73,6 +73,15 @@ const emptyUsage: LanguageModelUsage = {
|
|||||||
*/
|
*/
|
||||||
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
|
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out command-* tags from text content to prevent internal command
|
||||||
|
* messages from appearing in the user-facing UI.
|
||||||
|
* Removes tags like <command-message>...</command-message> and <command-name>...</command-name>
|
||||||
|
*/
|
||||||
|
const filterCommandTags = (text: string): string => {
|
||||||
|
return text.replace(/<command-[^>]+>.*?<\/command-[^>]+>/gs, '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts provider metadata from the raw Claude message so we can surface it
|
* Extracts provider metadata from the raw Claude message so we can surface it
|
||||||
* on every emitted stream part for observability and debugging purposes.
|
* on every emitted stream part for observability and debugging purposes.
|
||||||
@ -270,12 +279,17 @@ function handleUserMessage(
|
|||||||
const chunks: AgentStreamPart[] = []
|
const chunks: AgentStreamPart[] = []
|
||||||
const providerMetadata = sdkMessageToProviderMetadata(message)
|
const providerMetadata = sdkMessageToProviderMetadata(message)
|
||||||
const content = message.message.content
|
const content = message.message.content
|
||||||
|
const isSynthetic = message.isSynthetic ?? false
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredContent = filterCommandTags(content)
|
||||||
|
if (!filteredContent) {
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
const id = message.uuid?.toString() || generateMessageId()
|
const id = message.uuid?.toString() || generateMessageId()
|
||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'text-start',
|
type: 'text-start',
|
||||||
@ -285,7 +299,7 @@ function handleUserMessage(
|
|||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'text-delta',
|
type: 'text-delta',
|
||||||
id,
|
id,
|
||||||
text: content,
|
text: filteredContent,
|
||||||
providerMetadata
|
providerMetadata
|
||||||
})
|
})
|
||||||
chunks.push({
|
chunks.push({
|
||||||
@ -323,7 +337,12 @@ function handleUserMessage(
|
|||||||
providerExecuted: true
|
providerExecuted: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (block.type === 'text') {
|
} else if (block.type === 'text' && !isSynthetic) {
|
||||||
|
const rawText = (block as { text: string }).text
|
||||||
|
const filteredText = filterCommandTags(rawText)
|
||||||
|
|
||||||
|
// Only push text chunks if there's content after filtering
|
||||||
|
if (filteredText) {
|
||||||
const id = message.uuid?.toString() || generateMessageId()
|
const id = message.uuid?.toString() || generateMessageId()
|
||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'text-start',
|
type: 'text-start',
|
||||||
@ -333,7 +352,7 @@ function handleUserMessage(
|
|||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'text-delta',
|
type: 'text-delta',
|
||||||
id,
|
id,
|
||||||
text: (block as { text: string }).text,
|
text: filteredText,
|
||||||
providerMetadata
|
providerMetadata
|
||||||
})
|
})
|
||||||
chunks.push({
|
chunks.push({
|
||||||
@ -341,6 +360,7 @@ function handleUserMessage(
|
|||||||
id,
|
id,
|
||||||
providerMetadata
|
providerMetadata
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Unhandled user content block', { type: (block as any).type })
|
logger.warn('Unhandled user content block', { type: (block as any).type })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { AccordionItem } from '@heroui/react'
|
||||||
|
import { PencilRuler } from 'lucide-react'
|
||||||
|
|
||||||
|
import { ToolTitle } from './GenericTools'
|
||||||
|
import type { SkillToolInput, SkillToolOutput } from './types'
|
||||||
|
|
||||||
|
export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) {
|
||||||
|
return (
|
||||||
|
<AccordionItem
|
||||||
|
key="tool"
|
||||||
|
aria-label="Skill Tool"
|
||||||
|
title={<ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />}>
|
||||||
|
{output}
|
||||||
|
</AccordionItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import { MultiEditTool } from './MultiEditTool'
|
|||||||
import { NotebookEditTool } from './NotebookEditTool'
|
import { NotebookEditTool } from './NotebookEditTool'
|
||||||
import { ReadTool } from './ReadTool'
|
import { ReadTool } from './ReadTool'
|
||||||
import { SearchTool } from './SearchTool'
|
import { SearchTool } from './SearchTool'
|
||||||
|
import { SkillTool } from './SkillTool'
|
||||||
import { TaskTool } from './TaskTool'
|
import { TaskTool } from './TaskTool'
|
||||||
import { TodoWriteTool } from './TodoWriteTool'
|
import { TodoWriteTool } from './TodoWriteTool'
|
||||||
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
import { AgentToolsType, ToolInput, ToolOutput } from './types'
|
||||||
@ -24,6 +25,7 @@ import { UnknownToolRenderer } from './UnknownToolRenderer'
|
|||||||
import { WebFetchTool } from './WebFetchTool'
|
import { WebFetchTool } from './WebFetchTool'
|
||||||
import { WebSearchTool } from './WebSearchTool'
|
import { WebSearchTool } from './WebSearchTool'
|
||||||
import { WriteTool } from './WriteTool'
|
import { WriteTool } from './WriteTool'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MessageAgentTools')
|
const logger = loggerService.withContext('MessageAgentTools')
|
||||||
|
|
||||||
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
// 创建工具渲染器映射,这样就实现了完全的类型安全
|
||||||
@ -42,7 +44,8 @@ export const toolRenderers = {
|
|||||||
[AgentToolsType.MultiEdit]: MultiEditTool,
|
[AgentToolsType.MultiEdit]: MultiEditTool,
|
||||||
[AgentToolsType.BashOutput]: BashOutputTool,
|
[AgentToolsType.BashOutput]: BashOutputTool,
|
||||||
[AgentToolsType.NotebookEdit]: NotebookEditTool,
|
[AgentToolsType.NotebookEdit]: NotebookEditTool,
|
||||||
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool
|
[AgentToolsType.ExitPlanMode]: ExitPlanModeTool,
|
||||||
|
[AgentToolsType.Skill]: SkillTool
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// 类型守卫函数
|
// 类型守卫函数
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export enum AgentToolsType {
|
export enum AgentToolsType {
|
||||||
|
Skill = 'Skill',
|
||||||
Read = 'Read',
|
Read = 'Read',
|
||||||
Task = 'Task',
|
Task = 'Task',
|
||||||
Bash = 'Bash',
|
Bash = 'Bash',
|
||||||
@ -22,6 +23,15 @@ export type TextOutput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read 工具的类型定义
|
// Read 工具的类型定义
|
||||||
|
export interface SkillToolInput {
|
||||||
|
/**
|
||||||
|
* The skill to use
|
||||||
|
*/
|
||||||
|
command: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SkillToolOutput = string
|
||||||
|
|
||||||
export interface ReadToolInput {
|
export interface ReadToolInput {
|
||||||
/**
|
/**
|
||||||
* The absolute path to the file to read
|
* The absolute path to the file to read
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NormalToolResponse } from '@renderer/types'
|
|||||||
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
import type { ToolMessageBlock } from '@renderer/types/newMessage'
|
||||||
|
|
||||||
import { MessageAgentTools } from './MessageAgentTools'
|
import { MessageAgentTools } from './MessageAgentTools'
|
||||||
|
import { AgentToolsType } from './MessageAgentTools/types'
|
||||||
import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch'
|
import { MessageKnowledgeSearchToolTitle } from './MessageKnowledgeSearch'
|
||||||
import { MessageMemorySearchToolTitle } from './MessageMemorySearch'
|
import { MessageMemorySearchToolTitle } from './MessageMemorySearch'
|
||||||
import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
||||||
@ -9,27 +10,12 @@ import { MessageWebSearchToolTitle } from './MessageWebSearch'
|
|||||||
interface Props {
|
interface Props {
|
||||||
block: ToolMessageBlock
|
block: ToolMessageBlock
|
||||||
}
|
}
|
||||||
const prefix = 'builtin_'
|
const builtinToolsPrefix = 'builtin_'
|
||||||
const agentPrefix = 'mcp__'
|
const agentMcpToolsPrefix = 'mcp__'
|
||||||
const agentTools = [
|
const agentTools = Object.values(AgentToolsType)
|
||||||
'Read',
|
|
||||||
'Task',
|
const isAgentTool = (toolName: AgentToolsType) => {
|
||||||
'Bash',
|
if (agentTools.includes(toolName) || toolName.startsWith(agentMcpToolsPrefix)) {
|
||||||
'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 true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -38,8 +24,8 @@ const isAgentTool = (toolName: string) => {
|
|||||||
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null => {
|
||||||
let toolName = toolResponse.tool.name
|
let toolName = toolResponse.tool.name
|
||||||
const toolType = toolResponse.tool.type
|
const toolType = toolResponse.tool.type
|
||||||
if (toolName.startsWith(prefix)) {
|
if (toolName.startsWith(builtinToolsPrefix)) {
|
||||||
toolName = toolName.slice(prefix.length)
|
toolName = toolName.slice(builtinToolsPrefix.length)
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'web_search':
|
case 'web_search':
|
||||||
case 'web_search_preview':
|
case 'web_search_preview':
|
||||||
@ -51,7 +37,7 @@ const ChooseTool = (toolResponse: NormalToolResponse): React.ReactNode | null =>
|
|||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
} else if (isAgentTool(toolName)) {
|
} else if (isAgentTool(toolName as AgentToolsType)) {
|
||||||
return <MessageAgentTools toolResponse={toolResponse} />
|
return <MessageAgentTools toolResponse={toolResponse} />
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user