Files
AI/참고/OpenDeepResearcher-main/open_deep_researcher.ipynb
2026-05-12 19:40:31 +09:00

416 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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": []
}
]
}