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

348 lines
18 KiB
Plaintext
Raw Permalink Normal View History

2026-05-12 19:40:31 +09:00
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"authorship_tag": "ABX9TyM6XDBP8oqAaLL0GMT0mBj+",
"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_gradio.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "bSpd75_8O8rr"
},
"outputs": [],
"source": [
"!pip install nest_asyncio gradio aiohttp\n",
"\n",
"import nest_asyncio\n",
"nest_asyncio.apply()\n",
"\n",
"import asyncio\n",
"import aiohttp\n",
"import gradio as gr\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",
"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_MODEL = \"anthropic/claude-3.5-haiku\"\n",
"\n",
"# -------------------------------\n",
"# Asynchronous Helper Functions\n",
"# -------------------------------\n",
"\n",
"async def call_openrouter_async(session, messages, model=DEFAULT_MODEL):\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):\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",
"async def generate_search_queries_async(session, user_query):\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 complete 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",
" 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",
"async def perform_search_async(session, query):\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",
"async def fetch_webpage_text_async(session, url):\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",
"async def is_page_useful_async(session, user_query, page_text):\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 that is 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",
" if \"Yes\" in answer:\n",
" return \"Yes\"\n",
" elif \"No\" in answer:\n",
" return \"No\"\n",
" return \"No\"\n",
"\n",
"async def extract_relevant_context_async(session, user_query, search_query, page_text):\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 useful for answering the user's query. \"\n",
" \"Return only the relevant context as plain text without extra 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",
"async def get_new_search_queries_async(session, user_query, previous_search_queries, all_contexts):\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, decide 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 extra 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",
"async def generate_final_report_async(session, user_query, all_contexts):\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 complete, well-structured, and detailed report that addresses the query thoroughly. \"\n",
" \"Include all useful insights and conclusions without extra 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",
"async def process_link(session, link, user_query, search_query, log):\n",
" log.append(f\"Fetching content from: {link}\")\n",
" page_text = await fetch_webpage_text_async(session, link)\n",
" if not page_text:\n",
" log.append(f\"Failed to fetch content from: {link}\")\n",
" return None\n",
" usefulness = await is_page_useful_async(session, user_query, page_text)\n",
" log.append(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",
" log.append(f\"Extracted context from {link} (first 200 chars): {context[:200]}\")\n",
" return context\n",
" return None\n",
"\n",
"# -----------------------------\n",
"# Main Asynchronous Routine\n",
"# -----------------------------\n",
"\n",
"async def async_research(user_query, iteration_limit):\n",
" aggregated_contexts = []\n",
" all_search_queries = []\n",
" log_messages = [] # List to store intermediate steps\n",
" iteration = 0\n",
"\n",
" async with aiohttp.ClientSession() as session:\n",
" log_messages.append(\"Generating initial search queries...\")\n",
" new_search_queries = await generate_search_queries_async(session, user_query)\n",
" if not new_search_queries:\n",
" log_messages.append(\"No search queries were generated by the LLM. Exiting.\")\n",
" return \"No search queries were generated by the LLM. Exiting.\", \"\\n\".join(log_messages)\n",
" all_search_queries.extend(new_search_queries)\n",
" log_messages.append(f\"Initial search queries: {new_search_queries}\")\n",
"\n",
" while iteration < iteration_limit:\n",
" log_messages.append(f\"\\n=== Iteration {iteration + 1} ===\")\n",
" iteration_contexts = []\n",
" search_tasks = [perform_search_async(session, query) for query in new_search_queries]\n",
" search_results = await asyncio.gather(*search_tasks)\n",
" unique_links = {}\n",
" for idx, links in enumerate(search_results):\n",
" query_used = new_search_queries[idx]\n",
" for link in links:\n",
" if link not in unique_links:\n",
" unique_links[link] = query_used\n",
"\n",
" log_messages.append(f\"Aggregated {len(unique_links)} unique links from this iteration.\")\n",
" link_tasks = [\n",
" process_link(session, link, user_query, unique_links[link], log_messages)\n",
" for link in unique_links\n",
" ]\n",
" link_results = await asyncio.gather(*link_tasks)\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",
" log_messages.append(f\"Found {len(iteration_contexts)} useful contexts in this iteration.\")\n",
" else:\n",
" log_messages.append(\"No useful contexts were found in this iteration.\")\n",
"\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",
" log_messages.append(\"LLM indicated that no further research is needed.\")\n",
" break\n",
" elif new_search_queries:\n",
" log_messages.append(f\"LLM provided new search queries: {new_search_queries}\")\n",
" all_search_queries.extend(new_search_queries)\n",
" else:\n",
" log_messages.append(\"LLM did not provide any new search queries. Ending the loop.\")\n",
" break\n",
"\n",
" iteration += 1\n",
"\n",
" log_messages.append(\"\\nGenerating final report...\")\n",
" final_report = await generate_final_report_async(session, user_query, aggregated_contexts)\n",
" return final_report, \"\\n\".join(log_messages)\n",
"\n",
"def run_research(user_query, iteration_limit=10):\n",
" return asyncio.run(async_research(user_query, iteration_limit))\n",
"\n",
"# -----------------------------\n",
"# Gradio UI Setup\n",
"# -----------------------------\n",
"\n",
"def gradio_run(user_query, iteration_limit):\n",
" try:\n",
" final_report, logs = run_research(user_query, int(iteration_limit))\n",
" return final_report, logs\n",
" except Exception as e:\n",
" return f\"An error occurred: {e}\", \"\"\n",
"\n",
"iface = gr.Interface(\n",
" fn=gradio_run,\n",
" inputs=[\n",
" gr.Textbox(lines=2, label=\"Research Query/Topic\"),\n",
" gr.Number(value=10, label=\"Max Iterations\")\n",
" ],\n",
" outputs=[\n",
" gr.Textbox(label=\"Final Report\"),\n",
" gr.Textbox(label=\"Intermediate Steps Log\")\n",
" ],\n",
" title=\"Research Assistant\",\n",
" description=\"Enter your query and a maximum iteration count to generate a report. The log will show the steps taken.\"\n",
")\n",
"\n",
"iface.launch()"
]
}
]
}