Ir al contenido principal

Guía de GPT-5.1 Codex con proyecto práctico: crea un agente analizador de issues de GitHub

En este tutorial de GPT-5.1-Codex, transformarás issues de GitHub en planes de ingeniería reales usando GitHub CLI, FireCrawl API y OpenAI Agents.
Actualizado 17 sept 2026  · 10 min leer

Explorar con IA

ChatGPTClaudePerplexity

OpenAI ha lanzado discretamente GPT-5.1-Codex en la plataforma para desarrolladores, y muchos ya lo consideran el mejor modelo de programación disponible hoy. A diferencia de las versiones anteriores de Codex, GPT-5.1-Codex está diseñado para ingeniería de software real, razonamiento prolongado y agentes que usan herramientas.

En este tutorial construiremos un agente analizador de issues de GitHub completo usando OpenAI Agents y GPT-5.1 Codex.

Nuestro agente podrá:

  • Obtener issues directamente de cualquier repositorio de GitHub
  • Entender, desglosar y clasificar el issue
  • Inspeccionar solo los archivos y directorios relevantes del repo
  • Opcionalmente buscar en documentación y en la web con la API de Firecrawl
  • Generar un plan de ingeniería detallado, paso a paso, para resolver el issue

Este agente se comporta como un ingeniero senior: investiga, lee, razona y planifica antes de escribir nada. Puedes consultar nuestra guía sobre GPT-5.1 para ver qué más hay de nuevo.

¿Qué es GPT-5.1-Codex?

GPT-5.1 Codex es una versión especializada de GPT-5.1, creada para tareas de programación de larga duración y con agentes, no solo para autocompletar fragmentos. Está pensada para ingeniería de software real y flujos de trabajo con agentes, lo que la convierte en el motor perfecto para nuestro flujo de trabajo de automatización de Issue a Plan.

A diferencia de los modelos generales, Codex entiende los repositorios como lo haría un ingeniero senior: lee los issues, razona sobre la arquitectura, identifica los directorios adecuados y solo inspecciona los archivos que importan de verdad. Esto hace que el agente sea más rápido, más inteligente y mucho más eficiente en costes.

Codex está optimizado para tareas de programación largas y agentic. Se integra de forma natural con herramientas de desarrollo como GitHub CLI y la API de Firecrawl, permitiendo a nuestro agente obtener issues, explorar estructuras de proyectos online, leer archivos concretos y recopilar documentación cuando haga falta. Sigue las instrucciones al detalle, produce análisis limpios y fiables, y adapta su esfuerzo de razonamiento para avanzar rápido en tareas simples y profundizar en las complejas.

Al combinar una gran comprensión del código con razonamiento consciente de herramientas, GPT-5.1 Codex permite que nuestro agente convierta un issue de GitHub en un plan de ingeniería preciso y accionable, sin escanear todo el repo ni alucinar código. Es la columna vertebral del flujo de trabajo porque aporta la intuición, la estructura y la precisión de ingeniería de las que depende el proyecto.

Configuración del analizador de issues de GitHub

Antes de meternos en el proyecto, asegúrate de que tu entorno está listo. Necesitas tener Git instalado en tu máquina. Si no estás seguro, ejecuta git --version para confirmarlo. También necesitarás una cuenta de la OpenAI Developer Platform con al menos 6 $ de crédito para que las llamadas a la API no se interrumpan.

Después, crea una cuenta gratuita en Firecrawl y configura tus claves de API como variables de entorno. Son las que permiten que tu analizador se comunique con OpenAI y Firecrawl:

export OPENAI_API_KEY=sk-...
export FIRECRAWL_API_KEY="fc-..." 

Cuando lo tengas, instala los paquetes de Python que impulsan el flujo. El primero, openai-agents, es un framework ligero que facilita muchísimo crear canalizaciones multiagente. El segundo, firecrawl-py, se encarga de rastrear y extraer información útil de tus repositorios o documentación.

pip install openai-agents
pip install firecrawl-py

Por último, asegúrate de que GitHub CLI está instalado y configurado. El siguiente comando te ayudará a iniciar sesión en tu cuenta de GitHub.

gh auth login

inicio de sesión en github cli

Cómo crear un agente analizador de issues de GitHub con GPT-5.1-Codex

Crearemos una carpeta llamada "src" que contendrá todos los archivos de código. La carpeta "agents_pkg" guardará los archivos del agente y la carpeta "tools" contendrá los archivos de herramientas.

La aplicación principal, "app.py", ofrece una interfaz de línea de comandos (CLI) que utiliza los agentes de planificación para generar un informe de issue de GitHub a partir de la información que introduzcas.

Así debería verse el directorio de tu proyecto:

directorio del proyecto de github con gpt-5.1-codex

1. Herramientas web y de GitHub

Primero, crearemos un archivo de herramientas para agentes que ayudará al agente a acceder a GitHub y a la API de Firecrawl con funciones sencillas de Python.

Herramienta de la API de Firecrawl

Empezaremos por las herramientas. Crea un archivo llamado firecrawl_tools.py en el directorio src/tools y añade el siguiente código.

1. Crea y devuelve un cliente de Firecrawl usando FIRECRAWL_API_KEY de tu entorno, y lanzará un error si falta la clave.

import json
import os

from agents import function_tool
from firecrawl import firecrawl


def _get_firecrawl_client():
    api_key = os.getenv("FIRECRAWL_API_KEY")
    if not api_key:
        raise RuntimeError("FIRECRAWL_API_KEY is not set")
    return firecrawl(api_key=api_key)

