gemini/agents/genai-experience-concierge/langgraph-demo/backend/notebooks/langgraph-remote-agent.ipynb (464 lines of code) (raw):

{ "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/vertex-ai/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/vertex-ai/workbench/deploy-notebook?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 src=\"https://www.gstatic.com/images/branding/gcpiconscolors/vertexai/v1/32px.svg\" alt=\"Vertex AI logo\"><br> Open in Vertex AI 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://www.svgrepo.com/download/217753/github.svg\" alt=\"GitHub logo\"><br> View on GitHub\n", " </a>\n", " </td>\n", "</table>\n", "\n", "<div style=\"clear: both;\"></div>\n", "\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>" ] }, { "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", " elif \"response\" in chunk:\n", " text = chunk[\"response\"]\n", " return \"response\", text\n", "\n", " elif \"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", " elif \"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", " elif \"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", " elif \"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", " elif \"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", " elif \"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", " elif \"error\" in chunk:\n", " text = chunk[\"error\"]\n", "\n", " return \"error\", text\n", "\n", " else:\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 }