{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ec655328",
   "metadata": {},
   "source": [
    "# W6 Homework — A New Intent, End to End\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/week06/W6_hw_new_intent.ipynb)\n",
    "\n",
    "**Goal.** Push one new customer intent — an exchange — through a condensed version\n",
    "of the lab's pipeline: a tool for it, a planner that knows when to plan it, and a\n",
    "scored request that proves it works — extending a working pipeline without breaking\n",
    "its score.\n",
    "\n",
    "The path: setup → the condensed store and pipeline → three scored intents\n",
    "(baseline) → the exchange tool ✍️ → the planner extension ✍️ → four intents scored →\n",
    "completion.\n",
    "\n",
    "*Runtime:* ~45 minutes. Due before the W7 session. Reference answers:\n",
    "`labs/checkpoints/week06/solution.py`, published after the homework deadline.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d4685a4",
   "metadata": {},
   "source": [
    "## 1. Setup\n",
    "\n",
    "*Do:* run the three cells; the last must print `ready`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "314815d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install -q \"aisuite[openai,anthropic]==0.1.14\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fcfbe5c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "os.environ[\"OPENAI_API_KEY\"] = \"PASTE-YOUR-KEY-HERE\"\n",
    "\n",
    "MODEL = \"openai:gpt-4o-mini\"          # alt: \"anthropic:claude-haiku-4-5\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bdee2a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import aisuite\n",
    "\n",
    "client = aisuite.Client()\n",
    "\n",
    "\n",
    "def ask(prompt, system=None, temperature=0.0, **kwargs):\n",
    "    \"\"\"Single prompt -> reply text.\"\"\"\n",
    "    messages = ([{\"role\": \"system\", \"content\": system}] if system else []) + [\n",
    "        {\"role\": \"user\", \"content\": prompt}]\n",
    "    response = client.chat.completions.create(model=MODEL, messages=messages,\n",
    "                                              temperature=temperature, **kwargs)\n",
    "    return response.choices[0].message.content\n",
    "\n",
    "\n",
    "print(ask(\"Reply with exactly: ready\"))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b788a811",
   "metadata": {},
   "source": [
    "## 2. The Store and the Pipeline, Condensed\n",
    "\n",
    "This transfer task keeps the lab's **request → plan → Python execution → outcome\n",
    "check**. It uses the same `MODEL`, aisuite client, and explicit system/user messages.\n",
    "The `ask` helper makes one model call, like the lab's `chat_text` helper.\n",
    "\n",
    "Here a dictionary replaces DataFrames, and each plan step is `{\"tool\": ..., \"args\": ...}`.\n",
    "The tools combine lookup and update internally, so no `context...` result paths\n",
    "are needed. Review, failure explanation, and customer reply are omitted to focus\n",
    "on adding one capability. Read `PLANNER_SYSTEM`: its catalog is what the planner\n",
    "actually sees; a Python docstring is not automatically included in that input.\n",
    "\n",
    "Each scored request gets a fresh store and ledger. These small tools update that\n",
    "per-request store directly, unlike the lab's staged-table executor. The supplied\n",
    "exchange tool checks availability before either update so the exchange can\n",
    "succeed or fail together. Do not assume this small executor provides rollback\n",
    "for an arbitrary sequence of tools.\n",
    "\n",
    "Run both cells, then trace a request from the catalog through the returned plan\n",
    "to the Python registry."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "740c276f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import copy\n",
    "import json\n",
    "\n",
    "STORE0 = {\n",
    "    \"blue mug\": {\"stock\": 10, \"price\": 9.0},\n",
    "    \"red mug\":  {\"stock\": 4,  \"price\": 9.0},\n",
    "    \"tea pot\":  {\"stock\": 2,  \"price\": 24.0},\n",
    "}\n",
    "\n",
    "\n",
    "def make_tools(store, ledger):\n",
    "    \"\"\"Builds the tool registry over one store instance.\"\"\"\n",
    "\n",
    "    def check_stock(product: str) -> str:\n",
    "        \"\"\"Report the stock level and unit price for one product.\n",
    "\n",
    "        Use for availability or price questions; changes nothing.\n",
    "\n",
    "        Args:\n",
    "            product: exact product name in lowercase, e.g. \"blue mug\".\n",
    "        \"\"\"\n",
    "        item = store.get(product)\n",
    "        return (f\"{product}: {item['stock']} in stock at {item['price']}\"\n",
    "                if item else f\"(unknown product {product!r})\")\n",
    "\n",
    "    def sell_item(product: str, qty: int) -> str:\n",
    "        item = store.get(product)\n",
    "        if item is None:\n",
    "            return f\"(unknown product {product!r})\"\n",
    "        if item[\"stock\"] < int(qty):\n",
    "            return f\"(cannot sell {qty}: only {item['stock']} in stock)\"\n",
    "        item[\"stock\"] -= int(qty)\n",
    "        ledger.append({\"action\": \"sale\", \"product\": product, \"qty\": int(qty)})\n",
    "        return f\"sold {qty} x {product}\"\n",
    "\n",
    "    def return_item(product: str, qty: int) -> str:\n",
    "        item = store.get(product)\n",
    "        if item is None:\n",
    "            return f\"(unknown product {product!r})\"\n",
    "        item[\"stock\"] += int(qty)\n",
    "        ledger.append({\"action\": \"return\", \"product\": product, \"qty\": int(qty)})\n",
    "        return f\"returned {qty} x {product}\"\n",
    "\n",
    "    return {\"check_stock\": check_stock, \"sell_item\": sell_item,\n",
    "            \"return_item\": return_item}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a6e8b30",
   "metadata": {},
   "outputs": [],
   "source": [
    "PLANNER_SYSTEM = \"\"\"You plan tool calls for a store assistant. Reply with STRICT JSON only:\n",
    "a list of steps, each {\"tool\": \"<name>\", \"args\": {...}}. No prose, no code fences.\n",
    "\n",
    "Tools:\n",
    "- check_stock(product): read a product's stock and price. Use for inquiries.\n",
    "- sell_item(product, qty): sell qty units. Use for purchases.\n",
    "- return_item(product, qty): take qty units back. Use for returns.\n",
    "\n",
    "Products: \"blue mug\", \"red mug\", \"tea pot\". Use these exact names.\"\"\"\n",
    "\n",
    "\n",
    "def plan(request, planner_system):\n",
    "    \"\"\"Request -> list of {\"tool\", \"args\"} steps (raises on non-JSON).\"\"\"\n",
    "    reply = ask(f\"Request: {request}\", system=planner_system)\n",
    "    return json.loads(reply)\n",
    "\n",
    "\n",
    "def execute(steps, tools):\n",
    "    \"\"\"Runs a plan through the registry; unknown tools become error strings.\"\"\"\n",
    "    results = []\n",
    "    for step in steps:\n",
    "        fn = tools.get(step.get(\"tool\"))\n",
    "        results.append(fn(**step.get(\"args\", {})) if fn\n",
    "                       else f\"(unknown tool {step.get('tool')!r})\")\n",
    "    return results\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7339e940",
   "metadata": {},
   "source": [
    "## 3. Three Intents, Scored — the Baseline\n",
    "\n",
    "Each request runs on a fresh store, and the scorer checks outcomes\n",
    "programmatically: stock deltas and ledger rows, never prose.\n",
    "\n",
    "*Do:* run the cell; the baseline must be 3/3 before you extend anything —\n",
    "the regression baseline for this homework. If a request fails, re-run the cell once\n",
    "(plans are sampled); if it keeps failing, print the plan the planner emitted for\n",
    "that request and read it against the Tools list.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5bc269dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "REQUESTS = [\n",
    "    {\"request\": \"Sell two blue mugs to the customer at the counter.\",\n",
    "     \"expect\": lambda s, l: s[\"blue mug\"][\"stock\"] == 8\n",
    "               and any(r[\"action\"] == \"sale\" and r[\"product\"] == \"blue mug\" for r in l)},\n",
    "    {\"request\": \"How many tea pots do we have in stock?\",\n",
    "     \"expect\": lambda s, l: s == STORE0 and not l},\n",
    "    {\"request\": \"A customer returns one red mug.\",\n",
    "     \"expect\": lambda s, l: s[\"red mug\"][\"stock\"] == 5\n",
    "               and any(r[\"action\"] == \"return\" for r in l)},\n",
    "]\n",
    "\n",
    "\n",
    "def score(requests, planner_system, extra_tools=None):\n",
    "    correct = 0\n",
    "    for case in requests:\n",
    "        store, ledger = copy.deepcopy(STORE0), []\n",
    "        tools = make_tools(store, ledger)\n",
    "        if extra_tools:\n",
    "            tools.update(extra_tools(store, ledger))\n",
    "        try:\n",
    "            results = execute(plan(case[\"request\"], planner_system), tools)\n",
    "            ok = case[\"expect\"](store, ledger)\n",
    "        except Exception as exc:\n",
    "            results, ok = [f\"(pipeline error: {exc})\"], False\n",
    "        correct += ok\n",
    "        print(f\"{'PASS' if ok else 'FAIL':4}  {case['request'][:58]}\")\n",
    "    return correct\n",
    "\n",
    "\n",
    "baseline = score(REQUESTS, PLANNER_SYSTEM)\n",
    "print(f\"\\nbaseline: {baseline}/3\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e88c9083",
   "metadata": {},
   "source": [
    "## 4. The Exchange Tool ✍️\n",
    "\n",
    "An exchange is a return and a sale that must succeed or fail together: if the new\n",
    "item is out of stock, the old one must not be taken back. The body below implements\n",
    "that; the graded work is the docstring and the planner line you distill from it in\n",
    "Section 5 — there, the Tools list is the only text the planner reads (in W3's\n",
    "tool-calling setup, the docstring itself is what the model reads). Copy the shape of\n",
    "`check_stock`'s docstring in Section 2.\n",
    "\n",
    "Requirements: say what the tool does and when to use it (a customer swapping one\n",
    "product for another); say when **not** to use it (plain returns and plain\n",
    "purchases have their own tools); an `Args:` entry per parameter with the exact\n",
    "product-name format.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2082fd70",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_exchange_tool(store, ledger):\n",
    "\n",
    "    ### FILL IN (START) ###\n",
    "    def exchange_item(old_product: str, new_product: str, qty: int) -> str:\n",
    "        \"\"\"Exchange.\"\"\"\n",
    "        new = store.get(new_product)\n",
    "        if new is None or new[\"stock\"] < int(qty):\n",
    "            return f\"(cannot exchange: {new_product!r} unavailable)\"\n",
    "        store[old_product][\"stock\"] += int(qty)\n",
    "        ledger.append({\"action\": \"return\", \"product\": old_product, \"qty\": int(qty)})\n",
    "        new[\"stock\"] -= int(qty)\n",
    "        ledger.append({\"action\": \"sale\", \"product\": new_product, \"qty\": int(qty)})\n",
    "        return f\"exchanged {qty} x {old_product} for {new_product}\"\n",
    "    ### FILL IN (END) ###\n",
    "\n",
    "    return {\"exchange_item\": exchange_item}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b6599d4",
   "metadata": {},
   "source": [
    "## 5. The Planner Extension ✍️\n",
    "\n",
    "The planner cannot plan a tool it has never heard of. Append the exchange tool to\n",
    "`PLANNER_SYSTEM`: one line in the Tools list, in the same format as the other\n",
    "three — name, signature, one sentence of when to use it. Write the line without a\n",
    "leading dash; the next cell prepends `- ` itself.\n",
    "\n",
    "Hint: the planner's failure mode without this line is instructive — run Section 6\n",
    "once with the starter and read what plan it emits for the exchange request.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25bc614d",
   "metadata": {},
   "outputs": [],
   "source": [
    "### FILL IN (START) ###\n",
    "EXCHANGE_LINE = \"\"   # starter — one Tools-list line for exchange_item\n",
    "### FILL IN (END) ###\n",
    "\n",
    "PLANNER_SYSTEM_V2 = PLANNER_SYSTEM.replace(\n",
    "    \"\\nProducts:\", f\"- {EXCHANGE_LINE}\\n\\nProducts:\") if EXCHANGE_LINE else PLANNER_SYSTEM\n",
    "print(PLANNER_SYSTEM_V2)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2483cec1",
   "metadata": {},
   "source": [
    "## 6. Four Intents, Scored\n",
    "\n",
    "The exchange request joins the set. Its expectation: blue-mug stock up one,\n",
    "tea-pot stock down one, and both ledger rows present — the composed outcome, not\n",
    "the prose. Target: **4/4**, with the original three still passing.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9bc2386e",
   "metadata": {},
   "outputs": [],
   "source": [
    "EXCHANGE_REQUEST = {\n",
    "    \"request\": \"A customer exchanges one blue mug for one tea pot.\",\n",
    "    \"expect\": lambda s, l: s[\"blue mug\"][\"stock\"] == 11 and s[\"tea pot\"][\"stock\"] == 1\n",
    "              and any(r[\"action\"] == \"return\" and r[\"product\"] == \"blue mug\" for r in l)\n",
    "              and any(r[\"action\"] == \"sale\" and r[\"product\"] == \"tea pot\" for r in l),\n",
    "}\n",
    "\n",
    "final_score = score(REQUESTS + [EXCHANGE_REQUEST], PLANNER_SYSTEM_V2,\n",
    "                    extra_tools=make_exchange_tool)\n",
    "print(f\"\\nfinal: {final_score}/4\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d6c6d28",
   "metadata": {},
   "source": [
    "## 7. Completion Check\n",
    "\n",
    "Submit: run the notebook top to bottom, then **File → Download → Download .ipynb**\n",
    "and upload the file to the LMS.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf9dabda",
   "metadata": {},
   "outputs": [],
   "source": [
    "_store, _ledger = copy.deepcopy(STORE0), []\n",
    "_doc = make_exchange_tool(_store, _ledger)[\"exchange_item\"].__doc__ or \"\"\n",
    "completion = {\n",
    "    \"exchange docstring written (>= 100 chars, has Args)\":\n",
    "        len(_doc.strip()) >= 100 and \"Args\" in _doc,\n",
    "    \"docstring states when NOT to use it\":\n",
    "        \"not\" in _doc.lower(),\n",
    "    \"planner extended with the exchange line\": len(EXCHANGE_LINE.strip()) >= 30,\n",
    "    \"final score 4/4\": final_score == 4,\n",
    "}\n",
    "for item, ok in completion.items():\n",
    "    print(f\"{'PASS' if ok else 'FAIL':4}  {item}\")\n",
    "print(\"\\nHOMEWORK COMPLETE\" if all(completion.values()) else \"\\nNOT COMPLETE YET\")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
