Compartir en Twitter
Go to Homepage

CONSTRUYE UN AGENTE DE CODIFICACIÓN CON PYTHON Y GEMINI

October 9, 2025

Introducción al Agente de Codificación con Python y Gemini

Los agentes de codificación impulsados por inteligencia artificial están transformando la forma en que los desarrolladores abordan tareas repetitivas o complejas. En este tutorial, aprenderás a construir un agente de codificación con Python utilizando la API gratuita de Gemini de Google. Este agente será una herramienta de línea de comandos capaz de leer archivos, modificar código, ejecutar scripts de Python y resolver problemas de programación de manera iterativa. A lo largo del artículo, exploraremos cómo configurar el entorno, integrar la API de Gemini, implementar funciones clave y crear un bucle de agente que permita al sistema iterar sobre sus propias acciones para completar tareas. Este tutorial está diseñado para desarrolladores con conocimientos básicos de Python que deseen explorar la automatización de tareas de programación con inteligencia artificial.

Configuración del Entorno de Python

Para comenzar, es necesario configurar un entorno de desarrollo adecuado. Utilizaremos uv, una herramienta moderna para gestionar proyectos de Python, que facilita la creación de entornos virtuales y la gestión de dependencias. Sigue estos pasos para preparar el entorno:

  1. Crea un nuevo proyecto con uv ejecutando el siguiente comando en tu terminal:
uv init agente-codificacion
cd agente-codificacion
  1. Inicializa un entorno virtual en el directorio del proyecto:
uv venv
  1. Activa el entorno virtual. En sistemas Unix/Linux/MacOS, usa:
source .venv/bin/activate

En Windows, el comando sería:

.\.venv\Scripts\activate

Una vez activado, el nombre del proyecto aparecerá en el prompt de la terminal, indicando que estás dentro del entorno virtual.

  1. Agrega las dependencias necesarias al proyecto con:
uv add google-generativeai==0.8.3
uv add python-dotenv==1.0.1

Estas dependencias se registrarán en el archivo pyproject.toml. La biblioteca google-generativeai permite interactuar con la API de Gemini, mientras que python-dotenv gestiona variables de entorno para almacenar claves de API de forma segura.

  1. Asegúrate de añadir el directorio venv al archivo .gitignore para evitar que se suba al control de versiones:
echo "venv" >> .gitignore

Integración de la API de Gemini

La API de Gemini de Google es el núcleo de nuestro agente, ya que proporciona las capacidades de un modelo de lenguaje grande (LLM) para procesar prompts y generar respuestas. Para integrarla, primero necesitas una clave de API:

  1. Crea una cuenta en Google AI Studio y genera una clave de API.
  2. Crea un archivo .env en la raíz del proyecto y añade tu clave:
GEMINI_API_KEY=tu_clave_de_api_aqui
  1. Asegúrate de añadir .env al archivo .gitignore:
echo ".env" >> .gitignore
  1. En el archivo main.py, carga las variables de entorno y configura el cliente de Gemini:
import os
from dotenv import load_dotenv
from google import generativeai as genai

load_dotenv()
api_key = os.environ.get("GEMINI_API_KEY")
genai.configure(api_key=api_key)
  1. Realiza una llamada inicial a la API para probar la integración. Usa el modelo gemini-1.5-flash (versión actualizada al 2025) con un prompt simple:
model = genai.GenerativeModel("gemini-1.5-flash")
prompt = "Explica cómo funcionan los entornos virtuales de Python en una frase."
response = model.generate_content(prompt)
print(response.text)
print(f"Tokens de prompt: {response.usage_metadata.prompt_token_count}")
print(f"Tokens de respuesta: {response.usage_metadata.candidates_token_count}")

Este código envía un prompt al modelo y muestra la respuesta junto con el conteo de tokens utilizados. Los tokens son la unidad que mide el uso de la API, aproximadamente equivalentes a 4 caracteres de texto. Mantenerse dentro del límite gratuito de la API es crucial para evitar costos adicionales.

Aceptar Entrada desde la Línea de Comandos

Para hacer el agente más interactivo, permitiremos que los usuarios ingresen prompts a través de la línea de comandos. Esto elimina la necesidad de modificar el código para cada tarea. Actualiza main.py para manejar argumentos de línea de comandos:

import sys

def main():
    load_dotenv()
    api_key = os.environ.get("GEMINI_API_KEY")
    genai.configure(api_key=api_key)

    if len(sys.argv) < 2:
        print("Uso: uv run main.py 'tu prompt aquí'")
        sys.exit(1)

    user_prompt = " ".join(sys.argv[1:])
    model = genai.GenerativeModel("gemini-1.5-flash")
    response = model.generate_content(user_prompt)
    print(response.text)
    print(f"Tokens de prompt: {response.usage_metadata.prompt_token_count}")
    print(f"Tokens de respuesta: {response.usage_metadata.candidates_token_count}")

if __name__ == "__main__":
    main()

Ejecuta el programa con un prompt como:

uv run main.py "Explica qué es un entorno virtual en Python"

Si no se proporciona un prompt, el programa muestra un mensaje de error y termina con un código de salida 1.

Estructura de Mensajes para Conversaciones

Los LLMs como Gemini funcionan mejor cuando se les proporciona un contexto conversacional. En lugar de enviar prompts individuales, almacenaremos una lista de mensajes que representen la conversación completa, incluyendo los roles de “usuario” y “modelo”. Actualiza main.py para usar una lista de mensajes:

from google.generativeai.types import ContentDict, PartDict

messages = [
    ContentDict(role="user", parts=[PartDict(text=user_prompt)])
]
model = genai.GenerativeModel("gemini-1.5-flash")
response = model.generate_content(messages)
messages.append(response.candidates[0].content)
print(response.text)

Cada mensaje tiene un rol (user o model) y una lista de partes (parts) que contienen el texto. Esta estructura permite al modelo mantener el contexto de la conversación, mejorando la coherencia de las respuestas.

Modo Verbose para Depuración

Para facilitar la depuración, implementaremos un modo verbose que muestra información adicional, como el prompt del usuario y el conteo de tokens. Agrega una bandera --verbose al programa:

def main():
    load_dotenv()
    api_key = os.environ.get("GEMINI_API_KEY")
    genai.configure(api_key=api_key)

    verbose = "--verbose" in sys.argv
    args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]

    if not args:
        print("Uso: uv run main.py 'tu prompt aquí' [--verbose]")
        sys.exit(1)

    user_prompt = " ".join(args)
    if verbose:
        print(f"Prompt del usuario: {user_prompt}")

    messages = [
        ContentDict(role="user", parts=[PartDict(text=user_prompt)])
    ]
    model = genai.GenerativeModel("gemini-1.5-flash")
    response = model.generate_content(messages)
    if verbose:
        print(f"Tokens de prompt: {response.usage_metadata.prompt_token_count}")
        print(f"Tokens de respuesta: {response.usage_metadata.candidates_token_count}")
    print(response.text)

if __name__ == "__main__":
    main()

Ejecuta con:

uv run main.py "Explica entornos virtuales" --verbose

El modo verbose mostrará el prompt y los conteos de tokens, lo que es útil para monitorear el uso de la API y depurar problemas.

Proyecto de Calculadora de Ejemplo

Para probar el agente, usaremos un proyecto de calculadora simple que el agente podrá leer, modificar y ejecutar. Crea un directorio calculator en la raíz del proyecto con la siguiente estructura:

├── calculator
│   ├── main.py
│   ├── pkg
│   │   ├── calculator.py
│   │   └── render.py
│   └── tests.py

main.py

import sys
from pkg.calculator import Calculator
from pkg.render import format_json_output

def main():
    calculator = Calculator()
    if len(sys.argv) <= 1:
        print("Aplicación de Calculadora")
        print('Uso: python main.py "<expresión>"')
        print('Ejemplo: python main.py "3 + 5"')
        return

    expression = " ".join(sys.argv[1:])
    try:
        result = calculator.evaluate(expression)
        if result is not None:
            to_print = format_json_output(expression, result)
            print(to_print)
        else:
            print("Error: La expresión está vacía o contiene solo espacios.")
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()

calculator.py

class Calculator:
    def __init__(self):
        self.operators = {
            "+": lambda a, b: a + b,
            "-": lambda a, b: a - b,
            "*": lambda a, b: a * b,
            "/": lambda a, b: a / b,
        }
        self.precedence = {
            "+": 1,
            "-": 1,
            "*": 2,
            "/": 2,
        }

    def evaluate(self, expression):
        if not expression or expression.isspace():
            return None
        tokens = expression.strip().split()
        return self._evaluate_infix(tokens)

    def _evaluate_infix(self, tokens):
        values = []
        operators = []

        for token in tokens:
            if token in self.operators:
                while (
                    operators
                    and operators[-1] in self.operators
                    and self.precedence[operators[-1]] >= self.precedence[token]
                ):
                    self._apply_operator(operators, values)
                operators.append(token)
            else:
                try:
                    values.append(float(token))
                except ValueError:
                    raise ValueError(f"Token inválido: {token}")

        while operators:
            self._apply_operator(operators, values)

        if len(values) != 1:
            raise ValueError("Expresión inválida")

        return values[0]

    def _apply_operator(self, operators, values):
        if not operators:
            return
        operator = operators.pop()
        if len(values) < 2:
            raise ValueError(f"No hay suficientes operandos para el operador {operator}")
        b = values.pop()
        a = values.pop()
        values.append(self.operators[operator](a, b))

render.py

import json

def format_json_output(expression: str, result: float, indent: int = 2) -> str:
    if isinstance(result, float) and result.is_integer():
        result_to_dump = int(result)
    else:
        result_to_dump = result
    output_data = {
        "expression": expression,
        "result": result_to_dump,
    }
    return json.dumps(output_data, indent=indent)

tests.py

import unittest
from pkg.calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calculator = Calculator()

    def test_addition(self):
        result = self.calculator.evaluate("3 + 5")
        self.assertEqual(result, 8)

    def test_subtraction(self):
        result = self.calculator.evaluate("10 - 4")
        self.assertEqual(result, 6)

    def test_multiplication(self):
        result = self.calculator.evaluate("3 * 4")
        self.assertEqual(result, 12)

    def test_division(self):
        result = self.calculator.evaluate("10 / 2")
        self.assertEqual(result, 5)

if __name__ == "__main__":
    unittest.main()

Ejecuta las pruebas con:

uv run calculator/tests.py

Y prueba la calculadora con:

uv run calculator/main.py "3 + 5"

Esto debería devolver un resultado formateado en JSON con la expresión y el resultado.

Funciones del Agente

El agente necesita funciones que le permitan interactuar con el sistema de archivos y ejecutar código. Estas funciones deben devolver texto y manejar errores de forma segura. Todas estarán restringidas a un directorio de trabajo específico (./calculator) para garantizar la seguridad.

Listar Archivos

Crea un directorio functions y un archivo get_files_info.py:

import os
from google.generativeai.types import FunctionDeclaration, Schema, Type

def get_files_info(working_directory, directory="."):
    abs_working_dir = os.path.abspath(working_directory)
    target_dir = os.path.abspath(os.path.join(working_directory, directory))
    if not target_dir.startswith(abs_working_dir):
        return f'Error: No se puede listar "{directory}" ya que está fuera del directorio de trabajo permitido'
    if not os.path.isdir(target_dir):
        return f'Error: "{directory}" no es un directorio'
    try:
        files_info = []
        for filename in os.listdir(target_dir):
            filepath = os.path.join(target_dir, filename)
            file_size = os.path.getsize(filepath)
            is_dir = os.path.isdir(filepath)
            files_info.append(
                f"- {filename}: file_size={file_size} bytes, is_dir={is_dir}"
            )
        return "\n".join(files_info)
    except Exception as e:
        return f"Error al listar archivos: {e}"

