Weiter zum Inhalt
OpenAI
Künstliche Intelligenz
KI-Agenten

GPT-5.1 Codex: Leitfaden mit Praxisprojekt – Einen GitHub-Issue-Analyzer-Agenten bauen

In diesem GPT-5.1-Codex-Tutorial verwandelst du GitHub-Issues in echte Engineering-Pläne – mit GitHub CLI, FireCrawl API und OpenAI Agents.
Abid Ali Awan's photo
Abid Ali Awan
Aktualisiert · 10 Min. lesen

Entdecke KI

ChatGPTClaudePerplexity

Erstelle dein kostenloses Konto

Üben und Lernpfad deinen Fortschritt.

Weiter Mit Google
Durch Klick auf die Schaltfläche akzeptierst du unsere Nutzungsbedingungen, unsere Datenschutzrichtlinie und die Speicherung deiner Daten in den USA.

OpenAI hat still und leise GPT-5.1-Codex auf der Developer-Plattform veröffentlicht, und Entwickler nennen es schon jetzt das beste Coding-Modell derzeit. Anders als frühere Codex-Versionen ist GPT-5.1-Codex auf echte Software-Engineering-Aufgaben, langes Reasoning und toolnutzende Agenten ausgelegt.

In diesem Tutorial bauen wir einen vollständigen GitHub-Issue-Analyzer-Agenten mit OpenAI Agents und GPT-5.1 Codex.

Unser Agent wird:

  • Issues direkt aus jedem GitHub-Repository abrufen
  • Das Issue verstehen, aufschlüsseln und kategorisieren
  • Nur die relevanten Dateien und Verzeichnisse im Repo inspizieren
  • Optional eine Dokumentations- und Websuche über die Firecrawl API durchführen
  • Einen detaillierten, schrittweisen Engineering-Plan zur Lösung erstellen

Dieser Agent arbeitet wie eine Senior Engineerin: recherchiert, liest, denkt nach und plant, bevor er irgendetwas schreibt. Schau dir auch unseren separaten Guide zu GPT-5.1 an, um zu sehen, was es sonst noch Neues gibt.

Was ist GPT-5.1-Codex?

GPT-5.1 Codex ist eine spezialisierte Version von GPT-5.1, entwickelt für langlaufende, agentische Coding-Aufgaben statt nur Snippet-Autovervollständigung. Es ist für echtes Software Engineering und agentische Workflows gebaut – perfekt als Motor hinter unserem Issue-to-Plan-Workflow. 

Im Gegensatz zu allgemeinen Modellen versteht Codex Codebasen wie eine erfahrene Entwicklerin: Es liest Issues, denkt über die Architektur nach, identifiziert die richtigen Verzeichnisse und inspiziert nur die Dateien, die wirklich zählen. Das macht den Agenten schneller, intelligenter und deutlich kosteneffizienter.

Codex ist auf langlaufende, agentische Coding-Aufgaben optimiert. Es integriert sich nahtlos mit Entwickler-Tools wie der GitHub CLI und der Firecrawl API. So kann unser Agent Issues abrufen, Projektstrukturen online erkunden, gezielt Dateien lesen und bei Bedarf Dokumentation sammeln. Es folgt Anweisungen genau, liefert saubere, verlässliche Analysen und passt seinen Reasoning-Aufwand an – flott bei einfachen Tasks, tiefgehend bei komplexen.

Durch die Kombination aus starkem Codeverständnis und toolbewusstem Reasoning verwandelt GPT-5.1 Codex ein GitHub-Issue in einen präzisen, umsetzbaren Engineering-Plan – ohne das ganze Repo zu scannen oder Code zu halluzinieren. Es ist das Rückgrat des Workflows, weil es die nötige Engineering-Intuition, Struktur und Präzision mitbringt.

Einrichtung des GitHub-Issue-Analyzers

Bevor wir loslegen, stell sicher, dass deine Umgebung bereit ist. Du brauchst Git auf deinem Rechner. Wenn du unsicher bist, führe git --version aus, um das zu prüfen. Außerdem benötigst du ein Konto auf der OpenAI Developer Platform mit mindestens 6 $ Guthaben, damit die API-Calls ohne Unterbrechung laufen.

