{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The Core Agent Loop — build a ReAct agent from scratch\n",
    "\n",
    "### Agentic AI · Chapter 4 companion notebook\n",
    "\n",
    "In the chapter you saw the one idea everything else hangs off:\n",
    "\n",
    "> **perceive → think → act → observe**, over and over, until the task is done.\n",
    "\n",
    "Reading about it is one thing. In the next few cells **you'll build that loop yourself** — no LangChain, no CrewAI, no magic. Just a `for` loop, two tools, and a model that reasons out loud before every move (that's the **ReAct** pattern: *reasoning + acting*).\n",
    "\n",
    "Think of it like teaching a new intern. You don't hand them a 400-page manual. You give them a couple of tools, a goal, and one rule: *say what you're about to do before you do it.* That rule is the whole game — it stops impulsive moves and leaves a paper trail you can debug.\n",
    "\n",
    "**What you'll build**\n",
    "\n",
    "1. Two tools the agent is allowed to use — a `calculator` and a `get_weather` lookup.\n",
    "2. The ReAct loop itself — about a dozen lines, exactly like the chapter's `agent_loop.py`.\n",
    "3. A run you can *read*: the agent will print its **Action → Observation** trace as it works.\n",
    "\n",
    "By the end you'll have watched a model answer a question it can't possibly know on its own — by reaching for a tool, reading the result, and reasoning its way to the answer.\n",
    "\n",
    "Let's wire it up. ⚡"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 · Setup — install the SDK and add your key\n",
    "\n",
    "We'll use Google's **Gemini** models through the `google-genai` SDK. Gemini has a genuinely free tier — no credit card required — which is perfect for learning.\n",
    "\n",
    "**Grab a free API key** (takes 30 seconds):\n",
    "\n",
    "1. Go to **[aistudio.google.com/apikey](https://aistudio.google.com/apikey)**\n",
    "2. Click **Create API key** and copy it.\n",
    "\n",
    "**Where to put it** — two options, and the code below handles both:\n",
    "\n",
    "- **Colab Secrets (recommended):** click the 🔑 key icon in the left sidebar, add a secret named `GEMINI_API_KEY`, paste your key, and toggle *Notebook access* on. It stays out of your notebook, so you can share the file safely.\n",
    "- **Fallback:** if there's no secret, the cell will just prompt you to paste the key (hidden as you type).\n",
    "\n",
    "First, install the SDK:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -q google-genai"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now load the key. The `try/except` means this same cell works whether you used a Colab Secret or want to paste the key by hand:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from getpass import getpass\n",
    "\n",
    "# Try Colab Secrets first; fall back to a hidden prompt if that isn't available.\n",
    "try:\n",
    "    from google.colab import userdata\n",
    "    KEY = userdata.get('GEMINI_API_KEY')\n",
    "except Exception:\n",
    "    KEY = None\n",
    "\n",
    "if not KEY:\n",
    "    KEY = getpass('Gemini API key: ')\n",
    "\n",
    "# Stash it in the environment too, in case any tooling looks for it there.\n",
    "os.environ['GEMINI_API_KEY'] = KEY\n",
    "print('Key loaded ✓' if KEY else 'No key found — re-run this cell.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 · Give the agent two tools\n",
    "\n",
    "A tool is just a normal Python function — but the model can't *see* your code. So for each tool we hand the model a little **menu card**: a `FunctionDeclaration` describing the tool's name, what it does, and what arguments it takes. The model reads the menu, decides which tool it wants, and tells us how to call it. **We** run the actual Python.\n",
    "\n",
    "Two halves to every tool:\n",
    "\n",
    "- **The declaration** — the menu card the model reads (name + description + arguments).\n",
    "- **The implementation** — the real Python function that does the work.\n",
    "\n",
    "Our two tools:\n",
    "\n",
    "- `calculator(expression)` — evaluates arithmetic like `\"47 * 23\"`. Handy because LLMs are famously shaky at exact math.\n",
    "- `get_weather(city)` — returns a *canned* weather report. It's a mock (no real API), but the agent doesn't know or care — to the model it's live data it couldn't have known on its own.\n",
    "\n",
    "First, the client and the tool declarations:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from google import genai\n",
    "from google.genai import types\n",
    "\n",
    "client = genai.Client(api_key=KEY)\n",
    "MODEL = \"gemini-2.0-flash\"   # fast + free-tier friendly\n",
    "\n",
    "# --- Menu card #1: the calculator ---\n",
    "calc_decl = types.FunctionDeclaration(\n",
    "    name=\"calculator\",\n",
    "    description=\"Evaluate an arithmetic expression like '23*17+5'. Use this for any exact math.\",\n",
    "    parameters=types.Schema(\n",
    "        type=types.Type.OBJECT,\n",
    "        properties={\n",
    "            \"expression\": types.Schema(\n",
    "                type=types.Type.STRING,\n",
    "                description=\"A Python arithmetic expression, e.g. '47 * 23'.\",\n",
    "            ),\n",
    "        },\n",
    "        required=[\"expression\"],\n",
    "    ),\n",
    ")\n",
    "\n",
    "# --- Menu card #2: the weather lookup ---\n",
    "weather_decl = types.FunctionDeclaration(\n",
    "    name=\"get_weather\",\n",
    "    description=\"Get the current weather for a city. Use this when asked about live weather conditions.\",\n",
    "    parameters=types.Schema(\n",
    "        type=types.Type.OBJECT,\n",
    "        properties={\n",
    "            \"city\": types.Schema(\n",
    "                type=types.Type.STRING,\n",
    "                description=\"The city name, e.g. 'Tokyo'.\",\n",
    "            ),\n",
    "        },\n",
    "        required=[\"city\"],\n",
    "    ),\n",
    ")\n",
    "\n",
    "# Bundle both declarations into a Tool, and tell Gemini we'll run the tools ourselves.\n",
    "tools = types.Tool(function_declarations=[calc_decl, weather_decl])\n",
    "config = types.GenerateContentConfig(\n",
    "    tools=[tools],\n",
    "    # IMPORTANT: turn OFF automatic function calling so WE drive the loop by hand.\n",
    "    # That's the whole point of this chapter — we want to see every step.\n",
    "    automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),\n",
    ")\n",
    "\n",
    "print(\"Client ready ✓  Tools declared: calculator, get_weather\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now the **implementations** — the actual Python the model can ask us to run:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculator(expression):\n",
    "    # eval() is fine for a teaching demo like this — NEVER do this on untrusted\n",
    "    # input in production. The empty builtins dict blocks access to dangerous names.\n",
    "    return eval(expression, {\"__builtins__\": {}}, {})\n",
    "\n",
    "\n",
    "def get_weather(city):\n",
    "    # A mock. In a real agent this would call a weather API — but the agent\n",
    "    # can't tell the difference, and that's the point.\n",
    "    fake = {\n",
    "        \"Tokyo\":  {\"temp_c\": 18, \"conditions\": \"light rain\", \"humidity\": 82},\n",
    "        \"Austin\": {\"temp_c\": 34, \"conditions\": \"sunny\",      \"humidity\": 40},\n",
    "    }\n",
    "    return fake.get(city, {\"temp_c\": 21, \"conditions\": \"clear\", \"humidity\": 55})\n",
    "\n",
    "\n",
    "# The lookup table the loop uses to turn a tool *name* into a real function.\n",
    "TOOLS = {\"calculator\": calculator, \"get_weather\": get_weather}\n",
    "\n",
    "# Quick sanity check:\n",
    "print(calculator(\"47 * 23\"))       # -> 1081\n",
    "print(get_weather(\"Tokyo\"))         # -> {'temp_c': 18, ...}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 · The ReAct loop — the whole point\n",
    "\n",
    "Here it is: the dozen lines the entire chapter was building toward. Read it slowly, because once this clicks, every advanced agent idea is just *variations on this theme*.\n",
    "\n",
    "The rhythm, phase by phase:\n",
    "\n",
    "- **Perceive** — we seed the conversation with the user's goal.\n",
    "- **Think** — `generate_content` sends everything so far to the model; it reasons about the next move.\n",
    "- **Act** — if the reply contains a `function_call`, the model wants a tool. We look it up and run it.\n",
    "- **Observe** — we feed the tool's result back into the conversation so the model can see what happened.\n",
    "- …then the `for` loop carries us back to **Think** again. When the model replies with *plain text* instead of a tool call, it's done — that's the final answer.\n",
    "\n",
    "`MAX_STEPS` is the **safety belt** the chapter warned about: without it, one bad loop becomes thousands of API calls. We cap it and move on.\n",
    "\n",
    "We wrap it all in a `run_agent(goal)` function so you can call it with any question."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MAX_STEPS = 6   # safety belt — never loop forever\n",
    "\n",
    "def run_agent(goal):\n",
    "    # PERCEIVE — seed the conversation with the user's goal.\n",
    "    contents = [types.Content(role=\"user\", parts=[types.Part(text=goal)])]\n",
    "    print(f\"🧑 Goal: {goal}\\n\" + \"-\" * 60)\n",
    "\n",
    "    for step in range(MAX_STEPS):\n",
    "        # THINK — the model reads everything so far and decides the next move.\n",
    "        resp = client.models.generate_content(\n",
    "            model=MODEL, contents=contents, config=config\n",
    "        )\n",
    "        part = resp.candidates[0].content.parts[0]\n",
    "        fc = getattr(part, \"function_call\", None)\n",
    "\n",
    "        if fc:\n",
    "            # ACT — the model wants a tool. Run it.\n",
    "            args = dict(fc.args)\n",
    "            print(f\"🔧 Action:      {fc.name}({args})\")\n",
    "            result = TOOLS[fc.name](**args)\n",
    "            print(f\"👁  Observation: {result}\")\n",
    "\n",
    "            # OBSERVE — append BOTH the model's request and the tool's result,\n",
    "            # so the next THINK step can see what came back.\n",
    "            contents.append(resp.candidates[0].content)\n",
    "            contents.append(types.Content(\n",
    "                role=\"user\",\n",
    "                parts=[types.Part.from_function_response(\n",
    "                    name=fc.name, response={\"result\": result}\n",
    "                )],\n",
    "            ))\n",
    "            # loop back around and think again\n",
    "        else:\n",
    "            # No tool call → the model is answering. We're done.\n",
    "            print(\"-\" * 60)\n",
    "            print(f\"✅ Answer: {part.text}\")\n",
    "            return part.text\n",
    "\n",
    "    print(\"⚠️  Hit MAX_STEPS without a final answer (safety belt caught it).\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4 · Run it and read the trace\n",
    "\n",
    "Time for a goal that the model **cannot** answer on its own — it deliberately needs *both* tools plus a little reasoning:\n",
    "\n",
    "> *\"What's the weather in Tokyo, and should I bring an umbrella? Also, what is 47 × 23?\"*\n",
    "\n",
    "Watch the trace print as it runs. You'll see the agent:\n",
    "\n",
    "1. Reach for `get_weather(\"Tokyo\")`, read the rain, and reason its way to *yes, bring an umbrella*.\n",
    "2. Reach for `calculator(\"47 * 23\")` for the exact number.\n",
    "3. Stop and give a final answer once both sub-goals are met.\n",
    "\n",
    "That back-and-forth — **Action → Observation → Action → Observation → Answer** — *is* the ReAct pattern you saw animate in the chapter. Run it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "goal = (\"What's the weather in Tokyo, and should I bring an umbrella? \"\n",
    "        \"Also, what is 47 * 23?\")\n",
    "\n",
    "run_agent(goal)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Read the trace like a story.** Each `🔧 Action` line is the agent deciding to *do* something; each `👁 Observation` is it *reading what came back*. Notice it never guesses the weather and never does the multiplication in its head — it uses a tool, observes, and only then answers. That's exactly the behavior the chapter's ReAct animation was showing you.\n",
    "\n",
    "Try changing `goal` above and re-running. A few to try:\n",
    "\n",
    "- `\"What's 1234 * 5678 divided by 3?\"` — pure calculator.\n",
    "- `\"Is it hotter in Tokyo or Austin right now?\"` — two weather calls, then a comparison.\n",
    "- `\"What's the weather in Austin?\"` — watch it use a city that's in our mock table."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5 · 🎯 Your turn — add a third tool\n",
    "\n",
    "You've now built a working ReAct agent. The best way to lock it in is to **extend it**. Add a third tool and watch the loop pick it up with zero changes to the loop itself — because everything just plugs into the loop.\n",
    "\n",
    "**Challenge: add a `get_today()` tool** that returns the current date.\n",
    "\n",
    "Here's the recipe (you've done every step already):\n",
    "\n",
    "1. **Declaration** — write a `today_decl = types.FunctionDeclaration(...)`. It takes *no* arguments, so use an empty `properties={}` and drop `required`.\n",
    "2. **Implementation** — `def get_today(): return {\"date\": str(datetime.date.today())}` (remember `import datetime`).\n",
    "3. **Register it** — add it to the `function_declarations` list *and* to the `TOOLS` dict.\n",
    "4. **Re-run** the setup + loop cells, then ask something like *\"What's today's date, and what's 100 minus 37?\"*\n",
    "\n",
    "**Stretch goals** once that works:\n",
    "\n",
    "- Add a `word_count(text)` tool and ask it to count the words in a sentence.\n",
    "- Print the model's *reasoning* too: some replies include a text part alongside the tool call — try printing `part.text` when it exists to see the agent \"think out loud.\"\n",
    "- Lower `MAX_STEPS` to `1` and give it a two-tool goal — watch the safety belt catch it mid-task.\n",
    "\n",
    "Whatever you build, the loop stays the same. That's the master key from the chapter: **tools, memory, RAG, multi-agent systems — they all just hang off this one loop.**\n",
    "\n",
    "Next chapter goes deep on **Tools** — where they come from, how to design good ones, and how the model decides which to reach for."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "*Part of the Agentic AI course · Chapter 4*"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "name": "ch4-react-loop.ipynb",
   "provenance": [],
   "toc_visible": true
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}