schema_get_files_info = FunctionDeclaration(
    name="get_files_info",
    description="Lista los archivos en el directorio especificado con sus tamaños, restringido al directorio de trabajo.",
    parameters=Schema(
        type=Type.OBJECT,
        properties={
            "directory": Schema(
                type=Type.STRING,
                description="El directorio a listar, relativo al directorio de trabajo. Si no se proporciona, lista el directorio de trabajo.",
            ),
        },
    ),
)

Esta función lista los archivos y directorios en el directorio especificado, devolviendo una cadena con su nombre, tamaño y si es un directorio.

Leer Contenido de Archivos

Crea config.py para definir constantes:

MAX_CHARS = 10000
WORKING_DIR = "./calculator"

Luego, crea functions/get_file_content.py:

import os
from google.generativeai.types import FunctionDeclaration, Schema, Type
from config import MAX_CHARS

def get_file_content(working_directory, file_path):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: No se puede leer "{file_path}" ya que está fuera del directorio de trabajo permitido'
    if not os.path.isfile(abs_file_path):
        return f'Error: Archivo no encontrado o no es un archivo regular: "{file_path}"'
    try:
        with open(abs_file_path, "r") as f:
            content = f.read(MAX_CHARS)
            if os.path.getsize(abs_file_path) > MAX_CHARS:
                content += (
                    f'[...Archivo "{file_path}" truncado en {MAX_CHARS} caracteres]'
                )
        return content
    except Exception as e:
        return f'Error al leer archivo "{file_path}": {e}"

schema_get_file_content = FunctionDeclaration(
    name="get_file_content",
    description=f"Lee y devuelve los primeros {MAX_CHARS} caracteres del contenido de un archivo dentro del directorio de trabajo.",
    parameters=Schema(
        type=Type.OBJECT,
        properties={
            "file_path": Schema(
                type=Type.STRING,
                description="La ruta del archivo a leer, relativa al directorio de trabajo.",
            ),
        },
        required=["file_path"],
    ),
)

Esta función lee hasta 10,000 caracteres de un archivo y trunca el contenido si excede este límite, añadiendo un mensaje de truncamiento.

Escribir en Archivos

Crea functions/write_file_content.py:

import os
from google.generativeai.types import FunctionDeclaration, Schema, Type

def write_file(working_directory, file_path, content):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: No se puede escribir en "{file_path}" ya que está fuera del directorio de trabajo permitido'
    if not os.path.exists(abs_file_path):
        try:
            os.makedirs(os.path.dirname(abs_file_path), exist_ok=True)
        except Exception as e:
            return f"Error al crear directorio: {e}"
    if os.path.exists(abs_file_path) and os.path.isdir(abs_file_path):
        return f'Error: "{file_path}" es un directorio, no un archivo'
    try:
        with open(abs_file_path, "w") as f:
            f.write(content)
        return (
            f'Éxito: Escrito en "{file_path}" ({len(content)} caracteres escritos)'
        )
    except Exception as e:
        return f"Error al escribir en archivo: {e}"

schema_write_file = FunctionDeclaration(
    name="write_file",
    description="Escribe contenido en un archivo dentro del directorio de trabajo. Crea el archivo si no existe.",
    parameters=Schema(
        type=Type.OBJECT,
        properties={
            "file_path": Schema(
                type=Type.STRING,
                description="Ruta del archivo a escribir, relativa al directorio de trabajo.",
            ),
            "content": Schema(
                type=Type.STRING,
                description="Contenido a escribir en el archivo.",
            ),
        },
        required=["file_path", "content"],
    ),
)

Esta función escribe contenido en un archivo, creando directorios si es necesario, y devuelve un mensaje de éxito o error.

Ejecutar Archivos Python

Crea functions/run_python.py:

import os
import subprocess
from google.generativeai.types import FunctionDeclaration, Schema, Type

