#!/usr/bin/env python3 """Gemini function calling (REST): forced call (mode ANY), functionResponse round trip, parallel calls, parametersJsonSchema. STATUS: LIVE_VERIFIED 2026-09-18 (gemini-3.5-flash-lite; identical requests tmp-live/gemini-tools/a1..a4*.json; < $0.001). Key facts observed: functionCall parts carry `id` (call_…) and a `thoughtSignature`; omitting the signature when echoing the model turn back → HTTP 400 "Function call is missing a thought_signature"; in a parallel turn only the FIRST call has one. Run: .venv/bin/python examples/gemini/tools/function-calling/function_calling.py """ import json, sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[4])) from scripts.live import gemini_request MODEL = "gemini-3.5-flash-lite" PATH = f"/v1beta/models/{MODEL}:generateContent" WEATHER = {"name": "get_weather", "description": "Get the current weather for a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING", "description": "City name"}}, "required": ["city"]}} TIME = {"name": "get_time", "description": "Get the current local time in a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}} # 1. forced call: toolConfig.functionCallingConfig.mode ANY (+ allowedFunctionNames) user = {"role": "user", "parts": [{"text": "Weather in Paris?"}]} st, r, _ = gemini_request("POST", PATH, {"contents": [user], "tools": [{"functionDeclarations": [WEATHER]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY", "allowedFunctionNames": ["get_weather"]}}, "generationConfig": {"maxOutputTokens": 64}}, note="function_calling.py forced") model_turn = r["candidates"][0]["content"] # echo it back VERBATIM (keeps thoughtSignature) fc = next(p for p in model_turn["parts"] if "functionCall" in p) print("HTTP", st, "functionCall:", json.dumps(fc["functionCall"]), "thoughtSignature:", "yes" if fc.get("thoughtSignature") else "no") # 2. functionResponse round trip → final text st, r, _ = gemini_request("POST", PATH, {"contents": [user, model_turn, {"role": "user", "parts": [{"functionResponse": {"name": "get_weather", "response": {"temperature_c": 18, "conditions": "light rain"}}}]}], "tools": [{"functionDeclarations": [WEATHER]}], "generationConfig": {"maxOutputTokens": 32}}, note="function_calling.py functionResponse") print("HTTP", st, "final:", "".join(p.get("text", "") for p in r["candidates"][0]["content"]["parts"])) # 3. parallel calls (two declarations, mode ANY) st, r, _ = gemini_request("POST", PATH, {"contents": [{"role": "user", "parts": [{"text": "What is the weather in Paris and the time in Tokyo? Use both tools."}]}], "tools": [{"functionDeclarations": [WEATHER, TIME]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 128}}, note="function_calling.py parallel") print("HTTP", st, "parallel:", [p["functionCall"]["name"] for p in r["candidates"][0]["content"]["parts"] if "functionCall" in p]) # 4. parametersJsonSchema (plain JSON Schema instead of the OpenAPI subset) decl = {"name": "get_weather", "description": "Get the current weather for a city.", "parametersJsonSchema": {"type": "object", "properties": {"city": {"type": "string"}, "unit": {"type": "string", "enum": ["C", "F"]}}, "required": ["city"]}} st, r, _ = gemini_request("POST", PATH, {"contents": [{"role": "user", "parts": [{"text": "Weather in Rome in Celsius?"}]}], "tools": [{"functionDeclarations": [decl]}], "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "generationConfig": {"maxOutputTokens": 64}}, note="function_calling.py parametersJsonSchema") print("HTTP", st, "jsonschema call:", json.dumps(next(p["functionCall"] for p in r["candidates"][0]["content"]["parts"] if "functionCall" in p)), "usage:", r.get("usageMetadata", {}).get("totalTokenCount"))