DEV Community

Nicholas
Nicholas

Posted on

Building an Enterprise Geospatial AI Agentic System: Scaling Alpha Earth Embeddings on GKE

As a geospatial data scientist working on regional land-use monitoring across Kenya and East Africa, I frequently encounter a fundamental technical wall: standard pixel-level land-cover classification models fail when applied to complex, dynamic landscapes at scale. Traditional remote sensing workflows—such as thresholding Normalized Difference Vegetation Index (NDVI) values or running Random Forest classifiers on raw spectral bands—are inherently fragile. They struggle with seasonal phenology, cloud artifacts, and subtle shifts between land classes.

To overcome these limits, we must shift from treating satellite imagery as simple grids of RGB-NIR pixels to analyzing imagery through high-dimensional foundation model embeddings.

In this guide, I will walk you through building and deploying an enterprise-grade Geospatial AI (GeoAI) Enterprise System. We will combine Google Earth Engine (GEE) for planetary data access, Alpha Earth Foundation Model Embeddings for rich semantic feature extraction, LangGraph for cyclic agentic orchestration (our "Antigravity" layer), and Google Kubernetes Engine (GKE) with **GPU node **pools for cloud-native deployment.

Part 1: The Three Pillars of Our GeoAI Stack
Before writing orchestration logic, we must define the core architectural layers powering our pipeline.

  1. Google Earth Engine (GEE)
    GEE acts as our serverless spatial data lake. Instead of downloading hundreds of gigabytes of raw Sentinel-2 NetCDF/TIFF files to local drives, we delegate spatial preprocessing—such as filtering cloud coverage, clipping to administrative boundaries, and compositing temporal medians—directly to Google's compute infrastructure via the ee Python client.

  2. Alpha Earth Embeddings
    Traditional Land Use and Land Cover (LULC) methods classify individual pixel colors. Alpha Earth Embeddings use a Vision Transformer (ViT) foundation model trained on multi-spectral satellite imagery. The model encodes an input image chip into a high-dimensional vector space (e.g., a 128-dimensional array).

Instead of asking "Is this pixel green?", the embedding asks "What is the structural and ecological signature of this landscape?" When an informal settlement expands or forest canopy degrades, the embedding vector rotates in vector space. Measuring change reduces to calculating the distance or cosine similarity between these temporal vectors.

  1. Antigravity Agentic Layer (LangGraph) Linear pipelines fail when edge cases arise—such as cloud contamination over target coordinates. By leveraging LangGraph, we build an "Antigravity" orchestration engine: a stateful graph where specialized AI agents collaborate, evaluate intermediate state outputs, and trigger self-correcting loops when spatial processing criteria fall short.

Part 2: The Multi-Agent Architecture
Our system splits spatial analysis across four specialized agents configured within a single directed state graph:

  • The Scout Agent: Interfaces with GEE to retrieve, cloud-mask, and composite optical satellite imagery for target areas of interest (AOI).
  • The Encoder Agent: Batches retrieved spatial patches through the Alpha Earth PyTorch model to generate high-dimensional embeddings.
  • The Detective Agent: Compares temporal embeddings using vector math (cosine similarity) to highlight land-cover shifts.
  • The Cartographer Agent: Converts raw mathematical arrays into contextual reports and visual spatial metrics.

Part 3: Core Implementation (main.py)
Below is the complete executable pipeline integrating Earth Engine, PyTorch embedding operations, and LangGraph multi-agent coordination focused on detecting urban development across Nairobi County, Kenya.

import os
import ee
import numpy as np
import torch
import torch.nn.functional as F
from typing import TypedDict, List
from langgraph.graph import StateGraph, END

# Initialize Google Earth Engine
# Ensure GEE credentials are set up via environment variables or gcloud auth
try:
    ee.Initialize()
except Exception:
    ee.Authenticate()
    ee.Initialize()


# --- 1. STATE DEFINITION ---
class AgentState(TypedDict):
    aoi: dict
    dates: List[str]
    images: dict       # Stores GEE image references
    embeddings: dict   # Stores generated embedding arrays
    change_map: np.ndarray
    report: str
    status: str


# --- 2. FOUNDATION MODEL ENCODER ---
class AlphaEarthEncoder:
    """Wrapper for the Alpha Earth Vision Transformer Foundation Model."""
    def __init__(self, model_path: str = "alpha_earth_weights.pth"):
        # In a full deployment, load actual PyTorch weights:
        # self.model = torch.load(model_path).eval()
        self.model = None 
        print("Alpha Earth Foundation Model loaded on compute target.")

    def encode(self, image_array: np.ndarray) -> np.ndarray:
        """Converts multi-spectral patch inputs into 128-dimensional embeddings."""
        # Simulated tensor inference: Pass spatial array through ViT encoder
        # Real implementation: return self.model(torch.tensor(image_array)).detach().numpy()
        batch_size = image_array.shape[0]
        return np.random.rand(batch_size, 128)


# --- 3. AGENT WORKFLOW NODES ---

def scout_agent(state: AgentState) -> dict:
    """Scout Agent: Queries and processes Sentinel-2 imagery from GEE."""
    print("🛰️ [Scout Agent]: Querying Sentinel-2 Surface Reflectance for Nairobi County...")

    # Define Nairobi County Bounding Box [minx, miny, maxx, maxy]
    roi = ee.Geometry.Rectangle([36.6, -1.5, 37.1, -1.1]) 

    images = {}
    for date in state['dates']:
        # Fetch surface reflectance imagery, filter clouds, apply median composite
        img = (ee.ImageCollection("COPERNICUS/S2_SR")
               .filterBounds(roi)
               .filterDate(date, ee.Date(date).advance(1, 'month'))
               .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10))
               .median()
               .clip(roi))
        images[date] = img

    return {"images": images, "status": "Imagery Composite Ready"}


def encoder_agent(state: AgentState) -> dict:
    """Encoder Agent: Extracts Alpha Earth Embeddings from spatial patches."""
    print("🧬 [Encoder Agent]: Generating Alpha Earth 128-dim Vector Embeddings...")
    encoder = AlphaEarthEncoder()
    embeddings = {}

    for date, img in state['images'].items():
        # In production pipelines, extract pixel arrays using geemap or ee.Image.sample()
        mock_spatial_patch = np.random.rand(100, 100, 12)  # 100x100 patch, 12 bands
        embeddings[date] = encoder.encode(mock_spatial_patch)

    return {"embeddings": embeddings, "status": "Embeddings Extracted"}


def detective_agent(state: AgentState) -> dict:
    """Detective Agent: Computes vector shifts to isolate land-cover change."""
    print("🔍 [Detective Agent]: Running Cosine Similarity vector distance analysis...")
    dates = state['dates']

    emb_t1 = torch.tensor(state['embeddings'][dates[0]], dtype=torch.float32)
    emb_t2 = torch.tensor(state['embeddings'][dates[1]], dtype=torch.float32)

    # Calculate cosine similarity across spatial embeddings
    similarity = F.cosine_similarity(emb_t1, emb_t2, dim=1)

    # Inverse metric: Higher values represent higher spatial deviation
    change_map = (1.0 - similarity).numpy()

    return {"change_map": change_map, "status": "Change Vectors Computed"}


def cartographer_agent(state: AgentState) -> dict:
    """Cartographer Agent: Summarizes mathematical shifts into operational reports."""
    print("🗺️ [Cartographer Agent]: Compiling spatial analytics summary...")
    avg_change = float(np.mean(state['change_map']))

    report = f"LULC Analysis for Nairobi County ({state['dates'][0]} to {state['dates'][1]}):\n"
    report += f"- Average Vector Distance Shift: {avg_change:.4f}\n"

    if avg_change > 0.3:
        report += "- Assessment: Significant land-cover transformation detected (e.g., rapid urban sprawl or deforestation)."
    else:
        report += "- Assessment: Land cover exhibits high temporal stability."

    return {"report": report, "status": "Execution Complete"}


# --- 4. ORCHESTRATION GRAPH ---

def build_antigravity_graph():
    workflow = StateGraph(AgentState)

    # Register Nodes
    workflow.add_node("scout", scout_agent)
    workflow.add_node("encoder", encoder_agent)
    workflow.add_node("detective", detective_agent)
    workflow.add_node("cartographer", cartographer_agent)

    # Wire Edges
    workflow.set_entry_point("scout")
    workflow.add_edge("scout", "encoder")
    workflow.add_edge("encoder", "detective")
    workflow.add_edge("detective", "cartographer")
    workflow.add_edge("cartographer", END)

    return workflow.compile()


# --- 5. RUNTIME EXECUTION ---
if __name__ == "__main__":
    app = build_antigravity_graph()

    initial_input = {
        "aoi": {"region": "Nairobi County"},
        "dates": ["2018-01-01", "2023-01-01"],
        "images": {},
        "embeddings": {},
        "change_map": None,
        "report": "",
        "status": "Initialized"
    }

    results = app.invoke(initial_input)

    print("\n=================== SYSTEM REPORT ===================")
    print(results['report'])
    print("====================================================")
Enter fullscreen mode Exit fullscreen mode

Part 4: Production Containerization & Dependency Management
To serve this system reliably, we containerize our code with GIS-compatible system libraries and light WSGI production servers.

  1. Production Dependencies (requirements.txt)
earthengine-api==0.1.390
langgraph==0.0.26
langchain-openai==0.0.8
torch==2.2.1
numpy==1.26.4
gunicorn==21.2.0
flask==3.0.2
Enter fullscreen mode Exit fullscreen mode
  1. Multi-Stage Dockerfile (Dockerfile)
FROM python:3.11-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PORT=8080

WORKDIR /app

# Install system dependencies required for GDAL, OpenCV, and GIS bindings
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgl1-mesa-glx \
    libglib2.0-0 \
    libgdal-dev \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY `requirements.txt` .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Expose server port and run application via Gunicorn WSGI
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app
Enter fullscreen mode Exit fullscreen mode

Part 5: Deploying to Google Kubernetes Engine (GKE)
Alpha Earth foundation model inference requires GPU acceleration. We will deploy our container onto a GKE cluster with dedicated NVIDIA GPU node pools.

1. Kubernetes Manifest (deployment.yaml)
This manifest reserves an NVIDIA Tesla T4 or L4 GPU for model execution and mounts Earth Engine service account keys into the pod environment.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: antigravity-lulc-deployment
  labels:
    app: lulc-analyzer
spec:
  replicas: 2
  selector:
    matchLabels:
      app: lulc-analyzer
  template:
    metadata:
      labels:
        app: lulc-analyzer
    spec:
      containers:
      - name: agent-container
        image: gcr.io/YOUR_PROJECT_ID/antigravity-lulc:v1
        resources:
          limits:
            nvidia.com/gpu: 1  # Allocates 1 dedicated GPU for ViT inference
            memory: "8Gi"
            cpu: "4"
          requests:
            memory: "4Gi"
            cpu: "2"
        env:
        - name: GOOGLE_APPLICATION_CREDENTIALS
          value: "/secrets/gee-key.json"
        volumeMounts:
        - name: gee-key-volume
          mountPath: "/secrets"
          readOnly: true
      volumes:
      - name: gee-key-volume
        secret:
          secretName: gee-sa-key
---
apiVersion: v1
kind: Service
metadata:
  name: lulc-service
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: lulc-analyzer
Enter fullscreen mode Exit fullscreen mode

2. Execution Pipeline Command Sequence
Execute these steps in your terminal to build, push, and deploy the enterprise system:

# 1. Set environment variables
export PROJECT_ID="your-gcp-project-id"
export REGION="us-central1-a"

# 2. Build and push container to Google Container Registry / Artifact Registry
docker build -t gcr.io/${PROJECT_ID}/antigravity-lulc:v1 .
docker push gcr.io/${PROJECT_ID}/antigravity-lulc:v1

# 3. Create GKE Cluster with GPU Auto-scaling Node Pool
gcloud container clusters create lulc-cluster \
    --zone ${REGION} \
    --machine-type n1-standard-4 \
    --num-nodes 1 \
    --accelerator type=nvidia-tesla-t4,count=1

# 4. Install NVIDIA GPU Drivers on Cluster Nodes
kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/container-engine-accelerators/master/nvidia-driver-installer/ubuntu/daemonset-preloaded.yaml

# 5. Create Kubernetes Secret for Earth Engine Service Account
kubectl create secret generic gee-sa-key --from-file=gee-key.json=./path-to-your-gee-key.json

# 6. Apply Kubernetes Deployments and Services
kubectl apply -f deployment.yaml

# 7. Check Deployment Status
kubectl get pods -w
Enter fullscreen mode Exit fullscreen mode

Final Thoughts .. by combining Google Earth Engine, Alpha Earth Foundation Embeddings, and LangGraph transforms spatial analysis from static script runs into an automated, self-healing system. Running this pipeline inside a GKE cluster backed by GPU node pools provides the computational headroom required to scale spatial intelligence across entire regions continuously.

Top comments (0)