{ "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": [ "\"Open" ] }, { "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 .\"\n", " \"\\nOutput only a Python list or the token 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 == \"\":\n", " return \"\"\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 == \"\":\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()" ] } ] }