Skip to content

Instantly share code, notes, and snippets.

@Hassan-Naeem-code
Last active April 21, 2026 01:27
Show Gist options
  • Select an option

  • Save Hassan-Naeem-code/f265b0ee835d64f61f997b1fa99a8122 to your computer and use it in GitHub Desktop.

Select an option

Save Hassan-Naeem-code/f265b0ee835d64f61f997b1fa99a8122 to your computer and use it in GitHub Desktop.
Python: AI agent tool-calling loop from scratch — no LangChain, no framework, just the core loop
"""
Agent tool-calling loop from scratch — no LangChain, no framework.
Shows exactly what's happening when an "AI agent" uses tools:
1. Send user message + tool definitions to the model
2. If the model asks to call a tool, run it locally
3. Send the tool's result back
4. Repeat until the model gives a final answer
Install:
pip install openai requests
Env:
export OPENAI_API_KEY=sk-...
"""
import os
import json
import requests
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
MODEL = "gpt-4o-mini"
# --- Tool definitions (what the model sees) ---
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current temperature for a city in Celsius.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "Evaluate a simple arithmetic expression.",
"parameters": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
},
]
# --- Tool implementations (what actually runs locally) ---
def get_weather(city: str) -> str:
# Open-Meteo geocoding + forecast, no API key required
geo = requests.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1},
timeout=10,
).json()
if not geo.get("results"):
return f"Could not find city: {city}"
loc = geo["results"][0]
fc = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": loc["latitude"], "longitude": loc["longitude"], "current_weather": True},
timeout=10,
).json()
temp = fc["current_weather"]["temperature"]
return f"{city}: {temp}°C"
def calculator(expression: str) -> str:
try:
# safe enough for a demo; use a real parser in production
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return "Error: expression contains disallowed characters."
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
TOOL_IMPLS = {"get_weather": get_weather, "calculator": calculator}
def run_tool(name: str, args: dict) -> str:
fn = TOOL_IMPLS.get(name)
if not fn:
return f"Unknown tool: {name}"
return fn(**args)
# --- The loop ---
def agent(user_message: str, max_steps: int = 8) -> str:
messages = [{"role": "user", "content": user_message}]
for step in range(max_steps):
res = client.chat.completions.create(
model=MODEL,
tools=TOOLS,
messages=messages,
)
msg = res.choices[0].message
# Always append the assistant turn so tool_call_ids line up
messages.append(msg.model_dump(exclude_none=True))
# No tool calls? We're done.
if not msg.tool_calls:
return msg.content or ""
for call in msg.tool_calls:
args = json.loads(call.function.arguments or "{}")
print(f"[step {step}] → {call.function.name}({args})")
result = run_tool(call.function.name, args)
print(f"[step {step}] ← {result}")
messages.append(
{"role": "tool", "tool_call_id": call.id, "content": result}
)
return "[agent halted: max steps reached]"
if __name__ == "__main__":
print(agent("What's the weather in Lahore right now, and what is 18 * 47?"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment