├── Makefile ├── README.md ├── __init__.py ├── agent ├── __init__.py ├── coder.py ├── planner.py ├── react_agent.py ├── reporter.py └── researcher.py ├── crawler ├── __init__.py ├── article.py ├── crawler.py ├── jina_client.py └── readability_extractor.py ├── example_reports └── area_ratio_largest_smallest_state_in_us.md ├── llm ├── __init__.py └── llm.py ├── main.py ├── prompt ├── README.md ├── coder.md ├── planner.md ├── planner_model.py ├── reporter.md ├── researcher.md └── utils.py ├── state ├── __init__.py └── state.py ├── state_machine └── state_machine.py └── tools ├── __init__.py ├── crawler.py ├── python_repl.py ├── search.py └── tool.py /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | black . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanoDeepResearch 2 | OpenAI's [Deep Research](https://openai.com/index/introducing-deep-research/) is a really cool product. This project is inspired by ByteDance's [DeerFlow](https://github.com/bytedance/deer-flow) project, an open-source Deep Research project. DeerFlow is using [LangGraph](https://github.com/LangChain-AI/langgraph) to build the agentic workflow which abstracts away a lot of details, e.g. how the ReAct agent is working. 3 | 4 | To make sure I understand how the DeerFlow's Deep Research agent works under the hood, I decided to build it from scratch without relying on any existing agentic framework, e.g. [LangGraph](https://www.langchain.com/langgraph). 5 | 6 | Note that this project is currently pure backend without any frontend interface. 7 | 8 | ## Recommended way of using this repo 9 | 10 | This project is NOT for production use, since it lacks many production features, e.g. web UI, token streaming, async execution, etc. 11 | 12 | But since it is build from scratch, it is easy to see the output of each step in the workflow, e.g. 13 | 14 | * for web search, what LLM suggests to search 15 | * for coding, what code the LLM generates 16 | * for planning, what research plan the LLM outputs 17 | * whatever step you are curious about 18 | 19 | ## Prepare the API keys 20 | 21 | We need to prepare the API keys for the following services and store them as environment variables: 22 | - OpenAI: `OPENAI_API_KEY` 23 | - Claude: `ANTHROPIC_API_KEY` 24 | - Tavily: `TAVILY_API_KEY` 25 | - Jina: `JINA_API_KEY` 26 | 27 | 28 | 29 | ## Run the workflow 30 | 31 | Go to the parent directory of this repo and run the following command: 32 | 33 | ```bash 34 | python3 -m nanoDeepResearch.main \ 35 | --query "what is the area(land+water) ratio between the largest and smallest states in the US" 36 | ``` 37 | 38 | The example report can be found in `example_reports/area_ratio_largest_smallest_state_in_us.md` 39 | 40 | ## ReAct Agent 41 | 42 | The ReAct agent is a simple agent that uses a ReAct loop to reason and act. It is inspired by the ReAct paper: https://arxiv.org/abs/2210.03629 43 | 44 | Given a task query, the ReAct agent will: 45 | 1. Reason about the task using the available tools 46 | 2. Act on the task using the available tools 47 | 3. Get the observation from the action results 48 | 4. Repeat the process until the task is completed 49 | 50 | Note that all those steps are decided by the LLM without human intervention, really cool! 51 | 52 | ## State Machine(Graph) 53 | 54 | `state_machine.py` is the class for the whole Deep Research workflow. 55 | - Planner: the LLM of the planner agent will understand the user query and break it down into a list of task steps 56 | - Research Team: it will take the list of task steps and assign each step to either a researcher agent or a coder agent 57 | - Researcher: it is a ReAct agent and can use `web search` and `crawler` tools to solve the task 58 | - Coder: it is a ReAct agent and can use `python` to solve the task 59 | - Reporter: it will use the observations from the researcher and coder to generate the final report 60 | 61 | ## Lint 62 | 63 | ```bash 64 | make lint 65 | ``` 66 | 67 | ## Acknowledgement 68 | 69 | Thanks to [DeerFlow](https://github.com/bytedance/deer-flow) for open-sourcing their project and providing a lot of inspiration. 70 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyuan24/nanoDeepResearch/4c3a9f9cba5141c8e6f917688921203a1ae12348/__init__.py -------------------------------------------------------------------------------- /agent/__init__.py: -------------------------------------------------------------------------------- 1 | # Import and expose the ReactAgent class 2 | from .react_agent import ReactAgent 3 | 4 | # This allows users to do: from nanoDeepResearch.agent import ReactAgent 5 | __all__ = ["ReactAgent"] 6 | -------------------------------------------------------------------------------- /agent/coder.py: -------------------------------------------------------------------------------- 1 | from ..llm.llm import OpenAIClient, AnthropicClient 2 | from ..state.state import State 3 | from typing import Union 4 | from ..agent.react_agent import ReactAgent 5 | from ..tools.python_repl import PythonREPLTool 6 | 7 | 8 | class Coder: 9 | def __init__(self, llm_client: Union[OpenAIClient, AnthropicClient]): 10 | coder_tools = [PythonREPLTool()] 11 | coder_config = {"locale": "en-US"} 12 | self.coder_agent = ReactAgent( 13 | name="coder", 14 | llm_client=llm_client, 15 | tools=coder_tools, 16 | config=coder_config, 17 | ) 18 | 19 | def code(self, query: str, state: State) -> str: 20 | """ 21 | query: the specific task step description 22 | state: the state of the workflow 23 | """ 24 | res = self.coder_agent.run(query, state) 25 | return res 26 | -------------------------------------------------------------------------------- /agent/planner.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | from ..llm.llm import OpenAIClient, AnthropicClient 3 | from ..prompt.planner_model import Plan 4 | from ..state.state import State 5 | from ..prompt.utils import load_prompt 6 | 7 | 8 | class Planner: 9 | def __init__( 10 | self, 11 | llm_client: Union[OpenAIClient, AnthropicClient], 12 | max_step_num: int = 3, 13 | locale: str = "en-US", 14 | ): 15 | self.system_prompt = load_prompt( 16 | "planner", {"max_step_num": max_step_num, "locale": locale} 17 | ) 18 | self.llm_client = llm_client 19 | 20 | def plan(self, query: str, state: State = None) -> List[str]: 21 | response = self.llm_client.generate(query, self.system_prompt, state) 22 | # parse the response with pydantic 23 | return Plan.model_validate_json(response) 24 | 25 | 26 | if __name__ == "__main__": 27 | planner = Planner(OpenAIClient(model="gpt-4.1-mini")) 28 | print( 29 | planner.plan( 30 | "what is the ratio between the area of the largest and smallest states in the US?" 31 | ) 32 | ) 33 | -------------------------------------------------------------------------------- /agent/react_agent.py: -------------------------------------------------------------------------------- 1 | # ReAct paper: https://arxiv.org/abs/2210.03629 2 | import json 3 | from typing import Dict, List, Any, Optional, Union 4 | from dataclasses import dataclass, field 5 | from ..tools import Tool 6 | from ..prompt.utils import load_prompt 7 | from ..state.state import State 8 | 9 | 10 | @dataclass 11 | class AgentState: 12 | """State object for the React agent.""" 13 | 14 | # Track conversation history 15 | messages: List[Dict[str, str]] = field(default_factory=list) 16 | # Track intermediate steps (action, observation pairs) 17 | intermediate_steps: List[Dict[str, str]] = field(default_factory=list) 18 | # Current action details 19 | action: Optional[str] = None 20 | action_input: Optional[Dict[str, Any]] = None 21 | # Flag to determine if the agent is done 22 | is_done: bool = False 23 | 24 | 25 | class ReactAgent: 26 | """Implementation of a React agent that reasons and acts in cycles.""" 27 | 28 | def __init__( 29 | self, name: str, llm_client, tools: List[Tool], config: Dict[str, Any] 30 | ): 31 | """Initialize the React agent. 32 | 33 | Args: 34 | llm_client: Client for LLM API 35 | tools: List of tools available to the agent 36 | prompt: The prompt for the agent 37 | """ 38 | self.name = name 39 | self.llm_client = llm_client 40 | self.tools = tools 41 | self.tool_map = {tool.name: tool for tool in tools} 42 | tool_config = [ 43 | {"name": tool.name, "description": tool.description} for tool in tools 44 | ] 45 | config["tools"] = tool_config 46 | self.prompt = load_prompt(name, config) 47 | 48 | def _create_prompt( 49 | self, agent_input: str, intermediate_steps: List[Dict[str, str]] 50 | ) -> str: 51 | """Create the prompt for the LLM including the React format instructions. 52 | 53 | Args: 54 | agent_input: The input to the agent 55 | intermediate_steps: List of previous steps taken 56 | 57 | Returns: 58 | Complete prompt string for the LLM 59 | """ 60 | # Tools section 61 | tool_descriptions = "\n".join( 62 | [f"- {tool.name}: {tool.description}" for tool in self.tools] 63 | ) 64 | 65 | # Format intermediate steps for prompt 66 | steps_text = "" 67 | for step in intermediate_steps: 68 | steps_text += f"Thought: {step.get('thought', '')}\n" 69 | steps_text += f"Action: {step.get('action', '')}\n" 70 | steps_text += f"Action Input: {step.get('action_input', '')}\n" 71 | steps_text += f"Observation: {step.get('observation', '')}\n" 72 | 73 | # Construct the full prompt 74 | prompt = f"""{self.prompt} 75 | Human query: {agent_input} 76 | Follow this format: 77 | Thought: Think about the current situation and what to do 78 | Action: The action to take (must be one of: {', '.join([tool.name for tool in self.tools])}) 79 | Action Input: The input to the action (can be a string or a JSON object) 80 | Observation: The result of the action 81 | ... (this Thought/Action/Observation cycle can repeat multiple times) 82 | Thought: I now know the final answer 83 | Final Answer: The final answer to the original input question 84 | 85 | {steps_text} 86 | """ 87 | return prompt 88 | 89 | def _parse_llm_response(self, response: str) -> Dict[str, Any]: 90 | """Parse the LLM response into structured components. 91 | 92 | Args: 93 | response: Raw text response from the LLM 94 | 95 | Returns: 96 | Dictionary containing parsed components (thought, action, action_input, etc.) 97 | """ 98 | result = {} 99 | 100 | # Check if the response contains a final answer 101 | if "Final Answer:" in response: 102 | final_answer_idx = response.find("Final Answer:") 103 | # Extract everything after "Final Answer:" 104 | final_answer = response[final_answer_idx + len("Final Answer:") :].strip() 105 | result["final_answer"] = final_answer 106 | # Extract the last thought before final answer 107 | thought_match = response[:final_answer_idx].strip() 108 | if "Thought:" in thought_match: 109 | last_thought_idx = thought_match.rfind("Thought:") 110 | result["thought"] = thought_match[ 111 | last_thought_idx + len("Thought:") : 112 | ].strip() 113 | else: 114 | # Extract Thought 115 | if "Thought:" in response: 116 | thought_idx = response.find("Thought:") 117 | action_idx = response.find("Action:") 118 | if action_idx > thought_idx: 119 | result["thought"] = response[ 120 | thought_idx + len("Thought:") : action_idx 121 | ].strip() 122 | 123 | # Extract Action 124 | if "Action:" in response: 125 | action_idx = response.find("Action:") 126 | action_input_idx = response.find("Action Input:") 127 | if action_input_idx > action_idx: 128 | result["action"] = response[ 129 | action_idx + len("Action:") : action_input_idx 130 | ].strip() 131 | 132 | # Extract Action Input 133 | if "Action Input:" in response: 134 | action_input_idx = response.find("Action Input:") 135 | observation_idx = response.find("Observation:") 136 | 137 | # If observation isn't present yet (expecting it to be filled later) 138 | if observation_idx == -1: 139 | action_input_text = ( 140 | response[action_input_idx + len("Action Input:") :] 141 | .strip() 142 | .strip('"') 143 | ) 144 | else: 145 | action_input_text = ( 146 | response[ 147 | action_input_idx + len("Action Input:") : observation_idx 148 | ] 149 | .strip() 150 | .strip('"') 151 | ) 152 | 153 | # Try to parse as JSON if it looks like JSON 154 | if ( 155 | action_input_text.startswith("{") 156 | and action_input_text.endswith("}") 157 | ) or ( 158 | action_input_text.startswith("[") 159 | and action_input_text.endswith("]") 160 | ): 161 | try: 162 | result["action_input"] = json.loads(action_input_text) 163 | except json.JSONDecodeError: 164 | result["action_input"] = action_input_text 165 | else: 166 | result["action_input"] = action_input_text 167 | 168 | return result 169 | 170 | def _call_llm(self, prompt: Union[str, List[Dict[str, str]]]) -> str: 171 | """Call the LLM with the given prompt. 172 | 173 | This is an abstract method that should be implemented based on the specific LLM client. 174 | 175 | Args: 176 | prompt: The prompt to send to the LLM 177 | 178 | Returns: 179 | Raw text response from the LLM 180 | """ 181 | # add stop to avoid LLM generating the observation as we want to use the tool 182 | response = self.llm_client.generate( 183 | prompt, system_prompt=self.prompt, stop=["Observation:"] 184 | ) 185 | return response 186 | 187 | def _execute_tool(self, action: str, action_input: Any) -> str: 188 | """Execute a tool with the provided input. 189 | 190 | Args: 191 | action: Name of the tool to execute 192 | action_input: Input to pass to the tool 193 | 194 | Returns: 195 | Result from the tool execution as a string 196 | """ 197 | if action not in self.tool_map: 198 | return f"Error: Tool '{action}' not found. Available tools: {', '.join(self.tool_map.keys())}" 199 | 200 | try: 201 | tool_result = self.tool_map[action](action_input) 202 | return tool_result 203 | except Exception as e: 204 | return f"Error executing tool '{action}': {str(e)}" 205 | 206 | def run( 207 | self, agent_input: str, workflow_state: State, max_iterations: int = 10 208 | ) -> Dict[str, Any]: 209 | """Run the React agent on a user query. 210 | 211 | Args: 212 | agent_input: the input to the agent 213 | max_iterations: Maximum number of thought-action-observation cycles 214 | 215 | Returns: 216 | Complete result with all intermediate steps and final answer 217 | """ 218 | agent_state = AgentState() 219 | agent_state.messages.append({"role": "user", "content": agent_input}) 220 | observations = workflow_state.get("observations", []) 221 | 222 | # Main React loop 223 | for i in range(max_iterations): 224 | # Create prompt with current state 225 | prompt = self._create_prompt(agent_input, agent_state.intermediate_steps) 226 | 227 | # Get response from LLM 228 | llm_response = self._call_llm(prompt) 229 | 230 | # Parse LLM response 231 | parsed_response = self._parse_llm_response(llm_response) 232 | 233 | # Check if we have a final answer 234 | if "final_answer" in parsed_response: 235 | agent_state.is_done = True 236 | agent_state.messages.append( 237 | {"role": "assistant", "content": parsed_response["final_answer"]} 238 | ) 239 | break 240 | 241 | # Extract thought, action, action_input 242 | thought = parsed_response.get("thought", "") 243 | action = parsed_response.get("action", "") 244 | action_input = parsed_response.get("action_input", "") 245 | 246 | # Execute tool 247 | observation = self._execute_tool(action, action_input) 248 | print(f"thought: {thought}") 249 | print(f"action: {action}") 250 | print(f"action_input: {action_input}") 251 | print(f"observation: {observation}") 252 | print(f"react agent iter: {i}, max_iterations: {max_iterations}") 253 | print("--------------------------------") 254 | observations.append(observation) 255 | # Record step 256 | step = { 257 | "thought": thought, 258 | "action": action, 259 | "action_input": action_input, 260 | "observation": observation, 261 | } 262 | agent_state.intermediate_steps.append(step) 263 | 264 | # If we've hit max iterations without completion 265 | if not agent_state.is_done: 266 | agent_state.messages.append( 267 | { 268 | "role": "assistant", 269 | "content": "I was unable to complete the task within the maximum number of iterations.", 270 | } 271 | ) 272 | 273 | # Prepare result 274 | result = { 275 | "messages": agent_state.messages, 276 | "intermediate_steps": agent_state.intermediate_steps, 277 | "is_complete": agent_state.is_done, 278 | } 279 | workflow_state.set("observations", observations) 280 | messages = workflow_state.get("messages", []) 281 | messages.append(agent_state.messages[-1]) 282 | workflow_state.set("messages", messages) 283 | return result 284 | -------------------------------------------------------------------------------- /agent/reporter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | import os 3 | from jinja2 import Environment, FileSystemLoader 4 | from ..llm.llm import OpenAIClient, AnthropicClient 5 | from ..prompt.planner_model import Plan 6 | from ..state.state import State 7 | from ..prompt.utils import load_prompt 8 | 9 | class Reporter: 10 | def __init__( 11 | self, llm_client: Union[OpenAIClient, AnthropicClient], locale: str = "en-US" 12 | ): 13 | self.system_prompt = load_prompt("reporter", {"locale": locale}) 14 | self.llm_client = llm_client 15 | 16 | def report(self, state: State = None) -> List[str]: 17 | human_messages = [] 18 | current_plan = state.get("current_plan") 19 | human_messages.append( 20 | { 21 | "role": "user", 22 | "content": f"# Research Requirements\n\n## Task\n\n{current_plan.title}\n\n## Description\n\n{current_plan.thought}", 23 | } 24 | ) 25 | for observation in state.get("observations", []): 26 | human_messages.append( 27 | { 28 | "role": "user", 29 | "content": f"Below are some observations for the research task:\n\n{observation}", 30 | } 31 | ) 32 | response = self.llm_client.generate(human_messages, self.system_prompt) 33 | return response 34 | 35 | 36 | if __name__ == "__main__": 37 | reporter = Reporter(OpenAIClient(model="gpt-4.1-mini")) 38 | print(reporter.report()) 39 | -------------------------------------------------------------------------------- /agent/researcher.py: -------------------------------------------------------------------------------- 1 | from ..llm.llm import OpenAIClient, AnthropicClient 2 | from ..state.state import State 3 | from typing import Union 4 | from ..agent.react_agent import ReactAgent 5 | from ..tools.search import TavilySearchTool 6 | from ..tools.crawler import CrawlerTool 7 | 8 | class Researcher: 9 | def __init__(self, llm_client: Union[OpenAIClient, AnthropicClient]): 10 | researcher_tools = [TavilySearchTool(), CrawlerTool()] 11 | researcher_config = {"locale": "en-US"} 12 | self.research_agent = ReactAgent( 13 | name="researcher", 14 | llm_client=llm_client, 15 | tools=researcher_tools, 16 | config=researcher_config, 17 | ) 18 | 19 | def research(self, query: str, state: State) -> str: 20 | """ 21 | query: the specific task step description 22 | state: the state of the workflow 23 | """ 24 | res = self.research_agent.run(query, state) 25 | return res 26 | -------------------------------------------------------------------------------- /crawler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyuan24/nanoDeepResearch/4c3a9f9cba5141c8e6f917688921203a1ae12348/crawler/__init__.py -------------------------------------------------------------------------------- /crawler/article.py: -------------------------------------------------------------------------------- 1 | # copy from: https://github.com/bytedance/deer-flow/blob/main/src/crawler/article.py 2 | 3 | import re 4 | from urllib.parse import urljoin 5 | 6 | from markdownify import markdownify as md 7 | 8 | 9 | class Article: 10 | url: str 11 | 12 | def __init__(self, title: str, html_content: str): 13 | self.title = title 14 | self.html_content = html_content 15 | 16 | def to_markdown(self, including_title: bool = True) -> str: 17 | markdown = "" 18 | if including_title: 19 | markdown += f"# {self.title}\n\n" 20 | markdown += md(self.html_content) 21 | return markdown 22 | 23 | def to_message(self) -> list[dict]: 24 | image_pattern = r"!\[.*?\]\((.*?)\)" 25 | 26 | content: list[dict[str, str]] = [] 27 | parts = re.split(image_pattern, self.to_markdown()) 28 | 29 | for i, part in enumerate(parts): 30 | if i % 2 == 1: 31 | image_url = urljoin(self.url, part.strip()) 32 | content.append({"type": "image_url", "image_url": {"url": image_url}}) 33 | else: 34 | content.append({"type": "text", "text": part.strip()}) 35 | 36 | return content 37 | -------------------------------------------------------------------------------- /crawler/crawler.py: -------------------------------------------------------------------------------- 1 | # copy from: https://github.com/bytedance/deer-flow/blob/main/src/crawler/crawler.py 2 | 3 | import sys 4 | 5 | from .article import Article 6 | from .jina_client import JinaClient 7 | from .readability_extractor import ReadabilityExtractor 8 | 9 | 10 | class Crawler: 11 | def crawl(self, url: str) -> Article: 12 | # To help LLMs better understand content, we extract clean 13 | # articles from HTML, convert them to markdown, and split 14 | # them into text and image blocks for one single and unified 15 | # LLM message. 16 | # 17 | # Jina is not the best crawler on readability, however it's 18 | # much easier and free to use. 19 | # 20 | # Instead of using Jina's own markdown converter, we'll use 21 | # our own solution to get better readability results. 22 | jina_client = JinaClient() 23 | html = jina_client.crawl(url, return_format="html") 24 | extractor = ReadabilityExtractor() 25 | article = extractor.extract_article(html) 26 | article.url = url 27 | return article 28 | 29 | 30 | if __name__ == "__main__": 31 | if len(sys.argv) == 2: 32 | url = sys.argv[1] 33 | else: 34 | url = "https://fintel.io/zh-hant/s/br/nvdc34" 35 | crawler = Crawler() 36 | article = crawler.crawl(url) 37 | print(article.to_markdown()) 38 | -------------------------------------------------------------------------------- /crawler/jina_client.py: -------------------------------------------------------------------------------- 1 | # copy from: https://github.com/bytedance/deer-flow/blob/main/src/crawler/jina_client.py 2 | 3 | import logging 4 | import os 5 | 6 | import requests 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class JinaClient: 12 | def crawl(self, url: str, return_format: str = "html") -> str: 13 | headers = { 14 | "Content-Type": "application/json", 15 | "X-Return-Format": return_format, 16 | } 17 | if os.getenv("JINA_API_KEY"): 18 | headers["Authorization"] = f"Bearer {os.getenv('JINA_API_KEY')}" 19 | else: 20 | logger.warning( 21 | "Jina API key is not set. Provide your own key to access a higher rate limit. See https://jina.ai/reader for more information." 22 | ) 23 | data = {"url": url} 24 | response = requests.post("https://r.jina.ai/", headers=headers, json=data) 25 | return response.text 26 | -------------------------------------------------------------------------------- /crawler/readability_extractor.py: -------------------------------------------------------------------------------- 1 | # copy from: https://github.com/bytedance/deer-flow/blob/main/src/crawler/readability_extractor.py 2 | 3 | from readabilipy import simple_json_from_html_string 4 | 5 | from .article import Article 6 | 7 | 8 | class ReadabilityExtractor: 9 | def extract_article(self, html: str) -> Article: 10 | article = simple_json_from_html_string(html, use_readability=True) 11 | return Article( 12 | title=article.get("title"), 13 | html_content=article.get("content"), 14 | ) 15 | -------------------------------------------------------------------------------- /example_reports/area_ratio_largest_smallest_state_in_us.md: -------------------------------------------------------------------------------- 1 | # Area (Land+Water) Ratio Between the Largest and Smallest US States 2 | 3 | --- 4 | 5 | ## Key Points 6 | 7 | - **Alaska** is the largest US state by total area (land + water) with 663,267 square miles. 8 | - **Rhode Island** is the smallest US state by total area with 1,545 square miles. 9 | - The area ratio of Alaska to Rhode Island (Alaska/Rhode Island) is **429.30**. 10 | - Measurements include both land and perennial water areas, consistent with official USGS and Census Bureau methodologies. 11 | - The notable size disparity highlights the significant geographical diversity among US states. 12 | 13 | --- 14 | 15 | ## Overview 16 | 17 | The United States encompasses states with substantial variations in total area. Determining the area (land + water) ratio between the largest and smallest US states provides perspective on the extent of these differences. This report identifies the largest and smallest states by total area, presents their respective measurements, examines data sources, and calculates the ratio between the two. 18 | 19 | --- 20 | 21 | ## Detailed Analysis 22 | 23 | ### Identification of Largest and Smallest States 24 | 25 | | State | Total Area (sq mi) | 26 | |--------------|--------------------| 27 | | Alaska | 663,267 | 28 | | Rhode Island | 1,545 | 29 | 30 | **Alaska** is confirmed as the largest US state, and **Rhode Island** as the smallest, based on total area (land plus water), per data from authoritative sources such as the United States Census Bureau and USGS. 31 | 32 | --- 33 | 34 | ### Area Ratio Calculation 35 | 36 | The ratio of Alaska's total area to Rhode Island's total area is calculated as follows: 37 | 38 | ``` 39 | Area Ratio = Alaska Total Area / Rhode Island Total Area 40 | Area Ratio = 663,267 / 1,545 ≈ 429.30 41 | ``` 42 | 43 | **Interpretation:** Alaska is about 429 times larger than Rhode Island in terms of total area. 44 | 45 | --- 46 | 47 | ### Area Measurement Methodology 48 | 49 | - **Total area** includes both land and perennial water areas. 50 | - *Water area calculations* include only perennial (year-round) water, such as lakes and rivers. 51 | - *Exclusions:* Intermittent (seasonal) water, glaciers, marshes, and swamps are generally included in land area. 52 | - These standards ensure consistency in area comparison across all states. 53 | 54 | --- 55 | 56 | ### Comparative Table of Key Data 57 | 58 | | Feature | Alaska | Rhode Island | Difference | Ratio | 59 | |-----------------------|------------|--------------|-------------------------|--------------| 60 | | Total Area (sq mi) | 663,267 | 1,545 | 661,722 | 429.30 | 61 | | Rank by Area | 1st | 50th | N/A | N/A | 62 | 63 | --- 64 | 65 | ### Significance 66 | 67 | - The ratio illustrates the vast range of US state sizes, reflecting historical, geographic, and administrative factors. 68 | - This size disparity affects resource distribution, governance, and demographic patterns. 69 | 70 | --- 71 | 72 | ## Survey Note 73 | 74 | A review of USGS and Census Bureau sources confirms the use of "total area" metrics that aggregate both land and perennial water bodies. These measurements are periodically updated and adhere to consistent standards, ensuring data reliability. Historical and legal definitions of state boundaries have affected the observed disparities, with Alaska’s vast and varied geography contrasting sharply with Rhode Island’s compactness. 75 | 76 | --- 77 | 78 | ## Key Citations 79 | 80 | - [How Wet is Your State? The Water Area of Each State (USGS)](https://www.usgs.gov/special-topics/water-science-school/science/how-wet-your-state-water-area-each-state) 81 | 82 | - [List of U.S. states and territories by area - Wikipedia](https://en.wikipedia.org/wiki/List_of_U.S._states_and_territories_by_area) 83 | 84 | - [50 States Ranked By Size, In Square Miles - The Fact File](https://thefactfile.org/50-states-area/) 85 | 86 | - [State Area Measurements and Internal Point Coordinates (U.S. Census Bureau)](https://www.census.gov/geographies/reference-files/2010/geo/state-area.html) -------------------------------------------------------------------------------- /llm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyuan24/nanoDeepResearch/4c3a9f9cba5141c8e6f917688921203a1ae12348/llm/__init__.py -------------------------------------------------------------------------------- /llm/llm.py: -------------------------------------------------------------------------------- 1 | # Example usage with a specific LLM client (OpenAI in this case) 2 | import os 3 | from ..state.state import State 4 | from typing import List, Dict, Union 5 | 6 | 7 | def construct_messages( 8 | prompt: Union[str, List[Dict[str, str]]], 9 | system_prompt: str = "", 10 | state: State = None, 11 | ) -> List[Dict[str, str]]: 12 | messages = [] 13 | if system_prompt: 14 | messages.append({"role": "system", "content": system_prompt}) 15 | if isinstance(prompt, list): 16 | messages.extend(prompt) 17 | elif prompt: 18 | messages.append({"role": "user", "content": prompt}) 19 | if state: 20 | for message in state.get("messages", []): 21 | messages.append(message) 22 | return messages 23 | 24 | def get_client(model_name: str): 25 | if model_name.startswith("gpt"): 26 | return OpenAIClient(model=model_name) 27 | elif model_name.startswith("claude"): 28 | return AnthropicClient(model=model_name) 29 | else: 30 | raise ValueError(f"Model {model_name} not supported") 31 | 32 | class OpenAIClient: 33 | """Example OpenAI client for the React agent.""" 34 | 35 | def __init__(self, model: str = "gpt-4.1-mini"): 36 | """Initialize the OpenAI client. 37 | 38 | Args: 39 | api_key: OpenAI API key 40 | model: Model to use (default: gpt-4) 41 | """ 42 | import openai 43 | 44 | openai_api_key = os.getenv("OPENAI_API_KEY") 45 | self.client = openai.OpenAI(api_key=openai_api_key) 46 | self.model = model 47 | 48 | def generate( 49 | self, 50 | prompt: Union[str, List[Dict[str, str]]], 51 | system_prompt: str = "", 52 | state: State = None, 53 | stop: List[str] = [], 54 | ) -> str: 55 | """Generate a response from the OpenAI API. 56 | 57 | Args: 58 | prompt: The prompt to send to the API 59 | 60 | Returns: 61 | Generated text response 62 | """ 63 | messages = construct_messages(prompt, system_prompt, state) 64 | response = self.client.chat.completions.create( 65 | model=self.model, 66 | messages=messages, 67 | # prevent the model from generating the observation as we want to use the tool 68 | stop=stop, 69 | ) 70 | return response.choices[0].message.content 71 | 72 | 73 | class AnthropicClient: 74 | """Example Anthropic client for the React agent.""" 75 | 76 | def __init__(self, model: str = "claude-3-sonnet-20240229"): 77 | """Initialize the Anthropic client. 78 | 79 | Args: 80 | api_key: Anthropic API key 81 | model: Model to use (default: claude-3-sonnet-20240229) 82 | """ 83 | import anthropic 84 | 85 | anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") 86 | self.client = anthropic.Anthropic(api_key=anthropic_api_key) 87 | self.model = model 88 | 89 | def generate( 90 | self, 91 | prompt: Union[str, List[Dict[str, str]]], 92 | system_prompt: str = "", 93 | state: State = None, 94 | stop: List[str] = [], 95 | ) -> str: 96 | """Generate a response from the Anthropic API. 97 | 98 | Args: 99 | prompt: The prompt to send to the API 100 | 101 | Returns: 102 | Generated text response 103 | """ 104 | messages = construct_messages(prompt, system_prompt, state) 105 | response = self.client.messages.create( 106 | model=self.model, 107 | max_tokens=2000, 108 | messages=messages, 109 | # prevent the model from generating the observation as we want to use the tool 110 | stop_sequences=stop, 111 | ) 112 | return response.content[0].text 113 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from .state_machine.state_machine import StateMachine 2 | from .llm.llm import get_client 3 | import argparse 4 | 5 | if __name__ == "__main__": 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("--query", type=str, required=True) 8 | parser.add_argument("--model_name", type=str, default="gpt-4.1") 9 | args = parser.parse_args() 10 | state_machine = StateMachine( 11 | human_query=args.query, 12 | llm_client=get_client(args.model_name), 13 | ) 14 | state_machine.run_until_end() 15 | print("Report:") 16 | print(state_machine.state.get("report")) 17 | -------------------------------------------------------------------------------- /prompt/README.md: -------------------------------------------------------------------------------- 1 | # System Prompt 2 | 3 | This folder contains the system prompts for each agent. Currently they are copied from [DeerFlow](https://github.com/bytedance/deer-flow). 4 | 5 | System prompt is guiding the agent how to respond to the user's query(user prompt). To see more about system prompt vs user prompt, I highly recommend reading [AI Horseless Carriages](https://koomen.dev/essays/horseless-carriages/) from Pete Koomen, an YC partner. 6 | 7 | -------------------------------------------------------------------------------- /prompt/coder.md: -------------------------------------------------------------------------------- 1 | --- 2 | CURRENT_TIME: {{ CURRENT_TIME }} 3 | --- 4 | 5 | You are `coder` agent that is managed by `supervisor` agent. 6 | You are a professional software engineer proficient in Python scripting. Your task is to analyze requirements, implement efficient solutions using Python, and provide clear documentation of your methodology and results. 7 | 8 | # Steps 9 | 10 | 1. **Analyze Requirements**: Carefully review the task description to understand the objectives, constraints, and expected outcomes. 11 | 2. **Plan the Solution**: Determine whether the task requires Python. Outline the steps needed to achieve the solution. 12 | 3. **Implement the Solution**: 13 | - Use Python for data analysis, algorithm implementation, or problem-solving. 14 | - Print outputs using `print(...)` in Python to display results or debug values. 15 | 4. **Test the Solution**: Verify the implementation to ensure it meets the requirements and handles edge cases. 16 | 5. **Document the Methodology**: Provide a clear explanation of your approach, including the reasoning behind your choices and any assumptions made. 17 | 6. **Present Results**: Clearly display the final output and any intermediate results if necessary. 18 | 19 | # Notes 20 | 21 | - Always ensure the solution is efficient and adheres to best practices. 22 | - Handle edge cases, such as empty files or missing inputs, gracefully. 23 | - Use comments in code to improve readability and maintainability. 24 | - If you want to see the output of a value, you MUST print it out with `print(...)`. 25 | - Always and only use Python to do the math. 26 | - Always use `yfinance` for financial market data: 27 | - Get historical data with `yf.download()` 28 | - Access company info with `Ticker` objects 29 | - Use appropriate date ranges for data retrieval 30 | - Required Python packages are pre-installed: 31 | - `pandas` for data manipulation 32 | - `numpy` for numerical operations 33 | - `yfinance` for financial market data 34 | - Always output in the locale of **{{ locale }}**. 35 | -------------------------------------------------------------------------------- /prompt/planner.md: -------------------------------------------------------------------------------- 1 | --- 2 | CURRENT_TIME: {{ CURRENT_TIME }} 3 | --- 4 | 5 | You are a professional Deep Researcher. Study and plan information gathering tasks using a team of specialized agents to collect comprehensive data. 6 | 7 | # Details 8 | 9 | You are tasked with orchestrating a research team to gather comprehensive information for a given requirement. The final goal is to produce a thorough, detailed report, so it's critical to collect abundant information across multiple aspects of the topic. Insufficient or limited information will result in an inadequate final report. 10 | 11 | As a Deep Researcher, you can breakdown the major subject into sub-topics and expand the depth breadth of user's initial question if applicable. 12 | 13 | ## Information Quantity and Quality Standards 14 | 15 | The successful research plan must meet these standards: 16 | 17 | 1. **Comprehensive Coverage**: 18 | - Information must cover ALL aspects of the topic 19 | - Multiple perspectives must be represented 20 | - Both mainstream and alternative viewpoints should be included 21 | 22 | 2. **Sufficient Depth**: 23 | - Surface-level information is insufficient 24 | - Detailed data points, facts, statistics are required 25 | - In-depth analysis from multiple sources is necessary 26 | 27 | 3. **Adequate Volume**: 28 | - Collecting "just enough" information is not acceptable 29 | - Aim for abundance of relevant information 30 | - More high-quality information is always better than less 31 | 32 | ## Context Assessment 33 | 34 | Before creating a detailed plan, assess if there is sufficient context to answer the user's question. Apply strict criteria for determining sufficient context: 35 | 36 | 1. **Sufficient Context** (apply very strict criteria): 37 | - Set `has_enough_context` to true ONLY IF ALL of these conditions are met: 38 | - Current information fully answers ALL aspects of the user's question with specific details 39 | - Information is comprehensive, up-to-date, and from reliable sources 40 | - No significant gaps, ambiguities, or contradictions exist in the available information 41 | - Data points are backed by credible evidence or sources 42 | - The information covers both factual data and necessary context 43 | - The quantity of information is substantial enough for a comprehensive report 44 | - Even if you're 90% certain the information is sufficient, choose to gather more 45 | 46 | 2. **Insufficient Context** (default assumption): 47 | - Set `has_enough_context` to false if ANY of these conditions exist: 48 | - Some aspects of the question remain partially or completely unanswered 49 | - Available information is outdated, incomplete, or from questionable sources 50 | - Key data points, statistics, or evidence are missing 51 | - Alternative perspectives or important context is lacking 52 | - Any reasonable doubt exists about the completeness of information 53 | - The volume of information is too limited for a comprehensive report 54 | - When in doubt, always err on the side of gathering more information 55 | 56 | ## Step Types and Web Search 57 | 58 | Different types of steps have different web search requirements: 59 | 60 | 1. **Research Steps** (`need_web_search: true`): 61 | - Gathering market data or industry trends 62 | - Finding historical information 63 | - Collecting competitor analysis 64 | - Researching current events or news 65 | - Finding statistical data or reports 66 | 67 | 2. **Data Processing Steps** (`need_web_search: false`): 68 | - API calls and data extraction 69 | - Database queries 70 | - Raw data collection from existing sources 71 | - Mathematical calculations and analysis 72 | - Statistical computations and data processing 73 | 74 | ## Exclusions 75 | 76 | - **No Direct Calculations in Research Steps**: 77 | - Research steps should only gather data and information 78 | - All mathematical calculations must be handled by processing steps 79 | - Numerical analysis must be delegated to processing steps 80 | - Research steps focus on information gathering only 81 | 82 | ## Analysis Framework 83 | 84 | When planning information gathering, consider these key aspects and ensure COMPREHENSIVE coverage: 85 | 86 | 1. **Historical Context**: 87 | - What historical data and trends are needed? 88 | - What is the complete timeline of relevant events? 89 | - How has the subject evolved over time? 90 | 91 | 2. **Current State**: 92 | - What current data points need to be collected? 93 | - What is the present landscape/situation in detail? 94 | - What are the most recent developments? 95 | 96 | 3. **Future Indicators**: 97 | - What predictive data or future-oriented information is required? 98 | - What are all relevant forecasts and projections? 99 | - What potential future scenarios should be considered? 100 | 101 | 4. **Stakeholder Data**: 102 | - What information about ALL relevant stakeholders is needed? 103 | - How are different groups affected or involved? 104 | - What are the various perspectives and interests? 105 | 106 | 5. **Quantitative Data**: 107 | - What comprehensive numbers, statistics, and metrics should be gathered? 108 | - What numerical data is needed from multiple sources? 109 | - What statistical analyses are relevant? 110 | 111 | 6. **Qualitative Data**: 112 | - What non-numerical information needs to be collected? 113 | - What opinions, testimonials, and case studies are relevant? 114 | - What descriptive information provides context? 115 | 116 | 7. **Comparative Data**: 117 | - What comparison points or benchmark data are required? 118 | - What similar cases or alternatives should be examined? 119 | - How does this compare across different contexts? 120 | 121 | 8. **Risk Data**: 122 | - What information about ALL potential risks should be gathered? 123 | - What are the challenges, limitations, and obstacles? 124 | - What contingencies and mitigations exist? 125 | 126 | ## Step Constraints 127 | 128 | - **Maximum Steps**: Limit the plan to a maximum of {{ max_step_num }} steps for focused research. 129 | - Each step should be comprehensive but targeted, covering key aspects rather than being overly expansive. 130 | - Prioritize the most important information categories based on the research question. 131 | - Consolidate related research points into single steps where appropriate. 132 | 133 | ## Execution Rules 134 | 135 | - To begin with, repeat user's requirement in your own words as `thought`. 136 | - Rigorously assess if there is sufficient context to answer the question using the strict criteria above. 137 | - If context is sufficient: 138 | - Set `has_enough_context` to true 139 | - No need to create information gathering steps 140 | - If context is insufficient (default assumption): 141 | - Break down the required information using the Analysis Framework 142 | - Create NO MORE THAN {{ max_step_num }} focused and comprehensive steps that cover the most essential aspects 143 | - Ensure each step is substantial and covers related information categories 144 | - Prioritize breadth and depth within the {{ max_step_num }}-step constraint 145 | - For each step, carefully assess if web search is needed: 146 | - Research and external data gathering: Set `need_web_search: true` 147 | - Internal data processing: Set `need_web_search: false` 148 | - Specify the exact data to be collected in step's `description`. Include a `note` if necessary. 149 | - Prioritize depth and volume of relevant information - limited information is not acceptable. 150 | - Use the same language as the user to generate the plan. 151 | - Do not include steps for summarizing or consolidating the gathered information. 152 | 153 | # Output Format 154 | 155 | Directly output the raw JSON format of `Plan` without "```json". The `Plan` interface is defined as follows: 156 | 157 | ```ts 158 | interface Step { 159 | need_web_search: boolean; // Must be explicitly set for each step 160 | title: string; 161 | description: string; // Specify exactly what data to collect 162 | step_type: "research" | "processing"; // Indicates the nature of the step 163 | } 164 | 165 | interface Plan { 166 | locale: string; // e.g. "en-US" or "zh-CN", based on the user's language or specific request 167 | has_enough_context: boolean; 168 | thought: string; 169 | title: string; 170 | steps: Step[]; // Research & Processing steps to get more context 171 | } 172 | ``` 173 | 174 | # Notes 175 | 176 | - Focus on information gathering in research steps - delegate all calculations to processing steps 177 | - Ensure each step has a clear, specific data point or information to collect 178 | - Create a comprehensive data collection plan that covers the most critical aspects within {{ max_step_num }} steps 179 | - Prioritize BOTH breadth (covering essential aspects) AND depth (detailed information on each aspect) 180 | - Never settle for minimal information - the goal is a comprehensive, detailed final report 181 | - Limited or insufficient information will lead to an inadequate final report 182 | - Carefully assess each step's web search requirement based on its nature: 183 | - Research steps (`need_web_search: true`) for gathering information 184 | - Processing steps (`need_web_search: false`) for calculations and data processing 185 | - Default to gathering more information unless the strictest sufficient context criteria are met 186 | - Always use the language specified by the locale = **{{ locale }}**. -------------------------------------------------------------------------------- /prompt/planner_model.py: -------------------------------------------------------------------------------- 1 | # copy from https://github.com/bytedance/deer-flow/blob/main/src/prompts/planner_model.py 2 | 3 | from enum import Enum 4 | from typing import List, Optional 5 | 6 | from pydantic import BaseModel, Field 7 | 8 | 9 | class StepType(str, Enum): 10 | RESEARCH = "research" 11 | PROCESSING = "processing" 12 | 13 | 14 | class Step(BaseModel): 15 | need_web_search: bool = Field( 16 | ..., description="Must be explicitly set for each step" 17 | ) 18 | title: str 19 | description: str = Field(..., description="Specify exactly what data to collect") 20 | step_type: StepType = Field(..., description="Indicates the nature of the step") 21 | execution_res: Optional[str] = Field( 22 | default=None, description="The Step execution result" 23 | ) 24 | 25 | 26 | class Plan(BaseModel): 27 | locale: str = Field( 28 | ..., description="e.g. 'en-US' or 'zh-CN', based on the user's language" 29 | ) 30 | has_enough_context: bool 31 | thought: str 32 | title: str 33 | steps: List[Step] = Field( 34 | default_factory=list, 35 | description="Research & Processing steps to get more context", 36 | ) 37 | 38 | class Config: 39 | json_schema_extra = { 40 | "examples": [ 41 | { 42 | "has_enough_context": False, 43 | "thought": ( 44 | "To understand the current market trends in AI, we need to gather comprehensive information." 45 | ), 46 | "title": "AI Market Research Plan", 47 | "steps": [ 48 | { 49 | "need_web_search": True, 50 | "title": "Current AI Market Analysis", 51 | "description": ( 52 | "Collect data on market size, growth rates, major players, and investment trends in AI sector." 53 | ), 54 | "step_type": "research", 55 | } 56 | ], 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /prompt/reporter.md: -------------------------------------------------------------------------------- 1 | --- 2 | CURRENT_TIME: {{ CURRENT_TIME }} 3 | --- 4 | 5 | You are a professional reporter responsible for writing clear, comprehensive reports based ONLY on provided information and verifiable facts. 6 | 7 | # Role 8 | 9 | You should act as an objective and analytical reporter who: 10 | - Presents facts accurately and impartially. 11 | - Organizes information logically. 12 | - Highlights key findings and insights. 13 | - Uses clear and concise language. 14 | - To enrich the report, includes relevant images from the previous steps. 15 | - Relies strictly on provided information. 16 | - Never fabricates or assumes information. 17 | - Clearly distinguishes between facts and analysis 18 | 19 | # Report Structure 20 | 21 | Structure your report in the following format: 22 | 23 | **Note: All section titles below must be translated according to the locale={{locale}}.** 24 | 25 | 1. **Title** 26 | - Always use the first level heading for the title. 27 | - A concise title for the report. 28 | 29 | 2. **Key Points** 30 | - A bulleted list of the most important findings (4-6 points). 31 | - Each point should be concise (1-2 sentences). 32 | - Focus on the most significant and actionable information. 33 | 34 | 3. **Overview** 35 | - A brief introduction to the topic (1-2 paragraphs). 36 | - Provide context and significance. 37 | 38 | 4. **Detailed Analysis** 39 | - Organize information into logical sections with clear headings. 40 | - Include relevant subsections as needed. 41 | - Present information in a structured, easy-to-follow manner. 42 | - Highlight unexpected or particularly noteworthy details. 43 | - **Including images from the previous steps in the report is very helpful.** 44 | 45 | 5. **Survey Note** (for more comprehensive reports) 46 | - A more detailed, academic-style analysis. 47 | - Include comprehensive sections covering all aspects of the topic. 48 | - Can include comparative analysis, tables, and detailed feature breakdowns. 49 | - This section is optional for shorter reports. 50 | 51 | 6. **Key Citations** 52 | - List all references at the end in link reference format. 53 | - Include an empty line between each citation for better readability. 54 | - Format: `- [Source Title](URL)` 55 | 56 | # Writing Guidelines 57 | 58 | 1. Writing style: 59 | - Use professional tone. 60 | - Be concise and precise. 61 | - Avoid speculation. 62 | - Support claims with evidence. 63 | - Clearly state information sources. 64 | - Indicate if data is incomplete or unavailable. 65 | - Never invent or extrapolate data. 66 | 67 | 2. Formatting: 68 | - Use proper markdown syntax. 69 | - Include headers for sections. 70 | - Prioritize using Markdown tables for data presentation and comparison. 71 | - **Including images from the previous steps in the report is very helpful.** 72 | - Use tables whenever presenting comparative data, statistics, features, or options. 73 | - Structure tables with clear headers and aligned columns. 74 | - Use links, lists, inline-code and other formatting options to make the report more readable. 75 | - Add emphasis for important points. 76 | - DO NOT include inline citations in the text. 77 | - Use horizontal rules (---) to separate major sections. 78 | - Track the sources of information but keep the main text clean and readable. 79 | 80 | # Data Integrity 81 | 82 | - Only use information explicitly provided in the input. 83 | - State "Information not provided" when data is missing. 84 | - Never create fictional examples or scenarios. 85 | - If data seems incomplete, acknowledge the limitations. 86 | - Do not make assumptions about missing information. 87 | 88 | # Table Guidelines 89 | 90 | - Use Markdown tables to present comparative data, statistics, features, or options. 91 | - Always include a clear header row with column names. 92 | - Align columns appropriately (left for text, right for numbers). 93 | - Keep tables concise and focused on key information. 94 | - Use proper Markdown table syntax: 95 | 96 | ```markdown 97 | | Header 1 | Header 2 | Header 3 | 98 | |----------|----------|----------| 99 | | Data 1 | Data 2 | Data 3 | 100 | | Data 4 | Data 5 | Data 6 | 101 | ``` 102 | 103 | - For feature comparison tables, use this format: 104 | 105 | ```markdown 106 | | Feature/Option | Description | Pros | Cons | 107 | |----------------|-------------|------|------| 108 | | Feature 1 | Description | Pros | Cons | 109 | | Feature 2 | Description | Pros | Cons | 110 | ``` 111 | 112 | # Notes 113 | 114 | - If uncertain about any information, acknowledge the uncertainty. 115 | - Only include verifiable facts from the provided source material. 116 | - Place all citations in the "Key Citations" section at the end, not inline in the text. 117 | - For each citation, use the format: `- [Source Title](URL)` 118 | - Include an empty line between each citation for better readability. 119 | - Include images using `![Image Description](image_url)`. The images should be in the middle of the report, not at the end or separate section. 120 | - The included images should **only** be from the information gathered **from the previous steps**. **Never** include images that are not from the previous steps 121 | - Directly output the Markdown raw content without "```markdown" or "```". 122 | - Always use the language specified by the locale = **{{ locale }}**. 123 | -------------------------------------------------------------------------------- /prompt/researcher.md: -------------------------------------------------------------------------------- 1 | --- 2 | CURRENT_TIME: {{ CURRENT_TIME }} 3 | --- 4 | 5 | You are `researcher` agent that is managed by `supervisor` agent. 6 | 7 | You are dedicated to conducting thorough investigations using search tools and providing comprehensive solutions through systematic use of the available tools, including both built-in tools and dynamically loaded tools. 8 | 9 | # Available Tools 10 | {% for tool in tools %} 11 | - **{{ tool.name }}**: {{ tool.description }} 12 | {% endfor %} 13 | 14 | 15 | ## How to Use Dynamic Loaded Tools 16 | 17 | - **Tool Selection**: Choose the most appropriate tool for each subtask. Prefer specialized tools over general-purpose ones when available. 18 | - **Tool Documentation**: Read the tool documentation carefully before using it. Pay attention to required parameters and expected outputs. 19 | - **Error Handling**: If a tool returns an error, try to understand the error message and adjust your approach accordingly. 20 | - **Combining Tools**: Often, the best results come from combining multiple tools. For example, use a Github search tool to search for trending repos, then use the crawl tool to get more details. 21 | 22 | # Steps 23 | 24 | 1. **Understand the Problem**: Forget your previous knowledge, and carefully read the problem statement to identify the key information needed. 25 | 2. **Assess Available Tools**: Take note of all tools available to you, including any dynamically loaded tools. 26 | 3. **Plan the Solution**: Determine the best approach to solve the problem using the available tools. 27 | 4. **Execute the Solution**: 28 | - Forget your previous knowledge, so you **should leverage the tools** to retrieve the information. 29 | - Use the **web_search_tool** or other suitable search tool to perform a search with the provided keywords. 30 | - When the task includes time range requirements: 31 | - Incorporate appropriate time-based search parameters in your queries (e.g., "after:2020", "before:2023", or specific date ranges) 32 | - Ensure search results respect the specified time constraints. 33 | - Verify the publication dates of sources to confirm they fall within the required time range. 34 | - Use dynamically loaded tools when they are more appropriate for the specific task. 35 | - (Optional) Use the **crawl_tool** to read content from necessary URLs. Only use URLs from search results or provided by the user. 36 | 5. **Synthesize Information**: 37 | - Combine the information gathered from all tools used (search results, crawled content, and dynamically loaded tool outputs). 38 | - Ensure the response is clear, concise, and directly addresses the problem. 39 | - Track and attribute all information sources with their respective URLs for proper citation. 40 | - Include relevant images from the gathered information when helpful. 41 | 42 | # Output Format 43 | 44 | - Provide a structured response in markdown format. 45 | - Include the following sections: 46 | - **Problem Statement**: Restate the problem for clarity. 47 | - **Research Findings**: Organize your findings by topic rather than by tool used. For each major finding: 48 | - Summarize the key information 49 | - Track the sources of information but DO NOT include inline citations in the text 50 | - Include relevant images if available 51 | - **Conclusion**: Provide a synthesized response to the problem based on the gathered information. 52 | - **References**: List all sources used with their complete URLs in link reference format at the end of the document. Make sure to include an empty line between each reference for better readability. Use this format for each reference: 53 | ```markdown 54 | - [Source Title](https://example.com/page1) 55 | 56 | - [Source Title](https://example.com/page2) 57 | ``` 58 | - Always output in the locale of **{{ locale }}**. 59 | - DO NOT include inline citations in the text. Instead, track all sources and list them in the References section at the end using link reference format. 60 | 61 | # Notes 62 | 63 | - Always verify the relevance and credibility of the information gathered. 64 | - If no URL is provided, focus solely on the search results. 65 | - Never do any math or any file operations. 66 | - Do not try to interact with the page. The crawl tool can only be used to crawl content. 67 | - Do not perform any mathematical calculations. 68 | - Do not attempt any file operations. 69 | - Only invoke `crawl_tool` when essential information cannot be obtained from search results alone. 70 | - Always include source attribution for all information. This is critical for the final report's citations. 71 | - When presenting information from multiple sources, clearly indicate which source each piece of information comes from. 72 | - Include images using `![Image Description](image_url)` in a separate section. 73 | - The included images should **only** be from the information gathered **from the search results or the crawled content**. **Never** include images that are not from the search results or the crawled content. 74 | - Always use the locale of **{{ locale }}** for the output. 75 | - When time range requirements are specified in the task, strictly adhere to these constraints in your search queries and verify that all information provided falls within the specified time period. -------------------------------------------------------------------------------- /prompt/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from jinja2 import Environment, FileSystemLoader 3 | from typing import Dict, Any 4 | 5 | 6 | def load_prompt(agent_name: str, config: Dict[str, Any]) -> str: 7 | template_dir = os.path.join(os.path.dirname(__file__)) 8 | env = Environment(loader=FileSystemLoader(template_dir)) 9 | 10 | # Load and render the template 11 | template = env.get_template(f"{agent_name}.md") 12 | return template.render(**config) 13 | -------------------------------------------------------------------------------- /state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liyuan24/nanoDeepResearch/4c3a9f9cba5141c8e6f917688921203a1ae12348/state/__init__.py -------------------------------------------------------------------------------- /state/state.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class State: 5 | """ 6 | hold the history of the conversation 7 | """ 8 | 9 | def __init__(self): 10 | self.messages = [] 11 | self.state = {} 12 | self.state["messages"] = self.messages 13 | 14 | def get(self, key: str, default: Any = None) -> Any: 15 | return self.state.get(key, default) 16 | 17 | def set(self, key: str, value: Any): 18 | self.state[key] = value 19 | -------------------------------------------------------------------------------- /state_machine/state_machine.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | from typing import Union 3 | from ..agent.planner import Planner 4 | from ..state.state import State 5 | from ..llm.llm import OpenAIClient, AnthropicClient 6 | from ..agent.researcher import Researcher 7 | from ..agent.coder import Coder 8 | from ..agent.reporter import Reporter 9 | 10 | 11 | class Node(Enum): 12 | """Enumeration of possible states in the workflow.""" 13 | 14 | PLANNER = auto() 15 | RESEARCH_TEAM = auto() 16 | RESEARCH = auto() 17 | CODE = auto() 18 | REPORTER = auto() 19 | END = auto() # Terminal state to exit the state machine 20 | 21 | 22 | class StateMachine: 23 | """Implementation of a state machine with predefined transitions.""" 24 | 25 | def __init__( 26 | self, 27 | human_query: str, 28 | llm_client: Union[OpenAIClient, AnthropicClient], 29 | ): 30 | # Define the possible state transitions 31 | self.transitions = { 32 | Node.PLANNER: [Node.RESEARCH_TEAM, Node.REPORTER], 33 | Node.RESEARCH_TEAM: [Node.RESEARCH, Node.CODE], 34 | Node.RESEARCH: [Node.RESEARCH_TEAM], 35 | Node.CODE: [Node.RESEARCH_TEAM], 36 | Node.REPORTER: [Node.END], 37 | Node.END: [], # No transitions from END state 38 | } 39 | 40 | # Define actions to be performed in each state 41 | self.state_actions = { 42 | Node.PLANNER: self._planner_action, 43 | Node.RESEARCH_TEAM: self._research_team_action, 44 | Node.RESEARCH: self._research_action, 45 | Node.CODE: self._code_action, 46 | Node.REPORTER: self._reporter_action, 47 | } 48 | 49 | # Initialize the current state 50 | self.current_node = Node.PLANNER 51 | self.planner_agent = Planner(llm_client) 52 | self.researcher = Researcher(llm_client) 53 | self.coder = Coder(llm_client) 54 | self.reporter = Reporter(llm_client) 55 | 56 | messages = [{"role": "user", "content": human_query}] 57 | self.state = State() 58 | self.state.set("messages", messages) 59 | self.max_plan_iters = 2 60 | self.plan_iter = 0 61 | 62 | def _planner_action(self) -> None: 63 | """Action performed when in PLANNER state.""" 64 | plan = self.planner_agent.plan("", self.state) 65 | self.plan_iter += 1 66 | self.state.set("current_plan", plan) 67 | if self.plan_iter > self.max_plan_iters: 68 | return Node.REPORTER 69 | if plan.has_enough_context: 70 | return Node.REPORTER 71 | else: 72 | return Node.RESEARCH_TEAM 73 | 74 | def _research_team_action(self) -> None: 75 | """ 76 | Loop through the steps in the current plan and perform the action for each step by delegating to the research agent or code agent 77 | """ 78 | # get the first step that has not been executed 79 | current_plan = self.state.get("current_plan") 80 | # if all steps have been executed, generate the report 81 | if all(step.execution_res for step in current_plan.steps): 82 | return Node.REPORTER 83 | for step in current_plan.steps: 84 | if not step.execution_res: 85 | break 86 | if step.step_type == "research": 87 | next_node = Node.RESEARCH 88 | elif step.step_type == "processing": 89 | next_node = Node.CODE 90 | else: 91 | raise ValueError(f"Invalid step type: {step.step_type}") 92 | return next_node 93 | 94 | def _research_action(self) -> None: 95 | """Action performed when in RESEARCH state.""" 96 | # get the first step that has not been executed 97 | current_plan = self.state.get("current_plan") 98 | for step in current_plan.steps: 99 | if not step.execution_res: 100 | break 101 | input = f"#Task\n\n##title\n\n{step.title}\n\n##description\n\n{step.description}\n\n##locale\n\n{self.state.get('locale', 'en-US')}" 102 | res = self.researcher.research(input, self.state) 103 | step.execution_res = res['messages'][-1] 104 | return Node.RESEARCH_TEAM 105 | 106 | def _code_action(self) -> None: 107 | """Action performed when in CODE state.""" 108 | # get the first step that has not been executed 109 | current_plan = self.state.get("current_plan") 110 | for step in current_plan.steps: 111 | if not step.execution_res: 112 | break 113 | input = f"#Task\n\n##title\n\n{step.title}\n\n##description\n\n{step.description}\n\n##locale\n\n{self.state.get('locale', 'en-US')}" 114 | res = self.coder.code(input, self.state) 115 | step.execution_res = res['messages'][-1] 116 | return Node.RESEARCH_TEAM 117 | 118 | def _reporter_action(self) -> None: 119 | report = self.reporter.report(self.state) 120 | self.state.set("report", report) 121 | return Node.END 122 | 123 | def step(self) -> bool: 124 | return self.state_actions[self.current_node]() 125 | 126 | def run_until_end(self) -> None: 127 | """ 128 | Run the state machine until it reaches the END state or max_steps is reached. 129 | 130 | Args: 131 | max_steps: Maximum number of steps to prevent infinite loops. 132 | """ 133 | while self.current_node != Node.END: 134 | next_node = self.step() 135 | self.current_node = next_node 136 | return self.state.get("report") 137 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | # tools/__init__.py 2 | 3 | # Import specific classes or functions to make them available at the package level 4 | from .tool import Tool 5 | from .search import TavilySearchTool 6 | from .python_repl import PythonREPLTool 7 | 8 | # Define what is available when using 'from tools import *' 9 | __all__ = ["Tool", "TavilySearchTool", "PythonREPLTool"] 10 | -------------------------------------------------------------------------------- /tools/crawler.py: -------------------------------------------------------------------------------- 1 | # reference: https://github.com/bytedance/deer-flow/blob/main/src/tools/crawl.py 2 | from ..crawler.crawler import Crawler 3 | from .tool import Tool 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class CrawlerTool(Tool): 9 | def __init__(self): 10 | super().__init__("crawl", "Use this to crawl a url and get a readable content in markdown format.") 11 | 12 | def __call__(self, url: str) -> str: 13 | """Use this to crawl a url and get a readable content in markdown format.""" 14 | try: 15 | crawler = Crawler() 16 | article = crawler.crawl(url) 17 | return {"url": url, "crawled_content": article.to_markdown()[:1000]} 18 | except BaseException as e: 19 | error_msg = f"Failed to crawl. Error: {repr(e)}" 20 | logger.error(error_msg) 21 | return error_msg -------------------------------------------------------------------------------- /tools/python_repl.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | from .tool import Tool 4 | 5 | 6 | class PythonREPLTool(Tool): 7 | def __init__(self): 8 | super().__init__("python_repl", "Useful for executing Python code.") 9 | self.locals = {} 10 | 11 | def __call__(self, code_string: str) -> str: 12 | # Create a string buffer to capture output 13 | output_buffer = io.StringIO() 14 | # Save the current stdout 15 | old_stdout = sys.stdout 16 | # Redirect stdout to the buffer 17 | sys.stdout = output_buffer 18 | 19 | try: 20 | # Execute the code string 21 | exec(code_string, {}, self.locals) 22 | except Exception as e: 23 | # Capture any exceptions 24 | output_buffer.write(f"Error: {e}\n") 25 | finally: 26 | # Restore the original stdout 27 | sys.stdout = old_stdout 28 | 29 | # Get the output from the buffer 30 | output = output_buffer.getvalue() 31 | output_buffer.close() 32 | return output 33 | 34 | 35 | if __name__ == "__main__": 36 | repl = PythonREPLTool() 37 | code = """ 38 | x = 10 39 | y = 20 40 | sum = x + y 41 | # need to print because we use stringIO to capture output 42 | print(sum) 43 | """ 44 | print(repl(code)) 45 | -------------------------------------------------------------------------------- /tools/search.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from tavily import TavilyClient 4 | from .tool import Tool 5 | 6 | MAX_RESULTS = 2 7 | TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | client = TavilyClient(TAVILY_API_KEY) 13 | 14 | 15 | class TavilySearchTool(Tool): 16 | def __init__(self): 17 | super().__init__("search", "Useful for searching the web for information.") 18 | self.client = TavilyClient(TAVILY_API_KEY) 19 | 20 | def clean_results(self, response): 21 | results = response["results"] 22 | clean_results = [] 23 | for result in results: 24 | clean_result = {} 25 | clean_result["title"] = result["title"] 26 | clean_result["url"] = result["url"] 27 | clean_result["content"] = result["content"] 28 | clean_result["score"] = result["score"] 29 | if result["raw_content"]: 30 | clean_result["raw_content"] = result["raw_content"] 31 | clean_results.append(clean_result) 32 | return clean_results 33 | 34 | def __call__(self, query: str) -> str: 35 | response = self.client.search(query=query, max_results=MAX_RESULTS) 36 | clean_results = self.clean_results(response) 37 | return ";".join(str(result) for result in clean_results) 38 | 39 | 40 | if __name__ == "__main__": 41 | tavily_search_tool = TavilySearchTool() 42 | results = tavily_search_tool("current height of the Eiffel Tower in 2025") 43 | print(type(results)) 44 | print(results) 45 | -------------------------------------------------------------------------------- /tools/tool.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any 2 | 3 | 4 | class Tool: 5 | """Tool that the agent can use.""" 6 | 7 | def __init__(self, name: str, description: str): 8 | """Initialize a tool. 9 | 10 | Args: 11 | name: Name of the tool 12 | description: Description of what the tool does 13 | """ 14 | self.name = name 15 | self.description = description 16 | --------------------------------------------------------------------------------