{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "yWN6WGFthSVV"
      },
      "outputs": [],
      "source": [
        "# Copyright 2026 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": "foZ4s_XLamQc"
      },
      "source": [
        "# Getting Started with Bidirectional Streaming v2 on Agent Runtime\n",
        "\n",
        "<table align=\"left\">\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/agents/agent_engine/tutorial_bidi_stream_v2.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%2Fagents%2Fagent_engine%2Ftutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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/agents/agent_engine/tutorial_bidi_stream_v2.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": "qklO_oQQaeTt"
      },
      "source": [
        "| Author(s) |\n",
        "| --- |\n",
        "| Eric Gribkoff, Max Gong |"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "UmIYy1inhsYL"
      },
      "source": [
        "## Overview\n",
        "\n",
        "This tutorial demonstrates how to build, deploy, and interact with **bidirectional streaming agents** using **Agent Runtime** with Bring-Your-Own-Dockerfile Agent deployments including with the **Live API**.\n",
        "\n",
        "In this tutorial, you will:\n",
        "\n",
        "* **Build** two different types of streaming agents\n",
        "* **Interact** with agents\n",
        "* **Implement** real-time audio conversations"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "B9p8Qt9dhcl-"
      },
      "source": [
        "## Setup"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GD2deH5OHIUL"
      },
      "source": [
        "### Define your project\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "v3NyoLyzHMMe"
      },
      "outputs": [],
      "source": [
        "PROJECT_ID = \"YOUR_PROJECT_ID\"  # @param {type: \"string\"}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "G0DTFsw_21K0"
      },
      "source": [
        "### Installing the Python SDK\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "hD0sLtNRq-Of"
      },
      "outputs": [],
      "source": [
        "%pip install --upgrade google-cloud-aiplatform[agent_engines,adk]>=1.144"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mmR4EuDDWZEK"
      },
      "source": [
        "### Configure Authentication"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "D_d6aBmC0ZCa"
      },
      "outputs": [],
      "source": [
        "from google.colab import auth\n",
        "\n",
        "auth.authenticate_user()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "FOPNlRJmWceZ"
      },
      "source": [
        "### Configure the Agent Platform client"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "yU11NBYfYgRZ"
      },
      "outputs": [],
      "source": [
        "import vertexai\n",
        "\n",
        "LOCATION = \"us-central1\"  # @param {type: \"string\"}\n",
        "ENDPOINT = f\"https://{LOCATION}-aiplatform.googleapis.com\"\n",
        "\n",
        "client = vertexai.Client(\n",
        "    project=PROJECT_ID,\n",
        "    location=LOCATION,\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "rys_yOlaa5kE"
      },
      "source": [
        "# Echo Agent\n",
        "\n",
        "The bidirectional streaming API is compatible with arbitrary WebSocket protocols on the deployed agent. For simplicity, we start with a basic \"echo\" agent that accepts a bidirectional stream and echoes the messages back to the client. Later, we show how to use the bidirectional streaming API with a Live ADK Agent for real-time audio streaming."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BmjeiFu3ayek"
      },
      "source": [
        "## Setting up the source files\n",
        "\n",
        "For the Echo Agent, we will deploy using Agent Runtime's bring-your-own-Docker image deployment.\n",
        "\n",
        "We define the following files and subdirectories in the current working directory:\n",
        "\n",
        "* `echo_agent/`: the AI logic for the Echo Agent. It contains the following files:\n",
        "  * `__init__.py`\n",
        "  * `agent.py`\n",
        "* `main.py`: the API server to run\n",
        "* `requirements.txt`: the PyPI dependencies to pick up\n",
        "* `Dockerfile`: the commands to assemble the image\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "y1IRh4H1Xtkd"
      },
      "outputs": [],
      "source": [
        "!mkdir -p echo_agent"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "qPX89vNy2-qj"
      },
      "source": [
        "### Requirements"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "0S4ALeE8XtnO"
      },
      "outputs": [],
      "source": [
        "%%writefile requirements.txt\n",
        "google-cloud-aiplatform[agent_engines,adk]\n",
        "fastapi\n",
        "uvicorn\n",
        "pydantic"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "sD9hT5h53BGy"
      },
      "source": [
        "### Dockerfile"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "khcDm69ds7Cp"
      },
      "outputs": [],
      "source": [
        "%%writefile Dockerfile\n",
        "FROM python:3.13-alpine\n",
        "WORKDIR /app\n",
        "\n",
        "RUN adduser --disabled-password --gecos \"\" myuser\n",
        "\n",
        "COPY requirements.txt .\n",
        "RUN pip install --no-cache-dir -r requirements.txt\n",
        "\n",
        "\n",
        "COPY --chown=myuser:myuser . .\n",
        "\n",
        "ENV PATH=\"/home/myuser/.local/bin:$PATH\"\n",
        "\n",
        "USER myuser\n",
        "\n",
        "CMD [\"sh\", \"-c\", \"uvicorn main:app --host 0.0.0.0 --port $PORT\"]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "x-2yh9TU3FBe"
      },
      "source": [
        "### Echo Agent"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "IlZWHaxAaJSA"
      },
      "outputs": [],
      "source": [
        "%%writefile echo_agent/__init__.py\n",
        "from . import agent"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "HNhidBhKaJUL"
      },
      "outputs": [],
      "source": [
        "%%writefile echo_agent/agent.py\n",
        "import asyncio\n",
        "import logging\n",
        "\n",
        "from typing import Any, Dict, AsyncIterator\n",
        "\n",
        "class EchoAgent:\n",
        "    \"\"\"\n",
        "    A simple echo agent demonstrating bidirectional WebSocket capabilities.\n",
        "    \"\"\"\n",
        "    def __init__(self):\n",
        "        self.logger = logging.getLogger(__name__)\n",
        "        self.logger.info(\"Echo Agent ready!\")\n",
        "\n",
        "    async def bidi_stream_query(\n",
        "        self,\n",
        "        queue: asyncio.Queue\n",
        "    ) -> AsyncIterator[Dict[str, Any]]:\n",
        "        \"\"\"\n",
        "        Bidirectional streaming for continuous conversation.\n",
        "        \"\"\"\n",
        "        self.logger.info(\"Bidi session started\")\n",
        "\n",
        "        while True:\n",
        "            # Wait for message from the queue\n",
        "            message = await queue.get()\n",
        "            user_input = message.get(\"message\", \"\")\n",
        "\n",
        "            # Check for exit command\n",
        "            if user_input.lower() in (\"exit\", \"quit\"):\n",
        "                yield {\"output\": \"Goodbye!\"}\n",
        "                break\n",
        "\n",
        "            # Echo back the input\n",
        "            # In a real agent, this is where you'd process the input\n",
        "            yield {\"output\": f\"Echo: {user_input}\"}\n",
        "\n",
        "# Create an instance of our echo agent\n",
        "root_agent = EchoAgent()\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_TrWNpCc3C7p"
      },
      "source": [
        "### API Server\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "myinCFFLXtsL"
      },
      "outputs": [],
      "source": [
        "%%writefile main.py\n",
        "import os\n",
        "import asyncio\n",
        "import logging\n",
        "import uvicorn\n",
        "from fastapi import FastAPI, WebSocket\n",
        "\n",
        "from echo_agent.agent import root_agent\n",
        "\n",
        "logging.basicConfig(level=logging.INFO)\n",
        "logger = logging.getLogger(\"bidi_server\")\n",
        "app = FastAPI()\n",
        "\n",
        "@app.websocket(\"/bidi_echo\")\n",
        "async def bidi_handler(websocket: WebSocket):\n",
        "    await websocket.accept()\n",
        "\n",
        "    initial_data = await websocket.receive_json(mode=\"binary\")\n",
        "    logger.info(f\"Inbound initial handshake message: {initial_data}\")\n",
        "\n",
        "    queue = asyncio.Queue()\n",
        "    # Seed the queue if the initial frame contains a starting payload\n",
        "    if \"input\" in initial_data:\n",
        "        queue.put_nowait(initial_data[\"input\"])\n",
        "\n",
        "    async def receive_messages():\n",
        "        while True:\n",
        "            try:\n",
        "                data = await websocket.receive_json(mode=\"binary\")\n",
        "                logger.info(f\"Inbound bidi message: {data}\")\n",
        "\n",
        "                queue.put_nowait(data.get(\"input\", {}))\n",
        "            except Exception as e:\n",
        "                logger.info(f\"WebSocket connection closed or receive error: {e}\")\n",
        "                break\n",
        "\n",
        "    async def send_messages():\n",
        "        try:\n",
        "            async for response in root_agent.bidi_stream_query(queue):\n",
        "                await websocket.send_json({\"output\": response}, mode=\"binary\")\n",
        "        except Exception as e:\n",
        "            logger.error(f\"Error sending bidi response: {e}\")\n",
        "    # Run WebSocket communication loops\n",
        "    await asyncio.gather(receive_messages(), send_messages())\n",
        "\n",
        "# You can add more FastAPI routes or configurations below if needed\n",
        "# Example:\n",
        "# @app.get(\"/hello\")\n",
        "# async def read_root():\n",
        "#     return {\"Hello\": \"World\"}\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    # Use the PORT environment variable provided by Cloud Run, defaulting to 8080\n",
        "    uvicorn.run(app, host=\"0.0.0.0\", port=int(os.environ.get(\"PORT\", 8080)))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BVuzYhGy60-b"
      },
      "source": [
        "## Deploy\n",
        "\n",
        "\n",
        "We make the API call by following https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/deploy-an-agent#from-dockerfile"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "sY1lWbjJajb3"
      },
      "outputs": [],
      "source": [
        "remote_echo_agent = client.agent_engines.create(\n",
        "    config={\n",
        "        \"display_name\": \"Bidi Echo test agent\",\n",
        "        \"description\": \"bidi stream testing with echo agent\",\n",
        "        \"source_packages\": [\n",
        "            # The files in the current directory to upload. You can also use \".\"\n",
        "            \"echo_agent\",\n",
        "            \"Dockerfile\",\n",
        "            \"main.py\",\n",
        "            \"requirements.txt\",\n",
        "        ],\n",
        "        \"image_spec\": {},  # tells AgentRuntime to use the Dockerfile\n",
        "        \"agent_framework\": \"google-adk\",  # For usage through the console / UI\n",
        "        \"env_vars\": {\"GOOGLE_GENAI_USE_VERTEXAI\": \"1\"},\n",
        "        \"resource_limits\": {\"cpu\": \"2\", \"memory\": \"8Gi\"},\n",
        "        # Explicitly set max_instances to stay within quota\n",
        "        \"max_instances\": 5,\n",
        "        # You can also adjust min_instances if needed, default is 1\n",
        "        # \"min_instances\": 1,\n",
        "        # # Cloud Storage bucket for staging\n",
        "        # \"staging_bucket\": BUCKET_URI,\n",
        "    },\n",
        ")\n",
        "remote_echo_agent"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2GaSC4aWYsgn"
      },
      "source": [
        "## Query\n",
        "\n",
        "With a custom WebSocket server running in the deployed Agent, we will connect to the Agent Runtime API using a standard WebSocket client library rather than using the Agent Platform SDK. An example of this is shown below, sending a stream of messages to the agent before disconnecting."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "DA2qx-HeZsDW"
      },
      "outputs": [],
      "source": [
        "# Put the echo agent's reasoning engine id from the last step here\n",
        "ECHO_ENGINE_ID = \"1266316887558455296\"  # @param {type: \"string\"}"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "QBU6EQbvYt7x"
      },
      "outputs": [],
      "source": [
        "import asyncio\n",
        "import json\n",
        "\n",
        "import google.auth\n",
        "import websockets\n",
        "from google.auth.transport.requests import Request\n",
        "\n",
        "creds, _ = google.auth.default()\n",
        "if not creds.valid:\n",
        "    creds.refresh(Request())\n",
        "\n",
        "AGENT_PATH = \"bidi_echo\"  # This should match the API server's handler path\n",
        "url = f\"wss://{ENDPOINT.removeprefix('https://')}/reasoningEngines/ws/internal/projects/{PROJECT_ID}/locations/{LOCATION}/reasoningEngines/{ECHO_ENGINE_ID}/api/{AGENT_PATH}\"\n",
        "print(url)\n",
        "\n",
        "\n",
        "async def test_websocket():\n",
        "    headers = {\n",
        "        \"Authorization\": f\"Bearer {creds.token}\",\n",
        "        \"Content-Type\": \"application/octet-stream\",\n",
        "    }\n",
        "\n",
        "    messages = [\"first request\", \"second request\", \"almost done!\", \"exit\"]\n",
        "\n",
        "    try:\n",
        "        async with websockets.connect(url, additional_headers=headers) as websocket:\n",
        "            print(f\"Connected to {url}\\n\")\n",
        "\n",
        "            for msg in messages:\n",
        "                request_dict = {\"input\": {\"message\": msg}}\n",
        "                print(f\"Sending: {request_dict}\")\n",
        "                await websocket.send(json.dumps(request_dict))\n",
        "\n",
        "                # Listen for response\n",
        "                try:\n",
        "                    response = await asyncio.wait_for(websocket.recv(), timeout=10.0)\n",
        "                    print(f\"Received: {response}\\n\")\n",
        "                except asyncio.TimeoutError:\n",
        "                    print(\"No message received within the timeout period.\\n\")\n",
        "\n",
        "    except Exception as e:\n",
        "        print(f\"An error occurred: {e}\")\n",
        "        raise e\n",
        "\n",
        "\n",
        "await test_websocket()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "H6x4TBsqbGr_"
      },
      "source": [
        "# Live API Agent\n",
        "\n",
        "We now show how to use the bidirectional streaming API with a Live ADK Agent for real-time audio streaming."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3G5hcLXmbeLP"
      },
      "source": [
        "## Setting up the source files\n",
        "\n",
        "Once again, we will deploy using Agent Runtime's bring-your-own-Docker image deployment.\n",
        "\n",
        "We define the following files and subdirectories in the current working directory.\n",
        "\n",
        "* `capital_agent/`: the AI logic for the Capital Agent. It contains the following files:\n",
        "  * `__init__.py`\n",
        "  * `agent.py`\n",
        "* `main.py`: the API server to run\n",
        "* `requirements.txt`: the PyPI dependencies to pick up\n",
        "* `Dockerfile`: the commands to assemble the image"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "XtS21V96b8p9"
      },
      "outputs": [],
      "source": [
        "!mkdir -p capital_agent"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "XglhKKHWb3Je"
      },
      "source": [
        "### Requirements"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "2Xv0hndmb4wp"
      },
      "outputs": [],
      "source": [
        "%%writefile requirements.txt\n",
        "google-cloud-aiplatform[agent_engines,adk]\n",
        "fastapi\n",
        "uvicorn\n",
        "pydantic"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DQiC3imncLqN"
      },
      "source": [
        "### Dockerfile"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "tvAC9Ma9cMxi"
      },
      "outputs": [],
      "source": [
        "%%writefile Dockerfile\n",
        "FROM python:3.13-alpine\n",
        "WORKDIR /app\n",
        "\n",
        "RUN adduser --disabled-password --gecos \"\" myuser\n",
        "\n",
        "COPY requirements.txt .\n",
        "RUN pip install --no-cache-dir -r requirements.txt\n",
        "\n",
        "\n",
        "COPY --chown=myuser:myuser . .\n",
        "\n",
        "ENV PATH=\"/home/myuser/.local/bin:$PATH\"\n",
        "\n",
        "USER myuser\n",
        "\n",
        "CMD [\"sh\", \"-c\", \"uvicorn main:app --host 0.0.0.0 --port $PORT\"]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "CkEBQ9x3cQgx"
      },
      "source": [
        "### Capital Agent"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-kDoOWKNcRd0"
      },
      "outputs": [],
      "source": [
        "%%writefile capital_agent/__init__.py\n",
        "from . import agent"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "fpyXBf0GcVtn"
      },
      "outputs": [],
      "source": [
        "%%writefile capital_agent/agent.py\n",
        "from google.adk.agents import Agent\n",
        "\n",
        "# Define a tool function\n",
        "def get_capital_city(country: str) -> str:\n",
        "  \"\"\"Retrieves the capital city for a given country.\"\"\"\n",
        "  # Replace with actual logic (e.g., API call, database lookup)\n",
        "  capitals = {\"france\": \"Paris\", \"japan\": \"Tokyo\", \"canada\": \"Ottawa\"}\n",
        "  return capitals.get(country.lower(), f\"Sorry, I don't know the capital of {country}.\")\n",
        "\n",
        "root_agent = Agent(\n",
        "    model=\"gemini-live-2.5-flash-native-audio\",  # Specify the Live API model\n",
        "    name=\"capital_agent\",\n",
        "    description=\"Answers user questions about the capital city of a given country using Live API.\",\n",
        "    instruction=\"\"\"You are an agent that provides the capital city of a country.\n",
        "             When a user asks for the capital of a country:\n",
        "             1. Identify the country name from the user's query.\n",
        "             2. Use the `get_capital_city` tool to find the capital.\n",
        "             3. Respond clearly to the user, stating the capital city.\n",
        "             Example Query: \"What's the capital of France?\"\n",
        "             Example Response: \"The capital of France is Paris.\"\n",
        "        \"\"\",\n",
        "    tools=[get_capital_city]\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "13yehF0QcfZw"
      },
      "source": [
        "### API Server"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "KB60oo6LchQU"
      },
      "outputs": [],
      "source": [
        "%%writefile main.py\n",
        "import os\n",
        "\n",
        "import uvicorn\n",
        "from fastapi import FastAPI\n",
        "from google.adk.cli.fast_api import get_fast_api_app\n",
        "\n",
        "# Get the directory where main.py is located\n",
        "AGENT_DIR = os.path.dirname(os.path.abspath(__file__))\n",
        "# Example session service URI (e.g., SQLite)\n",
        "SESSION_SERVICE_URI = \"\"\n",
        "# Example allowed origins for CORS\n",
        "ALLOWED_ORIGINS = [\"http://localhost\", \"http://localhost:8080\", \"*\"]\n",
        "# Set web=True if you intend to serve a web interface, False otherwise\n",
        "SERVE_WEB_INTERFACE = True\n",
        "\n",
        "# Call the function to get the FastAPI app instance\n",
        "# Ensure the agent directory name ('capital_agent') matches your agent folder\n",
        "app: FastAPI = get_fast_api_app(\n",
        "    agents_dir=AGENT_DIR,\n",
        "    session_service_uri=SESSION_SERVICE_URI,\n",
        "    allow_origins=ALLOWED_ORIGINS,\n",
        "    web=SERVE_WEB_INTERFACE,\n",
        ")\n",
        "\n",
        "# You can add more FastAPI routes or configurations below if needed\n",
        "# Example:\n",
        "# @app.get(\"/hello\")\n",
        "# async def read_root():\n",
        "#     return {\"Hello\": \"World\"}\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    # Use the PORT environment variable provided by Cloud Run, defaulting to 8080\n",
        "    uvicorn.run(app, host=\"0.0.0.0\", port=int(os.environ.get(\"PORT\", 8080)))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EZV8BuKZcq0h"
      },
      "source": [
        "## Deploy\n",
        "\n",
        "We make the API call by following https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/deploy-an-agent#from-dockerfile"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "OllI3G8Vcs2-"
      },
      "outputs": [],
      "source": [
        "remote_live_agent = client.agent_engines.create(\n",
        "    config={\n",
        "        \"display_name\": \"Bidi Live API test agent\",\n",
        "        \"description\": \"bidi stream testing with live agent\",\n",
        "        \"source_packages\": [\n",
        "            # The files in the current directory to upload.\n",
        "            \"capital_agent\",\n",
        "            \"Dockerfile\",\n",
        "            \"main.py\",\n",
        "            \"requirements.txt\",\n",
        "        ],\n",
        "        \"image_spec\": {},  # tells AgentRuntime to use the Dockerfile\n",
        "        \"agent_framework\": \"google-adk\",  # For usage through the console / UI\n",
        "        \"env_vars\": {\"GOOGLE_GENAI_USE_VERTEXAI\": \"1\"},\n",
        "        \"resource_limits\": {\"cpu\": \"2\", \"memory\": \"8Gi\"},\n",
        "        # Explicitly set max_instances to stay within quota\n",
        "        \"max_instances\": 5,\n",
        "        # You can also adjust min_instances if needed, default is 1\n",
        "        # \"min_instances\": 1,\n",
        "    },\n",
        ")\n",
        "remote_live_agent"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eM9qYzCOdTSm"
      },
      "source": [
        "## Connect to the Agent"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Ay9Siu_MetgV"
      },
      "outputs": [],
      "source": [
        "# Put the live agent's reasoning engine id from the last step here\n",
        "LIVE_ENGINE_ID = \"7678296516062085120\"  # @param {type: \"string\"}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aqefAU0of1h1"
      },
      "source": [
        "### Create Session\n",
        "\n",
        "For the Live API Agent, you first need to create a session. For our example agent, the session service is running in-memory on the agent itself. We can connect via HTTP to the built-in `/apps/capital_agent/users/user-123/sessions` endpoint to create the session."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "uWN48DHYfVvs"
      },
      "outputs": [],
      "source": [
        "APP_NAME = \"capital_agent\"\n",
        "USER_ID = \"demo_user\"\n",
        "\n",
        "# Put the session id you want to create\n",
        "SESSION_ID = \"demo_session\"  # @param {type: \"string\"}"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "WaILod34qXoN"
      },
      "outputs": [],
      "source": [
        "import google.auth\n",
        "import requests\n",
        "from google.auth.transport.requests import Request\n",
        "\n",
        "creds, _ = google.auth.default()\n",
        "if not creds.valid:\n",
        "    creds.refresh(Request())\n",
        "\n",
        "base_ingress_url = f\"{ENDPOINT}/reasoningEngines/internal/projects/{PROJECT_ID}/locations/{LOCATION}/reasoningEngines/{LIVE_ENGINE_ID}/api\"\n",
        "create_session_url = (\n",
        "    f\"{base_ingress_url}/apps/{APP_NAME}/users/{USER_ID}/sessions/{SESSION_ID}\"\n",
        ")\n",
        "\n",
        "headers = {\"Authorization\": f\"Bearer {creds.token}\", \"Content-Type\": \"application/json\"}\n",
        "\n",
        "response = requests.post(create_session_url, headers=headers)\n",
        "response.json()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "c9S5YE3jtoRZ"
      },
      "source": [
        "### List Sessions"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ikfkZ3NHtp44"
      },
      "outputs": [],
      "source": [
        "import google.auth\n",
        "import requests\n",
        "from google.auth.transport.requests import Request\n",
        "\n",
        "creds, _ = google.auth.default()\n",
        "if not creds.valid:\n",
        "    creds.refresh(Request())\n",
        "\n",
        "base_ingress_url = f\"{ENDPOINT}/reasoningEngines/internal/projects/{PROJECT_ID}/locations/{LOCATION}/reasoningEngines/{LIVE_ENGINE_ID}/api\"\n",
        "list_sessions_url = f\"{base_ingress_url}/apps/{APP_NAME}/users/{USER_ID}/sessions/\"\n",
        "\n",
        "headers = {\"Authorization\": f\"Bearer {creds.token}\", \"Content-Type\": \"application/json\"}\n",
        "\n",
        "response = requests.get(list_sessions_url, headers=headers)\n",
        "response.json()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yXL77455gE6m"
      },
      "source": [
        "### Query\n",
        "\n",
        "This example sends a request to the agent and receives an audio response over the bidirectional WebSocket connection."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "QdEHaxCkdvxA"
      },
      "outputs": [],
      "source": [
        "LIVE_QUERY_TEXT = \"What is the capital of France?\"  # @param {type: \"string\"}"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "sMH2TL5Ddr5l"
      },
      "outputs": [],
      "source": [
        "import base64\n",
        "import time\n",
        "import wave\n",
        "\n",
        "import google.auth\n",
        "from IPython.display import Audio, display\n",
        "from google.auth.transport.requests import Request\n",
        "\n",
        "creds, _ = google.auth.default()\n",
        "if not creds.valid:\n",
        "    creds.refresh(Request())\n",
        "\n",
        "# Parameters for the audio\n",
        "sample_rate = 44100  # samples per second\n",
        "\n",
        "url = f\"wss://{ENDPOINT.removeprefix('https://')}/reasoningEngines/ws/internal/projects/{PROJECT_ID}/locations/{LOCATION}/reasoningEngines/{LIVE_ENGINE_ID}/api/run_live?app_name={APP_NAME}&user_id={USER_ID}&session_id={SESSION_ID}\"\n",
        "\n",
        "\n",
        "def save_wav_file(filename, pcm_data, channels=1, sampwidth=2, framerate=24000):\n",
        "    \"\"\"Saves raw PCM data to a WAV file.\"\"\"\n",
        "    with wave.open(filename, \"wb\") as wf:\n",
        "        wf.setnchannels(channels)\n",
        "        wf.setsampwidth(sampwidth)  # 2 bytes for 16-bit audio\n",
        "        wf.setframerate(framerate)\n",
        "        wf.writeframes(pcm_data)\n",
        "    print(f\"Audio saved to {filename}\")\n",
        "\n",
        "\n",
        "async def test_websocket():\n",
        "    headers = {\n",
        "        \"Authorization\": f\"Bearer {creds.token}\",\n",
        "    }\n",
        "\n",
        "    try:\n",
        "        async with websockets.connect(url, additional_headers=headers) as websocket:\n",
        "            print(f\"Connected to {url}\")\n",
        "            test_message = {\n",
        "                \"content\": {\"role\": \"user\", \"parts\": [{\"text\": LIVE_QUERY_TEXT}]}\n",
        "            }\n",
        "            print(f\"Sending: {test_message}\")\n",
        "            await websocket.send(json.dumps(test_message))\n",
        "            print(f\"Sent: {test_message}\")\n",
        "\n",
        "            # Listen for responses\n",
        "            print(\"--- Receiving Responses ---\")\n",
        "            audio_buffer = b\"\"\n",
        "            text_response = \"\"\n",
        "            try:\n",
        "                while True:\n",
        "                    response = await asyncio.wait_for(websocket.recv(), timeout=10.0)\n",
        "                    print(f\"Received: {response}\")\n",
        "                    try:\n",
        "                        data = json.loads(response)\n",
        "\n",
        "                        if \"content\" in data and data[\"content\"][\"role\"] == \"model\":\n",
        "                            for part in data[\"content\"].get(\"parts\", []):\n",
        "                                if \"text\" in part:\n",
        "                                    text_response += part[\"text\"]\n",
        "                                    print(\n",
        "                                        f\"====== MODEL TEXT PART: {part['text']} ======\"\n",
        "                                    )\n",
        "                                if \"inlineData\" in part:\n",
        "                                    mime_type = part[\"inlineData\"].get(\"mimeType\")\n",
        "                                    if mime_type == \"audio/pcm\":\n",
        "                                        b64_data = part[\"inlineData\"].get(\"data\")\n",
        "                                        if b64_data:\n",
        "                                            try:\n",
        "                                                print(type(b64_data))\n",
        "                                                decoded_chunk = (\n",
        "                                                    base64.urlsafe_b64decode(b64_data)\n",
        "                                                )\n",
        "                                                audio_buffer += decoded_chunk\n",
        "                                                print(\n",
        "                                                    f\"Decoded and appended {len(decoded_chunk)} audio bytes.\"\n",
        "                                                )\n",
        "                                            except base64.binascii.Error as e:\n",
        "                                                print(b64_data)\n",
        "                                                print(\n",
        "                                                    f\"Base64 Decode Error: {e} - on chunk length {len(b64_data)}\"\n",
        "                                                )\n",
        "\n",
        "                        if data.get(\"turnComplete\"):\n",
        "                            print(\"Turn complete received.\")\n",
        "                            if text_response:\n",
        "                                print(\n",
        "                                    f\"====== FINAL TEXT RESPONSE: {text_response} ======\"\n",
        "                                )\n",
        "                            if audio_buffer:\n",
        "                                print(\"Saving audio buffer\")\n",
        "                                filename = f\"response_{int(time.time())}.wav\"\n",
        "                                save_wav_file(filename, audio_buffer)\n",
        "                                display(Audio(filename=filename, rate=sample_rate))\n",
        "                                audio_buffer = b\"\"\n",
        "                                break\n",
        "                    except json.JSONDecodeError:\n",
        "                        print(f\"Received non-JSON message: {response}\")\n",
        "            except asyncio.TimeoutError:\n",
        "                print(\"No message received within the timeout period.\")\n",
        "            except websockets.exceptions.ConnectionClosedOK:\n",
        "                print(\"Connection closed normally.\")\n",
        "            except websockets.exceptions.ConnectionClosedError as e:\n",
        "                print(f\"Connection closed with error: {e}\")\n",
        "\n",
        "    except Exception as e:\n",
        "        print(f\"An error occurred: {e}\")\n",
        "        raise e\n",
        "\n",
        "\n",
        "await test_websocket()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "MXJ1DSBg23g5"
      },
      "source": [
        "# Clean up"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "M3d6JGUa6Mp7"
      },
      "outputs": [],
      "source": [
        "remote_echo_agent.delete(force=True)\n",
        "remote_live_agent.delete(force=True)"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "name": "tutorial_bidi_stream_v2.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
