Agentes de datos y GraphRAG integrado en el dispositivo con Spanner Omni

1. Descripción general

En este codelab, se explican los aspectos básicos de Spanner Omni, por qué sirve como la base de datos definitiva para los agentes de IA autónomos y una implementación aproximada de extremo a extremo de un entrenador táctico y simulador de partidos de pickleball basado en agentes, implementado en el perímetro y potenciado por Spanner Omni (relacional y GQL de Graph), Python Flask y Gemini 3.8 Flash.

Problema

Las plataformas deportivas interactivas modernas y los entornos de videojuegos en tiempo real requieren que se manejen tres cargas de trabajo diferentes de forma simultánea:

Transacciones operativas de alta simultaneidad:

Reservas de canchas, membresías de jugadores, facturación y programas de torneos que requieren una estricta serialización ACID para eliminar los conflictos de reservas dobles.

Telemetría de grafos conectados:

Secuencias continuas de tiros de rally, cadenas de Markov de transición y gráficos de cobertura de zonas que requieren un recorrido de baja latencia sin canalizaciones de ETL voluminosas.

Capacidad de respuesta perimetral en menos de un milisegundo:

Simulaciones de física de ritmo rápido de 60 FPS en las que los viajes de ida y vuelta de la red de nube de área amplia (de 100 a 200 ms) introducirían una inestabilidad inaceptable.

Históricamente, los equipos de ingeniería resolvían esto uniendo arquitecturas fragmentadas que se veían más o menos así: PostgreSQL para los datos transaccionales, Neo4j para los grafos de tomas, Redis para el almacenamiento en caché perimetral y canalizaciones de sincronización en segundo plano. Esto genera un desvío de la arquitectura, una alta complejidad operativa y retrasos en la coherencia eventual.

Cómo lo hacemos

Implementamos Google Cloud Spanner Omni en el perímetro y lo combinamos con Google Gemini 3.8 Flash para crear el juego de pickleball de Spanner Omni, una plataforma de juegos deportivos de un solo motor que unifica la lógica empresarial relacional, la telemetría de tiros ISO GQL nativa y el entrenamiento táctico de IA en tiempo real.

Spanner Omni

Spanner Omni lleva el motor de base de datos distribuida a escala planetaria de Google, con escalabilidad horizontal, transacciones ACID y capacidades multimodelo (relacional, de grafos, vectorial y de estadísticas), a entornos autoadministrados, locales, de múltiples nubes y aislados. Spanner Omni es una edición descargable y autoadministrada de Google Cloud Spanner. Ofrece las capacidades principales de la base de datos distribuida de Spanner (fragmentación automática, replicación basada en Paxos y coherencia externa sólida) dondequiera que elijas ejecutarla:

acc871a470f63b29.png

Qué aprenderás

En este codelab, pasarás de las arquitecturas de bases de datos tradicionales y aisladas a una solución moderna y unificada de bases de datos perimetrales. Aprenderás a hacer lo siguiente:

  1. Implementar y organizar Spanner Omni de forma local con Docker para administrar cargas de trabajo relacionales de instancia única y alto rendimiento
  2. Diseña un esquema de grafo relacional y de ISO GQL unificado que asigne modelos de negocio transaccionales junto con telemetría de juegos de alta dimensión sin requerir sincronización de bases de datos externas.
  3. Implementar la serializabilidad ACID estricta dentro de las transacciones de lectura y escritura de Python para evitar de forma programática las condiciones de carrera y eliminar los conflictos de reservas duplicadas
  4. Crear una canalización de GraphRAG nativa que aproveche las consultas de grafos de ISO GQL integradas en SQL para enviar telemetría de partidos en tiempo real y con base contextual directamente a Google Gemini para obtener entrenamiento táctico instantáneo

Requisitos

  • Una máquina local estándar o una VM de desarrollo que ejecute Linux (Ubuntu 22.04 o una versión posterior), macOS o Windows.
  • Docker Desktop o Docker Engine instalados y en ejecución
  • Tener instalado Python 3.10 o una versión posterior
  • Un editor de texto o IDE (como VS Code o PyCharm)
  • Una clave de API de Gemini de Google activa (Nota: No se requiere una cuenta de facturación de Google Cloud para ejecutar Spanner Omni de forma local, ya que se ejecuta con una licencia de evaluación para desarrolladores autoadministrada y de nivel gratuito).

2. Implementación y configuración de Spanner Omni

En este paso, iniciaremos nuestra instancia de base de datos perimetral local. Spanner Omni se ejecuta dentro de un contenedor altamente optimizado, lo que te brinda capacidades relacionales a escala planetaria en tu laptop de desarrollador local.

  1. Abre la terminal y crea un volumen de Docker persistente para almacenar los esquemas de la base de datos y la telemetría del juego:
