C.W.K.
Stream
Lesson 04 of 04 · published

Raw HTTP Tool Loop

~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

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.