2. Usa Firecrawl para ejecutar una búsqueda web enfocada (p. ej., docs, posts, errores) y devuelve los resultados como JSON para que el agente los use como contexto externo.

@function_tool
def firecrawl_search(query: str, limit: int = 3) -> str:
    """
    Run a Firecrawl search for docs related to the issue or tech stack.

    Args:
        query: Search query (usually based on issue title / framework / error message).
        limit: Max number of results to return.

    Returns:
        JSON string of Firecrawl search results.
    """
    client = _get_firecrawl_client()
    results = client.search(query=query, limit=limit)
    return json.dumps(results)

3. Extrae una única URL con Firecrawl (en formato markdown) y devuelve el contenido estructurado de la página como JSON para una investigación técnica más profunda.

@function_tool
def firecrawl_scrape(url: str) -> str:
    """
    Scrape a single URL using Firecrawl for deeper research.

    Args:
        url: URL to scrape (docs, blog, README in another repo, etc.).

    Returns:
        JSON (markdown/structured) content from Firecrawl scrape.
    """
    client = _get_firecrawl_client()
    result = client.scrape(url=url, params={"formats": ["markdown"]})
    return json.dumps(result)

Herramienta de GitHub CLI

Luego, crearemos un archivo llamado github_tools.py en el directorio src/tools y añadiremos el siguiente código.

1. Recupera un issue concreto de GitHub mediante GitHub CLI y devuelve sus detalles como JSON para que el agente los lea.

import base64
import json
import subprocess
from typing import List, Optional

from agents import function_tool


@function_tool
def get_github_issue(repo: str, issue_number: int) -> str:
    """
    Fetch a GitHub issue using the GitHub CLI.

    Args:
        repo: Repository in 'owner/name' format.
        issue_number: The issue number to fetch.

    Returns:
        A JSON string containing the issue fields (title, body, labels, URL, etc.),
        or an error payload if the command fails.
    """
    try:
        result = subprocess.run(
            [
                "gh",
                "issue",
                "view",
                str(issue_number),
                "--repo",
                repo,
                "--json",
                "number,title,body,labels,url,author,createdAt,state,assignees",
            ],
            capture_output=True,
            text=True,
            check=True,
        )
        return result.stdout
    except subprocess.CalledProcessError as e:
        return json.dumps(
            {
                "error": "Failed to fetch issue via GitHub CLI",
                "stderr": e.stderr,
                "repo": repo,
                "issue_number": issue_number,
            }
        )

2. Lista solo los archivos relevantes en un repo remoto de GitHub (opcionalmente filtrados por ruta y extensión) para que el agente no escanee todo el proyecto.

@function_tool
def list_repo_files_gh(
    repo: str,
    max_files: int = 80,
    extensions: Optional[List[str]] = None,
    path_prefixes: Optional[List[str]] = None,
) -> str:
    """
    List *relevant* files in the remote repo using GitHub CLI.

    Uses:
        gh api repos/{repo}/git/trees/HEAD?recursive=1

    The agent is expected to reason first which areas of the codebase are likely relevant
    (e.g. 'src/', 'app/', 'backend/api/', 'cli/'), and then call this tool with a small
    set of path_prefixes instead of scanning the entire project.

    Args:
        repo: Repository in 'owner/name' format (e.g. openai/openai-agents-python).
        max_files: Max number of files to return.
        extensions: Optional list of file extensions to keep (e.g. [".py", ".ts"]).
        path_prefixes: Optional list of path prefixes to include (e.g. ["src/", "app/api/"]).

    Returns:
        JSON string with file paths and filters applied.
    """
    try:
        result = subprocess.run(
            [
                "gh",
                "api",
                f"repos/{repo}/git/trees/HEAD?recursive=1",
            ],
            capture_output=True,
            text=True,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        return json.dumps(
            {
                "error": "Failed to list repo files via GitHub CLI",
                "stderr": e.stderr,
                "repo": repo,
            }
        )

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return json.dumps(
            {
                "error": "Failed to parse JSON from gh api",
                "raw": result.stdout[:2000],
                "repo": repo,
            }
        )

    tree = data.get("tree", [])

    if extensions is not None and not isinstance(extensions, list):
        extensions = [str(extensions)]
    exts = [e.lower() for e in (extensions or [])]

    if path_prefixes is not None and not isinstance(path_prefixes, list):
        path_prefixes = [str(path_prefixes)]
    prefixes = [p.strip() for p in (path_prefixes or []) if p.strip()]

    paths: List[str] = []
    for entry in tree:
        if entry.get("type") != "blob":
            continue  # only files
        path = entry.get("path", "")
        if not path:
            continue

        # If prefixes are provided, only keep files under those subtrees
        if prefixes and not any(path.startswith(pref) for pref in prefixes):
            continue

        if exts:
            suffix = "." + path.split(".")[-1].lower() if "." in path else ""
            if suffix not in exts:
                continue

        paths.append(path)
        if len(paths) >= max_files:
            break

    return json.dumps(
        {
            "repo": repo,
            "count": len(paths),
            "files": paths,
            "filtered_by_extensions": bool(exts),
            "filtered_by_prefixes": bool(prefixes),
        }
    )

3. Descarga y decodifica el contenido de un único archivo del repo usando GitHub CLI y devuelve el texto (recortado si es necesario) como JSON.

@function_tool
def get_repo_file_gh(
    repo: str,
    path: str,
    ref: str = "",
    max_chars: int = 8000,
) -> str:
    """
    Read a file's contents from the remote repo using GitHub CLI.

    Uses:
        gh api repos/{repo}/contents/{path} [ -F ref=<branch> ]

    Args:
        repo: Repository in 'owner/name' format.
        path: File path in the repo (e.g. 'src/main.py').
        ref: Optional branch / commit / tag ref (default: repo's default branch).
        max_chars: Max characters of decoded content to return.

    Returns:
        JSON with file metadata and decoded content (truncated if needed),
        or an error payload if anything fails.
    """
    cmd = ["gh", "api", f"repos/{repo}/contents/{path}"]
    # Only add ref when explicitly set (GitHub default branch otherwise)
    if ref:
        cmd += ["-F", f"ref={ref}"]

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        return json.dumps(
            {
                "error": "Failed to fetch file via GitHub CLI",
                "stderr": e.stderr,
                "repo": repo,
                "path": path,
                "ref": ref or "DEFAULT_BRANCH",
            }
        )

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return json.dumps(
            {
                "error": "Failed to parse JSON from gh api (contents)",
                "raw": result.stdout[:2000],
                "repo": repo,
                "path": path,
            }
        )

    if data.get("type") != "file":
        return json.dumps(
            {
                "error": "Path is not a file",
                "repo": repo,
                "path": path,
                "data_type": data.get("type"),
            }
        )

    encoding = data.get("encoding")
    content_b64 = data.get("content", "")

    if encoding != "base64":
        return json.dumps(
            {
                "error": "Unexpected encoding",
                "repo": repo,
                "path": path,
                "encoding": encoding,
            }
        )

    try:
        # GitHub often includes newlines in base64 payload
        raw_bytes = base64.b64decode(content_b64)
        text = raw_bytes.decode("utf-8", errors="replace")
    except Exception as e:  # noqa: BLE001
        return json.dumps(
            {
                "error": f"Failed to decode file content: {e}",
                "repo": repo,
                "path": path,
                "encoding": encoding,
            }
        )

    truncated = text[:max_chars]
    return json.dumps(
        {
            "repo": repo,
            "path": path,
            "ref": ref or "DEFAULT_BRANCH",
            "truncated": len(text) > max_chars,
            "content": truncated,
        }
    )

2. Agente de planificación

Aquí definimos un agente Issue Planner que sabe:

  1. Leer un issue de GitHub
  2. Decidir qué partes del código son relevantes
  3. Inspeccionar solo un conjunto reducido de archivos mediante las herramientas de GitHub CLI
  4. Llamar opcionalmente a Firecrawl para consultar documentación externa
  5. Y finalmente devolver un plan de ejecución concreto, paso a paso.

Conectamos las herramientas de GitHub y Firecrawl, damos al agente instrucciones detalladas para trabajar con cabeza y control de costes, y le indicamos que use el modelo gpt-5.1-codex.

Crea el archivo planner_agent.py en el directorio src/agents_pkg y añade el siguiente código:

from agents import Agent

from tools.github_tools import (
    get_github_issue,
    list_repo_files_gh,
    get_repo_file_gh,
)
from tools.firecrawl_tools import (
    firecrawl_search,
    firecrawl_scrape,
)


def build_planner_agent() -> Agent:
    """
    Issue Planner agent that:
    - Reads the GitHub issue
    - Reasons about which parts of the repo are relevant
    - Uses GitHub CLI to inspect a *small* set of files online
    - Uses Firecrawl for external research
    - Outputs a concrete, step-by-step execution plan
    """
    return Agent(
        name="Issue Planner",
        instructions=(
            "You are a senior software engineer.\n"
            "Goal: Given a GitHub issue and the online repo (structure + files), plus optional "
            "external research, produce a clear, step-by-step execution plan to resolve the issue.\n\n"
            "CONTEXT:\n"
            "- All repository interaction must be done *online* via GitHub CLI tools.\n"
            "- You have tools to: read the issue, list files under certain paths, read specific files, "
            "  and call Firecrawl search/scrape for docs.\n\n"
            "IMPORTANT STRATEGY (BE SMART):\n"
            "- Be selective and cost-aware. Do NOT scan the whole project.\n"
            "- First, deeply read the issue and infer which part of the system it affects:\n"
            "  routing layer, CLI, API handlers, DB layer, tests, etc.\n"
            "- Based on this reasoning, decide a small list of path prefixes and file types.\n\n"
            "RECOMMENDED WORKFLOW:\n"
            "1. Call get_github_issue(repo, issue_number) to fully understand the problem.\n"
            "2. From the issue, infer a small list of path prefixes where relevant code likely lives,\n"
            "   e.g. ['src/', 'app/', 'backend/api/', 'cli/'] depending on the project style.\n"
            "3. Call list_repo_files_gh with:\n"
            "   - extensions like ['.py', '.ts', '.js', '.tsx', '.jsx']\n"
            "   - path_prefixes set to that small, targeted list\n"
            "   This keeps the search focused instead of scanning the entire project.\n"
            "4. From the returned file list, pick at most ~5-15 key files that are most likely related\n"
            "   (entrypoints, routers, handlers, services, tests).\n"
            "5. Call get_repo_file_gh(repo, path=...) only on those selected files to inspect the "
            "   actual implementation.\n"
            "6. If you need framework or library context (FastAPI, Click, React, etc.), use\n"
            "   firecrawl_search and firecrawl_scrape to pull official docs or good examples.\n\n"
            "OUTPUT FORMAT (execution plan):\n"
            "After you have enough context from the issue + targeted code inspection (+ optional research), "
            "output a concise but concrete plan with sections:\n"
            "   - Issue summary\n"
            "   - Project/codebase understanding (where this issue lives in the architecture)\n"
            "   - Key files / components to touch (with file paths)\n"
            "   - Step-by-step implementation plan (Step 1, Step 2, ...)\n"
            "   - Testing strategy (unit / integration / manual)\n"
            "   - Edge cases, risks, and any open questions\n\n"
            "The plan must be actionable for a mid-level developer. Avoid generic advice; tie your steps "
            "to the actual files and modules you inspected.\n"
        ),
        tools=[
            get_github_issue,
            list_repo_files_gh,
            get_repo_file_gh,
            firecrawl_search,
            firecrawl_scrape,
        ],
        model="gpt-5.1-codex",
    )

3. App principal de CLI

Este es el archivo principal de Python que ofrece una interfaz de línea de comandos (CLI) e integra toda la lógica, callbacks y gestión de errores en un único archivo. Utiliza de forma eficiente las herramientas y agentes definidos en otros archivos.

1. Primero, configuramos e importamos todo lo necesario para la CLI. Incluimos librerías estándar, aseguramos salida Unicode en Windows para que se impriman correctamente emojis y símbolos, e importamos el runner de OpenAI junto con nuestro agente planificador.

2. Después, definimos las opciones de la línea de comandos, permitiéndote pasar un repositorio de GitHub y un número de issue al ejecutar la herramienta.

src/app.py:

import argparse
import asyncio
import json
import os
import pathlib
import sys
from datetime import datetime

# Set UTF-8 encoding for stdout to handle Unicode characters
if sys.platform == "win32":
    import codecs
    sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
    sys.stderr = codecs.getwriter("utf-8")(sys.stderr.detach())

from agents import Runner, ItemHelpers
from openai.types.responses import ResponseTextDeltaEvent

from agents_pkg.planner_agent import build_planner_agent


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Issue Planner: GPT-5.1-Codex + OpenAI Agents + GitHub CLI + Firecrawl\n"        )
    )
    parser.add_argument(
        "--repo",
        help="GitHub repo in 'owner/name' format (e.g. openai/openai-agents-python).",
    )
    parser.add_argument(
        "--issue",
        type=int,
        help="Issue number to plan for.",
    )
    return parser.parse_args()