docker volume create spanner_omni_data
  1. Extrae e inicia la imagen de contenedor oficial de Spanner Omni configurada en el modo de desarrollador de servidor único:
docker run -d --name spanner-omni \
  -p 9010:9010 -p 9020:9020 \
  -v spanner_omni_data:/spanner \
  us-docker.pkg.dev/spanner-omni/images/spanner-omni:2026.r2.1-beta \
  start-single-server
  1. Asegúrate de que el contenedor se haya iniciado correctamente:
docker ps

🛠️ Solución rápida de problemas en una línea: Si el inicio falla porque los puertos 9010 o 9020 ya están en uso por procesos en segundo plano o ejecuciones de emuladores heredados, ejecuta esta secuencia de comandos de limpieza en tu terminal:

pkill -f "spanner" || true
fuser -k 9010/tcp 9020/tcp 2>/dev/null || true
docker rm -f spanner-local spanner-omni spanner-omni-local 2>/dev/null || true

3. Diseño de esquemas DDL de grafos relacionales y de GQL de ISO

Con Spanner Omni activo, podemos establecer nuestro esquema de base de datos. Este esquema actúa como nuestra fuente única de información, ya que unifica las entidades comerciales relacionales (como canchas, jugadores y reservas) con esquemas de telemetría de grafos rápidos y conectados (tiros, zonas y transiciones).

Para crear las tablas de operaciones y telemetría, haz lo siguiente:

  1. Conéctate a tu base de datos con la CLI de Spanner o el cliente de tu aplicación
  2. Ejecuta las siguientes sentencias de DDL

No es necesario que realices ninguna acción en este momento, ya que estamos abordando este problema en el código de la aplicación:

-- ============================================================================
-- 1. OPERATIONAL & FACILITY MANAGEMENT (RELATIONAL ACID)
-- ============================================================================