def run_python_file(working_directory, file_path, args=None):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: No se puede ejecutar "{file_path}" ya que está fuera del directorio de trabajo permitido'
    if not os.path.exists(abs_file_path):
        return f'Error: Archivo "{file_path}" no encontrado.'
    if not file_path.endswith(".py"):
        return f'Error: "{file_path}" no es un archivo Python.'
    try:
        commands = ["python", abs_file_path]
        if args:
            commands.extend(args)
        result = subprocess.run(
            commands,
            capture_output=True,
            text=True,
            timeout=30,
            cwd=abs_working_dir,
        )
        output = []
        if result.stdout:
            output.append(f"STDOUT:\n{result.stdout}")
        if result.stderr:
            output.append(f"STDERR:\n{result.stderr}")
        if result.returncode != 0:
            output.append(f"Proceso finalizado con código {result.returncode}")
        return "\n".join(output) if output else "No se produjo salida."
    except Exception as e:
        return f"Error al ejecutar archivo Python: {e}"

schema_run_python_file = FunctionDeclaration(
    name="run_python_file",
    description="Ejecuta un archivo Python dentro del directorio de trabajo y devuelve la salida del intérprete.",
    parameters=Schema(
        type=Type.OBJECT,
        properties={
            "file_path": Schema(
                type=Type.STRING,
                description="Ruta del archivo Python a ejecutar, relativa al directorio de trabajo.",
            ),
            "args": Schema(
                type=Type.ARRAY,
                items=Schema(
                    type=Type.STRING,
                    description="Argumentos opcionales para pasar al archivo Python.",
                ),
                description="Argumentos opcionales para pasar al archivo Python.",
            ),
        },
        required=["file_path"],
    ),
)

Esta función ejecuta archivos Python con un tiempo límite de 30 segundos, capturando la salida estándar y los errores, y está restringida al directorio de trabajo.

Prompt del Sistema y Declaración de Funciones

El prompt del sistema define el comportamiento del agente y establece las reglas para su interacción. Crea un archivo prompts.py:

system_prompt = """
Eres un agente de codificación de IA útil.

Cuando un usuario hace una pregunta o solicitud, elabora un plan de llamadas a funciones. Puedes realizar las siguientes operaciones:
- Listar archivos y directorios
- Leer contenido de archivos
- Ejecutar archivos Python con argumentos opcionales
- Escribir o sobrescribir archivos

Todos los caminos que proporciones deben ser relativos al directorio de trabajo. No necesitas especificar el directorio de trabajo en tus llamadas a funciones, ya que se inyecta automáticamente por razones de seguridad.
"""

Declara las funciones disponibles para el agente en functions/call_function.py:

from google.generativeai.types import Tool, ContentDict, PartDict
from functions.get_files_info import get_files_info, schema_get_files_info
from functions.get_file_content import get_file_content, schema_get_file_content
from functions.run_python import run_python_file, schema_run_python_file
from functions.write_file_content import write_file, schema_write_file
from config import WORKING_DIR

available_functions = Tool(
    function_declarations=[
        schema_get_files_info,
        schema_get_file_content,
        schema_run_python_file,
        schema_write_file,
    ]
)

def call_function(function_call_part, verbose=False):
    if verbose:
        print(f" - Llamando función: {function_call_part.name}({function_call_part.args})")
    else:
        print(f" - Llamando función: {function_call_part.name}")
    function_map = {
        "get_files_info": get_files_info,
        "get_file_content": get_file_content,
        "run_python_file": run_python_file,
        "write_file": write_file,
    }
    function_name = function_call_part.name
    if function_name not in function_map:
        return ContentDict(
            role="tool",
            parts=[
                PartDict.from_function_response(
                    name=function_name,
                    response={"error": f"Función desconocida: {function_name}"},
                )
            ],
        )
    args = dict(function_call_part.args)
    args["working_directory"] = WORKING_DIR
    function_result = function_map[function_name](**args)
    return ContentDict(
        role="tool",
        parts=[
            PartDict.from_function_response(
                name=function_name,
                response={"result": function_result},
            )
        ],
    )

