Lesson 18: Dynamic Tool Selection
Discover an advanced pattern for building highly efficient and accurate agents by dynamically providing only the most relevant tools based on the user's prompt.
Code: lesson_18_dynamic_tool_selection.mjs
This pattern introduces a "router" function (selectToolsForPrompt
) that runs *before* the agent is configured. It analyzes the user's prompt to select a small subset of relevant tools from a larger library. This reduces the amount of information sent to the model, improving performance and accuracy.
// lesson_18_dynamic_tool_selection.mjs
// Merci SDK Tutorial: Lesson 18 - Dynamic Tool Selection for Context-Aware Agents
// --- IMPORTS ---
import {
MerciClient,
createUserMessage,
executeTools,
createAssistantToolCallMessage,
createToolResultMessage,
createAssistantTextMessage
} from '../lib/merci.2.14.0.mjs';
import { token } from '../secret/token.mjs';
const MODEL = 'google-chat-gemini-flash-2.5';
// --- STEP 1: CREATE A COMPREHENSIVE TOOL LIBRARY ---
// In a real application, these would be in separate modules.
const toolLibrary = {
calendar: [{
name: 'create_event',
parameters: { schema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] } },
execute: async ({ title }) => ({ status: `event '${title}' created` }),
}],
email: [{
name: 'send_email',
parameters: { schema: { type: 'object', properties: { recipient: { type: 'string' } }, required: ['recipient'] } },
execute: async ({ recipient }) => ({ status: `email sent to ${recipient}` }),
}],
database: [{
name: 'query_users',
parameters: { schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
execute: async ({ query }) => ({ results: [{ id: 1, name: 'Alice' }] }),
}],
};
// --- STEP 2: CREATE A "ROUTER" TO SELECT RELEVANT TOOLS ---
/**
* Analyzes a prompt and returns a subset of tools relevant to the user's request.
* @param {string} prompt The user's input.
* @returns {Array<object>} An array of tool definitions.
*/
function selectToolsForPrompt(prompt) {
console.log('
[ROUTER] Analyzing prompt to select relevant tools...');
const selectedTools = [];
const lowerCasePrompt = prompt.toLowerCase();
if (lowerCasePrompt.includes('meeting') || lowerCasePrompt.includes('schedule') || lowerCasePrompt.includes('event')) {
selectedTools.push(...toolLibrary.calendar);
}
if (lowerCasePrompt.includes('email') || lowerCasePrompt.includes('send')) {
selectedTools.push(...toolLibrary.email);
}
if (lowerCasePrompt.includes('database') || lowerCasePrompt.includes('users') || lowerCasePrompt.includes('query')) {
selectedTools.push(...toolLibrary.database);
}
if (selectedTools.length === 0) {
console.log('[ROUTER] No specific tools matched. Proceeding without tools.');
return [];
}
console.log(`[ROUTER] Selected tools: [${selectedTools.map(t => t.name).join(', ')}]`);
return selectedTools;
}
async function main() {
console.log(`--- Merci SDK Tutorial: Lesson 18 - Dynamic Tool Selection (Model: ${MODEL}) ---`);
try {
// --- STEP 3: INITIALIZE THE CLIENT ---
console.log('
[STEP 3] Initializing MerciClient...');
const client = new MerciClient({ token });
client.on('tool_start', ({ calls }) => {
console.log(`
[EVENT: tool_start] Model is calling the dynamically selected tool: '${calls[0].name}'`);
});
// --- STEP 4: DEFINE PROMPT ---
console.log('[STEP 4] Preparing user prompt...');
const userPrompt = "Use the 'query_users' tool to find all active users in the database. Once you have the results, provide a concise summary of the active users found.";
// --- STEP 5: DYNAMICALLY SELECT TOOLS BASED ON THE PROMPT ---
// This is the core of the lesson. The router runs *before* we configure the agent.
const relevantTools = selectToolsForPrompt(userPrompt);
// --- STEP 6: CONFIGURE THE AGENT WITH THE SELECTED TOOLS ---
console.log('
[STEP 6] Configuring the agent with ONLY the relevant tools...');
const agent = client.chat.session(MODEL).withTools(relevantTools);
// --- STEP 7: RUN THE AGENT (Manual Loop) ---
console.log('[STEP 7] Running the context-aware agent (manual loop)...');
console.log(`
👤 User > ${userPrompt}`);
let messagesForToolCall = [createUserMessage(userPrompt)];
let finalAnswer = '';
// First turn: Force tool choice if relevantTools are found
const chatSessionForToolCall = client.chat.session(MODEL)
.withTools(relevantTools)
.withParameters(builder => {
if (relevantTools.length > 0) {
builder.toolChoiceRequired(true);
}
return builder; // Explicitly return the builder
});
console.log('[INFO] Sending initial request to potentially force tool call...');
let toolCalls = [];
for await (const event of chatSessionForToolCall.stream(messagesForToolCall)) {
if (event.type === 'tool_calls') {
console.log(`
[EVENT: tool_start] Model decided to call tool: '${event.calls[0].name}'`);
toolCalls = event.calls;
}
}
if (toolCalls.length > 0) {
console.log('[INFO] Executing tool locally...');
const toolResults = await executeTools(toolCalls, relevantTools);
// Add tool call and result to messages history
toolResults.forEach((result, index) => {
const call = toolCalls[index];
const resultValue = result.success ? result.result : { error: result.error || 'Unknown execution error' };
messagesForToolCall.push(createAssistantToolCallMessage(call.id, call.name, call.arguments));
messagesForToolCall.push(createToolResultMessage(call.id, call.name, JSON.stringify(resultValue)));
});
// Second turn: Allow model to generate text response (no forced tool choice)
console.log('[INFO] Sending tool results back to model for final text response...');
const chatSessionForText = client.chat.session(MODEL)
.withTools(relevantTools); // Keep tools available, but don't force choice
for await (const event of chatSessionForText.stream(messagesForToolCall)) {
if (event.type === 'text') {
finalAnswer += event.content;
}
}
} else {
// If no tool calls were made (e.g., no relevant tools or model chose not to use tool in optional mode)
// We need to get the direct text response from the first turn.
const chatSessionForText = client.chat.session(MODEL).withTools(relevantTools);
for await (const event of chatSessionForText.stream(messagesForToolCall)) {
if (event.type === 'text') {
finalAnswer += event.content;
}
}
}
// --- FINAL RESULT ---
console.log('
--- FINAL RESULT ---');
console.log(`👤 User > ${userPrompt}`);
console.log('--------------------');
console.log('The agent successfully used the `query_users` tool because the router identified it as relevant, while ignoring the irrelevant email and calendar tools.');
} catch (error) {
console.error('
[FATAL ERROR] An error occurred during the operation.');
console.error(' Message:', error.message);
if (error.status) { console.error(' API Status:', error.status); }
if (error.details) { console.error(' Details:', JSON.stringify(error.details, null, 2)); }
if (error.stack) { console.error(' Stack:', error.stack); }
console.error('
Possible causes: Invalid token, network issues, or an API service problem.');
process.exit(1);
}
}
main().catch(console.error);
Expected Output
The script first analyzes the prompt \"Can you query the database...\". The router function identifies the keyword \"database\" and selects only the `query_users` tool. The agent is then initialized with just this single tool, ignoring the irrelevant calendar and email tools, before making the API call.
[ROUTER] Analyzing prompt to select relevant tools...
[ROUTER] Selected tools: [query_users]
[STEP 6] Configuring the agent with ONLY the relevant tools...
[STEP 7] Running the context-aware agent...
👤 User > Can you query the database for all active users?
[EVENT: tool_start] Model is calling the dynamically selected tool: \'query_users\'
--- FINAL RESULT ---
👤 User > Can you query the database for all active users?
🤖 Assistant > I found one user named Alice.
--------------------