CREATE TABLE Courts (
    court_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    location STRING(100) NOT NULL,
    surface_type STRING(50) NOT NULL,
    is_indoor BOOL NOT NULL,
    hourly_rate FLOAT64 NOT NULL,
    has_lighting BOOL NOT NULL,
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (court_id);

CREATE TABLE Players (
    player_id INT64 NOT NULL,
    name STRING(100) NOT NULL,
    email STRING(100) NOT NULL,
    skill_level STRING(20) NOT NULL,
    phone STRING(50),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (player_id);

CREATE TABLE Bookings (
    booking_id INT64 NOT NULL,
    court_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    booking_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    end_hour INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_name STRING(100) NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (booking_id);

CREATE TABLE MatchRequests (
    match_id INT64 NOT NULL,
    player_id INT64 NOT NULL,
    player_name STRING(100) NOT NULL,
    court_id INT64,
    court_name STRING(100),
    play_date DATE NOT NULL,
    start_hour INT64 NOT NULL,
    skill_required STRING(20) NOT NULL,
    match_type STRING(20) NOT NULL,
    spots_open INT64 NOT NULL,
    status STRING(20) NOT NULL,
    notes STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (match_id);

-- ============================================================================
-- 2. TACTICAL GAME TELEMETRY & PROPERTY GRAPH (ISO GQL)
-- ============================================================================

CREATE TABLE CourtZones (
    zone_id STRING(50) NOT NULL,
    name STRING(100) NOT NULL,
    is_kitchen BOOL NOT NULL,
    side STRING(50) NOT NULL
) PRIMARY KEY (zone_id);

CREATE TABLE Shots (
    shot_id STRING(50) NOT NULL,
    player_id STRING(50) NOT NULL,
    shot_type STRING(100) NOT NULL,
    speed_mph FLOAT64,
    spin_type STRING(50),
    outcome STRING(50),
    from_zone_id STRING(50),
    to_zone_id STRING(50)
) PRIMARY KEY (shot_id);

CREATE TABLE ShotTransitions (
    transition_id STRING(50) NOT NULL,
    from_shot_id STRING(50) NOT NULL,
    to_shot_id STRING(50) NOT NULL,
    rally_depth INT64,
    pressure_level STRING(20)
) PRIMARY KEY (transition_id);

-- ============================================================================
-- 3. MATCH SESSIONS & AI COACHING HISTORY
-- ============================================================================

CREATE TABLE GameSessions (
    session_id STRING(50) NOT NULL,
    player_id INT64 NOT NULL,
    player_score INT64 NOT NULL,
    bot_score INT64 NOT NULL,
    bot_difficulty STRING(20) NOT NULL,
    winner STRING(20) NOT NULL,
    rally_length INT64 NOT NULL,
    fault_type STRING(100),
    coaching_summary STRING(MAX),
    created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (session_id);
cat << 'EOF' > Dockerfile
FROM ollama/ollama:latest
ENV OLLAMA_HOST=0.0.0.0:8080
ENV OLLAMA_MODELS=/root/.ollama
ENV MODEL=gemma4:e2b
RUN ollama serve & sleep 5 && ollama pull $MODEL
ENTRYPOINT ["ollama", "serve"]
EOF

💡 Por qué son importantes las marcas de tiempo de confirmación: Especificar OPTIONS (allow_commit_timestamp=true) habilita un orden monótono y preciso en microsegundos en todas las réplicas de nodos locales. Esto es fundamental para sincronizar los eventos de juego de alta velocidad (como las grabaciones de video de las decisiones de línea) con los registros de telemetría de tiros de la base de datos correspondiente.

4. Control de simultaneidad de ACID y prevención de reservas duplicadas

En los portales activos de reservas deportivas, varios usuarios suelen competir para reservar los mismos horarios de las canchas. Para evitar condiciones de carrera, escribiremos una transacción de lectura y escritura de Python en Spanner Omni para garantizar una serializabilidad absoluta y a prueba de reservas dobles.

No es necesario que realices ninguna acción por ahora, solo observa el código:

from google.cloud import spanner

def create_booking_transaction(transaction, booking_data):
    # 1. Query for existing overlapping confirmed bookings on the same court
    query = """
        SELECT booking_id, player_name, start_hour, end_hour
        FROM Bookings
        WHERE court_id = @court_id
          AND booking_date = @booking_date
          AND status = 'CONFIRMED'
          AND NOT (end_hour <= @start_hour OR start_hour >= @end_hour)
    """
    params = {
        "court_id": booking_data["court_id"],
        "booking_date": booking_data["booking_date"],
        "start_hour": booking_data["start_hour"],
        "end_hour": booking_data["end_hour"],
    }
    param_types = {
        "court_id": spanner.param_types.INT64,
        "booking_date": spanner.param_types.DATE,
        "start_hour": spanner.param_types.INT64,
        "end_hour": spanner.param_types.INT64,
    }
    
    results = list(transaction.execute_sql(query, params=params, param_types=param_types))
    
    if results:
        conflict = results[0]
        raise ValueError(
            f"Court conflict: Already booked by {conflict[1]} ({conflict[2]}:00-{conflict[3]}:00)"
        )
    
    # 2. Insert new confirmed booking with Spanner commit timestamp
    insert_stmt = """
        INSERT INTO Bookings (
            booking_id, court_id, player_id, booking_date, start_hour, end_hour,
            player_name, court_name, status, notes, created_at
        ) VALUES (
            @booking_id, @court_id, @player_id, @booking_date, @start_hour, @end_hour,
            @player_name, @court_name, 'CONFIRMED', @notes, PENDING_COMMIT_TIMESTAMP()
        )
    """
    # execute_update statement writing to Spanner Omni...
    transaction.execute_update(insert_stmt, params=booking_data, param_types=booking_types)

5. Física en tiempo real a 60 FPS y motor de bot de DUPR ajustable

El frontend de nuestra aplicación utiliza una simulación de lienzo HTML5 en 2.5D que se renderiza a 60 fotogramas por segundo (FPS). Esta renderización se ejecuta de forma completamente desacoplada de las capas de la base de datos para mantener un rendimiento fluido y vaciar la telemetría solo cuando finalizan las carreras.

El simulador cuenta con un motor de IA de OmniBot ajustable que aprovecha una matriz de dificultad basada en los niveles de DUPR (Dynamic Universal Pickleball Rating) del mundo real.

1640056a824b47e6.png

No es necesario que realices ninguna acción por el momento, solo observa los detalles.

6. ISO GQL Shot Telemetry Graph Processing

Al final de cada rally, el frontend publica el gráfico de trayectoria conectado en nuestro extremo de backend /api/rally-end. Cada toma se registra como un nodo relacional, y las secuencias se vinculan de forma dinámica a través de bordes de transición.

No es necesario que realices ninguna acción por el momento, solo observa el código:

@app.route("/api/execute-shot", methods=["POST"])
def execute_shot():
    data = request.json or {}
    shot_id = f"shot_{int(time.time()*1000)}"
    
    # 1. Insert the Shot Node
    insert_shot_sql = """
        INSERT INTO Shots (shot_id, player_id, shot_type, speed_mph, spin_type, outcome, from_zone_id, to_zone_id)
        VALUES (@shot_id, @player_id, @shot_type, @speed, @spin, @outcome, @from_zone, @to_zone)
    """
    
    # 2. If part of a continuous rally, link with a ShotTransition Edge
    if data.get("prev_shot_id"):
        transition_id = f"trans_{int(time.time()*1000)}"
        insert_trans_sql = """
            INSERT INTO ShotTransitions (transition_id, from_shot_id, to_shot_id, rally_depth, pressure_level)
            VALUES (@trans_id, @from_shot, @to_shot, @depth, @pressure)
        """
        # Executed within a single, unified Spanner Omni transaction...

Aprovechando el motor de consultas de grafos ISO GQL nativo de Spanner, un entrenador o un programa de análisis puede consultar patrones estructurales complejos y de saltos profundos de forma instantánea sin ejecutar uniones de bases de datos voluminosas:

-- Find highest-frequency transition sequences leading to player unforced errors
GRAPH PickleballTelemetry
MATCH (s1:Shots)-[t:ShotTransitions]->(s2:Shots)
WHERE s1.shot_type = 'Speedup' AND s2.outcome = 'FAULT_NET'
RETURN s1.speed_mph, s2.from_zone_id, COUNT(*) AS occurrences
ORDER BY occurrences DESC
LIMIT 5;

7. Asesoramiento táctico de la IA de Gemini y chat de RAG fundamentado

Ahora, activaremos el cerebro de nuestra aplicación: Google Gemini 3.8 Flash. Nuestro backend de Python usa el modelo para ofrecer tres funciones de IA en tiempo real basadas directamente en Spanner Omni.

El sistema usa Google Gemini 3.8 Flash para algunos flujos de trabajo de IA en tiempo real específicos:

Real-Time Rally Breakdown & Tactical Referee:

Después de cada punto, Gemini analiza la cadena de tiros, la dificultad del bot y el posicionamiento en la cancha.

Asistente de IA y analista de partidos fundamentado en varias tablas de Spanner:

Los usuarios pueden consultar los horarios de las canchas y las tarifas de las instalaciones, y preguntar "¿Cómo me fue contra OmniBot?" en lenguaje natural. El backend consulta de forma dinámica Spanner Omni en cuatro dominios (Courts, Bookings, GameSessions y Shots) para fundamentar Gemini sin alucinaciones.

API de análisis de rendimiento en tiempo real (/api/performance):

Agrega proporciones de victorias y derrotas en vivo, la profundidad promedio de los intercambios y las sesiones de juego recientes directamente de la tabla GameSessions de Spanner Omni para el panel del jugador.

No es necesario que realices ninguna acción por el momento, solo observa los detalles.

8. Configura, ejecuta y prueba

Iniciemos la aplicación completa en tu máquina local para verificar el sistema de extremo a extremo.

  1. Abre la terminal del host y clona el repositorio del proyecto:
git clone https://github.com/AbiramiSukumaran/pickleball

cd pickleball
  1. Sigue los pasos que se indican en la siguiente documentación para obtener tu clave de API de Gemini:

https://medium.com/google-cloud/get-api-key-from-google-ai-studio-b2d2059a171d

  1. Abre el archivo .env en tu editor y configura los detalles de tu entorno:
SPANNER_EMULATOR_HOST="localhost:9010"
SPANNER_PROJECT="PROJECT_ID"
SPANNER_INSTANCE="omni-instance"
SPANNER_DATABASE="pickleball-db"
GEMINI_API_KEY="REPLACE_WITH_YOUR_ACTUAL_GEMINI_KEY"
  1. Instala las dependencias de Python y ejecuta la aplicación:
pip install -r requirements.txt

python app.py
  1. Abre tu navegador web y ve a http://localhost:5000 para comenzar a jugar. Puedes reservar canchas en la pestaña de reservas, jugar contra el OmniBot de DUPR ajustable y consultar a tu analista de partidos de Gemini en tiempo real al costado de la cancha.

Abre el navegador web o haz clic en el vínculo a la app que se ejecuta en tu localhost.

a2d18f8288607c66.png

9. ¡Felicitaciones!

Al completar este codelab, presenciaste un cambio de paradigma en la ingeniería de bases de datos. Durante años, la creación de agentes responsivos en tiempo real en el perímetro (en fábricas, zonas militares o redes deportivas en canchas) obligó a realizar compromisos de diseño dolorosos: unir canalizaciones de ETL frágiles entre sistemas transaccionales aislados, nodos de grafos separados e índices de búsqueda de vectores.

Spanner Omni elimina esta desventaja, ya que combina la coherencia distribuida a escala global, las consultas de grafos de GQL de ISO y el rendimiento relacional en un solo contenedor local descargable. Sin importar tu dominio, Spanner Omni garantiza que tus agentes perimetrales tengan una verdad fundamental inquebrantable.