Way Back Home - Level 1: Pinpoint Location


La mission

Durée : 2 min

En-tête

Vous vous êtes identifié auprès de l'IA d'urgence et votre balise clignote désormais sur la carte planétaire, mais elle est à peine visible, perdue dans le bruit. Les équipes de secours qui effectuent des recherches depuis l'orbite ont repéré quelque chose à vos coordonnées, mais elles ne peuvent pas obtenir de position précise. Le signal est trop faible.

Pour que votre balise fonctionne à pleine puissance, vous devez confirmer votre position exacte. Le système de navigation du pod est hors service, mais l'accident a dispersé des preuves récupérables sur le site d'atterrissage. Échantillons de sol. Une flore étrange. Vue dégagée du ciel nocturne extraterrestre.

Si vous pouvez analyser ces preuves et déterminer dans quelle région du monde vous vous trouvez, l'IA peut trianguler votre position et amplifier le signal de la balise. Quelqu'un vous trouvera peut-être.

Il est temps de rassembler les pièces.

Prérequis

⚠️ Pour accéder à ce niveau, vous devez avoir terminé le niveau 0.

Avant de commencer, vérifiez que vous disposez des éléments suivants :
- [ ] config.json dans la racine du projet avec votre ID de participant et vos coordonnées
- [ ] Votre avatar est visible sur la carte du monde
- [ ] Votre balise est visible (en grisé) à vos coordonnées

Si vous n'avez pas terminé le niveau 0, commencez par là.


Objectifs de l'atelier

Dans ce niveau, vous allez construire un système d'IA multi-agent qui analyse les preuves sur le lieu du crash à l'aide du traitement parallèle :

architecture


Objectifs de la formation

Concept Ce que vous allez apprendre
Systèmes multi-agent Créer des agents spécialisés avec des responsabilités uniques
ParallelAgent Composer des agents indépendants pour qu'ils s'exécutent simultanément
before_agent_callback Récupérer la configuration et définir l'état avant l'exécution de l'agent
ToolContext Accéder aux valeurs d'état dans les fonctions d'outil
Serveurs MCP personnalisés Créer des outils avec le modèle impératif (code Python sur Cloud Run)
OneMCP BigQuery Se connecter au MCP géré de Google pour accéder à BigQuery
IA multimodale Analyser des images et des vidéos avec audio avec Gemini
Orchestration d'agents Coordonner plusieurs agents avec un orchestrateur racine
Cloud Deployment Déployer le serveur et l'agent MCP sur Cloud Run
Préparation A2A Structurer les agents pour les futures communications entre agents

Biomes de la planète

La surface de la planète est divisée en quatre biomes distincts, chacun avec des caractéristiques uniques :

biome de la planète

Vos coordonnées déterminent le biome dans lequel vous vous êtes écrasé. Les éléments trouvés sur le site du crash reflètent les caractéristiques de ce biome :

Biome Quadrant Preuves géologiques Preuves botaniques Preuves astronomiques
🧊 CRYO NW (x<50, y≥50) Méthane gelé, cristaux de glace Fougères de glace, cryoflore Étoile géante bleue
🌋 VOLCANIC NE (x≥50, y≥50) Dépôts d'obsidienne Fleurs de feu, flore résistante à la chaleur Binaire naine rouge
💜 BIOLUMINESCENT SO (x<50, y<50) Sol phosphorescent Champignons et plantes luminescents Pulsar vert
🦴 FOSSILISÉ SE (x≥50, y<50) Dépôts d'ambre, minéraux ite Arbres pétrifiés, flore ancienne Soleil jaune

Votre mission : créer des agents d'IA capables d'analyser les preuves et de déduire le biome dans lequel vous vous trouvez.

Configurer votre environnement

Durée : 3 min

Avant de générer des preuves, vous devez activer les API Google Cloud requises, y compris OneMCP pour BigQuery, qui fournit un accès MCP géré à BigQuery.

Exécuter le script de configuration de l'environnement

👉💻 Exécutez le script de configuration de l'environnement :

cd ~/way-back-home/level_1
chmod +x setup/setup_env.sh
./setup/setup_env.sh

Le résultat doit ressembler à ce qui suit :

================================================================
Level 1: Environment Setup
================================================================
Project: your-project-id

[1/6] Enabling core Google Cloud APIs...
       Vertex AI API enabled
       Cloud Run API enabled
       Cloud Build API enabled
       BigQuery API enabled
       Artifact Registry API enabled
       IAM API enabled

[2/6] Enabling OneMCP BigQuery (Managed MCP)...
       OneMCP BigQuery enabled

[3/6] Setting up service account and IAM permissions...
       Service account 'way-back-home-sa' created
       Vertex AI User role granted
       Cloud Run Invoker role granted
       BigQuery User role granted
       BigQuery Data Viewer role granted
       Storage Object Viewer role granted

[4/6] Configuring Cloud Build IAM for deployments...
       Cloud Build can now deploy services as way-back-home-sa
       Cloud Run Admin role granted to Compute SA

[5/6] Creating Artifact Registry repository...
       Repository 'way-back-home' created

[6/6] Creating environment variables file...
      Found PARTICIPANT_ID in config.json: abc123...
       Created ../set_env.sh

================================================================
 Environment Setup Complete!
================================================================

Variables d'environnement source

👉💻 Définissez les variables d'environnement :

source ~/way-back-home/set_env.sh

Créer un environnement virtuel

👉💻 Créez et activez l'environnement virtuel Python pour le niveau 1 :

cd ~/way-back-home/level_1
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Configurer le catalogue d'étoiles

👉💻 Configurez le catalogue d'étoiles dans BigQuery :

python setup/setup_star_catalog.py

Vous devriez obtenir le résultat suivant :

Setting up star catalog in project: your-project-id
==================================================
✓ Dataset way_back_home already exists
✓ Created table star_catalog
✓ Inserted 12 rows into star_catalog

📊 Star Catalog Summary:
----------------------------------------
  NE (VOLCANIC): 3 stellar patterns
  NW (CRYO): 3 stellar patterns
  SE (FOSSILIZED): 3 stellar patterns
  SW (BIOLUMINESCENT): 3 stellar patterns
----------------------------------------
✓ Star catalog is ready for triangulation queries

==================================================
✅ Star catalog setup complete!

Générer des preuves de site de plantage

Durée : 2 min

Générez maintenant des preuves personnalisées du site de l'accident en fonction de vos coordonnées.

Exécuter le générateur de preuves

👉💻 Dans le répertoire level_1 (avec l'environnement virtuel activé), exécutez la commande suivante :

cd ~/way-back-home/level_1
python generate_evidence.py

Le résultat doit ressembler à ce qui suit :

 Welcome back, Explorer_Aria!
  Coordinates: (23, 67)
  Ready to analyze your crash site.

📍 Crash site analysis initiated...
   Generating evidence for your location...

🔬 Generating soil sample...
 Soil sample captured: outputs/soil_sample.png
 Capturing star field...
 Star field captured: outputs/star_field.png
🌿 Recording flora activity...
   (This may take 1-2 minutes for video generation)
   Generating video...
   Generating video...
   Generating video...
 Flora recorded: outputs/flora_recording.mp4

📤 Uploading evidence to Mission Control...
 Config updated with evidence URLs

==================================================
 Evidence generation complete!
==================================================

Vérifier vos pièces justificatives

👉 Prenez le temps d'examiner les fichiers de preuves générés dans le dossier outputs/. Chacun reflète les caractéristiques du biome de votre lieu de crash, mais vous ne saurez pas de quel biome il s'agit tant que vos agents IA ne les auront pas analysés.

En fonction de votre localisation, les preuves générées peuvent se présenter comme suit :

exemple d&#39;enregistrement de la flore exemple d&#39;échantillon de sol exemple de champ d&#39;étoiles

Créer le serveur MCP personnalisé

Durée : 8 min

Les systèmes d'analyse embarqués de votre capsule de sauvetage sont hors service, mais les données brutes des capteurs ont survécu au crash. Vous allez créer un serveur MCP avec FastMCP qui fournit des outils d'analyse géologique et botanique.

Créer l'outil d'analyse géologique

Cet outil analyse des images d'échantillons de sol pour identifier la composition minérale.

👉✏️ Ouvrez mcp-server/main.py et recherchez #REPLACE-GEOLOGICAL-TOOL. Remplacez-le par :

GEOLOGICAL_PROMPT = """Analyze this alien soil sample image.

Classify the PRIMARY characteristic (choose exactly one):

1. CRYO - Frozen/icy minerals, crystalline structures, frost patterns,
   blue-white coloration, permafrost indicators

2. VOLCANIC - Volcanic rock, basalt, obsidian, sulfur deposits,
   red-orange minerals, heat-formed crystite structures

3. BIOLUMINESCENT - Glowing particles, phosphorescent minerals,
   organic-mineral hybrids, purple-green luminescence

4. FOSSILIZED - Ancient compressed minerals, amber deposits,
   petrified organic matter, golden-brown stratification

Respond ONLY with valid JSON (no markdown, no explanation):
{
    "biome": "CRYO|VOLCANIC|BIOLUMINESCENT|FOSSILIZED",
    "confidence": 0.0-1.0,
    "minerals_detected": ["mineral1", "mineral2"],
    "description": "Brief description of what you observe"
}
"""


@mcp.tool()
def analyze_geological(
    image_url: Annotated[
        str,
        Field(description="Cloud Storage URL (gs://...) of the soil sample image")
    ]
) -> dict:
    """
    Analyzes a soil sample image to identify mineral composition and classify the planetary biome.
    
    Args:
        image_url: Cloud Storage URL of the soil sample image (gs://bucket/path/image.png)
        
    Returns:
        dict with biome, confidence, minerals_detected, and description
    """
    logger.info(f">>> 🔬 Tool: 'analyze_geological' called for '{image_url}'")
    
    try:
        response = client.models.generate_content(
            model="gemini-2.5-flash",
            contents=[
                GEOLOGICAL_PROMPT,
                genai_types.Part.from_uri(file_uri=image_url, mime_type="image/png")
            ]
        )
        
        result = parse_json_response(response.text)
        logger.info(f"    ✓ Geological analysis complete: {result.get('biome', 'UNKNOWN')}")
        return result
        
    except Exception as e:
        logger.error(f"    ✗ Geological analysis failed: {str(e)}")
        return {"error": str(e), "biome": "UNKNOWN", "confidence": 0.0}

Créer l'outil d'analyse botanique

Cet outil analyse les enregistrements vidéo de la flore, y compris la piste audio.

👉✏️ Recherchez #REPLACE-BOTANICAL-TOOL et remplacez-le par :

BOTANICAL_PROMPT = """Analyze this alien flora video recording.

Pay attention to BOTH:
1. VISUAL elements: Plant appearance, movement patterns, colors, bioluminescence
2. AUDIO elements: Ambient sounds, rustling, organic noises, frequencies

Classify the PRIMARY biome (choose exactly one):

1. CRYO - Crystalline ice-plants, frost-covered vegetation, 
   crackling/tinkling sounds, slow brittle movements, blue-white flora

2. VOLCANIC - Heat-resistant plants, sulfur-adapted species,
   hissing/bubbling sounds, smoke-filtering vegetation, red-orange flora

3. BIOLUMINESCENT - Glowing plants, pulsing light patterns,
   humming/resonating sounds, reactive to stimuli, purple-green flora

4. FOSSILIZED - Ancient petrified plants, amber-preserved specimens,
   deep resonant sounds, minimal movement, golden-brown flora

Respond ONLY with valid JSON (no markdown, no explanation):
{
    "biome": "CRYO|VOLCANIC|BIOLUMINESCENT|FOSSILIZED",
    "confidence": 0.0-1.0,
    "species_detected": ["species1", "species2"],
    "audio_signatures": ["sound1", "sound2"],
    "description": "Brief description of visual and audio observations"
}
"""


@mcp.tool()
def analyze_botanical(
    video_url: Annotated[
        str,
        Field(description="Cloud Storage URL (gs://...) of the flora video recording")
    ]
) -> dict:
    """
    Analyzes a flora video recording (visual + audio) to identify plant species and classify the biome.
    
    Args:
        video_url: Cloud Storage URL of the flora video (gs://bucket/path/video.mp4)
        
    Returns:
        dict with biome, confidence, species_detected, audio_signatures, and description
    """
    logger.info(f">>> 🌿 Tool: 'analyze_botanical' called for '{video_url}'")
    
    try:
        response = client.models.generate_content(
            model="gemini-2.5-flash",
            contents=[
                BOTANICAL_PROMPT,
                genai_types.Part.from_uri(file_uri=video_url, mime_type="video/mp4")
            ]
        )
        
        result = parse_json_response(response.text)
        logger.info(f"    ✓ Botanical analysis complete: {result.get('biome', 'UNKNOWN')}")
        return result
        
    except Exception as e:
        logger.error(f"    ✗ Botanical analysis failed: {str(e)}")
        return {"error": str(e), "biome": "UNKNOWN", "confidence": 0.0}

Tester le serveur MCP localement

👉💻 Testez le serveur MCP :

cd ~/way-back-home/level_1/mcp-server
pip install -r requirements.txt
python main.py

Vous devriez obtenir le résultat suivant :

[INFO] Initialized Gemini client for project: your-project-id
[INFO] 🚀 Location Analyzer MCP Server starting on port 8080
[INFO] 📍 MCP endpoint: http://0.0.0.0:8080/mcp
[INFO] 🔧 Tools: analyze_geological, analyze_botanical

Serveur fastmcp

Le serveur FastMCP est désormais en cours d'exécution avec le transport HTTP. Appuyez sur Ctrl+C pour arrêter.

Déployer le serveur MCP sur Cloud Run

👉 💻 Déployer :

cd ~/way-back-home/level_1/mcp-server
source ~/way-back-home/set_env.sh

gcloud builds submit . \
  --config=cloudbuild.yaml \
  --substitutions=_REGION="$REGION",_REPO_NAME="$REPO_NAME",_SERVICE_ACCOUNT="$SERVICE_ACCOUNT"

Enregistrer l'URL du service

👉💻 Enregistrez l'URL du service :

export MCP_SERVER_URL=$(gcloud run services describe location-analyzer \
  --region=$REGION --format='value(status.url)')
echo "MCP Server URL: $MCP_SERVER_URL"

# Add to set_env.sh for later use
echo "export MCP_SERVER_URL=\"$MCP_SERVER_URL\"" >> ~/way-back-home/set_env.sh

Créer les agents spécialisés

Durée : 8 min

Vous allez maintenant créer trois agents spécialisés, chacun avec une seule responsabilité.

Créer l'agent Geological Analyst

👉✏️ Ouvrez agent/agents/geological_analyst.py et recherchez #REPLACE-GEOLOGICAL-AGENT. Remplacez-le par :

from google.adk.agents import Agent
from agent.tools.mcp_tools import get_geological_tool

geological_analyst = Agent(
    name="GeologicalAnalyst",
    model="gemini-2.5-flash",
    description="Analyzes soil samples to classify planetary biome based on mineral composition.",
    instruction="""You are a geological specialist analyzing alien soil samples.

## YOUR EVIDENCE TO ANALYZE
Soil sample URL: {soil_url}

## YOUR TASK
1. Call the analyze_geological tool with the soil sample URL above
2. Examine the results for mineral composition and biome indicators
3. Report your findings clearly

The four possible biomes are:
- CRYO: Frozen, icy minerals, blue/white coloring
- VOLCANIC: Magma, obsidian, volcanic rock, red/orange coloring
- BIOLUMINESCENT: Glowing, phosphorescent minerals, purple/green
- FOSSILIZED: Amber, ancient preserved matter, golden/brown

## REPORTING FORMAT
Always report your classification clearly:
"GEOLOGICAL ANALYSIS: [BIOME] (confidence: X%)"

Include a brief description of what you observed in the sample.

## IMPORTANT
- You do NOT synthesize with other evidence
- You do NOT confirm locations
- Just analyze the soil sample and report what you find
- Call the tool immediately with the URL provided above""",
    tools=[get_geological_tool()]
)

Créer l'agent Botanical Analyst

👉✏️ Ouvrez agent/agents/botanical_analyst.py et recherchez #REPLACE-BOTANICAL-AGENT. Remplacez-le par :

from google.adk.agents import Agent
from agent.tools.mcp_tools import get_botanical_tool

botanical_analyst = Agent(
    name="BotanicalAnalyst",
    model="gemini-2.5-flash",
    description="Analyzes flora recordings to classify planetary biome based on plant life and ambient sounds.",
    instruction="""You are a botanical specialist analyzing alien flora recordings.

## YOUR EVIDENCE TO ANALYZE
Flora recording URL: {flora_url}

## YOUR TASK
1. Call the analyze_botanical tool with the flora recording URL above
2. Pay attention to BOTH visual AND audio elements in the recording
3. Report your findings clearly

The four possible biomes are:
- CRYO: Frost ferns, crystalline plants, cold wind sounds, crackling ice
- VOLCANIC: Fire blooms, heat-resistant flora, crackling/hissing sounds
- BIOLUMINESCENT: Glowing fungi, luminescent plants, ethereal hum, chiming
- FOSSILIZED: Petrified trees, ancient formations, deep resonant sounds

## REPORTING FORMAT
Always report your classification clearly:
"BOTANICAL ANALYSIS: [BIOME] (confidence: X%)"

Include descriptions of what you SAW and what you HEARD.

## IMPORTANT
- You do NOT synthesize with other evidence
- You do NOT confirm locations
- Just analyze the flora recording and report what you find
- Call the tool immediately with the URL provided above""",
    tools=[get_botanical_tool()]
)

Créer l'agent Astronomical Analyst

Cet agent utilise une approche différente avec deux modèles d'outils :

  1. Local FunctionTool : Gemini Vision pour extraire les caractéristiques des étoiles
  2. OneMCP BigQuery : interrogez le catalogue d'étoiles via le MCP géré de Google.

👉✏️ Ouvrez agent/agents/astronomical_analyst.py et recherchez #REPLACE-ASTRONOMICAL-AGENT. Remplacez-le par :

from google.adk.agents import Agent
from agent.tools.star_tools import (
    extract_star_features_tool,
    get_bigquery_mcp_toolset,
)

# Get the BigQuery MCP toolset
bigquery_toolset = get_bigquery_mcp_toolset()

astronomical_analyst = Agent(
    name="AstronomicalAnalyst",
    model="gemini-2.5-flash",
    description="Analyzes star field images and queries the star catalog via OneMCP BigQuery.",
    instruction="""You are an astronomical specialist analyzing alien night skies.

## YOUR EVIDENCE TO ANALYZE
Star field URL: {stars_url}

## YOUR TWO TOOLS

### TOOL 1: extract_star_features (Local Gemini Vision)
Call this FIRST with the star field URL above.
Returns: "primary_star": "...", "nebula_type": "...", "stellar_color": "..."

### TOOL 2: BigQuery MCP (execute_query)
Call this SECOND with the results from Tool 1.
Use this exact SQL query (replace the placeholders with values from Step 1):

SELECT quadrant, biome, primary_star, nebula_type
FROM `{project_id}.way_back_home.star_catalog`
WHERE LOWER(primary_star) = LOWER('PRIMARY_STAR_FROM_STEP_1')
  AND LOWER(nebula_type) = LOWER('NEBULA_TYPE_FROM_STEP_1')
LIMIT 1

## YOUR WORKFLOW
1. Call extract_star_features with: {stars_url}
2. Get the primary_star and nebula_type from the result
3. Call execute_query with the SQL above (replacing placeholders)
4. Report the biome and quadrant from the query result

## BIOME REFERENCE
| Biome | Quadrant | Primary Star | Nebula Type |
|-------|----------|--------------|-------------|
| CRYO | NW | blue_giant | ice_blue |
| VOLCANIC | NE | red_dwarf_binary | fire |
| BIOLUMINESCENT | SW | green_pulsar | purple_magenta |
| FOSSILIZED | SE | yellow_sun | golden |

## REPORTING FORMAT
"ASTRONOMICAL ANALYSIS: [BIOME] in [QUADRANT] quadrant (confidence: X%)"

Include a description of the stellar features you observed.

## IMPORTANT
- You do NOT synthesize with other evidence
- You do NOT confirm locations
- Just analyze the stars and report what you find
- Start by calling extract_star_features with the URL above""",
    tools=[extract_star_features_tool, bigquery_toolset]
)

Créer les connexions de l'outil MCP

Durée : 8 min

Créez les wrappers d'outils qui se connectent à votre serveur MCP déployé.

Créer une connexion à l'outil MCP (MCP personnalisé)

Cela permet de se connecter à votre serveur FastMCP personnalisé déployé sur Cloud Run.

👉✏️ Ouvrez agent/tools/mcp_tools.py et recherchez #REPLACE-MCP-TOOL-CONNECTION. Remplacez-le par :

import os
import logging

from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams

logger = logging.getLogger(__name__)

MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL")

_mcp_toolset = None

def get_mcp_toolset():
    """Get the MCPToolset connected to the location-analyzer server."""
    global _mcp_toolset
    
    if _mcp_toolset is not None:
        return _mcp_toolset
    
    if not MCP_SERVER_URL:
        raise ValueError(
            "MCP_SERVER_URL not set. Please run:\n"
            "  export MCP_SERVER_URL='https://location-analyzer-xxx.a.run.app'"
        )
    
    # FastMCP exposes MCP protocol at /mcp endpoint
    mcp_endpoint = f"{MCP_SERVER_URL}/mcp"
    logger.info(f"[MCP Tools] Connecting to: {mcp_endpoint}")
    
    _mcp_toolset = MCPToolset(
        connection_params=StreamableHTTPConnectionParams(
            url=mcp_endpoint,
            timeout=120,  # 2 minutes for Gemini analysis
        )
    )
    
    return _mcp_toolset

def get_geological_tool():
    """Get the geological analysis tool from the MCP server."""
    return get_mcp_toolset()

def get_botanical_tool():
    """Get the botanical analysis tool from the MCP server."""
    return get_mcp_toolset()

Créer des outils d'analyse des étoiles (OneMCP BigQuery)

Cette section présente le modèle MCP géré. Au lieu d'écrire notre propre code client BigQuery, nous nous connectons au serveur OneMCP BigQuery de Google.

👉✏️ Ouvrez agent/tools/star_tools.py et recherchez #REPLACE-STAR-TOOLS. Remplacez-le par :

import os
import json
import logging

from google import genai
from google.genai import types as genai_types
from google.adk.tools import FunctionTool
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
import google.auth
import google.auth.transport.requests

logger = logging.getLogger(__name__)

# =============================================================================
# CONFIGURATION - Environment variables only
# =============================================================================

PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "")

if not PROJECT_ID:
    logger.warning("[Star Tools] GOOGLE_CLOUD_PROJECT not set")

# Initialize Gemini client for star feature extraction
genai_client = genai.Client(
    vertexai=True,
    project=PROJECT_ID or "placeholder",
    location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
)

logger.info(f"[Star Tools] Initialized for project: {PROJECT_ID}")

# =============================================================================
# OneMCP BigQuery Connection
# =============================================================================

BIGQUERY_MCP_URL = "https://bigquery.googleapis.com/mcp"

_bigquery_toolset = None

def get_bigquery_mcp_toolset():
    """
    Get the MCPToolset connected to Google's BigQuery MCP server.
    
    This uses OAuth 2.0 authentication with Application Default Credentials.
    The toolset provides access to BigQuery's pre-built MCP tools like:
    - execute_query: Run SQL queries
    - list_datasets: List available datasets
    - get_table_schema: Get table structure
   """
    global _bigquery_toolset
    
    if _bigquery_toolset is not None:
        return _bigquery_toolset
    
    logger.info("[Star Tools] Connecting to OneMCP BigQuery...")
    
    # Get OAuth credentials
    credentials, project_id = google.auth.default(
        scopes=["https://www.googleapis.com/auth/bigquery"]
    )
    
    # Refresh to get a valid token
    credentials.refresh(google.auth.transport.requests.Request())
    oauth_token = credentials.token
    
    # Configure headers for BigQuery MCP
    headers = {
        "Authorization": f"Bearer {oauth_token}",
        "x-goog-user-project": project_id or PROJECT_ID
    }
    
    # Create MCPToolset with StreamableHTTP connection
    _bigquery_toolset = MCPToolset(
        connection_params=StreamableHTTPConnectionParams(
            url=BIGQUERY_MCP_URL,
            headers=headers
        )
    )
    
    logger.info("[Star Tools] Connected to BigQuery MCP")
    return _bigquery_toolset


# =============================================================================
# Local FunctionTool: Star Feature Extraction
# =============================================================================
# This is a LOCAL tool that calls Gemini directly - demonstrating that
# you can mix local FunctionTools with MCP tools in the same agent.

STAR_EXTRACTION_PROMPT = """Analyze this alien night sky image and extract stellar features.

Identify:
1. PRIMARY STAR TYPE: blue_giant, red_dwarf, red_dwarf_binary, green_pulsar, yellow_sun, etc.
2. NEBULA TYPE: ice_blue, fire, purple_magenta, golden, etc.
3. STELLAR COLOR: blue_white, red_orange, green_purple, yellow_gold, etc.

Respond ONLY with valid JSON:
{"primary_star": "...", "nebula_type": "...", "stellar_color": "...", "description": "..."}
"""


def _parse_json_response(text: str) -> dict:
    """Parse JSON from Gemini response, handling markdown formatting."""
    cleaned = text.strip()
    if cleaned.startswith("```json"):
        cleaned = cleaned[7:]
    elif cleaned.startswith("```"):
        cleaned = cleaned[3:]
    if cleaned.endswith("```"):
        cleaned = cleaned[:-3]
    cleaned = cleaned.strip()
    
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError as e:
        logger.error(f"Failed to parse JSON: {e}")
        return {"error": f"Failed to parse response: {str(e)}"}


def extract_star_features(image_url: str) -> dict:
    """
    Extract stellar features from a star field image using Gemini Vision.
    
    This is a LOCAL FunctionTool - we call Gemini directly, not through MCP.
    The agent will use this alongside the BigQuery MCP tools.
    """
    logger.info(f"[Stars] Extracting features from: {image_url}")
    
    response = genai_client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[
            STAR_EXTRACTION_PROMPT,
            genai_types.Part.from_uri(file_uri=image_url, mime_type="image/png")
        ]
    )
    
    result = _parse_json_response(response.text)
    logger.info(f"[Stars] Extracted: primary_star={result.get('primary_star')}")
    return result


# Create the local FunctionTool
extract_star_features_tool = FunctionTool(extract_star_features)

Créer l'outil d'orchestration

Durée : 8 min

Créez maintenant l'orchestrateur de l'équipage parallèle et l'orchestrateur racine qui coordonnent tout.

Créer l'équipe d'analyse parallèle

Commençons par créer la fonction de rappel et l'agent parallèle qui exécute les spécialistes simultanément.

👉✏️ Ouvrez agent/agent.py et recherchez #REPLACE-PARALLEL-CREW. Remplacez-le par :

import os
import logging
import httpx

from google.adk.agents import Agent, ParallelAgent
from google.adk.agents.callback_context import CallbackContext

# Import specialist agents
from agent.agents.geological_analyst import geological_analyst
from agent.agents.botanical_analyst import botanical_analyst
from agent.agents.astronomical_analyst import astronomical_analyst

# Import confirmation tool
from agent.tools.confirm_tools import confirm_location_tool

logger = logging.getLogger(__name__)


# =============================================================================
# BEFORE AGENT CALLBACK - Fetches config and sets state
# =============================================================================

async def setup_participant_context(callback_context: CallbackContext) -> None:
    """
    Fetch participant configuration and populate state for all agents.
    
    This callback:
    1. Reads PARTICIPANT_ID and BACKEND_URL from environment
    2. Fetches participant data from the backend API
    3. Sets state values: soil_url, flora_url, stars_url, username, x, y, etc.
    4. Returns None to continue normal agent execution
    """
    participant_id = os.environ.get("PARTICIPANT_ID", "")
    backend_url = os.environ.get("BACKEND_URL", "https://api.waybackhome.dev")
    project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", "")
    
    logger.info(f"[Callback] Setting up context for participant: {participant_id}")
    
    # Set project_id and backend_url in state immediately
    callback_context.state["project_id"] = project_id
    callback_context.state["backend_url"] = backend_url
    callback_context.state["participant_id"] = participant_id
    
    if not participant_id:
        logger.warning("[Callback] No PARTICIPANT_ID set - using placeholder values")
        callback_context.state["username"] = "Explorer"
        callback_context.state["x"] = 0
        callback_context.state["y"] = 0
        callback_context.state["soil_url"] = "Not available - set PARTICIPANT_ID"
        callback_context.state["flora_url"] = "Not available - set PARTICIPANT_ID"
        callback_context.state["stars_url"] = "Not available - set PARTICIPANT_ID"
        return None
    
    # Fetch participant data from backend API
    try:
        url = f"{backend_url}/participants/{participant_id}"
        logger.info(f"[Callback] Fetching from: {url}")
        
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(url)
            response.raise_for_status()
            data = response.json()
        
        # Extract evidence URLs
        evidence_urls = data.get("evidence_urls", {})
        
        # Set all state values for sub-agents to access
        callback_context.state["username"] = data.get("username", "Explorer")
        callback_context.state["x"] = data.get("x", 0)
        callback_context.state["y"] = data.get("y", 0)
        callback_context.state["soil_url"] = evidence_urls.get("soil", "Not available")
        callback_context.state["flora_url"] = evidence_urls.get("flora", "Not available")
        callback_context.state["stars_url"] = evidence_urls.get("stars", "Not available")
        
        logger.info(f"[Callback] State populated for {data.get('username')}")
        
    except Exception as e:
        logger.error(f"[Callback] Error fetching participant config: {e}")
        callback_context.state["username"] = "Explorer"
        callback_context.state["x"] = 0
        callback_context.state["y"] = 0
        callback_context.state["soil_url"] = f"Error: {e}"
        callback_context.state["flora_url"] = f"Error: {e}"
        callback_context.state["stars_url"] = f"Error: {e}"
    
    return None


# =============================================================================
# PARALLEL ANALYSIS CREW
# =============================================================================

evidence_analysis_crew = ParallelAgent(
    name="EvidenceAnalysisCrew",
    description="Runs geological, botanical, and astronomical analysis in parallel.",
    sub_agents=[geological_analyst, botanical_analyst, astronomical_analyst]
)

Créer l'orchestrateur racine

Créez maintenant l'agent racine qui coordonne tout et utilise le rappel.

👉✏️ Dans le même fichier (agent/agent.py), recherchez #REPLACE-ROOT-ORCHESTRATOR. Remplacez-le par :

# =============================================================================
# ROOT ORCHESTRATOR
# =============================================================================

root_agent = Agent(
    name="MissionAnalysisAI",
    model="gemini-2.5-flash",
    description="Coordinates crash site analysis to confirm explorer location.",
    instruction="""You are the Mission Analysis AI coordinating a rescue operation.

## Explorer Information
- Name: {username}
- Coordinates: ({x}, {y})

## Evidence URLs (automatically provided to specialists via state)
- Soil sample: {soil_url}
- Flora recording: {flora_url}
- Star field: {stars_url}

## Your Workflow

### STEP 1: DELEGATE TO ANALYSIS CREW
Tell the EvidenceAnalysisCrew to analyze all the evidence.
The evidence URLs are already available to the specialists.

### STEP 2: COLLECT RESULTS
Each specialist will report:
- "GEOLOGICAL ANALYSIS: [BIOME] (confidence: X%)"
- "BOTANICAL ANALYSIS: [BIOME] (confidence: X%)"
- "ASTRONOMICAL ANALYSIS: [BIOME] in [QUADRANT] quadrant (confidence: X%)"

### STEP 3: APPLY 2-OF-3 AGREEMENT RULE
- If 2 or 3 specialists agree → that's the answer
- If all 3 disagree → use judgment based on confidence

### STEP 4: CONFIRM LOCATION
Call confirm_location with the determined biome.

## Biome Reference
| Biome | Quadrant | Key Characteristics |
|-------|----------|---------------------|
| CRYO | NW | Frozen, blue, ice crystals |
| VOLCANIC | NE | Magma, red/orange, obsidian |
| BIOLUMINESCENT | SW | Glowing, purple/green |
| FOSSILIZED | SE | Amber, golden, ancient |

## Response Style
Be encouraging and narrative! Celebrate when the beacon activates!
""",
    sub_agents=[evidence_analysis_crew],
    tools=[confirm_location_tool],
    before_agent_callback=setup_participant_context
)

Créer l'outil de confirmation de l'établissement

Cet outil utilise ToolContext pour lire les valeurs d'état définies par le rappel.

👉✏️ Dans agent/tools/confirm_tools.py, recherchez #REPLACE-CONFIRM-TOOL. Remplacez-le par :

import os
import logging
import requests

from google.adk.tools import FunctionTool
from google.adk.tools.tool_context import ToolContext

logger = logging.getLogger(__name__)

BIOME_TO_QUADRANT = {
    "CRYO": "NW",
    "VOLCANIC": "NE",
    "BIOLUMINESCENT": "SW",
    "FOSSILIZED": "SE"
}


def _get_actual_biome(x: int, y: int) -> tuple[str, str]:
    """Determine actual biome and quadrant from coordinates."""
    if x < 50 and y >= 50:
        return "NW", "CRYO"
    elif x >= 50 and y >= 50:
        return "NE", "VOLCANIC"
    elif x < 50 and y < 50:
        return "SW", "BIOLUMINESCENT"
    else:
        return "SE", "FOSSILIZED"


def confirm_location(biome: str, tool_context: ToolContext) -> dict:
    """
    Confirm the explorer's location and activate the rescue beacon.
    
    Uses ToolContext to read state values set by before_agent_callback.
    """
    # Read from state (set by before_agent_callback)
    participant_id = tool_context.state.get("participant_id", "")
    x = tool_context.state.get("x", 0)
    y = tool_context.state.get("y", 0)
    backend_url = tool_context.state.get("backend_url", "https://api.waybackhome.dev")
    
    # Fallback to environment variables
    if not participant_id:
        participant_id = os.environ.get("PARTICIPANT_ID", "")
    if not backend_url:
        backend_url = os.environ.get("BACKEND_URL", "https://api.waybackhome.dev")

    if not participant_id:
        return {"success": False, "message": "❌ No participant ID available."}

    biome_upper = biome.upper().strip()

    if biome_upper not in BIOME_TO_QUADRANT:
        return {"success": False, "message": f"❌ Unknown biome: {biome}"}

    # Get actual biome from coordinates
    actual_quadrant, actual_biome = _get_actual_biome(x, y)

    if biome_upper != actual_biome:
        return {
            "success": False,
            "message": f"❌ Mismatch! Analysis: {biome_upper}, Actual: {actual_biome}"
        }

    quadrant = BIOME_TO_QUADRANT[biome_upper]

    try:
        response = requests.patch(
            f"{backend_url}/participants/{participant_id}/location",
            params={"x": x, "y": y},
            timeout=10
        )
        response.raise_for_status()

        return {
            "success": True,
            "message": f"🔦 BEACON ACTIVATED!\n\nLocation: {biome_upper} in {quadrant}\nCoordinates: ({x}, {y})"
        }

    except requests.exceptions.ConnectionError:
        return {
            "success": True,
            "message": f"🔦 BEACON ACTIVATED! (Local)\n\nLocation: {biome_upper} in {quadrant}",
            "simulated": True
        }

    except Exception as e:
        return {"success": False, "message": f"❌ Failed: {str(e)}"}


confirm_location_tool = FunctionTool(confirm_location)

Tester avec l'UI Web ADK

Durée : 5 min

Testons maintenant l'ensemble du système multi-agents en local.

Démarrer le serveur Web ADK

👉💻 Définissez les variables d'environnement et démarrez le serveur Web ADK :

cd ~/way-back-home/level_1
source ~/way-back-home/set_env.sh

# Verify environment is set
echo "PARTICIPANT_ID: $PARTICIPANT_ID"
echo "MCP Server: $MCP_SERVER_URL"

# Start ADK web server
adk web

Vous devriez obtenir le résultat suivant :

+-----------------------------------------------------------------------------+
| ADK Web Server started                                                      |
|                                                                             |
| For local testing, access at http://localhost:8000.                         |
+-----------------------------------------------------------------------------+

INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Accéder à l'UI Web

👉 Dans la barre d'outils Cloud Shell (en haut à droite), cliquez sur l'icône Aperçu Web, puis sélectionnez Modifier le port.

icône Aperçu sur le Web

👉 Définissez le port sur 8000, puis cliquez sur Modifier et prévisualiser.

Boîte de dialogue &quot;Modifier le port&quot;

👉 L'UI Web de l'ADK s'ouvre. Sélectionnez agent dans le menu déroulant.

sélection d&#39;agents

Exécuter l'analyse

👉 Dans l'interface de chat, saisissez :

Analyze the evidence from my crash site and confirm my location to activate the beacon.

Regardez le système multi-agents en action :

Démonstration adk web

  1. before_agent_callback s'exécute en premier pour récupérer les données de vos participants.
  2. L'orchestrateur racine reçoit votre demande avec l'état renseigné.
  3. EvidenceAnalysisCrew activé (ParallelAgent)
  4. Trois spécialistes s'exécutent en parallèle à l'aide du modèle {key} :
    • GeologicalAnalyst → voit {soil_url} résolu à partir de l'état
    • BotanicalAnalyst → voit l'état {flora_url} résolu
    • AstronomicalAnalyst → voit {stars_url} et {project_id} résolus
  5. L'orchestrateur racine effectue la synthèse (accord à deux sur trois)
  6. confirm_location appelé avec ToolContext → "🔦 BALISE ACTIVÉE !"

Le panneau de trace à droite affiche toutes les interactions de l'agent et les appels d'outils.

👉 Appuyez sur Ctrl+C dans le terminal pour arrêter le serveur une fois les tests terminés.

Déployer dans Cloud Run

Durée : 5 min

Déployez maintenant votre système multi-agent sur Cloud Run pour la compatibilité A2A.

Déployer l'agent

👉💻 Déployez sur Cloud Run à l'aide de l'ADK CLI :

cd ~/way-back-home/level_1
source ~/way-back-home/set_env.sh

adk deploy cloud_run \
  --project=$GOOGLE_CLOUD_PROJECT \
  --region=$REGION \
  --service_name=mission-analysis-ai \
  --with_ui \
  --a2a \
  ./agent

Lorsque l'invite Allow unauthenticated invocations to [mission-analysis-ai] (y/N)? s'affiche, saisissez y pour autoriser l'accès public.

Le résultat doit ressembler à ce qui suit :

Building and deploying agent to Cloud Run...
✓ Container built successfully
✓ Deploying to Cloud Run...
✓ Service deployed: https://mission-analysis-ai-abc123-uc.a.run.app

Your agent is now live!

Définir des variables d'environnement sur Cloud Run

L'agent déployé doit avoir accès aux variables d'environnement. Mettez à jour le service :

👉💻 Définissez les variables d'environnement requises :

gcloud run services update mission-analysis-ai \
  --region=$REGION \
  --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$REGION,MCP_SERVER_URL=$MCP_SERVER_URL,BACKEND_URL=$BACKEND_URL,PARTICIPANT_ID=$PARTICIPANT_ID,GOOGLE_GENAI_USE_VERTEXAI=True"

Enregistrer l'URL de l'agent

👉💻 Obtenez l'URL déployée :

export AGENT_URL=$(gcloud run services describe mission-analysis-ai \
  --region=$REGION --format='value(status.url)')
echo "Agent URL: $AGENT_URL"

# Add to set_env.sh
echo "export LEVEL1_AGENT_URL=\"$AGENT_URL\"" >> ~/way-back-home/set_env.sh

Vérifier le déploiement

👉💻 Testez l'agent déployé en ouvrant l'URL dans votre navigateur (l'indicateur --with_ui a déployé l'interface Web ADK) ou testez-le via curl :

curl -X GET "$AGENT_URL/list-apps"

Une réponse listant votre agent devrait s'afficher.

Conclusion

Durée : 1 minute

Checklist de validation

✅ Serveur MCP
- [ ] Déployé sur Cloud Run
- [ ] L'outil analyze_geological fonctionne
- [ ] L'outil analyze_botanical fonctionne

✅ Agents spécialisés
- [ ] GeologicalAnalyst utilise {soil_url} à partir de l'état
- [ ] BotanicalAnalyst utilise {flora_url} à partir de l'état
- [ ] AstronomicalAnalyst utilise {stars_url} et {project_id} à partir de l'état

✅ before_agent_callback
- [ ] Récupère les données des participants à partir de l'API backend
- [ ] Définit les valeurs d'état pour tous les sous-agents
- [ ] Fonctionne avec PARTICIPANT_ID à partir de l'environnement

✅ ParallelAgent
- [ ] Les trois spécialistes s'exécutent simultanément
- [ ] L'état est partagé via InvocationContext

