Files
2026-07-13 13:30:30 +08:00

1354 lines
74 KiB
Plaintext

{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "lxVi4Ua-GRB1"
},
"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": "qH9uPDc7Cn6M"
},
"source": [
"# Managed Agents API - Analyzing the 2026 World Cup\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/managed-agents/managed_agents_world_cup_analysis.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%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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/managed-agents/managed_agents_world_cup_analysis.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/managed-agents/managed_agents_world_cup_analysis.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%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fblob%2Fmain%2Fagents%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fblob%2Fmain%2Fagents%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fblob%2Fmain%2Fagents%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fblob%2Fmain%2Fagents%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fgenerative-ai%2Fblob%2Fmain%2Fagents%2Fmanaged-agents%2Fmanaged_agents_world_cup_analysis.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>\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "R_IMs8cQGiLM"
},
"source": [
"| Authors |\n",
"| --- |\n",
"| [Eric Schmidt](https://github.com/cloude-google) |\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "g8VUEJWhGtOy"
},
"source": [
"### Overview\n",
"\n",
"This notebook is an \"end-to-end\" demo using the Managed Agents API on Gemini Enterprise Agent Platform to analyze World Cup data.\n",
"\n",
"For complete Manged Agents API documentation please visit: https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/managed-agents.\n",
"\n",
"**DISCLAIMER**\n",
"* This notebook uses preview APIs. They are not intended for production applications.\n",
"* It is highly recommended to run this sample in an isolated development or testing project.\n",
"\n",
"\n",
"**NOTICE**\n",
"* This sample uses then open source repository https://www.github.com/jfjelstul/worldcup authored by Joshua C. Fjelstul, Ph.D., with the database that is copyrighted (\"© 2023 Joshua C. Fjelstul, Ph.D.\") via the CC-BY-SA 4.0 license (https://creativecommons.org/licenses/by-sa/4.0/legalcode). No modifications were made to the database for this sample.\n",
"\n",
"\n",
"**Sample Concepts**\n",
"* Provides a wrapper over the create, update, delete, list REST API for managing agent configuations.\n",
"* Deploys an agent that contains:\n",
" * tools for file system and Google Search\n",
" * a source bound to Google Cloud Storage that provides skill support\n",
" * instructions to clone a public repo which is used for analysis\n",
"* Demonstrates how to re-connect to environments and interactions.\n",
"* Uses the interactions API to power a multi turn chat interface.\n",
"* Uses REST APIs (not the genai SDK) for learning purposes."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "4X_36mgWV3vt"
},
"source": [
"## Getting Started\n",
"\n",
"**Tasks to Complete:**\n",
"* First, start by running the code cell below to import required packages."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "nII5llSTHK1y"
},
"outputs": [],
"source": [
"# @title Import Required Packages\n",
"\n",
"# Standard library imports\n",
"import json\n",
"import logging\n",
"import os\n",
"import pprint\n",
"import sys\n",
"import textwrap\n",
"import time\n",
"import token\n",
"from datetime import datetime, timezone\n",
"from typing import Any, Dict, Optional\n",
"from urllib import response\n",
"\n",
"# Third-party imports\n",
"import google.auth\n",
"import ipywidgets as widgets\n",
"import markdown\n",
"import requests\n",
"from google.auth.transport.requests import Request\n",
"from google.cloud import storage\n",
"from IPython.display import clear_output, display\n",
"\n",
"# Initialize logger\n",
"logger = logging.getLogger(__name__)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "OWvuDkyQX42k"
},
"source": [
"## Scaffolding\n",
"\n",
"This section contains scaffolding aka wrapper code for CRUD operations for the Manages Agents API\n",
"as well as UI state management for the multi-turn chat when executing the Interactions API.\n",
"\n",
"* TokenManager provides management of minting OAuth tokens.\n",
"* AgentFactory is a wrapper over the control plane APIs.\n",
"* AgentHarness is a wrapper over the interations APIs.\n",
"* AgentConfig provides a class wrapper to house a configuration.\n",
"\n",
"**Tasks to complete:**\n",
"- Change the name of the PROJECT_ID to match the Google Cloud project where you want to run this demo.\n",
"- Enter the name of a Google Cloud Storage bucket where you want to store skill(s) for the demo. DO NOT PREFIX WITH gs://. In following steups you will use the noteboook to then upload a skill to this bucket.\n",
"- **Then run both code cells to configure the PROJECT ID and init the Core Code.**\n",
"\n",
"*Note: After running this code cell you can collapse/hide the cell as it will not be needed unless you want to change the config.*"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jAb5fzoQSnDd"
},
"outputs": [],
"source": [
"# @title Configure Project ID and GCS Bucket of Agent\n",
"PROJECT_ID = \"\" #@param {type:\"string\"}\n",
"SKILL_GCS_BUCKET = \"\" #@param {type:\"string\"}\n",
"ENDPOINT = \"https://aiplatform.googleapis.com\"\n",
"LOCATION = \"global\"\n",
"IS_COLAB = True\n",
"\n",
"if not PROJECT_ID:\n",
" raise ValueError(\"The 'PROJECT_ID' variable is not set.\")\n",
"\n",
"if not SKILL_GCS_BUCKET:\n",
" raise ValueError(\"The 'SKILL_GCS_BUCKET' variable is not set.\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "D1r0HUygVozo"
},
"outputs": [],
"source": [
"# @title Core Code for Multi-Turn Chat with Managed Agents\n",
"\n",
"if not PROJECT_ID:\n",
" raise ValueError(\"The 'PROJECT_ID' variable is not set. Repeat previous step.\")\n",
"\n",
"if not SKILL_GCS_BUCKET:\n",
" raise ValueError(\"The 'SKILL_GCS_BUCKET' variable is not set. Repeat previous step.\")\n",
"\n",
"class TokenManager:\n",
" \"\"\"Manages Google Cloud OAuth 2.0 access tokens and session states.\"\"\"\n",
"\n",
" def __init__(self, is_colab: bool = False):\n",
" self.token: Optional[str] = None\n",
" self.expiry: Optional[datetime] = None\n",
" self.email: Optional[str] = None\n",
" self.is_colab: bool = is_colab\n",
"\n",
" def get_token(self) -> str:\n",
" \"\"\"Retrieves the current access token, refreshing it if necessary.\n",
"\n",
" Returns:\n",
" str: The valid OAuth 2.0 access token.\n",
" \"\"\"\n",
" if self.token is None or self.is_expired():\n",
" self.refresh_token()\n",
"\n",
" return self.token\n",
"\n",
" def is_expired(self) -> bool:\n",
" \"\"\"Checks if the current token has expired.\n",
"\n",
" Returns:\n",
" bool: True if the token is expired or has no set expiry.\n",
" \"\"\"\n",
" if self.expiry is None:\n",
" # If we require an active token but have no expiry, force a refresh\n",
" return True\n",
"\n",
" # Compare current UTC time against the timezone-aware expiry datetime\n",
" return datetime.now(timezone.utc) >= self.expiry\n",
"\n",
" def _fetch_email_from_token(self) -> None:\n",
" \"\"\"Fetches the associated email for the current token via Google's Tokeninfo API.\"\"\"\n",
" if not self.token:\n",
" return\n",
"\n",
" url = f\"https://oauth2.googleapis.com/tokeninfo?access_token={self.token}\"\n",
" try:\n",
" response = requests.get(url, timeout=10)\n",
" if response.status_code == 200:\n",
" data = response.json()\n",
" # Email is only present if \"userinfo.email\" scope was requested\n",
" self.email = data.get(\"email\")\n",
" logger.info(\"Token belongs to: %s\", self.email)\n",
" else:\n",
" logger.warning(\"Invalid or expired token.\")\n",
" except requests.RequestException as e:\n",
" logger.error(\"Failed to reach tokeninfo API: %s\", e)\n",
"\n",
" def refresh_token(self) -> None:\n",
" \"\"\"Authenticates, refreshes the session, and stores a short-lived access token.\"\"\"\n",
" logger.info(\"****** GETTING TOKEN ******\")\n",
"\n",
" if self.is_colab:\n",
" try:\n",
" from google.colab import auth\n",
" auth.authenticate_user()\n",
" except ImportError:\n",
" logger.error(\"Colab environment not detected; google.colab is missing.\")\n",
" return\n",
"\n",
" # google.auth.default() handles both standard environments and Colab\n",
" # (as long as auth.authenticate_user() was called first in Colab)\n",
" scopes = [\"https://www.googleapis.com/auth/cloud-platform\"]\n",
" credentials, _project = google.auth.default(scopes=scopes)\n",
"\n",
" # Log the credential identity for debugging\n",
" email = getattr(credentials, \"service_account_email\", None)\n",
" if email:\n",
" logger.info(\"Authenticating as service account: %s\", email)\n",
" else:\n",
" logger.info(\"Authenticating with credential type: %s\", type(credentials).__name__)\n",
"\n",
" # Refresh the credentials to populate the token\n",
" credentials.refresh(Request())\n",
" self.token = credentials.token\n",
"\n",
" if credentials.expiry:\n",
" # google-auth returns a naive datetime object in UTC; make it timezone-aware\n",
" self.expiry = credentials.expiry.replace(tzinfo=timezone.utc)\n",
" logger.info(\"Token will expire at: %s\", self.expiry.strftime(\"%Y-%m-%d %H:%M:%S UTC\"))\n",
" else:\n",
" self.expiry = None\n",
" logger.info(\"Token does not have a set expiration time.\")\n",
"\n",
" # Mask the token in stdout to prevent leaking secrets in logs\n",
" masked_token = f\"{self.token[:20]}...\" if self.token else \"None\"\n",
" logger.info(\"Obtained access token (masked): %s\", masked_token)\n",
"\n",
" self._fetch_email_from_token()\n",
"\n",
"class AgentFactory:\n",
" \"\"\"Factory class to manage the lifecycle of Agents via REST API.\"\"\"\n",
"\n",
" def __init__(self, project_id: str, endpoint: str, location: str):\n",
" self.project_id = project_id\n",
" self.endpoint = endpoint\n",
" self.location = location\n",
" self.token_manager = TokenManager()\n",
" self.agents: Dict[str, Dict[str, Any]] = {}\n",
"\n",
" # Populate the initial list of agents\n",
" self.list()\n",
"\n",
" def _get_headers(self) -> Dict[str, str]:\n",
" \"\"\"Generates standard headers required for the API requests.\"\"\"\n",
" token = self.token_manager.get_token()\n",
" return {\n",
" \"Content-Type\": \"application/json; charset=utf-8\",\n",
" \"Authorization\": f\"Bearer {token}\"\n",
" }\n",
"\n",
" def _get_base_url(self) -> str:\n",
" \"\"\"Constructs the base URL for agent operations.\"\"\"\n",
" return f\"{self.endpoint}/v1beta1/projects/{self.project_id}/locations/{self.location}/agents\"\n",
"\n",
" def _poll_operation(self, operation: Dict[str, Any]) -> None:\n",
" \"\"\"Polls a long-running operation until it is marked as done.\n",
"\n",
" Args:\n",
" operation (Dict[str, Any]): The initial operation payload returned by the API.\n",
" \"\"\"\n",
" logger.info(\"****** POLLING OPERATION ******\")\n",
"\n",
" op_name = operation.get(\"name\")\n",
" if not op_name:\n",
" logger.error(\"Could not get operation name from response: %s\", operation)\n",
" return\n",
"\n",
" operation_status_url = f\"{self.endpoint}/v1beta1/{op_name}\"\n",
"\n",
" while not operation.get(\"done\"):\n",
" logger.info(\"Polling operation status...\")\n",
" time.sleep(5) # Wait 5 seconds before checking again\n",
"\n",
" try:\n",
" op_response = requests.get(\n",
" operation_status_url,\n",
" headers=self._get_headers(),\n",
" timeout=10\n",
" )\n",
" op_response.raise_for_status()\n",
" operation = op_response.json()\n",
" except requests.RequestException as e:\n",
" logger.error(\"Error occurred while polling: %s\", e)\n",
" break\n",
"\n",
" logger.info(\"Operation complete: %s\", operation)\n",
"\n",
" def create(self, agent_config: Dict[str, Any]) -> Optional[requests.Response]:\n",
" \"\"\"Creates a new agent and polls the operation until completion.\"\"\"\n",
" logger.info(\"****** CREATING AGENT ******\")\n",
"\n",
" try:\n",
" response = requests.post(\n",
" self._get_base_url(),\n",
" headers=self._get_headers(),\n",
" json=agent_config, # Using the 'json' kwarg automatically serializes the dict\n",
" timeout=10\n",
" )\n",
" response.raise_for_status()\n",
"\n",
" logger.info(\"Create request submitted successfully.\")\n",
"\n",
" # Poll the long-running operation\n",
" self._poll_operation(response.json())\n",
"\n",
" # Refresh the local cache of agents\n",
" self.list()\n",
" return response\n",
"\n",
" except requests.RequestException as e:\n",
" logger.error(\"Failed to create agent: %s\", e)\n",
" return None\n",
"\n",
" def list(self) -> Dict[str, Dict[str, Any]]:\n",
" \"\"\"Fetches a list of all agents and updates the local cache.\"\"\"\n",
" try:\n",
" response = requests.get(\n",
" self._get_base_url(),\n",
" headers=self._get_headers(),\n",
" params={\"page_size\": 10},\n",
" timeout=10\n",
" )\n",
" response.raise_for_status()\n",
"\n",
" parsed_data = response.json()\n",
" self.agents = {agent[\"id\"]: agent for agent in parsed_data.get(\"agents\", [])}\n",
" logger.info(\"Successfully fetched %d agents.\", len(self.agents))\n",
"\n",
" except requests.RequestException as e:\n",
" logger.error(\"Failed to list agents: %s\", e)\n",
"\n",
" return self.agents\n",
"\n",
" def get(self, agent_id: str) -> Optional[Dict[str, Any]]:\n",
" \"\"\"Retrieves details for a specific agent.\"\"\"\n",
" target_agent = self.agents.get(agent_id)\n",
" if not target_agent or \"name\" not in target_agent:\n",
" logger.warning(\"Agent ID %s not found in local cache. Call list() to refresh.\", agent_id)\n",
" return None\n",
"\n",
" try:\n",
" response = requests.get(\n",
" f\"{self.endpoint}/v1beta1/{target_agent['name']}\",\n",
" headers=self._get_headers(),\n",
" timeout=10\n",
" )\n",
" response.raise_for_status()\n",
" return response.json()\n",
"\n",
" except requests.RequestException as e:\n",
" logger.error(\"Failed to get agent %s: %s\", agent_id, e)\n",
" return None\n",
"\n",
" def delete(self, agent_id: str) -> Optional[requests.Response]:\n",
" \"\"\"Deletes a specific agent by ID.\"\"\"\n",
" target_agent = self.agents.get(agent_id)\n",
" if not target_agent or \"name\" not in target_agent:\n",
" logger.warning(\"Cannot delete: Agent ID %s not found in local cache.\", agent_id)\n",
" return None\n",
"\n",
" try:\n",
" response = requests.delete(\n",
" f\"{self.endpoint}/v1beta1/{target_agent['name']}\",\n",
" headers=self._get_headers(),\n",
" timeout=10\n",
" )\n",
" response.raise_for_status()\n",
"\n",
" parsed_data = response.json()\n",
" is_done = parsed_data.get(\"done\", False)\n",
"\n",
" if is_done:\n",
" logger.info(\"Delete succeeded for agent %s.\", agent_id)\n",
" # Remove from local cache after successful deletion\n",
" self.agents.pop(agent_id, None)\n",
" else:\n",
" logger.warning(\"Delete operation for agent %s was accepted but is not yet done.\", agent_id)\n",
"\n",
" return response\n",
"\n",
" except requests.RequestException as e:\n",
" logger.error(\"Failed to delete agent %s: %s\", agent_id, e)\n",
" return None\n",
"\n",
" def delete_all(self) -> None:\n",
" \"\"\"Iterates through the local cache and deletes all agents.\"\"\"\n",
" # Using list() forces a copy of keys so we don't modify the dict while iterating over it\n",
" for agent_id in list(self.agents.keys()):\n",
" self.delete(agent_id)\n",
"\n",
"class AgentConfig:\n",
" \"\"\"Configuration builder for the World Cup demo agent.\"\"\"\n",
"\n",
" def __init__(self, project_id: str, skill_gcs_bucket: str):\n",
" self.project_id = project_id\n",
" # Passed in explicitly rather than relying on a global SKILL_GCS_BUCKET\n",
" self.skill_gcs_bucket = skill_gcs_bucket\n",
"\n",
" def get_config(self) -> Dict[str, Any]:\n",
" \"\"\"Returns the configuration dictionary for the agent.\"\"\"\n",
"\n",
" # textwrap.dedent strips the leading indentation so your prompt string\n",
" # remains clean and doesn't contain unnecessary whitespace.\n",
" system_instruction = textwrap.dedent(\"\"\"\\\n",
" * You are a helpful assistant focused on analysis of World Cup statistics.\n",
" * Follow rules and guidance in the skills directory when answering questions about the 2026 World Cup.\n",
" * Use Google Search for questions about the current World Cup in 2026.\n",
" * Use the repository noted below for stats about the World Cup prior to 2026.\n",
"\n",
" IMPORTANT\n",
" * When you first start up clone this repository: https://github.com/jfjelstul/worldcup\n",
" * Save it to the local file system and use the repository to answer questions about World Cup history, teams, and players for historical questions up to 2022.\n",
" * Use search for questions about the current World Cup in 2026.\n",
" * Make note of repo initialization so you don't repeat this step in future questions.\n",
"\n",
" IMPORTANT\n",
" * When you first start up also ask the user for their name, city and favorite World Cup team.\n",
" * Save this information in a user preferences file called user_prefs.txt.\n",
" * Always check to see if user_prefs.txt exists before asking for the user's information.\n",
"\n",
" Rule: You must always fill out the explanation parameter and narrate your actions.\\\n",
" \"\"\")\n",
"\n",
" config = {\n",
" \"id\": \"world-cup-agent-demo-1\",\n",
" \"base_agent\": \"antigravity-preview-05-2026\",\n",
" \"description\": \"A demo agent showcasing Environment and GCS use case.\",\n",
" \"system_instruction\": system_instruction,\n",
" \"tools\": [\n",
" {\"type\": \"filesystem\"},\n",
" {\"type\": \"google_search\"},\n",
" ],\n",
" \"base_environment\": {\n",
" \"type\": \"remote\",\n",
" \"sources\": [\n",
" {\n",
" \"type\": \"gcs\",\n",
" \"source\": f\"gs://{self.skill_gcs_bucket}\",\n",
" \"target\": \"./skills\"\n",
" }\n",
" ],\n",
" \"network\": {\n",
" \"allowlist\": [\n",
" {\"domain\": \"*\"}\n",
" ]\n",
" }\n",
" },\n",
" }\n",
"\n",
" return config\n",
"\n",
"\n",
"INTERACTION_ID = None\n",
"CALL_ID = None\n",
"\n",
"class AgentHarness:\n",
" def __init__(self, factory):\n",
" self.factory = factory\n",
" self.current_agent_config = None\n",
" self.previous_interaction_id = None\n",
" self.environment_id = None\n",
" self.history_file = None\n",
" self.history = {}\n",
"\n",
" # UI Widgets\n",
" self.out_widget = None\n",
" self.in_widget = None\n",
"\n",
" # Ensure the /logs directory exists right away\n",
" os.makedirs(\"logs\", exist_ok=True)\n",
"\n",
" def load_history(self):\n",
" \"\"\"Loads the saved environments and interactions from a local JSON file.\"\"\"\n",
" if self.history_file and os.path.exists(self.history_file):\n",
" try:\n",
" with open(self.history_file, 'r') as f:\n",
" return json.load(f)\n",
" except (json.JSONDecodeError, TypeError, AttributeError):\n",
" return {}\n",
" return {}\n",
"\n",
" def save_state(self, prompt):\n",
" \"\"\"Saves the current environment, interaction ID, timestamps, and input snippet.\"\"\"\n",
" if self.environment_id and self.previous_interaction_id:\n",
" now = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n",
"\n",
" snippet = prompt.replace('\\n', ' ')\n",
" if len(snippet) > 30:\n",
" snippet = snippet[:30] + \"...\"\n",
"\n",
" if self.environment_id not in self.history:\n",
" self.history[self.environment_id] = {\n",
" \"last_seen\": now,\n",
" \"interactions\": {}\n",
" }\n",
"\n",
" self.history[self.environment_id][\"last_seen\"] = now\n",
"\n",
" interactions_dict = self.history[self.environment_id][\"interactions\"]\n",
" if self.previous_interaction_id not in interactions_dict:\n",
" interactions_dict[self.previous_interaction_id] = {\n",
" \"snippet\": snippet,\n",
" \"last_seen\": now\n",
" }\n",
" else:\n",
" interactions_dict[self.previous_interaction_id][\"last_seen\"] = now\n",
"\n",
" if self.history_file:\n",
" with open(self.history_file, 'w') as f:\n",
" json.dump(self.history, f, indent=2)\n",
"\n",
" def handle_streaming_response(self, response, prompt):\n",
" global INTERACTION_ID, CALL_ID\n",
"\n",
" if response.status_code != 200:\n",
" print(f\"Error: HTTP {response.status_code}\")\n",
" print(response.text)\n",
" return\n",
"\n",
" print(\"\\nAgent:\")\n",
"\n",
" message_html = widgets.HTML(value=\"\")\n",
" display(message_html)\n",
"\n",
" # UI State Management\n",
" content_blocks = {} # Stores the final HTML/Markdown string for each index\n",
" text_buffers = {} # Buffers for text chunks to intelligently handle duplicates\n",
" active_tools = {} # Buffers for tool chunks\n",
"\n",
" def render_ui():\n",
" # Sort by index to keep everything in strict chronological order\n",
" full_text = \"\".join(content_blocks[idx] for idx in sorted(content_blocks.keys()))\n",
"\n",
" # THE FIX FOR ASCII CHARTS & TABLES IS HERE:\n",
" html_content = markdown.markdown(full_text, extensions=['nl2br', 'fenced_code', 'tables'])\n",
" message_html.value = f\"<div style='font-family: inherit; line-height: 1.5;'>{html_content}</div>\"\n",
"\n",
" for line in response.iter_lines():\n",
" if not line:\n",
" continue\n",
"\n",
" decoded_line = line.decode('utf-8').strip()\n",
"\n",
" if decoded_line.startswith(\"event:\"):\n",
" continue\n",
"\n",
" if decoded_line.startswith(\"data: \"):\n",
" data_str = decoded_line[6:]\n",
"\n",
" if data_str == \"[DONE]\":\n",
" print(\"\\n--- Stream Complete ---\")\n",
" print(f\"Interaction ID: {INTERACTION_ID}\")\n",
" self.previous_interaction_id = INTERACTION_ID\n",
" self.save_state(prompt)\n",
" break\n",
"\n",
" try:\n",
" event_data = json.loads(data_str)\n",
" event_type = event_data.get(\"event_type\")\n",
" index = event_data.get(\"index\")\n",
"\n",
" # ---------------------------------------------------------\n",
" # 1. SYSTEM EVENTS\n",
" # ---------------------------------------------------------\n",
" if event_type == \"interaction.start\":\n",
" interaction_id = event_data.get(\"interaction\", {}).get(\"id\")\n",
" if interaction_id and INTERACTION_ID != interaction_id:\n",
" INTERACTION_ID = interaction_id\n",
"\n",
" elif event_type == \"interaction.complete\":\n",
" interaction_data = event_data.get(\"interaction\", {})\n",
"\n",
" env_id = interaction_data.get(\"environment_id\")\n",
" if env_id and env_id != self.environment_id:\n",
" self.environment_id = env_id\n",
" print(f\"\\n[Environment Attached: {self.environment_id}]\")\n",
"\n",
" usage = interaction_data.get(\"usage\", {})\n",
" if usage:\n",
" print(\"\\n--- Usage Statistics ---\")\n",
" print(f\"Total Tokens: {usage.get('total_tokens')}\")\n",
" print(f\"Input Tokens: {usage.get('total_input_tokens')} (Thought: {usage.get('total_thought_tokens', 0)})\")\n",
" print(f\"Output Tokens: {usage.get('total_output_tokens')}\")\n",
"\n",
" # ---------------------------------------------------------\n",
" # 2. INDEXED EVENTS (Text and Tool Calls)\n",
" # ---------------------------------------------------------\n",
" if index is not None:\n",
"\n",
" # -- A. TOOL START --\n",
" if event_type == \"content.start\":\n",
" content = event_data.get(\"content\", {})\n",
" if content.get(\"type\") == \"function_call\":\n",
" call_id = content.get(\"id\")\n",
" if call_id and CALL_ID != call_id:\n",
" CALL_ID = call_id\n",
" active_tools[index] = {\"name\": \"\", \"arguments\": {}}\n",
"\n",
" # -- B. STREAMING DELTAS (Tools & Text) --\n",
" elif event_type == \"content.delta\":\n",
" delta = event_data.get(\"delta\", {})\n",
"\n",
" # Stream Tool Arguments\n",
" if index in active_tools:\n",
" if \"name\" in delta:\n",
" active_tools[index][\"name\"] = delta[\"name\"]\n",
"\n",
" if \"arguments\" in delta:\n",
" args_chunk = delta[\"arguments\"]\n",
" if isinstance(args_chunk, dict):\n",
" if not isinstance(active_tools[index][\"arguments\"], dict):\n",
" active_tools[index][\"arguments\"] = {}\n",
" active_tools[index][\"arguments\"].update(args_chunk)\n",
" elif isinstance(args_chunk, str):\n",
" if not isinstance(active_tools[index][\"arguments\"], str):\n",
" active_tools[index][\"arguments\"] = \"\"\n",
" active_tools[index][\"arguments\"] += args_chunk\n",
"\n",
" # Stream Conversational Text\n",
" elif \"text\" in delta and index not in active_tools:\n",
" delta_text = delta.get(\"text\", \"\")\n",
" if not delta_text:\n",
" continue\n",
"\n",
" if index not in text_buffers:\n",
" text_buffers[index] = \"\"\n",
"\n",
" # Anti-Doubling Logic:\n",
" if len(text_buffers[index]) > 0 and delta_text == text_buffers[index]:\n",
" pass # Ignore exact catch-up duplicates\n",
" elif len(text_buffers[index]) > 0 and delta_text.startswith(text_buffers[index]):\n",
" text_buffers[index] = delta_text # Replace with cumulative string\n",
" elif len(text_buffers[index]) > 0 and text_buffers[index].startswith(delta_text):\n",
" pass # Ignore redundant fragments\n",
" else:\n",
" text_buffers[index] += delta_text # Append sequential token\n",
"\n",
" # Update the UI block and redraw\n",
" content_blocks[index] = text_buffers[index]\n",
" render_ui()\n",
"\n",
" # Stream Citations seamlessly into the UI\n",
" if \"annotations\" in delta:\n",
" cit_text = \"\\n\\n**Citations:**\\n\"\n",
" for i, annotation in enumerate(delta.get(\"annotations\", []), start=1):\n",
" title = annotation.get(\"title\", \"Unknown Source\")\n",
" url = annotation.get(\"url\", \"No URL provided\")\n",
" cit_text += f\"[{i}] [{title}]({url})\\n\"\n",
"\n",
" if index not in text_buffers:\n",
" text_buffers[index] = \"\"\n",
" text_buffers[index] += cit_text\n",
" content_blocks[index] = text_buffers[index]\n",
" render_ui()\n",
"\n",
" # -- C. TOOL COMPLETE (Render to UI) --\n",
" elif event_type == \"content.stop\":\n",
" if index in active_tools:\n",
" tool = active_tools.pop(index)\n",
" tool_name = tool.get(\"name\", \"unknown_tool\")\n",
" args = tool.get(\"arguments\", {})\n",
"\n",
" if isinstance(args, str):\n",
" try:\n",
" args = json.loads(args) if args else {}\n",
" except json.JSONDecodeError:\n",
" args = {}\n",
"\n",
" # Try to grab explanation, fallback to summary, fallback to raw payload.\n",
" display_text = args.get(\"explanation\") or args.get(\"toolSummary\") or args.get(\"toolAction\")\n",
"\n",
" if not display_text:\n",
" clean_args = {k: v for k, v in args.items() if k not in ['explanation', 'toolSummary', 'toolAction']}\n",
" display_text = f\"Payload: {json.dumps(clean_args)}\" if clean_args else \"Executing tool...\"\n",
"\n",
" tool_ui = f\"\\n<br><span style='color: #6b7280;'><i>[🛠️ <b>{tool_name}</b>: {display_text}]</i></span><br>\\n\"\n",
"\n",
" content_blocks[index] = tool_ui\n",
" render_ui()\n",
"\n",
" except json.JSONDecodeError:\n",
" pass\n",
"\n",
" def interact(self, prompt):\n",
" api_url = f\"{self.factory.endpoint}/v1beta1/projects/{self.factory.project_id}/locations/{self.factory.location}/interactions\"\n",
"\n",
" headers = {\n",
" \"Authorization\": f\"Bearer {self.factory.token_manager.get_token()}\",\n",
" \"Content-Type\": \"application/json\"\n",
" }\n",
"\n",
" agent_uri = self.current_agent_config.get(\"name\")\n",
"\n",
" payload = {\n",
" \"agent\": agent_uri,\n",
" \"stream\": True,\n",
" \"background\": True,\n",
" \"store\": True,\n",
" \"input\": [{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": prompt}]}]\n",
" }\n",
"\n",
" if self.previous_interaction_id:\n",
" payload[\"previous_interaction_id\"] = self.previous_interaction_id\n",
"\n",
" if self.environment_id:\n",
" payload[\"environment\"] = self.environment_id\n",
" else:\n",
" payload[\"environment\"] = \"remote\"\n",
"\n",
" response = requests.post(api_url, headers=headers, json=payload, stream=True)\n",
" self.handle_streaming_response(response, prompt)\n",
"\n",
" def multi_turn_interact(self):\n",
" global INTERACTION_ID\n",
"\n",
" # Clear state on fresh start\n",
" self.environment_id = None\n",
" self.previous_interaction_id = None\n",
" self.current_agent_config = None\n",
" INTERACTION_ID = None\n",
"\n",
" print(\"--- Setup Multi-Turn Chat ---\")\n",
"\n",
" # Setup Wizard (Runs in standard console before loading UI)\n",
" agents_list = list(self.factory.agents.values())\n",
" print(\"Available Agents:\")\n",
" for index, agent in enumerate(agents_list, start=1):\n",
" description = agent.get('description', 'No description')\n",
" print(f\"{index}. {agent['id']} - {description}\")\n",
"\n",
" while True:\n",
" choice = input(\"\\nEnter the number of the agent you want to select: \")\n",
" try:\n",
" selected_index = int(choice) - 1\n",
" if 0 <= selected_index < len(agents_list):\n",
" self.current_agent_config = agents_list[selected_index]\n",
" print(f\"\\nSuccess! You selected: {self.current_agent_config['id']}\")\n",
"\n",
" proj = getattr(self.factory, 'project_id', 'unknown_project')\n",
" loc = getattr(self.factory, 'location', 'unknown_location')\n",
" agent_id = self.current_agent_config.get('id', 'unknown_agent')\n",
"\n",
" file_name = f\"{proj}_{loc}_{agent_id}.json\"\n",
" self.history_file = os.path.join(\"logs\", file_name)\n",
" self.history = self.load_history()\n",
" break\n",
" else:\n",
" print(f\"Invalid selection. Please enter a number between 1 and {len(agents_list)}.\")\n",
" except ValueError:\n",
" print(\"Invalid input. Please enter a valid number.\")\n",
"\n",
" if self.history:\n",
" print(\"\\nSaved Environments:\")\n",
" envs = list(self.history.keys())\n",
" for i, env in enumerate(envs, 1):\n",
" last_seen = self.history[env].get(\"last_seen\", \"Unknown time\")\n",
" print(f\"[{i}] Environment: {env} (Last used: {last_seen})\")\n",
"\n",
" print(\"[0] Start completely fresh\")\n",
" env_choice = input(\"\\nSelect Environment (or 0 to skip): \").strip()\n",
"\n",
" if env_choice.isdigit() and 0 < int(env_choice) <= len(envs):\n",
" self.environment_id = envs[int(env_choice) - 1]\n",
" print(f\"--> Loaded Environment: {self.environment_id}\")\n",
"\n",
" interaction_data = self.history[self.environment_id].get(\"interactions\", {})\n",
" ints = list(interaction_data.keys())\n",
"\n",
" if ints:\n",
" print(\"\\nSaved Interactions:\")\n",
" for i, interaction in enumerate(ints, 1):\n",
" int_snippet = interaction_data[interaction].get(\"snippet\", \"No preview\")\n",
" print(f\"[{i}] Topic: \\\"{int_snippet}\\\"\")\n",
"\n",
" print(\"[0] Start a NEW interaction\")\n",
" int_choice = input(\"\\nSelect Interaction (or 0 for new): \").strip()\n",
" if int_choice.isdigit() and 0 < int(int_choice) <= len(ints):\n",
" self.previous_interaction_id = ints[int(int_choice) - 1]\n",
" INTERACTION_ID = self.previous_interaction_id\n",
" print(f\"--> Resuming Interaction: {self.previous_interaction_id}\\n\")\n",
" else:\n",
" print(\"--> Starting NEW interaction.\\n\")\n",
" else:\n",
" print(\"--> Starting completely fresh.\\n\")\n",
"\n",
" clear_output() # Clean up the setup text to make room for the UI\n",
"\n",
" # --- UI WIDGET CREATION ---\n",
" self.out_widget = widgets.Output(layout=widgets.Layout(\n",
" width='100%', height='400px', border='1px solid #e0e0e0', overflow_y='auto', padding='10px'\n",
" ))\n",
"\n",
" self.in_widget = widgets.Text(\n",
" value='',\n",
" placeholder=\"Type your message and press Enter (or type 'quit' / 'reset')...\",\n",
" description='You:',\n",
" layout=widgets.Layout(width='100%')\n",
" )\n",
"\n",
" # Event Handler for when the user hits 'Enter'\n",
" def on_submit(sender):\n",
" user_input = self.in_widget.value.strip()\n",
" self.in_widget.value = \"\" # Clear the box instantly\n",
"\n",
" if not user_input:\n",
" return\n",
"\n",
" self.in_widget.disabled = True # Prevent duplicate sends while streaming\n",
"\n",
" # The 'with' context routes all print() statements into the widget\n",
" with self.out_widget:\n",
" if user_input.lower() in ['quit', 'exit']:\n",
" print(\"\\n[System] Conversation ended.\")\n",
" return # Leaves the box disabled\n",
"\n",
" if user_input.lower() == 'reset':\n",
" print(\"\\n\" + \"=\"*40)\n",
" print(\"[System] Session Reset. Your next message will start a fresh environment.\")\n",
" print(\"=\"*40 + \"\\n\")\n",
" self.environment_id = None\n",
" self.previous_interaction_id = None\n",
" global INTERACTION_ID\n",
" INTERACTION_ID = None\n",
" self.in_widget.disabled = False\n",
" return\n",
"\n",
" print(f\"\\n\\nYou: {user_input}\")\n",
" print(\"-\" * 40)\n",
"\n",
" # Make the API call and stream the response\n",
" self.interact(prompt=user_input)\n",
"\n",
" self.in_widget.disabled = False # Re-enable typing\n",
"\n",
" # Bind the submission function to the Text widget\n",
" self.in_widget.on_submit(on_submit)\n",
"\n",
" # Display the Chat UI\n",
" display(self.out_widget, self.in_widget)\n",
" with self.out_widget:\n",
" print(f\"Chat UI initialized for Agent: {self.current_agent_config['id']}\")\n",
" print(\"Type your message below and press Enter to begin.\")"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "7f2_3TclYGMG"
},
"source": [
"## Environment Setup\n",
"\n",
"This section contains evironment checks required to use the Managed Agents API.\n",
"\n",
"It the then deploys the agent defined in AgentConfig (in scafolding).\n",
"\n",
"You can use this code to also delete all agents deployed.\n",
"\n",
"It will:\n",
"- Check if aiplatform.googleapis.com\n",
"- Check if the aiplatform service account has the correct IAM role: aiplatform.serviceAgent\n",
"- Check if you have the correct IAM role: aiplatform.user or aiplatform.admin\n",
"- Mint a new OAuth token\n",
"- Create the agent configuration\n",
"\n",
"**Tasks to complete**:\n",
"* Run the code cell and authenticate via Colab OAuth process.\n",
"* Verify success of the agent deployment should see something like: Pooling... 'done':True'"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "tDc6utwRKUz2"
},
"outputs": [],
"source": [
"def check_project_settings(project_id: str) -> None:\n",
" \"\"\"Validates GCP project settings, APIs, and IAM roles for AI Platform.\"\"\"\n",
"\n",
" if \"google.colab\" in sys.modules:\n",
" from google.colab import auth\n",
" auth.authenticate_user()\n",
"\n",
" # Extract Token\n",
" printed_token = !gcloud auth application-default print-access-token\n",
" token = printed_token[0] if printed_token else None\n",
"\n",
" if token:\n",
" print(\"✅ Success on token creation.\")\n",
" else:\n",
" print(\"❌ Token creation failed.\")\n",
" return\n",
"\n",
" # Extract project number\n",
" project_info_response = !gcloud projects describe {project_id} --format=\"value(projectNumber)\"\n",
" if not project_info_response:\n",
" print(\"❌ Failed to retrieve project number.\")\n",
" return\n",
"\n",
" project_number = project_info_response[0]\n",
" print(f\"✅ The project number for {project_id} is: {project_number}\")\n",
"\n",
" # Is aiplatform enabled\n",
" api_name = \"aiplatform.googleapis.com\"\n",
" result = !gcloud services list --project={project_id} --enabled --filter=\"name:{api_name}\" --format=\"value(name)\"\n",
"\n",
" if any(api_name in service for service in result):\n",
" print(f\"✅ {api_name} is enabled.\")\n",
" else:\n",
" print(f\"❌ {api_name} is NOT enabled. You may need to run:\")\n",
" print(f\" !gcloud services enable {api_name} --project={project_id}\")\n",
"\n",
" # Does service account have needed bindings\n",
" sa_email = f\"service-{project_number}@gcp-sa-aiplatform.iam.gserviceaccount.com\"\n",
" sa_info_response = !gcloud projects get-iam-policy {project_id} \\\n",
" --flatten=\"bindings[].members\" \\\n",
" --filter=\"bindings.members:serviceAccount:{sa_email}\" \\\n",
" --format=\"value(bindings.role)\"\n",
"\n",
" if any(\"aiplatform.serviceAgent\" in role for role in sa_info_response):\n",
" print(\"✅ The service account has the aiplatform.serviceAgent role.\")\n",
" else:\n",
" print(\"❌ The service account does NOT have the aiplatform.serviceAgent role.\")\n",
" print(\"Attempting to add role...\")\n",
" attempt_add_role = !gcloud projects add-iam-policy-binding {project_id} \\\n",
" --member=\"serviceAccount:{sa_email}\" \\\n",
" --role=\"roles/aiplatform.serviceAgent\"\n",
"\n",
" # Extract email from token\n",
" email = None\n",
" try:\n",
" token_info_response = requests.get(f\"https://oauth2.googleapis.com/tokeninfo?access_token={token}\", timeout=10)\n",
"\n",
" if token_info_response.status_code == 200:\n",
" data = token_info_response.json()\n",
" # The email will only be present if the \"userinfo.email\" scope was requested\n",
" email = data.get(\"email\")\n",
" print(f\"Token belongs to: {email}\")\n",
" else:\n",
" print(\"Invalid or expired token.\")\n",
" return\n",
" except requests.RequestException as e:\n",
" print(f\"Failed to reach tokeninfo API: {e}\")\n",
" return\n",
"\n",
" if not email:\n",
" print(\"Email scope not requested; cannot verify user IAM roles.\")\n",
" return\n",
"\n",
" # Does user have needed bindings\n",
" user_roles_response = !gcloud projects get-iam-policy {project_id} \\\n",
" --flatten=\"bindings[].members\" \\\n",
" --filter=\"bindings.members:user:{email}\" \\\n",
" --format=\"value(bindings.role)\"\n",
"\n",
" print(f\"Raw roles found: {user_roles_response}\")\n",
"\n",
" if not user_roles_response:\n",
" print(f\"❌ The user {email} has no direct project-level roles assigned.\")\n",
" return\n",
"\n",
" # Check for owner, admin, and user roles\n",
" is_owner = any(\"roles/owner\" in role for role in user_roles_response)\n",
" is_ai_admin = any(\"aiplatform.admin\" in role for role in user_roles_response)\n",
" is_ai_user = any(\"aiplatform.user\" in role for role in user_roles_response)\n",
"\n",
" if is_owner:\n",
" print(f\"✅ The user {email} is a project Owner, which inherits all AI Platform permissions.\")\n",
" elif is_ai_admin:\n",
" print(f\"✅ The user {email} has the explicit aiplatform.admin role.\")\n",
" elif is_ai_user:\n",
" print(f\"✅ The user {email} has the explicit aiplatform.user role (standard user access).\")\n",
" else:\n",
" print(f\"❌ The user {email} does NOT have the aiplatform.admin, aiplatform.user, or owner role.\")\n",
"\n",
"def test_control_plane(project_id: str, endpoint: str, location: str, delete_all: bool = False) -> None:\n",
" factory = AgentFactory(project_id, endpoint, location)\n",
"\n",
" if delete_all:\n",
" print(\"Deleting all existing agents...\")\n",
" factory.delete_all()\n",
"\n",
" agent_list = factory.list()\n",
"\n",
" print(\"****** AGENT LIST ******\")\n",
"\n",
" if not agent_list:\n",
" print(\"No agents found. Creating simple chat agent...\")\n",
" config = AgentConfig(project_id=project_id, skill_gcs_bucket=SKILL_GCS_BUCKET)\n",
" agent_config = config.get_config()\n",
" factory.create(agent_config)\n",
" agent_list = factory.list() # Refresh the list after creation\n",
"\n",
" # agent_list is a dict, so we use .items() to print both the ID and the payload\n",
" for agent_id, agent_data in agent_list.items():\n",
" print(f\"Agent ID: {agent_id} \\nDetails: {agent_data}\\n\")\n",
"\n",
"check_project_settings(PROJECT_ID)\n",
"test_control_plane(PROJECT_ID, ENDPOINT, LOCATION, delete_all=False)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "VyeQz1opK3HF"
},
"source": [
"## Skill Deployment\n",
"\n",
"Before you start interacting with the agent you need to deploy a skill file to the bucket you identified above.\n",
"\n",
"Note: You alternatively could use the Google Cloud Skills Registry.\n",
"\n",
"**Tasks to complete:**\n",
"* Run the cell below to upload the skill file to the configured bucket"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "oLiUzICpKjCq"
},
"outputs": [],
"source": [
"skill = r\"\"\"\n",
"# SYSTEM DIRECTIVE: WorldCup_Analyst_2026_Master\n",
"\n",
"## 1. Core Identity & Persona\n",
"* **Role:** You are the authoritative 2026 FIFA World Cup Analyst.\n",
"* **Tone:** Objective, analytical, authoritative, and engaging. You speak like a top-tier pundit backed by a team of data scientists.\n",
"* **Terminology:** Use standard global football terminology (pitch, match, squad, fixture, extra time). If a user says \"soccer\" or \"roster,\" seamlessly understand them but maintain your professional football lexicon.\n",
"* **Zero Hallucination Policy:** You do not hallucinate data. If a squad has not been officially announced, state clearly that it is a \"projected\" or \"preliminary\" squad. If a match result hasn't happened, do not invent a scoreline.\n",
"\n",
"## 2. 2026 Tournament Framework & Baseline Context\n",
"*You must apply this framework to all answers:*\n",
"* **Current Timeframe:** It is currently May 2026. The tournament kicks off June 11, 2026. Teams are currently locked into pre-tournament friendlies and final roster cuts.\n",
"* **Format:** 48 teams, 104 matches, 12 groups of four.\n",
"* **Progression:** The top two teams from each group, plus the **8 best third-place teams**, advance to the new Round of 32.\n",
"* **Time Zones:** 16 Host cities span four North American time zones. Always convert kickoff times to the user's local time if known; otherwise, default to the local venue time and specify the time zone (e.g., PT, CT, ET).\n",
"\n",
"## 3. Data Routing Protocol (Intent -> Action)\n",
"When a user asks a question, you must first classify the **Data Intent**, and then execute your search or tool call using *only* the prescribed domains for that category. Do not mix sources.\n",
"\n",
"#### A. Official Logistics (Schedule, Venues, Results, Referees)\n",
"* **Intent:** The user wants to know *when* a match is, *where* it is played, the official score, or tournament rules.\n",
"* **Prescribed Action:** Route strictly to FIFA.\n",
"* **Search Execution:** `site:fifa.com/tournaments/mens/worldcup/2026`\n",
"* **Rule:** Never use third-party sites for fixture dates or kickoff times.\n",
"\n",
"#### B. Advanced Statistics & Analytics (xG, Pass Completion, Heat Maps)\n",
"* **Intent:** The user wants data on expected goals, player performance metrics, or historical tournament data.\n",
"* **Prescribed Action:** Route strictly to FBref or Opta.\n",
"* **Search Execution:** `site:fbref.com` OR `site:theanalyst.com`\n",
"* **Rule:** Ignore generic sports sites for stats. Provide the data in a Markdown table.\n",
"\n",
"#### C. Odds, Betting Lines & Win Probabilities\n",
"* **Intent:** The user is asking who is favored to win, over/under goals, or outright tournament winner odds.\n",
"* **Prescribed Action:** Route strictly to established odds aggregators.\n",
"* **Search Execution:** `site:vegasinsider.com \"world cup\"` OR `site:oddschecker.com/football/world-cup`\n",
"* **Rule:** Always state the date/time the odds were pulled, noting that \"lines are subject to change.\" Do not provide personal betting advice.\n",
"\n",
"#### D. Rosters, Injuries & Player Market Values\n",
"* **Intent:** The user asks who made the squad, who is injured, or what a player/team is worth.\n",
"* **Prescribed Action:** Route strictly to Transfermarkt.\n",
"* **Search Execution:** `site:transfermarkt.us`\n",
"* **Rule:** Distinguish clearly between \"provisional\" squads and \"final\" 26-man rosters.\n",
"\n",
"#### E. News, Journalism & Tactical Breakdown\n",
"* **Intent:** The user asks for tactical analysis, locker room news, manager quotes, or subjective match previews.\n",
"* **Prescribed Action:** Route strictly to premium sports journalism.\n",
"* **Search Execution:** `site:nytimes.com/athletic` OR `site:espn.com/soccer`\n",
"* **Rule:** Synthesize the analysis objectively. Say, \"According to The Athletic's tactical breakdown...\"\n",
"\n",
"#### F. Official Team Communications & Direct Federation Data\n",
"* **Intent:** The user asks for official press releases, direct quotes from a national federation, or the absolute first source of a squad announcement.\n",
"* **Prescribed Action:** Route strictly to the specific National Football Association's official domain using the exact 48-team directory below.\n",
"* **Directory of the 48 Qualified Nations:**\n",
"\n",
"| Nation | Region | Official Domain | Search Execution |\n",
"| :--- | :--- | :--- | :--- |\n",
"| **USA** | CONCACAF | ussoccer.com | `site:ussoccer.com \"World Cup\"` |\n",
"| **Canada** | CONCACAF | canadasoccer.com | `site:canadasoccer.com \"World Cup\"` |\n",
"| **Mexico** | CONCACAF | miseleccion.mx | `site:miseleccion.mx \"Mundial\"` |\n",
"| **Curaçao** | CONCACAF | ffk.cw | `site:ffk.cw \"World Cup\"` |\n",
"| **Haiti** | CONCACAF | fhfhaiti.com | `site:fhfhaiti.com \"Coupe du Monde\"` |\n",
"| **Panama** | CONCACAF | fepafut.com | `site:fepafut.com \"Mundial\"` |\n",
"| **Argentina** | CONMEBOL | afa.com.ar | `site:afa.com.ar \"Mundial\"` |\n",
"| **Brazil** | CONMEBOL | cbf.com.br | `site:cbf.com.br \"Copa do Mundo\"` |\n",
"| **Colombia** | CONMEBOL | fcf.com.co | `site:fcf.com.co \"Mundial\"` |\n",
"| **Ecuador** | CONMEBOL | fef.ec | `site:fef.ec \"Mundial\"` |\n",
"| **Paraguay** | CONMEBOL | apf.org.py | `site:apf.org.py \"Mundial\"` |\n",
"| **Uruguay** | CONMEBOL | auf.org.uy | `site:auf.org.uy \"Mundial\"` |\n",
"| **Austria** | UEFA | oefb.at | `site:oefb.at \"Weltmeisterschaft\"` |\n",
"| **Belgium** | UEFA | rbfa.be | `site:rbfa.be \"World Cup\"` |\n",
"| **Bosnia and Herz.** | UEFA | nfsbih.ba | `site:nfsbih.ba \"World Cup\"` |\n",
"| **Croatia** | UEFA | hns.family | `site:hns.family \"World Cup\"` |\n",
"| **Czechia** | UEFA | fotbal.cz | `site:fotbal.cz \"World Cup\"` |\n",
"| **England** | UEFA | englandfootball.com | `site:englandfootball.com \"World Cup\"` |\n",
"| **France** | UEFA | fff.fr | `site:fff.fr \"Coupe du Monde\"` |\n",
"| **Germany** | UEFA | dfb.de | `site:dfb.de \"Weltmeisterschaft\"` |\n",
"| **Netherlands** | UEFA | knvb.nl | `site:knvb.nl \"World Cup\"` |\n",
"| **Norway** | UEFA | fotball.no | `site:fotball.no \"World Cup\"` |\n",
"| **Portugal** | UEFA | fpf.pt | `site:fpf.pt \"Mundial\"` |\n",
"| **Scotland** | UEFA | scottishfa.co.uk | `site:scottishfa.co.uk \"World Cup\"` |\n",
"| **Spain** | UEFA | rfef.es | `site:rfef.es \"Mundial\"` |\n",
"| **Sweden** | UEFA | svenskfotboll.se | `site:svenskfotboll.se \"World Cup\"` |\n",
"| **Switzerland** | UEFA | football.ch | `site:football.ch \"World Cup\"` |\n",
"| **Türkiye** | UEFA | tff.org | `site:tff.org \"World Cup\"` |\n",
"| **Algeria** | CAF | faf.dz | `site:faf.dz \"Coupe du Monde\"` |\n",
"| **Cabo Verde** | CAF | fcf.cv | `site:fcf.cv \"Mundial\"` |\n",
"| **DR Congo** | CAF | fecofa.cd | `site:fecofa.cd \"Coupe du Monde\"` |\n",
"| **Egypt** | CAF | efa.com.eg | `site:efa.com.eg \"World Cup\"` |\n",
"| **Ghana** | CAF | ghanafa.org | `site:ghanafa.org \"World Cup\"` |\n",
"| **Côte d'Ivoire** | CAF | fifciv.com | `site:fifciv.com \"Coupe du Monde\"` |\n",
"| **Morocco** | CAF | frmf.ma | `site:frmf.ma \"Coupe du Monde\"` |\n",
"| **Senegal** | CAF | fsfoot.sn | `site:fsfoot.sn \"Coupe du Monde\"` |\n",
"| **South Africa** | CAF | safa.net | `site:safa.net \"World Cup\"` |\n",
"| **Tunisia** | CAF | ftf.org.tn | `site:ftf.org.tn \"Coupe du Monde\"` |\n",
"| **Australia** | AFC | footballaustralia.com.au | `site:footballaustralia.com.au \"World Cup\"` |\n",
"| **IR Iran** | AFC | ffiri.ir | `site:ffiri.ir \"World Cup\"` |\n",
"| **Iraq** | AFC | ifa.iq | `site:ifa.iq \"World Cup\"` |\n",
"| **Japan** | AFC | jfa.jp | `site:jfa.jp \"World Cup\"` |\n",
"| **Jordan** | AFC | jfa.jo | `site:jfa.jo \"World Cup\"` |\n",
"| **Qatar** | AFC | qfa.qa | `site:qfa.qa \"World Cup\"` |\n",
"| **Saudi Arabia** | AFC | saff.com.sa | `site:saff.com.sa \"World Cup\"` |\n",
"| **Korea Republic** | AFC | kfa.or.kr | `site:kfa.or.kr \"World Cup\"` |\n",
"| **Uzbekistan** | AFC | ufa.uz | `site:ufa.uz \"World Cup\"` |\n",
"| **New Zealand** | OFC | nzfootball.co.nz | `site:nzfootball.co.nz \"World Cup\"` |\n",
"\n",
"#### G. Environmental & Venue Constraints\n",
"* **Intent:** The user asks about match conditions, stadium details, or why a team might underperform.\n",
"* **Prescribed Knowledge & Routing:**\n",
" * **Altitude Factor:** Mexico City (Estadio Azteca) is at 7,200+ feet (2,200m+); Guadalajara is at 5,100+ feet. Flag travel and oxygen recovery fatigue for teams playing matches here.\n",
" * **Surface Factor:** Venues like MetLife, AT&T Stadium, and Mercedes-Benz Stadium use artificial turf natively but are installing temporary real grass pitches for FIFA. Flag potential seams, slickness, or higher hamstring/groin strain risks.\n",
" * **Climate/Travel Factor:** Teams traveling from cool Pacific Northwest venues to high-humidity summer venues face massive thermodynamic shifts. Always factor rest days and flight distances into tactical analysis.\n",
"\n",
"#### H. Tournament Progression & Tie-Breaker Logic\n",
"* **Directive:** When calculating or discussing who advances to the Round of 32 from the 3rd-place pool, evaluate teams strictly by the official FIFA Group Stage tie-breakers in this exact order:\n",
" 1. Highest number of points obtained in all group matches.\n",
" 2. Superior goal difference in all group matches.\n",
" 3. Highest number of goals scored in all group matches.\n",
" 4. Fair play points (lowest number of yellow/red cards).\n",
" 5. Drawing of lots by the FIFA organizing committee.\n",
"* **Action:** If a user asks \"Can [Team X] still qualify?\", explicitly map out their point potential and goal difference requirements compared to the other current 3rd-place teams.\n",
"\n",
"#### I. Broadcast & Media Rights Routing\n",
"* **Intent:** The user asks where to watch, stream, or listen to a live match.\n",
"* **Routing Logic:**\n",
" * **United States:** English -> FOX, FS1, Fox Sports App. Spanish -> Telemundo, Universo, Peacock.\n",
" * **Canada:** CTV, TSN, RDS.\n",
" * **Mexico:** TelevisaUnivision, TV Azteca, ViX.\n",
" * **United Kingdom:** BBC (BBC iPlayer), ITV (STV/ITVX).\n",
"* **Rule:** If the user's location is unknown, default to providing the US/Host broadcasters, but add: *\"Please specify your country if you are tuning in from outside North America.\"*\n",
"\n",
"#### J. Real-Time Anchor Rules\n",
"* **Directive:** For any query regarding active injuries, lineup changes, training camp drama, or breaking news, you must append the current month and year to your search tool query.\n",
"* **Search Formulation:** `[Player/Team Name] [Event] May 2026`\n",
"* **Recency Filter:** Sort search tool results by \"Past 24 Hours\" or \"Past Week\" to avoid pulling speculative articles from prior years.\n",
"\"\"\"\n",
"\n",
"def upload_skill_to_gcs(bucket_name: str, skill_content: str, destination_blob_name: str = \"world_cup_2026_skill.md\"):\n",
" \"\"\"Uploads a string literal to a Google Cloud Storage bucket.\n",
"\n",
" Args:\n",
" bucket_name (str): The name of the target GCS bucket.\n",
" skill_content (str): The markdown string content to upload.\n",
" destination_blob_name (str): The filename to save it as in GCS.\n",
" \"\"\"\n",
"\n",
" if not bucket_name:\n",
" print(\"❌ SKILL_GCS_BUCKET is not defined. Cannot upload.\")\n",
" return\n",
"\n",
" try:\n",
" # Initialize the GCS client\n",
" client = storage.Client()\n",
" print(f\"Connecting to bucket: {bucket_name}\")\n",
" bucket = client.bucket(bucket_name)\n",
"\n",
" # Create a new blob (file) within the bucket\n",
" blob = bucket.blob(destination_blob_name)\n",
"\n",
" # Upload the string data directly\n",
" blob.upload_from_string(skill_content)\n",
" print(f\"✅ Skill successfully uploaded to gs://{bucket_name}/{destination_blob_name}\")\n",
"\n",
" except Exception as e:\n",
" print(f\"❌ Failed to upload to GCS: {e}\")\n",
"\n",
"# Execute the uploa\n",
"upload_skill_to_gcs(bucket_name=SKILL_GCS_BUCKET, skill_content=skill)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "hZAE0fX-3bIs"
},
"source": [
"# Agent Harness\n",
"\n",
"This section calls the AgentFactory scafolding to list agents deployed in the project/region.\n",
"Then it lists any locally persisted evnironments for the agent selected.\n",
"Then the chat session kicks in running on a new environment or re-connects to an existing one.\n",
"\n",
"**Tasks to complete**:\n",
"- Hit run on the cell below and select the world cup agent from the list.\n",
"- Then type Hello in the lower input box.\n",
"- The interaction will get triggered and then you can chat with the agent.\n",
"- The first time you run this agent, it will have no evironment list.\n",
"- Play around with re-running the code cell to start a new Factory/Harness.\n",
"\n",
"Examples of questions you can ask:\n",
"- Use the cloned repo and make me a graph of the totals goals for each year of the world cup. Use ascii output to create the graph.\n",
"- Use the cloned repo and build an xG model for the teams in the 2026 World Cup. Follow the skills for the 2026 teams. Save all of the model code and ouputs to a new directory.\n",
"- How do your xG estimates for the first round games compare to the current odds for the matches?\n",
"- Can you create a table of the USA odds to make to the final. Who would they most likely face and what are their odds to get there?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"id": "MI8BkHYdYf0j"
},
"outputs": [],
"source": [
"factory = AgentFactory(project_id=PROJECT_ID, endpoint=ENDPOINT, location=LOCATION)\n",
"harness = AgentHarness(factory)\n",
"harness.multi_turn_interact()"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}