Als Nächstes hol dir ein kostenloses Firecrawl-Konto und setze deine API-Keys als Umgebungsvariablen. Darüber kommuniziert dein Analyzer mit OpenAI und Firecrawl:

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

Wenn das steht, installiere die Python-Pakete für den Workflow. Das erste, openai-agents, ist ein leichtgewichtiges Framework, mit dem sich Multi-Agent-Pipelines erstaunlich einfach bauen lassen. Das zweite, firecrawl-py, crawlt Repos oder Dokus und extrahiert nützliche Informationen.

pip install openai-agents
pip install firecrawl-py

Zum Schluss stelle sicher, dass deine GitHub CLI installiert und eingerichtet ist.  Mit dem folgenden Befehl meldest du dich bei GitHub an. 

gh auth login

github cli login

Einen GitHub-Issue-Analyzer-Agenten mit GPT-5.1-Codex bauen

Wir erstellen einen Ordner „src“, der alle Code-Dateien enthält. Der Ordner „agents_pkg“ hält alle Agenten-Dateien, während „tools“ die Tool-Dateien enthält. 

Die Hauptanwendung „app.py“ bietet eine Kommandozeilenoberfläche (CLI), die die Planungsagenten nutzt, um basierend auf Eingaben einen GitHub-Issue-Report zu erzeugen.

So sollte dein Projektverzeichnis aussehen:

gpt-5.1-codex github project directory

1. Web- und GitHub-Tools

Zuerst erstellen wir eine Datei mit agentischen Tools, mit der der Agent über einfache Python-Funktionen auf die GitHub- und Firecrawl-API zugreifen kann.

Firecrawl-API-Tool

Wir starten mit den Tools. Erstelle eine Datei namens firecrawl_tools.py im Verzeichnis src/tools und füge den folgenden Code ein. 

1. Erstellt und gibt einen Firecrawl-Client zurück, der den FIRECRAWL_API_KEY aus deiner Umgebung verwendet, und wirft einen Fehler, wenn der Schlüssel fehlt.

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. Führt mit Firecrawl eine fokussierte Websuche (z. B. Dokus, Blogposts, Fehlermeldungen) aus und gibt die Ergebnisse als JSON zurück, damit der Agent sie als externen Kontext nutzen kann.

@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. Crawlt eine einzelne URL mit Firecrawl (im Markdown-Format) und liefert den strukturierten Seiteninhalt als JSON für tiefere technische Recherchen.

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

GitHub-CLI-Tool

Dann erstellen wir eine Datei namens github_tools.py im Verzeichnis src/tools und fügen folgenden Code hinzu. 

1. Ruft ein bestimmtes GitHub-Issue über die GitHub CLI ab und gibt die Details als JSON zurück, damit der Agent sie lesen kann.

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. Listet nur die relevanten Dateien in einem Remote-GitHub-Repo auf (optional gefiltert nach Pfad und Extension), damit der Agent nicht das ganze Projekt scannt.

@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. Lädt den Inhalt einer einzelnen Datei über die GitHub CLI herunter und dekodiert ihn, gibt den Text (bei Bedarf gekürzt) als JSON zurück.

@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. Planungsagent 

Hier definieren wir einen Issue Planner, der Folgendes kann: 

  1. Ein GitHub-Issue lesen
  2. Entscheiden, welche Teile der Codebasis relevant sind
  3. Nur eine kleine Datei-Auswahl über die GitHub-CLI-Tools inspizieren
  4. Optional Firecrawl für externe Dokus aufrufen
  5. Abschließend einen konkreten, schrittweisen Umsetzungsplan zurückgeben. 

Wir verdrahten die GitHub- und Firecrawl-Tools, geben dem Agenten klare Anweisungen für kosteneffizientes Arbeiten und setzen das Modell auf gpt-5.1-codex.

Erstelle die Datei planner_agent.py im Verzeichnis src/agents_pkg und füge folgenden Code ein:

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. Haupt-CLI-App

