Day 9 — How AI Agents Work: Build a Job Search Agent
Yesterday you built a RAG system — a tool that answers questions when you ask.
Today we go further. Agents don't wait to be asked. They decide what to do, do it, check the result, and do the next thing — on their own.
By the end of this post you will have a working agent that searches jobs, filters by your criteria, ranks by relevance, and drafts outreach messages. You type one sentence. The agent does the rest.
Time required: 60 minutes
Cost: About ₹2 per run
Prerequisite: Python and virtual environment from Day 9
What Makes Something an Agent?
A basic AI call:
You → "Write a cover letter" → AI → Cover letter
One input. One output. Done.
An agent:
You → "Find me relevant jobs and draft applications"
↓
Agent thinks: I need to search for jobs first
↓
Agent calls: search_jobs("software engineer Hyderabad fresher")
↓
Agent sees the results, thinks: Now filter these
↓
Agent calls: filter_jobs(results, keywords=["Python"])
↓
Agent thinks: Now rank by my profile
↓
Agent calls: rank_jobs(filtered, my_profile)
↓
Agent thinks: Draft messages for top 3
↓
Agent calls: draft_message(job_1, my_profile) × 3
↓
Agent: "Done. Here are your 3 applications."
The agent uses tools, observes results, decides what to do next, and repeats until the task is complete.
The 4 Parts of Every Agent
1. The brain (LLM) — GPT-4o reads everything, makes decisions, knows when done.
2. Tools — Functions the AI can call. You define them. AI decides when to use them.
3. Memory — The growing list of messages as the agent works.
4. The loop:
Think → Call a tool → See result → Think → Call a tool → Done
Project Setup
Step 1: Create Project Folder
mkdir job-search-agent
cd job-search-agent
Step 2: Virtual Environment
python -m venv venv
# Windows
venv\Scripts\activate
# Mac/Linux
source venv/bin/activate
Step 3: Install Dependencies
pip install openai python-dotenv requests
Step 4: Create .env
OPENAI_API_KEY=sk-your-key-here
Optional for real job data:
ADZUNA_APP_ID=your_id
ADZUNA_APP_KEY=your_key
Step 5: Create .gitignore
.env
venv/
__pycache__/
Project Structure
job-search-agent/
├── .env
├── .gitignore
├── requirements.txt
├── tools.py ← the 4 functions the agent can call
├── agent.py ← the agent loop
└── run.py ← your profile + task (edit this)
File 1: tools.py
# tools.py
import os
import json
import requests
def search_jobs(query: str, location: str) -> list:
"""Search for job listings. Falls back to mock data if no API keys."""
app_id = os.getenv("ADZUNA_APP_ID")
app_key = os.getenv("ADZUNA_APP_KEY")
if not app_id or not app_key:
print(f" [mock data for '{query}' in {location}]")
return [
{"title": "Software Engineer Trainee", "company": "TCS",
"location": location, "salary": "350000",
"description": "Python, Java, SQL. 2025 batch. 60% cutoff.",
"apply_url": "https://tcs.com/careers"},
{"title": "Junior Developer", "company": "Infosys",
"location": location, "salary": "360000",
"description": "Python and communication skills. Any branch.",
"apply_url": "https://infosys.com/careers"},
{"title": "Associate Engineer", "company": "Wipro",
"location": location, "salary": "350000",
"description": "Java, SQL, OOPs. B.Tech 2025 batch.",
"apply_url": "https://wipro.com/careers"},
{"title": "Data Analyst", "company": "Zoho",
"location": location, "salary": "500000",
"description": "Python, pandas, SQL. Analytics experience preferred.",
"apply_url": "https://zoho.com/careers"},
{"title": "QA Engineer", "company": "Capgemini",
"location": location, "salary": "380000",
"description": "Testing, Selenium, Java. 2025 batch.",
"apply_url": "https://capgemini.com/careers"},
]
try:
url = "https://api.adzuna.com/v1/api/jobs/in/search/1"
params = {
"app_id": app_id, "app_key": app_key,
"what": query, "where": location,
"results_per_page": "10", "sort_by": "date",
}
res = requests.get(url, params=params, timeout=10)
data = res.json()
return [
{
"title": j.get("title", ""),
"company": j.get("company", {}).get("display_name", ""),
"location": j.get("location", {}).get("display_name", location),
"salary": str(j.get("salary_min", "")),
"description": (j.get("description") or "")[:300],
"apply_url": j.get("redirect_url", ""),
}
for j in data.get("results", [])
]
except Exception as e:
return [{"error": str(e)}]
def filter_jobs(jobs: list, required_skills: list = [], min_salary: int = 0) -> list:
"""Filter jobs by required skills and minimum salary."""
filtered = []
for job in jobs:
if job.get("error"):
continue
salary = int(job.get("salary") or 0)
if min_salary > 0 and salary > 0 and salary < min_salary:
continue
combined = ((job.get("description") or "") + " " + (job.get("title") or "")).lower()
if required_skills and not any(s.lower() in combined for s in required_skills):
continue
filtered.append(job)
return filtered
def rank_jobs(jobs: list, student_profile: dict) -> list:
"""Rank jobs by how many of the student's skills they mention."""
if not jobs:
return []
skills = [s.lower() for s in student_profile.get("skills", [])]
scored = []
for job in jobs:
combined = ((job.get("description") or "") + " " + (job.get("title") or "")).lower()
score = sum(1 for s in skills if s in combined)
copy = dict(job)
copy["match_score"] = score
copy["matching_skills"] = [s for s in skills if s in combined]
scored.append(copy)
scored.sort(key=lambda x: x["match_score"], reverse=True)
return scored
def draft_message(job: dict, student_profile: dict) -> str:
"""Draft a personalised outreach message for one job."""
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
prompt = f"""Write a short LinkedIn message to apply for this job.
Job: {job.get('title')} at {job.get('company')} in {job.get('location')}
Description: {job.get('description', '')[:200]}
Student:
Name: {student_profile.get('name')}
Skills: {', '.join(student_profile.get('skills', []))}
Batch: {student_profile.get('batch')}
Portfolio: {student_profile.get('portfolio_url', '')}
Rules: under 80 words, friendly, mention 1 specific skill from the job, include portfolio URL."""
res = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=200
)
return res.choices[0].message.content
File 2: agent.py
# agent.py
import os
import json
from openai import OpenAI
from tools import search_jobs, filter_jobs, rank_jobs, draft_message
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
TOOL_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "search_jobs",
"description": "Search for job listings. Call this first.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Job title or keywords"},
"location": {"type": "string", "description": "City name"},
},
"required": ["query", "location"]
}
}
},
{
"type": "function",
"function": {
"name": "filter_jobs",
"description": "Filter jobs by skills and salary. Call after search_jobs.",
"parameters": {
"type": "object",
"properties": {
"jobs": {"type": "array", "items": {"type": "object"}},
"required_skills": {"type": "array", "items": {"type": "string"}},
"min_salary": {"type": "integer"},
},
"required": ["jobs"]
}
}
},
{
"type": "function",
"function": {
"name": "rank_jobs",
"description": "Rank filtered jobs by relevance. Call after filter_jobs.",
"parameters": {
"type": "object",
"properties": {
"jobs": {"type": "array", "items": {"type": "object"}},
"student_profile": {"type": "object"},
},
"required": ["jobs", "student_profile"]
}
}
},
{
"type": "function",
"function": {
"name": "draft_message",
"description": "Draft outreach message for one job. Call for each top job.",
"parameters": {
"type": "object",
"properties": {
"job": {"type": "object"},
"student_profile": {"type": "object"},
},
"required": ["job", "student_profile"]
}
}
}
]
TOOL_FUNCTIONS = {
"search_jobs": search_jobs,
"filter_jobs": filter_jobs,
"rank_jobs": rank_jobs,
"draft_message": draft_message,
}
def run_agent(task: str, student_profile: dict):
print(f"\n{'='*60}")
print(f"AGENT TASK: {task}")
print(f"{'='*60}\n")
messages = [
{
"role": "system",
"content": f"""You are a job search assistant for an Indian fresher.
Steps to follow in order:
1. Call search_jobs to find openings
2. Call filter_jobs to filter by skills
3. Call rank_jobs to sort by relevance
4. Call draft_message for each of the top 3 jobs
5. Summarise what you found
Do NOT skip steps. Follow this order strictly.
Student profile: {json.dumps(student_profile)}"""
},
{"role": "user", "content": task}
]
step = 0
while True:
step += 1
if step > 15:
print("Max steps reached")
break
print(f"[Step {step}] Thinking...")
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto"
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
print(f"\n{'='*60}\nAGENT COMPLETE\n{'='*60}")
print(msg.content)
return msg.content
for call in msg.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
print(f"[Step {step}] → {name}({list(args.keys())})")
result = TOOL_FUNCTIONS[name](**args)
preview = json.dumps(result)[:80] + "..."
print(f" {preview}")
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False)
})
File 3: run.py
# run.py — edit your profile here, then run: python run.py
from agent import run_agent
MY_PROFILE = {
"name": "Your Name",
"skills": ["Python", "SQL", "Java"],
"batch": "2025",
"location": "Hyderabad",
"min_salary": 300000,
"portfolio_url": "https://resumeportfolio.in/yourusername"
}
TASK = f"""
Find software engineer jobs in {MY_PROFILE['location']} for a {MY_PROFILE['batch']} fresher.
My skills: {', '.join(MY_PROFILE['skills'])}.
Filter for matching roles. Rank by relevance.
Draft outreach messages for the top 3 jobs.
"""
if __name__ == "__main__":
run_agent(TASK, MY_PROFILE)
requirements.txt
openai>=1.0.0
python-dotenv>=1.0.0
requests>=2.28.0
Run It
python run.py
Expected output:
============================================================
AGENT TASK: Find software engineer jobs in Hyderabad...
============================================================
[Step 1] Thinking...
[Step 1] → search_jobs(['query', 'location'])
[{"title": "Software Engineer Trainee"...
[Step 2] Thinking...
[Step 2] → filter_jobs(['jobs', 'required_skills'])
[{"title": "Software Engineer Trainee"...
[Step 3] Thinking...
[Step 3] → rank_jobs(['jobs', 'student_profile'])
[{"title": "Software Engineer Trainee", "match_score": 2...
[Step 4] Thinking...
[Step 4] → draft_message(['job', 'student_profile'])
[Step 5] → draft_message(['job', 'student_profile'])
[Step 6] → draft_message(['job', 'student_profile'])
[Step 7] Thinking...
============================================================
AGENT COMPLETE
============================================================
Found 3 relevant jobs. Top matches:
1. TCS — Software Engineer Trainee
Message: "Hi! I came across TCS's opening and wanted to reach out.
With Python and SQL experience, I think I'd be a strong fit.
My portfolio: resumeportfolio.in/yourname"
...
📖 → 💻 → 🔨 → 🧠 → 🚀
🔨 Break It — Experiment 1: Remove a Tool
Open agent.py. Find TOOL_DEFINITIONS. Delete the entire filter_jobs block (the second tool).
Run python run.py again.
What happens? The agent skips filtering entirely. It goes search → rank → draft. Results may include irrelevant jobs that your skills do not match.
Why? The agent can only use tools that exist. Remove a tool → the agent cannot do that task. It adapts its plan based on available tools.
What you learned: Agents are limited by the tools you give them. A better toolbox = a smarter agent. This is why tool design is the most important skill when building agents.
Put the tool definition back before the next experiment.
🔨 Break It — Experiment 2: Remove Step Instructions from System Prompt
In agent.py, find the system prompt. Change it to:
"content": f"""You are a job search assistant for an Indian fresher.
Find relevant jobs and draft outreach messages.
Student profile: {json.dumps(student_profile)}"""
Remove the numbered steps entirely. Run again.
What happens? The agent may call tools in a different order. It might search then immediately draft without filtering or ranking. It might call filter_jobs twice. The behaviour becomes inconsistent across runs.
Why? The system prompt is the agent's work instructions. Without specific steps, the agent improvises. Sometimes the improvisation is fine. Often it is not.
What you learned: The system prompt is the agent's personality AND its workflow. "Be systematic. Use tools in order." — that one sentence is what makes the agent reliable. Prompt engineering matters at the agent level, not just the chat level.
Restore the original system prompt.
🔨 Break It — Experiment 3: Watch the Agent Get Confused
Change the task in run.py to something vague:
TASK = "Help me with jobs"
Run it.
What happens? The agent may ask clarifying questions. It may make assumptions. It may search for random things. The output quality drops significantly.
Why? Agents work best with specific, well-defined tasks. "Find me software engineer jobs in Hyderabad for a Python developer, filter for roles above 3 LPA, draft 3 applications" gives the agent clear direction.
What you learned: The task description you give an agent is as important as the system prompt. Garbage in, garbage out — even for agents.
Change the task back to something specific.
🧠 What These Experiments Taught You
| Experiment | Removed | Result | Lesson |
|---|---|---|---|
| 1 | filter_jobs tool | Agent skipped filtering | Agents are limited by their tools |
| 2 | Step instructions | Agent became unpredictable | System prompt defines workflow |
| 3 | Specific task | Agent got confused | Clear input = reliable output |
Every production agent system that works reliably has three things: good tools, a clear system prompt with steps, and specific task descriptions. You just learned all three by breaking them.
🚀 The Challenge: Add an Eligibility Checker
Add a fifth tool called check_eligibility:
def check_eligibility(job: dict, student_profile: dict) -> dict:
"""
Check if the student is eligible for this job.
Look for batch year, percentage requirements, or branch requirements
in the job description.
Return: {"eligible": True/False, "reason": "..."}
"""
# Your implementation here
description = (job.get("description") or "").lower()
batch = student_profile.get("batch", "")
# Check if batch year is mentioned and matches
# Check if there are percentage requirements
# Return eligible: True/False with a reason
pass
Add it to TOOL_DEFINITIONS and update the system prompt to use it after filter_jobs and before rank_jobs.
Expected result: The agent will filter out jobs where the student is not eligible before ranking and drafting.
This is a real feature — knowing you are ineligible before spending time on an application saves effort.
Push to GitHub
git init
git add .
git commit -m "Day 9: Job search agent with 4 tools"
git remote add origin https://github.com/yourusername/job-search-agent.git
git push -u origin main
Your repo should have:
job-search-agent/
├── .gitignore ← includes .env and venv/
├── README.md ← 3-line description of what it does
├── requirements.txt
├── tools.py
├── agent.py
└── run.py
Portfolio Description
Project title: AI Job Search Agent
Description: Built a multi-step AI agent using OpenAI's function calling API. The agent autonomously searches job listings, filters by required skills and salary, ranks by profile match, and drafts personalised outreach messages — all from a single natural language task. Implemented the tool-use loop with 4 custom tools and step-level logging for transparency.
Tech stack: Python, OpenAI API (GPT-4o), Adzuna Jobs API
Interview Explanation
"I built a multi-step job search agent using OpenAI's function calling. The agent gets a natural language task and autonomously decides which tools to call in what order. I defined 4 tools — search, filter, rank, and draft. The key insight I learned from breaking it: the system prompt defines the workflow, the tools define the capabilities, and the task defines the scope. Remove any one of those and the agent breaks in a specific, predictable way. Understanding why it breaks is how you understand how to build it right."
Tomorrow: Day 10 — How RAG + Agents work together. You will also learn what makes agents fail in production and how real companies handle it.
Day 9 of 15 — AI Survival Kit for Engineers