3. Luego, recopilamos los inputs y preparamos el contexto. Leemos el repo y el issue de los argumentos, nos aseguramos de que la clave de la API de OpenAI está configurada, construimos un prompt claro que indica al agente cómo analizar el issue paso a paso y creamos un archivo markdown con sello temporal donde se guardará el plan final.

def get_user_input(args: argparse.Namespace) -> tuple[str, int]:
    """Get repository and issue number from arguments or user input."""
    repo = args.repo or input("GitHub repo (owner/name): ").strip()
    issue_number = args.issue or int(input("Issue number: ").strip())
    return repo, issue_number


def validate_environment() -> None:
    """Validate that required environment variables are set."""
    if not os.getenv("OPENAI_API_KEY"):
        raise RuntimeError("OPENAI_API_KEY is not set")


def build_user_prompt(repo: str, issue_number: int) -> str:
    """Build the user prompt for the agent."""
    return (
        f"You are helping me plan how to implement GitHub issue #{issue_number} "
        f"in repo '{repo}'.\n\n"
        "Be selective and cost-aware:\n"
        "1. Use get_github_issue(repo, issue_number) to understand the problem.\n"
        "2. Based on the issue text, first reason about which directories and components "
        "   are likely relevant.\n"
        "3. Call list_repo_files_gh(repo, extensions=['.py', '.ts', '.js', '.tsx', '.jsx'], "
        "   path_prefixes=[<your inferred prefixes>]) to only explore those areas.\n"
        "4. From those results, choose a small set of the most relevant files and call "
        "   get_repo_file_gh(repo, path=...) on them.\n"
        "5. Optionally, use firecrawl_search and firecrawl_scrape if you need external docs.\n"
        "6. Finally, generate the execution plan in the structured format from your instructions."
    )


