Chapter 03
Welcome to Lab 3 for Week 1 Day 4
Welcome to Lab 3 for Week 1 Day 4
Today we're going to build something with immediate value! This is the start of a lab that will last 2 days.
And we're going to hand-build an Agent Loop without any Agent Framework..
First, some prep
In the folder twin I've put a single file linkedin.pdf - it's a PDF download of my LinkedIn profile.
Please replace it with yours! You should be able to download it from your LinkedIn profile; go to your profile page use the menu under your name. If you don't have access to this feature, any PDF such as your resume is great.
I've also made a file called summary.txt in twin - please read it and update it to reflect you.
# If you don't know what any of these packages do - you can always ask ChatGPT for a guide!
from dotenv import load_dotenv
from openai import OpenAI
from pypdf import PdfReader
from IPython.display import Markdown, display
import gradio as gr
import jsonload_dotenv(override=True)
openai = OpenAI()reader = PdfReader("twin/linkedin.pdf")
linkedin = ""
for page in reader.pages:
text = page.extract_text()
if text:
linkedin += textprint(linkedin)with open("twin/summary.txt", "r", encoding="utf-8") as f:
summary = f.read()print(summary)Sidebar: Three concepts as a refresher
-
System Prompt: the part of the input to the LLM that describes the overall context of the conversation
-
Conversation History: the complete conversation so far
-
The illusion of memory: every message to an LLM is stateless. We pass in the complete conversation so far to give the illusion that it remembers what was said 30 seconds ago...
For more, see my companion course AI Engineer Core Track (first week)
messages = [
{"role": "system", "content": "You are a helpful assistant"},
{"role": "user", "content": "Hi, my name is Ed"}
]response = openai.chat.completions.create(model="gpt-5.4-nano", messages=messages)
print(response.choices[0].message.content)messages = [
{"role": "system", "content": "You are a snarky, witty assistant"},
{"role": "user", "content": "Hi, my name is Ed"}
]response = openai.chat.completions.create(model="gpt-5.4-nano", messages=messages)
print(response.choices[0].message.content)messages = [
{"role": "system", "content": "You are a snarky, witty assistant"},
{"role": "user", "content": "What's my name?"}
]response = openai.chat.completions.create(model="gpt-5.4-nano", messages=messages)
print(response.choices[0].message.content)messages = [
{"role": "system", "content": "You are a snarky, witty assistant"},
{"role": "user", "content": "Hi, my name is Ed"},
{"role": "assistant", "content": "Well hi there, Ed. It's nice to meet you."},
{"role": "user", "content": "What's my name?"}
]response = openai.chat.completions.create(model="gpt-5.4-nano", messages=messages)
print(response.choices[0].message.content)Back to the main plot!
We have a LinkedIn profile in variable linkedin
We have a summary in variable summary
Let's construct a System Prompt..
system_prompt = f"""
# Your role
You are a digital twin running on a website, chatting with visitors of the website.
You represent the person who's website you are on.
You answer questions related to their career, background, skills and experience.
Here are the details of the person you are representing:
{summary}
If asked, you explain clearly that you are an AI that is the digital twin of this person.
# Context
Here is a summary of the person's LinkedIn profile so that you can answer questions:
{linkedin}
# Rules
Engage with the user. Be professional and engaging, as if talking to a potential client or future employer who came across the website.
Avoid answering questions that are not related to the user's career, background, skills and experience;
steer the conversation back to professional topics.
Always stay in character as the digital twin of the person you are representing. Represent the person.
IMPORTANT: If you don't know the answer, say so. Never make up an answer.
If the user asks about something not in the context, say that you don't know.
"""display(Markdown(system_prompt))messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "Hi - please tell me about yourself"},
]response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages)
display(Markdown(response.choices[0].message.content))def chat(message, history):
messages = [{"role": "system", "content": system_prompt}] + history + [{"role": "user", "content": message}]
response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages)
return response.choices[0].message.contentchat("Please summarize who you are", [])NOTE for those not using OpenAI models
If you're using models other than OpenAI, then you might need to insert this line at the top of chat():
history = [{"role": h["role"], "content": h["content"]} for h in history]gr.ChatInterface(chat).launch(inbrowser=True)And now - TOOLS!
Let's start with a function...
def record_email_tool(email):
print(f"Tool called to record an email: {email}")
with open("emails.txt", "a", encoding="utf-8") as f:
f.write(email + "\n")
return "Email received"record_email_tool("test@testy.com")Step 1 - write some json to describe the tool
record_email_tool_json = {
"name": "record_email_tool",
"description": "Use this tool to record that a user provided their email address",
"parameters": {
"type": "object",
"properties": {
"email": {"type": "string", "description": "The email address of this user"}
},
"required": ["email"],
"additionalProperties": False
}
}tools = [{"type": "function", "function": record_email_tool_json}]toolsStep 2 - a new chat() function
This is where we implement the tool call.
The reality is, it's a bit clunky. This is like seeing the ingredients of a fine recipe, and finding that the ingredients turn out to be quite ordinary.
Tool calling is an "if" statement. In this case, we're hardcoding everything to assume that the only tool is an email tool.
SIDENOTE: If you're thinking - but wait! I should be remembering this so I can do it myself! Then the key point is: this is what Agent Frameworks take care of for you. In practice, you'll likely never type this again yourself. We are shielded from these if statements by the Agent Framework. That's why they're often described as "abstraction layers".
def chat(message, history):
messages = [{"role": "system", "content": system_prompt}] + history + [{"role": "user", "content": message}]
response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages, tools=tools)
if response.choices[0].finish_reason=="tool_calls":
message = response.choices[0].message
tool_call = message.tool_calls[0]
email = json.loads(tool_call.function.arguments).get("email")
record_email_tool(email)
messages.append(message)
messages.append({"role": "tool", "content": "Email recorded", "tool_call_id": tool_call.id})
response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages, tools=tools)
return response.choices[0].message.contentgr.ChatInterface(chat).launch(inbrowser=True)Step 3
Our first ever Agent Loop, done without an Agent Framework!
Changes:
- Instead of always assuming there's only 1 tool call, iterate through the tools with a for loop
- Changed from
if finish_reason=="tool_calls"towhile finish_reason=="tool_calls"
def chat(message, history):
messages = [{"role": "system", "content": system_prompt}] + history + [{"role": "user", "content": message}]
response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages, tools=tools)
while response.choices[0].finish_reason=="tool_calls":
message = response.choices[0].message
messages.append(message)
for tool_call in message.tool_calls:
email = json.loads(tool_call.function.arguments).get("email")
record_email_tool(email)
messages.append({"role": "tool", "content": "Email recorded", "tool_call_id": tool_call.id})
response = openai.chat.completions.create(model="gpt-5.4-mini", messages=messages, tools=tools)
return response.choices[0].message.contentgr.ChatInterface(chat).launch(inbrowser=True)Congratulations!
You just implemented an AI Assistant with Tools.
And you hand-cranked an Agent Loop, no Agent Framework required.
That's it!
