Deepika, 16, from Lucknow spent three hours every day doing research for her school debates โ searching Google, reading papers, taking notes, summarising. "Can I automate this?" she wondered after Lesson 7.
A regular ChatGPT prompt wouldn't work โ it couldn't search the web or read PDFs. She needed an agent: a system that could decide what tools to use, use them in sequence, and synthesise the results into an answer.
Using LangGraph and OpenAI's tool-calling API, she built a research agent in 150 lines of Python. It searches arXiv, reads abstracts, cross-references sources, and writes a structured summary. What used to take 3 hours now takes 3 minutes. She now uses those 3 hours to actually understand the papers.
A chatbot answers one question per turn. An agent can take multiple actions autonomously to complete a goal. It has three components:
- Perceive: Read the current state โ user request, previous actions, tool outputs
- Plan: Decide what to do next โ which tool to use, or whether to give a final answer
- Act: Execute the tool call or produce the final response
The ReAct (Reason + Act) pattern alternates between thinking (generating a reasoning trace) and acting (calling tools), enabling the agent to use tool outputs to inform its next reasoning step.
import openai, json
client = openai.OpenAI()
# โโ Define tools as JSON schemas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web and return top 3 relevant results",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression safely",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Python math expression e.g. '2**10 + 5*3'"
}
},
"required": ["expression"]
}
}
}
]
# โโ Implement the actual tools โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def search_web(query: str) -> str:
"""Stub โ replace with SerpAPI, Tavily, or DuckDuckGo API."""
return f"Search results for '{query}': [3 relevant web snippets]"
def calculate(expression: str) -> str:
"""Safe math evaluation โ no exec/eval of arbitrary code."""
import ast, math
try:
# Only allow safe nodes
tree = ast.parse(expression, mode='eval')
# Whitelist: numbers, operators, basic math functions
allowed_nodes = {
ast.Expression, ast.BinOp, ast.UnaryOp, ast.Num,
ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow,
ast.USub, ast.UAdd, ast.Constant
}
for node in ast.walk(tree):
if type(node) not in allowed_nodes:
return "Error: unsafe expression"
result = eval(compile(tree, "", "eval"))
return str(result)
except Exception as e:
return f"Error: {e}"
TOOL_MAP = {"search_web": search_web, "calculate": calculate}
# โโ Agent loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def run_agent(user_message: str) -> str:
messages = [
{"role": "system", "content": "You are a helpful research assistant. Use tools to find accurate information."},
{"role": "user", "content": user_message}
]
for _ in range(10): # max 10 reasoning steps
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
# If no tool call: agent has a final answer
if not msg.tool_calls:
return msg.content
# Execute each tool call
messages.append(msg)
for tc in msg.tool_calls:
fn_name = tc.function.name
fn_args = json.loads(tc.function.arguments)
result = TOOL_MAP[fn_name](**fn_args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
return "Max steps reached"
print(run_agent("What is 2^10 and who wrote the Attention Is All You Need paper?"))
In-Context Memory
The message history in the current conversation. Limited by context window (e.g., 128k tokens for GPT-4o). Lost when conversation ends.
External / Vector Memory
Past facts stored in a vector database (ChromaDB, Pinecone). Retrieved by similarity search. Survives across sessions.
Episodic Memory
Structured log of past agent actions and outcomes. The agent can learn from past mistakes by reviewing its history.
# External memory with ChromaDB (persistent across sessions)
# pip install chromadb
import chromadb
chroma = chromadb.PersistentClient(path="./agent_memory")
collection = chroma.get_or_create_collection("research_notes")
def remember(key: str, text: str):
"""Store a research note in vector memory."""
collection.upsert(
documents=[text],
ids=[key]
)
def recall(query: str, n=3) -> list[str]:
"""Retrieve most relevant past notes."""
results = collection.query(query_texts=[query], n_results=n)
return results['documents'][0]
# Usage:
remember("rag_2024", "RAG 2024: Corrective RAG paper shows 15% improvement over naive RAG...")
similar_notes = recall("what do I know about RAG?")
print(similar_notes)
# pip install langgraph langchain-openai
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
import operator
# โโ Define agent state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
step_count: int
# โโ Nodes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_arxiv(query: str) -> str:
"""Search arXiv for papers on a topic."""
return f"Papers found for '{query}': [arxiv results]"
@tool
def summarise_abstract(arxiv_id: str) -> str:
"""Get and summarise the abstract of an arXiv paper."""
return f"Abstract summary for {arxiv_id}: [key findings]"
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([search_arxiv, summarise_abstract])
def agent_node(state: AgentState) -> AgentState:
response = llm.invoke(state["messages"])
return {"messages": [response], "step_count": state["step_count"] + 1}
def should_continue(state: AgentState) -> str:
last = state["messages"][-1]
if last.tool_calls:
return "tools"
return END
# โโ Build graph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
tool_node = ToolNode([search_arxiv, summarise_abstract])
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent") # after tools, return to agent
app = graph.compile()
# โโ Run โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
result = app.invoke({
"messages": [{"role": "user", "content": "Find 3 key papers on RAG from 2024 and summarise each"}],
"step_count": 0
})
print(result["messages"][-1].content)