Function calling enables LLMs to execute actions beyond text generation. ignitionstack.pro implements a secure, extensible tool system that works across all providers supporting function calling.
┌─────────────────────────────────────────────────────────────────┐
│ Function Calling Flow │
└─────────────────────────────────────────────────────────────────┘
User: "What's the weather in Tokyo?"
│
▼
┌─────────────────────────┐
│ LLM analyzes intent │
│ Decides to call tool │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Tool Call Request │
│ get_weather(tokyo) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Tool Executor runs │
│ Validates & executes │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Result returned │
│ {"temp": 22, ...} │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ LLM incorporates │
│ result into response │
└───────────┬─────────────┘
│
▼
"The current temperature in Tokyo is 22°C..."| Component | Location | Purpose |
|---|---|---|
| Tool Executor | lib/ai/tools/tool-executor.ts | Executes tool calls securely |
| Tool Definitions | Database ai_tools table | Stores tool schemas |
| Tool Executions | Database ai_tool_executions table | Audit log |
-- Tool definitions
CREATE TABLE ai_tools (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL,
description TEXT NOT NULL,
parameters JSONB NOT NULL, -- JSON Schema
handler TEXT NOT NULL, -- Handler function name
is_enabled BOOLEAN DEFAULT true,
requires_confirmation BOOLEAN DEFAULT false,
rate_limit INTEGER, -- Max calls per minute
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Execution audit log
CREATE TABLE ai_tool_executions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tool_id UUID REFERENCES ai_tools(id),
conversation_id UUID REFERENCES ai_conversations(id),
user_id UUID REFERENCES auth.users(id),
input JSONB NOT NULL,
output JSONB,
error TEXT,
latency_ms INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);Returns current time in specified timezone.
{
name: 'get_current_time',
description: 'Get the current date and time',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: 'IANA timezone (e.g., America/New_York)',
default: 'UTC',
},
},
},
}
// Usage: "What time is it in Tokyo?"
// Call: get_current_time({ timezone: 'Asia/Tokyo' })
// Result: { time: '2025-01-20T15:30:00+09:00', timezone: 'Asia/Tokyo' }Safely evaluates mathematical expressions.
{
name: 'calculate',
description: 'Evaluate a mathematical expression',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: 'Math expression (e.g., "2 + 2 * 3")',
},
},
required: ['expression'],
},
}
// Usage: "What's 15% of 230?"
// Call: calculate({ expression: '230 * 0.15' })
// Result: { result: 34.5, expression: '230 * 0.15' }Searches the web for information (placeholder for real implementation).
{
name: 'search_web',
description: 'Search the web for current information',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
num_results: {
type: 'number',
description: 'Number of results (1-10)',
default: 5,
},
},
required: ['query'],
},
}Returns weather information (placeholder for real API integration).
{
name: 'get_weather',
description: 'Get current weather for a location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'City name or coordinates',
},
units: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
default: 'celsius',
},
},
required: ['location'],
},
}// src/app/lib/ai/tools/tool-executor.ts
import { ToolExecutor } from '@/lib/ai/tools/tool-executor'
const executor = new ToolExecutor({
timeout: 10000, // 10 second timeout
maxConcurrent: 5, // Max parallel executions
enableLogging: true, // Log to database
})
// Execute a tool call from LLM
const result = await executor.execute({
name: 'get_current_time',
arguments: { timezone: 'America/New_York' },
conversationId: 'conv-123',
userId: 'user-456',
})
// Result: { time: '2025-01-20T10:30:00-05:00', timezone: 'America/New_York' }// During streaming, intercept tool calls
for await (const chunk of provider.chatStream(messages, options)) {
if (chunk.type === 'tool_call') {
// Execute the tool
const result = await executor.execute({
name: chunk.toolCall.name,
arguments: chunk.toolCall.arguments,
conversationId,
userId,
})
// Feed result back to LLM
messages.push({
role: 'tool',
toolCallId: chunk.toolCall.id,
content: JSON.stringify(result),
})
// Continue generation with tool result
const continuation = provider.chatStream(messages, options)
// ...
}
}// src/app/lib/ai/tools/definitions/my-tool.ts
export const myToolDefinition = {
name: 'lookup_customer',
description: 'Look up customer information by email or ID',
parameters: {
type: 'object',
properties: {
identifier: {
type: 'string',
description: 'Customer email or ID',
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to retrieve',
default: ['name', 'email', 'plan'],
},
},
required: ['identifier'],
},
}// src/app/lib/ai/tools/handlers/lookup-customer.ts
import { ToolHandler } from '@/lib/ai/tools/types'
export const lookupCustomerHandler: ToolHandler = async (args, context) => {
const { identifier, fields = ['name', 'email', 'plan'] } = args
// Validate access (tools run with user's permissions)
if (!context.userId) {
throw new Error('Authentication required')
}
// Query database
const customer = await db
.from('customers')
.select(fields.join(','))
.or(`email.eq.${identifier},id.eq.${identifier}`)
.single()
if (!customer) {
return { error: 'Customer not found' }
}
return {
customer: customer.data,
retrievedAt: new Date().toISOString(),
}
}// src/app/lib/ai/tools/registry.ts
import { myToolDefinition } from './definitions/my-tool'
import { lookupCustomerHandler } from './handlers/lookup-customer'
export const toolRegistry = {
// Built-in tools
get_current_time: getCurrentTimeHandler,
calculate: calculateHandler,
search_web: searchWebHandler,
get_weather: getWeatherHandler,
// Custom tools
lookup_customer: lookupCustomerHandler,
}
export const toolDefinitions = [
// Built-in
getCurrentTimeDefinition,
calculateDefinition,
searchWebDefinition,
getWeatherDefinition,
// Custom
myToolDefinition,
]INSERT INTO ai_tools (name, description, parameters, handler) VALUES (
'lookup_customer',
'Look up customer information by email or ID',
'{
"type": "object",
"properties": {
"identifier": {"type": "string", "description": "Customer email or ID"},
"fields": {"type": "array", "items": {"type": "string"}, "default": ["name", "email", "plan"]}
},
"required": ["identifier"]
}',
'lookup_customer'
);// OpenAI tool format
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
},
required: ['location'],
},
},
},
]
// Response contains tool_calls
{
role: 'assistant',
tool_calls: [{
id: 'call_abc123',
type: 'function',
function: {
name: 'get_weather',
arguments: '{"location": "Tokyo"}',
},
}],
}// Gemini function declarations
const tools = [{
functionDeclarations: [{
name: 'get_weather',
description: 'Get current weather',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
},
required: ['location'],
},
}],
}]
// Response contains function call
{
candidates: [{
content: {
parts: [{
functionCall: {
name: 'get_weather',
args: { location: 'Tokyo' },
},
}],
},
}],
}The adapters handle format conversion automatically:
// Adapter converts to provider-specific format
const openaiTools = adapter.formatTools(toolDefinitions)
const geminiTools = adapter.formatTools(toolDefinitions)
// And normalizes responses back
const normalizedCall = adapter.parseToolCall(providerResponse)
// Always returns: { name, arguments, id }// Tools execute in a restricted context
const context = {
userId: authenticatedUser.id,
conversationId,
permissions: user.permissions,
timeout: 10000,
}
// Handler receives context for access control
const result = await handler(args, context)// Validate arguments against JSON Schema
import Ajv from 'ajv'
const ajv = new Ajv()
const validate = ajv.compile(tool.parameters)
if (!validate(args)) {
throw new ValidationError(validate.errors)
}// Per-tool rate limits
const tool = await db.from('ai_tools').select().eq('name', name).single()
if (tool.rate_limit) {
const recentCalls = await db
.from('ai_tool_executions')
.select('id')
.eq('tool_id', tool.id)
.eq('user_id', userId)
.gte('created_at', oneMinuteAgo)
.count()
if (recentCalls >= tool.rate_limit) {
throw new RateLimitError('Tool rate limit exceeded')
}
}// Some tools require user confirmation
{
name: 'delete_account',
requires_confirmation: true,
// ...
}
// In the UI, show confirmation before executing
if (tool.requires_confirmation) {
const confirmed = await showConfirmationDialog({
title: 'Confirm Action',
message: `Allow AI to execute: ${tool.name}?`,
details: args,
})
if (!confirmed) {
return { cancelled: true }
}
}// src/app/components/ai/ToolExecution.tsx
export function ToolExecution({ execution }) {
return (
<div className="tool-execution">
<div className="tool-header">
<ToolIcon name={execution.tool} />
<span>{execution.tool}</span>
<StatusBadge status={execution.status} />
</div>
<div className="tool-input">
<code>{JSON.stringify(execution.input, null, 2)}</code>
</div>
{execution.output && (
<div className="tool-output">
<code>{JSON.stringify(execution.output, null, 2)}</code>
</div>
)}
{execution.error && (
<div className="tool-error">{execution.error}</div>
)}
</div>
)
}// Display tool calls inline in chat
{message.toolCalls?.map((call) => (
<ToolExecution
key={call.id}
execution={{
tool: call.name,
input: call.arguments,
output: call.result,
status: call.status,
}}
/>
))}// BAD - Vague description
{ name: 'search', description: 'Search stuff' }
// GOOD - Specific and helpful
{
name: 'search_knowledge_base',
description: 'Search the company knowledge base for articles, FAQs, and documentation. Returns relevant excerpts with source links.',
}// BAD - Too many optional params
{
properties: {
query: { type: 'string' },
limit: { type: 'number' },
offset: { type: 'number' },
sortBy: { type: 'string' },
sortOrder: { type: 'string' },
filters: { type: 'object' },
// ... many more
},
}
// GOOD - Only what's needed
{
properties: {
query: {
type: 'string',
description: 'Search query',
},
limit: {
type: 'number',
description: 'Max results (1-20)',
default: 5,
},
},
required: ['query'],
}// Return informative errors
const handler = async (args) => {
try {
const result = await riskyOperation(args)
return { success: true, data: result }
} catch (error) {
return {
success: false,
error: error.message,
suggestion: 'Try a different search term',
}
}
}// Wrap external calls with timeout
const result = await Promise.race([
externalApiCall(args),
timeout(10000, 'External API timeout'),
])