{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c00",
   "metadata": {},
   "source": [
    "# Practice — Core Code Patterns, from One Call to an Agent Loop\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ralbu85/stml_2026/blob/main/lectures/week05/W5_practice_core_patterns.ipynb)\n",
    "\n",
    "**Goal.** Write, without looking them up, the code patterns every lab is built from: (1) a model call, (2) the message list, (3) a prompt template in a function, (4) JSON output, (5) a tool call: request, run, return, (6) the agent loop, (7) ReAct, and (8) an agent kept in a class. The tools are easy ones: live weather, a calculator, and Wikipedia search.\n",
    "\n",
    "**Cell labels.** The first line of each code cell says what to do with it:\n",
    "\n",
    "- `# ✍️ TYPE` — an exercise: write the code between the `FILL IN` lines yourself, without copying from above.\n",
    "- `# ✍️ WRITE` — answer in words (not checked).\n",
    "- `# ▶ RUN · example` — the pattern being taught: read each line, then run.\n",
    "- `# ▶ RUN · setup` or `# ▶ RUN · check` — support cells: just run.\n",
    "\n",
    "About 85 minutes. Not collected. Needs basic Python: lists, dictionaries, functions, `for` loops, f-strings."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c01",
   "metadata": {},
   "source": [
    "## 0. Setup\n",
    "\n",
    "*Do:* run the three cells; paste your API key into the second. The third prints `ready`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c02",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "%pip install -q \"openai==1.109.1\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "import os\n",
    "\n",
    "OPENAI_API_KEY = \"PASTE-YOUR-KEY-HERE\"   # paste your key between the quotes\n",
    "if OPENAI_API_KEY != \"PASTE-YOUR-KEY-HERE\":\n",
    "    os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY   # the client reads the key from here"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c04",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "import json\n",
    "from openai import OpenAI\n",
    "\n",
    "client = OpenAI()                  # the OpenAI library; reads OPENAI_API_KEY from the environment\n",
    "MODEL = \"gpt-4.1\"\n",
    "done = {}                          # the check cells record your results here\n",
    "\n",
    "response = client.chat.completions.create(\n",
    "    model=MODEL,\n",
    "    messages=[{\"role\": \"user\", \"content\": \"Reply with exactly: ready\"}],\n",
    ")\n",
    "response.choices[0].message.content"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c05",
   "metadata": {},
   "source": [
    "## 1. One call and its reply\n",
    "\n",
    "```python\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "reply = response.choices[0].message.content\n",
    "```\n",
    "\n",
    "`messages` is a list of dictionaries; `response` is an object. In `.choices[0].message.content`, `choices` is a list of replies (one by default), `[0]` takes the first, `.message` is that reply, and `.content` is its text.\n",
    "\n",
    "### 1.1 Look inside the response\n",
    "\n",
    "*Do:* run the cell. Find the reply text inside the printed message, and the token count in `usage`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c06",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "messages = [{\"role\": \"user\", \"content\": \"In one sentence, what is a cancelled order in an online shop?\"}]\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "\n",
    "print(type(response))                        # an object\n",
    "print(response.choices[0].message)           # the whole reply: role, content, tool_calls, ...\n",
    "print(response.choices[0].message.content)   # only the text\n",
    "print(response.usage)                        # tokens counted for this call (what you pay for)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c07",
   "metadata": {},
   "source": [
    "### 1.2 ✍️ Write a call\n",
    "\n",
    "*Do:* the message list is given; write the call and read the reply text into `reply_1`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c08",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "messages = [{\"role\": \"user\", \"content\": \"Reply with exactly: PASS\"}]\n",
    "### FILL IN (START) ###\n",
    "reply_1 = \"\"        # call the model with messages and read the reply text\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c09",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "done[\"1.2\"] = reply_1 == \"PASS\"\n",
    "print(done[\"1.2\"], reply_1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c10",
   "metadata": {},
   "source": [
    "### 1.3 ✍️ Read another field of the response\n",
    "\n",
    "*Do:* the call is given; read the call's total token count into `tokens_2` (it is in `response.usage`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c11",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "messages = [{\"role\": \"user\", \"content\": \"In one sentence, what does a shop's order status mean?\"}]\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "reply_2 = response.choices[0].message.content\n",
    "### FILL IN (START) ###\n",
    "tokens_2 = 0        # read the call's total token count\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c12",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "done[\"1.3\"] = reply_2 != \"\" and tokens_2 > 0\n",
    "print(done[\"1.3\"], tokens_2, reply_2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c13",
   "metadata": {},
   "source": [
    "## 2. The message list\n",
    "\n",
    "A call sees only the list you send; the model keeps nothing between calls. Each message has a **role** that says who wrote it. The table lists every kind of text a call can carry; the last two appear in Part 5.\n",
    "\n",
    "| Kind | In the code | Written by | Holds |\n",
    "|---|---|---|---|\n",
    "| **System** message | `{\"role\": \"system\", \"content\": ...}`, first in the list | you, once | standing instructions for the whole conversation |\n",
    "| **User** message | `{\"role\": \"user\", \"content\": ...}` | you, for each request | the question or task, with any data it needs |\n",
    "| **Assistant** message | `{\"role\": \"assistant\", ...}` | the model; you copy it into the list | a reply, or a tool request (Part 5) |\n",
    "| **Tool** message | `{\"role\": \"tool\", \"tool_call_id\": ..., \"content\": ...}` | your program (Part 5) | the result of running a requested function |\n",
    "| **Tool schema** | `tools=[...]`, next to `messages`, not in it | you (Part 5) | the name, purpose, and arguments of a function the model may request |\n",
    "\n",
    "An **instruction** (text telling the model what to do) applies where you put it: in the system message, to every turn; in a user message, to that request; in a tool schema, whenever the model considers that tool. Names ending in `_SYSTEM` hold system messages; names ending in `_TOOL` hold tool schemas.\n",
    "\n",
    "```python\n",
    "messages = [{\"role\": \"system\", \"content\": STAFF_SYSTEM},\n",
    "            {\"role\": \"user\", \"content\": QUESTION}]\n",
    "messages.append({\"role\": \"assistant\", \"content\": reply})   # keep the model's reply\n",
    "messages.append({\"role\": \"user\", \"content\": NEXT_QUESTION})\n",
    "```\n",
    "\n",
    "### 2.1 Watch the list grow\n",
    "\n",
    "`show_messages` prints the list you send: each message's position, role, and text, with `<- NEW` on what was just added. You will use it from here to the end.\n",
    "\n",
    "*Do:* run the cells one by one. Each cell adds to the list and prints it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c14",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "def show_messages(messages, new_from=0):\n",
    "    \"\"\"Print the message list: position, role, and text. Messages at new_from and later are marked NEW.\"\"\"\n",
    "    for i in range(len(messages)):\n",
    "        m = messages[i]\n",
    "        if \"tool_calls\" in m:                              # an assistant message that holds requests\n",
    "            text = \"\"\n",
    "            for c in m[\"tool_calls\"]:\n",
    "                text = text + \"request \" + c[\"function\"][\"name\"] + \" \" + c[\"function\"][\"arguments\"] + \"  \"\n",
    "        elif m[\"role\"] == \"tool\":\n",
    "            text = str(json.loads(m[\"content\"])).replace(\"\\n\", \" \")   # the result, decoded from JSON text\n",
    "        else:\n",
    "            text = str(m[\"content\"]).replace(\"\\n\", \" \")   # keep each message on one line\n",
    "        mark = \"\"\n",
    "        if i >= new_from:\n",
    "            mark = \"   <- NEW\"\n",
    "        print(\"[\" + str(i) + \"]\", m[\"role\"], \"|\", text[:110] + mark)\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c15",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 1 - the list we send: a system message and a user message\n",
    "STAFF_SYSTEM = \"You answer questions from shop staff. Answer in at most two sentences.\"\n",
    "\n",
    "messages = [\n",
    "    {\"role\": \"system\", \"content\": STAFF_SYSTEM},              # SYSTEM message: standing instruction\n",
    "    {\"role\": \"user\", \"content\": \"A customer ordered 2 mugs and 1 desk lamp. List the items in the order.\"},   # USER message\n",
    "]\n",
    "show_messages(messages)                                       # 2 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c16",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 2 - call the model and keep its reply\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "reply = response.choices[0].message.content\n",
    "messages.append({\"role\": \"assistant\", \"content\": reply})      # ASSISTANT message\n",
    "show_messages(messages, new_from=2)                           # 3 messages"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c17",
   "metadata": {},
   "source": [
    "### 2.2 Continue the conversation\n",
    "\n",
    "The follow-up says only \"the lamp\"; the model can resolve it because the earlier turns are in the list it receives.\n",
    "\n",
    "*Do:* run the two cells. The reply should list only the 2 mugs. To rerun, start again from 2.1, or the list grows twice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c18",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 3 - add the follow-up question\n",
    "messages.append({\"role\": \"user\", \"content\": \"The lamp was cancelled. What is left in the order?\"})   # USER message\n",
    "show_messages(messages, new_from=3)                           # 4 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c19",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 4 - call the model with the whole list and keep its reply\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "reply = response.choices[0].message.content\n",
    "messages.append({\"role\": \"assistant\", \"content\": reply})      # ASSISTANT message\n",
    "show_messages(messages, new_from=4)                           # 5 messages"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c20",
   "metadata": {},
   "source": [
    "### 2.3 ✍️ One more turn\n",
    "\n",
    "*Do:* the question and the call are given; add the reply to the list as an assistant message. The check shows the list: 7 messages, the last two NEW."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c21",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "messages.append({\"role\": \"user\", \"content\": \"The customer adds 3 notebooks. What is in the order now?\"})\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "reply_3 = response.choices[0].message.content\n",
    "### FILL IN (START) ###\n",
    "# add reply_3 to messages as an assistant message\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c22",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages, new_from=5)\n",
    "roles = []\n",
    "for m in messages:\n",
    "    roles.append(m[\"role\"])\n",
    "done[\"2.3\"] = roles == [\"system\", \"user\", \"assistant\", \"user\", \"assistant\", \"user\", \"assistant\"] and \"notebook\" in reply_3.lower() and \"mug\" in reply_3.lower()\n",
    "done[\"2.3\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c23",
   "metadata": {},
   "source": [
    "## 3. A prompt template inside a function\n",
    "\n",
    "```python\n",
    "def ask(prompt):\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "    return response.choices[0].message.content\n",
    "\n",
    "text = ask(f\"Describe a {product}.\")           # f-string: {product} is replaced by its value\n",
    "review = ask(f\"Critique this text:\\n{text}\")   # the first output is part of the second prompt\n",
    "```\n",
    "\n",
    "`ask` sends one **user** message; the instruction and the data travel together in it. A call written many times goes into a function; inside `ask` is the Part 1 call. Later labs write one such function per step or role, for example draft, critique, and revision.\n",
    "\n",
    "### 3.1 Define `ask` and fill a template\n",
    "\n",
    "*Do:* run the cell. Change `product` and run it again."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c24",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "def ask(prompt):\n",
    "    \"\"\"Send one user message and return the reply text.\"\"\"\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]          # USER message: instruction + data\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages)\n",
    "    return response.choices[0].message.content\n",
    "\n",
    "product = \"desk lamp\"\n",
    "description = ask(f\"Write a one-sentence product description for a {product} sold in an office-supply shop.\")\n",
    "description"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c25",
   "metadata": {},
   "source": [
    "### 3.2 Use one output as the next input\n",
    "\n",
    "The critique prompt contains `description`, returned in 3.1. Triple quotes let a string span several lines.\n",
    "\n",
    "*Do:* run the cell; the critique should refer to the sentence above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c26",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "critique = ask(f\"\"\"List two concrete problems in this product description. Be brief.\n",
    "\n",
    "Description:\n",
    "{description}\"\"\")\n",
    "critique"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c27",
   "metadata": {},
   "source": [
    "### 3.3 ✍️ Revise with both texts\n",
    "\n",
    "*Do:* the template has two empty places; put `{description}` and `{critique}` there so the prompt carries both texts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c28",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "### FILL IN (START) ###\n",
    "# put {description} and {critique} in the two empty places below\n",
    "prompt = f\"\"\"Rewrite the product description to fix the problems listed in the critique.\n",
    "Return one sentence only.\n",
    "\n",
    "Description:\n",
    "\n",
    "Critique:\n",
    "\"\"\"\n",
    "### FILL IN (END) ###\n",
    "revised = ask(prompt)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c29",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "print(revised)\n",
    "done[\"3.3\"] = description in prompt and critique in prompt\n",
    "done[\"3.3\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c30",
   "metadata": {},
   "source": [
    "## 4. JSON output read into a Python dictionary\n",
    "\n",
    "```python\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages,\n",
    "                                          response_format={\"type\": \"json_object\"})\n",
    "data = json.loads(response.choices[0].message.content)   # JSON text -> dictionary\n",
    "data[\"city\"]                                             # read one field\n",
    "```\n",
    "\n",
    "A reply is always a string. When code needs its parts, ask for JSON and turn the text into a dictionary with `json.loads`. The instruction \"return JSON with these keys\" goes in the user message; `response_format={\"type\": \"json_object\"}` makes the API return valid JSON, and the messages must contain the word \"JSON\".\n",
    "\n",
    "### 4.1 Ask for JSON\n",
    "\n",
    "*Do:* run the cell. The printed type is `str`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c31",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "customer_message = \"Hi, I'd like to order 3 notebooks and 2 pen sets, delivered to Busan.\"\n",
    "\n",
    "prompt = f\"\"\"Extract the order from the customer message.\n",
    "Return JSON with keys \"items\" (a list of objects with keys \"product\" and \"quantity\") and \"city\".\n",
    "\n",
    "Customer message: {customer_message}\"\"\"\n",
    "\n",
    "messages = [{\"role\": \"user\", \"content\": prompt}]          # USER message: instruction + data\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages,\n",
    "                                          response_format={\"type\": \"json_object\"})   # the API returns valid JSON text\n",
    "reply = response.choices[0].message.content\n",
    "print(type(reply))\n",
    "reply"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c32",
   "metadata": {},
   "source": [
    "### 4.2 Read the JSON as a dictionary\n",
    "\n",
    "*Do:* run the cell. `order[\"items\"]` is a list, so a `for` loop reads each item."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c33",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "order = json.loads(reply)        # JSON text -> Python dictionary\n",
    "print(type(order))\n",
    "print(order[\"city\"])\n",
    "for item in order[\"items\"]:      # each item is a dictionary\n",
    "    print(item[\"product\"], item[\"quantity\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c34",
   "metadata": {},
   "source": [
    "### 4.3 ✍️ Another order\n",
    "\n",
    "*Do:* the prompt and the call are given; turn the reply into a dictionary with `json.loads`, and add each item's quantity inside the loop. Expected: `Seoul`, 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c35",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "customer_message_2 = \"Please send 1 desk lamp and 4 mugs to Seoul.\"\n",
    "prompt = f\"\"\"Extract the order from the customer message.\n",
    "Return JSON with keys \"items\" (a list of objects with keys \"product\" and \"quantity\") and \"city\".\n",
    "\n",
    "Customer message: {customer_message_2}\"\"\"\n",
    "messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages,\n",
    "                                          response_format={\"type\": \"json_object\"})\n",
    "### FILL IN (START) ###\n",
    "order_2 = {\"items\": [], \"city\": \"\"}      # turn the reply text into a dictionary\n",
    "### FILL IN (END) ###\n",
    "total_quantity = 0\n",
    "for item in order_2[\"items\"]:\n",
    "    ### FILL IN (START) ###\n",
    "    pass                                 # add this item's quantity to total_quantity\n",
    "    ### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c36",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "print(order_2)\n",
    "done[\"4.3\"] = \"city\" in order_2 and order_2[\"city\"] == \"Seoul\" and total_quantity == 5\n",
    "print(done[\"4.3\"], total_quantity)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c37",
   "metadata": {},
   "source": [
    "## 5. One tool call: request → run → return\n",
    "\n",
    "A **tool** is a Python function the model may ask your program to run. One tool call takes four steps:\n",
    "\n",
    "1. **Describe** the function to the model in a **tool schema**.\n",
    "2. The model returns a **request** (function name and arguments) in an **assistant** message. It runs nothing. Append the message to the list as soon as it arrives.\n",
    "3. Your code **runs** the function.\n",
    "4. Your code **returns** the result: append a **tool** message right after the request, and call the model again for the answer.\n",
    "\n",
    "### 5.1 The function\n",
    "\n",
    "`get_weather(city)` returns a city's current temperature from Open-Meteo, a free weather service (no key).\n",
    "\n",
    "*Do:* run the cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c38",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "import requests\n",
    "\n",
    "def get_weather(city: str):\n",
    "    \"\"\"Get the current temperature of a city in Celsius.\n",
    "\n",
    "    Args:\n",
    "        city: City name in English, for example \"Seoul\".\n",
    "    \"\"\"\n",
    "    try:                                                    # on a network or lookup error, return the error as text\n",
    "        place = requests.get(\"https://geocoding-api.open-meteo.com/v1/search\",\n",
    "                             params={\"name\": city, \"count\": 1}, timeout=10).json()[\"results\"][0]   # first match\n",
    "        weather = requests.get(\"https://api.open-meteo.com/v1/forecast\",\n",
    "                               params={\"latitude\": place[\"latitude\"], \"longitude\": place[\"longitude\"],\n",
    "                                       \"current\": \"temperature_2m\"}, timeout=10).json()\n",
    "        return {\"city\": place[\"name\"], \"temperature_c\": weather[\"current\"][\"temperature_2m\"]}\n",
    "    except Exception as error:\n",
    "        return \"error: \" + str(error)\n",
    "\n",
    "get_weather(\"Seoul\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c39",
   "metadata": {},
   "source": [
    "### 5.2 Step 1 — describe it: the tool schema\n",
    "\n",
    "The model cannot read Python code; it reads a schema, a dictionary with this shape:\n",
    "\n",
    "```python\n",
    "{\"type\": \"function\",\n",
    " \"function\": {\"name\": \"get_weather\",                                   # the function's name\n",
    "              \"description\": \"Get the current temperature of ...\",     # the docstring's first line\n",
    "              \"parameters\": {\"type\": \"object\",\n",
    "                             \"properties\": {\"city\": {\"type\": \"string\",               # from  city: str\n",
    "                                                     \"description\": \"City name ...\"}},  # from the Args: line\n",
    "                             \"required\": [\"city\"]}}}\n",
    "```\n",
    "\n",
    "Everything in it comes from the function, so `tool_from_function` builds it for any function written in this docstring format. The schema goes in `tools=[...]`, not in `messages`.\n",
    "\n",
    "*Do:* run the two cells (the first only defines the helper) and match each entry with the function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c40",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "import inspect\n",
    "\n",
    "PYTHON_TO_JSON = {str: \"string\", int: \"integer\", float: \"number\", bool: \"boolean\"}\n",
    "\n",
    "def tool_from_function(function):\n",
    "    \"\"\"Build a tool schema from a function's name, docstring, and type annotations.\"\"\"\n",
    "    doc = inspect.getdoc(function)                        # the docstring, indentation removed\n",
    "    arg_help = {}\n",
    "    for line in doc.split(\"\\n\"):                          # \"name: description\" lines under Args:\n",
    "        line = line.strip()\n",
    "        if \":\" in line and not line.endswith(\":\"):\n",
    "            name, text = line.split(\":\", 1)\n",
    "            arg_help[name.strip()] = text.strip()\n",
    "    properties = {}\n",
    "    for name, parameter in inspect.signature(function).parameters.items():   # each parameter and its annotation\n",
    "        properties[name] = {\"type\": PYTHON_TO_JSON[parameter.annotation], \"description\": arg_help[name]}\n",
    "    return {\n",
    "        \"type\": \"function\",\n",
    "        \"function\": {\n",
    "            \"name\": function.__name__,                    # the function's own name\n",
    "            \"description\": doc.split(\"\\n\")[0],            # the docstring's first line\n",
    "            \"parameters\": {\"type\": \"object\", \"properties\": properties, \"required\": list(properties)},\n",
    "        },\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c41",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "WEATHER_TOOL = tool_from_function(get_weather)     # TOOL SCHEMA\n",
    "print(json.dumps(WEATHER_TOOL, indent=2))          # indent=2 prints the dictionary on several lines"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c42",
   "metadata": {},
   "source": [
    "### 5.3 Steps 2–4, one cell each\n",
    "\n",
    "Each cell is one unit of the call and ends by printing the list. `**args` passes a dictionary as keyword arguments: `get_weather(**{\"city\": \"Seoul\"})` is `get_weather(city=\"Seoul\")`. `tool_call_id` links a result to its request; the API rejects a tool message with no request before it.\n",
    "\n",
    "*Do:* run the four cells in order and watch the list grow 2 → 3 → 4 → 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c43",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 1 - the list we send\n",
    "WEATHER_SYSTEM = \"You answer questions about the weather. Use the tools for current values; never guess them.\"\n",
    "\n",
    "messages = [\n",
    "    {\"role\": \"system\", \"content\": WEATHER_SYSTEM},                                  # SYSTEM message\n",
    "    {\"role\": \"user\", \"content\": \"What is the temperature in Seoul right now?\"},    # USER message\n",
    "]\n",
    "show_messages(messages)                                                             # 2 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c44",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 2 - call with the tool schema; the reply is a request: check it, then add it\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages, tools=[WEATHER_TOOL])\n",
    "message = response.choices[0].message                    # ASSISTANT message\n",
    "print(\"content:\", message.content)                       # None: no text answer\n",
    "print(\"tool_calls:\", message.tool_calls)                 # the request\n",
    "\n",
    "messages.append(message.model_dump(exclude_none=True))   # add it at once, as a dictionary like the others\n",
    "show_messages(messages, new_from=2)                      # 3 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c45",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 3 - run the requested function and add its result\n",
    "FUNCTIONS = {\"get_weather\": get_weather}                 # name -> Python function\n",
    "\n",
    "call = message.tool_calls[0]                             # the request from unit 2\n",
    "args = json.loads(call.function.arguments)               # JSON text -> dictionary\n",
    "result = FUNCTIONS[call.function.name](**args)           # get_weather(city=\"Seoul\")\n",
    "\n",
    "messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})   # TOOL message\n",
    "show_messages(messages, new_from=3)                      # 4 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c46",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 4 - call again with the result in the list; add the answer\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages, tools=[WEATHER_TOOL])\n",
    "answer = response.choices[0].message.content\n",
    "messages.append({\"role\": \"assistant\", \"content\": answer})    # ASSISTANT message\n",
    "show_messages(messages, new_from=4)                          # 5 messages"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c47",
   "metadata": {},
   "source": [
    "### 5.5 ✍️ The same units for Tokyo\n",
    "\n",
    "*Do:* the three units for `QUESTION_1` are given except the lines that add each message to `messages_1` (and running the request in unit 3). Each check cell prints your list. Afterwards, try a question that needs no tool (\"Say hello\"): `tool_calls` is `None`, which is how Part 6's loop knows the model has answered."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c48",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "## Unit 1-2 - the list, the call, and the request added\n",
    "QUESTION_1 = \"What is the temperature in Tokyo right now?\"\n",
    "messages_1 = [\n",
    "    {\"role\": \"system\", \"content\": WEATHER_SYSTEM},\n",
    "    {\"role\": \"user\", \"content\": QUESTION_1},\n",
    "]\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages_1, tools=[WEATHER_TOOL])\n",
    "message_1 = response.choices[0].message\n",
    "### FILL IN (START) ###\n",
    "# add message_1 to messages_1 as a dictionary\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c49",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages_1, new_from=2)                    # expected: 3 messages, the request NEW"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c50",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "## Unit 3 - run the request and add the result\n",
    "call_1 = message_1.tool_calls[0]\n",
    "args_1 = json.loads(call_1.function.arguments)\n",
    "### FILL IN (START) ###\n",
    "result_1 = None     # run the requested function with args_1\n",
    "# add result_1 as a tool message (tool_call_id = call_1.id)\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c51",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages_1, new_from=3)                    # expected: 4 messages, the result NEW"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c52",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "## Unit 4 - call again and add the answer\n",
    "response = client.chat.completions.create(model=MODEL, messages=messages_1, tools=[WEATHER_TOOL])\n",
    "answer_1 = response.choices[0].message.content\n",
    "### FILL IN (START) ###\n",
    "# add answer_1 to messages_1 as an assistant message\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c53",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages_1, new_from=4)                    # expected: 5 messages, the answer NEW\n",
    "roles = []\n",
    "for m in messages_1:\n",
    "    roles.append(m[\"role\"])\n",
    "found = False\n",
    "if isinstance(result_1, dict):                           # the tool returned a dictionary\n",
    "    value = result_1[\"temperature_c\"]\n",
    "    if str(value) in str(answer_1):\n",
    "        found = True\n",
    "    if value == round(value) and str(round(value)) in str(answer_1):   # 21.0 may be written as 21\n",
    "        found = True\n",
    "done[\"5.5\"] = roles == [\"system\", \"user\", \"assistant\", \"tool\", \"assistant\"] and \"Tokyo\" in str(result_1) and found\n",
    "done[\"5.5\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c54",
   "metadata": {},
   "source": [
    "## 6. The agent loop\n",
    "\n",
    "Part 5 was one exchange. Repeat it until a reply has no request, and you have the **agent loop**: the model picks the next action from everything in the list, the program runs it, and the result joins the next call. One reply can hold several requests, so an inner `for` runs each.\n",
    "\n",
    "```python\n",
    "messages = [{\"role\": \"system\", \"content\": SYSTEM}, {\"role\": \"user\", \"content\": QUESTION}]\n",
    "for step in range(MAX_STEPS):                       # a limit, so the loop always ends\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)\n",
    "    message = response.choices[0].message\n",
    "    messages.append(message.model_dump(exclude_none=True))       # ASSISTANT message\n",
    "    if message.tool_calls is None:                  # no request: this reply is the answer\n",
    "        break\n",
    "    for call in message.tool_calls:                 # one reply may hold several requests\n",
    "        args = json.loads(call.function.arguments)\n",
    "        result = FUNCTIONS[call.function.name](**args)\n",
    "        messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})   # TOOL message\n",
    "```\n",
    "\n",
    "### 6.1 Calculator tools\n",
    "\n",
    "Three small functions, each with a docstring in the 5.2 format, so `tool_from_function` builds their schemas. The system message tells the model to use a tool for every arithmetic step.\n",
    "\n",
    "*Do:* run the cell; it prints the three tool names."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c55",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "def add(a: float, b: float):\n",
    "    \"\"\"Add two numbers and return the sum.\n",
    "\n",
    "    Args:\n",
    "        a: The first number.\n",
    "        b: The second number.\n",
    "    \"\"\"\n",
    "    return a + b\n",
    "\n",
    "def subtract(a: float, b: float):\n",
    "    \"\"\"Subtract the second number from the first and return the difference.\n",
    "\n",
    "    Args:\n",
    "        a: The number to subtract from.\n",
    "        b: The number to subtract.\n",
    "    \"\"\"\n",
    "    return a - b\n",
    "\n",
    "def multiply(a: float, b: float):\n",
    "    \"\"\"Multiply two numbers and return the product.\n",
    "\n",
    "    Args:\n",
    "        a: The first number.\n",
    "        b: The second number.\n",
    "    \"\"\"\n",
    "    return a * b\n",
    "\n",
    "CALC_SYSTEM = \"You are a careful calculator. Use the tools for every arithmetic step, one operation per call; never compute in your head.\"\n",
    "CALC_TOOLS = [tool_from_function(add), tool_from_function(subtract), tool_from_function(multiply)]   # TOOL SCHEMAS\n",
    "CALC_FUNCTIONS = {\"add\": add, \"subtract\": subtract, \"multiply\": multiply}\n",
    "\n",
    "for tool in CALC_TOOLS:\n",
    "    print(tool[\"function\"][\"name\"], \"-\", tool[\"function\"][\"description\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c56",
   "metadata": {},
   "source": [
    "### 6.2 Run the loop\n",
    "\n",
    "The addition needs the product first, so the model must wait for one result before the next request.\n",
    "\n",
    "*Do:* run the cell. The list is printed after every step; follow what each step adds. Expected: 4817 × 293 = 1,411,381, then + 1569 = 1,412,950."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c57",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "QUESTION = \"What is (4817 × 293) + 1569?\"\n",
    "MAX_STEPS = 6\n",
    "\n",
    "messages = [{\"role\": \"system\", \"content\": CALC_SYSTEM}, {\"role\": \"user\", \"content\": QUESTION}]\n",
    "show_messages(messages)\n",
    "for step in range(MAX_STEPS):\n",
    "    start = len(messages)                                      # where this step's messages begin\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages, tools=CALC_TOOLS)\n",
    "    message = response.choices[0].message\n",
    "    messages.append(message.model_dump(exclude_none=True))\n",
    "    if message.tool_calls is None:\n",
    "        print(\"step\", step, \"- the reply has no request: it is the answer\")\n",
    "        show_messages(messages, new_from=start)\n",
    "        break\n",
    "    for call in message.tool_calls:\n",
    "        args = json.loads(call.function.arguments)\n",
    "        result = CALC_FUNCTIONS[call.function.name](**args)\n",
    "        messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})\n",
    "    print(\"step\", step, \"- a request and its result were added\")\n",
    "    show_messages(messages, new_from=start)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c58",
   "metadata": {},
   "source": [
    "### 6.3 ✍️ Write the loop yourself\n",
    "\n",
    "*Do:* the loop is given except its two appends: the assistant message after each call, and the tool message after each result. Expected: 58 × 94 = 5,452 and 37 × 21 = 777 (these two can come in one reply), then 5,452 + 777 = 6,229."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c59",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "QUESTION_2 = \"What is (58 × 94) + (37 × 21)?\"\n",
    "messages_2 = [{\"role\": \"system\", \"content\": CALC_SYSTEM}, {\"role\": \"user\", \"content\": QUESTION_2}]\n",
    "answer_2 = None\n",
    "for step in range(MAX_STEPS):\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages_2, tools=CALC_TOOLS)\n",
    "    message = response.choices[0].message\n",
    "    ### FILL IN (START) ###\n",
    "    # add message to messages_2 as a dictionary\n",
    "    ### FILL IN (END) ###\n",
    "    if message.tool_calls is None:\n",
    "        answer_2 = message.content\n",
    "        break\n",
    "    for call in message.tool_calls:\n",
    "        args = json.loads(call.function.arguments)\n",
    "        result = CALC_FUNCTIONS[call.function.name](**args)\n",
    "        ### FILL IN (START) ###\n",
    "        # add result as a tool message\n",
    "        ### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c60",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages_2)\n",
    "done[\"6.3\"] = answer_2 is not None and (\"6229\" in answer_2 or \"6,229\" in answer_2)\n",
    "done[\"6.3\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c61",
   "metadata": {},
   "source": [
    "### 6.4 ✍️ Mix two kinds of tools\n",
    "\n",
    "Registering a tool is now one line per function. Weather and calculator tools together answer a question that needs a lookup and then arithmetic.\n",
    "\n",
    "*Do:* the loop is given; complete the registration: `TOOLS_3` needs the schemas for `add` and `multiply` too, and `FUNCTIONS_3` their names."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c62",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "MIXED_SYSTEM = (\"Use get_weather for current temperatures and the calculator tools for every arithmetic step, \"\n",
    "                \"one operation per call; never compute in your head.\")\n",
    "QUESTION_3 = \"What is the current temperature in Seoul in Fahrenheit? Use F = C × 1.8 + 32.\"\n",
    "### FILL IN (START) ###\n",
    "TOOLS_3 = [tool_from_function(get_weather)]          # add the schemas for add and multiply\n",
    "FUNCTIONS_3 = {\"get_weather\": get_weather}           # add their names and functions\n",
    "### FILL IN (END) ###\n",
    "messages_3 = [{\"role\": \"system\", \"content\": MIXED_SYSTEM}, {\"role\": \"user\", \"content\": QUESTION_3}]\n",
    "answer_3 = None\n",
    "for step in range(MAX_STEPS):\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages_3, tools=TOOLS_3)\n",
    "    message = response.choices[0].message\n",
    "    messages_3.append(message.model_dump(exclude_none=True))\n",
    "    if message.tool_calls is None:\n",
    "        answer_3 = message.content\n",
    "        break\n",
    "    for call in message.tool_calls:\n",
    "        args = json.loads(call.function.arguments)\n",
    "        result = FUNCTIONS_3[call.function.name](**args)\n",
    "        messages_3.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c63",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(messages_3)\n",
    "requested = []\n",
    "for m in messages_3:\n",
    "    if m[\"role\"] == \"assistant\" and \"tool_calls\" in m:     # assistant messages that hold requests\n",
    "        for c in m[\"tool_calls\"]:\n",
    "            requested.append(c[\"function\"][\"name\"])\n",
    "done[\"6.4\"] = \"get_weather\" in requested and \"multiply\" in requested and \"add\" in requested and answer_3 is not None\n",
    "done[\"6.4\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c64",
   "metadata": {},
   "source": [
    "## 7. ReAct: a thought before each action\n",
    "\n",
    "ReAct has the model write a **Thought** (what the results show, what is missing, why the next action) before each **Action**; the tool's result is the **Observation**.\n",
    "\n",
    "With function calling, a request carries only a function name and arguments, so there is no place for a thought. The simple fix: give each tool a first argument named `thought`. The model must fill it in every request, so the reason arrives together with the action.\n",
    "\n",
    "```python\n",
    "def search(thought: str, query: str):\n",
    "    \"\"\"Search Wikipedia ... Args: thought: your reasoning ...  query: search words ...\"\"\"\n",
    "    print(\"Thought:\", thought)          # show the reason when the action runs\n",
    "    return wikipedia_search(query)\n",
    "```\n",
    "\n",
    "The loop is Part 6's loop unchanged. Only two things differ: the tools ask for a thought, and `parallel_tool_calls=False` asks for one request per reply, so each thought is written after the previous observation.\n",
    "\n",
    "### 7.1 Tools that ask for a thought\n",
    "\n",
    "*Do:* run the two cells. The first defines a plain Wikipedia search (no key). The second wraps search and weather as tools whose first argument is `thought`, and prints the search tool's schema: `thought` is now its first, required argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c65",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 1 - a plain Wikipedia search\n",
    "import html\n",
    "\n",
    "def wikipedia_search(query: str):\n",
    "    \"\"\"Search Wikipedia and return the top three results: title and a short snippet.\n",
    "\n",
    "    Args:\n",
    "        query: Search words, for example \"Gyeongbokgung Palace\".\n",
    "    \"\"\"\n",
    "    try:\n",
    "        data = requests.get(\"https://en.wikipedia.org/w/api.php\",\n",
    "                            params={\"action\": \"query\", \"list\": \"search\", \"srsearch\": query,\n",
    "                                    \"srlimit\": 3, \"format\": \"json\"},\n",
    "                            headers={\"User-Agent\": \"STML-course-practice/1.0\"}, timeout=10).json()\n",
    "        lines = []\n",
    "        for item in data[\"query\"][\"search\"]:\n",
    "            snippet = item[\"snippet\"].replace('<span class=\"searchmatch\">', \"\").replace(\"</span>\", \"\")   # drop highlight tags\n",
    "            lines.append(item[\"title\"] + \" | \" + html.unescape(snippet))                               # &#039; -> '\n",
    "        return \"\\n\".join(lines)\n",
    "    except Exception as error:                     # a failed search is returned as text, not raised\n",
    "        return \"error: \" + str(error)\n",
    "\n",
    "print(wikipedia_search(\"Gyeongbokgung Palace\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c66",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 2 - tools whose first argument is the thought\n",
    "def search(thought: str, query: str):\n",
    "    \"\"\"Search Wikipedia and return the top three results.\n",
    "\n",
    "    Args:\n",
    "        thought: Your reasoning: what the results so far show, what is still missing, and why this search.\n",
    "        query: Search words, for example \"Gyeongbokgung Palace\".\n",
    "    \"\"\"\n",
    "    print(\"Thought:\", thought)\n",
    "    return wikipedia_search(query)\n",
    "\n",
    "def check_weather(thought: str, city: str):\n",
    "    \"\"\"Get the current temperature of a city in Celsius.\n",
    "\n",
    "    Args:\n",
    "        thought: Your reasoning: what the results so far show, what is still missing, and why this lookup.\n",
    "        city: City name in English, for example \"Seoul\".\n",
    "    \"\"\"\n",
    "    print(\"Thought:\", thought)\n",
    "    return get_weather(city)\n",
    "\n",
    "REACT_TOOLS = [tool_from_function(search), tool_from_function(check_weather)]   # TOOL SCHEMAS, built as in Part 5\n",
    "REACT_FUNCTIONS = {\"search\": search, \"check_weather\": check_weather}\n",
    "print(json.dumps(REACT_TOOLS[0], indent=2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c67",
   "metadata": {},
   "source": [
    "### 7.2 The same loop, one request per reply\n",
    "\n",
    "*Do:* run the cell. At each step the tool prints the Thought, then the list shows the request (with its thought) and the result. Expected: a search that finds Seoul, then `check_weather` for Seoul."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c68",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "TRAVEL_SYSTEM = \"You help travellers. Use search to find the city of any place before checking its weather.\"\n",
    "QUESTION = \"I am visiting Gyeongbokgung Palace this afternoon. What is the temperature there right now?\"\n",
    "\n",
    "messages = [{\"role\": \"system\", \"content\": TRAVEL_SYSTEM}, {\"role\": \"user\", \"content\": QUESTION}]\n",
    "show_messages(messages)\n",
    "for step in range(MAX_STEPS):\n",
    "    start = len(messages)\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages, tools=REACT_TOOLS,\n",
    "                                              parallel_tool_calls=False)   # one request per reply\n",
    "    message = response.choices[0].message\n",
    "    messages.append(message.model_dump(exclude_none=True))\n",
    "    if message.tool_calls is None:\n",
    "        print(\"step\", step, \"- the reply has no request: it is the answer\")\n",
    "        show_messages(messages, new_from=start)\n",
    "        break\n",
    "    for call in message.tool_calls:\n",
    "        args = json.loads(call.function.arguments)\n",
    "        result = REACT_FUNCTIONS[call.function.name](**args)          # the tool prints its thought\n",
    "        messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})\n",
    "    print(\"step\", step, \"- a request with its thought, and the result, were added\")\n",
    "    show_messages(messages, new_from=start)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c69",
   "metadata": {},
   "source": [
    "### 7.3 ✍️ Read one step\n",
    "\n",
    "*Do:* choose one result and the Thought after it. In `reading`, write which fact from the result the Thought used, and whether the next action follows from it. (Not checked.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c70",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ WRITE · your answer in words (not checked)\n",
    "reading = \"\"\"\n",
    "\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c71",
   "metadata": {},
   "source": [
    "## 8. The agent as a class\n",
    "\n",
    "A loop written in a cell starts a new `messages` list each time, so a follow-up (\"And in Busan?\") has nothing to refer to. A **class** keeps the list inside an object: every call appends to the same `self.messages`.\n",
    "\n",
    "```python\n",
    "class Chat:\n",
    "    def __init__(self, system):                      # runs once, when the object is created\n",
    "        self.messages = [{\"role\": \"system\", \"content\": system}]\n",
    "\n",
    "    def ask(self, question):                         # a method: a function that belongs to the object\n",
    "        self.messages.append({\"role\": \"user\", \"content\": question})\n",
    "        ...                                          # call the model with self.messages\n",
    "        self.messages.append({\"role\": \"assistant\", \"content\": reply})\n",
    "        return reply\n",
    "```\n",
    "\n",
    "`self` is the object itself; `self.messages` is a value stored in it (an **attribute**) that stays between calls.\n",
    "\n",
    "### 8.1 Messages that accumulate\n",
    "\n",
    "*Do:* run the cells one by one. Compare with 2.1–2.2: the same list grows, but now `ask` appends for you. The second question says only \"the lamp\"; it works because the first exchange is still in `chat.messages`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c72",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "class Chat:\n",
    "    \"\"\"A conversation that keeps its own message history.\"\"\"\n",
    "\n",
    "    def __init__(self, system):\n",
    "        self.messages = [{\"role\": \"system\", \"content\": system}]               # SYSTEM message, once\n",
    "\n",
    "    def ask(self, question):\n",
    "        self.messages.append({\"role\": \"user\", \"content\": question})           # USER message\n",
    "        response = client.chat.completions.create(model=MODEL, messages=self.messages)\n",
    "        reply = response.choices[0].message.content\n",
    "        self.messages.append({\"role\": \"assistant\", \"content\": reply})         # ASSISTANT message\n",
    "        return reply"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c73",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 1 - create the object: its list holds only the system message\n",
    "chat = Chat(STAFF_SYSTEM)\n",
    "show_messages(chat.messages)                              # 1 message"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c74",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 2 - ask: the question and the reply are added by ask\n",
    "chat.ask(\"A customer ordered 2 mugs and 1 desk lamp. List the items in the order.\")\n",
    "show_messages(chat.messages, new_from=1)                  # 3 messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c75",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 3 - a follow-up: the same list grows\n",
    "chat.ask(\"The lamp was cancelled. What is left in the order?\")\n",
    "show_messages(chat.messages, new_from=3)                  # 5 messages"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c76",
   "metadata": {},
   "source": [
    "### 8.2 ✍️ Write the class yourself\n",
    "\n",
    "*Do:* complete `MyChat`: the two lines in `ask` that add the question and the reply to `self.messages`, and the body of `reset`. The check asks two connected questions, then calls `reset()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c77",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "class MyChat:\n",
    "    def __init__(self, system):\n",
    "        self.system = system\n",
    "        self.messages = [{\"role\": \"system\", \"content\": system}]\n",
    "\n",
    "    def ask(self, question):\n",
    "        ### FILL IN (START) ###\n",
    "        # add question to self.messages as a user message\n",
    "        ### FILL IN (END) ###\n",
    "        response = client.chat.completions.create(model=MODEL, messages=self.messages)\n",
    "        reply = response.choices[0].message.content\n",
    "        ### FILL IN (START) ###\n",
    "        # add reply to self.messages as an assistant message\n",
    "        ### FILL IN (END) ###\n",
    "        return reply\n",
    "\n",
    "    def reset(self):\n",
    "        ### FILL IN (START) ###\n",
    "        pass    # set self.messages back to a list with only the system message\n",
    "        ### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c78",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "my_chat = MyChat(\"You answer questions from shop staff. Answer in one sentence.\")\n",
    "my_chat.ask(\"A customer ordered 3 notebooks and 2 pen sets.\")\n",
    "follow_up = my_chat.ask(\"The customer adds 1 folder. What is in the order now?\")\n",
    "length_before = len(my_chat.messages)\n",
    "show_messages(my_chat.messages)\n",
    "my_chat.reset()\n",
    "print(\"after reset():\")\n",
    "show_messages(my_chat.messages)\n",
    "done[\"8.2\"] = length_before == 5 and len(my_chat.messages) == 1 and \"notebook\" in follow_up.lower() and \"folder\" in follow_up.lower()\n",
    "done[\"8.2\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c79",
   "metadata": {},
   "source": [
    "### 8.3 The agent loop in a class\n",
    "\n",
    "`Agent` takes a system message and a **list of functions**. It builds the tool schemas with `tool_from_function` and the name → function dictionary itself, so adding a tool means adding a function to the list. `ask` runs the Part 6 loop on `self.messages` and adds up `response.usage.total_tokens`.\n",
    "\n",
    "*Do:* run the three cells. The second question only makes sense after the first: its NEW messages sit below the first question's, and the subtraction uses Seoul's temperature from there."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c80",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "class Agent:\n",
    "    \"\"\"An agent that registers its tools from functions and keeps its history between questions.\"\"\"\n",
    "\n",
    "    def __init__(self, system, functions, max_steps=6):\n",
    "        self.tools = []                                                      # TOOL SCHEMAS\n",
    "        self.functions = {}                                                  # name -> Python function\n",
    "        for function in functions:\n",
    "            self.tools.append(tool_from_function(function))\n",
    "            self.functions[function.__name__] = function\n",
    "        self.max_steps = max_steps\n",
    "        self.messages = [{\"role\": \"system\", \"content\": system}]              # SYSTEM message, once\n",
    "        self.total_tokens = 0\n",
    "\n",
    "    def ask(self, question):\n",
    "        self.messages.append({\"role\": \"user\", \"content\": question})          # USER message\n",
    "        for step in range(self.max_steps):\n",
    "            response = client.chat.completions.create(model=MODEL, messages=self.messages, tools=self.tools)\n",
    "            self.total_tokens = self.total_tokens + response.usage.total_tokens\n",
    "            message = response.choices[0].message\n",
    "            self.messages.append(message.model_dump(exclude_none=True))      # ASSISTANT message\n",
    "            if message.tool_calls is None:\n",
    "                return message.content\n",
    "            for call in message.tool_calls:\n",
    "                args = json.loads(call.function.arguments)\n",
    "                result = self.functions[call.function.name](**args)\n",
    "                self.messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": json.dumps(result)})   # TOOL message\n",
    "        return None                                                          # the step limit ended the loop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c81",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 1 - create the agent and ask the first question\n",
    "weather = Agent(MIXED_SYSTEM, [get_weather, add, subtract, multiply])\n",
    "weather.ask(\"What is the temperature in Seoul right now?\")\n",
    "show_messages(weather.messages, new_from=1)               # user, request, result, answer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c82",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · example: read each line, then run\n",
    "## Unit 2 - a follow-up that needs the first answer\n",
    "start = len(weather.messages)                             # where the new messages will begin\n",
    "weather.ask(\"And in Busan? How many degrees warmer or colder is Busan than Seoul?\")\n",
    "show_messages(weather.messages, new_from=start)\n",
    "print(\"tokens so far:\", weather.total_tokens)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c83",
   "metadata": {},
   "source": [
    "### 8.4 ✍️ A second object with its own history\n",
    "\n",
    "*Do:* complete the line that creates `converter`: an `Agent` with `CONVERTER_SYSTEM` and the functions `add` and `multiply`. The two questions are given. Expected: 86 and 53.6. The check also confirms that `weather` did not receive these messages."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c84",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "CONVERTER_SYSTEM = (\"You convert Celsius to Fahrenheit with F = C × 1.8 + 32. \"\n",
    "                    \"Use the calculator tools for every arithmetic step, one operation per call.\")\n",
    "### FILL IN (START) ###\n",
    "converter = Agent(CONVERTER_SYSTEM, [add])      # give it both calculator functions it needs: add and multiply\n",
    "### FILL IN (END) ###\n",
    "answer_30 = converter.ask(\"Convert 30 °C to Fahrenheit.\")\n",
    "answer_12 = converter.ask(\"And 12 °C?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c85",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "weather_questions = 0\n",
    "for m in weather.messages:\n",
    "    if m[\"role\"] == \"user\":\n",
    "        weather_questions = weather_questions + 1\n",
    "show_messages(converter.messages)                    # the converter's own list\n",
    "print(\"user messages in weather:\", weather_questions)\n",
    "done[\"8.4\"] = \"multiply\" in converter.functions and \"86\" in str(answer_30) and \"53.6\" in str(answer_12) and weather_questions == 2\n",
    "done[\"8.4\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c86",
   "metadata": {},
   "source": [
    "### 8.5 ✍️ Two objects working together\n",
    "\n",
    "An advisor answers, a reviewer comments, the advisor revises. The reviewer's **system** message is `REVIEW_SYSTEM`; the answer reaches it in a **user** message, and the review goes back to the advisor in another **user** message. The advisor keeps its history, so \"revise your answer\" is enough.\n",
    "\n",
    "*Do:* the two objects and the first answer are given; write the two `ask` lines: the reviewer receives the question and the answer, and the advisor receives the review."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c87",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ✍️ TYPE · exercise: write the code between the FILL IN lines yourself\n",
    "REVIEW_SYSTEM = (\"You review weather advice. Point out anything missing, such as the actual temperature \"\n",
    "                 \"or a clear recommendation. Answer in at most three sentences.\")\n",
    "QUESTION_4 = \"Should I take a jacket for a walk in Seoul right now?\"\n",
    "advisor = Agent(WEATHER_SYSTEM, [get_weather])\n",
    "reviewer = Chat(REVIEW_SYSTEM)\n",
    "answer = advisor.ask(QUESTION_4)\n",
    "### FILL IN (START) ###\n",
    "review = \"\"     # ask the reviewer with a message that holds QUESTION_4 and answer\n",
    "revised = \"\"    # ask the advisor to revise, with a message that holds review\n",
    "### FILL IN (END) ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c88",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · check: just run\n",
    "show_messages(reviewer.messages)                     # the answer reached the reviewer as a USER message\n",
    "print(\"REVISED:\", revised)\n",
    "advisor_questions = 0\n",
    "for m in advisor.messages:\n",
    "    if m[\"role\"] == \"user\":\n",
    "        advisor_questions = advisor_questions + 1\n",
    "done[\"8.5\"] = advisor_questions == 2 and len(reviewer.messages) == 3 and revised != \"\"\n",
    "done[\"8.5\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c89",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "*Do:* run the cell; redo any `False` item."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c90",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ▶ RUN · setup: just run\n",
    "for key in done:\n",
    "    print(key, done[key])"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
