Function Calling Basics
In the previous module, you built sophisticated conversational AI that can chat, remember context, and stream responses in real-time. But what if your AI could do more than just talk? What if it could perform calculations, fetch current weather data, or execute specific tasks based on user requests - all while providing real-time feedback?
This is where function calling comes in - the bridge between conversational AI and practical, tool-enabled assistants. Function calling allows AI models to invoke external functions with structured parameters, transforming your streaming chatbot into a capable assistant that can actually do things.
What is Function Calling?
Function calling is a feature that allows AI models to recognize when they need to use external tools or functions to answer user queries. Instead of trying to guess or make up information, the AI can call predefined functions with appropriate parameters.
Here's the fundamental difference:
Without Function Calling:
User: "What's 15% of 1,250?"
AI: Let me think... 15% of 1,250 is approximately 187.5 (AI calculates in its head, might be wrong)
With Function Calling:
User: "What's 15% of 1,250?"
AI: I need to calculate this precisely. Let me use my calculator function.
Calling function: calculate
Arguments: {"operation":"multiply","a":1250,"b":0.15}
Result: 187.5
AI: 15% of 1,250 is exactly 187.5.
The AI recognizes it needs to perform a calculation, calls the appropriate function with the correct parameters, and then provides a precise response using the actual result.
Understanding the Function Calling Flow
graph TD
A[User Query] --> B[AI Analyzes Intent]
B --> C{Needs Function?}
C -->|No| D[Stream Direct Response]
C -->|Yes| E[AI Streams Initial Response]
E --> F[AI Makes Function Call]
F --> G[Execute Function]
G --> H[Show Function Result]
H --> I[AI Streams Final Response with Results]
I --> J[User Sees Complete Flow]
The key insight is that function calling involves multiple steps: initial AI analysis, function execution, and final response generation - all while maintaining the streaming experience users expect.
Environment Setup
You'll build on your existing TypeScript setup from previous tutorials:
# If starting fresh
mkdir function-calling-basics
cd function-calling-basics
npm init -y
npm install @google/genai dotenv
npm install -D typescript @types/node ts-node
Make sure your .env file contains your Gemini API key:
GEMINI_API_KEY=your_api_key_here
Working Code Example
Let's build a streaming function-calling AI assistant step by step. We'll break down each component to understand why it's needed and how it works.
Step 1: Import Required Dependencies
import { GoogleGenAI, FunctionDeclaration, Type } from "@google/genai";
import * as readline from "readline";
import * as dotenv from "dotenv";
dotenv.config();
Notice we're importing FunctionDeclaration and Type from the Google GenAI package. These are essential for defining function schemas that the AI can understand.
Step 2: Define What the AI Can Call - Function Schema
// 1. Define what the AI can call
const calculatorFunction: FunctionDeclaration = {
name: "calculate",
description: "Perform basic math calculations",
parameters: {
type: Type.OBJECT,
properties: {
operation: {
type: Type.STRING,
enum: ["add", "subtract", "multiply", "divide"],
description: "The math operation to perform",
},
a: { type: Type.NUMBER, description: "First number" },
b: { type: Type.NUMBER, description: "Second number" },
},
required: ["operation", "a", "b"],
},
};
This is the function schema - think of it as a detailed instruction manual for the AI. Let's break down why each part is crucial:
- name: The identifier the AI will use when calling this function
- description: Tells the AI WHEN to use this function (this is critical for proper function selection)
- parameters: Defines the exact structure and types of arguments the function expects
- Type.OBJECT: Indicates the parameters are structured as an object
- properties: Each parameter with its type and description
- enum: Restricts the operation to only valid mathematical operations
- required: Ensures the AI provides all necessary parameters
Without this schema, the AI wouldn't know this function exists or how to use it properly.
Step 3: Create the Actual Function Implementation
// 2. Create the actual function
function calculate(operation: string, a: number, b: number): number {
switch (operation) {
case "add":
return a + b;
case "subtract":
return a - b;
case "multiply":
return a * b;
case "divide":
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
default:
throw new Error(`Unknown operation: ${operation}`);
}
}
This is the actual function that performs the work. Notice how it matches exactly what we promised in the schema:
- Takes the same parameters (operation, a, b)
- Handles the four operations we listed in the enum
- Returns a number as expected
- Includes proper error handling for edge cases
The schema and implementation must match perfectly, or the function calling will fail.
Step 4: Setup AI Client
// 3. Setup AI
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
console.error("Error: GEMINI_API_KEY not found");
process.exit(1);
}
const genAI = new GoogleGenAI({ apiKey });
Standard AI client initialization with proper error handling for missing API keys.
Step 5: Core Function Calling Logic with Streaming
// 4. Simple function calling with streaming
async function askAI(userMessage: string): Promise<void> {
try {
const response = await genAI.models.generateContentStream({
model: "gemini-2.5-flash",
contents: [{ text: userMessage }],
config: {
tools: [{ functionDeclarations: [calculatorFunction] }],
temperature: 0.1,
},
});
let functionCalls = [];
// Stream the initial response
process.stdout.write("AI: ");
for await (const chunk of response) {
const chunkText = chunk.text || "";
process.stdout.write(chunkText);
// Collect any function calls
if (chunk.functionCalls) {
functionCalls.push(...chunk.functionCalls);
}
}
console.log(); // new line after streaming
This first part sends the user's message to the AI along with our function schema. The key points:
- tools: This is where we provide our function declarations to the AI
- temperature: 0.1: Lower temperature for more precise function calling
- Streaming: We stream the AI's initial response while collecting any function calls it wants to make
Step 6: Execute Function Calls
// If AI wants to call a function
if (functionCalls.length > 0) {
const functionCall = functionCalls[0]; // Get first function call
console.log(`\nCalling function: ${functionCall.name}`);
console.log(`Arguments: ${JSON.stringify(functionCall.args)}`);
// Execute the function
if (functionCall.name === "calculate" && functionCall.args) {
const args = functionCall.args;
const result = calculate(
args.operation as string,
args.a as number,
args.b as number
);
console.log(`Result: ${result}\n`);
This section executes any functions the AI requested. We:
- Check if the AI made any function calls
- Extract the function name and arguments
- Execute our actual function with the AI's parameters
- Display the result to the user
Step 7: Stream Final Response with Function Results
// Get AI's final response with streaming
const finalResponse = await genAI.models.generateContentStream({
model: "gemini-2.5-flash",
contents: [
{ role: "user", parts: [{ text: userMessage }] },
{ role: "model", parts: [{ functionCall }] },
{
role: "function",
parts: [
{
functionResponse: {
name: "calculate",
response: { result },
},
},
],
},
],
});
// Stream the final answer
process.stdout.write("AI: ");
for await (const chunk of finalResponse) {
const chunkText = chunk.text || "";
process.stdout.write(chunkText);
}
console.log(); // new line after streaming
}
}
} catch (error) {
console.log(
"\nError:",
error instanceof Error ? error.message : "Something went wrong"
);
}
}
This final step sends the function results back to the AI and streams its final response. The conversation structure includes:
- The original user message
- The AI's function call
- The function response with results
This allows the AI to formulate a complete answer using the precise function results.
Step 8: Terminal Chat Interface
// 5. Terminal chat interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function startChat(): void {
console.log("Function Calling AI Assistant");
console.log("Ask me to calculate something! (type 'exit' to quit)\n");
const chat = (): void => {
rl.question("You: ", async (input) => {
if (input.toLowerCase() === "exit") {
console.log("Goodbye!");
rl.close();
return;
}
if (input.trim()) {
await askAI(input);
}
console.log(); // blank line for readability
chat();
});
};
chat();
}
// Start the chat
startChat();
This creates a simple but effective chat interface that allows users to test the function calling system interactively.
Testing Your Function-Calling Assistant
Here's what the complete experience looks like:
Function Calling AI Assistant
Ask me to calculate something! (type 'exit' to quit)
You: What's 25 + 75?
AI: I'll help you calculate 25 + 75.
Calling function: calculate
Arguments: {"operation":"add","a":25,"b":75}
Result: 100
AI: 25 + 75 equals 100.
You: What's 20% of 500?
AI: I need to calculate 20% of 500 for you.
Calling function: calculate
Arguments: {"operation":"multiply","a":500,"b":0.2}
Result: 100
AI: 20% of 500 is 100.
You: Hello, how are you?
AI: Hello! I'm doing well, thank you for asking. I'm here to help you with calculations or answer any questions you might have.
Notice how the AI automatically recognizes when calculations are needed and when they're not. Simple greetings get direct responses, while mathematical questions trigger function calls.
Understanding the Complete Flow
Let's trace through what happens when a user asks "What's 15% of 1,250?":
- User Input: "What's 15% of 1,250?"
- AI Analysis: AI recognizes this requires calculation and streams initial response
- Function Call: AI calls
calculate("multiply", 1250, 0.15) - Function Execution: Our calculate function returns 187.5
- Final Response: AI streams final answer using the precise result
The AI automatically converts "15%" to the decimal 0.15 and chooses the multiply operation - this demonstrates the intelligence of function calling.
Common Pitfalls to Avoid
Mismatched schemas and implementations: The function schema must exactly match your actual function parameters and types. Any mismatch will cause function calling to fail.
Poor function descriptions: The description field is crucial - it tells the AI when to use each function. Vague descriptions lead to incorrect function selection.
Not handling function errors: Always include proper error handling in your functions. The AI needs to know when something goes wrong.
Using high temperature: Function calling works better with lower temperature settings (0.1-0.3) for more precise parameter extraction.
Forgetting the conversation structure: When sending function results back to the AI, you must include the complete conversation context (user message, function call, function response).
FAQ
Summary
You've learned the fundamentals of function calling with streaming responses - enabling AI models to use external tools while maintaining the real-time experience users expect. The key concepts are: defining clear function schemas that describe available tools to the AI, implementing actual functions that match the schema specifications exactly, handling the multi-step process of function calling with streaming responses, and managing the conversation flow between user input, function execution, and final AI responses. Function calling transforms conversational AI from simple chat to powerful, tool-enabled assistants that can perform real tasks with precision. This foundation prepares you for more complex function calling scenarios in the next tutorial.
Complete Code
You can find the complete, runnable code for this tutorial on GitHub: https://github.com/avestalabs/academy/tree/main/3-structured-interactions/function-calling-basics