← Back to DevBytes

Building a Research Assistant Agent with LangChain: Complete Guide

Understanding the Research Assistant Agent

A Research Assistant Agent built with LangChain is an intelligent, autonomous system that can perform multi-step research tasks using large language models (LLMs) and external tools. Unlike a simple chatbot that answers from its training data, a research agent actively gathers information from search engines, databases, or APIs, synthesizes findings, and delivers structured, cited reportsβ€”all while maintaining conversational context.

The agent leverages LangChain's AgentExecutor framework, which combines a reasoning LLM with a suite of tools (like web search, document readers, and calculators). The LLM decides which tool to invoke, in what order, and how to process intermediate results. This creates a dynamic, iterative research loop rather than a single static response.

Core Components Explained

Why Building a Research Agent Matters

Traditional LLMs suffer from knowledge cutoffs, hallucination, and an inability to verify claims against live sources. A Research Assistant Agent solves these problems by grounding responses in freshly retrieved data. This matters for several critical reasons:

Organizations use research agents for due diligence, literature reviews, market sizing, technical documentation generation, and even legal precedent analysis. The pattern is fundamentally about augmenting LLMs with real-time, verifiable knowledge.

Setting Up the Environment

Before building the agent, install the required packages. We'll use LangChain with OpenAI for reasoning and Tavily for web search capabilities.

# Create a virtual environment
python -m venv research_agent_env
source research_agent_env/bin/activate  # On Windows: research_agent_env\Scripts\activate

# Install core dependencies
pip install langchain langchain-openai langchain-community tavily-python
pip install python-dotenv  # For managing API keys securely

Create a .env file to store your API keys:

# .env file
OPENAI_API_KEY=your-openai-api-key-here
TAVILY_API_KEY=your-tavily-api-key-here

Load environment variables at the top of your script:

import os
from dotenv import load_dotenv

load_dotenv()

# Verify keys are loaded
assert os.getenv("OPENAI_API_KEY"), "Missing OPENAI_API_KEY"
assert os.getenv("TAVILY_API_KEY"), "Missing TAVILY_API_KEY"

Defining the Agent's Tools

Tools are the agent's handsβ€”they extend the LLM's capabilities beyond text generation. For a research agent, the most essential tool is web search. We'll configure Tavily, a search API optimized for LLM-based agents that returns clean, structured content rather than raw HTML.

from langchain_community.tools.tavily_search import TavilySearchResults

# Configure Tavily search tool
tavily_search = TavilySearchResults(
    max_results=3,
    include_answer=True,          # Returns a summarized answer
    include_raw_content=False,    # Skip raw HTML, keep it clean
    search_depth="advanced",      # Deeper, more comprehensive results
    include_images=False,
)

# You can also add a second tool for more specific research needs
from langchain.tools import Tool
from langchain_community.utilities import WikipediaAPIWrapper

wikipedia = WikipediaAPIWrapper(top_k_results=2, doc_content_chars_max=2000)
wikipedia_tool = Tool(
    name="Wikipedia",
    description="Search Wikipedia for encyclopedic information on a topic. "
                "Useful for factual background and historical context.",
    func=wikipedia.run,
)

# Combine tools into a list
tools = [tavily_search, wikipedia_tool]

Each tool needs a clear name and descriptionβ€”the LLM uses these to decide when to invoke which tool. Poor descriptions lead to tool misuse, so be specific about each tool's purpose and expected input format.

Crafting the Agent Prompt

The system prompt is the most critical element of a research agent. It defines the agent's methodology, output structure, and behavioral constraints. A well-designed prompt transforms a generic tool-calling LLM into a disciplined researcher.