Integra las funciones en main.py:

response = model.generate_content(
    messages,
    generation_config={"tools": [available_functions], "system_instruction": system_prompt}
)

Bucle del Agente

Para que el agente sea verdaderamente autónomo, implementaremos un bucle de agente que permita al modelo iterar sobre sus propias acciones hasta completar la tarea o alcanzar un límite de iteraciones. Actualiza main.py:

import sys
import os
from google import generativeai as genai
from google.generativeai.types import ContentDict, PartDict
from dotenv import load_dotenv
from prompts import system_prompt
from call_function import call_function, available_functions

def main():
    load_dotenv()
    verbose = "--verbose" in sys.argv
    args = [arg for arg in sys.argv[1:] if not arg.startswith("--")]

    if not args:
        print("Asistente de Código IA")
        print('Uso: uv run main.py "tu prompt aquí" [--verbose]')
        sys.exit(1)

    api_key = os.environ.get("GEMINI_API_KEY")
    genai.configure(api_key=api_key)

    user_prompt = " ".join(args)
    if verbose:
        print(f"Prompt del usuario: {user_prompt}\n")

    messages = [
        ContentDict(role="user", parts=[PartDict(text=user_prompt)])
    ]
    model = genai.GenerativeModel("gemini-1.5-flash")
    generate_content_loop(model, messages, verbose)

def generate_content_loop(model, messages, verbose, max_iterations=20):
    for iteration in range(max_iterations):
        try:
            response = model.generate_content(
                messages,
                generation_config={"tools": [available_functions], "system_instruction": system_prompt}
            )
            if verbose:
                print(f"Tokens de prompt: {response.usage_metadata.prompt_token_count}")
                print(f"Tokens de respuesta: {response.usage_metadata.candidates_token_count}")

            for candidate in response.candidates:
                messages.append(candidate.content)

            if response.text:
                print("Respuesta final:")
                print(response.text)
                break

            if response.function_calls:
                function_responses = []
                for function_call_part in response.function_calls:
                    function_call_result = call_function(function_call_part, verbose)
                    if not function_call_result.parts or not function_call_result.parts[0].function_response:
                        raise Exception("Resultado de llamada a función vacío")
                    if verbose:
                        print(f"-> {function_call_result.parts[0].function_response.response}")
                    function_responses.append(function_call_result.parts[0])
                if function_responses:
                    messages.append(ContentDict(role="user", parts=function_responses))
                else:
                    raise Exception("No se generaron respuestas de funciones, saliendo.")
        except Exception as e:
            print(f"Error: {e}")
            break
    else:
        print(f"Se alcanzó el máximo de iteraciones ({max_iterations}). El agente puede no haber completado la tarea.")

if __name__ == "__main__":
    main()

Este bucle permite al agente realizar múltiples llamadas a funciones, almacenando los resultados en la lista de mensajes y continuando hasta que se obtenga una respuesta final o se alcance el límite de 20 iteraciones.

Conclusiones

Construir un agente de codificación con Python y la API de Gemini es un proyecto práctico que combina habilidades de programación, integración de APIs y conceptos de inteligencia artificial. A través de este tutorial, has aprendido a configurar un entorno virtual, integrar un modelo de lenguaje grande, implementar funciones seguras para interactuar con el sistema de archivos y ejecutar código, y crear un bucle de agente que permite la automatización iterativa de tareas. Este agente puede listar archivos, leer y escribir contenido, y ejecutar scripts de Python, todo mientras se mantiene dentro de un directorio de trabajo seguro. Experimenta con prompts más complejos, como corregir errores en la calculadora o añadir nuevas funcionalidades, pero siempre con precaución debido a los riesgos de seguridad al permitir que un LLM ejecute código arbitrario. Este proyecto es un punto de partida para explorar aplicaciones más avanzadas de agentes de IA en el desarrollo de software.