Files
AI/참고/OpenDeepResearcher-main/open_deep_researcher.ipynb

416 lines
20 KiB
Plaintext
Raw Normal View History

2026-05-12 19:40:31 +09:00
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"authorship_tag": "ABX9TyOe5BsaH0aplNCjknkFtnjg",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/github/mshumer/OpenDeepResearcher/blob/main/open_deep_researcher.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "code",
"source": [
"!pip install nest_asyncio\n",
"import nest_asyncio\n",
"nest_asyncio.apply()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "y7cTpP9rDZW-",
"outputId": "5a443ad2-7a8d-4fef-f315-12108c28f1a2"
},
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Requirement already satisfied: nest_asyncio in /usr/local/lib/python3.11/dist-packages (1.6.0)\n"
]
}
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "GJTo96a7DGUz"
},
"outputs": [],
"source": [
"import asyncio\n",
"import aiohttp\n",
"import json\n",
"\n",
"# =======================\n",
"# Configuration Constants\n",
"# =======================\n",
"OPENROUTER_API_KEY = \"REDACTED\" # Replace with your OpenRouter API key\n",
"SERPAPI_API_KEY = \"REDACTED\" # Replace with your SERPAPI API key\n",
"JINA_API_KEY = \"REDACTED\" # Replace with your JINA API key\n",
"\n",
"# Endpoints\n",
"OPENROUTER_URL = \"https://openrouter.ai/api/v1/chat/completions\"\n",
"SERPAPI_URL = \"https://serpapi.com/search\"\n",
"JINA_BASE_URL = \"https://r.jina.ai/\"\n",
"\n",
"# Default LLM model (can be changed if desired)\n",
"DEFAULT_MODEL = \"anthropic/claude-3.5-haiku\"\n",
"\n",
"\n",
"# ============================\n",
"# Asynchronous Helper Functions\n",
"# ============================\n",
"\n",
"async def call_openrouter_async(session, messages, model=DEFAULT_MODEL):\n",
" \"\"\"\n",
" Asynchronously call the OpenRouter chat completion API with the provided messages.\n",
" Returns the content of the assistants reply.\n",
" \"\"\"\n",
" headers = {\n",
" \"Authorization\": f\"Bearer {OPENROUTER_API_KEY}\",\n",
" \"X-Title\": \"OpenDeepResearcher, by Matt Shumer\",\n",
" \"Content-Type\": \"application/json\"\n",
" }\n",
" payload = {\n",
" \"model\": model,\n",
" \"messages\": messages\n",
" }\n",
" try:\n",
" async with session.post(OPENROUTER_URL, headers=headers, json=payload) as resp:\n",
" if resp.status == 200:\n",
" result = await resp.json()\n",
" try:\n",
" return result['choices'][0]['message']['content']\n",
" except (KeyError, IndexError) as e:\n",
" print(\"Unexpected OpenRouter response structure:\", result)\n",
" return None\n",
" else:\n",
" text = await resp.text()\n",
" print(f\"OpenRouter API error: {resp.status} - {text}\")\n",
" return None\n",
" except Exception as e:\n",
" print(\"Error calling OpenRouter:\", e)\n",
" return None\n",
"\n",
"\n",
"async def generate_search_queries_async(session, user_query):\n",
" \"\"\"\n",
" Ask the LLM to produce up to four precise search queries (in Python list format)\n",
" based on the users query.\n",
" \"\"\"\n",
" prompt = (\n",
" \"You are an expert research assistant. Given the user's query, generate up to four distinct, \"\n",
" \"precise search queries that would help gather comprehensive information on the topic. \"\n",
" \"Return only a Python list of strings, for example: ['query1', 'query2', 'query3'].\"\n",
" )\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are a helpful and precise research assistant.\"},\n",
" {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\n{prompt}\"}\n",
" ]\n",
" response = await call_openrouter_async(session, messages)\n",
" if response:\n",
" try:\n",
" # Expect exactly a Python list (e.g., \"['query1', 'query2']\")\n",
" search_queries = eval(response)\n",
" if isinstance(search_queries, list):\n",
" return search_queries\n",
" else:\n",
" print(\"LLM did not return a list. Response:\", response)\n",
" return []\n",
" except Exception as e:\n",
" print(\"Error parsing search queries:\", e, \"\\nResponse:\", response)\n",
" return []\n",
" return []\n",
"\n",
"\n",
"async def perform_search_async(session, query):\n",
" \"\"\"\n",
" Asynchronously perform a Google search using SERPAPI for the given query.\n",
" Returns a list of result URLs.\n",
" \"\"\"\n",
" params = {\n",
" \"q\": query,\n",
" \"api_key\": SERPAPI_API_KEY,\n",
" \"engine\": \"google\"\n",
" }\n",
" try:\n",
" async with session.get(SERPAPI_URL, params=params) as resp:\n",
" if resp.status == 200:\n",
" results = await resp.json()\n",
" if \"organic_results\" in results:\n",
" links = [item.get(\"link\") for item in results[\"organic_results\"] if \"link\" in item]\n",
" return links\n",
" else:\n",
" print(\"No organic results in SERPAPI response.\")\n",
" return []\n",
" else:\n",
" text = await resp.text()\n",
" print(f\"SERPAPI error: {resp.status} - {text}\")\n",
" return []\n",
" except Exception as e:\n",
" print(\"Error performing SERPAPI search:\", e)\n",
" return []\n",
"\n",
"\n",
"async def fetch_webpage_text_async(session, url):\n",
" \"\"\"\n",
" Asynchronously retrieve the text content of a webpage using Jina.\n",
" The URL is appended to the Jina endpoint.\n",
" \"\"\"\n",
" full_url = f\"{JINA_BASE_URL}{url}\"\n",
" headers = {\n",
" \"Authorization\": f\"Bearer {JINA_API_KEY}\"\n",
" }\n",
" try:\n",
" async with session.get(full_url, headers=headers) as resp:\n",
" if resp.status == 200:\n",
" return await resp.text()\n",
" else:\n",
" text = await resp.text()\n",
" print(f\"Jina fetch error for {url}: {resp.status} - {text}\")\n",
" return \"\"\n",
" except Exception as e:\n",
" print(\"Error fetching webpage text with Jina:\", e)\n",
" return \"\"\n",
"\n",
"\n",
"async def is_page_useful_async(session, user_query, page_text):\n",
" \"\"\"\n",
" Ask the LLM if the provided webpage content is useful for answering the user's query.\n",
" The LLM must reply with exactly \"Yes\" or \"No\".\n",
" \"\"\"\n",
" prompt = (\n",
" \"You are a critical research evaluator. Given the user's query and the content of a webpage, \"\n",
" \"determine if the webpage contains information relevant and useful for addressing the query. \"\n",
" \"Respond with exactly one word: 'Yes' if the page is useful, or 'No' if it is not. Do not include any extra text.\"\n",
" )\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are a strict and concise evaluator of research relevance.\"},\n",
" {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\nWebpage Content (first 20000 characters):\\n{page_text[:20000]}\\n\\n{prompt}\"}\n",
" ]\n",
" response = await call_openrouter_async(session, messages)\n",
" if response:\n",
" answer = response.strip()\n",
" if answer in [\"Yes\", \"No\"]:\n",
" return answer\n",
" else:\n",
" # Fallback: try to extract Yes/No from the response.\n",
" if \"Yes\" in answer:\n",
" return \"Yes\"\n",
" elif \"No\" in answer:\n",
" return \"No\"\n",
" return \"No\"\n",
"\n",
"\n",
"async def extract_relevant_context_async(session, user_query, search_query, page_text):\n",
" \"\"\"\n",
" Given the original query, the search query used, and the page content,\n",
" have the LLM extract all information relevant for answering the query.\n",
" \"\"\"\n",
" prompt = (\n",
" \"You are an expert information extractor. Given the user's query, the search query that led to this page, \"\n",
" \"and the webpage content, extract all pieces of information that are relevant to answering the user's query. \"\n",
" \"Return only the relevant context as plain text without commentary.\"\n",
" )\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are an expert in extracting and summarizing relevant information.\"},\n",
" {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\nSearch Query: {search_query}\\n\\nWebpage Content (first 20000 characters):\\n{page_text[:20000]}\\n\\n{prompt}\"}\n",
" ]\n",
" response = await call_openrouter_async(session, messages)\n",
" if response:\n",
" return response.strip()\n",
" return \"\"\n",
"\n",
"\n",
"async def get_new_search_queries_async(session, user_query, previous_search_queries, all_contexts):\n",
" \"\"\"\n",
" Based on the original query, the previously used search queries, and all the extracted contexts,\n",
" ask the LLM whether additional search queries are needed. If yes, return a Python list of up to four queries;\n",
" if the LLM thinks research is complete, it should return \"<done>\".\n",
" \"\"\"\n",
" context_combined = \"\\n\".join(all_contexts)\n",
" prompt = (\n",
" \"You are an analytical research assistant. Based on the original query, the search queries performed so far, \"\n",
" \"and the extracted contexts from webpages, determine if further research is needed. \"\n",
" \"If further research is needed, provide up to four new search queries as a Python list (for example, \"\n",
" \"['new query1', 'new query2']). If you believe no further research is needed, respond with exactly <done>.\"\n",
" \"\\nOutput only a Python list or the token <done> without any additional text.\"\n",
" )\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are a systematic research planner.\"},\n",
" {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\nPrevious Search Queries: {previous_search_queries}\\n\\nExtracted Relevant Contexts:\\n{context_combined}\\n\\n{prompt}\"}\n",
" ]\n",
" response = await call_openrouter_async(session, messages)\n",
" if response:\n",
" cleaned = response.strip()\n",
" if cleaned == \"<done>\":\n",
" return \"<done>\"\n",
" try:\n",
" new_queries = eval(cleaned)\n",
" if isinstance(new_queries, list):\n",
" return new_queries\n",
" else:\n",
" print(\"LLM did not return a list for new search queries. Response:\", response)\n",
" return []\n",
" except Exception as e:\n",
" print(\"Error parsing new search queries:\", e, \"\\nResponse:\", response)\n",
" return []\n",
" return []\n",
"\n",
"\n",
"async def generate_final_report_async(session, user_query, all_contexts):\n",
" \"\"\"\n",
" Generate the final comprehensive report using all gathered contexts.\n",
" \"\"\"\n",
" context_combined = \"\\n\".join(all_contexts)\n",
" prompt = (\n",
" \"You are an expert researcher and report writer. Based on the gathered contexts below and the original query, \"\n",
" \"write a comprehensive, well-structured, and detailed report that addresses the query thoroughly. \"\n",
" \"Include all relevant insights and conclusions without extraneous commentary.\"\n",
" )\n",
" messages = [\n",
" {\"role\": \"system\", \"content\": \"You are a skilled report writer.\"},\n",
" {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\nGathered Relevant Contexts:\\n{context_combined}\\n\\n{prompt}\"}\n",
" ]\n",
" report = await call_openrouter_async(session, messages)\n",
" return report\n",
"\n",
"\n",
"async def process_link(session, link, user_query, search_query):\n",
" \"\"\"\n",
" Process a single link: fetch its content, judge its usefulness, and if useful, extract the relevant context.\n",
" \"\"\"\n",
" print(f\"Fetching content from: {link}\")\n",
" page_text = await fetch_webpage_text_async(session, link)\n",
" if not page_text:\n",
" return None\n",
" usefulness = await is_page_useful_async(session, user_query, page_text)\n",
" print(f\"Page usefulness for {link}: {usefulness}\")\n",
" if usefulness == \"Yes\":\n",
" context = await extract_relevant_context_async(session, user_query, search_query, page_text)\n",
" if context:\n",
" print(f\"Extracted context from {link} (first 200 chars): {context[:200]}\")\n",
" return context\n",
" return None\n",
"\n",
"\n",
"# =========================\n",
"# Main Asynchronous Routine\n",
"# =========================\n",
"\n",
"async def async_main():\n",
" user_query = input(\"Enter your research query/topic: \").strip()\n",
" iter_limit_input = input(\"Enter maximum number of iterations (default 10): \").strip()\n",
" iteration_limit = int(iter_limit_input) if iter_limit_input.isdigit() else 10\n",
"\n",
" aggregated_contexts = [] # All useful contexts from every iteration\n",
" all_search_queries = [] # Every search query used across iterations\n",
" iteration = 0\n",
"\n",
" async with aiohttp.ClientSession() as session:\n",
" # ----- INITIAL SEARCH QUERIES -----\n",
" new_search_queries = await generate_search_queries_async(session, user_query)\n",
" if not new_search_queries:\n",
" print(\"No search queries were generated by the LLM. Exiting.\")\n",
" return\n",
" all_search_queries.extend(new_search_queries)\n",
"\n",
" # ----- ITERATIVE RESEARCH LOOP -----\n",
" while iteration < iteration_limit:\n",
" print(f\"\\n=== Iteration {iteration + 1} ===\")\n",
" iteration_contexts = []\n",
"\n",
" # For each search query, perform SERPAPI searches concurrently.\n",
" search_tasks = [perform_search_async(session, query) for query in new_search_queries]\n",
" search_results = await asyncio.gather(*search_tasks)\n",
"\n",
" # Aggregate all unique links from all search queries of this iteration.\n",
" # Map each unique link to the search query that produced it.\n",
" unique_links = {}\n",
" for idx, links in enumerate(search_results):\n",
" query = new_search_queries[idx]\n",
" for link in links:\n",
" if link not in unique_links:\n",
" unique_links[link] = query\n",
"\n",
" print(f\"Aggregated {len(unique_links)} unique links from this iteration.\")\n",
"\n",
" # Process each link concurrently: fetch, judge, and extract context.\n",
" link_tasks = [\n",
" process_link(session, link, user_query, unique_links[link])\n",
" for link in unique_links\n",
" ]\n",
" link_results = await asyncio.gather(*link_tasks)\n",
"\n",
" # Collect non-None contexts.\n",
" for res in link_results:\n",
" if res:\n",
" iteration_contexts.append(res)\n",
"\n",
" if iteration_contexts:\n",
" aggregated_contexts.extend(iteration_contexts)\n",
" else:\n",
" print(\"No useful contexts were found in this iteration.\")\n",
"\n",
" # ----- ASK THE LLM IF MORE SEARCHES ARE NEEDED -----\n",
" new_search_queries = await get_new_search_queries_async(session, user_query, all_search_queries, aggregated_contexts)\n",
" if new_search_queries == \"<done>\":\n",
" print(\"LLM indicated that no further research is needed.\")\n",
" break\n",
" elif new_search_queries:\n",
" print(\"LLM provided new search queries:\", new_search_queries)\n",
" all_search_queries.extend(new_search_queries)\n",
" else:\n",
" print(\"LLM did not provide any new search queries. Ending the loop.\")\n",
" break\n",
"\n",
" iteration += 1\n",
"\n",
" # ----- FINAL REPORT -----\n",
" print(\"\\nGenerating final report...\")\n",
" final_report = await generate_final_report_async(session, user_query, aggregated_contexts)\n",
" print(\"\\n==== FINAL REPORT ====\\n\")\n",
" print(final_report)\n",
"\n",
"\n",
"def main():\n",
" asyncio.run(async_main())\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()\n"
]
},
{
"cell_type": "code",
"source": [],
"metadata": {
"id": "46Q5XpapDJZT"
},
"execution_count": null,
"outputs": []
}
]
}