✅ Orchestrateur racine
- [ ] Synthétise avec un accord de 2 sur 3
- [ ] confirm_location utilise ToolContext pour l'état
- [ ] Le signal est activé !

✅ Déploiement
- [ ] Agent déployé sur Cloud Run
- [ ] Point de terminaison A2A accessible

✅ Carte du monde
- [ ] Le voyant est désormais LUMINEUX (et non plus faible)
- [ ] Le biome s'affiche au survol


🎉 Niveau 1 terminé !

Votre balise de détresse émet désormais à pleine puissance. Le signal triangulé traverse les interférences atmosphériques, une impulsion régulière qui dit : "Je suis là. J'ai survécu. Viens me trouver."

Mais vous n'êtes pas seul sur cette planète. À mesure que votre balise s'active, vous remarquez d'autres lumières qui s'allument à l'horizon : d'autres survivants, d'autres sites de crash, d'autres explorateurs qui ont réussi à s'en sortir.

position trouvée

Dans le niveau 2, vous apprendrez à traiter les signaux SOS entrants et à coordonner vos actions avec les autres survivants. Il ne s'agit pas seulement d'être retrouvé, mais aussi de se retrouver.


Dépannage

"MCP_SERVER_URL not set" bash export MCP_SERVER_URL=$(gcloud run services describe location-analyzer \ --region=$REGION --format='value(status.url)')

"PARTICIPANT_ID not set" bash source ~/way-back-home/set_env.sh echo $PARTICIPANT_ID

"Table BigQuery introuvable" bash python setup/setup_star_catalog.py

"Spécialistes demandant des URL" Cela signifie que le modèle {key} ne fonctionne pas. Vérifiez les points suivants : - before_agent_callback est-il défini sur l'agent racine ? - Les valeurs d'état du paramètre de rappel sont-elles correctes ? - Les sous-agents utilisent-ils {soil_url} (et non des f-strings) ?

"Les trois analyses ne sont pas d'accord" Régénérer les preuves : python generate_evidence.py

"L'agent ne répond pas dans l'ADK Web" - Vérifiez que le port 8000 est correct. - Vérifiez que MCP_SERVER_URL et PARTICIPANT_ID sont définis. - Recherchez les messages d'erreur dans le terminal.


Résumé de l'architecture

Composant Type Schéma Objectif
setup_participant_context Rappel before_agent_callback Récupérer la configuration, définir l'état
GeologicalAnalyst Agent Modèles {soil_url} Classification des sols
BotanicalAnalyst Agent Modèles {flora_url} Classification de la flore
AstronomicalAnalyst Agent {stars_url}, {project_id} Triangulation en étoile
confirm_location Outil Accès à l'état ToolContext Activer la balise
EvidenceAnalysisCrew ParallelAgent Composition des sous-agents Exécuter des spécialistes simultanément
MissionAnalysisAI Agent (racine) Orchestrateur + rappel Coordonner et synthétiser
location-analyzer Serveur FastMCP MCP personnalisé Analyses géologiques et botaniques
bigquery.googleapis.com/mcp OneMCP MCP géré Accès à BigQuery

Concepts clés maîtrisés

✅ before_agent_callback : récupérez la configuration avant l'exécution de l'agent.
✅ Modèles d'état {key} : accédez aux valeurs d'état dans les instructions de l'agent.
✅ ToolContext : accédez aux valeurs d'état dans les fonctions d'outil.
✅ Partage d'état : l'état parent est automatiquement disponible pour les sous-agents via InvocationContext.
✅ Architecture multi-agents : agents spécialisés avec des responsabilités uniques.
✅ ParallelAgent : exécution simultanée de tâches indépendantes.
✅ Serveur MCP personnalisé : votre propre serveur MCP sur Cloud Run.
✅ OneMCP BigQuery : modèle MCP géré pour l'accès aux bases de données.
✅ Déploiement cloud : déploiement sans état à l'aide de variables d'environnement.
✅ Préparation A2A : agent prêt pour la communication entre agents.


Pour les non-gamers : applications concrètes

"Localisation précise" représente l'analyse parallèle par des experts avec consensus, qui consiste à exécuter plusieurs analyses d'IA spécialisées en même temps et à synthétiser les résultats.

Applications d'entreprise

Cas d'utilisation Experts parallèles Règle de synthèse
Diagnostic médical Analyste d'images, analyste de symptômes, analyste de laboratoire Seuil de confiance de 2 sur 3
Détection de fraudes Analyste des transactions, analyste du comportement, analyste du réseau 1 signalement = examen
Traitement de documents Agent OCR, agent de classification, agent d'extraction Tout le monde doit être d'accord
Contrôle qualité Inspecteur visuel, analyste de capteurs, vérificateur de spécifications Passage 2 sur 3

Informations clés sur l'architecture

  1. before_agent_callback pour la configuration : récupérez la configuration une seule fois au début et remplissez l'état pour tous les sous-agents. Aucune lecture de fichier de configuration dans les sous-agents.

  2. Modèles d'état {key} : déclaratifs, clairs et idiomatiques. Pas de f-strings, pas d'importations, pas de manipulation de sys.path.

  3. Mécanismes de consensus : un accord à deux sur trois gère l'ambiguïté de manière robuste sans nécessiter un accord unanime.

  4. ParallelAgent pour les tâches indépendantes : lorsque les analyses ne dépendent pas les unes des autres, exécutez-les simultanément pour plus de rapidité.

  5. Deux modèles MCP : personnalisé (créez votre propre modèle) ou OneMCP (hébergé par Google). Les deux utilisent StreamableHTTP.

  6. Déploiement sans état : le même code fonctionne en local et une fois déployé. Variables d'environnement + API de backend = aucun fichier de configuration dans les conteneurs.


Et maintenant ?

Niveau 2 : Traitement du signal SOS →

Apprenez à traiter les signaux de détresse entrants d'autres survivants à l'aide de modèles basés sur des événements et d'une coordination plus avancée des agents.