Python 88.3%
TypeScript 7.6%
Shell 4.1%
1#!/usr/bin/env python32"""Generic Gemini generateContent tool loop (functionDeclarations → functionCall parts → functionResponse parts).34STATUS: LIVE_VERIFIED 2026-09-18 — run with `.venv/bin/python examples/shared/tool-loop/gemini_tool_loop.py` (gemini-3.5-flash-lite,5one get_weather + get_time parallel round trip, ≈ 300 tokens, < $0.001). Keys come from .env via scripts/live.py; every request is6logged to reports/live-requests.jsonl.78Patterns implemented (see docs/gemini/tool-loop.md):9 * loop until the model turn contains no functionCall part10 * append the model turn VERBATIM (functionCall parts carry `thoughtSignature`; dropping it → HTTP 40011 "Function call is missing a thought_signature"; only the first parallel call carries one)12 * execute ALL calls of a turn, answer each with a functionResponse{name, response} part (the same `id` order); the whole13 functionResponse batch goes in ONE user Content14 * bounded turns; errors are returned to the model as {"error": …} instead of raising15 * Interactions API variant: same loop but statelessly chained with previous_interaction_id (see examples/gemini/interactions/interactions_tools.py)16"""17from __future__ import annotations1819import json20import sys21from pathlib import Path22from typing import Any, Callable2324ROOT = Path(__file__).resolve().parents[3]25sys.path.insert(0, str(ROOT))26from scripts.live import gemini_request # noqa: E4022728MODEL = "gemini-3.5-flash-lite"293031def run_tool_loop(prompt: str, declarations: list[dict], handlers: dict[str, Callable[[dict], Any]], max_turns: int = 6,32 system: str | None = None) -> str:33 contents: list[dict] = [{"role": "user", "parts": [{"text": prompt}]}]34 body: dict = {"contents": contents, "tools": [{"functionDeclarations": declarations}], "generationConfig": {"maxOutputTokens": 256}}35 if system:36 body["systemInstruction"] = {"parts": [{"text": system}]}37 for turn in range(max_turns):38 st, r, _ = gemini_request("POST", f"/v1beta/models/{MODEL}:generateContent", body, note=f"gemini_tool_loop turn {turn}")39 if st != 200:40 raise RuntimeError(f"HTTP {st}: {json.dumps(r)[:200]}")41 cand = r["candidates"][0]42 model_turn = cand["content"] # keep parts verbatim (thoughtSignature!)43 calls = [p["functionCall"] for p in model_turn.get("parts", []) if "functionCall" in p]44 if not calls:45 return "".join(p.get("text", "") for p in model_turn.get("parts", []))46 responses = []47 for c in calls:48 try:49 out = handlers[c["name"]](c.get("args", {}))50 except Exception as e: # noqa: BLE00151 out = {"error": str(e)}52 fr = {"name": c["name"], "response": out if isinstance(out, dict) else {"result": out}}53 if c.get("id"):54 fr["id"] = c["id"]55 responses.append({"functionResponse": fr})56 print(f" turn {turn}: {c['name']}({json.dumps(c.get('args'))}) -> {json.dumps(fr['response'])[:80]}")57 contents.extend([model_turn, {"role": "user", "parts": responses}])58 return "(max turns reached)"596061if __name__ == "__main__":62 decls = [{"name": "get_weather", "description": "Current weather for a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}},63 {"name": "get_time", "description": "Current local time in a city.", "parameters": {"type": "OBJECT", "properties": {"city": {"type": "STRING"}}, "required": ["city"]}}]64 handlers = {"get_weather": lambda a: {"city": a["city"], "temperature_c": 18, "conditions": "light rain"},65 "get_time": lambda a: {"city": a["city"], "time": "14:05"}}66 print(run_tool_loop("Weather in Paris and time in Tokyo? Use the tools, then answer in one sentence.", decls, handlers))67