mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +08:00
✨ feat(hub): improve prompts and tool descriptions for better LLM success rate
- Rewrite HUB_MODE_SYSTEM_PROMPT_BASE with Critical Rules section - Add Common Mistakes to Avoid section with examples - Update exec tool description with IMPORTANT return requirement - Improve search tool description clarity - Simplify generator output with return reminder in header - Add per-field @param JSDoc with required/optional markers Fixes issue where LLMs forgot to return values from exec code
This commit is contained in:
parent
0022087cc0
commit
087f0ecfb1
@ -106,7 +106,7 @@ describe('generator', () => {
|
||||
|
||||
const result = generateToolsCode(tools)
|
||||
|
||||
expect(result).toContain('Found 2 tool(s)')
|
||||
expect(result).toContain('2 tool(s)')
|
||||
expect(result).toContain('async function server1_tool1')
|
||||
expect(result).toContain('async function server2_tool2')
|
||||
})
|
||||
|
||||
@ -3,6 +3,56 @@ import type { MCPTool } from '@types'
|
||||
|
||||
import type { GeneratedTool } from './types'
|
||||
|
||||
type PropertySchema = Record<string, unknown>
|
||||
type InputSchema = {
|
||||
type?: string
|
||||
properties?: Record<string, PropertySchema>
|
||||
required?: string[]
|
||||
}
|
||||
|
||||
function schemaTypeToTS(prop: Record<string, unknown>): string {
|
||||
const type = prop.type as string | string[] | undefined
|
||||
const enumValues = prop.enum as unknown[] | undefined
|
||||
|
||||
if (enumValues && Array.isArray(enumValues)) {
|
||||
return enumValues.map((v) => (typeof v === 'string' ? `"${v}"` : String(v))).join(' | ')
|
||||
}
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
return type.map((t) => primitiveTypeToTS(t)).join(' | ')
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
const items = prop.items as Record<string, unknown> | undefined
|
||||
if (items) {
|
||||
return `${schemaTypeToTS(items)}[]`
|
||||
}
|
||||
return 'unknown[]'
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
return 'object'
|
||||
}
|
||||
|
||||
return primitiveTypeToTS(type)
|
||||
}
|
||||
|
||||
function primitiveTypeToTS(type: string | undefined): string {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'string'
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 'number'
|
||||
case 'boolean':
|
||||
return 'boolean'
|
||||
case 'null':
|
||||
return 'null'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function jsonSchemaToSignature(schema: Record<string, unknown> | undefined): string {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return '{}'
|
||||
@ -25,65 +75,28 @@ function jsonSchemaToSignature(schema: Record<string, unknown> | undefined): str
|
||||
return `{ ${parts.join(', ')} }`
|
||||
}
|
||||
|
||||
function schemaTypeToTS(prop: Record<string, unknown>): string {
|
||||
const type = prop.type as string | string[] | undefined
|
||||
const enumValues = prop.enum as unknown[] | undefined
|
||||
|
||||
if (enumValues && Array.isArray(enumValues)) {
|
||||
return enumValues.map((v) => (typeof v === 'string' ? `"${v}"` : String(v))).join(' | ')
|
||||
}
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
return type.map((t) => primitiveTypeToTS(t)).join(' | ')
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
const items = prop.items as Record<string, unknown> | undefined
|
||||
if (items) {
|
||||
return `Array<${schemaTypeToTS(items)}>`
|
||||
}
|
||||
return 'Array<unknown>'
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
const properties = prop.properties as Record<string, Record<string, unknown>> | undefined
|
||||
if (properties) {
|
||||
return jsonSchemaToSignature(prop)
|
||||
}
|
||||
return 'object'
|
||||
}
|
||||
|
||||
return primitiveTypeToTS(type)
|
||||
}
|
||||
|
||||
function primitiveTypeToTS(type: string | undefined): string {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'string'
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 'number'
|
||||
case 'boolean':
|
||||
return 'boolean'
|
||||
case 'null':
|
||||
return 'null'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function generateJSDoc(tool: MCPTool, signature: string, returns: string): string {
|
||||
function generateJSDoc(tool: MCPTool, inputSchema: InputSchema | undefined, returns: string): string {
|
||||
const lines: string[] = ['/**']
|
||||
|
||||
if (tool.description) {
|
||||
const descLines = tool.description.split('\n')
|
||||
for (const line of descLines) {
|
||||
lines.push(` * ${line}`)
|
||||
const desc = tool.description.split('\n')[0].slice(0, 100)
|
||||
lines.push(` * ${desc}`)
|
||||
}
|
||||
|
||||
const properties = inputSchema?.properties || {}
|
||||
const required = inputSchema?.required || []
|
||||
|
||||
if (Object.keys(properties).length > 0) {
|
||||
lines.push(` * @param {Object} params`)
|
||||
for (const [name, prop] of Object.entries(properties)) {
|
||||
const isReq = required.includes(name)
|
||||
const type = schemaTypeToTS(prop)
|
||||
const paramName = isReq ? `params.${name}` : `[params.${name}]`
|
||||
const desc = (prop.description as string)?.split('\n')[0]?.slice(0, 60) || ''
|
||||
lines.push(` * @param {${type}} ${paramName} ${desc}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(` *`)
|
||||
lines.push(` * @param {${signature}} params`)
|
||||
lines.push(` * @returns {Promise<${returns}>}`)
|
||||
lines.push(` */`)
|
||||
|
||||
@ -97,13 +110,13 @@ export function generateToolFunction(
|
||||
): GeneratedTool {
|
||||
const functionName = generateMcpToolFunctionName(tool.serverName, tool.name, existingNames)
|
||||
|
||||
const inputSchema = tool.inputSchema as Record<string, unknown> | undefined
|
||||
const inputSchema = tool.inputSchema as InputSchema | undefined
|
||||
const outputSchema = tool.outputSchema as Record<string, unknown> | undefined
|
||||
|
||||
const signature = jsonSchemaToSignature(inputSchema)
|
||||
const returns = outputSchema ? jsonSchemaToSignature(outputSchema) : 'unknown'
|
||||
|
||||
const jsDoc = generateJSDoc(tool, signature, returns)
|
||||
const jsDoc = generateJSDoc(tool, inputSchema, returns)
|
||||
|
||||
const jsCode = `${jsDoc}
|
||||
async function ${functionName}(params) {
|
||||
@ -132,8 +145,8 @@ export function generateToolsCode(tools: GeneratedTool[]): string {
|
||||
return '// No tools available'
|
||||
}
|
||||
|
||||
const header = `// Found ${tools.length} tool(s):\n`
|
||||
const header = `// ${tools.length} tool(s). ALWAYS use: const r = await ToolName({...}); return r;`
|
||||
const code = tools.map((t) => t.jsCode).join('\n\n')
|
||||
|
||||
return header + '\n' + code
|
||||
return header + '\n\n' + code
|
||||
}
|
||||
|
||||
@ -54,14 +54,14 @@ export class HubServer {
|
||||
{
|
||||
name: 'search',
|
||||
description:
|
||||
'Search for available MCP tools by keywords. Returns JavaScript function declarations with JSDoc that can be used in the exec tool.',
|
||||
'Search for available MCP tools by keywords. Use this FIRST to discover tools. Returns JavaScript async function declarations with JSDoc showing exact function names, parameters, and return types for use in `exec`.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Search keywords, comma-separated for OR matching. Example: "chrome,browser" matches tools with "chrome" OR "browser"'
|
||||
'Comma-separated search keywords. A tool matches if ANY keyword appears in its name, description, or server name. Example: "chrome,browser,tab" matches tools related to Chrome OR browser OR tabs.'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
@ -74,14 +74,14 @@ export class HubServer {
|
||||
{
|
||||
name: 'exec',
|
||||
description:
|
||||
'Execute JavaScript code that calls MCP tools. Use the search tool first to discover available tools and their signatures.',
|
||||
'Execute JavaScript that calls MCP tools discovered via `search`. IMPORTANT: You MUST explicitly `return` the final value, or the result will be `undefined`.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description:
|
||||
'JavaScript code to execute. Runs inside an async function. Can use async/await. Available helpers: parallel(...promises), settle(...promises). Use return (or a final expression) to produce output; otherwise result is undefined.'
|
||||
'JavaScript code wrapped as `(async () => { your code })()`. All discovered tools are async functions (call as `await ToolName(params)`). Helpers: `parallel(...promises)`, `settle(...promises)`, `console.*`. You MUST `return` the final value. Examples: `const r = await Tool({ id: "1" }); return r` or `return await Tool({ x: 1 })`'
|
||||
}
|
||||
},
|
||||
required: ['code']
|
||||
|
||||
@ -471,64 +471,125 @@ If have multiple citations, please directly list them like this:
|
||||
`
|
||||
|
||||
const HUB_MODE_SYSTEM_PROMPT_BASE = `
|
||||
## MCP Tools (Code Mode)
|
||||
## Hub MCP Tools – Code Execution Mode
|
||||
|
||||
You have access to MCP tools via the hub server.
|
||||
You can discover and call MCP tools through the hub server using two meta-tools: **search** and **exec**.
|
||||
|
||||
### Critical Rules (Read First)
|
||||
|
||||
1. You MUST explicitly \`return\` the final value from your \`exec\` code. If you do not return a value, the result will be \`undefined\`.
|
||||
2. All MCP tools are async functions. Always call them as \`await ToolName(params)\`.
|
||||
3. Use the exact function names and parameter shapes returned by \`search\`.
|
||||
4. You CANNOT call \`search\` or \`exec\` from inside \`exec\` code—use them only as MCP tools.
|
||||
5. \`console.log\` output is NOT the result. Logs are separate; the final answer must come from \`return\`.
|
||||
|
||||
### Workflow
|
||||
1. Call \`search\` with relevant keywords to discover tools
|
||||
2. Review the returned function signatures and their parameters
|
||||
3. Call \`exec\` with JavaScript code using those functions (you can chain multiple MCP tools in one \`exec\`)
|
||||
4. The last expression (or an explicit \`return\`) becomes the result. If you don't return anything, the result is \`undefined\`.
|
||||
|
||||
### Example Usage
|
||||
1. Call \`search\` with relevant keywords to discover tools.
|
||||
2. Read the returned JavaScript function declarations and JSDoc to understand names and parameters.
|
||||
3. Call \`exec\` with JavaScript code that uses the discovered tools and ends with an explicit \`return\`.
|
||||
4. Use the \`exec\` result as your answer.
|
||||
|
||||
**Step 1: Search for tools**
|
||||
\`\`\`
|
||||
search({ query: "github,repository" })
|
||||
\`\`\`
|
||||
### What \`search\` Does
|
||||
|
||||
- Input: keyword string (comma-separated for OR-matching), plus optional \`limit\`.
|
||||
- Output: JavaScript async function declarations with JSDoc showing exact function names, parameters, and return types.
|
||||
|
||||
### What \`exec\` Does
|
||||
|
||||
- Runs JavaScript code in an isolated async context (wrapped as \`(async () => { your code })())\`.
|
||||
- All discovered tools are exposed as async functions: \`await ToolName(params)\`.
|
||||
- Available helpers:
|
||||
- \`parallel(...promises)\` → \`Promise.all(promises)\`
|
||||
- \`settle(...promises)\` → \`Promise.allSettled(promises)\`
|
||||
- \`console.log/info/warn/error/debug\`
|
||||
- Returns JSON with: \`result\` (your returned value), \`logs\` (optional), \`error\` (optional), \`isError\` (optional).
|
||||
|
||||
### Example: Single Tool Call
|
||||
|
||||
**Step 2: Use discovered tools**
|
||||
\`\`\`javascript
|
||||
exec({
|
||||
code: \`
|
||||
const repos = await searchRepos({ query: "react", limit: 5 })
|
||||
const details = await parallel(
|
||||
repos.map(r => getRepoDetails({ owner: r.owner, repo: r.name }))
|
||||
)
|
||||
return { repos, details }
|
||||
\`
|
||||
})
|
||||
// Step 1: search({ query: "browser,fetch" })
|
||||
// Step 2: exec with:
|
||||
const page = await CherryBrowser_fetch({ url: "https://example.com" })
|
||||
return page
|
||||
\`\`\`
|
||||
|
||||
### Example: Open a URL and Save to a File
|
||||
### Example: Multiple Tools with Parallel
|
||||
|
||||
**Step 1: Search for tools**
|
||||
\`\`\`
|
||||
search({ query: "browser,chrome,file,open,write" })
|
||||
\`\`\`
|
||||
|
||||
**Step 2: Use discovered tools in a single exec**
|
||||
\`\`\`javascript
|
||||
exec({
|
||||
code: \`
|
||||
// Replace tool names with the exact signatures returned by search.
|
||||
await CherryBrowser_open({ url: "https://sspai.com", timeout: 10000 })
|
||||
const page = await CherryBrowser_fetch({ url: "https://sspai.com" })
|
||||
await CherryFilesystem_write({ path: "./sspai.html", content: page })
|
||||
({ saved: true, path: "./sspai.html" })
|
||||
\`
|
||||
})
|
||||
const [forecast, time] = await parallel(
|
||||
Weather_getForecast({ city: "Paris" }),
|
||||
Time_getLocalTime({ city: "Paris" })
|
||||
)
|
||||
return { city: "Paris", forecast, time }
|
||||
\`\`\`
|
||||
|
||||
### Example: Handle Partial Failures with Settle
|
||||
|
||||
\`\`\`javascript
|
||||
const results = await settle(
|
||||
Weather_getForecast({ city: "Paris" }),
|
||||
Weather_getForecast({ city: "Tokyo" })
|
||||
)
|
||||
const successful = results.filter(r => r.status === "fulfilled").map(r => r.value)
|
||||
return { results, successful }
|
||||
\`\`\`
|
||||
|
||||
### Example: Error Handling
|
||||
|
||||
\`\`\`javascript
|
||||
try {
|
||||
const user = await User_lookup({ email: "user@example.com" })
|
||||
return { found: true, user }
|
||||
} catch (error) {
|
||||
return { found: false, error: String(error) }
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
❌ **Forgetting to return** (result will be \`undefined\`):
|
||||
\`\`\`javascript
|
||||
const data = await SomeTool({ id: "123" })
|
||||
// Missing return!
|
||||
\`\`\`
|
||||
|
||||
✅ **Always return**:
|
||||
\`\`\`javascript
|
||||
const data = await SomeTool({ id: "123" })
|
||||
return data
|
||||
\`\`\`
|
||||
|
||||
❌ **Only logging, not returning**:
|
||||
\`\`\`javascript
|
||||
const data = await SomeTool({ id: "123" })
|
||||
console.log(data) // Logs are NOT the result!
|
||||
\`\`\`
|
||||
|
||||
❌ **Missing await**:
|
||||
\`\`\`javascript
|
||||
const data = SomeTool({ id: "123" }) // Returns Promise, not value!
|
||||
return data
|
||||
\`\`\`
|
||||
|
||||
❌ **Awaiting before parallel**:
|
||||
\`\`\`javascript
|
||||
await parallel(await ToolA(), await ToolB()) // Wrong: runs sequentially
|
||||
\`\`\`
|
||||
|
||||
✅ **Pass promises directly to parallel**:
|
||||
\`\`\`javascript
|
||||
await parallel(ToolA(), ToolB()) // Correct: runs in parallel
|
||||
\`\`\`
|
||||
|
||||
### Best Practices
|
||||
- Always search first to discover available tools and their exact signatures
|
||||
- Use descriptive variable names in your code
|
||||
- Handle errors gracefully with try/catch when needed
|
||||
- Use \`parallel()\` for independent operations to improve performance
|
||||
- Prefer a single \`exec\` for multi-step flows to reduce round-trips
|
||||
- Return a value (or end with a final expression) so \`exec\` produces output
|
||||
- Use the exact tool names/signatures returned by \`search\`
|
||||
|
||||
- Always call \`search\` first to discover tools and confirm signatures.
|
||||
- Always use an explicit \`return\` at the end of \`exec\` code.
|
||||
- Use \`parallel\` for independent operations that can run at the same time.
|
||||
- Use \`settle\` when some calls may fail but you still want partial results.
|
||||
- Prefer a single \`exec\` call for multi-step flows.
|
||||
- Treat \`console.*\` as debugging only, never as the primary result.
|
||||
`
|
||||
|
||||
interface ToolInfo {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user