mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add query-context smart tool selection
- Add ToolSelectionAnalyzer class for intelligent tool filtering - Analyze query complexity (simple/moderate/complex) - Detect tool mentions and group needs - Score tools based on relevance (0-1 scale) - Select 6-12 most relevant tools per query - Add configuration settings for the feature - Include comprehensive unit tests Fixes #9117
This commit is contained in:
parent
e98f4b9057
commit
7e456cb22d
7 changed files with 765 additions and 2 deletions
|
|
@ -182,6 +182,11 @@ const baseProviderSettingsSchema = z.object({
|
|||
|
||||
// Model verbosity.
|
||||
verbosity: verbosityLevelsSchema.optional(),
|
||||
|
||||
// Smart tool selection.
|
||||
smartToolSelectionEnabled: z.boolean().optional(),
|
||||
smartToolSelectionMinTools: z.number().min(1).max(20).optional(),
|
||||
smartToolSelectionMaxTools: z.number().min(1).max(20).optional(),
|
||||
})
|
||||
|
||||
// Several of the providers share common model config properties.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ async function generatePrompt(
|
|||
settings?: SystemPromptSettings,
|
||||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
userQuery?: string,
|
||||
): Promise<string> {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -108,6 +109,7 @@ ${getToolDescriptionsForMode(
|
|||
settings,
|
||||
enableMcpServerCreation,
|
||||
modelId,
|
||||
userQuery,
|
||||
)}
|
||||
|
||||
${getToolUseGuidelinesSection(codeIndexManager)}
|
||||
|
|
@ -153,6 +155,7 @@ export const SYSTEM_PROMPT = async (
|
|||
settings?: SystemPromptSettings,
|
||||
todoList?: TodoItem[],
|
||||
modelId?: string,
|
||||
userQuery?: string,
|
||||
): Promise<string> => {
|
||||
if (!context) {
|
||||
throw new Error("Extension context is required for generating system prompt")
|
||||
|
|
@ -225,5 +228,6 @@ ${customInstructions}`
|
|||
settings,
|
||||
todoList,
|
||||
modelId,
|
||||
userQuery,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
308
src/core/prompts/tools/ToolSelectionAnalyzer.test.ts
Normal file
308
src/core/prompts/tools/ToolSelectionAnalyzer.test.ts
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { ToolSelectionAnalyzer, SmartToolSelectionConfig } from "./ToolSelectionAnalyzer"
|
||||
import type { ModeConfig, ToolName, ToolGroup } from "@roo-code/types"
|
||||
|
||||
describe("ToolSelectionAnalyzer", () => {
|
||||
let analyzer: ToolSelectionAnalyzer
|
||||
|
||||
beforeEach(() => {
|
||||
analyzer = new ToolSelectionAnalyzer()
|
||||
})
|
||||
|
||||
const createModeConfig = (groups: ToolGroup[]): ModeConfig => ({
|
||||
slug: "test",
|
||||
name: "Test Mode",
|
||||
roleDefinition: "Test role",
|
||||
groups: groups as any, // Groups can be string or tuple format
|
||||
})
|
||||
|
||||
const createAvailableTools = (tools: ToolName[]): Set<ToolName> => new Set(tools)
|
||||
|
||||
describe("query analysis", () => {
|
||||
it("should detect simple queries", () => {
|
||||
const simpleQueries = [
|
||||
"what does this function do?",
|
||||
"explain this code",
|
||||
"what is the purpose of this file?",
|
||||
"can you summarize this?",
|
||||
]
|
||||
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"list_code_definition_names",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
simpleQueries.forEach((query) => {
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBeGreaterThanOrEqual(6)
|
||||
expect(selected.length).toBeLessThanOrEqual(8)
|
||||
// Should include essential read tools
|
||||
expect(selected).toContain("read_file")
|
||||
expect(selected).toContain("list_files")
|
||||
})
|
||||
})
|
||||
|
||||
it("should detect complex queries", () => {
|
||||
const complexQueries = [
|
||||
"refactor the entire authentication system to use JWT tokens",
|
||||
"implement a comprehensive logging system across all modules",
|
||||
"redesign the database schema and migrate all existing data",
|
||||
"build a complete REST API with authentication and rate limiting",
|
||||
]
|
||||
|
||||
const modeConfig = createModeConfig(["read", "edit"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"execute_command",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
complexQueries.forEach((query) => {
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBeGreaterThanOrEqual(8)
|
||||
expect(selected.length).toBeLessThanOrEqual(12)
|
||||
// Should include both read and edit tools
|
||||
expect(selected).toContain("read_file")
|
||||
expect(selected).toContain("write_to_file")
|
||||
expect(selected).toContain("apply_diff")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool-specific queries", () => {
|
||||
it("should prioritize mentioned tools", () => {
|
||||
const query = "run npm test and show me the results"
|
||||
const modeConfig = createModeConfig(["read", "command"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"execute_command",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
// execute_command should be included and highly ranked (top 5) due to explicit mention
|
||||
expect(selected).toContain("execute_command")
|
||||
const executeIndex = selected.indexOf("execute_command")
|
||||
expect(executeIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(executeIndex).toBeLessThan(5) // Within top 5 is still highly ranked
|
||||
})
|
||||
|
||||
it("should detect file operations", () => {
|
||||
const query = "create a new config file with the settings I provided"
|
||||
const modeConfig = createModeConfig(["read", "edit"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
// write_to_file should be highly ranked
|
||||
expect(selected.slice(0, 3)).toContain("write_to_file")
|
||||
})
|
||||
|
||||
it("should detect search operations", () => {
|
||||
const query = "find all places where the API key is used"
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"list_code_definition_names",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
// search_files should be included and highly ranked (top 5) due to explicit search intent
|
||||
expect(selected).toContain("search_files")
|
||||
const searchIndex = selected.indexOf("search_files")
|
||||
expect(searchIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(searchIndex).toBeLessThan(5) // Within top 5 is still highly ranked
|
||||
})
|
||||
})
|
||||
|
||||
describe("settings configuration", () => {
|
||||
it("should respect minimum tools setting", () => {
|
||||
const query = "simple task"
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const customAnalyzer = new ToolSelectionAnalyzer({
|
||||
enabled: true,
|
||||
minTools: 3,
|
||||
maxTools: 12,
|
||||
})
|
||||
|
||||
const selected = customAnalyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
it("should respect maximum tools setting", () => {
|
||||
const query = "complex refactoring task involving multiple systems"
|
||||
const modeConfig = createModeConfig(["read", "edit", "command"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"insert_content",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"list_code_definition_names",
|
||||
"execute_command",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
"update_todo_list",
|
||||
])
|
||||
|
||||
const customAnalyzer = new ToolSelectionAnalyzer({
|
||||
enabled: true,
|
||||
minTools: 6,
|
||||
maxTools: 8,
|
||||
})
|
||||
|
||||
const selected = customAnalyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBeLessThanOrEqual(8)
|
||||
})
|
||||
|
||||
it("should return all tools when smart selection is disabled", () => {
|
||||
const query = "some task"
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const disabledAnalyzer = new ToolSelectionAnalyzer({
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const selected = disabledAnalyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBe(availableTools.size)
|
||||
expect(selected.sort()).toEqual(Array.from(availableTools).sort())
|
||||
})
|
||||
})
|
||||
|
||||
describe("essential tools", () => {
|
||||
it("should always include ask_followup_question and attempt_completion", () => {
|
||||
const queries = ["simple query", "complex refactoring", "create a file", "run tests"]
|
||||
|
||||
const modeConfig = createModeConfig(["read", "edit"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"list_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
queries.forEach((query) => {
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected).toContain("ask_followup_question")
|
||||
expect(selected).toContain("attempt_completion")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty or invalid inputs", () => {
|
||||
it("should handle empty query gracefully", () => {
|
||||
const query = ""
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"list_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected.length).toBeGreaterThan(0)
|
||||
expect(selected).toContain("ask_followup_question")
|
||||
expect(selected).toContain("attempt_completion")
|
||||
})
|
||||
|
||||
it("should handle empty available tools", () => {
|
||||
const query = "do something"
|
||||
const modeConfig = createModeConfig(["read"])
|
||||
const availableTools = createAvailableTools([])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
expect(selected).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool scoring", () => {
|
||||
it("should score tools based on query relevance", () => {
|
||||
const query = "write a test file for the authentication module"
|
||||
const modeConfig = createModeConfig(["read", "edit"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"execute_command",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
|
||||
// write_to_file should be highly ranked for "write a test file"
|
||||
const writeIndex = selected.indexOf("write_to_file")
|
||||
expect(writeIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(writeIndex).toBeLessThan(4) // Should be in top 4 tools
|
||||
|
||||
// read_file should also be included for context
|
||||
expect(selected).toContain("read_file")
|
||||
})
|
||||
|
||||
it("should handle multiple tool mentions in query", () => {
|
||||
const query = "read the config file, update it, and run the build command"
|
||||
const modeConfig = createModeConfig(["read", "edit", "command"])
|
||||
const availableTools = createAvailableTools([
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"apply_diff",
|
||||
"execute_command",
|
||||
"list_files",
|
||||
"search_files",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
])
|
||||
|
||||
const selected = analyzer.selectTools(query, modeConfig, availableTools)
|
||||
|
||||
// Should include all mentioned operations
|
||||
expect(selected).toContain("read_file")
|
||||
expect(selected).toContain("apply_diff") // for "update"
|
||||
expect(selected).toContain("execute_command") // for "run"
|
||||
})
|
||||
})
|
||||
})
|
||||
394
src/core/prompts/tools/ToolSelectionAnalyzer.ts
Normal file
394
src/core/prompts/tools/ToolSelectionAnalyzer.ts
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
import type { ToolName, ToolGroup, ModeConfig } from "@roo-code/types"
|
||||
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tools"
|
||||
import { getGroupName } from "../../../shared/modes"
|
||||
|
||||
/**
|
||||
* Configuration for smart tool selection
|
||||
*/
|
||||
export interface SmartToolSelectionConfig {
|
||||
enabled?: boolean
|
||||
minTools?: number
|
||||
maxTools?: number
|
||||
defaultComplexityThreshold?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of query analysis
|
||||
*/
|
||||
interface QueryAnalysis {
|
||||
complexity: "simple" | "moderate" | "complex"
|
||||
mentionedTools: Set<ToolName>
|
||||
mentionedGroups: Set<ToolGroup>
|
||||
isReadOnly: boolean
|
||||
needsEditing: boolean
|
||||
needsCommandExecution: boolean
|
||||
needsBrowser: boolean
|
||||
needsMcp: boolean
|
||||
confidence: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool relevance score
|
||||
*/
|
||||
interface ToolScore {
|
||||
tool: ToolName
|
||||
score: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Patterns for detecting tool mentions in queries
|
||||
*/
|
||||
const TOOL_MENTION_PATTERNS: Record<string, { groups: ToolGroup[]; tools?: ToolName[]; keywords: RegExp }> = {
|
||||
mcp: {
|
||||
groups: ["mcp"],
|
||||
keywords: /\b(mcp|server|github\s+mcp|database\s+mcp|use\s+mcp)\b/i,
|
||||
},
|
||||
browser: {
|
||||
groups: ["browser"],
|
||||
keywords:
|
||||
/\b(browser|website|web\s+page|localhost|chrome|firefox|test\s+in\s+browser|check\s+the\s+(website|browser))\b/i,
|
||||
},
|
||||
command: {
|
||||
groups: ["command"],
|
||||
keywords: /\b(run|execute|npm|yarn|pnpm|test|install|dependencies|cli|command|terminal|shell)\b/i,
|
||||
},
|
||||
edit: {
|
||||
groups: ["edit"],
|
||||
keywords: /\b(fix|refactor|modify|change|update|write|create|add|remove|delete|implement|code)\b/i,
|
||||
},
|
||||
read: {
|
||||
groups: ["read"],
|
||||
keywords: /\b(explain|what\s+does|understand|analyze|show|describe|look\s+at|read|view|check)\b/i,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Complexity indicators in queries
|
||||
*/
|
||||
const COMPLEXITY_INDICATORS = {
|
||||
simple: [
|
||||
/^(what|how|why|when|where|who)\s+/i,
|
||||
/\b(explain|describe|show|tell\s+me)\b/i,
|
||||
/\b(typo|spelling|rename)\b/i,
|
||||
/\b(single|one|this)\s+\w+/i,
|
||||
],
|
||||
complex: [
|
||||
/\b(entire|whole|all|complete|full|system|architecture|refactor)\b/i,
|
||||
/\b(multiple|several|many|various)\b/i,
|
||||
/\b(and|also|then|after|before|while)\b.*\b(and|also|then|after|before|while)\b/i, // Multiple conjunctions
|
||||
/\b(implement|create|build|design|develop)\s+\w+\s+(system|module|feature)/i,
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzer for smart tool selection based on query context
|
||||
*/
|
||||
export class ToolSelectionAnalyzer {
|
||||
private config: SmartToolSelectionConfig
|
||||
private recentMessages: string[] = []
|
||||
private maxMessageHistory = 5
|
||||
|
||||
constructor(config: SmartToolSelectionConfig = {}) {
|
||||
this.config = {
|
||||
enabled: config.enabled ?? true,
|
||||
minTools: config.minTools ?? 6,
|
||||
maxTools: config.maxTools ?? 12,
|
||||
defaultComplexityThreshold: config.defaultComplexityThreshold ?? 0.7,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the conversation history
|
||||
*/
|
||||
public addMessage(message: string): void {
|
||||
this.recentMessages.push(message)
|
||||
if (this.recentMessages.length > this.maxMessageHistory) {
|
||||
this.recentMessages.shift()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the conversation history
|
||||
*/
|
||||
public clearHistory(): void {
|
||||
this.recentMessages = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a query to determine its characteristics
|
||||
*/
|
||||
private analyzeQuery(query: string): QueryAnalysis {
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const analysis: QueryAnalysis = {
|
||||
complexity: "moderate",
|
||||
mentionedTools: new Set(),
|
||||
mentionedGroups: new Set(),
|
||||
isReadOnly: false,
|
||||
needsEditing: false,
|
||||
needsCommandExecution: false,
|
||||
needsBrowser: false,
|
||||
needsMcp: false,
|
||||
confidence: 0.5,
|
||||
}
|
||||
|
||||
// Check for explicit tool/group mentions
|
||||
for (const [key, pattern] of Object.entries(TOOL_MENTION_PATTERNS)) {
|
||||
if (pattern.keywords.test(query)) {
|
||||
pattern.groups.forEach((group) => analysis.mentionedGroups.add(group))
|
||||
if (pattern.tools) {
|
||||
pattern.tools.forEach((tool) => analysis.mentionedTools.add(tool))
|
||||
}
|
||||
|
||||
// Set specific needs based on mentions
|
||||
switch (key) {
|
||||
case "mcp":
|
||||
analysis.needsMcp = true
|
||||
break
|
||||
case "browser":
|
||||
analysis.needsBrowser = true
|
||||
break
|
||||
case "command":
|
||||
analysis.needsCommandExecution = true
|
||||
break
|
||||
case "edit":
|
||||
analysis.needsEditing = true
|
||||
break
|
||||
case "read":
|
||||
analysis.isReadOnly = !analysis.needsEditing // Only read-only if not also editing
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine complexity
|
||||
const simpleCount = COMPLEXITY_INDICATORS.simple.filter((pattern) => pattern.test(query)).length
|
||||
const complexCount = COMPLEXITY_INDICATORS.complex.filter((pattern) => pattern.test(query)).length
|
||||
|
||||
if (complexCount > 1 || query.length > 500) {
|
||||
analysis.complexity = "complex"
|
||||
analysis.confidence = Math.min(0.9, 0.6 + complexCount * 0.1)
|
||||
} else if (simpleCount > 1 && complexCount === 0 && query.length < 100) {
|
||||
analysis.complexity = "simple"
|
||||
analysis.confidence = Math.min(0.9, 0.6 + simpleCount * 0.1)
|
||||
} else {
|
||||
analysis.complexity = "moderate"
|
||||
analysis.confidence = 0.7
|
||||
}
|
||||
|
||||
// If no editing keywords found but query seems to be about making changes
|
||||
if (!analysis.needsEditing && /\b(fix|update|change|modify|add|remove)\b/i.test(query)) {
|
||||
analysis.needsEditing = true
|
||||
analysis.isReadOnly = false
|
||||
}
|
||||
|
||||
// Adjust based on conversation history
|
||||
if (this.recentMessages.length > 0) {
|
||||
const recentContext = this.recentMessages.join(" ").toLowerCase()
|
||||
if (/\b(debug|error|issue|problem|bug)\b/i.test(recentContext)) {
|
||||
// In debugging context, likely need read and command tools
|
||||
analysis.needsCommandExecution = true
|
||||
analysis.confidence = Math.min(0.95, analysis.confidence + 0.1)
|
||||
}
|
||||
if (/\b(implement|create|build|write)\b/i.test(recentContext)) {
|
||||
// In implementation context, likely need editing tools
|
||||
analysis.needsEditing = true
|
||||
analysis.isReadOnly = false
|
||||
}
|
||||
}
|
||||
|
||||
return analysis
|
||||
}
|
||||
|
||||
/**
|
||||
* Score tools based on query analysis
|
||||
*/
|
||||
private scoreTools(availableTools: Set<ToolName>, analysis: QueryAnalysis, modeConfig: ModeConfig): ToolScore[] {
|
||||
const scores: ToolScore[] = []
|
||||
|
||||
for (const tool of availableTools) {
|
||||
let score = 0
|
||||
let reason = ""
|
||||
|
||||
// Always include essential tools with high score
|
||||
if (ALWAYS_AVAILABLE_TOOLS.includes(tool)) {
|
||||
score = 0.9
|
||||
reason = "essential tool"
|
||||
}
|
||||
// Explicitly mentioned tools get highest score
|
||||
else if (analysis.mentionedTools.has(tool)) {
|
||||
score = 1.0
|
||||
reason = "explicitly mentioned"
|
||||
}
|
||||
// Tools in mentioned groups get high score
|
||||
else {
|
||||
// Find which group this tool belongs to
|
||||
let toolGroup: ToolGroup | undefined
|
||||
for (const [groupName, groupConfig] of Object.entries(TOOL_GROUPS)) {
|
||||
if (groupConfig.tools.includes(tool)) {
|
||||
toolGroup = groupName as ToolGroup
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (toolGroup && analysis.mentionedGroups.has(toolGroup)) {
|
||||
score = 0.85
|
||||
reason = `part of mentioned ${toolGroup} group`
|
||||
} else {
|
||||
// Score based on query characteristics
|
||||
switch (tool) {
|
||||
case "read_file":
|
||||
case "list_files":
|
||||
case "search_files":
|
||||
case "list_code_definition_names":
|
||||
score = analysis.isReadOnly ? 0.8 : 0.6
|
||||
reason = analysis.isReadOnly ? "read-only query" : "may need to read files"
|
||||
break
|
||||
|
||||
case "write_to_file":
|
||||
case "apply_diff":
|
||||
case "insert_content":
|
||||
score = analysis.needsEditing ? 0.8 : analysis.isReadOnly ? 0.1 : 0.3
|
||||
reason = analysis.needsEditing ? "editing needed" : "editing tool"
|
||||
break
|
||||
|
||||
case "execute_command":
|
||||
score = analysis.needsCommandExecution ? 0.85 : 0.2
|
||||
reason = analysis.needsCommandExecution ? "command execution needed" : "command tool"
|
||||
break
|
||||
|
||||
case "browser_action":
|
||||
score = analysis.needsBrowser ? 0.9 : 0.1
|
||||
reason = analysis.needsBrowser ? "browser interaction needed" : "browser tool"
|
||||
break
|
||||
|
||||
case "use_mcp_tool":
|
||||
case "access_mcp_resource":
|
||||
score = analysis.needsMcp ? 0.9 : 0.1
|
||||
reason = analysis.needsMcp ? "MCP needed" : "MCP tool"
|
||||
break
|
||||
|
||||
case "codebase_search":
|
||||
score = analysis.complexity === "complex" ? 0.7 : 0.4
|
||||
reason = "codebase search"
|
||||
break
|
||||
|
||||
case "fetch_instructions":
|
||||
score = 0.3 // Lower priority unless specifically needed
|
||||
reason = "instruction fetcher"
|
||||
break
|
||||
|
||||
default:
|
||||
score = 0.5
|
||||
reason = "general tool"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust score based on complexity
|
||||
if (score > 0.1 && score < 0.9) {
|
||||
if (analysis.complexity === "simple") {
|
||||
score *= 0.8 // Reduce score for non-essential tools in simple queries
|
||||
} else if (analysis.complexity === "complex") {
|
||||
score *= 1.1 // Boost score for complex queries
|
||||
}
|
||||
score = Math.min(1.0, score)
|
||||
}
|
||||
|
||||
scores.push({ tool, score, reason })
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
|
||||
/**
|
||||
* Select tools based on query context
|
||||
*/
|
||||
public selectTools(
|
||||
query: string,
|
||||
modeConfig: ModeConfig,
|
||||
availableTools: Set<ToolName>,
|
||||
customModes?: ModeConfig[],
|
||||
): ToolName[] {
|
||||
// If feature is disabled, return all available tools
|
||||
if (!this.config.enabled) {
|
||||
return Array.from(availableTools)
|
||||
}
|
||||
|
||||
// Add query to history
|
||||
this.addMessage(query)
|
||||
|
||||
// Analyze the query
|
||||
const analysis = this.analyzeQuery(query)
|
||||
|
||||
// Score all available tools
|
||||
const scores = this.scoreTools(availableTools, analysis, modeConfig)
|
||||
|
||||
// Sort by score (descending)
|
||||
scores.sort((a, b) => b.score - a.score)
|
||||
|
||||
// Determine how many tools to include
|
||||
let targetCount = this.config.minTools!
|
||||
if (analysis.complexity === "simple") {
|
||||
targetCount = this.config.minTools!
|
||||
} else if (analysis.complexity === "moderate") {
|
||||
targetCount = Math.floor((this.config.minTools! + this.config.maxTools!) / 2)
|
||||
} else {
|
||||
targetCount = this.config.maxTools!
|
||||
}
|
||||
|
||||
// Always include tools with score >= threshold
|
||||
const threshold = this.config.defaultComplexityThreshold!
|
||||
const selectedTools = new Set<ToolName>()
|
||||
const highScoreTools = scores.filter((s) => s.score >= threshold)
|
||||
|
||||
for (const { tool } of highScoreTools) {
|
||||
selectedTools.add(tool)
|
||||
}
|
||||
|
||||
// If we have fewer tools than minimum, add more based on score
|
||||
let i = highScoreTools.length
|
||||
while (selectedTools.size < targetCount && i < scores.length) {
|
||||
selectedTools.add(scores[i].tool)
|
||||
i++
|
||||
}
|
||||
|
||||
// Ensure we always have essential tools
|
||||
for (const tool of ALWAYS_AVAILABLE_TOOLS) {
|
||||
if (availableTools.has(tool)) {
|
||||
selectedTools.add(tool)
|
||||
}
|
||||
}
|
||||
|
||||
// Log selection for debugging (in development)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log("Smart Tool Selection:", {
|
||||
query: query.substring(0, 100),
|
||||
analysis,
|
||||
selectedCount: selectedTools.size,
|
||||
totalAvailable: availableTools.size,
|
||||
topScores: scores
|
||||
.slice(0, 10)
|
||||
.map((s) => ({ tool: s.tool, score: s.score.toFixed(2), reason: s.reason })),
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(selectedTools)
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let analyzerInstance: ToolSelectionAnalyzer | undefined
|
||||
|
||||
/**
|
||||
* Get or create the singleton ToolSelectionAnalyzer instance
|
||||
*/
|
||||
export function getToolSelectionAnalyzer(config?: SmartToolSelectionConfig): ToolSelectionAnalyzer {
|
||||
if (!analyzerInstance) {
|
||||
analyzerInstance = new ToolSelectionAnalyzer(config)
|
||||
} else if (config) {
|
||||
// Update config if provided
|
||||
analyzerInstance = new ToolSelectionAnalyzer(config)
|
||||
}
|
||||
return analyzerInstance
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { McpHub } from "../../../services/mcp/McpHub"
|
|||
import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
|
||||
|
||||
import { ToolArgs } from "./types"
|
||||
import { getToolSelectionAnalyzer, type SmartToolSelectionConfig } from "./ToolSelectionAnalyzer"
|
||||
import { getExecuteCommandDescription } from "./execute-command"
|
||||
import { getReadFileDescription } from "./read-file"
|
||||
import { getSimpleReadFileDescription } from "./simple-read-file"
|
||||
|
|
@ -74,6 +75,7 @@ export function getToolDescriptionsForMode(
|
|||
settings?: Record<string, any>,
|
||||
enableMcpServerCreation?: boolean,
|
||||
modelId?: string,
|
||||
userQuery?: string,
|
||||
): string {
|
||||
const config = getModeConfig(mode, customModes)
|
||||
const args: ToolArgs = {
|
||||
|
|
@ -141,8 +143,24 @@ export function getToolDescriptionsForMode(
|
|||
tools.delete("run_slash_command")
|
||||
}
|
||||
|
||||
// Map tool descriptions for allowed tools
|
||||
const descriptions = Array.from(tools).map((toolName) => {
|
||||
// Apply smart tool selection if enabled and query is provided
|
||||
let selectedTools = Array.from(tools)
|
||||
|
||||
if (userQuery && settings?.smartToolSelection !== false) {
|
||||
const smartSelectionConfig: SmartToolSelectionConfig = {
|
||||
enabled: settings?.smartToolSelection?.enabled ?? true,
|
||||
minTools: settings?.smartToolSelection?.minTools ?? 6,
|
||||
maxTools: settings?.smartToolSelection?.maxTools ?? 12,
|
||||
defaultComplexityThreshold: settings?.smartToolSelection?.threshold ?? 0.7,
|
||||
}
|
||||
|
||||
const analyzer = getToolSelectionAnalyzer(smartSelectionConfig)
|
||||
const modeConfig = getModeConfig(mode, customModes)
|
||||
selectedTools = analyzer.selectTools(userQuery, modeConfig, tools as Set<ToolName>, customModes)
|
||||
}
|
||||
|
||||
// Map tool descriptions for selected tools
|
||||
const descriptions = selectedTools.map((toolName) => {
|
||||
const descriptionFn = toolDescriptionMap[toolName]
|
||||
if (!descriptionFn) {
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ export interface SystemPromptSettings {
|
|||
todoListEnabled: boolean
|
||||
useAgentRules: boolean
|
||||
newTaskRequireTodos: boolean
|
||||
// Smart tool selection settings
|
||||
smartToolSelectionEnabled?: boolean
|
||||
smartToolSelectionMinTools?: number
|
||||
smartToolSelectionMaxTools?: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2553,6 +2553,31 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
apiConfiguration,
|
||||
} = state ?? {}
|
||||
|
||||
// Extract the most recent user query from the conversation history
|
||||
let userQuery: string | undefined
|
||||
if (this.apiConversationHistory.length > 0) {
|
||||
// Find the last user message in the conversation
|
||||
const lastUserMessage = [...this.apiConversationHistory].reverse().find((msg) => msg.role === "user")
|
||||
|
||||
if (lastUserMessage && Array.isArray(lastUserMessage.content)) {
|
||||
// Extract text content from the user message
|
||||
const textContents = lastUserMessage.content
|
||||
.filter((block) => block.type === "text")
|
||||
.map((block) => (block as any).text || "")
|
||||
|
||||
// Combine all text content to form the query
|
||||
if (textContents.length > 0) {
|
||||
userQuery = textContents.join(" ").trim()
|
||||
// Remove task tags if present
|
||||
userQuery = userQuery.replace(/<\/?task>/g, "").trim()
|
||||
// Limit length to prevent overly long queries
|
||||
if (userQuery.length > 500) {
|
||||
userQuery = userQuery.substring(0, 500) + "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await (async () => {
|
||||
const provider = this.providerRef.deref()
|
||||
|
||||
|
|
@ -2595,9 +2620,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
newTaskRequireTodos: vscode.workspace
|
||||
.getConfiguration("roo-cline")
|
||||
.get<boolean>("newTaskRequireTodos", false),
|
||||
// Smart tool selection configuration
|
||||
smartToolSelectionEnabled: apiConfiguration?.smartToolSelectionEnabled ?? true,
|
||||
smartToolSelectionMinTools: apiConfiguration?.smartToolSelectionMinTools ?? 6,
|
||||
smartToolSelectionMaxTools: apiConfiguration?.smartToolSelectionMaxTools ?? 12,
|
||||
},
|
||||
undefined, // todoList
|
||||
this.api.getModel().id,
|
||||
userQuery, // Pass the extracted user query for smart tool selection
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue