Function Calling with Tools
Function calling, or "tools," is what bridges the AI's reasoning with real-world action. It stops being a chatbot and starts being an assistant that can do things.
How It Works
The dance is simple, and AIKit handles the choreography:
- You: Define your tools and give them to the AI.
- AI: Receives a prompt and decides if it needs a tool. If so, it pauses and says, "I need to use the
calculator
." - You: Execute the actual code for the
calculator
function with the arguments the AI provided. - AI: Gets the result back and uses it to give you a final, informed answer.
It’s a simple loop that makes your AI infinitely more capable.
Your First Tool: A Simple Calculator
Let's teach our AI some basic math. Even the smartest models can't be trusted with arithmetic.
import {
createProvider,
createTool,
userText,
assistant,
toolResult,
printStream,
collectStream,
} from '@chinmaymk/aikit';
// 1. Define your tool's schema and implementation
const tools = {
calculator: createTool(
'calculator',
'Performs basic math operations.',
{
type: 'object',
properties: {
operation: { type: 'string', enum: ['add', 'subtract'] },
a: { type: 'number' },
b: { type: 'number' },
},
required: ['operation', 'a', 'b'],
additionalProperties: false,
},
async ({ operation, a, b }) => {
if (operation === 'add') return a + b;
if (operation === 'subtract') return a - b;
throw new Error('Invalid operation');
}
),
};
// 2. Create your provider
const provider = createProvider('openai', {
apiKey: process.env.OPENAI_API_KEY!,
});
// 3. Start the conversation
const messages = [userText('What is 42 minus 15?')];
// 4. Let the model decide which tool to use
const stream = provider(messages, {
model: 'gpt-4o',
tools,
});
const { toolCalls } = await collectStream(stream);
// 5. If a tool is called, execute it
if (toolCalls) {
messages.push(assistantWithToolCalls(result.content, result.toolCalls));
const tool_outputs = await Promise.all(
tool_calls.map(async call => {
// Find the tool implementation
const tool = tools[call.tool_name];
if (!tool) throw new Error(`Tool not found: ${call.tool_name}`);
// Execute and return the result
const output = await tool.execute(call.args);
return toolResult(String(output), call.tool_call_id);
})
);
messages.push(...tool_outputs);
// 6. Get the final response from the model
await printStream(provider(messages));
}
This example shows the complete, manual loop for tool calling:
- Define the tool with its schema and execution logic.
- Call the provider and check for
tool_calls
in the response. - Execute the corresponding tool functions.
- Send the
toolResult
back to the provider. - Stream the final answer.
It's a bit more work, but it gives you complete control over the process.
Taking the Reins: Controlling Tool Usage
Sometimes you need to be the boss. AIKit lets you tell the AI exactly how to use its tools with the toolChoice
option.
toolChoice: 'auto'
(Default): The AI decides whether to use a tool or not. This is your go-to for most situations.toolChoice: 'required'
: The AI must use one of the provided tools. Perfect for forcing structured data extraction.toolChoice: 'none'
: The AI is forbidden from using any tools, even if they are provided. Use this when you need a pure text-based response.
// Force the AI to use the calculator
const stream = provider(messages, {
model: 'gpt-4o',
tools,
tool_choice: 'required',
});
const { toolCalls } = await collectStream(stream);
Streaming and Tools: A Perfect Match
Tools work seamlessly with streaming. The recommended way to handle this is with the processStream
helper, which can listen for onToolCall
events.
import { createProvider, userText, processStream } from '@chinmaymk/aikit';
// ... (setup provider and tools as above)
const stream = provider(messages, {
model: 'gpt-4o',
tools,
});
await processStream(stream, {
onToolCall: toolCall => {
console.log('Model wants to call:', toolCall);
// You would execute the tool here and send the result back
},
onText: text => process.stdout.write(text),
});
Check out the Streaming Guide for a deeper dive.
Golden Rules for Tools
- Be Specific: The better your tool descriptions, the better the model will be at using them. "Gets the 5-day weather forecast" is better than "Gets weather."
- Handle Errors: Your tool execution might fail. Wrap it in a
try...catch
block and return a helpful error message to the model so it can try again or inform the user. - Validate Inputs: Use a validation library like
zod
orjsonschema
to define your tool's input schema. - Keep It Simple: Don't try to cram too much logic into a single tool. A few simple, focused tools are better than one complex one.
Happy building! 🚀