diff --git a/examples/README.md b/examples/README.md
index 324d4a0fd..657c4b6a5 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -64,3 +64,4 @@ Some old examples are still using the legacy SDK, they should still work and are
* [JSON Capabilities](./json_capabilities/): A directory with guides containing different types of tasks you can do with JSON schemas.
* [Automate Google Workspace tasks with the Gemini API](./Apps_script_and_Workspace_codelab/): This codelabs shows you how to connect to the Gemini API using Apps Script, and uses the function calling, vision and text capabilities to automate Google Workspace tasks - summarizing a document, analyzing a chart, sending an email and generating some slides directly. All of this is done from a free text input.
* [Langchain examples](./langchain/): A directory with multiple examples using Gemini with Langchain.
+* [LangGraph examples](./langgraph/): A directory with multiple examples using Gemini with LangGraph.
diff --git a/examples/langgraph/Deep_Research.ipynb b/examples/langgraph/Deep_Research.ipynb
new file mode 100644
index 000000000..8e91b280e
--- /dev/null
+++ b/examples/langgraph/Deep_Research.ipynb
@@ -0,0 +1,1181 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "lTx8eQlc3cP-"
+ },
+ "source": [
+ "##### Copyright 2025 Google LLC."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "cellView": "form",
+ "id": "4HZoi8yf4GEU"
+ },
+ "outputs": [],
+ "source": [
+ "# @title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
+ "# you may not use this file except in compliance with the License.\n",
+ "# You may obtain a copy of the License at\n",
+ "#\n",
+ "# https://www.apache.org/licenses/LICENSE-2.0\n",
+ "#\n",
+ "# Unless required by applicable law or agreed to in writing, software\n",
+ "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
+ "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
+ "# See the License for the specific language governing permissions and\n",
+ "# limitations under the License."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "M9I7LG483nXB"
+ },
+ "source": [
+ "# Building an AI-Powered Research Agent with LangGraph and Gemini"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "awKO767lQIWh"
+ },
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "b1xoF_bU4NCP"
+ },
+ "source": [
+ "## Overview"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "CWedABji6bXJ"
+ },
+ "source": [
+ "This notebook demonstrates how to build an advanced AI research agent that can autonomously conduct web searches, analyze results, and synthesize comprehensive answers to complex questions. The agent uses Google's Gemini API for natural language processing and LangGraph for orchestrating a multi-step research workflow.\n",
+ "The research agent features:\n",
+ "\n",
+ "* Intelligent query generation - Automatically generates multiple search queries based on your question\n",
+ "* Web research capabilities - Conducts Google searches and gathers information\n",
+ "* Self-reflection - Evaluates if gathered information is sufficient and identifies knowledge gaps\n",
+ "* Iterative improvement - Generates follow-up queries to fill information gaps\n",
+ "Comprehensive synthesis - Produces well-structured answers based on all gathered research"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "a5cc7bb8d6bf"
+ },
+ "source": [
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ " | \n",
+ " \n",
+ " \n",
+ " This notebook was contributed by Anand Roy.\n",
+ " \n",
+ " \n",
+ " Have a cool Gemini example? Feel free to share it too!\n",
+ " | \n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "cae6x6Fq8siz"
+ },
+ "source": [
+ "## Setup\n",
+ "First, install the required packages and configure your environment."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "5gmoXdoc8uUQ"
+ },
+ "source": [
+ "### Installation\n",
+ "Install LangGraph for workflow orchestration, LangChain for LLM integration, and supporting libraries."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "G320Tczn8sFj"
+ },
+ "outputs": [],
+ "source": [
+ "%pip install -q langgraph\n",
+ "%pip install -q langchain-google-genai\n",
+ "%pip install -q langchain python-dotenv"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "PGp29ChN9A3b"
+ },
+ "source": [
+ "### Import Required Libraries"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {
+ "id": "L6zsaakF9Few"
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "from datetime import datetime\n",
+ "from typing import Any, Dict, List, TypedDict, Optional\n",
+ "import operator\n",
+ "import types\n",
+ "\n",
+ "from dotenv import load_dotenv\n",
+ "from google import genai\n",
+ "from langchain_core.messages import AnyMessage, AIMessage, HumanMessage\n",
+ "from langchain_core.runnables import RunnableConfig\n",
+ "from langchain_google_genai import ChatGoogleGenerativeAI\n",
+ "from langgraph.graph import StateGraph, add_messages, START, END\n",
+ "from langgraph.types import Send\n",
+ "from pydantic import BaseModel, Field\n",
+ "from typing_extensions import Annotated"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "yBNQz2CM812f"
+ },
+ "source": [
+ "### Configure your API key and load the environment\n",
+ "\n",
+ "To run the following cell, your API key must be stored it in a Colab Secret named `GOOGLE_API_KEY`. If you don't already have an API key, or you're not sure how to create a Colab Secret, see [Authentication](https://github.com/google-gemini/cookbook/blob/main/quickstarts/Authentication.ipynb) for an example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "id": "r1PVHPVE81fX"
+ },
+ "outputs": [],
+ "source": [
+ "from google.colab import userdata\n",
+ "GOOGLE_API_KEY = userdata.get('GOOGLE_API_KEY')\n",
+ "\n",
+ "os.environ['GOOGLE_API_KEY'] = GOOGLE_API_KEY\n",
+ "load_dotenv()\n",
+ "\n",
+ "genai_client = genai.Client()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "DXjni-TQ9dyB"
+ },
+ "source": [
+ "## Define Data Models and State\n",
+ "The research agent uses structured data models to ensure consistent communication between different components of the workflow."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "USi5ms7E9jFP"
+ },
+ "source": [
+ "### Pydantic Models for Structured Outputs\n",
+ "These models define the expected structure of outputs from our LLMs, ensuring type safety and validation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "id": "uj-8NscD9op6"
+ },
+ "outputs": [],
+ "source": [
+ "class SearchQueryList(BaseModel):\n",
+ " \"\"\"Model for generating search queries\"\"\"\n",
+ " query: List[str] = Field(\n",
+ " description=\"A list of search queries to be used for web research.\"\n",
+ " )\n",
+ " rationale: str = Field(\n",
+ " description=\"A brief explanation of why these queries are relevant to the research topic.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "class Reflection(BaseModel):\n",
+ " \"\"\"Model for evaluating research completeness\"\"\"\n",
+ " is_sufficient: bool = Field(\n",
+ " description=\"Whether the provided summaries are sufficient to answer the user's question.\"\n",
+ " )\n",
+ " knowledge_gap: str = Field(\n",
+ " description=\"A description of what information is missing or needs clarification.\"\n",
+ " )\n",
+ " follow_up_queries: List[str] = Field(\n",
+ " description=\"A list of follow-up queries to address the knowledge gap.\"\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "sNxDjSW99plj"
+ },
+ "source": [
+ "### State Definitions\n",
+ "LangGraph uses TypedDict classes to define the state that flows through the graph. Each node can read from and write to this shared state."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {
+ "id": "8BBGXbIi9rja"
+ },
+ "outputs": [],
+ "source": [
+ "class OverallState(TypedDict):\n",
+ " \"\"\"Main state that flows through the entire graph\"\"\"\n",
+ " messages: Annotated[list, add_messages]\n",
+ " search_query: Annotated[list, operator.add]\n",
+ " web_research_result: Annotated[list, operator.add]\n",
+ " sources_gathered: Annotated[list, operator.add]\n",
+ " initial_search_query_count: int\n",
+ " max_research_loops: int\n",
+ " research_loop_count: int\n",
+ " reasoning_model: str\n",
+ "\n",
+ "\n",
+ "class ReflectionState(TypedDict):\n",
+ " \"\"\"State for the reflection process\"\"\"\n",
+ " is_sufficient: bool\n",
+ " knowledge_gap: str\n",
+ " follow_up_queries: Annotated[list, operator.add]\n",
+ " research_loop_count: int\n",
+ " number_of_ran_queries: int\n",
+ "\n",
+ "\n",
+ "class Query(TypedDict):\n",
+ " \"\"\"Individual query structure\"\"\"\n",
+ " query: str\n",
+ " rationale: str\n",
+ "\n",
+ "\n",
+ "class QueryGenerationState(TypedDict):\n",
+ " \"\"\"State for query generation\"\"\"\n",
+ " query_list: list[Query]\n",
+ "\n",
+ "\n",
+ "class WebSearchState(TypedDict):\n",
+ " \"\"\"State for web search operations\"\"\"\n",
+ " search_query: str\n",
+ " id: str"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "tDEkf7cm9yVU"
+ },
+ "source": [
+ "### Configuration\n",
+ "Define configurable parameters that control the agent's behavior. You can adjust these to optimize for different use cases."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "id": "82gr7-lU91Vq"
+ },
+ "outputs": [],
+ "source": [
+ "configurable = types.SimpleNamespace(\n",
+ " query_generator_model=\"gemini-2.0-flash\",\n",
+ " reflection_model=\"gemini-2.5-flash\",\n",
+ " answer_model=\"gemini-2.5-flash\",\n",
+ " number_of_initial_queries=3,\n",
+ " max_research_loops=2,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "pMGW49EJ-DJy"
+ },
+ "source": [
+ "## Define Helper Functions\n",
+ "\n",
+ "These utility functions support the main workflow by extracting information from messages and formatting dates.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "id": "9TpNEcfV-F-o"
+ },
+ "outputs": [],
+ "source": [
+ "def get_research_topic(messages: List[AnyMessage]) -> str:\n",
+ " \"\"\"\n",
+ " Extract the research topic from the conversation messages.\n",
+ " \"\"\"\n",
+ " if len(messages) == 1:\n",
+ " research_topic = messages[-1].content\n",
+ " else:\n",
+ " research_topic = \"\"\n",
+ " for message in messages:\n",
+ " if isinstance(message, HumanMessage):\n",
+ " research_topic += f\"User: {message.content}\\n\"\n",
+ " elif isinstance(message, AIMessage):\n",
+ " research_topic += f\"Assistant: {message.content}\\n\"\n",
+ " return research_topic\n",
+ "\n",
+ "\n",
+ "def get_current_date():\n",
+ " \"\"\"Get the current date in a readable format.\"\"\"\n",
+ " return datetime.now().strftime(\"%B %d, %Y\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "IgDrUepZ-KUp"
+ },
+ "source": [
+ "## Define Prompt Templates\n",
+ "\n",
+ "These templates guide the LLMs at each stage of the research process. They're designed to produce high-quality, structured outputs.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "yy2qmJEQ-Kyy"
+ },
+ "source": [
+ "### Query Generation Prompt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "id": "OyKfXg6Y-L-i"
+ },
+ "outputs": [],
+ "source": [
+ "query_writer_instructions = \"\"\"Your goal is to generate sophisticated and diverse web search queries. These queries are intended for an advanced automated web research tool capable of analyzing complex results, following links, and synthesizing information.\n",
+ "\n",
+ "Instructions:\n",
+ "- Always prefer a single search query, only add another query if the original question requests multiple aspects or elements and one query is not enough.\n",
+ "- Each query should focus on one specific aspect of the original question.\n",
+ "- Don't produce more than {number_queries} queries.\n",
+ "- Queries should be diverse, if the topic is broad, generate more than 1 query.\n",
+ "- Don't generate multiple similar queries, 1 is enough.\n",
+ "- Query should ensure that the most current information is gathered. The current date is {current_date}.\n",
+ "\n",
+ "Format:\n",
+ "- Format your response as a JSON object with ALL three of these exact keys:\n",
+ " - \"rationale\": Brief explanation of why these queries are relevant\n",
+ " - \"query\": A list of search queries\n",
+ "\n",
+ "Example:\n",
+ "\n",
+ "Topic: What revenue grew more last year apple stock or the number of people buying an iphone\n",
+ "```json\n",
+ "{{\n",
+ " \"rationale\": \"To answer this comparative growth question accurately, we need specific data points on Apple's stock performance and iPhone sales metrics. These queries target the precise financial information needed: company revenue trends, product-specific unit sales figures, and stock price movement over the same fiscal period for direct comparison.\",\n",
+ " \"query\": [\"Apple total revenue growth fiscal year 2024\", \"iPhone unit sales growth fiscal year 2024\", \"Apple stock price growth fiscal year 2024\"],\n",
+ "}}\n",
+ "```\n",
+ "\n",
+ "Context: {research_topic}\"\"\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "jvQyptZs-P9Q"
+ },
+ "source": [
+ "### Web Search Prompt\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "id": "3gbX4acY-RJ8"
+ },
+ "outputs": [],
+ "source": [
+ "web_searcher_instructions = \"\"\"Conduct targeted Google Searches to gather the most recent, credible information on \"{research_topic}\" and synthesize it into a verifiable text artifact.\n",
+ "\n",
+ "Instructions:\n",
+ "- Query should ensure that the most current information is gathered. The current date is {current_date}.\n",
+ "- Conduct multiple, diverse searches to gather comprehensive information.\n",
+ "- Consolidate key findings while meticulously tracking the source(s) for each specific piece of information.\n",
+ "- The output should be a well-written summary or report based on your search findings.\n",
+ "- Only include the information found in the search results, don't make up any information.\n",
+ "\n",
+ "Research Topic:\n",
+ "{research_topic}\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "EvxX0xFr-bl1"
+ },
+ "source": [
+ "### Reflection Prompt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {
+ "id": "x6t2TAgV-hj-"
+ },
+ "outputs": [],
+ "source": [
+ "reflection_instructions = \"\"\"You are an expert research assistant analyzing summaries about \"{research_topic}\".\n",
+ "\n",
+ "Instructions:\n",
+ "- Identify knowledge gaps or areas that need deeper exploration and generate a follow-up query. (1 or multiple).\n",
+ "- If provided summaries are sufficient to answer the user's question, don't generate a follow-up query.\n",
+ "- If there is a knowledge gap, generate a follow-up query that would help expand your understanding.\n",
+ "- Focus on technical details, implementation specifics, or emerging trends that weren't fully covered.\n",
+ "\n",
+ "Requirements:\n",
+ "- Ensure the follow-up query is self-contained and includes necessary context for web search.\n",
+ "\n",
+ "Output Format:\n",
+ "- Format your response as a JSON object with these exact keys:\n",
+ " - \"is_sufficient\": true or false\n",
+ " - \"knowledge_gap\": Describe what information is missing or needs clarification\n",
+ " - \"follow_up_queries\": Write a specific question to address this gap\n",
+ "\n",
+ "Example:\n",
+ "```json\n",
+ "{{\n",
+ " \"is_sufficient\": true, // or false\n",
+ " \"knowledge_gap\": \"The summary lacks information about performance metrics and benchmarks\", // \"\" if is_sufficient is true\n",
+ " \"follow_up_queries\": [\"What are typical performance benchmarks and metrics used to evaluate [specific technology]?\"] // [] if is_sufficient is true\n",
+ "}}\n",
+ "```\n",
+ "\n",
+ "Reflect carefully on the Summaries to identify knowledge gaps and produce a follow-up query. Then, produce your output following this JSON format:\n",
+ "\n",
+ "Summaries:\n",
+ "{summaries}\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "O6KLwWQB_BV1"
+ },
+ "source": [
+ "### Answer Generation Prompt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "id": "e_ZInYOM_A-p"
+ },
+ "outputs": [],
+ "source": [
+ "answer_instructions = \"\"\"Generate a high-quality answer to the user's question based on the provided summaries.\n",
+ "\n",
+ "Instructions:\n",
+ "- The current date is {current_date}.\n",
+ "- You are the final step of a multi-step research process, don't mention that you are the final step.\n",
+ "- You have access to all the information gathered from the previous steps.\n",
+ "- You have access to the user's question.\n",
+ "- Generate a high-quality answer to the user's question based on the provided summaries and the user's question.\n",
+ "\n",
+ "User Context:\n",
+ "- {research_topic}\n",
+ "\n",
+ "Summaries:\n",
+ "{summaries}\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "vLii0v_6_N4S"
+ },
+ "source": [
+ "## Define Workflow Nodes\n",
+ "Each node in our LangGraph workflow represents a specific step in the research process. These functions process the state and return updates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "PWYgUEuu_Sc4"
+ },
+ "source": [
+ "### 1. Query Generation Node\n",
+ "This node generates initial search queries based on the user's question."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "id": "iR000h_t_VEJ"
+ },
+ "outputs": [],
+ "source": [
+ "def generate_query(state: OverallState) -> QueryGenerationState:\n",
+ " \"\"\"\n",
+ " Generate search queries based on the user's research topic.\n",
+ " \"\"\"\n",
+ " if state.get(\"initial_search_query_count\") is None:\n",
+ " state[\"initial_search_query_count\"] = configurable.number_of_initial_queries\n",
+ "\n",
+ " llm = ChatGoogleGenerativeAI(\n",
+ " model=configurable.query_generator_model,\n",
+ " temperature=1.0,\n",
+ " max_retries=2,\n",
+ " )\n",
+ " structured_llm = llm.with_structured_output(SearchQueryList)\n",
+ "\n",
+ " current_date = get_current_date()\n",
+ " formatted_prompt = query_writer_instructions.format(\n",
+ " current_date=current_date,\n",
+ " research_topic=get_research_topic(state[\"messages\"]),\n",
+ " number_queries=state[\"initial_search_query_count\"],\n",
+ " )\n",
+ "\n",
+ " result = structured_llm.invoke(formatted_prompt)\n",
+ " return {\"query_list\": result.query}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "zzaTptHJ_Vxh"
+ },
+ "source": [
+ "### 2. Query Distribution Function\n",
+ "This function distributes queries to parallel web research nodes."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "id": "gdx4bI1t_Zf-"
+ },
+ "outputs": [],
+ "source": [
+ "def continue_to_web_research(state: QueryGenerationState):\n",
+ " \"\"\"\n",
+ " Send each generated query to a separate web research node for parallel processing.\n",
+ " \"\"\"\n",
+ " return [\n",
+ " Send(\"web_research\", {\"search_query\": search_query, \"id\": int(idx)})\n",
+ " for idx, search_query in enumerate(state[\"query_list\"])\n",
+ " ]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "bcSkBP38_bm7"
+ },
+ "source": [
+ "### 3. Web Research Node\n",
+ "This node performs actual web searches using Gemini's built-in Google Search tool."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {
+ "id": "L2r3P4mp_cdM"
+ },
+ "outputs": [],
+ "source": [
+ "def web_research(state: WebSearchState) -> OverallState:\n",
+ " \"\"\"\n",
+ " Conduct web research for a specific search query using Gemini's search capabilities.\n",
+ " \"\"\"\n",
+ " formatted_prompt = web_searcher_instructions.format(\n",
+ " current_date=get_current_date(),\n",
+ " research_topic=state[\"search_query\"]\n",
+ " )\n",
+ "\n",
+ " response = genai_client.models.generate_content(\n",
+ " model=configurable.query_generator_model,\n",
+ " contents=formatted_prompt,\n",
+ " config={\"tools\": [{\"google_search\": {}}], \"temperature\": 0},\n",
+ " )\n",
+ "\n",
+ " return {\n",
+ " \"search_query\": [state[\"search_query\"]],\n",
+ " \"web_research_result\": [response.text],\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "JpDkGKrO_d5g"
+ },
+ "source": [
+ "### 4. Reflection Node\n",
+ "This node evaluates whether the gathered information is sufficient to answer the user's question."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "id": "WBw8iSVd_fil"
+ },
+ "outputs": [],
+ "source": [
+ "def reflection(state: OverallState) -> ReflectionState:\n",
+ " \"\"\"\n",
+ " Reflect on the gathered research to determine if it's sufficient or if more research is needed.\n",
+ " \"\"\"\n",
+ " state[\"research_loop_count\"] = state.get(\"research_loop_count\", 0) + 1\n",
+ " reflection_model = configurable.reflection_model\n",
+ " current_date = get_current_date()\n",
+ "\n",
+ " formatted_prompt = reflection_instructions.format(\n",
+ " current_date=current_date,\n",
+ " research_topic=get_research_topic(state[\"messages\"]),\n",
+ " summaries=\"\\n\\n---\\n\\n\".join(state[\"web_research_result\"]),\n",
+ " )\n",
+ "\n",
+ " llm = ChatGoogleGenerativeAI(\n",
+ " model=reflection_model,\n",
+ " temperature=1.0,\n",
+ " max_retries=2,\n",
+ " )\n",
+ " result = llm.with_structured_output(Reflection).invoke(formatted_prompt)\n",
+ "\n",
+ " print(f\"Reflection result: {result}\")\n",
+ "\n",
+ " return {\n",
+ " \"is_sufficient\": result.is_sufficient,\n",
+ " \"knowledge_gap\": result.knowledge_gap,\n",
+ " \"follow_up_queries\": result.follow_up_queries,\n",
+ " \"research_loop_count\": state[\"research_loop_count\"],\n",
+ " \"number_of_ran_queries\": len(state[\"search_query\"]),\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "M4SEOxih_hwR"
+ },
+ "source": [
+ "### 5. Research Evaluation Function\n",
+ "This function decides whether to continue researching or finalize the answer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {
+ "id": "jucTaEpN_iwq"
+ },
+ "outputs": [],
+ "source": [
+ "def evaluate_research(state: ReflectionState) -> OverallState:\n",
+ " \"\"\"\n",
+ " Evaluate if more research is needed based on reflection results and loop count.\n",
+ " \"\"\"\n",
+ " max_research_loops = (\n",
+ " state.get(\"max_research_loops\")\n",
+ " if state.get(\"max_research_loops\") is not None\n",
+ " else configurable.max_research_loops\n",
+ " )\n",
+ "\n",
+ " if state[\"is_sufficient\"] or state[\"research_loop_count\"] >= max_research_loops:\n",
+ " return \"finalize_answer\"\n",
+ " else:\n",
+ " # Generate follow-up research tasks\n",
+ " return [\n",
+ " Send(\n",
+ " \"web_research\",\n",
+ " {\n",
+ " \"search_query\": follow_up_query,\n",
+ " \"id\": state[\"number_of_ran_queries\"] + int(idx),\n",
+ " },\n",
+ " )\n",
+ " for idx, follow_up_query in enumerate(state[\"follow_up_queries\"])\n",
+ " ]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "R0q4QUPA_k46"
+ },
+ "source": [
+ "### 6. Answer Finalization Node\n",
+ "This node synthesizes all gathered research into a comprehensive answer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {
+ "id": "IA5nnJ94_lop"
+ },
+ "outputs": [],
+ "source": [
+ "def finalize_answer(state: OverallState):\n",
+ " \"\"\"\n",
+ " Generate the final answer based on all gathered research.\n",
+ " \"\"\"\n",
+ " print(\"Finalizing answer...\")\n",
+ " answer_model = configurable.answer_model\n",
+ " current_date = get_current_date()\n",
+ "\n",
+ " formatted_prompt = answer_instructions.format(\n",
+ " current_date=current_date,\n",
+ " research_topic=get_research_topic(state[\"messages\"]),\n",
+ " summaries=\"\\n---\\n\\n\".join(state[\"web_research_result\"]),\n",
+ " )\n",
+ "\n",
+ " llm = ChatGoogleGenerativeAI(\n",
+ " model=answer_model,\n",
+ " temperature=0,\n",
+ " max_retries=2,\n",
+ " )\n",
+ " result = llm.invoke(formatted_prompt)\n",
+ "\n",
+ " return {\n",
+ " \"messages\": [AIMessage(content=result.content)],\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "XYAAPqek_ojA"
+ },
+ "source": [
+ "## Build the Research Agent Graph\n",
+ "Now we'll assemble all the nodes into a LangGraph workflow that orchestrates the entire research process."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {
+ "id": "moF4-8sE_pQk"
+ },
+ "outputs": [],
+ "source": [
+ "# Create the graph\n",
+ "graph = StateGraph(OverallState)\n",
+ "\n",
+ "# Add nodes to the graph\n",
+ "graph.add_node(\"generate_query\", generate_query)\n",
+ "graph.add_node(\"web_research\", web_research)\n",
+ "graph.add_node(\"reflection\", reflection)\n",
+ "graph.add_node(\"finalize_answer\", finalize_answer)\n",
+ "\n",
+ "# Define the workflow edges\n",
+ "graph.add_edge(START, \"generate_query\")\n",
+ "graph.add_conditional_edges(\n",
+ " \"generate_query\", continue_to_web_research, [\"web_research\"]\n",
+ ")\n",
+ "graph.add_edge(\"web_research\", \"reflection\")\n",
+ "graph.add_conditional_edges(\n",
+ " \"reflection\", evaluate_research, [\"web_research\", \"finalize_answer\"]\n",
+ ")\n",
+ "graph.add_edge(\"finalize_answer\", END)\n",
+ "\n",
+ "# Compile the graph\n",
+ "app = graph.compile(name=\"pro-search-agent\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "Ubqb8ay4_w4T"
+ },
+ "source": [
+ "## Visualize the Workflow\n",
+ "Let's visualize the research agent's workflow to better understand how information flows through the system."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {
+ "id": "BC0jpvdB_yYf"
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAJ4AAAITCAIAAAB5VjIDAAAAAXNSR0IArs4c6QAAIABJREFUeJztnXdcFEcbx+d6v6P33rsghxg1RsXE3rH3HmsSxRZjiWKMQYkmxhZbEkUswV5ijBp7QaSDiiC93QHHVa6+f6wvIeYOQbldmNvvhz+2THnY3808s7NTCDqdDuDACBFrA3CMBS4ttODSQgsuLbTg0kILLi20kLE24F/UVavENSppvUYm1qgatFib0yIoNAKTQ2ZxSVxLCs+KgrU5/0BoD++1Fa8ULzMkr7Kk5rZUVYOWxSWzzSik9vWrM4hapZOK1NJ6NZlKrKtSugeyPYLZdm40rO3CWlpBmfLeBQGbRzazobgHss1t2tGv/h2orVQWZElrq1QysbrbYCtLeyqGxmAp7d1zwqJn0m5DrFz9mFjZYCQKc2R3zwvc/FndhlhiZQM20uq0ICGu6IOBlh7BLPRzR42X6dJHfwjHL3PBJHcMWshaDdi1LG/AVDu4dQUAeIawPplkt3NJnhaTFqEOXdRK7a5leShnijk7l77QaNDOFO1Se2xrEVYVFIaMj3E5FleEcqao+trbZwTOvkw3f9gaTS2hIEtamifvMcwKtRzRK7XlrxSVRQrT1BUA4B7IKiuQVxY1oJYjetLeuyDoNhi932w7pNtgq3sXBKhlh5K0xc9k1g40Bw86Otm1T5y8GOY21JIXcnSyQ0naF6kSSwe0+9769u1bWlra2ljHjx9ft26dcSwCVg7UvDSJkRJ/A5SkLciSugei+hZbUlJSV1f3DhGzsrKMYM5r3ANZBVkoSYtGC7mqqCHlRm3/qXbGSFyn0yUkJFy8eLGoqMjd3T0yMnLevHmPHz9euHAhEuCjjz7atm3by5cvT5069ejRo4qKCnd391GjRo0YMQIA8OzZs4kTJ27fvj02Ntbc3JzJZKalpSERjxw54ufn1+YGXz5cwe9rbu1k/DoMhXfn3OT6P36rMFLiCQkJ3bt3P3/+vEAgSEpKioqK+uWXX3Q63e3bt8PDw0tKSpBgc+fOHTFiRHJyck1NzcmTJ8PDw+/fv6/T6fLz88PDw8eNG3fkyJHMzEydTjd16tS1a9cayVqdTnfll/LnKWLjpd8IGl/OZPUaFpdkpMRTUlLCw8MHDx4MABgxYgSfz1coFP8NtmXLFplMZm9vDwCIjo4+ffr0vXv3unbtSiKRkJI9ceJEI1n4BkwuWVqvRiEjNKSV1qvZPGNl1KlTpx9//HHDhg09e/YMDw93dnbWG0yr1R49evTevXtFRa97hdzd3Rvv+vv7G8m8/8LikmT1GhQyQkNaApFAphqrvTZ+/Hgmk3nr1q2YmBgymdyvX79FixZZWf3rBVqj0SxatEin0y1atIjP53M4nGnTpjUNQKOh13onU4gEAhqfC9CQls4kimtVRkqcRCKNHDly5MiR+fn5Dx8+3Lt3r1Qq3bp1a9Mw2dnZubm5u3fvjoiIQK6IxWIj2fNWxLUqJgeNx47Gyw+LS5aKjOJddDrdhQsX8vPzAQAeHh7jx48fN25cbm7uG8GQtyBra2vkNC8vr7Cw0Bj2tARpvYbFM1bLoyloSMu1pBDJBGOkTCAQLly4sHz58tu3b9fX19+5c+fmzZshISEAADc3NwDAtWvXMjMzPT09CQTC0aNHJRJJQUFBfHx8165dy8vL9abp7OycnZ2NtKWNYTOJTOCaozKwBoVWuE6n27MiT6nQGiPl8vLypUuXhoeHh4eH9+vXb8+ePRKJBLm1fv36yMjIOXPm6HS6K1euREdHh4eHjxgxIjMz88aNG+Hh4WPGjCksLGx8EUJISUkZNWpURETEgwcP2txahUyzd9XLNk9WLyh91Lt6pNItgOnTmYNCXu2Z3MfikheyvhNsUcgLpY5GrxB2dQl637PaLYKyBo9gNjp5oTTY1yOEdf+SICCSa26r3828evXqjReSRkgkkkaj/0UwOjq6sUOxzYmJiUlOTtZ7y8LCwpAnXrNmTVRUlN5bwnJl8XMZal/j0RtlUZAlzXpQP3imvd67arW6qqpK7y2xWMzh6K/JWSwWj8drUzP/QSAQKJVKvbcUCgWdrv8Dpbm5OYPB0Hvr/L6ykA/NXNEajIDeEH33QNbLdGllUYOti57+ATKZ7ODggJoxLeGNfo/3pOKVgsklo6Yr2oNV+463SdpZolFhPxUFZVQNurN7SqPG2aCZKdojGscvczn6Hdpj+zAn4bvC8ctdUc4Ug9kDcon25I7iSStdiCSj9GO0K9Qq3dFvC8cucaGz0C5FGMweYLCJQ2Y77Fn5UgD761B1ccPPX+UPn+eIvq4YT+f682ilSqntPsSqXU1LbRPqqlX3zgsodOLHqPRO6AXjSZgv06X3Lgi8Qtm2znS3QBaxg0/S12pAQZa0qljxMl3SbbAVtpOa2sXU6RdPJS9SxQVZ0sCuPORjNduMQu4gJVnVoJPWq6X1Gp0O5DwUuQWyvEM53mEodTk1Q7uQtpHiZ7I6gUpWr5GJNUpFGw9FQMZXuLi08YwjCp3I4pCZXJKZNdXZR39nBSa0L2mNyr59+wAAc+bMwdoQlOjgzg3HMLi00IJLCy24tNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstHWR3s7aASqWazqBr05LW0OoFsIJXyNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstuLTQAv+SYIMHDyaRSDqdTiwW63Q6Ho+n0+k0Gs3FixexNs24wP8p3s3N7e7du8iOlwAAiUSi1Wq7d++OtV1GB/4Kefr06ebm5k2v8Hg8Q7uTwAT80oaHh7+xeXRISEh4eDh2FqEE/NICAGbMmMHlcpFjS0vLGTNmYG0RGpiEtHw+Pzg4GDkOCgrq1KkT1hahgUlICwCYNm2apaWlhYXF1KlTsbYFJd7eQpaKNMLyBqkYje3NjQcdeHb2GaLVaikNbjmP67E2571gcshW9rS3boL7lvfaPxMqywsUXEsKnQX/a1JHQSFRS+rUdm70vuOb2xOqOWnP7i138WN7hZr6zqTtk+cp9WV50iGz9e9R2Jy0lw9XOHiyPUKw3x4BxxB5qeLKQln/Kfr3m9HfjKosalCpdLiu7RyvUE6DXGtoY2D90grKGmgMNLaqx3lPaAyioKw10spEaq5FB9lnx7ThWlIlIv275uiXVqsFGjXkX4TgQKPW6bT6lTKVLgsTBJcWWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlpwaaEFlxZaIJd2/dcrLl0+i7UV2AC5tLnPsrA2ATP0D6B5eLlGpQKdPrJoeUJCoWDLd+uzstNdXNyHDx1dXFJ4997fhw6cAACo1eqf9+988PBOdXVlcHDYiGFjunbtAQDIy3s+e+6E77bsPHvu5N27f9vY2Pbu9cncOYsJBAIAQCCo3rU7Pis7XS6XR0Z2nzJplrOzKwDg1O8Jicd//fyzlevWLx8+fMyiBTEFBS/PnT/1JOVRVVWFq4v7kCGjBg8aoVarP+7XFbGNzWafP3sTAHDp8tnzF5JevXrp4eHdu9fHo0aOR/JqBplMtmnzVykpj9Rq9ayZC4RCwaPH9w4fPAkA+KT/BzOmzxs3dgoScvOWdcXFhbt2Hm7G+Bd5z+bMnbh50/at8bFmZuZ0OoPN5nz7zY7G7NasjRHWCJBEWkLqzRoaHXTpp0epNiu138V9XVxcuG3rnq/XfXf33t8PHt5pnEH1/fbNSacTR40cfyzhQs8P+6z7evmt29eRVWwBANviY/tGDbh65f7KFV8fP/HbjZt/Ir+GJTGfZmSmxixdc/jgSS6Xt2DhtLLyUgAAhUKVy2WJx39dtXLDiGFjAAA/7oxLfvJwyedfJiZcGDhw+Lb4TY+TH5DJ5CuX7gIAlsWsQXT9889LcVs3+vkGJBw5N33apydPHf1pV/xb/6/47d+8Kni5Y/v+48cuVldXXb58lkqhNh+lGeORuPsP/jR2zOSlS74aOGDY48f3RfUiJKJCoXjw8M4nHw9qC0HaSFqhUPDo8f1x46b6+QbY2NguXbK6oqIMuaVQKK7+eXHC+GlDh4zicXmDBg7v07vfkSMHAABEIhEAMGjgiF4f9aVQKGGhfFtbu9zcLABAWnpKcXHhqpUbIvhdLSwsF85fyuHykpISAQAkEkkmk82cMb9vVH8nJxcAwLp1W+K2/BQaGm5mZj5saLS3l++jR/f+a+T5i0khIWGfLV5hbm7BD4+cMW3embMnRKK6Zv4viUTy99/XxoyZ7OPtZ2FhuWD+Ep6Z+VunrTZvPACge7ePRkdP9PcL7Bs1gEql/vXXFSTinbs3AQB9+vR7Dyn+oW2kLXj1EgAQHBSKnPJ4ZqGhfOQ4NzdLrVZH8D9oDBwWyn+R90wqlSKnPj7+jbfYbI5EIgYAZGSkUiiUzmERyHUCgRDaKTwj42ljSF+fgMZjnVZ78vejk6eO7B3F7x3Ff5H3rK6u5g0L1Wp1dnbGv8wIi9BoNBkZqc38X0VFBWq12t8/qNEMP98AHXiLtG813sf79b9MpVL7fTL42l+XkdPbt6937/YRl8NtPv0W0jYDx6VSCQCAzmA0XjE3s0AKrkQqBgAs+mzmG1FqagSIn0PK7htIJGKVStU7it/0oqWlVeMxUpkDADQazYqVi3Q63ZzZi0JD+Rw2Z/5CPRMsFQqFRqM5cHDXgYO7ml6v/c+P4N9GCgEATAaz8QqdzmgmfEuNp9Eaj4cMHjVrzvjKygoez+zho7trVn/z1vRbSNtIS6PSAAAa9T+TRxofmYWFFQBg6ZLVjo7OTaNYWdkIhdWGErS0tGIwGJtiv/+XrSQ91j57lv38Re62rbsbSwlS7t+AzWbT6fT+/Yb07BnV9Lqjg/N/AzfC45khP4vGKzKZ1FBgrUbTWuMBAJ6e3n6+AZcun3F392IwmJGRbTanu22kdXBwQqplpB0okUhSUh4hF52dXalUKolECvt/FV1TIyQQCAxGcz9/Dw9vuVxuZ+dgb+eAXCktK7Ewt/xvSMRZWllaI6f5+XnFxYW+TSr5f6WpkDeaoVQqKyvLbWz0j89GsLNzAABk52R4efm8rtVzMtjs19MpaDSaXC5rDFxU9IpEJrfKeISBA4ef+j0hPz+vb9QAMrnNJuC0ja91cXFzdnY9/MvesvJSiUSyfcdme3tH5BaHzZk2de7hX/ZmZKQqlcqbf19btmLBjh+2NJ9gZJduXbp0i4vbUFlZIRLVJZ0+Pm/+lMtXzv03pJu7J4FAOHnqqEQiKSws2LU7PoLftaKyHHn01tY2KSmPnqYmq9XqubMX37r116XLZ7VabXr60w2xq5Yum9fQoH8QL4K1tU1QUKcDB3eVlpUIBNXbd3yLuB6EwMBOt+/cQBoNvx05IKwRtNZ4hKg+/auqKh4n3x84YNjbnnQraLOXnxXL1mm12kmTh3+xZI6vb0BQYCcK+fVI5vHjpsYsXZOQeHjIsF4//Pido4Pzspi1b01w86btPXtGbYhdNXxk3zNnT/TvN2TkiLH/DWZv57D6y9iMzNQhw3p9tXbpzJkLhg6NzsxMmzFrLABg4oQZyU8erlm7VK6Qh4SE7d19JD396YhRHy9bsUAmlcZujKc1cXt6WbVyg6+P/6zZ40aPHSCTST/s0afx1qKFy8x45oOHfvRxv64NDYq+UQMaXVILjUdgMpnh4ZGuLu7u7p5vfSwtp826LESiOoVCYWtrh5yuWv05nUZft/bbtjO1XbAtflNObub+fcfaME2FQjFm7IC5cz8bNHB4a+M202XRZjX7mnUxQkH1vE+/CAwMuXDx9JMnDzc36WTB0YtcLhcKq3ft+d7N3bNta+O2lHbD+ri4bRv37NshFFa7urivX7slvHOXtkrceGRlpa9ctdjQ3WMJF9hsI85pO3nq6KHDewIDQ9at+fatXZ6tpc0q5I5L+f87zv5LYxO33YJGhdxxaf/6vRuQf9QzZXBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlr0S0tnEYl4P1VHgEgi0Fn6V/jSL625DbXylULvLZx2RcUrmYWt/sGz+qV18mE2yLVqJb50VLtGqdBqlFpHT/1DkfRLSySC3qOtryca/CSC0x64caK89xgbgoH2UnOL5gpKG07uKOnUy8LMimqoQsdBH7lEUy9UPr0uHLvUxdLe4FSGtyx1rVHpUm7UVpc0SA0sBNiBEEskAACOMT+towOTS7JxpoVHWRCbLW7w787VyL59+wAAc+bMwdoQlDAhaUtKSnQ6nbNzc2PKYcKEpDU1TKg36vLly5cuXcLaCvQwoT6n4uJirE1AFROqkHFfiwMJuK+FFtzXQosJVci4r8WBBNzXQgvua6HFhCpk3NfiQALua6EF97XQYkIVcmlpqU6nc3JywtoQlDAhaU0NE/K1Fy9evHDhAtZWoIcJ+drS0lKsTUAVE6qQcV+LAwm4r4UWE/K1tbW1Wq0WayvQw4QqZNzX4kAC7muhxYR8Lf5eCy24r8WBBNzXQgvua6HFhCpk3NfiQALua6EF97XQYkIVMu5rcSAB97XQgvtaaIG/Qh40aBDyPyLf4UkkEnIK/UwC+Eutg4NDcnIyifTPondarTYiIgJTo9AAfl87depUC4t/bTlnbm4+efJk7CxCCfil7dGjh7e3d9MrXl5ePXr0wM4ilIBfWgDAhAkTeDwecszj8aZMmYK1RWhgEtL27NnTx8cHOfby8urevTvWFqGBSUjbWHC5XO60adOwtgUl3rGFXF3aoFR0pDG9Hg78QM8PAQAuNqGlL+VYm9MKqDSStZPBpcqbodXvtTdOVGU9qHfxY6k6lLQdFzKNWPxMGvgBr/do61ZFbIW0GrXu1A8lgR9YuAaw3slInHenIFPyPLlu5EKn5helb0orpD3+fXF4lLWtK/3dDcR5D8rz5Wm3hKM/a+mXq5Y2o3IeiR08WLiuGGLvwbB1ZT5LFrcwfEulrSpW0Jn4fjAYQ2MQK4sbWhi4pdKqGnQ863dpp+G0IWbW1Ja3XlsqrVSs1qgh/0bU/tFqdDJJSzdcMpUuCxMElxZacGmhBZcWWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoaWdSrtu/fKlMfOwtqKN2fTNV4s+m4ladu1UWpz3B5cWWowibUNDQ+8ofkZGKnJ67a8rvaP4587/jpzm5+f1juI/f5ELALh0+ey8BVMHDOqxYNH0U78nNB3NQyKRkp88jFk2f8CgHgsXz0DCN8+Qob2SkhI/+2J27yh+vbi+mfRF9aIffvxuwsShg4b0XLL008tXzjUmYihKQcHLHT9smTJtVP+B3ed+OunCxdPN5Hv37t/jJgyO+rjL3E8nXfnjfGNICpnyNDU5ekz/j/t1nbdganZO5ns/bIMYRVoajWZra5eR+VrazMxUc3OLzKw05DQ94ymPZ+bj7ffnn5fitm708w1IOHJu+rRPT546+tOu+MZECl69PHfu1MSJM77ZtF2r1X61Zslbh3FRqNSk04leXr5x3/3EZDCbSX/r1o1PU5O/+OLLg/tP+PkFbovfhDzlZqL8uDMu+cnDJZ9/mZhwYeDA4dviNz1OfqA337t3/1739fJZMxd+u/mH7t17bfnu6+s3riIhq6oqzp//ffWXsd9u/kGpbIjbuqGtn/0/GKtCDu8c2ShtWnrKkMEjszJfS5uW9iS8cxcAwPmLSSEhYZ8tXmFubsEPj5wxbd6ZsydEojokWG1tzeJFy8NC+WGh/CmTZ1dXV6WnP20+UxKJZGVts2hBDD88kkwmN5N+WnrKJx8PiuB3tbW1mzN70c4fD1laWDVv0rp1W+K2/BQaGm5mZj5saLS3l++jR/f05nvw8O6eH/bpG9U/gt91yuRZo6MnSqUSJGRVdeUXX3wZFsoP79xl5Ihxr17lN/6/bY6xpA0Li8jMTNVqtSJR3atX+cOGjq6oLBcKBQCAp6nJnTt3UavV2dkZEfwPmkbRaDSN1binh7eV1euRt0GBnQAA5RVvn/vs4+2PHDSffnBw6PETv+3d90Nq6hO1Wu3nG2Bra9d8FJ1We/L3o5Onjuwdxe8dxX+R96yurua/+Wo0moKCl/7+QY235s/7Ysjgka//KU8fDpuDHHM4XACAQqF4pwf8dow1vzYi4gOJRPIy/0VpabG3l6+FhaW/f1Bq2hNPD2+RqI4f3lWhUGg0mgMHdx04uKtpxNr/Py8Wi914kclkAgDE4vq35kulvh7A1Xz6K5avP3fu1F/XryQe/5XNYo8cOW7ypFnNRNFoNCtWLtLpdHNmLwoN5XPYnPkLp+nNVyqT6nQ6BoOp1zwyGb0JzcbKicfleXh4pac/LSsvCQ4JAwAEB4VmZadLJGInJxdbWzsAAJ1O799vSM+eUU0jOjq83qlSrvhn+oZEKgEAcDm8lhvAZrObSZ/L4U6aOGPihOmZmWm3bl//9bf9XA5v1KjxhqI8e5b9/EXutq27O4e9nnMtkegfNMpkMAkEgqG7aGLEH1FYaERubmZhYcGkSTORSvXwL3tFdbUR/K5IAA8Pb7lCHhbKR06VSmVlZbmNjS1yWlRUoFAo6HQ6ACAnJxMA4OTk0ioDDKUvEtX9df2PQQOH02i04ODQ4ODQ5y9ynr3IaSbKy5fPAQBWlq8dRH5+XnFxoa+P/38zJZPJ3l6+aekp48a+nur58/6dKpVq/rwv3ukpvjtGfK/tHBaRmZmW9/J5cFAoACAoqNPL/BfZ2Rmdw7ogAebOXnzr1l+XLp/VarXp6U83xK5aumxeQ0MDsiYBnc7YGh8rlohraoRHEw7a2doHBAS3ygBD6RNJpEOHdq/fsCIrK722tubq1YsvXuQi7txQFDd3TwKBcPLUUYlEUlhYsGt3fAS/a0Vlud58R44Y9/jx/eMnfnuamnz23Kljib94enjrDWlUjFlqwyIqKstdXNzMzS0AADyemYuLW2FhQXh4JBIgJCRs7+4jRxMO7d33g0IhDwwIid0YT6PRAABKlTIkOMzF2S16dD+tVuvvHxS7MZ5AILTKAEPp02i02I3xP/4Ut3DxDACAh4fXwgUxA/oPbSaKvZ3D6i9jfzuyf8iwXk5OLl+u2igUVq9ZGzNj1tiD+4+/kW+/foPrxaJfft0nlUotLa3mzlncr9/gtnuuLaWlc37O7i3zCTdz8tbfOsBBh5Ln0rzU+iGz7VsSGO9ohJaOtLhQVlb6ylWLDd09lnCBzWYbumuCdCRpAwND9u1LMHQX1/UNOpK0AAB7OwesTegw4L4WWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlpaKi3XgkzEfwZYQyASORYt7UBsqVwMFklQaqwBWjgtpLpEzmC1dGG2lkrr7MuS1KnfwyqcNkAqUrn4tvSTeUuldfSks81IDy9Vv4dhOO/FgwtVXEuyvXtLl8ls3XrIyddqBeUqR0+mlQOdRGndcBacd0Ot0gnLFCXPpbautM69zVoesdVLXRdkSnOTxQqZpqZC2Xo7UUWj0QKga7rDzxuoVGoKpb1/1jS3ozBYZP8IrltAK0cv6eBl6dKlN27cMHR33rx5gwYNSk5ORtco9ID5hSYnJ8ffX89QYQBAVVVVSUlJRUVFXFycSCRC3TQ0gFbampoalUpla2ur925ubm59fT0AIC8vb/Xq1ahbhwbQSpubm2uoyAIAHj58KBa/nruRkpKyY8cOFE1DCWilzcnJ8fPzM3T3yZMnje1HpVJ56dIl+DbGhFlaQ6U2Pz9fKpUSm3ScCoXCAwcOoGgdGkArbXZ2dkBAgN5bmZmZAoEAmVmk1Wp1Op25ublaDVtfW3t/q3s3BAKBVqu1sbHRe/fq1askEsnW1vbMmTOom4YecErbTG0MANi5c2fj8Y4dO6KiooKCggwF7rjAWSE3L21TqFTqw4cPjW8RBkBbakeOHNmSkFOmTKmrM9ZCIdgCp7TNv9Q2hcVisVhwbhEIYYVcXV0NALCysmph+JkzZyI9U5ABobTNvPbohcViZWYacdU1rICwQs7NzW2mH+q/bN682ZjmYAaEpbblzWMEWN0tLi0QCoXjxo0zpkXYAJu0lZWVZDLZ0tKy5VEsLS3r6uqEQqEx7cIA2HxtTk5Oq9pQCBcuXCBCN8waNmlzc3N9fX1bGwvNpRNRA7afamvffBBu3bq1ZMkS41iEGbBJ29o2FEJQUFB+fr5xLMIMqKStqKig0+nm5uatjWhhYQHfBz6opG1+0EzziMVipbK9j6xuFVBJW1lZGRoa+m5xz5w5A1nBhUpaNze3+/fvv1vc58+fv0PTuj3T6okh7RmRSDRq1Khr165hbUi7AKpSy+PxmExmWVlZayOqVKqioiLjGIUZUEkLAPD398/JyWltrJs3b+7evds4FmEGLi0AANTW1vbo0cM4FmEGbB1sAQEBv/zyS2tjjRkzxjjmYAlspdbPz+8dSm1qaqpGozGORZgBm7RcLpfL5ZaWvn0fr0YKCws3btzYzAzrDgps0iLuNjs7u+XhhULh8OHDjWkRNkD1Xovw66+/ikSiRYsWYW0IxuClFqSkpNTW1hrTImyAUNqAgIBWtaQ+++wzZHs3yIBQWhaLZW5uXlxc3JLAAoFg/PjxDAbD+HahDYTStqrgWllZzZ8/3/gWYQCc0rb87TYtLS0vL8/4FmEAnNIGBAS0sCW1c+fOxvVKIAO2jkYEpCd5yJAhUqm0rq5u8ODBGzZs0BsyLCwsOLh1e6d2FGCT9qOPPkJKIZFIlMlkSKuqV69ehsLD6mghrJA5HA6RSGw6Xtzc3NzQ8NW8vLzr16+jaB2qwCbtunXrLCwsGk+1Wq2tra2dnZ3ewGfPnq2srETROlSBTdqIiIiJEydSKBTklEgkhoWFGQrs7+8fFRWFonWoApu0AICpU6f27NkT6Rvncrl8Pt9QyIEDBxpagAhvkwe0AAAZ0ElEQVQCIJQWALBlyxYvLy+dTsfhcDp16qQ3jFAoPHjwIOqmoQeqLWSJSK3TopTXhrVxMTExvp7+ShlJKdOzktudm0+KCwTiWvQWeSMQANsMvQeO0ke9W6cFz1PEti50YXkDCtm1BK1Wizhj1HK0tKNVFit8wjg9R7Z0CZX3wejSqpW6favze4+xt3Kk0Vu8kQmsKKSa6pKGW0kVsza6k428eYPRpf15df7whW50JpxO/d2Q1avP7yuetdHdqLkYV9qHV2poLIpnCMd4WXRQ8lLFqgZVl08sWhD2HTFuYSp+JuOYU4yaRQeFY04ufiYzahbGlZZEJprb0IyaRQfF3JZOIhn34Rs39eoyhRa6YXVtgk6rE5QZd49CvHUDLbi00IJLCy24tNCCSwstuLTQgksLLbi00IJLCy24tNCCSwstHVja23duzJ4zoXcUPysrff3XK2KWtc1g8SHDeh1NONQmSWFLB5Y2IeEQACB+2x5XV4/3TGr91ysuXT6LHI8bOzU46B0XemxXdGBppTJpSKfOYaF8Npv9nknlPstqPJ44YXpIiMGhyx2I9iXtqd8Tosf0v3P3ZtTHXX78aSsAQK1W796zfer06IGDP1yxavGDB3cAAA0NDb2j+MXFhUlJiUiF3DQRgaB6w8ZVY8cPGjq8z6bNa4qLCxtviepF325Z3zuKP3xk39hNq6urq9Rqde8ofmVlRdzWjUOG9XqjQi6vKFv/9YroMf37Deg299NJCccOI9d///3YqNH9srLSp06P7h3Fnzl73B9/XED3Ub2d9iUthUKVy2WJx39dtXLDiGFjAADfb9+cdDpx1MjxxxIu9Pywz7qvl9+6fZ1Go934K9nZ2XXkyHE3/koODAxpTEGtVi+J+TQjMzVm6ZrDB09yubwFC6eVlZciCzGu+vIzUX1d/LY9ixYuq6gsX/nlYgDAlUt3AQDLYtacP3uzqTFarTZm2fxqQdWm2O9PJF7q0aP3z/t33vz7GgCAQqWKxfU/7oxbsWzd9WuPP+zRJ27bxurqKiyemUHal7QkEkkmk82cMb9vVH8nJxeFQnH1z4sTxk8bOmQUj8sbNHB4n979jhxpbufvtPSU4uLCVSs3RPC7WlhYLpy/lMPlJSUlAgDu3vs7Jydz3tzPw0L5UX36LZi/1N3dq7a2xlBSDx/eLSsrWbFsna+PP49nNnnSzODg0MtXziFDXFUq1YL5SwMCggkEwiefDNJoNM+ft3opMqPSvqRF8PV5PbEuNzdLrVZH8D9ovBUWyn+R90wqlRqKm5GRSqFQOodFIKcEAiG0U3hGxlMAQEFBHpvNdnFxQ275+wV+9WWstbXBiSGvCvOZTGZjeACAj7f/y5fPG0/9/AKRAzabAwCQSNrXFOz2OL+WSqUiBxKpGACw6LOZbwSoqREY2ipNIhGrVKreUf+a52NpaQUAkEgldHorliMRCgUMBrPpFSaTKZf/M1aNQDDuQOL3pD1K24iFhRUAYOmS1Y6Ozk2vW1kZLGqWllYMBmNT7PdNL5JJZAAAi8mSyaRarbaFMwZYLJZM9q/qQSqTWlpat/7/wIZ2La2zsyuVSiWRSGGhr0thTY2QQCA0sxaQh4e3XC63s3Owt3NArpSWlViYWyL1vEwme/Y8x98vEABQVPQqfvs3ixcuf+N304ivT4BcLs/Pz/Pw8EKu5ORkurt5GuEfNQrt0dc2wmFzpk2de/iXvRkZqUql8ubf15atWLDjhy3NRIns0q1Ll25xcRsqKytEorqk08fnzZ+CtH0iI7s7Ojrv2/fD7Ts3Hic/2L7jW6FQ4OLiRqPRrK1tUlIePU1NVqv/md3VpUs3B3vHrfGxuc+ya2qEBw7uysnJHDN6Eir/ehvQrkstAGD8uKleXr4JiYdTUh6xWOygwE7LYtY2H2Xzpu3nzv++IXZVdnaGs7Nr/35DRo4Yi+yutvW7XZu3rF27bhkA4IMPPty0MR7Zcm3ihBmHDu958PDOsYR/Xk/JZHLsxvg9e7fPXzCVRqN5eHhv2hjf9EWrnWPciSH7VuePXOxGo7frugETGmTaMz+9mhX7vl2kzYA/dGjBpYUWXFpowaWFFlxaaMGlhRZcWmjBpYUWXFpowaWFFlxaaMGlhRZcWmgxrrQ2zgwiaNejTLCCQADWzsbdN8q40mpUmprK9rLeZruipqJBozbuskvGldbVl1VfozJqFh2U+hqVqx+zBQHfHeNKy//EPP2WUFiGF9x/UV2syLpXGx5lbtRcjL6yqk4LDm14FR5laWlP41lTjZpX+0dUrRSWN6TeFE5Z7UYwchMWpaWuH1yuyUsTszjkqmLjrl7XDMh/iuHgYWsXuqxe7d2JEznAiAuqNoLq1sRqFdBpMVuyEdlpYMaMGVgZQCASyCguM4vqiEYyBQDs3oU6hQUCACg0U3kZg3BDcRwEE+qNSk1NTU1NxdoK9GjvQ8zbkEePHgEAQkNhWMygJZiQtKGhoSblfXBfCy24r4UWE6qQcV8LLbivxYEE3NdCiwlVyLivhRbc1+JAAu5rocWEKmTc10IL7mtxIAH3tdBiQhUy7muhBfe1OJCA+1poMSFps7Ozs7OzsbYCPUzI13p5eZmU98F9LbSYUIVsar7WhCpk/L0WWjp37mxS3gf3tdBiQr726dOnKSkpWFuBHiZUIT9+/BiplrE2BCVMSFrc1+JAAu5rocWEKmTc10IL7mtxIAH3tdBiQhUy7muhxdR8LfzSjh49+uXLlwTCP60KAoHg6uqalJSEtWnGBX5fO3bsWDqdTiAQiP+HRqNNnDgRa7uMDvzSDhs2zMXFpekVFxeXUaNGYWcRSsAvLYVCGT16NI1GQ05pNFp0dDTWRqEB/NICAEaMGOHo6Igcu7i44NLCA5FIHDduHI1Go1KpY8aMwdoclDCh3ihE1BMnTmBtCEq8XdqCTFna7TqJSC0SKNGyyijodDpMFzFvG3iWFI45pVNPM7eAt+xK8Zb32vTbosJcuX+kmaU9nUI3idq7naNSaARlDWm3RZJadVB3bjMhmyu1D6/U1FWpuw2zMY6ROO/FndOVVg7UiE8MbjtisCAKy5XCChWua7ulxwjbqpKGmkqD2ygZlLYsX06h4jVwu4ZCJZYXyA3dNSieVKS2cTHupm8474mNC0NSqzZ016C0MolGrTKV96IOilqllUs0hu7iVS604NJCCy4ttODSQgsuLbTg0kILLi204NJCCy4ttODSQgsuLbTg0kJLW0r78uWLFSsXfdyv69GEQ1+tXbp8xcJ3Tio/P693FD8jIxUA8J5JmSxtOTHk6p8X0zOefr3uOw8Pb1tbe43a4PemVtHro4/bKimToi2llcmkjo7O3br1BADY2dm3VbJ9o/q3VVImRZtJO3/htJycTABA7yj+rJkLcnIzlQ0N323ZCQAYOqz3hAnTpVLJkaMHWSxWl4huCxfEWFhYAgDu3799/cYfaekpEonY3y9o8qRZoaHhb6T81dqlSFI//rQ1KSmx6S1bW7vEhAsAAIGgetfu+KzsdLlcHhnZfcqkWc7Orm+1Oen08QcPbufkZFJptLBQ/syZC+ztHAAAv/9+LCHx8Ib1cd9t3VBU9MrDw2tM9KR+/QYDAET1ol9+2fvgwR1RfZ2vT8DHHw8c0H/ops1r6mpr4r77CUl26vRoqVRy6sQV5HT91ytUatWmjfFqtfrn/TsfPLxTXV0ZHBw2YtiYrl17AABe5D2bM3fi5k3bt8bHBgV2Wr9uS5so0ma+dtfOw4MHjfD09L7xV/LECdOb3qLSaAkJh2g0+rmzNw4fPJWe8fTX334GAMhksthvVqvV6q/Xxx06cNLR0Xn1mi/q6moNZTFi2Jj4bXuQv29iv2cymYEBIQAAtVq9JObTjMzUmKVrDh88yeXyFiycVlZe2rzBqalPftwZFxwctmfPkW82ba+qrvxm8xrkFoVKFYvrf9wZt2LZuuvXHn/Yo0/cto3V1VUAgK1bNz5NTf7iiy8P7j/h5xe4LX5Tdk5meOcuGZmpGo0GAFBTIywrK2lQKErLSpDU0tJTwjtHAgC+37456XTiqJHjjyVc6Plhn3VfL791+zoAgEqhAgD2H/xp7JjJUybPbgs1AEotZAKB4OsbMGniDA6bY2VlHR4eiZRvJpO5/+fEzz9b6e8XaGtrN2f2YplMlpmZZigdJyeXsFA+8vfH1QtWVjbLYtYiz664uHDVyg0R/K4WFpYL5y/lcHlvlO//EhwcenD/8Qnjpzk6OPn6+I8ZPSkzM00ikSCzDVQq1YL5SwMCggkEwiefDNJoNM+f5yB5ffLxoAh+V1tbuzmzF+388ZClhVXnsC4NDQ3PX+QiAfz8An18/DMzUgEAr17l19XV8sMjFQrF1T8vThg/beiQUTwub9DA4X169zty5AAAgEQiAQC6d/todPREDw+vtnrsKM2v9fHxbzxmszlSqQQ5lkml+/fvTEtPEQoFyJU6kcFS20hSUuKTlIe7d/1Gp9MBABkZqRQKpXNYBHKXQCCEdgrPyHjafCIkEqm0tPinXduyczLk8teDx+rqathsNnLs5xfYaDAAQCIRIz+I4yd+q68XRXbpHhTUyc83AAnj7OyamZnq7xeYkZnq7xfEYDAys9L69Ruclp5iY2Pr4uKWmvpErVZH8D9oNCAslH/lj/NSqfT1I/L2B20KStLqHbRfUVH+2RezIvgfrFn9TUBAsFar7T+w+1uTyn2WvXvv9m82bXdydEauSCRilUrVO4rfNJilpVXz6dy6fX3d+uVTJs/6dO7nnp7eDx/eXbX687favGL5+nPnTv11/Uri8V/ZLPbIkeMmT5pFJpPDQvnp6U9HR09MS3syfdqnNBp9509bAQCpqclhoREAAIlUDABY9NnMNxKsqREgGVH/P5ewrcByVvz1G3+oVKoVy9cjha+x4DZDvbh+zdqlEyfMiOB3bbxoaWnFYDA2xX7fNCSZ9JZ/7eLF0yEhYdOnfYqcSv5fkTQPl8OdNHHGxAnTMzPTbt2+/utv+7kc3qhR4zt37rItfpNIVJefn9c5rAuJRCouLhSJ6p6kPFq8aDkAwMLCCgCwdMlqx///IhGsrGyEwuqWZN1asJRWJKrjcLiIrgCAv2/91Xx4nU4XG/ull5fv1Cn/amt4eHjL5XI7OwekfQsAKC0rsTC3bD61+nqRg4NT4+mdOzdaYvBf1/8YNHA4jUYLDg4NDg59/iLn2YscAEBYWIREIv7j6gVPT28mkwkA8PbyvXT5rFhczw+PRGpsKpVKIpHCQl/XLjU1QgKBwGAw3prvu4FlR6OXp49QKLh46YxarX7w8G5GxlMul1dVVWEo/G9HDmRkpg4aMDw17cnT1GTkTy6XR3bp1qVLt7i4DZWVFSJRXdLp4/PmT7l85VzzuXt6+jxJeZSWlqJWq0+cPEImkwEAlYZzBwAQSaRDh3av37AiKyu9trbm6tWLL17kBgV2Qkqzj7ffuXOnkFMAQFBw6IULST7efmZm5gAADpszbercw7/szchIVSqVN/++tmzFgh0/tM17jl6wLLV9+w4oLCo4dHjP1m2xXbp0W7Fs3bHEX347ckAsrh8yWM+CBJcun1EoFGvWxTS9eODnRA8Pr82btp87//uG2FXZ2RnOzq79+w0ZOWJs87nPnrVQLpd9+dXncrl8dPTE5cvWlZYWxyybv27tt4aicNic2I3xP/4Ut3DxDACAh4fXwgUxA/oPRe6GhvKPn/htenAYchoYEJKUlDhm9KTG6OPHTfXy8k1IPJyS8ojFYgcFdkJa+EbC4HSu6yeqeNZ0n87NzQXDwZZnySJJjbLXaGu9d/EvP9AC87pRx0/8hvQJ/Bd3D68ftu9H3SJUgVna4cPGDBw4XO8tIgH+6gpmaWk0Gq2t+wE6EPD/eE0WXFpowaWFFlxaaMGlhRZcWmjBpYUWXFpoMSgtjUEiUzr4eoawQ6YSqUyDChq8weSQhGUNRrMKpw0QlipYHJKhuwaltXGiaVRao1mF0wZo1DprJ4PLthmU1tGLQSCAzHt1RjMM573IuF1LIgMHD4PSvmU95L+OVVPpJN8IHsNwwcdBGZlYk/uwTqPR9hmj/yM8wtuXun5yrTbtdh2NSQIdfFk/rU4HACB28LWutVqgatCEfmTWuY/B5XIRWrpAvbReo5AaXA6wQ3Dy5ElkRyesDXkvGGwSs2U1aEu/17K4JBa3Y9fJRLoUAGBpT8XaEJTAuyygBZcWWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlpwaaEFlxZacGmhBZcWWnBpoQWXFlpgXlzoDVgsllZrQrOYTEjaxvXCTQS8QoYWXFpowaWFFlxaaMGlhRZcWmjBpYUWXFpowaWFFlxaaMGlhRZcWmjBpYUWXFpowaWFFlxaaGnpam8dlxEjRhQVFWm1WiKRSCAQdDqdVqt1dXU9c+YM1qYZF/hL7bBhw0gkEolEQvbtJhAIVCp12LBhWNtldOCXNjo62snJqekVV1fXjr5SY0uAX1o2mz1gwADC/xdUJRAIgwcPZrPZWNtldOCXFgAwbtw4R0dH5NjJySk6Ohpri9DAJKTlcDiDBg0iEAhIkWUymVhbhAbwt5ARJBLJ5MmTtVrt0aNHTaE2bo/SimvVhTmyqmKFRKSRitQkClEuUbdJyhKJBHG9bZIak0tWK7UsLpnNI9k40139mRzz9jWoux1Jm/q3KPOeSCHXmtmxiRQymUai0EgkCgm0Gwv/BYGgUWpUSo26QaNRqUXlEjqLGPQBL/QjHtaWvaZdSJt+W3T3gsDe25zOY9DZHXWVcYVYKRfJK1/Wdh9sFdwDe4ExllarAaf3lKs1JGsPCyKpY2/4gKBVa6vzayhk7Yh59thuSI+ltMJy5bG4Ip/uzlRm+/JS70+DVPXiXsnEla7mNhSsbMBMWolI/fvOMtfOjpjkjg5FKWWjFtqzeNj8cLGpMiR16sStxXDrCgBw6exw9LtimRibTXSwkTbhuyL3CKcWBOzweHRxPLqlEJOsMaiQryVWNWiYLAsGyvlihUQoY1LlfcbYoJwv2qW2qqihLL/BdHQFALAtmcXPFdWlaO8Yi7a0t88KLFwtUM4UcyzdLG6fEaCcKarSVhY1KJVEtoXBfTmxpV4siFkTmZ51o81TZlsyFHJCdYmyzVNuBlSlzc+UkmgdtbPpPaEwqPlZEjRzRFfaDCnH2oS8bFNYloyXaaiuk4Le27RcrCFRiAwuzUjpi+qrz13eXlicoVTK/Xy69f1oho21KwDg9v3E67d+nTr+2xOnN1UJXtnbevXsPiEibBAS62n61St/7VUoJAG+PT7sNs5ItgEAmDx6PYmokGnpTJSKE3qlVirWyCXGennXaNR7Di0oKEwbPWx1zKJEJoP3476ZwppSAACZRJXJ689cjB878qu4DQ+CA3qdPLOpTlQFACivzEs4tZYfNnDFZyc7d+p/5mK8kcxDkEk0UlHbfKBsCShKK1KTacbaATf/1dNqQeH46PW+3pFcjuWwgV8wmbw7D04AAAhEokajGjrwc1fnYAKBEB46UKvVlJTlAgDuPfzdjGf3ca+ZTCbX2zMiMnyokcxDoNBIsnoYpVVINXS2sWrjgsJUEoni7cFHTgkEgqd754LC1MYALo6ByAGDzgEAyBViAICgptjO1qMxjLNjgJHMe501j6qQobfcHHq+lkQhqOQqIyUuV0g0GlXMmsimF7kcq8Zjgr4t4mWyehsr18ZTKtW4TbwGiYpERm9YFnrSsrhktdJYvpbDsaRSGTMmbmt6kUR6S/3PZHJV6n86iRoajNuCVSs1aG7Kjqa0JJXCWJ7GwdZbqZRbmNtbmDsgVwTCEg7HsvlY5mb2Oc/uInNGAAA5z+8ayTwEpULD5KL3wNHztVxLik4HtBqjfI3w8/nAz/uD46dja+sqJNK6Ow9O/LB3+uOU883H6hTYVywRnr+yQ6fT5eU/uf8oyRi2IWjUWiIRoDk0DtWvxI6ejPoqqZm9UcaKzpgUf/9x0pETXxUWZ1hbufLDBvfoOqb5KL7ekYM+Wfjg8enb9xPNeHYTotfvOvCpTmeUlk59lczJC9XuGlQ/6r14Knl8XewQgPbnrfZAaVZV1084niHoDYFGtaPRK5StUaoB9kMo0UanA1qV2jMY1aHtqFbIBAIIjGTnZdXYeOn/ridXSDZt0z89kkHnyhX1em/Z23otmLW3De1ct7mfRmugxafTAX3vUTZWbovnHjCUYFWeMPgDDkB3xCYGoyz2rc736OJMpuqpMLRabZ2oQm8slaqBQtHf40EiUXhc6za0sKa2zNAtpaqBqs8MIpFsxtPvaNQNmlfJpbNi3dvQwpaAgbQFWdInNyVWHlYtCAsD1S8FXaI4rv5ozyHDYNibeyDLxZsiLKxFP2v0ERTUuPtR0dcVsxGNXT6xsLEjVOVDrm5VXq2dE5Hf1xyT3DGbuvDhcEtrG52goAYrA4xNdb7Q1hH0GPqWHjHjgfGcn+Rrta+eKbn2ZlQGPHNDlDK1uKLOPYDauTc25RUB+5l6Rbnya4mVTDOGjaclkdyxZ3RpVLrqfKGiXtF3nI2TD8ZDhbCXFiHrQX3mfbGyQcc0Z/FsWcb7aG8MVA2a+kqprEZKYxCCPuAERHKxtgi0I2kRSl7I89IkZfkKkUBJZZCpDBKNTVUr0RuZ0HIoFJJColIqNEq5mmtFdfSge3ViO3m3o0F97UvaRlQNOmm9WlqvVip0una5ER6BQKAxiEwumcUlU2jt0Y+0U2lx3h+TWFzINMGlhRZcWmjBpYUWXFpowaWFlv8BG6bERQ1gTeoAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Display the graph structure\n",
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(app.get_graph().draw_mermaid_png()))\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "VPfOW4la_7kr"
+ },
+ "source": [
+ "## Try Your Research Agent\n",
+ "Now you can interact with your AI research agent! Ask complex questions that require gathering information from multiple sources."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {
+ "id": "kt2DypBEIJFD"
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Your question (or 'quit' to exit): how did humans come into existence?\n",
+ "Researching... Please wait.\n",
+ "Generated search queries\n",
+ "Conducting web research...\n",
+ "Conducting web research...\n",
+ "Reflection result: is_sufficient=False knowledge_gap=\"While the summaries mention the 'complex origins' of *Homo sapiens* involving a mixture of two ancient populations (Population A and B) and the challenge to the 'Out of Africa' theory by an 'Out of East Asia' model, the detailed genetic evidence supporting these claims and the specific mechanisms of interbreeding or genetic contributions from these ancestral groups are not fully elaborated. The computational methodology used (cobraa) is mentioned, but its specific application and the detailed findings it revealed about genetic contributions are high-level. More specific genetic and genomic details are needed to understand the nuances of these complex origin theories and how they relate to or challenge the traditional Out of Africa model, especially concerning alternative regional origins like 'Out of East Asia' and the roles of specific ancestral populations (A and B).\" follow_up_queries=[\"What specific genetic markers, genomic regions, or detailed findings from recent studies provide evidence for the proposed 'Population A' and 'Population B' ancestral groups in *Homo sapiens*, and how do these findings specifically challenge or refine the 'Out of Africa' model, particularly in relation to an 'Out of East Asia' hypothesis?\"]\n",
+ "Need more research: While the summaries mention the 'complex origins' of *Homo sapiens* involving a mixture of two ancie...\n",
+ "Conducting web research...\n",
+ "Reflection result: is_sufficient=False knowledge_gap=\"While the provided summaries introduce a new genetic model (Population A and B) for human origins, they explicitly state that this model cannot definitively assign specific Homo erectus or Homo heidelbergensis fossils to Population A or B. This indicates a knowledge gap in the methodologies or approaches for integrating these genetic findings with the existing paleontological fossil record to establish a concrete link between genetically inferred ancestral populations and known hominin species from the fossil record. Additionally, further details on the 'cobraa' computational algorithm's capabilities and limitations in this integration would be beneficial to fully address the question of how humans came into existence, by connecting the genetic and fossil evidence streams more thoroughly.\" follow_up_queries=[\"What are the current research methodologies or emerging approaches being developed to integrate the genetic insights from models like the 'Population A and B' framework with paleontological evidence, specifically to link these inferred ancestral genetic populations to known hominin fossil species such as Homo erectus or Homo heidelbergensis?\"]\n",
+ "Need more research: While the provided summaries introduce a new genetic model (Population A and B) for human origins, t...\n",
+ "Finalizing answer...\n",
+ "Synthesizing final answer...\n",
+ "--- Final Answer ---\n",
+ "The question of how humans came into existence is a complex and evolving field of scientific inquiry, with recent discoveries continually refining our understanding. Current research, particularly from 2024 and 2025, points to a more intricate evolutionary history than previously thought, involving multiple ancestral populations, interbreeding, and widespread migrations.\n",
+ "\n",
+ "Here's a summary of the leading theories and recent evidence:\n",
+ "\n",
+ "**1. A Complex Ancestral Origin: The \"Population A\" and \"Population B\" Model**\n",
+ "A significant development in March 2025, a study published in *Nature Genetics* by the University of Cambridge, challenges the traditional view of modern humans evolving from a single, continuous ancestral lineage. This research suggests that *Homo sapiens* are the result of a mixture of two ancient populations, termed \"Population A\" and \"Population B,\" that diverged approximately 1.5 million years ago and reconnected around 300,000 years ago.\n",
+ "* **Population A** contributed about 80% of the genetic material to modern humans. This group experienced a genetic bottleneck and is also considered ancestral to Neanderthals and Denisovans.\n",
+ "* **Population B** contributed the remaining 20% of modern human DNA. Genes from Population B are notably concentrated in regions of the genome associated with brain function and neural processing, suggesting a crucial role in the development of human cognition.\n",
+ "This model, developed using a computational algorithm called \"cobraa\" to analyze modern human DNA, indicates a more complex, interwoven origin for our species.\n",
+ "\n",
+ "**2. The \"Out of Africa\" Theory and its Challenges**\n",
+ "The prevailing \"Out of Africa\" theory posits that modern humans originated in Africa between 200,000 and 300,000 years ago and then migrated to other parts of the world, replacing earlier hominin populations.\n",
+ "* **Supporting Evidence:** Genetic studies overwhelmingly trace modern human ancestry back to a single African population. Archaeological discoveries in Africa continue to provide the earliest evidence of *Homo sapiens*. Research suggests that *Homo sapiens* successfully migrated out of Africa around 50,000 years ago due to their ability to adapt to a wider range of environments, expanding their geographic range within Africa around 70,000 years ago.\n",
+ "* **Recent Challenges:** The \"Population A\" and \"Population B\" model directly challenges the simplicity of a single ancestral lineage. Furthermore, archaeological discoveries in West and Central Africa are pushing back the dates of early human presence in these regions, suggesting a broader African origin than previously thought. Some scientists argue that human evolution involved multiple populations evolving in parallel across Africa, Europe, and Asia, rather than a simple replacement.\n",
+ "\n",
+ "**3. The \"Out of East Asia\" Hypothesis**\n",
+ "An alternative \"Out of East Asia\" model proposes that East Asia may have been the origin of modern humans.\n",
+ "* **Arguments:** Proponents suggest that East Asian populations exhibit the least genetic diversity, which could indicate an older ancestral root. This hypothesis is supported by a new molecular evolution theory called the maximum genetic diversity (MGD) theory, which places the roots of uniparental DNAs in East Asia.\n",
+ "* **Challenges:** Despite these arguments, the \"Out of East Asia\" theory faces significant challenges from the vast body of genetic evidence that points to an African origin for modern humans. The majority of early *Homo sapiens* fossils are also found in Africa. Genetic studies of human diversity in East Asia generally confirm an African origin, with subsequent gene flow into East Asia from the south.\n",
+ "\n",
+ "**4. Key Fossil Discoveries and Hominin Diversity**\n",
+ "Recent fossil discoveries continue to paint a picture of diverse hominin species coexisting and interacting across different regions:\n",
+ "* **East Asia:** Significant fossils from the Hualongdong site in China, dating back 300,000 years, show a mix of primitive and modern human characteristics, potentially representing a distinct hominin population linked to *Homo sapiens* origins. A new hominin species, *Homo juluensis*, proposed based on East Asian fossils (300,000 to 50,000 years ago), may be linked to Denisovans, highlighting the rich diversity of ancient human relatives in Asia.\n",
+ "* **Europe:** A partial upper jaw and cheekbone fossil, dating back 1.4 to 1.1 million years from Sima del Elefante in Spain, represents the oldest known human ancestor fossil in Western Europe, suggesting a previously unknown European *Homo* population. Cut marks on animal bones in Romania suggest hominins reached Eurasia nearly 2 million years ago.\n",
+ "* **Southeast Asia:** A large molar found in Cobra Cave, Laos, provides the first definitive evidence of Denisovans in Southeast Asia, expanding their known geographic range.\n",
+ "* **Coexistence:** Fossil footprints in Kenya provide direct evidence that *Homo erectus* and *Paranthropus boisei* coexisted around 1.5 million years ago, offering insights into inter-species interactions. New findings also indicate that *Paranthropus robustus* walked upright.\n",
+ "\n",
+ "**5. Genetic Insights into Brain Evolution and Interbreeding**\n",
+ "* **Brain Development:** Research suggests that the evolution of larger brains in modern humans, Neanderthals, and related species occurred gradually within each species. Studies on Human Accelerated Regions (HARs)—genetic switches that regulate gene expression—show that these regions fine-tune the expression of genes shared by humans and chimpanzees, influencing neuron development and communication. This indicates that brain function evolved by modifying existing genetic pathways.\n",
+ "* **Interbreeding:** Studies continue to reveal extensive interactions between Neanderthals, Denisovans, and modern humans, including evidence of interbreeding and the exchange of DNA. Analysis of a Neanderthal skeleton nicknamed \"Thorin\" suggests this individual was part of a genetically isolated population that may have overlapped with modern human presence in Europe as recently as 42,000 years ago. Denisovan ancestry is particularly significant in East Asian populations, suggesting multiple admixture events.\n",
+ "\n",
+ "**6. Other Evolutionary Adaptations**\n",
+ "* **Dietary Shifts:** Evidence suggests that adaptations to starch-rich diets, indicated by multiple copies of the AMY1 gene, may have evolved even before the divergence of modern humans, Neanderthals, and Denisovans.\n",
+ "* **Behavioral Parallels:** Studies documenting the use of medicinal plants by chimpanzees and gorillas suggest that our closest living relatives share similar practices for treating ailments, offering insights into the deep roots of such behaviors.\n",
+ "\n",
+ "In conclusion, the existence of humans is understood as a complex evolutionary journey spanning millions of years. Recent genetic models propose a multi-lineage origin for *Homo sapiens*, challenging simpler narratives. While the \"Out of Africa\" theory remains dominant, new evidence suggests a more nuanced picture involving extensive interbreeding, diverse hominin populations across continents, and continuous adaptation to various environments. The field of human origins is dynamic, with ongoing research and discoveries constantly refining our understanding of our deep past.\n",
+ " Your question (or 'quit' to exit): quit\n",
+ "Session Ended\n"
+ ]
+ }
+ ],
+ "source": [
+ "while True:\n",
+ " user_input = input(\" Your question (or 'quit' to exit): \")\n",
+ " if user_input.lower() in [\"quit\", \"exit\"]:\n",
+ " print(\"Session Ended\")\n",
+ " break\n",
+ "\n",
+ " initial_state = {\"messages\": [HumanMessage(content=user_input)]}\n",
+ " config = {\"recursion_limit\": 100}\n",
+ " print(\"Researching... Please wait.\")\n",
+ "\n",
+ " # Process the research workflow\n",
+ " final_state = None\n",
+ " for event in app.stream(initial_state, config=config):\n",
+ " for node, output in event.items():\n",
+ " if node == \"generate_query\":\n",
+ " print(f\"Generated search queries\")\n",
+ " elif node == \"web_research\":\n",
+ " print(f\"Conducting web research...\")\n",
+ " elif node == \"reflection\":\n",
+ " if 'is_sufficient' in output:\n",
+ " if output['is_sufficient']:\n",
+ " print(\"Gathered sufficient information\")\n",
+ " else:\n",
+ " print(f\"Need more research: {output.get('knowledge_gap', '')[:100]}...\")\n",
+ " elif node == \"finalize_answer\":\n",
+ " final_state = output\n",
+ " print(\"Synthesizing final answer...\")\n",
+ "\n",
+ " # Display the final answer\n",
+ " if final_state and 'messages' in final_state and final_state['messages']:\n",
+ " final_answer = final_state['messages'][-1]\n",
+ " if isinstance(final_answer, AIMessage):\n",
+ " print(\"--- Final Answer ---\")\n",
+ " print(final_answer.content)\n",
+ " else:\n",
+ " print(\"Could not retrieve a final answer.\")\n",
+ " else:\n",
+ " print(\"Something went wrong, and no final answer was produced.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "irDEEedoKzj9"
+ },
+ "source": [
+ "## Understanding the Research Process\n",
+ "\n",
+ "### How the Agent Works\n",
+ "\n",
+ "1. **Query Generation**: The agent analyzes your question and generates 1-3 focused search queries\n",
+ "2. **Parallel Research**: Each query is researched simultaneously using Google Search via Gemini\n",
+ "3. **Reflection**: The agent evaluates if the gathered information sufficiently answers your question\n",
+ "4. **Iterative Improvement**: If gaps are identified, the agent generates follow-up queries (up to 2 iterations)\n",
+ "5. **Answer Synthesis**: All research is combined into a comprehensive, well-structured answer\n",
+ "\n",
+ "### Key Features\n",
+ "\n",
+ "- **Autonomous Research**: The agent independently decides what to search for and when it has enough information\n",
+ "- **Quality Control**: Built-in reflection ensures thorough coverage of your topic\n",
+ "- **Current Information**: Searches are optimized for recent, up-to-date information\n",
+ "- **Parallel Processing**: Multiple searches run simultaneously for faster results\n",
+ "- **Structured Output**: Answers are well-organized and based solely on found information\n",
+ "\n",
+ "## Customization Options\n",
+ "\n",
+ "You can customize the agent's behavior by modifying the configuration:\n",
+ "\n",
+ "```python\n",
+ "# Example: More thorough research with more queries and iterations\n",
+ "thorough_config = types.SimpleNamespace(\n",
+ " query_generator_model=\"gemini-2.0-flash-exp\",\n",
+ " reflection_model=\"gemini-2.0-flash-exp\",\n",
+ " answer_model=\"gemini-2.0-flash-exp\",\n",
+ " number_of_initial_queries=5, # More initial queries\n",
+ " max_research_loops=3, # More follow-up iterations\n",
+ ")\n",
+ "\n",
+ "# Example: Faster research with fewer queries\n",
+ "fast_config = types.SimpleNamespace(\n",
+ " query_generator_model=\"gemini-2.0-flash-exp\",\n",
+ " reflection_model=\"gemini-2.0-flash-exp\",\n",
+ " answer_model=\"gemini-2.0-flash-exp\",\n",
+ " number_of_initial_queries=1, # Single query\n",
+ " max_research_loops=1, # One follow-up if needed\n",
+ ")\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "udWvXoV_K62H"
+ },
+ "source": [
+ "## Advanced Usage\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "kMm44gz6K-Jo"
+ },
+ "source": [
+ "### Tracking Research Sources\n",
+ "To track which sources were used for each piece of information:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {
+ "id": "LcQkj5r8K5VD"
+ },
+ "outputs": [],
+ "source": [
+ "def web_research_with_sources(state: WebSearchState) -> OverallState:\n",
+ " \"\"\"Enhanced web research that extracts and tracks sources\"\"\"\n",
+ " formatted_prompt = web_searcher_instructions.format(\n",
+ " current_date=get_current_date(),\n",
+ " research_topic=state[\"search_query\"]\n",
+ " )\n",
+ "\n",
+ " response = genai_client.models.generate_content(\n",
+ " model=configurable.query_generator_model,\n",
+ " contents=formatted_prompt,\n",
+ " config={\"tools\": [{\"google_search\": {}}], \"temperature\": 0},\n",
+ " )\n",
+ "\n",
+ " # Extract sources from the response\n",
+ " sources = []\n",
+ " if hasattr(response, 'candidates') and response.candidates:\n",
+ " for candidate in response.candidates:\n",
+ " if hasattr(candidate, 'grounding_metadata'):\n",
+ " sources.extend(candidate.grounding_metadata.get('sources', []))\n",
+ "\n",
+ " return {\n",
+ " \"search_query\": [state[\"search_query\"]],\n",
+ " \"web_research_result\": [response.text],\n",
+ " \"sources_gathered\": sources\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "wS_bJkzcLHZ4"
+ },
+ "source": [
+ "### Custom Research Strategies\n",
+ "You can implement different research strategies by modifying the prompts:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {
+ "id": "fwDGENVVLI0c"
+ },
+ "outputs": [],
+ "source": [
+ "# Academic research strategy\n",
+ "academic_query_instructions = \"\"\"Generate search queries for academic research on \"{research_topic}\".\n",
+ "Focus on:\n",
+ "- Peer-reviewed studies and papers\n",
+ "- Recent research findings (within last 3 years)\n",
+ "- Key researchers and institutions\n",
+ "- Methodologies and experimental results\n",
+ "Current date: {current_date}\n",
+ "\"\"\"\n",
+ "\n",
+ "# Market research strategy\n",
+ "market_query_instructions = \"\"\"Generate search queries for market analysis on \"{research_topic}\".\n",
+ "Focus on:\n",
+ "- Market size and growth trends\n",
+ "- Key players and competitors\n",
+ "- Consumer behavior and preferences\n",
+ "- Industry forecasts and analyst reports\n",
+ "Current date: {current_date}\n",
+ "\"\"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "EhYxseZ8LQtT"
+ },
+ "source": [
+ "## Next Steps\n",
+ "\n",
+ "This notebook demonstrated how to build an autonomous AI research agent using LangGraph and Gemini. Here are some ways to extend this system:\n",
+ "\n",
+ "### Enhancements You Can Try\n",
+ "\n",
+ "1. **Add Citation Tracking**: Modify the web research node to extract and format citations\n",
+ "2. **Implement Fact Checking**: Add a verification node that cross-references information\n",
+ "3. **Support Multi-Modal Research**: Extend to analyze images and videos in search results\n",
+ "4. **Add Domain Expertise**: Create specialized agents for different fields (medical, legal, technical)\n",
+ "5. **Build a Research UI**: Create a web interface for better interaction\n",
+ "\n",
+ "### Related Resources\n",
+ "\n",
+ "- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/): Learn more about building complex AI workflows\n",
+ "- [Gemini API Documentation](https://ai.google.dev/docs): Explore Gemini's full capabilities\n",
+ "- [LangChain Google Integration](https://python.langchain.com/docs/integrations/platforms/google): Additional Google AI integrations\n",
+ "\n",
+ "### Example Notebooks to Explore\n",
+ "\n",
+ "- **[Gemini Function Calling](../../quickstarts/Function_calling.ipynb)**: Learn how to give Gemini access to external tools and APIs\n",
+ "- **[Gemini LangChain Summarization](../langchain/Gemini_LangChain_Summarization_WebLoad.ipynb)**: Build agents that summarize content on the go"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "name": "Deep_Research.ipynb",
+ "toc_visible": true
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "name": "python3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/examples/langgraph/README.md b/examples/langgraph/README.md
new file mode 100644
index 000000000..779b36793
--- /dev/null
+++ b/examples/langgraph/README.md
@@ -0,0 +1,11 @@
+# LangGraph Examples with Gemini API
+
+Build advanced AI agents and workflows using [LangGraph](https://langchain-ai.github.io/langgraph/) with Google's Gemini API. LangGraph enables stateful, multi-actor applications with cycles and complex orchestration.
+
+## Examples
+
+* [Deep Research Agent](./Deep_Research.ipynb): Build an autonomous AI research agent that conducts web searches, evaluates information completeness, and synthesizes comprehensive answers through iterative refinement.
+
+## More Resources
+
+Find additional examples in the [examples](https://github.com/google-gemini/cookbook/tree/main/examples) and [quickstarts](https://github.com/google-gemini/cookbook/tree/main/quickstarts) folders.
\ No newline at end of file