{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "f705f4be70e9"
      },
      "outputs": [],
      "source": [
        "# Copyright 2025 Google LLC\n",
        "#\n",
        "# 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": "53d90692a4e0"
      },
      "source": [
        "# Query a Remote LangGraph Agent Server"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b2de76eacf15"
      },
      "source": [
        "<table align=\"left\">\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\">\n",
        "      <img width=\"32px\" src=\"https://www.gstatic.com/pantheon/images/bigquery/welcome_page/colab-logo.svg\" alt=\"Google Colaboratory logo\"><br> Open in Colab\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://console.cloud.google.com/agent-platform/colab/import/https:%2F%2Fraw.githubusercontent.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fmain%2Fgemini%2Fagents%2Fgenai-experience-concierge%2Flanggraph-demo%2Fbackend%2Fnotebooks%2Flanggraph-remote-agent.ipynb\">\n",
        "      <img width=\"32px\" src=\"https://lh3.googleusercontent.com/JmcxdQi-qOpctIvWKgPtrzZdJJK-J3sWE1RsfjZNwshCFgE_9fULcNpuXYTilIR2hjwN\" alt=\"Google Cloud Colab Enterprise logo\"><br> Open in Colab Enterprise\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://console.cloud.google.com/agent-platform/workbench/instances?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/generative-ai/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\">\n",
        "      <img width=\"32px\" src=\"https://storage.googleapis.com/github-repo/workbench-icon.svg\" alt=\"Workbench logo\"><br> Open in Workbench\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\">\n",
        "      <img width=\"32px\" src=\"https://raw.githubusercontent.com/primer/octicons/refs/heads/main/icons/mark-github-24.svg\" alt=\"GitHub logo\"><br> View on GitHub\n",
        "    </a>\n",
        "  </td>\n",
        "</table>\n",
        "\n",
        "<div style=\"clear: both;\"></div>\n",
        "\n",
        "<p>\n",
        "<b>Share to:</b>\n",
        "\n",
        "<a href=\"https://www.linkedin.com/sharing/share-offsite/?url=https%3A//github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\" target=\"_blank\">\n",
        "  <img width=\"20px\" src=\"https://upload.wikimedia.org/wikipedia/commons/8/81/LinkedIn_icon.svg\" alt=\"LinkedIn logo\">\n",
        "</a>\n",
        "\n",
        "<a href=\"https://bsky.app/intent/compose?text=https%3A//github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\" target=\"_blank\">\n",
        "  <img width=\"20px\" src=\"https://upload.wikimedia.org/wikipedia/commons/7/7a/Bluesky_Logo.svg\" alt=\"Bluesky logo\">\n",
        "</a>\n",
        "\n",
        "<a href=\"https://twitter.com/intent/tweet?url=https%3A//github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\" target=\"_blank\">\n",
        "  <img width=\"20px\" src=\"https://upload.wikimedia.org/wikipedia/commons/5/5a/X_icon_2.svg\" alt=\"X logo\">\n",
        "</a>\n",
        "\n",
        "<a href=\"https://reddit.com/submit?url=https%3A//github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\" target=\"_blank\">\n",
        "  <img width=\"20px\" src=\"https://redditinc.com/hubfs/Reddit%20Inc/Brand/Reddit_Logo.png\" alt=\"Reddit logo\">\n",
        "</a>\n",
        "\n",
        "<a href=\"https://www.facebook.com/sharer/sharer.php?u=https%3A//github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb\" target=\"_blank\">\n",
        "  <img width=\"20px\" src=\"https://upload.wikimedia.org/wikipedia/commons/5/51/Facebook_f_logo_%282019%29.svg\" alt=\"Facebook logo\">\n",
        "</a>\n",
        "</p>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "dbfe0a3c85ab"
      },
      "source": [
        "| | |\n",
        "|-|-|\n",
        "|Author(s) | [Pablo Gaeta](https://github.com/pablofgaeta) |"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "213b97720d22"
      },
      "source": [
        "## Overview\n",
        "\n",
        "This notebook demonstrates example usage of a deployed agent using the standard LangGraph `RemoteGraph` client. The notebook can be configured to point to a locally running instance or a deployed server (e.g. on Cloud Run).\n",
        "\n",
        "Some test queries have been added for the basic `gemini` agent that will stream chunks of text. This interactive notebook might be useful for a frontend team developing a custom user interface for the agent.\n",
        "\n",
        "Feel free to add new cells and experiment with streaming outputs from the other agent implementations (Check out the [frontend code](../../frontend/concierge_ui/pages/) for inspiration)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "faca4bb1709f"
      },
      "source": [
        "## Import dependencies"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "id": "de31a1f8d685"
      },
      "outputs": [],
      "source": [
        "import json\n",
        "import uuid\n",
        "\n",
        "from IPython import display as ipd\n",
        "from langgraph.pregel import remote"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2aafb7e2361d"
      },
      "source": [
        "## Configure notebook parameters\n",
        "\n",
        "By default, the notebook points to a local server at port 3000. Parameters for a remote deployed endpoint might look like:\n",
        "\n",
        "```python\n",
        "import subprocess\n",
        "\n",
        "agent_name = \"...\"\n",
        "agent_url = f\"https://concierge-XXXXXXXXXX-uc.a.run.app/{agent_name}\"\n",
        "id_token = subprocess.run(\n",
        "    [\"gcloud\", \"auth\", \"print-identity-token\"], capture_output=True, text=True\n",
        ").stdout.strip()\n",
        "```"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "id": "8f40a8599f1a"
      },
      "outputs": [],
      "source": [
        "agent_name = \"task-planner\"\n",
        "agent_url = f\"http://127.0.0.1:3000/{agent_name}\"\n",
        "id_token = None\n",
        "\n",
        "# Configure remote agent pointing to local development server\n",
        "graph = remote.RemoteGraph(\n",
        "    agent_name,\n",
        "    url=agent_url,\n",
        "    headers={\"Authorization\": f\"Bearer {id_token}\"} if id_token else {},\n",
        ")\n",
        "\n",
        "test_thread = f\"test-{uuid.uuid4().hex}\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "c0a2c49430cd"
      },
      "source": [
        "## Query the Remote Agent"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "f40afe2a20a6"
      },
      "source": [
        "Display graph visualization"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "id": "fce9b1a33b18"
      },
      "outputs": [],
      "source": [
        "ipd.Image(graph.get_graph().draw_mermaid_png())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4893cf22ec7b"
      },
      "source": [
        "Utility function to handle chunks across all agents. Not very practical in practice but useful for demo purposes."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "id": "e792a5a155c9"
      },
      "outputs": [],
      "source": [
        "def handle_chunk(chunk: dict, task_idx: int = 0) -> tuple[str, str]:\n",
        "    if \"text\" in chunk:\n",
        "        text = chunk[\"text\"]\n",
        "        return \"text\", text\n",
        "\n",
        "    if \"response\" in chunk:\n",
        "        text = chunk[\"response\"]\n",
        "        return \"response\", text\n",
        "\n",
        "    if \"guardrail_classification\" in chunk:\n",
        "        is_blocked = chunk[\"guardrail_classification\"][\"blocked\"]\n",
        "        classification_emoji = \"❌\" if is_blocked else \"✅\"\n",
        "        reason = chunk[\"guardrail_classification\"][\"reason\"]\n",
        "\n",
        "        text = f\"Guardrail classification: {classification_emoji}\\n\\nReason: {reason}\"\n",
        "\n",
        "        return \"guardrail_classification\", text\n",
        "\n",
        "    if \"router_classification\" in chunk:\n",
        "        target = chunk[\"router_classification\"][\"target\"]\n",
        "        reason = chunk[\"router_classification\"][\"reason\"]\n",
        "\n",
        "        text = f\"Agent Classification: {target}\\n\\nReason: {reason}\"\n",
        "\n",
        "        return \"router_classification\", text\n",
        "\n",
        "    if \"function_call\" in chunk:\n",
        "        function_call_dict = chunk[\"function_call\"]\n",
        "\n",
        "        fn_name = function_call_dict.get(\"name\") or \"unknown\"\n",
        "        fn_args = function_call_dict.get(\"args\") or {}\n",
        "\n",
        "        fn_args_string = \", \".join(f\"{k}={v}\" for k, v in fn_args.items())\n",
        "        fn_string = f\"**{fn_name}**({fn_args_string})\"\n",
        "\n",
        "        text = f\"Calling function... {fn_string}\"\n",
        "\n",
        "        return \"fn_call\", text\n",
        "\n",
        "    if \"function_response\" in chunk:\n",
        "        function_response_dict = chunk[\"function_response\"]\n",
        "\n",
        "        fn_name = function_response_dict.get(\"name\") or \"unknown\"\n",
        "\n",
        "        if function_response_dict.get(\"response\") is None:\n",
        "            text = f\"Received empty function response (name={fn_name}).\"\n",
        "\n",
        "        elif \"result\" in function_response_dict.get(\"response\"):\n",
        "            fn_result = function_response_dict[\"response\"][\"result\"]\n",
        "            text = \"\\n\\n\".join(\n",
        "                [\n",
        "                    f\"Function result for **{fn_name}**...\",\n",
        "                    \"```json\",\n",
        "                    json.dumps(fn_result, indent=2),\n",
        "                    \"```\",\n",
        "                ]\n",
        "            )\n",
        "\n",
        "        elif \"error\" in function_response_dict.get(\"response\"):\n",
        "            fn_result = function_response_dict[\"response\"][\"error\"]\n",
        "            text = f\"Function error (name={fn_name})... {fn_result}\"\n",
        "\n",
        "        return \"fn_response\", text\n",
        "\n",
        "    if \"plan\" in chunk:\n",
        "        plan_dict = chunk[\"plan\"]\n",
        "        plan_string = _stringify_plan(plan=plan_dict, include_results=False)\n",
        "        text = f\"### Generated execution plan...\\n\\n{plan_string}\"\n",
        "\n",
        "        return \"plan\", text\n",
        "\n",
        "    if \"executed_task\" in chunk:\n",
        "        task_idx += 1\n",
        "        task_dict = chunk[\"executed_task\"]\n",
        "        task_string = _stringify_task(task=task_dict, include_results=True)\n",
        "        text = f\"### Executed task #{task_idx}...\\n\\n{task_string}\"\n",
        "\n",
        "        return f\"executed_task_{task_idx}\", text\n",
        "\n",
        "    if \"error\" in chunk:\n",
        "        text = chunk[\"error\"]\n",
        "\n",
        "        return \"error\", text\n",
        "\n",
        "    text = f\"Unhandled chunk. keys={set(chunk.keys())}\"\n",
        "\n",
        "    return \"unhandled\", text\n",
        "\n",
        "\n",
        "def _stringify_plan(plan: dict, include_results: bool = True) -> str:\n",
        "    \"\"\"Formats an execution plan dictionary into a human-readable string.\"\"\"\n",
        "    tasks_str = \"\\n\\n\".join(\n",
        "        f\"**Task #{idx + 1}**\\n\\n\"\n",
        "        + _stringify_task(task, include_results=include_results)\n",
        "        for idx, task in enumerate(plan[\"tasks\"])\n",
        "    )\n",
        "\n",
        "    response = f\"**Plan**: {plan['goal']}\\n\\n{tasks_str}\"\n",
        "\n",
        "    return response\n",
        "\n",
        "\n",
        "def _stringify_task(task: dict, include_results: bool = True) -> str:\n",
        "    \"\"\"Formats a task dictionary into a human-readable string.\"\"\"\n",
        "    output = f\"**Goal**: {task['goal']}\"\n",
        "\n",
        "    if include_results:\n",
        "        output += f\"\\n\\n**Result**: {task.get('result') or 'incomplete'}\"\n",
        "\n",
        "    return output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "891e9cc7fbdc"
      },
      "source": [
        "Run a query in stream mode **_without_** custom stream writer. Streams node updates, not text stream."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "id": "1d9948776a15"
      },
      "outputs": [],
      "source": [
        "for chunk in graph.stream(\n",
        "    input={\"current_turn\": {\"user_input\": \"hi\"}},\n",
        "    config={\"configurable\": {\"thread_id\": test_thread}},\n",
        "    stream_mode=\"updates\",\n",
        "):\n",
        "    print(chunk)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "54d4addac10f"
      },
      "source": [
        "Run a query in stream mode **_with_** the custom stream writer mode. Streams text generated by Gemini to stdout."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {
        "id": "8a2312f5aed4"
      },
      "outputs": [],
      "source": [
        "task_idx = 0\n",
        "response_text = \"\"\n",
        "current_source = last_source = None\n",
        "for stream_mode, chunk in graph.stream(\n",
        "    input={\"current_turn\": {\"user_input\": \"what products does Cymbal Retail sell?\"}},\n",
        "    config={\"configurable\": {\"thread_id\": test_thread}},\n",
        "    stream_mode=[\"updates\", \"custom\"],\n",
        "):\n",
        "    if stream_mode == \"custom\":\n",
        "        assert isinstance(chunk, dict), \"Expected dictionary data\"\n",
        "\n",
        "        current_source, text = handle_chunk(chunk, task_idx)\n",
        "\n",
        "        if \"executed_task\" in current_source:\n",
        "            task_idx += 1\n",
        "\n",
        "        if last_source is not None and last_source != current_source:\n",
        "            text = \"\\n\\n---\\n\\n\" + text\n",
        "\n",
        "        last_source = current_source\n",
        "\n",
        "        response_text += text\n",
        "\n",
        "        display(ipd.Markdown(response_text), clear=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6074e5469cef"
      },
      "source": [
        "Get a snapshot of the current session state"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {
        "id": "b95eab126376"
      },
      "outputs": [],
      "source": [
        "snapshot = graph.get_state(config={\"configurable\": {\"thread_id\": test_thread}})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eac61b522743"
      },
      "source": [
        "Get history of session state snapshots"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {
        "id": "3556813902f5"
      },
      "outputs": [],
      "source": [
        "snapshot_list = list(\n",
        "    graph.get_state_history(config={\"configurable\": {\"thread_id\": test_thread}})\n",
        ")"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "name": "langgraph-remote-agent.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
