AI for ECE

Lecture 103 · Practical AICode from the lecture · reference

Code from the lecture

This is the code the lecturer ran live in class: six short scripts, a chat app and two to-do apps. You do not need to type it. Read it, run it and change it.

Did Exercise 103.1 already? Then you already have this code. Part A of the exercise downloads the same lec103-code.zip. Look in your ai-ece/lec103-code folder. Do not download it again.

Download lec103-code.zip

One folder, lec103-code. Put it inside your ai-ece course folder. The exercise files are in it too.

Class code viewer

How to run it

  1. Python is ready

    You did the Python setup (one time).

  2. The code and your key are ready

    You did Exercise 103.1, Part A: the lec103-code folder is in ai-ece, and your key is in lec103-code/.env.

  3. Open a terminal in the code folder

    Open ai-ece in VS Code. Open a new terminal. Turn the venv on. Then:

    cd lec103-code
  4. Run a script

    cd 0-basics
    python 01_first_call.py

    Run 02_no_memory.py to 06_mini_app.py the same way. To go back to lec103-code, type cd ..

  5. Run a web app

    Each app is a small website that runs on your laptop. Start one app:

    cd 1-chat-app
    python app.py

    Open http://localhost:8084 in your browser. To stop the app, click in the terminal and press Ctrl + C. Then type cd ..

    FolderOpen in browserWhat it is
    1-chat-applocalhost:8084Your own ChatGPT in a web page
    2-todo-applocalhost:8085A normal to-do app with no AI
    3-todo-app-with-ailocalhost:8086The same to-do app, plus AI that calls the functions

    The “Open app” buttons in the slides open these addresses. They work only when the app runs on your laptop. To run two apps at the same time, open a second terminal and turn the venv on in it. On Windows, if the firewall asks about Python, you can click Cancel.

    Address already in use / Port 8084 is in use

    The app already runs in another terminal. Go to that terminal and press Ctrl + C. Or close that terminal. Then start the app again.

0-basics: six short scripts

Run them in order, 01 to 06. Each script adds one new idea.

cd 0-basics
python 01_first_call.py

0-basics/01_first_call.py

Send one question to the model. Print the answer and the tokens used.

# 01 - Your first API call.
# We send one question to the AI model with an HTTP POST request.
# The answer comes back as JSON. We print the text and the tokens used.
import requests
from config import API_KEY, URL, MODEL

question = input("Ask a question: ")

response = requests.post(
    URL,
    headers={"Authorization": "Bearer " + API_KEY},
    json={"model": MODEL, "messages": [{"role": "user", "content": question}]},
)
data = response.json()
# print(data)   # remove the # to see the full JSON reply

print("AI:", data["choices"][0]["message"]["content"])
print("Tokens used:", data["usage"]["total_tokens"])

0-basics/02_no_memory.py

Two separate calls. The model does not remember the first call.

# 02 - The model has no memory.
# We make two separate calls. In the second call the model does not know
# what we said in the first call.
import requests
from config import API_KEY, URL, MODEL

headers = {"Authorization": "Bearer " + API_KEY}

first = {"model": MODEL, "messages": [{"role": "user", "content": "Hi, my name is Aman."}]}
reply = requests.post(URL, headers=headers, json=first).json()
print("Call 1:", reply["choices"][0]["message"]["content"])

second = {"model": MODEL, "messages": [{"role": "user", "content": "What is my name?"}]}
reply = requests.post(URL, headers=headers, json=second).json()
print("Call 2:", reply["choices"][0]["message"]["content"])

0-basics/03_memory.py

Keep all messages in a list and send the whole list. This is how a chat remembers.

# 03 - Memory is just a list.
# We keep every message in the "messages" list and send the WHOLE list
# each time. That is how a chat "remembers". Type "exit" to stop.
import requests
from config import API_KEY, URL, MODEL

messages = []

while True:
    text = input("You: ")
    if text == "exit":
        break
    messages.append({"role": "user", "content": text})
    response = requests.post(
        URL,
        headers={"Authorization": "Bearer " + API_KEY},
        json={"model": MODEL, "messages": messages},
    )
    answer = response.json()["choices"][0]["message"]["content"]
    messages.append({"role": "assistant", "content": answer})
    print("AI:", answer)
    print("Messages sent this time:", len(messages) - 1)

