~14 min · raw-http, tool-loop, function-calling, no-sdk
Level 0Spark
0 XP0/35 lessons0/10 achievements
0/140 XP to next level140 XP to go0% complete
Same loop, no SDK
The agentic tool loop from Track 5 works identically with raw HTTP. The wire format is exactly what the SDK was building for you. Useful when you're embedded in a constrained environment, debugging a wire-level issue, or implementing the loop in a language without a Gemini SDK.
The shape on the wire
Tool declarations go in the tools field of the request body. The model's response contains functionCall parts; you respond in the next user turn with functionResponse parts. Same as SDK, just spelled out.
Code
Raw HTTP tool loop — ~50 lines·python
import httpx, json
API_KEY = '...' # from env
MODEL = 'gemini-2.5-flash'
BASE = f'https://generativelanguage.googleapis.com/v1beta/models/{MODEL}'
TOOLS = [{
'functionDeclarations': [{
'name': 'get_weather',
'description': 'Get current weather for a location.',
'parameters': {
'type': 'object',
'properties': {
'location': {'type': 'string', 'description': 'City name'},
},
'required': ['location'],
},
}]
}]
def execute(name, args):
if name == 'get_weather':
# Pretend we called a real weather API
return {'temperature_c': 22, 'conditions': 'clear', 'location': args['location']}
return {'error': f'Unknown tool: {name}'}
def run_tool_loop(prompt: str, max_iter: int = 10) -> str:
contents = [{'role': 'user', 'parts': [{'text': prompt}]}]
headers = {'x-goog-api-key': API_KEY, 'Content-Type': 'application/json'}
with httpx.Client(timeout=60) as client:
for _ in range(max_iter):
resp = client.post(
f'{BASE}:generateContent',
headers=headers,
json={'contents': contents, 'tools': TOOLS},
)
resp.raise_for_status()
data = resp.json()
model_content = data['candidates'][0]['content']
parts = model_content.get('parts', [])
calls = [p['functionCall'] for p in parts if 'functionCall' in p]
if not calls:
return ''.join(p.get('text', '') for p in parts)
# Append model's call turn
contents.append(model_content)
# Execute and build user turn
fn_parts = []
for fc in calls:
result = execute(fc['name'], fc.get('args', {}))
fn_parts.append({
'functionResponse': {
'name': fc['name'],
'id': fc.get('id', ''),
'response': {'result': result},
},
})
contents.append({'role': 'user', 'parts': fn_parts})
raise RuntimeError(f'Loop exhausted after {max_iter} iterations')
print(run_tool_loop('What's the weather in Seoul?'))
Same idea via OAuth — wrap the body in 'request'·python
# OAuth version: prepend project, wrap original body in {model, project, request}
def run_tool_loop_oauth(prompt, max_iter=10):
token = get_access_token()
project = load_code_assist(token)
contents = [{'role': 'user', 'parts': [{'text': prompt}]}]
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
}
with httpx.Client(timeout=60) as client:
for _ in range(max_iter):
resp = client.post(
'https://cloudcode-pa.googleapis.com/v1internal:generateContent',
headers=headers,
json={
'model': 'gemini-2.5-flash',
'project': project,
'request': {'contents': contents, 'tools': TOOLS},
},
)
data = resp.json()['response']
model_content = data['candidates'][0]['content']
parts = model_content.get('parts', [])
calls = [p['functionCall'] for p in parts if 'functionCall' in p]
if not calls:
return ''.join(p.get('text', '') for p in parts)
# ... same loop as API key path
Take the raw HTTP loop from the first code block and add a second tool: convert_temperature(value: number, from_unit: string, to_unit: string). Ask Flash: "What's the weather in Seoul, and convert that temperature to Fahrenheit?" Confirm the model chains both tools, and that your response correctly uses functionResponse in user turns.
Progress
Progress is local-only — sign in to sync across devices.