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.
- 4 folders
- Python + Flask
- lec103-code.zip · 23 KB
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.
One folder, lec103-code. Put it inside your ai-ece course folder. The exercise files are in it too.
How to run it
Python is ready
You did the Python setup (one time).
The code and your key are ready
You did Exercise 103.1, Part A: the
lec103-codefolder is inai-ece, and your key is inlec103-code/.env.Open a terminal in the code folder
Open
ai-ecein VS Code. Open a new terminal. Turn the venv on. Then:cd lec103-codeRun a script
cd 0-basics python 01_first_call.pyRun
02_no_memory.pyto06_mini_app.pythe same way. To go back tolec103-code, typecd ..Run a web app
Each app is a small website that runs on your laptop. Start one app:
cd 1-chat-app python app.pyOpen http://localhost:8084 in your browser. To stop the app, click in the terminal and press Ctrl + C. Then type
cd ..Folder Open in browser What it is 1-chat-applocalhost:8084 Your own ChatGPT in a web page 2-todo-applocalhost:8085 A normal to-do app with no AI 3-todo-app-with-ailocalhost:8086 The 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.py0-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.py1-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.py2-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.py3-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.