Complex Function Calling

Published

In the previous tutorial, you learned function calling basics with a single calculator tool. But what happens when your AI needs to choose between multiple tools, or use several tools to complete one task?

Think of it like this: In 3.1, you gave your AI one tool (a calculator). Now you're giving it a whole toolbox, and teaching it to pick the right tools for each job.

What Makes Function Calling "Complex"?

Complex function calling involves three key scenarios:

  1. Multiple Tools Available: AI chooses the right tool from many options
  2. Sequential Tool Use: AI uses multiple tools in order to complete a task
  3. Smart Tool Selection: AI decides which combination of tools to use

Let's see the difference:

Simple (3.1):

User: "What's 25 + 75?"
AI: Uses calculator → Result: 100

Complex (3.2):

User: "What's 25 + 75 and what's the weather in Tokyo?"
AI: Uses calculator → Uses weather tool → Combined result

The Multi-Tool Approach

graph TD
    A[User Query] --> B[AI Analyzes What's Needed]
    B --> C{Multiple Tools Needed?}
    C -->|Single Tool| D[Call One Function]
    C -->|Multiple Tools| E[Call Multiple Functions]
    D --> F[Stream Final Response]
    E --> G[Execute All Functions]
    G --> F

Environment Setup

Build on your existing setup from tutorial 3.1:

# You should already have this from 3.1
npm install @google/genai dotenv
npm install -D typescript @types/node ts-node

Working Code Example

Let's build an AI assistant with multiple tools. We'll break this down into simple, understandable pieces.

Step 1: Import Dependencies and Setup

import { GoogleGenAI, FunctionDeclaration, Type } from "@google/genai";
import * as readline from "readline";
import * as fs from "fs";
import * as dotenv from "dotenv";

dotenv.config();

We're adding fs (file system) to enable file operations - something the AI genuinely can't do without tools.

Step 2: Create Multiple Function Schemas

// Tool 1: Enhanced Calculator
const calculatorFunction: FunctionDeclaration = {
  name: "calculate",
  description: "Perform math calculations including percentages",
  parameters: {
    type: Type.OBJECT,
    properties: {
      operation: {
        type: Type.STRING,
        enum: ["add", "subtract", "multiply", "divide", "percentage"],
        description: "Math operation to perform",
      },
      a: { type: Type.NUMBER, description: "First number" },
      b: { type: Type.NUMBER, description: "Second number" },
    },
    required: ["operation", "a", "b"],
  },
};

// Tool 2: Mock Weather Service
const weatherFunction: FunctionDeclaration = {
  name: "getWeather",
  description: "Get current weather information for a city",
  parameters: {
    type: Type.OBJECT,
    properties: {
      city: {
        type: Type.STRING,
        description: "City name (e.g., tokyo, london, new-york)",
      },
    },
    required: ["city"],
  },
};

// Tool 3: File Operations
const fileFunction: FunctionDeclaration = {
  name: "saveNote",
  description: "Save a note or information to a file",
  parameters: {
    type: Type.OBJECT,
    properties: {
      filename: { type: Type.STRING, description: "Name of the file" },
      content: { type: Type.STRING, description: "Content to save" },
    },
    required: ["filename", "content"],
  },
};

Notice how each tool has a clear, specific purpose. The AI will choose based on what the user asks for.

Step 3: Implement the Actual Functions

// Calculator implementation
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;
    case "percentage":
      return (a * b) / 100;
    default:
      throw new Error(`Unknown operation: ${operation}`);
  }
}

// Mock weather database (no API needed!)
const weatherData: Record<string, any> = {
  tokyo: { temp: 22, condition: "sunny", humidity: 65 },
  london: { temp: 15, condition: "rainy", humidity: 80 },
  "new-york": { temp: 18, condition: "cloudy", humidity: 70 },
  paris: { temp: 20, condition: "partly cloudy", humidity: 55 },
};

function getWeather(city: string) {
  const cityKey = city.toLowerCase().replace(/\s+/g, "-");
  const weather = weatherData[cityKey];

  if (!weather) {
    throw new Error(`Weather data not available for ${city}`);
  }

  return weather;
}

// File operations
function saveNote(filename: string, content: string): string {
  try {
    fs.writeFileSync(`${filename}.txt`, content);
    return `Successfully saved to ${filename}.txt`;
  } catch (error) {
    throw new Error(`Failed to save file: ${error}`);
  }
}

These are real, working functions that the AI genuinely needs to accomplish tasks.

Step 4: Setup AI Client with All Tools

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 });

// All available tools
const allTools = [calculatorFunction, weatherFunction, fileFunction];

Now the AI has access to all three tools and can choose which ones to use.

Step 5: Enhanced Function Calling Logic

