ANTIGRAVITY LABJP
Articles/AI Tools
AI Tools/2026-04-17Intermediate

Implementing Function Calling with Antigravity Python SDK: A Practical Guide to Giving AI Access to External Tools

Learn how to implement Function Calling with the Google Antigravity Python SDK. Includes working code examples for connecting AI to external APIs, handling multi-tool loops, and debugging within Antigravity IDE.

python26function-calling5google-antigravity2api13gemini16

There's a moment in almost every AI-assisted project when generating code isn't enough. You want the AI to actually do something — fetch real data from an API, save a file, look up a database record. That's exactly the gap Function Calling is designed to fill.

If you're building with the Google Antigravity Python SDK and haven't explored Function Calling yet, this guide walks you through a working implementation, from the basics to a multi-tool agent loop. All examples are tested and ready to run.

How Function Calling Actually Works

Before writing any code, it's worth getting the mental model right — especially because the name "Function Calling" is slightly misleading.

The model doesn't execute your functions directly. Here's what actually happens:

  1. Your app sends the model a list of available tools (function definitions) alongside the user's message
  2. The model responds with a function call request — it says "call this function with these arguments"
  3. Your app executes the function and sends the result back to the model
  4. The model incorporates the result into its final response

This design is intentional. Your app controls which functions are exposed, so the model can never trigger arbitrary code execution. It's a clean, safe boundary between AI reasoning and real-world actions.

Setting Up Your Environment

Install the required packages:

pip install google-generativeai python-dotenv

Get your API key from Google AI Studio, then store it in a .env file at your project root — never hardcode it:

GOOGLE_API_KEY=YOUR_GEMINI_API_KEY

Make sure .env is in your .gitignore. Antigravity's inline completion will suggest this if you start typing the gitignore entry.

Your First Function Call

Let's start with a simple weather lookup tool. This is intentionally minimal — the goal is to see the full round-trip before adding complexity.

import google.generativeai as genai
import os
from dotenv import load_dotenv
 
load_dotenv()
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
 
# ① The actual function (swap this out for a real API call)
def get_current_weather(city: str) -> dict:
    """Returns mock weather data for a given city."""
    mock_data = {
        "Tokyo": {"weather": "Sunny", "temp": 22, "humidity": 45},
        "Osaka": {"weather": "Cloudy", "temp": 20, "humidity": 58},
        "Fukuoka": {"weather": "Rainy", "temp": 18, "humidity": 72},
    }
    return mock_data.get(city, {"weather": "Unknown", "temp": 0, "humidity": 0})
 
# ② Define the tool schema the model will see
weather_tool = genai.protos.Tool(
    function_declarations=[
        genai.protos.FunctionDeclaration(
            name="get_current_weather",
            description="Retrieve current weather information for a specified city",
            parameters=genai.protos.Schema(
                type=genai.protos.Type.OBJECT,
                properties={
                    "city": genai.protos.Schema(
                        type=genai.protos.Type.STRING,
                        description="The city name to check weather for (e.g. Tokyo, Osaka)"
                    )
                },
                required=["city"]
            )
        )
    ]
)
 
# ③ Initialize model with tools attached
model = genai.GenerativeModel(
    model_name="gemini-2.0-flash",
    tools=[weather_tool]
)
chat = model.start_chat()
 
# ④ Send the user message
response = chat.send_message("What's the weather like in Tokyo today?")
 
# ⑤ Handle the function call response
part = response.candidates[0].content.parts[0]
if hasattr(part, "function_call") and part.function_call.name:
    fc = part.function_call
    print(f"Model requested: {fc.name}({dict(fc.args)})")
 
    # Execute the function
    result = get_current_weather(**dict(fc.args))
    print(f"Function returned: {result}")
 
    # Send the result back to the model
    final_response = chat.send_message(
        genai.protos.Content(
            parts=[genai.protos.Part(
                function_response=genai.protos.FunctionResponse(
                    name=fc.name,
                    response={"result": result}
                )
            )]
        )
    )
    print(f"\nModel's final answer:\n{final_response.text}")

Expected output:

Model requested: get_current_weather({'city': 'Tokyo'})
Function returned: {'weather': 'Sunny', 'temp': 22, 'humidity': 45}

Model's final answer:
The current weather in Tokyo is sunny, with a temperature of 22°C and 45% humidity...

Building a Multi-Tool Agent Loop

Real applications typically give the model several tools and let it sequence calls on its own. Here's a pattern that handles this cleanly — the model can call get_current_weather, get_today_schedule, and save_memo in whatever order makes sense for the request.

import google.generativeai as genai
import os
from dotenv import load_dotenv
from datetime import datetime
 
load_dotenv()
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
 
# Tool implementations
def get_current_weather(city: str) -> dict:
    """Returns weather data for a city."""
    return {"city": city, "weather": "Sunny", "temp": 22}
 
def get_today_schedule() -> list:
    """Returns today's schedule (mock data)."""
    return [
        {"time": "10:00", "event": "Team standup"},
        {"time": "14:00", "event": "Code review"},
    ]
 
def save_memo(title: str, content: str) -> dict:
    """Saves a memo to a local text file."""
    filename = f"memo_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
    try:
        with open(filename, "w", encoding="utf-8") as f:
            f.write(f"# {title}\n\n{content}")
        return {"saved": True, "filename": filename}
    except IOError as e:
        return {"saved": False, "error": str(e)}
 
# Tool registry: maps function names to their implementations
TOOL_FUNCTIONS = {
    "get_current_weather": get_current_weather,
    "get_today_schedule": get_today_schedule,
    "save_memo": save_memo,
}
 
tools = [genai.protos.Tool(function_declarations=[
    genai.protos.FunctionDeclaration(
        name="get_current_weather",
        description="Get current weather for a specified city",
        parameters=genai.protos.Schema(
            type=genai.protos.Type.OBJECT,
            properties={"city": genai.protos.Schema(
                type=genai.protos.Type.STRING, description="City name"
            )},
            required=["city"]
        )
    ),
    genai.protos.FunctionDeclaration(
        name="get_today_schedule",
        description="Retrieve today's calendar events",
        parameters=genai.protos.Schema(
            type=genai.protos.Type.OBJECT,
            properties={}
        )
    ),
    genai.protos.FunctionDeclaration(
        name="save_memo",
        description="Save a memo with a title and content to a file",
        parameters=genai.protos.Schema(
            type=genai.protos.Type.OBJECT,
            properties={
                "title": genai.protos.Schema(type=genai.protos.Type.STRING, description="Memo title"),
                "content": genai.protos.Schema(type=genai.protos.Type.STRING, description="Memo body text")
            },
            required=["title", "content"]
        )
    ),
])]
 
model = genai.GenerativeModel(model_name="gemini-2.0-flash", tools=tools)
chat = model.start_chat()
 
def run_agent(user_message: str) -> str:
    """
    Processes a user message, handling any Function Calls automatically,
    and returns the model's final text response.
    """
    response = chat.send_message(user_message)
 
    for _ in range(10):  # Safety cap to prevent infinite loops
        parts = response.candidates[0].content.parts
 
        # Find a function call part, if any
        fc_part = next(
            (p for p in parts if hasattr(p, "function_call") and p.function_call.name),
            None
        )
 
        if fc_part is None:
            break  # Model returned a text response — we're done
 
        fc = fc_part.function_call
        func = TOOL_FUNCTIONS.get(fc.name)
 
        if func is None:
            print(f"Warning: model requested unknown tool '{fc.name}'")
            break
 
        result = func(**dict(fc.args))
 
        # Feed the result back to the model
        response = chat.send_message(
            genai.protos.Content(parts=[
                genai.protos.Part(function_response=genai.protos.FunctionResponse(
                    name=fc.name,
                    response={"result": result}
                ))
            ])
        )
 
    return response.text
 
# Example run
output = run_agent(
    "Check today's weather in Tokyo and my schedule, then save a briefing memo titled 'Today's Briefing'"
)
print(output)

The key design decision here is centralizing the Function Call loop inside run_agent(). The function keeps feeding results back to the model until it produces a text response, regardless of how many tools get called in between. I prefer this over handling each tool call in the outer code — it keeps the caller clean.

Debugging Function Calling in Antigravity IDE

The most common issue: the model responds with text instead of making a function call. When that happens, print the raw response structure to understand what's going on:

# Debug: inspect the full response structure
response = chat.send_message("What's the weather in Tokyo?")
for i, part in enumerate(response.candidates[0].content.parts):
    print(f"[part {i}] type={type(part).__name__}")
    if hasattr(part, "function_call") and part.function_call.name:
        print(f"  → function_call: {part.function_call.name}")
        print(f"  → args: {dict(part.function_call.args)}")
    elif hasattr(part, "text"):
        print(f"  → text: {part.text[:100]}")

When the model skips a function call, it's almost always one of two things. Either the description field is too vague for the model to recognize this as the right tool for the situation, or the user message doesn't contain enough signal that an external lookup is needed. A prompt like "use the weather API to get Tokyo's current conditions" tends to trigger function calls more reliably than "what's the weather?"

Antigravity's inline completion is genuinely useful when writing genai.protos.Schema definitions — it surfaces the available parameter types and catches typos before they become runtime errors.

Writing Good Tool Descriptions

The quality of your description fields matters more than you might expect. The model uses them to decide whether a tool is relevant to the current request — a vague description leads to missed opportunities, while an overly broad one causes the model to reach for a tool when it shouldn't.

A few principles that work well in practice:

Be specific about the input format. Instead of "a city name", write "a city name in English, such as Tokyo or Osaka". This reduces ambiguous inputs that would cause your function to return empty results.

Describe the output, not just the purpose. "Returns weather data including temperature in Celsius, humidity percentage, and a short condition description" gives the model context to use the result intelligently in its response.

Avoid overlapping descriptions. If two tools sound like they do the same thing, the model may pick the wrong one or call both. Keep each tool's purpose distinct and make the difference explicit in the description.

Here's a before-and-after that illustrates the difference:

# Before: vague, likely to be skipped
genai.protos.FunctionDeclaration(
    name="get_current_weather",
    description="Get weather",  # Too vague
    ...
)
 
# After: specific, triggers reliably
genai.protos.FunctionDeclaration(
    name="get_current_weather",
    description="Retrieve real-time weather conditions for a specific city, returning temperature in Celsius, humidity percentage, and a short weather description (e.g. Sunny, Cloudy, Rainy)",
    ...
)

This kind of description tuning is often the fastest way to improve Function Calling reliability without changing any application logic.

Where to Go From Here

Once Function Calling is working, the natural next step is making the tool implementations production-grade — adding proper error handling, retries, and rate limit management. The Antigravity Python SDK Production Master Guide covers those patterns in detail.

If you want to build toward a more autonomous agent that manages its own state and coordinates multiple specialized agents, the AgentKit 2.0 Complete Guide is a good next read.

Function Calling is one of those features where the gap between "the AI suggests code" and "the AI takes action" starts to close. Try exposing one of your existing utility functions as a tool — that's usually the fastest way to feel the difference.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Tools2026-06-17
Fix PyTorch CUDA Errors: torch.cuda.is_available() False & Version Mismatches (2026)
Struggling with PyTorch or CUDA installation errors? This guide covers version mismatches, dependency conflicts, and GPU detection failures with step-by-step solutions.
AI Tools2026-04-19
Building Japanese NLP Apps with Gemma 4 in Antigravity — Summarization and Sentiment Analysis
Learn how to use Gemma 4's strong Japanese language capabilities in Antigravity to build Python apps for text summarization and sentiment analysis, with step-by-step code examples.
AI Tools2026-04-09
Fixing Hugging Face Transformers Errors — Identifying the Cause and Resolving It
Hugging Face Transformers errors sorted by symptom: ImportError, CUDA OOM, bf16 on unsupported GPUs, gated-model 401s, and cache bloat. How to identify the cause and work through the fix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →