Python Tool-Calling Agent: Runnable Code, Failures and Tests
Test the execution loop before connecting a live model. This lab queries a synthetic order with a read-only tool. You should already be able to run Python scripts and read functions and JSON.
#1. Download and run
Use Python 3.10+ and its standard library. Save tool_agent.py↗ and test_tool_agent.py↗ in the same folder:
bashpython3 tool_agent.py python3 -m unittest -v test_tool_agent.py
Verified offline output on 2026-09-07:
json{"mode": "fixture", "answer": "DEMO-1001: shipped (synthetic_fixture)", "steps": 2}
Nine tests cover the normal loop, missing orders, unknown tools, invalid JSON, invalid argument shapes, repeated calls, excessive parallel calls, empty answers and a provider timeout. The scripted model substitute verifies program behaviour, not LLM reasoning or live API availability.
#2. Understand the three responsibilities
| Function | Responsibility |
|---|---|
execute_tool(name, arguments) | Allow only lookup_order; validate exact argument fields and types |
run_agent(model, question) | Preserve output items, execute calls, return observations by call_id, limit turns |
fixture_model / live_model | Supply deterministic test output or call the real Responses API |
There is no arbitrary code execution. A model request for delete_order returns unknown_tool.
#3. Follow a request and observation
The model adapter returns:
json{"type":"function_call","call_id":"demo-call-1","name":"lookup_order","arguments":"{\"order_id\":\"DEMO-1001\"}"}
The program validates the request and adds a function_call_output item using the same call ID. Its output is a JSON string containing:
json{"ok":true,"order_id":"DEMO-1001","status":"shipped","source":"synthetic_fixture"}
The next model turn reads this observation before answering. The script preserves the full output array, including returned reasoning items, instead of retaining only function names. Official function-calling guide↗
#4. Make the tool fail
bashpython3 -c 'from tool_agent import execute_tool; print(execute_tool("lookup_order", "{\"order_id\":\"MISSING\"}"))'
The result is {'ok': False, 'error': 'order_not_found'}. A missing record is not evidence that an order is still being prepared. Never substitute a plausible status for an error.
A model that requests tools forever reaches step_limit_reached. An empty response raises model_returned_no_answer. Neither is reported as success.
#5. Connect a real model
Configure OPENAI_API_KEY and OPENAI_MODEL through your own secure environment setup. Choose a model available to your account that supports tool calling. Do not put keys in source files or screenshots.
bashpython3 tool_agent.py --live --question 'Look up DEMO-1001. Do not guess its status.'
This sends real requests to the OpenAI Responses API and can incur charges. The interface was checked against official documentation; live mode was not executed in this verification. Validate account access, model compatibility, actual output and cost yourself. The tool still uses a synthetic order and never accesses customer records. In default fixture mode, --question changes the history but not the scripted tool selection.
Each live request has a 20-second timeout. Authentication, rate-limit and network failures surface without automatic retries. Production work also needs a total time budget, cancellation, redacted logs and cost limits.
#6. Before using business data
- Authorise which records the signed-in user may access on the server; never trust a model-supplied record ID alone.
- Replace the synthetic dictionary with a read-only API and distinguish missing, forbidden and timed-out results.
- Evaluate correct lookups, unknown records, unauthorised requests and attempts to invoke unsupported tools.
- Add separate authorisation and confirmation before introducing refunds or outgoing messages.
Continue with evaluation, return to the Agent learning route, or review the AI Engineer Bootcamp curriculum and trial information.