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
}