{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 🧪 LearnAI Workshop — Session 2: Building\n",
    "### Practical notebook · runs on **Google Colab** · powered by **Google Gemini (free)**\n",
    "\n",
    "In Session 1 you *talked* to an AI. Today you **build** with it — and **test each step before moving on**:\n",
    "\n",
    "| Station | You'll do | ✅ You're done when |\n",
    "|---|---|---|\n",
    "| **4 · Prompt engineering** | Steer the same engine with better prompts | Weak vs strong, few-shot, and step-by-step all changed the answer |\n",
    "| **5 · Give AI a memory (RAG)** | Turn text into numbers, retrieve, then answer | The right note was retrieved *and* the with-context answer was correct |\n",
    "| **6 · Agents & tools** | Let the AI *call your Python functions* | The model used your tool and got an exact answer |\n",
    "\n",
    "> **How to use this:** click each grey **code cell** and press **▶️ (or Shift+Enter)**. Read the note above it first. Run them **in order** from the top."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## 🔑 Step 0 — Get your free Gemini key (once)\n",
    "1. Go to **https://aistudio.google.com/apikey** (sign in with a Google account).\n",
    "2. Click **Create API key** → copy it.\n",
    "3. Come back here and run the cells below. When asked, paste the key and press **Enter**.\n",
    "\n",
    "🔒 **Safety:** an API key is a password. Don't paste it into a public doc, a group chat, or GitHub.\n",
    "\n",
    "> Already did Session 1? These setup cells are the same — run them again in this fresh notebook to reconnect."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Install the Gemini SDK (takes ~15s the first time)\n",
    "!pip -q install google-genai"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Connect to Gemini using your key\n",
    "from google import genai\n",
    "from google.genai import types\n",
    "import getpass, time\n",
    "\n",
    "API_KEY = getpass.getpass('Paste your Gemini API key and press Enter: ').strip()\n",
    "client = genai.Client(api_key=API_KEY)\n",
    "print('Client ready ✅')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Pick models automatically\n",
    "Model names change over time, so instead of hard-coding one, we **ask your key which models it can use** and grab a fast one — plus an **embedding** model we'll need for RAG in Station 5. (This also means the notebook keeps working even after Google renames things.)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# List the models your key can see, then auto-pick a fast one AND an embedding one\n",
    "model_names = [m.name for m in client.models.list()]\n",
    "\n",
    "flash = [m for m in model_names if 'flash' in m and 'embedding' not in m]\n",
    "FAST_MODEL = flash[0] if flash else 'models/gemini-2.5-flash'\n",
    "\n",
    "embeds = [m for m in model_names if 'embedding' in m]\n",
    "EMBED_MODEL = embeds[0] if embeds else 'models/text-embedding-004'\n",
    "\n",
    "print('Using FAST_MODEL  =', FAST_MODEL)\n",
    "print('Using EMBED_MODEL =', EMBED_MODEL)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# ✅ TEST: does everything work? You should see a short reply, then a checkmark.\n",
    "resp = client.models.generate_content(model=FAST_MODEL, contents='Say hello in exactly 5 words.')\n",
    "print(resp.text)\n",
    "assert resp.text, 'No text came back — re-check your API key and the model name above.'\n",
    "print('\\n✅ Setup works — you are ready to build!')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "# 🟦 Station 4 — Prompt engineering\n",
    "You already know the AI is just **predicting the next chunk of text**. Prompting is how you *aim* that prediction. Your prompt **is** the 'text so far' — a vague prompt looks like the start of a vague answer; a precise, well-framed one looks like the start of a precise answer. Same engine, wildly different results.\n",
    "\n",
    "Think of the AI as a **brand-new intern**: eager and capable, but totally dependent on how well you brief it. Let's brief it well."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (a) Weak vs strong: the same task, two worlds\n",
    "A strong prompt quietly carries **5 ingredients**: **Role · Task · Context · Format · Constraints**. Watch the exact same request go from a shrug to a usable draft."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Same goal, two prompts. Run both and compare the replies.\n",
    "weak = 'Help me write an email to my teacher about a late assignment.'\n",
    "\n",
    "strong = ('ROLE: You are helping a high school student. '\n",
    "          'TASK: Write a short, respectful email to my history teacher, Mr. Lee. '\n",
    "          'CONTEXT: I missed the essay deadline because I was sick 2 days. '\n",
    "          'Ask for a 3-day extension. '\n",
    "          'FORMAT: greeting, 3 sentences, sign-off. '\n",
    "          'CONSTRAINTS: polite, not groveling, under 90 words.')\n",
    "\n",
    "print('===== WEAK PROMPT =====')\n",
    "print(client.models.generate_content(model=FAST_MODEL, contents=weak).text)\n",
    "print('\\n===== STRONG PROMPT (Role+Task+Context+Format+Constraints) =====')\n",
    "print(client.models.generate_content(model=FAST_MODEL, contents=strong).text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** both printed. The weak one is generic and had to guess your grade, tone, and length. The strong one barely needs editing — because every ingredient narrowed the field of 'likely next text'. You didn't make the AI smarter; you handed it your intent."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (b) Few-shot: show, don't just tell\n",
    "Sometimes you just ask (*zero-shot*). But when you have a specific judgment call in mind, **show 2–5 solved examples first**, then give the real one. Because the model continues a pattern, those examples set the rubric without you writing a single rule."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Two labeled examples set the pattern; then the model classifies a NEW message.\n",
    "few_shot = '''Classify each message as Happy or Sad.\n",
    "\n",
    "Message: \"I got the lead in the play!!\"   -> Happy\n",
    "Message: \"My best friend is moving away.\"  -> Sad\n",
    "Message: \"We lost the game but I played my best.\"  ->'''\n",
    "\n",
    "print(client.models.generate_content(model=FAST_MODEL, contents=few_shot).text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** having seen the `Message -> label` shape twice, the model's most-likely next text is a matching label. The two examples taught it your rubric by demonstration — often faster and more reliable than explaining every edge case in words."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (c) Chain-of-thought: make it show its work\n",
    "For anything with multiple steps — math, logic, planning — telling the model to **'think step by step'** makes it dramatically more accurate. Why? If it blurts the answer, it's predicting one lucky chunk with no scratch paper. If it writes its reasoning, each step becomes part of the 'text so far', so the next step is predicted *on top of* the work already done."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# The SAME word problem, asked two ways. Watch the accuracy change.\n",
    "problem = 'A shirt is $40, marked down 25%, then $5 shipping is added. What is the final price?'\n",
    "\n",
    "plain = problem + ' Just give the final number.'\n",
    "cot   = problem + ' Think step by step, showing each calculation, then give the final answer.'\n",
    "\n",
    "print('===== ASKED PLAINLY =====')\n",
    "print(client.models.generate_content(model=FAST_MODEL, contents=plain).text)\n",
    "print('\\n===== ASKED STEP BY STEP =====')\n",
    "print(client.models.generate_content(model=FAST_MODEL, contents=cot).text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** the correct answer is **$35** (25% of $40 = $10 → $30 → +$5 shipping = $35). The step-by-step version is more likely to land it *and* shows its work so you can check. Adding 'let's think step by step' is one of the cheapest accuracy upgrades in all of AI building."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 🎯 YOUR TURN: take a weak prompt and rebuild it with all 5 ingredients.\n",
    "my_weak = 'Give me ideas for my science project.'  # <-- your vague first try\n",
    "\n",
    "my_strong = ('ROLE: You are a 9th-grade science teacher. '\n",
    "             'TASK: Suggest science project ideas. '\n",
    "             'CONTEXT: I like environmental science, have 2 weeks and a $20 budget. '\n",
    "             'FORMAT: exactly 5 ideas as a numbered list, each with one sentence on what I would test. '\n",
    "             'CONSTRAINTS: nothing requiring lab equipment.')  # <-- edit all 5 ingredients\n",
    "\n",
    "print('WEAK:\\n', client.models.generate_content(model=FAST_MODEL, contents=my_weak).text)\n",
    "print('\\nSTRONG:\\n', client.models.generate_content(model=FAST_MODEL, contents=my_strong).text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ✅ **Station 4 complete** when a better prompt (strong, few-shot, or step-by-step) visibly beat the weak version. Remember: better prompting improves the *aim*, not the honesty — you still verify facts."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "# 🟩 Station 5 — Give AI a memory (RAG)\n",
    "An LLM never read **your** notes, and it stopped learning on a fixed date. **RAG** (Retrieval-Augmented Generation) fixes this without retraining anything: **retrieve** the relevant facts, **stuff** them into the prompt, then let the model **answer** from them. It's an open-book exam every time.\n",
    "\n",
    "First we need the trick that makes retrieval work: **embeddings**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (a) Embeddings: turning meaning into numbers\n",
    "An **embedding** is a list of numbers (a *vector*) that represents a piece of text's meaning. The magic rule: **text with similar meaning gets similar numbers**, so it lands nearby in 'meaning space'. Let's prove that `dog` and `puppy` sit closer than `dog` and `car`."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Turn three words into vectors and look at how big a vector is.\n",
    "emb = client.models.embed_content(model=EMBED_MODEL, contents=['dog', 'puppy', 'car'])\n",
    "dog   = emb.embeddings[0].values\n",
    "puppy = emb.embeddings[1].values\n",
    "car   = emb.embeddings[2].values\n",
    "\n",
    "print('Each embedding is a list of', len(dog), 'numbers.')\n",
    "print('First 5 numbers of \"dog\":', [round(x, 3) for x in dog[:5]])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** you should see a length (a few hundred numbers) — that's one word turned into a point in meaning space."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# A cosine-similarity function: 1.0 = identical meaning, 0 = unrelated.\n",
    "import numpy as np\n",
    "\n",
    "def cosine(a, b):\n",
    "    a, b = np.array(a), np.array(b)\n",
    "    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))\n",
    "\n",
    "print('dog vs puppy:', round(cosine(dog, puppy), 3), '  (related — should be HIGHER)')\n",
    "print('dog vs car  :', round(cosine(dog, car),   3), '  (unrelated — should be LOWER)')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** `dog`↔`puppy` should score noticeably **higher** than `dog`↔`car`. Matching happens by *meaning*, not shared letters — that's what lets RAG find the right note even when the question is worded nothing like it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### (b) A tiny RAG system, end to end\n",
    "Now the real thing. We have a few **notes** about a made-up student the model could not possibly know. We'll embed them all, embed a **question**, retrieve the single most similar note, and answer **with** that note vs **without** it."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 6 made-up facts the model has never seen (a fictional robotics club member).\n",
    "notes = [\n",
    "    'Maya joined the Northwood Robotics Club in September 2025.',\n",
    "    'The club meets every Tuesday and Thursday at 4pm in Room 214.',\n",
    "    'Maya is the lead programmer and codes the robot in Python.',\n",
    "    'The team qualified for the regional championship in March 2026.',\n",
    "    \"Maya's robot is named Pixel and it weighs 12 kilograms.\",\n",
    "    'The club dues are $25 per semester, waived for volunteers.',\n",
    "]\n",
    "\n",
    "# Embed all the notes once (this is your tiny 'vector database').\n",
    "note_vecs = client.models.embed_content(model=EMBED_MODEL, contents=notes).embeddings\n",
    "print('Embedded', len(notes), 'notes. Ready to retrieve.')"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# The user question. Embed it, then find the most similar note by cosine.\n",
    "question = 'What is the name of Maya\\'s robot?'\n",
    "\n",
    "q_vec = client.models.embed_content(model=EMBED_MODEL, contents=[question]).embeddings[0].values\n",
    "\n",
    "scores = [cosine(q_vec, nv.values) for nv in note_vecs]\n",
    "best = int(np.argmax(scores))\n",
    "retrieved = notes[best]\n",
    "\n",
    "print('Question:', question)\n",
    "print('Retrieved note (most similar):', retrieved)\n",
    "print('Similarity score:', round(scores[best], 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** the retrieved note should be the one about the robot named **Pixel** — the relevant fact, pulled by meaning. If retrieval grabbed the wrong note, no model could answer correctly (garbage in, garbage out)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Answer WITHOUT the note (closed-book) vs WITH it stuffed into the prompt (open-book).\n",
    "without = client.models.generate_content(model=FAST_MODEL, contents=question).text\n",
    "\n",
    "with_context = client.models.generate_content(\n",
    "    model=FAST_MODEL,\n",
    "    contents=f'Use ONLY this note to answer.\\nNote: {retrieved}\\n\\nQuestion: {question}',\n",
    ").text\n",
    "\n",
    "print('===== WITHOUT the note (closed-book) =====')\n",
    "print(without)\n",
    "print('\\n===== WITH the retrieved note (open-book RAG) =====')\n",
    "print(with_context)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** the **with-context** answer should correctly say **Pixel**. The **without-context** answer can only guess or admit it doesn't know — it never read your notes. RAG never made the model smarter; it changed what the model *sees*."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 🎯 YOUR TURN: add your OWN notes, then ask a question they answer.\n",
    "my_notes = [\n",
    "    'Our chess club president is Sam.',            # <-- edit / add your facts\n",
    "    'Chess club meets on Fridays in the library.',\n",
    "    'The club has 14 members this year.',\n",
    "]\n",
    "my_question = 'Who is the chess club president?'   # <-- ask about your notes\n",
    "\n",
    "my_vecs = client.models.embed_content(model=EMBED_MODEL, contents=my_notes).embeddings\n",
    "mq = client.models.embed_content(model=EMBED_MODEL, contents=[my_question]).embeddings[0].values\n",
    "my_best = int(np.argmax([cosine(mq, v.values) for v in my_vecs]))\n",
    "my_note = my_notes[my_best]\n",
    "print('Retrieved:', my_note)\n",
    "\n",
    "ans = client.models.generate_content(\n",
    "    model=FAST_MODEL,\n",
    "    contents=f'Use ONLY this note to answer.\\nNote: {my_note}\\n\\nQuestion: {my_question}',\n",
    ").text\n",
    "print('Answer:', ans)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ✅ **Station 5 complete** when your tiny RAG retrieved the *right* note and the with-context answer was correct. When a RAG app is wrong, the bug is usually in **retrieval**, not the model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "# 🟨 Station 6 — Agents & tools\n",
    "A chatbot only **talks**. An **agent** takes **action** — it uses *tools* and keeps going until the job is done. A tool is just a **function the AI is allowed to call**. You describe the tools; the *model* decides when to use them. This is called **function-calling**.\n",
    "\n",
    "The loop under every agent: **think → act → observe → repeat**. Think ('a calculator would help'), act (call the tool), observe (read the real result), repeat — then answer. Let's give the model real Python functions and watch it use them."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Define plain Python functions. Type hints + a docstring tell the model how to use them.\n",
    "def multiply(a: int, b: int) -> int:\n",
    "    \"\"\"Return the exact product of two integers a and b.\"\"\"\n",
    "    return a * b\n",
    "\n",
    "def count_letters(word: str, letter: str) -> int:\n",
    "    \"\"\"Return how many times `letter` appears in `word`.\"\"\"\n",
    "    return word.lower().count(letter.lower())\n",
    "\n",
    "print('Tools defined:', multiply(6, 7), '/', count_letters('strawberry', 'r'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** you should see `42 / 3`. These are ordinary functions right now — next we hand them to the model as tools."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# First, WITHOUT tools: the model must predict digits, and can fumble exact math.\n",
    "no_tool = client.models.generate_content(model=FAST_MODEL, contents='What is 231 * 47?').text\n",
    "print('No tool:', no_tool)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Now WITH tools: google-genai does automatic function calling.\n",
    "# Pass the functions in tools=[...]; the SDK runs the think->act->observe loop for you.\n",
    "cfg = types.GenerateContentConfig(tools=[multiply, count_letters])\n",
    "\n",
    "resp = client.models.generate_content(\n",
    "    model=FAST_MODEL,\n",
    "    contents='What is 231 * 47? Use the tool.',\n",
    "    config=cfg,\n",
    ")\n",
    "print('With tool:', resp.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** the correct answer is **10857** (231 × 47). The model *decided* to call `multiply`, your real Python ran it, and the exact number came back into the conversation. It never did the arithmetic itself — it used a tool, exactly like an agent."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# The model picks the RIGHT tool for the job. Here it should reach for count_letters.\n",
    "resp2 = client.models.generate_content(\n",
    "    model=FAST_MODEL,\n",
    "    contents='How many times does the letter r appear in the word strawberry? Use the tool.',\n",
    "    config=cfg,\n",
    ")\n",
    "print(resp2.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🧪 **Test:** the answer should be **3** — exact, because a tool counted instead of the model guessing."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# 🎯 YOUR TURN: add a NEW tool, then ask a question that needs it.\n",
    "def days_until(month: int, day: int) -> int:\n",
    "    \"\"\"Return the number of days from today until the given month/day this year.\"\"\"\n",
    "    import datetime\n",
    "    today = datetime.date.today()\n",
    "    target = datetime.date(today.year, month, day)\n",
    "    return (target - today).days\n",
    "\n",
    "my_cfg = types.GenerateContentConfig(tools=[multiply, count_letters, days_until])\n",
    "my_resp = client.models.generate_content(\n",
    "    model=FAST_MODEL,\n",
    "    contents='How many days until December 25? Use the tool.',  # <-- ask something your tool answers\n",
    "    config=my_cfg,\n",
    ")\n",
    "print(my_resp.text)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> ✅ **Station 6 complete** when the model used one of *your* tools and the result was exact. Reality check: an agent that takes *real* actions (deleting, emailing, spending) needs a human to review the risky ones — 'it seemed confident' is not a safety check."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## 🎉 You finished Session 2!\n",
    "You didn't just talk to an AI — you **built** with it. You:\n",
    "- **Steered** the same engine with better prompts — strong 5-part prompts, few-shot examples, and chain-of-thought.\n",
    "- Gave the AI a **memory** with RAG — embeddings turned words into numbers, and you retrieved the right note before answering.\n",
    "- Built a tiny **agent** — the model called *your* Python tools to get exact answers.\n",
    "\n",
    "**Next up — Session 3: Going Further.** We'll tackle grounding & hallucinations head-on, go beyond text (images & sound), and talk **safety** — how to put guardrails around agents that take real actions.\n",
    "\n",
    "_LearnAI · part of MillionRoots. Keep this notebook — you can reuse the setup cells any time._"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "name": "LearnAI Session 2 — Building",
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}