async function askAI(userMessage: string): Promise<void> {
  try {
    // Send message with all available tools
    const response = await genAI.models.generateContentStream({
      model: "gemini-2.5-flash",
      contents: [{ text: userMessage }],
      config: {
        tools: [{ functionDeclarations: allTools }],
        temperature: 0.1,
      },
    });

    let functionCalls: any[] = [];

    // Stream initial response
    process.stdout.write("AI: ");
    for await (const chunk of response) {
      const chunkText = chunk.text || "";
      process.stdout.write(chunkText);

      if (chunk.functionCalls) {
        functionCalls.push(...chunk.functionCalls);
      }
    }
    console.log();

    // Execute any function calls
    if (functionCalls.length > 0) {
      const functionResults = [];

      for (const functionCall of functionCalls) {
        console.log(`\nCalling: ${functionCall.name}`);
        console.log(`Arguments: ${JSON.stringify(functionCall.args)}`);

        try {
          let result;

          // Execute the appropriate function
          switch (functionCall.name) {
            case "calculate":
              const { operation, a, b } = functionCall.args;
              result = calculate(operation, a, b);
              break;

            case "getWeather":
              const { city } = functionCall.args;
              result = getWeather(city);
              break;

            case "saveNote":
              const { filename, content } = functionCall.args;
              result = saveNote(filename, content);
              break;

            default:
              throw new Error(`Unknown function: ${functionCall.name}`);
          }

          console.log(`Result: ${JSON.stringify(result)}`);

          functionResults.push({
            call: functionCall,
            result: { result: result },
          });

        } catch (error) {
          console.log(`Error: ${error}`);
          functionResults.push({
            call: functionCall,
            result: { error: error instanceof Error ? error.message : "Unknown error" },
          });
        }
      }

This section handles multiple function calls in sequence. The AI might call one function or several, and we execute them all.

Step 6: Stream Final Response with All Results

      // Build conversation context with all function results
      const conversationParts = [
        { role: "user", parts: [{ text: userMessage }] },
      ];

      // Add each function call and result
      for (const { call, result } of functionResults) {
        conversationParts.push({ role: "model", parts: [{ functionCall: call } as any] });
        conversationParts.push({
          role: "function",
          parts: [{
            functionResponse: {
              name: call.name,
              response: result,
            },
          } as any ],
        });
      }

      // Get final response with all context
      const finalResponse = await genAI.models.generateContentStream({
        model: "gemini-2.5-flash",
        contents: conversationParts,
      });

      console.log();
      process.stdout.write("AI: ");
      for await (const chunk of finalResponse) {
        const chunkText = chunk.text || "";
        process.stdout.write(chunkText);
      }
      console.log();
    }
  } catch (error) {
    console.log("\nError:", error instanceof Error ? error.message : "Something went wrong");
  }
}

This sends all function results back to the AI so it can provide a comprehensive response.

Step 7: Chat Interface

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

function startChat(): void {
  console.log("Multi-Tool AI Assistant");
  console.log("I can calculate, check weather, and save notes!");
  console.log("Try: 'What's 15% of 200 and what's the weather in Tokyo?'\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();
      chat();
    });
  };

  chat();
}

startChat();

Testing Your Multi-Tool Assistant

Here's what complex function calling looks like in action:

Multi-Tool AI Assistant
I can calculate, check weather, and save notes!

You: What's 25% of 400 and what's the weather in Tokyo?
AI: I'll help you with both calculations and weather information.

Calling: calculate
Arguments: {"operation":"percentage","a":400,"b":25}
Result: 100

Calling: getWeather
Arguments: {"city":"tokyo"}
Result: {"temp":22,"condition":"sunny","humidity":65}

AI: 25% of 400 is 100. The weather in Tokyo is currently sunny with a temperature of 22°C and humidity at 65%.

You: Save a note saying "Meeting at 3pm" in a file called "reminder"
AI: I'll save that note for you.

Calling: saveNote
Arguments: {"filename":"reminder","content":"Meeting at 3pm"}
Result: "Successfully saved to reminder.txt"

AI: I've successfully saved your note "Meeting at 3pm" to reminder.txt.

Notice how the AI automatically:

  • Recognizes when multiple tools are needed
  • Calls the right functions with correct parameters
  • Combines all results into a coherent response

Key Insights

Smart Tool Selection: The AI analyzes the user's request and determines which tools are needed. It doesn't randomly call functions - it understands intent.

Sequential Execution: When multiple functions are needed, they're executed in sequence, and all results are combined for the final response.

Error Handling: Each function can fail independently, and the AI handles errors gracefully while still providing useful information.

No External Dependencies: All functions use built-in Node.js features or mock data, making the code completely self-contained.

FAQ

Summary

You've learned to build AI assistants that can intelligently choose and use multiple tools. The key concepts are: defining multiple function schemas that give AI various capabilities, implementing real functions that provide genuine value the AI can't achieve alone, handling sequential function execution when multiple tools are needed, and maintaining streaming responses throughout the complex function calling process. This multi-tool approach transforms your AI from a single-purpose calculator into a versatile assistant that can handle diverse, complex requests efficiently.

Complete Code

You can find the complete, runnable code for this tutorial on GitHub: https://github.com/avestalabs/academy/tree/main/3-structured-interactions/complex-function-calling

Share this article: