Cómo crear un asistente de revisión de código de IA para producción con el ADK de Google

1. La revisión de código nocturna

Son las 2 a.m.

Llevas horas depurando. La función parece correcta, pero algo anda mal. Conoces esa sensación: cuando el código debería funcionar, pero no lo hace, y ya no puedes ver por qué, porque lo has estado mirando durante demasiado tiempo.

def dfs_search_v1(graph, start, target):
    """Find if target is reachable from start."""
    visited = set()
    stack = start  # Looks innocent enough...
   
    while stack:
        current = stack.pop()
       
        if current == target:
            return True
           
        if current not in visited:
            visited.add(current)
           
            for neighbor in graph[current]:
                if neighbor not in visited:
                    stack.append(neighbor)
   
    return False

El recorrido del desarrollador de IA

Si estás leyendo esto, es probable que ya hayas experimentado la transformación que la IA aporta a la programación. Herramientas como Gemini Code Assist, Claude Code y Cursor cambiaron la forma en que escribimos código. Son increíbles para generar código estándar, sugerir implementaciones y acelerar el desarrollo.

Pero estás aquí porque quieres profundizar más. Quieres comprender cómo crear estos sistemas de IA, no solo usarlos. Quieres crear algo que cumpla con los siguientes requisitos:

  • Tiene un comportamiento predecible y rastreable
  • Se puede implementar en producción con confianza
  • Proporciona resultados coherentes en los que puedes confiar
  • Te muestra exactamente cómo toma decisiones

De consumidor a creador

architecture.png

Hoy, pasarás de usar herramientas de IA a crearlas. Crearás un sistema multiagente que haga lo siguiente:

  1. Analiza la estructura del código de forma determinística.
  2. Ejecuta pruebas reales para verificar el comportamiento
  3. Valida el cumplimiento del estilo con verificadores reales
  4. Sintetiza los hallazgos en comentarios prácticos
  5. Implementa en Google Cloud con observabilidad completa

2. Tu primera implementación de un agente

La pregunta del desarrollador

"Entiendo los LLMs y usé las APIs, pero ¿cómo paso de una secuencia de comandos de Python a un agente de IA de producción que se escale?".

Para responder esta pregunta, configuraremos tu entorno de forma adecuada y, luego, compilaremos un agente simple para comprender los conceptos básicos antes de profundizar en los patrones de producción.

Primero, la configuración esencial

Antes de crear cualquier agente, asegúrate de que tu entorno de Google Cloud esté listo.

¿Necesitas créditos de Google Cloud?

Haz clic en Activar Cloud Shell en la parte superior de la consola de Google Cloud (es el ícono con forma de terminal en la parte superior del panel de Cloud Shell).

texto alternativo

Busca tu ID del proyecto de Google Cloud:

  • Abre la consola de Google Cloud: https://console.cloud.google.com
  • Selecciona el proyecto que deseas usar para este taller en el menú desplegable de proyectos que se encuentra en la parte superior de la página.
  • Tu ID del proyecto se muestra en la tarjeta de información del proyecto en el panel texto alternativo

Paso 1: Configura tu ID del proyecto

En Cloud Shell, la herramienta de línea de comandos de gcloud ya está configurada. Ejecuta el siguiente comando para establecer tu proyecto activo. Esto usa la variable de entorno $GOOGLE_CLOUD_PROJECT, que se configura automáticamente en tu sesión de Cloud Shell.

gcloud config set project $GOOGLE_CLOUD_PROJECT

Paso 2: Verifica tu configuración

A continuación, ejecuta los siguientes comandos para confirmar que tu proyecto esté configurado correctamente y que te autenticaste.

# Confirm project is set
echo "Current project: $(gcloud config get-value project)"

# Check authentication status
gcloud auth list

Deberías ver impreso el ID de tu proyecto y tu cuenta de usuario con (ACTIVE) junto a ella.

Si tu cuenta no aparece como activa o si recibes un error de autenticación, ejecuta el siguiente comando para acceder:

gcloud auth application-default login

Paso 3: Habilita las APIs esenciales

Necesitamos al menos estas APIs para el agente básico:

gcloud services enable \
    aiplatform.googleapis.com \
    compute.googleapis.com

Esto puede demorar uno o dos minutos. En esta página verá lo siguiente:

Operation "operations/..." finished successfully.

Paso 4: Instala el ADK

# Install the ADK CLI
pip install google-adk --upgrade

# Verify installation
adk --version

Deberías ver un número de versión como 1.15.0 o superior.

Ahora crea tu agente básico

Con el entorno listo, creemos ese agente simple.

Paso 5: Usa ADK Create

adk create my_first_agent

Sigue las indicaciones interactivas:

Choose a model for the root agent:
1. gemini-2.5-flash
2. Other models (fill later)
Choose model (1, 2): 1

1. Google AI
2. Vertex AI
Choose a backend (1, 2): 2

Enter Google Cloud project ID [auto-detected-from-gcloud]:
Enter Google Cloud region [us-central1]:

Paso 6: Examina lo que se creó

cd my_first_agent
ls -la

Encontrarás tres archivos:

.env          # Configuration (auto-populated with your project)
__init__.py   # Package marker
agent.py      # Your agent definition

Paso 7: Verificación rápida de la configuración

# Verify the .env was created correctly
cat .env

# Should show something like:
# GOOGLE_CLOUD_PROJECT=your-project-id
# GOOGLE_CLOUD_LOCATION=us-central1
# GOOGLE_GENAI_USE_VERTEXAI=1

Si el ID del proyecto falta o es incorrecto, edita el archivo .env:

nano .env  # or use your preferred editor

Paso 8: Observa el código del agente

cat agent.py
from google.adk.agents.llm_agent import Agent

root_agent = Agent(
    model='gemini-2.5-flash',
    name='root_agent',
    description='A helpful assistant for user questions.',
    instruction='Answer user questions to the best of your knowledge',
)

Simple, limpia y minimalista. Este es tu "Hola mundo" de los agentes.

Prueba tu agente básico

Paso 9: Ejecuta tu agente

cd ..
adk run my_first_agent

Deberías ver algo como lo siguiente:

Log setup complete: /tmp/agents_log/agent.20250930_162430.log
To access latest log: tail -F /tmp/agents_log/agent.latest.log
[user]:

Paso 10: Prueba algunas búsquedas

En la terminal en la que se ejecuta adk run, verás un mensaje. Escribe tus preguntas:

[user]: Hey, what can you do?
[root_agent]: I can help answer questions, generate text, explain concepts, assist with problem-solving, and provide information on a wide range of topics. What would you like to know or discuss?

[user]: What's the current weather in Mountain View?
[root_agent]: I'm sorry, but I cannot provide real-time weather information as I don't have access to current data or weather services. To get the current weather in Mountain View, I recommend checking a weather website or app like weather.com or using a search engine.

Observa la limitación: no puede acceder a los datos actuales. Profundicemos un poco más:

[user]: Review this Python code: def add(a, b): return a + b
[root_agent]: This is a simple Python function that adds two numbers. Here's my review:

**Strengths:**
- Clean and concise implementation
- Follows Python conventions for simple operations
- Clear function name that describes its purpose

**Suggestions for improvement:**
- Add a docstring to document the function
- Consider adding type hints for better code clarity
- You might want to handle edge cases

Here's an enhanced version:
def add(a: float, b: float) -> float:
    """Add two numbers and return their sum."""
    return a + b

El agente puede hablar sobre código, pero ¿puede hacer lo siguiente?

  • ¿Analizar realmente el AST para comprender la estructura?
  • ¿Ejecutar pruebas para verificar que funcione?
  • ¿Verificar el cumplimiento de los estándares de estilo?
  • ¿Recuerdas tus opiniones anteriores?

No. Aquí es donde necesitamos la arquitectura.

🏃🚪 Salir con

Ctrl+C

cuando termines de explorar.

3. Cómo preparar tu espacio de trabajo de producción

La solución: Una arquitectura lista para producción

Ese agente simple demostró el punto de partida, pero un sistema de producción requiere una estructura sólida. Ahora configuraremos un proyecto completo que incorpore los principios de producción.

Configuración de la base

Ya configuraste tu proyecto de Google Cloud para el agente básico. Ahora, preparemos el espacio de trabajo de producción completo con todas las herramientas, los patrones y la infraestructura necesarios para un sistema real.

Paso 1: Obtén el proyecto estructurado

Primero, sal de cualquier adk run en ejecución con Ctrl+C y limpia:

# Clean up the basic agent
cd ~  # Make sure you're not inside my_first_agent
rm -rf my_first_agent

# Get the production scaffold
git clone https://github.com/ayoisio/adk-code-review-assistant.git
cd adk-code-review-assistant
git checkout codelab

Paso 2: Crea y activa el entorno virtual

# Create the virtual environment
python -m venv .venv

# Activate it
# On macOS/Linux:
source .venv/bin/activate
# On Windows:
# .venv\Scripts\activate

Verificación: Tu instrucción ahora debería mostrar (.venv) al principio.

Paso 3: Instala las dependencias

pip install -r code_review_assistant/requirements.txt

# Install the package in editable mode (enables imports)
pip install -e .

Esto instala lo siguiente:

  • google-adk: El framework del ADK
  • pycodestyle: Para la verificación de PEP 8
  • vertexai: Para la implementación en la nube
  • Otras dependencias de producción

La marca -e te permite importar módulos code_review_assistant desde cualquier lugar.

Paso 4: Configura tu entorno

# Copy the example environment file
cp .env.example .env

# Edit .env and replace the placeholders:
# - GOOGLE_CLOUD_PROJECT=your-project-id → your actual project ID
# - Keep other defaults as-is

Verificación: Verifica tu configuración:

cat .env

Debería mostrar lo siguiente:

GOOGLE_CLOUD_PROJECT=your-actual-project-id
GOOGLE_CLOUD_LOCATION=us-central1
GOOGLE_GENAI_USE_VERTEXAI=TRUE

Paso 5: Asegúrate de que la autenticación funcione

Como ya ejecutaste gcloud auth antes, solo verificaremos lo siguiente:

# Check current authentication
gcloud auth list

# Should show your account with (ACTIVE)
# If not, run:
gcloud auth application-default login

Paso 6: Habilita APIs de producción adicionales

Ya habilitamos las APIs básicas. Ahora, agrega los de producción:

gcloud services enable \
    sqladmin.googleapis.com \
    run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com \
    storage.googleapis.com \
    cloudtrace.googleapis.com

Esto permite lo siguiente:

  • Administrador de SQL: Para Cloud SQL si se usa Cloud Run
  • Cloud Run: Para la implementación sin servidores
  • Cloud Build: Para implementaciones automatizadas
  • Artifact Registry: Para imágenes de contenedor
  • Cloud Storage: Para artefactos y almacenamiento provisional
  • Cloud Trace: Para la observabilidad

Paso 7: Crea el repositorio de Artifact Registry

Nuestra implementación compilará imágenes de contenedores que necesitan un lugar:

gcloud artifacts repositories create code-review-assistant-repo \
    --repository-format=docker \
    --location=us-central1 \
    --description="Docker repository for Code Review Assistant"

Deberías ver lo siguiente:

Created repository [code-review-assistant-repo].

Si ya existe (quizás de un intento anterior), no hay problema. Verás un mensaje de error que puedes ignorar.

Paso 8: Otorga permisos de IAM

# Get your project number
PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT \
    --format="value(projectNumber)")

# Define the service account
SERVICE_ACCOUNT="${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com"

# Grant necessary roles
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/run.admin"

gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/iam.serviceAccountUser"

gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/cloudsql.admin"

gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role="roles/storage.admin"

Cada comando generará el siguiente resultado:

Updated IAM policy for project [your-project-id].

Tus logros

Tu espacio de trabajo de producción ya está completamente preparado:

✅ Proyecto de Google Cloud configurado y autenticado
✅ Se probó el agente básico para comprender las limitaciones
✅ Código del proyecto con marcadores de posición estratégicos listos
✅ Dependencias aisladas en el entorno virtual
✅ Todas las APIs necesarias habilitadas
✅ Registro de contenedores listo para las implementaciones
✅ Permisos de IAM configurados correctamente
✅ Variables de entorno configuradas correctamente

Ahora ya puedes crear un sistema de IA real con herramientas determinísticas, administración de estados y una arquitectura adecuada.

4. Cómo compilar tu primer agente

building-your-first-agent-diagram.png

Qué diferencia a las herramientas de los LLM

Cuando le preguntas a un LLM "¿Cuántas funciones hay en este código?", este usa la correlación de patrones y la estimación. Cuando usas una herramienta que llama a ast.parse() de Python, se analiza el árbol de sintaxis real, sin suposiciones y con el mismo resultado cada vez.

En esta sección, se compila una herramienta que analiza la estructura del código de forma determinística y, luego, se conecta a un agente que sabe cuándo invocarla.

Paso 1: Comprende la estructura

Analicemos la estructura que completarás.

👉 Abrir

code_review_assistant/tools.py

Verás la función analyze_code_structure con comentarios de marcadores de posición que indican dónde agregarás código. La función ya tiene la estructura básica. La mejorarás paso a paso.

Paso 2: Agrega almacenamiento de estado

El almacenamiento de estado permite que otros agentes de la canalización accedan a los resultados de tu herramienta sin volver a ejecutar el análisis.

👉 Buscar:

        # MODULE_4_STEP_2_ADD_STATE_STORAGE

👉 Reemplaza esa sola línea por lo siguiente:

        # Store code and analysis for other agents to access
        tool_context.state[StateKeys.CODE_TO_REVIEW] = code
        tool_context.state[StateKeys.CODE_ANALYSIS] = analysis
        tool_context.state[StateKeys.CODE_LINE_COUNT] = len(code.splitlines())

Paso 3: Agrega el análisis asíncrono con grupos de subprocesos

Nuestra herramienta debe analizar el AST sin bloquear otras operaciones. Agreguemos la ejecución asíncrona con grupos de subprocesos.

👉 Buscar:

        # MODULE_4_STEP_3_ADD_ASYNC

👉 Reemplaza esa sola línea por lo siguiente:

        # Parse in thread pool to avoid blocking the event loop
        loop = asyncio.get_event_loop()
        with ThreadPoolExecutor() as executor:
            tree = await loop.run_in_executor(executor, ast.parse, code)

Paso 4: Extrae información integral

Ahora, extraigamos las clases, las importaciones y las métricas detalladas, todo lo que necesitamos para una revisión de código completa.

👉 Buscar:

        # MODULE_4_STEP_4_EXTRACT_DETAILS

👉 Reemplaza esa sola línea por lo siguiente:

        # Extract comprehensive structural information
        analysis = await loop.run_in_executor(
            executor, _extract_code_structure, tree, code
        )

👉 Verifica la función

analyze_code_structure

en

tools.py

tiene un cuerpo central que se ve así:

# Parse in thread pool to avoid blocking the event loop
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as executor:
    tree = await loop.run_in_executor(executor, ast.parse, code)

    # Extract comprehensive structural information
    analysis = await loop.run_in_executor(
        executor, _extract_code_structure, tree, code
    )

# Store code and analysis for other agents to access
tool_context.state[StateKeys.CODE_TO_REVIEW] = code
tool_context.state[StateKeys.CODE_ANALYSIS] = analysis
tool_context.state[StateKeys.CODE_LINE_COUNT] = len(code.splitlines())

👉 Ahora, desplázate hasta el final de

tools.py

y busca lo siguiente:

# MODULE_4_STEP_4_HELPER_FUNCTION

👉 Reemplaza esa sola línea por la función de ayuda completa:

def _extract_code_structure(tree: ast.AST, code: str) -> Dict[str, Any]:
    """
    Helper function to extract structural information from AST.
    Runs in thread pool for CPU-bound work.
    """
    functions = []
    classes = []
    imports = []
    docstrings = []

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            func_info = {
                'name': node.name,
                'args': [arg.arg for arg in node.args.args],
                'lineno': node.lineno,
                'has_docstring': ast.get_docstring(node) is not None,
                'is_async': isinstance(node, ast.AsyncFunctionDef),
                'decorators': [d.id for d in node.decorator_list
                               if isinstance(d, ast.Name)]
            }
            functions.append(func_info)

            if func_info['has_docstring']:
                docstrings.append(f"{node.name}: {ast.get_docstring(node)[:50]}...")

        elif isinstance(node, ast.ClassDef):
            methods = []
            for item in node.body:
                if isinstance(item, ast.FunctionDef):
                    methods.append(item.name)

            class_info = {
                'name': node.name,
                'lineno': node.lineno,
                'methods': methods,
                'has_docstring': ast.get_docstring(node) is not None,
                'base_classes': [base.id for base in node.bases
                                 if isinstance(base, ast.Name)]
            }
            classes.append(class_info)

        elif isinstance(node, ast.Import):
            for alias in node.names:
                imports.append({
                    'module': alias.name,
                    'alias': alias.asname,
                    'type': 'import'
                })
        elif isinstance(node, ast.ImportFrom):
            imports.append({
                'module': node.module or '',
                'names': [alias.name for alias in node.names],
                'type': 'from_import',
                'level': node.level
            })

    return {
        'functions': functions,
        'classes': classes,
        'imports': imports,
        'docstrings': docstrings,
        'metrics': {
            'line_count': len(code.splitlines()),
            'function_count': len(functions),
            'class_count': len(classes),
            'import_count': len(imports),
            'has_main': any(f['name'] == 'main' for f in functions),
            'has_if_main': '__main__' in code,
            'avg_function_length': _calculate_avg_function_length(tree)
        }
    }


def _calculate_avg_function_length(tree: ast.AST) -> float:
    """Calculate average function length in lines."""
    function_lengths = []

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            if hasattr(node, 'end_lineno') and hasattr(node, 'lineno'):
                length = node.end_lineno - node.lineno + 1
                function_lengths.append(length)

    if function_lengths:
        return sum(function_lengths) / len(function_lengths)
    return 0.0

Paso 5: Conéctate con un agente

Ahora conectamos la herramienta a un agente que sabe cuándo usarla y cómo interpretar sus resultados.

👉 Abrir

code_review_assistant/sub_agents/review_pipeline/code_analyzer.py

👉 Buscar:

# MODULE_4_STEP_5_CREATE_AGENT

👉 Reemplaza esa sola línea por el agente de producción completo:

code_analyzer_agent = Agent(
    name="CodeAnalyzer",
    model=config.worker_model,
    description="Analyzes Python code structure and identifies components",
    instruction="""You are a code analysis specialist responsible for understanding code structure.

Your task:
1. Take the code submitted by the user (it will be provided in the user message)
2. Use the analyze_code_structure tool to parse and analyze it
3. Pass the EXACT code to your tool - do not modify, fix, or "improve" it
4. Identify all functions, classes, imports, and structural patterns
5. Note any syntax errors or structural issues
6. Store the analysis in state for other agents to use

CRITICAL:
- Pass the code EXACTLY as provided to the analyze_code_structure tool
- Do not fix syntax errors, even if obvious
- Do not add missing imports or fix indentation
- The goal is to analyze what IS there, not what SHOULD be there

When calling the tool, pass the code as a string to the 'code' parameter.
If the analysis fails due to syntax errors, clearly report the error location and type.

Provide a clear summary including:
- Number of functions and classes found
- Key structural observations
- Any syntax errors or issues detected
- Overall code organization assessment""",
    tools=[FunctionTool(func=analyze_code_structure)],
    output_key="structure_analysis_summary"
)

Prueba tu analizador de código

Ahora verifica que el analizador funcione correctamente.

👉 Ejecuta la secuencia de comandos de prueba:

python tests/test_code_analyzer.py

La secuencia de comandos de prueba carga automáticamente la configuración desde tu archivo .env con python-dotenv, por lo que no es necesario configurar manualmente las variables de entorno.

Resultado esperado:

INFO:code_review_assistant.config:Code Review Assistant Configuration Loaded:
INFO:code_review_assistant.config:  - GCP Project: your-project-id
INFO:code_review_assistant.config:  - Artifact Bucket: gs://your-project-artifacts
INFO:code_review_assistant.config:  - Models: worker=gemini-2.5-flash, critic=gemini-2.5-pro
Testing code analyzer...
INFO:code_review_assistant.tools:Tool: Analysis complete - 2 functions, 1 classes

=== Analyzer Response ===
The analysis of the provided code shows the following:

* **Functions Found:** 2
    * `add(a, b)`: A global function at line 2.
    * `multiply(self, x, y)`: A method within the `Calculator` class.

* **Classes Found:** 1
    * `Calculator`: A class defined at line 5. Contains one method, `multiply`.

* **Imports:** 0

* **Structural Patterns:** The code defines one global function and one class 
  with a single method. Both are simple, each with a single return statement.

* **Syntax Errors/Issues:** No syntax errors detected.

* **Overall Code Organization:** The code is well-organized for its small size, 
  clearly defining a function and a class with a method.

Qué acaba de suceder:

  1. La secuencia de comandos de prueba cargó automáticamente tu configuración de .env.
  2. Tu herramienta analyze_code_structure() analizó el código con el AST de Python
  3. El asistente _extract_code_structure() extrajo funciones, clases y métricas
  4. Los resultados se almacenaron en el estado de la sesión con constantes StateKeys.
  5. El agente de Code Analyzer interpretó los resultados y proporcionó un resumen.

Solución de problemas:

  • "No se encontró el módulo ‘code_review_assistant’": Ejecuta pip install -e . desde la raíz del proyecto
  • "Falta el argumento de entradas de clave": Verifica que tu .env tenga GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION y GOOGLE_GENAI_USE_VERTEXAI=true.

Qué compilaste

Ahora tienes un analizador de código listo para la producción que hace lo siguiente:

Analiza el AST de Python real: Es determinístico, no se basa en la correlación de patrones
Almacena los resultados en el estado: Otros agentes pueden acceder al análisis
Se ejecuta de forma asíncrona: No bloquea otras herramientas
Extrae información integral: Funciones, clases, importaciones, métricas
Controla los errores de forma correcta: Informa los errores de sintaxis con números de línea
Se conecta a un agente: El LLM sabe cuándo y cómo usarlo

Conceptos clave dominados

Herramientas vs. agentes:

  • Las herramientas realizan un trabajo determinístico (análisis del AST).
  • Los agentes deciden cuándo usar herramientas e interpretar resultados

Valor de retorno en comparación con el estado:

  • Devuelve lo que el LLM ve de inmediato
  • Estado: Lo que persiste para otros agentes

Constantes de claves de estado:

  • Cómo evitar errores ortográficos en sistemas multiagente
  • Actúan como contratos entre agentes
  • Es fundamental cuando los agentes comparten datos

Async + Thread Pools:

  • async def permite que las herramientas pausen la ejecución
  • Los grupos de subprocesos ejecutan el trabajo vinculado a la CPU en segundo plano
  • Juntos, mantienen la capacidad de respuesta del bucle de eventos

Funciones auxiliares:

  • Separa los asistentes de sincronización de las herramientas asíncronas
  • Hace que el código sea reutilizable y fácil de probar

Instrucciones para el agente:

  • Las instrucciones detalladas evitan errores comunes del LLM
  • Explícita sobre lo que NO se debe hacer (no corregir el código)
  • Pasos claros del flujo de trabajo para garantizar la coherencia

Pasos siguientes

En el módulo 5, agregarás lo siguiente:

  • Verificador de estilo que lee el código del estado
  • Ejecutor de pruebas que ejecuta las pruebas
  • Sintetizador de comentarios que combina todo el análisis

Verás cómo el estado fluye a través de una canalización secuencial y por qué el patrón de constantes es importante cuando varios agentes leen y escriben los mismos datos.

5. Cómo crear una canalización: varios agentes trabajando juntos

building-a-pipeline-multiple-agents-working-together-diagram.png

Introducción

En el módulo 4, creaste un solo agente que analiza la estructura del código. Sin embargo, la revisión integral del código requiere más que solo el análisis: necesitas la verificación de estilo, la ejecución de pruebas y la síntesis inteligente de comentarios.

Este módulo crea una canalización de 4 agentes que trabajan juntos de forma secuencial, y cada uno aporta un análisis especializado:

  1. Analizador de código (del módulo 4): Analiza la estructura
  2. Style Checker: Identifica incumplimientos de estilo
  3. Test Runner: Ejecuta y valida pruebas
  4. Feedback Synthesizer: Combina todo en comentarios prácticos

Concepto clave: El estado como canal de comunicación. Cada agente lee lo que escribieron los agentes anteriores en el estado, agrega su propio análisis y pasa el estado enriquecido al siguiente agente. El patrón de constantes del módulo 4 se vuelve fundamental cuando varios agentes comparten datos.

Vista previa de lo que crearás: Envía código desordenado → observa el flujo de estado a través de 4 agentes → recibe un informe integral con comentarios personalizados basados en patrones anteriores.

Paso 1: Agrega la herramienta y el agente de Style Checker

El verificador de estilo identifica los incumplimientos de la PEP 8 con pycodestyle, un verificador determinista, no una interpretación basada en LLM.

Agrega la herramienta de verificación de estilo

👉 Abrir

code_review_assistant/tools.py

👉 Buscar:

# MODULE_5_STEP_1_STYLE_CHECKER_TOOL

👉 Reemplaza esa sola línea por lo siguiente:

async def check_code_style(code: str, tool_context: ToolContext) -> Dict[str, Any]:
    """
    Checks code style compliance using pycodestyle (PEP 8).

    Args:
        code: Python source code to check (or will retrieve from state)
        tool_context: ADK tool context

    Returns:
        Dictionary containing style score and issues
    """
    logger.info("Tool: Checking code style...")

    try:
        # Retrieve code from state if not provided
        if not code:
            code = tool_context.state.get(StateKeys.CODE_TO_REVIEW, '')
            if not code:
                return {
                    "status": "error",
                    "message": "No code provided or found in state"
                }

        # Run style check in thread pool
        loop = asyncio.get_event_loop()
        with ThreadPoolExecutor() as executor:
            result = await loop.run_in_executor(
                executor, _perform_style_check, code
            )

        # Store results in state
        tool_context.state[StateKeys.STYLE_SCORE] = result['score']
        tool_context.state[StateKeys.STYLE_ISSUES] = result['issues']
        tool_context.state[StateKeys.STYLE_ISSUE_COUNT] = result['issue_count']

        logger.info(f"Tool: Style check complete - Score: {result['score']}/100, "
                    f"Issues: {result['issue_count']}")

        return result

    except Exception as e:
        error_msg = f"Style check failed: {str(e)}"
        logger.error(f"Tool: {error_msg}", exc_info=True)

        # Set default values on error
        tool_context.state[StateKeys.STYLE_SCORE] = 0
        tool_context.state[StateKeys.STYLE_ISSUES] = []

        return {
            "status": "error",
            "message": error_msg,
            "score": 0
        }

👉 Ahora desplázate hasta el final del archivo y busca lo siguiente:

# MODULE_5_STEP_1_STYLE_HELPERS

👉 Reemplaza esa sola línea por las funciones de ayuda:

def _perform_style_check(code: str) -> Dict[str, Any]:
    """Helper to perform style check in thread pool."""
    import io
    import sys

    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
        tmp.write(code)
        tmp_path = tmp.name

    try:
        # Capture stdout to get pycodestyle output
        old_stdout = sys.stdout
        sys.stdout = captured_output = io.StringIO()

        style_guide = pycodestyle.StyleGuide(
            quiet=False,  # We want output
            max_line_length=100,
            ignore=['E501', 'W503']
        )

        result = style_guide.check_files([tmp_path])

        # Restore stdout
        sys.stdout = old_stdout

        # Parse captured output
        output = captured_output.getvalue()
        issues = []

        for line in output.strip().split('\n'):
            if line and ':' in line:
                parts = line.split(':', 4)
                if len(parts) >= 4:
                    try:
                        issues.append({
                            'line': int(parts[1]),
                            'column': int(parts[2]),
                            'code': parts[3].split()[0] if len(parts) > 3 else 'E000',
                            'message': parts[3].strip() if len(parts) > 3 else 'Unknown error'
                        })
                    except (ValueError, IndexError):
                        pass

        # Add naming convention checks
        try:
            tree = ast.parse(code)
            naming_issues = _check_naming_conventions(tree)
            issues.extend(naming_issues)
        except SyntaxError:
            pass  # Syntax errors will be caught elsewhere

        # Calculate weighted score
        score = _calculate_style_score(issues)

        return {
            "status": "success",
            "score": score,
            "issue_count": len(issues),
            "issues": issues[:10],  # First 10 issues
            "summary": f"Style score: {score}/100 with {len(issues)} violations"
        }

    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)


def _check_naming_conventions(tree: ast.AST) -> List[Dict[str, Any]]:
    """Check PEP 8 naming conventions."""
    naming_issues = []

    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            # Skip private/protected methods and __main__
            if not node.name.startswith('_') and node.name != node.name.lower():
                naming_issues.append({
                    'line': node.lineno,
                    'column': node.col_offset,
                    'code': 'N802',
                    'message': f"N802 function name '{node.name}' should be lowercase"
                })
        elif isinstance(node, ast.ClassDef):
            # Check if class name follows CapWords convention
            if not node.name[0].isupper() or '_' in node.name:
                naming_issues.append({
                    'line': node.lineno,
                    'column': node.col_offset,
                    'code': 'N801',
                    'message': f"N801 class name '{node.name}' should use CapWords convention"
                })

    return naming_issues


def _calculate_style_score(issues: List[Dict[str, Any]]) -> int:
    """Calculate weighted style score based on violation severity."""
    if not issues:
        return 100

    # Define weights by error type
    weights = {
        'E1': 10,  # Indentation errors
        'E2': 3,  # Whitespace errors
        'E3': 5,  # Blank line errors
        'E4': 8,  # Import errors
        'E5': 5,  # Line length
        'E7': 7,  # Statement errors
        'E9': 10,  # Syntax errors
        'W2': 2,  # Whitespace warnings
        'W3': 2,  # Blank line warnings
        'W5': 3,  # Line break warnings
        'N8': 7,  # Naming conventions
    }

    total_deduction = 0
    for issue in issues:
        code_prefix = issue['code'][:2] if len(issue['code']) >= 2 else 'E2'
        weight = weights.get(code_prefix, 3)
        total_deduction += weight

    # Cap at 100 points deduction
    return max(0, 100 - min(total_deduction, 100))

Agrega el agente de Style Checker

👉 Abrir

code_review_assistant/sub_agents/review_pipeline/style_checker.py

👉 Buscar:

# MODULE_5_STEP_1_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def style_checker_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects state variables."""
    template = """You are a code style expert focused on PEP 8 compliance.

Your task:
1. Use the check_code_style tool to validate PEP 8 compliance
2. The tool will retrieve the ORIGINAL code from state automatically
3. Report violations exactly as found
4. Present the results clearly and confidently

CRITICAL:
- The tool checks the code EXACTLY as provided by the user
- Do not suggest the code was modified or fixed
- Report actual violations found in the original code
- If there are style issues, they should be reported honestly

Call the check_code_style tool with an empty string for the code parameter,
as the tool will retrieve the code from state automatically.

When presenting results based on what the tool returns:
- State the exact score from the tool results
- If score >= 90: "Excellent style compliance!"
- If score 70-89: "Good style with minor improvements needed"
- If score 50-69: "Style needs attention"
- If score < 50: "Significant style improvements needed"

List the specific violations found (the tool will provide these):
- Show line numbers, error codes, and messages
- Focus on the top 10 most important issues

Previous analysis: {structure_analysis_summary}

Format your response as:
## Style Analysis Results
- Style Score: [exact score]/100
- Total Issues: [count]
- Assessment: [your assessment based on score]

## Top Style Issues
[List issues with line numbers and descriptions]

## Recommendations
[Specific fixes for the most critical issues]"""

    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_5_STEP_1_STYLE_CHECKER_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

style_checker_agent = Agent(
    name="StyleChecker",
    model=config.worker_model,
    description="Checks Python code style against PEP 8 guidelines",
    instruction=style_checker_instruction_provider,
    tools=[FunctionTool(func=check_code_style)],
    output_key="style_check_summary"
)

Paso 2: Agrega el agente de Test Runner

El ejecutor de pruebas genera pruebas integrales y las ejecuta con el ejecutor de código integrado.

👉 Abrir

code_review_assistant/sub_agents/review_pipeline/test_runner.py

👉 Buscar:

# MODULE_5_STEP_2_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def test_runner_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects the code_to_review directly."""
    template = """You are a testing specialist who creates and runs tests for Python code.

THE CODE TO TEST IS:
{code_to_review}

YOUR TASK:
1. Understand what the function appears to do based on its name and structure
2. Generate comprehensive tests (15-20 test cases)
3. Execute the tests using your code executor
4. Analyze results to identify bugs vs expected behavior
5. Output a detailed JSON analysis

TESTING METHODOLOGY:
- Test with the most natural interpretation first
- When something fails, determine if it's a bug or unusual design
- Test edge cases, boundaries, and error scenarios
- Document any surprising behavior

Execute your tests and output ONLY valid JSON with this structure:
- "test_summary": object with "total_tests_run", "tests_passed", "tests_failed", "tests_with_errors", "critical_issues_found"
- "critical_issues": array of objects, each with "type", "description", "example_input", "expected_behavior", "actual_behavior", "severity"
- "test_categories": object with "basic_functionality", "edge_cases", "error_handling" (each containing "passed", "failed", "errors" counts)
- "function_behavior": object with "apparent_purpose", "actual_interface", "unexpected_requirements"
- "verdict": object with "status" (WORKING/BUGGY/BROKEN), "confidence" (high/medium/low), "recommendation"

Do NOT output the test code itself, only the JSON analysis."""

    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_5_STEP_2_TEST_RUNNER_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

test_runner_agent = Agent(
    name="TestRunner",
    model=config.critic_model,
    description="Generates and runs tests for Python code using safe code execution",
    instruction=test_runner_instruction_provider,
    code_executor=BuiltInCodeExecutor(),
    output_key="test_execution_summary"
)

Paso 3: Comprende la memoria para el aprendizaje entre sesiones

Antes de crear el sintetizador de comentarios, debes comprender la diferencia entre estado y memoria, dos mecanismos de almacenamiento diferentes con dos propósitos distintos.

Estado vs. memoria: La distinción clave

Aclararemos esto con un ejemplo concreto de una revisión de código:

Estado (solo en la sesión actual):

# Data from THIS review session
tool_context.state[StateKeys.STYLE_ISSUES] = [
    {"line": 5, "code": "E231", "message": "missing whitespace"},
    {"line": 12, "code": "E701", "message": "multiple statements"}
]
  • Alcance: Solo esta conversación
  • Propósito: Pasar datos entre agentes en la canalización actual
  • Vive en: objeto Session
  • Ciclo de vida: Se descarta cuando finaliza la sesión

Memoria (todas las sesiones anteriores):

# Learned from 50 previous reviews
"User frequently forgets docstrings on helper functions"
"User tends to write long functions (avg 45 lines)"
"User improved error handling after feedback in session #23"
  • Alcance: Todas las sesiones anteriores de este usuario
  • Objetivo: Aprender patrones y proporcionar comentarios personalizados
  • Vive en: MemoryService
  • Ciclo de vida: Persiste en todas las sesiones y se puede buscar

Por qué los comentarios necesitan ambas cosas:

Imagina que el sintetizador crea comentarios:

Usar solo el estado (revisión actual):

"Function `calculate_total` has no docstring."

Comentarios mecánicos y genéricos

Uso de State + Memory (patrones actuales y anteriores):

"Function `calculate_total` has no docstring. This is the 4th review
where helper functions lacked documentation. Consider adding docstrings
as you write functions, not afterwards - you mentioned in our last
session that you find it easier that way."

Las referencias contextuales y personalizadas mejoran con el tiempo.

Para las implementaciones de producción, tienes las siguientes opciones:

Opción 1: VertexAiMemoryBankService (avanzada)

  • Qué hace: Extracción de hechos significativos de conversaciones con tecnología de LLM
  • Búsqueda: Búsqueda semántica (comprende el significado, no solo las palabras clave)
  • Administración de la memoria: Consolida y actualiza automáticamente los recuerdos con el tiempo
  • Requisitos: Proyecto de Google Cloud y configuración de Agent Engine
  • Úsala cuando: Quieras recuerdos sofisticados, evolutivos y personalizados
  • Ejemplo: "El usuario prefiere la programación funcional" (extraído de 10 conversaciones sobre el estilo de código)

Opción 2: Continúa con InMemoryMemoryService y sesiones persistentes

  • Función: Almacena el historial completo de conversaciones para la búsqueda por palabras clave
  • Búsqueda: Concordancia básica de palabras clave en sesiones anteriores
  • Administración de la memoria: Tú controlas lo que se almacena (a través de add_session_to_memory).
  • Requisitos: Solo un SessionService persistente (como VertexAiSessionService o DatabaseSessionService)
  • Úsala cuando: Necesitas una búsqueda simple en conversaciones anteriores sin procesamiento de LLM.
  • Ejemplo: La búsqueda de "docstring" devuelve todas las sesiones en las que se menciona esa palabra.

Cómo se completa la memoria

Después de que se completa cada revisión de código, haz lo siguiente:

# At the end of a session (typically in your application code)
await memory_service.add_session_to_memory(session)

Qué sucede:

  • InMemoryMemoryService: Almacena los eventos de sesión completos para la búsqueda de palabras clave.
  • VertexAiMemoryBankService: El LLM extrae hechos clave y los consolida con recuerdos existentes.

Las sesiones futuras pueden consultar lo siguiente:

# In a tool, search for relevant past feedback
results = tool_context.search_memory("feedback about docstrings")

Paso 4: Agrega las herramientas y el agente de Feedback Synthesizer

El sintetizador de comentarios es el agente más sofisticado de la canalización. Orquesta tres herramientas, usa instrucciones dinámicas y combina el estado, la memoria y los artefactos.

Agrega las tres herramientas de sintetizador

👉 Abrir

code_review_assistant/tools.py

👉 Buscar:

# MODULE_5_STEP_4_SEARCH_PAST_FEEDBACK

👉 Reemplazar con la herramienta 1: Búsqueda en la memoria (versión de producción):

async def search_past_feedback(developer_id: str, tool_context: ToolContext) -> Dict[str, Any]:
    """
    Search for past feedback in memory service.

    Args:
        developer_id: ID of the developer (defaults to "default_user")
        tool_context: ADK tool context with potential memory service access

    Returns:
        Dictionary containing feedback search results
    """
    logger.info(f"Tool: Searching for past feedback for developer {developer_id}...")

    try:
        # Default developer ID if not provided
        if not developer_id:
            developer_id = tool_context.state.get(StateKeys.USER_ID, 'default_user')

        # Check if memory service is available
        if hasattr(tool_context, 'search_memory'):
            try:
                # Perform structured searches
                queries = [
                    f"developer:{developer_id} code review feedback",
                    f"developer:{developer_id} common issues",
                    f"developer:{developer_id} improvements"
                ]

                all_feedback = []
                patterns = {
                    'common_issues': [],
                    'improvements': [],
                    'strengths': []
                }

                for query in queries:
                    search_result = await tool_context.search_memory(query)

                    if search_result and hasattr(search_result, 'memories'):
                        for memory in search_result.memories[:5]:
                            memory_text = memory.text if hasattr(memory, 'text') else str(memory)
                            all_feedback.append(memory_text)

                            # Extract patterns
                            if 'style' in memory_text.lower():
                                patterns['common_issues'].append('style compliance')
                            if 'improved' in memory_text.lower():
                                patterns['improvements'].append('showing improvement')
                            if 'excellent' in memory_text.lower():
                                patterns['strengths'].append('consistent quality')

                # Store in state
                tool_context.state[StateKeys.PAST_FEEDBACK] = all_feedback
                tool_context.state[StateKeys.FEEDBACK_PATTERNS] = patterns

                logger.info(f"Tool: Found {len(all_feedback)} past feedback items")

                return {
                    "status": "success",
                    "feedback_found": True,
                    "count": len(all_feedback),
                    "summary": " | ".join(all_feedback[:3]) if all_feedback else "No feedback",
                    "patterns": patterns
                }

            except Exception as e:
                logger.warning(f"Tool: Memory search error: {e}")

        # Fallback: Check state for cached feedback
        cached_feedback = tool_context.state.get(StateKeys.USER_PAST_FEEDBACK_CACHE, [])
        if cached_feedback:
            tool_context.state[StateKeys.PAST_FEEDBACK] = cached_feedback
            return {
                "status": "success",
                "feedback_found": True,
                "count": len(cached_feedback),
                "summary": "Using cached feedback",
                "patterns": {}
            }

        # No feedback found
        tool_context.state[StateKeys.PAST_FEEDBACK] = []
        logger.info("Tool: No past feedback found")

        return {
            "status": "success",
            "feedback_found": False,
            "message": "No past feedback available - this appears to be a first submission",
            "patterns": {}
        }

    except Exception as e:
        error_msg = f"Feedback search error: {str(e)}"
        logger.error(f"Tool: {error_msg}", exc_info=True)

        tool_context.state[StateKeys.PAST_FEEDBACK] = []

        return {
            "status": "error",
            "message": error_msg,
            "feedback_found": False
        }

👉 Buscar:

# MODULE_5_STEP_4_UPDATE_GRADING_PROGRESS

👉 Reemplazar por la herramienta 2: Grading Tracker (versión de producción):

async def update_grading_progress(tool_context: ToolContext) -> Dict[str, Any]:
    """
    Updates grading progress counters and metrics in state.
    """
    logger.info("Tool: Updating grading progress...")

    try:
        current_time = datetime.now().isoformat()

        # Build all state changes
        state_updates = {}

        # Temporary (invocation-level) state
        state_updates[StateKeys.TEMP_PROCESSING_TIMESTAMP] = current_time

        # Session-level state
        attempts = tool_context.state.get(StateKeys.GRADING_ATTEMPTS, 0) + 1
        state_updates[StateKeys.GRADING_ATTEMPTS] = attempts
        state_updates[StateKeys.LAST_GRADING_TIME] = current_time

        # User-level persistent state
        lifetime_submissions = tool_context.state.get(StateKeys.USER_TOTAL_SUBMISSIONS, 0) + 1
        state_updates[StateKeys.USER_TOTAL_SUBMISSIONS] = lifetime_submissions
        state_updates[StateKeys.USER_LAST_SUBMISSION_TIME] = current_time

        # Calculate improvement metrics
        current_style_score = tool_context.state.get(StateKeys.STYLE_SCORE, 0)
        last_style_score = tool_context.state.get(StateKeys.USER_LAST_STYLE_SCORE, 0)
        score_improvement = current_style_score - last_style_score

        state_updates[StateKeys.USER_LAST_STYLE_SCORE] = current_style_score
        state_updates[StateKeys.SCORE_IMPROVEMENT] = score_improvement

        # Track test results if available
        test_results = tool_context.state.get(StateKeys.TEST_EXECUTION_SUMMARY, {})

        # Parse if it's a string
        if isinstance(test_results, str):
            try:
                test_results = json.loads(test_results)
            except:
                test_results = {}

        if test_results and test_results.get('test_summary', {}).get('total_tests_run', 0) > 0:
            summary = test_results['test_summary']
            total = summary.get('total_tests_run', 0)
            passed = summary.get('tests_passed', 0)
            if total > 0:
                pass_rate = (passed / total) * 100
                state_updates[StateKeys.USER_LAST_TEST_PASS_RATE] = pass_rate

        # Apply all updates atomically
        for key, value in state_updates.items():
            tool_context.state[key] = value

        logger.info(f"Tool: Progress updated - Attempt #{attempts}, "
                    f"Lifetime: {lifetime_submissions}")

        return {
            "status": "success",
            "session_attempts": attempts,
            "lifetime_submissions": lifetime_submissions,
            "timestamp": current_time,
            "improvement": {
                "style_score_change": score_improvement,
                "direction": "improved" if score_improvement > 0 else "declined"
            },
            "summary": f"Attempt #{attempts} recorded, {lifetime_submissions} total submissions"
        }

    except Exception as e:
        error_msg = f"Progress update error: {str(e)}"
        logger.error(f"Tool: {error_msg}", exc_info=True)

        return {
            "status": "error",
            "message": error_msg
        }

👉 Buscar:

# MODULE_5_STEP_4_SAVE_GRADING_REPORT

👉 Reemplaza con la herramienta 3: Artifact Saver (versión de producción):

async def save_grading_report(feedback_text: str, tool_context: ToolContext) -> Dict[str, Any]:
    """
    Saves a detailed grading report as an artifact.

    Args:
        feedback_text: The feedback text to include in the report
        tool_context: ADK tool context for state management

    Returns:
        Dictionary containing save status and details
    """
    logger.info("Tool: Saving grading report...")

    try:
        # Gather all relevant data from state
        code = tool_context.state.get(StateKeys.CODE_TO_REVIEW, '')
        analysis = tool_context.state.get(StateKeys.CODE_ANALYSIS, {})
        style_score = tool_context.state.get(StateKeys.STYLE_SCORE, 0)
        style_issues = tool_context.state.get(StateKeys.STYLE_ISSUES, [])

        # Get test results
        test_results = tool_context.state.get(StateKeys.TEST_EXECUTION_SUMMARY, {})

        # Parse if it's a string
        if isinstance(test_results, str):
            try:
                test_results = json.loads(test_results)
            except:
                test_results = {}

        timestamp = datetime.now().isoformat()

        # Create comprehensive report dictionary
        report = {
            'timestamp': timestamp,
            'grading_attempt': tool_context.state.get(StateKeys.GRADING_ATTEMPTS, 1),
            'code': {
                'content': code,
                'line_count': len(code.splitlines()),
                'hash': hashlib.md5(code.encode()).hexdigest()
            },
            'analysis': analysis,
            'style': {
                'score': style_score,
                'issues': style_issues[:5]  # First 5 issues
            },
            'tests': test_results,
            'feedback': feedback_text,
            'improvements': {
                'score_change': tool_context.state.get(StateKeys.SCORE_IMPROVEMENT, 0),
                'from_last_score': tool_context.state.get(StateKeys.USER_LAST_STYLE_SCORE, 0)
            }
        }

        # Convert report to JSON string
        report_json = json.dumps(report, indent=2)
        report_part = types.Part.from_text(text=report_json)

        # Try to save as artifact if the service is available
        if hasattr(tool_context, 'save_artifact'):
            try:
                # Generate filename with timestamp (replace colons for filesystem compatibility)
                filename = f"grading_report_{timestamp.replace(':', '-')}.json"

                # Save the main report
                version = await tool_context.save_artifact(filename, report_part)

                # Also save a "latest" version for easy access
                await tool_context.save_artifact("latest_grading_report.json", report_part)

                logger.info(f"Tool: Report saved as {filename} (version {version})")

                # Store report in state as well for redundancy
                tool_context.state[StateKeys.USER_LAST_GRADING_REPORT] = report

                return {
                    "status": "success",
                    "artifact_saved": True,
                    "filename": filename,
                    "version": str(version),
                    "size": len(report_json),
                    "summary": f"Report saved as {filename}"
                }

            except Exception as artifact_error:
                logger.warning(f"Artifact service error: {artifact_error}, falling back to state storage")
                # Continue to fallback below

        # Fallback: Store in state if artifact service is not available or failed
        tool_context.state[StateKeys.USER_LAST_GRADING_REPORT] = report
        logger.info("Tool: Report saved to state (artifact service not available)")

        return {
            "status": "success",
            "artifact_saved": False,
            "message": "Report saved to state only",
            "size": len(report_json),
            "summary": "Report saved to session state"
        }

    except Exception as e:
        error_msg = f"Report save error: {str(e)}"
        logger.error(f"Tool: {error_msg}", exc_info=True)

        # Still try to save minimal data to state
        try:
            tool_context.state[StateKeys.USER_LAST_GRADING_REPORT] = {
                'error': error_msg,
                'feedback': feedback_text,
                'timestamp': datetime.now().isoformat()
            }
        except:
            pass

        return {
            "status": "error",
            "message": error_msg,
            "artifact_saved": False,
            "summary": f"Failed to save report: {error_msg}"
        }

Crea el agente de sintetizador

👉 Abrir

code_review_assistant/sub_agents/review_pipeline/feedback_synthesizer.py

👉 Buscar:

# MODULE_5_STEP_4_INSTRUCTION_PROVIDER

👉 Reemplaza con el proveedor de instrucciones de producción:

async def feedback_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects state variables."""
    template = """You are an expert code reviewer and mentor providing constructive, educational feedback.

CONTEXT FROM PREVIOUS AGENTS:
- Structure analysis summary: {structure_analysis_summary}
- Style check summary: {style_check_summary}  
- Test execution summary: {test_execution_summary}

YOUR TASK requires these steps IN ORDER:
1. Call search_past_feedback tool with developer_id="default_user"
2. Call update_grading_progress tool with no parameters
3. Carefully analyze the test results to understand what really happened
4. Generate comprehensive feedback following the structure below
5. Call save_grading_report tool with the feedback_text parameter
6. Return the feedback as your final output

CRITICAL - Understanding Test Results:
The test_execution_summary contains structured JSON. Parse it carefully:
- tests_passed = Code worked correctly
- tests_failed = Code produced wrong output
- tests_with_errors = Code crashed
- critical_issues = Fundamental problems with the code

If critical_issues array contains items, these are serious bugs that need fixing.
Do NOT count discovering bugs as test successes.

FEEDBACK STRUCTURE TO FOLLOW:

## 📊 Summary
Provide an honest assessment. Be encouraging but truthful about problems found.

## ✅ Strengths  
List 2-3 things done well, referencing specific code elements.

## 📈 Code Quality Analysis

### Structure & Organization
Comment on code organization, readability, and documentation.

### Style Compliance
Report the actual style score and any specific issues.

### Test Results
Report the actual test results accurately:
- If critical_issues exist, report them as bugs to fix
- Be clear: "X tests passed, Y critical issues were found"
- List each critical issue
- Don't hide or minimize problems

## 💡 Recommendations for Improvement
Based on the analysis, provide specific actionable fixes.
If critical issues exist, fixing them is top priority.

## 🎯 Next Steps
Prioritized action list based on severity of issues.

## 💬 Encouragement
End with encouragement while being honest about what needs fixing.

Remember: Complete ALL steps including calling save_grading_report."""

    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_5_STEP_4_SYNTHESIZER_AGENT

👉 Reemplaza con:

feedback_synthesizer_agent = Agent(
    name="FeedbackSynthesizer",
    model=config.critic_model,
    description="Synthesizes all analysis into constructive, personalized feedback",
    instruction=feedback_instruction_provider,
    tools=[
        FunctionTool(func=search_past_feedback),
        FunctionTool(func=update_grading_progress),
        FunctionTool(func=save_grading_report)
    ],
    output_key="final_feedback"
)

Paso 5: Conecta la canalización

Ahora conecta los cuatro agentes en una canalización secuencial y crea el agente raíz.

👉 Abrir

code_review_assistant/agent.py

👉 Agrega las importaciones necesarias en la parte superior del archivo (después de las importaciones existentes):

from google.adk.agents import Agent, SequentialAgent
from code_review_assistant.sub_agents.review_pipeline.code_analyzer import code_analyzer_agent
from code_review_assistant.sub_agents.review_pipeline.style_checker import style_checker_agent
from code_review_assistant.sub_agents.review_pipeline.test_runner import test_runner_agent
from code_review_assistant.sub_agents.review_pipeline.feedback_synthesizer import feedback_synthesizer_agent

Tu archivo ahora debería verse así:

"""
Main agent orchestration for the Code Review Assistant.
"""

from google.adk.agents import Agent, SequentialAgent
from .config import config
from code_review_assistant.sub_agents.review_pipeline.code_analyzer import code_analyzer_agent
from code_review_assistant.sub_agents.review_pipeline.style_checker import style_checker_agent
from code_review_assistant.sub_agents.review_pipeline.test_runner import test_runner_agent
from code_review_assistant.sub_agents.review_pipeline.feedback_synthesizer import feedback_synthesizer_agent

# MODULE_5_STEP_5_CREATE_PIPELINE

# MODULE_6_STEP_5_CREATE_FIX_LOOP

# MODULE_6_STEP_5_UPDATE_ROOT_AGENT

👉 Buscar:

# MODULE_5_STEP_5_CREATE_PIPELINE

👉 Reemplaza esa sola línea por lo siguiente:

# Create sequential pipeline
code_review_pipeline = SequentialAgent(
    name="CodeReviewPipeline",
    description="Complete code review pipeline with analysis, testing, and feedback",
    sub_agents=[
        code_analyzer_agent,
        style_checker_agent,
        test_runner_agent,
        feedback_synthesizer_agent
    ]
)

# Root agent - coordinates the review pipeline
root_agent = Agent(
    name="CodeReviewAssistant",
    model=config.worker_model,
    description="An intelligent code review assistant that analyzes Python code and provides educational feedback",
    instruction="""You are a specialized Python code review assistant focused on helping developers improve their code quality.

When a user provides Python code for review:
1. Immediately delegate to CodeReviewPipeline and pass the code EXACTLY as it was provided by the user.
2. The pipeline will handle all analysis and feedback
3. Return ONLY the final feedback from the pipeline - do not add any commentary

When a user asks what you can do or asks general questions:
- Explain your capabilities for code review
- Do NOT trigger the pipeline for non-code messages

The pipeline handles everything for code review - just pass through its final output.""",
    sub_agents=[code_review_pipeline],
    output_key="assistant_response"
)

Paso 6: Prueba la canalización completa

Es hora de ver a los cuatro agentes trabajando juntos.

👉 Inicia el sistema:

adk web code_review_assistant

Después de ejecutar el comando adk web, deberías ver un resultado en la terminal que indica que se inició el servidor web del ADK, similar a este:

+-----------------------------------------------------------------------------+
| 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)

👉 A continuación, para acceder a la IU de desarrollo del ADK desde tu navegador, haz lo siguiente:

En el ícono de Vista previa en la Web (a menudo, parece un ojo o un cuadrado con una flecha) en la barra de herramientas de Cloud Shell (por lo general, en la parte superior derecha), selecciona Cambiar puerto. En la ventana emergente, establece el puerto en 8000 y haz clic en "Cambiar y obtener vista previa". Luego, Cloud Shell abrirá una nueva pestaña o ventana del navegador en la que se mostrará la IU de desarrollo del ADK.

webpreview

👉 El agente ahora se está ejecutando. La IU de desarrollo del ADK en tu navegador es tu conexión directa con el agente.

  • Selecciona tu objetivo: En el menú desplegable de la parte superior de la IU, elige el agente code_review_assistant.

agent-select

👉 Instrucción de prueba:

Please analyze the following:
def dfs_search_v1(graph, start, target):
    """Find if target is reachable from start."""
    visited = set()
    stack = start
   
    while stack:
        current = stack.pop()
       
        if current == target:
            return True
           
        if current not in visited:
            visited.add(current)
           
            for neighbor in graph[current]:
                if neighbor not in visited:
                    stack.append(neighbor)
   
    return False

👉 Mira la canalización de revisión de código en acción:

Cuando envías la función dfs_search_v1 con errores, no solo obtienes una respuesta. Estás viendo tu canalización de varios agentes en acción. El resultado de transmisión que ves es el resultado de cuatro agentes especializados que se ejecutan en secuencia, y cada uno se basa en el anterior.

A continuación, se incluye un desglose de lo que cada agente aporta a la revisión final y completa, convirtiendo los datos sin procesar en inteligencia práctica.

code-review-pipeline-in-action

1. El informe estructural del Analizador de código

Primero, el agente CodeAnalyzer recibe el código sin procesar. No adivina lo que hace el código, sino que usa la herramienta analyze_code_structure para realizar un análisis determinístico del árbol de sintaxis abstracta (AST).

Su resultado son datos puros y fácticos sobre la estructura del código:

The analysis of the provided code reveals the following:

Summary:
- Functions Found: 1
- Classes Found: 0

Key Structural Observations:
- A single function, dfs_search_v1, is defined.
- It includes a docstring: "Find if target is reachable from start."
- No syntax errors were detected.

Overall Code Organization Assessment:
- The code snippet is a well-defined, self-contained function.

Valor: Este paso inicial proporciona una base limpia y confiable para los demás agentes. Confirma que el código es un código de Python válido y, luego, identifica los componentes exactos que se deben revisar.

2. Auditoría de PEP 8 del verificador de estilo

A continuación, el agente StyleChecker toma el control. Lee el código del estado compartido y usa la herramienta check_code_style, que aprovecha el verificador de código pycodestyle.

Su resultado es una puntuación de calidad cuantificable y los incumplimientos específicos:

Style Analysis Results
- Style Score: 88/100
- Total Issues: 6
- Assessment: Good style with minor improvements needed

Top Style Issues
- Line 5, W293: blank line contains whitespace
- Line 19, W292: no newline at end of file

Valor: Este agente proporciona comentarios objetivos y no negociables basados en los estándares de la comunidad establecidos (PEP 8). El sistema de puntuación ponderada le indica de inmediato al usuario la gravedad de los problemas.

3. Descubrimiento de errores críticos del ejecutor de pruebas

Aquí es donde el sistema va más allá del análisis superficial. El agente TestRunner genera y ejecuta un conjunto integral de pruebas para validar el comportamiento del código.

Su resultado es un objeto JSON estructurado que contiene un veredicto condenatorio:

{
  "critical_issues": [
    {
      "type": "Critical Bug",
      "description": "The function's initialization `stack = start` is incorrect... When a common input like a string... is provided... the function crashes with an AttributeError.",
      "severity": "Critical"
    }
  ],
  "verdict": {
    "status": "BROKEN",
    "confidence": "high",
    "recommendation": "The function is fundamentally broken... the stack initialization line `stack = start` must be changed to `stack = [start]`."
  }
}

Valor: Esta es la estadística más importante. El agente no solo adivinó, sino que demostró que el código estaba dañado ejecutándolo. Descubrió un error de tiempo de ejecución sutil pero crítico que un revisor humano podría pasar por alto fácilmente y señaló la causa exacta y la corrección necesaria.

4. Informe final del sintetizador de comentarios

Por último, el agente FeedbackSynthesizer actúa como director. Toma los datos estructurados de los tres agentes anteriores y elabora un solo informe fácil de usar que es tanto analítico como alentador.

El resultado es la opinión final y pulida que ves:

📊 Summary
Great effort on implementing the Depth-First Search algorithm! ... However, a critical bug in the initialization of the stack prevents the function from working correctly...

 Strengths
- Good Algorithm Structure
- Correct Use of `visited` Set

📈 Code Quality Analysis
...
### Style Compliance
The style analysis returned a good score of 88/100.
...
### Test Results
The automated testing revealed a critical issue... The line `stack = start` directly assigns the input... which results in an `AttributeError`.

💡 Recommendations for Improvement
**Fix the Critical Stack Initialization Bug:**
- Incorrect Code: `stack = start`
- Correct Code: `stack = [start]`

💬 Encouragement
You are very close to a perfect implementation! The core logic of your DFS algorithm is sound, which is the hardest part.

Valor: Este agente transforma los datos técnicos en una experiencia educativa y útil. Prioriza el problema más importante (el error), lo explica con claridad, proporciona la solución exacta y lo hace con un tono alentador. Integra con éxito los hallazgos de todas las etapas anteriores en un todo coherente y valioso.

Este proceso de varias etapas demuestra el poder de una canalización basada en agentes. En lugar de una respuesta única y monolítica, obtienes un análisis en capas en el que cada agente realiza una tarea especializada y verificable. Esto genera una revisión que no solo es reveladora, sino también determinística, confiable y profundamente educativa.

👉💻 Cuando termines de realizar las pruebas, vuelve a la terminal del editor de Cloud Shell y presiona Ctrl+C para detener la IU de desarrollo del ADK.

Qué compilaste

Ahora tienes una canalización completa de revisión de código que hace lo siguiente:

Analiza la estructura del código: Análisis determinístico del AST con funciones de ayuda
Verifica el estilo: Puntuación ponderada con convenciones de nomenclatura
Ejecuta pruebas: Generación integral de pruebas con salida JSON estructurada
Sintetiza comentarios: Integra estado, memoria y artefactos
Realiza un seguimiento del progreso: Estado de varios niveles en invocaciones, sesiones y usuarios
Aprende con el tiempo: Servicio de memoria para patrones entre sesiones
Proporciona artefactos: Informes JSON descargables con un registro de auditoría completo

Conceptos clave dominados

Canalizaciones secuenciales:

  • Cuatro agentes que se ejecutan en orden estricto
  • Cada uno enriquece el estado para el siguiente.
  • Las dependencias determinan la secuencia de ejecución

Patrones de producción:

  • Separación de funciones auxiliares (sincronización en grupos de subprocesos)
  • Degradación elegante (estrategias de resguardo)
  • Administración de estados de varios niveles (temporal, de sesión y de usuario)
  • Proveedores de instrucciones dinámicas (adaptados al contexto)
  • Almacenamiento doble (redundancia de artefactos y estado)

El estado como comunicación:

  • Las constantes evitan errores de escritura en todos los agentes
  • output_key escribe resúmenes del agente en el estado
  • Los agentes posteriores leen a través de StateKeys
  • El estado fluye de forma lineal a través de la canalización

Memoria vs. Estado:

  • Estado: Datos de la sesión actual
  • Memoria: Patrones en las sesiones
  • Diferentes propósitos, diferentes vidas útiles

Organización de herramientas:

  • Agentes de una sola herramienta (analyzer, style_checker)
  • Ejecutores integrados (test_runner)
  • Coordinación de múltiples herramientas (sintetizador)

Estrategia de selección del modelo:

  • Modelo de trabajador: Tareas mecánicas (análisis, linting, enrutamiento)
  • Modelo de crítico: Tareas de razonamiento (pruebas, síntesis)
  • Optimización de costos a través de la selección adecuada

Pasos siguientes

En el módulo 6, compilarás la canalización de corrección:

  • Arquitectura de LoopAgent para correcciones iterativas
  • Condiciones de salida a través de la derivación
  • Acumulación de estado en las iteraciones
  • Lógica de validación y reintento
  • Integración con la canalización de revisión para ofrecer correcciones

Verás cómo los mismos patrones de estado se adaptan a flujos de trabajo iterativos complejos en los que los agentes intentan varias veces hasta tener éxito, y cómo coordinar varias canalizaciones en una sola aplicación.

6. Cómo agregar la canalización de corrección: arquitectura de bucle

adding-the-fix-pipeline-loop-architecture-diagram.png

Introducción

En el módulo 5, creaste una canalización de revisión secuencial que analiza el código y proporciona comentarios. Sin embargo, identificar los problemas es solo la mitad de la solución: los desarrolladores necesitan ayuda para corregirlos.

En este módulo, se compila una canalización de corrección automatizada que hace lo siguiente:

  1. Genera correcciones basadas en los resultados de la revisión.
  2. Valida las correcciones ejecutando pruebas integrales.
  3. Reintenta automáticamente si las correcciones no funcionan (hasta 3 intentos)
  4. Resultados de los informes con comparaciones de antes y después

Concepto clave: LoopAgent para reintentos automáticos. A diferencia de los agentes secuenciales que se ejecutan una vez, un LoopAgent repite sus subagentes hasta que se cumple una condición de salida o se alcanza la cantidad máxima de iteraciones. Las herramientas indican que la operación se realizó correctamente configurando tool_context.actions.escalate = True.

Vista previa de lo que crearás: Envía código con errores → la revisión identifica problemas → el bucle de corrección genera correcciones → las pruebas validan → se reintenta si es necesario → informe final integral.

Conceptos básicos: LoopAgent vs. Sequential

Canalización secuencial (módulo 5):

SequentialAgent(agents=[A, B, C])
# Executes: A → B → C → Done
  • Flujo unidireccional
  • Cada agente se ejecuta exactamente una vez.
  • Sin lógica de reintento

Canalización de bucle (módulo 6):

LoopAgent(agents=[A, B, C], max_iterations=3)
# Executes: A → B → C → (check exit) → A → B → C → (check exit) → ...
  • Flujo cíclico
  • Los agentes se pueden ejecutar varias veces
  • Se cierra cuando:
    • Una herramienta establece tool_context.actions.escalate = True (éxito)
    • Se alcanzó el límite de max_iterations (límite de seguridad)
    • Se produce una excepción no controlada (error).

Por qué usar bucles para corregir código:

Las correcciones de código suelen requerir varios intentos:

  • Primer intento: Corrige errores evidentes (tipos de variables incorrectos)
  • Segundo intento: Corrige los problemas secundarios que revelaron las pruebas (casos extremos).
  • Tercer intento: Ajusta y valida que todas las pruebas pasen

Sin un bucle, necesitarías una lógica condicional compleja en las instrucciones del agente. Con LoopAgent, el reintento es automático.

Comparación de arquitecturas:

Sequential (Module 5):
User → Review Pipeline → Feedback → Done

Loop (Module 6):
User → Review Pipeline → Feedback → Fix Pipeline
                                         ↓
                          ┌──────────────┴──────────────┐
                          │   Fix Attempt Loop (1-3x)   │
                          │  ┌─────────────────────┐    │
                          │  │ 1. Generate Fixes   │    │
                          │  │ 2. Test Fixes       │    │
                          │  │ 3. Validate & Exit? │────┼─→ If escalate=True
                          │  └─────────────────────┘    │      exit loop
                          │         ↓ If not            │
                          │    Try Again (max 3)        │
                          └─────────────────────────────┘
                                     ↓
                          4. Synthesize Final Report → Done

Paso 1: Agrega el agente de Code Fixer

El corrector de código genera código de Python corregido en función de los resultados de la revisión.

👉 Abrir

code_review_assistant/sub_agents/fix_pipeline/code_fixer.py

👉 Buscar:

# MODULE_6_STEP_1_CODE_FIXER_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def code_fixer_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects state variables."""
    template = """You are an expert code fixing specialist.

Original Code:
{code_to_review}

Analysis Results:
- Style Score: {style_score}/100
- Style Issues: {style_issues}
- Test Results: {test_execution_summary}

Based on the test results, identify and fix ALL issues including:
- Interface bugs (e.g., if start parameter expects wrong type)
- Logic errors (e.g., KeyError when accessing graph nodes)
- Style violations
- Missing documentation

YOUR TASK:
Generate the complete fixed Python code that addresses all identified issues.

CRITICAL INSTRUCTIONS:
- Output ONLY the corrected Python code
- Do NOT include markdown code blocks (```python)
- Do NOT include any explanations or commentary
- The output should be valid, executable Python code and nothing else

Common fixes to apply based on test results:
- If tests show AttributeError with 'pop', fix: stack = [start] instead of stack = start
- If tests show KeyError accessing graph, fix: use graph.get(current, [])
- Add docstrings if missing
- Fix any style violations identified

Output the complete fixed code now:"""

    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_6_STEP_1_CODE_FIXER_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

code_fixer_agent = Agent(
    name="CodeFixer",
    model=config.worker_model,
    description="Generates comprehensive fixes for all identified code issues",
    instruction=code_fixer_instruction_provider,
    code_executor=BuiltInCodeExecutor(),
    output_key="code_fixes"
)

Paso 2: Agrega el agente de Fix Test Runner

El ejecutor de pruebas de corrección valida las correcciones ejecutando pruebas integrales en el código corregido.

👉 Abrir

code_review_assistant/sub_agents/fix_pipeline/fix_test_runner.py

👉 Buscar:

# MODULE_6_STEP_2_FIX_TEST_RUNNER_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def fix_test_runner_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that uses the clean code from the previous step."""
    template = """You are responsible for validating the fixed code by running tests.

THE FIXED CODE TO TEST:
{code_fixes}

ORIGINAL TEST RESULTS: {test_execution_summary}

YOUR TASK:
1. Understand the fixes that were applied
2. Generate the same comprehensive tests (15-20 test cases)
3. Execute the tests on the FIXED code using your code executor
4. Compare results with original test results
5. Output a detailed JSON analysis

TESTING METHODOLOGY:
- Run the same tests that revealed issues in the original code
- Verify that previously failing tests now pass
- Ensure no regressions were introduced
- Document the improvement

Execute your tests and output ONLY valid JSON with this structure:
- "passed": number of tests that passed
- "failed": number of tests that failed  
- "total": total number of tests
- "pass_rate": percentage as a number
- "comparison": object with "original_pass_rate", "new_pass_rate", "improvement"
- "newly_passing_tests": array of test names that now pass
- "still_failing_tests": array of test names still failing

Do NOT output the test code itself, only the JSON analysis."""

    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_6_STEP_2_FIX_TEST_RUNNER_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

fix_test_runner_agent = Agent(
    name="FixTestRunner",
    model=config.critic_model,
    description="Runs comprehensive tests on fixed code to verify all issues are resolved",
    instruction=fix_test_runner_instruction_provider,
    code_executor=BuiltInCodeExecutor(),
    output_key="fix_test_execution_summary"
)

Paso 3: Agrega el agente de Fix Validator

El validador verifica si las correcciones se realizaron correctamente y decide si se debe salir del bucle.

Conceptos básicos sobre las herramientas

Primero, agrega las tres herramientas que necesita el validador.

👉 Abrir

code_review_assistant/tools.py

👉 Buscar:

# MODULE_6_STEP_3_VALIDATE_FIXED_STYLE

👉 Reemplaza con la herramienta 1: Validador de estilo:

async def validate_fixed_style(tool_context: ToolContext) -> Dict[str, Any]:
    """
    Validates style compliance of the fixed code.

    Args:
        tool_context: ADK tool context containing fixed code in state

    Returns:
        Dictionary with style validation results
    """
    logger.info("Tool: Validating style of fixed code...")

    try:
        # Get the fixed code from state
        code_fixes = tool_context.state.get(StateKeys.CODE_FIXES, '')
       
        # Try to extract from markdown if present
        if '```python' in code_fixes:
            start = code_fixes.rfind('```python') + 9
            end = code_fixes.rfind('```')
            if start < end:
                code_fixes = code_fixes[start:end].strip()

        if not code_fixes:
            return {
                "status": "error",
                "message": "No fixed code found in state"
            }

        # Store the extracted fixed code
        tool_context.state[StateKeys.CODE_FIXES] = code_fixes

        # Run style check on fixed code
        loop = asyncio.get_event_loop()
        with ThreadPoolExecutor() as executor:
            style_result = await loop.run_in_executor(
                executor, _perform_style_check, code_fixes
            )

        # Compare with original
        original_score = tool_context.state.get(StateKeys.STYLE_SCORE, 0)
        improvement = style_result['score'] - original_score

        # Store results
        tool_context.state[StateKeys.FIXED_STYLE_SCORE] = style_result['score']
        tool_context.state[StateKeys.FIXED_STYLE_ISSUES] = style_result['issues']

        logger.info(f"Tool: Fixed code style score: {style_result['score']}/100 "
                    f"(improvement: +{improvement})")

        return {
            "status": "success",
            "fixed_style_score": style_result['score'],
            "original_style_score": original_score,
            "improvement": improvement,
            "remaining_issues": style_result['issues'],
            "perfect_style": style_result['score'] == 100
        }

    except Exception as e:
        logger.error(f"Tool: Style validation failed: {e}", exc_info=True)
        return {
            "status": "error",
            "message": str(e)
        }

👉 Buscar:

# MODULE_6_STEP_3_COMPILE_FIX_REPORT

👉 Reemplazar con la herramienta 2: Compilador de informes:

async def compile_fix_report(tool_context: ToolContext) -> Dict[str, Any]:
    """
    Compiles comprehensive report of the fix process.

    Args:
        tool_context: ADK tool context with all fix pipeline data

    Returns:
        Comprehensive fix report
    """
    logger.info("Tool: Compiling comprehensive fix report...")

    try:
        # Gather all data
        original_code = tool_context.state.get(StateKeys.CODE_TO_REVIEW, '')
        code_fixes = tool_context.state.get(StateKeys.CODE_FIXES, '')

        # Test results
        original_tests = tool_context.state.get(StateKeys.TEST_EXECUTION_SUMMARY, {})
        fixed_tests = tool_context.state.get(StateKeys.FIX_TEST_EXECUTION_SUMMARY, {})

        # Parse if strings
        if isinstance(original_tests, str):
            try:
                original_tests = json.loads(original_tests)
            except:
                original_tests = {}

        if isinstance(fixed_tests, str):
            try:
                fixed_tests = json.loads(fixed_tests)
            except:
                fixed_tests = {}

        # Extract pass rates
        original_pass_rate = 0
        if original_tests:
            if 'pass_rate' in original_tests:
                original_pass_rate = original_tests['pass_rate']
            elif 'test_summary' in original_tests:
                # Handle test_runner_agent's JSON structure
                summary = original_tests['test_summary']
                total = summary.get('total_tests_run', 0)
                passed = summary.get('tests_passed', 0)
                if total > 0:
                    original_pass_rate = (passed / total) * 100
            elif 'passed' in original_tests and 'total' in original_tests:
                if original_tests['total'] > 0:
                    original_pass_rate = (original_tests['passed'] / original_tests['total']) * 100

        fixed_pass_rate = 0
        all_tests_pass = False
        if fixed_tests:
            if 'pass_rate' in fixed_tests:
                fixed_pass_rate = fixed_tests['pass_rate']
                all_tests_pass = fixed_tests.get('failed', 1) == 0
            elif 'passed' in fixed_tests and 'total' in fixed_tests:
                if fixed_tests['total'] > 0:
                    fixed_pass_rate = (fixed_tests['passed'] / fixed_tests['total']) * 100
                all_tests_pass = fixed_tests.get('failed', 0) == 0

        # Style scores
        original_style = tool_context.state.get(StateKeys.STYLE_SCORE, 0)
        fixed_style = tool_context.state.get(StateKeys.FIXED_STYLE_SCORE, 0)

        # Calculate improvements
        test_improvement = {
            'original_pass_rate': original_pass_rate,
            'fixed_pass_rate': fixed_pass_rate,
            'improvement': fixed_pass_rate - original_pass_rate,
            'all_tests_pass': all_tests_pass
        }

        style_improvement = {
            'original_score': original_style,
            'fixed_score': fixed_style,
            'improvement': fixed_style - original_style,
            'perfect_style': fixed_style == 100
        }

        # Determine overall status
        if all_tests_pass and style_improvement['perfect_style']:
            fix_status = 'SUCCESSFUL'
            status_emoji = '✅'
        elif test_improvement['improvement'] > 0 or style_improvement['improvement'] > 0:
            fix_status = 'PARTIAL'
            status_emoji = '⚠️'
        else:
            fix_status = 'FAILED'
            status_emoji = '❌'

        # Build comprehensive report
        report = {
            'status': fix_status,
            'status_emoji': status_emoji,
            'timestamp': datetime.now().isoformat(),
            'original_code': original_code,
            'code_fixes': code_fixes,
            'improvements': {
                'tests': test_improvement,
                'style': style_improvement
            },
            'summary': f"{status_emoji} Fix Status: {fix_status}\n"
                      f"Tests: {original_pass_rate:.1f}% → {fixed_pass_rate:.1f}%\n"
                      f"Style: {original_style}/100 → {fixed_style}/100"
        }

        # Store report in state
        tool_context.state[StateKeys.FIX_REPORT] = report
        tool_context.state[StateKeys.FIX_STATUS] = fix_status

        logger.info(f"Tool: Fix report compiled - Status: {fix_status}")
        logger.info(f"Tool: Test improvement: {original_pass_rate:.1f}% → {fixed_pass_rate:.1f}%")
        logger.info(f"Tool: Style improvement: {original_style} → {fixed_style}")

        return {
            "status": "success",
            "fix_status": fix_status,
            "report": report
        }

    except Exception as e:
        logger.error(f"Tool: Failed to compile fix report: {e}", exc_info=True)
        return {
            "status": "error",
            "message": str(e)
        }

👉 Buscar:

# MODULE_6_STEP_3_EXIT_FIX_LOOP

👉 Reemplaza con Tool 3: Loop Exit Signal:

def exit_fix_loop(tool_context: ToolContext) -> Dict[str, Any]:
    """
    Signal that fixing is complete and should exit the loop.
   
    Args:
        tool_context: ADK tool context
       
    Returns:
        Confirmation message
    """
    logger.info("Tool: Setting escalate flag to exit fix loop")
   
    # This is the critical line that exits the LoopAgent
    tool_context.actions.escalate = True
   
    return {
        "status": "success",
        "message": "Fix complete, exiting loop"
    }

Crea el agente de Validator

👉 Abrir

code_review_assistant/sub_agents/fix_pipeline/fix_validator.py

👉 Buscar:

# MODULE_6_STEP_3_FIX_VALIDATOR_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def fix_validator_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects state variables."""
    template = """You are the final validation specialist for code fixes.

You have access to:
- Original issues from initial review
- Applied fixes: {code_fixes}
- Test results after fix: {fix_test_execution_summary}
- All state data from the fix process

Your responsibilities:
1. Use validate_fixed_style tool to check style compliance of fixed code
   - Pass no arguments, it will retrieve fixed code from state
2. Use compile_fix_report tool to generate comprehensive report
   - Pass no arguments, it will gather all data from state
3. Based on the report, determine overall fix status:
   - ✅ SUCCESSFUL: All tests pass, style score 100
   - ⚠️ PARTIAL: Improvements made but issues remain
   - ❌ FAILED: Fix didn't work or made things worse

4. CRITICAL: If status is SUCCESSFUL, call the exit_fix_loop tool to stop iterations
   - This prevents unnecessary additional fix attempts
   - If not successful, the loop will continue for another attempt

5. Provide clear summary of:
   - What was fixed
   - What improvements were achieved
   - Any remaining issues requiring manual attention

Be precise and quantitative in your assessment.
"""
    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_6_STEP_3_FIX_VALIDATOR_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

fix_validator_agent = Agent(
    name="FixValidator",
    model=config.worker_model,
    description="Validates fixes and generates final fix report",
    instruction=fix_validator_instruction_provider,
    tools=[
        FunctionTool(func=validate_fixed_style),
        FunctionTool(func=compile_fix_report),
        FunctionTool(func=exit_fix_loop)
    ],
    output_key="final_fix_report"
)

Paso 4: Comprende las condiciones de salida de LoopAgent

El LoopAgent tiene tres formas de salir:

1. Salida exitosa (a través de derivación)

# Inside any tool in the loop:
tool_context.actions.escalate = True

# Effect: Loop completes current iteration, then exits
# Use when: Fix is successful and no more attempts needed

Ejemplo de flujo:

Iteration 1:
  CodeFixer → generates fixes
  FixTestRunner → tests show 90% pass rate
  FixValidator → compiles report, sees PARTIAL status
  → Does NOT set escalate
  → Loop continues

Iteration 2:
  CodeFixer → refines fixes based on failures
  FixTestRunner → tests show 100% pass rate
  FixValidator → compiles report, sees SUCCESSFUL status
  → Calls exit_fix_loop() which sets escalate = True
  → Loop exits after this iteration

2. Max Iterations Exit

LoopAgent(
    name="FixAttemptLoop",
    sub_agents=[...],
    max_iterations=3  # Safety limit
)

# Effect: After 3 complete iterations, loop exits regardless of escalate
# Use when: Prevent infinite loops if fixes never succeed

Ejemplo de flujo:

Iteration 1: PARTIAL (continue)
Iteration 2: PARTIAL (continue)
Iteration 3: PARTIAL (but max reached)
→ Loop exits, synthesizer presents best attempt

3. Salida de error

# If any agent throws unhandled exception:
raise Exception("Unexpected error")

# Effect: Loop exits immediately with error state
# Use when: Critical failure that can't be recovered

Evolución del estado en las distintas iteraciones:

Cada iteración muestra el estado actualizado del intento anterior:

# Before Iteration 1:
state = {
    "code_to_review": "def add(a,b):return a+b",  # Original
    "style_score": 40,
    "test_execution_summary": {...}
}

# After Iteration 1:
state = {
    "code_to_review": "def add(a,b):return a+b",  # Unchanged
    "code_fixes": "def add(a, b):\n    return a + b",  # NEW
    "style_score": 40,  # Unchanged
    "fixed_style_score": 100,  # NEW
    "test_execution_summary": {...},  # Unchanged
    "fix_test_execution_summary": {...}  # NEW
}

# Iteration 2 starts with all this state
# If fixes still not perfect, code_fixes gets overwritten

Por qué

escalate

En lugar de valores de retorno:

# Bad: Using return value to signal exit
def validator_agent():
    report = compile_report()
    if report['status'] == 'SUCCESSFUL':
        return {"exit": True}  # How does loop know?

# Good: Using escalate
def validator_tool(tool_context):
    report = compile_report()
    if report['status'] == 'SUCCESSFUL':
        tool_context.actions.escalate = True  # Loop knows immediately
    return {"report": report}

Beneficios:

  • Funciona desde cualquier herramienta, no solo la última
  • No interfiere con los datos de devolución
  • Significado semántico claro
  • El framework controla la lógica de salida

Paso 5: Conecta la canalización de corrección

👉 Abrir

code_review_assistant/agent.py

👉 Agrega las importaciones de la canalización de corrección (después de las importaciones existentes):

from google.adk.agents import LoopAgent  # Add this to the existing Agent, SequentialAgent line
from code_review_assistant.sub_agents.fix_pipeline.code_fixer import code_fixer_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_test_runner import fix_test_runner_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_validator import fix_validator_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_synthesizer import fix_synthesizer_agent

Tus importaciones ahora deberían ser las siguientes:

from google.adk.agents import Agent, SequentialAgent, LoopAgent
from .config import config
# Review pipeline imports (from Module 5)
from code_review_assistant.sub_agents.review_pipeline.code_analyzer import code_analyzer_agent
from code_review_assistant.sub_agents.review_pipeline.style_checker import style_checker_agent
from code_review_assistant.sub_agents.review_pipeline.test_runner import test_runner_agent
from code_review_assistant.sub_agents.review_pipeline.feedback_synthesizer import feedback_synthesizer_agent
# Fix pipeline imports (NEW)
from code_review_assistant.sub_agents.fix_pipeline.code_fixer import code_fixer_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_test_runner import fix_test_runner_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_validator import fix_validator_agent
from code_review_assistant.sub_agents.fix_pipeline.fix_synthesizer import fix_synthesizer_agent

👉 Buscar:

# MODULE_6_STEP_5_CREATE_FIX_LOOP

👉 Reemplaza esa sola línea por lo siguiente:

# Create the fix attempt loop (retries up to 3 times)
fix_attempt_loop = LoopAgent(
    name="FixAttemptLoop",
    sub_agents=[
        code_fixer_agent,      # Step 1: Generate fixes
        fix_test_runner_agent, # Step 2: Validate with tests
        fix_validator_agent    # Step 3: Check success & possibly exit
    ],
    max_iterations=3  # Try up to 3 times
)

# Wrap loop with synthesizer for final report
code_fix_pipeline = SequentialAgent(
    name="CodeFixPipeline",
    description="Automated code fixing pipeline with iterative validation",
    sub_agents=[
        fix_attempt_loop,      # Try to fix (1-3 times)
        fix_synthesizer_agent  # Present final results (always runs once)
    ]
)

👉 Quita el existente.

root_agent

definición:

root_agent = Agent(...)

👉 Buscar:

# MODULE_6_STEP_5_UPDATE_ROOT_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

# Update root agent to include both pipelines
root_agent = Agent(
    name="CodeReviewAssistant",
    model=config.worker_model,
    description="An intelligent code review assistant that analyzes Python code and provides educational feedback",
    instruction="""You are a specialized Python code review assistant focused on helping developers improve their code quality.

When a user provides Python code for review:
1. Immediately delegate to CodeReviewPipeline and pass the code EXACTLY as it was provided by the user.
2. The pipeline will handle all analysis and feedback
3. Return ONLY the final feedback from the pipeline - do not add any commentary

After completing a review, if significant issues were identified:
- If style score < 100 OR tests are failing OR critical issues exist:
  * Add at the end: "\n\n💡 I can fix these issues for you. Would you like me to do that?"
 
- If the user responds yes or requests fixes:
  * Delegate to CodeFixPipeline
  * Return the fix pipeline's complete output AS-IS

When a user asks what you can do or general questions:
- Explain your capabilities for code review and fixing
- Do NOT trigger the pipeline for non-code messages

The pipelines handle everything for code review and fixing - just pass through their final output.""",
    sub_agents=[code_review_pipeline, code_fix_pipeline],
    output_key="assistant_response"
)

Paso 6: Agrega el agente de Fix Synthesizer

El sintetizador crea una presentación fácil de usar de los resultados de la corrección después de que se completa el bucle.

👉 Abrir

code_review_assistant/sub_agents/fix_pipeline/fix_synthesizer.py

👉 Buscar:

# MODULE_6_STEP_6_FIX_SYNTHESIZER_INSTRUCTION_PROVIDER

👉 Reemplaza esa sola línea por lo siguiente:

async def fix_synthesizer_instruction_provider(context: ReadonlyContext) -> str:
    """Dynamic instruction provider that injects state variables."""
    template = """You are responsible for presenting the fix results to the user.

Based on the validation report: {final_fix_report}
Fixed code from state: {code_fixes}
Fix status: {fix_status}

Create a comprehensive yet friendly response that includes:

## 🔧 Fix Summary
[Overall status and key improvements - be specific about what was achieved]

## 📊 Metrics
- Test Results: [original pass rate]% → [new pass rate]%
- Style Score: [original]/100 → [new]/100
- Issues Fixed: X of Y

## ✅ What Was Fixed
[List each fixed issue with brief explanation of the correction made]

## 📝 Complete Fixed Code
[Include the complete, corrected code from state - this is critical]

## 💡 Explanation of Key Changes
[Brief explanation of the most important changes made and why]

[If any issues remain]
## ⚠️ Remaining Issues
[List what still needs manual attention]

## 🎯 Next Steps
[Guidance on what to do next - either use the fixed code or address remaining issues]

Save the fix report using save_fix_report tool before presenting.
Call it with no parameters - it will retrieve the report from state automatically.

Be encouraging about improvements while being honest about any remaining issues.
Focus on the educational aspect - help the user understand what was wrong and how it was fixed.
"""
    return await instructions_utils.inject_session_state(template, context)

👉 Buscar:

# MODULE_6_STEP_6_FIX_SYNTHESIZER_AGENT

👉 Reemplaza esa sola línea por lo siguiente:

fix_synthesizer_agent = Agent(
    name="FixSynthesizer",
    model=config.critic_model,
    description="Creates comprehensive user-friendly fix report",
    instruction=fix_synthesizer_instruction_provider,
    tools=[FunctionTool(func=save_fix_report)],
    output_key="fix_summary"
)

👉 Agregar

save_fix_report

Herramienta de a

tools.py

:

👉 Buscar:

# MODULE_6_STEP_6_SAVE_FIX_REPORT

👉 Reemplaza con:

async def save_fix_report(tool_context: ToolContext) -> Dict[str, Any]:
    """
    Saves the fix report as an artifact.

    Args:
        tool_context: ADK tool context

    Returns:
        Save status
    """
    logger.info("Tool: Saving fix report...")

    try:
        # Get the report from state
        fix_report = tool_context.state.get(StateKeys.FIX_REPORT, {})

        if not fix_report:
            return {
                "status": "error",
                "message": "No fix report found in state"
            }

        # Convert to JSON
        report_json = json.dumps(fix_report, indent=2)
        report_part = types.Part.from_text(text=report_json)

        # Generate filename
        timestamp = datetime.now().isoformat().replace(':', '-')
        filename = f"fix_report_{timestamp}.json"

        # Try to save as artifact
        if hasattr(tool_context, 'save_artifact'):
            try:
                version = await tool_context.save_artifact(filename, report_part)
                await tool_context.save_artifact("latest_fix_report.json", report_part)

                logger.info(f"Tool: Fix report saved as {filename}")

                return {
                    "status": "success",
                    "filename": filename,
                    "version": str(version),
                    "size": len(report_json)
                }
            except Exception as e:
                logger.warning(f"Could not save as artifact: {e}")

        # Fallback: store in state
        tool_context.state[StateKeys.LAST_FIX_REPORT] = fix_report

        return {
            "status": "success",
            "message": "Fix report saved to state",
            "size": len(report_json)
        }

    except Exception as e:
        logger.error(f"Tool: Failed to save fix report: {e}", exc_info=True)
        return {
            "status": "error",
            "message": str(e)
        }

Paso 7: Prueba la canalización de Complete Fix

Es hora de ver todo el bucle en acción.

👉 Inicia el sistema:

adk web code_review_assistant

Después de ejecutar el comando adk web, deberías ver un resultado en la terminal que indica que se inició el servidor web del ADK, similar a este:

+-----------------------------------------------------------------------------+
| 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)

👉 Instrucción de prueba:

Please analyze the following:
def dfs_search_v1(graph, start, target):
    """Find if target is reachable from start."""
    visited = set()
    stack = start
   
    while stack:
        current = stack.pop()
       
        if current == target:
            return True
           
        if current not in visited:
            visited.add(current)
           
            for neighbor in graph[current]:
                if neighbor not in visited:
                    stack.append(neighbor)
   
    return False

Primero, envía el código con errores para activar la canalización de revisión. Después de identificar los errores, le pedirás al agente que "corrija el código", lo que activará la potente canalización de corrección iterativa.

fix-pipeline-in-action

1. La revisión inicial (cómo encontrar las fallas)

Esta es la primera mitad del proceso. La canalización de revisión de cuatro agentes analiza el código, verifica su estilo y ejecuta un conjunto de pruebas generado. Identifica correctamente un AttributeError crítico y otros problemas, y entrega un veredicto: el código está BROKEN, con una tasa de aprobación de pruebas de solo el 84.21%.

2. La corrección automática (el bucle en acción)

Esta es la parte más impresionante. Cuando le pides al agente que corrija el código, no solo realiza un cambio. Se inicia un bucle de corrección y validación iterativo que funciona como un desarrollador diligente: intenta una corrección, la prueba a fondo y, si no es perfecta, lo vuelve a intentar.

Iteración 1: El primer intento (éxito parcial)

  • La corrección: El agente CodeFixer lee el informe inicial y realiza las correcciones más evidentes. Cambia stack = start a stack = [start] y usa graph.get() para evitar excepciones de KeyError.
  • La validación: El TestRunner vuelve a ejecutar de inmediato el conjunto de pruebas completo en este código nuevo.
  • El resultado: El porcentaje de aprobación mejoró significativamente hasta alcanzar el 88.89%. Se corrigieron los errores críticos. Sin embargo, las pruebas son tan integrales que revelan dos errores nuevos y sutiles (regresiones) relacionados con el manejo de None como un gráfico o valores vecinos que no son de lista. El sistema marca la corrección como PARCIAL.

Iteración 2: Los últimos detalles (100% de éxito)

  • La solución: Como no se cumplió la condición de salida del bucle (índice de aprobación del 100%), se vuelve a ejecutar. El CodeFixer ahora tiene más información: los dos errores nuevos de regresión. Genera una versión final y más sólida del código que controla de forma explícita esos casos extremos.
  • La validación: TestRunner ejecuta el conjunto de pruebas por última vez en la versión final del código.
  • El resultado: Una tasa de aprobación del 100% perfecta. Se resolvieron todos los errores originales y todas las regresiones. El sistema marca la corrección como SUCCESSFUL y se cierra el bucle.

3. Informe final: Una puntuación perfecta

Con una corrección completamente validada, el agente de FixSynthesizer se hace cargo de presentar el informe final, transformando los datos técnicos en un resumen claro y educativo.

Métrica

Antes

Después

Mejora

Tasa de aprobación de la prueba

84.21%

100%

▲ 15.79%

Puntuación de estilo

88 / 100

98 / 100

▲ 10 pts

Errores corregidos

0 de 3

3 de 3

✅ El código final validado

A continuación, se muestra el código completo y corregido que ahora pasa las 19 pruebas, lo que demuestra que la corrección se realizó correctamente:

def dfs_search_v1(graph, start, target):
    """Find if target is reachable from start."""
    # Handles 'None' graph input
    if graph is None:
        return False

    visited = set()
    # Fixes the critical AttributeError
    stack = [start]

    while stack:
        current = stack.pop()

        if current == target:
            return True

        if current not in visited:
            visited.add(current)
            
            # Safely gets neighbors to prevent KeyError
            neighbors = graph.get(current)

            if neighbors is None:
                continue
            
            # Validates that neighbors are iterable
            if not isinstance(neighbors, (list, set, tuple)):
                raise TypeError(
                    f"Graph value for node '{current}' is of type "
                    f"{type(neighbors).__name__}. Expected a list, set, or tuple."
                )
            
            for neighbor in neighbors:
                if neighbor not in visited:
                    stack.append(neighbor)

    return False

👉💻 Cuando termines de realizar las pruebas, vuelve a la terminal del editor de Cloud Shell y presiona Ctrl+C para detener la IU de desarrollo del ADK.

Qué compilaste

Ahora tienes una canalización de corrección automatizada completa que hace lo siguiente:

Genera correcciones: Basado en el análisis de revisión
Valida de forma iterativa: Realiza pruebas después de cada intento de corrección
Reintenta automáticamente: Hasta 3 intentos para lograr el éxito
Sale de forma inteligente: A través de la derivación cuando se realiza correctamente
Realiza un seguimiento de las mejoras: Compara las métricas antes y después
Proporciona artefactos: Informes de correcciones descargables

Conceptos clave dominados

LoopAgent vs. Sequential:

  • Secuencial: Un pase a través de los agentes
  • LoopAgent: Repite hasta que se cumpla la condición de salida o se alcance la cantidad máxima de iteraciones
  • Sal por tool_context.actions.escalate = True

Evolución del estado en las distintas iteraciones:

  • CODE_FIXES se actualiza en cada iteración
  • Los resultados de las pruebas muestran una mejora con el tiempo
  • El validador ve los cambios acumulativos

Arquitectura de múltiples canalizaciones:

  • Revisión de la canalización: Análisis de solo lectura (módulo 5)
  • Corrección del bucle: Corrección iterativa (bucle interno del módulo 6)
  • Canalización de corrección: Loop + sintetizador (exterior del módulo 6)
  • Agente raíz: Coordina en función de la intención del usuario

Herramientas que controlan el flujo:

  • Los conjuntos de exit_fix_loop() aumentan
  • Cualquier herramienta puede indicar que se completó el bucle
  • Desvincula la lógica de salida de las instrucciones del agente

Seguridad de las iteraciones máximas:

  • Evita los bucles infinitos
  • Garantiza que el sistema siempre responda
  • Presenta el mejor intento, incluso si no es perfecto

Pasos siguientes

En el último módulo, aprenderás a implementar tu agente en producción:

  • Cómo configurar el almacenamiento persistente con VertexAiSessionService
  • Implementa en Agent Engine en Google Cloud
  • Supervisión y depuración de agentes de producción
  • Prácticas recomendadas para la escalabilidad y la confiabilidad

Compilaste un sistema multiagente completo con arquitecturas secuenciales y de bucle. Los patrones que aprendiste (administración de estados, instrucciones dinámicas, coordinación de herramientas y perfeccionamiento iterativo) son técnicas listas para producción que se usan en sistemas basados en agentes reales.

7. Implementación en producción

adk-deploy.png

Introducción

Tu asistente de revisión de código ahora está completo, con canalizaciones de revisión y corrección que funcionan de forma local. La pieza faltante: Solo se ejecuta en tu máquina. En este módulo, implementarás tu agente en Google Cloud, lo que permitirá que tu equipo acceda a él con sesiones persistentes y una infraestructura de nivel de producción.

Aprendizajes esperados

  • Tres rutas de implementación: local, Cloud Run y Agent Engine
  • Aprovisionamiento de infraestructura automatizado
  • Estrategias de persistencia de sesión
  • Prueba de agentes implementados

Información sobre las opciones de implementación

El ADK admite varios destinos de implementación, cada uno con diferentes ventajas y desventajas:

Rutas de implementación

Factor

Local (adk web)

Cloud Run (adk deploy cloud_run)

Motor del agente (adk deploy agent_engine)

Complejidad

Mínimo

Medio

Baja

Persistencia de sesión

Solo en memoria (se pierde cuando se reinicia)

Cloud SQL (PostgreSQL)

Administrado por Vertex AI (automático)

Infraestructura

Ninguno (solo para la máquina de desarrollo)

Contenedor y base de datos

Completamente administrado

Inicio en frío

N/A

De 100 a 2,000 ms

De 100 a 500 ms

Escalamiento

Instancia única

Automático (hasta cero)

Automático

Modelo de costos

Gratis (procesamiento local)

Basado en solicitudes y nivel gratuito

Basado en procesamiento

Asistencia de la IU

Sí (a través de adk web)

Sí (a través de --with_ui)

No (solo API)

Ideal para

Desarrollo/pruebas

Tráfico variable y control de costos

Agentes de producción

Opción de implementación adicional: Google Kubernetes Engine (GKE) está disponible para usuarios avanzados que requieren control a nivel de Kubernetes, redes personalizadas o coordinación de varios servicios. La implementación de GKE no se aborda en este codelab, pero se documenta en la guía de implementación del ADK.

Qué se implementa

Cuando se realiza la implementación en Cloud Run o Agent Engine, se empaqueta y se implementa lo siguiente:

  • Tu código de agente (agent.py, todos los subagentes y las herramientas)
  • Dependencias (requirements.txt)
  • Servidor de la API del ADK (se incluye automáticamente)
  • IU web (solo para Cloud Run, cuando se especifica --with_ui)

Diferencias importantes:

  • Cloud Run: Usa la CLI de adk deploy cloud_run (compila el contenedor automáticamente) o gcloud run deploy (requiere un Dockerfile personalizado)
  • Agent Engine: Usa la CLI de adk deploy agent_engine (no es necesario compilar contenedores, empaqueta directamente el código de Python)

Paso 1: Configura tu entorno

Configura tu archivo .env

Tu archivo .env (creado en el módulo 3) necesita actualizaciones para la implementación en la nube. Abre .env y verifica o actualiza estos parámetros de configuración:

Obligatorio para todas las implementaciones en la nube:

# Your actual GCP Project ID (REQUIRED)
GOOGLE_CLOUD_PROJECT=your-project-id

# GCP region for deployments (REQUIRED)
GOOGLE_CLOUD_LOCATION=us-central1

# Use Vertex AI (REQUIRED)
GOOGLE_GENAI_USE_VERTEXAI=true

# Model configuration (already set)
WORKER_MODEL=gemini-2.5-flash
CRITIC_MODEL=gemini-2.5-pro

Establece los nombres de los buckets (OBLIGATORIO antes de ejecutar deploy.sh):

La secuencia de comandos de implementación crea buckets basados en estos nombres. Configúralos ahora:

# Staging bucket for Agent Engine code uploads (REQUIRED for agent-engine)
STAGING_BUCKET=gs://your-project-id-staging

# Artifact storage for reports and fixed code (REQUIRED for both cloud-run and agent-engine)
ARTIFACT_BUCKET=gs://your-project-id-artifacts

Reemplaza your-project-id por el ID de tu proyecto real en ambos nombres de bucket. La secuencia de comandos creará estos buckets si no existen.

Variables opcionales (se crean automáticamente si se dejan en blanco):

# Agent Engine ID (populated after first deployment)
AGENT_ENGINE_ID=

# Cloud Run Database credentials (created automatically if blank)
CLOUD_SQL_INSTANCE_NAME=
DB_USER=
DB_PASSWORD=
DB_NAME=

Verificación de autenticación

Si encuentras errores de autenticación durante la implementación, haz lo siguiente:

gcloud auth application-default login
gcloud config set project $GOOGLE_CLOUD_PROJECT

Paso 2: Comprende la secuencia de comandos de implementación

La secuencia de comandos deploy.sh proporciona una interfaz unificada para todos los modos de implementación:

./deploy.sh {local|cloud-run|agent-engine}

Capacidades de la secuencia de comandos

Aprovisionamiento de infraestructura:

  • Habilitación de la API (AI Platform, Storage, Cloud Build, Cloud Trace, Cloud SQL)
  • Configuración de permisos de IAM (cuentas de servicio, roles)
  • Creación de recursos (instancias, bases de datos y buckets)
  • Implementación con marcas adecuadas
  • Verificación posterior a la implementación

Secciones clave del guion

  • Configuración (líneas 1 a 35): Proyecto, región, nombres de servicio, valores predeterminados
  • Funciones de ayuda (líneas 37 a 200): Habilitación de la API, creación de buckets y configuración de IAM
  • Lógica principal (líneas 202 a 400): Organización de la implementación específica del modo

Paso 3: Prepara el agente para Agent Engine

Antes de implementar en Agent Engine, se necesita un archivo agent_engine_app.py que encapsule tu agente para el tiempo de ejecución administrado. Ya se creó para ti.

Ver code_review_assistant/agent_engine_app.py

👉 Abre el archivo:

"""
Agent Engine application wrapper.
This file prepares the agent for deployment to Vertex AI Agent Engine.
"""

from vertexai import agent_engines
from .agent import root_agent

# Wrap the agent in an AdkApp object for Agent Engine deployment
app = agent_engines.AdkApp(
    agent=root_agent,
    enable_tracing=True,
)

Paso 4: Implementa en Agent Engine

Agent Engine es la implementación de producción recomendada para los agentes del ADK porque proporciona lo siguiente:

  • Infraestructura completamente administrada (no es necesario compilar contenedores)
  • Persistencia de sesión integrada a través de VertexAiSessionService
  • Ajuste de escala automático desde cero
  • La integración de Cloud Trace está habilitada de forma predeterminada

En qué se diferencia Agent Engine de otras implementaciones

Detrás de escena,

deploy.sh agent-engine

Usos:

adk deploy agent_engine \
  --project=$GOOGLE_CLOUD_PROJECT \
  --region=$GOOGLE_CLOUD_LOCATION \
  --staging_bucket=$STAGING_BUCKET \
  --display_name="Code Review Assistant" \
  --trace_to_cloud \
  code_review_assistant

Este comando realiza las siguientes acciones:

  • Empaqueta tu código de Python directamente (sin compilación de Docker).
  • Se sube al bucket de etapa de pruebas que especificaste en .env.
  • Crea una instancia administrada de Agent Engine
  • Habilita Cloud Trace para la observabilidad
  • Usa agent_engine_app.py para configurar el entorno de ejecución

A diferencia de Cloud Run, que contiene tu código, Agent Engine ejecuta tu código de Python directamente en un entorno de ejecución administrado, similar a las funciones sin servidores.

Ejecuta la implementación

Desde la raíz de tu proyecto, haz lo siguiente:

./deploy.sh agent-engine

Fases de implementación

Observa cómo la secuencia de comandos ejecuta estas fases:

Phase 1: API Enablement
   aiplatform.googleapis.com
   storage-api.googleapis.com
   cloudbuild.googleapis.com
   cloudtrace.googleapis.com

Phase 2: IAM Setup
   Getting project number
   Granting Storage Object Admin
   Granting AI Platform User
   Granting Cloud Trace Agent

Phase 3: Staging Bucket
   Creating gs://your-project-id-staging
   Setting permissions

Phase 4: Artifact Bucket
   Creating gs://your-project-id-artifacts
   Configuring access

Phase 5: Validation
   Checking agent.py exists
   Verifying root_agent defined
   Checking agent_engine_app.py exists
   Validating requirements.txt

Phase 6: Build & Deploy
   Packaging agent code
   Uploading to staging bucket
   Creating Agent Engine instance
   Configuring session persistence
   Setting up Cloud Trace integration
   Running health checks

Este proceso tarda entre 5 y 10 minutos, ya que empaqueta el agente y lo implementa en la infraestructura de Vertex AI.

Cómo guardar tu ID de Agent Engine

Después de una implementación exitosa, haz lo siguiente:

✅ Deployment successful!
   Agent Engine ID: 7917477678498709504
   Resource Name: projects/123456789/locations/us-central1/reasoningEngines/7917477678498709504
   Endpoint: https://us-central1-aiplatform.googleapis.com/v1/...

⚠️  IMPORTANT: Save this in your .env file:
   AGENT_ENGINE_ID=7917477678498709504

Actualiza tu

.env

archivo de inmediato:

echo "AGENT_ENGINE_ID=7917477678498709504" >> .env

Este ID es obligatorio para lo siguiente:

  • Prueba el agente implementado
  • Actualizar la implementación más tarde
  • Cómo acceder a los registros y seguimientos

Qué se implementó

Tu implementación de Agent Engine ahora incluye lo siguiente:

✅ Canalización de revisión completa (4 agentes)
✅ Canalización de corrección completa (bucle y sintetizador)
✅ Todas las herramientas (análisis de AST, verificación de estilo, generación de artefactos)
✅ Persistencia de sesión (automática a través de VertexAiSessionService)
✅ Administración de estado (niveles de sesión, usuario y ciclo de vida)
✅ Observabilidad (Cloud Trace habilitado)
✅ Infraestructura de ajuste de escala automático

Paso 5: Prueba el agente implementado

Actualiza tu archivo .env

Después de la implementación, verifica que tu .env incluya lo siguiente:

AGENT_ENGINE_ID=7917477678498709504  # From deployment output
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=us-central1

Ejecuta la secuencia de comandos de prueba

El proyecto incluye tests/test_agent_engine.py específicamente para probar las implementaciones de Agent Engine:

python tests/test_agent_engine.py

Qué hace la prueba

  1. Autentica con tu proyecto de Google Cloud
  2. Crea una sesión con el agente implementado
  3. Envía una solicitud de revisión de código (el ejemplo de error de DFS)
  4. Transmite la respuesta a través de eventos enviados por el servidor (SSE).
  5. Verifica la persistencia de la sesión y la administración del estado

Resultado esperado

Authenticated with project: your-project-id
Targeting Agent Engine: projects/.../reasoningEngines/7917477678498709504

Creating new session...
Created session: 4857885913439920384

Sending query to agent and streaming response:
data: {"content": {"parts": [{"text": "I'll analyze your code..."}]}}
data: {"content": {"parts": [{"text": "**Code Structure Analysis**\n..."}]}}
data: {"content": {"parts": [{"text": "**Style Check Results**\n..."}]}}
data: {"content": {"parts": [{"text": "**Test Results**\n..."}]}}
data: {"content": {"parts": [{"text": "**Final Feedback**\n..."}]}}

Stream finished.

Lista de tareas de verificación

  • ✅ Se ejecuta la canalización de revisión completa (los 4 agentes).
  • ✅ La respuesta de transmisión muestra una salida progresiva
  • ✅ El estado de la sesión persiste en todas las solicitudes
  • ✅ No hay errores de autenticación ni de conexión
  • ✅ Las llamadas a herramientas se ejecutan correctamente (análisis del AST y verificación de estilo).
  • ✅ Se guardaron los artefactos (se puede acceder al informe de calificación)

Alternativa: Implementa en Cloud Run

Si bien se recomienda Agent Engine para una implementación de producción optimizada, Cloud Run ofrece más control y admite la IU web del ADK. En esta sección, se proporciona una descripción general.

Cuándo usar Cloud Run

Elige Cloud Run si necesitas lo siguiente:

  • La IU web del ADK para la interacción del usuario
  • Control total del entorno del contenedor
  • Configuraciones de bases de datos personalizadas
  • Integración con los servicios existentes de Cloud Run

Cómo funciona la implementación de Cloud Run

Detrás de escena,

deploy.sh cloud-run

Usos:

adk deploy cloud_run \
  --project=$GOOGLE_CLOUD_PROJECT \
  --region=$GOOGLE_CLOUD_LOCATION \
  --service_name="code-review-assistant" \
  --app_name="code_review_assistant" \
  --port=8080 \
  --with_ui \
  --artifact_service_uri="gs://$ARTIFACT_BUCKET" \
  --trace_to_cloud \
  code_review_assistant

Este comando realiza las siguientes acciones:

  • Compila un contenedor de Docker con el código de tu agente
  • Envía a Google Artifact Registry
  • Se implementa como un servicio de Cloud Run
  • Incluye la IU web del ADK (--with_ui)
  • Configura la conexión de Cloud SQL (el script la agrega después de la implementación inicial).

La principal diferencia con Agent Engine es que Cloud Run aloja tu código en contenedores y requiere una base de datos para la persistencia de la sesión, mientras que Agent Engine se encarga de ambos automáticamente.

Comando de implementación de Cloud Run

./deploy.sh cloud-run

Qué diferencia a la versión nueva

Infraestructura:

  • Implementación en contenedores (Docker compilado automáticamente por el ADK)
  • Cloud SQL (PostgreSQL) para la persistencia de sesiones
  • La base de datos se crea automáticamente con una secuencia de comandos o usa una instancia existente

Administración de sesiones:

  • Usa DatabaseSessionService en lugar de VertexAiSessionService
  • Requiere credenciales de la base de datos en .env (o generadas automáticamente)
  • El estado persiste en la base de datos de PostgreSQL

Compatibilidad con la IU:

  • IU web disponible a través de la marca --with_ui (controlada por la secuencia de comandos)
  • Acceso a las https://code-review-assistant-xyz.a.run.app

Tus logros

La implementación de producción incluye lo siguiente:

Aprovisionamiento automatizado a través de la secuencia de comandos deploy.sh
Infraestructura administrada (Agent Engine controla el ajuste de escala, la persistencia y la supervisión)
Estado persistente en todos los niveles de memoria (sesión, usuario y ciclo de vida)
Administración segura de credenciales (generación automática y configuración de IAM)
Arquitectura escalable (de cero a miles de usuarios simultáneos)
Observabilidad integrada (integración de Cloud Trace habilitada)
✅ Control y recuperación de errores de nivel de producción

Conceptos clave dominados

Preparación de la implementación:

  • agent_engine_app.py: Envuelve el agente con AdkApp para el motor del agente
  • AdkApp configura automáticamente VertexAiSessionService para la persistencia
  • Se habilitó el registro a través de enable_tracing=True

Comandos de implementación:

  • adk deploy agent_engine: Paquetes de código de Python, sin contenedores
  • adk deploy cloud_run: Compila automáticamente el contenedor de Docker
  • gcloud run deploy: Alternativa con Dockerfile personalizado

Opciones de implementación:

  • Agent Engine: Completamente administrado y con el tiempo de producción más rápido
  • Cloud Run: Más control y compatibilidad con la IU web
  • GKE: Control avanzado de Kubernetes (consulta la guía de implementación de GKE)

Servicios administrados:

  • Agent Engine controla la persistencia de la sesión automáticamente
  • Cloud Run requiere la configuración de la base de datos (o la creación automática)
  • Ambos admiten el almacenamiento de artefactos a través de GCS

Administración de sesiones:

  • Agent Engine: VertexAiSessionService (automático)
  • Cloud Run: DatabaseSessionService (Cloud SQL)
  • Local: InMemorySessionService (temporal)

Tu agente ya está disponible

Tu asistente de revisión de código ahora es:

  • Accesible a través de extremos de API HTTPS
  • Persistent, con un estado que sobrevive a los reinicios
  • Escalable para manejar el crecimiento del equipo de forma automática
  • Observable con registros de solicitudes completos
  • Mantenimiento a través de implementaciones con secuencias de comandos

Próximos pasos En el módulo 8, aprenderás a usar Cloud Trace para comprender el rendimiento de tu agente, identificar cuellos de botella en las canalizaciones de revisión y corrección, y optimizar los tiempos de ejecución.

8. Observabilidad de producción

cloud-trace-agent-engine.png

Introducción

Tu asistente de revisión de código ahora está implementado y se ejecuta en producción en Agent Engine. Pero, ¿cómo sabes si funciona bien? ¿Puedes responder estas preguntas importantes?

  • ¿El agente responde lo suficientemente rápido?
  • ¿Qué operaciones son las más lentas?
  • ¿Los bucles de corrección se completan de manera eficiente?
  • ¿Dónde se encuentran los cuellos de botella de rendimiento?

Sin observabilidad, no podrás ver datos. La marca --trace-to-cloud que usaste durante la implementación habilitó automáticamente Cloud Trace, lo que te brinda visibilidad completa de cada solicitud que procesa tu agente.

En este módulo, aprenderás a leer registros, comprender las características de rendimiento de tu agente y a identificar áreas para la optimización.

Información sobre los seguimientos y los intervalos

¿Qué es un registro?

Un seguimiento es la línea de tiempo completa de tu agente que controla una sola solicitud. Captura todo el proceso, desde que un usuario envía una búsqueda hasta que se entrega la respuesta final. Cada registro muestra lo siguiente:

  • Duración total de la solicitud
  • Todas las operaciones que se ejecutaron
  • Cómo se relacionan las operaciones entre sí (relaciones principal-secundaria)
  • Cuándo comenzó y finalizó cada operación

¿Qué es un intervalo?

Un intervalo representa una sola unidad de trabajo dentro de un registro. Tipos de tramos comunes en tu asistente de revisión de código:

  • agent_run: Ejecución de un agente (agente raíz o subagente)
  • call_llm: Solicitud a un modelo de lenguaje
  • execute_tool: Ejecución de la función de la herramienta
  • state_read / state_write: Operaciones de administración de estados
  • code_executor: Ejecución de código con pruebas

Los tramos tienen las siguientes características:

  • Nombre: Operación que representa
  • Duración: Tiempo que tardó
  • Atributos: Metadatos como el nombre del modelo, los recuentos de tokens y las entradas o salidas
  • Estado: Éxito o error
  • Relaciones entre operaciones principales y secundarias: Qué operaciones activaron otras

Instrumentación automática

Cuando realizaste la implementación con --trace-to-cloud, el ADK instrumenta automáticamente lo siguiente:

  • Cada invocación del agente y llamada del subagente
  • Todas las solicitudes a LLM con recuentos de tokens
  • Ejecuciones de herramientas con entradas y salidas
  • Operaciones de estado (lectura/escritura)
  • Iteraciones de bucle en tu canalización de corrección
  • Condiciones de error y reintentos

No se requieren cambios en el código: El registro de seguimiento está integrado en el tiempo de ejecución del ADK.

Paso 1: Accede al Explorador de Cloud Trace

Abre Cloud Trace en la consola de Google Cloud:

  1. Navega al Explorador de Cloud Trace.
  2. Selecciona tu proyecto en el menú desplegable (debería estar preseleccionado).
  3. Deberías ver los registros de tu prueba en el módulo 7.

Si aún no ves los registros, haz lo siguiente:

La prueba que ejecutaste en el módulo 7 debería haber generado registros. Si la lista está vacía, genera algunos datos de seguimiento:

python tests/test_agent_engine.py

Espera de 1 a 2 minutos para que los registros aparezcan en la consola.

Qué estás viendo

El Explorador de seguimiento muestra lo siguiente:

  • Lista de registros: Cada fila representa una solicitud completa.
  • Cronograma: Cuándo se produjeron las solicitudes
  • Duración: Tiempo que tardó cada solicitud
  • Detalles de la solicitud: Marca de tiempo, latencia y recuento de tramos

Este es tu registro de tráfico de producción: cada interacción con tu agente crea un registro.

Paso 2: Examina un registro de canalización de revisión

Haz clic en cualquier registro de la lista para abrir la vista de cascada

Verás un diagrama de Gantt que muestra la línea de tiempo de ejecución completa. Así se ve un registro de canalización de revisión típico:

invocation (2.3s) ────────────────────────────────────────────►
├── agent_run: CodeReviewAssistant (2.2s) ──────────────────►
   ├── state_read: CODE_TO_REVIEW (0.01s) 
   ├── agent_run: CodeReviewPipeline (2.1s) ─────────────►
      ├── agent_run: CodeAnalyzer (0.3s) ──────►
         ├── execute_tool: analyze_code_structure (0.1s) ──►
         └── call_llm: gemini-2.5-flash (0.15s) ────►
      ├── agent_run: StyleChecker (0.2s) ──────►
         ├── execute_tool: check_code_style (0.1s) ──►
         └── call_llm: gemini-2.5-flash (0.08s) ──►
      ├── agent_run: TestRunner (1.2s) ─────────────►
         └── code_executor: BuiltInCodeExecutor (0.9s) ────►
      └── agent_run: FeedbackSynthesizer (0.4s) ────────►
          └── call_llm: gemini-2.5-flash (0.28s) ────►

Cómo leer la cascada

Cada barra representa un intervalo. La posición horizontal muestra cuándo comenzó, y la longitud muestra cuánto tardó.

Estadísticas clave de este registro:

  • Latencia total: 2.3 segundos desde la solicitud hasta la respuesta
  • Ruta de acceso crítica: TestRunner tarda 1.2 s (el 52% del tiempo total).
  • Cuello de botella: La ejecución de código dentro de TestRunner tarda 0.9 s (el 75% del tiempo de TestRunner).
  • Operaciones de estado: Muy rápidas (10 ms cada una), no son un problema
  • Estructura de la canalización: Ejecución secuencial: CodeAnalyzer → StyleChecker → TestRunner → FeedbackSynthesizer

Cómo inspeccionar los detalles del intervalo

Haz clic en el

call_llm: gemini-2.5-flash

intervalo en FeedbackSynthesizer

Verás los atributos detallados de esta llamada al LLM:

{
  "name": "call_llm",
  "span_kind": "LLM",
  "duration": "280ms",
  "attributes": {
    "llm.model": "models/gemini-2.5-flash",
    "llm.request_type": "GenerateContent",
    "llm.usage.prompt_tokens": 845,
    "llm.usage.completion_tokens": 234,
    "llm.usage.total_tokens": 1079,
    "llm.response.finish_reason": "STOP",
    "status_code": "OK"
  }
}

Esto muestra lo siguiente:

  • Qué modelo se usó
  • Cantidad de tokens consumidos (entrada + salida)
  • Duración de la solicitud
  • Estado de éxito o error
  • La instrucción completa también se puede ver en los atributos (desplázate para verla).

Información sobre el flujo de la canalización

Observa cómo el registro revela tu arquitectura:

  1. El agente raíz (CodeReviewAssistant) recibe la solicitud.
  2. State read recupera el código para revisarlo
  3. La canalización de revisión coordina cuatro subagentes de forma secuencial.
  4. Cada subagente usa herramientas y llamadas a LLM para completar su trabajo.
  5. El flujo de respuesta final vuelve a subir por la jerarquía

Esta visibilidad te ayuda a comprender exactamente qué sucede durante cada solicitud.

Paso 3: Analiza un registro de Fix Pipeline

El canal de corrección es más complejo porque incluye bucles. Veamos cómo los registros capturan el comportamiento iterativo.

Cómo encontrar un registro que incluya "CodeFixPipeline" en los nombres de los intervalos

Es posible que debas desplazarte por tus registros o enviar una solicitud que active la canalización de corrección. Si no tienes uno, puedes generarlo:

# In your test script, respond "yes" when asked to fix issues
python tests/test_agent_engine.py

Cómo examinar la estructura de bucle

Así se ve un registro de canalización de corrección con 2 iteraciones:

agent_run: CodeFixPipeline (8.5s) ───────────────────────►
├── agent_run: FixAttemptLoop (7.8s) ───────────────────►
   ├── loop_iteration: 1 (3.2s) ──────────►
      ├── agent_run: CodeFixer (0.8s) ────►
         └── call_llm: gemini-2.5-flash (0.7s) ───►
      ├── agent_run: FixTestRunner (1.8s) ─────────►
         └── code_executor: BuiltInCodeExecutor (1.5s) ─────►
      └── agent_run: FixValidator (0.6s) ────►
          ├── execute_tool: validate_fixed_style (0.2s) ──►
          └── state_write: FIX_STATUS = "PARTIAL" 
   
   ├── loop_iteration: 2 (4.5s) ─────────────────►
      ├── agent_run: CodeFixer (1.0s) ──────►
         └── call_llm: gemini-2.5-flash (0.9s) ───►
      ├── agent_run: FixTestRunner (2.0s) ────────►
         └── code_executor: BuiltInCodeExecutor (1.7s) ─────►
      └── agent_run: FixValidator (1.5s) ──────►
          ├── execute_tool: compile_fix_report (0.3s) ──►
          └── state_write: FIX_STATUS = "SUCCESSFUL" 
   
   └── loop_exit: escalation_triggered 

└── agent_run: FixSynthesizer (0.7s) ────►
    ├── execute_tool: save_fix_report (0.2s) ──►
    └── call_llm: gemini-2.5 (0.4s) ────►

Observaciones clave sobre los bucles

Patrones de iteración:

  • Dos iteraciones: El primer intento tuvo un éxito parcial, y el segundo se completó por completo.
  • Costo progresivo: La iteración 2 tarda más (4.5 s en comparación con 3.2 s).
  • Seguimiento del estado: Cada iteración escribe FIX_STATUS en el estado
  • Mecanismo de salida: El bucle finaliza a través de la derivación cuando FIX_STATUS = "SUCCESSFUL".

Qué revela:

  • Tu arquitectura de bucle funciona correctamente
  • La mayoría de las correcciones se completan en 1 o 2 iteraciones (buen diseño).
  • Cada iteración incluye: generación de correcciones → pruebas → validación
  • La ejecución del código domina cada iteración (de 1.5 a 1.7 s).
  • El bucle sale correctamente cuando se cumplen las condiciones

Desglose de costos:

  • Iteración 1: 3.2 s
  • Iteración 2: 4.5 s (más larga debido al contexto acumulado)
  • Bucle total: 7.8 s
  • Síntesis: 0.7 s
  • Canalización de corrección total: 8.5 s

Comparación con la canalización de revisión

Canalización de revisión: ~2.3 s
Canalización de corrección: ~8.5 s (con 2 iteraciones)

La canalización de corrección tarda aproximadamente 3.7 veces más, lo que tiene sentido:

  • Incluye perfeccionamiento iterativo
  • Ejecuta el código varias veces (una vez por iteración).
  • Acumula contexto de intentos anteriores

Paso 4: Lo que descubriste

Patrones de rendimiento

Después de examinar los registros, ahora sabes lo siguiente:

Canalización de revisión:

  • Duración típica: de 2 a 3 segundos
  • Principal consumidor de tiempo: TestRunner (ejecución de código)
  • Llamadas a LLM: Rápidas (de 100 a 300 ms cada una)
  • Operaciones de estado: insignificantes (10 ms)

Canalización de corrección:

  • Duración típica: De 4 a 5 segundos por iteración
  • La mayoría de las correcciones: de 1 a 2 iteraciones
  • Ejecución de código: de 1.5 a 2.0 segundos por iteración
  • Costo progresivo: Las iteraciones posteriores tardan más

Lo que es rápido:

  • Lecturas y escrituras de estado (10 ms)
  • Ejecuciones de herramientas para el análisis (100 ms)
  • Llamadas individuales al LLM (de 100 a 300 ms)

Qué es lento (pero necesario):

  • Ejecución de código con pruebas (0.9 a 2.0 s)
  • Varias iteraciones de bucle (acumulativas)

Dónde buscar problemas

Cuando revises los registros en producción, ten en cuenta lo siguiente:

  • Registros inusualmente largos (más de 15 segundos): Investiga qué salió mal.
  • Intervalos con errores (status != OK): Errores en la ejecución
  • Iteraciones de bucle excesivas (>2): Corrige los problemas de calidad.
  • Recuentos de tokens muy altos: Oportunidades de optimización de instrucciones

Qué aprendiste

Con Cloud Trace, ahora comprendes lo siguiente:

Flujo de solicitudes: Ruta de ejecución completa a través de tus canalizaciones
Características de rendimiento: Qué es rápido, qué es lento y por qué
Comportamiento de bucle: Cómo se ejecutan y finalizan las iteraciones
Jerarquía de tramos: Cómo se anidan las operaciones entre sí
Navegación de seguimiento: Cómo leer gráficos de cascada de manera eficaz
Visibilidad de tokens: Dónde se acumulan los costos de los LLM

Conceptos clave dominados

Registros y tramos:

  • Registros = Cronogramas de solicitudes completas
  • Intervalos = Operaciones individuales dentro de los registros
  • La vista de cascada muestra la jerarquía de ejecución
  • Instrumentación automática a través del ADK

Análisis de rendimiento:

  • Cómo leer las visualizaciones de diagramas de Gantt
  • Cómo identificar las rutas críticas
  • Información sobre las distribuciones de duración
  • Cómo detectar cuellos de botella

Visibilidad de la producción:

  • Cada operación se rastrea automáticamente
  • Uso de tokens capturado por llamada a LLM
  • Los cambios de estado son visibles y rastreables
  • Las iteraciones de bucle se rastrean de forma individual

Pasos siguientes

Sigue explorando Cloud Trace:

  • Supervisa los registros con frecuencia para detectar problemas a tiempo
  • Compara registros para identificar regresiones de rendimiento
  • Usa datos de registro para tomar decisiones de optimización
  • Cómo filtrar por duración para encontrar solicitudes lentas

Observabilidad avanzada (opcional):

  • Exporta registros a BigQuery para realizar análisis complejos (documentos)
  • Crea paneles personalizados en Cloud Monitoring
  • Configura alertas para la degradación del rendimiento
  • Correlaciona los registros con los registros de la aplicación

9. Conclusión: Del prototipo a la producción

Qué compilaste

Comenzaste con solo siete líneas de código y creaste un sistema de agentes de IA de nivel de producción:

# Where we started (7 lines)
agent = Agent(
    model="gemini-2.5-flash",
    instruction="Review Python code for issues"
)

# Where we ended (production system)
- Two distinct multi-agent pipelines (review and fix) built from 8 specialized agents.
- An iterative fix loop architecture for automated validation and retries.
- Real AST-based code analysis tools for deterministic, accurate feedback.
- Robust state management using the "constants pattern" for type-safe communication.
- Fully automated deployment to a managed, scalable cloud infrastructure.
- Complete, built-in observability with Cloud Trace for production monitoring.

Patrones arquitectónicos clave dominados

Patrón

Implementación

Impacto en la producción

Integración de herramientas

Análisis de AST y verificación de estilo

Validación real, no solo opiniones del LLM

Canalizaciones secuenciales

Flujos de trabajo de revisión → corrección

Ejecución predecible y depurable

Arquitectura de bucle

Corrección iterativa con condiciones de salida

Mejora por sí misma hasta alcanzar el éxito

Administración del estado

Patrón de constantes, memoria de tres niveles

Manejo de estados seguro y fácil de mantener

Implementación de producción

Agent Engine a través de deploy.sh

Infraestructura administrada y escalable

Observabilidad

Integración en Cloud Trace

Visibilidad completa del comportamiento de producción

Estadísticas de producción a partir de registros

Tus datos de Cloud Trace revelaron estadísticas importantes:
Se identificó un cuello de botella: Las llamadas al LLM de TestRunner dominan la latencia
Rendimiento de la herramienta: El análisis del AST se ejecuta en 100 ms (excelente)
Tasa de éxito: Los bucles de corrección convergen en 2 o 3 iteraciones
Uso de tokens: Alrededor de 600 tokens por revisión y alrededor de 1, 800 para las correcciones

Estas estadísticas generan mejoras continuas.

Limpia los recursos (opcional)

Si terminaste de experimentar y quieres evitar los cargos, haz lo siguiente:

Borra la implementación de Agent Engine:

import vertexai

client = vertexai.Client(  # For service interactions via client.agent_engines
    project="PROJECT_ID",
    location="LOCATION",
)

RESOURCE_NAME = "projects/{PROJECT_ID}/locations/{LOCATION}/reasoningEngines/{RESOURCE_ID}"

client.agent_engines.delete(
    name=RESOURCE_NAME,
    force=True, # Optional, if the agent has resources (e.g. sessions, memory)
)

Borra el servicio de Cloud Run (si se creó):

gcloud run services delete code-review-assistant \
    --region=$GOOGLE_CLOUD_LOCATION \
    --quiet

Borra la instancia de Cloud SQL (si se creó):

gcloud sql instances delete your-project-db \
    --quiet

Limpia los buckets de almacenamiento:

gsutil -m rm -r gs://your-project-staging
gsutil -m rm -r gs://your-project-artifacts

Próximos pasos

Una vez que hayas completado la base, considera estas mejoras:

  1. Agregar más idiomas: Se extendió la compatibilidad de las herramientas para incluir JavaScript, Go y Java
  2. Integración con GitHub: Revisiones automáticas de solicitudes de extracción
  3. Implementa el almacenamiento en caché: Reduce la latencia para los patrones comunes
  4. Agrega agentes especializados: Análisis de seguridad, análisis de rendimiento
  5. Habilita las pruebas A/B: Compara diferentes modelos y mensajes
  6. Exportar métricas: Envía registros a plataformas de observabilidad especializadas

Conclusiones principales

  1. Comienza con algo simple y realiza iteraciones rápidamente: Siete líneas de código para llegar a la producción en pasos manejables
  2. Herramientas por sobre instrucciones: El análisis real del AST supera a "Verifica si hay errores"
  3. La administración de estados es importante: El patrón de constantes evita errores de escritura.
  4. Los bucles necesitan condiciones de salida: Siempre establece iteraciones máximas y escalamiento
  5. Implementa con automatización: deploy.sh controla toda la complejidad
  6. La observabilidad es fundamental: No se puede mejorar lo que no se puede medir

Recursos para el aprendizaje continuo

Tu recorrido continúa

Creaste más que un asistente de revisión de código: dominaste los patrones para compilar cualquier agente de IA de producción:
✅ Flujos de trabajo complejos con varios agentes especializados
✅ Integración de herramientas reales para capacidades genuinas
✅ Implementación de producción con observabilidad adecuada
✅ Administración de estados para sistemas fáciles de mantener

Estos patrones se pueden escalar desde asistentes simples hasta sistemas autónomos complejos. La base que creaste aquí te será de gran utilidad a medida que abordes arquitecturas de agentes cada vez más sofisticadas.

Te damos la bienvenida al desarrollo de agentes de IA para producción. Tu asistente de revisión de código es solo el comienzo.