#!/usr/bin/env python """Tool-Using Agent — ask a question and watch the AI call real, live tools to answer it: a calculator, the current date/time, live weather, and Wikipedia. It runs the ReAct loop (decide → call tool → read result → repeat) and shows every tool call it made. All tools are keyless (Open-Meteo + MediaWiki), so the only secret is ANTHROPIC_API_KEY. pip install flask requests anthropic python app.py # http://127.0.0.1:7860 Project #9 of the "30 Projects in 15 Days" challenge — GritAI. UI: GritAI engineered/industrial design system (dark, grit-orange, IBM Plex + Archivo). """ import os, ast, json, math, operator, datetime, urllib.parse from flask import Flask, request, jsonify, Response import requests PORT = int(os.environ.get("PORT", "7860")) ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5") MAX_STEPS = 6 UA = {"User-Agent": "GritAI-ToolAgent/1.0 (https://gritai.solutions)"} # --------------------------------------------------------------------------- tools _OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.Mod: operator.mod, ast.FloorDiv: operator.floordiv, ast.USub: operator.neg, ast.UAdd: operator.pos} _FUNCS = {"sqrt": math.sqrt, "abs": abs, "round": round, "log": math.log, "log10": math.log10, "sin": math.sin, "cos": math.cos, "tan": math.tan, "pi": math.pi, "e": math.e, "floor": math.floor, "ceil": math.ceil, "exp": math.exp, "min": min, "max": max} def _safe_eval(node): if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value raise ValueError("only numbers") if isinstance(node, ast.BinOp): return _OPS[type(node.op)](_safe_eval(node.left), _safe_eval(node.right)) if isinstance(node, ast.UnaryOp): return _OPS[type(node.op)](_safe_eval(node.operand)) if isinstance(node, ast.Name): if node.id in _FUNCS: return _FUNCS[node.id] raise ValueError(f"unknown name {node.id}") if isinstance(node, ast.Call): fn = _FUNCS.get(getattr(node.func, "id", "")) if not callable(fn): raise ValueError("unknown function") return fn(*[_safe_eval(a) for a in node.args]) raise ValueError("unsupported expression") def tool_calculator(expression): try: val = _safe_eval(ast.parse(str(expression), mode="eval").body) return f"{expression} = {val}" except Exception as e: return f"Could not evaluate '{expression}': {e}" def tool_datetime(timezone="UTC"): now = datetime.datetime.now(datetime.timezone.utc) return f"Current UTC date/time: {now.strftime('%A, %d %B %Y, %H:%M UTC')}" _WMO = {0: "clear sky", 1: "mainly clear", 2: "partly cloudy", 3: "overcast", 45: "fog", 48: "rime fog", 51: "light drizzle", 53: "drizzle", 55: "dense drizzle", 61: "light rain", 63: "rain", 65: "heavy rain", 71: "light snow", 73: "snow", 75: "heavy snow", 80: "rain showers", 81: "rain showers", 82: "violent rain showers", 95: "thunderstorm", 96: "thunderstorm w/ hail"} def tool_weather(location): try: g = requests.get("https://geocoding-api.open-meteo.com/v1/search", params={"name": location, "count": 1}, headers=UA, timeout=15).json() if not g.get("results"): return f"No location found for '{location}'." r = g["results"][0] lat, lon = r["latitude"], r["longitude"] place = ", ".join(filter(None, [r.get("name"), r.get("admin1"), r.get("country")])) w = requests.get("https://api.open-meteo.com/v1/forecast", params={"latitude": lat, "longitude": lon, "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m"}, headers=UA, timeout=15).json() c = w.get("current", {}) desc = _WMO.get(c.get("weather_code"), "unknown") return (f"Weather in {place}: {c.get('temperature_2m')}°C, {desc}, " f"humidity {c.get('relative_humidity_2m')}%, wind {c.get('wind_speed_10m')} km/h.") except Exception as e: return f"Weather lookup failed: {e}" def tool_wikipedia(query): try: s = requests.get("https://en.wikipedia.org/w/api.php", headers=UA, timeout=15, params={"action": "query", "list": "search", "srsearch": query, "format": "json", "srlimit": 1}).json() hits = s.get("query", {}).get("search", []) if not hits: return f"No Wikipedia article found for '{query}'." title = hits[0]["title"] e = requests.get("https://en.wikipedia.org/w/api.php", headers=UA, timeout=15, params={"action": "query", "prop": "extracts", "exintro": 1, "explaintext": 1, "titles": title, "format": "json"}).json() pages = e.get("query", {}).get("pages", {}) extract = next(iter(pages.values()), {}).get("extract", "") return f"Wikipedia — {title}: {extract[:800]}" if extract else f"Found '{title}' but no summary." except Exception as e: return f"Wikipedia lookup failed: {e}" TOOLS_SPEC = [ {"name": "calculator", "description": "Evaluate an arithmetic expression. Supports + - * / ** % and " "sqrt, log, sin, cos, tan, floor, ceil, abs, round, pi, e.", "input_schema": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}}, {"name": "current_datetime", "description": "Get the current date and time (UTC).", "input_schema": {"type": "object", "properties": {"timezone": {"type": "string"}}}}, {"name": "weather", "description": "Get the current weather for a city/place name (live).", "input_schema": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}}, {"name": "wikipedia", "description": "Look up a topic on Wikipedia and get a summary (live).", "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}, ] EXECUTORS = {"calculator": tool_calculator, "current_datetime": tool_datetime, "weather": tool_weather, "wikipedia": tool_wikipedia} def run_agent(question): import anthropic client = anthropic.Anthropic(api_key=ANTHROPIC_KEY) sysp = ("You are a helpful assistant that answers using the provided tools whenever they help. " "Call tools to get live/precise data instead of guessing. You may call multiple tools. " "When you have enough, give a clear, concise final answer.") msgs = [{"role": "user", "content": question}] trace = [] for _ in range(MAX_STEPS): r = client.messages.create(model=ANTHROPIC_MODEL, max_tokens=1024, system=sysp, tools=TOOLS_SPEC, messages=msgs) if r.stop_reason == "tool_use": msgs.append({"role": "assistant", "content": r.content}) results = [] for b in r.content: if b.type == "tool_use": out = EXECUTORS.get(b.name, lambda **k: "unknown tool")(**b.input) trace.append({"tool": b.name, "input": b.input, "result": out[:400]}) results.append({"type": "tool_result", "tool_use_id": b.id, "content": out}) msgs.append({"role": "user", "content": results}) else: text = "".join(b.text for b in r.content if b.type == "text") return text.strip(), trace return "I reached the step limit before finishing — try a simpler question.", trace app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 512 * 1024 @app.route("/") def home(): return Response(PAGE, mimetype="text/html") @app.route("/api/ask", methods=["POST"]) def api_ask(): q = (request.get_json(force=True).get("question") or "").strip()[:1000] if not q: return jsonify(error="Ask a question."), 400 if not ANTHROPIC_KEY: return jsonify(error="Set ANTHROPIC_API_KEY on the server to run the agent."), 200 try: answer, trace = run_agent(q) except Exception as e: import sys; print("agent error:", type(e).__name__, file=sys.stderr, flush=True) return jsonify(error="The agent hit an error — please try again."), 200 return jsonify(answer=answer, trace=trace) # ===================== GritAI engineered/industrial design system ===================== PAGE = """ GritAI · Tool-Using Agent
GRITAI
Tool-Using Agent
▸ LiveSDVOSBLawton OK

Ask it anything. It calls real tools to answer.

A calculator, a clock, live weather, and Wikipedia — the agent decides what to call, runs it, and shows its work. Not a chatbot guessing. An operator.

01Ask
>_
02Trace — live tool calls
03Answer
""" if __name__ == "__main__": backend = "Claude API" if ANTHROPIC_KEY else "NO KEY (set ANTHROPIC_API_KEY)" print(f"Tool-Using Agent on http://127.0.0.1:{PORT} [backend: {backend}]", flush=True) app.run(host="0.0.0.0", port=PORT, threaded=True)