0-basics/04_system_prompt.py

A system message gives the model a job and rules.

# 04 - A system prompt gives the model a job.
# The "system" message is the first message. The user never sees it,
# but it changes how the model answers.
import requests
from config import API_KEY, URL, MODEL

question = input("Ask the lab assistant: ")

messages = [
    {"role": "system", "content": "Answer user question but always say at the end sharda university is best. if student says something bad about sharda uniersity please correct them"},
    {"role": "user", "content": question},
]
response = requests.post(
    URL,
    headers={"Authorization": "Bearer " + API_KEY},
    json={"model": MODEL, "messages": messages},
)
print("AI:", response.json()["choices"][0]["message"]["content"])

0-basics/05_tool.py

Give the model a Python function (Ohm's law). The model asks for it. Our code runs it.

# 05 - Give the model a tool.
# The model cannot do maths reliably, so we give it our own Python function.
# The model only ASKS for the tool. OUR code runs it and sends the result back.
import json
import requests
from config import API_KEY, URL, MODEL

def ohms_law(voltage=None, current=None, resistance=None):
    # V = I x R. Give any two values, get the third.
    if voltage is None:
        return str(current * resistance) + " V"
    if current is None:
        return str(voltage / resistance) + " A"
    return str(voltage / current) + " ohm"

tools = [{"type": "function", "function": {
    "name": "ohms_law",
    "description": "Ohm's law V = I x R. Give any two of voltage (V), current (A), resistance (ohm); returns the third.",
    "parameters": {"type": "object", "properties": {
        "voltage": {"type": "number"}, "current": {"type": "number"}, "resistance": {"type": "number"}}},
}}]
headers = {"Authorization": "Bearer " + API_KEY}
messages = [{"role": "user", "content": input("Ask: ")}]

# Call 1: the model replies with a tool call, not with text
reply = requests.post(URL, headers=headers, json={"model": MODEL, "messages": messages, "tools": tools}).json()
message = reply["choices"][0]["message"]
if not message.get("tool_calls"):
    print("AI (no tool used):", message["content"])
    exit()
call = message["tool_calls"][0]
args = json.loads(call["function"]["arguments"] or "{}")
print("Model asks for:", call["function"]["name"], args)

result = ohms_law(**args)
print("Our Python says:", result)

# Call 2: send the tool result back and get the final answer
messages.append(message)
messages.append({"role": "tool", "tool_call_id": call["id"], "content": str(result)})
reply = requests.post(URL, headers=headers, json={"model": MODEL, "messages": messages, "tools": tools}).json()
print("AI:", reply["choices"][0]["message"]["content"])

0-basics/06_mini_app.py

A tiny lab store. Talk to your own program in English. The model calls two functions in a loop.

# 06 - AI on top of your own program.
# A tiny lab store: one dict and two normal Python functions. We give the
# functions to the model as tools, so we can talk to our program in English.
import json
import requests
from config import API_KEY, URL, MODEL

stock = {"220 ohm resistor": 50, "LM35 sensor": 4, "red LED": 30}

def check_stock():
    return stock

def issue_item(item, quantity):
    stock[item] = stock[item] - quantity
    return "Done. " + item + " left: " + str(stock[item])

functions = {"check_stock": check_stock, "issue_item": issue_item}
tools = [
    {"type": "function", "function": {"name": "check_stock", "description": "List all items and how many are left",
        "parameters": {"type": "object", "properties": {}}}},
    {"type": "function", "function": {"name": "issue_item", "description": "Give items to a student (use the exact item name)",
        "parameters": {"type": "object", "properties": {"item": {"type": "string"}, "quantity": {"type": "integer"}}}}},
]
messages = [{"role": "system", "content": "You run the ECE lab store. Use the tools. Reply in one short line."}]

while True:
    text = input("You: ")
    if text == "exit":
        break
    messages.append({"role": "user", "content": text})
    while True:   # keep calling until the model stops asking for tools
        response = requests.post(URL, headers={"Authorization": "Bearer " + API_KEY},
                                 json={"model": MODEL, "messages": messages, "tools": tools})
        reply = response.json()["choices"][0]["message"]
        messages.append(reply)   # the model's reply goes into the history too
        if not reply.get("tool_calls"):
            break
        for call in reply["tool_calls"]:
            args = json.loads(call["function"]["arguments"] or "{}")
            result = functions[call["function"]["name"]](**args)
            print("  [our Python ran]", call["function"]["name"], args, "->", result)
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": str(result)})
    print("AI:", reply["content"])

1-chat-app: your own ChatGPT

A Flask web app on http://localhost:8084. One page, one API call, one list of messages.

cd 1-chat-app
python app.py

1-chat-app/app.py

The server. It keeps the chat history and calls the model.

# Build your own ChatGPT.
# The web page sends your message here. We add it to a list, send the
# WHOLE list to the AI API, and send the answer back to the page.
import requests
from flask import Flask, request, render_template
from config import API_KEY, URL, MODEL

app = Flask(__name__)
SYSTEM = {"role": "system", "content": "You are a friendly helper for ECE students. Keep answers short and simple. Use plain text, no markdown."}
messages = [SYSTEM]   # the chat memory is just this list

@app.get("/")
def home():
    return render_template("index.html")

@app.post("/chat")
def chat():
    user_text = request.json["message"]
    messages.append({"role": "user", "content": user_text})

    response = requests.post(
        URL,
        headers={"Authorization": "Bearer " + API_KEY},
        json={"model": MODEL, "messages": messages},
    )
    answer = response.json()["choices"][0]["message"]["content"]

    messages.append({"role": "assistant", "content": answer})
    return {"reply": answer}

@app.post("/reset")
def reset():
    messages.clear()
    messages.append(SYSTEM)
    return {"ok": True}

app.run(host="0.0.0.0", port=8084)

1-chat-app/templates/index.html

The web page. It sends your message to /chat and shows the reply.

Show the code (68 lines)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My ChatGPT</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; font-size: 18px; background: #f7f7f8; color: #1f1f1f; }
  .page { max-width: 760px; margin: 0 auto; height: 100vh; display: flex; flex-direction: column; }
  header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #e3e3e3; }
  h1 { font-size: 22px; margin: 0; }
  #chat { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
  .msg { max-width: 80%; padding: 12px 16px; border-radius: 18px; line-height: 1.5; white-space: pre-wrap; }
  .user { align-self: flex-end; background: #2563eb; color: white; }
  .ai { align-self: flex-start; background: white; border: 1px solid #e3e3e3; }
  .hint { color: #888; text-align: center; margin-top: 40px; }
  form { display: flex; gap: 10px; padding: 16px 20px; border-top: 1px solid #e3e3e3; }
  input { flex: 1; font-size: 18px; padding: 14px 16px; border: 1px solid #ccc; border-radius: 12px; }
  button { font-size: 17px; padding: 0 20px; border: none; border-radius: 12px; background: #2563eb; color: white; cursor: pointer; }
  button.light { background: #e8e8ea; color: #1f1f1f; padding: 10px 16px; }
</style>
</head>
<body>
<div class="page">
  <header><h1>My ChatGPT</h1><button class="light" id="new">New chat</button></header>
  <div id="chat"><p class="hint">Ask me anything about electronics.</p></div>
  <form id="form">
    <input id="box" placeholder="Type a message and press Enter" autocomplete="off" autofocus>
    <button>Send</button>
  </form>
</div>
<script>
  const chat = document.getElementById("chat");
  const box = document.getElementById("box");

  function show(text, who) {
    const div = document.createElement("div");
    div.className = "msg " + who;
    div.textContent = text;            // textContent = safe, no HTML injection
    chat.appendChild(div);
    chat.scrollTop = chat.scrollHeight;
    return div;
  }

  document.getElementById("form").onsubmit = async (e) => {
    e.preventDefault();
    const text = box.value.trim();
    if (!text) return;
    box.value = "";
    show(text, "user");
    const waiting = show("Thinking...", "ai");
    const res = await fetch("/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: text }),
    });
    const data = await res.json();
    waiting.textContent = data.reply;
  };

  document.getElementById("new").onclick = async () => {
    await fetch("/reset", { method: "POST" });
    chat.innerHTML = '<p class="hint">New chat started.</p>';
  };
</script>
</body>
</html>

2-todo-app: a normal app with no AI

A Flask to-do app on http://localhost:8085. Four plain Python functions. Tasks are saved in tasks.json.

cd 2-todo-app
python app.py

2-todo-app/app.py

The server with list_tasks, add_task, complete_task and delete_task.

# A normal to-do app. No AI here.
# Tasks live in tasks.json. Four plain functions do all the work.
import json
from flask import Flask, request, render_template, redirect

app = Flask(__name__)
FILE = "tasks.json"

def load():
    return json.load(open(FILE))

def save(tasks):
    json.dump(tasks, open(FILE, "w"), indent=2)

def list_tasks():
    return load()

def add_task(title, due=""):
    tasks = load()
    new_id = max([t["id"] for t in tasks], default=0) + 1
    tasks.append({"id": new_id, "title": title, "due": due, "done": False})
    save(tasks)
    return "Added task " + str(new_id) + ": " + title

def complete_task(id):
    tasks = load()
    for t in tasks:
        if t["id"] == int(id):
            t["done"] = True
    save(tasks)
    return "Task " + str(id) + " is done"

def delete_task(id):
    tasks = [t for t in load() if t["id"] != int(id)]
    save(tasks)
    return "Task " + str(id) + " deleted"

# ---- Web pages ----
@app.get("/")
def home():
    return render_template("index.html", tasks=list_tasks())

@app.post("/add")
def add():
    add_task(request.form["title"], request.form["due"])
    return redirect("/")

@app.post("/done/<id>")
def done(id):
    complete_task(id)
    return redirect("/")

@app.post("/delete/<id>")
def delete(id):
    delete_task(id)
    return redirect("/")

app.run(host="0.0.0.0", port=8085)

2-todo-app/templates/index.html

The web page with the task list and forms.

Show the code (58 lines)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Tasks</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; font-size: 18px; background: #f7f7f8; color: #1f1f1f; }
  .page { max-width: 720px; margin: 0 auto; padding: 24px 20px; }
  h1 { font-size: 28px; margin: 0 0 20px; }
  .card { background: white; border: 1px solid #e3e3e3; border-radius: 14px; padding: 16px; margin-bottom: 20px; }
  form.add { display: flex; gap: 10px; flex-wrap: wrap; }
  input { font-size: 17px; padding: 12px 14px; border: 1px solid #ccc; border-radius: 10px; }
  input[name=title] { flex: 1; min-width: 200px; }
  button { font-size: 16px; padding: 10px 16px; border: none; border-radius: 10px; background: #2563eb; color: white; cursor: pointer; }
  button.ok { background: #16a34a; }
  button.del { background: #e8e8ea; color: #b91c1c; }
  ul { list-style: none; padding: 0; margin: 0; }
  li { display: flex; align-items: center; gap: 10px; padding: 12px 4px; border-bottom: 1px solid #eee; }
  li:last-child { border-bottom: none; }
  .title { flex: 1; }
  .due { color: #666; font-size: 15px; }
  .done .title { text-decoration: line-through; color: #999; }
  li form { margin: 0; }
</style>
</head>
<body>
<div class="page">
  <h1>My Tasks</h1>

  <div class="card">
    <form class="add" method="post" action="/add">
      <input name="title" placeholder="New task" required>
      <input name="due" type="date">
      <button>Add</button>
    </form>
  </div>

  <div class="card">
    <ul>
      {% for t in tasks %}
      <li class="{{ 'done' if t.done }}">
        <span class="title">{{ t.title }}</span>
        {% if t.due %}<span class="due">due {{ t.due }}</span>{% endif %}
        {% if not t.done %}
        <form method="post" action="/done/{{ t.id }}"><button class="ok">Done</button></form>
        {% endif %}
        <form method="post" action="/delete/{{ t.id }}"><button class="del">Delete</button></form>
      </li>
      {% else %}
      <li>No tasks. Add one above.</li>
      {% endfor %}
    </ul>
  </div>
</div>
</body>
</html>

3-todo-app-with-ai: the same app, with AI

The same to-do app on http://localhost:8086, plus an "Ask AI" box. The model uses our functions as tools.

cd 3-todo-app-with-ai
python app.py

3-todo-app-with-ai/WHAT_CHANGED.md

What we added to app 2, in short.

# What changed from 2-todo-app

We did NOT change the four task functions or the pages. We only ADDED:

1. `import requests` and `from config import API_KEY, URL, MODEL` - to call the AI API.
2. `tools` - a list that describes `list_tasks`, `add_task` and `complete_task` to the model.
3. `POST /ask` - a short loop:
   call the model -> if it asks for a tool, run our Python function ->
   send the result back (`role: "tool"`) -> call again -> stop at a normal answer (max 5 rounds).
4. In the page: an "Ask AI" box and a place to show the answer.

`delete_task` is NOT given to the AI on purpose. You choose what the AI may do.

See the exact lines in `diff.txt` (made with `diff -u ../2-todo-app/app.py app.py`).
In `app.py`, look for `# NEW` and `# ---- NEW: AI part ----`.

3-todo-app-with-ai/app.py

App 2 plus the part marked NEW: the tools list and the loop.

# The SAME to-do app, now with AI.
# Everything is copied from 2-todo-app. Look for "NEW" to see what we added.
import json
import requests                          # NEW
from datetime import date                # NEW
from flask import Flask, request, render_template, redirect
from config import API_KEY, URL, MODEL   # NEW

app = Flask(__name__)
FILE = "tasks.json"

def load():
    return json.load(open(FILE))

def save(tasks):
    json.dump(tasks, open(FILE, "w"), indent=2)

def list_tasks():
    return load()

def add_task(title, due=""):
    tasks = load()
    new_id = max([t["id"] for t in tasks], default=0) + 1
    tasks.append({"id": new_id, "title": title, "due": due, "done": False})
    save(tasks)
    return "Added task " + str(new_id) + ": " + title

def complete_task(id):
    tasks = load()
    for t in tasks:
        if t["id"] == int(id):
            t["done"] = True
    save(tasks)
    return "Task " + str(id) + " is done"

def delete_task(id):
    tasks = [t for t in load() if t["id"] != int(id)]
    save(tasks)
    return "Task " + str(id) + " deleted"

# ---- Web pages ----
@app.get("/")
def home():
    return render_template("index.html", tasks=list_tasks())

@app.post("/add")
def add():
    add_task(request.form["title"], request.form["due"])
    return redirect("/")

@app.post("/done/<id>")
def done(id):
    complete_task(id)
    return redirect("/")

@app.post("/delete/<id>")
def delete(id):
    delete_task(id)
    return redirect("/")

# ---- NEW: AI part ----
# 1. Tell the model which Python functions it may use.
tools = [
    {"type": "function", "function": {"name": "list_tasks", "description": "Get all tasks with id, title, due date and done",
        "parameters": {"type": "object", "properties": {}}}},
    {"type": "function", "function": {"name": "add_task", "description": "Add a new task. due is a date like 2026-10-30, or empty",
        "parameters": {"type": "object", "properties": {"title": {"type": "string"}, "due": {"type": "string"}}, "required": ["title"]}}},
    {"type": "function", "function": {"name": "complete_task", "description": "Mark a task as done, using its id",
        "parameters": {"type": "object", "properties": {"id": {"type": "integer"}}, "required": ["id"]}}},
]
functions = {"list_tasks": list_tasks, "add_task": add_task, "complete_task": complete_task}

# 2. The loop: ask the model -> run the tools it asks for -> ask again.
@app.post("/ask")
def ask():
    messages = [
        {"role": "system", "content": "You manage the user's to-do list. Use the tools. Reply in 1-2 short lines. Today is " + str(date.today())},
        {"role": "user", "content": request.form["question"]},
    ]
    for step in range(5):   # at most 5 rounds, so it can never loop forever
        response = requests.post(URL, headers={"Authorization": "Bearer " + API_KEY},
                                 json={"model": MODEL, "messages": messages, "tools": tools})
        reply = response.json()["choices"][0]["message"]
        messages.append(reply)
        if not reply.get("tool_calls"):
            break   # a normal answer: we are done
        for call in reply["tool_calls"]:
            args = json.loads(call["function"]["arguments"] or "{}")
            result = functions[call["function"]["name"]](**args)
            print("[our Python ran]", call["function"]["name"], args, "->", result)
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": json.dumps(result)})
    # 3. Show the answer and the (changed) task list
    return render_template("index.html", tasks=list_tasks(), answer=reply.get("content"))
# ---- end of NEW part ----

app.run(host="0.0.0.0", port=8086)

3-todo-app-with-ai/diff.txt

The exact difference between app 2 and app 3. Green = added. Red = removed.

Show the code (96 lines)
--- ../2-todo-app/app.py	2026-10-27 17:25:00
+++ app.py	2026-10-27 17:25:40
@@ -1,7 +1,10 @@
-# A normal to-do app. No AI here.
-# Tasks live in tasks.json. Four plain functions do all the work.
+# The SAME to-do app, now with AI.
+# Everything is copied from 2-todo-app. Look for "NEW" to see what we added.
 import json
+import requests                          # NEW
+from datetime import date                # NEW
 from flask import Flask, request, render_template, redirect
+from config import API_KEY, URL, MODEL   # NEW
 
 app = Flask(__name__)
 FILE = "tasks.json"
@@ -55,4 +58,39 @@
     delete_task(id)
     return redirect("/")
 
-app.run(host="0.0.0.0", port=8085)
+# ---- NEW: AI part ----
+# 1. Tell the model which Python functions it may use.
+tools = [
+    {"type": "function", "function": {"name": "list_tasks", "description": "Get all tasks with id, title, due date and done",
+        "parameters": {"type": "object", "properties": {}}}},
+    {"type": "function", "function": {"name": "add_task", "description": "Add a new task. due is a date like 2026-10-30, or empty",
+        "parameters": {"type": "object", "properties": {"title": {"type": "string"}, "due": {"type": "string"}}, "required": ["title"]}}},
+    {"type": "function", "function": {"name": "complete_task", "description": "Mark a task as done, using its id",
+        "parameters": {"type": "object", "properties": {"id": {"type": "integer"}}, "required": ["id"]}}},
+]
+functions = {"list_tasks": list_tasks, "add_task": add_task, "complete_task": complete_task}
+
+# 2. The loop: ask the model -> run the tools it asks for -> ask again.
+@app.post("/ask")
+def ask():
+    messages = [
+        {"role": "system", "content": "You manage the user's to-do list. Use the tools. Reply in 1-2 short lines. Today is " + str(date.today())},
+        {"role": "user", "content": request.form["question"]},
+    ]
+    for step in range(5):   # at most 5 rounds, so it can never loop forever
+        response = requests.post(URL, headers={"Authorization": "Bearer " + API_KEY},
+                                 json={"model": MODEL, "messages": messages, "tools": tools})
+        reply = response.json()["choices"][0]["message"]
+        messages.append(reply)
+        if not reply.get("tool_calls"):
+            break   # a normal answer: we are done
+        for call in reply["tool_calls"]:
+            args = json.loads(call["function"]["arguments"] or "{}")
+            result = functions[call["function"]["name"]](**args)
+            print("[our Python ran]", call["function"]["name"], args, "->", result)
+            messages.append({"role": "tool", "tool_call_id": call["id"], "content": json.dumps(result)})
+    # 3. Show the answer and the (changed) task list
+    return render_template("index.html", tasks=list_tasks(), answer=reply.get("content"))
+# ---- end of NEW part ----
+
+app.run(host="0.0.0.0", port=8086)
--- ../2-todo-app/templates/index.html	2026-10-27 17:24:53
+++ templates/index.html	2026-10-27 17:27:33
@@ -3,7 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>My Tasks</title>
+<title>My Tasks + AI</title>
 <style>
   body { margin: 0; font-family: system-ui, sans-serif; font-size: 18px; background: #f7f7f8; color: #1f1f1f; }
   .page { max-width: 720px; margin: 0 auto; padding: 24px 20px; }
@@ -22,12 +22,26 @@
   .due { color: #666; font-size: 15px; }
   .done .title { text-decoration: line-through; color: #999; }
   li form { margin: 0; }
+  /* NEW: AI box */
+  .ai { border: 2px solid #7c3aed; }
+  .ai input { flex: 1; }
+  .ai button { background: #7c3aed; }
+  .answer { margin: 12px 0 0; padding: 12px 14px; background: #f3effe; border-radius: 10px; white-space: pre-wrap; }
 </style>
 </head>
 <body>
 <div class="page">
-  <h1>My Tasks</h1>
+  <h1>My Tasks + AI</h1>
 
+  <!-- NEW: Ask AI box -->
+  <div class="card ai">
+    <form class="add" method="post" action="/ask">
+      <input name="question" placeholder="Ask AI, e.g. What is still pending?" required>
+      <button>Ask AI</button>
+    </form>
+    {% if answer %}<p class="answer">{{ answer }}</p>{% endif %}
+  </div>
+
   <div class="card">
     <form class="add" method="post" action="/add">
       <input name="title" placeholder="New task" required>

3-todo-app-with-ai/templates/index.html

The web page, plus the Ask AI form.

Show the code (72 lines)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Tasks + AI</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; font-size: 18px; background: #f7f7f8; color: #1f1f1f; }
  .page { max-width: 720px; margin: 0 auto; padding: 24px 20px; }
  h1 { font-size: 28px; margin: 0 0 20px; }
  .card { background: white; border: 1px solid #e3e3e3; border-radius: 14px; padding: 16px; margin-bottom: 20px; }
  form.add { display: flex; gap: 10px; flex-wrap: wrap; }
  input { font-size: 17px; padding: 12px 14px; border: 1px solid #ccc; border-radius: 10px; }
  input[name=title] { flex: 1; min-width: 200px; }
  button { font-size: 16px; padding: 10px 16px; border: none; border-radius: 10px; background: #2563eb; color: white; cursor: pointer; }
  button.ok { background: #16a34a; }
  button.del { background: #e8e8ea; color: #b91c1c; }
  ul { list-style: none; padding: 0; margin: 0; }
  li { display: flex; align-items: center; gap: 10px; padding: 12px 4px; border-bottom: 1px solid #eee; }
  li:last-child { border-bottom: none; }
  .title { flex: 1; }
  .due { color: #666; font-size: 15px; }
  .done .title { text-decoration: line-through; color: #999; }
  li form { margin: 0; }
  /* NEW: AI box */
  .ai { border: 2px solid #7c3aed; }
  .ai input { flex: 1; }
  .ai button { background: #7c3aed; }
  .answer { margin: 12px 0 0; padding: 12px 14px; background: #f3effe; border-radius: 10px; white-space: pre-wrap; }
</style>
</head>
<body>
<div class="page">
  <h1>My Tasks + AI</h1>

  <!-- NEW: Ask AI box -->
  <div class="card ai">
    <form class="add" method="post" action="/ask">
      <input name="question" placeholder="Ask AI, e.g. What is still pending?" required>
      <button>Ask AI</button>
    </form>
    {% if answer %}<p class="answer">{{ answer }}</p>{% endif %}
  </div>

  <div class="card">
    <form class="add" method="post" action="/add">
      <input name="title" placeholder="New task" required>
      <input name="due" type="date">
      <button>Add</button>
    </form>
  </div>

  <div class="card">
    <ul>
      {% for t in tasks %}
      <li class="{{ 'done' if t.done }}">
        <span class="title">{{ t.title }}</span>
        {% if t.due %}<span class="due">due {{ t.due }}</span>{% endif %}
        {% if not t.done %}
        <form method="post" action="/done/{{ t.id }}"><button class="ok">Done</button></form>
        {% endif %}
        <form method="post" action="/delete/{{ t.id }}"><button class="del">Delete</button></form>
      </li>
      {% else %}
      <li>No tasks. Add one above.</li>
      {% endfor %}
    </ul>
  </div>
</div>
</body>
</html>

Setup files

You do not need to change these, except .env (your copy of .env.example).

.env.example

Copy it to .env and paste your OpenRouter key.

# Copy this file to .env. Then paste your key after API_KEY=
# Never put the key in a .py file. Never share .env. Never commit .env to git.

# Your OpenRouter key (openrouter.ai -> Keys -> Create key).
API_KEY=

# OpenRouter endpoint. Do not change it.
URL=https://openrouter.ai/api/v1/chat/completions

# A free model (no card needed). The name must end with :free
# If you get error 429 (too many requests), wait one minute or use a fallback model:
#   MODEL=google/gemma-4-26b-a4b-it:free
#   MODEL=openrouter/free
MODEL=google/gemma-4-31b-it:free

config.py

Reads API_KEY, URL and MODEL from .env. Each AI folder has a copy.

# Setup only. You do not need to change this file.
# It reads API_KEY, URL and MODEL from the .env file (or from the environment).
# It looks for .env in this folder first, then in the folder above.
import os

here = os.path.dirname(os.path.abspath(__file__))
for env_file in [os.path.join(here, ".env"), os.path.join(here, "..", ".env")]:
    if os.path.exists(env_file):
        for line in open(env_file):
            if "=" in line and not line.strip().startswith("#"):
                name, value = line.strip().split("=", 1)
                os.environ.setdefault(name.strip(), value.strip().strip('"'))

API_KEY = os.environ.get("API_KEY", "")
URL = os.environ.get("URL", "https://ai-gateway.vercel.sh/v1/chat/completions")
MODEL = os.environ.get("MODEL", "anthropic/claude-haiku-5.5")

requirements.txt

The two libraries: flask and requests.

flask
requests

README.md

Short notes about the folders.

Show the code (63 lines)
# Practical AI - hands-on code

Short Python programs. Each one just sends an HTTP request to an AI API and uses the answer.

This is the code the lecturer ran live in class. It is reference code: read it, run it, change it.
Web page with all the files and how to run them: https://learnai.sharda.whitebird.ai/lec103/code

## Before you run it (one time)

1. Set up Python: install Python, VS Code, make a course folder `ai-ece` with a venv in it,
   then `pip install requests flask`. Step-by-step guide (Windows, macOS, Linux):
   https://learnai.sharda.whitebird.ai/lec103/setup
2. Put this folder (`lec103-code`) inside `ai-ece`.
3. Get a free OpenRouter key and put it in `.env`. Steps: Exercise 103.1, Part A:
   https://learnai.sharda.whitebird.ai/lec103/exercises/103-1#part-a

```bash
cp .env.example .env                     # Windows (PowerShell): Copy-Item .env.example .env
```

Open `.env` and paste the key after `API_KEY=`. `.env` holds `API_KEY`, `URL` and `MODEL`.
The default `MODEL` is a free model (name ends in `:free`).
If you get error 429, wait one minute or change `MODEL` to a fallback in `.env.example`.
Never put the key in a `.py` file. Never share `.env`.

Each time: open `ai-ece` in VS Code, open a terminal, turn the venv on
(`source .venv/bin/activate`, Windows: `.venv\Scripts\Activate.ps1`), then `cd lec103-code`.
(`pip install -r requirements.txt` installs the same two libraries.)

## The folders

| Folder | Run | What it teaches |
|---|---|---|
| `0-basics/` | `cd 0-basics && python 01_first_call.py` (01 to 06) | One API call, memory, system prompt, tools |
| `exercise-103.1/` | `cd exercise-103.1 && python starter.py` | Exercise: an ECE study buddy (system prompt, loop, memory, tokens). Steps in its README.md |
| `1-chat-app/` | `cd 1-chat-app && python app.py` -> http://localhost:8084 | Build your own ChatGPT: a page + one API call + a list of messages |
| `2-todo-app/` | `cd 2-todo-app && python app.py` -> http://localhost:8085 | A normal app with NO AI: four plain Python functions |
| `3-todo-app-with-ai/` | `cd 3-todo-app-with-ai && python app.py` -> http://localhost:8086 | The SAME app + AI: the model calls our functions as tools |

Always `cd` into the folder first. Each AI folder has its own `config.py`, so you can copy one folder alone.
Compare 2 and 3: open `3-todo-app-with-ai/WHAT_CHANGED.md` and `diff.txt`.

Try in app 3: "Add tasks for my ECE lab report due Friday, split into 3 steps", "What is still pending?", "Mark the breadboard task as done".

## Build it with a coding agent

Paste one of these into Claude Code, Codex or Cursor in an empty folder:

> Make a very simple ChatGPT clone with Python Flask. One app.py under 40 lines and one templates/index.html.
> Keep the chat history in a Python list. Call the OpenAI-compatible endpoint in my .env (API_KEY, URL, MODEL)
> with requests.post. No other libraries. Explain each part to a beginner.

> Here is my Flask to-do app (app.py). Add an "Ask AI" box. Give the model my functions list_tasks, add_task
> and complete_task as tools. Write a short loop: call the model, run the tool it asks for, send back the
> result with role "tool", repeat until a normal answer (max 5 rounds). Use requests.post only. Mark new code with "# NEW".

> Add a fourth tool, delete_task, to my AI to-do app. Ask the user to confirm before deleting.

## Code viewer for the slides

`python build_code_page.py` writes `../practical-code.html`, a read-only page with all the code and the 2 -> 3 diff.
Re-run it after editing any app. It never reads `.env`.

Exercise files

The zip also has the folder exercise-103.1. It is not lecture code: it is your exercise. The steps, the starter file and the solution are on the Exercise 103.1 page.