Das ist die zentrale Python-Datei mit der Kommandozeilenoberfläche (CLI). Sie führt Logik, Callbacks und Fehlerbehandlung in einer Datei zusammen und nutzt effizient Tools und Agenten aus anderen Dateien.

1. Zuerst richten wir alles ein, was die CLI braucht. Wir binden Standardbibliotheken ein, sorgen unter Windows für korrektes Unicode-Output (Emojis, Symbole) und importieren den OpenAI-Runner sowie unseren Planner-Agenten. 

2. Dann definieren wir die Kommandozeilenoptionen, damit du beim Start ein GitHub-Repository und eine Issue-Nummer übergeben kannst.

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. Danach sammeln wir Eingaben und bereiten den Kontext vor. Wir lesen Repo und Issue aus den Argumenten, prüfen den OpenAI-API-Key, bauen einen klaren Prompt für die schrittweise Analyse und legen eine Markdown-Datei mit Zeitstempel an, in der der finale Plan gespeichert wird.

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. Als Nächstes machen wir das Denken des Agenten sichtbar. Wir ziehen aus jedem Event einen kompakten Reasoning-Schnipsel und drucken eine kurze „💭 Reasoning…“-Zeile, sodass du statt Rohdaten eine menschlich lesbare Einordnung bekommst, woran das Modell gerade arbeitet.

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. Danach kümmern wir uns um Tool-Calls und Tracking. Wir erkennen, welches Tool der Agent nutzt, formatieren die Argumente (z. B. Repo, Pfad oder Query) kompakt, drucken eine „🔧 Calling…“-Meldung und führen intern eine Liste aktiver Tools, um sie bei Abschluss zu markieren und am Ende zusammenzufassen.

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 and display reasoning events."""
    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. Jetzt verdrahten wir Streaming-Loop und Persistenz. Wir verarbeiten Streaming-Events des Agenten, drucken Tokens in Echtzeit, zeigen Reasoning und Tool-Calls live an, fallen bei Problemen elegant auf einen nicht-streamenden Lauf zurück und schreiben den kompletten Umsetzungsplan mit nützlichen Metadaten in eine Markdown-Datei.

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. Zum Schluss verbinden wir alles im Programmeinstiegspunkt. Wir parsen die Argumente, holen Repo und Issue, validieren die Umgebung, bauen Agent und Prompt, starten den Planner mit Streaming (und bei Bedarf Fallback) und speichern das Ergebnis. 

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

Hinweis: Quellcode, Konfiguration und Dokumentation findest du im GitHub-Repository: kingabzpro/Issue-Analyzer. Sieh es dir an und nutze es als Leitfaden, wenn du die Ergebnisse reproduzierst.

Den GitHub-Issue-Analyzer testen

Es gibt zwei Modi für unsere CLI-App: den Interaktivmodus, bei dem dich die App nacheinander nach Repository und Issue-Nummer fragt, und den CLI-Modus, bei dem du alle Infos direkt beim Start mitgibst.

Um den Interaktivmodus zu starten, gib folgenden Befehl ein:

python src/app.py

Nach dem Start wirst du nach Repository-Name und Issue-Nummer gefragt, und die App beginnt mit Tools und Reasoning, um dich zu unterstützen.

Testing the GitHub Issue Analyzer

Innerhalb weniger Sekunden erhältst du eine Zusammenfassung des Issues und Wege zur Lösung. Die Zusammenfassung zeigt auch, welche Tools genutzt wurden, und wo die Markdown-Datei mit allen Infos gespeichert ist.

summary of the issue and ways to resolve it.

Du kannst die Markdown-Datei öffnen, um den detaillierten Issue-Plan zu prüfen.

detailed issue plan.

Im CLI-Modus musst du Repository und Issue in den Befehl aufnehmen, wie hier gezeigt:

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

Der gesamte Prozess wird gestreamt. Du siehst also, welche Tools der Agent nutzt und wie er denkt. Am Ende wird auch die Antwort live gestreamt.

image5.gif

Zukünftige Verbesserungen

Die aktuelle Version des GitHub-Issue-Analyzers ist darauf ausgelegt, Issues zu verstehen und präzise Umsetzungspläne zu erstellen. 

Das wahre Potenzial agentischer Workflows liegt jedoch darin, alle Schritte nach der Planerstellung zu automatisieren. Hier sind größere Upgrades, die du umsetzen kannst:

1. Automatisches Branching und PR-Erstellung

Ziel ist es, einen Plan automatisch in einen funktionierenden Pull Request zu überführen. Das verschlankt den Entwicklungsprozess und beschleunigt den Workflow.

Dazu gehört, dass der Agent eigenständig einen neuen Branch anlegt, Änderungen gemäß Plan vornimmt und GitHub-CLI-Workflows wie gh pr create und gh pr view ausführt.

Außerdem generiert das System automatisch PR-Beschreibungen, Changelogs und verknüpfte Issue-Referenzen. Labels werden intelligent vergeben – etwa Bugfix, Enhancement oder Refactor.

Unterm Strich wird der Agent so zu einem vollständig autonomen „Issue to Pull Request“-Automationssystem – mit deutlich mehr Produktivität und Konsistenz im Entwicklungszyklus.

2. Batch-Analyse von Issues und gebündelte Tool-Ausführung

Dieses Upgrade erweitert die Issue-Analyse um Batch-Fähigkeiten. Statt einzelne Probleme zu betrachten, können Teams mehrere Issues parallel oder sequenziell scannen. So lassen sich große Backlogs effizienter bearbeiten.

Zudem erleichtert es das Erkennen doppelter oder verwandter Issues und verbessert die Organisation. Issues können etwa nach Komplexität, Subsystem oder Impact klassifiziert werden. 

Um Abläufe weiter zu straffen, lassen sich GitHub- oder Firecrawl-Tools im Batch-Modus ausführen. Am Ende reicht ein einziger Befehl, um Dutzende oder gar Hunderte Issues automatisch zu triagieren.

3. Tests, Sicherheitschecks und Validierung vor dem PR

Bevor ein Pull Request (PR) erstellt wird, sollte der Agent die vorgeschlagenen Änderungen gründlich validieren – nicht nur generieren. Geplante Features umfassen das Ausführen von Unit-Tests über GitHub Actions oder lokale Testrunner, um das erwartete Verhalten sicherzustellen. 

Auch die Prüfung von Abhängigkeiten ist entscheidend: Imports verifizieren, fehlende Module erkennen, Versionskonflikte beheben. Zusätzlich sichern Linting, Formatierung und Typprüfungen die Codequalität. 

Wichtig ist zudem, bestehende Build-Pipelines nicht zu brechen und Breaking Changes oder Regressionen zu erkennen. So bleibt der PR sauber, sicher und produktionsreif.

Abschließende Gedanken zu GPT-5.1 Codex

Komplexe, mehrstufige Agenten mit GPT-5.1 Codex und openai-agents zu bauen, ist überraschend einfach. Du musst im Kern nur eigene Tools definieren und dem Modell klar sagen, wann und wie es sie nutzen soll. 

In diesem Tutorial habe ich die GitHub CLI genutzt, weil sie schnell, intuitiv und leicht integrierbar ist. Genauso gut könntest du das GitHub-Python-SDK, direkte API-Calls oder andere CLI-/Bash-Tools verwenden. Die Flexibilität ist hier der eigentliche Hebel.

Du kannst dieses Setup beliebig erweitern. 

Zum Beispiel könntest du bauen:

  • Einen Planungsagenten (haben wir gebaut)
  • Einen Aktionsagenten, der basierend auf dem Plan Codeänderungen durchführt
  • Einen Testagenten, der Tests ausführt und prüft, ob Änderungen etwas brechen
  • Einen PR-Agenten, der einen Pull Request mit sauberer Zusammenfassung öffnet

Ziel dieses Tutorials war zu zeigen, was GPT-5.1 Codex wirklich kann: Es ruft Tools mühelos auf, versteht große Codebasen, denkt strukturiert und kann lange Automationsketten ohne ständige Nutzereingaben ausführen. 

Wenn du mehr über den Aufbau von KI-Agenten lernen willst, schau dir unseren AI Agents with Google ADK Kurs an – und unsere Liste mit spannenden KI-Agent-Projekten zum Nachbauen.


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

Als zertifizierter Data Scientist ist es meine Leidenschaft, modernste Technologien zu nutzen, um innovative Machine Learning-Anwendungen zu entwickeln. Mit meinem fundierten Hintergrund in den Bereichen Spracherkennung, Datenanalyse und Reporting, MLOps, KI und NLP habe ich meine Fähigkeiten bei der Entwicklung intelligenter Systeme verfeinert, die wirklich etwas bewirken können. Neben meinem technischen Fachwissen bin ich auch ein geschickter Kommunikator mit dem Talent, komplexe Konzepte in eine klare und prägnante Sprache zu fassen. Das hat dazu geführt, dass ich ein gefragter Blogger zum Thema Datenwissenschaft geworden bin und meine Erkenntnisse und Erfahrungen mit einer wachsenden Gemeinschaft von Datenexperten teile. Zurzeit konzentriere ich mich auf die Erstellung und Bearbeitung von Inhalten und arbeite mit großen Sprachmodellen, um aussagekräftige und ansprechende Inhalte zu entwickeln, die sowohl Unternehmen als auch Privatpersonen helfen, das Beste aus ihren Daten zu machen.

Themen
OpenAI
Künstliche Intelligenz
KI-Agenten

Top-DataCamp-Kurse

Kurs

Arbeiten mit der OpenAI-API

3 Std.
183.4K
Entwickle deine ersten KI-gestützten Anwendungen mit der API von OpenAI und lerne zugrunde liegende Funktionen von ChatGPT & Co. kennen.
Details anzeigenKurs Starten

Erstelle dein kostenloses Konto

Üben und Lernpfad deinen Fortschritt.

Weiter Mit Google
Durch Klick auf die Schaltfläche akzeptierst du unsere Nutzungsbedingungen, unsere Datenschutzrichtlinie und die Speicherung deiner Daten in den USA.
Mehr anzeigen
Ähnlich

Blog

Arten von KI-Agenten: Ihre Rollen, Strukturen und Anwendungen verstehen

Lerne die wichtigsten Arten von KI-Agenten kennen, wie sie mit ihrer Umgebung interagieren und wie sie in verschiedenen Branchen eingesetzt werden. Verstehe einfache reflexive, modellbasierte, zielbasierte, nutzenbasierte, lernende Agenten und mehr.

Blog

Die 36 wichtigsten Fragen und Antworten zum Thema generative KI für 2026

Dieser Blog hat eine ganze Reihe von Fragen und Antworten zu generativer KI, von den Grundlagen bis hin zu fortgeschrittenen Themen.
Hesam Sheikh Hassani's photo

Hesam Sheikh Hassani

15 Min.

Tutorial

Python Switch Case Statement: Ein Leitfaden für Anfänger

Erforsche Pythons match-case: eine Anleitung zu seiner Syntax, Anwendungen in Data Science und ML sowie eine vergleichende Analyse mit dem traditionellen switch-case.
Matt Crabtree's photo

Matt Crabtree

5 Min.

Tutorial

30 coole Python-Tricks für besseren Code mit Beispielen

Wir haben 30 coole Python-Tricks zusammengestellt, mit denen du deinen Code verbesserst und deine Python-Kompetenzen ausbaust.
Kurtis Pykes 's photo

Kurtis Pykes

15 Min.

Tutorial

Python-Lambda-Funktionen: Ein Leitfaden für Anfänger

Lerne mehr über Python-Lambda-Funktionen, wozu sie gut sind und wann man sie benutzt. Enthält praktische Beispiele und bewährte Methoden für eine effektive Umsetzung.
Mark Pedigo's photo

Mark Pedigo

10 Min.

Tutorial

Python-Tutorial zum Verknüpfen von Zeichenfolgen

Lerne verschiedene Methoden zum Verknüpfen von Zeichenfolgen in Python kennen, mit Beispielen, die jede Technik zeigen.
DataCamp Team's photo

DataCamp Team

5 Min.

Mehr anzeigen