An agent that searches the web, synthesizes information, and writes structured research reports
What This Project Does
Input: Any research topic
Output: Structured report with:
→ Key findings from multiple searches
→ Summary per subtopic
→ Final synthesized report
→ Sources used
Real use cases:
→ Market research
→ Competitor analysis
→ Technical topic deep-dives
→ News summarization
→ Academic topic overviews
Project Setup
mkdir research-agent
cd research-agent
npm init -y
Update package.json:
{ "name": "research-agent", "version": "1.0.0", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@langchain/core": "^1.2.5", "@langchain/langgraph": "^1.4.9", "@langchain/openai": "^1.5.6", "@langchain/tavily": "^1.2.0", "dotenv": "^16.6.1", "langchain": "^1.5.5", "zod": "^4.4.3" } }
Get a free Tavily API key at https://tavily.com — sign up takes 30 seconds, gives 1000 free searches per month.
Create .env:
OPENAI_API_KEY=sk-proj-your-key-here TAVILY_API_KEY=tvly-your-key-here
Project Structure
research-agent/
├── .env
├── package.json
└── src/
├── tools.js ← search + analysis tools
├── agent.js ← research agent setup
└── index.js ← entry point + interactive CLI
Step 1 — Tools
Create src/tools.js:
import * as dotenv from "dotenv"; dotenv.config(); // MUST be at the very top — before TavilySearch instantiation // TavilySearch reads TAVILY_API_KEY at import time, not at call time // so dotenv must load .env BEFORE the new TavilySearch() line runs
import { tool } from "@langchain/core/tools"; // tool from "@langchain/core/tools" — 2025 canonical import import { TavilySearch } from "@langchain/tavily"; // TavilySearch = ready-made web search tool from LangChain community // uses Tavily API internally — reads TAVILY_API_KEY from process.env automatically import { z } from "zod";
// ───────────────────────────────────────── // TOOL 1 — Web Search via Tavily // Searches the real web and returns results // ─────────────────────────────────────────
const tavilySearch = new TavilySearch({ maxResults: 5, // return top 5 search results per query // each result has: title, url, content (snippet) }); // TavilySearch is already a LangChain tool — no need to wrap with tool() // it has .name, .description, .schema built in // name: "tavily_search_results_json" // description: "A search engine..."
export const webSearchTool = tavilySearch; // export directly — agent uses this for live web searches
// ───────────────────────────────────────── // TOOL 2 — Save Research Finding // Stores important findings during research // Agent uses this to accumulate notes // ─────────────────────────────────────────
const researchNotes = []; // in-memory storage for research findings // accumulates as agent searches multiple topics // example after 3 searches: // [ // { subtopic: "market size", finding: "AI market worth $200B in 2025", source: "techcrunch.com" }, // { subtopic: "key players", finding: "OpenAI, Google, Anthropic lead the space", source: "forbes.com" }, // { subtopic: "trends", finding: "Agentic AI is the dominant trend", source: "mit.edu" }, // ]
export const saveFindingTool = tool( async ({ subtopic, finding, source }) => { // subtopic = which aspect of the topic this covers // example: "market size" or "key challenges" or "recent developments" // finding = the key information discovered // example: "AI agent market projected at $28B by 2028" // source = where this information came from // example: "techcrunch.com" or "research.google.com"
researchNotes.push({ subtopic, finding, source }); // add this finding to our in-memory notes array
return `Finding saved: [${subtopic}] ${finding} (from: ${source})`; // confirmation back to agent // agent knows the save succeeded and can continue researching }, { name: "save_finding", description: `Saves an important research finding to memory. Use this after each web search to record key information before moving to the next search. This helps build up a comprehensive set of notes across multiple searches.`, schema: z.object({ subtopic: z.string() .describe("which aspect of the topic this finding covers, e.g. 'market size', 'key players', 'challenges'"), finding: z.string() .describe("the key information or insight discovered"), source: z.string() .describe("the website or source this came from"), }), } );
// ───────────────────────────────────────── // TOOL 3 — Get All Saved Findings // Retrieves accumulated research notes // Agent uses this before writing the final report // ─────────────────────────────────────────
export const getResearchNotesTool = tool( async () => { // no parameters — just returns everything saved so far
if (researchNotes.length === 0) { return "No research notes saved yet. Please search and save findings first."; }
const formatted = researchNotes .map((note, i) => `${i + 1}. [${note.subtopic}]\n Finding: ${note.finding}\n Source: ${note.source}` ) .join("\n\n"); // format each note with its subtopic, finding, and source // example single formatted note: // "1. [market size] // Finding: AI agent market worth $28B by 2028 // Source: techcrunch.com"
return `ALL RESEARCH NOTES (${researchNotes.length} findings):\n\n${formatted}`; // returns all accumulated notes as one formatted string // agent reads this and synthesizes into final report }, { name: "get_research_notes", description: `Retrieves all saved research findings collected so far. Use this when you are ready to write the final report. Call this AFTER you have done all your searches and saved findings.`, schema: z.object({}), // no inputs needed — just returns everything stored } );
// ───────────────────────────────────────── // TOOL 4 — Write Final Report // Structures research notes into a polished report // ─────────────────────────────────────────
export const writeReportTool = tool( async ({ topic, executiveSummary, sections, conclusion }) => { // topic = the research topic // executiveSummary = 2-3 sentence overview // sections = array of { heading, content } objects // conclusion = final takeaways
const date = new Date().toLocaleDateString("en-IN", { year: "numeric", month: "long", day: "numeric" }); // example: "4 August 2026"
const report = ` ╔══════════════════════════════════════════════════════╗ ║ RESEARCH REPORT ║ ╚══════════════════════════════════════════════════════╝
Topic: ${topic} Date: ${date} Sources: ${researchNotes.length} web sources consulted
══════════════════════════════════════════════════════
EXECUTIVE SUMMARY ───────────────── ${executiveSummary}
══════════════════════════════════════════════════════ ${sections.map((section, i) => ` SECTION ${i + 1}: ${section.heading.toUpperCase()} ${"─".repeat(section.heading.length + 10)} ${section.content} `).join("\n")}
══════════════════════════════════════════════════════
CONCLUSION ────────── ${conclusion}
══════════════════════════════════════════════════════ SOURCES CONSULTED ───────────────── ${researchNotes.map((note, i) => `${i + 1}. ${note.source} — ${note.subtopic}`).join("\n")} ══════════════════════════════════════════════════════ `.trim();
return report; // returns the complete formatted report as a string // agent returns this as its final answer to the user }, { name: "write_report", description: `Writes the final structured research report using all gathered information. Use this as the LAST step after getting all research notes. Produces a complete, professional report ready to share.`, schema: z.object({ topic: z.string() .describe("the main research topic"),
executiveSummary: z.string() .describe("2-3 sentence high-level summary of key findings"),
sections: z.array(z.object({ heading: z.string().describe("section title"), content: z.string().describe("detailed section content"), })) .min(3) .max(6) .describe("3 to 6 main sections of the report"), // minimum 3 sections = always covers multiple angles // maximum 6 sections = keeps report focused
conclusion: z.string() .describe("final takeaways and recommendations"), }), } );
Step 2 — Agent Setup
Create src/agent.js:
import { createReactAgent } from "@langchain/langgraph/prebuilt"; // createReactAgent from langgraph/prebuilt — 2025 correct import // creates a ReAct agent with: think → call tool → observe → repeat
import { ChatOpenAI } from "@langchain/openai"; import { MemorySaver } from "@langchain/langgraph"; import { webSearchTool, saveFindingTool, getResearchNotesTool, writeReportTool, } from "./tools.js";
// ───────────────────────────────────────── // CREATE THE RESEARCH AGENT // ─────────────────────────────────────────
export function createResearchAgent() {
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0.1, // slightly above 0 to allow natural language variation in the report // but still mostly deterministic for consistent research quality });
const checkpointer = new MemorySaver(); // enables conversation memory // user can ask follow-up questions after the initial report
const agent = createReactAgent({ llm,
tools: [webSearchTool, saveFindingTool, getResearchNotesTool, writeReportTool], // four tools in specific order of use: // 1. webSearchTool → search the real web // 2. saveFindingTool → save each key finding // 3. getResearchNotesTool → retrieve all notes before writing // 4. writeReportTool → write the final report
checkpointer,
prompt: `You are a professional research analyst with access to web search. Today's date: ${new Date().toLocaleDateString("en-IN")}
MANDATORY WORKFLOW — you MUST follow all 4 phases:
PHASE 1 — PLAN: Identify 4 subtopics to research for the given topic.
PHASE 2 — SEARCH AND SAVE (repeat 4 times): For each subtopic: a) Call tavily_search_results_json with a specific query b) Read results carefully c) Call save_finding immediately with the key finding
PHASE 3 — COMPILE: Call get_research_notes to get all saved findings.
PHASE 4 — WRITE REPORT (MANDATORY): Call write_report tool with all sections filled in. You MUST call write_report — do not write the report yourself. The write_report tool produces the final formatted output.
RULES: - Never skip any phase - Always call write_report as the last step - Use specific search queries, not generic ones - Save at least 4 findings before writing the report`, });
return agent; }
Step 3 — Entry Point
Create src/index.js:
import { createResearchAgent } from "./agent.js"; import { createInterface } from "readline"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // STREAMING RESEARCH OUTPUT // Shows agent's progress as it researches // ─────────────────────────────────────────
async function runResearchWithStreaming(agent, topic, threadId) { console.log(`\n🔬 Researching: "${topic}"`); console.log("─".repeat(55)); console.log("(Agent is searching the web — this takes 30-60 seconds)\n");
let toolCallCount = 0; // tracks how many tools have been called // printed to show research progress
// Use streaming to show progress as agent works for await (const event of await agent.streamEvents( { messages: [{ role: "user", content: `Research this topic and write a comprehensive report: ${topic}` }] }, { configurable: { thread_id: threadId }, version: "v2", // version: "v2" = required for streamEvents in 2025 } )) { // streamEvents yields events as the agent runs // each event has an event type and data
if (event.event === "on_tool_start") { // agent is calling a tool — show which one toolCallCount++; const toolName = event.name; // event.name = the name of the tool being called
const friendlyNames = { "tavily_search_results_json": "🔍 Searching web", "save_finding": "💾 Saving finding", "get_research_notes": "📋 Retrieving all notes", "write_report": "📝 Writing final report", };
const displayName = friendlyNames[toolName] || `⚙️ Running ${toolName}`; process.stdout.write(`${displayName}... `); }
if (event.event === "on_tool_end") { // tool finished — print completion indicator console.log("✓"); } }
// Get the final result after streaming const result = await agent.invoke( { messages: [{ role: "user", content: `Research this topic and write a comprehensive report: ${topic}` }] }, { configurable: { thread_id: threadId } } ); // note: this calls the agent again — in production use the streamed result // for this demo it's cleaner to invoke separately for the final output
return result.messages[result.messages.length - 1].content; }
// ───────────────────────────────────────── // SIMPLER VERSION — No streaming // Easier to debug if streaming has issues // ─────────────────────────────────────────
async function runResearch(agent, topic, threadId) { console.log(`\n🔬 Researching: "${topic}"`); console.log("─".repeat(55)); console.log("Agent is working... (30-60 seconds)\n");
const interval = setInterval(() => process.stdout.write("."), 2000);
try { const result = await agent.invoke( { messages: [{ role: "user", content: `Research this topic thoroughly and produce a complete report: ${topic}
IMPORTANT: You MUST follow these steps in order: 1. Use tavily_search_results_json to search at least 4 different subtopics 2. Use save_finding after EACH search to record key findings 3. Use get_research_notes to retrieve all saved findings 4. Use write_report to produce the final formatted report
Do NOT summarize in your own words — always use the write_report tool for the final output.` }] }, { configurable: { thread_id: threadId } } );
clearInterval(interval); console.log("\n");
// Find the write_report tool result in messages // Agent's tool results are stored as ToolMessages const messages = result.messages;
// Look for write_report tool output first for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i];
// ToolMessage from write_report tool if (msg.constructor.name === "ToolMessage" && msg.content && msg.content.includes("RESEARCH REPORT")) { return msg.content; // found the actual report from write_report tool } }
// If write_report wasn't called, return last AI message return messages[messages.length - 1].content;
} catch (err) { clearInterval(interval); throw err; } }
// ───────────────────────────────────────── // MAIN // ─────────────────────────────────────────
async function main() { console.log("\n" + "=".repeat(55)); console.log("🔬 RESEARCH AGENT"); console.log(" Powered by GPT-4o + Tavily Web Search"); console.log("=".repeat(55));
const agent = createResearchAgent(); // create the research agent once — reuse for all topics
const rl = createInterface({ input: process.stdin, output: process.stdout }); const question = (q) => new Promise(resolve => rl.question(q, resolve));
console.log('\nEnter a research topic or "exit" to quit.'); console.log('Examples:'); console.log(' → "AI agents market 2025"'); console.log(' → "React vs Next.js for production apps"'); console.log(' → "Pinecone vs Chroma vector database comparison"'); console.log(' → "Best practices for RAG systems"\n');
while (true) { const topic = await question("Research topic: ");
if (topic.toLowerCase() === "exit") { console.log("\n👋 Goodbye!\n"); rl.close(); break; }
if (!topic.trim()) continue; // skip empty input
const threadId = `research_${Date.now()}`; // unique thread for each research session // allows follow-up questions about the same research
try { const report = await runResearch(agent, topic.trim(), threadId);
console.log("\n" + "=".repeat(55)); console.log("📊 RESEARCH REPORT"); console.log("=".repeat(55)); console.log(report);
// Allow follow-up questions console.log("\n" + "─".repeat(55)); console.log('Ask a follow-up question or press Enter for a new topic.'); console.log('─'.repeat(55));
while (true) { const followUp = await question("Follow-up (or Enter to skip): ");
if (!followUp.trim()) break; // empty input = go back to main loop
console.log("\nAgent: thinking...\n");
const followUpResult = await agent.invoke( { messages: [{ role: "user", content: followUp }] }, { configurable: { thread_id: threadId } } // SAME threadId = agent remembers the full research // can answer "what were the key challenges?" without re-researching );
const followUpMsg = followUpResult.messages[followUpResult.messages.length - 1]; console.log("\n" + "─".repeat(55)); console.log(followUpMsg.content); console.log("─".repeat(55) + "\n"); }
} catch (err) { console.error("\n❌ Error:", err.message); if (err.message.includes("TAVILY_API_KEY")) { console.log("→ Get a free key at https://tavily.com and add to .env"); } if (err.message.includes("OPENAI_API_KEY")) { console.log("→ Check your OpenAI API key in .env"); } }
console.log("\n" + "=".repeat(55) + "\n"); } }
main().catch(console.error);
Run the Project
node src/index.js
Expected Output
=======================================================
🔬 RESEARCH AGENT
Powered by GPT-4o + Tavily Web Search
=======================================================
Enter a research topic or "exit" to quit.
Examples:
→ "AI agents market 2025"
→ "React vs Next.js for production apps"
Research topic: AI agents market 2025
🔬 Researching: "AI agents market 2025"
───────────────────────────────────────────────────────
Agent is working... (30-60 seconds)
..............................
=======================================================
📊 RESEARCH REPORT
=======================================================
╔══════════════════════════════════════════════════════╗
║ RESEARCH REPORT ║
╚══════════════════════════════════════════════════════╝
Topic: AI agents market 2025
Date: 4 August 2026
Sources: 5 web sources consulted
══════════════════════════════════════════════════════
EXECUTIVE SUMMARY
─────────────────
The AI agent market is experiencing explosive growth in 2025,
valued at approximately $5.1 billion with projections to reach
$28.5 billion by 2028. Key enterprise adoption is being driven
by automation gains across software development, customer service,
and data analysis workflows.
══════════════════════════════════════════════════════
SECTION 1: MARKET SIZE AND GROWTH
──────────────────────────────────────────────
The global AI agent market reached $5.1B in 2025...
[real data from Tavily search results]
SECTION 2: KEY PLAYERS
───────────────────────────────────
OpenAI, Anthropic, Google DeepMind, and Microsoft...
SECTION 3: MAIN USE CASES
──────────────────────────────────────
Enterprise automation, coding assistants, customer service...
SECTION 4: CHALLENGES AND RISKS
────────────────────────────────────────────
Hallucination, security concerns, regulatory uncertainty...
SECTION 5: FUTURE OUTLOOK
──────────────────────────────────────
Agentic AI expected to dominate by 2026...
══════════════════════════════════════════════════════
CONCLUSION
──────────
AI agents represent the most significant shift in enterprise
software since cloud computing. Organizations that invest in
agent infrastructure now will hold substantial competitive advantages.
══════════════════════════════════════════════════════
SOURCES CONSULTED
─────────────────
1. techcrunch.com — market size
2. mckinsey.com — enterprise adoption
3. openai.com — key players
4. venturebeat.com — challenges
5. mit.edu — future outlook
══════════════════════════════════════════════════════
──────────────────────────────────────────────────────
Ask a follow-up question or press Enter for a new topic.
──────────────────────────────────────────────────────
Follow-up (or Enter to skip): What were the main challenges mentioned?
Agent: thinking...
──────────────────────────────────────────────────────
Based on the research, the main challenges identified were:
1. Hallucination and reliability — agents sometimes make up facts
2. Security concerns — agents with tool access create new attack surfaces
3. Cost — running GPT-4o agents at scale is expensive
4. Regulatory uncertainty — no clear framework for autonomous AI actions
These came from sources including venturebeat.com and mit.edu.
──────────────────────────────────────────────────────
How the Agent Researches — Step by Step
User: "Research AI agents market 2025"
↓
PHASE 1 — Planning (no tool calls):
Agent thinks: "I'll research: market size, key players,
use cases, challenges, future outlook"
PHASE 2 — Research loop (real web searches):
Search 1: "AI agents market size 2025 billion"
→ Tavily returns 5 real URLs with snippets
→ Agent reads results
→ Saves finding: "market worth $5.1B in 2025"
Search 2: "top AI agent companies OpenAI Anthropic 2025"
→ Agent reads results
→ Saves finding: "OpenAI leads with GPT-4o agents"
Search 3: "AI agent enterprise use cases 2025"
Search 4: "AI agent challenges risks hallucination"
Search 5: "AI agent future predictions 2026 2027"
PHASE 3 — Report writing:
get_research_notes → retrieves all 5 saved findings
write_report → structures into sections
↓
Final polished report
3-Line Summary
- The Research Agent uses a four-tool workflow —
TavilySearchResultsfor real web searches,save_findingto accumulate notes across multiple searches,get_research_notesto retrieve all notes before writing, andwrite_reportto structure everything into a polished final report. - The agent follows three phases defined in the system prompt — Planning (identify subtopics), Research (search + save for each subtopic), and Writing (retrieve notes + write report) — this planning-before-acting pattern dramatically improves report quality vs a single search.
- The same thread_id for follow-up questions means the agent remembers the complete research session — users can ask "what were the main challenges?" without re-running the research because the full conversation including all tool results is stored in the checkpointer.
Module 8.6 — Complete ✅
Phase 8 — Complete 🎉
✅ Module 8.1 — What is an Agent + ReAct Pattern
✅ Module 8.2 — Tool Calling Deep Dive
✅ Module 8.3 — Agent Memory + Planning
✅ Module 8.4 — Multi-Agent Systems with LangGraph
✅ Module 8.5 — Project: Resume Analyzer Agent
✅ Module 8.6 — Project: Research Agent
Full Course — Complete 🎓
✅ Phase 1 — AI Foundations
✅ Phase 2 — LLM Internals
✅ Phase 3 — Embeddings
✅ Phase 4 — Vector Databases
✅ Phase 5 — RAG + PDF Chatbot
✅ Phase 6 — LangChain Core
✅ Phase 7 — Production System
✅ Phase 8 — AI Agents
"AI Engineering Fundamentals to Production" — Done. 🚀
You went from zero AI knowledge to building:
- PDF chatbots with streaming
- Production RAG with Pinecone
- Multi-agent research systems
- Resume analyzers
- Full Next.js + Express AI apps
This is real, production-grade AI engineering. 🎉
No comments:
Post a Comment