research_agent_prompt = """You are an expert Research Assistant Agent. Your purpose is to conduct
thorough, multi-source research and deliver comprehensive, well-structured reports with citations.

## Research Methodology
1. ANALYZE the user's query and identify key subtopics or dimensions that need investigation
2. BREAK complex questions into 2-5 focused sub-queries
3. SEARCH for each sub-query independently using available tools
4. SYNTHESIZE findings, identifying consensus views and noting dissenting perspectives
5. VERIFY facts across multiple sources when possible; flag unverified claims explicitly

## Output Format
Structure your final response as follows:

### Executive Summary
(2-3 sentences capturing the core findings)

### Detailed Findings
(Organized by subtopic with clear headings. Each major claim should include a source reference.)

### Source Citations
(List all sources used with URLs and retrieval context)

### Limitations & Uncertainties
(Note any conflicting information, temporal limitations, or gaps in coverage)

## Critical Rules
- NEVER fabricate information. If you cannot find reliable data on a point, state that clearly.
- ALWAYS cite sources inline where possible, using [Source: URL] notation.
- If search results are contradictory, present both sides with their respective sources.
- Do not exceed 1500 words unless the user explicitly requests a longer report.
- Use Wikipedia only for established factual background, not for current events or opinions.

## Tool Usage Guidelines
- Use Tavily Search for current information, news, and web content.
- Use Wikipedia for encyclopedic background on well-established topics.
- If initial searches return insufficient information, refine your search terms and retry.
- Do not call the same tool with identical queries more than twice.
"""

Notice the prompt encodes a research methodology (Analyze β†’ Break β†’ Search β†’ Synthesize β†’ Verify), output structure requirements, and explicit tool usage rules. This structured approach dramatically improves output quality compared to a minimal system prompt.

Assembling the Agent with LangChain's AgentExecutor

Now we wire everything together using LangChain's agent framework. We'll use the create_tool_calling_agent function (recommended for models that natively support function/tool calling) paired with AgentExecutor for robust execution with error handling.

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# Initialize the LLM with controlled temperature for research consistency
llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0.3,  # Lower temperature for factual reliability
    max_tokens=4000,
)