def setup_output_file(repo: str, issue_number: int) -> pathlib.Path:
    """Create output directory and return the markdown file path."""
    output_dir = pathlib.Path("output")
    output_dir.mkdir(exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    markdown_file = output_dir / f"execution_plan_{repo.replace('/', '_')}_issue_{issue_number}_{timestamp}.md"
    return markdown_file

4. Ahora hacemos visible el razonamiento del agente. Intentamos extraer un fragmento claro de razonamiento de cada evento e imprimimos una línea corta de “💭 Razonando…”; así, en lugar de ver los internos en bruto, tienes una pista legible de en qué está pensando el modelo.

def extract_reasoning_text(event_item) -> str | None:
    """Extract reasoning text from a reasoning event item."""
    reasoning_text = None
   
    if hasattr(event_item, 'raw_item'):
        raw = event_item.raw_item
        # Try multiple attribute names
        for attr_name in ['content', 'text', 'reasoning', 'message', 'delta']:
            if hasattr(raw, attr_name):
                val = getattr(raw, attr_name)
                if val and str(val).strip() and str(val) != 'None':
                    reasoning_text = str(val)
                    break
       
        # If still not found, try to access as dict-like
        if not reasoning_text:
            try:
                if hasattr(raw, '__dict__'):
                    for key, val in raw.__dict__.items():
                        if val and str(val).strip() and str(val) != 'None' and key in ['content', 'text', 'reasoning', 'message', 'delta']:
                            reasoning_text = str(val)
                            break
            except:
                pass
   
    # Also try direct attributes on event.item
    if not reasoning_text:
        for attr_name in ['content', 'text', 'reasoning', 'message']:
            if hasattr(event_item, attr_name):
                val = getattr(event_item, attr_name)
                if val and str(val).strip() and str(val) != 'None':
                    reasoning_text = str(val)
                    break
   
    return reasoning_text


def handle_reasoning_event(event_item) -> None:
    """Handle and display reasoning events."""
    reasoning_text = extract_reasoning_text(event_item)
   
    if reasoning_text and reasoning_text.strip():
        # Show first line or first 100 chars
        first_line = reasoning_text.split('\n')[0].strip()[:100]
        if len(reasoning_text.split('\n')[0].strip()) > 100:
            first_line += "..."
        print(f"\n💭 Reasoning: {first_line}", flush=True)
    else:
        # Don't show "None" - just show that reasoning is happening
        print(f"\n💭 Reasoning...", flush=True)

5. Después, gestionamos las llamadas a herramientas y su seguimiento. Detectamos qué herramienta está usando el agente, formateamos sus argumentos (como repo, path o query) en una cadena compacta, imprimimos un mensaje “🔧 Llamando…” y mantenemos un mapa interno de herramientas activas para marcarlas como completadas y luego resumir todo lo que se ejecutó.

def extract_tool_info(event_item) -> tuple[str | None, str | None]:
    """Extract tool name and ID from a tool call event item."""
    tool_name = None
    tool_id = None
   
    # First try raw_item which contains the actual tool call data
    if hasattr(event_item, 'raw_item'):
        raw = event_item.raw_item
        # Try accessing tool_call through various paths
        tool_call = None
        if hasattr(raw, 'tool_call'):
            tool_call = raw.tool_call
        elif hasattr(raw, 'function_call'):
            tool_call = raw.function_call
       
        if tool_call:
            # Try to get name from tool_call
            if hasattr(tool_call, 'name'):
                tool_name = tool_call.name
            elif hasattr(tool_call, 'function') and hasattr(tool_call.function, 'name'):
                tool_name = tool_call.function.name
            # Try to get ID
            if hasattr(tool_call, 'id'):
                tool_id = tool_call.id
            elif hasattr(tool_call, 'tool_call_id'):
                tool_id = tool_call.tool_call_id
       
        # Fallback: try direct attributes on raw
        if not tool_name:
            if hasattr(raw, 'name'):
                tool_name = getattr(raw, 'name')
            elif hasattr(raw, 'function') and hasattr(raw.function, 'name'):
                tool_name = raw.function.name
            # Try using getattr with different possible attribute names
            for attr_name in ['tool_name', 'function_name', 'name']:
                if hasattr(raw, attr_name):
                    tool_name = getattr(raw, attr_name, None)
                    if tool_name:
                        break
   
    # Fallback to direct attributes
    if not tool_name and hasattr(event_item, 'tool_call'):
        tool_call = event_item.tool_call
        if hasattr(tool_call, 'name'):
            tool_name = tool_call.name
        if hasattr(tool_call, 'id'):
            tool_id = tool_call.id
        elif hasattr(tool_call, 'function') and hasattr(tool_call.function, 'name'):
            tool_name = tool_call.function.name
    if not tool_name and hasattr(event_item, 'name'):
        tool_name = event_item.name
    if not tool_name and hasattr(event_item, 'function'):
        func = event_item.function
        if hasattr(func, 'name'):
            tool_name = func.name
   
    return tool_name, tool_id



def format_tool_arguments(tool_call_obj) -> str:
    """Format tool arguments for display."""
    if not tool_call_obj or not hasattr(tool_call_obj, 'arguments'):
        return ""
   
    try:
        args_dict = json.loads(tool_call_obj.arguments) if isinstance(tool_call_obj.arguments, str) else tool_call_obj.arguments
        if 'repo' in args_dict:
            tool_args = f" → {args_dict['repo']}"
            if 'issue_number' in args_dict:
                tool_args += f"#{args_dict['issue_number']}"
            return tool_args
        elif 'path' in args_dict:
            return f" → {args_dict['path']}"
        elif 'query' in args_dict:
            q = str(args_dict['query'])
            return f" → {q[:40]}..." if len(q) > 40 else f" → {q}"
        elif 'url' in args_dict:
            return f" → {args_dict['url']}"
        elif 'extensions' in args_dict or 'path_prefixes' in args_dict:
            parts = []
            if 'extensions' in args_dict:
                parts.append(f"ext={args_dict['extensions']}")
            if 'path_prefixes' in args_dict:
                parts.append(f"paths={args_dict['path_prefixes']}")
            return f" → {', '.join(parts)}"
    except:
        pass
   
    return ""


def handle_tool_call_event(event_item, active_tools: dict, tool_counter: int) -> tuple[int, bool]:
    """Handle tool call events and return updated tool_counter and whether event was handled."""
    tool_name, tool_id = extract_tool_info(event_item)
   
    if tool_name:
        tool_counter += 1
        tool_id = tool_id or f"tool_{tool_counter}"
        active_tools[tool_id] = tool_name
       
        # Get tool arguments if available
        tool_call_obj = None
        if hasattr(event_item, 'raw_item') and hasattr(event_item.raw_item, 'tool_call'):
            tool_call_obj = event_item.raw_item.tool_call
        elif hasattr(event_item, 'tool_call'):
            tool_call_obj = event_item.tool_call
       
        tool_args = format_tool_arguments(tool_call_obj)
        print(f"\n[{tool_counter}] 🔧 Calling: {tool_name}{tool_args}...", flush=True)
        return tool_counter, True
    else:
        # Still couldn't extract - try to inspect raw_item structure
        if hasattr(event_item, 'raw_item'):
            raw = event_item.raw_item
            try:
                raw_attrs = [attr for attr in dir(raw) if not attr.startswith('_')]
                # Look for attributes that might contain the tool name
                for attr in raw_attrs:
                    try:
                        val = getattr(raw, attr)
                        if isinstance(val, str) and ('get_github' in val.lower() or 'list_repo' in val.lower() or 'firecrawl' in val.lower()):
                            tool_name = val
                            break
                        # Check if it's a dict-like object with name
                        if hasattr(val, 'name'):
                            tool_name = val.name
                            break
                    except:
                        continue
               
                if tool_name:
                    tool_counter += 1
                    tool_id = tool_id or f"tool_{tool_counter}"
                    active_tools[tool_id] = tool_name
                    print(f"\n[{tool_counter}] 🔧 Calling: {tool_name}...", flush=True)
                    return tool_counter, True
                else:
                    # Print raw_item structure for debugging
                    print(f"\n[DEBUG] raw_item attrs: {raw_attrs[:10]}", flush=True)
            except Exception as e:
                print(f"\n[DEBUG] Error inspecting raw_item: {e}", flush=True)
   
    return tool_counter, False


def handle_tool_output_event(event_item, active_tools: dict, completed_tools: list) -> None:
    """Handle tool output events and track completed tools."""
    tool_id = None
   
    # Try raw_item first
    if hasattr(event_item, 'raw_item') and hasattr(event_item.raw_item, 'tool_call_id'):
        tool_id = event_item.raw_item.tool_call_id
    elif hasattr(event_item, 'tool_call_id'):
        tool_id = event_item.tool_call_id
    elif hasattr(event_item, 'raw_item') and hasattr(event_item.raw_item, 'tool_call'):
        if hasattr(event_item.raw_item.tool_call, 'id'):
            tool_id = event_item.raw_item.tool_call.id
    elif hasattr(event_item, 'tool_call'):
        if hasattr(event_item.tool_call, 'id'):
            tool_id = event_item.tool_call.id
        elif hasattr(event_item.tool_call, 'function') and hasattr(event_item.tool_call.function, 'name'):
            # Try to match by name if ID not available
            tool_name_match = event_item.tool_call.function.name
            for tid, tname in active_tools.items():
                if tname == tool_name_match:
                    tool_id = tid
                    break
   
    if tool_id and tool_id in active_tools:
        tool_name = active_tools.pop(tool_id)
        completed_tools.append(tool_name)
    elif active_tools:
        # Fallback: use the first active tool
        tool_id, tool_name = next(iter(active_tools.items()))
        active_tools.pop(tool_id)
        completed_tools.append(tool_name)

6. Ahora conectamos el bucle de streaming y la persistencia. Procesamos los eventos en streaming del agente, imprimimos los tokens según llegan, mostramos en tiempo real el razonamiento y las llamadas a herramientas, hacemos fallback con elegancia a una ejecución sin streaming si algo falla y, por último, escribimos el plan de ejecución completo en un archivo markdown con metadatos útiles.

async def process_streaming_events(result, repo: str, issue_number: int) -> str:
    """Process streaming events from the agent execution."""
    final_output = ""
    active_tools = {}  # Track active tool calls by ID
    tool_counter = 0
    completed_tools = []
    first_event_received = False
   
    # Stream the events as they come in
    async for event in result.stream_events():
        # Handle raw response events (token-by-token streaming) - print immediately
        if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
            if not first_event_received:
                first_event_received = True
            delta = event.data.delta
            print(delta, end="", flush=True)
            final_output += delta
        # Handle run item events (higher level updates)
        elif event.type == "run_item_stream_event":
            item_type = getattr(event.item, 'type', 'unknown')
           
            # Show reasoning events
            if item_type == "reasoning_item":
                handle_reasoning_event(event.item)
            elif item_type == "tool_call_item":
                tool_counter, handled = handle_tool_call_event(event.item, active_tools, tool_counter)
                if handled:
                    first_event_received = True
            elif item_type == "tool_call_output_item":
                handle_tool_output_event(event.item, active_tools, completed_tools)
            elif item_type == "message_output_item":
                message_text = ItemHelpers.text_message_output(event.item)
                if message_text and (not final_output or message_text not in final_output):
                    print(f"\n{message_text}", flush=True)
                    final_output += message_text
   
    print()  # Add newline after streaming
   
    # Show summary of tools used
    if completed_tools:
        print(f"---\n\n📊 Tools used ({len(completed_tools)}): {', '.join(completed_tools)}", flush=True)
   
    # If no streaming events occurred, fall back to final output
    if not final_output:
        final_output = result.final_output
        if final_output:
            print(final_output, flush=True)
   
    return final_output

async def run_agent_with_streaming(agent, user_prompt: str, repo: str, issue_number: int) -> str:
    """Run the agent with streaming support and fallback handling."""
    try:
        # Run agent with streaming (run_streamed is synchronous, returns immediately)
        result = Runner.run_streamed(
            agent,
            input=user_prompt,
            context={"repo": repo, "issue_number": issue_number},
        )
       
        return await process_streaming_events(result, repo, issue_number)
       
    except Exception as e:
        print(f"⚠️  Error: {e}", flush=True)
        # Fallback to standard async run
        result = await Runner.run(
            agent,
            input=user_prompt,
            context={"repo": repo, "issue_number": issue_number},
        )
        print(result.final_output, flush=True)
        return result.final_output

def save_output_to_file(markdown_file: pathlib.Path, repo: str, issue_number: int, final_output: str) -> None:
    """Save the final output to a markdown file."""
    with open(markdown_file, 'w', encoding='utf-8') as f:
        f.write(f"# GitHub Issue Analysis: {repo}#{issue_number}\n\n")
        f.write(f"**Generated on:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
        f.write(f"**Repository:** {repo}\n")
        f.write(f"**Issue Number:** {issue_number}\n\n")
        f.write("---\n\n")
        f.write(final_output)
   
    print(f"---\n\n✅ Saved: {markdown_file}", flush=True)

7. Por último, unimos todo en el punto de entrada principal. Analizamos los argumentos, obtenemos el repo y el issue, validamos el entorno, construimos el agente y su prompt, ejecutamos el planificador con streaming (con fallback a ejecución síncrona si hace falta) y guardamos el resultado.

async def main() -> None:
    """Main entry point for the application."""
    args = parse_args()
    repo, issue_number = get_user_input(args)
    validate_environment()
   
    agent = build_planner_agent()
    user_prompt = build_user_prompt(repo, issue_number)
    markdown_file = setup_output_file(repo, issue_number)
   
    print(f"\n🔍 Analyzing {repo}#{issue_number}...\n")
   
    # Run the agent with streaming support
    try:
        final_output = await run_agent_with_streaming(agent, user_prompt, repo, issue_number)
    except Exception as e:
        print(f"⚠️  Error: {e}", flush=True)
        # Final fallback to standard sync run
        result = Runner.run_sync(
            agent,
            input=user_prompt,
            context={"repo": repo, "issue_number": issue_number},
        )
        print(result.final_output, flush=True)
        final_output = result.final_output

    # Save output to file
    save_output_to_file(markdown_file, repo, issue_number, final_output)


if __name__ == "__main__":
    asyncio.run(main())

Nota: El código fuente, la configuración y la documentación están disponibles en el repositorio de GitHub: kingabzpro/Issue-Analyzer. Revísalo y úsalo como guía al replicar los resultados.

Prueba del analizador de issues de GitHub

Hay dos formas de usar nuestra app CLI: en modo interactivo, donde la app te pedirá uno a uno el nombre del repositorio y el número del issue, y en modo CLI, donde debes proporcionar toda la información al lanzar la app.

Para iniciar el modo interactivo, escribe el siguiente comando:

python src/app.py

Una vez iniciado, se te pedirá el nombre del repositorio y el número del issue, y la app comenzará a usar herramientas y razonamiento para ayudarte.

Prueba del analizador de issues de GitHub

En pocos segundos, recibirás un resumen del issue y propuestas para resolverlo. Este resumen incluye detalles sobre las herramientas usadas y la ubicación del archivo markdown donde se ha guardado la información.

resumen del issue y formas de resolverlo.

Puedes abrir el archivo markdown para revisar un plan de issue detallado.

plan de issue detallado.

El modo CLI requiere incluir el nombre del repositorio y el número de issue directamente en el comando, como se muestra a continuación:

python src/app.py --repo kingabzpro/Travel-with-Kimi-K2 --issue 1

Todo el proceso se transmite en streaming, por lo que verás qué herramientas usa el agente y si está razonando bien o no. También verás la respuesta final en streaming.

image5.gif

Mejoras futuras

La versión actual del analizador de issues de GitHub está pensada para entender issues y generar planes de ejecución precisos.

Sin embargo, el verdadero potencial de un flujo de trabajo con agentes está en automatizar todos los pasos posteriores a la creación del plan. Estas son algunas mejoras importantes que puedes implementar en el sistema:

1. Creación automática de ramas y PR

El objetivo es transformar un plan en un pull request funcional de forma automática. Esto agilizará el desarrollo, permitiendo un flujo mucho más eficiente.

Esta función incluirá la capacidad de crear una nueva rama directamente desde el agente, lo que facilitará la implementación de cambios. Además, el agente aplicará modificaciones de código basadas en el plan generado y ejecutará flujos de GitHub CLI como gh pr create y gh pr view.

Además, el sistema generará automáticamente descripciones de PR, changelogs y referencias a issues vinculados. También aplicará etiquetas de forma inteligente, clasificando como correcciones de errores, mejoras o refactors.

En conjunto, esta iniciativa convierte al agente en un sistema autónomo de automatización de Issue a Pull Request, mejorando notablemente la productividad y la consistencia del ciclo de desarrollo.

2. Análisis por lotes de issues y ejecución masiva de herramientas

Esta mejora potenciará el análisis de issues introduciendo capacidades por lotes. En lugar de centrarse en problemas individuales, los equipos podrán escanear múltiples issues a la vez, ya sea en paralelo o en cola, lo que facilita gestionar grandes volúmenes de manera eficaz.

Además, facilitará identificar issues duplicados o relacionados, mejorando la organización. Se podrán clasificar por criterios como complejidad, subsistema o impacto.

Para agilizar aún más, los equipos podrán ejecutar herramientas de GitHub o Firecrawl en modo por lotes, incrementando la eficiencia. En definitiva, esta mejora ofrece un único comando capaz de priorizar automáticamente decenas o cientos de issues de una vez.

3. Pruebas pre-PR, comprobaciones de seguridad y validación

Antes de crear un pull request (PR), es esencial que el agente valide a fondo los cambios propuestos en lugar de simplemente generarlos. Esta validación debe incluir varias funciones planificadas, como ejecutar tests unitarios mediante GitHub Actions o runners locales para asegurar que el código se comporta como se espera.

La validación de dependencias también es clave: comprobar imports, detectar módulos que falten y resolver incompatibilidades de versiones que puedan afectar al proyecto. Además, linting, formateo y comprobación de tipos son cruciales para mantener la calidad del código.

Hay que asegurar que las modificaciones no rompen los pipelines de build existentes y detectar cambios rompientes en APIs o regresiones. Siguiendo estos pasos, garantizamos que el PR llegue limpio, seguro y listo para producción.

Reflexiones finales sobre GPT-5.1 Codex

Crear agentes avanzados y de varios pasos con GPT-5.1 Codex y openai-agents es sorprendentemente sencillo. Básicamente, define tus propias herramientas y dale al modelo instrucciones claras sobre cuándo y cómo usarlas.

En este tutorial he usado GitHub CLI porque es rápido, intuitivo y fácil de integrar, pero podrías usar igual de bien el SDK de Python de GitHub, llamadas directas a la API o cualquier otra utilidad de CLI o Bash. La flexibilidad es la clave.

Puedes ampliar esta configuración tanto como quieras.

Por ejemplo, podrías crear:

  • Un agente planificador (que ya hemos construido)
  • Un agente de acción que aplique cambios de código basados en el plan
  • Un agente de pruebas que ejecute tests y valide si los cambios han roto algo
  • Un agente de PR que abra un pull request con un resumen claro

El objetivo de este tutorial era mostrar de lo que GPT-5.1 Codex es realmente capaz: gestiona llamadas a herramientas sin esfuerzo, entiende grandes bases de código, realiza razonamiento estructurado y puede ejecutar cadenas de automatización largas sin necesidad de interacción constante.

Si te interesa aprender más sobre cómo crear agentes de IA, te recomiendo nuestro curso de AI Agents con Google ADK y también nuestra lista de proyectos de agentes de IA para construir.


Abid Ali Awan's photo
Author
Abid Ali Awan
LinkedIn
Twitter

Soy un científico de datos certificado que disfruta creando aplicaciones de aprendizaje automático y escribiendo blogs sobre ciencia de datos. Actualmente me centro en la creación de contenidos, la edición y el trabajo con grandes modelos lingüísticos.

Temas
OpenAI
Inteligencia Artificial
Agentes de IA

Los mejores cursos de DataCamp

Curso

Trabajar con la API de OpenAI

3 h
172.6K
Desarrolla aplicaciones basadas en IA con la API OpenAI. Conoce la funcionalidad que sustenta aplicaciones populares de IA como ChatGPT.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado
An avian AI exits its cage

blog

12 alternativas de código abierto a GPT-4

Alternativas de código abierto a GPT-4 que pueden ofrecer un rendimiento similar y requieren menos recursos informáticos para funcionar. Estos proyectos vienen con instrucciones, fuentes de código, pesos del modelo, conjuntos de datos e IU de chatbot.
Abid Ali Awan's photo

Abid Ali Awan

9 min

An AI juggles tasks

blog

Cinco proyectos que puedes crear con modelos de IA generativa (con ejemplos)

Aprende a utilizar modelos de IA generativa para crear un editor de imágenes, un chatbot similar a ChatGPT con pocos recursos y una aplicación clasificadora de aprobación de préstamos y a automatizar interacciones PDF y un asistente de voz con GPT.
Abid Ali Awan's photo

Abid Ali Awan

10 min

Tutorial

Visión GPT-4: Guía completa para principiantes

Este tutorial le presentará todo lo que necesita saber sobre GPT-4 Vision, desde cómo acceder a él hasta ejemplos prácticos del mundo real y sus limitaciones.
Arunn Thevapalan's photo

Arunn Thevapalan

12 min

Tutorial

Tutorial de DeepSeek-Coder-V2: Ejemplos, instalación, puntos de referencia

DeepSeek-Coder-V2 es un modelo de lenguaje de código de código abierto que rivaliza con el rendimiento de GPT-4, Gemini 1.5 Pro, Claude 3 Opus, Llama 3 70B o Codestral.
Dimitri Didmanidze's photo

Dimitri Didmanidze

8 min

Tutorial

Guía para principiantes sobre el uso de la API ChatGPT

Esta guía te acompanya a través de los fundamentos de la API ChatGPT, demostrando su potencial en el procesamiento del lenguaje natural y la comunicación impulsada por la IA.
Moez Ali's photo

Moez Ali

11 min

Tutorial

Uso de GPT-3.5 y GPT-4 mediante la API OpenAI en Python

En este tutorial, aprenderás a trabajar con el paquete OpenAI Python para mantener conversaciones programáticamente con ChatGPT.
Richie Cotton's photo

Richie Cotton

14 min

Ver MásVer Más