# Build the prompt template with placeholders for agent scratchpad
prompt = ChatPromptTemplate.from_messages([
    ("system", research_agent_prompt),
    ("human", "{input}"),
    # Critical: MessagesPlaceholder for agent's internal reasoning loop
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# Create the tool-calling agent
research_agent = create_tool_calling_agent(
    llm=llm,
    tools=tools,
    prompt=prompt,
)

# Wrap with AgentExecutor for production-grade execution
agent_executor = AgentExecutor(
    agent=research_agent,
    tools=tools,
    verbose=True,                    # Shows reasoning steps in console
    max_iterations=12,               # Prevent infinite loops
    max_execution_time=180,          # 3-minute timeout safety net
    handle_parsing_errors=True,      # Retry on malformed tool calls
    return_intermediate_steps=False, # Set True for debugging
    early_stopping_method="force",   # Stop if agent signals completion
)

The AgentExecutor adds critical production safeguards: iteration caps prevent runaway loops, timeout settings protect against stuck agents, and handle_parsing_errors=True enables graceful recovery when the LLM produces malformed tool calls. The MessagesPlaceholder for agent_scratchpad is essentialβ€”it's where the framework injects the agent's internal chain-of-thought and tool results during execution.

Running Research Queries

With the agent assembled, executing research queries is straightforward. The agent autonomously plans its approach, invokes tools, and returns a structured report.

# Execute a research query
query = "What are the latest developments in quantum computing hardware as of 2024? Focus on superconducting qubits and trapped ions."

try:
    result = agent_executor.invoke({
        "input": query,
    })
    print(result["output"])
except Exception as e:
    print(f"Agent execution failed: {e}")
    # Fallback: retry with simpler query or different model

For batch research processing, wrap queries in a loop with rate limiting:

import time
from typing import List, Dict

def batch_research(queries: List[str], delay_seconds: int = 5) -> List[Dict]:
    """
    Execute multiple research queries sequentially with rate limiting.
    
    Args:
        queries: List of research questions
        delay_seconds: Pause between queries to respect API rate limits
    
    Returns:
        List of dicts containing query and response
    """
    results = []
    for i, query in enumerate(queries):
        print(f"Processing query {i+1}/{len(queries)}: {query[:80]}...")
        try:
            response = agent_executor.invoke({"input": query})
            results.append({
                "query": query,
                "response": response["output"],
                "status": "success"
            })
        except Exception as e:
            results.append({
                "query": query,
                "response": f"Error: {str(e)}",
                "status": "failed"
            })
        time.sleep(delay_seconds)  # Respect API rate limits
    return results

# Example batch
research_questions = [
    "Analyze the current state of fusion energy research and major milestones achieved in 2024",
    "Compare leading vector database solutions: Pinecone, Weaviate, and Milvus",
    "What are the emerging trends in AI regulation across the EU, US, and China?",
]

batch_results = batch_research(research_questions)
for item in batch_results:
    print(f"\n{'='*60}\nQuery: {item['query']}\nStatus: {item['status']}")
    print(item['response'][:500] + "...")  # Print first 500 chars

Adding Memory for Conversational Research

For multi-turn research sessions where follow-up questions refine or extend findings, add conversation memory. This allows the agent to reference earlier discoveries without repeating searches.

from langchain.memory import ConversationSummaryMemory
from langchain_core.runnables import RunnableLambda

# Create summarization memory to keep context compact
memory = ConversationSummaryMemory(
    llm=ChatOpenAI(model="gpt-4o", temperature=0),
    max_token_limit=2000,  # Keep memory manageable
    return_messages=True,
)

# Modified prompt that includes conversation history
conversational_prompt = ChatPromptTemplate.from_messages([
    ("system", research_agent_prompt + """

## Conversation Context
Below is a summary of the ongoing research conversation. Use this to avoid
redundant searches and to build upon previously established findings.
If the user asks a follow-up, reference prior results rather than restarting."""),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# Rebuild agent with memory-aware prompt
conversational_agent = create_tool_calling_agent(
    llm=llm,
    tools=tools,
    prompt=conversational_prompt,
)

# For conversational execution, manage memory manually
def run_conversational_query(query: str, memory_store: dict = None) -> dict:
    """
    Execute a query while maintaining conversation memory.
    Returns both the response and updated memory state.
    """
    if memory_store is None:
        memory_store = {"chat_history": []}
    
    agent_with_memory = AgentExecutor(
        agent=conversational_agent,
        tools=tools,
        verbose=False,
        max_iterations=10,
        handle_parsing_errors=True,
    )
    
    result = agent_with_memory.invoke({
        "input": query,
        "chat_history": memory_store.get("chat_history", []),
    })
    
    # Update memory with this exchange
    memory_store["chat_history"].append({
        "role": "human",
        "content": query
    })
    memory_store["chat_history"].append({
        "role": "assistant", 
        "content": result["output"]
    })
    
    return {"response": result["output"], "memory": memory_store}

# Usage example
session_memory = None
session_memory = run_conversational_query(
    "What are the leading quantum computing companies in 2024?", 
    session_memory
)["memory"]

# Follow-up question leverages prior research
follow_up = run_conversational_query(
    "Of those companies, which one has the most promising superconducting qubit roadmap?",
    session_memory
)
print(follow_up["response"])

Using ConversationSummaryMemory rather than raw message history prevents context window overflow during long research sessions. The LLM summarizes prior exchanges, preserving key findings while discarding redundant tool call logs.

Advanced: Custom Research Tools

Beyond generic search, specialized research agents benefit from domain-specific tools. Here's how to build a custom ArXiv paper retrieval tool and a web scraping tool for deeper research capabilities.

import requests
import xml.etree.ElementTree as ET
from langchain.tools import Tool
from typing import Optional

# Custom ArXiv search tool
def search_arxiv(query: str, max_results: int = 5) -> str:
    """
    Search ArXiv for academic papers matching the query.
    Returns formatted paper summaries with links.
    """
    base_url = "http://export.arxiv.org/api/query"
    params = {
        "search_query": f"all:{query}",
        "start": 0,
        "max_results": max_results,
        "sortBy": "relevance",
        "sortOrder": "descending",
    }
    
    try:
        response = requests.get(base_url, params=params, timeout=15)
        response.raise_for_status()
        
        root = ET.fromstring(response.text)
        papers = []
        
        for entry in root.findall("{http://www.w3.org/2005/Atom}entry"):
            title = entry.find("{http://www.w3.org/2005/Atom}title").text.strip()
            summary = entry.find("{http://www.w3.org/2005/Atom}summary").text.strip()
            paper_id = entry.find("{http://www.w3.org/2005/Atom}id").text
            
            # Truncate summary for token efficiency
            summary_short = summary[:300] + "..." if len(summary) > 300 else summary
            papers.append(f"β€’ {title}\n  Summary: {summary_short}\n  URL: {paper_id}\n")
        
        if not papers:
            return f"No ArXiv papers found for query: '{query}'"
        
        return f"ArXiv Search Results for '{query}':\n\n" + "\n".join(papers)
    
    except Exception as e:
        return f"ArXiv search failed: {str(e)}. Please try with different search terms."

arxiv_tool = Tool(
    name="ArXivSearch",
    description="Search academic papers on ArXiv. Use for scientific and technical research "
                "questions requiring scholarly sources. Input: search query string.",
    func=lambda q: search_arxiv(q, max_results=3),
)

# Custom web page reader for deep-dive research
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.tools import tool

@tool
def read_webpage(url: str) -> str:
    """
    Fetch and extract text content from a specific URL. Use this when you need to
    read a specific article or page in detail after discovering it via search.
    Input: a complete URL starting with https://
    """
    try:
        loader = WebBaseLoader(url)
        docs = loader.load()
        if not docs:
            return f"No content extracted from {url}"
        # Return first 4000 characters to stay within context limits
        content = docs[0].page_content[:4000]
        return f"Content from {url}:\n\n{content}\n\n[Content may be truncated for length]"
    except Exception as e:
        return f"Failed to load {url}: {str(e)}"

# Add custom tools to the agent's toolkit
enhanced_tools = [tavily_search, wikipedia_tool, arxiv_tool, read_webpage]

When adding custom tools, invest time in writing clear, detailed tool descriptions. The LLM relies entirely on these descriptions to decide when and how to use each tool. Include information about expected input format, output characteristics, and typical use cases.

Building a Streaming Research Interface

For interactive applications, streaming the agent's thought process and intermediate steps creates a much better user experience. LangChain supports streaming through the astream_events API.

import asyncio
from langchain.callbacks import StreamingStdOutCallbackHandler

async def stream_research(query: str):
    """
    Stream the agent's research process in real-time,
    showing tool calls and intermediate reasoning.
    """
    # Create streaming-compatible LLM
    streaming_llm = ChatOpenAI(
        model="gpt-4o",
        temperature=0.3,
        streaming=True,
        callbacks=[StreamingStdOutCallbackHandler()],
    )
    
    streaming_agent = create_tool_calling_agent(
        llm=streaming_llm,
        tools=enhanced_tools,
        prompt=prompt,
    )
    
    executor = AgentExecutor(
        agent=streaming_agent,
        tools=enhanced_tools,
        verbose=True,
        max_iterations=8,
        handle_parsing_errors=True,
    )
    
    # Stream using astream_events
    async for event in executor.astream_events(
        {"input": query},
        version="v2",
    ):
        event_type = event.get("event")
        if event_type == "on_tool_start":
            print(f"\nπŸ” Using tool: {event['name']}")
        elif event_type == "on_tool_end":
            print(f"βœ… Tool complete: {event['name']}")
        elif event_type == "on_chain_end" and "output" in event.get("data", {}):
            print(f"\nπŸ“ Final Report:\n{event['data']['output']}")

# Run streaming research
asyncio.run(stream_research(
    "Compare the environmental impact of electric vehicles versus hydrogen fuel cell vehicles"
))

Streaming provides transparency into the agent's research processβ€”users see which tools are invoked, in what order, and how the agent builds its final report. This builds trust and allows users to intervene if the agent goes off track.

Best Practices for Production Research Agents

1. Structured Output Enforcement

Relying solely on prompt instructions for output format is fragile. For production, use LangChain's output parsers to enforce structure programmatically. If the agent fails to conform, the parser catches the error and triggers a retry.

from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List, Optional

class ResearchReport(BaseModel):
    executive_summary: str = Field(description="2-3 sentence core findings summary")
    detailed_findings: List[dict] = Field(description="List of finding sections with title and content")
    sources: List[str] = Field(description="All source URLs used in research")
    limitations: str = Field(description="Known limitations and uncertainties")
    confidence_score: float = Field(description="0-1 rating of overall finding reliability", ge=0, le=1)

parser = PydanticOutputParser(pydantic_object=ResearchReport)

# Add parser instructions to the system prompt
structured_prompt = research_agent_prompt + f"""

## Output Format Requirements (MANDATORY)
Your final response must be valid JSON conforming to this schema:
{parser.get_format_instructions()}

Wrap your JSON output in json code blocks.
Failure to produce valid JSON will result in an error and require regeneration."""

2. Tool Result Validation

Wrap tool functions with validation logic to catch empty results, rate limit errors, or malformed responses before they reach the LLM. This prevents the agent from reasoning over garbage data.

def validated_tavily_search(query: str) -> str:
    """Wrapper that validates Tavily results before returning to the agent."""
    try:
        raw_result = tavily_search.invoke(query)
        if not raw_result or len(str(raw_result)) < 20:
            return f"[Search returned insufficient results for '{query}'. Try broader or different search terms.]"
        return str(raw_result)
    except Exception as e:
        error_msg = str(e)
        if "rate limit" in error_msg.lower():
            return f"[Search rate limited. Wait 10 seconds and retry: '{query}']"
        return f"[Search error for '{query}': {error_msg}. Please use alternative sources.]"

validated_search = Tool(
    name="TavilySearch",
    description=tavily_search.description,
    func=validated_tavily_search,
)

3. Cost and Performance Monitoring

Track token usage and tool call counts to understand costs and optimize prompts. Research agents can burn through tokens quickly if not properly constrained.

import time
from langchain.callbacks import BaseCallbackHandler

class ResearchCostTracker(BaseCallbackHandler):
    """Tracks LLM and tool costs during agent execution."""
    
    def __init__(self):
        self.llm_calls = 0
        self.total_tokens = 0
        self.tool_calls = 0
        self.start_time = None
        self.tool_costs = 0.0
    
    def on_llm_start(self, *args, **kwargs):
        self.llm_calls += 1
        if self.start_time is None:
            self.start_time = time.time()
    
    def on_llm_end(self, response, **kwargs):
        usage = response.usage_metadata
        if usage:
            self.total_tokens += usage.get("total_tokens", 0)
    
    def on_tool_start(self, *args, **kwargs):
        self.tool_calls += 1
    
    def get_report(self) -> str:
        elapsed = time.time() - self.start_time if self.start_time else 0
        # Rough cost estimation for GPT-4o
        estimated_cost = (self.total_tokens / 1000) * 0.005  # ~$0.005/1K tokens
        return f"""
## Execution Report
- LLM Calls: {self.llm_calls}
- Total Tokens: {self.total_tokens:,}
- Tool Calls: {self.tool_calls}
- Duration: {elapsed:.1f}s
- Est. LLM Cost: ${estimated_cost:.4f}
"""

# Use the tracker
cost_tracker = ResearchCostTracker()
tracked_executor = AgentExecutor(
    agent=research_agent,
    tools=enhanced_tools,
    callbacks=[cost_tracker],
    verbose=False,
    max_iterations=10,
)

result = tracked_executor.invoke({"input": "Analyze recent breakthroughs in CRISPR gene editing"})
print(cost_tracker.get_report())

4. Graceful Degradation

Production agents must handle failures gracefully. Implement fallback strategies when primary tools fail or the agent exceeds iteration limits.

def resilient_research(query: str, max_retries: int = 2) -> str:
    """
    Execute research with fallback strategies.
    If the primary agent fails, retry with simplified tools or a direct LLM response.
    """
    for attempt in range(max_retries + 1):
        try:
            result = agent_executor.invoke({"input": query})
            return result["output"]
        except Exception as e:
            if attempt < max_retries:
                print(f"Attempt {attempt+1} failed: {e}. Retrying with reduced complexity...")
                # Fallback: use simpler agent config
                fallback_executor = AgentExecutor(
                    agent=research_agent,
                    tools=[tavily_search],  # Reduce to single reliable tool
                    max_iterations=6,
                    handle_parsing_errors=True,
                    verbose=False,
                )
                try:
                    result = fallback_executor.invoke({"input": query})
                    return f"[Produced with fallback pipeline]\n\n{result['output']}"
                except Exception:
                    continue
            else:
                # Ultimate fallback: direct LLM response with disclaimer
                direct_llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
                fallback_response = direct_llm.invoke(
                    f"Answer this question based on your training data. "
                    f"Clearly note that you cannot search for current information: {query}"
                )
                return (f"⚠️ RESEARCH AGENT UNAVAILABLE β€” FALLBACK RESPONSE (no live search):\n\n"
                        f"{fallback_response.content}\n\n"
                        f"[Error: {str(e)}]")

5. Prompt Optimization Through Iteration

The system prompt is your most powerful lever for agent quality. Iterate on it systematically:

6. Security Considerations

Research agents with web access introduce security risks. Implement these safeguards:

# URL allowlisting example for web scraping tool
import re

ALLOWED_DOMAINS = [
    r"^https?://.*\.arxiv\.org/",
    r"^https?://.*\.wikipedia\.org/",
    r"^https?://.*\.github\.com/",
    r"^https?://.*\.stackoverflow\.com/",
    r"^https?://scholar\.google\.com/",
]

def is_url_allowed(url: str) -> bool:
    """Check if URL matches allowed domains before scraping."""
    return any(re.match(pattern, url) for pattern in ALLOWED_DOMAINS)

@tool
def safe_read_webpage(url: str) -> str:
    """Read a webpage ONLY if the domain is in the allowlist."""
    if not is_url_allowed(url):
        return f"⚠️ URL not allowed: {url}. Only trusted domains (arxiv.org, wikipedia.org, github.com, stackoverflow.com, scholar.google.com) are permitted."
    return read_webpage.invoke(url)

Complete Agent Assembly Script

Below is the complete, production-ready script that ties together all the components discussed above. Save this as research_agent.py and run it directly.

#!/usr/bin/env python3
"""
Production-Ready Research Assistant Agent
Implements a multi-tool research agent with web search, Wikipedia,
ArXiv paper lookup, and web page reading capabilities.
"""

import os
import time
import asyncio
from dotenv import load_dotenv
from typing import List, Dict

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_community.tools import Tool
from langchain_community.utilities import WikipediaAPIWrapper

# ─── Configuration ───────────────────────────────────────────
load_dotenv()

# ─── Tool Definitions ────────────────────────────────────────
tavily_search = TavilySearchResults(
    max_results=3,
    include_answer=True,
    search_depth="advanced",
)

wikipedia = WikipediaAPIWrapper(top_k_results=2, doc_content_chars_max=2000)
wikipedia_tool = Tool(
    name="Wikipedia",
    description="Search Wikipedia for encyclopedic background information.",
    func=wikipedia.run,
)

def search_arxiv(query: str, max_results: int = 3) -> str:
    """Search ArXiv for academic papers."""
    import requests
    import xml.etree.ElementTree as ET
    
    base_url = "http://export.arxiv.org/api/query"
    params = {
        "search_query": f"all:{query}",
        "start": 0,
        "max_results": max_results,
        "sortBy": "relevance",
        "sortOrder": "descending",
    }
    try:
        response = requests.get(base_url, params=params, timeout=15)
        response.raise_for_status()
        root = ET.fromstring(response.text)
        papers = []
        ns = "{http://www.w3.org/2005/Atom}"
        for entry in root.findall(f"{ns}entry"):
            title = entry.find(f"{ns}title").text.strip()
            summary = entry.find(f"{ns}summary").text.strip()[:300]
            paper_id = entry.find(f"{ns}id").text
            papers.append(f"β€’ {title}\n  {summary}...\n  {paper_id}")
        return "\n".join(papers) if papers else f"No ArXiv results for '{query}'"
    except Exception as e:
        return f"ArXiv search error: {e}"

arxiv_tool = Tool(
    name="ArXivSearch",
    description="Search academic papers on ArXiv for scientific research questions.",
    func=lambda q: search_arxiv(q, max_results=3),
)

tools = [tavily_search, wikipedia_tool, arxiv_tool]

# ─── System Prompt ───────────────────────────────────────────
SYSTEM_PROMPT = """You are an expert Research Assistant Agent.

## Methodology
1. Analyze the query and identify key subtopics
2. Break complex questions into focused sub-queries
3. Search each sub-topic independently
4. Synthesize findings with source citations
5. Verify facts across multiple sources; flag uncertainties

## Output Structure
### Executive Summary
### Detailed Findings
### Source Citations
### Limitations & Uncertainties

## Rules
- Never fabricate information; state gaps explicitly
- Cite sources inline: [Source: URL]
- Present contradictory findings with sources for each side
- Keep reports concise unless detailed length is requested
- Use Tavily for current/web content, Wikipedia for background, ArXiv for academic papers
"""

# ─── Agent Assembly ──────────────────────────────────────────
prompt = ChatPromptTemplate.from_messages([
    ("system", SYSTEM_PROMPT),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

llm = ChatOpenAI(model="gpt-4o", temperature=0.3, max_tokens=4000)

agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt)

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    max_iterations=12,
    max_execution_time=180,
    handle_parsing_errors=True,
    early_stopping_method="force",
)

# ─── Main Interface ──────────────────────────────────────────
def research(query: str) -> str:
    """Execute a single research query."""
    result = executor.invoke({"input": query})
    return result["output"]

def batch_research(queries: List[str]) -> List[Dict]:
    """Run multiple research queries with rate limiting."""
    results = []
    for i, q in enumerate(queries):
        print(f"\n[{i+1}/{len(queries)}] Researching: {q[:60]}...")
        try:
            results.append({"query": q, "result": research(q), "status": "ok"})
        except Exception as e:
            results.append({"query": q, "result": str(e), "status": "error"})
        time.sleep(3)
    return results

# ─── Entry Point ─────────────────────────────────────────────
if __name__ == "__main__":
    print("=" * 60)
    print("Research Assistant Agent Ready")
    print("=" * 60)
    
    # Interactive mode
    while True:
        user_query = input("\nπŸ” Research query (or 'exit'): ").strip()
        if user_query.lower() in ("exit", "quit", "q"):
            break
        if not user_query:
            continue
        
        print("\n" + "─" * 40)
        response = research(user_query)

β€” Ad β€”

Google AdSense will appear here after approval

← Back to all articles